mirror of https://github.com/aruppi/aruppi-api
Improves and include new endpoints
New enpoints for seasons and for types
This commit is contained in:
parent
e40894d0e0
commit
03842ab1f9
|
@ -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,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)
|
||||
}
|
|
@ -69,9 +69,11 @@ 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 YEAR_INDEX = "/yearIndex"
|
||||
}
|
||||
|
||||
object TimerKey {
|
||||
|
|
|
@ -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,13 @@
|
|||
package com.jeluchu.features.anime.mappers
|
||||
|
||||
import com.jeluchu.core.extensions.getIntSafe
|
||||
import com.jeluchu.core.extensions.getStringSafe
|
||||
import com.jeluchu.features.anime.models.seasons.SeasonAnimeEntity
|
||||
import org.bson.Document
|
||||
|
||||
fun documentToSeasonEntity(doc: Document) = SeasonAnimeEntity(
|
||||
score = doc.getStringSafe("score"),
|
||||
malId = doc.getIntSafe("malId"),
|
||||
image = doc.getStringSafe("poster"),
|
||||
title = doc.getStringSafe("title")
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
package com.jeluchu.features.anime.models.seasons
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SeasonAnimeEntity(
|
||||
val malId: Int,
|
||||
val title: String,
|
||||
val image: String,
|
||||
val score: String,
|
||||
)
|
|
@ -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,27 @@ 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.mongodb.client.MongoDatabase
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.animeEndpoints(
|
||||
mongoDatabase: MongoDatabase,
|
||||
service: AnimeService = AnimeService(mongoDatabase),
|
||||
seasonService: SeasonService = SeasonService(mongoDatabase),
|
||||
directoryService: DirectoryService = DirectoryService(mongoDatabase),
|
||||
) = route(Routes.ANIME) {
|
||||
getToJson { service.getAnimeByType(call) }
|
||||
getToJson(Routes.ID) { service.getAnimeByMalId(call) }
|
||||
getToJson(Routes.LAST_EPISODES) { service.getLastEpisodes(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) }
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.jeluchu.features.anime.services
|
|||
|
||||
import com.jeluchu.core.connection.RestClient
|
||||
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
|
||||
|
@ -19,7 +20,8 @@ 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.Filters
|
||||
import com.mongodb.client.model.Sorts
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
@ -63,7 +65,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 +86,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))
|
||||
|
@ -110,38 +112,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 +156,23 @@ 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 animes = directoryCollection.find(
|
||||
Filters.and(
|
||||
Filters.eq("type", parseAnimeType(type)),
|
||||
Filters.eq("status", parseAnimeStatusType(status)),
|
||||
)
|
||||
)
|
||||
.sort(Sorts.descending("aired.from"))
|
||||
.toList()
|
||||
|
||||
val elements = animes.map { documentToAnimeTypeEntity(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,92 @@
|
|||
package com.jeluchu.features.anime.services
|
||||
|
||||
import com.jeluchu.core.enums.TimeUnit
|
||||
import com.jeluchu.core.enums.parseSeasons
|
||||
import com.jeluchu.core.extensions.badRequestError
|
||||
import com.jeluchu.core.extensions.getIntSafeQueryParam
|
||||
import com.jeluchu.core.extensions.needsUpdate
|
||||
import com.jeluchu.core.extensions.update
|
||||
import com.jeluchu.core.messages.ErrorMessages
|
||||
import com.jeluchu.core.utils.*
|
||||
import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity
|
||||
import com.jeluchu.features.anime.mappers.documentToSeasonEntity
|
||||
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["season"] ?: SeasonCalendar.currentSeason.name) ?: SeasonCalendar.currentSeason
|
||||
val page = call.getIntSafeQueryParam("page", 1)
|
||||
val size = call.getIntSafeQueryParam("size", 10)
|
||||
|
||||
val skipCount = (page - 1) * size
|
||||
if (page < 1 || size < 1) call.badRequestError(ErrorMessages.InvalidSizeAndPage.message)
|
||||
|
||||
val query = directory.find(
|
||||
Filters.and(
|
||||
Filters.eq("season.year", year),
|
||||
Filters.eq("season.station", station)
|
||||
)
|
||||
)
|
||||
.toList()
|
||||
.map { documentToSeasonEntity(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))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue