Anitube: Fix video extractor (#1393)

* Fix anitube extractor

* General refactor

* Bump version
This commit is contained in:
Claudemirovsky
2023-03-15 06:21:41 -03:00
committed by GitHub
parent a528304083
commit 4c38b2413e
3 changed files with 101 additions and 120 deletions

View File

@ -5,9 +5,8 @@ ext {
extName = 'Anitube' extName = 'Anitube'
pkgNameSuffix = 'pt.anitube' pkgNameSuffix = 'pt.anitube'
extClass = '.Anitube' extClass = '.Anitube'
extVersionCode = 8 extVersionCode = 9
libVersion = '13' libVersion = '13'
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -13,21 +13,15 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception import java.lang.Exception
import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -43,27 +37,25 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
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)
} }
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl) .add("Referer", baseUrl)
.add("Accept-Language", ACCEPT_LANGUAGE) .add("Accept-Language", ACCEPT_LANGUAGE)
// Popular // ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.lista_de_animes div.ani_loop_item_img > a" override fun popularAnimeSelector(): String = "div.lista_de_animes div.ani_loop_item_img > a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime/page/$page") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime/page/$page")
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element): SAnime {
val anime: SAnime = SAnime.create() return SAnime.create().apply {
anime.setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!! val img = element.selectFirst("img")!!
anime.title = img.attr("title") title = img.attr("title")
anime.thumbnail_url = img.attr("src") thumbnail_url = img.attr("src")
return anime }
} }
override fun popularAnimeNextPageSelector(): String = "a.page-numbers:contains(Próximo)" override fun popularAnimeNextPageSelector(): String = "a.page-numbers:contains(Próximo)"
@ -77,17 +69,19 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return AnimesPage(animes, hasNextPage) return AnimesPage(animes, hasNextPage)
} }
// Episodes // ============================== Episodes ==============================
override fun episodeListSelector(): String = "div.animepag_episodios_container > div.animepag_episodios_item > a" override fun episodeListSelector(): String = "div.animepag_episodios_container > div.animepag_episodios_item > a"
private fun getAllEps(response: Response): List<SEpisode> { private fun getAllEps(response: Response): List<SEpisode> {
val doc = if (response.request.url.toString().contains("/video/")) { val doc = response.asJsoup().let {
getRealDoc(response.asJsoup()) if (response.request.url.toString().contains("/video/")) {
} else { response.asJsoup() } getRealDoc(it)
} else { it }
}
val epElementList = doc.select(episodeListSelector()) val epElementList = doc.select(episodeListSelector())
val epList = mutableListOf<SEpisode>() val epList = mutableListOf<SEpisode>()
epList.addAll(epElementList.map { episodeFromElement(it) }) epList.addAll(epElementList.map(::episodeFromElement))
if (hasNextPage(doc)) { if (hasNextPage(doc)) {
val next = doc.selectFirst(popularAnimeNextPageSelector())!!.attr("href") val next = doc.selectFirst(popularAnimeNextPageSelector())!!.attr("href")
val request = GET(baseUrl + next) val request = GET(baseUrl + next)
@ -96,31 +90,34 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
return epList return epList
} }
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
return getAllEps(response).reversed() return getAllEps(response).reversed()
} }
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create() return SEpisode.create().apply {
episode.setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
episode.episode_number = try { episode_number = runCatching {
element.selectFirst("div.animepag_episodios_item_views")!! element.selectFirst("div.animepag_episodios_item_views")!!
.text()
.substringAfter(" ")
.toFloat()
}.getOrDefault(0F)
name = element.selectFirst("div.animepag_episodios_item_nome")!!.text()
date_upload = element.selectFirst("div.animepag_episodios_item_date")!!
.text() .text()
.substringAfter(" ").toFloat() .toDate()
} catch (e: NumberFormatException) { 0F } }
episode.name = element.selectFirst("div.animepag_episodios_item_nome")!!.text()
episode.date_upload = element.selectFirst("div.animepag_episodios_item_date")!!
.text().toDate()
return episode
} }
// Video links // ============================ Video Links =============================
override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response) override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response)
override fun videoListSelector() = throw Exception("not used") override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used") override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used") override fun videoUrlParse(document: Document) = throw Exception("not used")
// Search // =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = throw Exception("not used") override fun searchAnimeFromElement(element: Element) = throw Exception("not used")
override fun searchAnimeNextPageSelector() = throw Exception("not used") override fun searchAnimeNextPageSelector() = throw Exception("not used")
@ -128,23 +125,15 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response) override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response)
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> { override fun getFilterList(): AnimeFilterList = AnitubeFilters.filterList
val params = AnitubeFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw Exception("not used") override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
private fun searchAnimeRequest(page: Int, query: String, filters: AnitubeFilters.FilterSearchParams): Request {
return if (query.isBlank()) { return if (query.isBlank()) {
val season = filters.season val params = AnitubeFilters.getSearchParameters(filters)
val genre = filters.genre val season = params.season
val year = filters.year val genre = params.genre
val char = filters.initialChar val year = params.year
val char = params.initialChar
when { when {
!season.isBlank() -> GET("$baseUrl/temporada/$season/$year") !season.isBlank() -> GET("$baseUrl/temporada/$season/$year")
!genre.isBlank() -> GET("$baseUrl/genero/$genre/page/$page/${char.replace("todos", "")}") !genre.isBlank() -> GET("$baseUrl/genero/$genre/page/$page/${char.replace("todos", "")}")
@ -155,43 +144,44 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
// Anime Details // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val doc = getRealDoc(document) val doc = getRealDoc(document)
val content = doc.selectFirst("div.anime_container_content")!! return SAnime.create().apply {
val infos = content.selectFirst("div.anime_infos")!! setUrlWithoutDomain(doc.location())
val content = doc.selectFirst("div.anime_container_content")!!
val infos = content.selectFirst("div.anime_infos")!!
anime.title = doc.selectFirst("div.anime_container_titulo")!!.text() title = doc.selectFirst("div.anime_container_titulo")!!.text()
anime.thumbnail_url = content.selectFirst("img")!!.attr("src") thumbnail_url = content.selectFirst("img")!!.attr("src")
anime.genre = infos.getInfo("Gêneros") genre = infos.getInfo("Gêneros")
anime.author = infos.getInfo("Autor") author = infos.getInfo("Autor")
anime.artist = infos.getInfo("Estúdio") artist = infos.getInfo("Estúdio")
anime.status = parseStatus(infos.getInfo("Status")) status = parseStatus(infos.getInfo("Status"))
var desc = doc.selectFirst("div.sinopse_container_content")!!.text() + "\n" val infoItems = listOf("Ano", "Direção", "Episódios", "Temporada", "Título Alternativo")
infos.getInfo("Ano")?.let { desc += "\nAno: $it" }
infos.getInfo("Direção")?.let { desc += "\nDireção: $it" }
infos.getInfo("Episódios")?.let { desc += "\nEpisódios: $it" }
infos.getInfo("Temporada")?.let { desc += "\nTemporada: $it" }
infos.getInfo("Alternativo")?.let { desc += "\nTítulo alternativo: $it" }
anime.description = desc
return anime description = buildString {
append(doc.selectFirst("div.sinopse_container_content")!!.text() + "\n")
infoItems.forEach { item ->
infos.getInfo(item)?.let { append("\n$item: $it") }
}
}
}
} }
// Latest // =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector() override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
override fun latestUpdatesSelector(): String = "div.mContainer_content.threeItensPerContent > div.epi_loop_item" override fun latestUpdatesSelector(): String = "div.mContainer_content.threeItensPerContent > div.epi_loop_item"
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create() return SAnime.create().apply {
val img = element.selectFirst("img")!! val img = element.selectFirst("img")!!
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href")) setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.title = img.attr("title") title = img.attr("title")
anime.thumbnail_url = img.attr("src") thumbnail_url = img.attr("src")
return anime }
} }
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?page=$page") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?page=$page")
@ -205,14 +195,14 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return AnimesPage(animes, hasNextPage) return AnimesPage(animes, hasNextPage)
} }
// Settings // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply { val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality" key = PREF_QUALITY_KEY
title = "Qualidade preferida" title = PREF_QUALITY_TITLE
entries = QUALITIES entries = PREF_QUALITY_VALUES
entryValues = QUALITIES entryValues = PREF_QUALITY_VALUES
setDefaultValue(QUALITIES.last()) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val selected = newValue as String
@ -224,10 +214,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
screen.addPreference(videoQualityPref) screen.addPreference(videoQualityPref)
} }
// Filters // ============================= Utilities ==============================
override fun getFilterList(): AnimeFilterList = AnitubeFilters.filterList
// New functions
private fun getRealDoc(document: Document): Document { private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst("div.controles_ep > a[href] > i.spr.listaEP") val menu = document.selectFirst("div.controles_ep > a[href] > i.spr.listaEP")
if (menu != null) { if (menu != null) {
@ -252,10 +239,10 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val pagination = document.selectFirst("div.pagination") val pagination = document.selectFirst("div.pagination")
val items = pagination?.select("a.page-numbers") val items = pagination?.select("a.page-numbers")
if (pagination == null || items!!.size < 2) return false if (pagination == null || items!!.size < 2) return false
val currentPage: Int = pagination.selectFirst("a.page-numbers.current") val currentPage = pagination.selectFirst("a.page-numbers.current")
?.attr("href") ?.attr("href")
?.toPageNum() ?: 1 ?.toPageNum() ?: 1
val lastPage: Int = items[items.lastIndex - 1] val lastPage = items[items.lastIndex - 1]
.attr("href") .attr("href")
.toPageNum() .toPageNum()
return currentPage != lastPage return currentPage != lastPage
@ -269,47 +256,38 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} catch (e: NumberFormatException) { 1 } } catch (e: NumberFormatException) { 1 }
private fun Element.getInfo(key: String): String? { private fun Element.getInfo(key: String): String? {
val elementB: Element? = this.selectFirst("b:contains($key)") val parent = selectFirst("b:contains($key)")?.parent()
val parent = elementB?.parent() val genres = parent?.select("a")
val elementsA = parent?.select("a") val text = if (genres?.size == 0) {
val text = if (elementsA?.size == 0) { parent.ownText()
parent.text()?.replace(elementB.html(), "")?.trim()
} else { } else {
elementsA?.joinToString(", ") { it.text() } genres?.joinToString(", ") { it.text() }
} }
if (text == "") return null if (text == "") return null
return text return text
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null) val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
if (quality != null) { return sortedWith(
val newList = mutableListOf<Video>() compareByDescending { it.quality.equals(quality) },
var preferred = 0 )
for (video in this) {
if (video.quality.equals(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
} }
private fun String.toDate(): Long { private fun String.toDate(): Long {
return try { return runCatching {
DATE_FORMATTER.parse(this)?.time ?: 0L DATE_FORMATTER.parse(this)?.time ?: 0L
} catch (e: ParseException) { }.getOrNull() ?: 0L
0L
}
} }
companion object { companion object {
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
private val QUALITIES = arrayOf("SD", "HD", "FULLHD")
private val DATE_FORMATTER by lazy { SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH) } private val DATE_FORMATTER by lazy { SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH) }
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "HD"
private val PREF_QUALITY_VALUES = arrayOf("SD", "HD", "FULLHD")
} }
} }

View File

@ -7,19 +7,23 @@ import okhttp3.Response
object AnitubeExtractor { object AnitubeExtractor {
private val headers = Headers.headersOf("User-Agent", "VLC/3.0.16 LibVLC/3.0.16") private val headers = Headers.headersOf("Referer", "https://www.anitube.vip/")
fun getVideoList(response: Response): List<Video> { fun getVideoList(response: Response): List<Video> {
val doc = response.asJsoup() val doc = response.asJsoup()
val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!.attr("content") val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
.attr("content")
.replace("cdn1", "cdn3")
val type = serverUrl.split("/").get(3) val type = serverUrl.split("/").get(3)
val qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null) val qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null)
val paths = when (type) { val paths = listOf("appsd", "apphd", "appfullhd").let {
"appsd" -> mutableListOf("mobilesd", "mobilehd") if (type.endsWith("2")) {
else -> mutableListOf("sdr2", "hdr2") it.map { path -> path + "2" }
} else {
it
}
} }
paths.add("fullhdr2")
return qualities.mapIndexed { index, quality -> return qualities.mapIndexed { index, quality ->
val path = paths[index] val path = paths[index]
val url = serverUrl.replace(type, path) val url = serverUrl.replace(type, path)