diff --git a/src/all/kamyroll/build.gradle b/src/all/kamyroll/build.gradle index 0893381a9..c40ba8f1b 100644 --- a/src/all/kamyroll/build.gradle +++ b/src/all/kamyroll/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Yomiroll' pkgNameSuffix = 'all.kamyroll' extClass = '.Yomiroll' - extVersionCode = 20 + extVersionCode = 21 libVersion = '13' } diff --git a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt b/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt index cd20c4947..22e736cce 100644 --- a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt +++ b/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.animeextension.all.kamyroll import android.content.SharedPreferences +import android.net.Uri import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import kotlinx.serialization.decodeFromString @@ -18,6 +19,7 @@ import java.net.HttpURLConnection import java.net.InetSocketAddress import java.net.PasswordAuthentication import java.net.Proxy +import java.text.MessageFormat import java.text.SimpleDateFormat import java.util.Locale @@ -25,6 +27,7 @@ class AccessTokenInterceptor( private val crUrl: String, private val json: Json, private val preferences: SharedPreferences, + private val PREF_USE_LOCAL_Token: String, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { @@ -41,7 +44,7 @@ class AccessTokenInterceptor( if (accessTokenN != newAccessToken) { return chain.proceed(newRequestWithAccessToken(request, newAccessToken)) } - val refreshedToken = refreshAccessToken(TOKEN_PREF_KEY) + val refreshedToken = getAccessToken(true) // Retry the request return chain.proceed( newRequestWithAccessToken(chain.request(), refreshedToken), @@ -53,58 +56,69 @@ class AccessTokenInterceptor( } private fun newRequestWithAccessToken(request: Request, tokenData: AccessToken): Request { - return request.newBuilder() - .header("authorization", "${tokenData.token_type} ${tokenData.access_token}") - .build() + return request.newBuilder().let { + it.header("authorization", "${tokenData.token_type} ${tokenData.access_token}") + 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 { - return preferences.getString(TOKEN_PREF_KEY, null)?.toAccessToken() - ?: refreshAccessToken(TOKEN_PREF_KEY) - } - - fun getLocalToken(force: Boolean = false): AccessToken? { - if (!preferences.getBoolean(PREF_FETCH_LOCAL_SUBS, false) && !force) return null - synchronized(this) { - val now = System.currentTimeMillis() + 1800000 // add 30 minutes for safety - val localToken = preferences.getString(LOCAL_TOKEN_PREF_KEY, null)?.toAccessToken() - return if (force || localToken == null || localToken.policyExpire!! < now) { - refreshAccessToken(LOCAL_TOKEN_PREF_KEY, false) - } else { - localToken + fun getAccessToken(force: Boolean = false): AccessToken { + val token = preferences.getString(TOKEN_PREF_KEY, null) + return if (!force && token != null) { + token.toAccessToken() + } else { + synchronized(this) { + if (!preferences.getBoolean(PREF_USE_LOCAL_Token, false)) { + refreshAccessToken() + } else { + refreshAccessToken(false) + } } } } - fun removeLocalToken() { - preferences.edit().putString(LOCAL_TOKEN_PREF_KEY, null).apply() + fun removeToken() { + preferences.edit().putString(TOKEN_PREF_KEY, null).apply() } - private fun refreshAccessToken(PREF_KEY: String, useProxy: Boolean = true): AccessToken { - val client = OkHttpClient().newBuilder().build() - Authenticator.setDefault( - object : Authenticator() { - override fun getPasswordAuthentication(): PasswordAuthentication { - return PasswordAuthentication("crunblocker", "crunblocker".toCharArray()) - } - }, - ) - val usedClient = if (useProxy) { - client.newBuilder() - .proxy( + private fun refreshAccessToken(useProxy: Boolean = true): AccessToken { + removeToken() + val client = OkHttpClient().newBuilder().let { + if (useProxy) { + Authenticator.setDefault( + object : Authenticator() { + override fun getPasswordAuthentication(): PasswordAuthentication { + return PasswordAuthentication("crunblocker", "crunblocker".toCharArray()) + } + }, + ) + it.proxy( Proxy( Proxy.Type.SOCKS, InetSocketAddress("cr-unblocker.us.to", 1080), ), ) - .build() - } else { - client + .build() + } else { + it.build() + } } - val response = usedClient.newCall(getRequest(client)).execute() + val response = client.newCall(getRequest()).execute() val parsedJson = json.decodeFromString(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.body.string()) val allTokens = AccessToken( parsedJson.access_token, @@ -115,19 +129,24 @@ class AccessTokenInterceptor( policyJson.cms.bucket, 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 } - private fun getRequest(client: OkHttpClient): Request { + private fun getRequest(): Request { + val client = OkHttpClient().newBuilder().build() val refreshTokenResp = client.newCall( GET("https://raw.githubusercontent.com/Samfun75/File-host/main/aniyomi/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 headers = Headers.Builder() + .add("Content-Type", "application/x-www-form-urlencoded") + .add( + "Authorization", + "Basic a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8=", + ) + .build() val postBody = "grant_type=refresh_token&refresh_token=$refreshToken&scope=offline_access".toRequestBody( "application/x-www-form-urlencoded".toMediaType(), ) @@ -144,8 +163,6 @@ class AccessTokenInterceptor( companion object { 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 { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) diff --git a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Yomiroll.kt b/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Yomiroll.kt index e883a4a20..b2a86ce78 100644 --- a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Yomiroll.kt +++ b/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Yomiroll.kt @@ -58,7 +58,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { Injekt.get().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() .addInterceptor(tokenInterceptor).build() @@ -73,8 +73,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { private const val PREF_SUB = "preferred_sub" private const val PREF_SUB_TYPE = "preferred_sub_type" - // there is one in AccessTokenInterceptor too for below - private const val PREF_FETCH_LOCAL_SUBS = "preferred_local_subs" + private const val PREF_USE_LOCAL_Token = "preferred_local_Token" } // ============================== Popular =============================== @@ -203,14 +202,15 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { val body = episodeResp.body.string() val episodes = json.decodeFromString(body) - episodes.data.sortedBy { it.episode_number }.parallelMap { ep -> + episodes.data.sortedBy { it.episode_number }.parallelMap EpisodeMap@{ ep -> SEpisode.create().apply { url = 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") + ?: return@EpisodeMap null, ep.audio_locale, ), ), @@ -226,7 +226,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { ?.joinToString { it.audio_locale.substringBefore("-") } ?: ep.audio_locale.substringBefore("-") } - } + }.filterNotNull() }.getOrNull() }.filterNotNull().flatten() }.flatten().reversed() @@ -247,13 +247,19 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { override fun fetchVideoList(episode: SEpisode): Observable> { val urlJson = json.decodeFromString(episode.url) 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 { - 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 -> runCatching { - extractVideo(media, proxyToken, localToken) + extractVideo(media) }.getOrNull() }.filterNotNull().flatten() @@ -262,23 +268,11 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { // ============================= Utilities ============================== - private fun extractVideo( - media: Pair, - proxyToken: AccessToken, - localToken: AccessToken?, - ): List