Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".fr.animesama.AnimeSamaUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="anime-sama.fr"
android:pathPattern="/catalogue/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,13 @@
ext {
extName = 'Anime-Sama'
extClass = '.AnimeSama'
extVersionCode = 9
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:sibnet-extractor'))
implementation(project(':lib:vk-extractor'))
implementation(project(':lib:sendvid-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View file

@ -0,0 +1,295 @@
package eu.kanade.tachiyomi.animeextension.fr.animesama
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Anime-Sama"
override val baseUrl = "https://anime-sama.fr"
override val lang = "fr"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val database by lazy {
client.newCall(GET("$baseUrl/catalogue/listing_all.php", headers)).execute()
.asJsoup().select(".cardListAnime")
}
// ============================== Popular ===============================
override fun popularAnimeParse(response: Response): AnimesPage {
val doc = response.body.string()
val page = response.request.url.fragment?.toInt() ?: 0
val regex = Regex("^\\s*carteClassique\\(\\s*.*?\\s*,\\s*\"(.*?)\".*\\)", RegexOption.MULTILINE)
val chunks = regex.findAll(doc).chunked(5).toList()
val seasons = chunks.getOrNull(page - 1)?.flatMap {
val animeUrl = "$baseUrl/catalogue/${it.groupValues[1]}"
fetchAnimeSeasons(animeUrl)
}?.toList().orEmpty()
return AnimesPage(seasons, page < chunks.size)
}
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/#$page")
// =============================== Latest ===============================
override fun latestUpdatesParse(response: Response): AnimesPage {
val animes = response.asJsoup()
val seasons = animes.select("h2:contains(derniers ajouts) + .scrollBarStyled > div").flatMap {
val animeUrl = it.getElementsByTag("a").attr("href")
fetchAnimeSeasons(animeUrl)
}
return AnimesPage(seasons, false)
}
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
// =============================== Search ===============================
override fun getFilterList() = AnimeSamaFilters.FILTER_LIST
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
if (query.startsWith(PREFIX_SEARCH)) {
return AnimesPage(fetchAnimeSeasons("$baseUrl/catalogue/${query.removePrefix(PREFIX_SEARCH)}/"), false)
}
val params = AnimeSamaFilters.getSearchFilters(filters)
val elements = database
.asSequence()
.filter { it.select("h1, p").fold(false) { v, e -> v || e.text().contains(query, true) } }
.filter { params.include.all { p -> it.className().contains(p) } }
.filter { params.exclude.none { p -> it.className().contains(p) } }
.filter { params.types.fold(params.types.isEmpty()) { v, p -> v || it.className().contains(p) } }
.filter { params.language.fold(params.language.isEmpty()) { v, p -> v || it.className().contains(p) } }
.chunked(5)
.toList()
if (elements.isEmpty()) return AnimesPage(emptyList(), false)
val animes = elements[page - 1].flatMap {
fetchAnimeSeasons(it.getElementsByTag("a").attr("href"))
}
return AnimesPage(animes, page < elements.size)
}
override fun searchAnimeParse(response: Response): AnimesPage = throw UnsupportedOperationException()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw UnsupportedOperationException()
// =========================== Anime Details ============================
override suspend fun getAnimeDetails(anime: SAnime): SAnime = anime
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
// ============================== Episodes ==============================
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val animeUrl = "$baseUrl${anime.url.substringBeforeLast("/")}"
val movie = anime.url.split("#").getOrElse(1) { "" }.toIntOrNull()
val players = VOICES_VALUES.map { fetchPlayers("$animeUrl/$it") }
val episodes = playersToEpisodes(players)
return if (movie == null) episodes.reversed() else listOf(episodes[movie])
}
override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val playerUrls = json.decodeFromString<List<List<String>>>(episode.url)
val videos = playerUrls.flatMapIndexed { i, it ->
val prefix = "(${VOICES_VALUES[i].uppercase()}) "
it.parallelCatchingFlatMap { playerUrl ->
with(playerUrl) {
when {
contains("sibnet.ru") -> SibnetExtractor(client).videosFromUrl(playerUrl, prefix)
contains("vk.") -> VkExtractor(client, headers).videosFromUrl(playerUrl, prefix)
contains("sendvid.com") -> SendvidExtractor(client, headers).videosFromUrl(playerUrl, prefix)
else -> emptyList()
}
}
}
}.sort()
return videos
}
// ============================ Utils =============================
private fun sanitizeEpisodesJs(doc: String) = doc
.replace(Regex("[\"\t]"), "") // Fix trash format
.replace("'", "\"") // Fix quotes
.replace(Regex("/\\*.*?\\*/", setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)), "") // Remove block comments
.replace(Regex("(^|,|\\[)\\s*//.*?$", RegexOption.MULTILINE), "$1") // Remove line comments
.replace(Regex(",\\s*]"), "]") // Remove trailing comma
override fun List<Video>.sort(): List<Video> {
val voices = preferences.getString(PREF_VOICES_KEY, PREF_VOICES_DEFAULT)!!
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(voices, true) },
{ it.quality.contains(quality) },
),
).reversed()
}
private fun fetchAnimeSeasons(animeUrl: String): List<SAnime> {
val res = client.newCall(GET(animeUrl)).execute()
return fetchAnimeSeasons(res)
}
private fun fetchAnimeSeasons(response: Response): List<SAnime> {
val animeDoc = response.asJsoup()
val animeUrl = response.request.url
val animeName = animeDoc.getElementById("titreOeuvre")?.text() ?: ""
val seasonRegex = Regex("^\\s*panneauAnime\\(\"(.*)\", \"(.*)\"\\)", RegexOption.MULTILINE)
val scripts = animeDoc.select("h2 + p + div > script, h2 + div > script").toString()
val animes = seasonRegex.findAll(scripts).flatMapIndexed { animeIndex, seasonMatch ->
val (seasonName, seasonStem) = seasonMatch.destructured
if (seasonStem.contains("film", true)) {
val moviesUrl = "$animeUrl/$seasonStem"
val movies = fetchPlayers(moviesUrl).ifEmpty { return@flatMapIndexed emptyList() }
val movieNameRegex = Regex("^\\s*newSPF\\(\"(.*)\"\\);", RegexOption.MULTILINE)
val moviesDoc = client.newCall(GET(moviesUrl)).execute().body.string()
val matches = movieNameRegex.findAll(moviesDoc).toList()
List(movies.size) { i ->
val title = when {
animeIndex == 0 && movies.size == 1 -> animeName
matches.size > i -> "$animeName ${matches[i].destructured.component1()}"
movies.size == 1 -> "$animeName Film"
else -> "$animeName Film ${i + 1}"
}
Triple(title, "$moviesUrl#$i", SAnime.COMPLETED)
}
} else {
listOf(Triple("$animeName $seasonName", "$animeUrl/$seasonStem", SAnime.UNKNOWN))
}
}
return animes.map {
SAnime.create().apply {
title = it.first
thumbnail_url = animeDoc.getElementById("coverOeuvre")?.attr("src")
description = animeDoc.select("h2:contains(synopsis) + p").text()
genre = animeDoc.select("h2:contains(genres) + a").text()
setUrlWithoutDomain(it.second)
status = it.third
initialized = true
}
}.toList()
}
private fun playersToEpisodes(list: List<List<List<String>>>): List<SEpisode> =
List(list.fold(0) { acc, it -> maxOf(acc, it.size) }) { episodeNumber ->
val players = list.map { it.getOrElse(episodeNumber) { emptyList() } }
SEpisode.create().apply {
name = "Episode ${episodeNumber + 1}"
url = json.encodeToString(players)
episode_number = (episodeNumber + 1).toFloat()
scanlator = players.mapIndexedNotNull { i, it -> if (it.isNotEmpty()) VOICES_VALUES[i] else null }.joinToString().uppercase()
}
}
private fun fetchPlayers(url: String): List<List<String>> {
val docUrl = "$url/episodes.js"
val players = mutableListOf<List<String>>()
val doc = client.newCall(GET(docUrl)).execute().run {
if (code != 200) {
close()
return listOf()
}
body.string()
}
val sanitizedDoc = sanitizeEpisodesJs(doc)
for (i in 1..8) {
val numPlayers = getPlayers("eps$i", sanitizedDoc)
if (numPlayers != null) players.add(numPlayers)
}
val asPlayers = getPlayers("epsAS", sanitizedDoc)
if (asPlayers != null) players.add(asPlayers)
if (players.isEmpty()) return emptyList()
return List(players[0].size) { i -> players.mapNotNull { it.getOrNull(i) }.distinct() }
}
private fun getPlayers(playerName: String, doc: String): List<String>? {
val playerRegex = Regex("$playerName\\s*=\\s*(\\[.*?])", RegexOption.DOT_MATCHES_ALL)
val string = playerRegex.find(doc)?.groupValues?.get(1)
return if (string != null) json.decodeFromString<List<String>>(string) else null
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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)
ListPreference(screen.context).apply {
key = PREF_VOICES_KEY
title = "Préférence des voix"
entries = VOICES
entryValues = VOICES_VALUES
setDefaultValue(PREF_VOICES_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)
}
companion object {
const val PREFIX_SEARCH = "id:"
private val VOICES = arrayOf(
"Préférer VOSTFR",
"Préférer VF",
)
private val VOICES_VALUES = arrayOf(
"vostfr",
"vf",
)
private const val PREF_VOICES_KEY = "voices_preference"
private const val PREF_VOICES_DEFAULT = "vostfr"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
}
}

View file

@ -0,0 +1,125 @@
package eu.kanade.tachiyomi.animeextension.fr.animesama
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimeSamaFilters {
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)
open class TriStateFilterList(name: String, values: List<TriFilter>) : AnimeFilter.Group<AnimeFilter.TriState>(name, values)
class TriFilter(name: String) : AnimeFilter.TriState(name)
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>>,
): List<String> {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}
}
private inline fun <reified R> AnimeFilterList.parseTriFilter(
options: Array<Pair<String, String>>,
): List<List<String>> {
return (this.getFirst<R>() as TriStateFilterList).state
.filterNot { it.isIgnored() }
.map { filter -> filter.state to filter.name }
.groupBy { it.first }
.let {
val included = it.get(AnimeFilter.TriState.STATE_INCLUDE)?.map { options.find { o -> o.first == it.second }!!.second } ?: emptyList()
val excluded = it.get(AnimeFilter.TriState.STATE_EXCLUDE)?.map { options.find { o -> o.first == it.second }!!.second } ?: emptyList()
listOf(included, excluded)
}
}
class TypesFilter : CheckBoxFilterList(
"Type",
AnimeSamaFiltersData.TYPES.map { CheckBoxVal(it.first, false) },
)
class LangFilter : CheckBoxFilterList(
"Langage",
AnimeSamaFiltersData.LANGUAGES.map { CheckBoxVal(it.first, false) },
)
class GenresFilter : TriStateFilterList(
"Genre",
AnimeSamaFiltersData.GENRES.map { TriFilter(it.first) },
)
val FILTER_LIST get() = AnimeFilterList(
TypesFilter(),
LangFilter(),
GenresFilter(),
)
data class SearchFilters(
val types: List<String> = emptyList(),
val language: List<String> = emptyList(),
val include: List<String> = emptyList(),
val exclude: List<String> = emptyList(),
)
fun getSearchFilters(filters: AnimeFilterList): SearchFilters {
if (filters.isEmpty()) return SearchFilters()
val (include, exclude) = filters.parseTriFilter<GenresFilter>(AnimeSamaFiltersData.GENRES)
return SearchFilters(
filters.parseCheckbox<TypesFilter>(AnimeSamaFiltersData.TYPES),
filters.parseCheckbox<LangFilter>(AnimeSamaFiltersData.LANGUAGES),
include,
exclude,
)
}
private object AnimeSamaFiltersData {
val TYPES = arrayOf(
Pair("Anime", "Anime"),
Pair("Film", "Film"),
Pair("Autres", "Autres"),
)
val LANGUAGES = arrayOf(
Pair("VF", "VF"),
Pair("VOSTFR", "VOSTFR"),
)
val GENRES = arrayOf(
Pair("Action", "Action"),
Pair("Aventure", "Aventure"),
Pair("Combats", "Combats"),
Pair("Comédie", "Comédie"),
Pair("Drame", "Drame"),
Pair("Ecchi", "Ecchi"),
Pair("École", "School-Life"),
Pair("Fantaisie", "Fantasy"),
Pair("Horreur", "Horreur"),
Pair("Isekai", "Isekai"),
Pair("Josei", "Josei"),
Pair("Mystère", "Mystère"),
Pair("Psychologique", "Psychologique"),
Pair("Quotidien", "Slice-of-Life"),
Pair("Romance", "Romance"),
Pair("Seinen", "Seinen"),
Pair("Shônen", "Shônen"),
Pair("Shôjo", "Shôjo"),
Pair("Sports", "Sports"),
Pair("Surnaturel", "Surnaturel"),
Pair("Tournois", "Tournois"),
Pair("Yaoi", "Yaoi"),
Pair("Yuri", "Yuri"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.fr.animesama
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts www.anime-sama.fr/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimeSamaUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimeSama.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View file

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

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

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

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".fr.anisama.AniSamaUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="fr.anisama.net"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,16 @@
ext {
extName = 'AniSama'
extClass = '.AniSama'
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:sendvid-extractor'))
implementation(project(':lib:sibnet-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,346 @@
package eu.kanade.tachiyomi.animeextension.fr.anisama
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.fr.anisama.extractors.VidCdnExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AniSama : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "AniSama"
override val baseUrl = "https://fr.anisama.net"
override val lang = "fr"
override val supportsLatest = true
private val aniSamaFilters by lazy { AniSamaFilters(baseUrl, client) }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/most-popular/?page=$page")
override fun popularAnimeSelector() = ".film_list article"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
title = element.select(".dynamic-name").text()
thumbnail_url = element.select(".film-poster-img").attr("data-src")
setUrlWithoutDomain(element.select(".film-poster-ahref").attr("href"))
}
override fun popularAnimeNextPageSelector() = ".ap__-btn-next a:not(.disabled)"
override fun popularAnimeParse(response: Response): AnimesPage {
aniSamaFilters.fetchFilters()
return super.popularAnimeParse(response)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/recently-added/?page=$page")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesParse(response: Response): AnimesPage {
aniSamaFilters.fetchFilters()
return super.latestUpdatesParse(response)
}
// =============================== Search ===============================
override fun getFilterList() = aniSamaFilters.getFilterList()
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/anime/$id")).awaitSuccess().use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
.apply { setUrlWithoutDomain(response.request.url.toString()) }
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = "$baseUrl/filter".toHttpUrl().newBuilder()
url.addQueryParameter("keyword", query)
url.addQueryParameter("page", page.toString())
filters.filterIsInstance<AniSamaFilters.QueryParameterFilter>().forEach {
val (name, value) = it.toQueryParameter()
if (value != null) url.addQueryParameter(name, value)
}
return GET(url.build())
}
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
private fun Elements.getMeta(name: String) = select(".item:has(.item-title:contains($name)) > .item-content").text()
private fun Elements.parseStatus() = when (getMeta("Status")) {
"Terminer" -> SAnime.COMPLETED
"En cours" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
override fun animeDetailsParse(document: Document): SAnime {
val details = document.select(".anime-detail")
return SAnime.create().apply {
title = details.select(".dynamic-name").text().substringBeforeLast(" ")
thumbnail_url = details.select(".film-poster-img").attr("src")
url = document.select("link[rel=canonical]").attr("href")
artist = details.getMeta("Studio")
status = details.parseStatus()
description = details.select(".shorting").text()
genre = details.getMeta("Genre")
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = ".ep-item"
private fun SAnime.getId(): String = url.substringAfterLast("-")
override fun episodeListRequest(anime: SAnime) = GET(
"$baseUrl/ajax/episode/list/${anime.getId()}",
headers.newBuilder().set("Referer", "$baseUrl${anime.url}").build(),
)
@Serializable
data class HtmlResponseDTO(val html: String)
override fun episodeListParse(response: Response): List<SEpisode> {
val document = Jsoup.parse(response.parseAs<HtmlResponseDTO>().html)
return document.select(episodeListSelector()).parallelMapBlocking(::episodeFromElement).reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
episode_number = element.attr("data-number").toFloat()
name = element.attr("title")
val id = element.attr("href").substringAfterLast("=")
url = "/ajax/episode/servers?episodeId=$id"
}
}
// ============================ Video Links =============================
@Serializable
data class PlayerInfoDTO(val link: String)
override fun videoListSelector() = ".server-item"
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val vidCdnExtractor by lazy { VidCdnExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val streamHideVidExtractor by lazy { StreamHideVidExtractor(client) }
override fun videoListRequest(episode: SEpisode) = GET(
"$baseUrl${episode.url}",
headers.newBuilder().set("Referer", "$baseUrl/").build(),
)
override fun videoListParse(response: Response): List<Video> {
val document = Jsoup.parse(response.parseAs<HtmlResponseDTO>().html)
val epid = response.request.url.toString().substringAfterLast("=")
val serverBlacklist = preferences.serverBlacklist
return document.select(videoListSelector()).parallelCatchingFlatMapBlocking {
val playerUrl = client.newCall(
GET("$baseUrl/ajax/episode/sources?id=${it.attr("data-id")}&epid=$epid"),
).execute().parseAs<PlayerInfoDTO>().link
val prefix = "(${it.attr("data-type").uppercase()}) "
if (serverBlacklist.fold(false) { acc, v -> acc || playerUrl.contains(Regex(v)) }) {
emptyList()
} else {
with(playerUrl) {
when {
contains("toonanime.xyz") -> vidCdnExtractor.videosFromUrl(playerUrl, { "$prefix$it CDN" })
contains("filemoon.sx") -> filemoonExtractor.videosFromUrl(this, "$prefix Filemoon - ")
contains("sibnet.ru") -> sibnetExtractor.videosFromUrl(this, prefix)
contains("sendvid.com") -> sendvidExtractor.videosFromUrl(this, prefix)
contains("voe.sx") -> voeExtractor.videosFromUrl(this, prefix)
contains(Regex("(d000d|dood)")) -> doodExtractor.videosFromUrl(this, "${prefix}DoodStream")
contains("vidhide") -> streamHideVidExtractor.videosFromUrl(this, prefix)
else -> emptyList()
}
}
}
}
}
override fun List<Video>.sort(): List<Video> {
val language = preferences.language
val quality = preferences.quality
val server = preferences.server
return sortedWith(
compareBy(
{ it.quality.contains(server) },
{ it.quality.contains(language) },
{ it.quality.contains(quality) },
),
).reversed()
}
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================ Preferences =============================
@Suppress("UNCHECKED_CAST")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY
title = PREF_LANGUAGE_TITLE
entries = PREF_LANGUAGE_ENTRIES.map { it.first }.toTypedArray()
entryValues = PREF_LANGUAGE_ENTRIES.map { it.second }.toTypedArray()
setDefaultValue(PREF_LANGUAGE_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, new ->
val index = findIndexOfValue(new as String)
preferences.edit().putString(key, entryValues[index] as String).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES.map { it.first }.toTypedArray()
entryValues = PREF_QUALITY_ENTRIES.map { it.second }.toTypedArray()
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, new ->
val index = findIndexOfValue(new as String)
preferences.edit().putString(key, entryValues[index] as String).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE
entries = PREF_SERVER_ENTRIES.map { it.first }.toTypedArray()
entryValues = PREF_SERVER_ENTRIES.map { it.second }.toTypedArray()
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, new ->
val index = findIndexOfValue(new as String)
preferences.edit().putString(key, entryValues[index] as String).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_SERVER_BLACKLIST_KEY
title = PREF_SERVER_BLACKLIST_TITLE
entries = PREF_SERVER_ENTRIES.filter { it.second.isNotBlank() }.map { it.first }.toTypedArray()
entryValues = PREF_SERVER_ENTRIES.filter { it.second.isNotBlank() }.map { it.third }.toTypedArray()
setDefaultValue(PREF_SERVER_BLACKLIST_DEFAULT)
setSummaryFromValues(preferences.serverBlacklist)
setOnPreferenceChangeListener { _, new ->
val newValue = new as Set<String>
setSummaryFromValues(newValue)
preferences.edit().putStringSet(key, newValue).commit()
}
}.also(screen::addPreference)
}
private fun MultiSelectListPreference.setSummaryFromValues(values: Set<String>) {
summary = values.joinToString { entries[findIndexOfValue(it)] }.ifBlank { "Aucun" }
}
// ============================= Utilities ==============================
private val SharedPreferences.language get() = getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
private val SharedPreferences.quality get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.server get() = getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
private val SharedPreferences.serverBlacklist get() = getStringSet(PREF_SERVER_BLACKLIST_KEY, PREF_SERVER_BLACKLIST_DEFAULT)!!
companion object {
const val PREFIX_SEARCH = "id:"
private const val PREF_LANGUAGE_KEY = "preferred_sub"
private const val PREF_LANGUAGE_TITLE = "Langue préférée"
private const val PREF_LANGUAGE_DEFAULT = ""
private val PREF_LANGUAGE_ENTRIES = arrayOf(
Pair("VF", "VF"),
Pair("VOSTFR", "VOSTFR"),
Pair("Aucune", ""),
)
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualité préférée"
private const val PREF_QUALITY_DEFAULT = ""
private val PREF_QUALITY_ENTRIES = arrayOf(
Pair("1080p", "1080"),
Pair("720p", "720"),
Pair("480p", "480"),
Pair("360p", "360"),
Pair("Aucune", ""),
)
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Lecteur préféré"
private const val PREF_SERVER_DEFAULT = ""
private const val PREF_SERVER_BLACKLIST_KEY = "blacklist_server"
private const val PREF_SERVER_BLACKLIST_TITLE = "Lecteurs bloqués"
private val PREF_SERVER_BLACKLIST_DEFAULT = emptySet<String>()
private val PREF_SERVER_ENTRIES = arrayOf(
Triple("Toonanime", "CDN", "toonanime\\.xyz"),
Triple("Filemoon", "Filemoon", "filemoon\\.sx"),
Triple("Sibnet", "Sibnet", "sibnet\\.ru"),
Triple("Sendvid", "Sendvid", "sendvid\\.com"),
Triple("Voe", "Voe", "voe\\.sx"),
Triple("DoodStream", "DoodStream", "(dood|d000d)"),
Triple("StreamHideVid", "StreamHideVid", "vidhide"),
Triple("Aucun", "", "^$"),
)
}
}

View file

@ -0,0 +1,80 @@
package eu.kanade.tachiyomi.animeextension.fr.anisama
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
import org.jsoup.select.Elements
class AniSamaFilters(
private val baseUrl: String,
private val client: OkHttpClient,
) {
private var error = false
private lateinit var filterList: AnimeFilterList
interface QueryParameterFilter { fun toQueryParameter(): Pair<String, String?> }
private class Checkbox(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private class CheckboxList(name: String, private val paramName: String, private val pairs: List<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { Checkbox(it.first) }), QueryParameterFilter {
override fun toQueryParameter() = Pair(
paramName,
state.asSequence()
.filter { it.state }
.map { checkbox -> pairs.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
.joinToString(","),
)
}
private class Select(name: String, private val paramName: String, private val pairs: List<Pair<String, String>>) :
AnimeFilter.Select<String>(name, pairs.map { it.first }.toTypedArray()), QueryParameterFilter {
override fun toQueryParameter() = Pair(paramName, pairs[state].second)
}
fun getFilterList(): AnimeFilterList {
return if (error) {
AnimeFilterList(AnimeFilter.Header("Erreur lors de la récupération des filtres."))
} else if (this::filterList.isInitialized) {
filterList
} else {
AnimeFilterList(AnimeFilter.Header("Utilise \"Réinitialiser\" pour charger les filtres."))
}
}
fun fetchFilters() {
if (!this::filterList.isInitialized) {
runCatching {
error = false
filterList = client.newCall(GET("$baseUrl/filter"))
.execute()
.asJsoup()
.let(::filtersParse)
}.onFailure { error = true }
}
}
private fun Elements.parseFilterValues(name: String): List<Pair<String, String>> =
select(".item:has(.btn:contains($name)) li").map {
Pair(it.text(), it.select("input").attr("value"))
}
private fun filtersParse(document: Document): AnimeFilterList {
val form = document.select(".block_area-filter")
return AnimeFilterList(
CheckboxList("Genres", "genre", form.parseFilterValues("Genre")),
CheckboxList("Saisons", "season", form.parseFilterValues("Saison")),
CheckboxList("Années", "year", form.parseFilterValues("Année")),
CheckboxList("Types", "type", form.parseFilterValues("Type")),
CheckboxList("Status", "status", form.parseFilterValues("Status")),
CheckboxList("Langues", "language", form.parseFilterValues("Langue")),
Select("Trié par", "sort", form.parseFilterValues("Sort")),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.fr.anisama
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://fr.anisama.net/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AniSamaUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AniSama.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.animeextension.fr.anisama.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.internal.commonEmptyHeaders
class VidCdnExtractor(
private val client: OkHttpClient,
headers: Headers = commonEmptyHeaders,
) {
private val headers = headers.newBuilder()
.set("Referer", "https://msb.toonanime.xyz")
.build()
@Serializable
data class CdnSourceDto(val file: String)
@Serializable
data class CdnResponseDto(val sources: List<CdnSourceDto>)
fun videosFromUrl(
url: String,
videoNameGen: (String) -> String = { quality -> quality },
): List<Video> {
val httpUrl = url.toHttpUrl()
val source = when {
url.contains("embeds.html") -> Pair("sib2", "Sibnet")
// their sendvid server is currently borken lmao
// url.contains("embedsen.html") -> Pair("azz", "Sendvid")
else -> return emptyList()
}
val id = httpUrl.queryParameter("id")
val epid = httpUrl.queryParameter("epid")
val cdnUrl = "https://cdn2.vidcdn.xyz/${source.first}/$id?epid=$epid"
val res = client.newCall(GET(cdnUrl, headers)).execute().parseAs<CdnResponseDto>()
return res.sources.map {
val file = if (it.file.startsWith("http")) it.file else "https:${it.file}"
Video(
file,
videoNameGen(source.second),
file,
)
}
}
}

View file

@ -0,0 +1,11 @@
ext {
extName = 'EmpireStreaming'
extClass = '.EmpireStreaming'
extVersionCode = 16
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:dood-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,297 @@
package eu.kanade.tachiyomi.animeextension.fr.empirestreaming
import android.app.Application
import android.content.SharedPreferences
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
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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class EmpireStreaming : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "EmpireStreaming"
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
override val lang = "fr"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
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 popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
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 UnsupportedOperationException()
override fun searchAnimeNextPageSelector() = null
override fun searchAnimeSelector() = throw UnsupportedOperationException()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw UnsupportedOperationException()
override fun searchAnimeParse(response: Response) = throw UnsupportedOperationException()
private val searchItems by lazy {
client.newCall(GET("$baseUrl/api/views/contenitem", headers)).execute()
.let {
json.decodeFromString<SearchResultsDto>(it.body.string()).items
}
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
val entriesPages = searchItems.filter { it.title.contains(query, true) }
.sortedBy { it.title }
.chunked(30) // to prevent exploding the user screen with 984948984 results
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 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 UnsupportedOperationException()
override fun episodeListParse(response: Response): List<SEpisode> {
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 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)
}
}
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()
}
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
// val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
val videos = episode.url.split(", ").parallelCatchingFlatMap {
val (id, type, hoster) = it.split("|")
if (hoster !in hosterSelection) return@parallelCatchingFlatMap emptyList()
videosFromPath("$id/$type", hoster)
}
return videos
}
private suspend fun videosFromPath(path: String, hoster: String): List<Video> {
val url = client.newCall(GET("$baseUrl/player_submit/$path", headers)).await()
.body.string()
.substringAfter("window.location.href = \"")
.substringBefore('"')
return when (hoster) {
"doodstream" -> DoodExtractor(client).videosFromUrl(url)
"voe" -> VoeExtractor(client).videosFromUrl(url)
"Eplayer" -> EplayerExtractor(client).videosFromUrl(url)
else -> null
} ?: emptyList()
}
override fun videoListParse(response: Response) = throw UnsupportedOperationException()
override fun List<Video>.sort(): List<Video> {
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 UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
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 ->
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_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)
}
// ============================= Utilities ==============================
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
private fun List<VideoDto>.encode() = joinToString { it.encoded }
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 = "Voe"
private val PREF_HOSTER_ENTRIES = arrayOf("Voe", "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", "Dood", "Eplayer")
private val PREF_HOSTER_SELECTION_VALUES = arrayOf("voe", "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()
.body.string()
.substringAfter("videoSource\":\"")
.substringBefore('"')
.replace("\\", "")
// TODO: Use playlist-utils
val separator = "#EXT-X-STREAM-INF"
return client.newCall(GET(masterUrl, headers)).execute()
.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"
}
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".fr.franime.FrAnimeUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="franime.fr"
android:pathPattern="/anime/..*"
/>
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,14 @@
ext {
extName = 'FrAnime'
extClass = '.FrAnime'
extVersionCode = 11
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:vido-extractor'))
implementation(project(':lib:vk-extractor'))
implementation(project(':lib:sendvid-extractor'))
implementation(project(':lib:sibnet-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,187 @@
package eu.kanade.tachiyomi.animeextension.fr.franime
import eu.kanade.tachiyomi.animeextension.fr.franime.dto.Anime
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.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class FrAnime : AnimeHttpSource() {
override val name = "FRAnime"
private val domain = "franime.fr"
override val baseUrl = "https://$domain"
private val baseApiUrl = "https://api.$domain/api"
private val baseApiAnimeUrl = "$baseApiUrl/anime"
override val lang = "fr"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
private val json: Json by injectLazy()
private val database by lazy {
client.newCall(GET("$baseApiUrl/animes/", headers)).execute()
.body.string()
.let { json.decodeFromString<List<Anime>>(it) }
}
// ============================== Popular ===============================
override suspend fun getPopularAnime(page: Int) =
pagesToAnimesPage(database.sortedByDescending { it.note }, page)
override fun popularAnimeParse(response: Response) = throw UnsupportedOperationException()
override fun popularAnimeRequest(page: Int) = throw UnsupportedOperationException()
// =============================== Latest ===============================
override suspend fun getLatestUpdates(page: Int) = pagesToAnimesPage(database.reversed(), page)
override fun latestUpdatesParse(response: Response): AnimesPage = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
val pages = database.filter {
it.title.contains(query, true) ||
it.originalTitle.contains(query, true) ||
it.titlesAlt.en?.contains(query, true) == true ||
it.titlesAlt.enJp?.contains(query, true) == true ||
it.titlesAlt.jaJp?.contains(query, true) == true ||
titleToUrl(it.originalTitle).contains(query)
}
return pagesToAnimesPage(pages, page)
}
override fun searchAnimeParse(response: Response): AnimesPage = throw UnsupportedOperationException()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw UnsupportedOperationException()
// =========================== Anime Details ============================
override suspend fun getAnimeDetails(anime: SAnime): SAnime = anime
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
// ============================== Episodes ==============================
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val url = (baseUrl + anime.url).toHttpUrl()
val stem = url.encodedPathSegments.last()
val language = url.queryParameter("lang") ?: "vo"
val season = url.queryParameter("s")?.toIntOrNull() ?: 1
val animeData = database.first { titleToUrl(it.originalTitle) == stem }
val episodes = animeData.seasons[season - 1].episodes
.mapIndexedNotNull { index, episode ->
val players = when (language) {
"vo" -> episode.languages.vo
else -> episode.languages.vf
}.players
if (players.isEmpty()) return@mapIndexedNotNull null
SEpisode.create().apply {
setUrlWithoutDomain(anime.url + "&ep=${index + 1}")
name = episode.title
episode_number = (index + 1).toFloat()
}
}
return episodes.sortedByDescending { it.episode_number }
}
override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val url = (baseUrl + episode.url).toHttpUrl()
val seasonNumber = url.queryParameter("s")?.toIntOrNull() ?: 1
val episodeNumber = url.queryParameter("ep")?.toIntOrNull() ?: 1
val episodeLang = url.queryParameter("lang") ?: "vo"
val stem = url.encodedPathSegments.last()
val animeData = database.first { titleToUrl(it.originalTitle) == stem }
val episodeData = animeData.seasons[seasonNumber - 1].episodes[episodeNumber - 1]
val videoBaseUrl = "$baseApiAnimeUrl/${animeData.id}/${seasonNumber - 1}/${episodeNumber - 1}"
val players = if (episodeLang == "vo") episodeData.languages.vo.players else episodeData.languages.vf.players
val videos = players.withIndex().parallelCatchingFlatMap { (index, playerName) ->
val apiUrl = "$videoBaseUrl/$episodeLang/$index"
val playerUrl = client.newCall(GET(apiUrl, headers)).await().body.string()
when (playerName) {
"vido" -> listOf(Video(playerUrl, "FRAnime (Vido)", playerUrl))
"sendvid" -> SendvidExtractor(client, headers).videosFromUrl(playerUrl)
"sibnet" -> SibnetExtractor(client).videosFromUrl(playerUrl)
"vk" -> VkExtractor(client, headers).videosFromUrl(playerUrl)
else -> emptyList()
}
}
return videos
}
// ============================= Utilities ==============================
private fun pagesToAnimesPage(pages: List<Anime>, page: Int): AnimesPage {
val chunks = pages.chunked(50)
val hasNextPage = chunks.size > page
val entries = pageToSAnimes(chunks.getOrNull(page - 1) ?: emptyList())
return AnimesPage(entries, hasNextPage)
}
private val titleRegex by lazy { Regex("[^A-Za-z0-9 ]") }
private fun titleToUrl(title: String) = titleRegex.replace(title, "").replace(" ", "-").lowercase()
private fun pageToSAnimes(page: List<Anime>): List<SAnime> {
return page.flatMap { anime ->
anime.seasons.flatMapIndexed { index, season ->
val seasonTitle = anime.title + if (anime.seasons.size > 1) " S${index + 1}" else ""
val hasVostfr = season.episodes.any { ep -> ep.languages.vo.players.isNotEmpty() }
val hasVf = season.episodes.any { ep -> ep.languages.vf.players.isNotEmpty() }
// I want to die for writing this
val languages = listOfNotNull(
if (hasVostfr) Triple("VOSTFR", "vo", hasVf) else null,
if (hasVf) Triple("VF", "vf", hasVostfr) else null,
)
languages.map { lang ->
SAnime.create().apply {
title = seasonTitle + if (lang.third) " (${lang.first})" else ""
thumbnail_url = anime.poster
genre = anime.genres.joinToString()
status = parseStatus(anime.status, anime.seasons.size, index + 1)
description = anime.description
setUrlWithoutDomain("/anime/${titleToUrl(anime.originalTitle)}?lang=${lang.second}&s=${index + 1}")
initialized = true
}
}
}
}
}
private fun parseStatus(statusString: String?, seasonCount: Int = 1, season: Int = 1): Int {
if (season < seasonCount) return SAnime.COMPLETED
return when (statusString?.trim()) {
"EN COURS" -> SAnime.ONGOING
"TERMINÉ" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
}

View file

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.animeextension.fr.franime
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class FrAnimeUrlActivity : Activity() {
private val tag = "FrAnimeUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size == 2) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", pathSegments[1])
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.animeextension.fr.franime.dto
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonUnquotedLiteral
import kotlinx.serialization.json.jsonPrimitive
import java.math.BigInteger
typealias BigIntegerJson =
@Serializable(with = BigIntegerSerializer::class)
BigInteger
@OptIn(ExperimentalSerializationApi::class)
private object BigIntegerSerializer : KSerializer<BigInteger> {
override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)
override fun deserialize(decoder: Decoder): BigInteger =
when (decoder) {
is JsonDecoder -> decoder.decodeJsonElement().jsonPrimitive.content.toBigInteger()
else -> decoder.decodeString().toBigInteger()
}
override fun serialize(encoder: Encoder, value: BigInteger) =
when (encoder) {
is JsonEncoder -> encoder.encodeJsonElement(JsonUnquotedLiteral(value.toString()))
else -> encoder.encodeString(value.toString())
}
}
@Serializable
data class Anime(
@SerialName("themes") val genres: List<String>,
@SerialName("saisons") val seasons: List<Season>,
@SerialName("_id") val uid: String?,
@SerialName("id") val id: BigIntegerJson,
@SerialName("source_url") val sourceUrl: String,
@SerialName("banner") val banner: String?,
@SerialName("affiche") val poster: String,
@SerialName("titleO") val originalTitle: String,
@SerialName("title") val title: String,
@SerialName("titles") val titlesAlt: TitlesAlt,
@SerialName("description") val description: String,
@SerialName("note") val note: Float,
@SerialName("format") val format: String,
@SerialName("startDate") val startDate: String?, // deserialize as date
@SerialName("endDate") val endDate: String?, // ditto
@SerialName("status") val status: String,
@SerialName("nsfw") val nsfw: Boolean,
@SerialName("__v") val uuv: Int?, // no idea wtf is this
@SerialName("affiche_small") val posterSmall: String?,
@SerialName("updatedDate") val updateTime: Long?, // deserialize as timestamp
)
@Serializable
data class Season(
@SerialName("title") val title: String,
@SerialName("episodes") val episodes: List<Episode>,
)
@Serializable
data class Episode(
@SerialName("title") val title: String,
@SerialName("lang") val languages: EpisodeLanguages,
)
@Serializable
data class EpisodeLanguages(
@SerialName("vf") val vf: EpisodeLanguage,
@SerialName("vo") val vo: EpisodeLanguage,
)
@Serializable
data class EpisodeLanguage(
@SerialName("lecteurs") val players: List<String>,
)
@Serializable
data class TitlesAlt(
@SerialName("en") val en: String?,
@SerialName("en_jp") val enJp: String?,
@SerialName("ja_jp") val jaJp: String?,
)

View file

@ -0,0 +1,22 @@
ext {
extName = 'French Anime'
extClass = '.FrenchAnime'
themePkg = 'datalifeengine'
baseUrl = 'https://french-anime.com'
overrideVersionCode = 6
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:vido-extractor'))
implementation(project(':lib:uqload-extractor'))
implementation(project(':lib:vudeo-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
implementation(project(':lib:upstream-extractor'))
implementation(project(':lib:streamvid-extractor'))
implementation(project(':lib:sibnet-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:streamhub-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -0,0 +1,116 @@
package eu.kanade.tachiyomi.animeextension.fr.frenchanime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.streamhubextractor.StreamHubExtractor
import eu.kanade.tachiyomi.lib.streamvidextractor.StreamVidExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vidoextractor.VidoExtractor
import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor
import eu.kanade.tachiyomi.multisrc.datalifeengine.DataLifeEngine
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class FrenchAnime : DataLifeEngine(
"French Anime",
"https://french-anime.com",
"fr",
) {
override val categories = arrayOf(
Pair("<Sélectionner>", ""),
Pair("Animes VF", "/animes-vf/"),
Pair("Animes VOSTFR", "/animes-vostfr/"),
Pair("Films VF et VOSTFR", "/films-vf-vostfr/"),
)
override val genres = arrayOf(
Pair("<Sélectionner>", ""),
Pair("Action", "/genre/action/"),
Pair("Aventure", "/genre/aventure/"),
Pair("Arts martiaux", "/genre/arts-martiaux/"),
Pair("Combat", "/genre/combat/"),
Pair("Comédie", "/genre/comedie/"),
Pair("Drame", "/genre/drame/"),
Pair("Epouvante", "/genre/epouvante/"),
Pair("Fantastique", "/genre/fantastique/"),
Pair("Fantasy", "/genre/fantasy/"),
Pair("Mystère", "/genre/mystere/"),
Pair("Romance", "/genre/romance/"),
Pair("Shonen", "/genre/shonen/"),
Pair("Surnaturel", "/genre/surnaturel/"),
Pair("Sci-Fi", "/genre/sci-fi/"),
Pair("School life", "/genre/school-life/"),
Pair("Ninja", "/genre/ninja/"),
Pair("Seinen", "/genre/seinen/"),
Pair("Horreur", "/genre/horreur/"),
Pair("Tranche de vie", "/genre/tranchedevie/"),
Pair("Psychologique", "/genre/psychologique/"),
)
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes-vostfr/page/$page/")
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val epsData = document.selectFirst("div.eps")?.text() ?: return emptyList()
epsData.split(" ").filter { it.isNotBlank() }.forEach {
val data = it.split("!", limit = 2)
val episode = SEpisode.create()
episode.episode_number = data[0].toFloatOrNull() ?: 0F
episode.name = "Episode ${data[0]}"
episode.url = data[1]
episodeList.add(episode)
}
return episodeList.reversed()
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val list = episode.url.split(",").filter { it.isNotBlank() }.parallelCatchingFlatMap {
with(it) {
when {
contains("dood") -> DoodExtractor(client).videosFromUrl(this)
contains("upstream") -> UpstreamExtractor(client).videosFromUrl(this)
contains("vudeo") -> VudeoExtractor(client).videosFromUrl(this)
contains("uqload") -> UqloadExtractor(client).videosFromUrl(this)
contains("guccihide") ||
contains("streamhide") -> StreamHideVidExtractor(client).videosFromUrl(this)
contains("streamvid") -> StreamVidExtractor(client).videosFromUrl(this)
contains("vido") -> VidoExtractor(client).videosFromUrl(this)
contains("sibnet") -> SibnetExtractor(client).videosFromUrl(this)
contains("ok.ru") -> OkruExtractor(client).videosFromUrl(this)
contains("streamhub.gg") -> StreamHubExtractor(client).videosFromUrl(this)
else -> emptyList()
}
}
}.sort()
return list
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
}

14
src/fr/hds/build.gradle Normal file
View file

@ -0,0 +1,14 @@
ext {
extName = 'HDS'
extClass = '.Hds'
themePkg = 'dooplay'
baseUrl = 'https://www.hds.quest'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

View file

@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.animeextension.fr.hds
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Response
import org.jsoup.nodes.Document
import uy.kohesive.injekt.injectLazy
class Hds : DooPlay(
"fr",
"HDS",
"https://www.hds.quest",
) {
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/tendance/page/$page/", headers)
override fun popularAnimeSelector() = latestUpdatesSelector()
override fun popularAnimeNextPageSelector() = "#nextpagination"
// =============================== Latest ===============================
override val supportsLatest = false
// =============================== Search ===============================
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun genresListSelector() = ".genres.scrolling li a"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = super.animeDetailsParse(document).apply {
if (document.select(".dt-breadcrumb li:nth-child(2)").text() == "Films") {
status = SAnime.COMPLETED
}
}
// ============================ Video Links =============================
@Serializable
data class VideoLinkDTO(@SerialName("embed_url") val url: String)
private val fileMoonExtractor by lazy { FilemoonExtractor(client) }
private val streamHideVidExtractor by lazy { StreamHideVidExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("#playeroptions li:not(#player-option-trailer)")
return players.parallelCatchingFlatMapBlocking { it ->
val post = it.attr("data-post")
val nume = it.attr("data-nume")
val type = it.attr("data-type")
val raw = client.newCall(GET("$baseUrl/wp-json/dooplayer/v1/post/$post?type=$type&source=$nume", headers))
.execute()
.body.string()
val securedUrl = json.decodeFromString<VideoLinkDTO>(raw).url
val playerUrl = client.newCall(GET(securedUrl, headers)).execute().use { it.request.url.toString() }
when {
playerUrl.contains("sentinel") -> fileMoonExtractor.videosFromUrl(playerUrl)
playerUrl.contains("hdsplay.online") -> streamHideVidExtractor.videosFromUrl(playerUrl)
else -> emptyList()
}
}
}
}

View file

@ -0,0 +1,14 @@
ext {
extName = 'JetAnime'
extClass = '.JetAnime'
themePkg = 'dooplay'
baseUrl = 'https://ssl.jetanimes.com'
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
implementation(project(':lib:playlist-utils'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -0,0 +1,171 @@
package eu.kanade.tachiyomi.animeextension.fr.jetanime
import eu.kanade.tachiyomi.animeextension.fr.jetanime.extractors.HdsplayExtractor
import eu.kanade.tachiyomi.animeextension.fr.jetanime.extractors.SentinelExtractor
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.Video
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Element
class JetAnime : DooPlay(
"fr",
"JetAnime",
"https://ssl.jetanimes.com",
) {
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
override fun popularAnimeSelector(): String = "aside#dtw_content_views-2 div.dtw_content > article"
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesFromElement(element: Element): SAnime {
return SAnime.create().apply {
val img = element.selectFirst("img")!!
val url = element.selectFirst("a")?.attr("href") ?: element.attr("href")
val slug = url.substringAfter("/episodes/")
setUrlWithoutDomain("/serie/${slug.substringBeforeLast("-episode").substringBeforeLast("-saison")}")
title = img.attr("alt")
thumbnail_url = img.getImageUrl()
}
}
override fun latestUpdatesNextPageSelector(): String = "div.pagination > span.current + a"
// =============================== Search ===============================
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val url = response.request.url.toString()
val animeList = when {
"/?s=" in url -> { // Search by name.
document.select(searchSelector())
.map(::searchAnimeFromElement)
}
"/annee/" in url -> { // Search by year
document.select(searchYearSelector())
.map(::popularAnimeFromElement)
}
else -> { // Search by some kind of filter, like genres or popularity.
document.select(searchAnimeSelector())
.map(::popularAnimeFromElement)
}
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animeList, hasNextPage)
}
private fun searchSelector() = "div.search-page > div.result-item div.image a"
private fun searchYearSelector() = "div.content > div.items > article div.poster"
override fun searchAnimeSelector() = "div#archive-content > article > div.poster"
// ============================== Filters ===============================
override val fetchGenres = false
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
AnimeFilter.Header("Only one filter at a time works"),
SubPageFilter(),
YearFilter(),
)
private class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("FILMS Animes", "/films"),
Pair("SERIES Animes", "/serie"),
),
)
private class YearFilter : UriPartFilter(
"Year",
arrayOf(
Pair("<select>", ""),
Pair("2024", "/annee/2024"),
Pair("2023", "/annee/2023"),
Pair("2022", "/annee/2022"),
Pair("2021", "/annee/2021"),
Pair("2020", "/annee/2020"),
Pair("2019", "/annee/2019"),
Pair("2018", "/annee/2018"),
Pair("2017", "/annee/2017"),
Pair("2016", "/annee/2016"),
Pair("2015", "/annee/2015"),
Pair("2014", "/annee/2014"),
Pair("2013", "/annee/2013"),
Pair("2012", "/annee/2012"),
Pair("2011", "/annee/2011"),
Pair("2010", "/annee/2010"),
Pair("2009", "/annee/2009"),
),
)
// ============================ Video Links =============================
private val noRedirects = client.newBuilder()
.followRedirects(false)
.followSslRedirects(false)
.build()
override fun videoListParse(response: Response): List<Video> {
val players = response.asJsoup().select("ul#playeroptionsul li")
val videoList = players.mapNotNull { player ->
runCatching {
val url = getPlayerUrl(player).ifEmpty { return@mapNotNull null }
val redirected = noRedirects.newCall(
GET(url),
).execute().headers["location"] ?: url
val name = player.text().trim()
getPlayerVideos(redirected, name)
}.getOrNull()
}.flatten()
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return emptyList()
}
private fun getPlayerUrl(player: Element): String {
val type = player.attr("data-type")
val id = player.attr("data-post")
val num = player.attr("data-nume")
if (num == "trailer") return ""
return client.newCall(GET("$baseUrl/wp-json/dooplayer/v1/post/$id?type=$type&source=$num"))
.execute()
.body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
}
private fun getPlayerVideos(url: String, name: String): List<Video> {
return when {
url.contains("https://sentinel") -> SentinelExtractor(client).videoFromUrl(url, name)
url.contains("https://hdsplay") -> HdsplayExtractor(client).videoFromUrl(url, name)
else -> emptyList()
}
}
// ============================== Settings ==============================
override val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p", "240p")
override val prefQualityEntries = prefQualityValues
}

View file

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.animeextension.fr.jetanime.extractors
import dev.datlag.jsunpacker.JsUnpacker
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.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
class HdsplayExtractor(private val client: OkHttpClient) {
private val playListUtils: PlaylistUtils by lazy {
PlaylistUtils(client)
}
fun videoFromUrl(url: String, name: String): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val script = document.selectFirst("script:containsData(m3u8),script:containsData(mp4)")
?.data()
?.let { t -> JsUnpacker.unpackAndCombine(t) ?: t }
?: return emptyList()
val videoUrl = Regex("""file: ?\"(.*?(?:m3u8|mp4).*?)\"""").find(script)!!.groupValues[1]
val subtitleList = Regex("""file: ?\"(.*?(?:vtt|ass|srt).*?)\".*?label: ?\"(.*?)\"""").find(script)?.let {
listOf(Track(it.groupValues[1], it.groupValues[2]))
} ?: emptyList()
if (videoUrl.toHttpUrlOrNull() == null) {
return emptyList()
}
return when {
videoUrl.contains(".m3u8") -> playListUtils.extractFromHls(videoUrl, url, videoNameGen = { quality -> "Hdsplay: $quality ($name)" }, subtitleList = subtitleList)
else -> {
listOf(
Video(videoUrl, "Sentinel: Video ($name)", videoUrl, subtitleTracks = subtitleList),
)
}
}
}
}

View file

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.animeextension.fr.jetanime.extractors
import dev.datlag.jsunpacker.JsUnpacker
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.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
class SentinelExtractor(private val client: OkHttpClient) {
private val playListUtils: PlaylistUtils by lazy {
PlaylistUtils(client)
}
fun videoFromUrl(url: String, name: String): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val script = document.selectFirst("script:containsData(m3u8),script:containsData(mp4)")
?.data()
?.let { t -> JsUnpacker.unpackAndCombine(t) ?: t }
?: return emptyList()
val videoUrl = Regex("""file: ?\"(.*?(?:m3u8|mp4).*?)\"""").find(script)!!.groupValues[1]
val subtitleList = Regex("""file: ?\"(.*?(?:vtt|ass|srt).*?)\".*?label: ?\"(.*?)\"""").find(script)?.let {
listOf(Track(it.groupValues[1], it.groupValues[2]))
} ?: emptyList()
if (videoUrl.toHttpUrlOrNull() == null) {
return emptyList()
}
return when {
videoUrl.contains(".m3u8") -> playListUtils.extractFromHls(videoUrl, url, videoNameGen = { quality -> "Sentinel: $quality ($name)" }, subtitleList = subtitleList)
else -> {
listOf(
Video(videoUrl, "Sentinel: Video ($name)", videoUrl, subtitleTracks = subtitleList),
)
}
}
}
}

View file

@ -0,0 +1,16 @@
ext {
extName = 'MyKdrama'
extClass = '.MyKdrama'
themePkg = 'animestream'
baseUrl = 'https://mykdrama.co'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:okru-extractor"))
implementation(project(":lib:uqload-extractor"))
implementation(project(":lib:dood-extractor"))
implementation(project(":lib:vudeo-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View file

@ -0,0 +1,117 @@
package eu.kanade.tachiyomi.animeextension.fr.mykdrama
import eu.kanade.tachiyomi.animeextension.fr.mykdrama.MyKdramaFilters.CountryFilter
import eu.kanade.tachiyomi.animeextension.fr.mykdrama.MyKdramaFilters.GenresFilter
import eu.kanade.tachiyomi.animeextension.fr.mykdrama.MyKdramaFilters.OrderFilter
import eu.kanade.tachiyomi.animeextension.fr.mykdrama.MyKdramaFilters.StatusFilter
import eu.kanade.tachiyomi.animeextension.fr.mykdrama.MyKdramaFilters.TypeFilter
import eu.kanade.tachiyomi.animeextension.fr.mykdrama.MyKdramaFilters.getSearchParameters
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import java.text.SimpleDateFormat
import java.util.Locale
class MyKdrama : AnimeStream(
"fr",
"MyKdrama",
"https://mykdrama.co",
) {
override val animeListUrl = "$baseUrl/drama"
override val dateFormatter by lazy {
SimpleDateFormat("MMMM dd, yyyy", Locale("fr"))
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = getSearchParameters(filters)
return if (query.isNotEmpty()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val queryParams = params.run { listOf(genres, countries) }
.filter(String::isNotBlank)
.joinToString("&")
val url = "$animeListUrl/?$queryParams".toHttpUrl().newBuilder()
.addQueryParameter("page", "$page")
.addIfNotBlank("status", params.status)
.addIfNotBlank("type", params.type)
.addIfNotBlank("order", params.order)
.build()
GET(url, headers)
}
}
// ============================== Filters ===============================
override val filtersSelector = "div.filter > ul"
override fun getFilterList(): AnimeFilterList {
return if (AnimeStreamFilters.filterInitialized()) {
AnimeFilterList(
GenresFilter("Genres"),
CountryFilter("Pays"),
AnimeFilter.Separator(),
StatusFilter("Status"),
TypeFilter("Type"),
OrderFilter("Ordre"),
)
} else {
AnimeFilterList(AnimeFilter.Header(filtersMissingWarning))
}
}
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p", "240p", "144p")
override val prefQualityEntries = prefQualityValues
override fun videoListParse(response: Response): List<Video> {
val doc = response.use { it.asJsoup() }
return doc.select(videoListSelector()).parallelCatchingFlatMapBlocking { element ->
val name = element.text()
val url = getHosterUrl(element)
getVideoList(url, name)
}.ifEmpty {
doc.select(".gov-the-embed").parallelCatchingFlatMapBlocking { element ->
val name = element.text()
val pageUrl = element.attr("onClick").substringAfter("'").substringBefore("'")
val url = client.newCall(GET(pageUrl)).execute().use { it.asJsoup().select("#pembed iframe").attr("src") }
getVideoList(url, name)
}
}
}
private val okruExtractor by lazy { OkruExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val vudeoExtractor by lazy { VudeoExtractor(client) }
override fun getVideoList(url: String, name: String): List<Video> {
return when {
"ok.ru" in url -> okruExtractor.videosFromUrl(url)
"uqload" in url -> uqloadExtractor.videosFromUrl(url)
"dood" in url || "doodstream" in url -> doodExtractor.videosFromUrl(url)
"vudeo" in url -> vudeoExtractor.videosFromUrl(url)
else -> emptyList()
}
}
// ============================= Utilities ==============================
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String) = apply {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
}
}

View file

@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.animeextension.fr.mykdrama
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.CheckBoxFilterList
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.asQueryPart
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.filterInitialized
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.getPairListByIndex
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamFilters.parseCheckbox
object MyKdramaFilters {
internal class GenresFilter(name: String) : CheckBoxFilterList(name, GENRES_LIST)
internal class CountryFilter(name: String) : CheckBoxFilterList(name, COUNTRY_LIST)
internal class StatusFilter(name: String) : AnimeStreamFilters.QueryPartFilter(name, STATUS_LIST)
internal class TypeFilter(name: String) : AnimeStreamFilters.QueryPartFilter(name, TYPE_LIST)
internal class OrderFilter(name: String) : AnimeStreamFilters.QueryPartFilter(name, ORDER_LIST)
internal data class FilterSearchParams(
val genres: String = "",
val countries: String = "",
val status: String = "",
val type: String = "",
val order: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty() || !filterInitialized()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(GENRES_LIST, "genre"),
filters.parseCheckbox<CountryFilter>(COUNTRY_LIST, "country"),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<OrderFilter>(),
)
}
private val GENRES_LIST by lazy { getPairListByIndex(0) }
private val COUNTRY_LIST by lazy { getPairListByIndex(3) }
private val STATUS_LIST by lazy { getPairListByIndex(5) }
private val TYPE_LIST by lazy { getPairListByIndex(6) }
private val ORDER_LIST by lazy { getPairListByIndex(7) }
}

View file

@ -0,0 +1,13 @@
ext {
extName = 'NekoSama'
extClass = '.NekoSama'
extVersionCode = 10
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:fusevideo-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show more