diff --git a/src/all/kamyroll/build.gradle b/src/all/kamyroll/build.gradle deleted file mode 100644 index f14ece26cc..0000000000 --- a/src/all/kamyroll/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -ext { - extName = 'Yomiroll' - extClass = '.Yomiroll' - extVersionCode = 31 -} - -apply from: "$rootDir/common.gradle" diff --git a/src/all/kamyroll/res/mipmap-hdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index de285daf12..0000000000 Binary files a/src/all/kamyroll/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/kamyroll/res/mipmap-mdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 66d8a368e2..0000000000 Binary files a/src/all/kamyroll/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 9a281fbba8..0000000000 Binary files a/src/all/kamyroll/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index c27403f120..0000000000 Binary files a/src/all/kamyroll/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 438dc5dadc..0000000000 Binary files a/src/all/kamyroll/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null 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 63c77b7d1d..0000000000 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 deleted file mode 100644 index 22e7d5294f..0000000000 --- a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt +++ /dev/null @@ -1,170 +0,0 @@ -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.encodeToString -import kotlinx.serialization.json.Json -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.Authenticator -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 - -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 { - val accessTokenN = getAccessToken() - - val request = newRequestWithAccessToken(chain.request(), accessTokenN) - val response = chain.proceed(request) - when (response.code) { - HttpURLConnection.HTTP_UNAUTHORIZED -> { - synchronized(this) { - response.close() - // Access token is refreshed in another thread. Check if it has changed. - val newAccessToken = getAccessToken() - if (accessTokenN != newAccessToken) { - return chain.proceed(newRequestWithAccessToken(request, newAccessToken)) - } - val refreshedToken = getAccessToken(true) - // Retry the request - return chain.proceed( - newRequestWithAccessToken(chain.request(), refreshedToken), - ) - } - } - else -> return response - } - } - - private fun newRequestWithAccessToken(request: Request, tokenData: AccessToken): Request { - 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(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 removeToken() { - preferences.edit().putString(TOKEN_PREF_KEY, null).apply() - } - - 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 { - it.build() - } - } - val response = client.newCall(getRequest()).execute() - val parsedJson = json.decodeFromString(response.body.string()) - - val policy = client.newCall(newRequestWithAccessToken(GET("$crUrl/index/v2"), parsedJson)).execute() - val policyJson = json.decodeFromString(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, - DATE_FORMATTER.parse(policyJson.cms.expires)?.time, - ) - - preferences.edit().putString(TOKEN_PREF_KEY, allTokens.toJsonString()).apply() - return allTokens - } - - 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.Builder() - .add("Content-Type", "application/x-www-form-urlencoded") - .add( - "Authorization", - "Basic b2VkYXJteHN0bGgxanZhd2ltbnE6OWxFaHZIWkpEMzJqdVY1ZFc5Vk9TNTdkb3BkSnBnbzE=", - ) - .build() - val postBody = "grant_type=refresh_token&refresh_token=$refreshToken&scope=offline_access".toRequestBody( - "application/x-www-form-urlencoded".toMediaType(), - ) - return POST("$crUrl/auth/v1/token", headers, postBody) - } - - private fun AccessToken.toJsonString(): String { - return json.encodeToString(this) - } - - private fun String.toAccessToken(): AccessToken { - return json.decodeFromString(this) - } - - companion object { - private const val TOKEN_PREF_KEY = "access_token_data" - - private val DATE_FORMATTER 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/DataModel.kt b/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/DataModel.kt deleted file mode 100644 index c3194906e1..0000000000 --- a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/DataModel.kt +++ /dev/null @@ -1,191 +0,0 @@ -package eu.kanade.tachiyomi.animeextension.all.kamyroll - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject - -@Serializable -data class AccessToken( - val access_token: String, - val token_type: String, - val policy: String? = null, - val signature: String? = null, - val key_pair_id: String? = null, - val bucket: String? = null, - val policyExpire: Long? = 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, - val expires: String, - ) -} - -@Serializable -data class LinkData( - val id: String, - val media_type: String, -) - -@Serializable -data class Images( - val poster_tall: List>? = null, -) { - @Serializable - data class Image( - val width: Int, - val height: Int, - val type: String, - val source: String, - ) -} - -@Serializable -data class Anime( - val id: String, - val type: String? = null, - val title: String, - val description: String, - val images: Images, - @SerialName("keywords") - val genres: ArrayList? = null, - val series_metadata: Metadata? = null, - @SerialName("movie_listing_metadata") - val movie_metadata: Metadata? = null, - val content_provider: String? = null, - val audio_locale: String? = null, - val audio_locales: ArrayList? = null, - val subtitle_locales: ArrayList? = null, - val maturity_ratings: ArrayList? = null, - val is_dubbed: Boolean? = null, - val is_subbed: Boolean? = null, -) { - @Serializable - data class Metadata( - val maturity_ratings: ArrayList, - val is_simulcast: Boolean? = null, - val audio_locales: ArrayList? = null, - val subtitle_locales: ArrayList, - val is_dubbed: Boolean, - val is_subbed: Boolean, - @SerialName("tenant_categories") - val genres: ArrayList? = null, - ) -} - -@Serializable -data class AnimeResult( - val total: Int, - val data: ArrayList, -) - -@Serializable -data class SearchAnimeResult( - val data: ArrayList, -) { - @Serializable - data class SearchAnime( - val type: String, - val count: Int, - val items: ArrayList, - ) -} - -@Serializable -data class SeasonResult( - val total: Int, - val data: ArrayList, -) { - @Serializable - data class Season( - val id: String, - val season_number: Int? = null, - @SerialName("premium_available_date") - val date: String? = null, - ) -} - -@Serializable -data class EpisodeResult( - val total: Int, - val data: ArrayList, -) { - @Serializable - data class Episode( - val audio_locale: String, - val title: String, - @SerialName("sequence_number") - val episode_number: Float, - val episode: String? = null, - @SerialName("episode_air_date") - val airDate: String? = null, - val versions: ArrayList? = null, - val streams_link: String? = null, - ) { - @Serializable - data class Version( - val audio_locale: String, - @SerialName("media_guid") - val mediaId: String, - ) - } -} - -@Serializable -data class EpisodeData( - val ids: List>, -) - -@Serializable -data class VideoStreams( - val streams: Stream? = null, - val subtitles: JsonObject? = null, - val audio_locale: String? = null, -) { - @Serializable - data class Stream( - @SerialName("vo_adaptive_hls") - val adaptiveHls: JsonObject, - ) -} - -@Serializable -data class HlsLinks( - val hardsub_locale: String, - val url: String, -) - -@Serializable -data class Subtitle( - val locale: String, - val url: String, -) - -@Serializable -data class AnilistResult( - val data: AniData, -) { - @Serializable - data class AniData( - @SerialName("Media") - val media: Media? = null, - ) - - @Serializable - data class Media( - val status: String, - ) -} - -fun List.thirdLast(): T? { - if (size < 3) return null - return this[size - 3] -} 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 deleted file mode 100644 index a041db1a56..0000000000 --- a/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/Yomiroll.kt +++ /dev/null @@ -1,630 +0,0 @@ -package eu.kanade.tachiyomi.animeextension.all.kamyroll - -import android.app.Application -import android.content.Context -import android.content.SharedPreferences -import androidx.preference.ListPreference -import androidx.preference.PreferenceScreen -import androidx.preference.SwitchPreferenceCompat -import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource -import eu.kanade.tachiyomi.animesource.model.AnimeFilterList -import eu.kanade.tachiyomi.animesource.model.AnimesPage -import eu.kanade.tachiyomi.animesource.model.SAnime -import eu.kanade.tachiyomi.animesource.model.SEpisode -import eu.kanade.tachiyomi.animesource.model.Track -import eu.kanade.tachiyomi.animesource.model.Video -import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.util.parallelCatchingFlatMap -import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking -import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import okhttp3.FormBody -import okhttp3.Request -import okhttp3.Response -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import java.text.DecimalFormat -import java.text.SimpleDateFormat -import java.util.Locale - -@ExperimentalSerializationApi -class Yomiroll : ConfigurableAnimeSource, AnimeHttpSource() { - - // No more renaming, no matter what 3rd party service is used :) - override val name = "Yomiroll" - - override val baseUrl = "https://crunchyroll.com" - - private val crUrl = "https://beta-api.crunchyroll.com" - private val crApiUrl = "$crUrl/content/v2" - - override val lang = "all" - - override val supportsLatest = true - - override val id: Long = 7463514907068706782 - - private val json: Json by injectLazy() - - private val mainScope by lazy { MainScope() } - - private val preferences: SharedPreferences by lazy { - Injekt.get().getSharedPreferences("source_$id", 0x0000) - } - - private val tokenInterceptor by lazy { - AccessTokenInterceptor(crUrl, json, preferences, PREF_USE_LOCAL_TOKEN_KEY) - } - - override val client by lazy { - super.client.newBuilder().addInterceptor(tokenInterceptor).build() - } - - private val noTokenClient = super.client - - // ============================== Popular =============================== - - override fun popularAnimeRequest(page: Int): Request { - val start = if (page != 1) "start=${(page - 1) * 36}&" else "" - 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.mapNotNull { it.toSAnimeOrNull() } - val position = response.request.url.queryParameter("start")?.toIntOrNull() ?: 0 - return AnimesPage(animeList, position + 36 < parsed.total) - } - - // =============================== Latest =============================== - - override fun latestUpdatesRequest(page: Int): Request { - val start = if (page != 1) "start=${(page - 1) * 36}&" else "" - return GET("$crApiUrl/discover/browse?${start}n=36&sort_by=newly_added&locale=en-US") - } - - override fun latestUpdatesParse(response: Response): AnimesPage = popularAnimeParse(response) - - // =============================== Search =============================== - - override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { - val params = YomirollFilters.getSearchParameters(filters) - val start = if (page != 1) "start=${(page - 1) * 36}&" else "" - val url = if (query.isNotBlank()) { - val cleanQuery = query.replace(" ", "+").lowercase() - "$crApiUrl/discover/search?${start}n=36&q=$cleanQuery&type=${params.type}" - } else { - "$crApiUrl/discover/browse?${start}n=36${params.media}${params.language}&sort_by=${params.sort}${params.category}" - } - return GET(url) - } - - override fun searchAnimeParse(response: Response): AnimesPage { - val bod = response.body.string() - val total: Int - val items = - if (response.request.url.encodedPath.contains("search")) { - val parsed = json.decodeFromString(bod).data.first() - total = parsed.count - parsed.items - } else { - val parsed = json.decodeFromString(bod) - total = parsed.total - parsed.data - } - - val animeList = items.mapNotNull { it.toSAnimeOrNull() } - val position = response.request.url.queryParameter("start")?.toIntOrNull() ?: 0 - return AnimesPage(animeList, position + 36 < total) - } - - override fun getFilterList(): AnimeFilterList = YomirollFilters.FILTER_LIST - - // =========================== Anime Details ============================ - - // Function to fetch anime status using AniList GraphQL API ispired by OppaiStream.kt - private fun fetchStatusByTitle(title: String): Int { - val query = """ - query { - Media( - search: "$title", - sort: STATUS_DESC, - status_not_in: [NOT_YET_RELEASED], - format_not_in: [SPECIAL, MOVIE], - isAdult: false, - type: ANIME - ) { - id - idMal - title { - romaji - native - english - } - status - } - } - """.trimIndent() - - val requestBody = FormBody.Builder() - .add("query", query) - .build() - - val response = noTokenClient.newCall( - POST("https://graphql.anilist.co", body = requestBody), - ).execute().body.string() - - val responseParsed = json.decodeFromString(response) - - return when (responseParsed.data.media?.status) { - "FINISHED" -> SAnime.COMPLETED - "RELEASING" -> SAnime.ONGOING - "CANCELLED" -> SAnime.CANCELLED - "HIATUS" -> SAnime.ON_HIATUS - else -> SAnime.UNKNOWN - } - } - - override suspend fun getAnimeDetails(anime: SAnime): SAnime { - val mediaId = json.decodeFromString(anime.url) - val resp = client.newCall( - if (mediaId.media_type == "series") { - GET("$crApiUrl/cms/series/${mediaId.id}?locale=en-US") - } else { - GET("$crApiUrl/cms/movie_listings/${mediaId.id}?locale=en-US") - }, - ).execute().body.string() - val info = json.decodeFromString(resp) - return info.data.first().toSAnimeOrNull(anime) ?: anime - } - - override fun animeDetailsParse(response: Response): SAnime = - throw UnsupportedOperationException() - - // ============================== Episodes ============================== - - override fun episodeListRequest(anime: SAnime): Request { - val mediaId = json.decodeFromString(anime.url) - return if (mediaId.media_type == "series") { - GET("$crApiUrl/cms/series/${mediaId.id}/seasons") - } else { - 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 = Runtime.getRuntime().availableProcessors() - return if (series) { - seasons.data.sortedBy { it.season_number }.chunked(chunkSize).flatMap { chunk -> - chunk.parallelCatchingFlatMapBlocking(::getEpisodes) - }.reversed() - } else { - seasons.data.mapIndexed { index, movie -> - SEpisode.create().apply { - url = EpisodeData(listOf(Pair(movie.id, ""))).toJsonString() - name = "Movie ${index + 1}" - episode_number = (index + 1).toFloat() - date_upload = movie.date?.let(::parseDate) ?: 0L - } - } - } - } - - private fun getEpisodes(seasonData: SeasonResult.Season): List { - val body = - client.newCall(GET("$crApiUrl/cms/seasons/${seasonData.id}/episodes")) - .execute().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 suspend fun getVideoList(episode: SEpisode): List