Include two endpoins for news in spanish and english

v5 v5.4.0
Jéluchu 2 months ago
parent 08caaa60fc
commit f53ed50bb2

@ -20,6 +20,7 @@ repositories {
dependencies { dependencies {
implementation(libs.bson) implementation(libs.bson)
implementation(libs.rssparser)
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.cio)
implementation(libs.logback.classic) implementation(libs.logback.classic)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)

@ -2,8 +2,9 @@
[versions] [versions]
kotlin-version = "2.0.21" kotlin-version = "2.0.21"
ktor-version = "3.0.1" ktor-version = "3.0.1"
logback-version = "1.4.14" logback-version = "1.5.18"
mongo-version = "4.10.2" mongo-version = "4.10.2"
rss-version = "6.0.11"
[libraries] [libraries]
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-version" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-version" }
@ -15,6 +16,7 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti
mongodb-driver-core = { module = "org.mongodb:mongodb-driver-core", version.ref = "mongo-version" } mongodb-driver-core = { module = "org.mongodb:mongodb-driver-core", version.ref = "mongo-version" }
mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version.ref = "mongo-version" } mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version.ref = "mongo-version" }
bson = { module = "org.mongodb:bson", version.ref = "mongo-version" } bson = { module = "org.mongodb:bson", version.ref = "mongo-version" }
rssparser = { module = "com.prof18.rssparser:rssparser", version.ref = "rss-version" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor-version" } ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor-version" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-version" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-version" }
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml-jvm", version.ref = "ktor-version" } ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml-jvm", version.ref = "ktor-version" }

@ -1,8 +1,10 @@
package com.jeluchu.core.configuration package com.jeluchu.core.configuration
import com.jeluchu.features.anime.routes.animeEndpoints import com.jeluchu.features.anime.routes.animeEndpoints
import com.jeluchu.features.news.routes.newsEndpoints
import com.jeluchu.features.rankings.routes.rankingsEndpoints import com.jeluchu.features.rankings.routes.rankingsEndpoints
import com.jeluchu.features.schedule.routes.scheduleEndpoints import com.jeluchu.features.schedule.routes.scheduleEndpoints
import com.jeluchu.features.themes.routes.themesEndpoints
import com.mongodb.client.MongoDatabase import com.mongodb.client.MongoDatabase
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@ -12,7 +14,9 @@ fun Application.initRoutes(
) = routing { ) = routing {
route("api/v5") { route("api/v5") {
initDocumentation() initDocumentation()
newsEndpoints(mongoDatabase)
animeEndpoints(mongoDatabase) animeEndpoints(mongoDatabase)
themesEndpoints(mongoDatabase)
rankingsEndpoints(mongoDatabase) rankingsEndpoints(mongoDatabase)
scheduleEndpoints(mongoDatabase) scheduleEndpoints(mongoDatabase)
} }

@ -1,9 +1,34 @@
package com.jeluchu.core.extensions package com.jeluchu.core.extensions
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.bson.Document import org.bson.Document
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
fun RoutingCall.getIntSafeQueryParam(
key: String,
defaultValue: Int = 0
) = request.queryParameters[key]?.toIntOrNull() ?: defaultValue
fun RoutingCall.getStringSafeQueryParam(
key: String,
defaultValue: String = ""
) = request.queryParameters[key] ?: defaultValue
fun RoutingCall.getStringSafeParam(
key: String,
defaultValue: String = ""
) = parameters[key] ?: defaultValue
fun RoutingCall.getIntSafeParam(
key: String,
defaultValue: Int = 0
) = parameters[key]?.toIntOrNull() ?: defaultValue
suspend fun RoutingCall.badRequestError(message: String) = respond(HttpStatusCode.BadRequest, message)
fun Document.getStringSafe(key: String, defaultValue: String = ""): String { fun Document.getStringSafe(key: String, defaultValue: String = ""): String {
return try { return try {
when (val value = this[key]) { when (val value = this[key]) {
@ -113,3 +138,9 @@ fun Document.getDocumentSafe(key: String): Document? {
null null
} }
} }
fun String.parseRssDate(format: String = "dd/MM/yyyy"): String {
val date = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US).parse(this)
val outputDateFormat = SimpleDateFormat(format, Locale.US)
return date?.let { outputDateFormat.format(it) }.orEmpty()
}

@ -2,10 +2,38 @@ package com.jeluchu.core.utils
object BaseUrls { object BaseUrls {
const val JIKAN = "https://api.jikan.moe/v4/" const val JIKAN = "https://api.jikan.moe/v4/"
const val ANIME_FLV = "https://animeflv.ahmedrangel.com/api/" const val ANIME_THEMES = "https://api.animethemes.moe/"
}
object RssUrls {
// Spanish news
const val SOMOSKUDASAI = "https://somoskudasai.com/feed/"
const val MANGALATAM = "https://www.mangalatam.com/feeds/posts/default?alt=rss"
const val CRUNCHYROLL = "https://cr-news-api-service.prd.crunchyrollsvc.com/v1/es-ES/rss"
const val RAMENPARADOS = "https://ramenparados.com/feed/"
const val ANMOSUGOI = "https://www.anmosugoi.com/feed/"
// English news
const val OTAKUMODE = "https://otakumode.com/news/feed"
const val MYANIMELIST = "https://myanimelist.net/rss/news.xml"
const val HONEYSANIME = "https://honeysanime.com/feed/"
const val ANIMEHUNCH = "https://animehunch.com/feed/"
}
object RssSources {
const val SOMOSKUDASAI = "Somos Kudasai"
const val MANGALATAM = "MangaLatam"
const val CRUNCHYROLL = "Crunchyroll"
const val RAMENPARADOS = "Ramen para dos"
const val ANMOSUGOI = "Anmo Sugoi"
const val OTAKUMODE = "Tokyo Otaku Mode News"
const val MYANIMELIST = "MyAnimeList"
const val HONEYSANIME = "Honey's anime"
const val ANIMEHUNCH = "My Anime For Life"
} }
object Endpoints { object Endpoints {
const val ANIME = "anime"
const val SCHEDULES = "schedules" const val SCHEDULES = "schedules"
const val TOP_ANIME = "top/anime" const val TOP_ANIME = "top/anime"
const val TOP_MANGA = "top/manga" const val TOP_MANGA = "top/manga"
@ -15,7 +43,10 @@ object Endpoints {
} }
object Routes { object Routes {
const val ES = "/es"
const val EN = "/en"
const val TOP = "/top" const val TOP = "/top"
const val NEWS = "/news"
const val ANIME = "/anime" const val ANIME = "/anime"
const val MANGA = "/manga" const val MANGA = "/manga"
const val PEOPLE = "/people" const val PEOPLE = "/people"
@ -27,6 +58,7 @@ object Routes {
const val TYPE = "/{type}" const val TYPE = "/{type}"
const val SEASON = "/{year}/{season}" const val SEASON = "/{year}/{season}"
const val DAY = "/{day}" const val DAY = "/{day}"
const val THEMES = "/themes"
} }
object TimerKey { object TimerKey {
@ -34,12 +66,17 @@ object TimerKey {
const val SCHEDULE = "schedule" const val SCHEDULE = "schedule"
const val LAST_UPDATED = "lastUpdated" const val LAST_UPDATED = "lastUpdated"
const val ANIME_TYPE = "anime_" const val ANIME_TYPE = "anime_"
const val THEMES = "themes_"
const val LAST_EPISODES = "last_episodes" const val LAST_EPISODES = "last_episodes"
} }
object Collections { object Collections {
const val TIMERS = "timers" const val TIMERS = "timers"
const val NEWS_ES = "news_es"
const val NEWS_EN = "news_en"
const val SCHEDULES = "schedule" const val SCHEDULES = "schedule"
const val ANIME_THEMES = "anime_themes"
const val ARTISTS_INDEX = "artists_index"
const val ANIME_DETAILS = "anime_details" const val ANIME_DETAILS = "anime_details"
const val LAST_EPISODES = "last_episodes" const val LAST_EPISODES = "last_episodes"
const val ANIME_RANKING = "anime_ranking" const val ANIME_RANKING = "anime_ranking"

@ -1,14 +1,19 @@
package com.jeluchu.features.anime.mappers package com.jeluchu.features.anime.mappers
import com.jeluchu.features.themes.models.anime.AnimeThemeEntry
import com.jeluchu.core.extensions.* import com.jeluchu.core.extensions.*
import com.jeluchu.features.anime.models.lastepisodes.LastEpisodeData
import com.jeluchu.features.anime.models.anime.* import com.jeluchu.features.anime.models.anime.*
import com.jeluchu.features.anime.models.directory.AnimeTypeEntity import com.jeluchu.features.anime.models.directory.AnimeTypeEntity
import com.jeluchu.features.anime.models.lastepisodes.LastEpisodeData
import com.jeluchu.features.rankings.models.AnimeTopEntity import com.jeluchu.features.rankings.models.AnimeTopEntity
import com.jeluchu.features.rankings.models.CharacterTopEntity import com.jeluchu.features.rankings.models.CharacterTopEntity
import com.jeluchu.features.rankings.models.MangaTopEntity import com.jeluchu.features.rankings.models.MangaTopEntity
import com.jeluchu.features.rankings.models.PeopleTopEntity import com.jeluchu.features.rankings.models.PeopleTopEntity
import com.jeluchu.features.schedule.models.DayEntity import com.jeluchu.features.schedule.models.DayEntity
import com.jeluchu.features.themes.models.anime.Anime
import com.jeluchu.features.themes.models.anime.AnimeVideoTheme
import com.jeluchu.features.themes.models.anime.AnimesEntity
import com.jeluchu.features.themes.models.anime.Video
import org.bson.Document import org.bson.Document
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -82,13 +87,6 @@ fun documentToAnimeBroadcast(doc: Document): AnimeBroadcast {
) )
} }
fun documentToAnimeSource(doc: Document): AnimeSource {
return AnimeSource(
id = doc.getStringSafe("id"),
source = doc.getStringSafe("source")
)
}
fun documentToCharacter(doc: Document): Character { fun documentToCharacter(doc: Document): Character {
return Character( return Character(
character = doc.getDocumentSafe("character")?.let { documentToIndividual(it) } ?: Individual(), character = doc.getDocumentSafe("character")?.let { documentToIndividual(it) } ?: Individual(),
@ -278,23 +276,69 @@ fun documentToCharacterTopEntity(doc: Document) = CharacterTopEntity(
) )
fun documentToAnimeTypeEntity(doc: Document) = AnimeTypeEntity( fun documentToAnimeTypeEntity(doc: Document) = AnimeTypeEntity(
score = doc.getStringSafe("score"),
malId = doc.getIntSafe("malId"),
year = doc.getIntSafe("year"), year = doc.getIntSafe("year"),
season = doc.getStringSafe("season"), malId = doc.getIntSafe("malId"),
type = doc.getStringSafe("type"), type = doc.getStringSafe("type"),
score = doc.getStringSafe("score"),
title = doc.getStringSafe("title"), title = doc.getStringSafe("title"),
image = doc.getStringSafe("poster"), image = doc.getStringSafe("poster"),
episodes = doc.getListSafe<Document>("episodes").size season = doc.getStringSafe("season")
) )
fun documentToAnimeDirectoryEntity(doc: Document) = AnimeTypeEntity( fun documentToAnimeDirectoryEntity(doc: Document) = AnimeTypeEntity(
score = doc.getString("score"),
malId = doc.getIntSafe("malId"),
year = doc.getIntSafe("year"), year = doc.getIntSafe("year"),
season = doc.getStringSafe("season"), malId = doc.getIntSafe("malId"),
type = doc.getStringSafe("type"), type = doc.getStringSafe("type"),
score = doc.getStringSafe("score"),
title = doc.getStringSafe("title"), title = doc.getStringSafe("title"),
image = doc.getStringSafe("image"), image = doc.getStringSafe("image"),
episodes = doc.getListSafe<Document>("episodes").size season = doc.getStringSafe("season")
)
fun documentToAnimesEntity(doc: Document) = AnimesEntity(
year = doc.getIntSafe("year"),
slug = doc.getStringSafe("slug"),
name = doc.getStringSafe("name"),
image = doc.getStringSafe("image"),
season = doc.getStringSafe("season")
)
fun documentToAnimesThemeEntity(doc: Document) = Anime(
year = doc.getIntSafe("year"),
slug = doc.getStringSafe("slug"),
name = doc.getStringSafe("name"),
image = doc.getStringSafe("image"),
season = doc.getStringSafe("season"),
themes = doc.getListSafe<Document>("themes").map { documentToAnimeVideoTheme(it) }
)
fun documentToAnimeVideoTheme(doc: Document) = AnimeVideoTheme(
id = doc.getIntSafe("id"),
slug = doc.getStringSafe("slug"),
type = doc.getStringSafe("type"),
sequence = doc.getIntSafe("sequence"),
entries = doc.getListSafe<Document>("animethemeentries").map { documentToAnimeThemeEntry(it) }
)
fun documentToAnimeThemeEntry(doc: Document) = AnimeThemeEntry(
id = doc.getIntSafe("id"),
nsfw = doc.getBooleanSafe("nsfw"),
notes = doc.getStringSafe("notes"),
spoiler = doc.getBooleanSafe("spoiler"),
episodes = doc.getStringSafe("episodes"),
videos = doc.getListSafe<Document>("videos").map { documentToVideo(it) }
)
fun documentToVideo(doc: Document) = Video(
nc = doc.getBooleanSafe("nc"),
size = doc.getIntSafe("size"),
link = doc.getStringSafe("link"),
uncen = doc.getBooleanSafe("uncen"),
source = doc.getStringSafe("source"),
subbed = doc.getBooleanSafe("subbed"),
lyrics = doc.getBooleanSafe("lyrics"),
overlap = doc.getStringSafe("overlap"),
filename = doc.getStringSafe("filename"),
resolution = doc.getIntSafe("resolution")
) )

@ -6,7 +6,6 @@ import kotlinx.serialization.Serializable
data class AnimeTypeEntity( data class AnimeTypeEntity(
val malId: Int? = 0, val malId: Int? = 0,
val type: String? = "", val type: String? = "",
val episodes: Int? = 0,
val year: Int? = 0, val year: Int? = 0,
val season: String? = "", val season: String? = "",
val title: String? = "", val title: String? = "",

@ -2,15 +2,14 @@ package com.jeluchu.features.anime.services
import com.jeluchu.core.enums.TimeUnit import com.jeluchu.core.enums.TimeUnit
import com.jeluchu.core.enums.parseAnimeType import com.jeluchu.core.enums.parseAnimeType
import com.jeluchu.core.extensions.needsUpdate import com.jeluchu.core.extensions.*
import com.jeluchu.core.extensions.update
import com.jeluchu.core.messages.ErrorMessages import com.jeluchu.core.messages.ErrorMessages
import com.jeluchu.core.models.ErrorResponse
import com.jeluchu.core.models.PaginationResponse import com.jeluchu.core.models.PaginationResponse
import com.jeluchu.core.utils.Collections import com.jeluchu.core.utils.Collections
import com.jeluchu.core.utils.TimerKey import com.jeluchu.core.utils.TimerKey
import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity
import com.jeluchu.features.anime.mappers.documentToAnimeTypeEntity import com.jeluchu.features.anime.mappers.documentToAnimeTypeEntity
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase import com.mongodb.client.MongoDatabase
import com.mongodb.client.model.Filters import com.mongodb.client.model.Filters
import io.ktor.http.* import io.ktor.http.*
@ -19,143 +18,124 @@ import io.ktor.server.routing.*
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.bson.Document import org.bson.Document
import org.bson.conversions.Bson
class DirectoryService( class DirectoryService(
private val database: MongoDatabase private val database: MongoDatabase,
private val timers: MongoCollection<Document> = database.getCollection(Collections.TIMERS),
private val directory: MongoCollection<Document> = database.getCollection(Collections.ANIME_DETAILS)
) { ) {
private val timers = database.getCollection(Collections.TIMERS)
private val directory = database.getCollection(Collections.ANIME_DETAILS)
suspend fun getAnimeByType(call: RoutingCall) { suspend fun getAnimeByType(call: RoutingCall) {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 val param = call.getStringSafeParam("type").uppercase()
val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 10 val page = call.getIntSafeQueryParam("page", 1)
val param = call.parameters["type"] ?: throw IllegalArgumentException(ErrorMessages.InvalidAnimeType.message) val size = call.getIntSafeQueryParam("size", 10)
if (page < 1 || size < 1) call.respond(HttpStatusCode.BadRequest, ErrorMessages.InvalidSizeAndPage.message)
val skipCount = (page - 1) * size val skipCount = (page - 1) * size
if (parseAnimeType(param) == null) call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(ErrorMessages.InvalidAnimeType.message)
)
val timerKey = "${TimerKey.ANIME_TYPE}${param.lowercase()}" val timerKey = "${TimerKey.ANIME_TYPE}${param.lowercase()}"
val collection = database.getCollection(timerKey) val collection = database.getCollection(timerKey)
if (page < 1 || size < 1) call.badRequestError(ErrorMessages.InvalidSizeAndPage.message)
val needsUpdate = timers.needsUpdate( if (parseAnimeType(param) == null) call.badRequestError(ErrorMessages.InvalidAnimeType.message)
amount = 30,
key = timerKey, if (timers.needsUpdate(timerKey, 30, TimeUnit.DAY)) {
unit = TimeUnit.DAY, getRemoteData(
) newCollection = collection,
remoteCollection = directory,
if (needsUpdate) { mapper = { documentToAnimeDirectoryEntity(it) },
collection.deleteMany(Document()) filters = Filters.eq("type", param),
onQuerySuccess = { data ->
val animes = directory val documents = data.map { Document.parse(Json.encodeToString(it)) }
.find(Filters.eq("type", param.uppercase()))
.toList()
val animeTypes = animes.map { documentToAnimeTypeEntity(it) }
val documents = animeTypes.map { anime -> Document.parse(Json.encodeToString(anime)) }
if (documents.isNotEmpty()) collection.insertMany(documents) if (documents.isNotEmpty()) collection.insertMany(documents)
timers.update(timerKey) timers.update(timerKey)
}
val animeTypeDb = collection
.find()
.skip(skipCount)
.limit(size)
.toList()
val animeTypeEntity = animeTypeDb.map { documentToAnimeTypeEntity(it) }
val response = PaginationResponse(
page = page,
data = animeTypeEntity,
size = animeTypeEntity.size
) )
}
call.respond(HttpStatusCode.OK, Json.encodeToString(response)) getLocalData(
} else {
val elements = collection
.find()
.skip(skipCount)
.limit(size)
.toList()
val responseItems = elements.map { documentToAnimeDirectoryEntity(it) }
val response = PaginationResponse(
page = page, page = page,
data = responseItems, size = size,
size = responseItems.size skipCount = skipCount,
collection = collection,
mapper = { documentToAnimeDirectoryEntity(it) },
onQuerySuccess = { data -> call.respond(HttpStatusCode.OK, Json.encodeToString(data)) }
) )
call.respond(HttpStatusCode.OK, Json.encodeToString(response))
}
} }
suspend fun getAnimeBySeason(call: RoutingCall) { suspend fun getAnimeBySeason(call: RoutingCall) {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 val year = call.getIntSafeParam("year")
val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 10 val season = call.getStringSafeParam("season")
val year = call.parameters["year"]?.toIntOrNull() ?: throw IllegalArgumentException(ErrorMessages.NotFound.message) val page = call.getIntSafeQueryParam("page", 1)
val season = call.parameters["season"] ?: throw IllegalArgumentException(ErrorMessages.NotFound.message) val size = call.getIntSafeQueryParam("size", 10)
if (page < 1 || size < 1) call.respond(HttpStatusCode.BadRequest, ErrorMessages.InvalidSizeAndPage.message)
val skipCount = (page - 1) * size val skipCount = (page - 1) * size
val timerKey = "${TimerKey.ANIME_TYPE}${year}_${season.lowercase()}" val timerKey = "${TimerKey.ANIME_TYPE}${year}_${season.lowercase()}"
val collection = database.getCollection(timerKey) val collection = database.getCollection(timerKey)
if (page < 1 || size < 1) call.badRequestError(ErrorMessages.InvalidSizeAndPage.message)
val needsUpdate = timers.needsUpdate(
amount = 30, if (timers.needsUpdate(timerKey, 30, TimeUnit.DAY)) {
key = timerKey, getRemoteData(
unit = TimeUnit.DAY, newCollection = collection,
) remoteCollection = directory,
mapper = { documentToAnimeDirectoryEntity(it) },
if (needsUpdate) { filters = Filters.and(
collection.deleteMany(Document())
val animes = directory
.find(
Filters.and(
Filters.eq("year", year), Filters.eq("year", year),
Filters.eq("season", season.lowercase()) Filters.eq("season", season.lowercase())
) ),
).toList() onQuerySuccess = { data ->
val documents = data.map { Document.parse(Json.encodeToString(it)) }
val animeTypes = animes.map { documentToAnimeTypeEntity(it) }
val documents = animeTypes.map { anime -> Document.parse(Json.encodeToString(anime)) }
if (documents.isNotEmpty()) collection.insertMany(documents) if (documents.isNotEmpty()) collection.insertMany(documents)
timers.update(timerKey) timers.update(timerKey)
}
)
}
val animeSeasonDb = collection getLocalData(
.find()
.skip(skipCount)
.limit(size)
.toList()
val animeSeason = animeSeasonDb.map { documentToAnimeTypeEntity(it) }
val response = PaginationResponse(
page = page, page = page,
data = animeSeason, size = size,
size = animeSeason.size skipCount = skipCount,
collection = collection,
mapper = { documentToAnimeDirectoryEntity(it) },
onQuerySuccess = { data -> call.respond(HttpStatusCode.OK, Json.encodeToString(data)) }
) )
}
}
call.respond(HttpStatusCode.OK, Json.encodeToString(response)) private fun <T> getRemoteData(
} else { filters: Bson,
val elements = collection mapper: (Document) -> T,
onQuerySuccess: (List<T>) -> Unit,
newCollection: MongoCollection<Document>,
remoteCollection: MongoCollection<Document>,
) {
newCollection.deleteMany(Document())
val query = remoteCollection
.find(filters)
.toList()
.map { mapper(it) }
onQuerySuccess(query)
}
private suspend fun <T> getLocalData(
page: Int,
size: Int,
skipCount: Int,
mapper: (Document) -> T,
collection: MongoCollection<Document>,
onQuerySuccess: suspend (PaginationResponse<T>) -> Unit
) {
val query = collection
.find() .find()
.skip(skipCount) .skip(skipCount)
.limit(size) .limit(size)
.toList() .toList()
.map { mapper(it) }
val responseItems = elements.map { documentToAnimeDirectoryEntity(it) } val paginate = PaginationResponse(
val response = PaginationResponse(
page = page, page = page,
data = responseItems, data = query,
size = responseItems.size size = query.size
) )
call.respond(HttpStatusCode.OK, Json.encodeToString(response)) onQuerySuccess(paginate)
}
}
} }

@ -0,0 +1,47 @@
package com.jeluchu.features.news.mappers
import com.jeluchu.core.extensions.getBooleanSafe
import com.jeluchu.core.extensions.getDocumentSafe
import com.jeluchu.core.extensions.getFloatSafe
import com.jeluchu.core.extensions.getIntSafe
import com.jeluchu.core.extensions.getListSafe
import com.jeluchu.core.extensions.getStringSafe
import com.jeluchu.core.extensions.parseRssDate
import com.jeluchu.features.anime.mappers.documentToVideoPromo
import com.jeluchu.features.anime.models.anime.VideoPromo
import com.jeluchu.features.news.models.NewEntity
import com.prof18.rssparser.model.RssChannel
import org.bson.Document
fun RssChannel.toNews(
source: String,
list: MutableList<NewEntity>
) = list.apply{
items.forEach { item ->
add(
NewEntity(
source = source,
sourceDescription = description.orEmpty(),
link = item.link.orEmpty(),
title = item.title.orEmpty(),
date = item.pubDate?.parseRssDate().orEmpty(),
description = item.description.orEmpty(),
content = item.content.orEmpty(),
image = item.image.orEmpty(),
categories = item.categories
)
)
}
}
fun documentToNewsEntity(doc: Document) = NewEntity(
source = doc.getStringSafe("source"),
sourceDescription = doc.getStringSafe("sourceDescription"),
link = doc.getStringSafe("link"),
title = doc.getStringSafe("title"),
date = doc.getStringSafe("date"),
description = doc.getStringSafe("description"),
content = doc.getStringSafe("content"),
image = doc.getStringSafe("image"),
categories = doc.getListSafe<String>("categories")
)

@ -0,0 +1,16 @@
package com.jeluchu.features.news.models
import kotlinx.serialization.Serializable
@Serializable
data class NewEntity(
val source: String,
val sourceDescription: String,
val link: String,
val title: String,
val date: String,
val description: String,
val content: String,
val image: String,
val categories: List<String>
)

@ -0,0 +1,20 @@
package com.jeluchu.features.news.routes
import com.jeluchu.core.extensions.getToJson
import com.jeluchu.core.utils.Routes
import com.jeluchu.features.news.services.NewsService
import com.mongodb.client.MongoDatabase
import io.ktor.server.routing.*
fun Route.newsEndpoints(
mongoDatabase: MongoDatabase,
service: NewsService = NewsService(mongoDatabase)
) = route(Routes.NEWS) {
route(Routes.ES) {
getToJson { service.getSpanishNews(call) }
}
route(Routes.EN) {
getToJson { service.getEnglishNews(call) }
}
}

@ -0,0 +1,100 @@
package com.jeluchu.features.news.services
import com.jeluchu.core.enums.TimeUnit
import com.jeluchu.core.extensions.needsUpdate
import com.jeluchu.core.extensions.update
import com.jeluchu.core.utils.Collections
import com.jeluchu.core.utils.RssSources
import com.jeluchu.core.utils.RssUrls
import com.jeluchu.core.utils.parseDataToDocuments
import com.jeluchu.features.anime.mappers.documentToAnimeTopEntity
import com.jeluchu.features.news.mappers.documentToNewsEntity
import com.jeluchu.features.news.mappers.toNews
import com.jeluchu.features.news.models.NewEntity
import com.mongodb.client.MongoDatabase
import com.prof18.rssparser.RssParser
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.bson.Document
class NewsService(
database: MongoDatabase
) {
private val timers = database.getCollection(Collections.TIMERS)
private val spanishNews = database.getCollection(Collections.NEWS_ES)
private val englishNews = database.getCollection(Collections.NEWS_EN)
suspend fun getSpanishNews(call: RoutingCall) {
val needsUpdate = timers.needsUpdate(
amount = 1,
key = Collections.NEWS_ES,
unit = TimeUnit.DAY
)
if (needsUpdate) {
spanishNews.deleteMany(Document())
val response = mutableListOf<NewEntity>().apply {
with(RssParser()) {
getRssChannel(RssUrls.MANGALATAM).toNews(RssSources.MANGALATAM, this@apply)
getRssChannel(RssUrls.SOMOSKUDASAI).toNews(RssSources.SOMOSKUDASAI, this@apply)
getRssChannel(RssUrls.CRUNCHYROLL).toNews(RssSources.CRUNCHYROLL, this@apply)
getRssChannel(RssUrls.RAMENPARADOS).toNews(RssSources.RAMENPARADOS, this@apply)
getRssChannel(RssUrls.ANMOSUGOI).toNews(RssSources.ANMOSUGOI, this@apply)
}
}.shuffled()
val documentsToInsert = parseDataToDocuments(response, NewEntity.serializer())
if (documentsToInsert.isNotEmpty()) spanishNews.insertMany(documentsToInsert)
timers.update(Collections.NEWS_ES)
val elements = documentsToInsert.map { documentToNewsEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
} else {
val animes = spanishNews
.find()
.toList()
val elements = animes.map { documentToAnimeTopEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
}
}
suspend fun getEnglishNews(call: RoutingCall) {
val needsUpdate = timers.needsUpdate(
amount = 1,
key = Collections.NEWS_EN,
unit = TimeUnit.DAY
)
if (needsUpdate) {
englishNews.deleteMany(Document())
val response = mutableListOf<NewEntity>().apply {
with(RssParser()) {
getRssChannel(RssUrls.OTAKUMODE).toNews(RssSources.OTAKUMODE, this@apply)
getRssChannel(RssUrls.MYANIMELIST).toNews(RssSources.MYANIMELIST, this@apply)
getRssChannel(RssUrls.HONEYSANIME).toNews(RssSources.HONEYSANIME, this@apply)
getRssChannel(RssUrls.ANIMEHUNCH).toNews(RssSources.ANIMEHUNCH, this@apply)
}
}.shuffled()
val documentsToInsert = parseDataToDocuments(response, NewEntity.serializer())
if (documentsToInsert.isNotEmpty()) englishNews.insertMany(documentsToInsert)
timers.update(Collections.NEWS_EN)
val elements = documentsToInsert.map { documentToNewsEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
} else {
val animes = englishNews
.find()
.toList()
val elements = animes.map { documentToAnimeTopEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
}
}
}

@ -0,0 +1,15 @@
package com.jeluchu.features.themes.models.anime
import kotlinx.serialization.Serializable
@Serializable
data class Anime(
val id: Int? = 0,
val image: String? = "",
val name: String? = "",
val season: String? = "",
val slug: String? = "",
val synopsis: String? = "",
val year: Int? = 0,
val themes: List<AnimeVideoTheme>? = emptyList()
)

@ -0,0 +1,14 @@
package com.jeluchu.features.themes.models.anime
import com.jeluchu.features.themes.models.anime.Video
import kotlinx.serialization.Serializable
@Serializable
data class AnimeThemeEntry(
val episodes: String?,
val id: Int?,
val notes: String?,
val nsfw: Boolean?,
val spoiler: Boolean?,
val videos: List<Video>?
)

@ -0,0 +1,12 @@
package com.jeluchu.features.themes.models.anime
import kotlinx.serialization.Serializable
@Serializable
data class AnimeVideoTheme(
val id: Int? = 0,
val type: String? = "",
val slug: String? = "",
val sequence: Int? = 0,
val entries: List<AnimeThemeEntry>? = emptyList(),
)

@ -0,0 +1,12 @@
package com.jeluchu.features.themes.models.anime
import kotlinx.serialization.Serializable
@Serializable
data class AnimesEntity(
val year: Int? = null,
val slug: String? = null,
val name: String? = null,
val image: String? = null,
val season: String? = null,
)

@ -0,0 +1,11 @@
package com.jeluchu.features.themes.models.anime
import kotlinx.serialization.Serializable
@Serializable
data class Image(
val id: Int?,
val facet: String?,
val link: String?,
val path: String?
)

@ -0,0 +1,17 @@
package com.jeluchu.features.themes.models.anime
import kotlinx.serialization.Serializable
@Serializable
data class Video(
val size: Int? = 0,
val link: String? = "",
val resolution: Int? = 0,
val source: String? = "",
val nc: Boolean? = false,
val overlap: String? = "",
val filename: String? = "",
val uncen: Boolean? = false,
val lyrics: Boolean? = false,
val subbed: Boolean? = false
)

@ -0,0 +1,15 @@
package com.jeluchu.features.themes.routes
import com.jeluchu.core.extensions.getToJson
import com.jeluchu.core.utils.Routes
import com.jeluchu.features.schedule.services.ScheduleService
import com.jeluchu.features.themes.services.AnimeThemesService
import com.mongodb.client.MongoDatabase
import io.ktor.server.routing.*
fun Route.themesEndpoints(
mongoDatabase: MongoDatabase,
service: AnimeThemesService = AnimeThemesService(mongoDatabase)
) = route(Routes.THEMES) {
getToJson(Routes.ANIME) { service.getAnimeThemes(call) }
}

@ -0,0 +1,45 @@
package com.jeluchu.features.themes.services
import com.jeluchu.core.extensions.badRequestError
import com.jeluchu.core.extensions.getIntSafeQueryParam
import com.jeluchu.core.messages.ErrorMessages
import com.jeluchu.core.models.PaginationResponse
import com.jeluchu.core.utils.Collections
import com.jeluchu.features.anime.mappers.documentToAnimesEntity
import com.jeluchu.features.anime.mappers.documentToAnimesThemeEntity
import com.mongodb.client.MongoDatabase
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class AnimeThemesService(
private val database: MongoDatabase
) {
private val themesDirectory = database.getCollection(Collections.ANIME_THEMES)
private val artistsDirectory = database.getCollection(Collections.ARTISTS_INDEX)
suspend fun getAnimeThemes(call: RoutingCall) {
val page = call.getIntSafeQueryParam("page", 1)
val size = call.getIntSafeQueryParam("size", 25)
val skipCount = (page - 1) * size
if (page < 1 || size < 1) call.badRequestError(ErrorMessages.InvalidSizeAndPage.message)
val query = themesDirectory
.find()
.skip(skipCount)
.limit(size)
.toList()
.map { documentToAnimesThemeEntity(it) }
val paginate = PaginationResponse(
page = page,
data = query,
size = query.size
)
call.respond(HttpStatusCode.OK, Json.encodeToString(paginate))
}
}
Loading…
Cancel
Save