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'
pkgNameSuffix = 'pt.anitube'
extClass = '.Anitube'
extVersionCode = 8
extVersionCode = 9
libVersion = '13'
}
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.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -43,27 +37,25 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder(): Headers.Builder = Headers.Builder()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl)
.add("Accept-Language", ACCEPT_LANGUAGE)
// Popular
// ============================== Popular ===============================
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 popularAnimeFromElement(element: Element): SAnime {
val anime: SAnime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!!
anime.title = img.attr("title")
anime.thumbnail_url = img.attr("src")
return anime
return SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!!
title = img.attr("title")
thumbnail_url = img.attr("src")
}
}
override fun popularAnimeNextPageSelector(): String = "a.page-numbers:contains(Próximo)"
@ -77,17 +69,19 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return AnimesPage(animes, hasNextPage)
}
// Episodes
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "div.animepag_episodios_container > div.animepag_episodios_item > a"
private fun getAllEps(response: Response): List<SEpisode> {
val doc = if (response.request.url.toString().contains("/video/")) {
getRealDoc(response.asJsoup())
} else { response.asJsoup() }
val doc = response.asJsoup().let {
if (response.request.url.toString().contains("/video/")) {
getRealDoc(it)
} else { it }
}
val epElementList = doc.select(episodeListSelector())
val epList = mutableListOf<SEpisode>()
epList.addAll(epElementList.map { episodeFromElement(it) })
epList.addAll(epElementList.map(::episodeFromElement))
if (hasNextPage(doc)) {
val next = doc.selectFirst(popularAnimeNextPageSelector())!!.attr("href")
val request = GET(baseUrl + next)
@ -96,31 +90,34 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
return epList
}
override fun episodeListParse(response: Response): List<SEpisode> {
return getAllEps(response).reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(element.attr("href"))
episode.episode_number = try {
element.selectFirst("div.animepag_episodios_item_views")!!
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
episode_number = runCatching {
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()
.substringAfter(" ").toFloat()
} 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
.toDate()
}
}
// Video links
// ============================ Video Links =============================
override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response)
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = 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 searchAnimeNextPageSelector() = throw Exception("not used")
@ -128,23 +125,15 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response)
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val params = AnitubeFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
override fun getFilterList(): AnimeFilterList = AnitubeFilters.filterList
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: AnitubeFilters.FilterSearchParams): Request {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isBlank()) {
val season = filters.season
val genre = filters.genre
val year = filters.year
val char = filters.initialChar
val params = AnitubeFilters.getSearchParameters(filters)
val season = params.season
val genre = params.genre
val year = params.year
val char = params.initialChar
when {
!season.isBlank() -> GET("$baseUrl/temporada/$season/$year")
!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 {
val anime = SAnime.create()
val doc = getRealDoc(document)
val content = doc.selectFirst("div.anime_container_content")!!
val infos = content.selectFirst("div.anime_infos")!!
return SAnime.create().apply {
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()
anime.thumbnail_url = content.selectFirst("img")!!.attr("src")
anime.genre = infos.getInfo("Gêneros")
anime.author = infos.getInfo("Autor")
anime.artist = infos.getInfo("Estúdio")
anime.status = parseStatus(infos.getInfo("Status"))
title = doc.selectFirst("div.anime_container_titulo")!!.text()
thumbnail_url = content.selectFirst("img")!!.attr("src")
genre = infos.getInfo("Gêneros")
author = infos.getInfo("Autor")
artist = infos.getInfo("Estúdio")
status = parseStatus(infos.getInfo("Status"))
var desc = doc.selectFirst("div.sinopse_container_content")!!.text() + "\n"
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
val infoItems = listOf("Ano", "Direção", "Episódios", "Temporada", "Título Alternativo")
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 latestUpdatesSelector(): String = "div.mContainer_content.threeItensPerContent > div.epi_loop_item"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
val img = element.selectFirst("img")!!
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.title = img.attr("title")
anime.thumbnail_url = img.attr("src")
return anime
return SAnime.create().apply {
val img = element.selectFirst("img")!!
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
title = img.attr("title")
thumbnail_url = img.attr("src")
}
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?page=$page")
@ -205,14 +195,14 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return AnimesPage(animes, hasNextPage)
}
// Settings
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Qualidade preferida"
entries = QUALITIES
entryValues = QUALITIES
setDefaultValue(QUALITIES.last())
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_VALUES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
@ -224,10 +214,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
screen.addPreference(videoQualityPref)
}
// Filters
override fun getFilterList(): AnimeFilterList = AnitubeFilters.filterList
// New functions
// ============================= Utilities ==============================
private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst("div.controles_ep > a[href] > i.spr.listaEP")
if (menu != null) {
@ -252,10 +239,10 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val pagination = document.selectFirst("div.pagination")
val items = pagination?.select("a.page-numbers")
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")
?.toPageNum() ?: 1
val lastPage: Int = items[items.lastIndex - 1]
val lastPage = items[items.lastIndex - 1]
.attr("href")
.toPageNum()
return currentPage != lastPage
@ -269,47 +256,38 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} catch (e: NumberFormatException) { 1 }
private fun Element.getInfo(key: String): String? {
val elementB: Element? = this.selectFirst("b:contains($key)")
val parent = elementB?.parent()
val elementsA = parent?.select("a")
val text = if (elementsA?.size == 0) {
parent.text()?.replace(elementB.html(), "")?.trim()
val parent = selectFirst("b:contains($key)")?.parent()
val genres = parent?.select("a")
val text = if (genres?.size == 0) {
parent.ownText()
} else {
elementsA?.joinToString(", ") { it.text() }
genres?.joinToString(", ") { it.text() }
}
if (text == "") return null
return text
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
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
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareByDescending { it.quality.equals(quality) },
)
}
private fun String.toDate(): Long {
return try {
return runCatching {
DATE_FORMATTER.parse(this)?.time ?: 0L
} catch (e: ParseException) {
0L
}
}.getOrNull() ?: 0L
}
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 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 {
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> {
val doc = response.asJsoup()
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 qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null)
val paths = when (type) {
"appsd" -> mutableListOf("mobilesd", "mobilehd")
else -> mutableListOf("sdr2", "hdr2")
val paths = listOf("appsd", "apphd", "appfullhd").let {
if (type.endsWith("2")) {
it.map { path -> path + "2" }
} else {
it
}
}
paths.add("fullhdr2")
return qualities.mapIndexed { index, quality ->
val path = paths[index]
val url = serverUrl.replace(type, path)