chore(src/all): Remove yomiroll (#3053)
This commit is contained in:
parent
39bbf479f0
commit
ac4937a61e
@ -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 |
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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]
|
|
||||||
}
|
|
@ -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!)"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user