feat(fr/animesama): update AnimeSama with new filters, improved search functionality and add a default player preference in param (#1018)
Some checks failed
CI / Build individual modules (push) Blocked by required conditions
CI / Publish repo (push) Blocked by required conditions
CI / Prepare job (push) Has been cancelled

Checklist:

- [X] Updated `extVersionCode` value in `build.gradle` for individual extensions
- [X] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
- [X] Referenced all related issues in the PR body (e.g. "Closes #xyz")
- [X] Added the `isNsfw = true` flag in `build.gradle` when appropriate
- [X] Have not changed source names
- [X] Have explicitly kept the `id` if a source's name or language were changed
- [X] Have tested the modifications by compiling and running the extension through Android Studio
- [X] Have removed `web_hi_res_512.png` when adding a new extension
- [X] Have made sure all the icons are in png format

Reviewed-on: #1018
Co-authored-by: Mathis <mathis.quemener@gmail.com>
Co-committed-by: Mathis <mathis.quemener@gmail.com>
This commit is contained in:
Mathis 2025-06-20 01:04:42 -05:00 committed by AlmightyHak
parent 9581c675cf
commit f97d742c40
3 changed files with 166 additions and 84 deletions

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime-Sama'
extClass = '.AnimeSama'
extVersionCode = 12
extVersionCode = 13
}
apply from: "$rootDir/common.gradle"

View file

@ -4,6 +4,7 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -17,6 +18,7 @@ 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 eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -49,12 +51,11 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular ===============================
override fun popularAnimeParse(response: Response): AnimesPage {
val doc = response.body.string()
val doc = response.asJsoup()
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 chunks = doc.select("#containerPepites > div a").chunked(5)
val seasons = chunks.getOrNull(page - 1)?.flatMap {
val animeUrl = "$baseUrl/catalogue/${it.groupValues[1]}"
val animeUrl = "$baseUrl${it.attr("href")}"
fetchAnimeSeasons(animeUrl)
}?.toList().orEmpty()
return AnimesPage(seasons, page < chunks.size)
@ -72,7 +73,7 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
.removePathSegment(animeUrl.pathSize - 3)
.build()
fetchAnimeSeasons(url.toString())
}
}.distinctBy { it.url }
return AnimesPage(seasons, false)
}
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
@ -80,29 +81,26 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== 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)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = "$baseUrl/catalogue/".toHttpUrl().newBuilder()
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)
params.types.forEach { url.addQueryParameter("type[]", it) }
params.language.forEach { url.addQueryParameter("langue[]", it) }
params.genres.forEach { url.addQueryParameter("genre[]", it) }
url.addQueryParameter("search", query)
url.addQueryParameter("page", "$page")
return GET(url.build(), headers)
}
override fun searchAnimeParse(response: Response): AnimesPage = throw UnsupportedOperationException()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw UnsupportedOperationException()
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val anime = document.select("#list_catalog > div a").parallelFlatMapBlocking {
fetchAnimeSeasons(it.attr("href"))
}
val page = response.request.url.queryParameterValues("page").first()
val hasNextPage = document.select("#list_pagination a:last-child").text() != page
return AnimesPage(anime, hasNextPage)
}
// =========================== Anime Details ============================
override suspend fun getAnimeDetails(anime: SAnime): SAnime = anime
@ -121,6 +119,10 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException()
// ============================ Video Links =============================
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val vkExtractor by lazy { VkExtractor(client, headers) }
private val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val playerUrls = json.decodeFromString<List<List<String>>>(episode.url)
val videos = playerUrls.flatMapIndexed { i, it ->
@ -128,9 +130,9 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
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)
contains("sibnet.ru") -> sibnetExtractor.videosFromUrl(playerUrl, prefix)
contains("vk.") -> vkExtractor.videosFromUrl(playerUrl, prefix)
contains("sendvid.com") -> sendvidExtractor.videosFromUrl(playerUrl, prefix)
else -> emptyList()
}
}
@ -140,21 +142,16 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
}
// ============================ 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)!!
val player = preferences.getString(PREF_PLAYER_KEY, PREF_PLAYER_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(voices, true) },
{ it.quality.contains(quality) },
{ it.quality.contains(player, true) },
),
).reversed()
}
@ -164,14 +161,17 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
return fetchAnimeSeasons(res)
}
private val commentRegex by lazy { Regex("/\\*.*?\\*/", RegexOption.DOT_MATCHES_ALL) }
private val seasonRegex by lazy { Regex("^\\s*panneauAnime\\(\"(.*)\", \"(.*)\"\\)", RegexOption.MULTILINE) }
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 uncommented = commentRegex.replace(scripts, "")
val animes = seasonRegex.findAll(uncommented).flatMapIndexed { animeIndex, seasonMatch ->
val (seasonName, seasonStem) = seasonMatch.destructured
if (seasonStem.contains("film", true)) {
val moviesUrl = "$animeUrl/$seasonStem"
@ -219,23 +219,16 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
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 doc = client.newCall(GET(docUrl)).execute().use {
if (!it.isSuccessful) return emptyList()
it.body.string()
}
val sanitizedDoc = sanitizeEpisodesJs(doc)
for (i in 1..8) {
val numPlayers = getPlayers("eps$i", sanitizedDoc)
if (numPlayers != null) players.add(numPlayers)
val urls = QuickJs.create().use { qjs ->
qjs.evaluate(doc)
val res = qjs.evaluate("JSON.stringify(Array.from({length: 10}, (e,i) => this[`eps\${i}`]).filter(e => e))")
json.decodeFromString<List<List<String>>>(res as String)
}
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() }
return List(urls[0].size) { i -> urls.mapNotNull { it.getOrNull(i) }.distinct() }
}
private fun getPlayers(playerName: String, doc: String): List<String>? {
@ -276,6 +269,22 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_PLAYER_KEY
title = "Lecteur par défaut"
entries = PLAYERS
entryValues = PLAYERS_VALUES
setDefaultValue(PREF_PLAYER_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 {
@ -291,10 +300,25 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
"vf",
)
private val PLAYERS = arrayOf(
"Sendvid",
"Sibnet",
"VK",
)
private val PLAYERS_VALUES = arrayOf(
"sendvid",
"sibnet",
"vk",
)
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"
private const val PREF_PLAYER_KEY = "player_preference"
private const val PREF_PLAYER_DEFAULT = "sibnet"
}
}

View file

@ -9,10 +9,6 @@ object AnimeSamaFilters {
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()
}
@ -30,20 +26,6 @@ object AnimeSamaFilters {
}
}
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) },
@ -54,9 +36,9 @@ object AnimeSamaFilters {
AnimeSamaFiltersData.LANGUAGES.map { CheckBoxVal(it.first, false) },
)
class GenresFilter : TriStateFilterList(
class GenresFilter : CheckBoxFilterList(
"Genre",
AnimeSamaFiltersData.GENRES.map { TriFilter(it.first) },
AnimeSamaFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
)
val FILTER_LIST get() = AnimeFilterList(
@ -68,19 +50,15 @@ object AnimeSamaFilters {
data class SearchFilters(
val types: List<String> = emptyList(),
val language: List<String> = emptyList(),
val include: List<String> = emptyList(),
val exclude: List<String> = emptyList(),
val genres: 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,
filters.parseCheckbox<GenresFilter>(AnimeSamaFiltersData.GENRES),
)
}
@ -94,31 +72,111 @@ object AnimeSamaFilters {
val LANGUAGES = arrayOf(
Pair("VF", "VF"),
Pair("VOSTFR", "VOSTFR"),
Pair("VASTFR", "VASTFR"),
)
val GENRES = arrayOf(
Pair("Action", "Action"),
Pair("Adolescence", "Adolescence"),
Pair("Aliens / Extra-terrestres", "Aliens / Extra-terrestres"),
Pair("Amitié", "Amitié"),
Pair("Amour", "Amour"),
Pair("Apocalypse", "Apocalypse"),
Pair("Art", "Art"),
Pair("Arts martiaux", "Arts martiaux"),
Pair("Assassinat", "Assassinat"),
Pair("Autre monde", "Autre monde"),
Pair("Aventure", "Aventure"),
Pair("Combats", "Combats"),
Pair("Comédie", "Comédie"),
Pair("Crime", "Crime"),
Pair("Cyberpunk", "Cyberpunk"),
Pair("Danse", "Danse"),
Pair("Démons", "Démons"),
Pair("Détective", "Détective"),
Pair("Donghua", "Donghua"),
Pair("Drame", "Drame"),
Pair("Ecchi", "Ecchi"),
Pair("École", "School-Life"),
Pair("Fantaisie", "Fantasy"),
Pair("Ecole", "Ecole"),
Pair("Enquête", "Enquête"),
Pair("Famille", "Famille"),
Pair("Fantastique", "Fantastique"),
Pair("Fantasy", "Fantasy"),
Pair("Fantômes", "Fantômes"),
Pair("Futur", "Futur"),
Pair("Ghibli", "Ghibli"),
Pair("Guerre", "Guerre"),
Pair("Harcèlement", "Harcèlement"),
Pair("Harem", "Harem"),
Pair("Harem inversé", "Harem inversé"),
Pair("Histoire", "Histoire"),
Pair("Historique", "Historique"),
Pair("Horreur", "Horreur"),
Pair("Isekai", "Isekai"),
Pair("Jeunesse", "Jeunesse"),
Pair("Jeux", "Jeux"),
Pair("Jeux vidéo", "Jeux vidéo"),
Pair("Josei", "Josei"),
Pair("Journalisme", "Journalisme"),
Pair("Mafia", "Mafia"),
Pair("Magical girl", "Magical girl"),
Pair("Magie", "Magie"),
Pair("Maladie", "Maladie"),
Pair("Mariage", "Mariage"),
Pair("Mature", "Mature"),
Pair("Mechas", "Mechas"),
Pair("Médiéval", "Médiéval"),
Pair("Militaire", "Militaire"),
Pair("Monde virtuel", "Monde virtuel"),
Pair("Monstres", "Monstres"),
Pair("Musique", "Musique"),
Pair("Mystère", "Mystère"),
Pair("Nekketsu", "Nekketsu"),
Pair("Ninjas", "Ninjas"),
Pair("Nostalgie", "Nostalgie"),
Pair("Paranormal", "Paranormal"),
Pair("Philosophie", "Philosophie"),
Pair("Pirates", "Pirates"),
Pair("Police", "Police"),
Pair("Politique", "Politique"),
Pair("Post-apocalyptique", "Post-apocalyptique"),
Pair("Pouvoirs psychiques", "Pouvoirs psychiques"),
Pair("Préhistoire", "Préhistoire"),
Pair("Prison", "Prison"),
Pair("Psychologique", "Psychologique"),
Pair("Quotidien", "Slice-of-Life"),
Pair("Quotidien", "Quotidien"),
Pair("Religion", "Religion"),
Pair("Réincarnation / Transmigration", "Réincarnation / Transmigration"),
Pair("Romance", "Romance"),
Pair("Samouraïs", "Samouraïs"),
Pair("School Life", "School Life"),
Pair("Science-Fantasy", "Science-Fantasy"),
Pair("Science-fiction", "Science-fiction"),
Pair("Scientifique", "Scientifique"),
Pair("Seinen", "Seinen"),
Pair("Shônen", "Shônen"),
Pair("Shôjo", "Shôjo"),
Pair("Sports", "Sports"),
Pair("Shônen", "Shônen"),
Pair("Shônen-Ai", "Shônen-Ai"),
Pair("Slice of Life", "Slice of Life"),
Pair("Société", "Société"),
Pair("Sport", "Sport"),
Pair("Super pouvoirs", "Super pouvoirs"),
Pair("Super-héros", "Super-héros"),
Pair("Surnaturel", "Surnaturel"),
Pair("Survie", "Survie"),
Pair("Survival game", "Survival game"),
Pair("Technologies", "Technologies"),
Pair("Thriller", "Thriller"),
Pair("Tournois", "Tournois"),
Pair("Travail", "Travail"),
Pair("Vampires", "Vampires"),
Pair("Vengeance", "Vengeance"),
Pair("Voyage", "Voyage"),
Pair("Voyage temporel", "Voyage temporel"),
Pair("Webcomic", "Webcomic"),
Pair("Yakuza", "Yakuza"),
Pair("Yaoi", "Yaoi"),
Pair("Yokai", "Yokai"),
Pair("Yuri", "Yuri"),
)
}