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:
parent
9f385108fc
commit
1384df62f3
350 changed files with 12176 additions and 1064 deletions
22
src/all/debridindex/AndroidManifest.xml
Normal file
22
src/all/debridindex/AndroidManifest.xml
Normal 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>
|
8
src/all/debridindex/build.gradle
Normal file
8
src/all/debridindex/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Debrid Index'
|
||||
extClass = '.DebridIndex'
|
||||
extVersionCode = 1
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/debridindex/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/debridindex/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
src/all/debridindex/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/debridindex/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
src/all/debridindex/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/debridindex/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
BIN
src/all/debridindex/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/debridindex/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
BIN
src/all/debridindex/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/debridindex/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hikari'
|
||||
extClass = '.Hikari'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'JavGG'
|
||||
extClass = '.Javgg'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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") -> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Jav Guru'
|
||||
extClass = '.JavGuru'
|
||||
extVersionCode = 19
|
||||
extVersionCode = 24
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Sudatchi'
|
||||
extClass = '.Sudatchi'
|
||||
extVersionCode = 5
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ data class SubtitleLangDto(
|
|||
data class SubtitleDto(
|
||||
val url: String,
|
||||
@SerialName("SubtitlesName")
|
||||
val subtitlesName: SubtitleLangDto,
|
||||
val SubtitlesName: SubtitleLangDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Torrentio (Torrent / Debrid)'
|
||||
extClass = '.Torrentio'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 5
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Torrentio Anime (Torrent / Debrid)'
|
||||
extClass = '.Torrentio'
|
||||
extVersionCode = 11
|
||||
extVersionCode = 14
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue