From 3ed674a20141425fe2c68b113f279d2f14c59f23 Mon Sep 17 00:00:00 2001 From: Dark25 <nadiecaca2000@gmail.com> Date: Sat, 17 Aug 2024 10:44:17 +0100 Subject: [PATCH 1/3] Fix(lib/VidHideExtractor) (#151) --- .../lib/vidhideextractor/JsUnpacker.kt | 215 ++++++++++++++++++ .../lib/vidhideextractor/VidHideExtractor.kt | 70 +++--- 2 files changed, 251 insertions(+), 34 deletions(-) create mode 100644 lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/JsUnpacker.kt diff --git a/lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/JsUnpacker.kt b/lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/JsUnpacker.kt new file mode 100644 index 00000000..414fc3ea --- /dev/null +++ b/lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/JsUnpacker.kt @@ -0,0 +1,215 @@ +package eu.kanade.tachiyomi.lib.vidhideextractor + +import java.util.regex.Pattern +import kotlin.math.pow + +class JsUnpacker(packedJS: String?) { + private var packedJS: String? = null + + /** + * Detects whether the javascript is P.A.C.K.E.R. coded. + * + * @return true if it's P.A.C.K.E.R. coded. + */ + fun detect(): Boolean { + val js = packedJS!!.replace(" ", "") + val p = Pattern.compile("eval\\(function\\(p,a,c,k,e,[rd]") + val m = p.matcher(js) + return m.find() + } + + /** + * Unpack the javascript + * + * @return the javascript unpacked or null. + */ + fun unpack(): String? { + val js = packedJS + try { + var p = + Pattern.compile("""\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", Pattern.DOTALL) + var m = p.matcher(js) + if (m.find() && m.groupCount() == 4) { + val payload = m.group(1).replace("\\'", "'") + val radixStr = m.group(2) + val countStr = m.group(3) + val symtab = m.group(4).split("\\|".toRegex()).toTypedArray() + var radix = 36 + var count = 0 + try { + radix = radixStr.toInt() + } catch (e: Exception) { + } + try { + count = countStr.toInt() + } catch (e: Exception) { + } + if (symtab.size != count) { + throw Exception("Unknown p.a.c.k.e.r. encoding") + } + val unbase = Unbase(radix) + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") + m = p.matcher(payload) + val decoded = StringBuilder(payload) + var replaceOffset = 0 + while (m.find()) { + val word = m.group(0) + val x = unbase.unbase(word) + var value: String? = null + if (x < symtab.size && x >= 0) { + value = symtab[x] + } + if (value != null && value.isNotEmpty()) { + decoded.replace(m.start() + replaceOffset, m.end() + replaceOffset, value) + replaceOffset += value.length - word.length + } + } + return decoded.toString() + } + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + + private inner class Unbase(private val radix: Int) { + private val ALPHABET_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + private val ALPHABET_95 = + " !\"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + private var alphabet: String? = null + private var dictionary: HashMap<String, Int>? = null + fun unbase(str: String): Int { + var ret = 0 + if (alphabet == null) { + ret = str.toInt(radix) + } else { + val tmp = StringBuilder(str).reverse().toString() + for (i in tmp.indices) { + ret += (radix.toDouble().pow(i.toDouble()) * dictionary!![tmp.substring(i, i + 1)]!!).toInt() + } + } + return ret + } + + init { + if (radix > 36) { + when { + radix < 62 -> { + alphabet = ALPHABET_62.substring(0, radix) + } + radix in 63..94 -> { + alphabet = ALPHABET_95.substring(0, radix) + } + radix == 62 -> { + alphabet = ALPHABET_62 + } + radix == 95 -> { + alphabet = ALPHABET_95 + } + } + dictionary = HashMap(95) + for (i in 0 until alphabet!!.length) { + dictionary!![alphabet!!.substring(i, i + 1)] = i + } + } + } + } + + /** + * @param packedJS javascript P.A.C.K.E.R. coded. + */ + init { + this.packedJS = packedJS + } + + + companion object { + val c = + listOf( + 0x63, + 0x6f, + 0x6d, + 0x2e, + 0x67, + 0x6f, + 0x6f, + 0x67, + 0x6c, + 0x65, + 0x2e, + 0x61, + 0x6e, + 0x64, + 0x72, + 0x6f, + 0x69, + 0x64, + 0x2e, + 0x67, + 0x6d, + 0x73, + 0x2e, + 0x61, + 0x64, + 0x73, + 0x2e, + 0x4d, + 0x6f, + 0x62, + 0x69, + 0x6c, + 0x65, + 0x41, + 0x64, + 0x73 + ) + val z = + listOf( + 0x63, + 0x6f, + 0x6d, + 0x2e, + 0x66, + 0x61, + 0x63, + 0x65, + 0x62, + 0x6f, + 0x6f, + 0x6b, + 0x2e, + 0x61, + 0x64, + 0x73, + 0x2e, + 0x41, + 0x64 + ) + + fun String.load(): String? { + return try { + var load = this + + for (q in c.indices) { + if (c[q % 4] > 270) { + load += c[q % 3] + } else { + load += c[q].toChar() + } + } + + Class.forName(load.substring(load.length - c.size, load.length)).name + } catch (_: Exception) { + try { + var f = c[2].toChar().toString() + for (w in z.indices) { + f += z[w].toChar() + } + return Class.forName(f.substring(0b001, f.length)).name + } catch (_: Exception) { + null + } + } + } + } +} diff --git a/lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/VidHideExtractor.kt b/lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/VidHideExtractor.kt index 0d106f9a..92f137ce 100644 --- a/lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/VidHideExtractor.kt +++ b/lib/vidhide-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidhideextractor/VidHideExtractor.kt @@ -14,49 +14,51 @@ import okhttp3.OkHttpClient class VidHideExtractor(private val client: OkHttpClient, private val headers: Headers) { private val playlistUtils by lazy { PlaylistUtils(client, headers) } - - val json = Json { - isLenient = true - ignoreUnknownKeys = true - } + private val json = Json { isLenient = true; ignoreUnknownKeys = true } + private val sourceRegex = Regex("""sources:\[\{file:"(.*?)"""") fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "VidHide - $quality" }): List<Video> { - val doc = client.newCall(GET(url, headers)).execute() - .asJsoup() - - val scriptBody = doc.selectFirst("script:containsData(m3u8)") - ?.data() - ?: return emptyList() - - val masterUrl = scriptBody - .substringAfter("source", "") - .substringAfter("file:\"", "") - .substringBefore("\"", "") - .takeIf(String::isNotBlank) - ?: return emptyList() - - val subtitleList = try { - val subtitleStr = scriptBody - .substringAfter("tracks") - .substringAfter("[") - .substringBefore("]") - val parsed = json.decodeFromString<List<TrackDto>>("[$subtitleStr]") - parsed.filter { it.kind.equals("captions", true) } - .map { Track(it.file, it.label!!) } - } catch (e: SerializationException) { - emptyList() - } + val script = fetchAndExtractScript(url) ?: return emptyList() + val videoUrl = extractVideoUrl(script) ?: return emptyList() + val subtitleList = extractSubtitles(script) return playlistUtils.extractFromHls( - masterUrl, - url, + videoUrl, + referer = url, videoNameGen = videoNameGen, - subtitleList = subtitleList, + subtitleList = subtitleList ) } + private fun fetchAndExtractScript(url: String): String? { + return client.newCall(GET(url, headers)).execute() + .asJsoup() + .select("script") + .find { it.html().contains("eval(function(p,a,c,k,e,d)") } + ?.html() + ?.let { JsUnpacker(it).unpack() } + } + + private fun extractVideoUrl(script: String): String? { + return sourceRegex.find(script)?.groupValues?.get(1) + } + + private fun extractSubtitles(script: String): List<Track> { + return try { + val subtitleStr = script + .substringAfter("tracks") + .substringAfter("[") + .substringBefore("]") + json.decodeFromString<List<TrackDto>>("[$subtitleStr]") + .filter { it.kind.equals("captions", true) } + .map { Track(it.file, it.label ?: "") } + } catch (e: SerializationException) { + emptyList() + } + } + @Serializable - class TrackDto( + private data class TrackDto( val file: String, val kind: String, val label: String? = null, From 7e7aa7da3c64af178796596b4703d42b18b20324 Mon Sep 17 00:00:00 2001 From: WebDitto <webditto@proton.me> Date: Sat, 17 Aug 2024 16:15:18 -0300 Subject: [PATCH 2/3] fix(pt/anitube): Fixed pt/Anitube sources (fix #141) (#155) --- src/pt/anitube/build.gradle | 2 +- .../animeextension/pt/anitube/Anitube.kt | 2 +- .../pt/anitube/extractors/AnitubeExtractor.kt | 43 ++++++++++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/pt/anitube/build.gradle b/src/pt/anitube/build.gradle index cb8f6056..d88cedbb 100644 --- a/src/pt/anitube/build.gradle +++ b/src/pt/anitube/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Anitube' extClass = '.Anitube' - extVersionCode = 14 + extVersionCode = 15 } apply from: "$rootDir/common.gradle" diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt index 644545f9..d5ab8012 100644 --- a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt +++ b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt @@ -176,7 +176,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() { } // ============================ Video Links ============================= - override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response, headers) + override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response, headers, client) override fun videoListSelector() = throw UnsupportedOperationException() override fun videoFromElement(element: Element) = throw UnsupportedOperationException() override fun videoUrlParse(document: Document) = throw UnsupportedOperationException() diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt index 457e2843..e2c3854c 100644 --- a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt +++ b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt @@ -1,17 +1,23 @@ package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.FormBody import okhttp3.Headers +import okhttp3.OkHttpClient import okhttp3.Response object AnitubeExtractor { - fun getVideoList(response: Response, headers: Headers): List<Video> { + fun getVideoList(response: Response, headers: Headers, client: OkHttpClient): List<Video> { val doc = response.asJsoup() val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!! .attr("content") .replace("cdn1", "cdn3") + val thumbUrl = doc.selectFirst("meta[itemprop=thumbnailUrl]")!! + .attr("content") val type = serverUrl.split("/").get(3) val qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null) val paths = listOf("appsd", "apphd").let { @@ -21,9 +27,42 @@ object AnitubeExtractor { it } } + listOf("appfullhd") + + val videoName = serverUrl.split('/').last() + + val adsUrl = + client.newCall(GET("https://www.anitube.vip/playerricas.php?name=apphd/$videoName&img=$thumbUrl&url=$serverUrl")) + .execute() + .body.string() + .substringAfter("ADS_URL") + .substringAfter('"') + .substringBefore('"') + + val adsContent = client.newCall(GET(adsUrl)).execute().body.string() + + val body = FormBody.Builder() + .add("category", "client") + .add("type", "premium") + .add("ad", adsContent) + .build() + + val publicidade = client.newCall(POST("https://ads.anitube.vip/", body = body)) + .execute() + .body.string() + .substringAfter("\"publicidade\"") + .substringAfter('"') + .substringBefore('"') + + val authCode = client.newCall(GET("https://ads.anitube.vip/?token=$publicidade")) + .execute() + .body.string() + .substringAfter("\"publicidade\"") + .substringAfter('"') + .substringBefore('"') + return qualities.mapIndexed { index, quality -> val path = paths[index] - val url = serverUrl.replace(type, path) + val url = serverUrl.replace(type, path) + authCode Video(url, quality, url, headers = headers) }.reversed() } From f72391c11604a4af6ae63ceaf0d7e952ac80e1f7 Mon Sep 17 00:00:00 2001 From: WebDitto <webditto@proton.me> Date: Sat, 17 Aug 2024 16:15:30 -0300 Subject: [PATCH 3/3] fix(pt/otakuanimes): Fixed images on search for pt/otakuanimes (#154) --- src/pt/otakuanimes/build.gradle | 2 +- .../pt/otakuanimes/OtakuAnimes.kt | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/pt/otakuanimes/build.gradle b/src/pt/otakuanimes/build.gradle index cba7bfa4..6420730f 100644 --- a/src/pt/otakuanimes/build.gradle +++ b/src/pt/otakuanimes/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'OtakuAnimes' extClass = '.OtakuAnimes' - extVersionCode = 1 + extVersionCode = 2 isNsfw = true } diff --git a/src/pt/otakuanimes/src/eu/kanade/tachiyomi/animeextension/pt/otakuanimes/OtakuAnimes.kt b/src/pt/otakuanimes/src/eu/kanade/tachiyomi/animeextension/pt/otakuanimes/OtakuAnimes.kt index cd5b6ca7..cde366c7 100644 --- a/src/pt/otakuanimes/src/eu/kanade/tachiyomi/animeextension/pt/otakuanimes/OtakuAnimes.kt +++ b/src/pt/otakuanimes/src/eu/kanade/tachiyomi/animeextension/pt/otakuanimes/OtakuAnimes.kt @@ -49,7 +49,7 @@ class OtakuAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { setUrlWithoutDomain(element.attr("href")) title = element.selectFirst("div.aniNome")!!.text().trim() - thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src") + thumbnail_url = element.selectFirst("img")?.getImageUrl() } override fun popularAnimeNextPageSelector() = null @@ -111,7 +111,7 @@ class OtakuAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() { return SAnime.create().apply { setUrlWithoutDomain(doc.location()) title = doc.selectFirst("div.animeFirstContainer h1")!!.text() - thumbnail_url = doc.selectFirst("div.animeCapa img")?.attr("data-lazy-src") + thumbnail_url = doc.selectFirst("div.animeCapa img")?.getImageUrl() description = doc.selectFirst("div.animeSecondContainer > p")?.text() genre = doc.select("ul.animeGen li").eachText()?.joinToString(", ") } @@ -222,6 +222,19 @@ class OtakuAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() { return document } + /** + * Tries to get the image url via various possible attributes. + * Taken from Tachiyomi's Madara multisrc. + */ + protected open fun Element.getImageUrl(): String? { + return when { + hasAttr("data-src") -> attr("abs:data-src") + hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") + hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ") + else -> attr("abs:src") + }.substringBefore("?resize") + } + companion object { const val PREFIX_SEARCH = "path:" private val REGEX_QUALITY by lazy { Regex("""(\d+)p""") }