fix(all/yomiroll): Fetch anime status from AniList (#2728)

Co-authored-by: Samfun75 <38332931+Samfun75@users.noreply.github.com>
Co-authored-by: jmir1 <jhmiramon@gmail.com>
This commit is contained in:
mklive
2024-01-13 11:36:10 +01:00
committed by GitHub
parent facbb2a526
commit 7ab9dc9a61
3 changed files with 162 additions and 83 deletions

View File

@ -8,7 +8,7 @@ ext {
extName = 'Yomiroll' extName = 'Yomiroll'
pkgNameSuffix = 'all.kamyroll' pkgNameSuffix = 'all.kamyroll'
extClass = '.Yomiroll' extClass = '.Yomiroll'
extVersionCode = 26 extVersionCode = 27
libVersion = '13' libVersion = '13'
} }

View File

@ -59,30 +59,26 @@ data class Anime(
val genres: ArrayList<String>? = null, val genres: ArrayList<String>? = null,
val series_metadata: Metadata? = null, val series_metadata: Metadata? = null,
@SerialName("movie_listing_metadata") @SerialName("movie_listing_metadata")
val movie_metadata: MovieMeta? = null, val movie_metadata: Metadata? = null,
val content_provider: String? = null, val content_provider: String? = null,
val audio_locale: String? = null,
val audio_locales: ArrayList<String>? = null,
val subtitle_locales: ArrayList<String>? = null,
val maturity_ratings: ArrayList<String>? = null,
val is_dubbed: Boolean? = null,
val is_subbed: Boolean? = null,
) { ) {
@Serializable @Serializable
data class Metadata( data class Metadata(
val maturity_ratings: ArrayList<String>, val maturity_ratings: ArrayList<String>,
val is_simulcast: Boolean, val is_simulcast: Boolean? = null,
val audio_locales: ArrayList<String>, val audio_locales: ArrayList<String>? = null,
val subtitle_locales: ArrayList<String>, val subtitle_locales: ArrayList<String>,
val is_dubbed: Boolean, val is_dubbed: Boolean,
val is_subbed: Boolean, val is_subbed: Boolean,
@SerialName("tenant_categories") @SerialName("tenant_categories")
val genres: ArrayList<String>? = null, val genres: ArrayList<String>? = null,
) )
@Serializable
data class MovieMeta(
val is_dubbed: Boolean,
val is_subbed: Boolean,
val maturity_ratings: ArrayList<String>,
val subtitle_locales: ArrayList<String>,
@SerialName("tenant_categories")
val genres: ArrayList<String>? = null,
)
} }
@Serializable @Serializable
@ -172,6 +168,22 @@ data class Subtitle(
val url: String, val url: String,
) )
@Serializable
data class AnilistResult(
val data: AniData,
) {
@Serializable
data class AniData(
@SerialName("Media")
val media: Media? = null,
)
@Serializable
data class Media(
val status: String,
)
}
fun <T> List<T>.thirdLast(): T? { fun <T> List<T>.thirdLast(): T? {
if (size < 3) return null if (size < 3) return null
return this[size - 3] return this[size - 3]

View File

@ -15,15 +15,19 @@ import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
@ -53,6 +57,8 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val mainScope by lazy { MainScope() }
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
@ -65,6 +71,8 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
super.client.newBuilder().addInterceptor(tokenInterceptor).build() super.client.newBuilder().addInterceptor(tokenInterceptor).build()
} }
private val noTokenClient = super.client
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request {
@ -73,7 +81,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
} }
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = json.decodeFromString<AnimeResult>(response.body.string()) val parsed = json.decodeFromString<AnimeResult>(response.use { it.body.string() })
val animeList = parsed.data.mapNotNull { it.toSAnimeOrNull() } val animeList = parsed.data.mapNotNull { it.toSAnimeOrNull() }
val position = response.request.url.queryParameter("start")?.toIntOrNull() ?: 0 val position = response.request.url.queryParameter("start")?.toIntOrNull() ?: 0
return AnimesPage(animeList, position + 36 < parsed.total) return AnimesPage(animeList, position + 36 < parsed.total)
@ -103,7 +111,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
val bod = response.body.string() val bod = response.use { it.body.string() }
val total: Int val total: Int
val items = val items =
if (response.request.url.encodedPath.contains("search")) { if (response.request.url.encodedPath.contains("search")) {
@ -125,6 +133,43 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
// Function to fetch anime status using AniList GraphQL API ispired by OppaiStream.kt
private fun fetchStatusByTitle(title: String): Int {
val query = """
query {
Media(search: "$title", isAdult: false, type: ANIME) {
id
idMal
title {
romaji
native
english
}
status
}
}
""".trimIndent()
val requestBody = FormBody.Builder()
.add("query", query)
.build()
val response = noTokenClient.newCall(
POST("https://graphql.anilist.co", body = requestBody),
).execute().use { it.body.string() }
val responseParsed = json.decodeFromString<AnilistResult>(response)
return when (responseParsed.data.media?.status) {
"FINISHED" -> SAnime.COMPLETED
"RELEASING" -> SAnime.ONGOING
"NOT_YET_RELEASED" -> SAnime.LICENSED
"CANCELLED" -> SAnime.CANCELLED
"HIATUS" -> SAnime.ON_HIATUS
else -> SAnime.UNKNOWN
}
}
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
val mediaId = json.decodeFromString<LinkData>(anime.url) val mediaId = json.decodeFromString<LinkData>(anime.url)
val resp = client.newCall( val resp = client.newCall(
@ -133,17 +178,10 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
} else { } else {
GET("$crApiUrl/cms/movie_listings/${mediaId.id}?locale=en-US") GET("$crApiUrl/cms/movie_listings/${mediaId.id}?locale=en-US")
}, },
).execute() ).execute().use { it.body.string() }
val info = json.decodeFromString<AnimeResult>(resp.body.string()) val info = json.decodeFromString<AnimeResult>(resp)
return Observable.just( return Observable.just(
anime.apply { info.data.first().toSAnimeOrNull(anime) ?: anime,
author = info.data.first().content_provider
status = SAnime.COMPLETED
if (genre.isNullOrBlank()) {
genre =
info.data.first().genres?.joinToString { gen -> gen.replaceFirstChar { it.uppercase() } }
}
},
) )
} }
@ -161,7 +199,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
} }
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val seasons = json.decodeFromString<SeasonResult>(response.body.string()) val seasons = json.decodeFromString<SeasonResult>(response.use { it.body.string() })
val series = response.request.url.encodedPath.contains("series/") val series = response.request.url.encodedPath.contains("series/")
val chunkSize = Runtime.getRuntime().availableProcessors() val chunkSize = Runtime.getRuntime().availableProcessors()
return if (series) { return if (series) {
@ -176,7 +214,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
seasons.data.mapIndexed { index, movie -> seasons.data.mapIndexed { index, movie ->
SEpisode.create().apply { SEpisode.create().apply {
url = EpisodeData(listOf(Pair(movie.id, ""))).toJsonString() url = EpisodeData(listOf(Pair(movie.id, ""))).toJsonString()
name = "Movie" name = "Movie ${index + 1}"
episode_number = (index + 1).toFloat() episode_number = (index + 1).toFloat()
date_upload = movie.date?.let(::parseDate) ?: 0L date_upload = movie.date?.let(::parseDate) ?: 0L
} }
@ -185,10 +223,9 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
} }
private fun getEpisodes(seasonData: SeasonResult.Season): List<SEpisode> { private fun getEpisodes(seasonData: SeasonResult.Season): List<SEpisode> {
val episodeResp = val body =
client.newCall(GET("$crApiUrl/cms/seasons/${seasonData.id}/episodes")) client.newCall(GET("$crApiUrl/cms/seasons/${seasonData.id}/episodes"))
.execute() .execute().use { it.body.string() }
val body = episodeResp.body.string()
val episodes = json.decodeFromString<EpisodeResult>(body) val episodes = json.decodeFromString<EpisodeResult>(body)
return episodes.data.sortedBy { it.episode_number }.mapNotNull EpisodeMap@{ ep -> return episodes.data.sortedBy { it.episode_number }.mapNotNull EpisodeMap@{ ep ->
@ -246,8 +283,8 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
private fun extractVideo(media: Pair<String, String>): List<Video> { private fun extractVideo(media: Pair<String, String>): List<Video> {
val (mediaId, aud) = media val (mediaId, aud) = media
val response = client.newCall(getVideoRequest(mediaId)).execute() val response = client.newCall(getVideoRequest(mediaId)).execute().use { it.body.string() }
val streams = json.decodeFromString<VideoStreams>(response.body.string()) val streams = json.decodeFromString<VideoStreams>(response)
val subLocale = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!.getLocale() val subLocale = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!.getLocale()
val subsList = runCatching { val subsList = runCatching {
@ -276,7 +313,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
runCatching { runCatching {
val playlist = client.newCall(GET(stream.url)).execute() val playlist = client.newCall(GET(stream.url)).execute()
if (playlist.code != 200) return@parallelMap null if (playlist.code != 200) return@parallelMap null
playlist.body.string().substringAfter("#EXT-X-STREAM-INF:") playlist.use { it.body.string() }.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").map { .split("#EXT-X-STREAM-INF:").map {
val hardsub = stream.hardsub_locale.let { hs -> val hardsub = stream.hardsub_locale.let { hs ->
if (hs.isNotBlank()) " - HardSub: $hs" else "" if (hs.isNotBlank()) " - HardSub: $hs" else ""
@ -340,6 +377,14 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
Pair("sv-SE", "Swedish"), Pair("sv-SE", "Swedish"),
Pair("zh-CN", "Chinese (PRC)"), Pair("zh-CN", "Chinese (PRC)"),
Pair("zh-HK", "Chinese (Hong Kong)"), Pair("zh-HK", "Chinese (Hong Kong)"),
Pair("zh-TW", "Chinese (Taiwan)"),
Pair("ca-ES", "Català"),
Pair("id-ID", "Bahasa Indonesia"),
Pair("ms-MY", "Bahasa Melayu"),
Pair("ta-IN", "Tamil"),
Pair("te-IN", "Telugu"),
Pair("th-TH", "Thai"),
Pair("vi-VN", "Vietnamese"),
) )
private fun LinkData.toJsonString(): String { private fun LinkData.toJsonString(): String {
@ -355,55 +400,69 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
.getOrNull() ?: 0L .getOrNull() ?: 0L
} }
private fun Anime.toSAnimeOrNull() = runCatching { toSAnime() }.getOrNull() private fun Anime.toSAnimeOrNull(anime: SAnime? = null) =
runCatching { toSAnime(anime) }.getOrNull()
private fun Anime.toSAnime(): SAnime = private fun Anime.toSAnime(anime: SAnime? = null): SAnime =
SAnime.create().apply { SAnime.create().apply {
title = this@toSAnime.title title = this@toSAnime.title
thumbnail_url = images.poster_tall?.getOrNull(0)?.thirdLast()?.source thumbnail_url = images.poster_tall?.getOrNull(0)?.thirdLast()?.source
?: images.poster_tall?.getOrNull(0)?.last()?.source ?: images.poster_tall?.getOrNull(0)?.last()?.source
url = LinkData(id, type!!).toJsonString() url = anime?.url ?: LinkData(id, type!!).toJsonString()
genre = series_metadata?.genres?.joinToString() genre = anime?.genre ?: (series_metadata?.genres ?: movie_metadata?.genres ?: genres)
?: movie_metadata?.genres?.joinToString() ?: "" ?.joinToString { gen -> gen.replaceFirstChar { it.uppercase() } }
status = SAnime.COMPLETED status = if (anime != null) fetchStatusByTitle(this@toSAnime.title) else SAnime.UNKNOWN
var desc = this@toSAnime.description + "\n" author = content_provider
desc += "\nLanguage:" + description = anime?.description ?: StringBuilder().apply {
( appendLine(this@toSAnime.description)
if (series_metadata?.subtitle_locales?.any() == true || appendLine()
movie_metadata?.subtitle_locales?.any() == true ||
series_metadata?.is_subbed == true append("Language:")
if ((
subtitle_locales ?: (
series_metadata
?: movie_metadata
)?.subtitle_locales
)?.any() == true ||
(series_metadata ?: movie_metadata)?.is_subbed == true ||
is_subbed == true
) { ) {
" Sub" append(" Sub")
} else {
""
} }
) + if (((series_metadata?.audio_locales ?: audio_locales)?.size ?: 0) > 1 ||
( (series_metadata ?: movie_metadata)?.is_dubbed == true ||
if (series_metadata?.audio_locales?.size ?: 0 > 1 || is_dubbed == true
movie_metadata?.is_dubbed == true
) { ) {
" Dub" append(" Dub")
} else {
""
} }
appendLine()
append("Maturity Ratings: ")
appendLine(
((series_metadata ?: movie_metadata)?.maturity_ratings ?: maturity_ratings)
?.joinToString() ?: "-",
) )
desc += "\nMaturity Ratings: " + if (series_metadata?.is_simulcast == true) appendLine("Simulcast")
appendLine()
append("Audio: ")
appendLine(
(series_metadata?.audio_locales ?: audio_locales ?: listOf(audio_locale ?: "-"))
.sortedBy { it.getLocale() }
.joinToString { it.getLocale() },
)
appendLine()
append("Subs: ")
append(
( (
series_metadata?.maturity_ratings?.joinToString() subtitle_locales ?: series_metadata?.subtitle_locales
?: movie_metadata?.maturity_ratings?.joinToString() ?: "" ?: movie_metadata?.subtitle_locales
) )
desc += if (series_metadata?.is_simulcast == true) "\nSimulcast" else "" ?.sortedBy { it.getLocale() }
desc += "\n\nAudio: " + ( ?.joinToString { it.getLocale() },
series_metadata?.audio_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() } ?: ""
) )
desc += "\n\nSubs: " + ( }.toString()
series_metadata?.subtitle_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() }
?: movie_metadata?.subtitle_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() } ?: ""
)
description = desc
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
@ -506,15 +565,23 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
this.apply { this.apply {
key = PREF_USE_LOCAL_TOKEN_KEY key = PREF_USE_LOCAL_TOKEN_KEY
title = PREF_USE_LOCAL_TOKEN_TITLE title = PREF_USE_LOCAL_TOKEN_TITLE
summary = runBlocking { mainScope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) { getTokenDetail() } getTokenDetail().let {
withContext(Dispatchers.Main) {
summary = it
}
}
} }
setDefaultValue(false) setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit().also { preferences.edit().putBoolean(key, new).commit().also {
summary = runBlocking { mainScope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) { getTokenDetail(true) } getTokenDetail(true).let {
withContext(Dispatchers.Main) {
summary = it
}
}
} }
} }
} }
@ -529,12 +596,12 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
} }
private fun getTokenDetail(force: Boolean = false): String { private fun getTokenDetail(force: Boolean = false): String {
return try { return runCatching {
val storedToken = tokenInterceptor.getAccessToken(force) val storedToken = tokenInterceptor.getAccessToken(force)
"Token location: " + storedToken.bucket?.substringAfter("/")?.substringBefore("/") "Token location: " + storedToken.bucket?.substringAfter("/")?.substringBefore("/")
} catch (e: Exception) { }.getOrElse {
tokenInterceptor.removeToken() tokenInterceptor.removeToken()
"Error: ${e.localizedMessage ?: "Something Went Wrong"}" "Error: ${it.localizedMessage ?: "Something Went Wrong"}"
} }
} }