AniList: Implement AniListAnimeHttpSource (#114)
This commit is contained in:
parent
d0436b55ee
commit
f54a92be5c
4 changed files with 302 additions and 0 deletions
5
lib-multisrc/anilist/build.gradle.kts
Normal file
5
lib-multisrc/anilist/build.gradle.kts
Normal file
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
|
@ -0,0 +1,167 @@
|
|||
package eu.kanade.tachiyomi.multisrc.anilist
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class AniListAnimeHttpSource : AnimeHttpSource() {
|
||||
override val supportsLatest = true
|
||||
val json by injectLazy<Json>()
|
||||
|
||||
/* =============================== Mapping AniList <> Source =============================== */
|
||||
abstract fun mapAnimeDetailUrl(animeId: Int): String
|
||||
|
||||
abstract fun mapAnimeId(animeDetailUrl: String): Int
|
||||
|
||||
open fun getPreferredTitleLanguage(): TitleLanguage {
|
||||
return TitleLanguage.ROMAJI
|
||||
}
|
||||
|
||||
/* ===================================== Popular Anime ===================================== */
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
return buildAnimeListRequest(
|
||||
query = ANIME_LIST_QUERY,
|
||||
variables = AnimeListVariables(
|
||||
page = page,
|
||||
sort = AnimeListVariables.MediaSort.POPULARITY_DESC,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
return parseAnimeListResponse(response)
|
||||
}
|
||||
|
||||
/* ===================================== Latest Anime ===================================== */
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return buildAnimeListRequest(
|
||||
query = LATEST_ANIME_LIST_QUERY,
|
||||
variables = AnimeListVariables(
|
||||
page = page,
|
||||
sort = AnimeListVariables.MediaSort.START_DATE_DESC,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
return parseAnimeListResponse(response)
|
||||
}
|
||||
|
||||
/* ===================================== Search Anime ===================================== */
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return buildAnimeListRequest(
|
||||
query = ANIME_LIST_QUERY,
|
||||
variables = AnimeListVariables(
|
||||
page = page,
|
||||
sort = AnimeListVariables.MediaSort.SEARCH_MATCH,
|
||||
search = query.ifBlank { null },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
return parseAnimeListResponse(response)
|
||||
}
|
||||
|
||||
/* ===================================== Anime Details ===================================== */
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
return buildRequest(
|
||||
query = ANIME_DETAILS_QUERY,
|
||||
variables = json.encodeToString(AnimeDetailsVariables(mapAnimeId(anime.url))),
|
||||
)
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val media = response.parseAs<AniListAnimeDetailsResponse>().data.media
|
||||
|
||||
return media.toSAnime()
|
||||
}
|
||||
|
||||
override fun getAnimeUrl(anime: SAnime): String {
|
||||
return anime.url
|
||||
}
|
||||
|
||||
/* ==================================== AniList Utility ==================================== */
|
||||
private fun buildAnimeListRequest(
|
||||
query: String,
|
||||
variables: AnimeListVariables,
|
||||
): Request {
|
||||
return buildRequest(query, json.encodeToString(variables))
|
||||
}
|
||||
|
||||
private fun buildRequest(query: String, variables: String): Request {
|
||||
val requestBody = FormBody.Builder()
|
||||
.add("query", query)
|
||||
.add("variables", variables)
|
||||
.build()
|
||||
|
||||
return POST(url = "https://graphql.anilist.co", body = requestBody)
|
||||
}
|
||||
|
||||
private fun parseAnimeListResponse(response: Response): AnimesPage {
|
||||
val page = response.parseAs<AniListAnimeListResponse>().data.page
|
||||
|
||||
return AnimesPage(
|
||||
animes = page.media.map { it.toSAnime() },
|
||||
hasNextPage = page.pageInfo.hasNextPage,
|
||||
)
|
||||
}
|
||||
|
||||
private fun AniListMedia.toSAnime(): SAnime {
|
||||
val otherNames = when (getPreferredTitleLanguage()) {
|
||||
TitleLanguage.ROMAJI -> listOfNotNull(title.english, title.native)
|
||||
TitleLanguage.ENGLISH -> listOfNotNull(title.romaji, title.native)
|
||||
TitleLanguage.NATIVE -> listOfNotNull(title.romaji, title.english)
|
||||
}
|
||||
val newDescription = buildString {
|
||||
append(
|
||||
description
|
||||
?.replace("<br>\n<br>", "\n")
|
||||
?.replace("<.*?>".toRegex(), ""),
|
||||
)
|
||||
if (otherNames.isNotEmpty()) {
|
||||
appendLine()
|
||||
appendLine()
|
||||
append("Other name(s): ${otherNames.joinToString(", ")}")
|
||||
}
|
||||
}
|
||||
val media = this
|
||||
|
||||
return SAnime.create().apply {
|
||||
url = mapAnimeDetailUrl(media.id)
|
||||
title = parseTitle(media.title)
|
||||
author = media.studios.nodes.joinToString(", ") { it.name }
|
||||
description = newDescription
|
||||
genre = media.genres.joinToString(", ")
|
||||
status = when (media.status) {
|
||||
AniListMedia.Status.RELEASING -> SAnime.ONGOING
|
||||
AniListMedia.Status.FINISHED -> SAnime.COMPLETED
|
||||
}
|
||||
thumbnail_url = media.coverImage.large
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTitle(title: AniListMedia.Title): String {
|
||||
return when (getPreferredTitleLanguage()) {
|
||||
TitleLanguage.ROMAJI -> title.romaji
|
||||
TitleLanguage.ENGLISH -> title.english ?: title.romaji
|
||||
TitleLanguage.NATIVE -> title.native ?: title.romaji
|
||||
}
|
||||
}
|
||||
|
||||
enum class TitleLanguage {
|
||||
ROMAJI,
|
||||
ENGLISH,
|
||||
NATIVE,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package eu.kanade.tachiyomi.multisrc.anilist
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal const val MEDIA_QUERY = """
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
description
|
||||
status
|
||||
genres
|
||||
studios(isMain: true) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
internal const val ANIME_LIST_QUERY = """
|
||||
query (${"$"}page: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) {
|
||||
Page(page: ${"$"}page, perPage: 30) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in: [RELEASING, FINISHED], countryOfOrigin: "JP", isAdult: false) {
|
||||
$MEDIA_QUERY
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
internal const val LATEST_ANIME_LIST_QUERY = """
|
||||
query (${"$"}page: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) {
|
||||
Page(page: ${"$"}page, perPage: 30) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in: [RELEASING, FINISHED], countryOfOrigin: "JP", isAdult: false, startDate_greater: 1, episodes_greater: 1) {
|
||||
$MEDIA_QUERY
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
internal const val ANIME_DETAILS_QUERY = """
|
||||
query (${"$"}id: Int) {
|
||||
Media(id: ${"$"}id) {
|
||||
$MEDIA_QUERY
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@Serializable
|
||||
internal data class AnimeListVariables(
|
||||
val page: Int,
|
||||
val sort: MediaSort,
|
||||
val search: String? = null,
|
||||
) {
|
||||
enum class MediaSort {
|
||||
POPULARITY_DESC,
|
||||
SEARCH_MATCH,
|
||||
START_DATE_DESC,
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class AnimeDetailsVariables(val id: Int)
|
|
@ -0,0 +1,57 @@
|
|||
package eu.kanade.tachiyomi.multisrc.anilist
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class AniListAnimeListResponse(val data: Data) {
|
||||
@Serializable
|
||||
data class Data(@SerialName("Page") val page: Page) {
|
||||
@Serializable
|
||||
data class Page(
|
||||
val pageInfo: PageInfo,
|
||||
val media: List<AniListMedia>,
|
||||
) {
|
||||
@Serializable
|
||||
data class PageInfo(val hasNextPage: Boolean)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class AniListAnimeDetailsResponse(val data: Data) {
|
||||
@Serializable
|
||||
data class Data(@SerialName("Media") val media: AniListMedia)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class AniListMedia(
|
||||
val id: Int,
|
||||
val title: Title,
|
||||
val coverImage: CoverImage,
|
||||
val description: String?,
|
||||
val status: Status,
|
||||
val genres: List<String>,
|
||||
val studios: Studios,
|
||||
) {
|
||||
@Serializable
|
||||
data class Title(
|
||||
val romaji: String,
|
||||
val english: String?,
|
||||
val native: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CoverImage(val large: String)
|
||||
|
||||
enum class Status {
|
||||
RELEASING,
|
||||
FINISHED,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Studios(val nodes: List<Node>) {
|
||||
@Serializable
|
||||
data class Node(val name: String)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue