[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.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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -10,13 +10,13 @@ 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 ""
|
||||
val newQuality = quality ?: "Doodstream" + if (redirect) " mirror" else ""
|
||||
|
||||
return try {
|
||||
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()
|
||||
@ -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 {
|
||||
|
@ -16,4 +16,3 @@ android {
|
||||
dependencies {
|
||||
compileOnly(libs.bundles.common)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
@ -12,7 +12,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.bundles.common)
|
||||
implementation(project(":lib-unpacker"))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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 ===============================
|
||||
|
Loading…
x
Reference in New Issue
Block a user