diff --git a/src/fr/animesama/build.gradle b/src/fr/animesama/build.gradle index f0a5b5a1e..290ac8eda 100644 --- a/src/fr/animesama/build.gradle +++ b/src/fr/animesama/build.gradle @@ -8,7 +8,7 @@ ext { extName = 'Anime-Sama' pkgNameSuffix = 'fr.animesama' extClass = '.AnimeSama' - extVersionCode = 5 + extVersionCode = 6 libVersion = 13 containsNsfw = false } diff --git a/src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSama.kt b/src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSama.kt index dfa463e7e..2d91b2cd0 100644 --- a/src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSama.kt +++ b/src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSama.kt @@ -15,7 +15,6 @@ 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.POST import eu.kanade.tachiyomi.util.asJsoup import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -23,7 +22,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -31,7 +29,6 @@ import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy -import java.text.Normalizer class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() { @@ -51,6 +48,11 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() { Injekt.get().getSharedPreferences("source_$id", 0x0000) } + private val database by lazy { + client.newCall(GET("$baseUrl/catalogue/listing_all.php", headers)).execute() + .use { it.asJsoup().select(".cardListAnime") } + } + // ============================== Popular =============================== override fun popularAnimeParse(response: Response): AnimesPage { val doc = response.body.string() @@ -78,25 +80,31 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() { override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl) // =============================== Search =============================== - override fun searchAnimeParse(response: Response): AnimesPage { - return if (response.request.method == "GET") { - AnimesPage(fetchAnimeSeasons(response), false) - } else { - val page = response.request.url.fragment?.toInt() ?: 1 - val elements = response.asJsoup().select(".cardListAnime").chunked(5) - val animes = elements[page - 1].flatMap { - fetchAnimeSeasons(it.getElementsByTag("a").attr("href")) - } - AnimesPage(animes, page < elements.size) + override fun getFilterList() = AnimeSamaFilters.FILTER_LIST + + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + if (query.startsWith(PREFIX_SEARCH)) { + return Observable.just(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 Observable.just(AnimesPage(emptyList(), false)) + val animes = elements[page - 1].flatMap { + fetchAnimeSeasons(it.getElementsByTag("a").attr("href")) + } + return Observable.just(AnimesPage(animes, page < elements.size)) } - override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = - if (query.startsWith(PREFIX_SEARCH)) { // Activity Intent Handler - GET("$baseUrl/catalogue/${query.removePrefix(PREFIX_SEARCH)}/") - } else { - POST("$baseUrl/catalogue/searchbar.php#$page", headers, FormBody.Builder().add("query", query).build()) - } + override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("not used") + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used") // =========================== Anime Details ============================ override fun fetchAnimeDetails(anime: SAnime): Observable = Observable.just(anime) @@ -124,7 +132,6 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() { when { contains("anime-sama.fr") -> listOf(Video(playerUrl, "${prefix}AS Player", playerUrl)) contains("sibnet.ru") -> SibnetExtractor(client).videosFromUrl(playerUrl, prefix) - // contains("myvi.") -> MytvExtractor(client).videosFromUrl(playerUrl, prefix) contains("vk.") -> VkExtractor(client, headers).videosFromUrl(playerUrl, prefix) contains("sendvid.com") -> SendvidExtractor(client, headers).videosFromUrl(playerUrl, prefix) else -> emptyList() @@ -136,12 +143,11 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() { } // ============================ Utils ============================= - inline fun Iterable.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable): List = + private inline fun Iterable.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable): List = runBlocking { map { async(Dispatchers.Default) { runCatching { f(it) }.getOrElse { emptyList() } } }.awaitAll().flatten() } - private fun removeDiacritics(string: String) = Normalizer.normalize(string, Normalizer.Form.NFD).replace(Regex("\\p{Mn}+"), "") private fun sanitizeEpisodesJs(doc: String) = doc .replace(Regex("[\"\t]"), "") // Fix trash format .replace("'", "\"") // Fix quotes @@ -172,7 +178,8 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() { val animeName = animeDoc.getElementById("titreOeuvre")?.text() ?: "" val seasonRegex = Regex("^\\s*panneauAnime\\(\"(.*)\", \"(.*)\"\\)", RegexOption.MULTILINE) - val animes = seasonRegex.findAll(animeDoc.toString()).flatMapIndexed { animeIndex, seasonMatch -> + 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" @@ -232,6 +239,7 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() { } 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() } } diff --git a/src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSamaFilters.kt b/src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSamaFilters.kt new file mode 100644 index 000000000..dae71a117 --- /dev/null +++ b/src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSamaFilters.kt @@ -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) : AnimeFilter.Group(name, values) + + private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state) + + open class TriStateFilterList(name: String, values: List) : AnimeFilter.Group(name, values) + + class TriFilter(name: String) : AnimeFilter.TriState(name) + + private inline fun AnimeFilterList.getFirst(): R { + return this.filterIsInstance().first() + } + + private inline fun AnimeFilterList.parseCheckbox( + options: Array>, + ): List { + return (this.getFirst() as CheckBoxFilterList).state + .mapNotNull { checkbox -> + if (checkbox.state) { + options.find { it.first == checkbox.name }!!.second + } else { + null + } + } + } + + private inline fun AnimeFilterList.parseTriFilter( + options: Array>, + ): List> { + return (this.getFirst() 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 = emptyList(), + val language: List = emptyList(), + val include: List = emptyList(), + val exclude: List = emptyList(), + ) + + fun getSearchFilters(filters: AnimeFilterList): SearchFilters { + if (filters.isEmpty()) return SearchFilters() + val (include, exclude) = filters.parseTriFilter(AnimeSamaFiltersData.GENRES) + + return SearchFilters( + filters.parseCheckbox(AnimeSamaFiltersData.TYPES), + filters.parseCheckbox(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"), + ) + } +}