fix(multisrc/dopeflix): Fix extractor + some refactor (#1983)

This commit is contained in:
Claudemirovsky
2023-07-30 09:37:48 +00:00
committed by GitHub
parent aa23330d6d
commit 7609b54cdc
7 changed files with 228 additions and 287 deletions

View File

@ -1,3 +1,4 @@
dependencies {
implementation(project(":lib-dood-extractor"))
implementation(project(":lib-cryptoaes"))
}

View File

@ -6,7 +6,6 @@ import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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.Track
@ -16,22 +15,18 @@ import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.multisrc.dopeflix.dto.VideoDto
import eu.kanade.tachiyomi.multisrc.dopeflix.extractors.DopeFlixExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -54,15 +49,9 @@ abstract class DopeFlix(
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json = Json {
ignoreUnknownKeys = true
}
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/")
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun popularAnimeRequest(page: Int): Request {
@ -70,48 +59,103 @@ abstract class DopeFlix(
return GET("$baseUrl/$type?page=$page")
}
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.thumbnail_url = element.selectFirst("img")!!.attr("data-src")
anime.title = element.selectFirst("a")!!.attr("title")
return anime
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
val ahref = element.selectFirst("a")!!
setUrlWithoutDomain(ahref.attr("href"))
title = ahref.attr("title")
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination li.page-item a[title=next]"
override fun popularAnimeNextPageSelector() = "ul.pagination li.page-item a[title=next]"
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/")
override fun latestUpdatesSelector(): String {
val sectionLabel = preferences.getString(PREF_LATEST_KEY, PREF_LATEST_DEFAULT)!!
return "section.block_area:has(h2.cat-heading:contains($sectionLabel)) div.film-poster"
}
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = DopeFlixFilters.getSearchParameters(filters)
val url = if (query.isNotBlank()) {
val fixedQuery = query.replace(" ", "-")
"$baseUrl/search/$fixedQuery?page=$page"
} else {
"$baseUrl/filter?".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("type", params.type)
.addQueryParameter("quality", params.quality)
.addQueryParameter("release_year", params.releaseYear)
.addQueryParameter("genre", params.genres)
.addQueryParameter("country", params.countries)
.build()
.toString()
}
return GET(url, headers)
}
override fun getFilterList() = DopeFlixFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
thumbnail_url = document.selectFirst("img.film-poster-img")!!.attr("src")
title = document.selectFirst("img.film-poster-img")!!.attr("title")
genre = document.select("div.row-line:contains(Genre) a").eachText().joinToString()
description = document.selectFirst("div.detail_page-watch div.description")!!
.text().replace("Overview:", "")
author = document.select("div.row-line:contains(Production) a").eachText().joinToString()
status = parseStatus(document.selectFirst("li.status span.value")?.text())
}
private fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()) {
"Ongoing" -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = throw Exception("not used")
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val infoElement = document.select("div.detail_page-watch")
val document = response.use { it.asJsoup() }
val infoElement = document.selectFirst("div.detail_page-watch")!!
val id = infoElement.attr("data-id")
val dataType = infoElement.attr("data-type") // Tv = 2 or movie = 1
if (dataType == "2") {
return if (dataType == "2") {
val seasonUrl = "$baseUrl/ajax/v2/tv/seasons/$id"
val seasonsHtml = client.newCall(
GET(
seasonUrl,
headers = Headers.headersOf("Referer", document.location()),
),
).execute().asJsoup()
val seasonsElements = seasonsHtml.select("a.dropdown-item.ss-item")
seasonsElements.forEach {
val seasonEpList = parseEpisodesFromSeries(it)
episodeList.addAll(seasonEpList)
}
).execute().use { it.asJsoup() }
seasonsHtml
.select("a.dropdown-item.ss-item")
.flatMap(::parseEpisodesFromSeries)
.reversed()
} else {
val movieUrl = "$baseUrl/ajax/movie/episodes/$id"
val episode = SEpisode.create()
episode.name = document.select("h2.heading-name").text()
episode.episode_number = 1F
episode.setUrlWithoutDomain(movieUrl)
episodeList.add(episode)
SEpisode.create().apply {
name = document.selectFirst("h2.heading-name")!!.text()
episode_number = 1F
setUrlWithoutDomain(movieUrl)
}.let(::listOf)
}
return episodeList.reversed()
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
@ -120,99 +164,77 @@ abstract class DopeFlix(
val seasonId = element.attr("data-id")
val seasonName = element.text()
val episodesUrl = "$baseUrl/ajax/v2/season/episodes/$seasonId"
val episodesHtml = client.newCall(GET(episodesUrl))
.execute()
.asJsoup()
val episodesHtml = client.newCall(GET(episodesUrl)).execute()
.use { it.asJsoup() }
val episodeElements = episodesHtml.select("div.eps-item")
return episodeElements.map { episodeFromElement(it, seasonName) }
}
private fun episodeFromElement(element: Element, seasonName: String): SEpisode {
private fun episodeFromElement(element: Element, seasonName: String) = SEpisode.create().apply {
val episodeId = element.attr("data-id")
val epNum = element.selectFirst("div.episode-number")!!.text()
val epName = element.selectFirst("h3.film-name a")!!.text()
val episode = SEpisode.create().apply {
name = "$seasonName $epNum $epName"
setUrlWithoutDomain("$baseUrl/ajax/v2/episode/servers/$episodeId")
}
return episode
name = "$seasonName $epNum $epName"
episode_number = "${seasonName.getNumber()}.${epNum.getNumber().padStart(3, '0')}".toFloatOrNull() ?: 1F
setUrlWithoutDomain("$baseUrl/ajax/v2/episode/servers/$episodeId")
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
private fun String.getNumber() = filter(Char::isDigit)
// ============================ Video Links =============================
private val extractor by lazy { DopeFlixExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val extractor = DopeFlixExtractor(client)
val videoList = doc.select("ul.fss-list a.btn-play")
return doc.select("ul.fss-list a.btn-play")
.parallelMap { server ->
val name = server.selectFirst("span")!!.text()
val id = server.attr("data-id")
val url = "$baseUrl/ajax/sources/$id"
val reqBody = client.newCall(GET(url, episodeReferer)).execute()
.body.string()
.use { it.body.string() }
val sourceUrl = reqBody.substringAfter("\"link\":\"")
.substringBefore("\"")
runCatching {
when {
"DoodStream" in name ->
DoodExtractor(client).videoFromUrl(sourceUrl)?.let {
listOf(it)
}
DoodExtractor(client).videoFromUrl(sourceUrl)
?.let(::listOf)
"Vidcloud" in name || "UpCloud" in name -> {
val source = extractor.getSourcesJson(sourceUrl)
source?.let { getVideosFromServer(it, name) }
val video = extractor.getVideoDto(sourceUrl)
getVideosFromServer(video, name)
}
else -> null
}
}.getOrNull()
}
.filterNotNull()
.flatten()
return videoList
}.getOrNull() ?: emptyList()
}.flatten()
}
private fun getVideosFromServer(source: String, name: String): List<Video>? {
if (!source.contains("{\"sources\":[{\"file\":\"")) return null
val response = json.decodeFromString<VideoDto>(source)
val masterUrl = response.sources.first().file
val subs2 = response.tracks
private fun getVideosFromServer(video: VideoDto, name: String): List<Video> {
val masterUrl = video.sources.first().file
val subs2 = video.tracks
?.filter { it.kind == "captions" }
?.mapNotNull {
runCatching { Track(it.file, it.label) }.getOrNull()
} ?: emptyList<Track>()
?.mapNotNull { Track(it.file, it.label) }
?: emptyList<Track>()
val subs = subLangOrder(subs2)
if (masterUrl.contains("playlist.m3u8")) {
val prefix = "#EXT-X-STREAM-INF:"
val playlist = client.newCall(GET(masterUrl)).execute()
.body.string()
val videoList = playlist.substringAfter(prefix).split(prefix).map {
.use { it.body.string() }
return playlist.substringAfter(prefix).split(prefix).map {
val quality = "$name - " + it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore("\n")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
try {
Video(videoUrl, quality, videoUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(videoUrl, quality, videoUrl)
}
Video(videoUrl, quality, videoUrl, subtitleTracks = subs)
}
return videoList
}
val defaultVideoList = listOf(
try {
Video(masterUrl, "$name - Default", masterUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(masterUrl, "$name - Default", masterUrl)
},
return listOf(
Video(masterUrl, "$name - Default", masterUrl, subtitleTracks = subs),
)
return defaultVideoList
}
override fun List<Video>.sort(): List<Video> {
@ -235,86 +257,9 @@ abstract class DopeFlix(
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val params = DopeFlixFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: DopeFlixFilters.FilterSearchParams): Request {
val url = if (query.isNotBlank()) {
val fixedQuery = query.replace(" ", "-")
"$baseUrl/search/$fixedQuery?page=$page"
} else {
"$baseUrl/filter?".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("type", filters.type)
.addQueryParameter("quality", filters.quality)
.addQueryParameter("release_year", filters.releaseYear)
.addQueryParameter("genre", filters.genres)
.addQueryParameter("country", filters.countries)
.build()
.toString()
}
return GET(url, headers)
}
override fun getFilterList(): AnimeFilterList = DopeFlixFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create().apply {
thumbnail_url = document.selectFirst("img.film-poster-img")!!.attr("src")
title = document.selectFirst("img.film-poster-img")!!.attr("title")
genre = document.select("div.row-line:contains(Genre) a")
.joinToString(", ") { it.text() }
description = document.selectFirst("div.detail_page-watch div.description")!!
.text().replace("Overview:", "")
author = document.select("div.row-line:contains(Production) a")
.joinToString(", ") { it.text() }
status = parseStatus(document.select("li.status span.value").text())
}
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/")
override fun latestUpdatesSelector(): String {
val sectionLabel = preferences.getString(PREF_LATEST_KEY, PREF_LATEST_DEFAULT)!!
return "section.block_area:has(h2.cat-heading:contains($sectionLabel)) div.film-poster"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = domainArray
@ -328,8 +273,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
@ -343,8 +289,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subLangPref = ListPreference(screen.context).apply {
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES
@ -358,8 +305,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val latestType = ListPreference(screen.context).apply {
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_LATEST_KEY
title = PREF_LATEST_TITLE
entries = PREF_LATEST_PAGES
@ -373,8 +321,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val popularType = ListPreference(screen.context).apply {
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_POPULAR_KEY
title = PREF_POPULAR_TITLE
entries = PREF_POPULAR_ENTRIES
@ -388,17 +337,11 @@ abstract class DopeFlix(
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref)
screen.addPreference(subLangPref)
screen.addPreference(latestType)
screen.addPreference(popularType)
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}

View File

@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object DopeFlixFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
@ -20,30 +19,20 @@ object DopeFlixFilters {
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart()
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("-").let {
if (it.isBlank()) {
"all"
} else {
it
}
}
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.joinToString("-") { it.ifBlank { "all" } }
}
class TypeFilter : QueryPartFilter("Type", DopeFlixFiltersData.TYPES)
@ -106,6 +95,7 @@ object DopeFlixFilters {
val YEARS = arrayOf(
ALL,
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),

View File

@ -8,7 +8,7 @@ class DopeFlixGenerator : ThemeSourceGenerator {
override val themeClass = "DopeFlix"
override val baseVersionCode = 17
override val baseVersionCode = 18
override val sources = listOf(
SingleLang("DopeBox", "https://dopebox.to", "en", isNsfw = false, overrideVersionCode = 2),

View File

@ -1,11 +1,19 @@
package eu.kanade.tachiyomi.multisrc.dopeflix.dto
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable
data class VideoDto(
val sources: List<VideoLink>,
val tracks: List<TrackDto>?,
val tracks: List<TrackDto>? = null,
)
@Serializable
data class SourceResponseDto(
val sources: JsonElement,
val encrypted: Boolean = true,
val tracks: List<TrackDto>? = null,
)
@Serializable

View File

@ -1,18 +1,97 @@
package eu.kanade.tachiyomi.multisrc.dopeflix.extractors
import eu.kanade.tachiyomi.multisrc.dopeflix.utils.Decryptor
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.multisrc.dopeflix.dto.SourceResponseDto
import eu.kanade.tachiyomi.multisrc.dopeflix.dto.VideoDto
import eu.kanade.tachiyomi.multisrc.dopeflix.dto.VideoLink
import eu.kanade.tachiyomi.network.GET
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class DopeFlixExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
companion object {
private const val SOURCES_PATH = "/ajax/embed-4/getSources?id="
private const val SCRIPT_URL = "https://rabbitstream.net/js/player/prod/e4-player.min.js"
private val MUTEX = Mutex()
private var realIndexPairs: List<List<Int>> = emptyList()
private fun <R> runLocked(block: () -> R) = runBlocking(Dispatchers.IO) {
MUTEX.withLock { block() }
}
}
fun getSourcesJson(url: String): String? {
private fun generateIndexPairs(): List<List<Int>> {
val script = client.newCall(GET(SCRIPT_URL)).execute().use { it.body.string() }
return script.substringAfter("const ")
.substringBefore("()")
.substringBeforeLast(",")
.split(",")
.map {
val value = it.substringAfter("=")
when {
value.contains("0x") -> value.substringAfter("0x").toInt(16)
else -> value.toInt()
}
}
.drop(1)
.chunked(2)
.map(List<Int>::reversed) // just to look more like the original script
}
private fun cipherTextCleaner(data: String): Pair<String, String> {
val (password, ciphertext, _) = indexPairs.fold(Triple("", data, 0)) { previous, item ->
val start = item.first() + previous.third
val end = start + item.last()
val passSubstr = data.substring(start, end)
val passPart = previous.first + passSubstr
val cipherPart = previous.second.replace(passSubstr, "")
Triple(passPart, cipherPart, previous.third + item.last())
}
return Pair(ciphertext, password)
}
private val mutex = Mutex()
private var indexPairs: List<List<Int>>
get() {
return runLocked {
if (realIndexPairs.isEmpty()) {
realIndexPairs = generateIndexPairs()
}
realIndexPairs
}
}
set(value) {
runLocked {
if (realIndexPairs.isNotEmpty()) {
realIndexPairs = value
}
}
}
private fun tryDecrypting(ciphered: String, attempts: Int = 0): String {
if (attempts > 2) throw Exception("PLEASE NUKE DOPEBOX AND SFLIX")
val (ciphertext, password) = cipherTextCleaner(ciphered)
return CryptoAES.decrypt(ciphertext, password).ifEmpty {
indexPairs = emptyList() // force re-creation
tryDecrypting(ciphered, attempts + 1)
}
}
fun getVideoDto(url: String): VideoDto {
val id = url.substringAfter("/embed-4/", "")
.substringBefore("?", "").ifEmpty { return null }
.substringBefore("?", "").ifEmpty { throw Exception("I HATE THE ANTICHRIST") }
val serverUrl = url.substringBefore("/embed")
val srcRes = client.newCall(
GET(
@ -23,17 +102,11 @@ class DopeFlixExtractor(private val client: OkHttpClient) {
.execute()
.body.string()
val key = client.newCall(GET("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"))
.execute()
.body.string()
// encrypted data will start with "U2Fsd..." because they put
// "Salted__" at the start of encrypted data, thanks openssl
// if its not encrypted, then return it
if ("\"sources\":\"U2FsdGVk" !in srcRes) return srcRes
if (!srcRes.contains("{\"sources\":")) return null
val encrypted = srcRes.substringAfter("sources\":\"").substringBefore("\"")
val decrypted = Decryptor.decrypt(encrypted, key) ?: return null
val end = srcRes.replace("\"$encrypted\"", decrypted)
return end
val data = json.decodeFromString<SourceResponseDto>(srcRes)
if (!data.encrypted) return json.decodeFromString<VideoDto>(srcRes)
val ciphered = data.sources.jsonPrimitive.content.toString()
val decrypted = json.decodeFromString<List<VideoLink>>(tryDecrypting(ciphered))
return VideoDto(decrypted, data.tracks)
}
}

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.multisrc.dopeflix.utils
import android.util.Base64
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object Decryptor {
fun decrypt(encodedData: String, remoteKey: String): String? {
val saltedData = Base64.decode(encodedData, Base64.DEFAULT)
val salt = saltedData.copyOfRange(8, 16)
val ciphertext = saltedData.copyOfRange(16, saltedData.size)
val password = remoteKey.toByteArray()
val (key, iv) = generateKeyAndIv(password, salt) ?: return null
val keySpec = SecretKeySpec(key, "AES")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decryptedData = String(cipher.doFinal(ciphertext))
return decryptedData
}
// https://stackoverflow.com/a/41434590/8166854
private fun generateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1,
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.getDigestLength()
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
var generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0) {
md.update(
generatedData,
generatedLength - digestLength,
digestLength,
)
}
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
val result = listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize),
)
return result
} catch (e: DigestException) {
return null
}
}
}