Zoro: add search filters and URL intent handler (#887)

This commit is contained in:
Claudemirovsky
2022-09-25 07:45:07 -03:00
committed by GitHub
parent 8bdff87ae2
commit d33029e3f4
6 changed files with 424 additions and 46 deletions

View File

@ -1,2 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" /> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".en.zoro.ZoroUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="zoro.to"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -5,8 +5,12 @@ ext {
extName = 'zoro.to (experimental)' extName = 'zoro.to (experimental)'
pkgNameSuffix = 'en.zoro' pkgNameSuffix = 'en.zoro'
extClass = '.Zoro' extClass = '.Zoro'
extVersionCode = 10 extVersionCode = 11
libVersion = '13' libVersion = '13'
} }
dependencies {
compileOnly libs.bundles.coroutines
}
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -8,12 +8,18 @@ import eu.kanade.tachiyomi.animeextension.en.zoro.extractors.ZoroExtractor
import eu.kanade.tachiyomi.animeextension.en.zoro.utils.JSONUtil import eu.kanade.tachiyomi.animeextension.en.zoro.utils.JSONUtil
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@ -21,12 +27,15 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -55,13 +64,11 @@ class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/most-popular?page=$page") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/most-popular?page=$page")
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
val anime = SAnime.create() thumbnail_url = element.selectFirst("div.film-poster > img").attr("data-src")
anime.thumbnail_url = element.select("div.film-poster > img").attr("data-src") val filmDetail = element.selectFirst("div.film-detail a")
anime.setUrlWithoutDomain(element.select("div.film-detail a").attr("href")) setUrlWithoutDomain(filmDetail.attr("href"))
anime.title = element.select("div.film-detail a").attr("data-jname") title = filmDetail.attr("data-jname")
anime.description = element.selectFirst("div.film-detail div.description")?.text()
return anime
} }
override fun popularAnimeNextPageSelector(): String = "li.page-item a[title=Next]" override fun popularAnimeNextPageSelector(): String = "li.page-item a[title=Next]"
@ -106,27 +113,29 @@ class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val data = body.substringAfter("\"html\":\"").substringBefore("<script>") val data = body.substringAfter("\"html\":\"").substringBefore("<script>")
val unescapedData = JSONUtil.unescape(data) val unescapedData = JSONUtil.unescape(data)
val serversHtml = Jsoup.parse(unescapedData) val serversHtml = Jsoup.parse(unescapedData)
val videoList = mutableListOf<Video>() val ignoredServers = listOf("StreamSB", "StreamTape")
for (server in serversHtml.select("div.server-item")) { val extractor = ZoroExtractor(client)
if (server.text() == "StreamSB" || server.text() == "Streamtape") continue val videoList = serversHtml.select("div.server-item")
val id = server.attr("data-id") .filterNot { it.text() in ignoredServers }
val subDub = server.attr("data-type") .parallelMap { server ->
val videos = runCatching { val id = server.attr("data-id")
getVideosFromServer( val subDub = server.attr("data-type")
client.newCall(GET("$baseUrl/ajax/v2/episode/sources?id=$id", episodeReferer)).execute(), val url = "$baseUrl/ajax/v2/episode/sources?id=$id"
subDub val reqBody = client.newCall(GET(url, episodeReferer)).execute()
) .body!!.string()
}.getOrNull() val sourceUrl = reqBody.substringAfter("\"link\":\"")
if (videos != null) videoList.addAll(videos) .substringBefore("\"") + "&autoPlay=1&oa=0"
} runCatching {
val source = extractor.getSourcesJson(sourceUrl)
source?.let { getVideosFromServer(it, subDub) }
}.getOrNull()
}
.filterNotNull()
.flatten()
return videoList return videoList
} }
private fun getVideosFromServer(response: Response, subDub: String): List<Video>? { private fun getVideosFromServer(source: String, subDub: String): List<Video>? {
val body = response.body!!.string()
val url = body.substringAfter("\"link\":\"").substringBefore("\"") + "&autoPlay=1&oa=0"
val source = ZoroExtractor(client).getSourcesJson(url) ?: return null
if (!source.contains("{\"sources\":[{\"file\":\"")) return null if (!source.contains("{\"sources\":[{\"file\":\"")) return null
val json = json.decodeFromString<JsonObject>(source) val json = json.decodeFromString<JsonObject>(source)
val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"]!!.jsonPrimitive.content val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"]!!.jsonPrimitive.content
@ -141,18 +150,20 @@ class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} catch (e: Error) {} } catch (e: Error) {}
} ?: emptyList() } ?: emptyList()
val subs = subLangOrder(subs2) val subs = subLangOrder(subs2)
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body!!.string() val prefix = "#EXT-X-STREAM-INF:"
val videoList = mutableListOf<Video>() val playlist = client.newCall(GET(masterUrl)).execute()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach { .body!!.string()
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p - $subDub" val videoList = playlist.substringAfter(prefix).split(prefix).map {
val videoUrl = masterUrl.substringBeforeLast("/") + "/" + it.substringAfter("\n").substringBefore("\n") val quality = it.substringAfter("RESOLUTION=")
videoList.add( .substringAfter("x")
try { .substringBefore(",") + "p - $subDub"
Video(videoUrl, quality, videoUrl, subtitleTracks = subs) val videoUrl = masterUrl.substringBeforeLast("/") + "/" +
} catch (e: Error) { it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl) try {
} Video(videoUrl, quality, videoUrl, subtitleTracks = subs)
) } catch (e: Error) {
Video(videoUrl, quality, videoUrl)
}
} }
return videoList return videoList
} }
@ -169,6 +180,7 @@ class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
for (video in this) { for (video in this) {
if (item in video.quality) { if (item in video.quality) {
newList.add(preferred, video) newList.add(preferred, video)
preferred++
} else { } else {
newList.add(video) newList.add(video)
} }
@ -179,7 +191,7 @@ class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, "720p")!! val quality = preferences.getString(PREF_QUALITY_KEY, "720p")!!
val type = preferences.getString(PREF_TYPE_KEY, "dub")!! val type = preferences.getString(PREF_TYPE_KEY, "dub")!!
val newList = this.sortIfContains(type).reversed().sortIfContains(quality) val newList = this.sortIfContains(type).sortIfContains(quality)
return newList return newList
} }
@ -208,7 +220,53 @@ class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/search?keyword=$query&page=$page") override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$slug"))
.asObservableSuccess()
.map { response ->
searchAnimeBySlugParse(response, slug)
}
} else {
val params = ZoroFilters.getSearchParameters(filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
}
private fun searchAnimeBySlugParse(response: Response, slug: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/$slug"
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: ZoroFilters.FilterSearchParams): Request {
val url = "$baseUrl/search?".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("keyword", query)
.addIfNotBlank("type", filters.type)
.addIfNotBlank("status", filters.status)
.addIfNotBlank("rated", filters.rated)
.addIfNotBlank("score", filters.score)
.addIfNotBlank("season", filters.season)
.addIfNotBlank("language", filters.language)
.addIfNotBlank("sort", filters.sort)
.addIfNotBlank("sy", filters.start_year)
.addIfNotBlank("sm", filters.start_month)
.addIfNotBlank("ey", filters.end_year)
.addIfNotBlank("em", filters.end_month)
.addIfNotBlank("genres", filters.genres)
return GET(url.build().toString())
}
override fun getFilterList(): AnimeFilterList = ZoroFilters.filterList
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
@ -317,8 +375,21 @@ class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return if (full) "\n$tag $value" else value return if (full) "\n$tag $value" else value
} }
companion object { private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String):
HttpUrl.Builder {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
return this
}
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
const val PREFIX_SEARCH = "slug:"
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred video quality" private const val PREF_QUALITY_TITLE = "Preferred video quality"
private val PREF_QUALITY_ENTRIES = arrayOf("360p", "720p", "1080p") private val PREF_QUALITY_ENTRIES = arrayOf("360p", "720p", "1080p")

View File

@ -0,0 +1,231 @@
package eu.kanade.tachiyomi.animeextension.en.zoro
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object ZoroFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray()
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class TypeFilter : QueryPartFilter("Type", ZoroFiltersData.types)
class StatusFilter : QueryPartFilter("Status", ZoroFiltersData.status)
class RatedFilter : QueryPartFilter("Rated", ZoroFiltersData.rated)
class ScoreFilter : QueryPartFilter("Score", ZoroFiltersData.scores)
class SeasonFilter : QueryPartFilter("Season", ZoroFiltersData.seasons)
class LanguageFilter : QueryPartFilter("Language", ZoroFiltersData.languages)
class SortFilter : QueryPartFilter("Sort by", ZoroFiltersData.sorts)
class StartYearFilter : QueryPartFilter("Start year", ZoroFiltersData.years)
class StartMonthFilter : QueryPartFilter("Start month", ZoroFiltersData.months)
class EndYearFilter : QueryPartFilter("End year", ZoroFiltersData.years)
class EndMonthFilter : QueryPartFilter("End month", ZoroFiltersData.months)
class GenresFilter : CheckBoxFilterList(
"Genres",
ZoroFiltersData.genres.map { CheckBoxVal(it.first, false) }
)
val filterList = AnimeFilterList(
TypeFilter(),
StatusFilter(),
RatedFilter(),
ScoreFilter(),
SeasonFilter(),
LanguageFilter(),
SortFilter(),
AnimeFilter.Separator(),
StartYearFilter(),
StartMonthFilter(),
EndYearFilter(),
EndMonthFilter(),
AnimeFilter.Separator(),
GenresFilter()
)
data class FilterSearchParams(
val type: String = "",
val status: String = "",
val rated: String = "",
val score: String = "",
val season: String = "",
val language: String = "",
val sort: String = "",
val start_year: String = "",
val start_month: String = "",
val end_year: String = "",
val end_month: String = "",
val genres: String = ""
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
val genres: String = filters.filterIsInstance<GenresFilter>()
.first()
.state.mapNotNull { format ->
if (format.state) {
ZoroFiltersData.genres.find { it.first == format.name }!!.second
} else { null }
}.joinToString(",")
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<RatedFilter>(),
filters.asQueryPart<ScoreFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<LanguageFilter>(),
filters.asQueryPart<SortFilter>(),
filters.asQueryPart<StartYearFilter>(),
filters.asQueryPart<StartMonthFilter>(),
filters.asQueryPart<EndYearFilter>(),
filters.asQueryPart<EndMonthFilter>(),
genres
)
}
private object ZoroFiltersData {
val all = Pair("All", "")
val types = arrayOf(
all,
Pair("Movie", "1"),
Pair("TV", "2"),
Pair("OVA", "3"),
Pair("ONA", "4"),
Pair("Special", "5"),
Pair("Music", "6")
)
val status = arrayOf(
all,
Pair("Finished Airing", "1"),
Pair("Currently Airing", "2"),
Pair("Not yet aired", "3")
)
val rated = arrayOf(
all,
Pair("G", "1"),
Pair("PG", "2"),
Pair("PG-13", "3"),
Pair("R", "4"),
Pair("R+", "5"),
Pair("Rx", "6")
)
val scores = arrayOf(
all,
Pair("(1) Appalling", "1"),
Pair("(2) Horrible", "2"),
Pair("(3) Very Bad", "3"),
Pair("(4) Bad", "4"),
Pair("(5) Average", "5"),
Pair("(6) Fine", "6"),
Pair("(7) Good", "7"),
Pair("(8) Very Good", "8"),
Pair("(9) Great", "9"),
Pair("(10) Masterpiece", "10")
)
val seasons = arrayOf(
all,
Pair("Spring", "1"),
Pair("Summer", "2"),
Pair("Fall", "3"),
Pair("Winter", "4")
)
val languages = arrayOf(
all,
Pair("SUB", "1"),
Pair("DUB", "2"),
Pair("SUB & DUB", "3")
)
val sorts = arrayOf(
Pair("Default", "default"),
Pair("Recently Added", "recently_added"),
Pair("Recently Updated", "recently_updated"),
Pair("Score", "score"),
Pair("Name A-Z", "name_az"),
Pair("Released Date", "released_date"),
Pair("Most Watched", "most_watched")
)
val years = arrayOf(all) + (1917..2022).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val months = arrayOf(all) + (1..12).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val genres = arrayOf(
Pair("Action", "1"),
Pair("Adventure", "2"),
Pair("Cars", "3"),
Pair("Comedy", "4"),
Pair("Dementia", "5"),
Pair("Demons", "6"),
Pair("Drama", "8"),
Pair("Ecchi", "9"),
Pair("Fantasy", "10"),
Pair("Game", "11"),
Pair("Harem", "35"),
Pair("Historical", "13"),
Pair("Horror", "14"),
Pair("Isekai", "44"),
Pair("Josei", "43"),
Pair("Kids", "15"),
Pair("Magic", "16"),
Pair("Martial Arts", "17"),
Pair("Mecha", "18"),
Pair("Military", "38"),
Pair("Music", "19"),
Pair("Mystery", "7"),
Pair("Parody", "20"),
Pair("Police", "39"),
Pair("Psychological", "40"),
Pair("Romance", "22"),
Pair("Samurai", "21"),
Pair("School", "23"),
Pair("Sci-Fi", "24"),
Pair("Seinen", "42"),
Pair("Shoujo", "25"),
Pair("Shoujo Ai", "26"),
Pair("Shounen", "27"),
Pair("Shounen Ai", "28"),
Pair("Slice of Life", "36"),
Pair("Space", "29"),
Pair("Sports", "30"),
Pair("Super Power", "31"),
Pair("Supernatural", "37"),
Pair("Thriller", "41"),
Pair("Vampire", "32"),
Pair("Yaoi", "33"),
Pair("Yuri", "34")
)
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.en.zoro
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://zoro.to/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class ZoroUrlActivity : Activity() {
private val TAG = "ZoroUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
val slug = pathSegments[0]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${Zoro.PREFIX_SEARCH}$slug")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.toString())
}
} else {
Log.e(TAG, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -7,7 +7,10 @@ import okhttp3.OkHttpClient
class ZoroExtractor(private val client: OkHttpClient) { class ZoroExtractor(private val client: OkHttpClient) {
// Prevent caching the .JS file // Prevent (automatic) caching the .JS file for different episodes, because it
// changes everytime, and a cached old .js will have a invalid AES password,
// invalidating the decryption algorithm.
// We cache it manually when initializing the class.
private val cacheControl = CacheControl.Builder().noStore().build() private val cacheControl = CacheControl.Builder().noStore().build()
private val newClient = client.newBuilder() private val newClient = client.newBuilder()
.cache(null) .cache(null)
@ -19,9 +22,15 @@ class ZoroExtractor(private val client: OkHttpClient) {
private const val SOURCES_URL = SERVER_URL + "/ajax/embed-6/getSources?id=" private const val SOURCES_URL = SERVER_URL + "/ajax/embed-6/getSources?id="
} }
fun getSourcesJson(url: String): String? { // This will create a lag of 1~3s at the initialization of the class, but the
val js = newClient.newCall(GET(JS_URL, cache = cacheControl)).execute() // speedup of the manual cache will be worth it.
private val cachedJs by lazy {
newClient.newCall(GET(JS_URL, cache = cacheControl)).execute()
.body!!.string() .body!!.string()
}
init { cachedJs }
fun getSourcesJson(url: String): String? {
val id = url.substringAfter("/embed-6/", "") val id = url.substringAfter("/embed-6/", "")
.substringBefore("?", "").ifEmpty { return null } .substringBefore("?", "").ifEmpty { return null }
val srcRes = newClient.newCall(GET(SOURCES_URL + id, cache = cacheControl)) val srcRes = newClient.newCall(GET(SOURCES_URL + id, cache = cacheControl))
@ -30,7 +39,7 @@ class ZoroExtractor(private val client: OkHttpClient) {
if ("\"encrypted\":false" in srcRes) return srcRes if ("\"encrypted\":false" in srcRes) return srcRes
if (!srcRes.contains("{\"sources\":")) return null if (!srcRes.contains("{\"sources\":")) return null
val encrypted = srcRes.substringAfter("sources\":\"").substringBefore("\"") val encrypted = srcRes.substringAfter("sources\":\"").substringBefore("\"")
val decrypted = Decryptor.decrypt(encrypted, js) ?: return null val decrypted = Decryptor.decrypt(encrypted, cachedJs) ?: return null
val end = srcRes.replace("\"$encrypted\"", decrypted) val end = srcRes.replace("\"$encrypted\"", decrypted)
return end return end
} }