Merged with dark25 (#636)

* merge

merged lib, lib-multisrc, all, ar, de, en, es, fr, hi, id, it, pt, tr src from dark25

* patch
This commit is contained in:
Hak 2025-02-10 15:41:59 +07:00 committed by GitHub
parent 9f385108fc
commit 1384df62f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
350 changed files with 12176 additions and 1064 deletions

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.debridindex.DebirdIndexUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="torrentio.strem.fun"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,8 @@
ext {
extName = 'Debrid Index'
extClass = '.DebridIndex'
extVersionCode = 1
containsNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.animeextension.all.debridindex
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://torrentio.strem.fun/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class DebirdIndexUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${DebridIndex.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,208 @@
package eu.kanade.tachiyomi.animeextension.all.debridindex
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animeextension.all.debridindex.dto.RootFiles
import eu.kanade.tachiyomi.animeextension.all.debridindex.dto.SubFiles
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
class DebridIndex : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Debrid Index"
override val baseUrl = "https://torrentio.strem.fun"
override val lang = "all"
override val supportsLatest = false
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val tokenKey = preferences.getString(PREF_TOKEN_KEY, null)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "RealDebrid")
when {
tokenKey.isNullOrBlank() -> throw Exception("Please enter the token in extension settings.")
else -> {
return GET("$baseUrl/${debridProvider!!.lowercase()}=$tokenKey/catalog/other/torrentio-${debridProvider.lowercase()}.json")
}
}
}
override fun popularAnimeParse(response: Response): AnimesPage {
val animeList = json.decodeFromString<RootFiles>(response.body.string()).metas?.map { meta ->
SAnime.create().apply {
title = meta.name
url = meta.id
thumbnail_url = if (meta.name == "Downloads") {
"https://i.ibb.co/MGmhmJg/download.png"
} else {
"https://i.ibb.co/Q9GPtbC/default.png"
}
}
} ?: emptyList()
return AnimesPage(animeList, false)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("Not used")
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val tokenKey = preferences.getString(PREF_TOKEN_KEY, null)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "RealDebrid")
when {
tokenKey.isNullOrBlank() -> throw Exception("Please enter the token in extension settings.")
else -> {
// Used Debrid Search v0.1.8 https://68d69db7dc40-debrid-search.baby-beamup.club/configure
return GET("https://68d69db7dc40-debrid-search.baby-beamup.club/%7B%22DebridProvider%22%3A%22$debridProvider%22%2C%22DebridApiKey%22%3A%22$tokenKey%22%7D/catalog/other/debridsearch/search=$query.json")
}
}
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
return anime
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val tokenKey = preferences.getString(PREF_TOKEN_KEY, null)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "RealDebrid")
return GET("$baseUrl/${debridProvider!!.lowercase()}=$tokenKey/meta/other/${anime.url}.json")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val jsonData = response.body.string()
return json.decodeFromString<SubFiles>(jsonData).meta?.videos?.mapIndexed { index, video ->
SEpisode.create().apply {
episode_number = (index + 1).toFloat()
name = if (preferences.getBoolean(IS_FILENAME_KEY, IS_FILENAME_DEFAULT)) {
video.title.trim().split('/').last()
} else {
video.title.trim()
.replace("[", "(")
.replace(Regex("]"), ")")
.replace("/", "\uD83D\uDCC2 ")
}
url = video.streams.firstOrNull()?.url.orEmpty()
date_upload = parseDate(video.released)
}
}?.reversed() ?: emptyList()
}
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
override suspend fun getVideoList(episode: SEpisode): List<Video> {
return listOf(Video(episode.url, episode.name.split("/").last(), episode.url))
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Debrid provider
ListPreference(screen.context).apply {
key = PREF_DEBRID_KEY
title = "Debird Provider"
entries = PREF_DEBRID_ENTRIES
entryValues = PREF_DEBRID_VALUES
setDefaultValue("realdebrid")
summary = "Don't forget to enter your token key."
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)
// Token
EditTextPreference(screen.context).apply {
key = PREF_TOKEN_KEY
title = "Real Debrid Token"
setDefaultValue(PREF_TOKEN_DEFAULT)
summary = PREF_TOKEN_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = (newValue as String).trim().ifBlank { PREF_TOKEN_DEFAULT }
Toast.makeText(screen.context, "Restart app to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = IS_FILENAME_KEY
title = "Only display filename"
setDefaultValue(IS_FILENAME_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
summary = "Will note display full path of episode."
}.also(screen::addPreference)
}
companion object {
const val PREFIX_SEARCH = "id:"
// Token
private const val PREF_TOKEN_KEY = "token"
private const val PREF_TOKEN_DEFAULT = "none"
private const val PREF_TOKEN_SUMMARY = "For temporary uses. Updating the extension will erase this setting."
// Debird
private const val PREF_DEBRID_KEY = "debrid_provider"
private val PREF_DEBRID_ENTRIES = arrayOf(
"RealDebrid",
"Premiumize",
"AllDebrid",
"DebridLink",
)
private val PREF_DEBRID_VALUES = arrayOf(
"RealDebrid",
"Premiumize",
"AllDebrid",
"DebridLink",
)
private const val IS_FILENAME_KEY = "filename"
private const val IS_FILENAME_DEFAULT = false
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
}
}

View file

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.animeextension.all.debridindex.dto
import kotlinx.serialization.Serializable
// Root
@Serializable
data class RootFiles(
val metas: List<Meta>? = null,
)
@Serializable
data class SubFiles(
val meta: Meta? = null,
)
@Serializable
data class Meta(
val id: String,
val type: String,
val name: String,
val videos: List<Video>? = null,
)
@Serializable
data class Video(
val id: String,
val title: String,
val released: String,
val streams: List<Stream>,
)
@Serializable
data class Stream(
val url: String,
)

View file

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

View file

@ -208,11 +208,17 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override fun episodeListSelector() = "a[class~=ep-item]"
override fun episodeFromElement(element: Element): SEpisode {
val ep = element.selectFirst(".ssli-order")!!.text()
val epText = element.selectFirst(".ssli-order")?.text()?.trim()
?: element.attr("data-number").trim()
val ep = epText.toFloatOrNull() ?: 0F
val nameText = element.selectFirst(".ep-name")?.text()?.trim()
?: element.attr("title").replace("Episode-", "Ep. ") ?: ""
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
episode_number = ep.toFloat()
name = "Ep. $ep - ${element.selectFirst(".ep-name")?.text() ?: ""}"
episode_number = ep
name = "Ep. $ep - $nameText"
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'JavGG'
extClass = '.Javgg'
extVersionCode = 3
extVersionCode = 4
isNsfw = true
}

View file

@ -154,8 +154,7 @@ class Javgg : ConfigurableAnimeSource, AnimeHttpSource() {
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
}
embedUrl.contains("vidhide") || embedUrl.contains("streamhide") ||
embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideVidExtractor(client).videosFromUrl(url)
embedUrl.contains("vidhide") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideVidExtractor(client, headers).videosFromUrl(url)
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
embedUrl.contains("turboplay") -> {

View file

@ -1,7 +1,7 @@
ext {
extName = 'Jav Guru'
extClass = '.JavGuru'
extVersionCode = 19
extVersionCode = 24
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Sudatchi'
extClass = '.Sudatchi'
extVersionCode = 5
extVersionCode = 10
isNsfw = true
}

View file

@ -59,7 +59,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
private fun Int.parseStatus() = when (this) {
1 -> SAnime.UNKNOWN // Not Yet Released
1 -> SAnime.LICENSED // Not Yet Released
2 -> SAnime.ONGOING
3 -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
@ -86,7 +86,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
val titleLang = preferences.title
val document = response.asJsoup()
val data = document.parseAs<HomePageDto>().animeSpotlight
return AnimesPage(data.map { it.toSAnime(titleLang) }, false)
return AnimesPage(data.map { it.toSAnime(titleLang) }.filterNot { it.status == SAnime.LICENSED }, false)
}
// =============================== Latest ===============================
@ -96,7 +96,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
sudatchiFilters.fetchFilters()
val titleLang = preferences.title
return response.parseAs<DirectoryDto>().let {
AnimesPage(it.animes.map { it.toSAnime(titleLang) }, it.page != it.pages)
AnimesPage(it.animes.map { it.toSAnime(titleLang) }.filterNot { it.status == SAnime.LICENSED }, it.page != it.pages)
}
}
@ -176,7 +176,13 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
videoUrl,
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
subtitleList = subtitles.map {
Track("$ipfsUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
Track(
when {
it.url.startsWith("/ipfs") -> "$ipfsUrl${it.url}"
else -> "$baseUrl${it.url}"
},
"${it.SubtitlesName.name} (${it.SubtitlesName.language})",
)
}.sort(),
)
}

View file

@ -79,7 +79,7 @@ data class SubtitleLangDto(
data class SubtitleDto(
val url: String,
@SerialName("SubtitlesName")
val subtitlesName: SubtitleLangDto,
val SubtitlesName: SubtitleLangDto,
)
@Serializable

View file

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

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
@ -60,7 +59,12 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
{"query": "${query.replace("\n", "")}", "variables": $variables}
""".trimIndent().toRequestBody("application/json; charset=utf-8".toMediaType())
return POST("https://apis.justwatch.com/graphql", headers = headers, body = requestBody)
val request = Request.Builder()
.url("https://apis.justwatch.com/graphql")
.post(requestBody)
.build()
return request
}
// ============================== JustWatch Api Query ======================
@ -135,7 +139,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
val content = node.content ?: return@mapNotNull null
SAnime.create().apply {
url = "${content.externalIds?.imdbId ?: ""},${node.objectType ?: ""},${content.fullPath ?: ""}"
url = "${content.externalIds?.imdbId ?: ""},${if (node.objectType == "SHOW") "series" else node.objectType ?: ""},${content.fullPath ?: ""}"
title = content.title ?: ""
thumbnail_url = "https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}"
description = content.shortDescription ?: ""
@ -155,7 +159,31 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
return searchAnimeRequest(page, "", AnimeFilterList())
val country = preferences.getString(PREF_REGION_KEY, PREF_REGION_DEFAULT)
val language = preferences.getString(PREF_JW_LANG_KEY, PREF_JW_LANG_DEFAULT)
val perPage = 40
val packages = ""
val year = 0
val objectTypes = ""
val variables = """
{
"first": $perPage,
"offset": ${(page - 1) * perPage},
"platform": "WEB",
"country": "$country",
"language": "$language",
"searchQuery": "",
"packages": [$packages],
"objectTypes": [$objectTypes],
"popularTitlesSortBy": "TRENDING",
"releaseYear": {
"min": $year,
"max": $year
}
}
""".trimIndent()
return makeGraphQLRequest(justWatchQuery(), variables)
}
override fun popularAnimeParse(response: Response): AnimesPage {
@ -171,7 +199,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/anime/$id", headers))
client.newCall(GET("$baseUrl/anime/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
@ -198,7 +226,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"platform": "WEB",
"country": "$country",
"language": "$language",
"searchQuery": "${query.replace(searchQueryRegex, "").trim()}",
"searchQuery": "${query.replace(Regex("[^A-Za-z0-9 ]"), "").trim()}",
"packages": [$packages],
"objectTypes": [$objectTypes],
"popularTitlesSortBy": "TRENDING",
@ -212,10 +240,6 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
return makeGraphQLRequest(justWatchQuery(), variables)
}
private val searchQueryRegex by lazy {
Regex("[^A-Za-z0-9 ]")
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// =========================== Anime Details ============================
@ -288,18 +312,18 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
val responseString = response.body.string()
val episodeList = json.decodeFromString<EpisodeList>(responseString)
return when (episodeList.meta?.type) {
"show" -> {
"series" -> {
episodeList.meta.videos
?.let { videos ->
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.firstAired?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
}
?.map { video ->
SEpisode.create().apply {
episode_number = "${video.season}.${video.number}".toFloat()
url = "/stream/series/${video.id}.json"
date_upload = video.firstAired?.let { parseDate(it) } ?: 0L
name = "S${video.season.toString().trim()}:E${video.number} - ${video.name}"
scanlator = (video.firstAired?.let { parseDate(it) } ?: 0L)
date_upload = video.released?.let { parseDate(it) } ?: 0L
name = "S${video.season.toString().trim()}:E${video.number} - ${video.title}"
scanlator = (video.released?.let { parseDate(it) } ?: 0L)
.takeIf { it > System.currentTimeMillis() }
?.let { "Upcoming" }
?: ""
@ -402,7 +426,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
udp://tracker.tiny-vps.com:6969/announce,
udp://tracker.torrent.eu.org:451/announce,
udp://valakas.rollo.dnsabr.com:2710/announce,
udp://www.torrent.eu.org:451/announce
udp://www.torrent.eu.org:451/announce,
${fetchTrackers().split("\n").joinToString(",")}
""".trimIndent()
return streamList.streams?.map { stream ->
@ -428,6 +453,17 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
)
}
private fun fetchTrackers(): String {
val request = Request.Builder()
.url("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("Unexpected code $response")
return response.body.string().trim()
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Debrid provider
ListPreference(screen.context).apply {
@ -652,7 +688,10 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"🇫🇷 Torrent9",
"🇪🇸 MejorTorrent",
"🇲🇽 Cinecalidad",
"🇮🇹 ilCorsaroNero",
"🇪🇸 Wolfmax4k",
)
private val PREF_PROVIDERS_VALUE = arrayOf(
"yts",
"eztv",
@ -673,6 +712,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"torrent9",
"mejortorrent",
"cinecalidad",
"ilcorsaronero",
"wolfmax4k",
)
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
@ -691,12 +732,15 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
)
private val PREF_PROVIDERS_DEFAULT = PREF_DEFAULT_PROVIDERS_VALUE.toSet()
// Qualities/Resolutions
// / Qualities/Resolutions
private const val PREF_QUALITY_KEY = "quality_selection"
private val PREF_QUALITY = arrayOf(
"BluRay REMUX",
"HDR/HDR10+/Dolby Vision",
"Dolby Vision",
"Dolby Vision + HDR",
"3D",
"Non 3D (DO NOT SELECT IF NOT SURE)",
"4k",
"1080p",
"720p",
@ -706,10 +750,14 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"Cam",
"Unknown",
)
private val PREF_QUALITY_VALUE = arrayOf(
"brremux",
"hdrall",
"dolbyvision",
"dolbyvisionwithhdr",
"threed",
"nonthreed",
"4k",
"1080p",
"720p",
@ -832,7 +880,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
private val PREF_LANG_DEFAULT = setOf<String>()
private const val UPCOMING_EP_KEY = "upcoming_ep"
private const val UPCOMING_EP_DEFAULT = true
private const val UPCOMING_EP_DEFAULT = false
private const val IS_DUB_KEY = "dubbed"
private const val IS_DUB_DEFAULT = false

View file

@ -110,6 +110,6 @@ class EpisodeVideo(
val id: String? = null,
val season: Int? = null,
val number: Int? = null,
val firstAired: String? = null,
val name: String? = null,
val released: String? = null,
val title: String? = null,
)

View file

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

View file

@ -0,0 +1,155 @@
package eu.kanade.tachiyomi.animeextension.all.torrentioanime
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniListFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckboxList(
options: Array<Pair<String, String>>,
): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkBox -> options.find { it.first == checkBox.name }!!.second }
.filter(String::isNotBlank)
}
private inline fun <reified R> AnimeFilterList.getSort(): String {
val state = (getFirst<R>() as AnimeFilter.Sort).state ?: return ""
val index = state.index
val suffix = if (state.ascending) "" else "_DESC"
return AniListFiltersData.SORT_LIST[index].second + suffix
}
class GenreFilter : CheckBoxFilterList("Genres", AniListFiltersData.GENRE_LIST)
class YearFilter : QueryPartFilter("Year", AniListFiltersData.YEAR_LIST)
class SeasonFilter : QueryPartFilter("Season", AniListFiltersData.SEASON_LIST)
class FormatFilter : CheckBoxFilterList("Format", AniListFiltersData.FORMAT_LIST)
class StatusFilter : QueryPartFilter("Airing Status", AniListFiltersData.STATUS_LIST)
class SortFilter : AnimeFilter.Sort(
"Sort",
AniListFiltersData.SORT_LIST.map { it.first }.toTypedArray(),
Selection(1, false),
)
val FILTER_LIST get() = AnimeFilterList(
SortFilter(),
FormatFilter(),
GenreFilter(),
YearFilter(),
SeasonFilter(),
StatusFilter(),
)
class FilterSearchParams(
val sort: String = "",
val format: List<String> = emptyList(),
val genres: List<String> = emptyList(),
val year: String = "",
val season: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.getSort<SortFilter>(),
filters.parseCheckboxList<FormatFilter>(AniListFiltersData.FORMAT_LIST),
filters.parseCheckboxList<GenreFilter>(AniListFiltersData.GENRE_LIST),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<StatusFilter>(),
)
}
private object AniListFiltersData {
val GENRE_LIST = arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mahou Shoujo", "Mahou Shoujo"),
Pair("Mecha", "Mecha"),
Pair("Music", "Music"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Slice of Life", "Slice of Life"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
)
val YEAR_LIST: Array<Pair<String, String>> = arrayOf(
Pair("Any", ""),
) + (1940..2025).reversed().map { Pair(it.toString(), it.toString()) }.toTypedArray()
val SEASON_LIST = arrayOf(
Pair("Any", ""),
Pair("Winter", "WINTER"),
Pair("Spring", "SPRING"),
Pair("Summer", "SUMMER"),
Pair("Fall", "FALL"),
)
val FORMAT_LIST = arrayOf(
Pair("Any", ""),
Pair("TV Show", "TV"),
Pair("Movie", "MOVIE"),
Pair("TV Short", "TV_SHORT"),
Pair("Special", "SPECIAL"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Music", "MUSIC"),
)
val STATUS_LIST = arrayOf(
Pair("Any", ""),
Pair("Airing", "RELEASING"),
Pair("Finished", "FINISHED"),
Pair("Not Yet Aired", "NOT_YET_RELEASED"),
Pair("Cancelled", "CANCELLED"),
)
val SORT_LIST = arrayOf(
Pair("Title", "TITLE_ENGLISH"),
Pair("Popularity", "POPULARITY"),
Pair("Average Score", "SCORE"),
Pair("Trending", "TRENDING"),
Pair("Favorites", "FAVOURITES"),
Pair("Date Added", "ID"),
Pair("Release Date", "START_DATE"),
)
}
}

View file

@ -0,0 +1,159 @@
package eu.kanade.tachiyomi.animeextension.all.torrentioanime
private fun String.toQuery() = this.trimIndent().replace("%", "$")
fun anilistQuery() = """
query (
%page: Int,
%perPage: Int,
%sort: [MediaSort],
%search: String,
%genres: [String],
%year: String,
%seasonYear: Int,
%season: MediaSeason,
%format: [MediaFormat],
%status: [MediaStatus],
) {
Page(page: %page, perPage: %perPage) {
pageInfo {
currentPage
hasNextPage
}
media(
type: ANIME,
sort: %sort,
search: %search,
status_in: %status,
genre_in: %genres,
startDate_like: %year,
seasonYear: %seasonYear,
season: %season,
format_in: %format
) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
""".toQuery()
fun anilistLatestQuery() = """
query (%page: Int, %perPage: Int, %sort: [AiringSort]) {
Page(page: %page, perPage: %perPage) {
pageInfo {
currentPage
hasNextPage
}
airingSchedules(
airingAt_greater: 0,
airingAt_lesser: ${System.currentTimeMillis() / 1000 - 10000},
sort: %sort
) {
media {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
""".toQuery()
fun getDetailsQuery() = """
query media(%id: Int) {
Media(id: %id) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
medium
}
description
season
seasonYear
format
status
genres
episodes
format
countryOfOrigin
isAdult
tags{
name
}
studios {
nodes {
id
name
}
}
}
}
""".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

@ -25,15 +25,19 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.json.JSONObject
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
@ -62,93 +66,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
.add("query", query)
.add("variables", variables)
.build()
return POST("https://graphql.anilist.co", body = requestBody)
}
// ============================== Anilist Meta List ======================
private fun anilistQuery(): String {
return """
query (${"$"}page: Int, ${"$"}perPage: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) {
Page(page: ${"$"}page, perPage: ${"$"}perPage) {
pageInfo{
currentPage
hasNextPage
}
media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in:[RELEASING,FINISHED,NOT_YET_RELEASED]) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags{
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
""".trimIndent()
}
private fun anilistLatestQuery(): String {
return """
query (${"$"}page: Int, ${"$"}perPage: Int, ${"$"}sort: [AiringSort]) {
Page(page: ${"$"}page, perPage: ${"$"}perPage) {
pageInfo {
currentPage
hasNextPage
}
airingSchedules(
airingAt_greater: 0
airingAt_lesser: ${System.currentTimeMillis() / 1000 - 10000}
sort: ${"$"}sort
) {
media{
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags{
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
""".trimIndent()
}
private fun parseSearchJson(jsonLine: String?, isLatestQuery: Boolean = false): AnimesPage {
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
val metaData: Any = if (!isLatestQuery) {
@ -218,7 +138,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
{
"page": $page,
"perPage": 30,
"sort": "TRENDING_DESC"
"sort": "TRENDING_DESC",
"status": ["FINISHED", "RELEASING"]
}
""".trimIndent()
@ -227,8 +148,7 @@ 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 {
@ -261,67 +181,73 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
val details = animeDetailsParse(response)
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val variables = """
{
"page": $page,
"perPage": 30,
"sort": "POPULARITY_DESC",
"search": "$query"
val params = AniListFilters.getSearchParameters(filters)
val variablesObject = buildJsonObject {
put("page", page)
put("perPage", 30)
put("sort", params.sort)
if (query.isNotBlank()) put("search", query)
if (params.genres.isNotEmpty()) {
putJsonArray("genres") {
params.genres.forEach { add(it) }
}
}
""".trimIndent()
if (params.format.isNotEmpty()) {
putJsonArray("format") {
params.format.forEach { add(it) }
}
}
if (params.season.isBlank() && params.year.isNotBlank()) {
put("year", "${params.year}%")
}
if (params.season.isNotBlank() && params.year.isBlank()) {
throw Exception("Year cannot be blank if season is set")
}
if (params.season.isNotBlank() && params.year.isNotBlank()) {
put("season", params.season)
put("seasonYear", params.year)
}
if (params.status.isNotBlank()) {
putJsonArray("status") {
params.status.forEach { add(it.toString()) }
}
}
}
val variables = json.encodeToString(variablesObject)
println(anilistQuery())
println(variables)
return makeGraphQLRequest(anilistQuery(), variables)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AniListFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
val query = """
query(${"$"}id: Int){
Media(id: ${"$"}id){
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags{
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
""".trimIndent()
val variables = """{"id": ${anime.url}}"""
val metaData = runCatching {
json.decodeFromString<DetailsById>(client.newCall(makeGraphQLRequest(query, variables)).execute().body.string())
json.decodeFromString<DetailsById>(client.newCall(makeGraphQLRequest(getDetailsQuery(), variables)).execute().body.string())
}.getOrNull()?.data?.media
anime.title = metaData?.title?.let { title ->
@ -334,10 +260,24 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
} ?: ""
anime.thumbnail_url = metaData?.coverImage?.extraLarge
anime.description = metaData?.description
?.replace(Regex("<br><br>"), "\n")
?.replace(Regex("<.*?>"), "")
?: "No Description"
anime.description = buildString {
append(
metaData?.description?.let {
Jsoup.parseBodyFragment(
it.replace("<br>\n", "br2n")
.replace("<br>", "br2n")
.replace("\n", "br2n"),
).text().replace("br2n", "\n")
},
)
append("\n\n")
if (!(metaData?.season == null && metaData?.seasonYear == null)) {
append("Release: ${ metaData.season ?: ""} ${ metaData.seasonYear ?: ""}")
}
metaData?.format?.let { append("\nType: ${metaData.format}") }
metaData?.episodes?.let { append("\nTotal Episode Count: ${metaData.episodes}") }
}.trim()
anime.status = when (metaData?.status) {
"RELEASING" -> SAnime.ONGOING
@ -360,9 +300,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val res = URL("https://api.ani.zip/mappings?anilist_id=${anime.url}").readText()
val kitsuId = JSONObject(res).getJSONObject("mappings").getInt("kitsu_id").toString()
return GET("https://anime-kitsu.strem.fun/meta/series/kitsu%3A$kitsuId.json")
return GET("https://anime-kitsu.strem.fun/meta/series/anilist%3A${anime.url}.json")
}
override fun episodeListParse(response: Response): List<SEpisode> {
@ -375,7 +313,6 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
?.let { videos ->
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
}
?.filter { it.thumbnail != null }
?.map { video ->
SEpisode.create().apply {
episode_number = video.episode?.toFloat() ?: 0.0F
@ -481,9 +418,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
udp://tracker.tiny-vps.com:6969/announce,
udp://tracker.torrent.eu.org:451/announce,
udp://valakas.rollo.dnsabr.com:2710/announce,
udp://www.torrent.eu.org:451/announce
udp://www.torrent.eu.org:451/announce,
${fetchTrackers().split("\n").joinToString(",")}
""".trimIndent()
return streamList.streams?.map { stream ->
val urlOrHash =
if (debridProvider == "none") {
@ -509,6 +446,17 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
)
}
private fun fetchTrackers(): String {
val request = Request.Builder()
.url("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("Unexpected code $response")
return response.body.string().trim()
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Debrid provider
ListPreference(screen.context).apply {
@ -714,7 +662,10 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"🇫🇷 Torrent9",
"🇪🇸 MejorTorrent",
"🇲🇽 Cinecalidad",
"🇮🇹 ilCorsaroNero",
"🇪🇸 Wolfmax4k",
)
private val PREF_PROVIDERS_VALUE = arrayOf(
"yts",
"eztv",
@ -735,6 +686,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"torrent9",
"mejortorrent",
"cinecalidad",
"ilcorsaronero",
"wolfmax4k",
)
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
@ -759,6 +712,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"BluRay REMUX",
"HDR/HDR10+/Dolby Vision",
"Dolby Vision",
"Dolby Vision + HDR",
"3D",
"Non 3D (DO NOT SELECT IF NOT SURE)",
"4k",
"1080p",
"720p",
@ -768,10 +724,14 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"Cam",
"Unknown",
)
private val PREF_QUALITY_VALUE = arrayOf(
"brremux",
"hdrall",
"dolbyvision",
"dolbyvisionwithhdr",
"threed",
"nonthreed",
"4k",
"1080p",
"720p",

View file

@ -73,7 +73,11 @@ data class AnilistMedia(
val status: String? = null,
val tags: List<AnilistTag>? = null,
val genres: List<String>? = null,
val episodes: Int? = null,
val format: String? = null,
val studios: AnilistStudios? = null,
val season: String? = null,
val seasonYear: Int? = null,
val countryOfOrigin: String? = null,
val isAdult: Boolean = false,
)