diff --git a/src/fr/empirestreaming/build.gradle b/src/fr/empirestreaming/build.gradle index 8bf5fd806..6297c5122 100644 --- a/src/fr/empirestreaming/build.gradle +++ b/src/fr/empirestreaming/build.gradle @@ -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' } diff --git a/src/fr/empirestreaming/src/eu/kanade/tachiyomi/animeextension/fr/empirestreaming/CloudflareInterceptor.kt b/src/fr/empirestreaming/src/eu/kanade/tachiyomi/animeextension/fr/empirestreaming/CloudflareInterceptor.kt deleted file mode 100644 index cb6c966e1..000000000 --- a/src/fr/empirestreaming/src/eu/kanade/tachiyomi/animeextension/fr/empirestreaming/CloudflareInterceptor.kt +++ /dev/null @@ -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() - 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 - } -} diff --git a/src/fr/empirestreaming/src/eu/kanade/tachiyomi/animeextension/fr/empirestreaming/EmpireStreaming.kt b/src/fr/empirestreaming/src/eu/kanade/tachiyomi/animeextension/fr/empirestreaming/EmpireStreaming.kt index 178343821..93b6b7226 100644 --- a/src/fr/empirestreaming/src/eu/kanade/tachiyomi/animeextension/fr/empirestreaming/EmpireStreaming.kt +++ b/src/fr/empirestreaming/src/eu/kanade/tachiyomi/animeextension/fr/empirestreaming/EmpireStreaming.kt @@ -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().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(it.body.string()).items + } } - override fun popularAnimeNextPageSelector(): String? = null + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + 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 { - val document = response.asJsoup() - val episodeList = mutableListOf() - 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(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) + val data = json.decodeFromString(scriptJson) + SEpisode.create().apply { + name = data.title + date_upload = data.date.toDate() + url = data.videos.encode() + episode_number = 1F + }.let(::listOf) } - return episodeList.reversed() } - private fun parseEpisodesFromSeries(element: Element, response: Response): List { - val episodeElements = element.select("li.card-serie") - return episodeElements.map { episodeFromElementR(it, response) } + 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() } - 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 Links ============================= + // val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!! + override fun fetchVideoList(episode: SEpisode): Observable> { + 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) } - override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used") + private fun videosFromPath(path: String, hoster: String): List