[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.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 {

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

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

View File

@ -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>() }
} }
} }

View File

@ -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,
) )

View File

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

View File

@ -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
}
} }
} }
} }

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 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> {

View File

@ -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,
) )

View File

@ -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()

View File

@ -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")

View File

@ -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

View File

@ -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 ===============================