fix(en/animeflix): Fix popular & latest animes page (#2094)

This commit is contained in:
Claudemirovsky
2023-08-29 09:06:28 -03:00
committed by GitHub
parent e8164f9c0b
commit 68e02edc1d
2 changed files with 104 additions and 120 deletions

View File

@ -1,12 +1,14 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
apply plugin: 'kotlinx-serialization' alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext { ext {
extName = 'AnimeFlix' extName = 'AnimeFlix'
pkgNameSuffix = 'en.animeflix' pkgNameSuffix = 'en.animeflix'
extClass = '.AnimeFlix' extClass = '.AnimeFlix'
extVersionCode = 5 extVersionCode = 6
libVersion = '13' libVersion = '13'
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.animeextension.en.animeflix package eu.kanade.tachiyomi.animeextension.en.animeflix
import android.app.Application import android.app.Application
import android.content.SharedPreferences
import android.util.Base64 import android.util.Base64
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
@ -45,26 +44,27 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page/")
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/page/$page/") override fun popularAnimeSelector() = "div#content_box > div.post-cards > article"
override fun popularAnimeSelector(): String = "div#page > div#content_box > article" override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href")) setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
thumbnail_url = element.selectFirst("img")!!.attr("src") // prevent base64 images
thumbnail_url = element.selectFirst("img")!!.run {
attr("data-pagespeed-high-res-src").ifEmpty { attr("src") }
}
title = element.selectFirst("header")!!.text() title = element.selectFirst("header")!!.text()
} }
override fun popularAnimeNextPageSelector(): String = "div.nav-links > span.current ~ a" override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
override fun latestUpdatesSelector(): String = popularAnimeSelector() override fun latestUpdatesSelector(): String = popularAnimeSelector()
@ -74,7 +74,6 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector() override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val cleanQuery = query.replace(" ", "+").lowercase() val cleanQuery = query.replace(" ", "+").lowercase()
@ -97,7 +96,6 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector() override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters =============================== // ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"), AnimeFilter.Header("Text search ignores filters"),
GenreFilter(), GenreFilter(),
@ -141,133 +139,122 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.single_post > header > h1")!!.text()
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
override fun animeDetailsParse(document: Document): SAnime { val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
val animeInfo = document.select("div.thecontent h3:contains(Anime Info) ~ ul li").joinToString("\n") { it.text() } status = when (infosDiv.getInfo("Status").toString()) {
"Completed" -> SAnime.COMPLETED
return SAnime.create().apply { "Currently Airing" -> SAnime.ONGOING
title = document.selectFirst("div.single_post > header > h1")!!.text() else -> SAnime.UNKNOWN
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)").joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
} }
artist = infosDiv.getInfo("Studios")
author = infosDiv.getInfo("Producers")
genre = infosDiv.getInfo("Genres")
val animeInfo = infosDiv.select("li").joinToString("\n") { it.text() }
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)")
.joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
} }
private fun Element.getInfo(info: String) =
selectFirst("li:contains($info)")?.ownText()?.trim()
// ============================== Episodes ============================== // ============================== Episodes ==============================
val seasonRegex by lazy { Regex("""season (\d+)""", RegexOption.IGNORE_CASE) }
val qualityRegex by lazy { """(\d+)p""".toRegex() }
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> { override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
val document = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup() val document = client.newCall(GET(baseUrl + anime.url)).execute()
val episodeList = mutableListOf<SEpisode>() .use { it.asJsoup() }
val serversList = mutableListOf<List<EpUrl>>()
val seasonRegex = Regex("""season (\d+)""", RegexOption.IGNORE_CASE)
val qualityRegex = """(\d+)p""".toRegex()
val driveList = mutableListOf<Pair<String, String>>()
val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)") val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)")
if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) { val episodeList = if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) {
val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy { val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy {
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1] seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
}.values.toList() }
seasonsLinks.forEach { season ->
val serverListSeason = mutableListOf<List<EpUrl>>() seasonsLinks.flatMap { (seasonNumber, season) ->
val serverListSeason = season.map {
season.forEach { val previousText = it.previousElementSibling()!!.text()
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality" val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
val seasonNumber = seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
val url = it.selectFirst("a")!!.attr("href") val url = it.selectFirst("a")!!.attr("href")
val episodesDocument = client.newCall(GET(url)).execute().asJsoup() val episodesDocument = client.newCall(GET(url)).execute()
serverListSeason.add( .use { it.asJsoup() }
episodesDocument.select("div.entry-content > h3 > a").map { episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}") EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
}, }
)
} }
transpose(serverListSeason).forEachIndexed { index, serverList -> transposeEpisodes(serverListSeason)
episodeList.add(
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(
json.encodeToString(serverList),
)
},
)
}
} }
} else { } else {
document.select("div.thecontent p:has(span:contains(Gdrive))").forEach { val driveList = document.select("div.thecontent p:has(span:contains(Gdrive))").map {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality" val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
driveList.add(Pair(it.selectFirst("a")!!.attr("href"), quality)) Pair(it.selectFirst("a")!!.attr("href"), quality)
} }
// Load episodes // Load episodes
driveList.forEach { drive -> val serversList = driveList.map { drive ->
val episodesDocument = client.newCall(GET(drive.first)).execute().asJsoup() val episodesDocument = client.newCall(GET(drive.first)).execute()
serversList.add( .use { it.asJsoup() }
episodesDocument.select("div.entry-content > h3 > a").map { episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(drive.second, it.attr("href"), it.text()) EpUrl(drive.second, it.attr("href"), it.text())
}, }
)
} }
transpose(serversList).forEachIndexed { index, serverList -> transposeEpisodes(serversList)
episodeList.add(
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(
json.encodeToString(serverList),
)
},
)
}
} }
return Observable.just(episodeList.reversed()) return Observable.just(episodeList.reversed())
} }
private fun transposeEpisodes(serversList: List<List<EpUrl>>) =
transpose(serversList).mapIndexed { index, serverList ->
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(json.encodeToString(serverList))
}
}
override fun episodeListSelector(): String = throw Exception("Not Used") override fun episodeListSelector(): String = throw Exception("Not Used")
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not Used") override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not Used")
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> { override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val videoList = mutableListOf<Video>()
val failedMediaUrl = mutableListOf<Pair<String, String>>()
val urls = json.decodeFromString<List<EpUrl>>(episode.url) val urls = json.decodeFromString<List<EpUrl>>(episode.url)
val leechUrls = urls.map { val leechUrls = urls.map {
val firstLeech = client.newCall(GET(it.url)).execute().asJsoup().selectFirst( val firstLeech = client.newCall(GET(it.url)).execute()
"script:containsData(downlaod_button)", .use { it.asJsoup() }
)!!.data().substringAfter("<a href=\"").substringBefore("\">") .selectFirst("script:containsData(downlaod_button)")!!
val link = "https://" + firstLeech.toHttpUrl().host + client.newCall(GET(firstLeech)).execute().body.string() .data()
.substringAfter("replace(\"").substringBefore("\"") .substringAfter("<a href=\"")
.substringBefore("\">")
val path = client.newCall(GET(firstLeech)).execute()
.use { it.body.string() }
.substringAfter("replace(\"")
.substringBefore("\"")
val link = "https://" + firstLeech.toHttpUrl().host + path
EpUrl(it.quality, link, it.name) EpUrl(it.quality, link, it.name)
} }
videoList.addAll( val videoList = leechUrls.parallelMap { url ->
leechUrls.parallelMap { url -> runCatching {
runCatching { if (url.url.toHttpUrl().encodedPath == "/404") return@runCatching null
if (url.url.toHttpUrl().encodedPath == "/404") return@runCatching null val (videos, mediaUrl) = extractVideo(url)
val (videos, mediaUrl) = extractVideo(url) when {
if (videos.isEmpty()) failedMediaUrl.add(Pair(mediaUrl, url.quality)) videos.isEmpty() -> extractGDriveLink(mediaUrl, url.quality)
return@runCatching videos else -> videos
}.getOrNull() }
} }.getOrNull()
.filterNotNull() }.filterNotNull().flatten()
.flatten(),
)
videoList.addAll(
failedMediaUrl.mapNotNull { (url, quality) ->
runCatching {
extractGDriveLink(url, quality)
}.getOrNull()
}.flatten(),
)
require(videoList.isNotEmpty()) { "Failed to fetch videos" } require(videoList.isNotEmpty()) { "Failed to fetch videos" }
@ -281,26 +268,19 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
// https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt // https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> { private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
val videoList = mutableListOf<Video>()
val qualityRegex = """(\d+)p""".toRegex()
val matchResult = qualityRegex.find(epUrl.name) val matchResult = qualityRegex.find(epUrl.name)
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
for (type in 1..3) { return (1..3).toList().flatMap { type ->
videoList.addAll( extractWorkerLinks(epUrl.url, quality, type)
extractWorkerLinks(epUrl.url, quality, type), }.let { Pair(it, epUrl.url) }
)
}
return Pair(videoList, epUrl.url)
} }
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> { private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type" val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup() val resp = client.newCall(GET(reqLink)).execute().use { it.asJsoup() }
val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim()) val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: "" val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement -> return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
@ -321,12 +301,12 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> { private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
val tokenClient = client.newBuilder().addInterceptor(TokenInterceptor()).build() val tokenClient = client.newBuilder().addInterceptor(TokenInterceptor()).build()
val response = tokenClient.newCall(GET(mediaUrl)).execute().asJsoup() val response = tokenClient.newCall(GET(mediaUrl)).execute().use { it.asJsoup() }
val gdBtn = response.selectFirst("div.card-body a.btn")!! val gdBtn = response.selectFirst("div.card-body a.btn")!!
val gdLink = gdBtn.attr("href") val gdLink = gdBtn.attr("href")
val sizeMatch = SIZE_REGEX.find(gdBtn.text()) val sizeMatch = SIZE_REGEX.find(gdBtn.text())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: "" val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup() val gdResponse = client.newCall(GET(gdLink)).execute().use { it.asJsoup() }
val link = gdResponse.select("form#download-form") val link = gdResponse.select("form#download-form")
return if (link.isNullOrEmpty()) { return if (link.isNullOrEmpty()) {
emptyList() emptyList()
@ -339,7 +319,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith( return sortedWith(
compareBy { it.quality.contains(quality) }, compareBy { it.quality.contains(quality) },
).reversed() ).reversed()
} }
@ -366,7 +346,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
) )
// From Dopebox // From Dopebox
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking { runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
@ -374,18 +354,20 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
companion object { companion object {
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE) private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080" private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
} }
// ============================== Settings ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = "Preferred quality" title = PREF_QUALITY_TITLE
entries = arrayOf("1080p", "720p", "480p", "360p") entries = PREF_QUALITY_ENTRIES
entryValues = arrayOf("1080", "720", "480", "360") entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"