diff --git a/src/all/kamyroll/build.gradle b/src/all/kamyroll/build.gradle index e55da2789..3e6938c59 100644 --- a/src/all/kamyroll/build.gradle +++ b/src/all/kamyroll/build.gradle @@ -1,12 +1,14 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlinx-serialization' +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) +} ext { extName = 'Yomiroll' pkgNameSuffix = 'all.kamyroll' extClass = '.Yomiroll' - extVersionCode = 22 + extVersionCode = 23 libVersion = '13' } 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 bcead5175..dce6924ff 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 @@ -25,7 +25,6 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable @@ -45,6 +44,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { override val baseUrl = "https://crunchyroll.com" private val crUrl = "https://beta-api.crunchyroll.com" + private val crApiUrl = "$crUrl/content/v2" override val lang = "all" @@ -58,31 +58,25 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { Injekt.get().getSharedPreferences("source_$id", 0x0000) } - private val tokenInterceptor = AccessTokenInterceptor(crUrl, json, preferences, PREF_USE_LOCAL_Token) + private val tokenInterceptor by lazy { + AccessTokenInterceptor(crUrl, json, preferences, PREF_USE_LOCAL_TOKEN_KEY) + } - override val client: OkHttpClient = OkHttpClient().newBuilder() - .addInterceptor(tokenInterceptor).build() + override val client by lazy { + super.client.newBuilder().addInterceptor(tokenInterceptor).build() + } // ============================== Popular =============================== 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") + return GET("$crApiUrl/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.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 - } + val animeList = parsed.data.mapNotNull { it.toSAnimeOrNull() } + val position = response.request.url.queryParameter("start")?.toIntOrNull() ?: 0 return AnimesPage(animeList, position + 36 < parsed.total) } @@ -90,7 +84,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { 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") + return GET("$crApiUrl/discover/browse?${start}n=36&sort_by=newly_added&locale=en-US") } override fun latestUpdatesParse(response: Response): AnimesPage = popularAnimeParse(response) @@ -102,9 +96,9 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { 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}" + "$crApiUrl/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}" + "$crApiUrl/discover/browse?${start}n=36${params.media}${params.language}&sort_by=${params.sort}${params.category}" } return GET(url) } @@ -112,7 +106,7 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { override fun searchAnimeParse(response: Response): AnimesPage { val bod = response.body.string() val total: Int - val animeList = ( + val items = if (response.request.url.encodedPath.contains("search")) { val parsed = json.decodeFromString(bod).data.first() total = parsed.count @@ -122,17 +116,9 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { 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 - } + + val animeList = items.mapNotNull { it.toSAnimeOrNull() } + val position = response.request.url.queryParameter("start")?.toIntOrNull() ?: 0 return AnimesPage(animeList, position + 36 < total) } @@ -144,9 +130,9 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { val mediaId = json.decodeFromString(anime.url) val resp = client.newCall( if (mediaId.media_type == "series") { - GET("$crUrl/content/v2/cms/series/${mediaId.id}?locale=en-US") + GET("$crApiUrl/cms/series/${mediaId.id}?locale=en-US") } else { - GET("$crUrl/content/v2/cms/movie_listings/${mediaId.id}?locale=en-US") + GET("$crApiUrl/cms/movie_listings/${mediaId.id}?locale=en-US") }, ).execute() val info = json.decodeFromString(resp.body.string()) @@ -169,75 +155,78 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { override fun episodeListRequest(anime: SAnime): Request { val mediaId = json.decodeFromString(anime.url) return if (mediaId.media_type == "series") { - GET("$crUrl/content/v2/cms/series/${mediaId.id}/seasons") + GET("$crApiUrl/cms/series/${mediaId.id}/seasons") } else { - GET("$crUrl/content/v2/cms/movie_listings/${mediaId.id}/movies") + GET("$crApiUrl/cms/movie_listings/${mediaId.id}/movies") } } override fun episodeListParse(response: Response): List { val seasons = json.decodeFromString(response.body.string()) val series = response.request.url.encodedPath.contains("series/") - val chunkSize = if (preferences.getBoolean(PREF_DISABLE_SEASON_PARALLEL_MAP, false)) 1 else 6 - + val chunkSize = Runtime.getRuntime().availableProcessors() return if (series) { - seasons.data.sortedBy { it.season_number }.chunked(chunkSize).map { chunk -> + seasons.data.sortedBy { it.season_number }.chunked(chunkSize).flatMap { chunk -> chunk.parallelMap { seasonData -> runCatching { - val episodeResp = - client.newCall(GET("$crUrl/content/v2/cms/seasons/${seasonData.id}/episodes")) - .execute() - val body = episodeResp.body.string() - val episodes = - json.decodeFromString(body) - 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") - ?: return@EpisodeMap null, - 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 - scanlator = ep.versions?.sortedBy { it.audio_locale } - ?.joinToString { it.audio_locale.substringBefore("-") } - ?: ep.audio_locale.substringBefore("-") - } - }.filterNotNull() + getEpisodes(seasonData) }.getOrNull() }.filterNotNull().flatten() - }.flatten().reversed() + }.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 + url = EpisodeData(listOf(Pair(movie.id, ""))).toJsonString() + name = "Movie" + episode_number = (index + 1).toFloat() + date_upload = movie.date?.let(::parseDate) ?: 0L } } } } + private fun getEpisodes(seasonData: SeasonResult.Season): List { + val episodeResp = + client.newCall(GET("$crApiUrl/cms/seasons/${seasonData.id}/episodes")) + .execute() + val body = episodeResp.body.string() + val episodes = json.decodeFromString(body) + + return episodes.data.sortedBy { it.episode_number }.mapNotNull 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") + ?: return@EpisodeMap null, + 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) ?: 0L + scanlator = ep.versions?.sortedBy { it.audio_locale } + ?.joinToString { it.audio_locale.substringBefore("-") } + ?: ep.audio_locale.substringBefore("-") + } + } + } + // ============================ Video Links ============================= override fun fetchVideoList(episode: SEpisode): Observable> { val urlJson = json.decodeFromString(episode.url) - val dubLocale = preferences.getString("preferred_audio", "en-US")!! + val dubLocale = preferences.getString(PREF_AUD_KEY, PREF_AUD_DEFAULT)!! if (urlJson.ids.isEmpty()) throw Exception("No IDs found for episode") - val isUsingLocalToken = preferences.getBoolean(PREF_USE_LOCAL_Token, false) + val isUsingLocalToken = preferences.getBoolean(PREF_USE_LOCAL_TOKEN_KEY, false) val videoList = urlJson.ids.filter { it.second == dubLocale || @@ -261,22 +250,18 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { val response = client.newCall(getVideoRequest(mediaId)).execute() val streams = json.decodeFromString(response.body.string()) - var subsList = emptyList() - val subLocale = preferences.getString("preferred_sub", "en-US")!!.getLocale() - try { - val tempSubs = mutableListOf() + val subLocale = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!.getLocale() + val subsList = runCatching { streams.subtitles?.entries?.map { (_, value) -> val sub = json.decodeFromString(value.jsonObject.toString()) - tempSubs.add(Track(sub.url, sub.locale.getLocale())) - } - - subsList = tempSubs.sortedWith( + Track(sub.url, sub.locale.getLocale()) + }?.sortedWith( compareBy( { it.lang }, { it.lang.contains(subLocale) }, ), ) - } catch (_: Error) {} + }.getOrNull() ?: emptyList() val audLang = aud.ifBlank { streams.audio_locale } ?: "ja-JP" return getStreams(streams, audLang, subsList) @@ -322,15 +307,13 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { 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 by lazy { DecimalFormat("0.#") } private fun String.getLocale(): String { return locale.firstOrNull { it.first == this }?.second ?: "" } - private fun String?.isNumeric(): Boolean { - return this@isNumeric?.toDoubleOrNull() != null - } + private fun String?.isNumeric() = this?.toDoubleOrNull() != null // Add new locales to the bottom so it doesn't mess with pref indexes private val locale = arrayOf( @@ -373,21 +356,23 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { .getOrNull() ?: 0L } + private fun Anime.toSAnimeOrNull() = runCatching { toSAnime() }.getOrNull() + private fun Anime.toSAnime(): SAnime = SAnime.create().apply { 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 = LinkData(this@toSAnime.id, this@toSAnime.type!!).toJsonString() - genre = this@toSAnime.series_metadata?.genres?.joinToString() - ?: this@toSAnime.movie_metadata?.genres?.joinToString() ?: "" + thumbnail_url = images.poster_tall?.getOrNull(0)?.thirdLast()?.source + ?: images.poster_tall?.getOrNull(0)?.last()?.source + url = LinkData(id, type!!).toJsonString() + genre = series_metadata?.genres?.joinToString() + ?: movie_metadata?.genres?.joinToString() ?: "" status = SAnime.COMPLETED var desc = this@toSAnime.description + "\n" 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 + if (series_metadata?.subtitle_locales?.any() == true || + movie_metadata?.subtitle_locales?.any() == true || + series_metadata?.is_subbed == true ) { " Sub" } else { @@ -395,8 +380,8 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { } ) + ( - if ((this@toSAnime.series_metadata?.audio_locales?.size ?: 0) > 1 || - this@toSAnime.movie_metadata?.is_dubbed == true + if ((series_metadata?.audio_locales?.size ?: 0) > 1 || + movie_metadata?.is_dubbed == true ) { " Dub" } else { @@ -405,48 +390,47 @@ class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { ) desc += "\nMaturity Ratings: " + ( - this@toSAnime.series_metadata?.maturity_ratings?.joinToString() - ?: this@toSAnime.movie_metadata?.maturity_ratings?.joinToString() ?: "" + series_metadata?.maturity_ratings?.joinToString() + ?: movie_metadata?.maturity_ratings?.joinToString() ?: "" ) - desc += if (this@toSAnime.series_metadata?.is_simulcast == true) "\nSimulcast" else "" + desc += if (series_metadata?.is_simulcast == true) "\nSimulcast" else "" desc += "\n\nAudio: " + ( - this@toSAnime.series_metadata?.audio_locales?.sortedBy { it.getLocale() } + series_metadata?.audio_locales?.sortedBy { it.getLocale() } ?.joinToString { it.getLocale() } ?: "" ) desc += "\n\nSubs: " + ( - this@toSAnime.series_metadata?.subtitle_locales?.sortedBy { it.getLocale() } + series_metadata?.subtitle_locales?.sortedBy { it.getLocale() } ?.joinToString { it.getLocale() } - ?: this@toSAnime.movie_metadata?.subtitle_locales?.sortedBy { it.getLocale() } + ?: movie_metadata?.subtitle_locales?.sortedBy { it.getLocale() } ?.joinToString { it.getLocale() } ?: "" ) description = desc } override fun List