fix(src/es): JkAnime fixes (#504)

* JkAnime fixes

Closes #355

* Update StreamWishExtractor.kt
This commit is contained in:
imper1aldev 2025-01-09 05:34:54 -06:00 committed by GitHub
parent 48f1bac534
commit f9650081e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 101 additions and 27 deletions

View file

@ -1,15 +1,20 @@
package eu.kanade.tachiyomi.lib.streamwishextractor package eu.kanade.tachiyomi.lib.streamwishextractor
import dev.datlag.jsunpacker.JsUnpacker import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) { class StreamWishExtractor(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 }
fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "$prefix - $it" } fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "$prefix - $it" }
@ -32,7 +37,9 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
?.takeIf(String::isNotBlank) ?.takeIf(String::isNotBlank)
?: return emptyList() ?: return emptyList()
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen) val subtitleList = extractSubtitles(scriptBody)
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen, subtitleList = subtitleList)
} }
private fun getEmbedUrl(url: String): String { private fun getEmbedUrl(url: String): String {
@ -43,4 +50,21 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
url url
} }
} }
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
private data class TrackDto(val file: String, val kind: String, val label: String? = null)
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Jkanime' extName = 'Jkanime'
extClass = '.Jkanime' extClass = '.Jkanime'
extVersionCode = 25 extVersionCode = 26
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
@ -14,4 +14,6 @@ dependencies {
implementation(project(':lib:filemoon-extractor')) implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamtape-extractor')) implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:voe-extractor')) implementation(project(':lib:voe-extractor'))
implementation(project(':lib:vidhide-extractor'))
implementation 'org.mozilla:rhino:1.7.14'
} }

View file

@ -20,16 +20,21 @@ import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
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 org.mozilla.javascript.Context
import org.mozilla.javascript.Scriptable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -43,6 +48,13 @@ class Jkanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true override val supportsLatest = true
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
coerceInputValues = true
}
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)
} }
@ -172,6 +184,7 @@ class Jkanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) } private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val mixDropExtractor by lazy { MixDropExtractor(client) } private val mixDropExtractor by lazy { MixDropExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) } private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
private val jkanimeExtractor by lazy { JkanimeExtractor(client) } private val jkanimeExtractor by lazy { JkanimeExtractor(client) }
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
@ -180,12 +193,12 @@ class Jkanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
when { when {
"ok" in url -> okruExtractor.videosFromUrl(url, "$lang ") "ok" in url -> okruExtractor.videosFromUrl(url, "$lang ")
"voe" in url -> voeExtractor.videosFromUrl(url, "$lang ") "voe" in url -> voeExtractor.videosFromUrl(url, "$lang ")
"filemoon" in url || "moonplayer" in url -> filemoonExtractor.videosFromUrl(url, "$lang Filemoon:") arrayOf("filemoon", "filemooon", "moon").any(url) -> filemoonExtractor.videosFromUrl(url, "$lang Filemoon:")
"streamtape" in url || "stp" in url || "stape" in url -> listOf(streamTapeExtractor.videoFromUrl(url, quality = "$lang StreamTape")!!) arrayOf("streamtape", "stp", "stape").any(url) -> listOf(streamTapeExtractor.videoFromUrl(url, quality = "$lang StreamTape")!!)
"mp4upload" in url -> mp4uploadExtractor.videosFromUrl(url, prefix = "$lang ", headers = headers) arrayOf("mp4upload").any(url) -> mp4uploadExtractor.videosFromUrl(url, prefix = "$lang ", headers = headers)
"mixdrop" in url || "mdbekjwqa" in url -> mixDropExtractor.videosFromUrl(url, prefix = "$lang ") arrayOf("vidhide", "filelions.top", "vid.").any(url) -> vidHideExtractor.videosFromUrl(url) { "$lang VidHide:$it" }
"sfastwish" in url || "wishembed" in url || "streamwish" in url || "strwish" in url || "wish" in url arrayOf("mixdrop", "mdbekjwqa").any(url) -> mixDropExtractor.videosFromUrl(url, prefix = "$lang ")
-> streamWishExtractor.videosFromUrl(url, videoNameGen = { "$lang StreamWish:$it" }) arrayOf("wishembed", "streamwish", "strwish", "wish").any(url) -> streamWishExtractor.videosFromUrl(url, videoNameGen = { "$lang StreamWish:$it" })
"stream/jkmedia" in url -> jkanimeExtractor.getDesukaFromUrl(url, "$lang ") "stream/jkmedia" in url -> jkanimeExtractor.getDesukaFromUrl(url, "$lang ")
"um2.php" in url -> jkanimeExtractor.getNozomiFromUrl(url, "$lang ") "um2.php" in url -> jkanimeExtractor.getNozomiFromUrl(url, "$lang ")
"um.php" in url -> jkanimeExtractor.getDesuFromUrl(url, "$lang ") "um.php" in url -> jkanimeExtractor.getDesuFromUrl(url, "$lang ")
@ -277,23 +290,56 @@ class Jkanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
anime anime
} }
} else { // is filtered } else { // is filtered
document.select(".card.mb-3.custom_item2").map { animeData -> document.selectFirst("script:containsData(var animes =)")?.data()?.let { script ->
latestUpdatesFromElement(animeData) parseJavaScriptArray(decodeUnicode(script.substringAfter("var animes = ").substringBefore("];")) + "]")?.let { jsonData ->
} json.decodeFromString<List<SearchAnimeDto>>(jsonData).map {
SAnime.create().apply {
title = it.title ?: ""
thumbnail_url = it.image
setUrlWithoutDomain("$baseUrl/${it.slug}")
}
}
}
} ?: emptyList()
} }
return AnimesPage(animeList, hasNextPage) return AnimesPage(animeList, hasNextPage)
} }
private fun parseJavaScriptArray(jsCode: String): String? {
return try {
val cx = Context.enter()
cx.optimizationLevel = -1
val scope: Scriptable = cx.initStandardObjects()
val script = """
var animes = $jsCode;
JSON.stringify(animes);
""".trimIndent()
cx.evaluateString(scope, script, "script", 1, null) as String
} catch (e: Exception) {
null
} finally {
Context.exit()
}
}
private fun decodeUnicode(input: String): String {
val unicodePattern = Regex("""\\u([0-9a-fA-F]{4})""")
return unicodePattern.replace(input) { match ->
val charCode = match.groupValues[1].toInt(16)
charCode.toChar().toString()
}
}
override fun searchAnimeFromElement(element: Element): SAnime = throw UnsupportedOperationException() override fun searchAnimeFromElement(element: Element): SAnime = throw UnsupportedOperationException()
override fun searchAnimeNextPageSelector(): String = throw UnsupportedOperationException() override fun searchAnimeNextPageSelector(): String = throw UnsupportedOperationException()
override fun searchAnimeSelector(): String = throw UnsupportedOperationException() override fun searchAnimeSelector(): String = throw UnsupportedOperationException()
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create() val anime = SAnime.create()
anime.thumbnail_url = document.selectFirst("div.col-lg-3 div.anime__details__pic.set-bg")!!.attr("data-setbg") anime.thumbnail_url = document.selectFirst(".anime__details__content .set-bg")?.attr("data-setbg")
anime.title = document.selectFirst("div.anime__details__text div.anime__details__title h3")!!.text() anime.title = document.selectFirst(".anime__details__title h3")?.text() ?: ""
anime.description = document.selectFirst("div.col-lg-9 div.anime__details__text p")!!.ownText() anime.description = document.selectFirst(".sinopsis")?.ownText()
document.select("div.row div.col-lg-6.col-md-6 ul li").forEach { animeData -> document.select(".aninfo li").forEach { animeData ->
val data = animeData.select("span").text() val data = animeData.select("span").text()
if (data.contains("Genero")) { if (data.contains("Genero")) {
anime.genre = animeData.select("a").joinToString { it.text() } anime.genre = animeData.select("a").joinToString { it.text() }
@ -318,20 +364,13 @@ class Jkanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun latestUpdatesNextPageSelector(): String = "div.container div.navigation a.text.nav-next"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select(".custom_thumb2 > a").attr("abs:href"))
anime.title = element.select(".card-title > a").text()
anime.thumbnail_url = element.select(".custom_thumb2 a img").attr("abs:src")
anime.description = element.select(".synopsis").text()
return anime
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/directorio/$page/desc/") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/directorio/$page/desc/")
override fun latestUpdatesSelector(): String = "div.card.mb-3.custom_item2" override fun latestUpdatesParse(response: Response): AnimesPage = searchAnimeParse(response)
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto no incluye todos los filtros"), AnimeFilter.Header("La busqueda por texto no incluye todos los filtros"),
@ -459,6 +498,8 @@ class Jkanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private class Tags(name: String) : AnimeFilter.Text(name) private class Tags(name: String) : AnimeFilter.Text(name)
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) : private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
@ -521,4 +562,11 @@ class Jkanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val lang: Long? = null, val lang: Long? = null,
val slug: String? = null, val slug: String? = null,
) )
@Serializable
data class SearchAnimeDto(
@SerialName("title") var title: String? = null,
@SerialName("image") var image: String? = null,
@SerialName("slug") var slug: String? = null,
)
} }