Yomiroll: fix no episode error when free episodes are available (#1498)

This commit is contained in:
Samfun75
2023-04-15 15:14:58 +03:00
committed by GitHub
parent 0b994a3992
commit da607e4bb8
3 changed files with 99 additions and 106 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Yomiroll' extName = 'Yomiroll'
pkgNameSuffix = 'all.kamyroll' pkgNameSuffix = 'all.kamyroll'
extClass = '.Yomiroll' extClass = '.Yomiroll'
extVersionCode = 20 extVersionCode = 21
libVersion = '13' libVersion = '13'
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.animeextension.all.kamyroll package eu.kanade.tachiyomi.animeextension.all.kamyroll
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@ -18,6 +19,7 @@ import java.net.HttpURLConnection
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.PasswordAuthentication import java.net.PasswordAuthentication
import java.net.Proxy import java.net.Proxy
import java.text.MessageFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -25,6 +27,7 @@ class AccessTokenInterceptor(
private val crUrl: String, private val crUrl: String,
private val json: Json, private val json: Json,
private val preferences: SharedPreferences, private val preferences: SharedPreferences,
private val PREF_USE_LOCAL_Token: String,
) : Interceptor { ) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
@ -41,7 +44,7 @@ class AccessTokenInterceptor(
if (accessTokenN != newAccessToken) { if (accessTokenN != newAccessToken) {
return chain.proceed(newRequestWithAccessToken(request, newAccessToken)) return chain.proceed(newRequestWithAccessToken(request, newAccessToken))
} }
val refreshedToken = refreshAccessToken(TOKEN_PREF_KEY) val refreshedToken = getAccessToken(true)
// Retry the request // Retry the request
return chain.proceed( return chain.proceed(
newRequestWithAccessToken(chain.request(), refreshedToken), newRequestWithAccessToken(chain.request(), refreshedToken),
@ -53,58 +56,69 @@ class AccessTokenInterceptor(
} }
private fun newRequestWithAccessToken(request: Request, tokenData: AccessToken): Request { private fun newRequestWithAccessToken(request: Request, tokenData: AccessToken): Request {
return request.newBuilder() return request.newBuilder().let {
.header("authorization", "${tokenData.token_type} ${tokenData.access_token}") it.header("authorization", "${tokenData.token_type} ${tokenData.access_token}")
.build() val requestUrl = Uri.decode(request.url.toString())
if (requestUrl.contains("/cms/v2")) {
it.url(
MessageFormat.format(
requestUrl,
tokenData.bucket,
tokenData.policy,
tokenData.signature,
tokenData.key_pair_id,
),
)
}
it.build()
}
} }
fun getAccessToken(): AccessToken { fun getAccessToken(force: Boolean = false): AccessToken {
return preferences.getString(TOKEN_PREF_KEY, null)?.toAccessToken() val token = preferences.getString(TOKEN_PREF_KEY, null)
?: refreshAccessToken(TOKEN_PREF_KEY) return if (!force && token != null) {
} token.toAccessToken()
} else {
fun getLocalToken(force: Boolean = false): AccessToken? { synchronized(this) {
if (!preferences.getBoolean(PREF_FETCH_LOCAL_SUBS, false) && !force) return null if (!preferences.getBoolean(PREF_USE_LOCAL_Token, false)) {
synchronized(this) { refreshAccessToken()
val now = System.currentTimeMillis() + 1800000 // add 30 minutes for safety } else {
val localToken = preferences.getString(LOCAL_TOKEN_PREF_KEY, null)?.toAccessToken() refreshAccessToken(false)
return if (force || localToken == null || localToken.policyExpire!! < now) { }
refreshAccessToken(LOCAL_TOKEN_PREF_KEY, false)
} else {
localToken
} }
} }
} }
fun removeLocalToken() { fun removeToken() {
preferences.edit().putString(LOCAL_TOKEN_PREF_KEY, null).apply() preferences.edit().putString(TOKEN_PREF_KEY, null).apply()
} }
private fun refreshAccessToken(PREF_KEY: String, useProxy: Boolean = true): AccessToken { private fun refreshAccessToken(useProxy: Boolean = true): AccessToken {
val client = OkHttpClient().newBuilder().build() removeToken()
Authenticator.setDefault( val client = OkHttpClient().newBuilder().let {
object : Authenticator() { if (useProxy) {
override fun getPasswordAuthentication(): PasswordAuthentication { Authenticator.setDefault(
return PasswordAuthentication("crunblocker", "crunblocker".toCharArray()) object : Authenticator() {
} override fun getPasswordAuthentication(): PasswordAuthentication {
}, return PasswordAuthentication("crunblocker", "crunblocker".toCharArray())
) }
val usedClient = if (useProxy) { },
client.newBuilder() )
.proxy( it.proxy(
Proxy( Proxy(
Proxy.Type.SOCKS, Proxy.Type.SOCKS,
InetSocketAddress("cr-unblocker.us.to", 1080), InetSocketAddress("cr-unblocker.us.to", 1080),
), ),
) )
.build() .build()
} else { } else {
client it.build()
}
} }
val response = usedClient.newCall(getRequest(client)).execute() val response = client.newCall(getRequest()).execute()
val parsedJson = json.decodeFromString<AccessToken>(response.body.string()) val parsedJson = json.decodeFromString<AccessToken>(response.body.string())
val policy = usedClient.newCall(newRequestWithAccessToken(GET("$crUrl/index/v2"), parsedJson)).execute() val policy = client.newCall(newRequestWithAccessToken(GET("$crUrl/index/v2"), parsedJson)).execute()
val policyJson = json.decodeFromString<Policy>(policy.body.string()) val policyJson = json.decodeFromString<Policy>(policy.body.string())
val allTokens = AccessToken( val allTokens = AccessToken(
parsedJson.access_token, parsedJson.access_token,
@ -115,19 +129,24 @@ class AccessTokenInterceptor(
policyJson.cms.bucket, policyJson.cms.bucket,
DateFormatter.parse(policyJson.cms.expires)?.time, DateFormatter.parse(policyJson.cms.expires)?.time,
) )
preferences.edit().putString(PREF_KEY, allTokens.toJsonString()).apply()
preferences.edit().putString(TOKEN_PREF_KEY, allTokens.toJsonString()).apply()
return allTokens return allTokens
} }
private fun getRequest(client: OkHttpClient): Request { private fun getRequest(): Request {
val client = OkHttpClient().newBuilder().build()
val refreshTokenResp = client.newCall( val refreshTokenResp = client.newCall(
GET("https://raw.githubusercontent.com/Samfun75/File-host/main/aniyomi/refreshToken.txt"), GET("https://raw.githubusercontent.com/Samfun75/File-host/main/aniyomi/refreshToken.txt"),
).execute() ).execute()
val refreshToken = refreshTokenResp.body.string().replace("[\n\r]".toRegex(), "") val refreshToken = refreshTokenResp.body.string().replace("[\n\r]".toRegex(), "")
val headers = Headers.headersOf( val headers = Headers.Builder()
"Content-Type", "application/x-www-form-urlencoded", .add("Content-Type", "application/x-www-form-urlencoded")
"Authorization", "Basic a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8=" .add(
) "Authorization",
"Basic a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8=",
)
.build()
val postBody = "grant_type=refresh_token&refresh_token=$refreshToken&scope=offline_access".toRequestBody( val postBody = "grant_type=refresh_token&refresh_token=$refreshToken&scope=offline_access".toRequestBody(
"application/x-www-form-urlencoded".toMediaType(), "application/x-www-form-urlencoded".toMediaType(),
) )
@ -144,8 +163,6 @@ class AccessTokenInterceptor(
companion object { companion object {
private const val TOKEN_PREF_KEY = "access_token_data" private const val TOKEN_PREF_KEY = "access_token_data"
private const val LOCAL_TOKEN_PREF_KEY = "local_access_token_data"
private const val PREF_FETCH_LOCAL_SUBS = "preferred_local_subs"
private val DateFormatter by lazy { private val DateFormatter by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)

View File

@ -58,7 +58,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val tokenInterceptor = AccessTokenInterceptor(crUrl, json, preferences) private val tokenInterceptor = AccessTokenInterceptor(crUrl, json, preferences, PREF_USE_LOCAL_Token)
override val client: OkHttpClient = OkHttpClient().newBuilder() override val client: OkHttpClient = OkHttpClient().newBuilder()
.addInterceptor(tokenInterceptor).build() .addInterceptor(tokenInterceptor).build()
@ -73,8 +73,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
private const val PREF_SUB = "preferred_sub" private const val PREF_SUB = "preferred_sub"
private const val PREF_SUB_TYPE = "preferred_sub_type" private const val PREF_SUB_TYPE = "preferred_sub_type"
// there is one in AccessTokenInterceptor too for below private const val PREF_USE_LOCAL_Token = "preferred_local_Token"
private const val PREF_FETCH_LOCAL_SUBS = "preferred_local_subs"
} }
// ============================== Popular =============================== // ============================== Popular ===============================
@ -203,14 +202,15 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
val body = episodeResp.body.string() val body = episodeResp.body.string()
val episodes = val episodes =
json.decodeFromString<EpisodeResult>(body) json.decodeFromString<EpisodeResult>(body)
episodes.data.sortedBy { it.episode_number }.parallelMap { ep -> episodes.data.sortedBy { it.episode_number }.parallelMap EpisodeMap@{ ep ->
SEpisode.create().apply { SEpisode.create().apply {
url = EpisodeData( url = EpisodeData(
ep.versions?.map { Pair(it.mediaId, it.audio_locale) } ep.versions?.map { Pair(it.mediaId, it.audio_locale) }
?: listOf( ?: listOf(
Pair( Pair(
ep.streams_link!!.substringAfter("videos/") ep.streams_link?.substringAfter("videos/")
.substringBefore("/streams"), ?.substringBefore("/streams")
?: return@EpisodeMap null,
ep.audio_locale, ep.audio_locale,
), ),
), ),
@ -226,7 +226,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
?.joinToString { it.audio_locale.substringBefore("-") } ?.joinToString { it.audio_locale.substringBefore("-") }
?: ep.audio_locale.substringBefore("-") ?: ep.audio_locale.substringBefore("-")
} }
} }.filterNotNull()
}.getOrNull() }.getOrNull()
}.filterNotNull().flatten() }.filterNotNull().flatten()
}.flatten().reversed() }.flatten().reversed()
@ -247,13 +247,19 @@ class Yomiroll : 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 proxyToken = tokenInterceptor.getAccessToken()
val localToken = tokenInterceptor.getLocalToken() if (urlJson.ids.isEmpty()) throw Exception("No IDs found for episode")
val isUsingLocalToken = preferences.getBoolean(PREF_USE_LOCAL_Token, false)
val videoList = urlJson.ids.filter { val videoList = urlJson.ids.filter {
it.second == dubLocale || it.second == "ja-JP" || it.second == "en-US" || it.second == "" it.second == dubLocale ||
it.second == "ja-JP" ||
it.second == "en-US" ||
it.second == "" ||
if (isUsingLocalToken) it.second == urlJson.ids.first().second else false
}.parallelMap { media -> }.parallelMap { media ->
runCatching { runCatching {
extractVideo(media, proxyToken, localToken) extractVideo(media)
}.getOrNull() }.getOrNull()
}.filterNotNull().flatten() }.filterNotNull().flatten()
@ -262,23 +268,11 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun extractVideo( private fun extractVideo(media: Pair<String, String>): List<Video> {
media: Pair<String, String>,
proxyToken: AccessToken,
localToken: AccessToken?,
): List<Video> {
val (mediaId, aud) = media val (mediaId, aud) = media
val response = client.newCall(getVideoRequest(mediaId, proxyToken)).execute() val response = client.newCall(getVideoRequest(mediaId)).execute()
val streams = json.decodeFromString<VideoStreams>(response.body.string()) val streams = json.decodeFromString<VideoStreams>(response.body.string())
val localStreams =
if (aud == "ja-JP" && preferences.getBoolean(PREF_FETCH_LOCAL_SUBS, false)) {
val localResponse = client.newCall(getVideoRequest(mediaId, localToken!!)).execute()
json.decodeFromString(localResponse.body.string())
} else {
VideoStreams()
}
var subsList = emptyList<Track>() var subsList = emptyList<Track>()
val subLocale = preferences.getString("preferred_sub", "en-US")!!.getLocale() val subLocale = preferences.getString("preferred_sub", "en-US")!!.getLocale()
try { try {
@ -287,29 +281,17 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
val sub = json.decodeFromString<Subtitle>(value.jsonObject.toString()) val sub = json.decodeFromString<Subtitle>(value.jsonObject.toString())
tempSubs.add(Track(sub.url, sub.locale.getLocale())) tempSubs.add(Track(sub.url, sub.locale.getLocale()))
} }
localStreams.subtitles?.entries?.map { (_, value) ->
val sub = json.decodeFromString<Subtitle>(value.jsonObject.toString())
tempSubs.add(Track(sub.url, sub.locale.getLocale()))
}
subsList = tempSubs.sortedWith( subsList = tempSubs.sortedWith(
compareBy( compareBy(
{ it.lang }, { it.lang },
{ it.lang.contains(subLocale) }, { it.lang.contains(subLocale) },
), ),
) )
} catch (_: Error) { } catch (_: Error) {}
}
val audLang = aud.ifBlank { streams.audio_locale } ?: localStreams.audio_locale ?: "ja-JP" val audLang = aud.ifBlank { streams.audio_locale } ?: "ja-JP"
val videoList = mutableListOf<Video>() return getStreams(streams, audLang, subsList)
videoList.addAll(
getStreams(streams, audLang, subsList),
)
videoList.addAll(
getStreams(localStreams, audLang, subsList),
)
return videoList.distinctBy { it.quality }
} }
private fun getStreams( private fun getStreams(
@ -348,8 +330,8 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
}?.filterNotNull()?.flatten() ?: emptyList() }?.filterNotNull()?.flatten() ?: emptyList()
} }
private fun getVideoRequest(mediaId: String, token: AccessToken): Request { private fun getVideoRequest(mediaId: String): Request {
return GET("$crUrl/cms/v2${token.bucket}/videos/$mediaId/streams?Policy=${token.policy}&Signature=${token.signature}&Key-Pair-Id=${token.key_pair_id}") return GET("$crUrl/cms/v2{0}/videos/$mediaId/streams?Policy={1}&Signature={2}&Key-Pair-Id={3}")
} }
private val df = DecimalFormat("0.#") private val df = DecimalFormat("0.#")
@ -552,29 +534,23 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
object : LocalSubsPreference(screen.context) { object : LocalSubsPreference(screen.context) {
override fun reload() { override fun reload() {
this.apply { this.apply {
key = PREF_FETCH_LOCAL_SUBS key = PREF_USE_LOCAL_Token
title = "Fetch Local Subs (Don't Spam this please!)" title = "Use Local Token (Don't Spam this please!)"
runBlocking { runBlocking {
withContext(Dispatchers.IO) { summary = getTokenDetail() } withContext(Dispatchers.IO) { summary = getTokenDetail() }
} }
setDefaultValue(false) setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean val new = newValue as Boolean
Thread { preferences.edit().putBoolean(key, new).commit().also {
runBlocking { Thread {
if (new) { runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
summary = getTokenDetail(true) summary = getTokenDetail(true)
} }
} else {
tokenInterceptor.removeLocalToken()
summary = """Token location:
|Expires:
""".trimMargin()
} }
} }.start()
}.start() }
preferences.edit().putBoolean(key, new).commit()
} }
} }
} }
@ -589,14 +565,14 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() {
private fun getTokenDetail(force: Boolean = false): String { private fun getTokenDetail(force: Boolean = false): String {
return try { return try {
val storedToken = tokenInterceptor.getLocalToken(force) val storedToken = tokenInterceptor.getAccessToken(force)
"""Token location: ${ """Token location: ${
storedToken?.bucket?.substringAfter("/")?.substringBefore("/") ?: "" storedToken.bucket?.substringAfter("/")?.substringBefore("/") ?: ""
} }
|Expires: ${storedToken?.policyExpire?.let { DateFormatter.format(it) } ?: ""}
""".trimMargin() """.trimMargin()
} catch (e: Exception) { } catch (e: Exception) {
"" tokenInterceptor.removeToken()
"Error: ${e.localizedMessage ?: "Something Went Wrong"}"
} }
} }
} }