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' apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Consumyroll' extName = 'Yomiroll'
pkgNameSuffix = 'all.kamyroll' pkgNameSuffix = 'all.kamyroll'
extClass = '.Consumyroll' extClass = '.Yomiroll'
extVersionCode = 15 extVersionCode = 16
libVersion = '13' 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 android.content.SharedPreferences
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import java.net.Authenticator
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.PasswordAuthentication
import java.net.Proxy
class AccessTokenInterceptor( class AccessTokenInterceptor(
private val baseUrl: String, private val crUrl: String,
private val json: Json, private val json: Json,
private val preferences: SharedPreferences private val preferences: SharedPreferences
) : Interceptor { ) : Interceptor {
private var accessToken = preferences.getString(TOKEN_PREF_KEY, null) ?: ""
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
if (accessToken.isBlank()) accessToken = refreshAccessToken() val accessTokenN = getAccessToken()
val parsed = json.decodeFromString<AccessToken>(accessToken)
val request = newRequestWithAccessToken(chain.request(), "${parsed.token_type} ${parsed.access_token}")
val request = newRequestWithAccessToken(chain.request(), accessTokenN)
val response = chain.proceed(request) val response = chain.proceed(request)
when (response.code) { when (response.code) {
HttpURLConnection.HTTP_UNAUTHORIZED -> { HttpURLConnection.HTTP_UNAUTHORIZED -> {
synchronized(this) { synchronized(this) {
response.close() response.close()
// Access token is refreshed in another thread. // Access token is refreshed in another thread. Check if it has changed.
accessToken = refreshAccessToken() val newAccessToken = getAccessToken()
val newParsed = json.decodeFromString<AccessToken>(accessToken) if (accessTokenN != newAccessToken) {
return chain.proceed(newRequestWithAccessToken(request, newAccessToken))
}
val refreshedToken = refreshAccessToken()
// Retry the request // Retry the request
return chain.proceed( 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() return request.newBuilder()
.header("authorization", accessToken) .header("authorization", "${tokenData.token_type} ${tokenData.access_token}")
.build() .build()
} }
fun refreshAccessToken(): String { fun getAccessToken(): AccessToken {
val client = OkHttpClient().newBuilder().build() return preferences.getString(TOKEN_PREF_KEY, null)?.toAccessToken()
val response = client.newCall(GET("$baseUrl/token")).execute() ?: refreshAccessToken()
val parsedJson = json.decodeFromString<AccessToken>(response.body!!.string()).toJsonString() }
preferences.edit().putString(TOKEN_PREF_KEY, parsedJson).apply()
return parsedJson 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 { private fun AccessToken.toJsonString(): String {
return json.encodeToString(this) return json.encodeToString(this)
} }
private fun String.toAccessToken(): AccessToken {
return json.decodeFromString(this)
}
companion object { companion object {
val TOKEN_PREF_KEY = "access_token_data" val TOKEN_PREF_KEY = "access_token_data"
} }

View File

@ -8,12 +8,25 @@ import kotlinx.serialization.json.JsonObject
data class AccessToken( data class AccessToken(
val access_token: String, val access_token: String,
val token_type: String, val token_type: String,
val policy: String, val policy: String? = null,
val signature: String, val signature: String? = null,
val key_pair_id: String, val key_pair_id: String? = null,
val bucket: String 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 @Serializable
data class LinkData( data class LinkData(
val id: String, val id: String,
@ -43,9 +56,9 @@ data class Anime(
@SerialName("keywords") @SerialName("keywords")
val genres: ArrayList<String>? = null, val genres: ArrayList<String>? = null,
val series_metadata: Metadata? = null, val series_metadata: Metadata? = null,
val content_provider: String? = null, @SerialName("movie_listing_metadata")
val audio_locales: ArrayList<String>? = null, val movie_metadata: MovieMeta? = null,
val subtitle_locales: ArrayList<String>? = null val content_provider: String? = null
) { ) {
@Serializable @Serializable
data class Metadata( data class Metadata(
@ -53,6 +66,18 @@ data class Anime(
val is_simulcast: Boolean, val is_simulcast: Boolean,
val audio_locales: ArrayList<String>, val audio_locales: ArrayList<String>,
val subtitle_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") @SerialName("tenant_categories")
val genres: ArrayList<String>? = null val genres: ArrayList<String>? = null
) )
@ -71,6 +96,7 @@ data class SearchAnimeResult(
@Serializable @Serializable
data class SearchAnime( data class SearchAnime(
val type: String, val type: String,
val count: Int,
val items: ArrayList<Anime> val items: ArrayList<Anime>
) )
} }
@ -83,7 +109,9 @@ data class SeasonResult(
@Serializable @Serializable
data class Season( data class Season(
val id: String, 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 @Serializable
data class EpisodeData( data class EpisodeData(
val ids: List<Pair<String, String>> val ids: List<Pair<String, String>>
@ -121,7 +157,8 @@ data class EpisodeData(
@Serializable @Serializable
data class VideoStreams( data class VideoStreams(
val streams: Stream, val streams: Stream,
val subtitles: JsonObject val subtitles: JsonObject,
val audio_locale: String
) { ) {
@Serializable @Serializable
data class Stream( data class Stream(

View File

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