Include new endpoints, Anitakume for Spanish Podcasts and Gallery

v5
Jéluchu 2 months ago
parent 6f56611f18
commit c54a28967b

@ -1,6 +1,8 @@
package com.jeluchu.core.configuration package com.jeluchu.core.configuration
import com.jeluchu.features.anime.routes.animeEndpoints import com.jeluchu.features.anime.routes.animeEndpoints
import com.jeluchu.features.anitakume.routes.anitakumeEndpoints
import com.jeluchu.features.gallery.routes.galleryEndpoints
import com.jeluchu.features.news.routes.newsEndpoints import com.jeluchu.features.news.routes.newsEndpoints
import com.jeluchu.features.rankings.routes.rankingsEndpoints import com.jeluchu.features.rankings.routes.rankingsEndpoints
import com.jeluchu.features.schedule.routes.scheduleEndpoints import com.jeluchu.features.schedule.routes.scheduleEndpoints
@ -17,7 +19,9 @@ fun Application.initRoutes(
newsEndpoints(mongoDatabase) newsEndpoints(mongoDatabase)
animeEndpoints(mongoDatabase) animeEndpoints(mongoDatabase)
themesEndpoints(mongoDatabase) themesEndpoints(mongoDatabase)
galleryEndpoints(mongoDatabase)
rankingsEndpoints(mongoDatabase) rankingsEndpoints(mongoDatabase)
scheduleEndpoints(mongoDatabase) scheduleEndpoints(mongoDatabase)
anitakumeEndpoints(mongoDatabase)
} }
} }

@ -4,6 +4,7 @@ import com.jeluchu.core.enums.*
sealed class ErrorMessages(val message: String) { sealed class ErrorMessages(val message: String) {
data object NotFound : ErrorMessages("Nyaaaaaaaan! This request has not been found by our alpaca-neko") data object NotFound : ErrorMessages("Nyaaaaaaaan! This request has not been found by our alpaca-neko")
data object NotFoundContent : ErrorMessages("No related content found")
data object AnimeNotFound : ErrorMessages("This malId is not in our database") data object AnimeNotFound : ErrorMessages("This malId is not in our database")
data object InvalidMalId : ErrorMessages("The provided id of malId is invalid") 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 InvalidDay : ErrorMessages("Invalid 'day' parameter. Valid values are: ${Day.entries.joinToString(", ") { it.name.lowercase() }}")

@ -3,9 +3,13 @@ package com.jeluchu.core.utils
object BaseUrls { object BaseUrls {
const val JIKAN = "https://api.jikan.moe/v4/" const val JIKAN = "https://api.jikan.moe/v4/"
const val ANIME_THEMES = "https://api.animethemes.moe/" const val ANIME_THEMES = "https://api.animethemes.moe/"
const val ANIME_PICTURES = "https://api.anime-pictures.net/api/v3/"
} }
object RssUrls { object RssUrls {
// Anitakume Podcast
const val ANITAKUME = "https://www.ivoox.com/feed_fg_f1660716_filtro_1.xml"
// Spanish news // Spanish news
const val SOMOSKUDASAI = "https://somoskudasai.com/feed/" const val SOMOSKUDASAI = "https://somoskudasai.com/feed/"
const val MANGALATAM = "https://www.mangalatam.com/feeds/posts/default?alt=rss" const val MANGALATAM = "https://www.mangalatam.com/feeds/posts/default?alt=rss"
@ -33,6 +37,7 @@ object RssSources {
} }
object Endpoints { object Endpoints {
const val POSTS = "posts"
const val ANIME = "anime" const val ANIME = "anime"
const val SCHEDULES = "schedules" const val SCHEDULES = "schedules"
const val TOP_ANIME = "top/anime" const val TOP_ANIME = "top/anime"
@ -50,8 +55,12 @@ object Routes {
const val ANIME = "/anime" const val ANIME = "/anime"
const val MANGA = "/manga" const val MANGA = "/manga"
const val PEOPLE = "/people" const val PEOPLE = "/people"
const val SEARCH = "/search"
const val GALLERY = "/gallery"
const val SCHEDULE = "/schedule" const val SCHEDULE = "/schedule"
const val LAST_POST = "/lastPosts"
const val DIRECTORY = "/directory" const val DIRECTORY = "/directory"
const val ANITAKUME = "/anitakume"
const val CHARACTER = "/characters" const val CHARACTER = "/characters"
const val LAST_EPISODES = "/lastEpisodes" const val LAST_EPISODES = "/lastEpisodes"
const val ID = "/{id}" const val ID = "/{id}"
@ -75,6 +84,7 @@ object Collections {
const val NEWS_ES = "news_es" const val NEWS_ES = "news_es"
const val NEWS_EN = "news_en" const val NEWS_EN = "news_en"
const val SCHEDULES = "schedule" const val SCHEDULES = "schedule"
const val ANITAKUME = "anitakume"
const val ANIME_THEMES = "anime_themes" const val ANIME_THEMES = "anime_themes"
const val ARTISTS_INDEX = "artists_index" const val ARTISTS_INDEX = "artists_index"
const val ANIME_DETAILS = "anime_details" const val ANIME_DETAILS = "anime_details"
@ -83,4 +93,6 @@ object Collections {
const val MANGA_RANKING = "manga_ranking" const val MANGA_RANKING = "manga_ranking"
const val PEOPLE_RANKING = "people_ranking" const val PEOPLE_RANKING = "people_ranking"
const val CHARACTER_RANKING = "character_ranking" const val CHARACTER_RANKING = "character_ranking"
const val ANIME_PICTURES_QUERY = "anime_pictures_query"
const val ANIME_PICTURES_RECENT = "anime_pictures_recent"
} }

@ -35,7 +35,7 @@ fun documentToMoreInfoEntity(doc: Document): MoreInfoEntity {
status = doc.getStringSafe("status"), status = doc.getStringSafe("status"),
type = doc.getStringSafe("type"), type = doc.getStringSafe("type"),
url = doc.getStringSafe("url"), url = doc.getStringSafe("url"),
promo = doc.getDocumentSafe("promo")?.let { documentToVideoPromo(it) } ?: VideoPromo(), promo = doc.getDocumentSafe("promo")?.let { documentToVideoPromo(it) },
duration = doc.getStringSafe("duration"), duration = doc.getStringSafe("duration"),
rank = doc.getIntSafe("rank", 0), rank = doc.getIntSafe("rank", 0),
titles = doc.getListSafe<Document>("titles").map { documentToAlternativeTitles(it) }, titles = doc.getListSafe<Document>("titles").map { documentToAlternativeTitles(it) },

@ -22,7 +22,7 @@ data class MoreInfoEntity(
var status: String = "", var status: String = "",
var type: String = "", var type: String = "",
val url: String = "", val url: String = "",
val promo: VideoPromo = VideoPromo(), val promo: VideoPromo? = VideoPromo(),
val duration: String = "", val duration: String = "",
val rank: Int = 0, val rank: Int = 0,
val titles: List<AlternativeTitles> = emptyList(), val titles: List<AlternativeTitles> = emptyList(),

@ -0,0 +1,35 @@
package com.jeluchu.features.anitakume.mappers
import com.jeluchu.core.extensions.getStringSafe
import com.jeluchu.core.extensions.parseRssDate
import com.jeluchu.features.anitakume.models.AnitakumeEntity
import com.prof18.rssparser.model.RssChannel
import org.bson.Document
fun RssChannel.toPodcast(
list: MutableList<AnitakumeEntity>
) = list.apply{
items.forEach { item ->
add(
AnitakumeEntity(
image = item.itunesItemData?.image.orEmpty(),
title = item.title?.substringAfter(" · ").orEmpty(),
description = item.description.orEmpty(),
date = item.pubDate?.parseRssDate().orEmpty(),
link = item.link.orEmpty(),
audio = item.audio.orEmpty(),
duration = item.itunesItemData?.duration.orEmpty()
)
)
}
}
fun documentToAnitakumeEntity(doc: Document) = AnitakumeEntity(
image = doc.getStringSafe("image"),
title = doc.getStringSafe("title"),
description = doc.getStringSafe("description"),
date = doc.getStringSafe("date"),
link = doc.getStringSafe("link"),
audio = doc.getStringSafe("audio"),
duration = doc.getStringSafe("duration")
)

@ -0,0 +1,14 @@
package com.jeluchu.features.anitakume.models
import kotlinx.serialization.Serializable
@Serializable
data class AnitakumeEntity(
val image: String,
val title: String,
val description: String,
val date: String,
val link: String,
val audio: String,
val duration: String
)

@ -0,0 +1,15 @@
package com.jeluchu.features.anitakume.routes
import com.jeluchu.core.extensions.getToJson
import com.jeluchu.core.utils.Routes
import com.jeluchu.features.anitakume.services.AnitakumeService
import com.jeluchu.features.news.services.NewsService
import com.mongodb.client.MongoDatabase
import io.ktor.server.routing.*
fun Route.anitakumeEndpoints(
mongoDatabase: MongoDatabase,
service: AnitakumeService = AnitakumeService(mongoDatabase)
) = route(Routes.ANITAKUME) {
getToJson { service.getEpisodes(call) }
}

@ -0,0 +1,60 @@
package com.jeluchu.features.anitakume.services
import com.jeluchu.core.enums.TimeUnit
import com.jeluchu.core.extensions.needsUpdate
import com.jeluchu.core.extensions.update
import com.jeluchu.core.utils.Collections
import com.jeluchu.core.utils.RssSources
import com.jeluchu.core.utils.RssUrls
import com.jeluchu.core.utils.parseDataToDocuments
import com.jeluchu.features.anitakume.mappers.documentToAnitakumeEntity
import com.jeluchu.features.anitakume.mappers.toPodcast
import com.jeluchu.features.anitakume.models.AnitakumeEntity
import com.jeluchu.features.news.mappers.documentToNewsEntity
import com.jeluchu.features.news.mappers.toNews
import com.jeluchu.features.news.models.NewEntity
import com.mongodb.client.MongoDatabase
import com.prof18.rssparser.RssParser
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 AnitakumeService(
database: MongoDatabase
) {
private val timers = database.getCollection(Collections.TIMERS)
private val anitakume = database.getCollection(Collections.ANITAKUME)
suspend fun getEpisodes(call: RoutingCall) {
val needsUpdate = timers.needsUpdate(
amount = 1,
key = Collections.ANITAKUME,
unit = TimeUnit.DAY
)
if (needsUpdate) {
anitakume.deleteMany(Document())
val response = mutableListOf<AnitakumeEntity>().apply {
RssParser().getRssChannel(RssUrls.ANITAKUME).toPodcast(this@apply)
}
val documentsToInsert = parseDataToDocuments(response, AnitakumeEntity.serializer())
if (documentsToInsert.isNotEmpty()) anitakume.insertMany(documentsToInsert)
timers.update(Collections.ANITAKUME)
val elements = documentsToInsert.map { documentToAnitakumeEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
} else {
val animes = anitakume
.find()
.toList()
val elements = animes.map { documentToNewsEntity(it) }
call.respond(HttpStatusCode.OK, Json.encodeToString(elements))
}
}
}

@ -0,0 +1,63 @@
package com.jeluchu.features.gallery.mappers
import com.jeluchu.core.extensions.getBooleanSafe
import com.jeluchu.core.extensions.getDocumentSafe
import com.jeluchu.core.extensions.getFloatSafe
import com.jeluchu.core.extensions.getIntSafe
import com.jeluchu.core.extensions.getListSafe
import com.jeluchu.core.extensions.getStringSafe
import com.jeluchu.core.extensions.parseRssDate
import com.jeluchu.features.anime.mappers.documentToVideoPromo
import com.jeluchu.features.anime.models.anime.VideoPromo
import com.jeluchu.features.gallery.models.PostsResponse
import com.jeluchu.features.gallery.models.ProcessedPost
import com.jeluchu.features.news.models.NewEntity
import com.prof18.rssparser.model.RssChannel
import org.bson.Document
fun PostsResponse.Post.toProcessedPost(page: Int): ProcessedPost {
val imageUrl = "https://oimages.anime-pictures.net/${md5.take(3)}/$md5$ext"
return ProcessedPost(
id = id,
image = imageUrl,
width = width,
height = height,
pubtime = pubtime,
size = size,
erotics = erotics,
spoiler = spoiler,
haveAlpha = haveAlpha,
page = page
)
}
fun PostsResponse.Post.toProcessedPostQuery(page: Int, query: String): ProcessedPost {
val imageUrl = "https://oimages.anime-pictures.net/${md5.take(3)}/$md5$ext"
return ProcessedPost(
id = id,
image = imageUrl,
width = width,
height = height,
pubtime = pubtime,
size = size,
erotics = erotics,
spoiler = spoiler,
haveAlpha = haveAlpha,
page = page,
query = query
)
}
fun documentToProcessedPost(doc: Document) = ProcessedPost(
id = doc.getIntSafe("id"),
image = doc.getStringSafe("image"),
width = doc.getIntSafe("width"),
height = doc.getIntSafe("height"),
pubtime = doc.getStringSafe("pubtime"),
size = doc.getIntSafe("size"),
erotics = doc.getIntSafe("erotics"),
spoiler = doc.getBooleanSafe("spoiler"),
haveAlpha = doc.getBooleanSafe("haveAlpha")
)

@ -0,0 +1,97 @@
package com.jeluchu.features.gallery.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PostsResponse(
@SerialName("exclusive_tag")
val exclusiveTag: String? = null,
@SerialName("posts_per_page")
val postsPerPage: Int = 0,
@SerialName("response_posts_count")
val responsePostsCount: Int = 0,
@SerialName("page_number")
val pageNumber: Int = 0,
@SerialName("posts")
val posts: List<Post> = emptyList(),
@SerialName("posts_count")
val postsCount: Int = 0,
@SerialName("max_pages")
val maxPages: Int = 0
) {
@Serializable
data class Post(
@SerialName("id")
val id: Int = 0,
@SerialName("md5")
val md5: String = "",
@SerialName("md5_pixels")
val md5Pixels: String = "",
@SerialName("width")
val width: Int = 0,
@SerialName("height")
val height: Int = 0,
@SerialName("pubtime")
val pubtime: String = "",
@SerialName("datetime")
val datetime: String = "",
@SerialName("score")
val score: Int = 0,
@SerialName("score_number")
val scoreNumber: Int = 0,
@SerialName("size")
val size: Int = 0,
@SerialName("download_count")
val downloadCount: Int = 0,
@SerialName("erotics")
val erotics: Int = 0,
@SerialName("color")
val color: List<Int> = emptyList(),
@SerialName("ext")
val ext: String = "",
@SerialName("status")
val status: Int = 0,
@SerialName("status_type")
val statusType: Int = 0,
@SerialName("redirect_id")
val redirectId: String? = null,
@SerialName("spoiler")
val spoiler: Boolean = false,
@SerialName("have_alpha")
val haveAlpha: Boolean = false,
@SerialName("tags_count")
val tagsCount: Int = 0,
@SerialName("artefacts_degree")
val artefactsDegree: Double = 0.0,
@SerialName("smooth_degree")
val smoothDegree: Double = 0.0
)
}

@ -0,0 +1,40 @@
package com.jeluchu.features.gallery.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ProcessedPost(
@SerialName("id")
val id: Int,
@SerialName("image")
val image: String,
@SerialName("width")
val width: Int,
@SerialName("height")
val height: Int,
@SerialName("pubtime")
val pubtime: String,
@SerialName("size")
val size: Int,
@SerialName("erotics")
val erotics: Int,
@SerialName("spoiler")
val spoiler: Boolean,
@SerialName("have_alpha")
val haveAlpha: Boolean,
@SerialName("page")
val page: Int = 0,
@SerialName("query")
val query: String = "0"
)

@ -0,0 +1,19 @@
package com.jeluchu.features.gallery.routes
import com.jeluchu.core.extensions.getToJson
import com.jeluchu.core.utils.Routes
import com.jeluchu.features.gallery.services.GalleryService
import com.jeluchu.features.news.services.NewsService
import com.mongodb.client.MongoDatabase
import io.ktor.server.routing.*
fun Route.galleryEndpoints(
mongoDatabase: MongoDatabase,
service: GalleryService = GalleryService(mongoDatabase)
) = route(Routes.GALLERY) {
route(Routes.LAST_POST) {
getToJson { service.getLastPosts(call) }
}
getToJson { service.getQueryImages(call) }
}

@ -0,0 +1,149 @@
package com.jeluchu.features.gallery.services
import com.jeluchu.core.connection.RestClient
import com.jeluchu.core.enums.TimeUnit
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.utils.BaseUrls
import com.jeluchu.core.utils.Collections
import com.jeluchu.core.utils.Endpoints
import com.jeluchu.core.utils.parseDataToDocuments
import com.jeluchu.features.gallery.mappers.documentToProcessedPost
import com.jeluchu.features.gallery.mappers.toProcessedPost
import com.jeluchu.features.gallery.mappers.toProcessedPostQuery
import com.jeluchu.features.gallery.models.PostsResponse
import com.jeluchu.features.gallery.models.ProcessedPost
import com.mongodb.client.MongoDatabase
import com.mongodb.client.model.Filters
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.response.respond
import io.ktor.server.routing.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okio.IOException
import java.net.URLEncoder
class GalleryService(
database: MongoDatabase
) {
private val timers = database.getCollection(Collections.TIMERS)
private val queryPosts = database.getCollection(Collections.ANIME_PICTURES_QUERY)
private val recentPosts = database.getCollection(Collections.ANIME_PICTURES_RECENT)
suspend fun getLastPosts(call: RoutingCall) {
val size = 80
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val timerKey = "${Collections.ANIME_PICTURES_RECENT}_${page}"
val skipCount = (page - 1) * size
val needsUpdate = timers.needsUpdate(
amount = 30,
key = timerKey,
unit = TimeUnit.DAY
)
if (needsUpdate) {
recentPosts.deleteMany(Filters.and(Filters.eq("page", page)))
val response = RestClient.request(
BaseUrls.ANIME_PICTURES + Endpoints.POSTS + "?page=$page",
PostsResponse.serializer()
).posts.map { it.toProcessedPost(page) }
val documentsToInsert = parseDataToDocuments(response, ProcessedPost.serializer())
if (documentsToInsert.isNotEmpty()) recentPosts.insertMany(documentsToInsert)
timers.update(timerKey)
val elements = documentsToInsert.map { documentToProcessedPost(it) }
val paginationResponse = PaginationResponse(
page = page,
size = size,
data = elements
)
call.respond(HttpStatusCode.OK, Json.encodeToString(paginationResponse))
} else {
val posts = recentPosts
.find(Filters.and(Filters.eq("page", page)))
.skip(skipCount)
.limit(size)
.toList()
val elements = posts.map { documentToProcessedPost(it) }
val response = PaginationResponse(
page = page,
size = size,
data = elements
)
call.respond(HttpStatusCode.OK, Json.encodeToString(response))
}
}
suspend fun getQueryImages(call: RoutingCall) {
val size = 80
val query = call.request.queryParameters["query"].orEmpty()
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val timerKey = "${Collections.ANIME_PICTURES_QUERY}_${query}_${page}"
val skipCount = (page - 1) * size
val fixedPage = page + 1
val needsUpdate = timers.needsUpdate(
amount = 30,
key = timerKey,
unit = TimeUnit.DAY
)
if (needsUpdate) {
queryPosts.deleteMany(Filters.and(Filters.eq("page", fixedPage)))
try {
val response = RestClient.request(
BaseUrls.ANIME_PICTURES + Endpoints.POSTS + "?search_tag=${URLEncoder.encode(query, "UTF-8")}" + "&lang=en&type=json_v3" + "&page=$page",
PostsResponse.serializer()
).posts.map { it.toProcessedPostQuery(fixedPage, query) }
val documentsToInsert = parseDataToDocuments(response, ProcessedPost.serializer())
if (documentsToInsert.isNotEmpty()) queryPosts.insertMany(documentsToInsert)
timers.update(timerKey)
val elements = documentsToInsert.map { documentToProcessedPost(it) }
val paginationResponse = PaginationResponse(
page = fixedPage,
size = size,
data = elements
)
call.respond(HttpStatusCode.OK, Json.encodeToString(paginationResponse))
} catch (e: IOException) {
val paginationResponse = PaginationResponse(
page = fixedPage,
size = size,
data = emptyList<ProcessedPost>()
)
call.respond(HttpStatusCode.OK, Json.encodeToString(paginationResponse))
}
} else {
val posts = queryPosts
.find(Filters.and(Filters.eq("page", fixedPage)))
.skip(skipCount)
.limit(size)
.toList()
val elements = posts.map { documentToProcessedPost(it) }
val response = PaginationResponse(
page = fixedPage,
size = size,
data = elements
)
call.respond(HttpStatusCode.OK, Json.encodeToString(response))
}
}
}
Loading…
Cancel
Save