Merge branch 'Kohi-den:main' into main
This commit is contained in:
commit
3d744de4a7
7 changed files with 310 additions and 41 deletions
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,49 +14,51 @@ import okhttp3.OkHttpClient
|
||||||
class VidHideExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class VidHideExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
private val json = Json { isLenient = true; ignoreUnknownKeys = true }
|
||||||
val json = Json {
|
private val sourceRegex = Regex("""sources:\[\{file:"(.*?)"""")
|
||||||
isLenient = true
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "VidHide - $quality" }): List<Video> {
|
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "VidHide - $quality" }): List<Video> {
|
||||||
val doc = client.newCall(GET(url, headers)).execute()
|
val script = fetchAndExtractScript(url) ?: return emptyList()
|
||||||
.asJsoup()
|
val videoUrl = extractVideoUrl(script) ?: return emptyList()
|
||||||
|
val subtitleList = extractSubtitles(script)
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
masterUrl,
|
videoUrl,
|
||||||
url,
|
referer = url,
|
||||||
videoNameGen = videoNameGen,
|
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
|
@Serializable
|
||||||
class TrackDto(
|
private data class TrackDto(
|
||||||
val file: String,
|
val file: String,
|
||||||
val kind: String,
|
val kind: String,
|
||||||
val label: String? = null,
|
val label: String? = null,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anitube'
|
extName = 'Anitube'
|
||||||
extClass = '.Anitube'
|
extClass = '.Anitube'
|
||||||
extVersionCode = 14
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -176,7 +176,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
// ============================ 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 videoListSelector() = throw UnsupportedOperationException()
|
||||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors
|
package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
object AnitubeExtractor {
|
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 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]")!!
|
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
|
||||||
.attr("content")
|
.attr("content")
|
||||||
.replace("cdn1", "cdn3")
|
.replace("cdn1", "cdn3")
|
||||||
|
val thumbUrl = doc.selectFirst("meta[itemprop=thumbnailUrl]")!!
|
||||||
|
.attr("content")
|
||||||
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 = listOf("appsd", "apphd").let {
|
val paths = listOf("appsd", "apphd").let {
|
||||||
|
@ -21,9 +27,42 @@ object AnitubeExtractor {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
} + listOf("appfullhd")
|
} + 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 ->
|
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) + authCode
|
||||||
Video(url, quality, url, headers = headers)
|
Video(url, quality, url, headers = headers)
|
||||||
}.reversed()
|
}.reversed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'OtakuAnimes'
|
extName = 'OtakuAnimes'
|
||||||
extClass = '.OtakuAnimes'
|
extClass = '.OtakuAnimes'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ class OtakuAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
title = element.selectFirst("div.aniNome")!!.text().trim()
|
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
|
override fun popularAnimeNextPageSelector() = null
|
||||||
|
@ -111,7 +111,7 @@ class OtakuAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
return SAnime.create().apply {
|
return SAnime.create().apply {
|
||||||
setUrlWithoutDomain(doc.location())
|
setUrlWithoutDomain(doc.location())
|
||||||
title = doc.selectFirst("div.animeFirstContainer h1")!!.text()
|
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()
|
description = doc.selectFirst("div.animeSecondContainer > p")?.text()
|
||||||
genre = doc.select("ul.animeGen li").eachText()?.joinToString(", ")
|
genre = doc.select("ul.animeGen li").eachText()?.joinToString(", ")
|
||||||
}
|
}
|
||||||
|
@ -222,6 +222,19 @@ class OtakuAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
return document
|
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 {
|
companion object {
|
||||||
const val PREFIX_SEARCH = "path:"
|
const val PREFIX_SEARCH = "path:"
|
||||||
private val REGEX_QUALITY by lazy { Regex("""(\d+)p""") }
|
private val REGEX_QUALITY by lazy { Regex("""(\d+)p""") }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue