[skip ci]refactor(lib): General refactoration (#1579)
This commit is contained in:
parent
b1f87d246c
commit
d07a8f28bf
@ -8,7 +8,6 @@ import android.webkit.CookieManager
|
|||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
@ -22,7 +21,6 @@ import java.util.concurrent.CountDownLatch
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class CloudflareInterceptor(private val client: OkHttpClient) : Interceptor {
|
class CloudflareInterceptor(private val client: OkHttpClient) : Interceptor {
|
||||||
private val network: NetworkHelper by injectLazy()
|
|
||||||
private val context: Application by injectLazy()
|
private val context: Application by injectLazy()
|
||||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
|
|
||||||
@ -141,7 +139,6 @@ class CloudflareInterceptor(private val client: OkHttpClient) : Interceptor {
|
|||||||
companion object {
|
companion object {
|
||||||
private val ERROR_CODES = listOf(403, 503)
|
private val ERROR_CODES = listOf(403, 503)
|
||||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
|
||||||
|
|
||||||
// ref: https://github.com/vvanglro/cf-clearance/blob/0d3455b5b4f299b131f357dd6e0a27316cf26f9a/cf_clearance/retry.py#L15
|
// ref: https://github.com/vvanglro/cf-clearance/blob/0d3455b5b4f299b131f357dd6e0a27316cf26f9a/cf_clearance/retry.py#L15
|
||||||
private val CHECK_SCRIPT by lazy {
|
private val CHECK_SCRIPT by lazy {
|
||||||
|
@ -23,8 +23,9 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
object CryptoAES {
|
object CryptoAES {
|
||||||
|
|
||||||
private const val KEY_SIZE = 256
|
private const val KEY_SIZE = 32 // 256 bits
|
||||||
private const val IV_SIZE = 128
|
private const val IV_SIZE = 16 // 128 bits
|
||||||
|
private const val SALT_SIZE = 8 // 64 bits
|
||||||
private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING"
|
private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING"
|
||||||
private const val HASH_CIPHER_FALLBACK = "AES/CBC/PKCS5PADDING"
|
private const val HASH_CIPHER_FALLBACK = "AES/CBC/PKCS5PADDING"
|
||||||
private const val AES = "AES"
|
private const val AES = "AES"
|
||||||
@ -41,14 +42,14 @@ object CryptoAES {
|
|||||||
fun decrypt(cipherText: String, password: String): String {
|
fun decrypt(cipherText: String, password: String): String {
|
||||||
return try {
|
return try {
|
||||||
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
||||||
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
val saltBytes = Arrays.copyOfRange(ctBytes, SALT_SIZE, IV_SIZE)
|
||||||
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
val cipherTextBytes = Arrays.copyOfRange(ctBytes, IV_SIZE, ctBytes.size)
|
||||||
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
val md5 = MessageDigest.getInstance("MD5")
|
||||||
val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
|
val keyAndIV = generateKeyAndIV(KEY_SIZE, IV_SIZE, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
|
||||||
decryptAES(
|
decryptAES(
|
||||||
cipherTextBytes,
|
cipherTextBytes,
|
||||||
keyAndIV?.get(0) ?: ByteArray(32),
|
keyAndIV?.get(0) ?: ByteArray(KEY_SIZE),
|
||||||
keyAndIV?.get(1) ?: ByteArray(16),
|
keyAndIV?.get(1) ?: ByteArray(IV_SIZE),
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
@ -60,17 +61,17 @@ object CryptoAES {
|
|||||||
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
||||||
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||||
val keyAndIV = generateKeyAndIV(
|
val keyAndIV = generateKeyAndIV(
|
||||||
32,
|
KEY_SIZE,
|
||||||
16,
|
IV_SIZE,
|
||||||
1,
|
1,
|
||||||
salt.decodeHex(),
|
salt.decodeHex(),
|
||||||
password.toByteArray(Charsets.UTF_8),
|
password.toByteArray(Charsets.UTF_8),
|
||||||
md5
|
md5,
|
||||||
)
|
)
|
||||||
decryptAES(
|
decryptAES(
|
||||||
ctBytes,
|
ctBytes,
|
||||||
keyAndIV?.get(0) ?: ByteArray(32),
|
keyAndIV?.get(0) ?: ByteArray(KEY_SIZE),
|
||||||
keyAndIV?.get(1) ?: ByteArray(16),
|
keyAndIV?.get(1) ?: ByteArray(IV_SIZE),
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
@ -129,7 +130,14 @@ object CryptoAES {
|
|||||||
* @param md the message digest algorithm to use
|
* @param md the message digest algorithm to use
|
||||||
* @return an two-element array with the generated key and IV
|
* @return an two-element array with the generated key and IV
|
||||||
*/
|
*/
|
||||||
private fun generateKeyAndIV(keyLength: Int, ivLength: Int, iterations: Int, salt: ByteArray, password: ByteArray, md: MessageDigest): Array<ByteArray?>? {
|
private fun generateKeyAndIV(
|
||||||
|
keyLength: Int,
|
||||||
|
ivLength: Int,
|
||||||
|
iterations: Int,
|
||||||
|
salt: ByteArray,
|
||||||
|
password: ByteArray,
|
||||||
|
md: MessageDigest,
|
||||||
|
): Array<ByteArray?>? {
|
||||||
val digestLength = md.digestLength
|
val digestLength = md.digestLength
|
||||||
val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
|
val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
|
||||||
val generatedData = ByteArray(requiredLength)
|
val generatedData = ByteArray(requiredLength)
|
||||||
@ -142,7 +150,7 @@ object CryptoAES {
|
|||||||
// Digest data (last digest if available, password data, salt if available)
|
// Digest data (last digest if available, password data, salt if available)
|
||||||
if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength)
|
if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength)
|
||||||
md.update(password)
|
md.update(password)
|
||||||
md.update(salt, 0, 8)
|
md.update(salt, 0, SALT_SIZE)
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
|
||||||
// additional rounds
|
// additional rounds
|
||||||
|
@ -13,26 +13,22 @@ import org.jsoup.nodes.Element
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use if the attribute tag could have a data:image string or URL
|
* Use if the attribute tag has a data:image string but real URLs are on a different attribute
|
||||||
* Transforms data:image in to a fake URL that OkHttp won't die on
|
|
||||||
*/
|
*/
|
||||||
fun Element.dataImageAsUrl(attr: String): String {
|
fun Element.dataImageAsUrlOrNull(attr: String): String? {
|
||||||
return if (this.attr(attr).startsWith("data")) {
|
return if (attr(attr).startsWith("data")) {
|
||||||
"https://127.0.0.1/?" + this.attr(attr).substringAfter(":")
|
"https://127.0.0.1/?" + attr(attr).substringAfter(":")
|
||||||
} else {
|
} else {
|
||||||
this.attr("abs:$attr")
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use if the attribute tag has a data:image string but real URLs are on a different attribute
|
* Use if the attribute tag could have a data:image string or URL
|
||||||
|
* Transforms data:image in to a fake URL that OkHttp won't die on
|
||||||
*/
|
*/
|
||||||
fun Element.dataImageAsUrlOrNull(attr: String): String? {
|
fun Element.dataImageAsUrl(attr: String): String {
|
||||||
return if (this.attr(attr).startsWith("data")) {
|
return dataImageAsUrlOrNull(attr) ?: attr("abs:$attr")
|
||||||
"https://127.0.0.1/?" + this.attr(attr).substringAfter(":")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,7 +47,7 @@ class DataImageInterceptor : Interceptor {
|
|||||||
} else {
|
} else {
|
||||||
dataString.substringAfter(",").toByteArray()
|
dataString.substringAfter(",").toByteArray()
|
||||||
}
|
}
|
||||||
val mediaType = mediaTypePattern.find(dataString)!!.value.toMediaTypeOrNull()
|
val mediaType = mediaTypePattern.find(dataString)?.value?.toMediaTypeOrNull()
|
||||||
Response.Builder().body(byteArray.toResponseBody(mediaType))
|
Response.Builder().body(byteArray.toResponseBody(mediaType))
|
||||||
.request(chain.request())
|
.request(chain.request())
|
||||||
.protocol(Protocol.HTTP_1_0)
|
.protocol(Protocol.HTTP_1_0)
|
||||||
|
@ -10,11 +10,11 @@ class DoodExtractor(private val client: OkHttpClient) {
|
|||||||
fun videoFromUrl(
|
fun videoFromUrl(
|
||||||
url: String,
|
url: String,
|
||||||
quality: String? = null,
|
quality: String? = null,
|
||||||
redirect: Boolean = true
|
redirect: Boolean = true,
|
||||||
): Video? {
|
): Video? {
|
||||||
val newQuality = quality ?: "Doodstream" + if (redirect) " mirror" else ""
|
val newQuality = quality ?: "Doodstream" + if (redirect) " mirror" else ""
|
||||||
|
|
||||||
return try {
|
return runCatching {
|
||||||
val response = client.newCall(GET(url)).execute()
|
val response = client.newCall(GET(url)).execute()
|
||||||
val newUrl = if (redirect) response.request.url.toString() else url
|
val newUrl = if (redirect) response.request.url.toString() else url
|
||||||
|
|
||||||
@ -28,23 +28,21 @@ class DoodExtractor(private val client: OkHttpClient) {
|
|||||||
val videoUrlStart = client.newCall(
|
val videoUrlStart = client.newCall(
|
||||||
GET(
|
GET(
|
||||||
"https://dood.$doodTld/pass_md5/$md5",
|
"https://dood.$doodTld/pass_md5/$md5",
|
||||||
Headers.headersOf("referer", newUrl)
|
Headers.headersOf("referer", newUrl),
|
||||||
)
|
),
|
||||||
).execute().body.string()
|
).execute().body.string()
|
||||||
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
|
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
|
||||||
Video(newUrl, newQuality, videoUrl, headers = doodHeaders(doodTld))
|
Video(newUrl, newQuality, videoUrl, headers = doodHeaders(doodTld))
|
||||||
} catch (e: Exception) {
|
}.getOrNull()
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun videosFromUrl(
|
fun videosFromUrl(
|
||||||
url: String,
|
url: String,
|
||||||
quality: String? = null,
|
quality: String? = null,
|
||||||
redirect: Boolean = true
|
redirect: Boolean = true,
|
||||||
): List<Video> {
|
): List<Video> {
|
||||||
val video = videoFromUrl(url, quality, redirect)
|
val video = videoFromUrl(url, quality, redirect)
|
||||||
return video?.let { listOf(it) } ?: emptyList<Video>()
|
return video?.let(::listOf) ?: emptyList<Video>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRandomString(length: Int = 10): String {
|
private fun getRandomString(length: Int = 10): String {
|
||||||
|
@ -16,4 +16,3 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.bundles.common)
|
compileOnly(libs.bundles.common)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,29 +6,35 @@ import eu.kanade.tachiyomi.network.POST
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class FembedExtractor(private val client: OkHttpClient) {
|
class FembedExtractor(private val client: OkHttpClient) {
|
||||||
|
private val json: Json by injectLazy()
|
||||||
fun videosFromUrl(url: String, prefix: String = "", redirect: Boolean = false): List<Video> {
|
fun videosFromUrl(url: String, prefix: String = "", redirect: Boolean = false): List<Video> {
|
||||||
val videoApi = if (redirect) {
|
val videoApi = when {
|
||||||
(runCatching { client.newCall(GET(url)).execute().request.url.toString()
|
redirect -> runCatching {
|
||||||
.replace("/v/", "/api/source/") }.getOrNull() ?: return emptyList<Video>())
|
client.newCall(GET(url)).execute().request.url.toString()
|
||||||
} else {
|
}.getOrNull() ?: return emptyList<Video>()
|
||||||
url.replace("/v/", "/api/source/")
|
else -> url
|
||||||
|
}.replace("/v/", "/api/source/")
|
||||||
|
|
||||||
|
val jsonResponse = runCatching {
|
||||||
|
client.newCall(POST(videoApi)).execute().use {
|
||||||
|
json.decodeFromString<FembedResponse>(it.body.string())
|
||||||
}
|
}
|
||||||
val body = runCatching {
|
|
||||||
client.newCall(POST(videoApi)).execute().body.string()
|
|
||||||
}.getOrNull() ?: return emptyList<Video>()
|
}.getOrNull() ?: return emptyList<Video>()
|
||||||
|
|
||||||
val jsonResponse = try{ Json { ignoreUnknownKeys = true }.decodeFromString<FembedResponse>(body) } catch (e: Exception) { FembedResponse(false, emptyList()) }
|
if (!jsonResponse.success) return emptyList<Video>()
|
||||||
|
|
||||||
return if (jsonResponse.success) {
|
return jsonResponse.data.map {
|
||||||
jsonResponse.data.map {
|
|
||||||
val quality = ("Fembed:${it.label}").let {
|
val quality = ("Fembed:${it.label}").let {
|
||||||
if (prefix.isNotBlank()) "$prefix $it"
|
if (prefix.isNotBlank()) {
|
||||||
else it
|
"$prefix $it"
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Video(it.file, quality, it.file)
|
Video(it.file, quality, it.file)
|
||||||
}
|
}
|
||||||
} else { emptyList<Video>() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class FembedResponse(
|
data class FembedResponse(
|
||||||
val success: Boolean,
|
val success: Boolean,
|
||||||
val data: List<FembedVideo> = emptyList()
|
val data: List<FembedVideo> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class FembedVideo(
|
data class FembedVideo(
|
||||||
val file: String,
|
val file: String,
|
||||||
val label: String
|
val label: String,
|
||||||
)
|
)
|
@ -12,7 +12,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.bundles.common)
|
compileOnly(libs.bundles.common)
|
||||||
implementation(project(":lib-unpacker"))
|
implementation(project(":lib-unpacker"))
|
||||||
|
@ -16,7 +16,7 @@ class OkruExtractor(private val client: OkHttpClient) {
|
|||||||
Pair("sd", "480p"),
|
Pair("sd", "480p"),
|
||||||
Pair("low", "360p"),
|
Pair("low", "360p"),
|
||||||
Pair("lowest", "240p"),
|
Pair("lowest", "240p"),
|
||||||
Pair("mobile", "144p")
|
Pair("mobile", "144p"),
|
||||||
)
|
)
|
||||||
return qualities.find { it.first == quality }?.second ?: quality
|
return qualities.find { it.first == quality }?.second ?: quality
|
||||||
}
|
}
|
||||||
@ -33,16 +33,24 @@ class OkruExtractor(private val client: OkHttpClient) {
|
|||||||
.substringBefore("\\\"")
|
.substringBefore("\\\"")
|
||||||
.replace("\\\\u0026", "&")
|
.replace("\\\\u0026", "&")
|
||||||
val quality = it.substringBefore("\\\"").let {
|
val quality = it.substringBefore("\\\"").let {
|
||||||
if (fixQualities) fixQuality(it)
|
if (fixQualities) {
|
||||||
else it
|
fixQuality(it)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val videoQuality = ("Okru:" + quality).let {
|
val videoQuality = ("Okru:" + quality).let {
|
||||||
if (prefix.isNotBlank()) "$prefix $it"
|
if (prefix.isNotBlank()) {
|
||||||
else it
|
"$prefix $it"
|
||||||
|
} else {
|
||||||
|
it
|
||||||
}
|
}
|
||||||
if (videoUrl.startsWith("https://"))
|
}
|
||||||
|
if (videoUrl.startsWith("https://")) {
|
||||||
Video(videoUrl, videoQuality, videoUrl)
|
Video(videoUrl, videoQuality, videoUrl)
|
||||||
else null
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,14 @@ class StreamSBExtractor(private val client: OkHttpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun videosFromUrl(url: String, headers: Headers, prefix: String = "", suffix: String = "", common: Boolean = true, manualData: Boolean = false): List<Video> {
|
fun videosFromUrl(
|
||||||
|
url: String,
|
||||||
|
headers: Headers,
|
||||||
|
prefix: String = "",
|
||||||
|
suffix: String = "",
|
||||||
|
common: Boolean = true,
|
||||||
|
manualData: Boolean = false,
|
||||||
|
): List<Video> {
|
||||||
val trimmedUrl = url.trim() // Prevents some crashes
|
val trimmedUrl = url.trim() // Prevents some crashes
|
||||||
val newHeaders = if (manualData) {
|
val newHeaders = if (manualData) {
|
||||||
headers
|
headers
|
||||||
@ -78,7 +85,7 @@ class StreamSBExtractor(private val client: OkHttpClient) {
|
|||||||
.set("authority", "embedsb.com")
|
.set("authority", "embedsb.com")
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
return try {
|
return runCatching {
|
||||||
val master = if (manualData) trimmedUrl else fixUrl(trimmedUrl, common)
|
val master = if (manualData) trimmedUrl else fixUrl(trimmedUrl, common)
|
||||||
val request = client.newCall(GET(master, newHeaders)).execute()
|
val request = client.newCall(GET(master, newHeaders)).execute()
|
||||||
|
|
||||||
@ -86,6 +93,7 @@ class StreamSBExtractor(private val client: OkHttpClient) {
|
|||||||
if (request.code == 200) {
|
if (request.code == 200) {
|
||||||
request.use { it.body.string() }
|
request.use { it.body.string() }
|
||||||
} else {
|
} else {
|
||||||
|
request.close()
|
||||||
updateEndpoint()
|
updateEndpoint()
|
||||||
client.newCall(GET(fixUrl(trimmedUrl, common), newHeaders))
|
client.newCall(GET(fixUrl(trimmedUrl, common), newHeaders))
|
||||||
.execute()
|
.execute()
|
||||||
@ -94,9 +102,9 @@ class StreamSBExtractor(private val client: OkHttpClient) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val masterUrl = json.stream_data.file.trim('"')
|
val masterUrl = json.stream_data.file.trim('"')
|
||||||
val subtitleList = json.stream_data.subs?.let {
|
val subtitleList = json.stream_data.subs
|
||||||
it.map { s -> Track(s.file, s.label) }
|
?.map { Track(it.file, it.label) }
|
||||||
} ?: emptyList()
|
?: emptyList()
|
||||||
|
|
||||||
val masterPlaylist = client.newCall(GET(masterUrl, newHeaders))
|
val masterPlaylist = client.newCall(GET(masterUrl, newHeaders))
|
||||||
.execute()
|
.execute()
|
||||||
@ -118,28 +126,16 @@ class StreamSBExtractor(private val client: OkHttpClient) {
|
|||||||
.substringAfter("x")
|
.substringAfter("x")
|
||||||
.substringBefore(",") + "p"
|
.substringBefore(",") + "p"
|
||||||
val quality = ("StreamSB:" + resolution).let {
|
val quality = ("StreamSB:" + resolution).let {
|
||||||
if (prefix.isNotBlank()) {
|
buildString {
|
||||||
"$prefix $it"
|
if (prefix.isNotBlank()) append("$prefix ")
|
||||||
} else {
|
append(it)
|
||||||
it
|
if (prefix.isNotBlank()) append(" $suffix")
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
if (suffix.isNotBlank()) {
|
|
||||||
"$it $suffix"
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
if (audioList.isEmpty()) {
|
|
||||||
Video(videoUrl, quality, videoUrl, headers = newHeaders, subtitleTracks = subtitleList)
|
|
||||||
} else {
|
|
||||||
Video(videoUrl, quality, videoUrl, headers = newHeaders, subtitleTracks = subtitleList, audioTracks = audioList)
|
Video(videoUrl, quality, videoUrl, headers = newHeaders, subtitleTracks = subtitleList, audioTracks = audioList)
|
||||||
}
|
}
|
||||||
}
|
}.getOrNull() ?: emptyList<Video>()
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList<Video>()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun videosFromDecryptedUrl(realUrl: String, headers: Headers, prefix: String = "", suffix: String = ""): List<Video> {
|
fun videosFromDecryptedUrl(realUrl: String, headers: Headers, prefix: String = "", suffix: String = ""): List<Video> {
|
||||||
|
@ -4,17 +4,17 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Response(
|
data class Response(
|
||||||
val stream_data: ResponseObject
|
val stream_data: ResponseObject,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ResponseObject(
|
data class ResponseObject(
|
||||||
val file: String,
|
val file: String,
|
||||||
val subs: List<Subtitle>? = null
|
val subs: List<Subtitle>? = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Subtitle(
|
data class Subtitle(
|
||||||
val label: String,
|
val label: String,
|
||||||
val file: String
|
val file: String,
|
||||||
)
|
)
|
||||||
|
@ -10,7 +10,7 @@ class StreamTapeExtractor(private val client: OkHttpClient) {
|
|||||||
val baseUrl = "https://streamtape.com/e/"
|
val baseUrl = "https://streamtape.com/e/"
|
||||||
val newUrl = if (!url.startsWith(baseUrl)) {
|
val newUrl = if (!url.startsWith(baseUrl)) {
|
||||||
// ["https", "", "<domain>", "<???>", "<id>", ...]
|
// ["https", "", "<domain>", "<???>", "<id>", ...]
|
||||||
val id = runCatching { url.split("/").get(4) }.getOrNull() ?: return null
|
val id = url.split("/").getOrNull(4) ?: return null
|
||||||
baseUrl + id
|
baseUrl + id
|
||||||
} else { url }
|
} else { url }
|
||||||
val document = client.newCall(GET(newUrl)).execute().asJsoup()
|
val document = client.newCall(GET(newUrl)).execute().asJsoup()
|
||||||
|
@ -7,25 +7,25 @@ import app.cash.quickjs.QuickJs
|
|||||||
*/
|
*/
|
||||||
object Deobfuscator {
|
object Deobfuscator {
|
||||||
fun deobfuscateScript(source: String): String? {
|
fun deobfuscateScript(source: String): String? {
|
||||||
val engine = QuickJs.create()
|
|
||||||
val originalScript = javaClass.getResource("/assets/$SCRIPT_NAME")
|
val originalScript = javaClass.getResource("/assets/$SCRIPT_NAME")
|
||||||
?.readText() ?: return null
|
?.readText() ?: return null
|
||||||
|
|
||||||
// Sadly needed until QuickJS properly supports module imports:
|
// Sadly needed until QuickJS properly supports module imports:
|
||||||
// Regex for finding one and two in "export{one as Deobfuscator,two as Transformer};"
|
// Regex for finding one and two in "export{one as Deobfuscator,two as Transformer};"
|
||||||
val regex = """export\{(.*) as Deobfuscator,(.*) as Transformer\};""".toRegex()
|
val regex = """export\{(.*) as Deobfuscator,(.*) as Transformer\};""".toRegex()
|
||||||
val synchronyScript = regex.find(originalScript)!!.let { match ->
|
val synchronyScript = regex.find(originalScript)?.let { match ->
|
||||||
val (deob, trans) = match.destructured
|
val (deob, trans) = match.destructured
|
||||||
val replacement = "const Deobfuscator = $deob, Transformer = $trans;"
|
val replacement = "const Deobfuscator = $deob, Transformer = $trans;"
|
||||||
originalScript.replace(match.value, replacement)
|
originalScript.replace(match.value, replacement)
|
||||||
}
|
} ?: return null
|
||||||
|
|
||||||
|
return QuickJs.create().use { engine ->
|
||||||
engine.evaluate("globalThis.console = { log: () => {}, warn: () => {}, error: () => {}, trace: () => {} };")
|
engine.evaluate("globalThis.console = { log: () => {}, warn: () => {}, error: () => {}, trace: () => {} };")
|
||||||
engine.evaluate(synchronyScript)
|
engine.evaluate(synchronyScript)
|
||||||
|
|
||||||
engine.set("source", TestInterface::class.java, object : TestInterface { override fun getValue() = source })
|
engine.set("source", TestInterface::class.java, object : TestInterface { override fun getValue() = source })
|
||||||
val result = engine.evaluate("new Deobfuscator().deobfuscateSource(source.getValue())") as? String
|
engine.evaluate("new Deobfuscator().deobfuscateSource(source.getValue())") as? String
|
||||||
engine.close()
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
@ -12,7 +12,7 @@ class YourUploadExtractor(private val client: OkHttpClient) {
|
|||||||
return runCatching {
|
return runCatching {
|
||||||
val request = client.newCall(GET(url, headers = newHeaders)).execute()
|
val request = client.newCall(GET(url, headers = newHeaders)).execute()
|
||||||
val document = request.asJsoup()
|
val document = request.asJsoup()
|
||||||
val baseData = document.selectFirst("script:containsData(jwplayerOptions)")!!.data()
|
val baseData = document.selectFirst("script:containsData(jwplayerOptions)")?.data()
|
||||||
if (!baseData.isNullOrEmpty()) {
|
if (!baseData.isNullOrEmpty()) {
|
||||||
val basicUrl = baseData.substringAfter("file: '").substringBefore("',")
|
val basicUrl = baseData.substringAfter("file: '").substringBefore("',")
|
||||||
val quality = prefix + name
|
val quality = prefix + name
|
||||||
|
@ -132,12 +132,12 @@ abstract class DooPlay(
|
|||||||
return SEpisode.create().apply {
|
return SEpisode.create().apply {
|
||||||
val epNum = element.selectFirst("div.numerando")!!.text()
|
val epNum = element.selectFirst("div.numerando")!!.text()
|
||||||
.trim()
|
.trim()
|
||||||
.let {
|
.let(episodeNumberRegex::find)
|
||||||
episodeNumberRegex.find(it)?.groupValues?.last() ?: "0"
|
?.groupValues
|
||||||
}
|
?.last() ?: "0"
|
||||||
val href = element.selectFirst("a[href]")!!
|
val href = element.selectFirst("a[href]")!!
|
||||||
val episodeName = href.ownText()
|
val episodeName = href.ownText()
|
||||||
episode_number = runCatching { epNum.toFloat() }.getOrDefault(0F)
|
episode_number = epNum.toFloatOrNull() ?: 0F
|
||||||
date_upload = element.selectFirst(episodeDateSelector)
|
date_upload = element.selectFirst(episodeDateSelector)
|
||||||
?.text()
|
?.text()
|
||||||
?.toDate() ?: 0L
|
?.toDate() ?: 0L
|
||||||
@ -250,7 +250,7 @@ abstract class DooPlay(
|
|||||||
override fun animeDetailsParse(document: Document): SAnime {
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
val doc = getRealAnimeDoc(document)
|
val doc = getRealAnimeDoc(document)
|
||||||
val sheader = doc.selectFirst("div.sheader")!!
|
val sheader = doc.selectFirst("div.sheader")!!
|
||||||
val anime = SAnime.create().apply {
|
return SAnime.create().apply {
|
||||||
setUrlWithoutDomain(doc.location())
|
setUrlWithoutDomain(doc.location())
|
||||||
sheader.selectFirst("div.poster > img")!!.let {
|
sheader.selectFirst("div.poster > img")!!.let {
|
||||||
thumbnail_url = it.getImageUrl()
|
thumbnail_url = it.getImageUrl()
|
||||||
@ -261,7 +261,7 @@ abstract class DooPlay(
|
|||||||
|
|
||||||
genre = sheader.select("div.data > div.sgeneros > a")
|
genre = sheader.select("div.data > div.sgeneros > a")
|
||||||
.eachText()
|
.eachText()
|
||||||
.joinToString(", ")
|
.joinToString()
|
||||||
|
|
||||||
doc.selectFirst(additionalInfoSelector)?.let { info ->
|
doc.selectFirst(additionalInfoSelector)?.let { info ->
|
||||||
description = buildString {
|
description = buildString {
|
||||||
@ -272,7 +272,6 @@ abstract class DooPlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return anime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
|
Loading…
x
Reference in New Issue
Block a user