diff --git a/build.gradle.kts b/build.gradle.kts index f3da4e7..bc84327 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,9 @@ repositories { dependencies { implementation(libs.bson) + implementation(libs.ktor.client.cio) implementation(libs.logback.classic) + implementation(libs.ktor.client.core) implementation(libs.ktor.server.core) implementation(libs.ktor.server.netty) implementation(libs.ktor.server.swagger) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02d0798..d234518 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,8 @@ logback-version = "1.4.14" mongo-version = "4.10.2" [libraries] +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-version" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor-version" } ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor-version" } ktor-server-swagger = { module = "io.ktor:ktor-server-swagger-jvm", version.ref = "ktor-version" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor-version" } @@ -16,8 +18,6 @@ bson = { module = "org.mongodb:bson", version.ref = "mongo-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" } -ktor-server-test-host = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor-version" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin-version" } ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "kotlin-version" } [plugins] diff --git a/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt b/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt index aa4e381..2ee2541 100644 --- a/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt +++ b/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt @@ -1,6 +1,8 @@ package com.jeluchu.core.configuration import com.jeluchu.features.anime.routes.animeEndpoints +import com.jeluchu.features.rankings.routes.rankingsEndpoints +import com.jeluchu.features.schedule.routes.scheduleEndpoints import com.mongodb.client.MongoDatabase import io.ktor.server.application.* import io.ktor.server.routing.* @@ -11,5 +13,7 @@ fun Application.initRoutes( route("api/v5") { initDocumentation() animeEndpoints(mongoDatabase) + rankingsEndpoints(mongoDatabase) + scheduleEndpoints(mongoDatabase) } } \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/connection/RestClient.kt b/src/main/kotlin/com/jeluchu/core/connection/RestClient.kt new file mode 100644 index 0000000..577275b --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/connection/RestClient.kt @@ -0,0 +1,27 @@ +package com.jeluchu.core.connection + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.json.Json + +object RestClient { + private val client = HttpClient(CIO) + private val json = Json { ignoreUnknownKeys = true } + + suspend fun request( + url: String, + deserializer: DeserializationStrategy + ): T { + return runCatching { + val response = client.get(url) { + headers { append(HttpHeaders.Accept, ContentType.Application.Json.toString()) } + } + + json.decodeFromString(deserializer, response.bodyAsText()) + }.getOrElse { throwable -> throw throwable } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/extensions/UpdatingExtensions.kt b/src/main/kotlin/com/jeluchu/core/extensions/UpdatingExtensions.kt new file mode 100644 index 0000000..36cd4ab --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/extensions/UpdatingExtensions.kt @@ -0,0 +1,45 @@ +package com.jeluchu.core.extensions + +import com.jeluchu.core.utils.TimeUnit +import com.jeluchu.core.utils.TimerKey +import com.mongodb.client.MongoCollection +import com.mongodb.client.model.Filters.eq +import com.mongodb.client.model.ReplaceOptions +import org.bson.Document +import java.time.Duration +import java.time.Instant +import java.util.* + +fun MongoCollection.needsUpdate( + key: String, + amount: Long = 5, + unit: TimeUnit = TimeUnit.HOUR +): Boolean { + val currentTime = Instant.now() + val timestampEntry = find(eq(TimerKey.KEY, key)).firstOrNull() + + return if (timestampEntry == null) true else { + val lastUpdatedDate = timestampEntry.getDate(TimerKey.LAST_UPDATED) + val lastUpdated = lastUpdatedDate.toInstant() + val duration = Duration.between(lastUpdated, currentTime) + + when (unit) { + TimeUnit.DAY -> duration.toDays() >= amount + TimeUnit.HOUR -> duration.toHours() >= amount + TimeUnit.MINUTE -> duration.toMinutes() >= amount + TimeUnit.SECOND -> duration.toSeconds() >= amount + } + } +} + +fun MongoCollection.update(key: String) { + val currentTime = Instant.now() + val newTimestampDocument = Document(TimerKey.KEY, key) + .append(TimerKey.LAST_UPDATED, Date.from(currentTime)) + + replaceOne( + eq(TimerKey.KEY, TimerKey.SCHEDULE), + newTimestampDocument, + ReplaceOptions().upsert(true) + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt b/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt index 5f8d123..e886e39 100644 --- a/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt +++ b/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt @@ -1,10 +1,12 @@ package com.jeluchu.core.messages +import com.jeluchu.core.utils.Day + sealed class ErrorMessages(val message: String) { - data class Custom(val error: String) : ErrorMessages(error) data object NotFound : ErrorMessages("Nyaaaaaaaan! This request has not been found by our alpaca-neko") 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 InvalidInput : ErrorMessages("Invalid input provided") data object UnauthorizedMongo : ErrorMessages("Check the MongoDb Connection String to be able to correctly access this request.") } \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/TimestampEntry.kt b/src/main/kotlin/com/jeluchu/core/models/TimestampEntry.kt new file mode 100644 index 0000000..5333162 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/TimestampEntry.kt @@ -0,0 +1,8 @@ +package com.jeluchu.core.models + +import java.time.Instant + +data class TimestampEntry( + val key: String, + val lastUpdated: Instant +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Aired.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Aired.kt new file mode 100644 index 0000000..a584707 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Aired.kt @@ -0,0 +1,29 @@ +package com.jeluchu.core.models.jikan.anime + +import com.jeluchu.core.models.jikan.anime.Prop +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Aired data class. + */ +@Serializable +data class Aired( + /** + * Start date in ISO8601 format. + */ + @SerialName("from") + val from: String? = "", + + /** + * @see Prop for the detail. + */ + @SerialName("prop") + val prop: Prop? = Prop(), + + /** + * End date in ISO8601 format. + */ + @SerialName("to") + val to: String? = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Anime.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Anime.kt new file mode 100644 index 0000000..6123bc8 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Anime.kt @@ -0,0 +1,15 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName + +/** + * Anime data class. + */ +data class Anime( + + /** + * Data for anime requested. + */ + @SerialName("data") + val data: AnimeData +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/AnimeData.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/AnimeData.kt new file mode 100644 index 0000000..6d6742d --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/AnimeData.kt @@ -0,0 +1,285 @@ +package com.jeluchu.core.models.jikan.anime + +import com.jeluchu.core.utils.Day +import com.jeluchu.features.schedule.models.DayEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * AnimeInfo data class. + */ +@Serializable +data class AnimeData( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int? = 0, + + /** + * Anime's MyAnimeList link. + */ + @SerialName("url") + val url: String? = "", + + /** + * Anime's MyAnimeList cover/image link. + * @see Images for the detail. + */ + @SerialName("images") + val images: Images? = Images(), + + /** + * Anime's official trailer URL. + * @see Trailer for the detail. + */ + @SerialName("trailer") + val trailer: Trailer? = Trailer(), + + /** + * When entry is pending approval on MAL. + */ + @SerialName("approved") + val approved: Boolean? = false, + + /** + * Title of the anime. + * @see Title for the detail. + */ + @SerialName("titles") + val titles: List? = emptyList(), + + /** + * Title of the anime. + */ + @Deprecated("Use 'titles: List<Title>' to get the title") + @SerialName("title") + val title: String? = "", + + /** + * Title of the anime in English. + */ + @Deprecated("Use 'titles: List<Title>' to get the title") + @SerialName("title_english") + val titleEnglish: String? = "", + + /** + * Title of the anime in Japanese. + */ + @Deprecated("Use 'titles: List<Title>' to get the title") + @SerialName("title_japanese") + val titleJapanese: String? = "", + + /** + * List of anime's synonyms. + * @return null if there's none. + */ + @Deprecated("Use 'titles: List<Title>' to get the title") + @SerialName("title_synonyms") + val titleSynonyms: List<String>? = emptyList(), + + /** + * Type of the anime. + * @see AnimeType for the detail. + */ + @SerialName("type") + val type: String? = "", + + /** + * Source of the anime. + */ + @SerialName("source") + val source : String? = "", + + /** + * Total episode(s) of the anime. + */ + @SerialName("episodes") + val episodes: Int? = 0, + + /** + * Status of the anime (e.g "Airing", "Not yet airing", etc). + */ + @SerialName("status") + val status : String? = "", + + /** + * Whether the anime is currently airing or not. + */ + @SerialName("airing") + val airing: Boolean? = false, + + /** + * Interval of airing time in ISO8601 format. + * @see Aired for the detail. + * @return null if there's none + */ + @SerialName("aired") + val aired: Aired? = Aired(), + + /** + * Duration per episode. + */ + @SerialName("duration") + val duration : String? = "", + + /** + * Age rating of the anime. + */ + @SerialName("rating") + val rating : String? = "", + + /** + * Score at MyAnimeList. Formatted up to 2 decimal places. + */ + @SerialName("score") + val score: Float? = 0f, + + /** + * Number of people/users that scored the anime. + */ + @SerialName("scored_by") + val scoredBy: Int? = 0, + + /** + * Anime's score rank on MyAnimeList. + */ + @SerialName("rank") + val rank: Int? = 0, + + /** + * Anime's popularity rank on MyAnimeList. + */ + @SerialName("popularity") + val popularity: Int? = 0, + + /** + * Anime's members count on MyAnimeList. + */ + @SerialName("members") + val members: Int? = 0, + + /** + * Anime's favorites count on MyAnimeList. + */ + @SerialName("favorites") + val favorites: Int? = 0, + + /** + * Synopsis of the anime. + */ + @SerialName("synopsis") + val synopsis : String? = "", + + /** + * Background info of the anime. + */ + @SerialName("background") + val background : String? = "", + + /** + * Season where anime premiered. + */ + @SerialName("season") + val season: String? = "", + + /** + * Year where anime premiered. + */ + @SerialName("year") + val year: Int? = 0, + + /** + * Broadcast date of the anime (day and time). + * @see Broadcast for the detail. + */ + @SerialName("broadcast") + val broadcast: Broadcast? = Broadcast(), + + /** + * List of producers of this anime. + * @see Producer for the detail. + */ + @SerialName("producers") + val producers: List<Producer>? = emptyList(), + + /** + * List of licensors of this anime. + * @see Licensor for the detail. + */ + @SerialName("licensors") + val licensors: List<Licensor>? = emptyList(), + + /** + * List of studios of this anime. + * @see Studio for the detail. + * + */ + @SerialName("studios") + val studios: List<Studio>? = emptyList(), + + /** + * List of genre of this anime. + * @see Genre for the detail. + */ + @SerialName("genres") + val genres: List<Genre>? = emptyList(), + + /** + * List of explicit genre of this anime. + * @see ExplicitGenre for the detail. + */ + @SerialName("explicit_genres") + val explicitGenres: List<ExplicitGenre>? = emptyList(), + + /** + * List of themes of this anime. + * @see Themes for the detail. + */ + @SerialName("themes") + val themes: List<Themes>? = emptyList(), + + /** + * Demographic of this anime. + * @see Demographic for the detail. + */ + @SerialName("demographics") + val demographics: List<Demographic>? = emptyList(), + + /** + * Relation of this anime. + * @see Relation for the detail. + */ + @SerialName("relations") + val relations: List<Relation>? = emptyList(), + + /** + * Theme of this anime. + * @see Theme for the detail. + */ + @SerialName("theme") + val theme: Theme? = Theme(), + + /** + * Theme of this anime. + * @see External for the detail. + */ + @SerialName("external") + val external: List<External>? = emptyList(), + + /** + * Theme of this anime. + * @see Streaming for the detail. + */ + @SerialName("streaming") + val streaming: List<Streaming>? = emptyList() +) { + companion object { + fun AnimeData.toDayEntity(day: Day) = DayEntity( + malId = malId ?: 0, + day = day.name.lowercase(), + image = images?.webp?.large.orEmpty(), + title = titles?.first()?.title.orEmpty() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/AnimeStatistics.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/AnimeStatistics.kt new file mode 100644 index 0000000..878a7ff --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/AnimeStatistics.kt @@ -0,0 +1,31 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName + +data class AnimeStatistics( + @SerialName("data") + val data: Statistics? +) { + data class Statistics( + @SerialName("completed") + val completed: Int?, + + @SerialName("dropped") + val dropped: Int?, + + @SerialName("on_hold") + val onHold: Int?, + + @SerialName("plan_to_watch") + val planToWatch: Int?, + + @SerialName("scores") + val scores: List<Score>?, + + @SerialName("total") + val total: Int?, + + @SerialName("watching") + val watching: Int? + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Broadcast.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Broadcast.kt new file mode 100644 index 0000000..52b1926 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Broadcast.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Broadcast data class. + */ +@Serializable +data class Broadcast( + /** + * Day in broadcast. + */ + @SerialName("day") + val day: String? = "", + + /** + * String date in broadcast. + */ + @SerialName("string") + val string: String? = "", + + /** + * Time date in broadcast. + */ + @SerialName("time") + val time: String? = "", + + /** + * Timezone in broadcast. + */ + @SerialName("timezone") + val timezone: String? = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/DateProp.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/DateProp.kt new file mode 100644 index 0000000..f66ca2a --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/DateProp.kt @@ -0,0 +1,28 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * DateProp data class. + */ +@Serializable +data class DateProp( + /** + * Day in date. + */ + @SerialName("day") + val day: Int? = 0, + + /** + * Month in date. + */ + @SerialName("month") + val month: Int? = 0, + + /** + * Year in date. + */ + @SerialName("year") + val year: Int? = 0 +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Demographic.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Demographic.kt new file mode 100644 index 0000000..1e2b362 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Demographic.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Demographic data class. + */ +@Serializable +data class Demographic( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for demographic. + */ + @SerialName("name") + val name: String?, + + /** + * Type for demographic. + */ + @SerialName("type") + val type: String?, + + /** + * Url for demographic. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Entry.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Entry.kt new file mode 100644 index 0000000..f6af559 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Entry.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Entry data class. + */ +@Serializable +data class Entry( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for entry. + */ + @SerialName("name") + val name: String?, + + /** + * Type for entry. + */ + @SerialName("type") + val type: String?, + + /** + * Url for entry. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/ExplicitGenre.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/ExplicitGenre.kt new file mode 100644 index 0000000..b720ecd --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/ExplicitGenre.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * ExplicitGenre data class. + */ +@Serializable +data class ExplicitGenre( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for explicit genre. + */ + @SerialName("name") + val name: String?, + + /** + * Type for explicit genre. + */ + @SerialName("type") + val type: String?, + + /** + * Url for explicit genre. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/External.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/External.kt new file mode 100644 index 0000000..78994ea --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/External.kt @@ -0,0 +1,22 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * External data class. + */ +@Serializable +data class External( + /** + * Name of external info. + */ + @SerialName("name") + val name: String?, + + /** + * Url of external info. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Genre.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Genre.kt new file mode 100644 index 0000000..246f501 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Genre.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Genre data class. + */ +@Serializable +data class Genre( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for genre. + */ + @SerialName("name") + val name: String?, + + /** + * Type for genre. + */ + @SerialName("type") + val type: String?, + + /** + * Url for genre. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/ImageFormat.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/ImageFormat.kt new file mode 100644 index 0000000..cf71e7b --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/ImageFormat.kt @@ -0,0 +1,22 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ImageFormat( + @SerialName("image_url") + val generic: String? = "", + + @SerialName("small_image_url") + val small: String? = "", + + @SerialName("medium_image_url") + val medium: String? = "", + + @SerialName("large_image_url") + val large: String? = "", + + @SerialName("maximum_image_url") + val maximum: String? = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Images.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Images.kt new file mode 100644 index 0000000..4d0089f --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Images.kt @@ -0,0 +1,22 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Images data class. + */ +@Serializable +data class Images( + /** + * Images for jpg image type. + */ + @SerialName("jpg") + val jpg: ImageFormat? = null, + + /** + * Images for webp image type. + */ + @SerialName("webp") + val webp: ImageFormat? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Licensor.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Licensor.kt new file mode 100644 index 0000000..767784d --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Licensor.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Licensor data class. + */ +@Serializable +data class Licensor( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for licensor. + */ + @SerialName("name") + val name: String?, + + /** + * Type for licensor. + */ + @SerialName("type") + val type: String?, + + /** + * Url for licensor. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Producer.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Producer.kt new file mode 100644 index 0000000..13866a3 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Producer.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Producer data class. + */ +@Serializable +data class Producer( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for producer. + */ + @SerialName("name") + val name: String?, + + /** + * Type for producer. + */ + @SerialName("type") + val type: String?, + + /** + * Url for producer. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Prop.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Prop.kt new file mode 100644 index 0000000..bfbb770 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Prop.kt @@ -0,0 +1,30 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Prop data class. + */ +@Serializable +data class Prop( + /** + * Start date. + * @see DateProp for the detail. + */ + @SerialName("from") + val from: DateProp? = DateProp(), + + /** + * String with date. + */ + @SerialName("string") + val string: String? = "", + + /** + * End date. + * @see DateProp for the detail. + */ + @SerialName("to") + val to: DateProp? = DateProp() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Relation.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Relation.kt new file mode 100644 index 0000000..fa5fb13 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Relation.kt @@ -0,0 +1,23 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Relation data class. + */ +@Serializable +data class Relation( + /** + * List of entries for relation in anime. + * @see Entry for the detail. + */ + @SerialName("entry") + val entry: List<Entry>?, + + /** + * Relation for anime + */ + @SerialName("relation") + val relation: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Score.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Score.kt new file mode 100644 index 0000000..5741b02 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Score.kt @@ -0,0 +1,14 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName + +data class Score( + @SerialName("percentage") + val percentage: Double, + + @SerialName("score") + val score: Int, + + @SerialName("votes") + val votes: Int +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Streaming.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Streaming.kt new file mode 100644 index 0000000..f48ee05 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Streaming.kt @@ -0,0 +1,22 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Streaming data class. + */ +@Serializable +data class Streaming( + /** + * Name of streaming info. + */ + @SerialName("name") + val name: String?, + + /** + * Url of streaming info. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Studio.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Studio.kt new file mode 100644 index 0000000..0554396 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Studio.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Studio data class. + */ +@Serializable +data class Studio( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for studio. + */ + @SerialName("name") + val name: String?, + + /** + * Type for studio. + */ + @SerialName("type") + val type: String?, + + /** + * Url for studio. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Theme.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Theme.kt new file mode 100644 index 0000000..244bb4d --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Theme.kt @@ -0,0 +1,22 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Theme data class. + */ +@Serializable +data class Theme( + /** + * List of endings. + */ + @SerialName("endings") + val endings: List<String>? = emptyList(), + + /** + * List of openings. + */ + @SerialName("openings") + val openings: List<String>? = emptyList() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Themes.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Themes.kt new file mode 100644 index 0000000..f3c46f9 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Themes.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Themes data class. + */ +@Serializable +data class Themes( + /** + * ID associated with MyAnimeList. + */ + @SerialName("mal_id") + val malId: Int?, + + /** + * Name for themes. + */ + @SerialName("name") + val name: String?, + + /** + * Type for themes. + */ + @SerialName("type") + val type: String?, + + /** + * Url for themes. + */ + @SerialName("url") + val url: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Title.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Title.kt new file mode 100644 index 0000000..f2fb436 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Title.kt @@ -0,0 +1,22 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Title data class. + */ +@Serializable +data class Title( + /** + * Title for anime. + */ + @SerialName("title") + val title: String?, + + /** + * Title type for anime. + */ + @SerialName("type") + val type: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Trailer.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Trailer.kt new file mode 100644 index 0000000..fa4993c --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/anime/Trailer.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.anime + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Trailer data class. + */ +@Serializable +data class Trailer( + /** + * Embed url for trailer. + */ + @SerialName("embed_url") + val embedUrl: String? = "", + + /** + * Url for trailer. + */ + @SerialName("url") + val url: String? = "", + + /** + * Youtube id for trailer. + */ + @SerialName("youtube_id") + val youtubeId: String? = "", + + /** + * Images for trailer. + */ + @SerialName("images") + val images: ImageFormat? = ImageFormat() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/search/ItemsPage.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/search/ItemsPage.kt new file mode 100644 index 0000000..810d555 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/search/ItemsPage.kt @@ -0,0 +1,28 @@ +package com.jeluchu.core.models.jikan.search + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * ItemsPage data class. + */ +@Serializable +data class ItemsPage( + /** + * Count page. + */ + @SerialName("count") + val count: Int? = 0, + + /** + * Total items availables. + */ + @SerialName("total") + val total: Int? = 0, + + /** + * Total items per page. + */ + @SerialName("per_page") + val itemsPerPage: Int? = 0 +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/search/Pagination.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/search/Pagination.kt new file mode 100644 index 0000000..86c53b8 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/search/Pagination.kt @@ -0,0 +1,34 @@ +package com.jeluchu.core.models.jikan.search + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Pagination data class. + */ +@Serializable +data class Pagination( + /** + * Current page available. + */ + @SerialName("current_page") + val currentPage: Int? = 0, + + /** + * Last page available. + */ + @SerialName("last_visible_page") + val lastPage: Int? = 0, + + /** + * Items information. + */ + @SerialName("items") + val itemsPage: ItemsPage? = ItemsPage(), + + /** + * Request hast next page or not. + */ + @SerialName("has_next_page") + val hasNextPage: Boolean? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/jikan/search/Search.kt b/src/main/kotlin/com/jeluchu/core/models/jikan/search/Search.kt new file mode 100644 index 0000000..1ab2620 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/jikan/search/Search.kt @@ -0,0 +1,23 @@ +package com.jeluchu.core.models.jikan.search + +import com.jeluchu.core.models.jikan.anime.AnimeData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Search data class. + */ +@Serializable +data class Search( + /** + * Pagination info for request + */ + @SerialName("pagination") + val pagination: Pagination? = Pagination(), + + /** + * Data list of all anime found. + */ + @SerialName("data") + val data: List<AnimeData>? = emptyList() +) \ 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 08c1154..763c3c4 100644 --- a/src/main/kotlin/com/jeluchu/core/utils/Constants.kt +++ b/src/main/kotlin/com/jeluchu/core/utils/Constants.kt @@ -1,6 +1,27 @@ package com.jeluchu.core.utils +object BaseUrls { + const val JIKAN = "https://api.jikan.moe/v4/" +} + +object Endpoints { + const val SCHEDULES = "schedules" +} + + object Routes { + const val SCHEDULE = "/schedule" const val DIRECTORY = "/directory" + const val TOP_ANIME = "/top/anime" + const val TOP_MANGA = "/top/manga" + const val TOP_PEOPLE = "/top/people" const val ANIME_DETAILS = "/anime/{id}" + const val SCHEDULE_DAY = "/schedule/{day}" + const val TOP_CHARACTER = "/top/character" +} + +object TimerKey { + const val KEY = "key" + const val SCHEDULE = "schedule" + const val LAST_UPDATED = "lastUpdated" } \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/utils/Day.kt b/src/main/kotlin/com/jeluchu/core/utils/Day.kt new file mode 100644 index 0000000..ed18d38 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/utils/Day.kt @@ -0,0 +1,16 @@ +package com.jeluchu.core.utils + +import kotlinx.serialization.Serializable + +@Serializable +enum class Day { + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY, + SUNDAY, +} + +fun parseDay(day: String) = Day.entries.firstOrNull { it.name.equals(day, ignoreCase = true) } \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/utils/TimeUnit.kt b/src/main/kotlin/com/jeluchu/core/utils/TimeUnit.kt new file mode 100644 index 0000000..13c3b93 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/utils/TimeUnit.kt @@ -0,0 +1,8 @@ +package com.jeluchu.core.utils + +enum class TimeUnit { + DAY, + HOUR, + MINUTE, + SECOND +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/utils/parseJsonArrayToDocuments.kt b/src/main/kotlin/com/jeluchu/core/utils/parseJsonArrayToDocuments.kt new file mode 100644 index 0000000..a708842 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/utils/parseJsonArrayToDocuments.kt @@ -0,0 +1,28 @@ +package com.jeluchu.core.utils + +import com.jeluchu.features.schedule.models.DayEntity +import com.jeluchu.features.schedule.models.ScheduleData +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.bson.Document + +fun parseScheduleDataToDocuments(data: ScheduleData): List<Document> { + val documents = mutableListOf<Document>() + fun processDay(dayList: List<DayEntity>?) { + dayList?.forEach { animeData -> + val animeJsonString = Json.encodeToString(animeData) + val document = Document.parse(animeJsonString) + documents.add(document) + } + } + + processDay(data.monday) + processDay(data.tuesday) + processDay(data.wednesday) + processDay(data.thursday) + processDay(data.friday) + processDay(data.saturday) + processDay(data.sunday) + + return documents +} \ No newline at end of file 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 9208655..7657a5e 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt @@ -2,7 +2,9 @@ package com.jeluchu.features.anime.mappers import com.example.models.* import com.jeluchu.core.extensions.* +import com.jeluchu.features.anime.models.anime.Images import com.jeluchu.features.anime.models.directory.AnimeDirectoryEntity +import com.jeluchu.features.schedule.models.DayEntity import org.bson.Document fun documentToAnimeDirectoryEntity(doc: Document) = AnimeDirectoryEntity( @@ -211,3 +213,10 @@ fun documentToVideoPromo(doc: Document): VideoPromo { images = doc.get("images", Document::class.java)?.let { documentToImages(it) } ?: Images() ) } + +fun documentToScheduleDayEntity(doc: Document) = DayEntity( + day = doc.getStringSafe("day"), + malId = doc.getIntSafe("malId"), + image = doc.getStringSafe("image"), + title = doc.getStringSafe("title") +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Images.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Images.kt index 4e99f1d..610e118 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Images.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Images.kt @@ -1,4 +1,4 @@ -package com.example.models +package com.jeluchu.features.anime.models.anime import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/VideoPromo.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/VideoPromo.kt index 0715092..0f58cc9 100644 --- a/src/main/kotlin/com/jeluchu/features/anime/models/anime/VideoPromo.kt +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/VideoPromo.kt @@ -1,5 +1,6 @@ package com.example.models +import com.jeluchu.features.anime.models.anime.Images import kotlinx.serialization.Serializable @Serializable diff --git a/src/main/kotlin/com/jeluchu/features/rankings/routes/RankingsRoutes.kt b/src/main/kotlin/com/jeluchu/features/rankings/routes/RankingsRoutes.kt new file mode 100644 index 0000000..b5b40a8 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/rankings/routes/RankingsRoutes.kt @@ -0,0 +1,15 @@ +package com.jeluchu.features.rankings.routes + +import com.jeluchu.core.extensions.getToJson +import com.jeluchu.core.utils.Routes +import com.jeluchu.features.rankings.services.RankingsService +import com.mongodb.client.MongoDatabase +import io.ktor.server.routing.* + +fun Route.rankingsEndpoints( + mongoDatabase: MongoDatabase, + service: RankingsService = RankingsService(mongoDatabase) +) { + getToJson(Routes.TOP_ANIME) { service.getAnimeByMalId(call) } + getToJson(Routes.TOP_MANGA) { service.getDirectory(call) } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/rankings/services/RankingsService.kt b/src/main/kotlin/com/jeluchu/features/rankings/services/RankingsService.kt new file mode 100644 index 0000000..83eb506 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/rankings/services/RankingsService.kt @@ -0,0 +1,76 @@ +package com.jeluchu.features.rankings.services + +import com.jeluchu.core.connection.RestClient +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.jikan.anime.AnimeData.Companion.toDayEntity +import com.jeluchu.core.utils.* +import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity +import com.jeluchu.features.anime.mappers.documentToMoreInfoEntity +import com.jeluchu.features.anime.mappers.documentToScheduleDayEntity +import com.jeluchu.features.schedule.models.ScheduleData +import com.jeluchu.features.schedule.models.ScheduleEntity +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 RankingsService( + database: MongoDatabase +) { + private val timers = database.getCollection("timers") + private val schedules = database.getCollection("schedule") + + suspend fun getAnimeRanking(call: RoutingCall) { + val needsUpdate = timers.needsUpdate( + amount = 30, + unit = TimeUnit.DAY, + key = TimerKey.SCHEDULE + ) + + if (needsUpdate) { + schedules.deleteMany(Document()) + + val response = ScheduleData( + sunday = getSchedule(Day.SUNDAY).data?.map { it.toDayEntity(Day.SUNDAY) }.orEmpty(), + friday = getSchedule(Day.FRIDAY).data?.map { it.toDayEntity(Day.FRIDAY) }.orEmpty(), + monday = getSchedule(Day.MONDAY).data?.map { it.toDayEntity(Day.MONDAY) }.orEmpty(), + tuesday = getSchedule(Day.TUESDAY).data?.map { it.toDayEntity(Day.TUESDAY) }.orEmpty(), + thursday = getSchedule(Day.THURSDAY).data?.map { it.toDayEntity(Day.THURSDAY) }.orEmpty(), + saturday = getSchedule(Day.SATURDAY).data?.map { it.toDayEntity(Day.SATURDAY) }.orEmpty(), + wednesday = getSchedule(Day.WEDNESDAY).data?.map { it.toDayEntity(Day.WEDNESDAY) }.orEmpty() + ) + + val documentsToInsert = parseScheduleDataToDocuments(response) + if (documentsToInsert.isNotEmpty()) schedules.insertMany(documentsToInsert) + timers.update(TimerKey.SCHEDULE) + + call.respond(HttpStatusCode.OK, Json.encodeToString(response)) + } else { + val elements = schedules.find().toList() + val directory = elements.map { documentToScheduleDayEntity(it) } + val json = Json.encodeToString(directory) + call.respond(HttpStatusCode.OK, json) + } + } + + suspend fun getScheduleByDay(call: RoutingCall) { + val param = call.parameters["day"] ?: throw IllegalArgumentException(ErrorMessages.InvalidMalId.message) + if (parseDay(param) == null) call.respond(HttpStatusCode.BadRequest, ErrorResponse(ErrorMessages.InvalidDay.message)) + + val elements = schedules.find(Filters.eq("day", param.lowercase())).toList() + val directory = elements.map { documentToScheduleDayEntity(it) } + val json = Json.encodeToString(directory) + call.respond(HttpStatusCode.OK, json) + } + + private suspend fun getSchedule(day: Day) = + RestClient.request(BaseUrls.JIKAN + Endpoints.SCHEDULES + "/" + day, ScheduleEntity.serializer()) +} + diff --git a/src/main/kotlin/com/jeluchu/features/schedule/models/DayEntity.kt b/src/main/kotlin/com/jeluchu/features/schedule/models/DayEntity.kt new file mode 100644 index 0000000..e4c4e0a --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/schedule/models/DayEntity.kt @@ -0,0 +1,11 @@ +package com.jeluchu.features.schedule.models + +import kotlinx.serialization.Serializable + +@Serializable +data class DayEntity( + val malId: Int, + val day: String, + val title: String, + val image: String, +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/schedule/models/ScheduleData.kt b/src/main/kotlin/com/jeluchu/features/schedule/models/ScheduleData.kt new file mode 100644 index 0000000..947087a --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/schedule/models/ScheduleData.kt @@ -0,0 +1,49 @@ +package com.jeluchu.features.schedule.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ScheduleData( + /** + * All current season entries scheduled for Monday. + */ + @SerialName("monday") + val monday: List<DayEntity>? = emptyList(), + + /** + * All current season entries scheduled for Tuesday. + */ + @SerialName("tuesday") + val tuesday: List<DayEntity>? = emptyList(), + + /** + * All current season entries scheduled for Wednesday. + */ + @SerialName("wednesday") + val wednesday: List<DayEntity>? = emptyList(), + + /** + * All current season entries scheduled for Thursday. + */ + @SerialName("thursday") + val thursday: List<DayEntity>? = emptyList(), + + /** + * All current season entries scheduled for Friday. + */ + @SerialName("friday") + val friday: List<DayEntity>? = emptyList(), + + /** + * All current season entries scheduled for Saturday. + */ + @SerialName("saturday") + val saturday: List<DayEntity>? = emptyList(), + + /** + * All current season entries scheduled for Sunday. + */ + @SerialName("sunday") + val sunday: List<DayEntity>? = emptyList() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/schedule/models/ScheduleEntity.kt b/src/main/kotlin/com/jeluchu/features/schedule/models/ScheduleEntity.kt new file mode 100644 index 0000000..6c662da --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/schedule/models/ScheduleEntity.kt @@ -0,0 +1,24 @@ +package com.jeluchu.features.schedule.models + +import com.jeluchu.core.models.jikan.search.Pagination +import com.jeluchu.core.models.jikan.anime.AnimeData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Schedule data class. + */ +@Serializable +data class ScheduleEntity( + /** + * Pagination info for request + */ + @SerialName("pagination") + val pagination: Pagination? = Pagination(), + + /** + * Data for anime requested. + */ + @SerialName("data") + val data: List<AnimeData>? = emptyList() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/schedule/routes/ScheduleRoutes.kt b/src/main/kotlin/com/jeluchu/features/schedule/routes/ScheduleRoutes.kt new file mode 100644 index 0000000..74fc22e --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/schedule/routes/ScheduleRoutes.kt @@ -0,0 +1,15 @@ +package com.jeluchu.features.schedule.routes + +import com.jeluchu.core.extensions.getToJson +import com.jeluchu.core.utils.Routes +import com.jeluchu.features.schedule.services.ScheduleService +import com.mongodb.client.MongoDatabase +import io.ktor.server.routing.* + +fun Route.scheduleEndpoints( + mongoDatabase: MongoDatabase, + service: ScheduleService = ScheduleService(mongoDatabase) +) { + getToJson(Routes.SCHEDULE) { service.getSchedule(call) } + getToJson(Routes.SCHEDULE_DAY) { service.getScheduleByDay(call) } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/schedule/services/ScheduleService.kt b/src/main/kotlin/com/jeluchu/features/schedule/services/ScheduleService.kt new file mode 100644 index 0000000..b2b0f70 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/schedule/services/ScheduleService.kt @@ -0,0 +1,73 @@ +package com.jeluchu.features.schedule.services + +import com.jeluchu.core.connection.RestClient +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.jikan.anime.AnimeData.Companion.toDayEntity +import com.jeluchu.core.utils.* +import com.jeluchu.features.anime.mappers.documentToScheduleDayEntity +import com.jeluchu.features.schedule.models.ScheduleData +import com.jeluchu.features.schedule.models.ScheduleEntity +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 ScheduleService( + database: MongoDatabase +) { + private val timers = database.getCollection("timers") + private val schedules = database.getCollection("schedule") + + suspend fun getSchedule(call: RoutingCall) { + val needsUpdate = timers.needsUpdate( + amount = 7, + unit = TimeUnit.DAY, + key = TimerKey.SCHEDULE + ) + + if (needsUpdate) { + schedules.deleteMany(Document()) + + val response = ScheduleData( + sunday = getSchedule(Day.SUNDAY).data?.map { it.toDayEntity(Day.SUNDAY) }.orEmpty(), + friday = getSchedule(Day.FRIDAY).data?.map { it.toDayEntity(Day.FRIDAY) }.orEmpty(), + monday = getSchedule(Day.MONDAY).data?.map { it.toDayEntity(Day.MONDAY) }.orEmpty(), + tuesday = getSchedule(Day.TUESDAY).data?.map { it.toDayEntity(Day.TUESDAY) }.orEmpty(), + thursday = getSchedule(Day.THURSDAY).data?.map { it.toDayEntity(Day.THURSDAY) }.orEmpty(), + saturday = getSchedule(Day.SATURDAY).data?.map { it.toDayEntity(Day.SATURDAY) }.orEmpty(), + wednesday = getSchedule(Day.WEDNESDAY).data?.map { it.toDayEntity(Day.WEDNESDAY) }.orEmpty() + ) + + val documentsToInsert = parseScheduleDataToDocuments(response) + if (documentsToInsert.isNotEmpty()) schedules.insertMany(documentsToInsert) + timers.update(TimerKey.SCHEDULE) + + call.respond(HttpStatusCode.OK, Json.encodeToString(response)) + } else { + val elements = schedules.find().toList() + val directory = elements.map { documentToScheduleDayEntity(it) } + val json = Json.encodeToString(directory) + call.respond(HttpStatusCode.OK, json) + } + } + + suspend fun getScheduleByDay(call: RoutingCall) { + val param = call.parameters["day"] ?: throw IllegalArgumentException(ErrorMessages.InvalidMalId.message) + if (parseDay(param) == null) call.respond(HttpStatusCode.BadRequest, ErrorResponse(ErrorMessages.InvalidDay.message)) + + val elements = schedules.find(Filters.eq("day", param.lowercase())).toList() + val directory = elements.map { documentToScheduleDayEntity(it) } + val json = Json.encodeToString(directory) + call.respond(HttpStatusCode.OK, json) + } + + private suspend fun getSchedule(day: Day) = + RestClient.request(BaseUrls.JIKAN + Endpoints.SCHEDULES + "/" + day, ScheduleEntity.serializer()) +} \ No newline at end of file