Rename Consumyroll to Yomiroll to make it more generic, last rename (#1329)

Really fix token synchronized refresh
Merge same season episodes
Add Filters
Use a different token from Stormunblessed
This commit is contained in:
Samfun75
2023-02-25 13:11:01 +03:00
committed by GitHub
parent f8c906b077
commit b3f569e4c3
11 changed files with 471 additions and 85 deletions

View File

@ -3,10 +3,10 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Consumyroll'
extName = 'Yomiroll'
pkgNameSuffix = 'all.kamyroll'
extClass = '.Consumyroll'
extVersionCode = 15
extClass = '.Yomiroll'
extVersionCode = 16
libVersion = '13'
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -2,40 +2,47 @@ package eu.kanade.tachiyomi.animeextension.all.kamyroll
import android.content.SharedPreferences
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.net.Authenticator
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.PasswordAuthentication
import java.net.Proxy
class AccessTokenInterceptor(
private val baseUrl: String,
private val crUrl: String,
private val json: Json,
private val preferences: SharedPreferences
) : Interceptor {
private var accessToken = preferences.getString(TOKEN_PREF_KEY, null) ?: ""
override fun intercept(chain: Interceptor.Chain): Response {
if (accessToken.isBlank()) accessToken = refreshAccessToken()
val parsed = json.decodeFromString<AccessToken>(accessToken)
val request = newRequestWithAccessToken(chain.request(), "${parsed.token_type} ${parsed.access_token}")
val accessTokenN = getAccessToken()
val request = newRequestWithAccessToken(chain.request(), accessTokenN)
val response = chain.proceed(request)
when (response.code) {
HttpURLConnection.HTTP_UNAUTHORIZED -> {
synchronized(this) {
response.close()
// Access token is refreshed in another thread.
accessToken = refreshAccessToken()
val newParsed = json.decodeFromString<AccessToken>(accessToken)
// Access token is refreshed in another thread. Check if it has changed.
val newAccessToken = getAccessToken()
if (accessTokenN != newAccessToken) {
return chain.proceed(newRequestWithAccessToken(request, newAccessToken))
}
val refreshedToken = refreshAccessToken()
// Retry the request
return chain.proceed(
newRequestWithAccessToken(chain.request(), "${newParsed.token_type} ${newParsed.access_token}")
newRequestWithAccessToken(chain.request(), refreshedToken)
)
}
}
@ -43,24 +50,70 @@ class AccessTokenInterceptor(
}
}
private fun newRequestWithAccessToken(request: Request, accessToken: String): Request {
private fun newRequestWithAccessToken(request: Request, tokenData: AccessToken): Request {
return request.newBuilder()
.header("authorization", accessToken)
.header("authorization", "${tokenData.token_type} ${tokenData.access_token}")
.build()
}
fun refreshAccessToken(): String {
val client = OkHttpClient().newBuilder().build()
val response = client.newCall(GET("$baseUrl/token")).execute()
val parsedJson = json.decodeFromString<AccessToken>(response.body!!.string()).toJsonString()
preferences.edit().putString(TOKEN_PREF_KEY, parsedJson).apply()
return parsedJson
fun getAccessToken(): AccessToken {
return preferences.getString(TOKEN_PREF_KEY, null)?.toAccessToken()
?: refreshAccessToken()
}
private fun refreshAccessToken(): AccessToken {
val client = OkHttpClient()
.newBuilder().build()
val proxy = client.newBuilder()
.proxy(
Proxy(
Proxy.Type.SOCKS,
InetSocketAddress("cr-unblocker.us.to", 1080)
)
)
.build()
Authenticator.setDefault(
object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication("crunblocker", "crunblocker".toCharArray())
}
}
)
// Thanks Stormzy
val refreshTokenResp = client.newCall(GET("https://raw.githubusercontent.com/Stormunblessed/IPTV-CR-NIC/main/logos/refreshtoken.txt")).execute()
val refreshToken = refreshTokenResp.body!!.string().replace("[\n\r]".toRegex(), "")
val headers = Headers.headersOf(
"Content-Type", "application/x-www-form-urlencoded",
"Authorization", "Basic a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8="
)
val postBody = "grant_type=refresh_token&refresh_token=$refreshToken&scope=offline_access".toRequestBody("application/x-www-form-urlencoded".toMediaType())
val response = proxy.newCall(POST("$crUrl/auth/v1/token", headers, postBody)).execute()
val parsedJson = json.decodeFromString<AccessToken>(response.body!!.string())
val policy = proxy.newCall(newRequestWithAccessToken(GET("$crUrl/index/v2"), parsedJson)).execute()
val policyJson = json.decodeFromString<Policy>(policy.body!!.string())
val allTokens = AccessToken(
parsedJson.access_token,
parsedJson.token_type,
policyJson.cms.policy,
policyJson.cms.signature,
policyJson.cms.key_pair_id,
policyJson.cms.bucket
)
preferences.edit().putString(TOKEN_PREF_KEY, allTokens.toJsonString()).apply()
return allTokens
}
private fun AccessToken.toJsonString(): String {
return json.encodeToString(this)
}
private fun String.toAccessToken(): AccessToken {
return json.decodeFromString(this)
}
companion object {
val TOKEN_PREF_KEY = "access_token_data"
}

View File

@ -8,11 +8,24 @@ import kotlinx.serialization.json.JsonObject
data class AccessToken(
val access_token: String,
val token_type: String,
val policy: String? = null,
val signature: String? = null,
val key_pair_id: String? = null,
val bucket: String? = null
)
@Serializable
data class Policy(
val cms: Tokens
) {
@Serializable
data class Tokens(
val policy: String,
val signature: String,
val key_pair_id: String,
val bucket: String
)
}
@Serializable
data class LinkData(
@ -43,9 +56,9 @@ data class Anime(
@SerialName("keywords")
val genres: ArrayList<String>? = null,
val series_metadata: Metadata? = null,
val content_provider: String? = null,
val audio_locales: ArrayList<String>? = null,
val subtitle_locales: ArrayList<String>? = null
@SerialName("movie_listing_metadata")
val movie_metadata: MovieMeta? = null,
val content_provider: String? = null
) {
@Serializable
data class Metadata(
@ -53,6 +66,18 @@ data class Anime(
val is_simulcast: Boolean,
val audio_locales: ArrayList<String>,
val subtitle_locales: ArrayList<String>,
val is_dubbed: Boolean,
val is_subbed: Boolean,
@SerialName("tenant_categories")
val genres: ArrayList<String>? = null
)
@Serializable
data class MovieMeta(
val is_dubbed: Boolean,
val is_subbed: Boolean,
val maturity_ratings: ArrayList<String>,
val subtitle_locales: ArrayList<String>,
@SerialName("tenant_categories")
val genres: ArrayList<String>? = null
)
@ -71,6 +96,7 @@ data class SearchAnimeResult(
@Serializable
data class SearchAnime(
val type: String,
val count: Int,
val items: ArrayList<Anime>
)
}
@ -83,7 +109,9 @@ data class SeasonResult(
@Serializable
data class Season(
val id: String,
val season_number: Int
val season_number: Int? = null,
@SerialName("premium_available_date")
val date: String? = null
)
}
@ -113,6 +141,14 @@ data class EpisodeResult(
}
}
data class TempEpisode(
var epData: EpisodeData,
var name: String,
var episode_number: Float,
var date_upload: Long,
var scanlator: String?
)
@Serializable
data class EpisodeData(
val ids: List<Pair<String, String>>
@ -121,7 +157,8 @@ data class EpisodeData(
@Serializable
data class VideoStreams(
val streams: Stream,
val subtitles: JsonObject
val subtitles: JsonObject,
val audio_locale: String
) {
@Serializable
data class Stream(

View File

@ -34,11 +34,12 @@ import java.text.SimpleDateFormat
import java.util.Locale
@ExperimentalSerializationApi
class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Consumyroll"
// No more renaming, no matter what 3rd party service is used :)
override val name = "Yomiroll"
override val baseUrl = "https://cronchy.consumet.stream"
override val baseUrl = "https://crunchyroll.com"
private val crUrl = "https://beta-api.crunchyroll.com"
@ -54,10 +55,10 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val TokenInterceptor = AccessTokenInterceptor(baseUrl, json, preferences)
private val tokenInterceptor = AccessTokenInterceptor(crUrl, json, preferences)
override val client: OkHttpClient = OkHttpClient().newBuilder()
.addInterceptor(TokenInterceptor).build()
.addInterceptor(tokenInterceptor).build()
companion object {
private val DateFormatter by lazy {
@ -74,10 +75,16 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = json.decodeFromString<AnimeResult>(response.body!!.string())
val animeList = parsed.data.filter { it.type == "series" }.map { ani ->
val animeList = parsed.data.parallelMap { ani ->
runCatching {
ani.toSAnime()
}
return AnimesPage(animeList, true)
}.getOrNull()
}.filterNotNull()
val queries = response.request.url.encodedQuery ?: "0"
val position = if (queries.contains("start=")) {
queries.substringAfter("start=").substringBefore("&").toInt()
} else { 0 }
return AnimesPage(animeList, position + 36 < parsed.total)
}
// =============================== Latest ===============================
@ -92,25 +99,52 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = YomirollFilters.getSearchParameters(filters)
val start = if (page != 1) "start=${(page - 1) * 36}&" else ""
val url = if (query.isNotBlank()) {
val cleanQuery = query.replace(" ", "+").lowercase()
return GET("$crUrl/content/v2/discover/search?q=$cleanQuery&n=6&type=&locale=en-US")
"$crUrl/content/v2/discover/search?${start}n=36&q=$cleanQuery&type=${params.type}"
} else {
"$crUrl/content/v2/discover/browse?${start}n=36${params.media}${params.language}&sort_by=${params.sort}${params.category}"
}
return GET(url)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val parsed = json.decodeFromString<SearchAnimeResult>(response.body!!.string())
val animeList = parsed.data.filter { it.type == "top_results" }.map { result ->
result.items.filter { it.type == "series" }.map { ani ->
val bod = response.body!!.string()
val total: Int
val animeList = (
if (response.request.url.encodedPath.contains("search")) {
val parsed = json.decodeFromString<SearchAnimeResult>(bod).data.first()
total = parsed.count
parsed.items
} else {
val parsed = json.decodeFromString<AnimeResult>(bod)
total = parsed.total
parsed.data
}
).parallelMap { ani ->
runCatching {
ani.toSAnime()
}.getOrNull()
}.filterNotNull()
val queries = response.request.url.encodedQuery ?: "0"
val position = if (queries.contains("start=")) {
queries.substringAfter("start=").substringBefore("&").toInt()
} else { 0 }
return AnimesPage(animeList, position + 36 < total)
}
}.flatten()
return AnimesPage(animeList, false)
}
override fun getFilterList(): AnimeFilterList = YomirollFilters.filterList
// =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
val mediaId = json.decodeFromString<LinkData>(anime.url)
val resp = client.newCall(GET("$crUrl/content/v2/cms/series/${mediaId.id}?locale=en-US")).execute()
val resp = client.newCall(
if (mediaId.media_type == "series") GET("$crUrl/content/v2/cms/series/${mediaId.id}?locale=en-US")
else GET("$crUrl/content/v2/cms/movie_listings/${mediaId.id}?locale=en-US")
).execute()
val info = json.decodeFromString<AnimeResult>(resp.body!!.string())
return Observable.just(
anime.apply {
@ -129,36 +163,76 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
override fun episodeListRequest(anime: SAnime): Request {
val mediaId = json.decodeFromString<LinkData>(anime.url)
return GET("$crUrl/content/v2/cms/series/${mediaId.id}/seasons")
return if (mediaId.media_type == "series") {
GET("$crUrl/content/v2/cms/series/${mediaId.id}/seasons")
} else {
GET("$crUrl/content/v2/cms/movie_listings/${mediaId.id}/movies")
}
}
override fun episodeListParse(response: Response): List<SEpisode> {
val seasons = json.decodeFromString<SeasonResult>(response.body!!.string())
return seasons.data.parallelMap { seasonData ->
val series = response.request.url.encodedPath.contains("series/")
// Why all this? well crunchy sends same season twice with different quality eg. One Piece
// which causes the number of episodes to be higher that what it actually is.
return if (series) {
seasons.data.sortedBy { it.season_number }.groupBy { it.season_number }
.map { (_, sList) ->
sList.parallelMap { seasonData ->
runCatching {
val episodeResp = client.newCall(GET("$crUrl/content/v2/cms/seasons/${seasonData.id}/episodes")).execute()
val episodes = json.decodeFromString<EpisodeResult>(episodeResp.body!!.string())
episodes.data.sortedBy { it.episode_number }.map { ep ->
SEpisode.create().apply {
url = EpisodeData(
ep.versions?.map { Pair(it.mediaId, it.audio_locale) } ?: listOf(
val episodeResp =
client.newCall(GET("$crUrl/content/v2/cms/seasons/${seasonData.id}/episodes"))
.execute()
val episodes =
json.decodeFromString<EpisodeResult>(episodeResp.body!!.string())
episodes.data.sortedBy { it.episode_number }.parallelMap { ep ->
TempEpisode(
epData = EpisodeData(
ep.versions?.map { Pair(it.mediaId, it.audio_locale) }
?: listOf(
Pair(
ep.streams_link.substringAfter("videos/").substringBefore("/streams"),
ep.streams_link.substringAfter("videos/")
.substringBefore("/streams"),
ep.audio_locale
)
)
).toJsonString()
),
name = if (ep.episode_number > 0 && ep.episode.isNumeric()) {
"Season ${seasonData.season_number} Ep ${df.format(ep.episode_number)}: " + ep.title
} else { ep.title }
episode_number = ep.episode_number
date_upload = ep.airDate?.let { parseDate(it) } ?: 0L
} else {
ep.title
},
episode_number = ep.episode_number,
date_upload = ep.airDate?.let { parseDate(it) } ?: 0L,
scanlator = ep.versions?.sortedBy { it.audio_locale }
?.joinToString { it.audio_locale.substringBefore("-") } ?: ep.audio_locale.substringBefore("-")
}
?.joinToString { it.audio_locale.substringBefore("-") }
?: ep.audio_locale.substringBefore("-")
)
}
}.getOrNull()
}.filterNotNull().flatten().reversed()
}.asSequence().filterNotNull().flatten().groupBy { it.episode_number }
.map { (_, eList) ->
val versions = EpisodeData(eList.parallelMap { it.epData.ids }.flatten()).toJsonString()
val ep = eList.first()
SEpisode.create().apply {
this.url = versions
this.name = ep.name
this.episode_number = ep.episode_number
this.date_upload = ep.date_upload
this.scanlator = eList.map { it.scanlator }.joinToString()
}
}
}.flatten().reversed()
} else {
seasons.data.mapIndexed { index, movie ->
SEpisode.create().apply {
this.url = EpisodeData(listOf(Pair(movie.id, ""))).toJsonString()
this.name = "Movie"
this.episode_number = (index + 1).toFloat()
this.date_upload = movie.date?.let { parseDate(it) } ?: 0L
}
}
}
}
// ============================ Video Links =============================
@ -166,11 +240,9 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val urlJson = json.decodeFromString<EpisodeData>(episode.url)
val dubLocale = preferences.getString("preferred_audio", "en-US")!!
val tokenJson = preferences.getString(AccessTokenInterceptor.TOKEN_PREF_KEY, null)
?: TokenInterceptor.refreshAccessToken()
val policyJson = json.decodeFromString<AccessToken>(tokenJson)
val policyJson = tokenInterceptor.getAccessToken()
val videoList = urlJson.ids.filter {
it.second == dubLocale || it.second == "ja-JP" || it.second == "en-US"
it.second == dubLocale || it.second == "ja-JP" || it.second == "en-US" || it.second == ""
}.parallelMap { media ->
runCatching {
extractVideo(media, policyJson)
@ -183,7 +255,7 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ==============================
private fun extractVideo(media: Pair<String, String>, policyJson: AccessToken): List<Video> {
val (mediaId, audLang) = media
val (mediaId, aud) = media
val response = client.newCall(GET("$crUrl/cms/v2${policyJson.bucket}/videos/$mediaId/streams?Policy=${policyJson.policy}&Signature=${policyJson.signature}&Key-Pair-Id=${policyJson.key_pair_id}")).execute()
val streams = json.decodeFromString<VideoStreams>(response.body!!.string())
@ -201,6 +273,7 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
)
} catch (_: Error) {}
val audLang = aud.ifBlank { streams.audio_locale }
return streams.streams.adaptive_hls.entries.parallelMap { (_, value) ->
val stream = json.decodeFromString<HlsLinks>(value.jsonObject.toString())
runCatching {
@ -289,15 +362,39 @@ class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
title = this@toSAnime.title
thumbnail_url = this@toSAnime.images.poster_tall?.getOrNull(0)?.thirdLast()?.source
?: this@toSAnime.images.poster_tall?.getOrNull(0)?.last()?.source
url = this@toSAnime.type!!.let { LinkData(this@toSAnime.id, it).toJsonString() }
genre = this@toSAnime.series_metadata!!.genres?.joinToString() ?: "Anime"
url = LinkData(this@toSAnime.id, this@toSAnime.type!!).toJsonString()
genre = this@toSAnime.series_metadata?.genres?.joinToString()
?: this@toSAnime.movie_metadata?.genres?.joinToString() ?: ""
status = SAnime.COMPLETED
var desc = this@toSAnime.description + "\n"
desc += "\nLanguage: Sub" + (if (this@toSAnime.series_metadata.audio_locales.size > 1) " Dub" else "")
desc += "\nMaturity Ratings: ${this@toSAnime.series_metadata.maturity_ratings.joinToString()}"
desc += if (this@toSAnime.series_metadata.is_simulcast) "\nSimulcast" else ""
desc += "\n\nAudio: " + this@toSAnime.series_metadata.audio_locales.sortedBy { it.getLocale() }.joinToString { it.getLocale() }
desc += "\n\nSubs: " + this@toSAnime.series_metadata.subtitle_locales.sortedBy { it.getLocale() }.joinToString { it.getLocale() }
desc += "\nLanguage:" +
(
if (this@toSAnime.series_metadata?.subtitle_locales?.any() == true ||
this@toSAnime.movie_metadata?.subtitle_locales?.any() == true ||
this@toSAnime.series_metadata?.is_subbed == true
) " Sub" else ""
) +
(
if (this@toSAnime.series_metadata?.audio_locales?.any() == true ||
this@toSAnime.movie_metadata?.is_dubbed == true
) " Dub" else ""
)
desc += "\nMaturity Ratings: " +
(
this@toSAnime.series_metadata?.maturity_ratings?.joinToString()
?: this@toSAnime.movie_metadata?.maturity_ratings?.joinToString() ?: ""
)
desc += if (this@toSAnime.series_metadata?.is_simulcast == true) "\nSimulcast" else ""
desc += "\n\nAudio: " + (
this@toSAnime.series_metadata?.audio_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() } ?: ""
)
desc += "\n\nSubs: " + (
this@toSAnime.series_metadata?.subtitle_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() }
?: this@toSAnime.movie_metadata?.subtitle_locales?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() } ?: ""
)
description = desc
}

View File

@ -0,0 +1,199 @@
package eu.kanade.tachiyomi.animeextension.all.kamyroll
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object YomirollFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray()
) {
fun toQueryPart() = vals[state].second
}
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)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart()
}
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>>
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state)
options.find { it.first == checkbox.name }!!.second
else null
}.joinToString("")
}
class TypeFilter : QueryPartFilter("Type", CrunchyFiltersData.searchType)
class CategoryFilter : QueryPartFilter("Category", CrunchyFiltersData.categories)
class SortFilter : QueryPartFilter("Sort By", CrunchyFiltersData.sortType)
class MediaFilter : QueryPartFilter("Media", CrunchyFiltersData.mediaType)
class LanguageFilter : CheckBoxFilterList(
"Language",
CrunchyFiltersData.language.map { CheckBoxVal(it.first, false) }
)
val filterList = AnimeFilterList(
AnimeFilter.Header("Search Filter (ignored if browsing)"),
TypeFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("Browse Filters (ignored if searching)"),
CategoryFilter(),
SortFilter(),
MediaFilter(),
LanguageFilter()
)
data class FilterSearchParams(
val type: String = "",
val category: String = "",
val sort: String = "",
val language: String = "",
val media: String = ""
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<CategoryFilter>(),
filters.asQueryPart<SortFilter>(),
filters.parseCheckbox<LanguageFilter>(CrunchyFiltersData.language),
filters.asQueryPart<MediaFilter>(),
)
}
private object CrunchyFiltersData {
val searchType = arrayOf(
Pair("Top Results", "top_results"),
Pair("Series", "series"),
Pair("Movies", "movie_listing")
)
val categories = arrayOf(
Pair("-", ""),
Pair("Action", "&categories=action"),
Pair("Action, Adventure", "&categories=action,adventure"),
Pair("Action, Comedy", "&categories=action,comedy"),
Pair("Action, Drama", "&categories=action,drama"),
Pair("Action, Fantasy", "&categories=action,fantasy"),
Pair("Action, Historical", "&categories=action,historical"),
Pair("Action, Post-Apocalyptic", "&categories=action,post-apocalyptic"),
Pair("Action, Sci-Fi", "&categories=action,sci-fi"),
Pair("Action, Supernatural", "&categories=action,supernatural"),
Pair("Action, Thriller", "&categories=action,thriller"),
Pair("Adventure", "&categories=adventure"),
Pair("Adventure, Fantasy", "&categories=adventure,fantasy"),
Pair("Adventure, Isekai", "&categories=adventure,isekai"),
Pair("Adventure, Romance", "&categories=adventure,romance"),
Pair("Adventure, Sci-Fi", "&categories=adventure,sci-fi"),
Pair("Adventure, Supernatural", "&categories=adventure,supernatural"),
Pair("Comedy", "&categories=comedy"),
Pair("Comedy, Drama", "&categories=comedy,drama"),
Pair("Comedy, Fantasy", "&categories=comedy,fantasy"),
Pair("Comedy, Historical", "&categories=comedy,historical"),
Pair("Comedy, Music", "&categories=comedy,music"),
Pair("Comedy, Romance", "&categories=comedy,romance"),
Pair("Comedy, Sci-Fi", "&categories=comedy,sci-fi"),
Pair("Comedy, Slice of life", "&categories=comedy,slice+of+life"),
Pair("Comedy, Supernatural", "&categories=comedy,supernatural"),
Pair("Drama", "&categories=drama"),
Pair("Drama, Adventure", "&categories=drama,adventure"),
Pair("Drama, Fantasy", "&categories=drama,fantasy"),
Pair("Drama, Historical", "&categories=drama,historical"),
Pair("Drama, Mecha", "&categories=drama,mecha"),
Pair("Drama, Mystery", "&categories=drama,mystery"),
Pair("Drama, Romance", "&categories=drama,romance"),
Pair("Drama, Sci-Fi", "&categories=drama,sci-fi"),
Pair("Drama, Slice of life", "&categories=drama,slice+of+life"),
Pair("Fantasy", "&categories=fantasy"),
Pair("Fantasy, Historical", "&categories=fantasy,historical"),
Pair("Fantasy, Isekai", "&categories=fantasy,isekai"),
Pair("Fantasy, Mystery", "&categories=fantasy,mystery"),
Pair("Fantasy, Romance", "&categories=fantasy,romance"),
Pair("Fantasy, Supernatural", "&categories=fantasy,supernatural"),
Pair("Music", "&categories=music"),
Pair("Music, Drama", "&categories=music,drama"),
Pair("Music, Idols", "&categories=music,idols"),
Pair("Music, slice of life", "&categories=music,slice+of+life"),
Pair("Romance", "&categories=romance"),
Pair("Romance, Harem", "&categories=romance,harem"),
Pair("Romance, Historical", "&categories=romance,historical"),
Pair("Sci-Fi", "&categories=sci-fi"),
Pair("Sci-Fi, Fantasy", "&categories=sci-fi,Fantasy"),
Pair("Sci-Fi, Historical", "&categories=sci-fi,historical"),
Pair("Sci-Fi, Mecha", "&categories=sci-fi,mecha"),
Pair("Seinen", "&categories=seinen"),
Pair("Seinen, Action", "&categories=seinen,action"),
Pair("Seinen, Drama", "&categories=seinen,drama"),
Pair("Seinen, Fantasy", "&categories=seinen,fantasy"),
Pair("Seinen, Historical", "&categories=seinen,historical"),
Pair("Seinen, Supernatural", "&categories=seinen,supernatural"),
Pair("Shojo", "&categories=shojo"),
Pair("Shojo, Fantasy", "&categories=shojo,Fantasy"),
Pair("Shojo, Magical Girls", "&categories=shojo,magical-girls"),
Pair("Shojo, Romance", "&categories=shojo,romance"),
Pair("Shojo, Slice of life", "&categories=shojo,slice+of+life"),
Pair("Shonen", "&categories=shonen"),
Pair("Shonen, Action", "&categories=shonen,action"),
Pair("Shonen, Adventure", "&categories=shonen,adventure"),
Pair("Shonen, Comedy", "&categories=shonen,comedy"),
Pair("Shonen, Drama", "&categories=shonen,drama"),
Pair("Shonen, Fantasy", "&categories=shonen,fantasy"),
Pair("Shonen, Mystery", "&categories=shonen,mystery"),
Pair("Shonen, Post-Apocalyptic", "&categories=shonen,post-apocalyptic"),
Pair("Shonen, Supernatural", "&categories=shonen,supernatural"),
Pair("Slice of life", "&categories=slice+of+life"),
Pair("Slice of life, Fantasy", "&categories=slice+of+life,fantasy"),
Pair("Slice of life, Romance", "&categories=slice+of+life,romance"),
Pair("Slice of life, Sci-Fi", "&categories=slice+of+life,sci-fi"),
Pair("Sports", "&categories=sports"),
Pair("Sports, Action", "&categories=sports,action"),
Pair("Sports, Comedy", "&categories=sports,comedy"),
Pair("Sports, Drama", "&categories=sports,drama"),
Pair("Supernatural", "&categories=supernatural"),
Pair("Supernatural, Drama", "&categories=supernatural,drama"),
Pair("Supernatural, Historical", "&categories=supernatural,historical"),
Pair("Supernatural, Mystery", "&categories=supernatural,mystery"),
Pair("Supernatural, Slice of life", "&categories=supernatural,slice+of+life"),
Pair("Thriller", "&categories=thriller"),
Pair("Thriller, Drama", "&categories=thriller,drama"),
Pair("Thriller, Fantasy", "&categories=thriller,fantasy"),
Pair("Thriller, Sci-Fi", "&categories=thriller,sci-fi"),
Pair("Thriller, Supernatural", "&categories=thriller,supernatural")
)
val sortType = arrayOf(
Pair("Popular", "popularity"),
Pair("New", "newly_added"),
Pair("Alphabetical", "alphabetical")
)
val language = arrayOf(
Pair("Sub", "&is_subbed=true"),
Pair("Dub", "&is_dubbed=true")
)
val mediaType = arrayOf(
Pair("All", ""),
Pair("Series", "&type=series"),
Pair("Movies", "&type=movie_listing")
)
}
}