You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aruppi-api/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt

223 lines
8.8 KiB
Kotlin

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
import com.jeluchu.core.utils.BaseUrls
import com.jeluchu.core.utils.Collections
import com.jeluchu.core.utils.TimerKey
import com.jeluchu.core.utils.parseDataToDocuments
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.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.coroutines.delay
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.bson.Document
import java.time.LocalDate
import java.time.format.TextStyle
import java.util.*
class AnimeService(
private val database: MongoDatabase
) {
private val timers = database.getCollection(Collections.TIMERS)
private val directoryCollection = database.getCollection(Collections.ANIME_DIRECTORY)
suspend fun getDirectory(call: RoutingCall) = try {
val type = call.request.queryParameters["type"].orEmpty()
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 10
if (page < 1 || size < 1) call.respond(HttpStatusCode.BadRequest, ErrorMessages.InvalidSizeAndPage.message)
val skipCount = (page - 1) * size
if (parseAnimeType(type) == null) {
val animes = directoryCollection
.find()
.skip(skipCount)
.limit(size)
.toList()
val elements = animes.map { documentToAnimeTypeEntity(it) }
val response = PaginationResponse(
page = page,
size = size,
data = elements
)
call.respond(HttpStatusCode.OK, Json.encodeToString(response))
} else {
val animes = directoryCollection
.find(Filters.eq("type", type.uppercase()))
.skip(skipCount)
.limit(size)
.toList()
val elements = animes.map { documentToAnimeTypeEntity(it) }
val response = PaginationResponse(
page = page,
size = size,
data = elements
)
call.respond(HttpStatusCode.OK, Json.encodeToString(response))
}
} catch (ex: Exception) {
call.respond(HttpStatusCode.Unauthorized, ErrorResponse(ErrorMessages.UnauthorizedMongo.message))
}
suspend fun getAnimeByMalId(call: RoutingCall) = try {
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException(ErrorMessages.InvalidMalId.message)
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))
} catch (ex: Exception) {
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-cache, no-store, must-revalidate, private")
call.response.headers.append("Pragma", "no-cache")
call.response.headers.append("Expires", "0")
call.response.headers.append("Vary", "*")
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
.getDisplayName(TextStyle.FULL, Locale.ENGLISH)
.plus("s")
val timerKey = TimerKey.LAST_EPISODES
val collection = database.getCollection(timerKey)
val needsUpdate = timers.needsUpdate(
amount = 6,
key = timerKey,
unit = TimeUnit.HOUR,
)
if (needsUpdate) {
collection.deleteMany(Document())
val animes = mutableListOf<LastEpisodeEntity>()
RestClient.request(
BaseUrls.JIKAN + "anime?status=airing&type=tv&page=1",
AnimeSearch.serializer()
).let { firstPage ->
val totalPage = firstPage.pagination?.lastPage ?: 2
firstPage.data?.let { firstAnimes ->
firstAnimes.map { it.toLastEpisodeData() }.let { animes.addAll(it) }
}
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(Filters.eq("day", dayOfWeek))
.toList()
val elements = queryDb.map { documentToAnimeLastEpisodeEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
} else {
val elements = collection
.find(Filters.eq("day", dayOfWeek))
.toList()
.map { documentToAnimeLastEpisodeEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
}
} 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))
}
}