Include Schedule endpoints and prepare for Ranking endpoints

This commit is contained in:
Jéluchu 2024-11-28 22:47:44 +01:00
parent 1976c82603
commit dfc1b5a2b8
48 changed files with 1450 additions and 4 deletions

View File

@ -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)

View File

@ -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]

View File

@ -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)
}
}

View File

@ -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 <T> request(
url: String,
deserializer: DeserializationStrategy<T>
): 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 }
}
}

View File

@ -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<Document>.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<Document>.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)
)
}

View File

@ -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.")
}

View File

@ -0,0 +1,8 @@
package com.jeluchu.core.models
import java.time.Instant
data class TimestampEntry(
val key: String,
val lastUpdated: Instant
)

View File

@ -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? = ""
)

View File

@ -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
)

View File

@ -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<Title>? = 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()
)
}
}

View File

@ -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?
)
}

View File

@ -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? = ""
)

View File

@ -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
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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? = ""
)

View File

@ -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
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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()
)

View File

@ -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?
)

View File

@ -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
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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()
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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()
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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()
)

View File

@ -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"
}

View File

@ -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) }

View File

@ -0,0 +1,8 @@
package com.jeluchu.core.utils
enum class TimeUnit {
DAY,
HOUR,
MINUTE,
SECOND
}

View File

@ -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
}

View File

@ -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")
)

View File

@ -1,4 +1,4 @@
package com.example.models
package com.jeluchu.features.anime.models.anime
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,6 @@
package com.example.models
import com.jeluchu.features.anime.models.anime.Images
import kotlinx.serialization.Serializable
@Serializable

View File

@ -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) }
}

View File

@ -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())
}

View File

@ -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,
)

View File

@ -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()
)

View File

@ -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()
)

View File

@ -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) }
}

View File

@ -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())
}