Kamyroll -> Consumyroll (#1247)

* Kamyroll -> Consumyroll

* Remove locale translation for hardsub

* Remove auto from softsub quality

* Rename only

* add content provider and list audio & sub versions

* move episode list to official cr api
update locales
list audio and sub versions in description
fix some inconsistencies
This commit is contained in:
Samfun75
2023-02-08 14:14:42 +03:00
committed by GitHub
parent 172ad279ca
commit 4ba680b983
16 changed files with 234 additions and 289 deletions

View File

@ -3,10 +3,10 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Kamyroll' extName = 'Consumyroll'
pkgNameSuffix = 'all.kamyroll' pkgNameSuffix = 'all.kamyroll'
extClass = '.Kamyroll' extClass = '.Consumyroll'
extVersionCode = 8 extVersionCode = 9
libVersion = '13' libVersion = '13'
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,51 +1,57 @@
package eu.kanade.tachiyomi.animeextension.all.kamyroll package eu.kanade.tachiyomi.animeextension.all.kamyroll
import android.content.SharedPreferences import android.content.SharedPreferences
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import java.net.HttpURLConnection import java.net.HttpURLConnection
class AccessTokenInterceptor(val baseUrl: String, val json: Json, val preferences: SharedPreferences) : Interceptor { class AccessTokenInterceptor(
private val deviceId = randomId() private val json: Json,
private val preferences: SharedPreferences
) : Interceptor {
private var accessToken = preferences.getString("access_token", null) ?: "" private var accessToken = preferences.getString("access_token", null) ?: ""
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
if (accessToken.isBlank()) accessToken = refreshAccessToken() if (accessToken.isBlank()) accessToken = refreshAccessToken()
val request = if (chain.request().url.toString().contains("kamyroll")) { val request = chain.request().newBuilder()
chain.request().newBuilder() .header("authorization", accessToken)
.header("authorization", accessToken) .build()
.build()
} else {
chain.request()
}
val response = chain.proceed(request) val response = chain.proceed(request)
if (response.code == HttpURLConnection.HTTP_UNAUTHORIZED) { when (response.code) {
synchronized(this) { HttpURLConnection.HTTP_UNAUTHORIZED -> {
response.close() synchronized(this) {
val newAccessToken = refreshAccessToken() response.close()
// Access token is refreshed in another thread. val newAccessToken = refreshAccessToken()
if (accessToken != newAccessToken) { // Access token is refreshed in another thread.
accessToken = newAccessToken if (accessToken != newAccessToken) {
return chain.proceed(newRequestWithAccessToken(chain.request(), newAccessToken)) accessToken = newAccessToken
return chain.proceed(
newRequestWithAccessToken(chain.request(), newAccessToken)
)
}
// Need to refresh an access token
val updatedAccessToken = refreshAccessToken()
accessToken = updatedAccessToken
// Retry the request
return chain.proceed(
newRequestWithAccessToken(chain.request(), updatedAccessToken)
)
} }
// Need to refresh an access token
val updatedAccessToken = refreshAccessToken()
accessToken = updatedAccessToken
// Retry the request
return chain.proceed(newRequestWithAccessToken(chain.request(), updatedAccessToken))
} }
else -> return response
} }
return response
} }
private fun newRequestWithAccessToken(request: Request, accessToken: String): Request { private fun newRequestWithAccessToken(request: Request, accessToken: String): Request {
@ -56,22 +62,17 @@ class AccessTokenInterceptor(val baseUrl: String, val json: Json, val preference
private fun refreshAccessToken(): String { private fun refreshAccessToken(): String {
val client = OkHttpClient().newBuilder().build() val client = OkHttpClient().newBuilder().build()
val url = "$baseUrl/auth/v1/token".toHttpUrlOrNull()!!.newBuilder() val headers = Headers.headersOf(
.addQueryParameter("device_id", deviceId) "Content-Type", "application/x-www-form-urlencoded",
.addQueryParameter("device_type", "aniyomi") "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0",
.addQueryParameter("access_token", "HMbQeThWmZq4t7w") "Authorization", "Basic a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8="
.build() )
val response = client.newCall(GET(url.toString())).execute() val postBody = "grant_type=client_id".toRequestBody("application/x-www-form-urlencoded".toMediaType())
val parsedJson = json.decodeFromString<AccessToken>(response.body!!.string()) val response = client.newCall(POST("https://beta-api.crunchyroll.com/auth/v1/token", headers, postBody)).execute()
val respBody = response.body!!.string()
val parsedJson = json.decodeFromString<AccessToken>(respBody)
val token = "${parsedJson.token_type} ${parsedJson.access_token}" val token = "${parsedJson.token_type} ${parsedJson.access_token}"
preferences.edit().putString("access_token", token).apply() preferences.edit().putString("access_token", token).apply()
return token return token
} }
// Random 15 length string
private fun randomId(): String {
return (0..14).joinToString("") {
(('0'..'9') + ('a'..'f')).random().toString()
}
}
} }

View File

@ -33,74 +33,71 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ExperimentalSerializationApi @ExperimentalSerializationApi
class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() { class Consumyroll : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Kamyroll" override val name = "Consumyroll"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://api.kamyroll.tech")!! } override val baseUrl = "https://cronchy.consumet.stream"
private val crUrl = "https://beta-api.crunchyroll.com"
override val lang = "all" override val lang = "all"
override val supportsLatest = false override val supportsLatest = true
override val id: Long = 7463514907068706782
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val channelId = "crunchyroll"
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override val client: OkHttpClient = OkHttpClient().newBuilder() override val client: OkHttpClient = OkHttpClient().newBuilder()
.addInterceptor(AccessTokenInterceptor(baseUrl, json, preferences)).build() .addInterceptor(AccessTokenInterceptor(json, preferences)).build()
companion object { companion object {
private val DateFormatter by lazy { private val DateFormatter by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)
} }
} }
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = override fun popularAnimeRequest(page: Int): Request {
GET("$baseUrl/content/v1/updated?channel_id=$channelId&limit=20") val start = if (page != 1) "start=${(page - 1) * 36}&" else ""
return GET("$crUrl/content/v2/discover/browse?${start}n=36&sort_by=popularity&locale=en-US")
}
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = json.decodeFromString<Updated>(response.body!!.string()) val parsed = json.decodeFromString<AnimeResult>(response.body!!.string())
val animeList = parsed.items.map { ani -> val animeList = parsed.data.filter { it.type == "series" }.map { ani ->
SAnime.create().apply { ani.toSAnime()
title = ani.series_title
thumbnail_url = ani.images.poster_tall!!.thirdLast().source
url = LinkData(ani.series_id, "series").toJsonString()
description = ani.description
}
} }
return AnimesPage(animeList, false) return AnimesPage(animeList, true)
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw Exception("not used") override fun latestUpdatesRequest(page: Int): Request {
val start = if (page != 1) "start=${(page - 1) * 36}&" else ""
return GET("$crUrl/content/v2/discover/browse?${start}n=36&sort_by=newly_added&locale=en-US")
}
override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("not used") override fun latestUpdatesParse(response: Response): AnimesPage = popularAnimeParse(response)
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val cleanQuery = query.replace(" ", "+").lowercase() val cleanQuery = query.replace(" ", "+").lowercase()
return GET("$baseUrl/content/v1/search?query=$cleanQuery&channel_id=$channelId") return GET("$crUrl/content/v2/discover/search?q=$cleanQuery&n=6&type=&locale=en-US")
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
val parsed = json.decodeFromString<SearchResult>(response.body!!.string()) val parsed = json.decodeFromString<SearchAnimeResult>(response.body!!.string())
val animeList = parsed.items.map { media -> val animeList = parsed.data.filter { it.type == "top_results" }.map { result ->
media.items.map { ani -> result.items.filter { it.type == "series" }.map { ani ->
SAnime.create().apply { ani.toSAnime()
title = ani.title
thumbnail_url = ani.images.poster_tall!!.thirdLast().source
url = LinkData(ani.id, ani.media_type).toJsonString()
description = ani.description
}
} }
}.flatten() }.flatten()
return AnimesPage(animeList, false) return AnimesPage(animeList, false)
@ -110,85 +107,49 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() {
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
val mediaId = json.decodeFromString<LinkData>(anime.url) val mediaId = json.decodeFromString<LinkData>(anime.url)
val response = client.newCall( val resp = client.newCall(GET("$crUrl/content/v2/cms/series/${mediaId.id}?locale=en-US")).execute()
GET("$baseUrl/content/v1/media?id=${mediaId.id}&channel_id=$channelId") val info = json.decodeFromString<AnimeResult>(resp.body!!.string())
).execute() return Observable.just(anime.apply { author = info.data.first().content_provider })
return Observable.just(animeDetailsParse(response))
} }
override fun animeDetailsParse(response: Response): SAnime { override fun animeDetailsParse(response: Response): SAnime = throw Exception("not used")
val media = json.decodeFromString<MediaResult>(response.body!!.string())
val anime = SAnime.create()
anime.title = media.title
anime.author = media.content_provider
anime.status = SAnime.COMPLETED
var description = media.description + "\n"
description += "\nLanguage: Sub" + (if (media.is_dubbed) " Dub" else "")
description += "\nMaturity Ratings: ${media.maturity_ratings}"
description += if (media.is_simulcast!!) "\nSimulcast" else ""
anime.description = description
return anime
}
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request { override fun episodeListRequest(anime: SAnime): Request {
val mediaId = json.decodeFromString<LinkData>(anime.url) val mediaId = json.decodeFromString<LinkData>(anime.url)
val path = if (mediaId.media_type == "series") "seasons" else "movies" return GET("$crUrl/content/v2/cms/series/${mediaId.id}/seasons")
return GET("$baseUrl/content/v1/$path?id=${mediaId.id}&channel_id=$channelId")
} }
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val medias = json.decodeFromString<EpisodeList>(response.body!!.string()) val seasons = json.decodeFromString<SeasonResult>(response.body!!.string())
return seasons.data.parallelMap { seasonData ->
if (medias.items.first().media_class == "movie") { runCatching {
return medias.items.map { media -> val episodeResp = client.newCall(GET("$crUrl/content/v2/cms/seasons/${seasonData.id}/episodes")).execute()
SEpisode.create().apply { val episodes = json.decodeFromString<EpisodeResult>(episodeResp.body!!.string())
url = media.id episodes.data.sortedBy { it.episode_number }.map { ep ->
name = "Movie"
episode_number = 0F
}
}
} else {
val rawEpsiodes = medias.items.map { season ->
season.episodes!!.map {
RawEpisode(
it.id,
it.title,
it.season_number,
it.sequence_number,
it.air_date
)
}
}.flatten()
return rawEpsiodes.groupBy { "${it.season}_${it.episode}" }
.mapNotNull { group ->
val (season, episode) = group.key.split("_")
val ep = episode.toFloatOrNull() ?: 0F
SEpisode.create().apply { SEpisode.create().apply {
url = EpisodeData(group.value.map { it.id }).toJsonString() url = EpisodeData(
name = if (ep > 0) "Season $season Ep ${df.format(ep)}: " + group.value.first().title else group.value.first().title ep.versions.map { Pair(it.id, it.audio_locale) }
episode_number = ep ).toJsonString()
date_upload = parseDate(group.value.first().air_date) name = if (ep.episode_number > 0 || ep.episode.isNumeric()) {
"Season ${seasonData.season_number} Ep ${df.format(ep.episode_number)}: " + ep.title
} else { ep.title }
episode_number = ep.episode_number
date_upload = parseDate(ep.airDate)
} }
}.reversed() }
} }.getOrNull()
}.filterNotNull().flatten().reversed()
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> { override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val urlJson = json.decodeFromString<EpisodeData>(episode.url) val urlJson = json.decodeFromString<EpisodeData>(episode.url)
val videoList = urlJson.ids.parallelMap { vidId -> val videoList = urlJson.ids.parallelMap { media ->
runCatching { runCatching {
extractVideo(vidId) extractVideo(media)
}.getOrNull() }.getOrNull()
} }
.filterNotNull() .filterNotNull()
@ -198,48 +159,53 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun extractVideo(vidId: String): List<Video> { private fun extractVideo(media: Pair<String, String>): List<Video> {
val url = "$baseUrl/videos/v1/streams?channel_id=$channelId&id=$vidId&type=adaptive_hls" val (vidId, audLang) = media
val response = client.newCall(GET(url)).execute() val response = client.newCall(GET("$baseUrl/episode/$vidId")).execute()
val streams = json.decodeFromString<VideoStreams>(response.body!!.string()) val streams = json.decodeFromString<VideoStreams>(response.body!!.string())
val subsList = mutableListOf<Track>() var subsList = emptyList<Track>()
val subLocale = preferences.getString("preferred_sub", "en-US")!! val subLocale = preferences.getString("preferred_sub", "en-US")!!.getLocale()
var subPreferred = 0
try { try {
streams.subtitles.forEach { sub -> subsList = streams.subtitles.map { sub ->
if (sub.locale == subLocale) { Track(sub.url, sub.lang.getLocale())
subsList.add( }.sortedWith(
subPreferred, compareBy(
Track(sub.url, sub.locale.getLocale()) { it.lang },
) { it.lang.contains(subLocale) }
subPreferred++ )
} else { )
subsList.add( } catch (_: Error) {}
Track(sub.url, sub.locale.getLocale())
)
}
}
} catch (_: Error) { }
return streams.streams.parallelMap { stream -> return streams.sources.filter { it.quality.contains("auto") || it.quality.contains("hardsub") }
runCatching { .parallelMap { stream ->
val playlist = client.newCall(GET(stream.url)).execute().body!!.string() runCatching {
playlist.substringAfter("#EXT-X-STREAM-INF:") val playlist = client.newCall(GET(stream.url)).execute().body!!.string()
.split("#EXT-X-STREAM-INF:").map { playlist.substringAfter("#EXT-X-STREAM-INF:")
val quality = it.substringAfter("RESOLUTION=").split(",")[0].split("\n")[0].substringAfter("x") + "p" + .split("#EXT-X-STREAM-INF:").map {
(if (stream.audio.getLocale().isNotBlank()) " - Aud: ${stream.audio.getLocale()}" else "") + val hardsub = stream.quality.replace("hardsub", "").replace("auto", "").trim()
(if (stream.hardsub.getLocale().isNotBlank()) " - HardSub: ${stream.hardsub}" else "") .let { hs ->
val videoUrl = it.substringAfter("\n").substringBefore("\n") if (hs.isNotBlank()) " - HardSub: $hs" else ""
}
val quality = it.substringAfter("RESOLUTION=")
.split(",")[0].split("\n")[0].substringAfter("x") +
"p - Aud: ${audLang.getLocale()}$hardsub"
try { val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl, subtitleTracks = if (stream.hardsub.getLocale().isNotBlank()) emptyList() else subsList)
} catch (e: Error) { try {
Video(videoUrl, quality, videoUrl) Video(
videoUrl,
quality,
videoUrl,
subtitleTracks = if (hardsub.isNotBlank()) emptyList() else subsList
)
} catch (_: Error) {
Video(videoUrl, quality, videoUrl)
}
} }
} }.getOrNull()
}.getOrNull() }
}
.filterNotNull() .filterNotNull()
.flatten() .flatten()
} }
@ -250,25 +216,34 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() {
return locale.firstOrNull { it.first == this }?.second ?: "" return locale.firstOrNull { it.first == this }?.second ?: ""
} }
private fun String.isNumeric(): Boolean {
return this@isNumeric.toDoubleOrNull() != null
}
private val locale = arrayOf( private val locale = arrayOf(
Pair("ar-ME", "Arabic"), Pair("ar-ME", "Arabic"),
Pair("ar-SA", "Arabic (Saudi Arabia)"), Pair("ar-SA", "Arabic (Saudi Arabia)"),
Pair("de-DE", "German"), Pair("de-DE", "German"),
Pair("en-US", "English"), Pair("en-US", "English"),
Pair("es-419", "Spanish"), Pair("en-IN", "English (India)"),
Pair("es-ES", "Spanish (Spain)"), Pair("es-419", "Spanish (América Latina)"),
Pair("es-LA", "Spanish (Spanish)"), Pair("es-ES", "Spanish (España)"),
Pair("es-LA", "Spanish (América Latina)"),
Pair("fr-FR", "French"), Pair("fr-FR", "French"),
Pair("ja-JP", "Japanese"), Pair("ja-JP", "Japanese"),
Pair("hi-IN", "Hindi"),
Pair("it-IT", "Italian"), Pair("it-IT", "Italian"),
Pair("pt-BR", "Portuguese (Brazil)"), Pair("ko-KR", "Korean"),
Pair("pt-BR", "Português (Brasil)"),
Pair("pt-PT", "Português (Portugal)"),
Pair("pl-PL", "Polish"), Pair("pl-PL", "Polish"),
Pair("ru-RU", "Russian"), Pair("ru-RU", "Russian"),
Pair("tr-TR", "Turkish"), Pair("tr-TR", "Turkish"),
Pair("uk-UK", "Ukrainian"), Pair("uk-UK", "Ukrainian"),
Pair("he-IL", "Hebrew"), Pair("he-IL", "Hebrew"),
Pair("ro-RO", "Romanian"), Pair("ro-RO", "Romanian"),
Pair("sv-SE", "Swedish") Pair("sv-SE", "Swedish"),
Pair("zh-CN", "Chinese")
) )
private fun LinkData.toJsonString(): String { private fun LinkData.toJsonString(): String {
@ -284,6 +259,23 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() {
.getOrNull() ?: 0L .getOrNull() ?: 0L
} }
private fun Anime.toSAnime(): SAnime =
SAnime.create().apply {
title = this@toSAnime.title
thumbnail_url = this@toSAnime.images.poster_tall?.getOrNull(0)?.thirdLast()?.source
?: this@toSAnime.images.poster_tall?.getOrNull(0)?.last()?.source
url = this@toSAnime.type!!.let { LinkData(this@toSAnime.id, it).toJsonString() }
genre = this@toSAnime.series_metadata!!.genres?.joinToString() ?: "Anime"
status = SAnime.COMPLETED
var desc = this@toSAnime.description + "\n"
desc += "\nLanguage: Sub" + (if (this@toSAnime.series_metadata.audio_locales.size > 1) " Dub" else "")
desc += "\nMaturity Ratings: ${this@toSAnime.series_metadata.maturity_ratings.joinToString()}"
desc += if (this@toSAnime.series_metadata.is_simulcast) "\nSimulcast" else ""
desc += "\n\nAudio: " + this@toSAnime.series_metadata.audio_locales.joinToString { it.getLocale() }
desc += "\n\nSubs: " + this@toSAnime.series_metadata.subtitle_locales.joinToString { it.getLocale() }
description += desc
}
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!! val quality = preferences.getString("preferred_quality", "1080")!!
val dubLocale = preferences.getString("preferred_audio", "en-US")!! val dubLocale = preferences.getString("preferred_audio", "en-US")!!
@ -302,22 +294,6 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() {
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("kamyroll.tech")
entryValues = arrayOf("https://api.kamyroll.tech")
setDefaultValue("https://api.kamyroll.tech")
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 videoQualityPref = ListPreference(screen.context).apply { val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality" key = "preferred_quality"
title = "Preferred quality" title = "Preferred quality"
@ -381,8 +357,6 @@ class Kamyroll : ConfigurableAnimeSource, AnimeHttpSource() {
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref) screen.addPreference(videoQualityPref)
screen.addPreference(audLocalePref) screen.addPreference(audLocalePref)
screen.addPreference(subLocalePref) screen.addPreference(subLocalePref)

View File

@ -17,7 +17,7 @@ data class LinkData(
@Serializable @Serializable
data class Images( data class Images(
val poster_tall: ArrayList<Image>? = null val poster_tall: List<ArrayList<Image>>? = null
) { ) {
@Serializable @Serializable
data class Image( data class Image(
@ -29,137 +29,107 @@ data class Images(
} }
@Serializable @Serializable
data class Metadata( data class Anime(
val is_dubbed: Boolean, val id: String,
val is_mature: Boolean, val type: String? = null,
val is_subbed: Boolean, val title: String,
val maturity_ratings: String, val description: String,
val episode_count: Int? = null, val images: Images,
val is_simulcast: Boolean? = null, val series_metadata: Metadata? = null,
val season_count: Int? = null val content_provider: String? = null,
) val audio_locales: ArrayList<String>? = null,
val subtitle_locales: ArrayList<String>? = null
@Serializable
data class Updated(
val total: Int,
val items: ArrayList<Item>
) { ) {
@Serializable @Serializable
data class Item( data class Metadata(
val id: String, val maturity_ratings: ArrayList<String>,
val series_id: String, val is_simulcast: Boolean,
val series_title: String, val audio_locales: ArrayList<String>,
val description: String, val subtitle_locales: ArrayList<String>,
val images: Images @SerialName("tenant_categories")
val genres: ArrayList<String>?
) )
} }
@Serializable @Serializable
data class SearchResult( data class AnimeResult(
val total: Int, val total: Int,
val items: ArrayList<SearchItem> val data: ArrayList<Anime>
)
@Serializable
data class SearchAnimeResult(
val total: Int,
val data: ArrayList<Result>
) { ) {
@Serializable @Serializable
data class SearchItem( data class Result(
val type: String, val type: String,
val total: Int, val count: Int,
val items: ArrayList<Item> val items: ArrayList<Anime>
) { )
@Serializable
data class Item(
val id: String,
val description: String,
val media_type: String,
val title: String,
val images: Images,
val series_metadata: Metadata? = null,
val movie_listing_metadata: Metadata? = null
)
}
} }
@Serializable @Serializable
data class EpisodeList( data class SeasonResult(
val total: Int, val total: Int,
val items: ArrayList<Item> val data: ArrayList<Season>
) { ) {
@Serializable @Serializable
data class Item( data class Season(
@SerialName("__class__")
val media_class: String,
val id: String, val id: String,
val type: String? = null, val season_number: Int
val is_subbed: Boolean? = null, )
val is_dubbed: Boolean? = null,
val episodes: ArrayList<Episode>? = null
) {
@Serializable
data class Episode(
val id: String,
val title: String,
val season_number: Int,
val sequence_number: Float,
val is_subbed: Boolean,
val is_dubbed: Boolean,
@SerialName("episode_air_date")
val air_date: String
)
}
} }
@Serializable @Serializable
data class MediaResult( data class EpisodeResult(
val id: String, val total: Int,
val title: String, val data: ArrayList<Episode>
val description: String, ) {
val images: Images, @Serializable
val maturity_ratings: String, data class Episode(
val content_provider: String, val title: String,
val is_mature: Boolean, @SerialName("sequence_number")
val is_subbed: Boolean, val episode_number: Float,
val is_dubbed: Boolean, val episode: String,
val episode_count: Int? = null, @SerialName("episode_air_date")
val season_count: Int? = null, val airDate: String,
val media_count: Int? = null, val versions: ArrayList<Version>
val is_simulcast: Boolean? = null ) {
) @Serializable
data class Version(
@Serializable val audio_locale: String,
data class RawEpisode( @SerialName("guid")
val id: String, val id: String
val title: String, )
val season: Int, }
val episode: Float, }
val air_date: String
)
@Serializable @Serializable
data class EpisodeData( data class EpisodeData(
val ids: List<String> val ids: List<Pair<String, String>>
) )
@Serializable @Serializable
data class VideoStreams( data class VideoStreams(
val streams: List<Stream>, val sources: List<Stream>,
val subtitles: List<Subtitle> val subtitles: List<Subtitle>
) { ) {
@Serializable @Serializable
data class Stream( data class Stream(
@SerialName("audio_locale") val url: String,
val audio: String, val quality: String
@SerialName("hardsub_locale")
val hardsub: String,
val url: String
) )
@Serializable @Serializable
data class Subtitle( data class Subtitle(
val locale: String, val url: String,
val url: String val lang: String
) )
} }
fun <T> List<T>.thirdLast(): T { fun <T> List<T>.thirdLast(): T? {
if (size < 3) throw NoSuchElementException("List has less than three elements") if (size < 3) return null
return this[size - 3] return this[size - 3]
} }