Merge branch 'Kohi-den:main' into main
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -117,7 +117,11 @@ abstract class AnimeStream(
|
|||
}
|
||||
|
||||
protected open fun searchAnimeByPathParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.asJsoup())
|
||||
val details = animeDetailsParse(response.asJsoup()).apply {
|
||||
setUrlWithoutDomain(response.request.url.toString())
|
||||
initialized = true
|
||||
}
|
||||
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
baseVersionCode = 2
|
||||
|
|
|
@ -155,7 +155,11 @@ abstract class DooPlay(
|
|||
// =============================== Search ===============================
|
||||
|
||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response)
|
||||
val details = animeDetailsParse(response).apply {
|
||||
setUrlWithoutDomain(response.request.url.toString())
|
||||
initialized = true
|
||||
}
|
||||
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
|
@ -51,6 +52,7 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||
val subtitleList = buildList {
|
||||
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
||||
subtitles.forEach {
|
||||
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
|
||||
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,36 +5,50 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
|||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.URI
|
||||
|
||||
class DoodExtractor(private val client: OkHttpClient) {
|
||||
|
||||
fun videoFromUrl(
|
||||
url: String,
|
||||
quality: String? = null,
|
||||
prefix: String? = null,
|
||||
redirect: Boolean = true,
|
||||
externalSubs: List<Track> = emptyList(),
|
||||
): Video? {
|
||||
val newQuality = quality ?: ("Doodstream" + if (redirect) " mirror" else "")
|
||||
|
||||
return runCatching {
|
||||
val response = client.newCall(GET(url)).execute()
|
||||
val newUrl = if (redirect) response.request.url.toString() else url
|
||||
|
||||
val doodHost = Regex("https://(.*?)/").find(newUrl)!!.groupValues[1]
|
||||
val doodHost = getBaseUrl(newUrl)
|
||||
val content = response.body.string()
|
||||
if (!content.contains("'/pass_md5/")) return null
|
||||
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
|
||||
|
||||
// Obtener la calidad del título de la página
|
||||
val extractedQuality = Regex("\\d{3,4}p")
|
||||
.find(content.substringAfter("<title>").substringBefore("</title>"))
|
||||
?.groupValues
|
||||
?.getOrNull(0)
|
||||
|
||||
// Determinar la calidad a usar
|
||||
val newQuality = extractedQuality ?: ( if (redirect) " mirror" else "")
|
||||
|
||||
// Obtener el hash MD5
|
||||
val md5 = doodHost + (Regex("/pass_md5/[^']*").find(content)?.value ?: return null)
|
||||
val token = md5.substringAfterLast("/")
|
||||
val randomString = getRandomString()
|
||||
val randomString = createHashTable()
|
||||
val expiry = System.currentTimeMillis()
|
||||
|
||||
// Obtener la URL del video
|
||||
val videoUrlStart = client.newCall(
|
||||
GET(
|
||||
"https://$doodHost/pass_md5/$md5",
|
||||
md5,
|
||||
Headers.headersOf("referer", newUrl),
|
||||
),
|
||||
).execute().body.string()
|
||||
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
|
||||
Video(videoUrl, newQuality, videoUrl, headers = doodHeaders(doodHost), subtitleTracks = externalSubs)
|
||||
|
||||
val trueUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
|
||||
|
||||
Video(trueUrl, prefix + "Doodstream " + newQuality , trueUrl, headers = doodHeaders(doodHost), subtitleTracks = externalSubs)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
|
@ -44,16 +58,27 @@ class DoodExtractor(private val client: OkHttpClient) {
|
|||
redirect: Boolean = true,
|
||||
): List<Video> {
|
||||
val video = videoFromUrl(url, quality, redirect)
|
||||
return video?.let(::listOf) ?: emptyList<Video>()
|
||||
return video?.let(::listOf) ?: emptyList()
|
||||
}
|
||||
|
||||
private fun getRandomString(length: Int = 10): String {
|
||||
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||
return (1..length)
|
||||
.map { allowedChars.random() }
|
||||
.joinToString("")
|
||||
// Método para generar una cadena aleatoria
|
||||
private fun createHashTable(): String {
|
||||
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||
return buildString {
|
||||
repeat(10) {
|
||||
append(alphabet.random())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Método para obtener la base de la URL
|
||||
private fun getBaseUrl(url: String): String {
|
||||
return URI(url).let {
|
||||
"${it.scheme}://${it.host}"
|
||||
}
|
||||
}
|
||||
|
||||
// Método para obtener headers personalizados
|
||||
private fun doodHeaders(host: String) = Headers.Builder().apply {
|
||||
add("User-Agent", "Aniyomi")
|
||||
add("Referer", "https://$host/")
|
||||
|
|
3
lib/goodstream-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,3 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package eu.kanade.tachiyomi.lib.goodstramextractor
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class GoodStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
fun videosFromUrl(url: String, name: String): List<Video> {
|
||||
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||
val videos = mutableListOf<Video>()
|
||||
|
||||
doc.select("script").forEach { script ->
|
||||
if (script.data().contains(Regex("file|player"))) {
|
||||
val urlRegex = Regex("file: \"(https:\\/\\/[a-z0-9.\\/-_?=&]+)\",")
|
||||
urlRegex.find(script.data())?.groupValues?.get(1)?.let { link ->
|
||||
videos.add(
|
||||
Video(
|
||||
url = link,
|
||||
quality = name,
|
||||
videoUrl = link,
|
||||
headers = headers
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return videos
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import okhttp3.Headers
|
|||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.commonEmptyHeaders
|
||||
import kotlin.math.abs
|
||||
|
||||
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
||||
|
||||
|
@ -126,10 +127,16 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
|||
}.toList()
|
||||
|
||||
return masterPlaylist.substringAfter(PLAYLIST_SEPARATOR).split(PLAYLIST_SEPARATOR).mapNotNull {
|
||||
val codec = it.substringAfter("CODECS=\"", "").substringBefore("\"", "")
|
||||
if (codec.isNotEmpty()) {
|
||||
if (codec.startsWith("mp4a")) return@mapNotNull null
|
||||
}
|
||||
|
||||
|
||||
val resolution = it.substringAfter("RESOLUTION=")
|
||||
.substringBefore("\n")
|
||||
.substringAfter("x")
|
||||
.substringBefore(",") + "p"
|
||||
.substringBefore(",").let(::stnQuality)
|
||||
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
||||
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
||||
|
@ -328,6 +335,13 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
|||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun stnQuality(quality: String): String {
|
||||
val intQuality = quality.toInt()
|
||||
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
|
||||
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
|
||||
return "${result}p"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
|
||||
|
||||
|
|
|
@ -1,25 +1,48 @@
|
|||
package eu.kanade.tachiyomi.lib.streamhidevidextractor
|
||||
|
||||
import android.util.Log
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
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.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class StreamHideVidExtractor(private val client: OkHttpClient) {
|
||||
class StreamHideVidExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
val page = client.newCall(GET(url)).execute().body.string()
|
||||
val playlistUrl = (JsUnpacker.unpackAndCombine(page) ?: page)
|
||||
.substringAfter("sources:")
|
||||
.substringAfter("file:\"") // StreamHide
|
||||
.substringAfter("src:\"") // StreamVid
|
||||
.substringBefore('"')
|
||||
if (!playlistUrl.startsWith("http")) return emptyList()
|
||||
return playlistUtils.extractFromHls(playlistUrl,
|
||||
videoNameGen = { "${prefix}StreamHideVid - $it" }
|
||||
)
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
|
||||
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "StreamHideVid - $quality" }): List<Video> {
|
||||
|
||||
val doc = client.newCall(GET(getEmbedUrl(url), headers)).execute().asJsoup()
|
||||
|
||||
val scriptBody = doc.selectFirst("script:containsData(m3u8)")?.data()
|
||||
?.let { script ->
|
||||
if (script.contains("eval(function(p,a,c")) {
|
||||
JsUnpacker.unpackAndCombine(script)
|
||||
} else {
|
||||
script
|
||||
}
|
||||
}
|
||||
val masterUrl = scriptBody
|
||||
?.substringAfter("source", "")
|
||||
?.substringAfter("file:\"", "")
|
||||
?.substringBefore("\"", "")
|
||||
?.takeIf(String::isNotBlank)
|
||||
?: return emptyList()
|
||||
|
||||
Log.d("StreamHideVidExtractor", "Playlist URL: $masterUrl")
|
||||
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen)
|
||||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String {
|
||||
return when {
|
||||
url.contains("/d/") -> url.replace("/d/", "/v/")
|
||||
url.contains("/download/") -> url.replace("/download/", "/v/")
|
||||
url.contains("/file/") -> url.replace("/file/", "/v/")
|
||||
else -> url.replace("/f/", "/v/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
lib/universal-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package eu.kanade.tachiyomi.lib.universalextractor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UniversalExtractor(private val client: OkHttpClient) {
|
||||
private val context: Application by injectLazy()
|
||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun videosFromUrl(origRequestUrl: String, origRequestHeader: Headers, customQuality: String? = null, prefix: String = ""): List<Video> {
|
||||
val host = origRequestUrl.toHttpUrl().host.substringBefore(".").proper()
|
||||
val latch = CountDownLatch(1)
|
||||
var webView: WebView? = null
|
||||
var resultUrl = ""
|
||||
val playlistUtils by lazy { PlaylistUtils(client, origRequestHeader) }
|
||||
val headers = origRequestHeader.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||
|
||||
handler.post {
|
||||
val newView = WebView(context)
|
||||
webView = newView
|
||||
with(newView.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
useWideViewPort = false
|
||||
loadWithOverviewMode = false
|
||||
userAgentString = origRequestHeader["User-Agent"]
|
||||
}
|
||||
newView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
): WebResourceResponse? {
|
||||
val url = request.url.toString()
|
||||
if (VIDEO_REGEX.containsMatchIn(url)) {
|
||||
resultUrl = url
|
||||
latch.countDown()
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
}
|
||||
|
||||
webView?.loadUrl(origRequestUrl, headers)
|
||||
}
|
||||
|
||||
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||
|
||||
handler.post {
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
// terabox special case start
|
||||
if ("M3U8_AUTO_360" in resultUrl) {
|
||||
val qualities = listOf("1080", "720", "480", "360")
|
||||
val allVideos = mutableListOf<Video>()
|
||||
|
||||
for (quality in qualities) {
|
||||
val modifiedUrl = resultUrl.replace("M3U8_AUTO_360", "M3U8_AUTO_$quality")
|
||||
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it $quality" + "p" })
|
||||
|
||||
if (videos.isNotEmpty()) {
|
||||
allVideos.addAll(videos)
|
||||
}
|
||||
}
|
||||
|
||||
if (allVideos.isNotEmpty()) {
|
||||
return allVideos
|
||||
}
|
||||
}
|
||||
// terabox special case end
|
||||
|
||||
return when {
|
||||
"m3u8" in resultUrl -> {
|
||||
Log.d("UniversalExtractor", "m3u8 URL: $resultUrl")
|
||||
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it" })
|
||||
}
|
||||
"mpd" in resultUrl -> {
|
||||
Log.d("UniversalExtractor", "mpd URL: $resultUrl")
|
||||
playlistUtils.extractFromDash(resultUrl, { it -> "$prefix - $host: $it" }, referer = origRequestUrl)
|
||||
}
|
||||
"mp4" in resultUrl -> {
|
||||
Log.d("UniversalExtractor", "mp4 URL: $resultUrl")
|
||||
Video(resultUrl, "$prefix - $host: ${customQuality ?: "Mirror"}", resultUrl, origRequestHeader.newBuilder().add("referer", origRequestUrl).build()).let(::listOf)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.proper(): String {
|
||||
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
|
||||
Locale.getDefault()) else it.toString() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TIMEOUT_SEC: Long = 10
|
||||
private val VIDEO_REGEX by lazy { Regex(".*\\.(mp4|m3u8|mpd)(\\?.*)?$") }
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ class UqloadExtractor(private val client: OkHttpClient) {
|
|||
?.takeIf { it.startsWith("http") }
|
||||
?: return emptyList()
|
||||
|
||||
val videoHeaders = Headers.headersOf("Referer", "https://uqload.co/")
|
||||
val videoHeaders = Headers.headersOf("Referer", "https://uqload.ws/")
|
||||
val quality = if (prefix.isNotBlank()) "$prefix Uqload" else "Uqload"
|
||||
|
||||
return listOf(Video(videoUrl, quality, videoUrl, videoHeaders))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeWorld India'
|
||||
extClass = '.AnimeWorldIndiaFactory'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.AnimeXin'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animexin.vip'
|
||||
overrideVersionCode = 9
|
||||
overrideVersionCode = 10
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AniZone'
|
||||
extClass = '.AniZone'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.ChineseAnime'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://www.chineseanime.vip'
|
||||
overrideVersionCode = 11
|
||||
overrideVersionCode = 12
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
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
|
@ -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
After Width: | Height: | Size: 2.7 KiB |
BIN
src/all/debridindex/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/all/debridindex/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/all/debridindex/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/all/debridindex/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
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 = 16
|
||||
}
|
||||
|
||||
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 = 5
|
||||
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 = 25
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MissAV'
|
||||
extClass = '.MissAV'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Sudatchi'
|
||||
extClass = '.Sudatchi'
|
||||
extVersionCode = 5
|
||||
extVersionCode = 11
|
||||
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 = 'SupJav'
|
||||
extClass = '.SupJavFactory'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime4up'
|
||||
extClass = '.Anime4Up'
|
||||
extVersionCode = 61
|
||||
extVersionCode = 62
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeLek'
|
||||
extClass = '.AnimeLek'
|
||||
extVersionCode = 30
|
||||
extVersionCode = 31
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Animerco'
|
||||
extClass = '.Animerco'
|
||||
extVersionCode = 40
|
||||
extVersionCode = 41
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Arab Seed'
|
||||
extClass = '.ArabSeed'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'asia2tv'
|
||||
extClass = '.Asia2TV'
|
||||
extVersionCode = 21
|
||||
extVersionCode = 22
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Cimaleek'
|
||||
extClass = '.Cimaleek'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Egy Dead'
|
||||
extClass = '.EgyDead'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'FASELHD'
|
||||
extClass = '.FASELHD'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 16
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MY CIMA'
|
||||
extClass = '.MyCima'
|
||||
extVersionCode = 22
|
||||
extVersionCode = 23
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Okanime'
|
||||
extClass = '.Okanime'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Tuktuk Cinema'
|
||||
extClass = '.Tuktukcinema'
|
||||
extVersionCode = 22
|
||||
extVersionCode = 23
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'WIT ANIME'
|
||||
extClass = '.WitAnime'
|
||||
extVersionCode = 50
|
||||
extVersionCode = 51
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime-Base'
|
||||
extClass = '.AnimeBase'
|
||||
extVersionCode = 29
|
||||
extVersionCode = 30
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime-Loads'
|
||||
extClass = '.AnimeLoads'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 17
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeToast'
|
||||
extClass = '.AnimeToast'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 21
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -166,7 +166,6 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
DoodExtractor(client).videoFromUrl(
|
||||
link,
|
||||
quality,
|
||||
false,
|
||||
)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
|
@ -224,7 +223,7 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
) == true -> {
|
||||
val quality = "DoodStream"
|
||||
val video =
|
||||
DoodExtractor(client).videoFromUrl(link, quality, false)
|
||||
DoodExtractor(client).videoFromUrl(link, quality)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AniWorld'
|
||||
extClass = '.AniWorld'
|
||||
extVersionCode = 24
|
||||
extVersionCode = 25
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'CineClix'
|
||||
extClass = '.CineClix'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.Cinemathek'
|
||||
themePkg = 'dooplay'
|
||||
baseUrl = 'https://cinemathek.net'
|
||||
overrideVersionCode = 23
|
||||
overrideVersionCode = 24
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Einfach'
|
||||
extClass = '.Einfach'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.Kinoking'
|
||||
themePkg = 'dooplay'
|
||||
baseUrl = 'https://kinoking.cc'
|
||||
overrideVersionCode = 22
|
||||
overrideVersionCode = 23
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Moflix-Stream'
|
||||
extClass = '.MoflixStream'
|
||||
extVersionCode = 13
|
||||
extVersionCode = 14
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Movie2k'
|
||||
extClass = '.Movie2k'
|
||||
extVersionCode = 7
|
||||
extVersionCode = 8
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Serienstream'
|
||||
extClass = '.Serienstream'
|
||||
extVersionCode = 20
|
||||
extVersionCode = 24
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -10,4 +10,4 @@ dependencies {
|
|||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
|
||||
override val name = "Serienstream"
|
||||
|
||||
override val baseUrl = "https://s.to"
|
||||
override val baseUrl = "http://186.2.175.5"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
|
@ -91,7 +91,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val headers = Headers.Builder()
|
||||
.add("Referer", "https://s.to/search")
|
||||
.add("Referer", "http://186.2.175.5/search")
|
||||
.add("origin", baseUrl)
|
||||
.add("connection", "keep-alive")
|
||||
.add("user-agent", "Mozilla/5.0 (Linux; Android 12; Pixel 5 Build/SP2A.220405.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Safari/537.36")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'StreamCloud'
|
||||
extClass = '.StreamCloud'
|
||||
extVersionCode = 9
|
||||
extVersionCode = 10
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AllAnime'
|
||||
extClass = '.AllAnime'
|
||||
extVersionCode = 34
|
||||
extVersionCode = 35
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AllAnimeChi'
|
||||
extClass = '.AllAnimeChi'
|
||||
extVersionCode = 10
|
||||
extVersionCode = 11
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
7
src/en/animeflix/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeFlix'
|
||||
extClass = '.AnimeFlix'
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/animeflix/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/en/animeflix/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/en/animeflix/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
src/en/animeflix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/en/animeflix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,398 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeflix
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Base64
|
||||
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.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeFlix"
|
||||
|
||||
override val baseUrl = "https://animeflix.mobi"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page/")
|
||||
|
||||
override fun popularAnimeSelector() = "div#content_box > div.post-cards > article"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
// prevent base64 images
|
||||
thumbnail_url = element.selectFirst("img")!!.run {
|
||||
attr("data-pagespeed-high-res-src").ifEmpty { attr("src") }
|
||||
}
|
||||
title = element.selectFirst("header")!!.text()
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val cleanQuery = query.replace(" ", "+").lowercase()
|
||||
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
|
||||
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
|
||||
subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("Text search ignores filters"),
|
||||
GenreFilter(),
|
||||
SubPageFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Genres",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Action", "action"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Isekai", "isekai"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Psychological", "psychological"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Sci-Fi", "sci-fi"),
|
||||
Pair("Magic", "magic"),
|
||||
Pair("Slice Of Life", "slice-of-life"),
|
||||
Pair("Sports", "sports"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
),
|
||||
)
|
||||
|
||||
private class SubPageFilter : UriPartFilter(
|
||||
"Sub-page",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Latest Release", "latest-release"),
|
||||
Pair("Movies", "movies"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
title = document.selectFirst("div.single_post > header > h1")!!.text()
|
||||
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
|
||||
|
||||
val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
|
||||
status = when (infosDiv.getInfo("Status").toString()) {
|
||||
"Completed" -> SAnime.COMPLETED
|
||||
"Currently Airing" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
artist = infosDiv.getInfo("Studios")
|
||||
author = infosDiv.getInfo("Producers")
|
||||
genre = infosDiv.getInfo("Genres")
|
||||
val animeInfo = infosDiv.select("li").joinToString("\n") { it.text() }
|
||||
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)")
|
||||
.joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
|
||||
}
|
||||
|
||||
private fun Element.getInfo(info: String) =
|
||||
selectFirst("li:contains($info)")?.ownText()?.trim()
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
val seasonRegex by lazy { Regex("""season (\d+)""", RegexOption.IGNORE_CASE) }
|
||||
val qualityRegex by lazy { """(\d+)p""".toRegex() }
|
||||
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||
val document = client.newCall(GET(baseUrl + anime.url)).execute()
|
||||
.asJsoup()
|
||||
|
||||
val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)")
|
||||
|
||||
val episodeList = if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) {
|
||||
val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy {
|
||||
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
|
||||
}
|
||||
|
||||
seasonsLinks.flatMap { (seasonNumber, season) ->
|
||||
val serverListSeason = season.map {
|
||||
val previousText = it.previousElementSibling()!!.text()
|
||||
val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
|
||||
|
||||
val url = it.selectFirst("a")!!.attr("href")
|
||||
val episodesDocument = client.newCall(GET(url)).execute()
|
||||
.asJsoup()
|
||||
episodesDocument.select("div.entry-content > h3 > a").map {
|
||||
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
|
||||
}
|
||||
}
|
||||
|
||||
transposeEpisodes(serverListSeason)
|
||||
}
|
||||
} else {
|
||||
val driveList = document.select("div.thecontent p:has(span:contains(Gdrive))").map {
|
||||
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
|
||||
Pair(it.selectFirst("a")!!.attr("href"), quality)
|
||||
}
|
||||
|
||||
// Load episodes
|
||||
val serversList = driveList.map { drive ->
|
||||
val episodesDocument = client.newCall(GET(drive.first)).execute()
|
||||
.asJsoup()
|
||||
episodesDocument.select("div.entry-content > h3 > a").map {
|
||||
EpUrl(drive.second, it.attr("href"), it.text())
|
||||
}
|
||||
}
|
||||
|
||||
transposeEpisodes(serversList)
|
||||
}
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
private fun transposeEpisodes(serversList: List<List<EpUrl>>) =
|
||||
transpose(serversList).mapIndexed { index, serverList ->
|
||||
SEpisode.create().apply {
|
||||
name = serverList.first().name
|
||||
episode_number = (index + 1).toFloat()
|
||||
setUrlWithoutDomain(json.encodeToString(serverList))
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val urls = json.decodeFromString<List<EpUrl>>(episode.url)
|
||||
|
||||
val leechUrls = urls.map {
|
||||
val firstLeech = client.newCall(GET(it.url)).execute()
|
||||
.asJsoup()
|
||||
.selectFirst("script:containsData(downlaod_button)")!!
|
||||
.data()
|
||||
.substringAfter("<a href=\"")
|
||||
.substringBefore("\">")
|
||||
|
||||
val path = client.newCall(GET(firstLeech)).execute()
|
||||
.body.string()
|
||||
.substringAfter("replace(\"")
|
||||
.substringBefore("\"")
|
||||
|
||||
val link = "https://" + firstLeech.toHttpUrl().host + path
|
||||
EpUrl(it.quality, link, it.name)
|
||||
}
|
||||
|
||||
val videoList = leechUrls.parallelCatchingFlatMap { url ->
|
||||
if (url.url.toHttpUrl().encodedPath == "/404") return@parallelCatchingFlatMap emptyList()
|
||||
val (videos, mediaUrl) = extractVideo(url)
|
||||
when {
|
||||
videos.isEmpty() -> {
|
||||
extractGDriveLink(mediaUrl, url.quality).ifEmpty {
|
||||
getDirectLink(mediaUrl, "instant", "/mfile/")?.let {
|
||||
listOf(Video(it, "${url.quality}p - GDrive Instant link", it))
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
else -> videos
|
||||
}
|
||||
}
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
// https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
|
||||
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
|
||||
val matchResult = qualityRegex.find(epUrl.name)
|
||||
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
|
||||
|
||||
return (1..3).toList().flatMap { type ->
|
||||
extractWorkerLinks(epUrl.url, quality, type)
|
||||
}.let { Pair(it, epUrl.url) }
|
||||
}
|
||||
|
||||
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
|
||||
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
|
||||
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
|
||||
val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
|
||||
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
|
||||
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
|
||||
val link = linkElement.attr("href")
|
||||
val decodedLink = if (link.contains("workers.dev")) {
|
||||
link
|
||||
} else {
|
||||
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
|
||||
}
|
||||
|
||||
Video(
|
||||
url = decodedLink,
|
||||
quality = "${quality}p - CF $type Worker ${index + 1}$size",
|
||||
videoUrl = decodedLink,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDirectLink(url: String, action: String = "direct", newPath: String = "/file/"): String? {
|
||||
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||
val script = doc.selectFirst("script:containsData(async function taskaction)")
|
||||
?.data()
|
||||
?: return url
|
||||
|
||||
val key = script.substringAfter("key\", \"").substringBefore('"')
|
||||
val form = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("action", action)
|
||||
.addFormDataPart("key", key)
|
||||
.addFormDataPart("action_token", "")
|
||||
.build()
|
||||
|
||||
val headers = headersBuilder().set("x-token", url.toHttpUrl().host).build()
|
||||
|
||||
val req = client.newCall(POST(url.replace("/file/", newPath), headers, form)).execute()
|
||||
return runCatching {
|
||||
json.decodeFromString<DriveLeechDirect>(req.body.string()).url
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
|
||||
val neoUrl = getDirectLink(mediaUrl) ?: mediaUrl
|
||||
val response = client.newCall(GET(neoUrl)).execute().asJsoup()
|
||||
val gdBtn = response.selectFirst("div.card-body a.btn")!!
|
||||
val gdLink = gdBtn.attr("href")
|
||||
val sizeMatch = SIZE_REGEX.find(gdBtn.text())
|
||||
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
|
||||
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
|
||||
val link = gdResponse.select("form#download-form")
|
||||
return if (link.isNullOrEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
val realLink = link.attr("action")
|
||||
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
|
||||
}
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private fun <E> transpose(xs: List<List<E>>): List<List<E>> {
|
||||
// Helpers
|
||||
fun <E> List<E>.head(): E = this.first()
|
||||
fun <E> List<E>.tail(): List<E> = this.takeLast(this.size - 1)
|
||||
fun <E> E.append(xs: List<E>): List<E> = listOf(this).plus(xs)
|
||||
|
||||
xs.filter { it.isNotEmpty() }.let { ys ->
|
||||
return when (ys.isNotEmpty()) {
|
||||
true -> ys.map { it.head() }.append(transpose(ys.map { it.tail() }))
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EpUrl(
|
||||
val quality: String,
|
||||
val url: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DriveLeechDirect(val url: String? = null)
|
||||
|
||||
companion object {
|
||||
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private const val PREF_QUALITY_KEY = "pref_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
|
||||
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_VALUES
|
||||
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)
|
||||
}
|
||||
}
|
12
src/en/animeflixlive/build.gradle
Normal file
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'Animeflix.live'
|
||||
extClass = '.AnimeflixLive'
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:gogostream-extractor'))
|
||||
implementation(project(':lib:playlist-utils'))
|
||||
}
|
BIN
src/en/animeflixlive/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
src/en/animeflixlive/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/en/animeflixlive/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/en/animeflixlive/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/en/animeflixlive/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,503 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
|
||||
|
||||
import GenreFilter
|
||||
import SortFilter
|
||||
import SubPageFilter
|
||||
import TypeFilter
|
||||
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.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLDecoder
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.min
|
||||
|
||||
class AnimeflixLive : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "Animeflix.live"
|
||||
|
||||
override val baseUrl by lazy { preferences.baseUrl }
|
||||
|
||||
private val apiUrl by lazy { preferences.apiUrl }
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val apiHeaders = headersBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", apiUrl.toHttpUrl().host)
|
||||
add("Origin", baseUrl)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
private val docHeaders = headersBuilder().apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
add("Host", apiUrl.toHttpUrl().host)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request =
|
||||
GET("$apiUrl/popular?page=${page - 1}", apiHeaders)
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<List<AnimeDto>>()
|
||||
val titlePref = preferences.titleType
|
||||
|
||||
val animeList = parsed.map {
|
||||
it.toSAnime(titlePref)
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$apiUrl/trending?page=${page - 1}", apiHeaders)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<TrendingDto>()
|
||||
val titlePref = preferences.titleType
|
||||
|
||||
val animeList = parsed.trending.map {
|
||||
it.toSAnime(titlePref)
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val sort = filters.filterIsInstance<SortFilter>().first().getValue()
|
||||
val type = filters.filterIsInstance<TypeFilter>().first().getValues()
|
||||
val genre = filters.filterIsInstance<GenreFilter>().first().getValues()
|
||||
val subPage = filters.filterIsInstance<SubPageFilter>().first().getValue()
|
||||
|
||||
if (subPage.isNotBlank()) {
|
||||
return GET("$apiUrl/$subPage?page=${page - 1}", apiHeaders)
|
||||
}
|
||||
|
||||
if (query.isEmpty()) {
|
||||
throw Exception("Search must not be empty")
|
||||
}
|
||||
|
||||
val filtersObj = buildJsonObject {
|
||||
put("sort", sort)
|
||||
if (type.isNotEmpty()) {
|
||||
put("type", json.encodeToString(type))
|
||||
}
|
||||
if (genre.isNotEmpty()) {
|
||||
put("genre", json.encodeToString(genre))
|
||||
}
|
||||
}.toJsonString()
|
||||
|
||||
val url = apiUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("info")
|
||||
addPathSegment("")
|
||||
addQueryParameter("query", query)
|
||||
addQueryParameter("limit", "15")
|
||||
addQueryParameter("filters", filtersObj)
|
||||
addQueryParameter("k", query.substr(0, 3).sk())
|
||||
}.build()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<List<AnimeDto>>()
|
||||
val titlePref = preferences.titleType
|
||||
|
||||
val animeList = parsed.map {
|
||||
it.toSAnime(titlePref)
|
||||
}
|
||||
|
||||
val hasNextPage = if (response.request.url.queryParameter("limit") == null) {
|
||||
animeList.size == 44
|
||||
} else {
|
||||
animeList.size == 15
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
SortFilter(),
|
||||
TypeFilter(),
|
||||
GenreFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
AnimeFilter.Header("NOTE: Subpage overrides search and other filters!"),
|
||||
SubPageFilter(),
|
||||
)
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
return GET("$apiUrl/getslug/${anime.url}", apiHeaders)
|
||||
}
|
||||
|
||||
override fun getAnimeUrl(anime: SAnime): String {
|
||||
return "$baseUrl/search/${anime.title}?anime=${anime.url}"
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val titlePref = preferences.titleType
|
||||
return response.parseAs<DetailsDto>().toSAnime(titlePref)
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val lang = preferences.lang
|
||||
|
||||
val url = apiUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("episodes")
|
||||
addQueryParameter("id", anime.url)
|
||||
addQueryParameter("dub", (lang == "Dub").toString())
|
||||
addQueryParameter("c", anime.url.sk())
|
||||
}.build()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val slug = response.request.url.queryParameter("id")!!
|
||||
|
||||
return response.parseAs<EpisodeResponseDto>().episodes.map {
|
||||
it.toSEpisode(slug)
|
||||
}.sortedByDescending { it.episode_number }
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val url = "$apiUrl${episode.url}".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("server", "")
|
||||
addQueryParameter("c", episode.url.substringAfter("/watch/").sk())
|
||||
}.build()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val initialPlayerUrl = apiUrl + response.parseAs<ServerDto>().source
|
||||
val initialServer = initialPlayerUrl.toHttpUrl().queryParameter("server")!!
|
||||
|
||||
val initialPlayerDocument = client.newCall(
|
||||
GET(initialPlayerUrl, docHeaders),
|
||||
).execute().asJsoup().unescape()
|
||||
|
||||
videoList.addAll(
|
||||
videosFromPlayer(
|
||||
initialPlayerDocument,
|
||||
initialServer.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
|
||||
),
|
||||
)
|
||||
|
||||
// Go through rest of servers
|
||||
val servers = initialPlayerDocument.selectFirst("script:containsData(server-settings)")!!.data()
|
||||
val serversHtml = SERVER_REGEX.findAll(servers).map {
|
||||
Jsoup.parseBodyFragment(it.groupValues[1])
|
||||
}.toList()
|
||||
|
||||
videoList.addAll(
|
||||
serversHtml.parallelCatchingFlatMapBlocking {
|
||||
val server = serverMapping[
|
||||
it.selectFirst("button")!!
|
||||
.attr("onclick")
|
||||
.substringAfter("postMessage('")
|
||||
.substringBefore("'"),
|
||||
]
|
||||
if (server == initialServer) {
|
||||
return@parallelCatchingFlatMapBlocking emptyList()
|
||||
}
|
||||
|
||||
val serverUrl = response.request.url.newBuilder()
|
||||
.setQueryParameter("server", server)
|
||||
.build()
|
||||
val playerUrl = apiUrl + client.newCall(
|
||||
GET(serverUrl, apiHeaders),
|
||||
).execute().parseAs<ServerDto>().source
|
||||
|
||||
if (server != playerUrl.toHttpUrl().queryParameter("server")!!) {
|
||||
return@parallelCatchingFlatMapBlocking emptyList()
|
||||
}
|
||||
|
||||
val playerDocument = client.newCall(
|
||||
GET(playerUrl, docHeaders),
|
||||
).execute().asJsoup().unescape()
|
||||
|
||||
videosFromPlayer(
|
||||
playerDocument,
|
||||
server.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
private val serverMapping = mapOf(
|
||||
"settings-0" to "moon",
|
||||
"settings-1" to "sun",
|
||||
"settings-2" to "zoro",
|
||||
"settings-3" to "gogo",
|
||||
)
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
|
||||
private fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers {
|
||||
return baseHeaders.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Accept-Language", "en-US,en;q=0.5")
|
||||
add("Host", videoUrl.toHttpUrl().host)
|
||||
add("Origin", "https://${apiUrl.toHttpUrl().host}")
|
||||
add("Referer", "$apiUrl/")
|
||||
add("Sec-Fetch-Dest", "empty")
|
||||
add("Sec-Fetch-Mode", "cors")
|
||||
add("Sec-Fetch-Site", "cross-site")
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun Document.unescape(): Document {
|
||||
val unescapeScript = this.selectFirst("script:containsData(unescape)")
|
||||
return if (unescapeScript == null) {
|
||||
this
|
||||
} else {
|
||||
val data = URLDecoder.decode(unescapeScript.data(), "UTF-8")
|
||||
Jsoup.parse(data, this.location())
|
||||
}
|
||||
}
|
||||
|
||||
private fun videosFromPlayer(document: Document, name: String): List<Video> {
|
||||
val dataScript = document.selectFirst("script:containsData(m3u8)")
|
||||
?.data() ?: return emptyList()
|
||||
|
||||
val subtitleList = document.select("video > track[kind=captions]").map {
|
||||
Track(it.attr("id"), it.attr("label"))
|
||||
}
|
||||
|
||||
var masterPlaylist = M3U8_REGEX.find(dataScript)?.groupValues?.get(1)
|
||||
?: return emptyList()
|
||||
|
||||
if (name.equals("moon", true)) {
|
||||
masterPlaylist += dataScript.substringAfter("`${'$'}{url}")
|
||||
.substringBefore("`")
|
||||
}
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
masterPlaylist,
|
||||
videoHeadersGen = ::getVideoHeaders,
|
||||
videoNameGen = { q -> "$name - $q" },
|
||||
subtitleList = subtitleList,
|
||||
)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.quality
|
||||
val server = preferences.server
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.contains(server, true) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private fun JsonObject.toJsonString(): String {
|
||||
return json.encodeToString(this)
|
||||
}
|
||||
|
||||
private fun String.sk(): String {
|
||||
val t = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
val n = 17 + (t.get(Calendar.DAY_OF_MONTH) - t.get(Calendar.MONTH)) / 2
|
||||
return this.toCharArray().fold("") { acc, c ->
|
||||
acc + c.code.toString(n).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.substr(start: Int, end: Int): String {
|
||||
val stop = min(end, this.length)
|
||||
return this.substring(start, stop)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SERVER_REGEX = Regex("""'1' === '1'.*?(<button.*?</button>)""", RegexOption.DOT_MATCHES_ALL)
|
||||
private val M3U8_REGEX = Regex("""const ?\w*? ?= ?`(.*?)`""")
|
||||
private const val PAGE_SIZE = 24
|
||||
|
||||
private const val PREF_DOMAIN_KEY = "pref_domain_key"
|
||||
private const val PREF_DOMAIN_DEFAULT = "https://animeflix.live,https://api.animeflix.dev"
|
||||
private val PREF_DOMAIN_ENTRIES = arrayOf("animeflix.live", "animeflix.ro")
|
||||
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf(
|
||||
"https://animeflix.live,https://api.animeflix.dev",
|
||||
"https://animeflix.ro,https://api.animeflixtv.to",
|
||||
)
|
||||
|
||||
private const val PREF_TITLE_KEY = "pref_title_type_key"
|
||||
private const val PREF_TITLE_DEFAULT = "English"
|
||||
private val PREF_TITLE_ENTRIES = arrayOf("English", "Native", "Romaji")
|
||||
|
||||
private const val PREF_LANG_KEY = "pref_lang_key"
|
||||
private const val PREF_LANG_DEFAULT = "Sub"
|
||||
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
|
||||
|
||||
private const val PREF_QUALITY_KEY = "pref_quality_key"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
|
||||
|
||||
private const val PREF_SERVER_KEY = "pref_server_key"
|
||||
private const val PREF_SERVER_DEFAULT = "Moon"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Moon", "Sun", "Zoro", "Gogo")
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = "Preferred domain (requires app restart)"
|
||||
entries = PREF_DOMAIN_ENTRIES
|
||||
entryValues = PREF_DOMAIN_ENTRY_VALUES
|
||||
setDefaultValue(PREF_DOMAIN_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_TITLE_KEY
|
||||
title = "Preferred Title Type"
|
||||
entries = PREF_TITLE_ENTRIES
|
||||
entryValues = PREF_TITLE_ENTRIES
|
||||
setDefaultValue(PREF_TITLE_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_LANG_KEY
|
||||
title = "Preferred Language"
|
||||
entries = PREF_LANG_ENTRIES
|
||||
entryValues = PREF_LANG_ENTRIES
|
||||
setDefaultValue(PREF_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_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
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_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = PREF_SERVER_ENTRIES
|
||||
entryValues = PREF_SERVER_ENTRIES
|
||||
setDefaultValue(PREF_SERVER_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)
|
||||
}
|
||||
|
||||
private val SharedPreferences.baseUrl
|
||||
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
.split(",").first()
|
||||
|
||||
private val SharedPreferences.apiUrl
|
||||
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
.split(",").last()
|
||||
|
||||
private val SharedPreferences.titleType
|
||||
get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.lang
|
||||
get() = getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.quality
|
||||
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.server
|
||||
get() = getString(PREF_SERVER_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
@Serializable
|
||||
class TrendingDto(
|
||||
val trending: List<AnimeDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AnimeDto(
|
||||
val slug: String,
|
||||
@SerialName("title") val titleObj: TitleObject,
|
||||
val images: ImageObject,
|
||||
) {
|
||||
@Serializable
|
||||
class TitleObject(
|
||||
val english: String? = null,
|
||||
val native: String? = null,
|
||||
val romaji: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ImageObject(
|
||||
val large: String? = null,
|
||||
val medium: String? = null,
|
||||
val small: String? = null,
|
||||
)
|
||||
|
||||
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
|
||||
title = when (titlePref) {
|
||||
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
|
||||
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
|
||||
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
|
||||
}
|
||||
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
|
||||
url = slug
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class DetailsDto(
|
||||
val slug: String,
|
||||
@SerialName("title") val titleObj: TitleObject,
|
||||
val description: String,
|
||||
val genres: List<String>,
|
||||
val status: String? = null,
|
||||
val images: ImageObject,
|
||||
) {
|
||||
@Serializable
|
||||
class TitleObject(
|
||||
val english: String? = null,
|
||||
val native: String? = null,
|
||||
val romaji: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ImageObject(
|
||||
val large: String? = null,
|
||||
val medium: String? = null,
|
||||
val small: String? = null,
|
||||
)
|
||||
|
||||
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
|
||||
title = when (titlePref) {
|
||||
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
|
||||
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
|
||||
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
|
||||
}
|
||||
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
|
||||
url = slug
|
||||
genre = genres.joinToString()
|
||||
status = this@DetailsDto.status.parseStatus()
|
||||
description = Jsoup.parseBodyFragment(
|
||||
this@DetailsDto.description.replace("<br>", "br2n"),
|
||||
).text().replace("br2n", "\n")
|
||||
}
|
||||
|
||||
private fun String?.parseStatus(): Int = when (this?.lowercase()) {
|
||||
"releasing" -> SAnime.ONGOING
|
||||
"finished" -> SAnime.COMPLETED
|
||||
"cancelled" -> SAnime.CANCELLED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class EpisodeResponseDto(
|
||||
val episodes: List<EpisodeDto>,
|
||||
) {
|
||||
@Serializable
|
||||
class EpisodeDto(
|
||||
val number: Float,
|
||||
val title: String? = null,
|
||||
) {
|
||||
fun toSEpisode(slug: String): SEpisode = SEpisode.create().apply {
|
||||
val epNum = if (floor(number) == ceil(number)) {
|
||||
number.toInt().toString()
|
||||
} else {
|
||||
number.toString()
|
||||
}
|
||||
|
||||
url = "/watch/$slug-episode-$epNum"
|
||||
episode_number = number
|
||||
name = if (title == null) {
|
||||
"Episode $epNum"
|
||||
} else {
|
||||
"Ep. $epNum - $title"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ServerDto(
|
||||
val source: String,
|
||||
)
|
|
@ -0,0 +1,81 @@
|
|||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
|
||||
open class UriPartFilter(
|
||||
name: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
defaultValue: String? = null,
|
||||
) : AnimeFilter.Select<String>(
|
||||
name,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||
) {
|
||||
fun getValue(): String {
|
||||
return vals[state].second
|
||||
}
|
||||
}
|
||||
|
||||
open class UriMultiSelectOption(name: String, val value: String) : AnimeFilter.CheckBox(name)
|
||||
|
||||
open class UriMultiSelectFilter(
|
||||
name: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }) {
|
||||
fun getValues(): List<String> {
|
||||
return state.filter { it.state }.map { it.value }
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : UriPartFilter(
|
||||
"Sort",
|
||||
arrayOf(
|
||||
Pair("Recently Updated", "recently_updated"),
|
||||
Pair("Recently Added", "recently_added"),
|
||||
Pair("Release Date ↓", "release_date_down"),
|
||||
Pair("Release Date ↑", "release_date_up"),
|
||||
Pair("Name A-Z", "title_az"),
|
||||
Pair("Best Rating", "scores"),
|
||||
Pair("Most Watched", "most_watched"),
|
||||
Pair("Anime Length", "number_of_episodes"),
|
||||
),
|
||||
)
|
||||
|
||||
class TypeFilter : UriMultiSelectFilter(
|
||||
"Type",
|
||||
arrayOf(
|
||||
Pair("TV", "TV"),
|
||||
Pair("Movie", "MOVIE"),
|
||||
Pair("OVA", "OVA"),
|
||||
Pair("ONA", "ONA"),
|
||||
Pair("Special", "SPECIAL"),
|
||||
),
|
||||
)
|
||||
|
||||
class GenreFilter : UriMultiSelectFilter(
|
||||
"Genre",
|
||||
arrayOf(
|
||||
Pair("Action", "Action"),
|
||||
Pair("Adventure", "Adventure"),
|
||||
Pair("Comedy", "Comedy"),
|
||||
Pair("Drama", "Drama"),
|
||||
Pair("Ecchi", "Ecchi"),
|
||||
Pair("Fantasy", "Fantasy"),
|
||||
Pair("Horror", "Horror"),
|
||||
Pair("Mecha", "Mecha"),
|
||||
Pair("Mystery", "Mystery"),
|
||||
Pair("Psychological", "Psychological"),
|
||||
Pair("Romance", "Romance"),
|
||||
Pair("Sci-Fi", "Sci-Fi"),
|
||||
Pair("Sports", "Sports"),
|
||||
Pair("Supernatural", "Supernatural"),
|
||||
Pair("Thriller", "Thriller"),
|
||||
),
|
||||
)
|
||||
|
||||
class SubPageFilter : UriPartFilter(
|
||||
"Sub-page",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Movies", "movies"),
|
||||
Pair("Series", "series"),
|
||||
),
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Animension'
|
||||
extClass = '.Animension'
|
||||
extVersionCode = 20
|
||||
extVersionCode = 21
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeOwl'
|
||||
extClass = '.AnimeOwl'
|
||||
extVersionCode = 19
|
||||
extVersionCode = 20
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeTake'
|
||||
extClass = '.AnimeTake'
|
||||
extVersionCode = 6
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -2,7 +2,7 @@ ext {
|
|||
extName = 'AniPlay'
|
||||
extClass = '.AniPlay'
|
||||
themePkg = 'anilist'
|
||||
overrideVersionCode = 10
|
||||
overrideVersionCode = 11
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AsiaFlix'
|
||||
extClass = '.AsiaFlix'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.DonghuaStream'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://donghuastream.org'
|
||||
overrideVersionCode = 7
|
||||
overrideVersionCode = 8
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.DopeBox'
|
||||
themePkg = 'dopeflix'
|
||||
baseUrl = 'https://dopebox.to'
|
||||
overrideVersionCode = 9
|
||||
overrideVersionCode = 10
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'DramaCool'
|
||||
extClass = '.DramaCool'
|
||||
extVersionCode = 53
|
||||
extVersionCode = 54
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'GenoAnime'
|
||||
extClass = '.GenoAnime'
|
||||
extVersionCode = 33
|
||||
extVersionCode = 34
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Gogoanime'
|
||||
extClass = '.GogoAnime'
|
||||
extVersionCode = 86
|
||||
extVersionCode = 87
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'KickAssAnime'
|
||||
extClass = '.KickAssAnime'
|
||||
extVersionCode = 43
|
||||
extVersionCode = 44
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|