SFlix: quick-fix (#914)

This commit is contained in:
Claudemirovsky
2022-10-09 19:51:36 -03:00
committed by GitHub
parent bacab16408
commit 908aa60d72
7 changed files with 656 additions and 199 deletions

View File

@ -5,8 +5,12 @@ ext {
extName = 'Sflix'
pkgNameSuffix = 'en.sflix'
extClass = '.SFlix'
extVersionCode = 9
extVersionCode = 10
libVersion = '13'
}
dependencies {
compileOnly libs.bundles.coroutines
}
apply from: "$rootDir/common.gradle"

View File

@ -2,73 +2,86 @@ package eu.kanade.tachiyomi.animeextension.en.sflix
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.sflix.extractors.DoodExtractor
import eu.kanade.tachiyomi.animeextension.en.sflix.extractors.SFlixExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
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.Jsoup
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 name = "SFlix"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://sflix.to")!! }
override val baseUrl by lazy {
"https://" + preferences.getString(PREF_DOMAIN_KEY, "dopebox.to")!!
}
override val lang = "en"
override val supportsLatest = false
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
// private val domain = "aHR0cHM6Ly9yYWJiaXRzdHJlYW0ubmV0OjQ0Mw.."
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder(): Headers.Builder {
return super.headersBuilder()
.add("Referer", "$baseUrl/")
}
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 = GET("$baseUrl/movie?page=$page")
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.select("a").attr("href"))
anime.thumbnail_url = element.select("img").attr("data-src")
anime.title = element.select("a").attr("title")
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
// ============================== Episodes ==============================
override fun episodeListSelector() = throw Exception("not used")
@ -108,22 +121,21 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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()
.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 episode = SEpisode.create()
val epNum = element.select("div.episode-number").text()
val epName = element.select("h3.film-name a").text()
episode.name = "$seasonName $epNum $epName"
episode.setUrlWithoutDomain("$baseUrl/ajax/v2/episode/servers/$episodeId")
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
}
@ -131,92 +143,86 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return epsStr.filter { it.isDigit() }
}
// Video Extractor
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
// referers
val referer1 = response.request.url.toString()
val refererHeaders = Headers.headersOf("Referer", referer1)
val referer = response.request.url.encodedPath
val newHeaders = Headers.headersOf("Referer", referer)
// get embed id
val getVidID = document.selectFirst("a:contains(Vidcloud)").attr("data-id")
val getVidApi = client.newCall(GET("$baseUrl/ajax/get_link/" + getVidID)).execute().asJsoup()
// streamrapid URL
val getVideoEmbed = getVidApi.text().substringAfter("link\":\"").substringBefore("\"")
val videoEmbedUrlId = getVideoEmbed.substringAfterLast("/").substringBefore("?")
val callVideolink = client.newCall(GET(getVideoEmbed, refererHeaders)).execute().asJsoup()
val uri = Uri.parse(getVideoEmbed)
val domain = (Base64.encodeToString((uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), Base64.NO_PADDING) + ".").replace("\n", "")
val soup = Jsoup.connect(getVideoEmbed).referrer("$baseUrl/").get().toString().replace("\n", "")
val key = soup.substringAfter("var recaptchaSiteKey = '").substringBefore("',")
val number = soup.substringAfter("recaptchaNumber = '").substringBefore("';")
val vToken = Jsoup.connect("https://www.google.com/recaptcha/api.js?render=$key").referrer("https://rabbitstream.net/").get().toString().replace("\n", "").substringAfter("/releases/").substringBefore("/recaptcha")
val recapToken = Jsoup.connect("https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=kr60249sk&k=$key&co=$domain&v=$vToken").get().selectFirst("#recaptcha-token")?.attr("value")
val token = Jsoup.connect("https://www.google.com/recaptcha/api2/reload?k=$key").ignoreContentType(true)
.data("v", vToken).data("k", key).data("c", recapToken).data("co", domain).data("sa", "").data("reason", "q")
.post().toString().replace("\n", "").substringAfter("rresp\",\"").substringBefore("\"")
val jsonLink = "https://rabbitstream.net/ajax/embed-4/getSources?id=$videoEmbedUrlId&_token=$token&_number=$number&sId=test"
/*val reloadHeaderss = headers.newBuilder()
.set("X-Requested-With", "XMLHttpRequest")
.build()
val iframeResponse = client.newCall(GET(jsonLink, reloadHeaderss))
.execute().asJsoup()
*/
return videosFromElement(jsonLink)
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 videosFromElement(url: String): List<Video> {
val reloadHeaderss = headers.newBuilder()
.set("X-Requested-With", "XMLHttpRequest")
.build()
val json = Json.decodeFromString<JsonObject>(Jsoup.connect(url).header("X-Requested-With", "XMLHttpRequest").ignoreContentType(true).execute().body())
val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"].toString().trim('"')
val subsList = mutableListOf<Track>()
json["tracks"]!!.jsonArray.forEach {
val subLang = it.jsonObject["label"].toString().substringAfter("\"").substringBefore("\"") // .trim('"')
val subUrl = it.jsonObject["file"].toString().trim('"')
try {
subsList.add(Track(subUrl, subLang))
} catch (e: Error) {}
}
val prefSubsList = subLangOrder(subsList)
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 masterPlaylist = client.newCall(GET(masterUrl)).execute().body!!.string()
val videoList = mutableListOf<Video>()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p"
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")
videoList.add(
try {
Video(videoUrl, quality, videoUrl, subtitleTracks = prefSubsList)
} catch (e: Error) {
Video(videoUrl, quality, videoUrl)
}
)
try {
Video(videoUrl, quality, videoUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(videoUrl, quality, videoUrl)
}
}
return videoList
} else if (masterUrl.contains("index.m3u8")) {
return listOf(
try {
Video(masterUrl, "Default", masterUrl, subtitleTracks = prefSubsList)
} catch (e: Error) {
Video(masterUrl, "Default", masterUrl)
}
)
} else {
throw Exception("never give up and try again :)")
}
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("preferred_quality", null)
val quality = preferences.getString(PREF_QUALITY_KEY, null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
@ -234,7 +240,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun subLangOrder(tracks: List<Track>): List<Track> {
val language = preferences.getString("preferred_subLang", null)
val language = preferences.getString(PREF_SUB_KEY, null)
if (language != null) {
val newList = mutableListOf<Track>()
var preferred = 0
@ -250,87 +256,98 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
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
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.thumbnail_url = element.select("img").attr("data-src")
anime.title = element.select("a").attr("title")
return anime
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 searchAnimeNextPageSelector(): String = "ul.pagination li.page-item a[title=next]"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
override fun searchAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
private fun searchAnimeRequest(page: Int, query: String, filters: SFlixFilters.FilterSearchParams): Request {
val url = if (query.isNotBlank()) {
"$baseUrl/search/$query?page=$page".replace(" ", "-")
val fixedQuery = query.replace(" ", "-")
"$baseUrl/search/$fixedQuery?page=$page"
} else {
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is GenreList -> {
if (filter.state > 0) {
val GenreN = getGenreList()[filter.state].query
val genreUrl = "$baseUrl/genre/$GenreN".toHttpUrlOrNull()!!.newBuilder()
return GET(genreUrl.toString(), headers)
}
}
}
}
throw Exception("Choose Filter")
"$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)
}
// Details
override fun getFilterList(): AnimeFilterList = SFlixFilters.filterList
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = document.select("img.film-poster-img").attr("src")
anime.title = document.select("img.film-poster-img").attr("title")
anime.genre = document.select("div.row-line:contains(Genre) a").joinToString(", ") { it.text() }
anime.description = document.select("div.detail_page-watch div.description").text().replace("Overview:", "")
anime.author = document.select("div.row-line:contains(Production) a").joinToString(", ") { it.text() }
anime.status = parseStatus(document.select("li.status span.value").text())
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
"Completed" -> SAnime.COMPLETED
else -> SAnime.COMPLETED
}
}
// Latest
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String? = throw Exception("Not used")
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/")
override fun latestUpdatesSelector(): String = throw Exception("Not used")
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"
}
// Preferences
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("sflix.to", "sflix.se")
entryValues = arrayOf("https://sflix.to", "https://sflix.se")
setDefaultValue("https://sflix.to")
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_LIST
entryValues = PREF_DOMAIN_LIST
setDefaultValue("dopebox.to")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -341,11 +358,11 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST
setDefaultValue("1080p")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -356,10 +373,10 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
val subLangPref = ListPreference(screen.context).apply {
key = "preferred_subLang"
title = "Preferred sub language"
entries = arrayOf("Arabic", "English", "French", "German", "Hungarian", "Italian", "Japanese", "Portuguese", "Romanian", "Russian", "Spanish")
entryValues = arrayOf("Arabic", "English", "French", "German", "Hungarian", "Italian", "Japanese", "Portuguese", "Romanian", "Russian", "Spanish")
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES
entryValues = PREF_SUB_LANGUAGES
setDefaultValue("English")
summary = "%s"
@ -370,53 +387,74 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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("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()
}
}
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref)
screen.addPreference(subLangPref)
screen.addPreference(latestType)
screen.addPreference(popularType)
}
// Filter
// ============================= Utilities ==============================
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("Ignored If Using Text Search"),
GenreList(genresName),
)
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 class GenreList(genres: Array<String>) : AnimeFilter.Select<String>("Genre", genres)
private data class Genre(val name: String, val query: String)
private val genresName = getGenreList().map {
it.name
}.toTypedArray()
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 fun getGenreList() = listOf(
Genre("CHOOSE", ""),
Genre("Action", "action"),
Genre("Action & Adventure", "action-adventure"),
Genre("Adventure", "adventure"),
Genre("Animation", "animation"),
Genre("Biography", "biography"),
Genre("Comedy", "comedy"),
Genre("Crime", "crime"),
Genre("Documentary", "documentary"),
Genre("Drama", "drama"),
Genre("Family", "family"),
Genre("Fantasy", "fantasy"),
Genre("History", "history"),
Genre("Horror", "horror"),
Genre("Kids", "kids"),
Genre("Music", "music"),
Genre("Mystery", "mystery"),
Genre("News", "news"),
Genre("Reality", "reality"),
Genre("Romance", "romance"),
Genre("Sci-Fi & Fantasy", "sci-fi-fantasy"),
Genre("Science Fiction", "science-fiction"),
Genre("Soap", "soap"),
Genre("Talk", "talk"),
Genre("Thriller", "thriller"),
Genre("TV Movie", "tv-movie"),
Genre("War", "war"),
Genre("War & Politics", "war-politics"),
Genre("Western", "western")
)
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"
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

@ -0,0 +1,180 @@
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 {
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

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.animeextension.en.sflix.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class DoodExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String): Video? {
val response = client.newCall(GET(url)).execute()
// Overwrite url to the redirected one
val url = response.request.url.toString()
val doodTld = url.substringAfter("https://dood.").substringBefore("/")
val content = response.body!!.string()
if (!content.contains("'/pass_md5/")) return null
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
val token = md5.substringAfterLast("/")
val randomString = getRandomString()
val expiry = System.currentTimeMillis()
val videoUrlStart = client.newCall(
GET(
"https://dood.$doodTld/pass_md5/$md5",
Headers.headersOf("referer", url)
)
).execute().body!!.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
val newQuality = "DoodStream mirror"
return Video(url, newQuality, videoUrl, headers = doodHeaders(doodTld))
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
private fun doodHeaders(tld: String) = Headers.Builder().apply {
add("User-Agent", "Aniyomi")
add("Referer", "https://dood.$tld/")
}.build()
}

View File

@ -0,0 +1,60 @@
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()
// 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, cachedJs) ?: return null
val end = srcRes.replace("\"$encrypted\"", decrypted)
return end
}
}

View File

@ -0,0 +1,74 @@
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, js: String): String? {
val saltedData = Base64.decode(encodedData, Base64.DEFAULT)
val salt = saltedData.copyOfRange(8, 16)
val ciphertext = saltedData.copyOfRange(16, saltedData.size)
val password = FindPassword.getPassword(js).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

@ -0,0 +1,58 @@
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
}
}