From c155157ffbff5d85bde8b85e8d0b6322f1097833 Mon Sep 17 00:00:00 2001 From: Claudemirovsky <63046606+Claudemirovsky@users.noreply.github.com> Date: Mon, 2 Oct 2023 07:11:33 -0300 Subject: [PATCH] fix(lib): Fix chillx-extractor + refactorate tokuzilla (#2303) --- .../lib/chillxextractor/ChillxExtractor.kt | 26 ++- src/en/tokuzilla/build.gradle | 10 +- .../animeextension/en/tokuzilla/Tokuzilla.kt | 198 +++++++----------- 3 files changed, 109 insertions(+), 125 deletions(-) diff --git a/lib/chillx-extractor/src/main/java/eu/kanade/tachiyomi/lib/chillxextractor/ChillxExtractor.kt b/lib/chillx-extractor/src/main/java/eu/kanade/tachiyomi/lib/chillxextractor/ChillxExtractor.kt index 6a4ee7da0..14b87f5c7 100644 --- a/lib/chillx-extractor/src/main/java/eu/kanade/tachiyomi/lib/chillxextractor/ChillxExtractor.kt +++ b/lib/chillx-extractor/src/main/java/eu/kanade/tachiyomi/lib/chillxextractor/ChillxExtractor.kt @@ -19,9 +19,8 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea private val playlistUtils by lazy { PlaylistUtils(client, headers) } companion object { - private const val KEY = "m4H6D9%0${'$'}N&F6rQ&" - - private val REGEX_MASTER_JS by lazy { Regex("""MasterJS\s*=\s*'([^']+)""") } + private val REGEX_MASTER_JS by lazy { Regex("""JScript\s*=\s*'([^']+)""") } + private val REGEX_EVAL_KEY by lazy { Regex("""eval\(\S+\("(\S+)",\d+,"(\S+)",(\d+),(\d+),""") } private val REGEX_SOURCES by lazy { Regex("""sources:\s*\[\{"file":"([^"]+)""") } private val REGEX_FILE by lazy { Regex("""file: ?"([^"]+)"""") } private val REGEX_SOURCE by lazy { Regex("""source = ?"([^"]+)"""")} @@ -37,7 +36,8 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList() val aesJson = json.decodeFromString(master) - val decryptedScript = decryptWithSalt(aesJson.ciphertext, aesJson.salt, KEY) + val key = getKey(body) + val decryptedScript = decryptWithSalt(aesJson.ciphertext, aesJson.salt, key) .replace("\\n", "\n") .replace("\\", "") @@ -80,6 +80,24 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea ) } + private fun getKey(body: String): String { + val (encrypted, pass, offset, index) = REGEX_EVAL_KEY.find(body)!!.groupValues.drop(1) + val decrypted = decryptScript(encrypted, pass, offset.toInt(), index.toInt()) + return decrypted.substringAfter("'").substringBefore("'") + } + + private fun decryptScript(encrypted: String, pass: String, offset: Int, index: Int): String { + val trimmedPass = pass.substring(0, index) + val bits = encrypted.split(pass[index]).map { item -> + trimmedPass.foldIndexed(item) { index, acc, it -> + acc.replace(it.toString(), index.toString()) + } + }.filter(String::isNotBlank) + + return bits.joinToString("") { Char(it.toInt(index) - offset).toString() } + } + + @Serializable data class CryptoInfo( @SerialName("ct") diff --git a/src/en/tokuzilla/build.gradle b/src/en/tokuzilla/build.gradle index 0c592340c..8611fba00 100644 --- a/src/en/tokuzilla/build.gradle +++ b/src/en/tokuzilla/build.gradle @@ -1,11 +1,13 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} ext { extName = 'Tokuzilla' pkgNameSuffix = 'en.tokuzilla' extClass = '.Tokuzilla' - extVersionCode = 9 + extVersionCode = 10 libVersion = '13' containsNsfw = false } @@ -14,4 +16,4 @@ dependencies { implementation(project(':lib-chillx-extractor')) } -apply from: "$rootDir/common.gradle" \ No newline at end of file +apply from: "$rootDir/common.gradle" diff --git a/src/en/tokuzilla/src/eu/kanade/tachiyomi/animeextension/en/tokuzilla/Tokuzilla.kt b/src/en/tokuzilla/src/eu/kanade/tachiyomi/animeextension/en/tokuzilla/Tokuzilla.kt index 28eb27728..03221d053 100644 --- a/src/en/tokuzilla/src/eu/kanade/tachiyomi/animeextension/en/tokuzilla/Tokuzilla.kt +++ b/src/en/tokuzilla/src/eu/kanade/tachiyomi/animeextension/en/tokuzilla/Tokuzilla.kt @@ -1,13 +1,11 @@ package eu.kanade.tachiyomi.animeextension.en.tokuzilla import android.app.Application -import android.content.SharedPreferences import androidx.preference.ListPreference import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.model.AnimeFilter 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.SEpisode import eu.kanade.tachiyomi.animesource.model.Video @@ -32,101 +30,42 @@ class Tokuzilla : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override val supportsLatest = false - private val preferences: SharedPreferences by lazy { + private val preferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } // ============================== Popular =============================== + override fun popularAnimeSelector() = "div.col-sm-4.col-xs-12.item.post" - override fun popularAnimeSelector(): String = "div.col-sm-4.col-xs-12.item.post" + override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page") - override fun popularAnimeRequest(page: Int): Request { - return GET("$baseUrl/page/$page") - } - - override fun popularAnimeFromElement(element: Element): SAnime { - val anime = SAnime.create() - anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").replace("https://tokuzilla.net", "")) - anime.thumbnail_url = element.selectFirst("img")!!.attr("src") - anime.title = element.selectFirst("a")!!.attr("title") - return anime - } - - override fun popularAnimeNextPageSelector(): String = "a.next.page-numbers" - - // ============================== Episodes ============================== - - override fun episodeListSelector() = throw Exception("not used") - - override fun episodeListParse(response: Response): List { - val document = response.asJsoup() - val episodeList = mutableListOf() - val infoElement = document.selectFirst("ul.pagination.post-tape") - if (infoElement != null) { - infoElement.html().split("").substringAfter(">") - val episode = SEpisode.create() - episode.setUrlWithoutDomain(link) - episode.episode_number = episodeNumber.toFloat() - episode.name = "Episode $episodeNumber" - episodeList.add(episode) - } - } else { - val link = document.selectFirst("meta[property=og:url]")!!.attr("content") - val episode = SEpisode.create() - episode.setUrlWithoutDomain(link) - episode.episode_number = 1F - episode.name = "Movie" - episodeList.add(episode) + override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { + element.selectFirst("a")!!.run { + setUrlWithoutDomain(attr("href")) + title = attr("title") } - return episodeList.reversed() + thumbnail_url = element.selectFirst("img")!!.attr("src") } - override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used") + override fun popularAnimeNextPageSelector() = "a.next.page-numbers" - // ============================ Video Links ============================= + // =============================== Latest =============================== + override fun latestUpdatesNextPageSelector() = throw Exception("not used") - override fun videoListParse(response: Response): List").substringBefore("")) - append(", ") + genre = details.select("span.meta > a").eachText().joinToString().takeIf(String::isNotBlank) + description = document.selectFirst("h2#plot + p")!!.text() + author = details.selectFirst("th:contains(Year) + td")?.text()?.let { "Year $it" } + status = details.selectFirst("th:contains(Status) + td")?.text().orEmpty().let { + when { + it.contains("Ongoing") -> SAnime.ONGOING + it.contains("Complete") -> SAnime.COMPLETED + else -> SAnime.UNKNOWN } } - return genres.dropLast(2) } - private fun parseYear(yearString: String): String { - return "Year " + yearString.substringAfter("Year").substring(47, 51) - } + // ============================== Episodes ============================== + override fun episodeListSelector() = "ul.pagination.post-tape a" + override fun episodeListParse(response: Response): List { + val document = response.use { it.asJsoup() } - private fun parseStatus(statusString: String): Int { - return if (statusString.contains("Ongoing")) { - SAnime.ONGOING + val episodes = document.select(episodeListSelector()) + return if (episodes.isNotEmpty()) { + episodes.map { + SEpisode.create().apply { + setUrlWithoutDomain(it.attr("href")) + val epNum = it.text() + name = "Episode $epNum" + episode_number = epNum.toFloatOrNull() ?: 1F + } + }.reversed() } else { - SAnime.COMPLETED + SEpisode.create().apply { + setUrlWithoutDomain(document.selectFirst("meta[property=og:url]")!!.attr("content")) + episode_number = 1F + name = "Movie" + }.let(::listOf) } } - // =============================== Latest =============================== + override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used") - override fun latestUpdatesNextPageSelector(): String = throw Exception("not used") + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List