fix(de/animebase): Some fixes + refactoration (#2236)

This commit is contained in:
Claudemirovsky
2023-09-22 05:19:04 -03:00
committed by GitHub
parent 4dd76e3fa1
commit 11c9369ba0
4 changed files with 247 additions and 256 deletions

View File

@ -1,13 +1,21 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
apply plugin: 'kotlinx-serialization' alias(libs.plugins.kotlin.android)
}
ext { ext {
extName = 'Anime-Base' extName = 'Anime-Base'
pkgNameSuffix = 'de.animebase' pkgNameSuffix = 'de.animebase'
extClass = '.AnimeBase' extClass = '.AnimeBase'
extVersionCode = 13 extVersionCode = 14
libVersion = '13' libVersion = '13'
} }
dependencies {
implementation(project(":lib-voe-extractor"))
implementation(project(":lib-streamwish-extractor"))
implementation(project(":lib-playlist-utils"))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,22 +1,27 @@
package eu.kanade.tachiyomi.animeextension.de.animebase package eu.kanade.tachiyomi.animeextension.de.animebase
import android.app.Application import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.de.animebase.extractors.UnpackerExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode 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.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.MediaType.Companion.toMediaType import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
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
@ -31,110 +36,181 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val lang = "de" override val lang = "de"
override val supportsLatest = false override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
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)
} }
override fun popularAnimeSelector(): String = "div.table-responsive a" // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/favorites", headers)
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeSelector() = "div.table-responsive > a"
val cookieInterceptor = client.newBuilder().addInterceptor(CookieInterceptor(baseUrl)).build()
val headers = cookieInterceptor.newCall(GET(baseUrl)).execute().request.headers override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
return GET("$baseUrl/favorites", headers = headers) setUrlWithoutDomain(element.attr("href").replace("/link/", "/anime/"))
thumbnail_url = element.selectFirst("div.thumbnail img")?.absUrl("src")
title = element.selectFirst("div.caption h3")!!.text()
} }
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeNextPageSelector() = null
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href")) // =============================== Latest ===============================
anime.thumbnail_url = element.select("div.thumbnail img").attr("src") override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/updates", headers)
anime.title = element.select("div.thumbnail div.caption h3").text()
return anime override fun latestUpdatesSelector() = "div.box-header + div.box-body > a"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = null
// =============================== Search ===============================
private val searchToken by lazy {
client.newCall(GET("$baseUrl/searching", headers)).execute()
.use { it.asJsoup() }
.selectFirst("form > input[name=_token]")!!
.attr("value")
} }
override fun popularAnimeNextPageSelector(): String? = null override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val body = FormBody.Builder()
.add("_token", searchToken)
.add("_token", searchToken)
.add("name_serie", query)
.add("jahr", "")
.build()
return POST("$baseUrl/searching", headers, body)
}
// episodes override fun searchAnimeSelector(): String = "div.col-lg-9.col-md-8 div.box-body a"
override fun episodeListSelector() = throw Exception("not used") override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun episodeListParse(response: Response): List<SEpisode> { override fun searchAnimeNextPageSelector() = null
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>() // =========================== Anime Details ============================
val episodeElement = document.select( override fun animeDetailsParse(document: Document) = SAnime.create().apply {
"div.tab-content #gersub div.panel, div.tab-content #filme div.panel button[${ setUrlWithoutDomain(document.location())
if (document.select("div.tab-content #filme div.panel button[data-dubbed=\"0\"]").isNullOrEmpty()) {
"data-dubbed=\"1\"" val boxBody = document.selectFirst("div.box-body.box-profile > center")!!
} else { title = boxBody.selectFirst("h3")!!.text()
"data-dubbed=\"0\"" thumbnail_url = boxBody.selectFirst("img")!!.absUrl("src")
}
}][data-hoster=\"1\"], div.tab-content #specials div.panel button[data-dubbed=\"0\"][data-hoster=\"1\"]", val infosDiv = document.selectFirst("div.box-body > div.col-md-9")!!
status = parseStatus(infosDiv.getInfo("Status"))
genre = infosDiv.select("strong:contains(Genre) + p > a").eachText()
.joinToString()
.takeIf(String::isNotBlank)
description = buildString {
infosDiv.getInfo("Beschreibung")?.also(::append)
infosDiv.getInfo("Originalname")?.also { append("\nOriginal name: $it") }
infosDiv.getInfo("Erscheinungsjahr")?.also { append("\nErscheinungsjahr: $it") }
}
}
private fun parseStatus(status: String?) = when (status?.orEmpty()) {
"Laufend" -> SAnime.ONGOING
"Abgeschlossen" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
private fun Element.getInfo(selector: String) =
selectFirst("strong:contains($selector) + p")?.text()?.trim()
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) =
super.episodeListParse(response).sortedWith(
compareBy(
{ it.name.startsWith("Film ") },
{ it.name.startsWith("Special ") },
{ it.episode_number },
),
).reversed()
override fun episodeListSelector() = "div.tab-content > div > div.panel"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val epname = element.selectFirst("h3")?.text() ?: "Episode 1"
val language = when (element.selectFirst("button")?.attr("data-dubbed").orEmpty()) {
"0" -> "Subbed"
else -> "Dubbed"
}
name = epname
scanlator = language
episode_number = epname.substringBefore(":").substringAfter(" ").toFloatOrNull() ?: 0F
val selectorClass = element.classNames().first { it.startsWith("episode-div") }
setUrlWithoutDomain(element.baseUri() + "?selector=div.panel.$selectorClass")
}
// ============================ Video Links =============================
private val hosterSettings by lazy {
mapOf(
"Streamwish" to "https://streamwish.to/e/",
"Voe.SX" to "https://voe.sx/e/",
"Lulustream" to "https://lulustream.com/e/",
"VTube" to "https://vtbe.to/embed-",
) )
episodeElement.forEach {
val episode = episodeFromElement(it)
episodeList.add(episode)
}
return episodeList.reversed()
} }
override fun episodeFromElement(element: Element): SEpisode { override fun videoListParse(response: Response): List<Video> {
val episode = SEpisode.create() val doc = response.use { it.asJsoup() }
val id = element.select("button[data-hoster=\"1\"]").attr("data-serieid") val selector = response.request.url.queryParameter("selector")
val epnum = element.select("button[data-hoster=\"1\"]").attr("data-folge") ?: return emptyList()
val host = element.select("button[data-hoster=\"1\"]").attr("data-hoster")
if (element.attr("data-dubbed").contains("1")) { return doc.select("$selector div.panel-body > button").toList()
if (element.attr("data-special").contains("2")) { .filter { it.text() in hosterSettings.keys }
episode.episode_number = 1F .parallelMap {
episode.name = "Film $epnum" runCatching {
episode.setUrlWithoutDomain("/episode/$id/$epnum/1/$host/2") val language = when (it.attr("data-dubbed")) {
} "0" -> "SUB"
} else { else -> "DUB"
if (element.select("button[data-hoster=\"1\"]").attr("data-special").contains("2")) { }
episode.episode_number = 1F
episode.name = "Film ${epnum.toInt() - 1}" getVideosFromHoster(it.text(), it.attr("data-streamlink"))
episode.setUrlWithoutDomain("/episode/$id/$epnum/0/$host/2") .map { video ->
} else { Video(
val season = element.select("button[data-hoster=\"1\"]").attr("data-embedcontainer") video.url,
.substringAfter("-").substringBefore("-") "$language ${video.quality}",
episode.name = "Staffel $season Folge $epnum : " + element.select("h3.panel-title").text() video.videoUrl,
.substringAfter(": ") video.headers,
.replace("<span title=\"", "").replace("<span class=\"label label-danger\">Filler!</span>", "").replace("&nbsp;", "") video.subtitleTracks,
episode.episode_number = element.select("button[data-hoster=\"1\"]").attr("data-folge").toFloat() video.audioTracks,
episode.setUrlWithoutDomain("/episode/$id/$epnum/0/$host/0") )
} }
if (element.select("button[data-hoster=\"1\"]").attr("data-special").contains("1")) { }.getOrElse { emptyList() }
episode.episode_number = 1F }.flatten().ifEmpty { throw Exception("No videos xDDDDDD") }
episode.name = "Special ${epnum.toInt() - 1}"
episode.setUrlWithoutDomain("/episode/$id/$epnum/0/$host/1")
}
}
return episode
} }
// Video Extractor private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val unpackerExtractor by lazy { UnpackerExtractor(client, headers) }
override fun videoListParse(response: Response) = private fun getVideosFromHoster(hoster: String, urlpart: String): List<Video> {
throw Exception("This source only uses StreamSB as video hoster, and StreamSB is down.") val url = hosterSettings.get(hoster)!! + urlpart
return when (hoster) {
"Streamwish" -> streamWishExtractor.videosFromUrl(url)
"Voe.SX" -> voeExtractor.videoFromUrl(url)?.let(::listOf)
"VTube", "Lulustream" -> unpackerExtractor.videosFromUrl(url, hoster)
else -> null
} ?: emptyList()
}
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val hoster = preferences.getString("preferred_sub", null) val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
if (hoster != null) { val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val newList = mutableListOf<Video>()
var preferred = 0 return sortedWith(
for (video in this) { compareBy(
if (video.quality.contains(hoster)) { { it.quality.contains(lang) },
newList.add(preferred, video) { it.quality.contains(quality) },
preferred++ { Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
} else { ),
newList.add(video) ).reversed()
}
}
return newList
}
return this
} }
override fun videoListSelector() = throw Exception("not used") override fun videoListSelector() = throw Exception("not used")
@ -143,70 +219,14 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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): SAnime {
val anime = SAnime.create()
if (!element.text().contains("PainterCrowe")) {
anime.setUrlWithoutDomain(element.attr("href"))
anime.thumbnail_url = element.select("div.thumbnail img").attr("src")
anime.title = element.select("div.caption h3").text()
} else {
throw Exception("Keine Ergebnisse gefunden")
}
return anime
}
override fun searchAnimeNextPageSelector(): String? = null
override fun searchAnimeSelector(): String = "div.col-lg-9.col-md-8 div.box-body a"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val cookieInterceptor = client.newBuilder().addInterceptor(CookieInterceptor(baseUrl)).build()
val headers = cookieInterceptor.newCall(GET(baseUrl)).execute().request.headers
val token = client.newCall(GET("$baseUrl/searching", headers = headers)).execute().asJsoup()
.select("div.box-body form input[name=\"_token\"]").attr("value")
return POST("$baseUrl/searching", headers = headers, body = "_token=$token&_token=$token&name_serie=$query&jahr=".toRequestBody("application/x-www-form-urlencoded".toMediaType()))
}
// Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = document.select("div.box-body.box-profile center img").attr("src")
anime.title = document.select("section.content-header small").text()
anime.genre = document.select("div.box-body p a span").joinToString(", ") { it.text() }
anime.description = document.select("div.box-body p.text-muted[style=\"text-align: justify;\"]").toString()
.substringAfter(";\">").substringBefore("<br")
anime.status = parseStatus(document.select("div.box-body span.label.label-info").text())
return anime
}
private fun parseStatus(status: String?) = when {
status == null -> SAnime.UNKNOWN
status.contains("Laufend", ignoreCase = true) -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
// Latest
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesSelector(): String = throw Exception("Not used")
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val subPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = "preferred_sub" key = PREF_LANG_KEY
title = "Standardmäßig Sub oder Dub?" title = PREF_LANG_TITLE
entries = arrayOf("Sub", "Dub") entries = PREF_LANG_ENTRIES
entryValues = arrayOf("SUB", "DUB") entryValues = PREF_LANG_VALUES
setDefaultValue("SUB") setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -215,7 +235,42 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
screen.addPreference(subPref)
companion object {
private const val PREF_LANG_KEY = "preferred_sub"
private const val PREF_LANG_TITLE = "Standardmäßig Sub oder Dub?"
private const val PREF_LANG_DEFAULT = "SUB"
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
private val PREF_LANG_VALUES = arrayOf("SUB", "DUB")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080p", "720p", "480p", "360p")
} }
} }

View File

@ -1,103 +0,0 @@
package eu.kanade.tachiyomi.animeextension.de.animebase
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CookieInterceptor(private val baseUrl: String) : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("bruh")
return chain.proceed(newRequest)
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
// We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
var newRequest: Request? = null
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = request.header("User-Agent")
?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63\""
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
handler.post {
webView?.clearHistory()
}
if (request.url.toString().contains("/?d=1") && request.url.toString().contains("anime-base")) {
newRequest = GET(baseUrl, request.requestHeaders.toHeaders())
latch.countDown()
}
if (request.url.toString().contains("favicon.ico") && request.url.toString().contains("anime-base")) {
newRequest = GET(baseUrl, request.requestHeaders.toHeaders())
latch.countDown()
}
if (request.url.toString().contains("/search") || request.url.toString().contains("/search?d=1")) {
newRequest = GET(request.url.toString(), request.requestHeaders.toHeaders())
latch.countDown()
}
if (request.url.toString().contains("/favorites") || request.url.toString().contains("/favorites?d=1")) {
newRequest = GET(request.url.toString(), request.requestHeaders.toHeaders())
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, headers)
}
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
// around 4 seconds but it can take more due to slow networks or server issues.
latch.await(12, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return newRequest
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.animeextension.de.animebase.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class UnpackerExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, hoster: String): List<Video> {
val doc = client.newCall(GET(url, headers)).execute()
.use { it.asJsoup() }
val script = doc.selectFirst("script:containsData(eval)")
?.data()
?.let(JsUnpacker::unpackAndCombine)
?: return emptyList()
val playlistUrl = script.substringAfter("file:\"").substringBefore('"')
return playlistUtils.extractFromHls(
playlistUrl,
referer = playlistUrl,
videoNameGen = { "$hoster - $it" },
)
}
}