diff --git a/src/all/kamyroll/build.gradle b/src/all/kamyroll/build.gradle index 4649e993a..9be55e458 100644 --- a/src/all/kamyroll/build.gradle +++ b/src/all/kamyroll/build.gradle @@ -3,10 +3,10 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlinx-serialization' ext { - extName = 'Kamyroll' + extName = 'Consumyroll' pkgNameSuffix = 'all.kamyroll' - extClass = '.Kamyroll' - extVersionCode = 8 + extClass = '.Consumyroll' + extVersionCode = 9 libVersion = '13' } diff --git a/src/all/kamyroll/res/mipmap-hdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-hdpi/ic_launcher.png index 4d537d555..f91b37ce2 100644 Binary files a/src/all/kamyroll/res/mipmap-hdpi/ic_launcher.png and b/src/all/kamyroll/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/kamyroll/res/mipmap-hdpi/ic_launcher_adaptive_fore.png b/src/all/kamyroll/res/mipmap-hdpi/ic_launcher_adaptive_fore.png index 53b306e9f..5f7656b02 100644 Binary files a/src/all/kamyroll/res/mipmap-hdpi/ic_launcher_adaptive_fore.png and b/src/all/kamyroll/res/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/all/kamyroll/res/mipmap-mdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-mdpi/ic_launcher.png index 0db821198..d884e2904 100644 Binary files a/src/all/kamyroll/res/mipmap-mdpi/ic_launcher.png and b/src/all/kamyroll/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/kamyroll/res/mipmap-mdpi/ic_launcher_adaptive_fore.png b/src/all/kamyroll/res/mipmap-mdpi/ic_launcher_adaptive_fore.png index 014f44231..87400f509 100644 Binary files a/src/all/kamyroll/res/mipmap-mdpi/ic_launcher_adaptive_fore.png and b/src/all/kamyroll/res/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher.png index 0f3ea4d94..139633fee 100644 Binary files a/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher.png and b/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png index 95bba295d..570c40ac1 100644 Binary files a/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png and b/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher.png index be304e340..12ef47a82 100644 Binary files a/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher.png and b/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png index 9c4293100..bc2aa166f 100644 Binary files a/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and b/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher.png index 4f680e21c..650460a7f 100644 Binary files a/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher.png and b/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png index e02ba1db7..63c6a94c1 100644 Binary files a/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and b/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/src/all/kamyroll/res/play_store_512.png b/src/all/kamyroll/res/play_store_512.png new file mode 100644 index 000000000..2fe0a36ab Binary files /dev/null and b/src/all/kamyroll/res/play_store_512.png differ diff --git a/src/all/kamyroll/res/web_hi_res_512.png b/src/all/kamyroll/res/web_hi_res_512.png deleted file mode 100644 index 83e560e63..000000000 Binary files a/src/all/kamyroll/res/web_hi_res_512.png and /dev/null differ 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 63b3ded39..f4c090e01 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,51 +1,57 @@ 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.json.Json -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +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.HttpURLConnection -class AccessTokenInterceptor(val baseUrl: String, val json: Json, val preferences: SharedPreferences) : Interceptor { - private val deviceId = randomId() +class AccessTokenInterceptor( + private val json: Json, + private val preferences: SharedPreferences +) : Interceptor { private var accessToken = preferences.getString("access_token", null) ?: "" override fun intercept(chain: Interceptor.Chain): Response { if (accessToken.isBlank()) accessToken = refreshAccessToken() - val request = if (chain.request().url.toString().contains("kamyroll")) { - chain.request().newBuilder() - .header("authorization", accessToken) - .build() - } else { - chain.request() - } + val request = chain.request().newBuilder() + .header("authorization", accessToken) + .build() + val response = chain.proceed(request) - if (response.code == HttpURLConnection.HTTP_UNAUTHORIZED) { - synchronized(this) { - response.close() - val newAccessToken = refreshAccessToken() - // Access token is refreshed in another thread. - if (accessToken != newAccessToken) { - accessToken = newAccessToken - return chain.proceed(newRequestWithAccessToken(chain.request(), newAccessToken)) + when (response.code) { + HttpURLConnection.HTTP_UNAUTHORIZED -> { + synchronized(this) { + response.close() + val newAccessToken = refreshAccessToken() + // Access token is refreshed in another thread. + if (accessToken != newAccessToken) { + accessToken = newAccessToken + return chain.proceed( + newRequestWithAccessToken(chain.request(), newAccessToken) + ) + } + + // Need to refresh an access token + val updatedAccessToken = refreshAccessToken() + accessToken = updatedAccessToken + // Retry the request + return chain.proceed( + newRequestWithAccessToken(chain.request(), updatedAccessToken) + ) } - - // Need to refresh an access token - val updatedAccessToken = refreshAccessToken() - accessToken = updatedAccessToken - // Retry the request - return chain.proceed(newRequestWithAccessToken(chain.request(), updatedAccessToken)) } + else -> return response } - - return response } private fun newRequestWithAccessToken(request: Request, accessToken: String): Request { @@ -56,22 +62,17 @@ class AccessTokenInterceptor(val baseUrl: String, val json: Json, val preference private fun refreshAccessToken(): String { val client = OkHttpClient().newBuilder().build() - val url = "$baseUrl/auth/v1/token".toHttpUrlOrNull()!!.newBuilder() - .addQueryParameter("device_id", deviceId) - .addQueryParameter("device_type", "aniyomi") - .addQueryParameter("access_token", "HMbQeThWmZq4t7w") - .build() - val response = client.newCall(GET(url.toString())).execute() - val parsedJson = json.decodeFromString(response.body!!.string()) + val headers = Headers.headersOf( + "Content-Type", "application/x-www-form-urlencoded", + "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0", + "Authorization", "Basic a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8=" + ) + val postBody = "grant_type=client_id".toRequestBody("application/x-www-form-urlencoded".toMediaType()) + val response = client.newCall(POST("https://beta-api.crunchyroll.com/auth/v1/token", headers, postBody)).execute() + val respBody = response.body!!.string() + val parsedJson = json.decodeFromString(respBody) val token = "${parsedJson.token_type} ${parsedJson.access_token}" preferences.edit().putString("access_token", token).apply() return token } - - // Random 15 length string - private fun randomId(): String { - return (0..14).joinToString("") { - (('0'..'9') + ('a'..'f')).random().toString() - } - } } diff --git a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Kamyroll.kt b/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Consumyroll.kt similarity index 55% rename from src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Kamyroll.kt rename to src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Consumyroll.kt index e53b57c29..3b9f7d88c 100644 --- a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Kamyroll.kt +++ b/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Consumyroll.kt @@ -33,74 +33,71 @@ import java.text.SimpleDateFormat import java.util.Locale @ExperimentalSerializationApi -class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() { +class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() { - override val name = "Kamyroll" + override val name = "Consumyroll" - override val baseUrl by lazy { preferences.getString("preferred_domain", "https://api.kamyroll.tech")!! } + override val baseUrl = "https://cronchy.consumet.stream" + + private val crUrl = "https://beta-api.crunchyroll.com" override val lang = "all" - override val supportsLatest = false + override val supportsLatest = true + + override val id: Long = 7463514907068706782 private val json: Json by injectLazy() - private val channelId = "crunchyroll" - private val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } override val client: OkHttpClient = OkHttpClient().newBuilder() - .addInterceptor(AccessTokenInterceptor(baseUrl, json, preferences)).build() + .addInterceptor(AccessTokenInterceptor(json, preferences)).build() companion object { private val DateFormatter by lazy { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) } } // ============================== Popular =============================== - override fun popularAnimeRequest(page: Int): Request = - GET("$baseUrl/content/v1/updated?channel_id=$channelId&limit=20") + override fun popularAnimeRequest(page: Int): Request { + val start = if (page != 1) "start=${(page - 1) * 36}&" else "" + return GET("$crUrl/content/v2/discover/browse?${start}n=36&sort_by=popularity&locale=en-US") + } override fun popularAnimeParse(response: Response): AnimesPage { - val parsed = json.decodeFromString(response.body!!.string()) - val animeList = parsed.items.map { ani -> - SAnime.create().apply { - title = ani.series_title - thumbnail_url = ani.images.poster_tall!!.thirdLast().source - url = LinkData(ani.series_id, "series").toJsonString() - description = ani.description - } + val parsed = json.decodeFromString(response.body!!.string()) + val animeList = parsed.data.filter { it.type == "series" }.map { ani -> + ani.toSAnime() } - return AnimesPage(animeList, false) + return AnimesPage(animeList, true) } // =============================== Latest =============================== - override fun latestUpdatesRequest(page: Int): Request = throw Exception("not used") + override fun latestUpdatesRequest(page: Int): Request { + val start = if (page != 1) "start=${(page - 1) * 36}&" else "" + return GET("$crUrl/content/v2/discover/browse?${start}n=36&sort_by=newly_added&locale=en-US") + } - override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("not used") + override fun latestUpdatesParse(response: Response): AnimesPage = popularAnimeParse(response) // =============================== Search =============================== override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { val cleanQuery = query.replace(" ", "+").lowercase() - return GET("$baseUrl/content/v1/search?query=$cleanQuery&channel_id=$channelId") + return GET("$crUrl/content/v2/discover/search?q=$cleanQuery&n=6&type=&locale=en-US") } override fun searchAnimeParse(response: Response): AnimesPage { - val parsed = json.decodeFromString(response.body!!.string()) - val animeList = parsed.items.map { media -> - media.items.map { ani -> - SAnime.create().apply { - title = ani.title - thumbnail_url = ani.images.poster_tall!!.thirdLast().source - url = LinkData(ani.id, ani.media_type).toJsonString() - description = ani.description - } + val parsed = json.decodeFromString(response.body!!.string()) + val animeList = parsed.data.filter { it.type == "top_results" }.map { result -> + result.items.filter { it.type == "series" }.map { ani -> + ani.toSAnime() } }.flatten() return AnimesPage(animeList, false) @@ -110,85 +107,49 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() { override fun fetchAnimeDetails(anime: SAnime): Observable { val mediaId = json.decodeFromString(anime.url) - val response = client.newCall( - GET("$baseUrl/content/v1/media?id=${mediaId.id}&channel_id=$channelId") - ).execute() - return Observable.just(animeDetailsParse(response)) + val resp = client.newCall(GET("$crUrl/content/v2/cms/series/${mediaId.id}?locale=en-US")).execute() + val info = json.decodeFromString(resp.body!!.string()) + return Observable.just(anime.apply { author = info.data.first().content_provider }) } - override fun animeDetailsParse(response: Response): SAnime { - val media = json.decodeFromString(response.body!!.string()) - val anime = SAnime.create() - anime.title = media.title - anime.author = media.content_provider - anime.status = SAnime.COMPLETED - - var description = media.description + "\n" - - description += "\nLanguage: Sub" + (if (media.is_dubbed) " Dub" else "") - - description += "\nMaturity Ratings: ${media.maturity_ratings}" - - description += if (media.is_simulcast!!) "\nSimulcast" else "" - - anime.description = description - - return anime - } + override fun animeDetailsParse(response: Response): SAnime = throw Exception("not used") // ============================== Episodes ============================== override fun episodeListRequest(anime: SAnime): Request { val mediaId = json.decodeFromString(anime.url) - val path = if (mediaId.media_type == "series") "seasons" else "movies" - return GET("$baseUrl/content/v1/$path?id=${mediaId.id}&channel_id=$channelId") + return GET("$crUrl/content/v2/cms/series/${mediaId.id}/seasons") } override fun episodeListParse(response: Response): List { - val medias = json.decodeFromString(response.body!!.string()) - - if (medias.items.first().media_class == "movie") { - return medias.items.map { media -> - SEpisode.create().apply { - url = media.id - name = "Movie" - episode_number = 0F - } - } - } else { - val rawEpsiodes = medias.items.map { season -> - season.episodes!!.map { - RawEpisode( - it.id, - it.title, - it.season_number, - it.sequence_number, - it.air_date - ) - } - }.flatten() - - return rawEpsiodes.groupBy { "${it.season}_${it.episode}" } - .mapNotNull { group -> - val (season, episode) = group.key.split("_") - val ep = episode.toFloatOrNull() ?: 0F + val seasons = json.decodeFromString(response.body!!.string()) + return seasons.data.parallelMap { seasonData -> + runCatching { + val episodeResp = client.newCall(GET("$crUrl/content/v2/cms/seasons/${seasonData.id}/episodes")).execute() + val episodes = json.decodeFromString(episodeResp.body!!.string()) + episodes.data.sortedBy { it.episode_number }.map { ep -> SEpisode.create().apply { - url = EpisodeData(group.value.map { it.id }).toJsonString() - name = if (ep > 0) "Season $season Ep ${df.format(ep)}: " + group.value.first().title else group.value.first().title - episode_number = ep - date_upload = parseDate(group.value.first().air_date) + url = EpisodeData( + ep.versions.map { Pair(it.id, it.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 = parseDate(ep.airDate) } - }.reversed() - } + } + }.getOrNull() + }.filterNotNull().flatten().reversed() } // ============================ Video Links ============================= override fun fetchVideoList(episode: SEpisode): Observable> { val urlJson = json.decodeFromString(episode.url) - val videoList = urlJson.ids.parallelMap { vidId -> + val videoList = urlJson.ids.parallelMap { media -> runCatching { - extractVideo(vidId) + extractVideo(media) }.getOrNull() } .filterNotNull() @@ -198,48 +159,53 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() { // ============================= Utilities ============================== - private fun extractVideo(vidId: String): List