Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fr/animesama): Add search filters #2449

Merged
merged 1 commit into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/fr/animesama/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ext {
extName = 'Anime-Sama'
pkgNameSuffix = 'fr.animesama'
extClass = '.AnimeSama'
extVersionCode = 5
extVersionCode = 6
libVersion = 13
containsNsfw = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,20 @@ 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
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
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() {

Expand All @@ -51,6 +48,11 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
Injekt.get<Application>().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()
Expand Down Expand Up @@ -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<AnimesPage> {
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<SAnime> = Observable.just(anime)
Expand Down Expand Up @@ -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()
Expand All @@ -136,12 +143,11 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
}

// ============================ Utils =============================
inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
private inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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() }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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"),
)
}
}