forked from AlmightyHak/extensions-source
parent
45cff438ce
commit
821cbc1d59
89 changed files with 668 additions and 557 deletions
3
lib/buzzheavier-extractor/build.gradle.kts
Normal file
3
lib/buzzheavier-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,3 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package eu.kanade.tachiyomi.lib.buzzheavierextractor
|
||||
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.EMPTY_HEADERS
|
||||
|
||||
class BuzzheavierExtractor(
|
||||
private val client: OkHttpClient,
|
||||
private val headers: Headers,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
val id = httpUrl.pathSegments.first()
|
||||
|
||||
val dlHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", httpUrl.host)
|
||||
add("HX-Current-URL", url)
|
||||
add("HX-Request", "true")
|
||||
add("Referer", url)
|
||||
}.build()
|
||||
|
||||
val videoHeaders = headers.newBuilder().apply {
|
||||
add("Referer", url)
|
||||
}.build()
|
||||
|
||||
val path = client.newCall(GET("$url/download", dlHeaders)).execute().headers["hx-redirect"].orEmpty()
|
||||
|
||||
return if (path.isNotEmpty()) {
|
||||
val videoUrl = if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
|
||||
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders))
|
||||
} else if (proxyUrl?.isNotEmpty() == true) {
|
||||
val videoUrl = client.newCall(GET(proxyUrl + id)).execute().parseAs<UrlDto>().url
|
||||
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UrlDto(val url: String)
|
||||
}
|
|
@ -1,73 +1,53 @@
|
|||
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
private val json: Json by injectLazy()
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
private val webViewResolver by lazy { WebViewResolver(client, headers) }
|
||||
|
||||
companion object {
|
||||
private val REGEX_MASTER_JS = Regex("""\s*=\s*'([^']+)""")
|
||||
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
|
||||
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
|
||||
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
|
||||
private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""")
|
||||
private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
|
||||
}
|
||||
|
||||
fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
|
||||
val newHeaders = headers.newBuilder()
|
||||
.set("Referer", "$referer/")
|
||||
.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.set("Accept-Language", "en-US,en;q=0.5")
|
||||
.build()
|
||||
fun videoFromUrl(url: String, prefix: String = "Chillx - "): List<Video> {
|
||||
val data = webViewResolver.getDecryptedData(url) ?: return emptyList()
|
||||
|
||||
val body = client.newCall(GET(url, newHeaders)).execute().body.string()
|
||||
|
||||
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
|
||||
val aesJson = json.decodeFromString<CryptoInfo>(master)
|
||||
val key = fetchKey() ?: throw ErrorLoadingException("Unable to get key")
|
||||
val decryptedScript = CryptoAES.decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\", "")
|
||||
|
||||
val masterUrl = REGEX_SOURCES.find(decryptedScript)?.groupValues?.get(1)
|
||||
?: REGEX_FILE.find(decryptedScript)?.groupValues?.get(1)
|
||||
?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1)
|
||||
val masterUrl = REGEX_SOURCES.find(data)?.groupValues?.get(1)
|
||||
?: REGEX_FILE.find(data)?.groupValues?.get(1)
|
||||
?: REGEX_SOURCE.find(data)?.groupValues?.get(1)
|
||||
?: return emptyList()
|
||||
|
||||
val subtitleList = buildList {
|
||||
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
||||
val subtitles = REGEX_SUBS.findAll(data)
|
||||
subtitles.forEach {
|
||||
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
|
||||
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
||||
}
|
||||
}
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
val videoList = playlistUtils.extractFromHls(
|
||||
playlistUrl = masterUrl,
|
||||
referer = url,
|
||||
videoNameGen = { "$prefix$it" },
|
||||
subtitleList = subtitleList,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private fun fetchKey(): String? {
|
||||
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
|
||||
return videoList.map {
|
||||
Video(
|
||||
url = it.url,
|
||||
quality = it.quality,
|
||||
videoUrl = it.videoUrl,
|
||||
audioTracks = it.audioTracks,
|
||||
subtitleTracks = playlistUtils.fixSubtitles(it.subtitleTracks),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeUnicodeEscape(input: String): String {
|
||||
|
@ -76,16 +56,4 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||
it.groupValues[1].toInt(16).toChar().toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CryptoInfo(
|
||||
@SerialName("ct") val ciphertext: String,
|
||||
@SerialName("s") val salt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KeysData(
|
||||
@SerialName("chillx") val keys: List<String>
|
||||
)
|
||||
}
|
||||
class ErrorLoadingException(message: String) : Exception(message)
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.JavascriptInterface
|
||||
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
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WebViewResolver(
|
||||
private val client: OkHttpClient,
|
||||
private val globalHeaders: Headers,
|
||||
) {
|
||||
private val context: Application by injectLazy()
|
||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
class JsInterface(private val latch: CountDownLatch) {
|
||||
var result: String? = null
|
||||
|
||||
@JavascriptInterface
|
||||
fun passPayload(payload: String) {
|
||||
result = payload
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun getDecryptedData(embedUrl: String): String? {
|
||||
val latch = CountDownLatch(1)
|
||||
var webView: WebView? = null
|
||||
val jsi = JsInterface(latch)
|
||||
val interfaceName = randomString()
|
||||
|
||||
handler.post {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
|
||||
with(webview.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
useWideViewPort = false
|
||||
loadWithOverviewMode = false
|
||||
userAgentString = globalHeaders["User-Agent"]
|
||||
}
|
||||
|
||||
webview.addJavascriptInterface(jsi, interfaceName)
|
||||
webview.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? {
|
||||
if (request?.url.toString().contains("assets/js/library")) {
|
||||
return patchScript(request!!.url.toString(), interfaceName)
|
||||
?: super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
}
|
||||
|
||||
webView?.loadUrl(embedUrl)
|
||||
}
|
||||
|
||||
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||
|
||||
handler.post {
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
|
||||
return jsi.result
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TIMEOUT_SEC: Long = 30
|
||||
}
|
||||
|
||||
private fun randomString(length: Int = 10): String {
|
||||
val charPool = ('a'..'z') + ('A'..'Z')
|
||||
return List(length) { charPool.random() }.joinToString("")
|
||||
}
|
||||
|
||||
private fun patchScript(scriptUrl: String, interfaceName: String): WebResourceResponse? {
|
||||
val scriptBody = client.newCall(GET(scriptUrl)).execute().body.string()
|
||||
|
||||
val oldFunc = randomString()
|
||||
val newBody = buildString {
|
||||
append(
|
||||
"""
|
||||
const $oldFunc = Function;
|
||||
window.Function = function (...args) {
|
||||
if (args.length == 1) {
|
||||
window.$interfaceName.passPayload(args[0]);
|
||||
}
|
||||
return $oldFunc(...args);
|
||||
};
|
||||
""".trimIndent()
|
||||
)
|
||||
append(scriptBody)
|
||||
}
|
||||
|
||||
|
||||
return WebResourceResponse(
|
||||
"application/javascript",
|
||||
"utf-8",
|
||||
200,
|
||||
"ok",
|
||||
mapOf("server" to "cloudflare"),
|
||||
ByteArrayInputStream(newBody.toByteArray()),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
package eu.kanade.tachiyomi.lib.filemoonextractor
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
|
@ -13,20 +16,28 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class FilemoonExtractor(private val client: OkHttpClient) {
|
||||
class FilemoonExtractor(
|
||||
private val client: OkHttpClient,
|
||||
private val preferences: SharedPreferences? = null,
|
||||
) {
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
var httpUrl = url.toHttpUrl()
|
||||
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
||||
.set("Referer", url)
|
||||
.set("Origin", "https://${httpUrl.host}")
|
||||
.build()
|
||||
|
||||
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
||||
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
||||
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")?.data() ?: run {
|
||||
val iframeUrl = doc.selectFirst("iframe[src]")!!.attr("src")
|
||||
httpUrl = iframeUrl.toHttpUrl()
|
||||
val iframeDoc = client.newCall(GET(iframeUrl, videoHeaders)).execute().asJsoup()
|
||||
iframeDoc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
||||
}
|
||||
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
||||
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
||||
?.substringAfter("{file:\"", "")
|
||||
|
@ -50,14 +61,39 @@ class FilemoonExtractor(private val client: OkHttpClient) {
|
|||
}
|
||||
}
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
val videoList = playlistUtils.extractFromHls(
|
||||
masterUrl,
|
||||
subtitleList = subtitleTracks,
|
||||
referer = "https://${httpUrl.host}/",
|
||||
videoNameGen = { "$prefix$it" },
|
||||
)
|
||||
|
||||
val subPref = preferences?.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
||||
return videoList.map {
|
||||
Video(
|
||||
url = it.url,
|
||||
quality = it.quality,
|
||||
videoUrl = it.videoUrl,
|
||||
audioTracks = it.audioTracks,
|
||||
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SubtitleDto(val file: String, val label: String)
|
||||
|
||||
companion object {
|
||||
fun addSubtitlePref(screen: PreferenceScreen) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = PREF_SUBTITLE_KEY
|
||||
title = "Filemoon subtitle preference"
|
||||
summary = "Leave blank to use all subs"
|
||||
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private const val PREF_SUBTITLE_KEY = "pref_filemoon_sub_lang_key"
|
||||
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
||||
}
|
||||
}
|
||||
|
|
7
lib/savefile-extractor/build.gradle.kts
Normal file
7
lib/savefile-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package eu.kanade.tachiyomi.lib.savefileextractor
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
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.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class SavefileExtractor(
|
||||
private val client: OkHttpClient,
|
||||
private val preferences: SharedPreferences,
|
||||
) {
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = "Savefile - ", headers: Headers? = null): List<Video> {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
||||
.set("Referer", url)
|
||||
.set("Origin", "https://${httpUrl.host}")
|
||||
.build()
|
||||
|
||||
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
||||
val js = doc.selectFirst("script:containsData(m3u8)")!!.data()
|
||||
val masterUrl = js.takeIf(String::isNotBlank)
|
||||
?.substringAfter("{file:\"", "")
|
||||
?.substringBefore("\"}", "")
|
||||
?.takeIf(String::isNotBlank)
|
||||
?: return emptyList()
|
||||
|
||||
val videoList = playlistUtils.extractFromHls(
|
||||
masterUrl,
|
||||
referer = "https://${httpUrl.host}/",
|
||||
videoNameGen = { "$prefix$it" },
|
||||
)
|
||||
|
||||
val subPref = preferences.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
||||
return videoList.map {
|
||||
Video(
|
||||
url = it.url,
|
||||
quality = it.quality,
|
||||
videoUrl = it.videoUrl,
|
||||
audioTracks = it.audioTracks,
|
||||
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun addSubtitlePref(screen: PreferenceScreen) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = PREF_SUBTITLE_KEY
|
||||
title = "Savefile subtitle preference"
|
||||
summary = "Leave blank to use all subs"
|
||||
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private const val PREF_SUBTITLE_KEY = "pref_savefile_sub_lang_key"
|
||||
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
@ -30,16 +31,19 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
|||
script
|
||||
}
|
||||
}
|
||||
val masterUrl = scriptBody
|
||||
?.substringAfter("source", "")
|
||||
?.substringAfter("file:\"", "")
|
||||
?.substringBefore("\"", "")
|
||||
?.takeIf(String::isNotBlank)
|
||||
val masterUrl = scriptBody?.let {
|
||||
M3U8_REGEX.find(it)?.value
|
||||
}
|
||||
?: return emptyList()
|
||||
|
||||
val subtitleList = extractSubtitles(scriptBody)
|
||||
|
||||
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen, subtitleList = subtitleList)
|
||||
return playlistUtils.extractFromHls(
|
||||
playlistUrl = masterUrl,
|
||||
referer = "https://${url.toHttpUrl().host}/",
|
||||
videoNameGen = videoNameGen,
|
||||
subtitleList = playlistUtils.fixSubtitles(subtitleList),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String {
|
||||
|
@ -57,7 +61,11 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
|||
.substringAfter("tracks")
|
||||
.substringAfter("[")
|
||||
.substringBefore("]")
|
||||
json.decodeFromString<List<TrackDto>>("[$subtitleStr]")
|
||||
val fixedSubtitleStr = FIX_TRACKS_REGEX.replace(subtitleStr) { match ->
|
||||
"\"${match.value}\""
|
||||
}
|
||||
|
||||
json.decodeFromString<List<TrackDto>>("[$fixedSubtitleStr]")
|
||||
.filter { it.kind.equals("captions", true) }
|
||||
.map { Track(it.file, it.label ?: "") }
|
||||
} catch (e: SerializationException) {
|
||||
|
@ -67,4 +75,7 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
|||
|
||||
@Serializable
|
||||
private data class TrackDto(val file: String, val kind: String, val label: String? = null)
|
||||
|
||||
private val M3U8_REGEX = Regex("""https[^"]*m3u8[^"]*""")
|
||||
private val FIX_TRACKS_REGEX = Regex("""(?<!["])(file|kind|label)(?!["])""")
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue