Initial commit
22
src/fr/animesama/AndroidManifest.xml
Normal 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>
|
13
src/fr/animesama/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/fr/animesama/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/fr/animesama/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/fr/animesama/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/fr/animesama/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
src/fr/animesama/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/fr/animesama/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 65 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
7
src/fr/animevostfr/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeVostFr'
|
||||
extClass = '.AnimeVostFr'
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/fr/animevostfr/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/fr/animevostfr/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/fr/animevostfr/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/fr/animevostfr/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
src/fr/animevostfr/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/fr/animevostfr/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 78 KiB |
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
22
src/fr/anisama/AndroidManifest.xml
Normal 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>
|
16
src/fr/anisama/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/fr/anisama/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/fr/anisama/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/fr/anisama/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/fr/anisama/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/fr/anisama/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
|
@ -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", "", "^$"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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")),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
11
src/fr/empirestreaming/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/fr/empirestreaming/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.2 KiB |
BIN
src/fr/empirestreaming/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2 KiB |
After Width: | Height: | Size: 2.8 KiB |
BIN
src/fr/empirestreaming/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 6.4 KiB |
BIN
src/fr/empirestreaming/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 10 KiB |
BIN
src/fr/empirestreaming/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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(" ") }
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
22
src/fr/franime/AndroidManifest.xml
Normal 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>
|
14
src/fr/franime/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/fr/franime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/fr/franime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/fr/franime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/fr/franime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/fr/franime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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?,
|
||||
)
|
22
src/fr/frenchanime/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/fr/frenchanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/fr/frenchanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/fr/frenchanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
src/fr/frenchanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/fr/frenchanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/fr/frenchanime/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 77 KiB |
|
@ -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
|
@ -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'))
|
||||
}
|
BIN
src/fr/hds/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/fr/hds/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/fr/hds/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/fr/hds/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
src/fr/hds/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9 KiB |
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
src/fr/jetanime/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/fr/jetanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/fr/jetanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/fr/jetanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/fr/jetanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/fr/jetanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/fr/jetanime/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 70 KiB |
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
src/fr/mykdrama/build.gradle
Normal 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"))
|
||||
}
|
BIN
src/fr/mykdrama/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
src/fr/mykdrama/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/fr/mykdrama/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src/fr/mykdrama/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/fr/mykdrama/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/fr/mykdrama/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 123 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
13
src/fr/nekosama/build.gradle
Normal 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'))
|
||||
}
|
BIN
src/fr/nekosama/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/fr/nekosama/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/fr/nekosama/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/fr/nekosama/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |