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'
|
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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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"
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|