Merge branch 'Kohi-den:main' into main

This commit is contained in:
Dark25 2024-09-25 18:07:51 +02:00 committed by GitHub
commit ebb2a4bd19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 95 additions and 627 deletions

View file

@ -19,11 +19,11 @@ kotlin-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", ver
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines_version" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines_version" }
injekt-core = { module = "com.github.inorichi.injekt:injekt-core", version = "65b0440" }
injekt = "com.github.mihonapp:injekt:91edab2317"
rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" }
jsoup = { module = "org.jsoup:jsoup", version = "1.16.1" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version = "5.0.0-alpha.11" }
quickjs = { module = "app.cash.quickjs:quickjs-android", version = "0.9.2" }
[bundles]
common = ["kotlin-stdlib", "injekt-core", "rxjava", "kotlin-protobuf", "kotlin-json", "jsoup", "okhttp", "aniyomi-lib", "quickjs", "coroutines-core", "coroutines-android"]
common = ["kotlin-stdlib", "injekt", "rxjava", "kotlin-protobuf", "kotlin-json", "jsoup", "okhttp", "aniyomi-lib", "quickjs", "coroutines-core", "coroutines-android"]

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.lib.chillxextractor
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES.decryptWithSalt
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
@ -19,11 +19,11 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
companion object {
private val REGEX_MASTER_JS by lazy { Regex("""\s*=\s*'([^']+)""") }
private val REGEX_SOURCES by lazy { Regex("""sources:\s*\[\{"file":"([^"]+)""") }
private val REGEX_FILE by lazy { Regex("""file: ?"([^"]+)"""") }
private val REGEX_SOURCE by lazy { Regex("""source = ?"([^"]+)"""") }
private val REGEX_SUBS by lazy { Regex("""\[(.*?)\](https?://[^\s,]+)""") }
private val REGEX_MASTER_JS = Regex("""\s*=\s*'([^']+)""")
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
private val REGEX_SUBS = Regex("""\[(.*?)\](https?://[^\s,]+)""")
private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
}
@ -38,7 +38,7 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
val aesJson = json.decodeFromString<CryptoInfo>(master)
val key = fetchKey() ?: throw ErrorLoadingException("Unable to get key")
val decryptedScript = decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
val decryptedScript = CryptoAES.decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
.replace("\\n", "\n")
.replace("\\", "")
@ -76,16 +76,14 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
@Serializable
data class CryptoInfo(
@SerialName("ct")
val ciphertext: String,
@SerialName("s")
val salt: String,
@SerialName("ct") val ciphertext: String,
@SerialName("s") val salt: String,
)
@Serializable
data class KeysData(
@SerialName("chillx")
val keys: List<String>
@SerialName("chillx") val keys: List<String>
)
}
class ErrorLoadingException(message: String) : Exception(message)

View file

@ -1,7 +1,7 @@
ext {
extName = 'Hikari'
extClass = '.Hikari'
extVersionCode = 7
extVersionCode = 8
}
apply from: "$rootDir/common.gradle"
@ -9,4 +9,5 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:vidhide-extractor'))
implementation(project(':lib:chillx-extractor'))
}

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
import eu.kanade.tachiyomi.network.GET
@ -218,6 +219,7 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
private val embedRegex = Regex("""getEmbed\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\d+)'""")
override fun videoListRequest(episode: SEpisode): Request {
@ -336,10 +338,8 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl, videoNameGen = { s -> "$name - $s" })
embedUrl.contains("filemoon", true) -> {
filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
}
else -> emptyList()
embedUrl.contains("filemoon", true) -> filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
else -> chillxExtractor.videoFromUrl(embedUrl, referer = baseUrl, prefix = "$name - ")
}
override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Torrentio Anime (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 8
extVersionCode = 9
containsNsfw = false
}

View file

@ -75,7 +75,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
currentPage
hasNextPage
}
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in:[RELEASING,FINISHED]) {
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in:[RELEASING,FINISHED,NOT_YET_RELEASED]) {
id
title {
romaji

View file

@ -1,12 +1,11 @@
ext {
extName = 'FilmPalast'
extClass = '.FilmPalast'
extVersionCode = 18
extVersionCode = 19
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:voe-extractor'))
}
}

View file

@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
@ -87,13 +86,13 @@ class FilmPalast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
element.attr("abs:data-player-url")
}
when {
url.contains("https://voe.sx") && hosterSelection.contains("voe") ->
url.contains("voe") && hosterSelection.contains("voe") ->
VoeExtractor(client).videosFromUrl(url)
url.contains("https://upstream.to") && hosterSelection.contains("up") ->
url.contains("upstream") && hosterSelection.contains("up") ->
UpstreamExtractor(client).videoFromUrl(url)
url.contains("https://streamtape.com") && hosterSelection.contains("stape") -> {
url.contains("streamtape") && hosterSelection.contains("stape") -> {
runCatching {
val stapeHeaders = Headers.headersOf(
"Referer",
@ -119,7 +118,7 @@ class FilmPalast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}.getOrNull()
}
url.contains("https://evoload.io") && hosterSelection.contains("evo") -> {
url.contains("evoload") && hosterSelection.contains("evo") -> {
val quality = "Evoload"
document.selectFirst("#EvoVid_html5_api")?.attr("src")?.let { videoUrl ->
if (videoUrl.contains("EvoStreams")) {
@ -129,12 +128,9 @@ class FilmPalast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
}
url.contains("filemoon.sx") && hosterSelection.contains("moon") ->
FilemoonExtractor(client).videosFromUrl(url)
url.contains("hide.com") && hosterSelection.contains("hide") ->
url.contains("hide") && hosterSelection.contains("hide") ->
StreamHideVidExtractor(client).videosFromUrl(url, "StreamHide")
url.contains("streamvid.net") && hosterSelection.contains("vid") ->
url.contains("streamvid") && hosterSelection.contains("vid") ->
StreamHideVidExtractor(client).videosFromUrl(url, "StreamVid")
"wolfstream" in url && hosterSelection.contains("wolf") -> {
@ -258,7 +254,6 @@ class FilmPalast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
"Streamtape",
"Evoload",
"Upstream",
"Filemoon",
"StreamHide",
"StreamVid",
"WolfStream",
@ -268,7 +263,6 @@ class FilmPalast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
"https://streamtape.com",
"https://evoload.io",
"https://upstream.to",
"https://filemoon.sx",
"hide.com",
"streamvid.net",
"https://wolfstream",
@ -282,7 +276,6 @@ class FilmPalast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
"stape",
"evo",
"up",
"moon",
"hide",
"vid",
"wolf",

View file

@ -2,7 +2,7 @@ ext {
extName = 'AniPlay'
extClass = '.AniPlay'
themePkg = 'anilist'
overrideVersionCode = 2
overrideVersionCode = 3
}
apply from: "$rootDir/common.gradle"

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.animeextension.en.aniplay
import android.app.Application
import android.net.Uri
import android.util.Base64
import android.util.Log
import android.widget.Toast
@ -14,7 +15,6 @@ import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.multisrc.anilist.AniListAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
@ -73,14 +73,27 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
val httpUrl = anime.url.toHttpUrl()
val animeId = httpUrl.pathSegments[2]
return GET("$baseUrl/api/anime/episode/$animeId")
val requestBody = "[\"${animeId}\",true,false]"
.toRequestBody("text/plain;charset=UTF-8".toMediaType())
val headersWithAction =
headers.newBuilder()
// next.js stuff I guess
.add("Next-Action", HEADER_NEXT_ACTION_EPISODE_LIST_VALUE)
.build()
return POST(url = "$baseUrl/anime/info/$animeId", headersWithAction, requestBody)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val isMarkFiller = preferences.getBoolean(PREF_MARK_FILLER_EPISODE_KEY, PREF_MARK_FILLER_EPISODE_DEFAULT)
val episodeListUrl = response.request.url
val animeId = episodeListUrl.pathSegments[3]
val providers = response.parseAs<List<EpisodeListResponse>>()
val animeId = episodeListUrl.pathSegments[2]
val responseString = response.body.string()
val episodesArrayString = responseString.split("1:").last()
val providers = episodesArrayString.parseAs<List<EpisodeListResponse>>()
val episodes = mutableMapOf<Int, EpisodeListResponse.Episode>()
val episodeExtras = mutableMapOf<Int, List<EpisodeExtra>>()
@ -94,6 +107,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
val episodeExtra = EpisodeExtra(
source = provider.providerId,
episodeId = episode.id,
episodeNum = episode.number,
hasDub = episode.hasDub,
)
episodeExtras[episodeNumber] = existingEpisodeExtras + listOf(episodeExtra)
@ -142,7 +156,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val episodeUrl = episode.url.toHttpUrl()
val animeId = episodeUrl.queryParameter("id") ?: return emptyList()
val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList()
// val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList()
val extras = episodeUrl.queryParameter("extras")
?.let {
try {
@ -162,33 +176,55 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
}
?: emptyList()
val headersWithAction =
headers.newBuilder()
// next.js stuff I guess
.add("Next-Action", HEADER_NEXT_ACTION_SOURCES_LIST_VALUE)
.build()
val episodeDataList = extras.parallelFlatMapBlocking { extra ->
val languages = mutableListOf("sub").apply {
if (extra.hasDub) add("dub")
}
val url = "$baseUrl/api/anime/source/$animeId"
languages.map { language ->
val requestBody = json.encodeToString(
VideoSourceRequest(
source = extra.source,
episodeId = extra.episodeId,
episodeNum = episodeNum,
subType = language,
),
).toRequestBody("application/json".toMediaType())
val epNum = if (extra.episodeNum == extra.episodeNum.toInt().toFloat()) {
extra.episodeNum.toInt().toString() // If it has no fractional part, convert it to an integer
} else {
extra.episodeNum.toString() // If it has a fractional part, leave it as a float
}
val requestBody = "[\"$animeId\",\"${extra.source}\",\"${extra.episodeId}\",\"$epNum\",\"$language\"]"
.toRequestBody("application/json".toMediaType())
val params = mapOf(
"id" to animeId,
"host" to extra.source,
"ep" to epNum,
"type" to language,
)
val builder = Uri.parse("$baseUrl/anime/watch").buildUpon()
params.map { (k, v) -> builder.appendQueryParameter(k, v); }
val url = builder.build().toString()
Log.i("AniPlay", "Url: $url")
try {
val response = client.newCall(POST(url = url, body = requestBody)).execute().parseAs<VideoSourceResponse>()
val request = POST(url, headersWithAction, requestBody)
val response = client.newCall(request).execute()
val responseString = response.body.string()
val sourcesString = responseString.split("1:").last()
if (sourcesString.startsWith("null")) return@map null
val data = sourcesString.parseAs<VideoSourceResponse>()
EpisodeData(
source = extra.source,
language = language,
response = response,
response = data,
)
} catch (e: IOException) {
Log.w("AniPlay", "VideoList $url IOException", e)
null // Return null to be filtered out
} catch (e: Exception) {
Log.w("AniPlay", "VideoList $url Exception", e)
null // Return null to be filtered out
}
}.filterNotNull() // Filter out null values due to errors
@ -384,6 +420,10 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
private const val PREF_MARK_FILLER_EPISODE_KEY = "mark_filler_episode"
private const val PREF_MARK_FILLER_EPISODE_DEFAULT = true
// These values has probably something to do with Next.js server and hydration
private const val HEADER_NEXT_ACTION_EPISODE_LIST_VALUE = "f3422af67c84852f5e63d50e1f51718f1c0225c4"
private const val HEADER_NEXT_ACTION_SOURCES_LIST_VALUE = "5dbcd21c7c276c4d15f8de29d9ef27aef5ea4a5e"
private val DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
}

View file

@ -57,6 +57,7 @@ data class VideoSourceResponse(
@Serializable
data class EpisodeExtra(
val source: String,
val episodeNum: Float,
val episodeId: String,
val hasDub: Boolean,
)

View file

@ -1,7 +0,0 @@
ext {
extName = 'AnimeVostFr'
extClass = '.AnimeVostFr'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View file

@ -1,410 +0,0 @@
package eu.kanade.tachiyomi.animeextension.fr.animevostfr
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.fr.animevostfr.extractors.CdopeExtractor
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.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AnimeVostFr : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeVostFr"
override val baseUrl = "https://animevostfr.tv"
override val lang = "fr"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/filter-advance/page/$page/")
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/filter-advance/page/$page/?status=ongoing")
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) {
return GET("$baseUrl/?s=$query")
} else {
filters
}
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val typeFilter = filterList.find { it is TypeFilter } as TypeFilter
val yearFilter = filterList.find { it is YearFilter } as YearFilter
val statusFilter = filterList.find { it is StatusFilter } as StatusFilter
val langFilter = filterList.find { it is LangFilter } as LangFilter
val filterPath = if (query.isEmpty()) "/filter-advance" else ""
var urlBuilder = "$baseUrl$filterPath/page/$page/".toHttpUrl().newBuilder()
when {
query.isNotEmpty() ->
urlBuilder =
urlBuilder.addQueryParameter("s", query)
typeFilter.state != 0 ->
urlBuilder =
urlBuilder.addQueryParameter("topic", typeFilter.toUriPart())
genreFilter.state != 0 ->
urlBuilder =
urlBuilder.addQueryParameter("genre", genreFilter.toUriPart())
yearFilter.state != 0 ->
urlBuilder =
urlBuilder.addQueryParameter("years", yearFilter.toUriPart())
statusFilter.state != 0 ->
urlBuilder =
urlBuilder.addQueryParameter("status", statusFilter.toUriPart())
langFilter.state != 0 ->
urlBuilder =
urlBuilder.addQueryParameter("typesub", langFilter.toUriPart())
}
return GET(urlBuilder.build().toString())
}
override fun searchAnimeSelector() = "div.ml-item"
override fun searchAnimeNextPageSelector() = "ul.pagination li:not(.active):last-child"
override fun searchAnimeFromElement(element: Element): SAnime {
val a = element.select("a:has(img)")
val img = a.select("img")
val h2 = a.select("span.mli-info > h2")
return SAnime.create().apply {
title = h2.text()
setUrlWithoutDomain(a.attr("href"))
thumbnail_url = img.attr("data-original")
}
}
override fun popularAnimeSelector() = searchAnimeSelector()
override fun latestUpdatesSelector() = searchAnimeSelector()
override fun popularAnimeNextPageSelector() = searchAnimeNextPageSelector()
override fun latestUpdatesNextPageSelector() = searchAnimeNextPageSelector()
override fun popularAnimeFromElement(element: Element) = searchAnimeFromElement(element)
override fun latestUpdatesFromElement(element: Element) = searchAnimeFromElement(element)
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
return SAnime.create().apply {
title = document.select("h1[itemprop=name]").text()
status = parseStatus(
document.select(
"div.mvici-right > p:contains(Statut) > a:last-child",
).text(),
)
genre = document.select("div.mvici-left > p:contains(Genres)")
.text().substringAfter("Genres: ")
thumbnail_url = document.select("div.thumb > img")
.firstOrNull()?.attr("data-lazy-src")
description = document.select("div[itemprop=description]")
.firstOrNull()?.wholeText()?.trim()
?.substringAfter("\n")
}
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val type = document
.select("div.mvici-right > p:contains(Type) > a:last-child")
.text()
return if (type == "MOVIE") {
return listOf(
SEpisode.create().apply {
url = response.request.url.toString()
name = "Movie"
},
)
} else {
document.select(episodeListSelector()).map { episodeFromElement(it) }.reversed()
}
}
override fun episodeListSelector() = "div#seasonss > div.les-title > a"
override fun episodeFromElement(element: Element): SEpisode {
val number = element.text()
.substringAfterLast("-episode-")
.substringBefore("-")
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = "Épisode $number"
episode_number = number.toFloat()
}
}
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val videoList = mutableListOf<Video>()
val url = if (episode.url.startsWith("https:")) {
episode.url
} else {
baseUrl + episode.url
}
val response = client.newCall(GET(url)).execute()
val parsedResponse = response.asJsoup()
if (parsedResponse.select("title").text().contains("Warning")) {
throw Exception(parsedResponse.select("body").text())
}
val epId = parsedResponse.select("link[rel=shortlink]").attr("href")
.substringAfter("?p=")
parsedResponse.select("div.list-server > select > option").forEach { server ->
videoList.addAll(
extractVideos(
server.attr("value"),
server.text(),
epId,
),
)
}
return videoList
}
private fun extractVideos(serverValue: String, serverName: String, epId: String): List<Video> {
Log.i("bruh", "ID: $epId \nLink: $")
val xhr = Headers.headersOf("x-requested-with", "XMLHttpRequest")
val epLink = client.newCall(GET("$baseUrl/ajax-get-link-stream/?server=$serverValue&filmId=$epId", xhr))
.execute().body.string()
val playlist = mutableListOf<Video>()
when {
epLink.contains("comedyshow.to") -> {
val playlistInterceptor = CloudFlareInterceptor()
val cfClient = client.newBuilder().addInterceptor(playlistInterceptor).build()
val headers = Headers.headersOf(
"referer",
"$baseUrl/",
"user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
)
val playlistResponse = cfClient.newCall(GET(epLink, headers)).execute().body.string()
val headersVideo = Headers.headersOf(
"referer",
epLink,
"user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
)
playlistResponse.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").map {
val quality = it.substringAfter("RESOLUTION=").split(",")[0].split("\n")[0].substringAfter("x") + "p ($serverName)"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
playlist.add(Video(videoUrl, quality, videoUrl, headers = headersVideo))
}
}
epLink.contains("cdopetimes.xyz") -> {
val extractor = CdopeExtractor(client)
playlist.addAll(
extractor.videosFromUrl(epLink),
)
}
}
return playlist.sort()
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "720")
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
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("div.slide-middle h1").text()
anime.description = document.selectFirst("div.slide-desc")!!.ownText()
anime.genre = document.select("div.image-bg-content div.slide-block div.slide-middle ul.slide-top li.right a").joinToString { it.text() }
return anime
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
TypeFilter(),
GenreFilter(),
YearFilter(),
StatusFilter(),
LangFilter(),
)
private class TypeFilter : UriPartFilter(
"Type",
arrayOf(
Pair("-----", ""),
Pair("Anime", "anime"),
Pair("Cartoon", "cartoon"),
Pair("MOVIE", "movie"),
Pair("SERIES", "series"),
),
)
private class GenreFilter : UriPartFilterReverse(
"Genre",
arrayOf(
Pair("", "-----"),
Pair("action", "Action"),
Pair("adventure", "Adventure"),
Pair("animation", "Animation"),
Pair("martial-arts", "Arts martiaux"),
Pair("biography", "Biographie"),
Pair("comedy", "Comédie"),
Pair("crime", "Crime"),
Pair("demence", "Démence"),
Pair("demon", "Demons"),
Pair("documentaire", "Documentaire"),
Pair("drame", "Drama"),
Pair("ecchi", "Ecchi"),
Pair("enfants", "Enfants"),
Pair("espace", "Espace"),
Pair("famille", "Famille"),
Pair("fantasy", "Fantastique"),
Pair("game", "Game"),
Pair("harem", "Harem"),
Pair("historical", "Historique"),
Pair("horror", "Horreur"),
Pair("jeux", "Jeux"),
Pair("josei", "Josei"),
Pair("kids", "Kids"),
Pair("magic", "Magie"),
Pair("mecha", "Mecha"),
Pair("military", "Militaire"),
Pair("monster", "Monster"),
Pair("music", "Musique"),
Pair("mystere", "Mystère"),
Pair("parody", "Parodie"),
Pair("police", "Policier"),
Pair("psychological", "Psychologique"),
Pair("romance", "Romance"),
Pair("samurai", "Samurai"),
Pair("sci-fi", "Sci-Fi"),
Pair("school", "Scolaire"),
Pair("seinen", "Seinen"),
Pair("short", "Short"),
Pair("shoujo", "Shoujo"),
Pair("shoujo-ai", "Shoujo Ai"),
Pair("shounen", "Shounen"),
Pair("shounen-ai", "Shounen Ai"),
Pair("sport", "Sport"),
Pair("super-power", "Super Pouvoir"),
Pair("supernatural", "Surnaturel"),
Pair("suspense", "Suspense"),
Pair("thriller", "Thriller"),
Pair("silce-of-life", "Tranche de vie"),
Pair("vampire", "Vampire"),
Pair("cars", "Voitures"),
Pair("war", "War"),
Pair("western", "Western"),
),
)
private class YearFilter : UriPartFilterYears(
"Year",
Array(62) {
if (it == 0) {
"-----"
} else {
(2022 - (it - 1)).toString()
}
},
)
private class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("-----", ""),
Pair("Fin", "completed"),
Pair("En cours", "ongoing"),
),
)
private class LangFilter : UriPartFilter(
"La langue",
arrayOf(
Pair("-----", ""),
Pair("VO", "vo"),
Pair("Animé Vostfr", "vostfr"),
Pair("Animé VF", "vf"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private open class UriPartFilterReverse(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first
}
private open class UriPartFilterYears(displayName: String, val years: Array<String>) :
AnimeFilter.Select<String>(displayName, years) {
fun toUriPart() = years[state]
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Qualité préférée"
entries = arrayOf("720p", "360p")
entryValues = arrayOf("720", "360")
setDefaultValue("720")
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(videoQualityPref)
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Fin" -> SAnime.COMPLETED
"En cours" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
}

View file

@ -1,88 +0,0 @@
package eu.kanade.tachiyomi.animeextension.fr.animevostfr
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 eu.kanade.tachiyomi.network.GET
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.url.toString()
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 = request.header("User-Agent")
?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63\""
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if (request.url.toString().contains("master.txt")) {
newRequest = GET(request.url.toString(), request.requestHeaders.toHeaders())
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, 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

@ -1,65 +0,0 @@
package eu.kanade.tachiyomi.animeextension.fr.animevostfr.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
@Serializable
data class CdopeResponse(
val data: List<FileObject>,
) {
@Serializable
data class FileObject(
val file: String,
val label: String,
val type: String,
)
}
class CdopeExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val videoList = mutableListOf<Video>()
val id = url.substringAfter("/v/")
val body = "r=&d=cdopetimes.xyz".toRequestBody("application/x-www-form-urlencoded".toMediaType())
val headers = Headers.headersOf(
"Accept", "*/*",
"Content-Type", "application/x-www-form-urlencoded; charset=UTF-8",
"Host", "cdopetimes.xyz",
"Origin", "https://cdopetimes.xyz",
"Referer", url,
"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0",
"X-Requested-With", "XMLHttpRequest",
)
val response = client.newCall(
POST("https://cdopetimes.xyz/api/source/$id", body = body, headers = headers),
).execute()
Json { ignoreUnknownKeys = true }.decodeFromString<CdopeResponse>(response.body.string()).data.forEach { file ->
val videoHeaders = Headers.headersOf(
"Accept",
"video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5",
"Referer",
"https://cdopetimes.xyz/",
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0",
)
videoList.add(
Video(
file.file,
"${file.label} (Cdope - ${file.type})",
file.file,
headers = videoHeaders,
),
)
}
return videoList
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'OtakuDesu'
extClass = '.OtakuDesu'
extVersionCode = 26
extVersionCode = 27
}
apply from: "$rootDir/common.gradle"
@ -9,4 +9,5 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:yourupload-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:streamhidevid-extractor"))
}

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
@ -222,6 +223,7 @@ class OtakuDesu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val filelionsExtractor by lazy { StreamWishExtractor(client, headers) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private val streamHideVidExtractor by lazy { StreamHideVidExtractor(client) }
private fun getVideosFromEmbed(quality: String, link: String): List<Video> {
return when {
@ -251,6 +253,9 @@ class OtakuDesu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
listOf(Video(videoUrl, "Mp4upload - $quality", videoUrl, headers))
}
}
"vidhide" in link -> {
streamHideVidExtractor.videosFromUrl(link)
}
else -> emptyList()
}
}