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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 162 additions and 83 deletions

View File

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

View File

@ -59,30 +59,26 @@ data class Anime(
val genres: ArrayList<String>? = null,
val series_metadata: Metadata? = null,
@SerialName("movie_listing_metadata")
val movie_metadata: MovieMeta? = null,
val movie_metadata: Metadata? = 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
data class Metadata(
val maturity_ratings: ArrayList<String>,
val is_simulcast: Boolean,
val audio_locales: ArrayList<String>,
val is_simulcast: Boolean? = null,
val audio_locales: ArrayList<String>? = null,
val subtitle_locales: ArrayList<String>,
val is_dubbed: Boolean,
val is_subbed: Boolean,
@SerialName("tenant_categories")
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
@ -172,6 +168,22 @@ data class Subtitle(
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? {
if (size < 3) return null
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.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import rx.Observable
@ -53,6 +57,8 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
private val json: Json by injectLazy()
private val mainScope by lazy { MainScope() }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
@ -65,6 +71,8 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
super.client.newBuilder().addInterceptor(tokenInterceptor).build()
}
private val noTokenClient = super.client
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
@ -73,7 +81,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
}
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 position = response.request.url.queryParameter("start")?.toIntOrNull() ?: 0
return AnimesPage(animeList, position + 36 < parsed.total)
@ -103,7 +111,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
}
override fun searchAnimeParse(response: Response): AnimesPage {
val bod = response.body.string()
val bod = response.use { it.body.string() }
val total: Int
val items =
if (response.request.url.encodedPath.contains("search")) {
@ -125,6 +133,43 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
// =========================== 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> {
val mediaId = json.decodeFromString<LinkData>(anime.url)
val resp = client.newCall(
@ -133,17 +178,10 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
} else {
GET("$crApiUrl/cms/movie_listings/${mediaId.id}?locale=en-US")
},
).execute()
val info = json.decodeFromString<AnimeResult>(resp.body.string())
).execute().use { it.body.string() }
val info = json.decodeFromString<AnimeResult>(resp)
return Observable.just(
anime.apply {
author = info.data.first().content_provider
status = SAnime.COMPLETED
if (genre.isNullOrBlank()) {
genre =
info.data.first().genres?.joinToString { gen -> gen.replaceFirstChar { it.uppercase() } }
}
},
info.data.first().toSAnimeOrNull(anime) ?: anime,
)
}
@ -161,7 +199,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
}
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 chunkSize = Runtime.getRuntime().availableProcessors()
return if (series) {
@ -176,7 +214,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
seasons.data.mapIndexed { index, movie ->
SEpisode.create().apply {
url = EpisodeData(listOf(Pair(movie.id, ""))).toJsonString()
name = "Movie"
name = "Movie ${index + 1}"
episode_number = (index + 1).toFloat()
date_upload = movie.date?.let(::parseDate) ?: 0L
}
@ -185,10 +223,9 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun getEpisodes(seasonData: SeasonResult.Season): List<SEpisode> {
val episodeResp =
val body =
client.newCall(GET("$crApiUrl/cms/seasons/${seasonData.id}/episodes"))
.execute()
val body = episodeResp.body.string()
.execute().use { it.body.string() }
val episodes = json.decodeFromString<EpisodeResult>(body)
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> {
val (mediaId, aud) = media
val response = client.newCall(getVideoRequest(mediaId)).execute()
val streams = json.decodeFromString<VideoStreams>(response.body.string())
val response = client.newCall(getVideoRequest(mediaId)).execute().use { it.body.string() }
val streams = json.decodeFromString<VideoStreams>(response)
val subLocale = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!.getLocale()
val subsList = runCatching {
@ -276,7 +313,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
runCatching {
val playlist = client.newCall(GET(stream.url)).execute()
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 {
val hardsub = stream.hardsub_locale.let { hs ->
if (hs.isNotBlank()) " - HardSub: $hs" else ""
@ -340,6 +377,14 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
Pair("sv-SE", "Swedish"),
Pair("zh-CN", "Chinese (PRC)"),
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 {
@ -355,55 +400,69 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
.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 {
title = this@toSAnime.title
thumbnail_url = images.poster_tall?.getOrNull(0)?.thirdLast()?.source
?: images.poster_tall?.getOrNull(0)?.last()?.source
url = LinkData(id, type!!).toJsonString()
genre = series_metadata?.genres?.joinToString()
?: movie_metadata?.genres?.joinToString() ?: ""
status = SAnime.COMPLETED
var desc = this@toSAnime.description + "\n"
desc += "\nLanguage:" +
(
if (series_metadata?.subtitle_locales?.any() == true ||
movie_metadata?.subtitle_locales?.any() == true ||
series_metadata?.is_subbed == true
) {
" Sub"
} else {
""
}
) +
(
if (series_metadata?.audio_locales?.size ?: 0 > 1 ||
movie_metadata?.is_dubbed == true
) {
" Dub"
} else {
""
}
)
desc += "\nMaturity Ratings: " +
(
series_metadata?.maturity_ratings?.joinToString()
?: movie_metadata?.maturity_ratings?.joinToString() ?: ""
)
desc += if (series_metadata?.is_simulcast == true) "\nSimulcast" else ""
desc += "\n\nAudio: " + (
series_metadata?.audio_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() } ?: ""
url = anime?.url ?: LinkData(id, type!!).toJsonString()
genre = anime?.genre ?: (series_metadata?.genres ?: movie_metadata?.genres ?: genres)
?.joinToString { gen -> gen.replaceFirstChar { it.uppercase() } }
status = if (anime != null) fetchStatusByTitle(this@toSAnime.title) else SAnime.UNKNOWN
author = content_provider
description = anime?.description ?: StringBuilder().apply {
appendLine(this@toSAnime.description)
appendLine()
append("Language:")
if ((
subtitle_locales ?: (
series_metadata
?: movie_metadata
)?.subtitle_locales
)?.any() == true ||
(series_metadata ?: movie_metadata)?.is_subbed == true ||
is_subbed == true
) {
append(" Sub")
}
if (((series_metadata?.audio_locales ?: audio_locales)?.size ?: 0) > 1 ||
(series_metadata ?: movie_metadata)?.is_dubbed == true ||
is_dubbed == true
) {
append(" Dub")
}
appendLine()
append("Maturity Ratings: ")
appendLine(
((series_metadata ?: movie_metadata)?.maturity_ratings ?: maturity_ratings)
?.joinToString() ?: "-",
)
desc += "\n\nSubs: " + (
series_metadata?.subtitle_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() }
?: movie_metadata?.subtitle_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() } ?: ""
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() },
)
description = desc
appendLine()
append("Subs: ")
append(
(
subtitle_locales ?: series_metadata?.subtitle_locales
?: movie_metadata?.subtitle_locales
)
?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() },
)
}.toString()
}
override fun List<Video>.sort(): List<Video> {
@ -506,15 +565,23 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
this.apply {
key = PREF_USE_LOCAL_TOKEN_KEY
title = PREF_USE_LOCAL_TOKEN_TITLE
summary = runBlocking {
withContext(Dispatchers.IO) { getTokenDetail() }
mainScope.launch(Dispatchers.IO) {
getTokenDetail().let {
withContext(Dispatchers.Main) {
summary = it
}
}
}
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit().also {
summary = runBlocking {
withContext(Dispatchers.IO) { getTokenDetail(true) }
mainScope.launch(Dispatchers.IO) {
getTokenDetail(true).let {
withContext(Dispatchers.Main) {
summary = it
}
}
}
}
}
@ -529,12 +596,12 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun getTokenDetail(force: Boolean = false): String {
return try {
return runCatching {
val storedToken = tokenInterceptor.getAccessToken(force)
"Token location: " + storedToken.bucket?.substringAfter("/")?.substringBefore("/")
} catch (e: Exception) {
}.getOrElse {
tokenInterceptor.removeToken()
"Error: ${e.localizedMessage ?: "Something Went Wrong"}"
"Error: ${it.localizedMessage ?: "Something Went Wrong"}"
}
}