Enable multisrc support (#1352)

* Re-add multisrc support

* Convert SFlix and DopeBox to multisrc

* Re-enable multisrc tasks on workflows
This commit is contained in:
Claudemirovsky
2023-03-02 08:37:11 -03:00
committed by GitHub
parent d42f5c6cc6
commit ec19a31c3d
39 changed files with 756 additions and 1209 deletions

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -1,16 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'DopeBox'
pkgNameSuffix = 'en.dopebox'
extClass = '.DopeBox'
extVersionCode = 17
libVersion = '13'
}
dependencies {
implementation(project(':lib-dood-extractor'))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

@ -1,460 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.dopebox.extractors.DopeBoxExtractor
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
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
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 kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
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
import uy.kohesive.injekt.injectLazy
class DopeBox : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "DopeBox"
override val baseUrl by lazy {
"https://" + preferences.getString(PREF_DOMAIN_KEY, "dopebox.to")!!
}
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun popularAnimeRequest(page: Int): Request {
val type = preferences.getString(PREF_POPULAR_KEY, "movie")!!
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 popularAnimeNextPageSelector(): String = "ul.pagination li.page-item a[title=next]"
// ============================== 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 id = infoElement.attr("data-id")
val dataType = infoElement.attr("data-type") // Tv = 2 or movie = 1
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)
}
} 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)
}
return episodeList.reversed()
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
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 episodeElements = episodesHtml.select("div.eps-item")
return episodeElements.map { episodeFromElement(it, seasonName) }
}
private fun episodeFromElement(element: Element, seasonName: String): SEpisode {
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
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val extractor = DopeBoxExtractor(client)
val videoList = 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()
val sourceUrl = reqBody.substringAfter("\"link\":\"")
.substringBefore("\"")
runCatching {
when {
"DoodStream" in name ->
DoodExtractor(client).videoFromUrl(sourceUrl)?.let {
listOf(it)
}
"Vidcloud" in name || "UpCloud" in name -> {
val source = extractor.getSourcesJson(sourceUrl)
source?.let { getVideosFromServer(it, name) }
}
else -> null
}
}.getOrNull()
}
.filterNotNull()
.flatten()
return videoList
}
private fun getVideosFromServer(source: String, name: String): List<Video>? {
if (!source.contains("{\"sources\":[{\"file\":\"")) return null
val json = json.decodeFromString<JsonObject>(source)
val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"]!!.jsonPrimitive.content
val subs2 = mutableListOf<Track>()
json["tracks"]?.jsonArray
?.filter { it.jsonObject["kind"]!!.jsonPrimitive.content == "captions" }
?.map { track ->
val trackUrl = track.jsonObject["file"]!!.jsonPrimitive.content
val lang = track.jsonObject["label"]!!.jsonPrimitive.content
try {
subs2.add(Track(trackUrl, lang))
} catch (e: Error) {}
} ?: emptyList()
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 {
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)
}
}
return videoList
}
val defaultVideoList = listOf(
try {
Video(masterUrl, "$name - Default", masterUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(masterUrl, "$name - Default", masterUrl)
},
)
return defaultVideoList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
private fun subLangOrder(tracks: List<Track>): List<Track> {
val language = preferences.getString(PREF_SUB_KEY, null)
if (language != null) {
val newList = mutableListOf<Track>()
var preferred = 0
for (track in tracks) {
if (track.lang.contains(language)) {
newList.add(preferred, track)
preferred++
} else {
newList.add(track)
}
}
return newList
}
return tracks
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = 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 = DopeBoxFilters.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: DopeBoxFilters.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 = DopeBoxFilters.filterList
// =========================== 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, "Movies")!!
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 {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_LIST
entryValues = PREF_DOMAIN_LIST
setDefaultValue("dopebox.to")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST
setDefaultValue("1080p")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subLangPref = ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES
entryValues = PREF_SUB_LANGUAGES
setDefaultValue("English")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val latestType = ListPreference(screen.context).apply {
key = PREF_LATEST_KEY
title = PREF_LATEST_TITLE
entries = PREF_LATEST_PAGES
entryValues = PREF_LATEST_PAGES
setDefaultValue("Movies")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val popularType = ListPreference(screen.context).apply {
key = PREF_POPULAR_KEY
title = PREF_POPULAR_TITLE
entries = PREF_POPULAR_ENTRIES
entryValues = PREF_POPULAR_VALUES
setDefaultValue("movie")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
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)
}
// ============================= Utilities ==============================
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
private const val PREF_DOMAIN_KEY = "preferred_domain_new"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private val PREF_DOMAIN_LIST = arrayOf("dopebox.to", "dopebox.se")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p")
private const val PREF_SUB_KEY = "preferred_subLang"
private const val PREF_SUB_TITLE = "Preferred sub language"
private val PREF_SUB_LANGUAGES = arrayOf(
"Arabic", "English", "French", "German", "Hungarian",
"Italian", "Japanese", "Portuguese", "Romanian", "Russian",
"Spanish",
)
private const val PREF_LATEST_KEY = "preferred_latest_page"
private const val PREF_LATEST_TITLE = "Preferred latest page"
private val PREF_LATEST_PAGES = arrayOf("Movies", "TV Shows")
private const val PREF_POPULAR_KEY = "preferred_popular_page_new"
private const val PREF_POPULAR_TITLE = "Preferred popular page"
private val PREF_POPULAR_ENTRIES = PREF_LATEST_PAGES
private val PREF_POPULAR_VALUES = arrayOf("movie", "tv-show")
}
}

View File

@ -1,187 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object DopeBoxFilters {
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.getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
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
}
}
}
class TypeFilter : QueryPartFilter("Type", DopeBoxFiltersData.types)
class QualityFilter : QueryPartFilter("Quality", DopeBoxFiltersData.qualities)
class ReleaseYearFilter : QueryPartFilter("Released at", DopeBoxFiltersData.years)
class GenresFilter : CheckBoxFilterList(
"Genres",
DopeBoxFiltersData.genres.map { CheckBoxVal(it.first, false) },
)
class CountriesFilter : CheckBoxFilterList(
"Countries",
DopeBoxFiltersData.countries.map { CheckBoxVal(it.first, false) },
)
val filterList = AnimeFilterList(
TypeFilter(),
QualityFilter(),
ReleaseYearFilter(),
AnimeFilter.Separator(),
GenresFilter(),
CountriesFilter(),
)
data class FilterSearchParams(
val type: String = "",
val quality: String = "",
val releaseYear: String = "",
val genres: String = "",
val countries: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<QualityFilter>(),
filters.asQueryPart<ReleaseYearFilter>(),
filters.parseCheckbox<GenresFilter>(DopeBoxFiltersData.genres),
filters.parseCheckbox<CountriesFilter>(DopeBoxFiltersData.countries),
)
}
private object DopeBoxFiltersData {
val all = Pair("All", "all")
val types = arrayOf(
all,
Pair("Movies", "movies"),
Pair("TV Shows", "tv"),
)
val qualities = arrayOf(
all,
Pair("HD", "HD"),
Pair("SD", "SD"),
Pair("CAM", "CAM"),
)
val years = arrayOf(
all,
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("Older", "older-2018"),
)
val genres = arrayOf(
Pair("Action", "10"),
Pair("Action & Adventure", "24"),
Pair("Adventure", "18"),
Pair("Animation", "3"),
Pair("Biography", "37"),
Pair("Comedy", "7"),
Pair("Crime", "2"),
Pair("Documentary", "11"),
Pair("Drama", "4"),
Pair("Family", "9"),
Pair("Fantasy", "13"),
Pair("History", "19"),
Pair("Horror", "14"),
Pair("Kids", "27"),
Pair("Music", "15"),
Pair("Mystery", "1"),
Pair("News", "34"),
Pair("Reality", "22"),
Pair("Romance", "12"),
Pair("Sci-Fi & Fantasy", "31"),
Pair("Science Fiction", "5"),
Pair("Soap", "35"),
Pair("Talk", "29"),
Pair("Thriller", "16"),
Pair("TV Movie", "8"),
Pair("War", "17"),
Pair("War & Politics", "28"),
Pair("Western", "6"),
)
val countries = arrayOf(
Pair("Argentina", "11"),
Pair("Australia", "151"),
Pair("Austria", "4"),
Pair("Belgium", "44"),
Pair("Brazil", "190"),
Pair("Canada", "147"),
Pair("China", "101"),
Pair("Czech Republic", "231"),
Pair("Denmark", "222"),
Pair("Finland", "158"),
Pair("France", "3"),
Pair("Germany", "96"),
Pair("Hong Kong", "93"),
Pair("Hungary", "72"),
Pair("India", "105"),
Pair("Ireland", "196"),
Pair("Israel", "24"),
Pair("Italy", "205"),
Pair("Japan", "173"),
Pair("Luxembourg", "91"),
Pair("Mexico", "40"),
Pair("Netherlands", "172"),
Pair("New Zealand", "122"),
Pair("Norway", "219"),
Pair("Poland", "23"),
Pair("Romania", "170"),
Pair("Russia", "109"),
Pair("South Africa", "200"),
Pair("South Korea", "135"),
Pair("Spain", "62"),
Pair("Sweden", "114"),
Pair("Switzerland", "41"),
Pair("Taiwan", "119"),
Pair("Thailand", "57"),
Pair("United Kingdom", "180"),
Pair("United States of America", "129"),
)
}
}

View File

@ -1,65 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox.extractors
import eu.kanade.tachiyomi.animeextension.en.dopebox.utils.Decryptor
import eu.kanade.tachiyomi.network.GET
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
class DopeBoxExtractor(private val client: OkHttpClient) {
// 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 newClient = client.newBuilder()
.cache(null)
.build()
companion object {
// its the same .js file for any server it may use,
// so we choose rabbitstream arbitrarily
private const val JS_URL = "https://rabbitstream.net/js/player/prod/e4-player.min.js"
// unlike the case of the .js file, here it is not possible to
// simply use the same host.
private const val SOURCES_PATH = "/ajax/embed-4/getSources?id="
}
// This will create a lag of 1~3s at the initialization of the class, but the
// speedup of the manual cache will be worth it.
private val cachedJs by lazy {
newClient.newCall(GET(JS_URL, cache = cacheControl)).execute()
.body.string()
}
init { cachedJs }
fun getSourcesJson(url: String): String? {
val id = url.substringAfter("/embed-4/", "")
.substringBefore("?", "").ifEmpty { return null }
val serverUrl = url.substringBefore("/embed")
val srcRes = newClient.newCall(
GET(
serverUrl + SOURCES_PATH + id,
headers = Headers.headersOf("x-requested-with", "XMLHttpRequest"),
cache = cacheControl,
),
)
.execute()
.body.string()
val key = newClient.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
}
}

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox.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
}
}
}

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox.utils
import app.cash.quickjs.QuickJs
// For e4.min.js
object FindPassword {
fun getPassword(js: String): String {
val funcName = js.substringBefore("CryptoJS[")
.substringBeforeLast("document")
.substringAfterLast(",")
.substringBefore("=")
val suspiciousPass = js.substringAfter(":" + funcName)
.substringAfter(",")
.substringBefore("||")
.substringBeforeLast(")")
if (suspiciousPass.startsWith("'")) {
return suspiciousPass.trim('\'')
}
return getPasswordFromJS(js, "(" + suspiciousPass.substringAfter("("))
}
private fun getPasswordFromJS(js: String, getKeyArgs: String): String {
var script = "(function" + js.substringBefore(",(!function")
.substringAfter("(function") + ")"
val decoderFunName = script.substringAfter("=").substringBefore(",")
val decoderFunPrefix = "function " + decoderFunName
var decoderFunBody = js.substringAfter(decoderFunPrefix)
val decoderFunSuffix = decoderFunName + decoderFunBody.substringBefore("{") + ";}"
decoderFunBody = (
decoderFunPrefix +
decoderFunBody.substringBefore(decoderFunSuffix) +
decoderFunSuffix
)
if ("=[" !in js.substring(0, 30)) {
val superArrayName = decoderFunBody.substringAfter("=")
.substringBefore(";")
val superArrayPrefix = "function " + superArrayName
val superArraySuffix = "return " + superArrayName + ";}"
val superArrayBody = (
superArrayPrefix +
js.substringAfter(superArrayPrefix)
.substringBefore(superArraySuffix) +
superArraySuffix
)
script += "\n\n" + superArrayBody
}
script += "\n\n" + decoderFunBody
script += "\n\n$decoderFunName$getKeyArgs"
val qjs = QuickJs.create()
// this part can be really slow, like 5s or even more >:(
val result = qjs.evaluate(script).toString()
qjs.close()
return result
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -1,16 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Sflix'
pkgNameSuffix = 'en.sflix'
extClass = '.SFlix'
extVersionCode = 16
libVersion = '13'
}
dependencies {
implementation(project(':lib-dood-extractor'))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

View File

@ -1,460 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.sflix.extractors.SFlixExtractor
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
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
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 kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
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
import uy.kohesive.injekt.injectLazy
class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "SFlix"
override val baseUrl by lazy {
"https://" + preferences.getString(PREF_DOMAIN_KEY, "sflix.to")!!
}
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun popularAnimeRequest(page: Int): Request {
val type = preferences.getString(PREF_POPULAR_KEY, "movie")!!
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 popularAnimeNextPageSelector(): String = "ul.pagination li.page-item a[title=next]"
// ============================== 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 id = infoElement.attr("data-id")
val dataType = infoElement.attr("data-type") // Tv = 2 or movie = 1
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)
}
} 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)
}
return episodeList.reversed()
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
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 episodeElements = episodesHtml.select("div.eps-item")
return episodeElements.map { episodeFromElement(it, seasonName) }
}
private fun episodeFromElement(element: Element, seasonName: String): SEpisode {
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
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val extractor = SFlixExtractor(client)
val videoList = 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()
val sourceUrl = reqBody.substringAfter("\"link\":\"")
.substringBefore("\"")
runCatching {
when {
"DoodStream" in name ->
DoodExtractor(client).videoFromUrl(sourceUrl)?.let {
listOf(it)
}
"Vidcloud" in name || "UpCloud" in name -> {
val source = extractor.getSourcesJson(sourceUrl)
source?.let { getVideosFromServer(it, name) }
}
else -> null
}
}.getOrNull()
}
.filterNotNull()
.flatten()
return videoList
}
private fun getVideosFromServer(source: String, name: String): List<Video>? {
if (!source.contains("{\"sources\":[{\"file\":\"")) return null
val json = json.decodeFromString<JsonObject>(source)
val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"]!!.jsonPrimitive.content
val subs2 = mutableListOf<Track>()
json["tracks"]?.jsonArray
?.filter { it.jsonObject["kind"]!!.jsonPrimitive.content == "captions" }
?.map { track ->
val trackUrl = track.jsonObject["file"]!!.jsonPrimitive.content
val lang = track.jsonObject["label"]!!.jsonPrimitive.content
try {
subs2.add(Track(trackUrl, lang))
} catch (e: Error) {}
} ?: emptyList()
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 {
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)
}
}
return videoList
}
val defaultVideoList = listOf(
try {
Video(masterUrl, "$name - Default", masterUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(masterUrl, "$name - Default", masterUrl)
},
)
return defaultVideoList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
private fun subLangOrder(tracks: List<Track>): List<Track> {
val language = preferences.getString(PREF_SUB_KEY, null)
if (language != null) {
val newList = mutableListOf<Track>()
var preferred = 0
for (track in tracks) {
if (track.lang.contains(language)) {
newList.add(preferred, track)
preferred++
} else {
newList.add(track)
}
}
return newList
}
return tracks
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = 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 = SFlixFilters.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: SFlixFilters.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 = SFlixFilters.filterList
// =========================== 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, "Movies")!!
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 {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_LIST
entryValues = PREF_DOMAIN_LIST
setDefaultValue("sflix.to")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST
setDefaultValue("1080p")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subLangPref = ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES
entryValues = PREF_SUB_LANGUAGES
setDefaultValue("English")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val latestType = ListPreference(screen.context).apply {
key = PREF_LATEST_KEY
title = PREF_LATEST_TITLE
entries = PREF_LATEST_PAGES
entryValues = PREF_LATEST_PAGES
setDefaultValue("Movies")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val popularType = ListPreference(screen.context).apply {
key = PREF_POPULAR_KEY
title = PREF_POPULAR_TITLE
entries = PREF_POPULAR_ENTRIES
entryValues = PREF_POPULAR_VALUES
setDefaultValue("movie")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
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)
}
// ============================= Utilities ==============================
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
private const val PREF_DOMAIN_KEY = "preferred_domain_new"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private val PREF_DOMAIN_LIST = arrayOf("sflix.to", "sflix.se")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p")
private const val PREF_SUB_KEY = "preferred_subLang"
private const val PREF_SUB_TITLE = "Preferred sub language"
private val PREF_SUB_LANGUAGES = arrayOf(
"Arabic", "English", "French", "German", "Hungarian",
"Italian", "Japanese", "Portuguese", "Romanian", "Russian",
"Spanish",
)
private const val PREF_LATEST_KEY = "preferred_latest_page"
private const val PREF_LATEST_TITLE = "Preferred latest page"
private val PREF_LATEST_PAGES = arrayOf("Movies", "TV Shows")
private const val PREF_POPULAR_KEY = "preferred_popular_page_new"
private const val PREF_POPULAR_TITLE = "Preferred popular page"
private val PREF_POPULAR_ENTRIES = PREF_LATEST_PAGES
private val PREF_POPULAR_VALUES = arrayOf("movie", "tv-show")
}
}

View File

@ -1,187 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object SFlixFilters {
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.getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
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
}
}
}
class TypeFilter : QueryPartFilter("Type", SFlixFiltersData.types)
class QualityFilter : QueryPartFilter("Quality", SFlixFiltersData.qualities)
class ReleaseYearFilter : QueryPartFilter("Released at", SFlixFiltersData.years)
class GenresFilter : CheckBoxFilterList(
"Genres",
SFlixFiltersData.genres.map { CheckBoxVal(it.first, false) },
)
class CountriesFilter : CheckBoxFilterList(
"Countries",
SFlixFiltersData.countries.map { CheckBoxVal(it.first, false) },
)
val filterList = AnimeFilterList(
TypeFilter(),
QualityFilter(),
ReleaseYearFilter(),
AnimeFilter.Separator(),
GenresFilter(),
CountriesFilter(),
)
data class FilterSearchParams(
val type: String = "",
val quality: String = "",
val releaseYear: String = "",
val genres: String = "",
val countries: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<QualityFilter>(),
filters.asQueryPart<ReleaseYearFilter>(),
filters.parseCheckbox<GenresFilter>(SFlixFiltersData.genres),
filters.parseCheckbox<CountriesFilter>(SFlixFiltersData.countries),
)
}
private object SFlixFiltersData {
val all = Pair("All", "all")
val types = arrayOf(
all,
Pair("Movies", "movies"),
Pair("TV Shows", "tv"),
)
val qualities = arrayOf(
all,
Pair("HD", "HD"),
Pair("SD", "SD"),
Pair("CAM", "CAM"),
)
val years = arrayOf(
all,
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("Older", "older-2018"),
)
val genres = arrayOf(
Pair("Action", "10"),
Pair("Action & Adventure", "24"),
Pair("Adventure", "18"),
Pair("Animation", "3"),
Pair("Biography", "37"),
Pair("Comedy", "7"),
Pair("Crime", "2"),
Pair("Documentary", "11"),
Pair("Drama", "4"),
Pair("Family", "9"),
Pair("Fantasy", "13"),
Pair("History", "19"),
Pair("Horror", "14"),
Pair("Kids", "27"),
Pair("Music", "15"),
Pair("Mystery", "1"),
Pair("News", "34"),
Pair("Reality", "22"),
Pair("Romance", "12"),
Pair("Sci-Fi & Fantasy", "31"),
Pair("Science Fiction", "5"),
Pair("Soap", "35"),
Pair("Talk", "29"),
Pair("Thriller", "16"),
Pair("TV Movie", "8"),
Pair("War", "17"),
Pair("War & Politics", "28"),
Pair("Western", "6"),
)
val countries = arrayOf(
Pair("Argentina", "11"),
Pair("Australia", "151"),
Pair("Austria", "4"),
Pair("Belgium", "44"),
Pair("Brazil", "190"),
Pair("Canada", "147"),
Pair("China", "101"),
Pair("Czech Republic", "231"),
Pair("Denmark", "222"),
Pair("Finland", "158"),
Pair("France", "3"),
Pair("Germany", "96"),
Pair("Hong Kong", "93"),
Pair("Hungary", "72"),
Pair("India", "105"),
Pair("Ireland", "196"),
Pair("Israel", "24"),
Pair("Italy", "205"),
Pair("Japan", "173"),
Pair("Luxembourg", "91"),
Pair("Mexico", "40"),
Pair("Netherlands", "172"),
Pair("New Zealand", "122"),
Pair("Norway", "219"),
Pair("Poland", "23"),
Pair("Romania", "170"),
Pair("Russia", "109"),
Pair("South Africa", "200"),
Pair("South Korea", "135"),
Pair("Spain", "62"),
Pair("Sweden", "114"),
Pair("Switzerland", "41"),
Pair("Taiwan", "119"),
Pair("Thailand", "57"),
Pair("United Kingdom", "180"),
Pair("United States of America", "129"),
)
}
}

View File

@ -1,66 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix.extractors
import eu.kanade.tachiyomi.animeextension.en.sflix.utils.Decryptor
import eu.kanade.tachiyomi.network.GET
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
class SFlixExtractor(private val client: OkHttpClient) {
// 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 newClient = client.newBuilder()
.cache(null)
.build()
companion object {
// its the same .js file for any server it may use,
// so we choose rabbitstream arbitrarily
private const val JS_URL = "https://rabbitstream.net/js/player/prod/e4-player.min.js"
// unlike the case of the .js file, here it is not possible to
// simply use the same host.
private const val SOURCES_PATH = "/ajax/embed-4/getSources?id="
}
// This will create a lag of 1~3s at the initialization of the class, but the
// speedup of the manual cache will be worth it.
private val cachedJs by lazy {
newClient.newCall(GET(JS_URL, cache = cacheControl)).execute()
.body.string()
}
init { cachedJs }
fun getSourcesJson(url: String): String? {
val id = url.substringAfter("/embed-4/", "")
.substringBefore("?", "").ifEmpty { return null }
val serverUrl = url.substringBefore("/embed")
val srcRes = newClient.newCall(
GET(
serverUrl + SOURCES_PATH + id,
headers = Headers.headersOf("x-requested-with", "XMLHttpRequest"),
cache = cacheControl,
),
)
.execute()
.body.string()
val key = newClient.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
}
}

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix.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
}
}
}

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix.utils
import app.cash.quickjs.QuickJs
// For e4.min.js
object FindPassword {
fun getPassword(js: String): String {
val funcName = js.substringBefore("CryptoJS[")
.substringBeforeLast("document")
.substringAfterLast(",")
.substringBefore("=")
val suspiciousPass = js.substringAfter(":" + funcName)
.substringAfter(",")
.substringBefore("||")
.substringBeforeLast(")")
if (suspiciousPass.startsWith("'")) {
return suspiciousPass.trim('\'')
}
return getPasswordFromJS(js, "(" + suspiciousPass.substringAfter("("))
}
private fun getPasswordFromJS(js: String, getKeyArgs: String): String {
var script = "(function" + js.substringBefore(",(!function")
.substringAfter("(function") + ")"
val decoderFunName = script.substringAfter("=").substringBefore(",")
val decoderFunPrefix = "function " + decoderFunName
var decoderFunBody = js.substringAfter(decoderFunPrefix)
val decoderFunSuffix = decoderFunName + decoderFunBody.substringBefore("{") + ";}"
decoderFunBody = (
decoderFunPrefix +
decoderFunBody.substringBefore(decoderFunSuffix) +
decoderFunSuffix
)
if ("=[" !in js.substring(0, 30)) {
val superArrayName = decoderFunBody.substringAfter("=")
.substringBefore(";")
val superArrayPrefix = "function " + superArrayName
val superArraySuffix = "return " + superArrayName + ";}"
val superArrayBody = (
superArrayPrefix +
js.substringAfter(superArrayPrefix)
.substringBefore(superArraySuffix) +
superArraySuffix
)
script += "\n\n" + superArrayBody
}
script += "\n\n" + decoderFunBody
script += "\n\n$decoderFunName$getKeyArgs"
val qjs = QuickJs.create()
// this part can be really slow, like 5s or even more >:(
val result = qjs.evaluate(script).toString()
qjs.close()
return result
}
}