chore(src/all): Remove yomiroll (#3053)

This commit is contained in:
Secozzi 2024-03-16 19:28:32 +00:00 committed by GitHub
parent 39bbf479f0
commit ac4937a61e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 0 additions and 1199 deletions

View File

@ -1,7 +0,0 @@
ext {
extName = 'Yomiroll'
extClass = '.Yomiroll'
extVersionCode = 31
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -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<AccessToken>(response.body.string())
val policy = client.newCall(newRequestWithAccessToken(GET("$crUrl/index/v2"), parsedJson)).execute()
val policyJson = json.decodeFromString<Policy>(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)
}
}
}

View File

@ -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<ArrayList<Image>>? = 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<String>? = 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<String>? = null,
val subtitle_locales: ArrayList<String>? = null,
val maturity_ratings: ArrayList<String>? = null,
val is_dubbed: Boolean? = null,
val is_subbed: Boolean? = null,
) {
@Serializable
data class Metadata(
val maturity_ratings: ArrayList<String>,
val is_simulcast: Boolean? = null,
val audio_locales: ArrayList<String>? = null,
val subtitle_locales: ArrayList<String>,
val is_dubbed: Boolean,
val is_subbed: Boolean,
@SerialName("tenant_categories")
val genres: ArrayList<String>? = null,
)
}
@Serializable
data class AnimeResult(
val total: Int,
val data: ArrayList<Anime>,
)
@Serializable
data class SearchAnimeResult(
val data: ArrayList<SearchAnime>,
) {
@Serializable
data class SearchAnime(
val type: String,
val count: Int,
val items: ArrayList<Anime>,
)
}
@Serializable
data class SeasonResult(
val total: Int,
val data: ArrayList<Season>,
) {
@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<Episode>,
) {
@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<Version>? = 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<Pair<String, String>>,
)
@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 <T> List<T>.thirdLast(): T? {
if (size < 3) return null
return this[size - 3]
}

View File

@ -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<Application>().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<AnimeResult>(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<SearchAnimeResult>(bod).data.first()
total = parsed.count
parsed.items
} else {
val parsed = json.decodeFromString<AnimeResult>(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<AnilistResult>(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<LinkData>(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<AnimeResult>(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<LinkData>(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<SEpisode> {
val seasons = json.decodeFromString<SeasonResult>(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<SEpisode> {
val body =
client.newCall(GET("$crApiUrl/cms/seasons/${seasonData.id}/episodes"))
.execute().body.string()
val episodes = json.decodeFromString<EpisodeResult>(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<Video> {
val urlJson = json.decodeFromString<EpisodeData>(episode.url)
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_KEY, false)
val videoList = urlJson.ids.filter {
it.second == dubLocale ||
it.second == "ja-JP" ||
it.second == "en-US" ||
it.second == "" ||
if (isUsingLocalToken) it.second == urlJson.ids.first().second else false
}.parallelCatchingFlatMap(::extractVideo)
return videoList.sort()
}
// ============================= Utilities ==============================
private fun extractVideo(media: Pair<String, String>): List<Video> {
val (mediaId, aud) = media
val response = client.newCall(getVideoRequest(mediaId)).execute().body.string()
val streams = json.decodeFromString<VideoStreams>(response)
val subLocale = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!.getLocale()
val subsList = runCatching {
streams.subtitles?.entries?.map { (_, value) ->
val sub = json.decodeFromString<Subtitle>(value.jsonObject.toString())
Track(sub.url, sub.locale.getLocale())
}?.sortedWith(
compareByDescending<Track> { it.lang.contains(subLocale) }
.thenBy { it.lang },
)
}.getOrNull() ?: emptyList()
val audLang = aud.ifBlank { streams.audio_locale } ?: "ja-JP"
return getStreams(streams, audLang, subsList)
}
private fun getStreams(
streams: VideoStreams,
audLang: String,
subsList: List<Track>,
): List<Video> {
return streams.streams?.adaptiveHls?.entries?.parallelMapNotNullBlocking { (_, value) ->
val stream = json.decodeFromString<HlsLinks>(value.jsonObject.toString())
runCatching {
val playlist = client.newCall(GET(stream.url)).execute()
if (playlist.code != 200) return@parallelMapNotNullBlocking null
playlist.body.string().substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").map {
val hardsub = stream.hardsub_locale.let { hs ->
if (hs.isNotBlank()) " - HardSub: $hs" else ""
}
val quality = it.substringAfter("RESOLUTION=")
.split(",")[0].split("\n")[0].substringAfter("x") +
"p - Aud: ${audLang.getLocale()}$hardsub"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
try {
Video(
videoUrl,
quality,
videoUrl,
subtitleTracks = if (hardsub.isNotBlank()) emptyList() else subsList,
)
} catch (_: Error) {
Video(videoUrl, quality, videoUrl)
}
}
}.getOrNull()
}?.flatten() ?: emptyList()
}
private fun getVideoRequest(mediaId: String): Request {
return GET("$crUrl/cms/v2{0}/videos/$mediaId/streams?Policy={1}&Signature={2}&Key-Pair-Id={3}")
}
private val df by lazy { DecimalFormat("0.#") }
private fun String.getLocale(): String {
return locale.firstOrNull { it.first == this }?.second ?: ""
}
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(
Pair("ar-ME", "Arabic"),
Pair("ar-SA", "Arabic (Saudi Arabia)"),
Pair("de-DE", "German"),
Pair("en-US", "English"),
Pair("en-IN", "English (India)"),
Pair("es-419", "Spanish (América Latina)"),
Pair("es-ES", "Spanish (España)"),
Pair("fr-FR", "French"),
Pair("ja-JP", "Japanese"),
Pair("hi-IN", "Hindi"),
Pair("it-IT", "Italian"),
Pair("ko-KR", "Korean"),
Pair("pt-BR", "Português (Brasil)"),
Pair("pt-PT", "Português (Portugal)"),
Pair("pl-PL", "Polish"),
Pair("ru-RU", "Russian"),
Pair("tr-TR", "Turkish"),
Pair("uk-UK", "Ukrainian"),
Pair("he-IL", "Hebrew"),
Pair("ro-RO", "Romanian"),
Pair("sv-SE", "Swedish"),
Pair("zh-CN", "Chinese (PRC)"),
Pair("zh-HK", "Chinese (Hong Kong)"),
Pair("zh-TW", "Chinese (Taiwan)"),
Pair("ca-ES", "Català"),
Pair("id-ID", "Bahasa Indonesia"),
Pair("ms-MY", "Bahasa Melayu"),
Pair("ta-IN", "Tamil"),
Pair("te-IN", "Telugu"),
Pair("th-TH", "Thai"),
Pair("vi-VN", "Vietnamese"),
)
private fun LinkData.toJsonString(): String {
return json.encodeToString(this)
}
private fun EpisodeData.toJsonString(): String {
return json.encodeToString(this)
}
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
private fun Anime.toSAnimeOrNull(anime: SAnime? = null) =
runCatching { toSAnime(anime) }.getOrNull()
private fun Anime.toSAnime(anime: SAnime? = null): SAnime =
SAnime.create().apply {
title = this@toSAnime.title
thumbnail_url = images.poster_tall?.getOrNull(0)?.thirdLast()?.source
?: images.poster_tall?.getOrNull(0)?.last()?.source
url = anime?.url ?: LinkData(id, type!!).toJsonString()
genre = anime?.genre ?: (series_metadata?.genres ?: movie_metadata?.genres ?: genres)
?.joinToString { gen -> gen.replaceFirstChar { it.uppercase() } }
status = anime?.let {
val media = json.decodeFromString<LinkData>(anime.url)
if (media.media_type == "series") {
fetchStatusByTitle(this@toSAnime.title)
} else {
SAnime.COMPLETED
}
} ?: SAnime.UNKNOWN
author = content_provider
description = StringBuilder().apply {
appendLine(this@toSAnime.description)
appendLine()
append("Language:")
if ((
subtitle_locales ?: (
series_metadata
?: movie_metadata
)?.subtitle_locales
)?.any() == true ||
(series_metadata ?: movie_metadata)?.is_subbed == true ||
is_subbed == true
) {
append(" Sub")
}
if (((series_metadata?.audio_locales ?: audio_locales)?.size ?: 0) > 1 ||
(series_metadata ?: movie_metadata)?.is_dubbed == true ||
is_dubbed == true
) {
append(" Dub")
}
appendLine()
append("Maturity Ratings: ")
appendLine(
((series_metadata ?: movie_metadata)?.maturity_ratings ?: maturity_ratings)
?.joinToString() ?: "-",
)
if (series_metadata?.is_simulcast == true) appendLine("Simulcast")
appendLine()
append("Audio: ")
appendLine(
(series_metadata?.audio_locales ?: audio_locales ?: listOf(audio_locale ?: "-"))
.sortedBy { it.getLocale() }
.joinToString { it.getLocale() },
)
appendLine()
append("Subs: ")
append(
(
subtitle_locales ?: series_metadata?.subtitle_locales
?: movie_metadata?.subtitle_locales
)
?.sortedBy { it.getLocale() }
?.joinToString { it.getLocale() },
)
}.toString()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QLT_KEY, PREF_QLT_DEFAULT)!!
val dubLocale = preferences.getString(PREF_AUD_KEY, PREF_AUD_DEFAULT)!!
val subLocale = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
val subType = preferences.getString(PREF_SUB_TYPE_KEY, PREF_SUB_TYPE_DEFAULT)!!
val shouldContainHard = subType == "hard"
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains("Aud: ${dubLocale.getLocale()}") },
{ it.quality.contains("HardSub") == shouldContainHard },
{ it.quality.contains(subLocale) },
),
).reversed()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QLT_KEY
title = PREF_QLT_TITLE
entries = PREF_QLT_ENTRIES
entryValues = PREF_QLT_VALUES
setDefaultValue(PREF_QLT_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val audLocalePref = ListPreference(screen.context).apply {
key = PREF_AUD_KEY
title = PREF_AUD_TITLE
entries = locale.map { it.second }.toTypedArray()
entryValues = locale.map { it.first }.toTypedArray()
setDefaultValue(PREF_AUD_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subLocalePref = ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = locale.map { it.second }.toTypedArray()
entryValues = locale.map { it.first }.toTypedArray()
setDefaultValue(PREF_SUB_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subTypePref = ListPreference(screen.context).apply {
key = PREF_SUB_TYPE_KEY
title = PREF_SUB_TYPE_TITLE
entries = PREF_SUB_TYPE_ENTRIES
entryValues = PREF_SUB_TYPE_VALUES
setDefaultValue(PREF_SUB_TYPE_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(audLocalePref)
screen.addPreference(subLocalePref)
screen.addPreference(subTypePref)
screen.addPreference(localSubsPreference(screen))
}
// From Jellyfin
private abstract class LocalSubsPreference(context: Context) : SwitchPreferenceCompat(context) {
abstract fun reload()
}
private fun localSubsPreference(screen: PreferenceScreen) =
object : LocalSubsPreference(screen.context) {
override fun reload() {
this.apply {
key = PREF_USE_LOCAL_TOKEN_KEY
title = PREF_USE_LOCAL_TOKEN_TITLE
mainScope.launch(Dispatchers.IO) {
getTokenDetail().let {
withContext(Dispatchers.Main) {
summary = it
}
}
}
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit().also {
mainScope.launch(Dispatchers.IO) {
getTokenDetail(true).let {
withContext(Dispatchers.Main) {
summary = it
}
}
}
}
}
}
}
}.apply { reload() }
private fun getTokenDetail(force: Boolean = false): String {
return runCatching {
val storedToken = tokenInterceptor.getAccessToken(force)
"Token location: " + storedToken.bucket?.substringAfter("/")?.substringBefore("/")
}.getOrElse {
tokenInterceptor.removeToken()
"Error: ${it.localizedMessage ?: "Something Went Wrong"}"
}
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)
}
private const val PREF_QLT_KEY = "preferred_quality"
private const val PREF_QLT_TITLE = "Preferred quality"
private const val PREF_QLT_DEFAULT = "1080p"
private val PREF_QLT_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p", "80p")
private val PREF_QLT_VALUES = PREF_QLT_ENTRIES
private const val PREF_AUD_KEY = "preferred_audio"
private const val PREF_AUD_TITLE = "Preferred Audio Language"
private const val PREF_AUD_DEFAULT = "en-US"
private const val PREF_SUB_KEY = "preferred_sub"
private const val PREF_SUB_TITLE = "Preferred Sub Language"
private const val PREF_SUB_DEFAULT = "en-US"
private const val PREF_SUB_TYPE_KEY = "preferred_sub_type"
private const val PREF_SUB_TYPE_TITLE = "Preferred Sub Type"
private const val PREF_SUB_TYPE_DEFAULT = "soft"
private val PREF_SUB_TYPE_ENTRIES = arrayOf("Softsub", "Hardsub")
private val PREF_SUB_TYPE_VALUES = arrayOf("soft", "hard")
private const val PREF_USE_LOCAL_TOKEN_KEY = "preferred_local_Token"
private const val PREF_USE_LOCAL_TOKEN_TITLE = "Use Local Token (Don't Spam this please!)"
}
}

View File

@ -1,201 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.kamyroll
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object YomirollFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("")
}
class TypeFilter : QueryPartFilter("Type", CrunchyFiltersData.SEARCH_TYPE)
class CategoryFilter : QueryPartFilter("Category", CrunchyFiltersData.CATEGORIES)
class SortFilter : QueryPartFilter("Sort By", CrunchyFiltersData.SORT_TYPE)
class MediaFilter : QueryPartFilter("Media", CrunchyFiltersData.MEDIA_TYPE)
class LanguageFilter : CheckBoxFilterList(
"Language",
CrunchyFiltersData.LANGUAGE.map { CheckBoxVal(it.first, false) },
)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("Search Filter (ignored if browsing)"),
TypeFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("Browse Filters (ignored if searching)"),
CategoryFilter(),
SortFilter(),
MediaFilter(),
LanguageFilter(),
)
data class FilterSearchParams(
val type: String = "",
val category: String = "",
val sort: String = "",
val language: String = "",
val media: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<CategoryFilter>(),
filters.asQueryPart<SortFilter>(),
filters.parseCheckbox<LanguageFilter>(CrunchyFiltersData.LANGUAGE),
filters.asQueryPart<MediaFilter>(),
)
}
private object CrunchyFiltersData {
val SEARCH_TYPE = arrayOf(
Pair("Top Results", "top_results"),
Pair("Series", "series"),
Pair("Movies", "movie_listing"),
)
val CATEGORIES = arrayOf(
Pair("-", ""),
Pair("Action", "&categories=action"),
Pair("Action, Adventure", "&categories=action,adventure"),
Pair("Action, Comedy", "&categories=action,comedy"),
Pair("Action, Drama", "&categories=action,drama"),
Pair("Action, Fantasy", "&categories=action,fantasy"),
Pair("Action, Historical", "&categories=action,historical"),
Pair("Action, Post-Apocalyptic", "&categories=action,post-apocalyptic"),
Pair("Action, Sci-Fi", "&categories=action,sci-fi"),
Pair("Action, Supernatural", "&categories=action,supernatural"),
Pair("Action, Thriller", "&categories=action,thriller"),
Pair("Adventure", "&categories=adventure"),
Pair("Adventure, Fantasy", "&categories=adventure,fantasy"),
Pair("Adventure, Isekai", "&categories=adventure,isekai"),
Pair("Adventure, Romance", "&categories=adventure,romance"),
Pair("Adventure, Sci-Fi", "&categories=adventure,sci-fi"),
Pair("Adventure, Supernatural", "&categories=adventure,supernatural"),
Pair("Comedy", "&categories=comedy"),
Pair("Comedy, Drama", "&categories=comedy,drama"),
Pair("Comedy, Fantasy", "&categories=comedy,fantasy"),
Pair("Comedy, Historical", "&categories=comedy,historical"),
Pair("Comedy, Music", "&categories=comedy,music"),
Pair("Comedy, Romance", "&categories=comedy,romance"),
Pair("Comedy, Sci-Fi", "&categories=comedy,sci-fi"),
Pair("Comedy, Slice of life", "&categories=comedy,slice+of+life"),
Pair("Comedy, Supernatural", "&categories=comedy,supernatural"),
Pair("Drama", "&categories=drama"),
Pair("Drama, Adventure", "&categories=drama,adventure"),
Pair("Drama, Fantasy", "&categories=drama,fantasy"),
Pair("Drama, Historical", "&categories=drama,historical"),
Pair("Drama, Mecha", "&categories=drama,mecha"),
Pair("Drama, Mystery", "&categories=drama,mystery"),
Pair("Drama, Romance", "&categories=drama,romance"),
Pair("Drama, Sci-Fi", "&categories=drama,sci-fi"),
Pair("Drama, Slice of life", "&categories=drama,slice+of+life"),
Pair("Fantasy", "&categories=fantasy"),
Pair("Fantasy, Historical", "&categories=fantasy,historical"),
Pair("Fantasy, Isekai", "&categories=fantasy,isekai"),
Pair("Fantasy, Mystery", "&categories=fantasy,mystery"),
Pair("Fantasy, Romance", "&categories=fantasy,romance"),
Pair("Fantasy, Supernatural", "&categories=fantasy,supernatural"),
Pair("Music", "&categories=music"),
Pair("Music, Drama", "&categories=music,drama"),
Pair("Music, Idols", "&categories=music,idols"),
Pair("Music, slice of life", "&categories=music,slice+of+life"),
Pair("Romance", "&categories=romance"),
Pair("Romance, Harem", "&categories=romance,harem"),
Pair("Romance, Historical", "&categories=romance,historical"),
Pair("Sci-Fi", "&categories=sci-fi"),
Pair("Sci-Fi, Fantasy", "&categories=sci-fi,Fantasy"),
Pair("Sci-Fi, Historical", "&categories=sci-fi,historical"),
Pair("Sci-Fi, Mecha", "&categories=sci-fi,mecha"),
Pair("Seinen", "&categories=seinen"),
Pair("Seinen, Action", "&categories=seinen,action"),
Pair("Seinen, Drama", "&categories=seinen,drama"),
Pair("Seinen, Fantasy", "&categories=seinen,fantasy"),
Pair("Seinen, Historical", "&categories=seinen,historical"),
Pair("Seinen, Supernatural", "&categories=seinen,supernatural"),
Pair("Shojo", "&categories=shojo"),
Pair("Shojo, Fantasy", "&categories=shojo,Fantasy"),
Pair("Shojo, Magical Girls", "&categories=shojo,magical-girls"),
Pair("Shojo, Romance", "&categories=shojo,romance"),
Pair("Shojo, Slice of life", "&categories=shojo,slice+of+life"),
Pair("Shonen", "&categories=shonen"),
Pair("Shonen, Action", "&categories=shonen,action"),
Pair("Shonen, Adventure", "&categories=shonen,adventure"),
Pair("Shonen, Comedy", "&categories=shonen,comedy"),
Pair("Shonen, Drama", "&categories=shonen,drama"),
Pair("Shonen, Fantasy", "&categories=shonen,fantasy"),
Pair("Shonen, Mystery", "&categories=shonen,mystery"),
Pair("Shonen, Post-Apocalyptic", "&categories=shonen,post-apocalyptic"),
Pair("Shonen, Supernatural", "&categories=shonen,supernatural"),
Pair("Slice of life", "&categories=slice+of+life"),
Pair("Slice of life, Fantasy", "&categories=slice+of+life,fantasy"),
Pair("Slice of life, Romance", "&categories=slice+of+life,romance"),
Pair("Slice of life, Sci-Fi", "&categories=slice+of+life,sci-fi"),
Pair("Sports", "&categories=sports"),
Pair("Sports, Action", "&categories=sports,action"),
Pair("Sports, Comedy", "&categories=sports,comedy"),
Pair("Sports, Drama", "&categories=sports,drama"),
Pair("Supernatural", "&categories=supernatural"),
Pair("Supernatural, Drama", "&categories=supernatural,drama"),
Pair("Supernatural, Historical", "&categories=supernatural,historical"),
Pair("Supernatural, Mystery", "&categories=supernatural,mystery"),
Pair("Supernatural, Slice of life", "&categories=supernatural,slice+of+life"),
Pair("Thriller", "&categories=thriller"),
Pair("Thriller, Drama", "&categories=thriller,drama"),
Pair("Thriller, Fantasy", "&categories=thriller,fantasy"),
Pair("Thriller, Sci-Fi", "&categories=thriller,sci-fi"),
Pair("Thriller, Supernatural", "&categories=thriller,supernatural"),
)
val SORT_TYPE = arrayOf(
Pair("Popular", "popularity"),
Pair("New", "newly_added"),
Pair("Alphabetical", "alphabetical"),
)
val LANGUAGE = arrayOf(
Pair("Sub", "&is_subbed=true"),
Pair("Dub", "&is_dubbed=true"),
)
val MEDIA_TYPE = arrayOf(
Pair("All", ""),
Pair("Series", "&type=series"),
Pair("Movies", "&type=movie_listing"),
)
}
}