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
@ -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'
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 76 KiB |
@ -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"
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|