diff --git a/lib-multisrc/anilist/build.gradle.kts b/lib-multisrc/anilist/build.gradle.kts index 9dce2478..e2f11e9c 100644 --- a/lib-multisrc/anilist/build.gradle.kts +++ b/lib-multisrc/anilist/build.gradle.kts @@ -2,4 +2,4 @@ plugins { id("lib-multisrc") } -baseVersionCode = 2 +baseVersionCode = 3 diff --git a/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListAnimeHttpSource.kt b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListAnimeHttpSource.kt index a2c8780d..e9465a0b 100644 --- a/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListAnimeHttpSource.kt +++ b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListAnimeHttpSource.kt @@ -8,6 +8,10 @@ import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.util.parseAs import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray import okhttp3.FormBody import okhttp3.Request import okhttp3.Response @@ -32,7 +36,7 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() { query = ANIME_LIST_QUERY, variables = AnimeListVariables( page = page, - sort = AnimeListVariables.MediaSort.POPULARITY_DESC, + sort = AnimeListVariables.MediaSort.TRENDING_DESC, ), ) } @@ -58,20 +62,58 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() { /* ===================================== Search Anime ===================================== */ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { - return buildAnimeListRequest( - query = ANIME_LIST_QUERY, - variables = AnimeListVariables( - page = page, - sort = AnimeListVariables.MediaSort.SEARCH_MATCH, - search = query.ifBlank { null }, - ), - ) + val params = AniListFilters.getSearchParameters(filters) + + val variablesObject = buildJsonObject { + put("page", page) + put("perPage", 30) + put("isAdult", false) + put("type", "ANIME") + put("sort", params.sort) + if (query.isNotBlank()) put("search", query) + + if (params.genres.isNotEmpty()) { + putJsonArray("genres") { + params.genres.forEach { add(it) } + } + } + + if (params.format.isNotEmpty()) { + putJsonArray("format") { + params.format.forEach { add(it) } + } + } + + if (params.season.isBlank() && params.year.isNotBlank()) { + put("year", "${params.year}%") + } + + if (params.season.isNotBlank() && params.year.isBlank()) { + throw Exception("Year cannot be blank if season is set") + } + + if (params.season.isNotBlank() && params.year.isNotBlank()) { + put("season", params.season) + put("seasonYear", params.year) + } + + if (params.status.isNotBlank()) { + put("status", params.status) + } + } + val variables = json.encodeToString(variablesObject) + + return buildRequest(query = SORT_QUERY, variables = variables) } override fun searchAnimeParse(response: Response): AnimesPage { return parseAnimeListResponse(response) } + // ============================== Filters =============================== + + override fun getFilterList(): AnimeFilterList = AniListFilters.FILTER_LIST + /* ===================================== Anime Details ===================================== */ override fun animeDetailsRequest(anime: SAnime): Request { return buildRequest( @@ -145,6 +187,9 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() { status = when (media.status) { AniListMedia.Status.RELEASING -> SAnime.ONGOING AniListMedia.Status.FINISHED -> SAnime.COMPLETED + AniListMedia.Status.NOT_YET_RELEASED -> SAnime.LICENSED + AniListMedia.Status.CANCELLED -> SAnime.CANCELLED + AniListMedia.Status.HIATUS -> SAnime.ON_HIATUS } thumbnail_url = media.coverImage.large } diff --git a/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListFilters.kt b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListFilters.kt new file mode 100644 index 00000000..688d64e4 --- /dev/null +++ b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListFilters.kt @@ -0,0 +1,236 @@ +package eu.kanade.tachiyomi.multisrc.anilist + +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList + +object AniListFilters { + open class QueryPartFilter( + displayName: String, + val vals: Array>, + ) : AnimeFilter.Select( + displayName, + vals.map { it.first }.toTypedArray(), + ) { + fun toQueryPart() = vals[state].second + } + + open class CheckBoxFilterList(name: String, val pairs: Array>) : + AnimeFilter.Group(name, pairs.map { CheckBoxVal(it.first, false) }) + + private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state) + + private inline fun AnimeFilterList.asQueryPart(): String { + return (getFirst() as QueryPartFilter).toQueryPart() + } + + private inline fun AnimeFilterList.getFirst(): R { + return first { it is R } as R + } + + private inline fun AnimeFilterList.parseCheckboxList( + options: Array>, + ): List { + return (getFirst() as CheckBoxFilterList).state + .filter { it.state } + .map { checkBox -> options.find { it.first == checkBox.name }!!.second } + .filter(String::isNotBlank) + } + + private inline fun AnimeFilterList.getSort(): String { + val state = (getFirst() as AnimeFilter.Sort).state ?: return "" + val index = state.index + val suffix = if (state.ascending) "" else "_DESC" + return AniListFiltersData.SORT_LIST[index].second + suffix + } + + class GenreFilter : CheckBoxFilterList("Genres", AniListFiltersData.GENRE_LIST) + class YearFilter : QueryPartFilter("Year", AniListFiltersData.YEAR_LIST) + class SeasonFilter : QueryPartFilter("Season", AniListFiltersData.SEASON_LIST) + class FormatFilter : CheckBoxFilterList("Format", AniListFiltersData.FORMAT_LIST) + class StatusFilter : QueryPartFilter("Airing Status", AniListFiltersData.STATUS_LIST) + + class SortFilter : AnimeFilter.Sort( + "Sort", + AniListFiltersData.SORT_LIST.map { it.first }.toTypedArray(), + Selection(1, false), + ) + + val FILTER_LIST get() = AnimeFilterList( + GenreFilter(), + YearFilter(), + SeasonFilter(), + FormatFilter(), + StatusFilter(), + SortFilter(), + ) + + class FilterSearchParams( + val genres: List = emptyList(), + val year: String = "", + val season: String = "", + val format: List = emptyList(), + val status: String = "", + val sort: String = "", + ) + + internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { + if (filters.isEmpty()) return FilterSearchParams() + + return FilterSearchParams( + filters.parseCheckboxList(AniListFiltersData.GENRE_LIST), + filters.asQueryPart(), + filters.asQueryPart(), + filters.parseCheckboxList(AniListFiltersData.FORMAT_LIST), + filters.asQueryPart(), + filters.getSort(), + ) + } + + private object AniListFiltersData { + val GENRE_LIST = arrayOf( + Pair("Action", "Action"), + Pair("Adventure", "Adventure"), + Pair("Comedy", "Comedy"), + Pair("Drama", "Drama"), + Pair("Ecchi", "Ecchi"), + Pair("Fantasy", "Fantasy"), + Pair("Horror", "Horror"), + Pair("Mahou Shoujo", "Mahou Shoujo"), + Pair("Mecha", "Mecha"), + Pair("Music", "Music"), + Pair("Mystery", "Mystery"), + Pair("Psychological", "Psychological"), + Pair("Romance", "Romance"), + Pair("Sci-Fi", "Sci-Fi"), + Pair("Slice of Life", "Slice of Life"), + Pair("Sports", "Sports"), + Pair("Supernatural", "Supernatural"), + Pair("Thriller", "Thriller"), + ) + + val YEAR_LIST = arrayOf( + Pair("", ""), + Pair("Winter", "WINTER"), + Pair("Spring", "SPRING"), + Pair("Summer", "SUMMER"), + Pair("Fall", "FALL"), + ) + + val FORMAT_LIST = arrayOf( + Pair("TV Show", "TV"), + Pair("Movie", "MOVIE"), + Pair("TV Short", "TV_SHORT"), + Pair("Special", "SPECIAL"), + Pair("OVA", "OVA"), + Pair("ONA", "ONA"), + Pair("Music", "MUSIC"), + ) + + val STATUS_LIST = arrayOf( + Pair("