Merge branch 'Kohi-den:main' into fix-lycoris
7
lib/amazon-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package eu.kanade.tachiyomi.lib.amazonextractor
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class AmazonExtractor(private val client: OkHttpClient) {
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
if (url.contains("disable", true)) return emptyList()
|
||||
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
|
||||
val shareIdScript = document.select("script:containsData(var shareId)").firstOrNull()?.data()
|
||||
if (shareIdScript.isNullOrBlank()) return emptyList()
|
||||
|
||||
val shareId = shareIdScript.substringAfter("shareId = \"").substringBefore("\"")
|
||||
val amazonApiJsonUrl = "https://www.amazon.com/drive/v1/shares/$shareId?resourceVersion=V2&ContentType=JSON&asset=ALL"
|
||||
|
||||
val amazonApiJson = client.newCall(GET(amazonApiJsonUrl)).execute().asJsoup()
|
||||
|
||||
val epId = amazonApiJson.toString().substringAfter("\"id\":\"").substringBefore("\"")
|
||||
val amazonApiUrl = "https://www.amazon.com/drive/v1/nodes/$epId/children?resourceVersion=V2&ContentType=JSON&limit=200&sort=%5B%22kind+DESC%22%2C+%22modifiedDate+DESC%22%5D&asset=ALL&tempLink=true&shareId=$shareId"
|
||||
|
||||
val amazonApi = client.newCall(GET(amazonApiUrl)).execute().asJsoup()
|
||||
|
||||
val videoUrl = amazonApi.toString().substringAfter("\"FOLDER\":").substringAfter("tempLink\":\"").substringBefore("\"")
|
||||
|
||||
val serverName = if (videoUrl.contains("&ext=es")) "AmazonES" else "Amazon"
|
||||
|
||||
return if (videoUrl.contains(".m3u8")) {
|
||||
playlistUtils.extractFromHls(videoUrl, videoNameGen = { "${prefix}$serverName:$it" })
|
||||
} else {
|
||||
listOf(Video(videoUrl, "${prefix}$serverName", videoUrl))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -141,7 +141,7 @@ class MegaCloudExtractor(
|
|||
}
|
||||
|
||||
private fun getVideoDto(url: String): VideoDto {
|
||||
val type = if (url.startsWith("https://megacloud.tv")) 0 else 1
|
||||
val type = if (url.startsWith("https://megacloud.tv") or url.startsWith("https://megacloud.club")) 0 else 1
|
||||
|
||||
val keyType = SOURCES_KEY[type]
|
||||
|
||||
|
|
Before Width: | Height: | Size: 164 KiB |
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeWorld India'
|
||||
extClass = '.AnimeWorldIndiaFactory'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -29,7 +29,7 @@ class AnimeWorldIndia(
|
|||
|
||||
override val name = "AnimeWorld India"
|
||||
|
||||
override val baseUrl = "https://anime-world.in"
|
||||
override val baseUrl = "https://anime-world.co"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ class AnimeWorldIndiaFilters {
|
|||
|
||||
private fun getYearList() = listOf(
|
||||
StringQuery("Any", "all"),
|
||||
StringQuery("2025", "2025"),
|
||||
StringQuery("2024", "2024"),
|
||||
StringQuery("2023", "2023"),
|
||||
StringQuery("2022", "2022"),
|
||||
|
|
Before Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 64 KiB |
11
src/all/shabakatycinemana/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'ShabakatyCinemana'
|
||||
extClass = '.ShabakatyCinemana'
|
||||
extVersionCode = 2
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
}
|
BIN
src/all/shabakatycinemana/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/all/shabakatycinemana/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
src/all/shabakatycinemana/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
src/all/shabakatycinemana/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/all/shabakatycinemana/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,629 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.shabakatycinemana
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
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.util.parseAs
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
inline fun <reified T> Response.asModel(deserializer: DeserializationStrategy<T>): T {
|
||||
return Json.decodeFromString(deserializer, this.body.string())
|
||||
}
|
||||
|
||||
inline fun <reified T> Response.asModelList(deserializer: DeserializationStrategy<T>): List<T> {
|
||||
return Json.parseToJsonElement(this.body.string()).jsonArray.map {
|
||||
Json.decodeFromJsonElement(deserializer, it)
|
||||
}
|
||||
}
|
||||
|
||||
object SEpisodeDeserializer : DeserializationStrategy<SEpisode> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = buildClassSerialDescriptor("SEpisode")
|
||||
|
||||
override fun deserialize(decoder: Decoder): SEpisode {
|
||||
val jsonDecoder = decoder as JsonDecoder
|
||||
val jsonObject = jsonDecoder.decodeJsonElement() as JsonObject
|
||||
|
||||
val nb = jsonObject["nb"]?.jsonPrimitive?.content!!
|
||||
val episodeNumber = jsonObject["episodeNummer"]?.jsonPrimitive?.content
|
||||
val seasonNumber = jsonObject["season"]?.jsonPrimitive?.content
|
||||
val seasonEpisode = arrayOf(seasonNumber, episodeNumber).joinToString(ShabakatyCinemana.SEASON_EPISODE_DELIMITER)
|
||||
val uploadDate = jsonObject["videoUploadDate"]?.jsonPrimitive?.content.runCatching {
|
||||
this?.let { ShabakatyCinemana.DATE_FORMATTER.parse(it)?.time }
|
||||
}.getOrNull() ?: 0L
|
||||
|
||||
return SEpisode.create().apply {
|
||||
url = nb
|
||||
episode_number = "$seasonNumber.$episodeNumber".parseAs()
|
||||
name = seasonEpisode
|
||||
date_upload = uploadDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object VideoDeserializer : DeserializationStrategy<Video> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = buildClassSerialDescriptor("Video")
|
||||
|
||||
override fun deserialize(decoder: Decoder): Video {
|
||||
val jsonDecoder = decoder as JsonDecoder
|
||||
val jsonObject = jsonDecoder.decodeJsonElement() as JsonObject
|
||||
|
||||
val videoUrl = jsonObject["videoUrl"]?.jsonPrimitive?.content!!
|
||||
val quality = jsonObject["resolution"]?.jsonPrimitive?.content.orEmpty()
|
||||
|
||||
return Video(url = videoUrl, videoUrl = videoUrl, quality = quality)
|
||||
}
|
||||
}
|
||||
|
||||
object SubtitleDeserialize : DeserializationStrategy<List<Track>> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = buildClassSerialDescriptor("Track")
|
||||
|
||||
override fun deserialize(decoder: Decoder): List<Track> {
|
||||
val jsonDecoder = decoder as JsonDecoder
|
||||
val jsonObject = jsonDecoder.decodeJsonElement() as JsonObject
|
||||
|
||||
return jsonObject["translations"]?.jsonArray?.map {
|
||||
val url = it.jsonObject["file"]?.jsonPrimitive?.content!!
|
||||
val name = it.jsonObject["name"]?.jsonPrimitive?.content
|
||||
val extension = it.jsonObject["extention"]?.jsonPrimitive?.content
|
||||
val lang = arrayOf(name, extension).joinToString(ShabakatyCinemana.SUBTITLE_DELIMITER)
|
||||
|
||||
Track(url, lang)
|
||||
}.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
data class SAnimeListWithInfo(val animes: List<SAnime>, val offset: Int)
|
||||
|
||||
object SAnimeWithInfoDeserializer : DeserializationStrategy<SAnimeListWithInfo> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = buildClassSerialDescriptor("SAnimeListWithInfo")
|
||||
|
||||
override fun deserialize(decoder: Decoder): SAnimeListWithInfo {
|
||||
val jsonDecoder = decoder as JsonDecoder
|
||||
val jsonObject = jsonDecoder.decodeJsonElement() as JsonObject
|
||||
|
||||
val animeList = jsonObject["info"]?.jsonArray?.map {
|
||||
Json.decodeFromJsonElement(SAnimeDeserializer, it)
|
||||
}.orEmpty()
|
||||
val offset = jsonObject["offset"]?.jsonPrimitive?.int ?: 0
|
||||
|
||||
return SAnimeListWithInfo(animeList, offset)
|
||||
}
|
||||
}
|
||||
|
||||
object SAnimeDeserializer : DeserializationStrategy<SAnime> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = buildClassSerialDescriptor("SAnime")
|
||||
|
||||
override fun deserialize(decoder: Decoder): SAnime {
|
||||
val jsonDecoder = decoder as JsonDecoder
|
||||
val jsonObject = jsonDecoder.decodeJsonElement() as JsonObject
|
||||
|
||||
val nb = jsonObject["nb"]?.jsonPrimitive?.content!!
|
||||
val enTitle = jsonObject["en_title"]?.jsonPrimitive?.content ?: "no title"
|
||||
val imgObjUrl = jsonObject["imgObjUrl"]?.jsonPrimitive?.content
|
||||
val categories = jsonObject["categories"]?.jsonArray?.map {
|
||||
it.jsonObject["en_title"]?.jsonPrimitive?.content
|
||||
}?.joinToString(", ")
|
||||
val enContent = jsonObject["en_content"]?.jsonPrimitive?.content
|
||||
val year = jsonObject["year"]?.jsonPrimitive?.content ?: "N/A"
|
||||
val stars = jsonObject["stars"]?.jsonPrimitive?.content?.parseAs<Float>()?.toInt() ?: 0
|
||||
val starsText = "${"★".repeat(stars / 2)}${"☆".repeat(5 - (stars / 2))}"
|
||||
val likes = jsonObject["Likes"]?.jsonPrimitive?.content?.parseAs<Int>() ?: 0
|
||||
val dislikes = jsonObject["DisLikes"]?.jsonPrimitive?.content?.parseAs<Int>() ?: 0
|
||||
// val ref = jsonObject["imdbUrlRef"]?.jsonPrimitive?.content ?: ""
|
||||
|
||||
return SAnime.create().apply {
|
||||
url = nb
|
||||
title = enTitle
|
||||
thumbnail_url = imgObjUrl
|
||||
genre = categories
|
||||
description = "$year | $starsText | $likes\uD83D\uDC4D $dislikes\uD83D\uDC4E\n\n$enContent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShabakatyCinemana : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "Shabakaty Cinemana"
|
||||
|
||||
override val baseUrl = "https://cinemana.shabakaty.com"
|
||||
|
||||
private val apiBaseUrl = "$baseUrl/api/android"
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IS_BROWSING_FILTER_NAME = "Browse"
|
||||
private const val KIND_FILTER_NAME = "Kind"
|
||||
private const val MAIN_CATEGORY_FILTER_NAME = "Main Category"
|
||||
private const val SUB_CATEGORY_FILTER_NAME = "Sub Category"
|
||||
private const val LANGUAGE_FILTER_NAME = "Language"
|
||||
private const val YEAR_FILTER_NAME = "Year"
|
||||
private const val BROWSE_RESULT_SORT_FILTER = "Browse Sort"
|
||||
|
||||
private const val POPULAR_ITEMS_PER_PAGE = 30
|
||||
private const val SEARCH_ITEMS_PER_PAGE = 12
|
||||
private const val LATEST_ITEMS_PER_PAGE = 24
|
||||
|
||||
private const val PREF_LATEST_KIND_KEY = "preferred_latest_kind"
|
||||
private const val PREF_LATEST_KIND_DEFAULT = "Movies"
|
||||
private val KINDS_LIST = arrayOf(
|
||||
Pair("Movies", 1),
|
||||
Pair("Series", 2),
|
||||
)
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("2160", "1080", "720", "480", "360", "240")
|
||||
|
||||
private const val PREF_SUBTITLE_LANG_KEY = "preferred_subtitle_language"
|
||||
private const val PREF_SUBTITLE_LANG_DEFAULT = "arabic"
|
||||
private val LANG_LIST = arrayOf("arabic", "english")
|
||||
|
||||
private const val PREF_SUBTITLE_EXT_KEY = "preferred_subtitle_extension"
|
||||
private const val PREF_SUBTITLE_EXT_DEFAULT = "ass"
|
||||
private val EXT_LIST = arrayOf("srt", "vtt", "ass")
|
||||
|
||||
const val SUBTITLE_DELIMITER = " - "
|
||||
const val SEASON_EPISODE_DELIMITER = " - "
|
||||
|
||||
val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAnimeUrl(anime: SAnime) = "$baseUrl/video/en/${anime.url}"
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime) = GET("$apiBaseUrl/allVideoInfo/id/${anime.url}")
|
||||
|
||||
override fun animeDetailsParse(response: Response) = response.asModel(SAnimeDeserializer)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val kind = preferences.getString(PREF_LATEST_KIND_KEY, PREF_LATEST_KIND_DEFAULT)!!
|
||||
|
||||
return GET("$apiBaseUrl/latest$kind/level/0/itemsPerPage/$LATEST_ITEMS_PER_PAGE/page/${page - 1}/", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val animeList = response.asModelList(SAnimeDeserializer)
|
||||
return AnimesPage(animeList, animeList.size == LATEST_ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
val kindPref = preferences.getString(PREF_LATEST_KIND_KEY, PREF_LATEST_KIND_DEFAULT)!!
|
||||
val kind = KINDS_LIST.first { it.first == kindPref }.second
|
||||
|
||||
val url = "$apiBaseUrl/video/V/2/itemsPerPage/$POPULAR_ITEMS_PER_PAGE/level/0/videoKind/$kind/sortParam/desc/pageNumber/${page - 1}"
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val animeList = response.asModelList(SAnimeDeserializer)
|
||||
return AnimesPage(animeList, animeList.size == POPULAR_ITEMS_PER_PAGE)
|
||||
}
|
||||
|
||||
override suspend fun getSearchAnime(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: AnimeFilterList,
|
||||
): AnimesPage {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val isBrowsingFilter = filterList.find { it.name == IS_BROWSING_FILTER_NAME } as CheckBoxFilter
|
||||
val kindFilter = filterList.find { it.name == KIND_FILTER_NAME } as SingleSelectFilter
|
||||
val mainCategoryFilter = filterList.find { it.name == MAIN_CATEGORY_FILTER_NAME } as MultipleSelectFilter
|
||||
val subCategoryFilter = filterList.find { it.name == SUB_CATEGORY_FILTER_NAME } as MultipleSelectFilter
|
||||
val languageFilter = filterList.find { it.name == LANGUAGE_FILTER_NAME } as SingleSelectFilter
|
||||
val yearFilter = filterList.find { it.name == YEAR_FILTER_NAME } as YearFilter
|
||||
val browseResultSortFilter = filterList.find { it.name == BROWSE_RESULT_SORT_FILTER } as BrowseResultSort
|
||||
val isBrowsing = isBrowsingFilter.state
|
||||
val kindName = kindFilter.getNameValue()
|
||||
val kindNumber = kindFilter.getNumberValue().toString()
|
||||
val selectedMainCategories = mainCategoryFilter.getSelectedIds()
|
||||
val mainCategory = selectedMainCategories.joinToString(",")
|
||||
val selectedSubCategories = subCategoryFilter.getSelectedIds()
|
||||
val bothCategory = (selectedMainCategories + selectedSubCategories).joinToString(",")
|
||||
val language = languageFilter.getNumberValue().toString()
|
||||
val year = yearFilter.getFormatted()
|
||||
val browseResultSort = browseResultSortFilter.getValue()
|
||||
|
||||
var url = apiBaseUrl.toHttpUrl()
|
||||
if (isBrowsing) {
|
||||
if (languageFilter.state != 0 && mainCategory.isNotBlank()) {
|
||||
url = url.newBuilder()
|
||||
.addPathSegment("videosByCategoryAndLanguage")
|
||||
.addQueryParameter("language_id", language)
|
||||
.addQueryParameter("category_id", mainCategory)
|
||||
.build()
|
||||
} else {
|
||||
url = url.newBuilder()
|
||||
.addPathSegment("videosByCategory")
|
||||
.build()
|
||||
|
||||
if (mainCategoryFilter.hasSelected()) {
|
||||
url = url.newBuilder().addQueryParameter("categoryID", mainCategory).build()
|
||||
}
|
||||
}
|
||||
|
||||
url = url.newBuilder()
|
||||
.addQueryParameter("level", "0")
|
||||
.addQueryParameter("offset", "${(page - 1) * POPULAR_ITEMS_PER_PAGE}")
|
||||
.addQueryParameter("videoKind", kindNumber)
|
||||
.addQueryParameter("orderby", browseResultSort)
|
||||
.build()
|
||||
|
||||
val resp = client.newCall(GET(url, headers)).execute()
|
||||
// Todo: remove SAnimeWithInfo data class if no longer needed
|
||||
val animeListWithInfo = resp.asModel(SAnimeWithInfoDeserializer)
|
||||
return AnimesPage(animeListWithInfo.animes, animeListWithInfo.animes.size == POPULAR_ITEMS_PER_PAGE)
|
||||
} else {
|
||||
// star=8&year=1900,2025
|
||||
url = url.newBuilder()
|
||||
.addQueryParameter("level", "0")
|
||||
.addPathSegment("AdvancedSearch")
|
||||
.addQueryParameter("type", kindName)
|
||||
.addQueryParameter("page", "${page - 1}")
|
||||
.addQueryParameter("year", year)
|
||||
.build()
|
||||
|
||||
if (bothCategory.isNotBlank()) {
|
||||
url = url.newBuilder().addQueryParameter("category_id", bothCategory).build()
|
||||
}
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
url = url.newBuilder()
|
||||
.addQueryParameter("videoTitle", query)
|
||||
.addQueryParameter("staffTitle", query)
|
||||
.build()
|
||||
}
|
||||
|
||||
val resp = client.newCall(GET(url, headers)).execute()
|
||||
val animeList = resp.asModelList(SAnimeDeserializer)
|
||||
return AnimesPage(animeList, animeList.size == SEARCH_ITEMS_PER_PAGE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response) =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||
val episodeList = super.getEpisodeList(anime)
|
||||
|
||||
if (episodeList.isNotEmpty()) {
|
||||
return episodeList.sortedWith(
|
||||
compareBy(
|
||||
{ it.name.split(SEASON_EPISODE_DELIMITER).first().parseAs<Int>() },
|
||||
{ it.name.split(SEASON_EPISODE_DELIMITER).last().parseAs<Int>() },
|
||||
),
|
||||
).reversed()
|
||||
} else {
|
||||
return listOf(
|
||||
SEpisode.create().apply {
|
||||
url = anime.url
|
||||
episode_number = 1.0F
|
||||
name = "movie"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request = GET("$apiBaseUrl/videoSeason/id/${anime.url}")
|
||||
|
||||
override fun episodeListParse(response: Response) = response.asModelList(SEpisodeDeserializer)
|
||||
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val extension = preferences.getString(PREF_SUBTITLE_EXT_KEY, PREF_SUBTITLE_EXT_DEFAULT)!!
|
||||
val language = preferences.getString(PREF_SUBTITLE_LANG_KEY, PREF_SUBTITLE_LANG_DEFAULT)!!
|
||||
val subs = this.client.newCall(GET("$apiBaseUrl/translationFiles/id/${episode.url}")).execute()
|
||||
.asModel(SubtitleDeserialize)
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.lang.split(SUBTITLE_DELIMITER).contains(extension) },
|
||||
{ it.lang.split(SUBTITLE_DELIMITER).contains(language) },
|
||||
),
|
||||
).reversed()
|
||||
|
||||
return super.getVideoList(episode).map {
|
||||
Video(url = it.url, quality = it.quality, videoUrl = it.videoUrl, subtitleTracks = subs)
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoListRequest(episode: SEpisode) = GET("$apiBaseUrl/transcoddedFiles/id/${episode.url}")
|
||||
|
||||
override fun videoListParse(response: Response) = response.asModelList(VideoDeserializer)
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private open class SingleSelectFilter(displayName: String, val vals: Array<Pair<String, Int>>, default: Int = 0) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), default) {
|
||||
fun getNameValue() = vals[state].first.lowercase()
|
||||
fun getNumberValue() = vals[state].second
|
||||
}
|
||||
|
||||
private open class MultipleSelectFilter(displayName: String, vals: Array<Pair<String, Int>>) :
|
||||
AnimeFilter.Group<CheckBoxFilter>(displayName, vals.map { CheckBoxFilter(it.first, false, it.second) }) {
|
||||
fun getSelectedIds(): List<Int> =
|
||||
this.state.filter { it.state }.map { it.value }
|
||||
fun hasSelected(): Boolean = this.state.any { it.state }
|
||||
}
|
||||
|
||||
private open class CheckBoxFilter(displayName: String, default: Boolean, val value: Int = 0) : AnimeFilter.CheckBox(displayName, default)
|
||||
|
||||
private open class YearFilter(displayName: String, years: Pair<YearTextFilter, YearTextFilter>) : AnimeFilter.Group<YearTextFilter>(
|
||||
displayName,
|
||||
years.toList(),
|
||||
) {
|
||||
fun getFormatted(): String = this.state.map {
|
||||
it.state.ifBlank { it.default }
|
||||
}.joinToString(",")
|
||||
}
|
||||
|
||||
private open class YearTextFilter(displayName: String, val default: String) : AnimeFilter.Text(displayName, default)
|
||||
|
||||
private open class BrowseResultSort(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, Pair<String, String>>>,
|
||||
val default: Selection = Selection(0, false),
|
||||
) : AnimeFilter.Sort(displayName, vals.map { it.first }.toTypedArray(), default) {
|
||||
fun getValue(): String {
|
||||
val currentState = state ?: default
|
||||
val sortKind = vals[currentState.index].second
|
||||
return if (currentState.ascending) {
|
||||
sortKind.first
|
||||
} else {
|
||||
sortKind.second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("Filter Search Result"),
|
||||
CheckBoxFilter(IS_BROWSING_FILTER_NAME, false),
|
||||
SingleSelectFilter(
|
||||
KIND_FILTER_NAME,
|
||||
KINDS_LIST,
|
||||
),
|
||||
MultipleSelectFilter(
|
||||
MAIN_CATEGORY_FILTER_NAME,
|
||||
arrayOf(
|
||||
Pair("Action", 84),
|
||||
Pair("Adventure", 56),
|
||||
Pair("Animation", 57),
|
||||
Pair("Comedy", 59),
|
||||
Pair("Crime", 60),
|
||||
Pair("Documentary", 61),
|
||||
Pair("Drama", 62),
|
||||
Pair("Fantasy", 67),
|
||||
Pair("Horror", 70),
|
||||
Pair("Mystery", 76),
|
||||
Pair("Romance", 77),
|
||||
Pair("Sci-Fi", 78),
|
||||
Pair("Sport", 79),
|
||||
Pair("Thriller", 80),
|
||||
Pair("Western", 89),
|
||||
),
|
||||
),
|
||||
MultipleSelectFilter(
|
||||
SUB_CATEGORY_FILTER_NAME,
|
||||
arrayOf(
|
||||
Pair("Biography", 58),
|
||||
Pair("Family", 65),
|
||||
Pair("History", 68),
|
||||
Pair("Musical", 75),
|
||||
Pair("War", 81),
|
||||
Pair("Supernatural", 87),
|
||||
Pair("Music", 88),
|
||||
Pair("Talk-Show", 90),
|
||||
Pair("Short", 97),
|
||||
Pair("Reality-TV", 101),
|
||||
Pair("Arabic dubbed", 102),
|
||||
Pair("News", 104),
|
||||
Pair("Ecchi", 105),
|
||||
Pair("Film-Noir", 106),
|
||||
Pair("Game", 111),
|
||||
Pair("Psychological", 112),
|
||||
Pair("Slice of Life", 113),
|
||||
Pair("Game-Show", 118),
|
||||
Pair("Magic", 123),
|
||||
Pair("Super Power", 124),
|
||||
Pair("Seinen", 125),
|
||||
Pair("Shounen", 126),
|
||||
Pair("School", 127),
|
||||
Pair("Sports", 128),
|
||||
Pair("Iraqi", 130),
|
||||
),
|
||||
),
|
||||
SingleSelectFilter(
|
||||
LANGUAGE_FILTER_NAME,
|
||||
arrayOf(
|
||||
Pair("", 0),
|
||||
Pair("English", 7),
|
||||
Pair("Arabic", 9),
|
||||
Pair("Hindi", 10),
|
||||
Pair("French", 11),
|
||||
Pair("German", SEARCH_ITEMS_PER_PAGE),
|
||||
Pair("Italian", 13),
|
||||
Pair("Spanish", 14),
|
||||
Pair("Chinese", 21),
|
||||
Pair("Japanese", 22),
|
||||
Pair("Korean", 23),
|
||||
Pair("Russian", LATEST_ITEMS_PER_PAGE),
|
||||
Pair("Turkish", 25),
|
||||
Pair("Norwegian", 26),
|
||||
Pair("Persian", 27),
|
||||
Pair("Swedish", 35),
|
||||
Pair("Hungary", 36),
|
||||
Pair("Polish", 38),
|
||||
Pair("Dutch", 39),
|
||||
Pair("Portuguese", 40),
|
||||
Pair("Indonesian", 41),
|
||||
Pair("Danish", 43),
|
||||
Pair("Romania", 44),
|
||||
Pair("Ukrainian", 48),
|
||||
Pair("Mandarin", 52),
|
||||
Pair("Catalan", 65),
|
||||
Pair("Filipino", 68),
|
||||
Pair("Hungarian", 76),
|
||||
Pair("Thai", 80),
|
||||
Pair("Croatian", 84),
|
||||
Pair(" Malay", 85),
|
||||
Pair("Finnish", 86),
|
||||
Pair("Vietnamese", 88),
|
||||
Pair("Zulu", 89),
|
||||
Pair("Taiwan", 47),
|
||||
Pair("Bulgarian", 95),
|
||||
Pair("Serbian", 97),
|
||||
Pair("Greek", 28),
|
||||
Pair("Finland", 37),
|
||||
Pair("Iran", 42),
|
||||
Pair("Hebrew", 46),
|
||||
Pair("Icelandic", 56),
|
||||
Pair("Georgian", 58),
|
||||
Pair("Pakistani", 61),
|
||||
Pair("Czeck", 72),
|
||||
Pair("Latvian", 87),
|
||||
Pair("Kazakh", 90),
|
||||
Pair("Estonian", 91),
|
||||
Pair("Quechua", 92),
|
||||
Pair("Multi Language", 93),
|
||||
Pair("Papiamento", 94),
|
||||
Pair("Albanian", 100),
|
||||
Pair("Slovenian", 103),
|
||||
Pair("Macedonian", 109),
|
||||
Pair("Kurdish", 112),
|
||||
Pair("Irish", 115),
|
||||
Pair("Afghani", 106),
|
||||
),
|
||||
),
|
||||
YearFilter(
|
||||
YEAR_FILTER_NAME,
|
||||
YearTextFilter("start", "1900") to
|
||||
YearTextFilter("end", Calendar.getInstance().get(Calendar.YEAR).toString()),
|
||||
),
|
||||
BrowseResultSort(
|
||||
BROWSE_RESULT_SORT_FILTER,
|
||||
arrayOf(
|
||||
Pair("Upload", Pair("asc", "desc")),
|
||||
Pair("Release", Pair("r_asc", "r_desc")),
|
||||
Pair("Name", Pair("title_asc", "title_desc")),
|
||||
Pair("View", Pair("views_asc", "views_desc")),
|
||||
Pair("Age Rating", Pair("rating_asc", "rating_desc")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_LATEST_KIND_KEY
|
||||
title = "Preferred Latest kind"
|
||||
entries = KINDS_LIST.map { it.first }.toTypedArray()
|
||||
entryValues = KINDS_LIST.map { it.first }.toTypedArray()
|
||||
setDefaultValue(PREF_LATEST_KIND_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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SUBTITLE_LANG_KEY
|
||||
title = "Preferred Subtitle Language"
|
||||
entries = LANG_LIST
|
||||
entryValues = LANG_LIST
|
||||
setDefaultValue(PREF_SUBTITLE_LANG_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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SUBTITLE_EXT_KEY
|
||||
title = "Preferred Subtitle Extension"
|
||||
entries = EXT_LIST
|
||||
entryValues = EXT_LIST
|
||||
setDefaultValue(PREF_SUBTITLE_EXT_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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Torrentio Anime (Torrent / Debrid)'
|
||||
extClass = '.Torrentio'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 15
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ fun anilistQuery() = """
|
|||
startDate_like: %year,
|
||||
seasonYear: %seasonYear,
|
||||
season: %season,
|
||||
format_in: %format
|
||||
format_in: %format,
|
||||
isAdult: false
|
||||
) {
|
||||
id
|
||||
title {
|
||||
|
@ -103,7 +104,7 @@ fun anilistLatestQuery() = """
|
|||
|
||||
fun getDetailsQuery() = """
|
||||
query media(%id: Int) {
|
||||
Media(id: %id) {
|
||||
Media(id: %id, isAdult: false) {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
|
@ -137,23 +138,3 @@ query media(%id: Int) {
|
|||
}
|
||||
}
|
||||
""".toQuery()
|
||||
|
||||
fun getEpisodeQuery() = """
|
||||
query media(%id: Int, %type: MediaType) {
|
||||
Media(id: %id, type: %type) {
|
||||
episodes
|
||||
nextAiringEpisode {
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
""".toQuery()
|
||||
|
||||
fun getMalIdQuery() = """
|
||||
query media(%id: Int, %type: MediaType) {
|
||||
Media(id: %id, type: %type) {
|
||||
idMal
|
||||
id
|
||||
}
|
||||
}
|
||||
""".toQuery()
|
||||
|
|
|
@ -10,10 +10,10 @@ import androidx.preference.ListPreference
|
|||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.AniZipResponse
|
||||
import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.AnilistMeta
|
||||
import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.AnilistMetaLatest
|
||||
import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.DetailsById
|
||||
import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.EpisodeList
|
||||
import eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto.StreamDataTorrent
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
@ -66,6 +66,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||
.add("query", query)
|
||||
.add("variables", variables)
|
||||
.build()
|
||||
|
||||
return POST("https://graphql.anilist.co", body = requestBody)
|
||||
}
|
||||
|
||||
|
@ -148,7 +149,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val jsonData = response.body.string()
|
||||
return parseSearchJson(jsonData) }
|
||||
return parseSearchJson(jsonData)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
|
@ -300,41 +302,55 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
return GET("https://anime-kitsu.strem.fun/meta/series/anilist%3A${anime.url}.json")
|
||||
return GET("https://api.ani.zip/mappings?anilist_id=${anime.url}")
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val responseString = response.body.string()
|
||||
val episodeList = json.decodeFromString<EpisodeList>(responseString)
|
||||
val aniZipResponse = json.decodeFromString<AniZipResponse>(responseString)
|
||||
|
||||
return when (episodeList.meta?.type) {
|
||||
"series" -> {
|
||||
episodeList.meta.videos
|
||||
?.let { videos ->
|
||||
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
|
||||
return when (aniZipResponse.mappings?.type) {
|
||||
"TV" -> {
|
||||
aniZipResponse.episodes
|
||||
?.let { episodes ->
|
||||
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) {
|
||||
episodes
|
||||
} else {
|
||||
episodes.filter { (_, episode) -> (episode?.airDate?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() }
|
||||
}
|
||||
?.map { video ->
|
||||
}
|
||||
?.mapNotNull { (_, episode) ->
|
||||
val episodeNumber = runCatching { episode?.episode?.toFloat() }.getOrNull()
|
||||
|
||||
if (episodeNumber == null) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val title = episode?.title?.get("en")
|
||||
|
||||
SEpisode.create().apply {
|
||||
episode_number = video.episode?.toFloat() ?: 0.0F
|
||||
url = "/stream/series/${video.videoId}.json"
|
||||
date_upload = video.released?.let { parseDate(it) } ?: 0L
|
||||
name = "Episode ${video.episode} : ${
|
||||
video.title?.removePrefix("Episode ")
|
||||
?.replaceFirst("\\d+\\s*".toRegex(), "")
|
||||
?.trim()
|
||||
}"
|
||||
scanlator = (video.released?.let { parseDate(it) } ?: 0L).takeIf { it > System.currentTimeMillis() }?.let { "Upcoming" } ?: ""
|
||||
episode_number = episodeNumber
|
||||
url = "/stream/series/kitsu:${aniZipResponse.mappings.kitsuId}:${String.format(Locale.ENGLISH, "%.0f", episodeNumber)}.json"
|
||||
date_upload = episode?.airDate?.let { parseDate(it) } ?: 0L
|
||||
name = if (title == null) "Episode ${episode?.episode}" else "Episode ${episode.episode}: $title"
|
||||
scanlator = (episode?.airDate?.let { parseDate(it) } ?: 0L).takeIf { it > System.currentTimeMillis() }?.let { "Upcoming" } ?: ""
|
||||
}
|
||||
}.orEmpty().reversed()
|
||||
}
|
||||
|
||||
"movie" -> {
|
||||
// Handle movie response
|
||||
"MOVIE" -> {
|
||||
val dateUpload = if (!aniZipResponse.episodes.isNullOrEmpty()) {
|
||||
aniZipResponse.episodes["1"]?.airDate?.let { parseDate(it) } ?: 0L
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
|
||||
listOf(
|
||||
SEpisode.create().apply {
|
||||
episode_number = 1.0F
|
||||
url = "/stream/movie/${episodeList.meta.kitsuId}.json"
|
||||
url = "/stream/movie/kitsu:${aniZipResponse.mappings.kitsuId}.json"
|
||||
name = "Movie"
|
||||
date_upload = dateUpload
|
||||
},
|
||||
).reversed()
|
||||
}
|
||||
|
@ -342,6 +358,12 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDateTime(dateStr: String): Long {
|
||||
return runCatching { DATE_TIME_FORMATTER.parse(dateStr)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
|
@ -421,6 +443,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||
udp://www.torrent.eu.org:451/announce,
|
||||
${fetchTrackers().split("\n").joinToString(",")}
|
||||
""".trimIndent()
|
||||
|
||||
return streamList.streams?.map { stream ->
|
||||
val urlOrHash =
|
||||
if (debridProvider == "none") {
|
||||
|
@ -875,8 +898,12 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||
private const val IS_EFFICIENT_KEY = "efficient"
|
||||
private const val IS_EFFICIENT_DEFAULT = false
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
private val DATE_TIME_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.torrentioanime.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AniZipResponse(
|
||||
val titles: Map<String, String?>? = null,
|
||||
val episodes: Map<String, AniZipEpisode?>? = null,
|
||||
val episodeCount: Int? = null,
|
||||
val specialCount: Int? = null,
|
||||
val images: List<AniZipImage?>? = null,
|
||||
val mappings: AniZipMappings? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AniZipEpisode(
|
||||
val episode: String? = null,
|
||||
val episodeNumber: Int? = null,
|
||||
val absoluteEpisodeNumber: Int? = null,
|
||||
val seasonNumber: Int? = null,
|
||||
val title: Map<String, String?>? = null,
|
||||
val length: Int? = null,
|
||||
val runtime: Int? = null,
|
||||
@SerialName("airdate")
|
||||
val airDate: String? = null,
|
||||
val rating: String? = null,
|
||||
@SerialName("anidbEid")
|
||||
val aniDbEpisodeId: Long? = null,
|
||||
val tvdbShowId: Long? = null,
|
||||
val tvdbId: Long? = null,
|
||||
val overview: String? = null,
|
||||
val image: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AniZipImage(
|
||||
val coverType: String? = null,
|
||||
val url: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AniZipMappings(
|
||||
@SerialName("animeplanet_id")
|
||||
val animePlanetId: String? = null,
|
||||
@SerialName("kitsu_id")
|
||||
val kitsuId: Long? = null,
|
||||
@SerialName("mal_id")
|
||||
val myAnimeListId: Long? = null,
|
||||
val type: String? = null,
|
||||
@SerialName("anilist_id")
|
||||
val aniListId: Long? = null,
|
||||
@SerialName("anisearch_id")
|
||||
val aniSearchId: Long? = null,
|
||||
@SerialName("anidb_id")
|
||||
val aniDbId: Long? = null,
|
||||
@SerialName("notifymoe_id")
|
||||
val notifyMoeId: String? = null,
|
||||
@SerialName("livechart_id")
|
||||
val liveChartId: Long? = null,
|
||||
@SerialName("thetvdb_id")
|
||||
val theTvDbId: Long? = null,
|
||||
@SerialName("imdb_id")
|
||||
val imdbId: String? = null,
|
||||
@SerialName("themoviedb_id")
|
||||
val theMovieDbId: String? = null,
|
||||
)
|
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 842 B |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 448 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 8.8 KiB |