fix(de/Cinemathek): Fix popular & latest updates page, add more extractors (#1651)

This commit is contained in:
Claudemirovsky
2023-05-30 09:48:06 +00:00
committed by GitHub
parent 864128bb47
commit 26a6f43dce
8 changed files with 121 additions and 268 deletions

View File

@ -1,3 +1,5 @@
dependencies { dependencies {
implementation(project(':lib-dood-extractor'))
implementation(project(':lib-streamsb-extractor')) implementation(project(':lib-streamsb-extractor'))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
} }

View File

@ -4,14 +4,14 @@ import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors.FilemoonExtractor import eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors.FilemoonExtractor
import eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors.StreamHideExtractor
import eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors.StreamlareExtractor import eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors.StreamlareExtractor
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET 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.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -26,12 +26,13 @@ class Cinemathek : DooPlay(
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeSelector(): String = "article.movies div.poster" override fun popularAnimeSelector(): String = "article.movies div.poster"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/movies/page/$page/") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/filme/page/$page/")
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector() override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "#nextpagination" override fun latestUpdatesNextPageSelector(): String = "#nextpagination"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/episoden/page/$page")
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override val additionalInfoItems = listOf("Original", "Start", "Staffeln", "letzte", "Episoden") override val additionalInfoItems = listOf("Original", "Start", "Staffeln", "letzte", "Episoden")
@ -48,28 +49,24 @@ class Cinemathek : DooPlay(
val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!! val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
return players.mapNotNull { player -> return players.mapNotNull { player ->
runCatching { runCatching {
val url = getPlayerUrl(player) val url = getPlayerUrl(player).ifEmpty { return@mapNotNull null }
getPlayerVideos(url, hosterSelection) getPlayerVideos(url, hosterSelection)
}.getOrDefault(emptyList<Video>()) }.getOrNull()
}.flatten() }.flatten()
} }
private fun getPlayerUrl(player: Element): String { private fun getPlayerUrl(player: Element): String {
val body = FormBody.Builder() val type = player.attr("data-type")
.add("action", "doo_player_ajax") val id = player.attr("data-post")
.add("post", player.attr("data-post")) val num = player.attr("data-nume")
.add("nume", player.attr("data-nume")) if (num == "trailer") return ""
.add("type", player.attr("data-type")) return client.newCall(GET("$baseUrl/wp-json/dooplayer/v2/$id/$type/$num"))
.build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
.execute() .execute()
.use { response -> .body.string()
response.body.string()
.substringAfter("\"embed_url\":\"") .substringAfter("\"embed_url\":\"")
.substringBefore("\",") .substringBefore("\",")
.replace("\\", "") .replace("\\", "")
} }
}
private fun getPlayerVideos(url: String, hosterSelection: Set<String>): List<Video>? { private fun getPlayerVideos(url: String, hosterSelection: Set<String>): List<Video>? {
return when { return when {
@ -77,10 +74,16 @@ class Cinemathek : DooPlay(
StreamlareExtractor(client).videosFromUrl(url) StreamlareExtractor(client).videosFromUrl(url)
} }
url.contains("https://streamsb") && hosterSelection.contains("streamsb") -> { url.contains("https://streamsb") && hosterSelection.contains("streamsb") -> {
StreamSBExtractor(client).videosFromUrl(url, headers = headers, common = false) StreamSBExtractor(client).videosFromUrl(url, headers = headers)
} }
url.contains("https://filemoon") && hosterSelection.contains("fmoon") -> { url.contains("https://filemoon") && hosterSelection.contains("fmoon") -> {
FilemoonExtractor(client).videoFromUrl(url) FilemoonExtractor(client).videosFromUrl(url)
}
url.contains("https://dooood") && hosterSelection.contains("dood") -> {
DoodExtractor(client).videosFromUrl(url)
}
url.contains("https://streamhide") && hosterSelection.contains("shide") -> {
StreamHideExtractor(client).videosFromUrl(url)
} }
else -> null else -> null
} }
@ -155,13 +158,13 @@ class Cinemathek : DooPlay(
private const val PREF_HOSTER_KEY = "preferred_hoster" private const val PREF_HOSTER_KEY = "preferred_hoster"
private const val PREF_HOSTER_TITLE = "Standard-Hoster" private const val PREF_HOSTER_TITLE = "Standard-Hoster"
private const val PREF_HOSTER_DEFAULT = "https://viewsb.com" private const val PREF_HOSTER_DEFAULT = "https://viewsb.com"
private val PREF_HOSTER_ENTRIES = arrayOf("Streamlare", "StreamSB", "Filemoon") private val PREF_HOSTER_ENTRIES = arrayOf("Streamlare", "StreamSB", "Filemoon", "DoodStream", "StreamHide")
private val PREF_HOSTER_VALUES = arrayOf("https://streamlare", "https://viewsb.com", "https://filemoon") private val PREF_HOSTER_VALUES = arrayOf("https://streamlare", "https://viewsb.com", "https://filemoon", "https://dooood", "https://streamhide")
private const val PREF_HOSTER_SELECTION_KEY = "hoster_selection" private const val PREF_HOSTER_SELECTION_KEY = "hoster_selection"
private const val PREF_HOSTER_SELECTION_TITLE = "Hoster auswählen" private const val PREF_HOSTER_SELECTION_TITLE = "Hoster auswählen"
private val PREF_HOSTER_SELECTION_ENTRIES = PREF_HOSTER_ENTRIES private val PREF_HOSTER_SELECTION_ENTRIES = PREF_HOSTER_ENTRIES
private val PREF_HOSTER_SELECTION_VALUES = arrayOf("slare", "streamsb", "fmoon") private val PREF_HOSTER_SELECTION_VALUES = arrayOf("slare", "streamsb", "fmoon", "dood", "shide")
private val PREF_HOSTER_SELECTION_DEFAULT = PREF_HOSTER_SELECTION_VALUES.toSet() private val PREF_HOSTER_SELECTION_DEFAULT = PREF_HOSTER_SELECTION_VALUES.toSet()
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"

View File

@ -1,28 +1,28 @@
package eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors package eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors
import dev.datlag.jsunpacker.JsUnpacker
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.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
class FilemoonExtractor(private val client: OkHttpClient) { class FilemoonExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
return runCatching {
val doc = client.newCall(GET(url)).execute().asJsoup()
val jsEval = doc.selectFirst("script:containsData(eval)")!!.data()
val masterUrl = JsUnpacker.unpackAndCombine(jsEval)
?.substringAfter("{file:\"")
?.substringBefore("\"}")
?: return emptyList()
fun videoFromUrl(url: String): MutableList<Video>? {
try {
val jsE = client.newCall(GET(url)).execute().asJsoup().selectFirst("script:containsData(eval)")!!.data()
val masterUrl = JsUnpacker(jsE).unpack().toString()
.substringAfter("{file:\"").substringBefore("\"}")
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string() val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
val videoList = mutableListOf<Video>() val separator = "#EXT-X-STREAM-INF:"
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:") masterPlaylist.substringAfter(separator).split(separator).map {
.forEach {
val quality = "Filemoon:" + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p " val quality = "Filemoon:" + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p "
val videoUrl = it.substringAfter("\n").substringBefore("\n") val videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl)) Video(videoUrl, quality, videoUrl)
}
return videoList
} catch (e: Exception) {
return null
} }
}.getOrElse { emptyList() }
} }
} }

View File

@ -1,214 +0,0 @@
package eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors
import java.util.regex.Pattern
import kotlin.math.pow
// https://github.com/cylonu87/JsUnpacker
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\\w+\\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) {
}
return null
}
private inner class Unbase(private val radix: Int) {
private val alphabet62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
private val alphabet95 =
" !\"#$%&\\'()*+,-./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 = alphabet62.substring(0, radix)
}
radix in 63..94 -> {
alphabet = alphabet95.substring(0, radix)
}
radix == 62 -> {
alphabet = alphabet62
}
radix == 95 -> {
alphabet = alphabet95
}
}
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
}
}
}
}
}

View File

@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.OkHttpClient
class StreamHideExtractor(private val client: OkHttpClient) {
// from nineanime / ask4movie FilemoonExtractor
private val subtitleRegex = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
fun videosFromUrl(url: String): List<Video> {
val page = client.newCall(GET(url)).execute().body.string()
val unpacked = JsUnpacker.unpackAndCombine(page) ?: return emptyList()
val playlistUrl = unpacked.substringAfter("sources:")
.substringAfter("file:\"")
.substringBefore('"')
val playlistData = client.newCall(GET(playlistUrl)).execute().body.string()
val subs = subtitleRegex.findAll(playlistData).map {
val subUrl = fixUrl(it.groupValues[2], playlistUrl)
Track(subUrl, it.groupValues[1])
}.toList()
val separator = "#EXT-X-STREAM-INF"
return playlistData.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
val urlPart = it.substringAfter("\n").substringBefore("\n")
val videoUrl = fixUrl(urlPart, playlistUrl)
Video(videoUrl, "StreamHide:$resolution", videoUrl, subtitleTracks = subs)
}
}
private fun fixUrl(urlPart: String, playlistUrl: String) =
when {
!urlPart.startsWith("https:") -> playlistUrl.substringBeforeLast("/") + "/$urlPart"
else -> urlPart
}
}

View File

@ -1,33 +1,50 @@
package eu.kanade.tachiyomi.animeextension.de.cinemathek.extractors package eu.kanade.tachiyomi.animeextension.de.cinemathek.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.network.POST
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
class StreamlareExtractor(private val client: OkHttpClient) { class StreamlareExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> { fun videosFromUrl(url: String): List<Video> {
val id = url.substringAfter("/e/").substringBefore("/") val id = url.split("/").last()
val videoList = mutableListOf<Video>()
val playlist = client.newCall( val playlist = client.newCall(
POST( POST(
"https://slwatch.co/api/video/stream/get", "https://slwatch.co/api/video/stream/get",
body = "{\"id\":\"$id\"}" body = "{\"id\":\"$id\"}"
.toRequestBody("application/json".toMediaType()), .toRequestBody("application/json".toMediaType()),
), ),
) ).execute().body.string()
.execute().body.string()
playlist.substringAfter("\"label\":\"").split("\"label\":\"").forEach { val type = playlist.substringAfter("\"type\":\"").substringBefore("\"")
val quality = "Streamlare:" + it.substringAfter("\"label\":\"").substringBefore("\",") return if (type == "hls") {
val token = it.substringAfter("\"file\":\"https:\\/\\/larecontent.com\\/video?token=") val masterPlaylistUrl = playlist.substringAfter("\"file\":\"").substringBefore("\"").replace("\\/", "/")
.substringBefore("\",") val masterPlaylist = client.newCall(GET(masterPlaylistUrl)).execute().body.string()
val response = client.newCall(POST("https://larecontent.com/video?token=$token")).execute()
val videoUrl = response.request.url.toString() val separator = "#EXT-X-STREAM-INF"
videoList.addAll((listOf(Video(videoUrl, quality, videoUrl)))) masterPlaylist.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { urlPart ->
when {
!urlPart.startsWith("http") ->
masterPlaylistUrl.substringBefore("master.m3u8") + urlPart
else -> urlPart
}
}
Video(videoUrl, "Streamlare:$quality", videoUrl)
}
} else {
val separator = "\"label\":\""
playlist.substringAfter(separator).split(separator).map {
val quality = it.substringAfter(separator).substringBefore("\",") + " (Sl-mp4)"
val apiUrl = it.substringAfter("\"file\":\"").substringBefore("\",")
.replace("\\", "")
val response = client.newCall(POST(apiUrl)).execute()
val videoUrl = response.request.url.toString()
Video(videoUrl, "Streamlare:$quality", videoUrl)
}
} }
return videoList
} }
} }

View File

@ -16,7 +16,7 @@ class DooPlayGenerator : ThemeSourceGenerator {
SingleLang("AnimePlayer", "https://animeplayer.com.br", "pt-BR", isNsfw = true), SingleLang("AnimePlayer", "https://animeplayer.com.br", "pt-BR", isNsfw = true),
SingleLang("AnimesFox BR", "https://animesfoxbr.com", "pt-BR", isNsfw = false, overrideVersionCode = 1), SingleLang("AnimesFox BR", "https://animesfoxbr.com", "pt-BR", isNsfw = false, overrideVersionCode = 1),
SingleLang("Animes House", "https://animeshouse.net", "pt-BR", isNsfw = false, overrideVersionCode = 4), SingleLang("Animes House", "https://animeshouse.net", "pt-BR", isNsfw = false, overrideVersionCode = 4),
SingleLang("Cinemathek", "https://cinemathek.net", "de", isNsfw = true, overrideVersionCode = 11), SingleLang("Cinemathek", "https://cinemathek.net", "de", isNsfw = true, overrideVersionCode = 12),
SingleLang("CineVision", "https://cinevisionv3.online", "pt-BR", isNsfw = true, overrideVersionCode = 5), SingleLang("CineVision", "https://cinevisionv3.online", "pt-BR", isNsfw = true, overrideVersionCode = 5),
SingleLang("DonghuaX", "https://donghuax.com", "pt-BR", isNsfw = false), SingleLang("DonghuaX", "https://donghuax.com", "pt-BR", isNsfw = false),
SingleLang("GoAnimes", "https://goanimes.net", "pt-BR", isNsfw = true), SingleLang("GoAnimes", "https://goanimes.net", "pt-BR", isNsfw = true),

View File

@ -76,7 +76,7 @@ class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
username = JFConstants.getPrefUsername(preferences) username = JFConstants.getPrefUsername(preferences)
password = JFConstants.getPrefPassword(preferences) password = JFConstants.getPrefPassword(preferences)
if (username.isEmpty() || password.isEmpty()) { if (username.isEmpty() || password.isEmpty()) {
return null if (username != "demo") return null
} }
val (newKey, newUid) = runBlocking { val (newKey, newUid) = runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {