forked from AlmightyHak/extensions-source
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