mirror of https://github.com/aruppi/aruppi-api
Compare commits
11 Commits
d7ca2965d2
...
a113b7e677
Author | SHA1 | Date |
---|---|---|
![]() |
a113b7e677 | |
![]() |
e57926050b | |
![]() |
05227eca80 | |
![]() |
937d5c6ab3 | |
![]() |
e4334fe9bb | |
![]() |
6931ff7084 | |
![]() |
b0fd2e84ad | |
![]() |
03842ab1f9 | |
![]() |
e40894d0e0 | |
![]() |
5552904983 | |
![]() |
1566c84e8b |
|
@ -0,0 +1,11 @@
|
|||
package com.jeluchu.core.enums
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class AnimeStatusTypes {
|
||||
FINISHED, ONGOING, UPCOMING, UNKNOWN
|
||||
}
|
||||
|
||||
val animeStatusTypesErrorList = AnimeStatusTypes.entries.joinToString(", ") { it.name.lowercase() }
|
||||
fun parseAnimeStatusType(type: String) = AnimeStatusTypes.entries.firstOrNull { it.name.equals(type, ignoreCase = true) }
|
|
@ -0,0 +1,8 @@
|
|||
package com.jeluchu.core.enums
|
||||
|
||||
enum class Season {
|
||||
WINTER, SPRING, SUMMER, FALL
|
||||
}
|
||||
|
||||
val seasonsErrorList = AnimeTypes.entries.joinToString(", ") { it.name.lowercase() }
|
||||
fun parseSeasons(type: String) = Season.entries.firstOrNull { it.name.equals(type, ignoreCase = true) }
|
|
@ -12,6 +12,7 @@ sealed class ErrorMessages(val message: String) {
|
|||
data object InvalidMangaType : ErrorMessages("Invalid 'type' parameter. Valid values are: ${MangaTypes.entries.joinToString(", ") { it.name.lowercase() }}")
|
||||
data object InvalidSizeAndPage : ErrorMessages("Invalid page and size parameters")
|
||||
data object InvalidTopAnimeType : ErrorMessages("Invalid 'type' parameter. Valid values are: $animeTypesErrorList")
|
||||
data object InvalidAnimeStatusType : ErrorMessages("Invalid 'status' parameter. Valid values are: $animeStatusTypesErrorList")
|
||||
data object InvalidTopAnimeFilterType : ErrorMessages("Invalid 'type' parameter. Valid values are: $animeFilterTypesErrorList")
|
||||
data object InvalidTopMangaType : ErrorMessages("Invalid 'type' parameter. Valid values are: $mangaTypesErrorList")
|
||||
data object InvalidTopMangaFilterType : ErrorMessages("Invalid 'type' parameter. Valid values are: $mangaFilterTypesErrorList")
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package com.jeluchu.core.models
|
||||
|
||||
import com.jeluchu.core.extensions.getDocumentSafe
|
||||
import com.jeluchu.core.extensions.getIntSafe
|
||||
import com.jeluchu.core.extensions.getStringSafe
|
||||
import com.jeluchu.core.utils.SeasonCalendar
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.bson.Document
|
||||
|
||||
@Serializable
|
||||
data class SimpleAnimeEntity(
|
||||
val malId: Int,
|
||||
val type: String,
|
||||
val title: String,
|
||||
val image: String,
|
||||
val score: String,
|
||||
val season: SeasonInfo
|
||||
) {
|
||||
@Serializable
|
||||
data class SeasonInfo(
|
||||
val year: Int? = null,
|
||||
val station: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
fun documentToSimpleAnimeEntity(doc: Document) = SimpleAnimeEntity(
|
||||
malId = doc.getIntSafe("malId"),
|
||||
title = doc.getStringSafe("title"),
|
||||
type = doc.getStringSafe("type"),
|
||||
score = doc.getStringSafe("score"),
|
||||
image = doc.getStringSafe("poster"),
|
||||
season = doc.getDocumentSafe("season")?.let { documentToSeasonInfo(it) } ?: SimpleAnimeEntity.SeasonInfo(),
|
||||
)
|
||||
|
||||
fun documentToSeasonInfo(doc: Document) = SimpleAnimeEntity.SeasonInfo(
|
||||
year = doc.getIntSafe("year"),
|
||||
station = doc.getStringSafe("station")
|
||||
)
|
|
@ -0,0 +1,47 @@
|
|||
package com.jeluchu.core.utils
|
||||
|
||||
import com.jeluchu.core.models.PaginationResponse
|
||||
import com.mongodb.client.MongoCollection
|
||||
import org.bson.Document
|
||||
import org.bson.conversions.Bson
|
||||
|
||||
fun <T> getRemoteData(
|
||||
filters: Bson,
|
||||
mapper: (Document) -> T,
|
||||
onQuerySuccess: (List<T>) -> Unit,
|
||||
newCollection: MongoCollection<Document>,
|
||||
remoteCollection: MongoCollection<Document>,
|
||||
) {
|
||||
newCollection.deleteMany(Document())
|
||||
|
||||
val query = remoteCollection
|
||||
.find(filters)
|
||||
.toList()
|
||||
.map { mapper(it) }
|
||||
|
||||
onQuerySuccess(query)
|
||||
}
|
||||
|
||||
suspend fun <T> getLocalData(
|
||||
page: Int,
|
||||
size: Int,
|
||||
skipCount: Int,
|
||||
mapper: (Document) -> T,
|
||||
collection: MongoCollection<Document>,
|
||||
onQuerySuccess: suspend (PaginationResponse<T>) -> Unit
|
||||
) {
|
||||
val query = collection
|
||||
.find()
|
||||
.skip(skipCount)
|
||||
.limit(size)
|
||||
.toList()
|
||||
.map { mapper(it) }
|
||||
|
||||
val paginate = PaginationResponse(
|
||||
page = page,
|
||||
data = query,
|
||||
size = query.size
|
||||
)
|
||||
|
||||
onQuerySuccess(paginate)
|
||||
}
|
|
@ -52,11 +52,13 @@ object Routes {
|
|||
const val ES = "/es"
|
||||
const val EN = "/en"
|
||||
const val TOP = "/top"
|
||||
const val TAGS = "/tags"
|
||||
const val NEWS = "/news"
|
||||
const val ANIME = "/anime"
|
||||
const val MANGA = "/manga"
|
||||
const val PEOPLE = "/people"
|
||||
const val SEARCH = "/search"
|
||||
const val TOP_TEN = "/topTen"
|
||||
const val GALLERY = "/gallery"
|
||||
const val SCHEDULE = "/schedule"
|
||||
const val RADIO_STATIONS = "/radio"
|
||||
|
@ -68,9 +70,13 @@ object Routes {
|
|||
const val EPISODES = "/episodes"
|
||||
const val ID = "/{id}"
|
||||
const val TYPE = "/{type}"
|
||||
const val SEASON = "/{year}/{season}"
|
||||
const val SEASON = "/season"
|
||||
const val SEASON_PARAMS = "/{year}/{season}"
|
||||
const val DAY = "/{day}"
|
||||
const val THEMES = "/themes"
|
||||
const val SUGGESTIONS = "/suggestions"
|
||||
const val YEAR_INDEX = "/yearIndex"
|
||||
const val RANDOM = "/random"
|
||||
}
|
||||
|
||||
object TimerKey {
|
||||
|
@ -86,6 +92,7 @@ object TimerKey {
|
|||
|
||||
object Collections {
|
||||
const val TIMERS = "timers"
|
||||
const val TOP_TEN = "top_ten"
|
||||
const val NEWS_ES = "news_es"
|
||||
const val NEWS_EN = "news_en"
|
||||
const val SCHEDULES = "schedule"
|
||||
|
@ -100,5 +107,6 @@ object Collections {
|
|||
const val ANIME_DIRECTORY = "anime_directory"
|
||||
const val CHARACTER_RANKING = "character_ranking"
|
||||
const val ANIME_PICTURES_QUERY = "anime_pictures_query"
|
||||
const val ANIME_RANKING_TOP_TEN = "anime_ranking_top_ten"
|
||||
const val ANIME_PICTURES_RECENT = "anime_pictures_recent"
|
||||
}
|
|
@ -1,44 +1,9 @@
|
|||
package com.jeluchu.core.utils
|
||||
|
||||
import com.jeluchu.features.rankings.models.AnimeTopEntity
|
||||
import com.jeluchu.features.schedule.models.DayEntity
|
||||
import com.jeluchu.features.schedule.models.ScheduleData
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.bson.Document
|
||||
|
||||
fun parseTopDataToDocuments(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
|
||||
}
|
||||
|
||||
fun parseTopDataToDocuments(data: List<AnimeTopEntity>?): List<Document> {
|
||||
val documents = mutableListOf<Document>()
|
||||
data?.forEach { animeData ->
|
||||
val animeJsonString = Json.encodeToString(animeData)
|
||||
val document = Document.parse(animeJsonString)
|
||||
documents.add(document)
|
||||
}
|
||||
return documents
|
||||
}
|
||||
|
||||
fun <T> parseDataToDocuments(data: List<T>?, serializer: KSerializer<T>): List<Document> {
|
||||
val documents = mutableListOf<Document>()
|
||||
data?.forEach { item ->
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package com.jeluchu.core.utils
|
||||
|
||||
import com.jeluchu.core.enums.Season
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
object SeasonCalendar {
|
||||
private val calendar: Calendar by lazy {
|
||||
Calendar.getInstance(Locale.getDefault())
|
||||
}
|
||||
|
||||
val currentYear = calendar.get(Calendar.YEAR)
|
||||
private val month = calendar.get(Calendar.MONTH)
|
||||
|
||||
val currentSeason = when (month) {
|
||||
0, 1, 11 -> Season.WINTER
|
||||
2, 3, 4 -> Season.SPRING
|
||||
5, 6, 7 -> Season.SUMMER
|
||||
8, 9, 10 -> Season.FALL
|
||||
else -> Season.SPRING
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.jeluchu.features.anime.models.seasons
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class YearSeasons(
|
||||
@SerialName("year") val year: Int,
|
||||
@SerialName("seasons") val seasons: List<String>
|
||||
)
|
|
@ -4,20 +4,34 @@ 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.jeluchu.features.anime.services.SeasonService
|
||||
import com.jeluchu.features.anime.services.TagsService
|
||||
import com.mongodb.client.MongoDatabase
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.animeEndpoints(
|
||||
mongoDatabase: MongoDatabase,
|
||||
service: AnimeService = AnimeService(mongoDatabase),
|
||||
tagsService: TagsService = TagsService(mongoDatabase),
|
||||
seasonService: SeasonService = SeasonService(mongoDatabase),
|
||||
directoryService: DirectoryService = DirectoryService(mongoDatabase),
|
||||
) = route(Routes.ANIME) {
|
||||
getToJson { service.getAnimeByType(call) }
|
||||
getToJson(Routes.ID) { service.getAnimeByMalId(call) }
|
||||
getToJson(Routes.RANDOM) { service.getRandomAnime(call) }
|
||||
getToJson(Routes.LAST_EPISODES) { service.getLastEpisodes(call) }
|
||||
|
||||
route(Routes.SUGGESTIONS) {
|
||||
getToJson { tagsService.getAnimeByAnyTag(call) }
|
||||
}
|
||||
|
||||
route(Routes.SEASON) {
|
||||
getToJson { seasonService.getAnimeBySeason(call) }
|
||||
getToJson(Routes.YEAR_INDEX) { seasonService.getYearsAndSeasons(call) }
|
||||
}
|
||||
|
||||
route(Routes.DIRECTORY) {
|
||||
getToJson { service.getDirectory(call) }
|
||||
getToJson(Routes.TYPE) { directoryService.getAnimeByType(call) }
|
||||
getToJson(Routes.SEASON) { directoryService.getAnimeBySeason(call) }
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
package com.jeluchu.features.anime.services
|
||||
|
||||
import com.jeluchu.core.connection.RestClient
|
||||
import com.jeluchu.core.enums.AnimeStatusTypes
|
||||
import com.jeluchu.core.enums.AnimeTypes
|
||||
import com.jeluchu.core.enums.TimeUnit
|
||||
import com.jeluchu.core.enums.parseAnimeStatusType
|
||||
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.models.PaginationResponse
|
||||
import com.jeluchu.core.models.documentToSimpleAnimeEntity
|
||||
import com.jeluchu.features.anime.models.lastepisodes.LastEpisodeEntity
|
||||
import com.jeluchu.features.anime.models.lastepisodes.LastEpisodeEntity.Companion.toLastEpisodeData
|
||||
import com.jeluchu.core.models.jikan.search.AnimeSearch
|
||||
|
@ -19,7 +23,9 @@ import com.jeluchu.features.anime.mappers.documentToAnimeLastEpisodeEntity
|
|||
import com.jeluchu.features.anime.mappers.documentToAnimeTypeEntity
|
||||
import com.jeluchu.features.anime.mappers.documentToMoreInfoEntity
|
||||
import com.mongodb.client.MongoDatabase
|
||||
import com.mongodb.client.model.Filters.eq
|
||||
import com.mongodb.client.model.Aggregates
|
||||
import com.mongodb.client.model.Filters
|
||||
import com.mongodb.client.model.Sorts
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
@ -63,7 +69,7 @@ class AnimeService(
|
|||
call.respond(HttpStatusCode.OK, Json.encodeToString(response))
|
||||
} else {
|
||||
val animes = directoryCollection
|
||||
.find(eq("type", type.uppercase()))
|
||||
.find(Filters.eq("type", type.uppercase()))
|
||||
.skip(skipCount)
|
||||
.limit(size)
|
||||
.toList()
|
||||
|
@ -84,7 +90,7 @@ class AnimeService(
|
|||
|
||||
suspend fun getAnimeByMalId(call: RoutingCall) = try {
|
||||
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException(ErrorMessages.InvalidMalId.message)
|
||||
directoryCollection.find(eq("malId", id)).firstOrNull()?.let { anime ->
|
||||
directoryCollection.find(Filters.eq("malId", id)).firstOrNull()?.let { anime ->
|
||||
val info = documentToMoreInfoEntity(anime)
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(info))
|
||||
} ?: call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.AnimeNotFound.message))
|
||||
|
@ -92,6 +98,42 @@ class AnimeService(
|
|||
call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.InvalidInput.message))
|
||||
}
|
||||
|
||||
suspend fun getRandomAnime(call: RoutingCall) = try {
|
||||
val nsfw = call.request.queryParameters["nsfw"]?.toBoolean() ?: false
|
||||
|
||||
val filters = mutableListOf<org.bson.conversions.Bson>().apply {
|
||||
add(Filters.`in`("type", listOf(
|
||||
AnimeTypes.TV,
|
||||
AnimeTypes.MOVIE,
|
||||
AnimeTypes.OVA,
|
||||
AnimeTypes.SPECIAL,
|
||||
AnimeTypes.ONA,
|
||||
AnimeTypes.TV_SPECIAL
|
||||
)))
|
||||
|
||||
add(Filters.nin("status", listOf(
|
||||
AnimeStatusTypes.UPCOMING
|
||||
)))
|
||||
|
||||
if (!nsfw) add(Filters.eq("nsfw", false))
|
||||
}
|
||||
|
||||
val aggregates = listOf(
|
||||
Aggregates.match(Filters.and(filters)),
|
||||
Aggregates.sample(1)
|
||||
)
|
||||
|
||||
directoryCollection.aggregate(aggregates).firstOrNull()?.let { anime ->
|
||||
val info = documentToMoreInfoEntity(anime)
|
||||
|
||||
call.response.headers.append("Cache-Control", "no-store")
|
||||
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(info))
|
||||
} ?: call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.AnimeNotFound.message))
|
||||
} catch (ex: Exception) {
|
||||
call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.InvalidInput.message))
|
||||
}
|
||||
|
||||
suspend fun getLastEpisodes(call: RoutingCall) = try {
|
||||
val dayOfWeek = LocalDate.now()
|
||||
.dayOfWeek
|
||||
|
@ -110,38 +152,42 @@ class AnimeService(
|
|||
if (needsUpdate) {
|
||||
collection.deleteMany(Document())
|
||||
|
||||
val response = RestClient.request(
|
||||
BaseUrls.JIKAN + "anime?status=airing&type=tv",
|
||||
AnimeSearch.serializer()
|
||||
)
|
||||
|
||||
val animes = mutableListOf<LastEpisodeEntity>()
|
||||
val totalPage = response.pagination?.lastPage ?: 0
|
||||
response.data?.map { it.toLastEpisodeData() }.orEmpty().let { animes.addAll(it) }
|
||||
|
||||
for (page in 2..totalPage) {
|
||||
val responsePage = RestClient.request(
|
||||
BaseUrls.JIKAN + "anime?status=airing&type=tv&page=$page",
|
||||
RestClient.request(
|
||||
BaseUrls.JIKAN + "anime?status=airing&type=tv&page=1",
|
||||
AnimeSearch.serializer()
|
||||
).data?.map { it.toLastEpisodeData() }.orEmpty()
|
||||
).let { firstPage ->
|
||||
val totalPage = firstPage.pagination?.lastPage ?: 2
|
||||
|
||||
animes.addAll(responsePage)
|
||||
delay(1000)
|
||||
firstPage.data?.let { firstAnimes ->
|
||||
firstAnimes.map { it.toLastEpisodeData() }.let { animes.addAll(it) }
|
||||
}
|
||||
|
||||
val documentsToInsert = parseDataToDocuments(animes, LastEpisodeEntity.serializer())
|
||||
for (page in 2..totalPage) {
|
||||
RestClient.request(
|
||||
BaseUrls.JIKAN + "anime?status=airing&type=tv&page=$page",
|
||||
AnimeSearch.serializer()
|
||||
).data?.let { pagesAnimes ->
|
||||
animes.addAll(pagesAnimes.map { it.toLastEpisodeData() })
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val documentsToInsert = parseDataToDocuments(animes.distinctBy { it.malId }, LastEpisodeEntity.serializer())
|
||||
if (documentsToInsert.isNotEmpty()) collection.insertMany(documentsToInsert)
|
||||
timers.update(timerKey)
|
||||
|
||||
val queryDb = collection
|
||||
.find(eq("day", dayOfWeek))
|
||||
.find(Filters.eq("day", dayOfWeek))
|
||||
.toList()
|
||||
|
||||
val elements = queryDb.map { documentToAnimeLastEpisodeEntity(it) }
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
|
||||
} else {
|
||||
val elements = collection
|
||||
.find(eq("day", dayOfWeek))
|
||||
.find(Filters.eq("day", dayOfWeek))
|
||||
.toList()
|
||||
.map { documentToAnimeLastEpisodeEntity(it) }
|
||||
|
||||
|
@ -150,4 +196,25 @@ class AnimeService(
|
|||
} catch (ex: Exception) {
|
||||
call.respond(HttpStatusCode.Unauthorized, ErrorResponse(ErrorMessages.UnauthorizedMongo.message))
|
||||
}
|
||||
|
||||
suspend fun getAnimeByType(call: RoutingCall) = try {
|
||||
val type = call.request.queryParameters["type"] ?: throw IllegalArgumentException(ErrorMessages.InvalidTopAnimeType.message)
|
||||
val status = call.request.queryParameters["status"] ?: throw IllegalArgumentException(ErrorMessages.InvalidAnimeStatusType.message)
|
||||
val nsfw = call.request.queryParameters["nsfw"].toBoolean()
|
||||
|
||||
val animes = directoryCollection.find(
|
||||
Filters.and(
|
||||
Filters.eq("type", parseAnimeType(type)),
|
||||
Filters.eq("status", parseAnimeStatusType(status)),
|
||||
Filters.eq("nsfw", nsfw),
|
||||
)
|
||||
)
|
||||
.sort(Sorts.descending("aired.from"))
|
||||
.toList()
|
||||
|
||||
val elements = animes.map { documentToSimpleAnimeEntity(it) }
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
|
||||
} catch (ex: Exception) {
|
||||
call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.InvalidInput.message))
|
||||
}
|
||||
}
|
|
@ -4,9 +4,10 @@ import com.jeluchu.core.enums.TimeUnit
|
|||
import com.jeluchu.core.enums.parseAnimeType
|
||||
import com.jeluchu.core.extensions.*
|
||||
import com.jeluchu.core.messages.ErrorMessages
|
||||
import com.jeluchu.core.models.PaginationResponse
|
||||
import com.jeluchu.core.utils.Collections
|
||||
import com.jeluchu.core.utils.TimerKey
|
||||
import com.jeluchu.core.utils.getLocalData
|
||||
import com.jeluchu.core.utils.getRemoteData
|
||||
import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity
|
||||
import com.mongodb.client.MongoCollection
|
||||
import com.mongodb.client.MongoDatabase
|
||||
|
@ -17,7 +18,6 @@ import io.ktor.server.routing.*
|
|||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.bson.Document
|
||||
import org.bson.conversions.Bson
|
||||
|
||||
class DirectoryService(
|
||||
private val database: MongoDatabase,
|
||||
|
@ -58,83 +58,4 @@ class DirectoryService(
|
|||
onQuerySuccess = { data -> call.respond(HttpStatusCode.OK, Json.encodeToString(data)) }
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getAnimeBySeason(call: RoutingCall) {
|
||||
val year = call.getIntSafeParam("year")
|
||||
val season = call.getStringSafeParam("season")
|
||||
val page = call.getIntSafeQueryParam("page", 1)
|
||||
val size = call.getIntSafeQueryParam("size", 10)
|
||||
|
||||
val skipCount = (page - 1) * size
|
||||
val timerKey = "${TimerKey.ANIME_TYPE}${year}_${season.lowercase()}"
|
||||
val collection = database.getCollection(timerKey)
|
||||
if (page < 1 || size < 1) call.badRequestError(ErrorMessages.InvalidSizeAndPage.message)
|
||||
|
||||
if (timers.needsUpdate(timerKey, 30, TimeUnit.DAY)) {
|
||||
getRemoteData(
|
||||
newCollection = collection,
|
||||
remoteCollection = directory,
|
||||
mapper = { documentToAnimeDirectoryEntity(it) },
|
||||
filters = Filters.and(
|
||||
Filters.eq("year", year),
|
||||
Filters.eq("season", season.lowercase())
|
||||
),
|
||||
onQuerySuccess = { data ->
|
||||
val documents = data.map { Document.parse(Json.encodeToString(it)) }
|
||||
if (documents.isNotEmpty()) collection.insertMany(documents)
|
||||
timers.update(timerKey)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getLocalData(
|
||||
page = page,
|
||||
size = size,
|
||||
skipCount = skipCount,
|
||||
collection = collection,
|
||||
mapper = { documentToAnimeDirectoryEntity(it) },
|
||||
onQuerySuccess = { data -> call.respond(HttpStatusCode.OK, Json.encodeToString(data)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> getRemoteData(
|
||||
filters: Bson,
|
||||
mapper: (Document) -> T,
|
||||
onQuerySuccess: (List<T>) -> Unit,
|
||||
newCollection: MongoCollection<Document>,
|
||||
remoteCollection: MongoCollection<Document>,
|
||||
) {
|
||||
newCollection.deleteMany(Document())
|
||||
|
||||
val query = remoteCollection
|
||||
.find(filters)
|
||||
.toList()
|
||||
.map { mapper(it) }
|
||||
|
||||
onQuerySuccess(query)
|
||||
}
|
||||
|
||||
private suspend fun <T> getLocalData(
|
||||
page: Int,
|
||||
size: Int,
|
||||
skipCount: Int,
|
||||
mapper: (Document) -> T,
|
||||
collection: MongoCollection<Document>,
|
||||
onQuerySuccess: suspend (PaginationResponse<T>) -> Unit
|
||||
) {
|
||||
val query = collection
|
||||
.find()
|
||||
.skip(skipCount)
|
||||
.limit(size)
|
||||
.toList()
|
||||
.map { mapper(it) }
|
||||
|
||||
val paginate = PaginationResponse(
|
||||
page = page,
|
||||
data = query,
|
||||
size = query.size
|
||||
)
|
||||
|
||||
onQuerySuccess(paginate)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package com.jeluchu.features.anime.services
|
||||
|
||||
import com.jeluchu.core.enums.parseSeasons
|
||||
import com.jeluchu.core.models.documentToSimpleAnimeEntity
|
||||
import com.jeluchu.core.utils.Collections
|
||||
import com.jeluchu.core.utils.SeasonCalendar
|
||||
import com.jeluchu.features.anime.models.seasons.YearSeasons
|
||||
import com.mongodb.client.MongoCollection
|
||||
import com.mongodb.client.MongoDatabase
|
||||
import com.mongodb.client.model.Accumulators
|
||||
import com.mongodb.client.model.Aggregates
|
||||
import com.mongodb.client.model.Filters
|
||||
import com.mongodb.client.model.Sorts
|
||||
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
|
||||
import java.time.Year
|
||||
|
||||
class SeasonService(
|
||||
private val database: MongoDatabase,
|
||||
private val directory: MongoCollection<Document> = database.getCollection(Collections.ANIME_DIRECTORY)
|
||||
) {
|
||||
suspend fun getAnimeBySeason(call: RoutingCall) {
|
||||
val year = call.request.queryParameters["year"]?.toInt() ?: SeasonCalendar.currentYear
|
||||
val station = parseSeasons(call.request.queryParameters["station"] ?: SeasonCalendar.currentSeason.name)
|
||||
?: SeasonCalendar.currentSeason
|
||||
|
||||
val query = directory.find(
|
||||
Filters.and(
|
||||
Filters.eq("season.year", year),
|
||||
Filters.eq("season.station", station),
|
||||
Filters.ne("type", "MUSIC"),
|
||||
Filters.ne("type", "PV"),
|
||||
)
|
||||
)
|
||||
.toList()
|
||||
.map { documentToSimpleAnimeEntity(it) }
|
||||
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(query))
|
||||
}
|
||||
|
||||
suspend fun getYearsAndSeasons(call: RoutingCall) {
|
||||
val currentYear = Year.now().value
|
||||
val validSeasons = listOf("SUMMER", "FALL", "WINTER", "SPRING")
|
||||
|
||||
val pipeline = listOf(
|
||||
Aggregates.match(
|
||||
Document(
|
||||
"\$and", listOf(
|
||||
Document("season.year", Document("\$gt", 0)),
|
||||
Document("season.year", Document("\$lte", currentYear)),
|
||||
Document("season.station", Document("\$in", validSeasons))
|
||||
)
|
||||
)
|
||||
),
|
||||
Aggregates.group(
|
||||
"\$season.year",
|
||||
Accumulators.addToSet("seasons", "\$season.station")
|
||||
),
|
||||
Aggregates.project(
|
||||
Document().apply {
|
||||
put("year", "\$_id")
|
||||
put("seasons", 1)
|
||||
put("_id", 0)
|
||||
}
|
||||
),
|
||||
Aggregates.sort(Sorts.descending("year"))
|
||||
)
|
||||
|
||||
val results = directory.aggregate(pipeline).toList()
|
||||
val index = results.map { document ->
|
||||
YearSeasons(
|
||||
year = document.getInteger("year"),
|
||||
seasons = document.getList("seasons", String::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(index))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package com.jeluchu.features.anime.services
|
||||
|
||||
import com.jeluchu.core.enums.AnimeStatusTypes
|
||||
import com.jeluchu.core.enums.AnimeTypes
|
||||
import com.jeluchu.core.models.documentToSimpleAnimeEntity
|
||||
import com.jeluchu.core.utils.Collections
|
||||
import com.mongodb.client.MongoCollection
|
||||
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 TagsService(
|
||||
private val database: MongoDatabase,
|
||||
private val directory: MongoCollection<Document> = database.getCollection(Collections.ANIME_DIRECTORY)
|
||||
) {
|
||||
suspend fun getAnimeByAnyTag(call: RoutingCall) {
|
||||
val tags = call.request.queryParameters["tags"].orEmpty()
|
||||
val nsfw = call.request.queryParameters["nsfw"].toBoolean()
|
||||
|
||||
val tagsList = if (tags.isNotEmpty()) {
|
||||
tags.split(",").map { it.trim() }
|
||||
} else emptyList()
|
||||
|
||||
if (tagsList.isEmpty()) {
|
||||
call.respond(HttpStatusCode.BadRequest, "No tags provided")
|
||||
return
|
||||
}
|
||||
|
||||
val filters = mutableListOf<org.bson.conversions.Bson>().apply {
|
||||
add(Filters.or(
|
||||
Filters.`in`("tags.es", tagsList),
|
||||
Filters.`in`("tags.en", tagsList)
|
||||
))
|
||||
|
||||
add(Filters.`in`("status", listOf(AnimeStatusTypes.FINISHED, AnimeStatusTypes.ONGOING)))
|
||||
|
||||
add(Filters.ne("type", AnimeTypes.MUSIC))
|
||||
add(Filters.ne("type", AnimeTypes.PV))
|
||||
add(Filters.ne("type", AnimeTypes.CM))
|
||||
|
||||
if (!nsfw) add(Filters.eq("nsfw", false))
|
||||
}
|
||||
|
||||
val query = directory.find(Filters.and(filters))
|
||||
.toList()
|
||||
.map { documentToSimpleAnimeEntity(it) }
|
||||
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(query))
|
||||
}
|
||||
}
|
|
@ -12,6 +12,10 @@ fun Route.rankingsEndpoints(
|
|||
) = route(Routes.TOP) {
|
||||
route(Routes.ANIME) {
|
||||
getToJson { service.getAnimeRanking(call) }
|
||||
|
||||
route(Routes.TOP_TEN) {
|
||||
getToJson { service.getAnimeTopTenRanking(call) }
|
||||
}
|
||||
}
|
||||
route(Routes.MANGA) {
|
||||
getToJson { service.getMangaRanking(call) }
|
||||
|
|
|
@ -43,6 +43,7 @@ class RankingsService(
|
|||
private val mangaRanking = database.getCollection(Collections.MANGA_RANKING)
|
||||
private val peopleRanking = database.getCollection(Collections.PEOPLE_RANKING)
|
||||
private val characterRanking = database.getCollection(Collections.CHARACTER_RANKING)
|
||||
private val animeRankingTopTen = database.getCollection(Collections.ANIME_RANKING_TOP_TEN)
|
||||
|
||||
suspend fun getAnimeRanking(call: RoutingCall) {
|
||||
val filter = call.request.queryParameters["filter"] ?: "airing"
|
||||
|
@ -327,4 +328,65 @@ class RankingsService(
|
|||
call.respond(HttpStatusCode.OK, Json.encodeToString(response))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAnimeTopTenRanking(call: RoutingCall) {
|
||||
val filter = call.request.queryParameters["filter"] ?: "airing"
|
||||
val type = call.parameters["type"] ?: throw IllegalArgumentException(ErrorMessages.InvalidTopAnimeType.message)
|
||||
|
||||
if (parseAnimeType(type) == null) call.respond(HttpStatusCode.BadRequest, ErrorResponse(ErrorMessages.InvalidTopAnimeType.message))
|
||||
|
||||
val timerKey = "${Collections.ANIME_RANKING}_${Collections.TOP_TEN}_${type}_${filter}"
|
||||
|
||||
val needsUpdate = timers.needsUpdate(
|
||||
amount = 7,
|
||||
key = timerKey,
|
||||
unit = TimeUnit.DAY
|
||||
)
|
||||
|
||||
if (needsUpdate) {
|
||||
animeRankingTopTen.deleteMany(
|
||||
Filters.and(
|
||||
Filters.eq("type", type),
|
||||
Filters.eq("subtype", filter)
|
||||
)
|
||||
)
|
||||
|
||||
val params = mutableListOf<String>()
|
||||
params.add("type=$type")
|
||||
params.add("filter=$filter")
|
||||
|
||||
val response = RestClient.request(
|
||||
BaseUrls.JIKAN + Endpoints.TOP_ANIME + "?${params.joinToString("&")}",
|
||||
AnimeSearch.serializer()
|
||||
).data?.map { anime ->
|
||||
anime.toAnimeTopEntity(
|
||||
page = 0,
|
||||
top = "anime",
|
||||
type = type,
|
||||
subType = filter
|
||||
)
|
||||
}.orEmpty().take(11).distinctBy { it.malId }
|
||||
|
||||
val documentsToInsert = parseDataToDocuments(response, AnimeTopEntity.serializer())
|
||||
if (documentsToInsert.isNotEmpty()) animeRankingTopTen.insertMany(documentsToInsert)
|
||||
timers.update(timerKey)
|
||||
|
||||
val elements = documentsToInsert.map { documentToAnimeTopEntity(it) }
|
||||
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
|
||||
} else {
|
||||
val animes = animeRankingTopTen
|
||||
.find(
|
||||
Filters.and(
|
||||
Filters.eq("type", type),
|
||||
Filters.eq("subtype", filter)
|
||||
)
|
||||
)
|
||||
.toList()
|
||||
|
||||
val elements = animes.map { documentToAnimeTopEntity(it) }
|
||||
|
||||
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ 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.DayEntity
|
||||
import com.jeluchu.features.schedule.models.ScheduleData
|
||||
import com.jeluchu.features.schedule.models.ScheduleEntity
|
||||
import com.mongodb.client.MongoDatabase
|
||||
|
@ -37,22 +38,20 @@ class ScheduleService(
|
|||
|
||||
if (needsUpdate) {
|
||||
schedules.deleteMany(Document())
|
||||
val documents = mutableListOf<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()
|
||||
)
|
||||
Day.entries.forEach { day ->
|
||||
val animes = getSchedule(day).data?.map { it.toDayEntity(day) }.orEmpty()
|
||||
val documentsToInsert = parseDataToDocuments(animes, DayEntity.serializer())
|
||||
if (documentsToInsert.isNotEmpty()) {
|
||||
documents.addAll(documentsToInsert)
|
||||
schedules.insertMany(documentsToInsert)
|
||||
}
|
||||
}
|
||||
|
||||
val elements = parseTopDataToDocuments(response)
|
||||
if (elements.isNotEmpty()) schedules.insertMany(elements)
|
||||
timers.update(TimerKey.SCHEDULE)
|
||||
|
||||
call.respond(HttpStatusCode.OK, elements.documentWeekMapper())
|
||||
call.respond(HttpStatusCode.OK, documents.documentWeekMapper())
|
||||
} else {
|
||||
val elements = schedules.find().toList()
|
||||
call.respond(HttpStatusCode.OK, elements.documentWeekMapper())
|
||||
|
@ -78,7 +77,16 @@ class ScheduleService(
|
|||
)
|
||||
|
||||
private fun List<Document>.documentWeekMapper(): String {
|
||||
val directory = map { documentToScheduleDayEntity(it) }
|
||||
return Json.encodeToString(directory)
|
||||
val elements = map { documentToScheduleDayEntity(it) }
|
||||
|
||||
return Json.encodeToString(ScheduleData(
|
||||
monday = elements.filter { it.day == Day.MONDAY.name.lowercase() }.distinctBy { it.malId },
|
||||
tuesday = elements.filter { it.day == Day.TUESDAY.name.lowercase() }.distinctBy { it.malId },
|
||||
wednesday = elements.filter { it.day == Day.WEDNESDAY.name.lowercase() }.distinctBy { it.malId },
|
||||
thursday = elements.filter { it.day == Day.THURSDAY.name.lowercase() }.distinctBy { it.malId },
|
||||
friday = elements.filter { it.day == Day.FRIDAY.name.lowercase() }.distinctBy { it.malId },
|
||||
saturday = elements.filter { it.day == Day.SATURDAY.name.lowercase() }.distinctBy { it.malId },
|
||||
sunday = elements.filter { it.day == Day.SUNDAY.name.lowercase() }.distinctBy { it.malId }
|
||||
))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue