diff --git a/build.gradle.kts b/build.gradle.kts index bc84327..7767f85 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ repositories { dependencies { implementation(libs.bson) + implementation(libs.rssparser) implementation(libs.ktor.client.cio) implementation(libs.logback.classic) implementation(libs.ktor.client.core) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d234518..bf48ee2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,9 @@ [versions] kotlin-version = "2.0.21" ktor-version = "3.0.1" -logback-version = "1.4.14" +logback-version = "1.5.18" mongo-version = "4.10.2" +rss-version = "6.0.11" [libraries] 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-sync = { module = "org.mongodb:mongodb-driver-sync", 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" } 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" } diff --git a/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt b/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt index 2ee2541..dd75b8d 100644 --- a/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt +++ b/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt @@ -1,8 +1,10 @@ package com.jeluchu.core.configuration 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.schedule.routes.scheduleEndpoints +import com.jeluchu.features.themes.routes.themesEndpoints import com.mongodb.client.MongoDatabase import io.ktor.server.application.* import io.ktor.server.routing.* @@ -12,7 +14,9 @@ fun Application.initRoutes( ) = routing { route("api/v5") { initDocumentation() + newsEndpoints(mongoDatabase) animeEndpoints(mongoDatabase) + themesEndpoints(mongoDatabase) rankingsEndpoints(mongoDatabase) scheduleEndpoints(mongoDatabase) } diff --git a/src/main/kotlin/com/jeluchu/core/extensions/CommonExtensions.kt b/src/main/kotlin/com/jeluchu/core/extensions/CommonExtensions.kt index 4ca21d6..b7d1eaf 100644 --- a/src/main/kotlin/com/jeluchu/core/extensions/CommonExtensions.kt +++ b/src/main/kotlin/com/jeluchu/core/extensions/CommonExtensions.kt @@ -1,9 +1,34 @@ package com.jeluchu.core.extensions +import io.ktor.http.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import org.bson.Document import java.text.SimpleDateFormat 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 { return try { when (val value = this[key]) { @@ -112,4 +137,10 @@ fun Document.getDocumentSafe(key: String): Document? { } catch (e: Exception) { 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() } \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/utils/Constants.kt b/src/main/kotlin/com/jeluchu/core/utils/Constants.kt index d63e250..6dfd46d 100644 --- a/src/main/kotlin/com/jeluchu/core/utils/Constants.kt +++ b/src/main/kotlin/com/jeluchu/core/utils/Constants.kt @@ -2,10 +2,38 @@ package com.jeluchu.core.utils object BaseUrls { 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 { + const val ANIME = "anime" const val SCHEDULES = "schedules" const val TOP_ANIME = "top/anime" const val TOP_MANGA = "top/manga" @@ -15,7 +43,10 @@ object Endpoints { } object Routes { + const val ES = "/es" + const val EN = "/en" const val TOP = "/top" + const val NEWS = "/news" const val ANIME = "/anime" const val MANGA = "/manga" const val PEOPLE = "/people" @@ -27,6 +58,7 @@ object Routes { const val TYPE = "/{type}" const val SEASON = "/{year}/{season}" const val DAY = "/{day}" + const val THEMES = "/themes" } object TimerKey { @@ -34,12 +66,17 @@ object TimerKey { const val SCHEDULE = "schedule" const val LAST_UPDATED = "lastUpdated" const val ANIME_TYPE = "anime_" + const val THEMES = "themes_" const val LAST_EPISODES = "last_episodes" } object Collections { const val TIMERS = "timers" + const val NEWS_ES = "news_es" + const val NEWS_EN = "news_en" const val SCHEDULES = "schedule" + const val ANIME_THEMES = "anime_themes" + const val ARTISTS_INDEX = "artists_index" const val ANIME_DETAILS = "anime_details" const val LAST_EPISODES = "last_episodes" const val ANIME_RANKING = "anime_ranking" diff --git a/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt b/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt index cda67aa..4eeb902 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt @@ -1,14 +1,19 @@ package com.jeluchu.features.anime.mappers +import com.jeluchu.features.themes.models.anime.AnimeThemeEntry 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.directory.AnimeTypeEntity +import com.jeluchu.features.anime.models.lastepisodes.LastEpisodeData import com.jeluchu.features.rankings.models.AnimeTopEntity import com.jeluchu.features.rankings.models.CharacterTopEntity import com.jeluchu.features.rankings.models.MangaTopEntity import com.jeluchu.features.rankings.models.PeopleTopEntity 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 java.time.ZonedDateTime 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 { return Character( character = doc.getDocumentSafe("character")?.let { documentToIndividual(it) } ?: Individual(), @@ -278,23 +276,69 @@ fun documentToCharacterTopEntity(doc: Document) = CharacterTopEntity( ) fun documentToAnimeTypeEntity(doc: Document) = AnimeTypeEntity( - score = doc.getStringSafe("score"), - malId = doc.getIntSafe("malId"), year = doc.getIntSafe("year"), - season = doc.getStringSafe("season"), + malId = doc.getIntSafe("malId"), type = doc.getStringSafe("type"), + score = doc.getStringSafe("score"), title = doc.getStringSafe("title"), image = doc.getStringSafe("poster"), - episodes = doc.getListSafe("episodes").size + season = doc.getStringSafe("season") ) fun documentToAnimeDirectoryEntity(doc: Document) = AnimeTypeEntity( - score = doc.getString("score"), - malId = doc.getIntSafe("malId"), year = doc.getIntSafe("year"), - season = doc.getStringSafe("season"), + malId = doc.getIntSafe("malId"), type = doc.getStringSafe("type"), + score = doc.getStringSafe("score"), title = doc.getStringSafe("title"), image = doc.getStringSafe("image"), - episodes = doc.getListSafe("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("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("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("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") ) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/directory/AnimeTypeEntity.kt b/src/main/kotlin/com/jeluchu/features/anime/models/directory/AnimeTypeEntity.kt index 19ede49..6647caf 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/models/directory/AnimeTypeEntity.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/models/directory/AnimeTypeEntity.kt @@ -6,7 +6,6 @@ import kotlinx.serialization.Serializable data class AnimeTypeEntity( val malId: Int? = 0, val type: String? = "", - val episodes: Int? = 0, val year: Int? = 0, val season: String? = "", val title: String? = "", diff --git a/src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt b/src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt index 7c76281..aa223e7 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt @@ -2,15 +2,14 @@ package com.jeluchu.features.anime.services import com.jeluchu.core.enums.TimeUnit import com.jeluchu.core.enums.parseAnimeType -import com.jeluchu.core.extensions.needsUpdate -import com.jeluchu.core.extensions.update +import com.jeluchu.core.extensions.* import com.jeluchu.core.messages.ErrorMessages -import com.jeluchu.core.models.ErrorResponse import com.jeluchu.core.models.PaginationResponse import com.jeluchu.core.utils.Collections import com.jeluchu.core.utils.TimerKey import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity import com.jeluchu.features.anime.mappers.documentToAnimeTypeEntity +import com.mongodb.client.MongoCollection import com.mongodb.client.MongoDatabase import com.mongodb.client.model.Filters import io.ktor.http.* @@ -19,143 +18,124 @@ import io.ktor.server.routing.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.bson.Document +import org.bson.conversions.Bson class DirectoryService( - private val database: MongoDatabase + private val database: MongoDatabase, + private val timers: MongoCollection = database.getCollection(Collections.TIMERS), + private val directory: MongoCollection = 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) { - val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 - val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 10 - val param = call.parameters["type"] ?: throw IllegalArgumentException(ErrorMessages.InvalidAnimeType.message) + val param = call.getStringSafeParam("type").uppercase() + val page = call.getIntSafeQueryParam("page", 1) + val size = call.getIntSafeQueryParam("size", 10) - if (page < 1 || size < 1) call.respond(HttpStatusCode.BadRequest, ErrorMessages.InvalidSizeAndPage.message) 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 collection = database.getCollection(timerKey) - - val needsUpdate = timers.needsUpdate( - amount = 30, - key = timerKey, - unit = TimeUnit.DAY, - ) - - if (needsUpdate) { - collection.deleteMany(Document()) - - val animes = directory - .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) - 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)) - } else { - val elements = collection - .find() - .skip(skipCount) - .limit(size) - .toList() - - val responseItems = elements.map { documentToAnimeDirectoryEntity(it) } - val response = PaginationResponse( - page = page, - data = responseItems, - size = responseItems.size + if (page < 1 || size < 1) call.badRequestError(ErrorMessages.InvalidSizeAndPage.message) + if (parseAnimeType(param) == null) call.badRequestError(ErrorMessages.InvalidAnimeType.message) + + if (timers.needsUpdate(timerKey, 30, TimeUnit.DAY)) { + getRemoteData( + newCollection = collection, + remoteCollection = directory, + mapper = { documentToAnimeDirectoryEntity(it) }, + filters = Filters.eq("type", param), + onQuerySuccess = { data -> + val documents = data.map { Document.parse(Json.encodeToString(it)) } + if (documents.isNotEmpty()) collection.insertMany(documents) + timers.update(timerKey) + } ) - - call.respond(HttpStatusCode.OK, Json.encodeToString(response)) } + + getLocalData( + page = page, + size = size, + skipCount = skipCount, + collection = collection, + mapper = { documentToAnimeDirectoryEntity(it) }, + onQuerySuccess = { data -> call.respond(HttpStatusCode.OK, Json.encodeToString(data)) } + ) } suspend fun getAnimeBySeason(call: RoutingCall) { - val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 - val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 10 - val year = call.parameters["year"]?.toIntOrNull() ?: throw IllegalArgumentException(ErrorMessages.NotFound.message) - val season = call.parameters["season"] ?: throw IllegalArgumentException(ErrorMessages.NotFound.message) + val year = call.getIntSafeParam("year") + val season = call.getStringSafeParam("season") + val page = call.getIntSafeQueryParam("page", 1) + val size = call.getIntSafeQueryParam("size", 10) - if (page < 1 || size < 1) call.respond(HttpStatusCode.BadRequest, ErrorMessages.InvalidSizeAndPage.message) val skipCount = (page - 1) * size - val timerKey = "${TimerKey.ANIME_TYPE}${year}_${season.lowercase()}" val collection = database.getCollection(timerKey) - - val needsUpdate = timers.needsUpdate( - amount = 30, - key = timerKey, - unit = TimeUnit.DAY, - ) - - if (needsUpdate) { - collection.deleteMany(Document()) - - val animes = directory - .find( - Filters.and( - Filters.eq("year", year), - Filters.eq("season", season.lowercase()) - ) - ).toList() - - val animeTypes = animes.map { documentToAnimeTypeEntity(it) } - val documents = animeTypes.map { anime -> Document.parse(Json.encodeToString(anime)) } - if (documents.isNotEmpty()) collection.insertMany(documents) - timers.update(timerKey) - - val animeSeasonDb = collection - .find() - .skip(skipCount) - .limit(size) - .toList() - - val animeSeason = animeSeasonDb.map { documentToAnimeTypeEntity(it) } - val response = PaginationResponse( - page = page, - data = animeSeason, - size = animeSeason.size + if (page < 1 || size < 1) call.badRequestError(ErrorMessages.InvalidSizeAndPage.message) + + if (timers.needsUpdate(timerKey, 30, TimeUnit.DAY)) { + getRemoteData( + newCollection = collection, + remoteCollection = directory, + mapper = { documentToAnimeDirectoryEntity(it) }, + filters = Filters.and( + Filters.eq("year", year), + Filters.eq("season", season.lowercase()) + ), + onQuerySuccess = { data -> + val documents = data.map { Document.parse(Json.encodeToString(it)) } + if (documents.isNotEmpty()) collection.insertMany(documents) + timers.update(timerKey) + } ) - - call.respond(HttpStatusCode.OK, Json.encodeToString(response)) - } else { - val elements = collection - .find() - .skip(skipCount) - .limit(size) - .toList() - - val responseItems = elements.map { documentToAnimeDirectoryEntity(it) } - val response = PaginationResponse( - page = page, - data = responseItems, - size = responseItems.size - ) - - call.respond(HttpStatusCode.OK, Json.encodeToString(response)) } + + getLocalData( + page = page, + size = size, + skipCount = skipCount, + collection = collection, + mapper = { documentToAnimeDirectoryEntity(it) }, + onQuerySuccess = { data -> call.respond(HttpStatusCode.OK, Json.encodeToString(data)) } + ) } +} + +private fun getRemoteData( + filters: Bson, + mapper: (Document) -> T, + onQuerySuccess: (List) -> Unit, + newCollection: MongoCollection, + remoteCollection: MongoCollection, +) { + newCollection.deleteMany(Document()) + + val query = remoteCollection + .find(filters) + .toList() + .map { mapper(it) } + + onQuerySuccess(query) +} + +private suspend fun getLocalData( + page: Int, + size: Int, + skipCount: Int, + mapper: (Document) -> T, + collection: MongoCollection, + onQuerySuccess: suspend (PaginationResponse) -> Unit +) { + val query = collection + .find() + .skip(skipCount) + .limit(size) + .toList() + .map { mapper(it) } + + val paginate = PaginationResponse( + page = page, + data = query, + size = query.size + ) + + onQuerySuccess(paginate) } \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/news/mappers/NewsMappers.kt b/src/main/kotlin/com/jeluchu/features/news/mappers/NewsMappers.kt new file mode 100644 index 0000000..a512486 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/news/mappers/NewsMappers.kt @@ -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 +) = 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("categories") +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/news/models/NewEntity.kt b/src/main/kotlin/com/jeluchu/features/news/models/NewEntity.kt new file mode 100644 index 0000000..e6963f6 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/news/models/NewEntity.kt @@ -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 +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/news/routes/NewsRoutes.kt b/src/main/kotlin/com/jeluchu/features/news/routes/NewsRoutes.kt new file mode 100644 index 0000000..46495e4 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/news/routes/NewsRoutes.kt @@ -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) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/news/services/NewsService.kt b/src/main/kotlin/com/jeluchu/features/news/services/NewsService.kt new file mode 100644 index 0000000..6b0dc77 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/news/services/NewsService.kt @@ -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().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().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)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/themes/models/anime/Anime.kt b/src/main/kotlin/com/jeluchu/features/themes/models/anime/Anime.kt new file mode 100644 index 0000000..e33de7e --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/themes/models/anime/Anime.kt @@ -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? = emptyList() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/themes/models/anime/AnimeThemeEntry.kt b/src/main/kotlin/com/jeluchu/features/themes/models/anime/AnimeThemeEntry.kt new file mode 100644 index 0000000..ca210fc --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/themes/models/anime/AnimeThemeEntry.kt @@ -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