From 5d1b33e1e8dc6ff211e65d4e504ee0538ec6a5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9luchu?= Date: Fri, 10 Jan 2025 17:31:33 +0100 Subject: [PATCH] Include filter directory by types and last episodes endpoint added --- .../jeluchu/core/messages/ErrorMessages.kt | 1 + .../animeflv/lastepisodes/EpisodeEntity.kt | 10 +++ .../animeflv/lastepisodes/LastEpisodeData.kt | 19 ++++++ .../animeflv/lastepisodes/LastEpisodes.kt | 9 +++ .../com/jeluchu/core/utils/Constants.kt | 10 ++- .../features/anime/mappers/AnimeMappers.kt | 17 +++++ .../anime/models/directory/AnimeTypeEntity.kt | 13 ++++ .../features/anime/routes/AnimeRoutes.kt | 9 ++- .../features/anime/services/AnimeService.kt | 51 +++++++++++++++ .../anime/services/DirectoryService.kt | 62 +++++++++++++++++++ 10 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/EpisodeEntity.kt create mode 100644 src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodeData.kt create mode 100644 src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodes.kt create mode 100644 src/main/kotlin/com/jeluchu/features/anime/models/directory/AnimeTypeEntity.kt create mode 100644 src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt diff --git a/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt b/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt index adf2189..aeac368 100644 --- a/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt +++ b/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt @@ -7,6 +7,7 @@ sealed class ErrorMessages(val message: String) { data object AnimeNotFound : ErrorMessages("This malId is not in our database") data object InvalidMalId : ErrorMessages("The provided id of malId is invalid") data object InvalidDay : ErrorMessages("Invalid 'day' parameter. Valid values are: ${Day.entries.joinToString(", ") { it.name.lowercase() }}") + data object InvalidAnimeType : ErrorMessages("Invalid 'type' parameter. Valid values are: ${AnimeTypes.entries.joinToString(", ") { it.name.lowercase() }}") data object InvalidTopAnimeType : ErrorMessages("Invalid 'type' parameter. Valid values are: $animeTypesErrorList") data object InvalidTopAnimeFilterType : ErrorMessages("Invalid 'type' parameter. Valid values are: $animeFilterTypesErrorList") data object InvalidTopMangaType : ErrorMessages("Invalid 'type' parameter. Valid values are: $mangaTypesErrorList") diff --git a/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/EpisodeEntity.kt b/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/EpisodeEntity.kt new file mode 100644 index 0000000..87aa89b --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/EpisodeEntity.kt @@ -0,0 +1,10 @@ +package com.jeluchu.core.models.animeflv.lastepisodes + +import kotlinx.serialization.Serializable + +@Serializable +data class EpisodeEntity( + var number: Int, + var image: String, + var title: String +) diff --git a/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodeData.kt b/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodeData.kt new file mode 100644 index 0000000..b05086b --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodeData.kt @@ -0,0 +1,19 @@ +package com.jeluchu.core.models.animeflv.lastepisodes + +import kotlinx.serialization.Serializable + +@Serializable +data class LastEpisodeData( + val cover: String?, + val number: Int?, + val title: String?, + val url: String? +) { + companion object { + fun LastEpisodeData.toEpisodeEntity() = EpisodeEntity( + number = number ?: 0, + image = cover.orEmpty(), + title = title.orEmpty() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodes.kt b/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodes.kt new file mode 100644 index 0000000..6f24861 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/animeflv/lastepisodes/LastEpisodes.kt @@ -0,0 +1,9 @@ +package com.jeluchu.core.models.animeflv.lastepisodes + +import kotlinx.serialization.Serializable + +@Serializable +data class LastEpisodes( + val data: List?, + val success: Boolean? +) \ 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 746b48e..1f68b07 100644 --- a/src/main/kotlin/com/jeluchu/core/utils/Constants.kt +++ b/src/main/kotlin/com/jeluchu/core/utils/Constants.kt @@ -2,6 +2,7 @@ package com.jeluchu.core.utils object BaseUrls { const val JIKAN = "https://api.jikan.moe/v4/" + const val ANIME_FLV = "https://animeflv.ahmedrangel.com/api/" } object Endpoints { @@ -14,6 +15,7 @@ object Endpoints { const val STATISTICS = "statistics" const val CHARACTERS = "characters" const val TOP_CHARACTER = "top/characters" + const val LAST_EPISODES = "list/latest-episodes" } object Routes { @@ -25,7 +27,9 @@ object Routes { const val SCHEDULE = "/schedule" const val DIRECTORY = "/directory" const val CHARACTER = "/characters" - const val ANIME_DETAILS = "/anime/{id}" + const val LAST_EPISODES = "/lastEpisodes" + const val ID = "/{id}" + const val TYPE = "/{type}" const val DAY = "/{day}" const val TOP_CHARACTER = "/top/character" const val RANKINGS = "/{type}/{filter}/{page}" @@ -36,12 +40,16 @@ object TimerKey { const val RANKING = "ranking" const val SCHEDULE = "schedule" const val LAST_UPDATED = "lastUpdated" + const val ANIME_TYPE = "anime_" + const val LAST_EPISODES = "last_episodes" } object Collections { const val TIMERS = "timers" const val SCHEDULES = "schedule" + const val ANIME_TYPE = "anime_" const val ANIME_DETAILS = "anime_details" + const val LAST_EPISODES = "last_episodes" const val ANIME_RANKING = "anime_ranking" const val MANGA_RANKING = "manga_ranking" const val PEOPLE_RANKING = "people_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 37e7f3b..9733b7f 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt @@ -1,8 +1,10 @@ package com.jeluchu.features.anime.mappers import com.jeluchu.core.extensions.* +import com.jeluchu.core.models.animeflv.lastepisodes.EpisodeEntity import com.jeluchu.features.anime.models.anime.* import com.jeluchu.features.anime.models.directory.AnimeDirectoryEntity +import com.jeluchu.features.anime.models.directory.AnimeTypeEntity import com.jeluchu.features.rankings.models.AnimeTopEntity import com.jeluchu.features.schedule.models.DayEntity import org.bson.Document @@ -243,4 +245,19 @@ fun documentToTopEntity(doc: Document) = AnimeTopEntity( type = doc.getStringSafe("type"), subtype = doc.getStringSafe("subtype"), page = doc.getIntSafe("page"), +) + +fun documentToAnimeTypeEntity(doc: Document) = AnimeTypeEntity( + score = doc.getString("score"), + malId = doc.getIntSafe("malId"), + type = doc.getStringSafe("type"), + title = doc.getStringSafe("title"), + image = doc.getStringSafe("poster"), + episodes = doc.getListSafe("episodes").size +) + +fun documentToLastEpisodesEntity(doc: Document) = EpisodeEntity( + number = doc.getIntSafe("number"), + title = doc.getStringSafe("title"), + image = doc.getStringSafe("image") ) \ 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 new file mode 100644 index 0000000..dbb15c2 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/directory/AnimeTypeEntity.kt @@ -0,0 +1,13 @@ +package com.jeluchu.features.anime.models.directory + +import kotlinx.serialization.Serializable + +@Serializable +data class AnimeTypeEntity( + val malId: Int? = 0, + val type: String? = "", + val episodes: Int? = 0, + val title: String? = "", + val image: String? = "", + val score: String? = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/routes/AnimeRoutes.kt b/src/main/kotlin/com/jeluchu/features/anime/routes/AnimeRoutes.kt index 015be1f..1ddb313 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/routes/AnimeRoutes.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/routes/AnimeRoutes.kt @@ -3,15 +3,22 @@ package com.jeluchu.features.anime.routes import com.jeluchu.core.extensions.getToJson import com.jeluchu.core.utils.Routes import com.jeluchu.features.anime.services.AnimeService +import com.jeluchu.features.anime.services.DirectoryService import com.mongodb.client.MongoDatabase import io.ktor.server.routing.* fun Route.animeEndpoints( mongoDatabase: MongoDatabase, service: AnimeService = AnimeService(mongoDatabase), + directoryService: DirectoryService = DirectoryService(mongoDatabase), ) { - getToJson(Routes.ANIME_DETAILS) { service.getAnimeByMalId(call) } + route(Routes.ANIME) { + getToJson(Routes.ID) { service.getAnimeByMalId(call) } + getToJson(Routes.LAST_EPISODES) { service.getLastEpisodes(call) } + } + route(Routes.DIRECTORY) { getToJson { service.getDirectory(call) } + getToJson(Routes.TYPE) { directoryService.getAnimeByType(call) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt b/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt index 91532a1..7b03868 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt @@ -1,10 +1,24 @@ package com.jeluchu.features.anime.services +import com.jeluchu.core.connection.RestClient +import com.jeluchu.core.enums.Day +import com.jeluchu.core.enums.TimeUnit +import com.jeluchu.core.extensions.needsUpdate +import com.jeluchu.core.extensions.update import com.jeluchu.core.messages.ErrorMessages import com.jeluchu.core.models.ErrorResponse +import com.jeluchu.core.models.animeflv.lastepisodes.LastEpisodeData.Companion.toEpisodeEntity +import com.jeluchu.core.models.animeflv.lastepisodes.LastEpisodes +import com.jeluchu.core.models.jikan.anime.AnimeData.Companion.toDayEntity +import com.jeluchu.core.utils.BaseUrls import com.jeluchu.core.utils.Collections +import com.jeluchu.core.utils.Endpoints +import com.jeluchu.core.utils.TimerKey import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity +import com.jeluchu.features.anime.mappers.documentToLastEpisodesEntity import com.jeluchu.features.anime.mappers.documentToMoreInfoEntity +import com.jeluchu.features.anime.mappers.documentToScheduleDayEntity +import com.jeluchu.features.schedule.models.ScheduleEntity import com.mongodb.client.MongoDatabase import com.mongodb.client.model.Filters import io.ktor.http.* @@ -12,11 +26,14 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.bson.Document class AnimeService( database: MongoDatabase ) { + private val timers = database.getCollection(Collections.TIMERS) private val directoryCollection = database.getCollection(Collections.ANIME_DETAILS) + private val lastEpisodesCollection = database.getCollection(Collections.LAST_EPISODES) suspend fun getDirectory(call: RoutingCall) = try { val elements = directoryCollection.find().toList() @@ -36,5 +53,39 @@ class AnimeService( } catch (ex: Exception) { call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.InvalidInput.message)) } + + suspend fun getLastEpisodes(call: RoutingCall) = try { + val needsUpdate = timers.needsUpdate( + amount = 3, + unit = TimeUnit.HOUR, + key = TimerKey.LAST_EPISODES + ) + + if (needsUpdate) { + lastEpisodesCollection.deleteMany(Document()) + + val episodes = getLastedEpisodes().data?.map { it.toEpisodeEntity() }.orEmpty() + val documents = episodes.map { anime -> Document.parse(Json.encodeToString(anime)) } + if (documents.isNotEmpty()) lastEpisodesCollection.insertMany(documents) + timers.update(TimerKey.LAST_EPISODES) + + call.respond(HttpStatusCode.OK, Json.encodeToString(episodes)) + } else { + val elements = lastEpisodesCollection.find().toList() + call.respond(HttpStatusCode.OK, elements.documentToLastEpisodesEntity()) + } + } catch (ex: Exception) { + call.respond(HttpStatusCode.Unauthorized, ErrorResponse(ErrorMessages.UnauthorizedMongo.message)) + } + + private suspend fun getLastedEpisodes() = RestClient.requestWithDelay( + url = BaseUrls.ANIME_FLV + Endpoints.LAST_EPISODES, + deserializer = LastEpisodes.serializer() + ) + + private fun List.documentToLastEpisodesEntity(): String { + val directory = map { documentToLastEpisodesEntity(it) } + return Json.encodeToString(directory) + } } diff --git a/src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt b/src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt new file mode 100644 index 0000000..7e1aab0 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/services/DirectoryService.kt @@ -0,0 +1,62 @@ +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.messages.ErrorMessages +import com.jeluchu.core.models.ErrorResponse +import com.jeluchu.core.utils.Collections +import com.jeluchu.core.utils.TimerKey +import com.jeluchu.features.anime.mappers.documentToAnimeTypeEntity +import com.mongodb.client.MongoDatabase +import com.mongodb.client.model.Filters +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 DirectoryService( + private val database: MongoDatabase +) { + private val timers = database.getCollection(Collections.TIMERS) + private val directory = database.getCollection(Collections.ANIME_DETAILS) + + suspend fun getAnimeByType(call: RoutingCall) { + val param = call.parameters["type"] ?: throw IllegalArgumentException(ErrorMessages.InvalidAnimeType.message) + if (parseAnimeType(param) == null) call.respond( + HttpStatusCode.BadRequest, + ErrorResponse(ErrorMessages.InvalidAnimeType.message) + ) + + val timerKey = "${TimerKey.ANIME_TYPE}${param.lowercase()}" + val needsUpdate = timers.needsUpdate( + amount = 30, + key = timerKey, + unit = TimeUnit.DAY, + ) + + if (needsUpdate) { + val collection = database.getCollection(timerKey) + 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) + + call.respond(HttpStatusCode.OK, Json.encodeToString(animeTypes)) + } else { + val elements = directory.find().toList() + call.respond(HttpStatusCode.OK, elements.documentAnimeTypeMapper()) + } + } + + private fun List.documentAnimeTypeMapper(): String { + val directory = map { documentToAnimeTypeEntity(it) } + return Json.encodeToString(directory) + } +} \ No newline at end of file