fix(fr/empirestreaming): General fixes (#1871)

This commit is contained in:
Claudemirovsky
2023-07-08 16:40:35 +00:00
committed by GitHub
parent 48483f11b2
commit 599d101e8d
5 changed files with 349 additions and 349 deletions

View File

@ -1,12 +1,14 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext {
extName = 'EmpireStreaming'
pkgNameSuffix = 'fr.empirestreaming'
extClass = '.EmpireStreaming'
extVersionCode = 10
extVersionCode = 11
libVersion = '13'
}

View File

@ -1,86 +0,0 @@
package eu.kanade.tachiyomi.animeextension.fr.empirestreaming
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CloudflareInterceptor : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("bruh")
return chain.proceed(newRequest)
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
// We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
var newRequest: Request? = null
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = "Mozilla/5.0 (Linux; Android 12; SM-T870 Build/SP2A.220305.013; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/106.0.5249.126 Safari/537.36"
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if (request.url.toString().contains("/build/")) {
newRequest = origRequestUrl.newBuilder().headers(request.requestHeaders.toHeaders()).build()
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl.url.toString(), headers)
}
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
// around 4 seconds but it can take more due to slow networks or server issues.
latch.await(12, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return newRequest
}
}

View File

@ -2,10 +2,15 @@ package eu.kanade.tachiyomi.animeextension.fr.empirestreaming
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.fr.empirestreaming.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.fr.empirestreaming.dto.MovieInfoDto
import eu.kanade.tachiyomi.animeextension.fr.empirestreaming.dto.SearchResultsDto
import eu.kanade.tachiyomi.animeextension.fr.empirestreaming.dto.SerieEpisodesDto
import eu.kanade.tachiyomi.animeextension.fr.empirestreaming.dto.VideoDto
import eu.kanade.tachiyomi.animeextension.fr.empirestreaming.extractors.EplayerExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -17,38 +22,36 @@ import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
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
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.Exception
class EmpireStreaming : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "EmpireStreaming"
override val baseUrl = "https://empire-streaming.co"
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
override val lang = "fr"
override val supportsLatest = false
override val supportsLatest = true
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(CloudflareInterceptor())
.build()
override val client = network.cloudflareClient
private val vclient: OkHttpClient = network.client
@ -56,191 +59,147 @@ class EmpireStreaming : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
override fun popularAnimeSelector() = "div.block-forme:has(p:contains(Les plus vus)) div.content-card"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a.play")!!.attr("abs:href"))
thumbnail_url = baseUrl + element.selectFirst("picture img")!!.attr("data-src")
title = element.selectFirst("h3.line-h-s, p.line-h-s")!!.text()
}
override fun popularAnimeSelector(): String = "div.d-f.fd-r.h-100.ox-s.w-100.py-2 div.card-custom-4"
override fun popularAnimeNextPageSelector() = null
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.url = "/" + element.select("a.btn-link-card-5").attr("href")
Log.i("animeUrl", anime.url)
anime.thumbnail_url = baseUrl + element.select("picture img").attr("data-src")
anime.title = element.select("h3.line-h-s").text()
return anime
override fun latestUpdatesSelector() = "div.block-forme:has(p:contains(Ajout récents)) div.content-card"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = null
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = throw Exception("not used")
override fun searchAnimeNextPageSelector() = null
override fun searchAnimeSelector() = throw Exception("not used")
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw Exception("not used")
override fun searchAnimeParse(response: Response) = throw Exception("not used")
private val searchItems by lazy {
client.newCall(GET("$baseUrl/api/views/contenitem", headers)).execute()
.use {
json.decodeFromString<SearchResultsDto>(it.body.string()).items
}
}
override fun popularAnimeNextPageSelector(): String? = null
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val entriesPages = searchItems.filter { it.title.contains(query, true) }
.sortedBy { it.title }
.chunked(30) // to prevent exploding the user screen with 984948984 results
// episodes
val hasNextPage = entriesPages.size > page
val entries = entriesPages.getOrNull(page - 1)?.map {
SAnime.create().apply {
title = it.title
setUrlWithoutDomain("/${it.urlPath}")
thumbnail_url = "$baseUrl/images/medias/${it.thumbnailPath}"
}
} ?: emptyList()
return Observable.just(AnimesPage(entries, hasNextPage))
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
title = document.selectFirst("h3#title_media")!!.text()
val thumbPath = document.html().substringAfter("backdrop\":\"").substringBefore('"')
thumbnail_url = "$baseUrl/images/medias/$thumbPath".replace("\\", "")
genre = document.select("div > button.bc-w.fs-12.ml-1.c-b").eachText().joinToString()
description = document.selectFirst("div.target-media-desc p.content")!!.text()
status = SAnime.UNKNOWN
}
// ============================== Episodes ==============================
override fun episodeListSelector() = throw Exception("not used")
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
if (document.select("div.c-w span.ff-fb.tt-u").text().contains("serie")) {
val season = document.select("div.episode.w-100 ul.episode-by-season")
season.forEach {
val episode = parseEpisodesFromSeries(it, response)
episodeList.addAll(episode)
}
val doc = response.asJsoup()
val scriptJson = doc.selectFirst("script:containsData(window.empire):containsData(data:)")!!
.data()
.substringAfter("data:")
.substringBefore("countpremiumaccount:")
.substringBeforeLast(",")
return if (doc.location().contains("serie")) {
val data = json.decodeFromString<SerieEpisodesDto>(scriptJson)
data.seasons.values
.flatMap { it.map(::episodeFromObject) }
.sortedByDescending { it.episode_number }
} else {
val episode = SEpisode.create()
episode.name = document.select("h1.fs-84").text()
episode.episode_number = 1F
episode.setUrlWithoutDomain(response.request.url.toString())
episodeList.add(episode)
}
return episodeList.reversed()
}
private fun parseEpisodesFromSeries(element: Element, response: Response): List<SEpisode> {
val episodeElements = element.select("li.card-serie")
return episodeElements.map { episodeFromElementR(it, response) }
}
private fun episodeFromElementR(element: Element, response: Response): SEpisode {
val episode = SEpisode.create()
val url = response.request.url.toString()
val season = element.attr("data-season")
val ep = element.attr("data-episode")
episode.name = "Saison $season Épisode $ep : " + element.select("p.mb-0.fs-14").text()
episode.episode_number = element.attr("data-episode").toFloat()
episode.setUrlWithoutDomain("$url?saison=$season&episode=$ep")
return episode
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used")
// Video Extractor
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val season = response.request.url.toString()
.substringAfter("saison=").substringBefore("&").toInt()
val ep = response.request.url.toString()
.substringAfter("episode=").toInt()
return videosFromElement(document, season, ep)
}
private fun videosFromElement(document: Document, season: Int, ep: Int): List<Video> {
val videoList = mutableListOf<Video>()
val hosterSelection = preferences.getStringSet("hoster_selection", setOf("voe", "streamsb", "dood"))
if (document.select("div.c-w span.ff-fb.tt-u").text().contains("film")) {
val script = document.select("script:containsData(const result = [)").toString()
val hosts = script.split("},{")
hosts.forEach {
val hostn = it.substringAfter("\"property\":\"").substringBefore("\",")
when {
hostn.contains("voe") && hosterSelection?.contains("voe") == true -> {
val id = it.substringAfter("\"code\":\"").substringBefore("\",")
val url = "https://voe.sx/e/$id"
val video = VoeExtractor(vclient).videoFromUrl(url)
if (video != null) {
videoList.add(video)
val data = json.decodeFromString<MovieInfoDto>(scriptJson)
SEpisode.create().apply {
name = data.title
date_upload = data.date.toDate()
url = data.videos.encode()
episode_number = 1F
}.let(::listOf)
}
}
hostn.contains("streamsb") && hosterSelection?.contains("streamsb") == true -> {
val id = it.substringAfter("\"code\":\"").substringBefore("\",")
val url = "https://playersb.com/e/$id"
val video = StreamSBExtractor(vclient).videosFromUrl(url, headers, common = false)
videoList.addAll(video)
private fun episodeFromObject(obj: EpisodeDto) = SEpisode.create().apply {
name = "Saison ${obj.season} Épisode ${obj.episode} : ${obj.title}"
episode_number = "${obj.season}.${obj.episode}".toFloatOrNull() ?: 1F
url = obj.video.encode()
date_upload = obj.date.toDate()
}
hostn.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
val id = it.substringAfter("\"code\":\"").substringBefore("\",")
val url = "https://dood.pm/e/$id"
val quality = "Dood"
val video = DoodExtractor(vclient).videosFromUrl(url, quality)
videoList.addAll(video)
}
}
}
} else {
val script = document.select("script:containsData(const result = {\"1\")").toString()
val hosts = script.split("]},{")
hosts.forEach { host ->
if (host.substringAfter("\"episode\":").substringBefore(",").toInt() == ep && host.substringAfter("\"saison\":").substringBefore(",").toInt() == season) {
val videoarray = host.substringAfter("\"video\":[").substringBefore("],")
val videos = videoarray.split("},{")
videos.forEach { videofile ->
val hostn = videofile.substringAfter("\"property\":\"").substringBefore("\",")
when {
hostn.contains("voe") && hosterSelection?.contains("voe") == true -> {
val id = videofile.substringAfter("\"code\":\"").substringBefore("\",")
val version = videofile.substringAfter("\"version\":\"").substringBefore("\"")
val quality = "Voe $version"
val url = "https://voe.sx/e/$id"
val video = VoeExtractor(vclient).videoFromUrl(url, quality)
if (video != null) {
videoList.add(video)
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
// ============================ Video Links =============================
// val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
val videos = episode.url.split(", ").parallelMap {
runCatching {
val (id, type, hoster) = it.split("|")
if (hoster !in hosterSelection) return@parallelMap emptyList()
videosFromPath("$id/$type", hoster)
}.getOrElse { emptyList() }
}.flatten().sort()
return Observable.just(videos)
}
hostn.contains("streamsb") && hosterSelection?.contains("streamsb") == true -> {
val id = videofile.substringAfter("\"code\":\"").substringBefore("\",")
val quality = videofile.substringAfter("\"version\":\"").substringBefore("\"")
val url = "https://playersb.com/e/$id"
val video = StreamSBExtractor(vclient).videosFromUrl(url, headers, quality, common = false)
videoList.addAll(video)
private fun videosFromPath(path: String, hoster: String): List<Video> {
val url = client.newCall(GET("$baseUrl/player_submit/$path", headers)).execute()
.use { it.body.string() }
.substringAfter("window.location.href = \"")
.substringBefore('"')
return when (hoster) {
"doodstream" -> DoodExtractor(vclient).videosFromUrl(url)
"voe" -> VoeExtractor(vclient).videoFromUrl(url)?.let(::listOf)
"streamsb" -> StreamSBExtractor(vclient).videosFromUrl(url, headers, common = false)
"Eplayer" -> EplayerExtractor(client).videosFromUrl(url)
else -> null
} ?: emptyList()
}
hostn.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
val id = videofile.substringAfter("\"code\":\"").substringBefore("\",")
val url = "https://dood.pm/e/$id"
val version = videofile.substringAfter("\"version\":\"").substringBefore("\"")
val quality = "Dood $version"
val video = DoodExtractor(vclient).videosFromUrl(url, quality)
videoList.addAll(video)
}
}
}
}
}
}
return videoList
}
override fun videoListParse(response: Response) = throw Exception("not used")
override fun List<Video>.sort(): List<Video> {
val hoster = preferences.getString("preferred_hoster", null)
val hosterList = mutableListOf<Video>()
val otherList = mutableListOf<Video>()
if (hoster != null) {
for (video in this) {
if (video.url.contains(hoster)) {
hosterList.add(video)
} else {
otherList.add(video)
}
}
} else {
otherList += this
}
val newList = mutableListOf<Video>()
var preferred = 0
for (video in hosterList) {
if (hoster?.let { video.quality.contains(it) } == true) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
for (video in otherList) {
if (hoster?.let { video.quality.contains(it) } == true) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
val hoster = preferences.getString(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(hoster) },
{ it.quality.contains(quality) },
),
).reversed()
}
override fun videoListSelector() = throw Exception("not used")
@ -249,82 +208,14 @@ class EmpireStreaming : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document) = throw Exception("not used")
// Search
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("not Used")
override fun searchAnimeNextPageSelector(): String? = null
override fun searchAnimeSelector(): String = throw Exception("not Used")
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = POST("$baseUrl/api/views/search", body = "{\"search\":\"$query\"}".toRequestBody("application/json".toMediaType()))
override fun searchAnimeParse(response: Response): AnimesPage {
val responseString = response.body.string()
return parseSearchAnimeJson(responseString)
}
private fun parseSearchAnimeJson(jsonLine: String?): AnimesPage {
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
val jObject = json.decodeFromString<JsonObject>(jsonData)
val animeList = mutableListOf<SAnime>()
val data = jObject["data"]!!.jsonObject
val arrayf = data.jsonObject["films"]!!.jsonArray
Log.i("search", arrayf.toString())
for (item in arrayf) {
val anime = SAnime.create()
anime.title = item.jsonObject["title"]!!.jsonPrimitive.content
val urlpath = item.jsonObject["urlPath"]!!.jsonPrimitive.content
anime.setUrlWithoutDomain("/$urlpath")
val symimage = item.jsonObject["sym_image"]!!.jsonObject
val poster = symimage.jsonObject["poster"]!!.jsonPrimitive.content
anime.thumbnail_url = "$baseUrl/images/medias/$poster"
animeList.add(anime)
}
val arrays = data.jsonObject["series"]!!.jsonArray
for (item in arrays) {
val anime = SAnime.create()
anime.title = item.jsonObject["title"]!!.jsonPrimitive.content
val urlpath = item.jsonObject["urlPath"]!!.jsonPrimitive.content
anime.setUrlWithoutDomain("/$urlpath")
val symimage = item.jsonObject["sym_image"]!!.jsonObject
val poster = symimage.jsonObject["poster"]!!.jsonPrimitive.content
anime.thumbnail_url = "$baseUrl/images/medias/$poster"
animeList.add(anime)
}
return AnimesPage(animeList, hasNextPage = false)
}
// Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("h1.fs-40").text()
anime.genre = document.select("ul.d-f li.mr-1 font").joinToString(", ") { it.text() }
anime.description = document.select("p.description").text()
anime.status = SAnime.COMPLETED
return anime
}
// Latest
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesSelector(): String = throw Exception("Not used")
// Preferences
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val hosterPref = ListPreference(screen.context).apply {
key = "preferred_hoster"
title = "Hébergeur standard"
entries = arrayOf("Voe", "StreamSB", "Dood")
entryValues = arrayOf("https://voe.sx", "https://playersb.com", "https://dood")
setDefaultValue("https://voe.sx")
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_VALUES
setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -333,19 +224,92 @@ class EmpireStreaming : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subSelection = MultiSelectListPreference(screen.context).apply {
key = "hoster_selection"
title = "Sélectionnez l'hôte"
entries = arrayOf("Voe", "StreamSB", "Dood")
entryValues = arrayOf("voe", "streamsb", "dood")
setDefaultValue(setOf("voe", "streamsb", "dood"))
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE
entries = PREF_HOSTER_ENTRIES
entryValues = PREF_HOSTER_VALUES
setDefaultValue(PREF_HOSTER_DEFAULT)
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()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
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()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_SELECTION_KEY
title = PREF_HOSTER_SELECTION_TITLE
entries = PREF_HOSTER_SELECTION_ENTRIES
entryValues = PREF_HOSTER_SELECTION_VALUES
setDefaultValue(PREF_HOSTER_SELECTION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
screen.addPreference(hosterPref)
screen.addPreference(subSelection)
// ============================= Utilities ==============================
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
private fun List<VideoDto>.encode() = joinToString { it.encoded }
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.US)
}
private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private const val PREF_DOMAIN_DEFAULT = "https://empire-stream.net"
private val PREF_DOMAIN_ENTRIES = arrayOf("https://empire-stream.net", "https://empire-streaming.app")
private val PREF_DOMAIN_VALUES = PREF_DOMAIN_ENTRIES
private const val PREF_HOSTER_KEY = "preferred_hoster_new"
private const val PREF_HOSTER_TITLE = "Hébergeur standard"
private const val PREF_HOSTER_DEFAULT = "StreamSB"
private val PREF_HOSTER_ENTRIES = arrayOf("Voe", "StreamSB", "Dood", "E-Player")
private val PREF_HOSTER_VALUES = PREF_HOSTER_ENTRIES
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualité préférée" // DeepL
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "800p", "720p", "480p")
private const val PREF_HOSTER_SELECTION_KEY = "hoster_selection_new"
private const val PREF_HOSTER_SELECTION_TITLE = "Sélectionnez l'hôte"
private val PREF_HOSTER_SELECTION_ENTRIES = arrayOf("Voe", "StreamSB", "Dood", "Eplayer")
private val PREF_HOSTER_SELECTION_VALUES = arrayOf("voe", "streamsb", "doodstream", "Eplayer")
private val PREF_HOSTER_SELECTION_DEFAULT by lazy { PREF_HOSTER_SELECTION_VALUES.toSet() }
}
}

View File

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.animeextension.fr.empirestreaming.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SearchResultsDto(val contentItem: ContentDto) {
val items by lazy { contentItem.films + contentItem.series }
}
@Serializable
data class ContentDto(
val films: List<EntryDto>,
val series: List<EntryDto>,
)
@Serializable
data class EntryDto(
val urlPath: String,
val title: String,
val image: List<ImageDto>,
) {
@Serializable
data class ImageDto(val path: String)
val thumbnailPath by lazy { image.first().path }
}
@Serializable
data class SerieEpisodesDto(
@SerialName("Saison")
val seasons: Map<String, List<EpisodeDto>>,
)
@Serializable
data class EpisodeDto(
val episode: Int = 1,
@SerialName("saison")
val season: Int = 1,
val title: String,
val createdAt: DateDto,
val video: List<VideoDto>,
) {
val date by lazy { createdAt.date.substringBefore(" ") }
}
@Serializable
data class DateDto(val date: String)
@Serializable
data class VideoDto(val id: Int, val property: String, val version: String) {
val encoded by lazy { "$id|$version|$property" }
}
@Serializable
data class MovieInfoDto(
@SerialName("Titre")
val title: String,
@SerialName("CreatedAt")
val createdAt: DateDto,
@SerialName("Iframe")
val videos: List<VideoDto>,
) {
val date by lazy { createdAt.date.substringBefore(" ") }
}

View File

@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.animeextension.fr.empirestreaming.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
class EplayerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val id = url.substringAfterLast("/")
val postUrl = "$EPLAYER_HOST/player/index.php?data=$id&do=getVideo"
val body = FormBody.Builder()
.add("hash", id)
.add("r", "")
.build()
val headers = Headers.headersOf(
"X-Requested-With",
"XMLHttpRequest",
"Referer",
EPLAYER_HOST,
"Origin",
EPLAYER_HOST,
)
val masterUrl = client.newCall(POST(postUrl, headers, body = body)).execute().use {
it.body.string()
.substringAfter("videoSource\":\"")
.substringBefore('"')
.replace("\\", "")
}
val separator = "#EXT-X-STREAM-INF"
return client.newCall(GET(masterUrl, headers)).execute()
.use { it.body.string() }
.substringAfter(separator)
.split(separator)
.map {
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, "E-Player - $resolution", videoUrl, headers)
}
}
companion object {
private const val EPLAYER_HOST = "https://e-player-stream.app"
}
}