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'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext {
extName = 'AnimeFlix'
pkgNameSuffix = 'en.animeflix'
extClass = '.AnimeFlix'
extVersionCode = 5
extVersionCode = 6
libVersion = '13'
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.animeextension.en.animeflix
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
@ -45,26 +44,27 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== 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 = SAnime.create().apply {
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
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()
}
override fun popularAnimeNextPageSelector(): String = "div.nav-links > span.current ~ a"
override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
override fun latestUpdatesSelector(): String = popularAnimeSelector()
@ -74,7 +74,6 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val cleanQuery = query.replace(" ", "+").lowercase()
@ -97,7 +96,6 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
@ -141,133 +139,122 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val animeInfo = document.select("div.thecontent h3:contains(Anime Info) ~ ul li").joinToString("\n") { it.text() }
return SAnime.create().apply {
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.single_post > header > h1")!!.text()
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)").joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
status = when (infosDiv.getInfo("Status").toString()) {
"Completed" -> SAnime.COMPLETED
"Currently Airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
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 ==============================
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>> {
val document = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
val episodeList = mutableListOf<SEpisode>()
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 document = client.newCall(GET(baseUrl + anime.url)).execute()
.use { it.asJsoup() }
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 {
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
}.values.toList()
seasonsLinks.forEach { season ->
}
val serverListSeason = mutableListOf<List<EpUrl>>()
season.forEach {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
val seasonNumber = seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
seasonsLinks.flatMap { (seasonNumber, season) ->
val serverListSeason = season.map {
val previousText = it.previousElementSibling()!!.text()
val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
val url = it.selectFirst("a")!!.attr("href")
val episodesDocument = client.newCall(GET(url)).execute().asJsoup()
serverListSeason.add(
val episodesDocument = client.newCall(GET(url)).execute()
.use { it.asJsoup() }
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
},
)
}
}
transpose(serverListSeason).forEachIndexed { index, serverList ->
episodeList.add(
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(
json.encodeToString(serverList),
)
},
)
}
transposeEpisodes(serverListSeason)
}
} 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"
driveList.add(Pair(it.selectFirst("a")!!.attr("href"), quality))
Pair(it.selectFirst("a")!!.attr("href"), quality)
}
// Load episodes
driveList.forEach { drive ->
val episodesDocument = client.newCall(GET(drive.first)).execute().asJsoup()
serversList.add(
val serversList = driveList.map { drive ->
val episodesDocument = client.newCall(GET(drive.first)).execute()
.use { it.asJsoup() }
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(drive.second, it.attr("href"), it.text())
},
)
}
}
transpose(serversList).forEachIndexed { index, serverList ->
episodeList.add(
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(
json.encodeToString(serverList),
)
},
)
}
transposeEpisodes(serversList)
}
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 episodeFromElement(element: Element): SEpisode = throw Exception("Not Used")
// ============================ Video Links =============================
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 leechUrls = urls.map {
val firstLeech = client.newCall(GET(it.url)).execute().asJsoup().selectFirst(
"script:containsData(downlaod_button)",
)!!.data().substringAfter("<a href=\"").substringBefore("\">")
val link = "https://" + firstLeech.toHttpUrl().host + client.newCall(GET(firstLeech)).execute().body.string()
.substringAfter("replace(\"").substringBefore("\"")
val firstLeech = client.newCall(GET(it.url)).execute()
.use { it.asJsoup() }
.selectFirst("script:containsData(downlaod_button)")!!
.data()
.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)
}
videoList.addAll(
leechUrls.parallelMap { url ->
val videoList = leechUrls.parallelMap { url ->
runCatching {
if (url.url.toHttpUrl().encodedPath == "/404") return@runCatching null
val (videos, mediaUrl) = extractVideo(url)
if (videos.isEmpty()) failedMediaUrl.add(Pair(mediaUrl, url.quality))
return@runCatching videos
}.getOrNull()
when {
videos.isEmpty() -> extractGDriveLink(mediaUrl, url.quality)
else -> videos
}
.filterNotNull()
.flatten(),
)
videoList.addAll(
failedMediaUrl.mapNotNull { (url, quality) ->
runCatching {
extractGDriveLink(url, quality)
}.getOrNull()
}.flatten(),
)
}.filterNotNull().flatten()
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")
// ============================= Utilities ==============================
// 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> {
val videoList = mutableListOf<Video>()
val qualityRegex = """(\d+)p""".toRegex()
val matchResult = qualityRegex.find(epUrl.name)
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
for (type in 1..3) {
videoList.addAll(
extractWorkerLinks(epUrl.url, quality, type),
)
}
return Pair(videoList, epUrl.url)
return (1..3).toList().flatMap { type ->
extractWorkerLinks(epUrl.url, quality, type)
}.let { Pair(it, epUrl.url) }
}
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
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 size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
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> {
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 gdLink = gdBtn.attr("href")
val sizeMatch = SIZE_REGEX.find(gdBtn.text())
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")
return if (link.isNullOrEmpty()) {
emptyList()
@ -339,7 +319,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith(
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
@ -366,7 +346,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
)
// 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 {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
@ -374,18 +354,20 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
companion object {
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 val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"