Merge branch 'Kohi-den:main' into fix-lycoris

This commit is contained in:
Cezary 2025-03-11 01:31:29 +01:00 committed by GitHub
commit eb6728a83b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
479 changed files with 3626 additions and 9676 deletions

View file

@ -0,0 +1,7 @@
plugins {
id("lib-android")
}
dependencies {
implementation(project(":lib:playlist-utils"))
}

View file

@ -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))
}
}
}

View file

@ -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]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

View file

@ -1,11 +1,11 @@
ext {
extName = 'AnimeWorld India'
extClass = '.AnimeWorldIndiaFactory'
extVersionCode = 14
extVersionCode = 15
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:playlist-utils"))
}
}

View file

@ -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

View file

@ -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"),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View file

@ -0,0 +1,11 @@
ext {
extName = 'ShabakatyCinemana'
extClass = '.ShabakatyCinemana'
extVersionCode = 2
isNsfw = false
}
apply from: "$rootDir/common.gradle"
dependencies {
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -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)
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Torrentio Anime (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 14
extVersionCode = 15
containsNsfw = false
}

View file

@ -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()

View file

@ -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)
}
}
}

View file

@ -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,
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Some files were not shown because too many files have changed in this diff Show more