[skip ci]refactor(lib): General refactoration (#1579)

This commit is contained in:
Claudemirovsky 2023-05-06 09:50:45 -03:00 committed by GitHub
parent b1f87d246c
commit d07a8f28bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 124 additions and 118 deletions

View File

@ -8,7 +8,6 @@ import android.webkit.CookieManager
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
@ -22,7 +21,6 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CloudflareInterceptor(private val client: OkHttpClient) : Interceptor {
private val network: NetworkHelper by injectLazy()
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
@ -141,7 +139,6 @@ class CloudflareInterceptor(private val client: OkHttpClient) : Interceptor {
companion object {
private val ERROR_CODES = listOf(403, 503)
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
private val CHECK_SCRIPT by lazy {

View File

@ -23,8 +23,9 @@ import javax.crypto.spec.SecretKeySpec
@Suppress("unused")
object CryptoAES {
private const val KEY_SIZE = 256
private const val IV_SIZE = 128
private const val KEY_SIZE = 32 // 256 bits
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_FALLBACK = "AES/CBC/PKCS5PADDING"
private const val AES = "AES"
@ -41,14 +42,14 @@ object CryptoAES {
fun decrypt(cipherText: String, password: String): String {
return try {
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
val md5: MessageDigest = MessageDigest.getInstance("MD5")
val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
val saltBytes = Arrays.copyOfRange(ctBytes, SALT_SIZE, IV_SIZE)
val cipherTextBytes = Arrays.copyOfRange(ctBytes, IV_SIZE, ctBytes.size)
val md5 = MessageDigest.getInstance("MD5")
val keyAndIV = generateKeyAndIV(KEY_SIZE, IV_SIZE, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
decryptAES(
cipherTextBytes,
keyAndIV?.get(0) ?: ByteArray(32),
keyAndIV?.get(1) ?: ByteArray(16),
keyAndIV?.get(0) ?: ByteArray(KEY_SIZE),
keyAndIV?.get(1) ?: ByteArray(IV_SIZE),
)
} catch (e: Exception) {
""
@ -60,17 +61,17 @@ object CryptoAES {
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
val md5: MessageDigest = MessageDigest.getInstance("MD5")
val keyAndIV = generateKeyAndIV(
32,
16,
KEY_SIZE,
IV_SIZE,
1,
salt.decodeHex(),
password.toByteArray(Charsets.UTF_8),
md5
md5,
)
decryptAES(
ctBytes,
keyAndIV?.get(0) ?: ByteArray(32),
keyAndIV?.get(1) ?: ByteArray(16),
keyAndIV?.get(0) ?: ByteArray(KEY_SIZE),
keyAndIV?.get(1) ?: ByteArray(IV_SIZE),
)
} catch (e: Exception) {
""
@ -129,7 +130,14 @@ object CryptoAES {
* @param md the message digest algorithm to use
* @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 requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
@ -142,7 +150,7 @@ object CryptoAES {
// Digest data (last digest if available, password data, salt if available)
if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength)
md.update(password)
md.update(salt, 0, 8)
md.update(salt, 0, SALT_SIZE)
md.digest(generatedData, generatedLength, digestLength)
// additional rounds

View File

@ -13,26 +13,22 @@ import org.jsoup.nodes.Element
*/
/**
* 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
* Use if the attribute tag has a data:image string but real URLs are on a different attribute
*/
fun Element.dataImageAsUrl(attr: String): String {
return if (this.attr(attr).startsWith("data")) {
"https://127.0.0.1/?" + this.attr(attr).substringAfter(":")
fun Element.dataImageAsUrlOrNull(attr: String): String? {
return if (attr(attr).startsWith("data")) {
"https://127.0.0.1/?" + attr(attr).substringAfter(":")
} 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? {
return if (this.attr(attr).startsWith("data")) {
"https://127.0.0.1/?" + this.attr(attr).substringAfter(":")
} else {
null
}
fun Element.dataImageAsUrl(attr: String): String {
return dataImageAsUrlOrNull(attr) ?: attr("abs:$attr")
}
/**
@ -51,7 +47,7 @@ class DataImageInterceptor : Interceptor {
} else {
dataString.substringAfter(",").toByteArray()
}
val mediaType = mediaTypePattern.find(dataString)!!.value.toMediaTypeOrNull()
val mediaType = mediaTypePattern.find(dataString)?.value?.toMediaTypeOrNull()
Response.Builder().body(byteArray.toResponseBody(mediaType))
.request(chain.request())
.protocol(Protocol.HTTP_1_0)

View File

@ -10,14 +10,14 @@ class DoodExtractor(private val client: OkHttpClient) {
fun videoFromUrl(
url: String,
quality: String? = null,
redirect: Boolean = true
redirect: Boolean = true,
): Video? {
val newQuality = quality ?: "Doodstream" + if(redirect) " mirror" else ""
return try {
val newQuality = quality ?: "Doodstream" + if (redirect) " mirror" else ""
return runCatching {
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
val doodTld = newUrl.substringAfter("https://dood.").substringBefore("/")
val content = response.body.string()
if (!content.contains("'/pass_md5/")) return null
@ -28,23 +28,21 @@ class DoodExtractor(private val client: OkHttpClient) {
val videoUrlStart = client.newCall(
GET(
"https://dood.$doodTld/pass_md5/$md5",
Headers.headersOf("referer", newUrl)
)
Headers.headersOf("referer", newUrl),
),
).execute().body.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
Video(newUrl, newQuality, videoUrl, headers = doodHeaders(doodTld))
} catch (e: Exception) {
null
}
}.getOrNull()
}
fun videosFromUrl(
url: String,
quality: String? = null,
redirect: Boolean = true
redirect: Boolean = true,
): List<Video> {
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 {

View File

@ -16,4 +16,3 @@ android {
dependencies {
compileOnly(libs.bundles.common)
}

View File

@ -6,29 +6,35 @@ import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class FembedExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String = "", redirect: Boolean = false): List<Video> {
val videoApi = if (redirect) {
(runCatching { client.newCall(GET(url)).execute().request.url.toString()
.replace("/v/", "/api/source/") }.getOrNull() ?: return emptyList<Video>())
} else {
url.replace("/v/", "/api/source/")
}
val body = runCatching {
client.newCall(POST(videoApi)).execute().body.string()
val videoApi = when {
redirect -> runCatching {
client.newCall(GET(url)).execute().request.url.toString()
}.getOrNull() ?: return emptyList<Video>()
else -> url
}.replace("/v/", "/api/source/")
val jsonResponse = runCatching {
client.newCall(POST(videoApi)).execute().use {
json.decodeFromString<FembedResponse>(it.body.string())
}
}.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) {
jsonResponse.data.map {
val quality = ("Fembed:${it.label}").let {
if (prefix.isNotBlank()) "$prefix $it"
else it
return jsonResponse.data.map {
val quality = ("Fembed:${it.label}").let {
if (prefix.isNotBlank()) {
"$prefix $it"
} else {
it
}
Video(it.file, quality, it.file)
}
} else { emptyList<Video>() }
Video(it.file, quality, it.file)
}
}
}

View File

@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
@Serializable
data class FembedResponse(
val success: Boolean,
val data: List<FembedVideo> = emptyList()
val data: List<FembedVideo> = emptyList(),
)
@Serializable
data class FembedVideo(
val file: String,
val label: String
)
val label: String,
)

View File

@ -12,7 +12,6 @@ android {
}
}
dependencies {
compileOnly(libs.bundles.common)
implementation(project(":lib-unpacker"))

View File

@ -5,7 +5,7 @@ plugins {
android {
compileSdk = AndroidConfig.compileSdk
namespace = "eu.kanade.tachiyomi.lib.okruextractor"
namespace = "eu.kanade.tachiyomi.lib.okruextractor"
defaultConfig {
minSdk = AndroidConfig.minSdk

View File

@ -16,7 +16,7 @@ class OkruExtractor(private val client: OkHttpClient) {
Pair("sd", "480p"),
Pair("low", "360p"),
Pair("lowest", "240p"),
Pair("mobile", "144p")
Pair("mobile", "144p"),
)
return qualities.find { it.first == quality }?.second ?: quality
}
@ -33,16 +33,24 @@ class OkruExtractor(private val client: OkHttpClient) {
.substringBefore("\\\"")
.replace("\\\\u0026", "&")
val quality = it.substringBefore("\\\"").let {
if (fixQualities) fixQuality(it)
else it
if (fixQualities) {
fixQuality(it)
} else {
it
}
}
val videoQuality = ("Okru:" + quality).let {
if (prefix.isNotBlank()) "$prefix $it"
else it
if (prefix.isNotBlank()) {
"$prefix $it"
} else {
it
}
}
if (videoUrl.startsWith("https://"))
if (videoUrl.startsWith("https://")) {
Video(videoUrl, videoQuality, videoUrl)
else null
} else {
null
}
}
}
}

View File

@ -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 newHeaders = if (manualData) {
headers
@ -78,7 +85,7 @@ class StreamSBExtractor(private val client: OkHttpClient) {
.set("authority", "embedsb.com")
.build()
}
return try {
return runCatching {
val master = if (manualData) trimmedUrl else fixUrl(trimmedUrl, common)
val request = client.newCall(GET(master, newHeaders)).execute()
@ -86,6 +93,7 @@ class StreamSBExtractor(private val client: OkHttpClient) {
if (request.code == 200) {
request.use { it.body.string() }
} else {
request.close()
updateEndpoint()
client.newCall(GET(fixUrl(trimmedUrl, common), newHeaders))
.execute()
@ -94,9 +102,9 @@ class StreamSBExtractor(private val client: OkHttpClient) {
)
val masterUrl = json.stream_data.file.trim('"')
val subtitleList = json.stream_data.subs?.let {
it.map { s -> Track(s.file, s.label) }
} ?: emptyList()
val subtitleList = json.stream_data.subs
?.map { Track(it.file, it.label) }
?: emptyList()
val masterPlaylist = client.newCall(GET(masterUrl, newHeaders))
.execute()
@ -118,28 +126,16 @@ class StreamSBExtractor(private val client: OkHttpClient) {
.substringAfter("x")
.substringBefore(",") + "p"
val quality = ("StreamSB:" + resolution).let {
if (prefix.isNotBlank()) {
"$prefix $it"
} else {
it
}
}.let {
if (suffix.isNotBlank()) {
"$it $suffix"
} else {
it
buildString {
if (prefix.isNotBlank()) append("$prefix ")
append(it)
if (prefix.isNotBlank()) append(" $suffix")
}
}
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)
}
} catch (e: Exception) {
emptyList<Video>()
}
}.getOrNull() ?: emptyList<Video>()
}
fun videosFromDecryptedUrl(realUrl: String, headers: Headers, prefix: String = "", suffix: String = ""): List<Video> {

View File

@ -4,17 +4,17 @@ import kotlinx.serialization.Serializable
@Serializable
data class Response(
val stream_data: ResponseObject
val stream_data: ResponseObject,
) {
@Serializable
data class ResponseObject(
val file: String,
val subs: List<Subtitle>? = null
val subs: List<Subtitle>? = null,
)
}
@Serializable
data class Subtitle(
val label: String,
val file: String
val file: String,
)

View File

@ -10,7 +10,7 @@ class StreamTapeExtractor(private val client: OkHttpClient) {
val baseUrl = "https://streamtape.com/e/"
val newUrl = if (!url.startsWith(baseUrl)) {
// ["https", "", "<domain>", "<???>", "<id>", ...]
val id = runCatching { url.split("/").get(4) }.getOrNull() ?: return null
val id = url.split("/").getOrNull(4) ?: return null
baseUrl + id
} else { url }
val document = client.newCall(GET(newUrl)).execute().asJsoup()

View File

@ -7,25 +7,25 @@ import app.cash.quickjs.QuickJs
*/
object Deobfuscator {
fun deobfuscateScript(source: String): String? {
val engine = QuickJs.create()
val originalScript = javaClass.getResource("/assets/$SCRIPT_NAME")
?.readText() ?: return null
// Sadly needed until QuickJS properly supports module imports:
// Regex for finding one and two in "export{one as Deobfuscator,two as Transformer};"
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 replacement = "const Deobfuscator = $deob, Transformer = $trans;"
originalScript.replace(match.value, replacement)
}
engine.evaluate("globalThis.console = { log: () => {}, warn: () => {}, error: () => {}, trace: () => {} };")
engine.evaluate(synchronyScript)
} ?: return null
engine.set("source", TestInterface::class.java, object: TestInterface { override fun getValue() = source })
val result = engine.evaluate("new Deobfuscator().deobfuscateSource(source.getValue())") as? String
engine.close()
return result
return QuickJs.create().use { engine ->
engine.evaluate("globalThis.console = { log: () => {}, warn: () => {}, error: () => {}, trace: () => {} };")
engine.evaluate(synchronyScript)
engine.set("source", TestInterface::class.java, object : TestInterface { override fun getValue() = source })
engine.evaluate("new Deobfuscator().deobfuscateSource(source.getValue())") as? String
}
}
@Suppress("unused")

View File

@ -12,7 +12,7 @@ class YourUploadExtractor(private val client: OkHttpClient) {
return runCatching {
val request = client.newCall(GET(url, headers = newHeaders)).execute()
val document = request.asJsoup()
val baseData = document.selectFirst("script:containsData(jwplayerOptions)")!!.data()
val baseData = document.selectFirst("script:containsData(jwplayerOptions)")?.data()
if (!baseData.isNullOrEmpty()) {
val basicUrl = baseData.substringAfter("file: '").substringBefore("',")
val quality = prefix + name

View File

@ -132,12 +132,12 @@ abstract class DooPlay(
return SEpisode.create().apply {
val epNum = element.selectFirst("div.numerando")!!.text()
.trim()
.let {
episodeNumberRegex.find(it)?.groupValues?.last() ?: "0"
}
.let(episodeNumberRegex::find)
?.groupValues
?.last() ?: "0"
val href = element.selectFirst("a[href]")!!
val episodeName = href.ownText()
episode_number = runCatching { epNum.toFloat() }.getOrDefault(0F)
episode_number = epNum.toFloatOrNull() ?: 0F
date_upload = element.selectFirst(episodeDateSelector)
?.text()
?.toDate() ?: 0L
@ -250,7 +250,7 @@ abstract class DooPlay(
override fun animeDetailsParse(document: Document): SAnime {
val doc = getRealAnimeDoc(document)
val sheader = doc.selectFirst("div.sheader")!!
val anime = SAnime.create().apply {
return SAnime.create().apply {
setUrlWithoutDomain(doc.location())
sheader.selectFirst("div.poster > img")!!.let {
thumbnail_url = it.getImageUrl()
@ -261,7 +261,7 @@ abstract class DooPlay(
genre = sheader.select("div.data > div.sgeneros > a")
.eachText()
.joinToString(", ")
.joinToString()
doc.selectFirst(additionalInfoSelector)?.let { info ->
description = buildString {
@ -272,7 +272,6 @@ abstract class DooPlay(
}
}
}
return anime
}
// =============================== Latest ===============================