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 { dependencies {
implementation(project(":lib-dood-extractor")) implementation(project(":lib-dood-extractor"))
implementation(project(":lib-cryptoaes"))
} }

View File

@ -6,7 +6,6 @@ import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
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
@ -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.dto.VideoDto
import eu.kanade.tachiyomi.multisrc.dopeflix.extractors.DopeFlixExtractor import eu.kanade.tachiyomi.multisrc.dopeflix.extractors.DopeFlixExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
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
@ -54,15 +49,9 @@ abstract class DopeFlix(
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val json = Json { override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
ignoreUnknownKeys = true
}
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/")
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster" override fun popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request {
@ -70,48 +59,103 @@ abstract class DopeFlix(
return GET("$baseUrl/$type?page=$page") return GET("$baseUrl/$type?page=$page")
} }
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
val anime = SAnime.create() val ahref = element.selectFirst("a")!!
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href")) setUrlWithoutDomain(ahref.attr("href"))
anime.thumbnail_url = element.selectFirst("img")!!.attr("data-src") title = ahref.attr("title")
anime.title = element.selectFirst("a")!!.attr("title") thumbnail_url = element.selectFirst("img")!!.attr("data-src")
return anime
} }
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 ============================== // ============================== Episodes ==============================
override fun episodeListSelector() = throw Exception("not used") override fun episodeListSelector() = throw Exception("not used")
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.use { it.asJsoup() }
val episodeList = mutableListOf<SEpisode>() val infoElement = document.selectFirst("div.detail_page-watch")!!
val infoElement = document.select("div.detail_page-watch")
val id = infoElement.attr("data-id") val id = infoElement.attr("data-id")
val dataType = infoElement.attr("data-type") // Tv = 2 or movie = 1 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 seasonUrl = "$baseUrl/ajax/v2/tv/seasons/$id"
val seasonsHtml = client.newCall( val seasonsHtml = client.newCall(
GET( GET(
seasonUrl, seasonUrl,
headers = Headers.headersOf("Referer", document.location()), headers = Headers.headersOf("Referer", document.location()),
), ),
).execute().asJsoup() ).execute().use { it.asJsoup() }
val seasonsElements = seasonsHtml.select("a.dropdown-item.ss-item") seasonsHtml
seasonsElements.forEach { .select("a.dropdown-item.ss-item")
val seasonEpList = parseEpisodesFromSeries(it) .flatMap(::parseEpisodesFromSeries)
episodeList.addAll(seasonEpList) .reversed()
}
} else { } else {
val movieUrl = "$baseUrl/ajax/movie/episodes/$id" val movieUrl = "$baseUrl/ajax/movie/episodes/$id"
val episode = SEpisode.create() SEpisode.create().apply {
episode.name = document.select("h2.heading-name").text() name = document.selectFirst("h2.heading-name")!!.text()
episode.episode_number = 1F episode_number = 1F
episode.setUrlWithoutDomain(movieUrl) setUrlWithoutDomain(movieUrl)
episodeList.add(episode) }.let(::listOf)
} }
return episodeList.reversed()
} }
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used") override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
@ -120,99 +164,77 @@ abstract class DopeFlix(
val seasonId = element.attr("data-id") val seasonId = element.attr("data-id")
val seasonName = element.text() val seasonName = element.text()
val episodesUrl = "$baseUrl/ajax/v2/season/episodes/$seasonId" val episodesUrl = "$baseUrl/ajax/v2/season/episodes/$seasonId"
val episodesHtml = client.newCall(GET(episodesUrl)) val episodesHtml = client.newCall(GET(episodesUrl)).execute()
.execute() .use { it.asJsoup() }
.asJsoup()
val episodeElements = episodesHtml.select("div.eps-item") val episodeElements = episodesHtml.select("div.eps-item")
return episodeElements.map { episodeFromElement(it, seasonName) } 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 episodeId = element.attr("data-id")
val epNum = element.selectFirst("div.episode-number")!!.text() val epNum = element.selectFirst("div.episode-number")!!.text()
val epName = element.selectFirst("h3.film-name a")!!.text() val epName = element.selectFirst("h3.film-name a")!!.text()
val episode = SEpisode.create().apply { name = "$seasonName $epNum $epName"
name = "$seasonName $epNum $epName" episode_number = "${seasonName.getNumber()}.${epNum.getNumber().padStart(3, '0')}".toFloatOrNull() ?: 1F
setUrlWithoutDomain("$baseUrl/ajax/v2/episode/servers/$episodeId") setUrlWithoutDomain("$baseUrl/ajax/v2/episode/servers/$episodeId")
}
return episode
} }
private fun getNumberFromEpsString(epsStr: String): String { private fun String.getNumber() = filter(Char::isDigit)
return epsStr.filter { it.isDigit() }
}
// ============================ Video Links ============================= // ============================ Video Links =============================
private val extractor by lazy { DopeFlixExtractor(client) }
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup() val doc = response.asJsoup()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!) val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val extractor = DopeFlixExtractor(client) return doc.select("ul.fss-list a.btn-play")
val videoList = doc.select("ul.fss-list a.btn-play")
.parallelMap { server -> .parallelMap { server ->
val name = server.selectFirst("span")!!.text() val name = server.selectFirst("span")!!.text()
val id = server.attr("data-id") val id = server.attr("data-id")
val url = "$baseUrl/ajax/sources/$id" val url = "$baseUrl/ajax/sources/$id"
val reqBody = client.newCall(GET(url, episodeReferer)).execute() val reqBody = client.newCall(GET(url, episodeReferer)).execute()
.body.string() .use { it.body.string() }
val sourceUrl = reqBody.substringAfter("\"link\":\"") val sourceUrl = reqBody.substringAfter("\"link\":\"")
.substringBefore("\"") .substringBefore("\"")
runCatching { runCatching {
when { when {
"DoodStream" in name -> "DoodStream" in name ->
DoodExtractor(client).videoFromUrl(sourceUrl)?.let { DoodExtractor(client).videoFromUrl(sourceUrl)
listOf(it) ?.let(::listOf)
}
"Vidcloud" in name || "UpCloud" in name -> { "Vidcloud" in name || "UpCloud" in name -> {
val source = extractor.getSourcesJson(sourceUrl) val video = extractor.getVideoDto(sourceUrl)
source?.let { getVideosFromServer(it, name) } getVideosFromServer(video, name)
} }
else -> null else -> null
} }
}.getOrNull() }.getOrNull() ?: emptyList()
} }.flatten()
.filterNotNull()
.flatten()
return videoList
} }
private fun getVideosFromServer(source: String, name: String): List<Video>? { private fun getVideosFromServer(video: VideoDto, name: String): List<Video> {
if (!source.contains("{\"sources\":[{\"file\":\"")) return null val masterUrl = video.sources.first().file
val response = json.decodeFromString<VideoDto>(source) val subs2 = video.tracks
val masterUrl = response.sources.first().file
val subs2 = response.tracks
?.filter { it.kind == "captions" } ?.filter { it.kind == "captions" }
?.mapNotNull { ?.mapNotNull { Track(it.file, it.label) }
runCatching { Track(it.file, it.label) }.getOrNull() ?: emptyList<Track>()
} ?: emptyList<Track>()
val subs = subLangOrder(subs2) val subs = subLangOrder(subs2)
if (masterUrl.contains("playlist.m3u8")) { if (masterUrl.contains("playlist.m3u8")) {
val prefix = "#EXT-X-STREAM-INF:" val prefix = "#EXT-X-STREAM-INF:"
val playlist = client.newCall(GET(masterUrl)).execute() val playlist = client.newCall(GET(masterUrl)).execute()
.body.string() .use { it.body.string() }
val videoList = playlist.substringAfter(prefix).split(prefix).map { return playlist.substringAfter(prefix).split(prefix).map {
val quality = "$name - " + it.substringAfter("RESOLUTION=") val quality = "$name - " + it.substringAfter("RESOLUTION=")
.substringAfter("x") .substringAfter("x")
.substringBefore("\n") .substringBefore("\n")
.substringBefore(",") + "p" .substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n") val videoUrl = it.substringAfter("\n").substringBefore("\n")
try { Video(videoUrl, quality, videoUrl, subtitleTracks = subs)
Video(videoUrl, quality, videoUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(videoUrl, quality, videoUrl)
}
} }
return videoList
} }
val defaultVideoList = listOf( return listOf(
try { Video(masterUrl, "$name - Default", masterUrl, subtitleTracks = subs),
Video(masterUrl, "$name - Default", masterUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(masterUrl, "$name - Default", masterUrl)
},
) )
return defaultVideoList
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
@ -235,86 +257,9 @@ abstract class DopeFlix(
override fun videoUrlParse(document: Document) = throw Exception("not used") 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 ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE title = PREF_DOMAIN_TITLE
entries = domainArray entries = domainArray
@ -328,8 +273,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val videoQualityPref = ListPreference(screen.context).apply {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST entries = PREF_QUALITY_LIST
@ -343,8 +289,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val subLangPref = ListPreference(screen.context).apply {
ListPreference(screen.context).apply {
key = PREF_SUB_KEY key = PREF_SUB_KEY
title = PREF_SUB_TITLE title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES entries = PREF_SUB_LANGUAGES
@ -358,8 +305,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val latestType = ListPreference(screen.context).apply {
ListPreference(screen.context).apply {
key = PREF_LATEST_KEY key = PREF_LATEST_KEY
title = PREF_LATEST_TITLE title = PREF_LATEST_TITLE
entries = PREF_LATEST_PAGES entries = PREF_LATEST_PAGES
@ -373,8 +321,9 @@ abstract class DopeFlix(
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val popularType = ListPreference(screen.context).apply {
ListPreference(screen.context).apply {
key = PREF_POPULAR_KEY key = PREF_POPULAR_KEY
title = PREF_POPULAR_TITLE title = PREF_POPULAR_TITLE
entries = PREF_POPULAR_ENTRIES entries = PREF_POPULAR_ENTRIES
@ -388,17 +337,11 @@ abstract class DopeFlix(
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref)
screen.addPreference(subLangPref)
screen.addPreference(latestType)
screen.addPreference(popularType)
} }
// ============================= Utilities ============================== // ============================= 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 { runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() 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 import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object DopeFlixFilters { object DopeFlixFilters {
open class QueryPartFilter( open class QueryPartFilter(
displayName: String, displayName: String,
val vals: Array<Pair<String, 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 class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String { 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 { 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( private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>, options: Array<Pair<String, String>>,
): String { ): String {
return (this.getFirst<R>() as CheckBoxFilterList).state return (getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox -> .filter { it.state }
if (checkbox.state) { .map { checkbox -> options.find { it.first == checkbox.name }!!.second }
options.find { it.first == checkbox.name }!!.second .joinToString("-") { it.ifBlank { "all" } }
} else {
null
}
}.joinToString("-").let {
if (it.isBlank()) {
"all"
} else {
it
}
}
} }
class TypeFilter : QueryPartFilter("Type", DopeFlixFiltersData.TYPES) class TypeFilter : QueryPartFilter("Type", DopeFlixFiltersData.TYPES)
@ -106,6 +95,7 @@ object DopeFlixFilters {
val YEARS = arrayOf( val YEARS = arrayOf(
ALL, ALL,
Pair("2023", "2023"),
Pair("2022", "2022"), Pair("2022", "2022"),
Pair("2021", "2021"), Pair("2021", "2021"),
Pair("2020", "2020"), Pair("2020", "2020"),

View File

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

View File

@ -1,11 +1,19 @@
package eu.kanade.tachiyomi.multisrc.dopeflix.dto package eu.kanade.tachiyomi.multisrc.dopeflix.dto
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable @Serializable
data class VideoDto( data class VideoDto(
val sources: List<VideoLink>, 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 @Serializable

View File

@ -1,18 +1,97 @@
package eu.kanade.tachiyomi.multisrc.dopeflix.extractors 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 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.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class DopeFlixExtractor(private val client: OkHttpClient) { class DopeFlixExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
companion object { companion object {
private const val SOURCES_PATH = "/ajax/embed-4/getSources?id=" 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/", "") val id = url.substringAfter("/embed-4/", "")
.substringBefore("?", "").ifEmpty { return null } .substringBefore("?", "").ifEmpty { throw Exception("I HATE THE ANTICHRIST") }
val serverUrl = url.substringBefore("/embed") val serverUrl = url.substringBefore("/embed")
val srcRes = client.newCall( val srcRes = client.newCall(
GET( GET(
@ -23,17 +102,11 @@ class DopeFlixExtractor(private val client: OkHttpClient) {
.execute() .execute()
.body.string() .body.string()
val key = client.newCall(GET("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt")) val data = json.decodeFromString<SourceResponseDto>(srcRes)
.execute() if (!data.encrypted) return json.decodeFromString<VideoDto>(srcRes)
.body.string()
// encrypted data will start with "U2Fsd..." because they put val ciphered = data.sources.jsonPrimitive.content.toString()
// "Salted__" at the start of encrypted data, thanks openssl val decrypted = json.decodeFromString<List<VideoLink>>(tryDecrypting(ciphered))
// if its not encrypted, then return it return VideoDto(decrypted, data.tracks)
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
} }
} }

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