Fix(en/AniPlay): Update source (#206)

This commit is contained in:
Dark25 2024-09-02 12:28:49 +01:00 committed by GitHub
parent 7a71a9d6fd
commit ffb39ec544
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 413 additions and 55 deletions

View file

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3

View file

@ -8,6 +8,10 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -32,7 +36,7 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() {
query = ANIME_LIST_QUERY, query = ANIME_LIST_QUERY,
variables = AnimeListVariables( variables = AnimeListVariables(
page = page, page = page,
sort = AnimeListVariables.MediaSort.POPULARITY_DESC, sort = AnimeListVariables.MediaSort.TRENDING_DESC,
), ),
) )
} }
@ -58,20 +62,58 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() {
/* ===================================== Search Anime ===================================== */ /* ===================================== Search Anime ===================================== */
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return buildAnimeListRequest( val params = AniListFilters.getSearchParameters(filters)
query = ANIME_LIST_QUERY,
variables = AnimeListVariables( val variablesObject = buildJsonObject {
page = page, put("page", page)
sort = AnimeListVariables.MediaSort.SEARCH_MATCH, put("perPage", 30)
search = query.ifBlank { null }, put("isAdult", false)
), put("type", "ANIME")
) put("sort", params.sort)
if (query.isNotBlank()) put("search", query)
if (params.genres.isNotEmpty()) {
putJsonArray("genres") {
params.genres.forEach { add(it) }
}
}
if (params.format.isNotEmpty()) {
putJsonArray("format") {
params.format.forEach { add(it) }
}
}
if (params.season.isBlank() && params.year.isNotBlank()) {
put("year", "${params.year}%")
}
if (params.season.isNotBlank() && params.year.isBlank()) {
throw Exception("Year cannot be blank if season is set")
}
if (params.season.isNotBlank() && params.year.isNotBlank()) {
put("season", params.season)
put("seasonYear", params.year)
}
if (params.status.isNotBlank()) {
put("status", params.status)
}
}
val variables = json.encodeToString(variablesObject)
return buildRequest(query = SORT_QUERY, variables = variables)
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
return parseAnimeListResponse(response) return parseAnimeListResponse(response)
} }
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AniListFilters.FILTER_LIST
/* ===================================== Anime Details ===================================== */ /* ===================================== Anime Details ===================================== */
override fun animeDetailsRequest(anime: SAnime): Request { override fun animeDetailsRequest(anime: SAnime): Request {
return buildRequest( return buildRequest(
@ -145,6 +187,9 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() {
status = when (media.status) { status = when (media.status) {
AniListMedia.Status.RELEASING -> SAnime.ONGOING AniListMedia.Status.RELEASING -> SAnime.ONGOING
AniListMedia.Status.FINISHED -> SAnime.COMPLETED AniListMedia.Status.FINISHED -> SAnime.COMPLETED
AniListMedia.Status.NOT_YET_RELEASED -> SAnime.LICENSED
AniListMedia.Status.CANCELLED -> SAnime.CANCELLED
AniListMedia.Status.HIATUS -> SAnime.ON_HIATUS
} }
thumbnail_url = media.coverImage.large thumbnail_url = media.coverImage.large
} }

View file

@ -0,0 +1,236 @@
package eu.kanade.tachiyomi.multisrc.anilist
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniListFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckboxList(
options: Array<Pair<String, String>>,
): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkBox -> options.find { it.first == checkBox.name }!!.second }
.filter(String::isNotBlank)
}
private inline fun <reified R> AnimeFilterList.getSort(): String {
val state = (getFirst<R>() as AnimeFilter.Sort).state ?: return ""
val index = state.index
val suffix = if (state.ascending) "" else "_DESC"
return AniListFiltersData.SORT_LIST[index].second + suffix
}
class GenreFilter : CheckBoxFilterList("Genres", AniListFiltersData.GENRE_LIST)
class YearFilter : QueryPartFilter("Year", AniListFiltersData.YEAR_LIST)
class SeasonFilter : QueryPartFilter("Season", AniListFiltersData.SEASON_LIST)
class FormatFilter : CheckBoxFilterList("Format", AniListFiltersData.FORMAT_LIST)
class StatusFilter : QueryPartFilter("Airing Status", AniListFiltersData.STATUS_LIST)
class SortFilter : AnimeFilter.Sort(
"Sort",
AniListFiltersData.SORT_LIST.map { it.first }.toTypedArray(),
Selection(1, false),
)
val FILTER_LIST get() = AnimeFilterList(
GenreFilter(),
YearFilter(),
SeasonFilter(),
FormatFilter(),
StatusFilter(),
SortFilter(),
)
class FilterSearchParams(
val genres: List<String> = emptyList(),
val year: String = "",
val season: String = "",
val format: List<String> = emptyList(),
val status: String = "",
val sort: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckboxList<GenreFilter>(AniListFiltersData.GENRE_LIST),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.parseCheckboxList<FormatFilter>(AniListFiltersData.FORMAT_LIST),
filters.asQueryPart<StatusFilter>(),
filters.getSort<SortFilter>(),
)
}
private object AniListFiltersData {
val GENRE_LIST = arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mahou Shoujo", "Mahou Shoujo"),
Pair("Mecha", "Mecha"),
Pair("Music", "Music"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Slice of Life", "Slice of Life"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
)
val YEAR_LIST = arrayOf(
Pair("<Select>", ""),
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015", "2015"),
Pair("2014", "2014"),
Pair("2013", "2013"),
Pair("2012", "2012"),
Pair("2011", "2011"),
Pair("2010", "2010"),
Pair("2009", "2009"),
Pair("2008", "2008"),
Pair("2007", "2007"),
Pair("2006", "2006"),
Pair("2005", "2005"),
Pair("2004", "2004"),
Pair("2003", "2003"),
Pair("2002", "2002"),
Pair("2001", "2001"),
Pair("2000", "2000"),
Pair("1999", "1999"),
Pair("1998", "1998"),
Pair("1997", "1997"),
Pair("1996", "1996"),
Pair("1995", "1995"),
Pair("1994", "1994"),
Pair("1993", "1993"),
Pair("1992", "1992"),
Pair("1991", "1991"),
Pair("1990", "1990"),
Pair("1989", "1989"),
Pair("1988", "1988"),
Pair("1987", "1987"),
Pair("1986", "1986"),
Pair("1985", "1985"),
Pair("1984", "1984"),
Pair("1983", "1983"),
Pair("1982", "1982"),
Pair("1981", "1981"),
Pair("1980", "1980"),
Pair("1979", "1979"),
Pair("1978", "1978"),
Pair("1977", "1977"),
Pair("1976", "1976"),
Pair("1975", "1975"),
Pair("1974", "1974"),
Pair("1973", "1973"),
Pair("1972", "1972"),
Pair("1971", "1971"),
Pair("1970", "1970"),
Pair("1969", "1969"),
Pair("1968", "1968"),
Pair("1967", "1967"),
Pair("1966", "1966"),
Pair("1965", "1965"),
Pair("1964", "1964"),
Pair("1963", "1963"),
Pair("1962", "1962"),
Pair("1961", "1961"),
Pair("1960", "1960"),
Pair("1959", "1959"),
Pair("1958", "1958"),
Pair("1957", "1957"),
Pair("1956", "1956"),
Pair("1955", "1955"),
Pair("1954", "1954"),
Pair("1953", "1953"),
Pair("1952", "1952"),
Pair("1951", "1951"),
Pair("1950", "1950"),
Pair("1949", "1949"),
Pair("1948", "1948"),
Pair("1947", "1947"),
Pair("1946", "1946"),
Pair("1945", "1945"),
Pair("1944", "1944"),
Pair("1943", "1943"),
Pair("1942", "1942"),
Pair("1941", "1941"),
Pair("1940", "1940"),
)
val SEASON_LIST = arrayOf(
Pair("<Select>", ""),
Pair("Winter", "WINTER"),
Pair("Spring", "SPRING"),
Pair("Summer", "SUMMER"),
Pair("Fall", "FALL"),
)
val FORMAT_LIST = arrayOf(
Pair("TV Show", "TV"),
Pair("Movie", "MOVIE"),
Pair("TV Short", "TV_SHORT"),
Pair("Special", "SPECIAL"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Music", "MUSIC"),
)
val STATUS_LIST = arrayOf(
Pair("<Select>", ""),
Pair("Airing", "RELEASING"),
Pair("Finished", "FINISHED"),
Pair("Not Yet Aired", "NOT_YET_RELEASED"),
Pair("Cancelled", "CANCELLED"),
)
val SORT_LIST = arrayOf(
Pair("Title", "TITLE_ENGLISH"),
Pair("Popularity", "POPULARITY"),
Pair("Average Score", "SCORE"),
Pair("Trending", "TRENDING"),
Pair("Favorites", "FAVOURITES"),
Pair("Date Added", "ID"),
Pair("Release Date", "START_DATE"),
)
}
}

View file

@ -55,16 +55,76 @@ internal const val ANIME_DETAILS_QUERY = """
} }
} }
""" """
private fun String.toQuery() = this.trimIndent().replace("%", "$")
internal val SORT_QUERY = """
query (
${"$"}page: Int,
${"$"}perPage: Int,
${"$"}isAdult: Boolean,
${"$"}type: MediaType,
${"$"}sort: [MediaSort],
${"$"}status: MediaStatus,
${"$"}search: String,
${"$"}genres: [String],
${"$"}year: String,
${"$"}seasonYear: Int,
${"$"}season: MediaSeason,
${"$"}format: [MediaFormat]
) {
Page (page: ${"$"}page, perPage: ${"$"}perPage) {
pageInfo {
hasNextPage
}
media (
isAdult: ${"$"}isAdult,
type: ${"$"}type,
sort: ${"$"}sort,
status: ${"$"}status,
search: ${"$"}search,
genre_in: ${"$"}genres,
startDate_like: ${"$"}year,
seasonYear: ${"$"}seasonYear,
season: ${"$"}season,
format_in: ${"$"}format
) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
medium
}
status
genres
studios {
nodes {
name
}
}
}
}
}
""".toQuery()
@Serializable @Serializable
internal data class AnimeListVariables( internal data class AnimeListVariables(
val page: Int, val page: Int,
val sort: MediaSort, val sort: MediaSort,
val search: String? = null, val search: String? = null,
val genre: String? = null,
val year: String? = null,
val status: String? = null,
val format: String? = null,
val season: String? = null,
val seasonYear: String? = null,
val isAdult: Boolean = false,
) { ) {
enum class MediaSort { enum class MediaSort {
POPULARITY_DESC, TRENDING_DESC,
SEARCH_MATCH,
START_DATE_DESC, START_DATE_DESC,
} }
} }

View file

@ -47,6 +47,9 @@ internal data class AniListMedia(
enum class Status { enum class Status {
RELEASING, RELEASING,
FINISHED, FINISHED,
NOT_YET_RELEASED,
CANCELLED,
HIATUS,
} }
@Serializable @Serializable

View file

@ -2,7 +2,7 @@ ext {
extName = 'AniPlay' extName = 'AniPlay'
extClass = '.AniPlay' extClass = '.AniPlay'
themePkg = 'anilist' themePkg = 'anilist'
overrideVersionCode = 1 overrideVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animeextension.en.aniplay
import android.app.Application import android.app.Application
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
@ -17,6 +18,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -25,6 +27,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -83,26 +86,26 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
providers.forEach { provider -> providers.forEach { provider ->
provider.episodes.forEach { episode -> provider.episodes.forEach { episode ->
if (!episodes.containsKey(episode.number)) { val episodeNumber = episode.number.toString().toIntOrNull() ?: episode.number.toInt()
episodes[episode.number] = episode if (!episodes.containsKey(episodeNumber)) {
episodes[episodeNumber] = episode
} }
val existingEpisodeExtras = episodeExtras.getOrElse(episode.number) { emptyList() } val existingEpisodeExtras = episodeExtras.getOrElse(episodeNumber) { emptyList() }
val episodeExtra = EpisodeExtra( val episodeExtra = EpisodeExtra(
source = provider.providerId, source = provider.providerId,
episodeId = episode.id, episodeId = episode.id,
hasDub = episode.hasDub, hasDub = episode.hasDub,
) )
episodeExtras[episode.number] = existingEpisodeExtras + listOf(episodeExtra) episodeExtras[episodeNumber] = existingEpisodeExtras + listOf(episodeExtra)
} }
} }
return episodes.map { episodeMap -> return episodes.map { episodeMap ->
val episode = episodeMap.value val episode = episodeMap.value
val episodeNumber = episode.number val episodeNumber = episodeMap.key
val episodeExtra = episodeExtras.getValue(episodeNumber) val episodeExtra = episodeExtras.getValue(episodeNumber)
val episodeExtraString = json.encodeToString(episodeExtra) val episodeExtraString = json.encodeToString(episodeExtra)
.let { Base64.encode(it.toByteArray(), Base64.DEFAULT) } .let { Base64.encodeToString(it.toByteArray(), Base64.DEFAULT) }
.toString(Charsets.UTF_8)
val url = baseUrl.toHttpUrl().newBuilder() val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("anime") .addPathSegment("anime")
@ -112,7 +115,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
.addQueryParameter("extras", episodeExtraString) .addQueryParameter("extras", episodeExtraString)
.build() .build()
val name = parseEpisodeName(episodeNumber, episode.title) val name = parseEpisodeName(episodeNumber.toString(), episode.title)
val uploadDate = parseDate(episode.createdAt) val uploadDate = parseDate(episode.createdAt)
val dub = when { val dub = when {
episodeExtra.any { it.hasDub } -> ", Dub" episodeExtra.any { it.hasDub } -> ", Dub"
@ -142,45 +145,57 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList() val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList()
val extras = episodeUrl.queryParameter("extras") val extras = episodeUrl.queryParameter("extras")
?.let { ?.let {
Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) try {
Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8)
} catch (e: IllegalArgumentException) {
Log.e("AniPlay", "Error decoding base64", e)
return emptyList()
}
}
?.let {
try {
json.decodeFromString<List<EpisodeExtra>>(it)
} catch (e: SerializationException) {
Log.e("AniPlay", "Error parsing JSON", e)
emptyList()
}
} }
?.let { json.decodeFromString<List<EpisodeExtra>>(it) }
?: emptyList() ?: emptyList()
val episodeDataList = extras.parallelFlatMapBlocking { extra -> val episodeDataList = extras.parallelFlatMapBlocking { extra ->
val languages = mutableListOf("sub") val languages = mutableListOf("sub").apply {
if (extra.hasDub) { if (extra.hasDub) add("dub")
languages.add("dub")
} }
val url = "$baseUrl/api/anime/source/$animeId" val url = "$baseUrl/api/anime/source/$animeId"
languages.map { language -> languages.map { language ->
val requestBody = json val requestBody = json.encodeToString(
.encodeToString( VideoSourceRequest(
VideoSourceRequest( source = extra.source,
source = extra.source, episodeId = extra.episodeId,
episodeId = extra.episodeId, episodeNum = episodeNum,
episodeNum = episodeNum, subType = language,
subType = language, ),
), ).toRequestBody("application/json".toMediaType())
try {
val response = client.newCall(POST(url = url, body = requestBody)).execute().parseAs<VideoSourceResponse>()
EpisodeData(
source = extra.source,
language = language,
response = response,
) )
.toRequestBody("application/json".toMediaType()) } catch (e: IOException) {
null // Return null to be filtered out
val response = client } catch (e: Exception) {
.newCall(POST(url = url, body = requestBody)) null // Return null to be filtered out
.execute() }
.parseAs<VideoSourceResponse>() }.filterNotNull() // Filter out null values due to errors
EpisodeData(
source = extra.source,
language = language,
response = response,
)
}
} }
val videos = episodeDataList.flatMap { episodeData -> val videos = episodeDataList.flatMap { episodeData ->
val defaultSource = episodeData.response.sources?.first { val defaultSource = episodeData.response.sources?.firstOrNull {
it.quality in listOf("default", "auto") it.quality in listOf("default", "auto")
} ?: return@flatMap emptyList() } ?: return@flatMap emptyList()
@ -316,14 +331,13 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
} }
/* =================================== AniPlay Utilities =================================== */ /* =================================== AniPlay Utilities =================================== */
private fun parseEpisodeName(number: String, title: String?): String {
private fun parseEpisodeName(number: Int, name: String): String { return if (title.isNullOrBlank()) {
return when { "Episode $number"
listOf("EP ", "EPISODE ").any(name::startsWith) -> "Episode $number" } else {
else -> "Episode $number: $name" "Episode $number: $title"
} }
} }
private fun getServerName(value: String): String { private fun getServerName(value: String): String {
val index = PREF_SERVER_ENTRY_VALUES.indexOf(value) val index = PREF_SERVER_ENTRY_VALUES.indexOf(value)
return PREF_SERVER_ENTRIES[index] return PREF_SERVER_ENTRIES[index]

View file

@ -12,7 +12,7 @@ data class EpisodeListResponse(
@Serializable @Serializable
data class Episode( data class Episode(
val id: String, val id: String,
val number: Int, val number: Float,
val title: String, val title: String,
val hasDub: Boolean, val hasDub: Boolean,
val isFiller: Boolean, val isFiller: Boolean,