Merged with dark25 (#636)
* merge merged lib, lib-multisrc, all, ar, de, en, es, fr, hi, id, it, pt, tr src from dark25 * patch
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 3
|
||||||
|
|
|
@ -117,7 +117,11 @@ abstract class AnimeStream(
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun searchAnimeByPathParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 1
|
baseVersionCode = 2
|
||||||
|
|
|
@ -155,7 +155,11 @@ abstract class DooPlay(
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.lib.chillxextractor
|
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
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 subtitleList = buildList {
|
||||||
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
||||||
subtitles.forEach {
|
subtitles.forEach {
|
||||||
|
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
|
||||||
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
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 eu.kanade.tachiyomi.network.GET
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
class DoodExtractor(private val client: OkHttpClient) {
|
class DoodExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
fun videoFromUrl(
|
fun videoFromUrl(
|
||||||
url: String,
|
url: String,
|
||||||
quality: String? = null,
|
prefix: String? = null,
|
||||||
redirect: Boolean = true,
|
redirect: Boolean = true,
|
||||||
externalSubs: List<Track> = emptyList(),
|
externalSubs: List<Track> = emptyList(),
|
||||||
): Video? {
|
): Video? {
|
||||||
val newQuality = quality ?: ("Doodstream" + if (redirect) " mirror" else "")
|
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val response = client.newCall(GET(url)).execute()
|
val response = client.newCall(GET(url)).execute()
|
||||||
val newUrl = if (redirect) response.request.url.toString() else url
|
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()
|
val content = response.body.string()
|
||||||
if (!content.contains("'/pass_md5/")) return null
|
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 token = md5.substringAfterLast("/")
|
||||||
val randomString = getRandomString()
|
val randomString = createHashTable()
|
||||||
val expiry = System.currentTimeMillis()
|
val expiry = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Obtener la URL del video
|
||||||
val videoUrlStart = client.newCall(
|
val videoUrlStart = client.newCall(
|
||||||
GET(
|
GET(
|
||||||
"https://$doodHost/pass_md5/$md5",
|
md5,
|
||||||
Headers.headersOf("referer", newUrl),
|
Headers.headersOf("referer", newUrl),
|
||||||
),
|
),
|
||||||
).execute().body.string()
|
).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()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,16 +58,27 @@ class DoodExtractor(private val client: OkHttpClient) {
|
||||||
redirect: Boolean = true,
|
redirect: Boolean = true,
|
||||||
): List<Video> {
|
): List<Video> {
|
||||||
val video = videoFromUrl(url, quality, redirect)
|
val video = videoFromUrl(url, quality, redirect)
|
||||||
return video?.let(::listOf) ?: emptyList<Video>()
|
return video?.let(::listOf) ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRandomString(length: Int = 10): String {
|
// Método para generar una cadena aleatoria
|
||||||
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
private fun createHashTable(): String {
|
||||||
return (1..length)
|
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||||
.map { allowedChars.random() }
|
return buildString {
|
||||||
.joinToString("")
|
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 {
|
private fun doodHeaders(host: String) = Headers.Builder().apply {
|
||||||
add("User-Agent", "Aniyomi")
|
add("User-Agent", "Aniyomi")
|
||||||
add("Referer", "https://$host/")
|
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.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.internal.commonEmptyHeaders
|
import okhttp3.internal.commonEmptyHeaders
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
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()
|
}.toList()
|
||||||
|
|
||||||
return masterPlaylist.substringAfter(PLAYLIST_SEPARATOR).split(PLAYLIST_SEPARATOR).mapNotNull {
|
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=")
|
val resolution = it.substringAfter("RESOLUTION=")
|
||||||
.substringBefore("\n")
|
.substringBefore("\n")
|
||||||
.substringAfter("x")
|
.substringAfter("x")
|
||||||
.substringBefore(",") + "p"
|
.substringBefore(",").let(::stnQuality)
|
||||||
|
|
||||||
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
||||||
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
||||||
|
@ -328,6 +335,13 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= 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 {
|
companion object {
|
||||||
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
|
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,48 @@
|
||||||
package eu.kanade.tachiyomi.lib.streamhidevidextractor
|
package eu.kanade.tachiyomi.lib.streamhidevidextractor
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import dev.datlag.jsunpacker.JsUnpacker
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
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> {
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
val page = client.newCall(GET(url)).execute().body.string()
|
|
||||||
val playlistUrl = (JsUnpacker.unpackAndCombine(page) ?: page)
|
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "StreamHideVid - $quality" }): List<Video> {
|
||||||
.substringAfter("sources:")
|
|
||||||
.substringAfter("file:\"") // StreamHide
|
val doc = client.newCall(GET(getEmbedUrl(url), headers)).execute().asJsoup()
|
||||||
.substringAfter("src:\"") // StreamVid
|
|
||||||
.substringBefore('"')
|
val scriptBody = doc.selectFirst("script:containsData(m3u8)")?.data()
|
||||||
if (!playlistUrl.startsWith("http")) return emptyList()
|
?.let { script ->
|
||||||
return playlistUtils.extractFromHls(playlistUrl,
|
if (script.contains("eval(function(p,a,c")) {
|
||||||
videoNameGen = { "${prefix}StreamHideVid - $it" }
|
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") }
|
?.takeIf { it.startsWith("http") }
|
||||||
?: return emptyList()
|
?: 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"
|
val quality = if (prefix.isNotBlank()) "$prefix Uqload" else "Uqload"
|
||||||
|
|
||||||
return listOf(Video(videoUrl, quality, videoUrl, videoHeaders))
|
return listOf(Video(videoUrl, quality, videoUrl, videoHeaders))
|
||||||
|
|
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 {
|
ext {
|
||||||
extName = 'Hikari'
|
extName = 'Hikari'
|
||||||
extClass = '.Hikari'
|
extClass = '.Hikari'
|
||||||
extVersionCode = 14
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -208,11 +208,17 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
override fun episodeListSelector() = "a[class~=ep-item]"
|
override fun episodeListSelector() = "a[class~=ep-item]"
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode {
|
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 {
|
return SEpisode.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("abs:href"))
|
setUrlWithoutDomain(element.attr("abs:href"))
|
||||||
episode_number = ep.toFloat()
|
episode_number = ep
|
||||||
name = "Ep. $ep - ${element.selectFirst(".ep-name")?.text() ?: ""}"
|
name = "Ep. $ep - $nameText"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'JavGG'
|
extName = 'JavGG'
|
||||||
extClass = '.Javgg'
|
extClass = '.Javgg'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,8 +154,7 @@ class Javgg : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
.build()
|
.build()
|
||||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
|
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
|
||||||
}
|
}
|
||||||
embedUrl.contains("vidhide") || embedUrl.contains("streamhide") ||
|
embedUrl.contains("vidhide") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideVidExtractor(client, headers).videosFromUrl(url)
|
||||||
embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideVidExtractor(client).videosFromUrl(url)
|
|
||||||
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
|
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
|
||||||
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
|
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
|
||||||
embedUrl.contains("turboplay") -> {
|
embedUrl.contains("turboplay") -> {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Jav Guru'
|
extName = 'Jav Guru'
|
||||||
extClass = '.JavGuru'
|
extClass = '.JavGuru'
|
||||||
extVersionCode = 19
|
extVersionCode = 24
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Sudatchi'
|
extName = 'Sudatchi'
|
||||||
extClass = '.Sudatchi'
|
extClass = '.Sudatchi'
|
||||||
extVersionCode = 5
|
extVersionCode = 10
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
|
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
|
||||||
|
|
||||||
private fun Int.parseStatus() = when (this) {
|
private fun Int.parseStatus() = when (this) {
|
||||||
1 -> SAnime.UNKNOWN // Not Yet Released
|
1 -> SAnime.LICENSED // Not Yet Released
|
||||||
2 -> SAnime.ONGOING
|
2 -> SAnime.ONGOING
|
||||||
3 -> SAnime.COMPLETED
|
3 -> SAnime.COMPLETED
|
||||||
else -> SAnime.UNKNOWN
|
else -> SAnime.UNKNOWN
|
||||||
|
@ -86,7 +86,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
val titleLang = preferences.title
|
val titleLang = preferences.title
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val data = document.parseAs<HomePageDto>().animeSpotlight
|
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 ===============================
|
// =============================== Latest ===============================
|
||||||
|
@ -96,7 +96,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
sudatchiFilters.fetchFilters()
|
sudatchiFilters.fetchFilters()
|
||||||
val titleLang = preferences.title
|
val titleLang = preferences.title
|
||||||
return response.parseAs<DirectoryDto>().let {
|
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,
|
videoUrl,
|
||||||
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
|
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
|
||||||
subtitleList = subtitles.map {
|
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(),
|
}.sort(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ data class SubtitleLangDto(
|
||||||
data class SubtitleDto(
|
data class SubtitleDto(
|
||||||
val url: String,
|
val url: String,
|
||||||
@SerialName("SubtitlesName")
|
@SerialName("SubtitlesName")
|
||||||
val subtitlesName: SubtitleLangDto,
|
val SubtitlesName: SubtitleLangDto,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Torrentio (Torrent / Debrid)'
|
extName = 'Torrentio (Torrent / Debrid)'
|
||||||
extClass = '.Torrentio'
|
extClass = '.Torrentio'
|
||||||
extVersionCode = 2
|
extVersionCode = 5
|
||||||
containsNsfw = false
|
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.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
@ -60,7 +59,12 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
{"query": "${query.replace("\n", "")}", "variables": $variables}
|
{"query": "${query.replace("\n", "")}", "variables": $variables}
|
||||||
""".trimIndent().toRequestBody("application/json; charset=utf-8".toMediaType())
|
""".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 ======================
|
// ============================== JustWatch Api Query ======================
|
||||||
|
@ -135,7 +139,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val content = node.content ?: return@mapNotNull null
|
val content = node.content ?: return@mapNotNull null
|
||||||
|
|
||||||
SAnime.create().apply {
|
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 ?: ""
|
title = content.title ?: ""
|
||||||
thumbnail_url = "https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}"
|
thumbnail_url = "https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}"
|
||||||
description = content.shortDescription ?: ""
|
description = content.shortDescription ?: ""
|
||||||
|
@ -155,7 +159,31 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
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 {
|
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 {
|
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
||||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||||
val id = query.removePrefix(PREFIX_SEARCH)
|
val id = query.removePrefix(PREFIX_SEARCH)
|
||||||
client.newCall(GET("$baseUrl/anime/$id", headers))
|
client.newCall(GET("$baseUrl/anime/$id"))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.use(::searchAnimeByIdParse)
|
.use(::searchAnimeByIdParse)
|
||||||
} else {
|
} else {
|
||||||
|
@ -198,7 +226,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"platform": "WEB",
|
"platform": "WEB",
|
||||||
"country": "$country",
|
"country": "$country",
|
||||||
"language": "$language",
|
"language": "$language",
|
||||||
"searchQuery": "${query.replace(searchQueryRegex, "").trim()}",
|
"searchQuery": "${query.replace(Regex("[^A-Za-z0-9 ]"), "").trim()}",
|
||||||
"packages": [$packages],
|
"packages": [$packages],
|
||||||
"objectTypes": [$objectTypes],
|
"objectTypes": [$objectTypes],
|
||||||
"popularTitlesSortBy": "TRENDING",
|
"popularTitlesSortBy": "TRENDING",
|
||||||
|
@ -212,10 +240,6 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
return makeGraphQLRequest(justWatchQuery(), variables)
|
return makeGraphQLRequest(justWatchQuery(), variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val searchQueryRegex by lazy {
|
|
||||||
Regex("[^A-Za-z0-9 ]")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
|
@ -288,18 +312,18 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val responseString = response.body.string()
|
val responseString = response.body.string()
|
||||||
val episodeList = json.decodeFromString<EpisodeList>(responseString)
|
val episodeList = json.decodeFromString<EpisodeList>(responseString)
|
||||||
return when (episodeList.meta?.type) {
|
return when (episodeList.meta?.type) {
|
||||||
"show" -> {
|
"series" -> {
|
||||||
episodeList.meta.videos
|
episodeList.meta.videos
|
||||||
?.let { 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 ->
|
?.map { video ->
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
episode_number = "${video.season}.${video.number}".toFloat()
|
episode_number = "${video.season}.${video.number}".toFloat()
|
||||||
url = "/stream/series/${video.id}.json"
|
url = "/stream/series/${video.id}.json"
|
||||||
date_upload = video.firstAired?.let { parseDate(it) } ?: 0L
|
date_upload = video.released?.let { parseDate(it) } ?: 0L
|
||||||
name = "S${video.season.toString().trim()}:E${video.number} - ${video.name}"
|
name = "S${video.season.toString().trim()}:E${video.number} - ${video.title}"
|
||||||
scanlator = (video.firstAired?.let { parseDate(it) } ?: 0L)
|
scanlator = (video.released?.let { parseDate(it) } ?: 0L)
|
||||||
.takeIf { it > System.currentTimeMillis() }
|
.takeIf { it > System.currentTimeMillis() }
|
||||||
?.let { "Upcoming" }
|
?.let { "Upcoming" }
|
||||||
?: ""
|
?: ""
|
||||||
|
@ -402,7 +426,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
udp://tracker.tiny-vps.com:6969/announce,
|
udp://tracker.tiny-vps.com:6969/announce,
|
||||||
udp://tracker.torrent.eu.org:451/announce,
|
udp://tracker.torrent.eu.org:451/announce,
|
||||||
udp://valakas.rollo.dnsabr.com:2710/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()
|
""".trimIndent()
|
||||||
|
|
||||||
return streamList.streams?.map { stream ->
|
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) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
// Debrid provider
|
// Debrid provider
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
|
@ -652,7 +688,10 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"🇫🇷 Torrent9",
|
"🇫🇷 Torrent9",
|
||||||
"🇪🇸 MejorTorrent",
|
"🇪🇸 MejorTorrent",
|
||||||
"🇲🇽 Cinecalidad",
|
"🇲🇽 Cinecalidad",
|
||||||
|
"🇮🇹 ilCorsaroNero",
|
||||||
|
"🇪🇸 Wolfmax4k",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val PREF_PROVIDERS_VALUE = arrayOf(
|
private val PREF_PROVIDERS_VALUE = arrayOf(
|
||||||
"yts",
|
"yts",
|
||||||
"eztv",
|
"eztv",
|
||||||
|
@ -673,6 +712,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"torrent9",
|
"torrent9",
|
||||||
"mejortorrent",
|
"mejortorrent",
|
||||||
"cinecalidad",
|
"cinecalidad",
|
||||||
|
"ilcorsaronero",
|
||||||
|
"wolfmax4k",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
|
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()
|
private val PREF_PROVIDERS_DEFAULT = PREF_DEFAULT_PROVIDERS_VALUE.toSet()
|
||||||
|
|
||||||
// Qualities/Resolutions
|
// / Qualities/Resolutions
|
||||||
private const val PREF_QUALITY_KEY = "quality_selection"
|
private const val PREF_QUALITY_KEY = "quality_selection"
|
||||||
private val PREF_QUALITY = arrayOf(
|
private val PREF_QUALITY = arrayOf(
|
||||||
"BluRay REMUX",
|
"BluRay REMUX",
|
||||||
"HDR/HDR10+/Dolby Vision",
|
"HDR/HDR10+/Dolby Vision",
|
||||||
"Dolby Vision",
|
"Dolby Vision",
|
||||||
|
"Dolby Vision + HDR",
|
||||||
|
"3D",
|
||||||
|
"Non 3D (DO NOT SELECT IF NOT SURE)",
|
||||||
"4k",
|
"4k",
|
||||||
"1080p",
|
"1080p",
|
||||||
"720p",
|
"720p",
|
||||||
|
@ -706,10 +750,14 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"Cam",
|
"Cam",
|
||||||
"Unknown",
|
"Unknown",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val PREF_QUALITY_VALUE = arrayOf(
|
private val PREF_QUALITY_VALUE = arrayOf(
|
||||||
"brremux",
|
"brremux",
|
||||||
"hdrall",
|
"hdrall",
|
||||||
"dolbyvision",
|
"dolbyvision",
|
||||||
|
"dolbyvisionwithhdr",
|
||||||
|
"threed",
|
||||||
|
"nonthreed",
|
||||||
"4k",
|
"4k",
|
||||||
"1080p",
|
"1080p",
|
||||||
"720p",
|
"720p",
|
||||||
|
@ -832,7 +880,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
private val PREF_LANG_DEFAULT = setOf<String>()
|
private val PREF_LANG_DEFAULT = setOf<String>()
|
||||||
|
|
||||||
private const val UPCOMING_EP_KEY = "upcoming_ep"
|
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_KEY = "dubbed"
|
||||||
private const val IS_DUB_DEFAULT = false
|
private const val IS_DUB_DEFAULT = false
|
||||||
|
|
|
@ -110,6 +110,6 @@ class EpisodeVideo(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val season: Int? = null,
|
val season: Int? = null,
|
||||||
val number: Int? = null,
|
val number: Int? = null,
|
||||||
val firstAired: String? = null,
|
val released: String? = null,
|
||||||
val name: String? = null,
|
val title: String? = null,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Torrentio Anime (Torrent / Debrid)'
|
extName = 'Torrentio Anime (Torrent / Debrid)'
|
||||||
extClass = '.Torrentio'
|
extClass = '.Torrentio'
|
||||||
extVersionCode = 11
|
extVersionCode = 14
|
||||||
containsNsfw = false
|
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.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
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.FormBody
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.json.JSONObject
|
import org.jsoup.Jsoup
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URL
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -62,93 +66,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
.add("query", query)
|
.add("query", query)
|
||||||
.add("variables", variables)
|
.add("variables", variables)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return POST("https://graphql.anilist.co", body = requestBody)
|
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 {
|
private fun parseSearchJson(jsonLine: String?, isLatestQuery: Boolean = false): AnimesPage {
|
||||||
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
||||||
val metaData: Any = if (!isLatestQuery) {
|
val metaData: Any = if (!isLatestQuery) {
|
||||||
|
@ -218,7 +138,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
{
|
{
|
||||||
"page": $page,
|
"page": $page,
|
||||||
"perPage": 30,
|
"perPage": 30,
|
||||||
"sort": "TRENDING_DESC"
|
"sort": "TRENDING_DESC",
|
||||||
|
"status": ["FINISHED", "RELEASING"]
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
@ -227,8 +148,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
val jsonData = response.body.string()
|
val jsonData = response.body.string()
|
||||||
return parseSearchJson(jsonData)
|
return parseSearchJson(jsonData) }
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
@ -261,67 +181,73 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||||
val details = animeDetailsParse(response).apply {
|
val details = animeDetailsParse(response)
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val variables = """
|
val params = AniListFilters.getSearchParameters(filters)
|
||||||
{
|
val variablesObject = buildJsonObject {
|
||||||
"page": $page,
|
put("page", page)
|
||||||
"perPage": 30,
|
put("perPage", 30)
|
||||||
"sort": "POPULARITY_DESC",
|
put("sort", params.sort)
|
||||||
"search": "$query"
|
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)
|
return makeGraphQLRequest(anilistQuery(), variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
||||||
|
|
||||||
|
// ============================== Filters ===============================
|
||||||
|
|
||||||
|
override fun getFilterList(): AnimeFilterList = AniListFilters.FILTER_LIST
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
|
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
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 variables = """{"id": ${anime.url}}"""
|
||||||
|
|
||||||
val metaData = runCatching {
|
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
|
}.getOrNull()?.data?.media
|
||||||
|
|
||||||
anime.title = metaData?.title?.let { title ->
|
anime.title = metaData?.title?.let { title ->
|
||||||
|
@ -334,10 +260,24 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
anime.thumbnail_url = metaData?.coverImage?.extraLarge
|
anime.thumbnail_url = metaData?.coverImage?.extraLarge
|
||||||
anime.description = metaData?.description
|
|
||||||
?.replace(Regex("<br><br>"), "\n")
|
anime.description = buildString {
|
||||||
?.replace(Regex("<.*?>"), "")
|
append(
|
||||||
?: "No Description"
|
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) {
|
anime.status = when (metaData?.status) {
|
||||||
"RELEASING" -> SAnime.ONGOING
|
"RELEASING" -> SAnime.ONGOING
|
||||||
|
@ -360,9 +300,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
// ============================== Episodes ==============================
|
||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
val res = URL("https://api.ani.zip/mappings?anilist_id=${anime.url}").readText()
|
return GET("https://anime-kitsu.strem.fun/meta/series/anilist%3A${anime.url}.json")
|
||||||
val kitsuId = JSONObject(res).getJSONObject("mappings").getInt("kitsu_id").toString()
|
|
||||||
return GET("https://anime-kitsu.strem.fun/meta/series/kitsu%3A$kitsuId.json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
@ -375,7 +313,6 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
?.let { videos ->
|
?.let { videos ->
|
||||||
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
|
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 ->
|
?.map { video ->
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
episode_number = video.episode?.toFloat() ?: 0.0F
|
episode_number = video.episode?.toFloat() ?: 0.0F
|
||||||
|
@ -481,9 +418,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
udp://tracker.tiny-vps.com:6969/announce,
|
udp://tracker.tiny-vps.com:6969/announce,
|
||||||
udp://tracker.torrent.eu.org:451/announce,
|
udp://tracker.torrent.eu.org:451/announce,
|
||||||
udp://valakas.rollo.dnsabr.com:2710/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()
|
""".trimIndent()
|
||||||
|
|
||||||
return streamList.streams?.map { stream ->
|
return streamList.streams?.map { stream ->
|
||||||
val urlOrHash =
|
val urlOrHash =
|
||||||
if (debridProvider == "none") {
|
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) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
// Debrid provider
|
// Debrid provider
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
|
@ -714,7 +662,10 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"🇫🇷 Torrent9",
|
"🇫🇷 Torrent9",
|
||||||
"🇪🇸 MejorTorrent",
|
"🇪🇸 MejorTorrent",
|
||||||
"🇲🇽 Cinecalidad",
|
"🇲🇽 Cinecalidad",
|
||||||
|
"🇮🇹 ilCorsaroNero",
|
||||||
|
"🇪🇸 Wolfmax4k",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val PREF_PROVIDERS_VALUE = arrayOf(
|
private val PREF_PROVIDERS_VALUE = arrayOf(
|
||||||
"yts",
|
"yts",
|
||||||
"eztv",
|
"eztv",
|
||||||
|
@ -735,6 +686,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"torrent9",
|
"torrent9",
|
||||||
"mejortorrent",
|
"mejortorrent",
|
||||||
"cinecalidad",
|
"cinecalidad",
|
||||||
|
"ilcorsaronero",
|
||||||
|
"wolfmax4k",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
|
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
|
||||||
|
@ -759,6 +712,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"BluRay REMUX",
|
"BluRay REMUX",
|
||||||
"HDR/HDR10+/Dolby Vision",
|
"HDR/HDR10+/Dolby Vision",
|
||||||
"Dolby Vision",
|
"Dolby Vision",
|
||||||
|
"Dolby Vision + HDR",
|
||||||
|
"3D",
|
||||||
|
"Non 3D (DO NOT SELECT IF NOT SURE)",
|
||||||
"4k",
|
"4k",
|
||||||
"1080p",
|
"1080p",
|
||||||
"720p",
|
"720p",
|
||||||
|
@ -768,10 +724,14 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
"Cam",
|
"Cam",
|
||||||
"Unknown",
|
"Unknown",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val PREF_QUALITY_VALUE = arrayOf(
|
private val PREF_QUALITY_VALUE = arrayOf(
|
||||||
"brremux",
|
"brremux",
|
||||||
"hdrall",
|
"hdrall",
|
||||||
"dolbyvision",
|
"dolbyvision",
|
||||||
|
"dolbyvisionwithhdr",
|
||||||
|
"threed",
|
||||||
|
"nonthreed",
|
||||||
"4k",
|
"4k",
|
||||||
"1080p",
|
"1080p",
|
||||||
"720p",
|
"720p",
|
||||||
|
|
|
@ -73,7 +73,11 @@ data class AnilistMedia(
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
val tags: List<AnilistTag>? = null,
|
val tags: List<AnilistTag>? = null,
|
||||||
val genres: List<String>? = null,
|
val genres: List<String>? = null,
|
||||||
|
val episodes: Int? = null,
|
||||||
|
val format: String? = null,
|
||||||
val studios: AnilistStudios? = null,
|
val studios: AnilistStudios? = null,
|
||||||
|
val season: String? = null,
|
||||||
|
val seasonYear: Int? = null,
|
||||||
val countryOfOrigin: String? = null,
|
val countryOfOrigin: String? = null,
|
||||||
val isAdult: Boolean = false,
|
val isAdult: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeToast'
|
extName = 'AnimeToast'
|
||||||
extClass = '.AnimeToast'
|
extClass = '.AnimeToast'
|
||||||
extVersionCode = 16
|
extVersionCode = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -166,7 +166,6 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
DoodExtractor(client).videoFromUrl(
|
DoodExtractor(client).videoFromUrl(
|
||||||
link,
|
link,
|
||||||
quality,
|
quality,
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
videoList.add(video)
|
videoList.add(video)
|
||||||
|
@ -224,7 +223,7 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
) == true -> {
|
) == true -> {
|
||||||
val quality = "DoodStream"
|
val quality = "DoodStream"
|
||||||
val video =
|
val video =
|
||||||
DoodExtractor(client).videoFromUrl(link, quality, false)
|
DoodExtractor(client).videoFromUrl(link, quality)
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
videoList.add(video)
|
videoList.add(video)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Serienstream'
|
extName = 'Serienstream'
|
||||||
extClass = '.Serienstream'
|
extClass = '.Serienstream'
|
||||||
extVersionCode = 20
|
extVersionCode = 23
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -37,7 +37,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override val name = "Serienstream"
|
override val name = "Serienstream"
|
||||||
|
|
||||||
override val baseUrl = "https://s.to"
|
override val baseUrl = "http://186.2.175.5"
|
||||||
|
|
||||||
override val lang = "de"
|
override val lang = "de"
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val headers = Headers.Builder()
|
val headers = Headers.Builder()
|
||||||
.add("Referer", "https://s.to/search")
|
.add("Referer", "http://186.2.175.5/search")
|
||||||
.add("origin", baseUrl)
|
.add("origin", baseUrl)
|
||||||
.add("connection", "keep-alive")
|
.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")
|
.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")
|
||||||
|
|
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 = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
ext {
|
||||||
extName = 'Wcofun'
|
extName = 'Wcofun'
|
||||||
extClass = '.Wcofun'
|
extClass = '.Wcofun'
|
||||||
extVersionCode = 13
|
extVersionCode = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -99,13 +99,13 @@ class Wcofun : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
val epName = element.ownText()
|
val title = element.attr("title")
|
||||||
val season = epName.substringAfter("Season ")
|
val season = title.substringAfter("Season ").substringBefore(" ")
|
||||||
val ep = epName.substringAfter("Episode ")
|
val episode = title.substringAfter("Episode ").substringBefore(" ")
|
||||||
val seasonNum = season.substringBefore(" ").toIntOrNull() ?: 1
|
val seasonNum = season.toIntOrNull() ?: 1
|
||||||
val epNum = ep.substringBefore(" ").toIntOrNull() ?: 1
|
val episodeNum = episode.toIntOrNull() ?: 1
|
||||||
episode_number = (seasonNum * 100 + epNum).toFloat()
|
episode_number = ((seasonNum - 1) * 100 + episodeNum).toFloat()
|
||||||
name = "Season $seasonNum - Episode $epNum"
|
name = "Season $seasonNum - Episode $episodeNum"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
|
|
22
src/en/yugenanime/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=".en.yugenanime.YugenAnimeUrlActivity"
|
||||||
|
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="yugenanime.sx"
|
||||||
|
android:pathPattern="/anime/..*"
|
||||||
|
android:scheme="https" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
7
src/en/yugenanime/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
ext {
|
||||||
|
extName = 'YugenAnime'
|
||||||
|
extClass = '.YugenAnime'
|
||||||
|
extVersionCode = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/yugenanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
src/en/yugenanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/en/yugenanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/en/yugenanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
src/en/yugenanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 33 KiB |
|
@ -0,0 +1,406 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.en.yugenanime
|
||||||
|
|
||||||
|
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.parseAs
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.net.URI
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class YugenAnime : ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "YugenAnime"
|
||||||
|
|
||||||
|
override val baseUrl = "https://yugenanime.sx"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = OkHttpClient()
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
|
val url = "$baseUrl/discover/?page=$page"
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeSelector(): String = "div.cards-grid a.anime-meta"
|
||||||
|
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.title = element.attr("title").ifBlank { element.select("span.anime-name").text() }
|
||||||
|
anime.setUrlWithoutDomain(element.attr("href"))
|
||||||
|
anime.thumbnail_url = element.selectFirst("img.lozad")?.attr("data-src")
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector(): String = "div.sidepanel--content > nav > ul > li:nth-child(7) > a"
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val url = "$baseUrl/discover/?page=$page&sort=Newest+Addition"
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String = "div.cards-grid a.anime-meta"
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.title = element.attr("title").ifBlank { element.select("span.anime-name").text() }
|
||||||
|
anime.setUrlWithoutDomain(element.attr("href"))
|
||||||
|
anime.thumbnail_url = element.selectFirst("img.lozad")?.attr("data-src")
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String = "ul.pagination li.next a"
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
val genreFilter = filterList.find { it is GenreFilter } as? GenreFilter
|
||||||
|
val sortFilter = filterList.find { it is SortFilter } as? SortFilter
|
||||||
|
val statusFilter = filterList.find { it is StatusFilter } as? StatusFilter
|
||||||
|
val yearFilter = filterList.find { it is YearFilter } as? YearFilter
|
||||||
|
val languageFilter = filterList.find { it is LanguageFilter } as? LanguageFilter
|
||||||
|
|
||||||
|
val queryString = mutableListOf<String>()
|
||||||
|
|
||||||
|
genreFilter?.let {
|
||||||
|
val genrePart = it.toUriPart()
|
||||||
|
if (genrePart.isNotBlank()) {
|
||||||
|
queryString.add(genrePart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortFilter?.let { if (it.state != 0) queryString.add(it.toUriPart()) }
|
||||||
|
statusFilter?.let { if (it.state != 0) queryString.add(it.toUriPart()) }
|
||||||
|
yearFilter?.let { if (it.state != 0) queryString.add(it.toUriPart()) }
|
||||||
|
languageFilter?.let { if (it.state != 0) queryString.add(it.toUriPart()) }
|
||||||
|
|
||||||
|
val url = when {
|
||||||
|
query.isNotBlank() -> "$baseUrl/discover/?page=$page&q=$query${if (queryString.isNotEmpty()) "&${queryString.joinToString("&")}" else ""}"
|
||||||
|
queryString.isNotEmpty() -> "$baseUrl/discover/?page=$page&${queryString.joinToString("&")}"
|
||||||
|
else -> "$baseUrl/discover/?page=$page"
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StatusFilter : UriPartFilter(
|
||||||
|
"Status",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", ""),
|
||||||
|
Pair("Not yet aired", "status=Not+yet+aired"),
|
||||||
|
Pair("Currently Airing", "status=Currently+Airing"),
|
||||||
|
Pair("Finished Airing", "status=Finished+Airing"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class YearFilter : UriPartFilter(
|
||||||
|
"Year",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", ""),
|
||||||
|
Pair("2024", "year=2024"),
|
||||||
|
Pair("2023", "year=2023"),
|
||||||
|
Pair("2022", "year=2022"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class LanguageFilter : UriPartFilter(
|
||||||
|
"Language",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Both", ""),
|
||||||
|
Pair("Sub", "language=Sub"),
|
||||||
|
Pair("Dub", "language=Dub"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class GenreFilter : CheckBoxFilterList(
|
||||||
|
"Genres",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Action", "genreIncluded=Action"),
|
||||||
|
Pair("Adventure", "genreIncluded=Adventure"),
|
||||||
|
Pair("Comedy", "genreIncluded=Comedy"),
|
||||||
|
Pair("Drama", "genreIncluded=Drama"),
|
||||||
|
Pair("Ecchi", "genreIncluded=Ecchi"),
|
||||||
|
Pair("Fantasy", "genreIncluded=Fantasy"),
|
||||||
|
Pair("Harem", "genreIncluded=Harem"),
|
||||||
|
Pair("Historical", "genreIncluded=Historical"),
|
||||||
|
Pair("Horror", "genreIncluded=Horror"),
|
||||||
|
Pair("Magic", "genreIncluded=Magic"),
|
||||||
|
Pair("Martial Arts", "genreIncluded=Martial+Arts"),
|
||||||
|
Pair("Mecha", "genreIncluded=Mecha"),
|
||||||
|
Pair("Military", "genreIncluded=Military"),
|
||||||
|
Pair("Music", "genreIncluded=Music"),
|
||||||
|
Pair("Mystery", "genreIncluded=Mystery"),
|
||||||
|
Pair("Parody", "genreIncluded=Parody"),
|
||||||
|
Pair("Police", "genreIncluded=Police"),
|
||||||
|
Pair("Psychological", "genreIncluded=Psychological"),
|
||||||
|
Pair("Romance", "genreIncluded=Romance"),
|
||||||
|
Pair("Samurai", "genreIncluded=Samurai"),
|
||||||
|
Pair("School", "genreIncluded=School"),
|
||||||
|
Pair("Sci-Fi", "genreIncluded=Sci-Fi"),
|
||||||
|
Pair("Seinen", "genreIncluded=Seinen"),
|
||||||
|
Pair("Shoujo", "genreIncluded=Shoujo"),
|
||||||
|
Pair("Shoujo Ai", "genreIncluded=Shoujo+Ai"),
|
||||||
|
Pair("Shounen", "genreIncluded=Shounen"),
|
||||||
|
Pair("Shounen Ai", "genreIncluded=Shounen+Ai"),
|
||||||
|
Pair("Slice of Life", "genreIncluded=Slice+of+Life"),
|
||||||
|
Pair("Space", "genreIncluded=Space"),
|
||||||
|
Pair("Sports", "genreIncluded=Sports"),
|
||||||
|
Pair("Super Power", "genreIncluded=Super+Power"),
|
||||||
|
Pair("Supernatural", "genreIncluded=Supernatural"),
|
||||||
|
Pair("Thriller", "genreIncluded=Thriller"),
|
||||||
|
Pair("Vampire", "genreIncluded=Vampire"),
|
||||||
|
Pair("Yaoi", "genreIncluded=Yaoi"),
|
||||||
|
Pair("Yuri", "genreIncluded=Yuri"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private open class CheckBoxFilterList(name: String, pairs: Array<Pair<String, String>>) :
|
||||||
|
AnimeFilter.Group<CheckBoxFilterList.CheckBoxVal>(name, pairs.map { CheckBoxVal(it.first, false, it.second) }) {
|
||||||
|
|
||||||
|
fun toUriPart(): String {
|
||||||
|
return state.filter { it.state }.joinToString("&") { it.uriPart }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CheckBoxVal(displayName: String, defaultState: Boolean, val uriPart: String) :
|
||||||
|
CheckBox(displayName, defaultState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SortFilter : UriPartFilter(
|
||||||
|
"Sort By",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Default", ""),
|
||||||
|
Pair("Newest Addition", "sort=Newest+Addition"),
|
||||||
|
Pair("Oldest Addition", "sort=Oldest+Addition"),
|
||||||
|
Pair("Alphabetical", "sort=Alphabetical"),
|
||||||
|
Pair("Rating", "sort=Rating"),
|
||||||
|
Pair("Views", "sort=Views"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||||
|
AnimeFilter.Header("Text search ignores filters"),
|
||||||
|
GenreFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
YearFilter(),
|
||||||
|
LanguageFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun searchAnimeSelector(): String {
|
||||||
|
return "div.cards-grid a.anime-meta"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element): Video {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.title = element.attr("title").ifBlank { element.select("span.anime-name").text() }
|
||||||
|
anime.setUrlWithoutDomain(element.attr("href"))
|
||||||
|
anime.thumbnail_url = (element.selectFirst("img.lozad")?.attr("data-src"))
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector(): String = "ul.pagination li.next a"
|
||||||
|
|
||||||
|
// =========================== Anime Details ============================
|
||||||
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.title = document.selectFirst("div.content h1")?.text().orEmpty()
|
||||||
|
anime.thumbnail_url = document.selectFirst("img.cover")?.attr("src")
|
||||||
|
|
||||||
|
val metaDetails = document.select("div.anime-metadetails div.data")
|
||||||
|
metaDetails.forEach { data ->
|
||||||
|
val title = data.selectFirst("div.ap--data-title")?.text()
|
||||||
|
val description = data.selectFirst("span.description")?.text()
|
||||||
|
|
||||||
|
when (title) {
|
||||||
|
"Romaji" -> anime.title = description.orEmpty()
|
||||||
|
"Studios" -> anime.author = description.orEmpty()
|
||||||
|
"Status" -> anime.status = parseStatus(description.orEmpty())
|
||||||
|
"Genres" -> anime.genre = description.orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anime.description = document.select("p.description").text()
|
||||||
|
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String): Int {
|
||||||
|
return when (status.lowercase()) {
|
||||||
|
"finished airing" -> SAnime.COMPLETED
|
||||||
|
"currently airing" -> SAnime.ONGOING
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Episodes ==============================
|
||||||
|
override fun episodeListSelector(): String = "ul.ep-grid li.ep-card"
|
||||||
|
|
||||||
|
private fun episodeListRequest(anime: SAnime, page: Int): Request {
|
||||||
|
val url = "$baseUrl${anime.url}watch/?page=$page"
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode {
|
||||||
|
val episode = SEpisode.create()
|
||||||
|
val title = element.select("a.ep-title").text()
|
||||||
|
val link = fixUrl(element.select("a.ep-title").attr("href"))
|
||||||
|
val dateElement = element.selectFirst("time[datetime]")
|
||||||
|
val releaseDate = dateElement?.attr("datetime") ?: ""
|
||||||
|
|
||||||
|
val date = try {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(releaseDate)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val episodeNumber = title.substringBefore(":").filter { it.isDigit() }.toIntOrNull()
|
||||||
|
|
||||||
|
episode.setUrlWithoutDomain(link)
|
||||||
|
episode.name = title
|
||||||
|
episode.episode_number = episodeNumber?.toFloat() ?: 0F
|
||||||
|
episode.date_upload = date?.time ?: 0
|
||||||
|
|
||||||
|
return episode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.url = response.request.url.encodedPath
|
||||||
|
return fetchAllEpisodes(anime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixUrl(url: String?): String {
|
||||||
|
return when {
|
||||||
|
url == null -> ""
|
||||||
|
url.startsWith("http") -> url
|
||||||
|
url.startsWith("//") -> "https:$url"
|
||||||
|
url.startsWith("/") -> "$baseUrl$url"
|
||||||
|
else -> "$baseUrl/$url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchAllEpisodes(anime: SAnime, page: Int = 1, episodes: MutableList<SEpisode> = mutableListOf()): List<SEpisode> {
|
||||||
|
val response = client.newCall(episodeListRequest(anime, page)).execute()
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val newEpisodes = document.select(episodeListSelector()).map { element -> episodeFromElement(element) }
|
||||||
|
episodes.addAll(newEpisodes)
|
||||||
|
|
||||||
|
val hasNextPage = document.select("ul.pagination li a:contains(Next)").isNotEmpty()
|
||||||
|
return if (hasNextPage) {
|
||||||
|
fetchAllEpisodes(anime, page + 1, episodes)
|
||||||
|
} else {
|
||||||
|
episodes.sortedByDescending { it.episode_number }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val data = response.request.url.toString()
|
||||||
|
val episode = data.removeSuffix("/").split("/").last()
|
||||||
|
val dubData = data.substringBeforeLast("/$episode").let { "$it-dub/$episode" }
|
||||||
|
val api = "$baseUrl/api/embed/"
|
||||||
|
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
|
||||||
|
listOf(data, dubData).forEach { url ->
|
||||||
|
val doc = client.newCall(GET(url)).execute().asJsoup()
|
||||||
|
val iframe = doc.select("iframe#main-embed").attr("src") ?: return@forEach
|
||||||
|
val id = iframe.removeSuffix("/").split("/").lastOrNull() ?: return@forEach
|
||||||
|
val sourceResponse = client.newCall(
|
||||||
|
POST(
|
||||||
|
api,
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("id", id)
|
||||||
|
.add("ac", "0")
|
||||||
|
.build(),
|
||||||
|
headers = headers.newBuilder()
|
||||||
|
.add("Miru-Url", api)
|
||||||
|
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||||
|
.add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.add("Referer", "$baseUrl/e/$id/")
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
).execute().body.string()
|
||||||
|
|
||||||
|
val source = sourceResponse.parseAs<Sources>().hls?.distinct()?.firstOrNull() ?: return@forEach
|
||||||
|
val isDub = if (url.contains("-dub")) "dub" else "sub"
|
||||||
|
val sourceType = getSourceType(getBaseUrl(source))
|
||||||
|
|
||||||
|
videoList.add(
|
||||||
|
Video(
|
||||||
|
source,
|
||||||
|
"$sourceType [$isDub]",
|
||||||
|
source,
|
||||||
|
headers = headers,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseUrl(url: String): String {
|
||||||
|
return URI(url).let {
|
||||||
|
"${it.scheme}://${it.host}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSourceType(url: String): String {
|
||||||
|
return when {
|
||||||
|
url.contains("cache", true) -> "Cache"
|
||||||
|
url.contains("allanime", true) -> "Crunchyroll-AL"
|
||||||
|
|
||||||
|
else -> Regex("\\.(\\S+)\\.").find(url)?.groupValues?.getOrNull(1)?.let { fixTitle(it) } ?: this.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixTitle(title: String): String {
|
||||||
|
return title.replace("_", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListSelector(): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Sources(
|
||||||
|
@SerialName("hls")
|
||||||
|
val hls: List<String>? = null,
|
||||||
|
)
|
||||||
|
companion object {
|
||||||
|
const val PREFIX_SEARCH = "id:"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.en.yugenanime
|
||||||
|
|
||||||
|
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://yugenanime.sx/anime/<item> intents
|
||||||
|
* and redirects them to the main Aniyomi process.
|
||||||
|
*/
|
||||||
|
class YugenAnimeUrlActivity : 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", "${YugenAnime.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)
|
||||||
|
}
|
||||||
|
}
|
17
src/es/animebum/build.gradle
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
ext {
|
||||||
|
extName = 'AnimeBum'
|
||||||
|
extClass = '.AnimeBum'
|
||||||
|
extVersionCode = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:okru-extractor'))
|
||||||
|
implementation(project(':lib:streamwish-extractor'))
|
||||||
|
implementation(project(':lib:universal-extractor'))
|
||||||
|
implementation(project(':lib:streamhidevid-extractor'))
|
||||||
|
implementation(project(':lib:vidguard-extractor'))
|
||||||
|
implementation(project(':lib:filemoon-extractor'))
|
||||||
|
implementation(project(':lib:gdriveplayer-extractor'))
|
||||||
|
}
|
BIN
src/es/animebum/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/es/animebum/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/es/animebum/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/es/animebum/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/es/animebum/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,356 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.es.animebum
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
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.Video
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class AnimeBum : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "AnimeBum"
|
||||||
|
|
||||||
|
override val baseUrl = "https://www.animebum.net"
|
||||||
|
|
||||||
|
override val lang = "es"
|
||||||
|
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/series?page=$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeSelector(): String = "article.serie"
|
||||||
|
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
|
||||||
|
// Extraer el título y enlace
|
||||||
|
val titleElement = element.selectFirst("div.title h3 a")
|
||||||
|
anime.title = titleElement?.attr("title") ?: "Sin título"
|
||||||
|
anime.setUrlWithoutDomain(titleElement?.attr("href") ?: "")
|
||||||
|
// Extraer la imagen
|
||||||
|
val imageElement = element.selectFirst("figure.image img")
|
||||||
|
anime.thumbnail_url = imageElement?.attr("src") ?: ""
|
||||||
|
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector(): String {
|
||||||
|
return "ul.pagination li a[rel=next]"
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||||
|
|
||||||
|
return when {
|
||||||
|
query.isNotBlank() -> GET("$baseUrl/search?s=$query&page=$page", headers)
|
||||||
|
genreFilter.state != 0 -> GET("$baseUrl/${genreFilter.toUriPart()}?page=$page", headers)
|
||||||
|
else -> popularAnimeRequest(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val animes = document.select(searchAnimeSelector()).map { searchAnimeFromElement(it) }
|
||||||
|
val hasNextPage = searchAnimeNextPageSelector().let { selector ->
|
||||||
|
document.select(selector).firstOrNull() != null
|
||||||
|
}
|
||||||
|
return AnimesPage(animes, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeSelector(): String {
|
||||||
|
return "div.search-results__item"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
|
||||||
|
val titleElement = element.selectFirst("div.search-results__left a h2")
|
||||||
|
anime.title = titleElement?.text().orEmpty()
|
||||||
|
|
||||||
|
val urlElement = element.selectFirst("div.search-results__left a")
|
||||||
|
anime.setUrlWithoutDomain(urlElement?.attr("href").orEmpty())
|
||||||
|
|
||||||
|
val imgElement = element.selectFirst("div.search-results__img a img")
|
||||||
|
anime.thumbnail_url = imgElement?.attr("src").orEmpty()
|
||||||
|
|
||||||
|
val descriptionElement = element.selectFirst("div.search-results__left div.description")
|
||||||
|
anime.description = descriptionElement?.text().orEmpty()
|
||||||
|
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector(): String {
|
||||||
|
return "a.next.page-numbers"
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== Anime Details ============================
|
||||||
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
|
||||||
|
val synopsisElement = document.selectFirst("div.description p")
|
||||||
|
anime.description = synopsisElement?.text() ?: "Sin sinopsis"
|
||||||
|
|
||||||
|
val yearElement = document.selectFirst("p.datos-serie strong:contains(Año)")
|
||||||
|
anime.genre = yearElement?.text() ?: ""
|
||||||
|
// sie es fin o emison la clase
|
||||||
|
val statusElement = if (document.selectFirst("p.datos-serie strong.emision") != null) {
|
||||||
|
document.selectFirst("p.datos-serie strong.emision")
|
||||||
|
} else {
|
||||||
|
document.selectFirst("p.datos-serie strong.fin")
|
||||||
|
}
|
||||||
|
anime.status = parseStatus(statusElement?.text() ?: "")
|
||||||
|
|
||||||
|
val genresElement = document.select("div.boom-categories a")
|
||||||
|
anime.genre = genresElement.joinToString(", ") { it.text() }
|
||||||
|
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String): Int {
|
||||||
|
return when (status) {
|
||||||
|
"En emisión" -> SAnime.ONGOING
|
||||||
|
"Finalizado" -> SAnime.COMPLETED
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Episodes ==============================
|
||||||
|
override fun episodeListSelector(): String {
|
||||||
|
return "ul.list-episodies li"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode {
|
||||||
|
val episode = SEpisode.create()
|
||||||
|
|
||||||
|
val episodeUrl = element.selectFirst("a")?.attr("href").orEmpty()
|
||||||
|
val episodeTitle = element.selectFirst("a")?.ownText()?.trim().orEmpty()
|
||||||
|
val episodeNumber = Regex("""Episodio (\d+)""").find(episodeTitle)?.groupValues?.get(1)?.toFloatOrNull()
|
||||||
|
|
||||||
|
episode.setUrlWithoutDomain(episodeUrl)
|
||||||
|
episode.name = episodeTitle
|
||||||
|
episode.episode_number = episodeNumber ?: 1F
|
||||||
|
|
||||||
|
return episode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return document.select(episodeListSelector()).map { episodeFromElement(it) }.sortedByDescending { it.episode_number }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Extractor ==========================
|
||||||
|
|
||||||
|
private val vidHideExtractor by lazy { StreamHideVidExtractor(client, headers) }
|
||||||
|
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||||
|
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||||
|
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
|
||||||
|
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val scriptContent = document.select("script:containsData(var video = [])").firstOrNull()?.data()
|
||||||
|
?: return videoList
|
||||||
|
|
||||||
|
val iframeRegex = """video\[\d+\]\s*=\s*['"]<iframe[^>]+src=["']([^"']+)["']""".toRegex()
|
||||||
|
val matches = iframeRegex.findAll(scriptContent)
|
||||||
|
|
||||||
|
for (match in matches) {
|
||||||
|
var videoUrl = match.groupValues[1]
|
||||||
|
|
||||||
|
if (videoUrl.startsWith("//")) {
|
||||||
|
videoUrl = "https:$videoUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
val vidHideDomains = listOf("vidhide", "VidHidePro", "luluvdo", "vidhideplus")
|
||||||
|
|
||||||
|
val video = when {
|
||||||
|
vidHideDomains.any { videoUrl.contains(it, ignoreCase = true) } -> vidHideExtractor.videosFromUrl(videoUrl)
|
||||||
|
"drive.google" in videoUrl -> {
|
||||||
|
val newUrl = "https://gdriveplayer.to/embed2.php?link=$videoUrl"
|
||||||
|
Log.d("AnimeBum", "New URL: $newUrl")
|
||||||
|
gdrivePlayerExtractor.videosFromUrl(newUrl, "GdrivePlayer", headers)
|
||||||
|
}
|
||||||
|
videoUrl.contains("streamwish") -> streamWishExtractor.videosFromUrl(videoUrl)
|
||||||
|
videoUrl.contains("ok.ru") -> okruExtractor.videosFromUrl(videoUrl)
|
||||||
|
videoUrl.contains("listeamed") -> vidGuardExtractor.videosFromUrl(videoUrl)
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
videoList.addAll(video)
|
||||||
|
}
|
||||||
|
return videoList.sortedByDescending { it.quality }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListSelector(): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element): Video {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Filters =============================
|
||||||
|
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||||
|
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||||
|
GenreFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class GenreFilter : UriPartFilter(
|
||||||
|
"Género",
|
||||||
|
arrayOf(
|
||||||
|
Pair("<Seleccionar>", ""),
|
||||||
|
Pair("Acción", "genero/accion"),
|
||||||
|
Pair("Aventura", "genero/aventura"),
|
||||||
|
Pair("Ciencia Ficción", "genero/ciencia-ficcion"),
|
||||||
|
Pair("Comedia", "genero/comedia"),
|
||||||
|
Pair("Drama", "genero/drama"),
|
||||||
|
Pair("Terror", "genero/terror"),
|
||||||
|
Pair("Suspenso", "genero/suspenso"),
|
||||||
|
Pair("Romance", "genero/romance"),
|
||||||
|
Pair("Magia", "genero/magia"),
|
||||||
|
Pair("Misterio", "genero/misterio"),
|
||||||
|
Pair("Superpoderes", "genero/super-poderes"),
|
||||||
|
Pair("Shounen", "genero/shounen"),
|
||||||
|
Pair("Deportes", "genero/deportes"),
|
||||||
|
Pair("Fantasía", "genero/fantasia"),
|
||||||
|
Pair("Sobrenatural", "genero/sobrenatural"),
|
||||||
|
Pair("Música", "genero/musica"),
|
||||||
|
Pair("Escolares", "genero/escolares"),
|
||||||
|
Pair("Seinen", "genero/seinen"),
|
||||||
|
Pair("Histórico", "genero/historico"),
|
||||||
|
Pair("Psicológico", "genero/psicologico"),
|
||||||
|
Pair("Mecha", "genero/mecha"),
|
||||||
|
Pair("Juegos", "genero/juegos"),
|
||||||
|
Pair("Militar", "genero/militar"),
|
||||||
|
Pair("Recuentos de la Vida", "genero/recuentos-de-la-vida"),
|
||||||
|
Pair("Demonios", "genero/demonios"),
|
||||||
|
Pair("Artes Marciales", "genero/artes-marciales"),
|
||||||
|
Pair("Espacial", "genero/espacial"),
|
||||||
|
Pair("Shoujo", "genero/shoujo"),
|
||||||
|
Pair("Samurái", "genero/samurai"),
|
||||||
|
Pair("Harem", "genero/harem"),
|
||||||
|
Pair("Parodia", "genero/parodia"),
|
||||||
|
Pair("Ecchi", "genero/ecchi"),
|
||||||
|
Pair("Demencia", "genero/demencia"),
|
||||||
|
Pair("Vampiros", "genero/vampiros"),
|
||||||
|
Pair("Josei", "genero/josei"),
|
||||||
|
Pair("Shounen Ai", "genero/shounen-ai"),
|
||||||
|
Pair("Shoujo Ai", "genero/shoujo-ai"),
|
||||||
|
Pair("Latino", "genero/latino"),
|
||||||
|
Pair("Policía", "genero/policia"),
|
||||||
|
Pair("Yaoi", "genero/yaoi"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Preferences =============================
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
|
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||||
|
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||||
|
|
||||||
|
private const val PREF_SERVER_KEY = "preferred_server"
|
||||||
|
private const val PREF_SERVER_DEFAULT = "Voe"
|
||||||
|
private val SERVER_LIST = arrayOf(
|
||||||
|
"YourUpload", "BurstCloud", "Voe", "Mp4Upload", "Doodstream",
|
||||||
|
"Upload", "BurstCloud", "Upstream", "StreamTape", "Amazon",
|
||||||
|
"Fastream", "Filemoon", "StreamWish", "Okru", "Streamlare",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
|
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||||
|
return this.sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.quality.contains(server, true) },
|
||||||
|
{ it.quality.contains(quality) },
|
||||||
|
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||||
|
),
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_SERVER_KEY
|
||||||
|
title = "Preferred server"
|
||||||
|
entries = SERVER_LIST
|
||||||
|
entryValues = SERVER_LIST
|
||||||
|
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)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_QUALITY_KEY
|
||||||
|
title = "Preferred quality"
|
||||||
|
entries = QUALITY_LIST
|
||||||
|
entryValues = QUALITY_LIST
|
||||||
|
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(key, entry).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
}
|
25
src/es/animefenix/build.gradle
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Animefenix'
|
||||||
|
extClass = '.Animefenix'
|
||||||
|
extVersionCode = 54
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:mp4upload-extractor'))
|
||||||
|
implementation(project(':lib:streamtape-extractor'))
|
||||||
|
implementation(project(':lib:yourupload-extractor'))
|
||||||
|
implementation(project(':lib:uqload-extractor'))
|
||||||
|
implementation(project(':lib:okru-extractor'))
|
||||||
|
implementation(project(':lib:burstcloud-extractor'))
|
||||||
|
implementation(project(':lib:streamwish-extractor'))
|
||||||
|
implementation(project(':lib:filemoon-extractor'))
|
||||||
|
implementation(project(':lib:voe-extractor'))
|
||||||
|
implementation(project(':lib:streamlare-extractor'))
|
||||||
|
implementation(project(':lib:fastream-extractor'))
|
||||||
|
implementation(project(':lib:dood-extractor'))
|
||||||
|
implementation(project(':lib:upstream-extractor'))
|
||||||
|
implementation(project(':lib:streamhidevid-extractor'))
|
||||||
|
implementation(project(':lib:universal-extractor'))
|
||||||
|
}
|
BIN
src/es/animefenix/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,161 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.es.animefenix
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
object AnimeFenixFilters {
|
||||||
|
open class QueryPartFilter(displayName: String, val vals: Array<Pair<String, String>>) : AnimeFilter.Select<String>(
|
||||||
|
displayName,
|
||||||
|
vals.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
fun toQueryPart(name: String) = vals[state].second.takeIf { it.isNotEmpty() }?.let { "&$name=${vals[state].second}" } ?: run { "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
|
||||||
|
|
||||||
|
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
||||||
|
options: Array<Pair<String, String>>,
|
||||||
|
name: String,
|
||||||
|
): String {
|
||||||
|
return (this.getFirst<R>() as CheckBoxFilterList).state
|
||||||
|
.mapNotNull { checkbox ->
|
||||||
|
if (checkbox.state) {
|
||||||
|
options.find { it.first == checkbox.name }!!.second
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.joinToString("&$name[]=").let {
|
||||||
|
if (it.isBlank()) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"&$name[]=$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
|
||||||
|
return (this.getFirst<R>() as QueryPartFilter).toQueryPart(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||||
|
return this.filterIsInstance<R>().first()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.changePrefix() = this.takeIf { it.startsWith("&") }?.let { this.replaceFirst("&", "?") } ?: run { this }
|
||||||
|
|
||||||
|
data class FilterSearchParams(val filter: String = "") { fun getQuery() = filter.changePrefix() }
|
||||||
|
|
||||||
|
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||||
|
if (filters.isEmpty()) return FilterSearchParams()
|
||||||
|
return FilterSearchParams(
|
||||||
|
filters.parseCheckbox<GenresFilter>(AnimeFenixFiltersData.GENRES, "genero") +
|
||||||
|
filters.parseCheckbox<YearsFilter>(AnimeFenixFiltersData.YEARS, "year") +
|
||||||
|
filters.parseCheckbox<TypesFilter>(AnimeFenixFiltersData.TYPES, "type") +
|
||||||
|
filters.parseCheckbox<StateFilter>(AnimeFenixFiltersData.STATE, "estado") +
|
||||||
|
filters.asQueryPart<SortFilter>("order"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val FILTER_LIST get() = AnimeFilterList(
|
||||||
|
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||||
|
GenresFilter(),
|
||||||
|
YearsFilter(),
|
||||||
|
TypesFilter(),
|
||||||
|
StateFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
class GenresFilter : CheckBoxFilterList("Género", AnimeFenixFiltersData.GENRES.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class YearsFilter : CheckBoxFilterList("Año", AnimeFenixFiltersData.YEARS.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class TypesFilter : CheckBoxFilterList("Tipo", AnimeFenixFiltersData.TYPES.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class StateFilter : CheckBoxFilterList("Estado", AnimeFenixFiltersData.STATE.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class SortFilter : QueryPartFilter("Orden", AnimeFenixFiltersData.SORT)
|
||||||
|
|
||||||
|
private object AnimeFenixFiltersData {
|
||||||
|
val YEARS = (1990..Calendar.getInstance().get(Calendar.YEAR)).map { Pair("$it", "$it") }.reversed().toTypedArray()
|
||||||
|
|
||||||
|
val TYPES = arrayOf(
|
||||||
|
Pair("TV", "tv"),
|
||||||
|
Pair("Película", "movie"),
|
||||||
|
Pair("Especial", "special"),
|
||||||
|
Pair("OVA", "ova"),
|
||||||
|
Pair("DONGHUA", "donghua"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val STATE = arrayOf(
|
||||||
|
Pair("Emisión", "1"),
|
||||||
|
Pair("Finalizado", "2"),
|
||||||
|
Pair("Próximamente", "3"),
|
||||||
|
Pair("En Cuarentena", "4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val SORT = arrayOf(
|
||||||
|
Pair("Por Defecto", "default"),
|
||||||
|
Pair("Recientemente Actualizados", "updated"),
|
||||||
|
Pair("Recientemente Agregados", "added"),
|
||||||
|
Pair("Nombre A-Z", "title"),
|
||||||
|
Pair("Calificación", "likes"),
|
||||||
|
Pair("Más Vistos", "visits"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val GENRES = arrayOf(
|
||||||
|
Pair("Acción", "accion"),
|
||||||
|
Pair("Ángeles", "angeles"),
|
||||||
|
Pair("Artes Marciales", "artes-marciales"),
|
||||||
|
Pair("Aventura", "aventura"),
|
||||||
|
Pair("Ciencia Ficción", "Ciencia Ficción"),
|
||||||
|
Pair("Comedia", "comedia"),
|
||||||
|
Pair("Cyberpunk", "cyberpunk"),
|
||||||
|
Pair("Demonios", "demonios"),
|
||||||
|
Pair("Deportes", "deportes"),
|
||||||
|
Pair("Dragones", "dragones"),
|
||||||
|
Pair("Drama", "drama"),
|
||||||
|
Pair("Ecchi", "ecchi"),
|
||||||
|
Pair("Escolares", "escolares"),
|
||||||
|
Pair("Fantasía", "fantasia"),
|
||||||
|
Pair("Gore", "gore"),
|
||||||
|
Pair("Harem", "harem"),
|
||||||
|
Pair("Histórico", "historico"),
|
||||||
|
Pair("Horror", "horror"),
|
||||||
|
Pair("Infantil", "infantil"),
|
||||||
|
Pair("Isekai", "isekai"),
|
||||||
|
Pair("Josei", "josei"),
|
||||||
|
Pair("Juegos", "juegos"),
|
||||||
|
Pair("Magia", "magia"),
|
||||||
|
Pair("Mecha", "mecha"),
|
||||||
|
Pair("Militar", "militar"),
|
||||||
|
Pair("Misterio", "misterio"),
|
||||||
|
Pair("Música", "Musica"),
|
||||||
|
Pair("Ninjas", "ninjas"),
|
||||||
|
Pair("Parodia", "parodia"),
|
||||||
|
Pair("Policía", "policia"),
|
||||||
|
Pair("Psicológico", "psicologico"),
|
||||||
|
Pair("Recuerdos de la vida", "Recuerdos de la vida"),
|
||||||
|
Pair("Romance", "romance"),
|
||||||
|
Pair("Samurai", "samurai"),
|
||||||
|
Pair("Sci-Fi", "sci-fi"),
|
||||||
|
Pair("Seinen", "seinen"),
|
||||||
|
Pair("Shoujo", "shoujo"),
|
||||||
|
Pair("Shoujo Ai", "shoujo-ai"),
|
||||||
|
Pair("Shounen", "shounen"),
|
||||||
|
Pair("Slice of life", "slice-of-life"),
|
||||||
|
Pair("Sobrenatural", "sobrenatural"),
|
||||||
|
Pair("Space", "space"),
|
||||||
|
Pair("Spokon", "spokon"),
|
||||||
|
Pair("Steampunk", "steampunk"),
|
||||||
|
Pair("Superpoder", "superpoder"),
|
||||||
|
Pair("Thriller", "thriller"),
|
||||||
|
Pair("Vampiro", "vampiro"),
|
||||||
|
Pair("Yaoi", "yaoi"),
|
||||||
|
Pair("Yuri", "yuri"),
|
||||||
|
Pair("Zombies", "zombies"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,307 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.es.animefenix
|
||||||
|
|
||||||
|
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.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.lib.burstcloudextractor.BurstCloudExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.universalextractor.UniversalExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.net.URLDecoder
|
||||||
|
|
||||||
|
class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "AnimeFenix"
|
||||||
|
|
||||||
|
override val baseUrl = "https://www3.animefenix.tv"
|
||||||
|
|
||||||
|
override val lang = "es"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy { Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SERVER_REGEX = """tabsArray\['?\d+'?]\s*=\s*['\"](https[^'\"]+)['\"]""".toRegex()
|
||||||
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
|
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||||
|
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||||
|
|
||||||
|
private const val PREF_SERVER_KEY = "preferred_server"
|
||||||
|
private const val PREF_SERVER_DEFAULT = "Amazon"
|
||||||
|
private val SERVER_LIST = arrayOf(
|
||||||
|
"YourUpload", "Voe", "Mp4Upload", "Doodstream",
|
||||||
|
"Upload", "BurstCloud", "Upstream", "StreamTape",
|
||||||
|
"Fastream", "Filemoon", "StreamWish", "Okru",
|
||||||
|
"Amazon", "AmazonES", "Fireload", "FileLions",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes?order=likes&page=$page")
|
||||||
|
|
||||||
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val elements = document.select("div.container .grid.gap-4 a[href]")
|
||||||
|
val nextPage = document.select("nav[aria-label=Pagination] span:containsOwn(Next)").any()
|
||||||
|
val animeList = elements.map { element ->
|
||||||
|
SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||||
|
title = element.selectFirst("div h3.text-primary")!!.ownText()
|
||||||
|
thumbnail_url = element.selectFirst("img.object-cover")?.attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AnimesPage(animeList, nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/animes?order=added&page=$page")
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
val params = AnimeFenixFilters.getSearchParameters(filters)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
query.isNotBlank() -> GET("$baseUrl/animes?q=$query&page=$page", headers)
|
||||||
|
params.filter.isNotBlank() -> GET("$baseUrl/animes${params.getQuery()}&page=$page", headers)
|
||||||
|
else -> GET("$baseUrl/animes?order=likes&page=$page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return document.select("div.container > div > ul > li").map { element ->
|
||||||
|
SEpisode.create().apply {
|
||||||
|
name = element.selectFirst("span > span")!!.ownText()
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val serversData = document.selectFirst("script:containsData(var tabsArray)")?.data() ?: throw Exception("No se encontraron servidores")
|
||||||
|
val servers = SERVER_REGEX.findAll(serversData).map { it.groupValues[1] }.toList()
|
||||||
|
|
||||||
|
servers.parallelForEachBlocking { server ->
|
||||||
|
val decodedUrl = URLDecoder.decode(server, "UTF-8")
|
||||||
|
val realUrl = try {
|
||||||
|
client.newCall(GET(decodedUrl)).execute().asJsoup().selectFirst("script")!!
|
||||||
|
.data().substringAfter("src=\"").substringBefore("\"")
|
||||||
|
} catch (e: Exception) { "" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
serverVideoResolver(realUrl).let { videoList.addAll(it) }
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
return videoList.filter { it.url.contains("https") || it.url.contains("http") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serverVideoResolver(url: String): List<Video> {
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val embedUrl = url.lowercase()
|
||||||
|
try {
|
||||||
|
when {
|
||||||
|
embedUrl.contains("voe") -> {
|
||||||
|
VoeExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||||
|
}
|
||||||
|
(embedUrl.contains("amazon") || embedUrl.contains("amz")) && !embedUrl.contains("disable") -> {
|
||||||
|
val video = amazonExtractor(baseUrl + url.substringAfter(".."))
|
||||||
|
if (video.isNotBlank()) {
|
||||||
|
if (url.contains("&ext=es")) {
|
||||||
|
videoList.add(Video(video, "AmazonES", video))
|
||||||
|
} else {
|
||||||
|
videoList.add(Video(video, "Amazon", video))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> {
|
||||||
|
OkruExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||||
|
}
|
||||||
|
embedUrl.contains("filemoon") || embedUrl.contains("moonplayer") -> {
|
||||||
|
val vidHeaders = headers.newBuilder()
|
||||||
|
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||||
|
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||||
|
.build()
|
||||||
|
FilemoonExtractor(client).videosFromUrl(url, prefix = "Filemoon:", headers = vidHeaders).also(videoList::addAll)
|
||||||
|
}
|
||||||
|
embedUrl.contains("uqload") -> {
|
||||||
|
UqloadExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||||
|
}
|
||||||
|
embedUrl.contains("mp4upload") -> {
|
||||||
|
Mp4uploadExtractor(client).videosFromUrl(url, headers).let { videoList.addAll(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("wishembed") || embedUrl.contains("embedwish") || embedUrl.contains("streamwish") || embedUrl.contains("strwish") || embedUrl.contains("wish") -> {
|
||||||
|
val docHeaders = headers.newBuilder()
|
||||||
|
.add("Origin", "https://streamwish.to")
|
||||||
|
.add("Referer", "https://streamwish.to/")
|
||||||
|
.build()
|
||||||
|
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" }).also(videoList::addAll)
|
||||||
|
}
|
||||||
|
embedUrl.contains("doodstream") || embedUrl.contains("dood.") -> {
|
||||||
|
DoodExtractor(client).videoFromUrl(url, "DoodStream")?.let { videoList.add(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("streamlare") -> {
|
||||||
|
StreamlareExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> {
|
||||||
|
YourUploadExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("burstcloud") || embedUrl.contains("burst") -> {
|
||||||
|
BurstCloudExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("fastream") -> {
|
||||||
|
FastreamExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
|
||||||
|
}
|
||||||
|
embedUrl.contains("upstream") -> {
|
||||||
|
UpstreamExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> {
|
||||||
|
StreamTapeExtractor(client).videoFromUrl(url)?.let { videoList.add(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") -> {
|
||||||
|
StreamHideVidExtractor(client, headers).videosFromUrl(url).let { videoList.addAll(it) }
|
||||||
|
}
|
||||||
|
embedUrl.contains("/stream/fl.php") -> {
|
||||||
|
val video = url.substringAfter("/stream/fl.php?v=")
|
||||||
|
if (client.newCall(GET(video)).execute().code == 200) {
|
||||||
|
videoList.add(Video(video, "FireLoad", video))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
embedUrl.contains("filelions") || embedUrl.contains("lion") -> {
|
||||||
|
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "FileLions:$it" }).also(videoList::addAll)
|
||||||
|
}
|
||||||
|
else ->
|
||||||
|
UniversalExtractor(client).videosFromUrl(url, headers).let { videoList.addAll(it) }
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
|
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||||
|
return this.sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.quality.contains(server, true) },
|
||||||
|
{ it.quality.contains(quality) },
|
||||||
|
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||||
|
),
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
with(document.selectFirst("main > div.relative > div.container > div.flex")!!) {
|
||||||
|
title = selectFirst("h1.font-bold")!!.ownText()
|
||||||
|
genre = select("div:has(h2:containsOwn(Géneros)) > div.flex > a").joinToString { it.text() }
|
||||||
|
status = parseStatus(selectFirst("li:has(> span:containsOwn(Estado))")!!.ownText())
|
||||||
|
description = select("div:has(h2:containsOwn(Sinopsis)) > p").text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(statusString: String): Int {
|
||||||
|
return when {
|
||||||
|
statusString.contains("Emisión") -> SAnime.ONGOING
|
||||||
|
statusString.contains("Finalizado") -> SAnime.COMPLETED
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun amazonExtractor(url: String): String {
|
||||||
|
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||||
|
val videoURl = document.selectFirst("script:containsData(sources: [)")!!.data()
|
||||||
|
.substringAfter("[{\"file\":\"")
|
||||||
|
.substringBefore("\",").replace("\\", "")
|
||||||
|
|
||||||
|
return try {
|
||||||
|
if (client.newCall(GET(videoURl)).execute().code == 200) videoURl else ""
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): AnimeFilterList = AnimeFenixFilters.FILTER_LIST
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_QUALITY_KEY
|
||||||
|
title = "Preferred quality"
|
||||||
|
entries = QUALITY_LIST
|
||||||
|
entryValues = QUALITY_LIST
|
||||||
|
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(key, entry).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_SERVER_KEY
|
||||||
|
title = "Preferred server"
|
||||||
|
entries = SERVER_LIST
|
||||||
|
entryValues = SERVER_LIST
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <A> Iterable<A>.parallelForEach(crossinline f: suspend (A) -> Unit) {
|
||||||
|
coroutineScope {
|
||||||
|
for (item in this@parallelForEach) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
f(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <A> Iterable<A>.parallelForEachBlocking(crossinline f: suspend (A) -> Unit) {
|
||||||
|
runBlocking {
|
||||||
|
this@parallelForEachBlocking.parallelForEach(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.es.animefenix.extractors
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class SolidFilesExtractor(private val client: OkHttpClient) {
|
||||||
|
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
return try {
|
||||||
|
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||||
|
document.select("script").forEach { script ->
|
||||||
|
if (script.data().contains("\"downloadUrl\":")) {
|
||||||
|
val data = script.data().substringAfter("\"downloadUrl\":").substringBefore(",")
|
||||||
|
val url = data.replace("\"", "")
|
||||||
|
val videoUrl = url
|
||||||
|
val quality = prefix + "SolidFiles"
|
||||||
|
videoList.add(Video(videoUrl, quality, videoUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
videoList
|
||||||
|
} catch (e: Exception) {
|
||||||
|
videoList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeFLV'
|
extName = 'AnimeFLV'
|
||||||
extClass = '.AnimeFlv'
|
extClass = '.AnimeFlv'
|
||||||
extVersionCode = 59
|
extVersionCode = 63
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -11,4 +11,5 @@ dependencies {
|
||||||
implementation(project(':lib:streamtape-extractor'))
|
implementation(project(':lib:streamtape-extractor'))
|
||||||
implementation(project(':lib:okru-extractor'))
|
implementation(project(':lib:okru-extractor'))
|
||||||
implementation(project(':lib:streamwish-extractor'))
|
implementation(project(':lib:streamwish-extractor'))
|
||||||
|
implementation(project(':lib:universal-extractor'))
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.universalextractor.UniversalExtractor
|
||||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
@ -57,7 +58,7 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun popularAnimeSelector(): String = "div.Container ul.ListAnimes li article"
|
override fun popularAnimeSelector(): String = "div.Container ul.ListAnimes li article"
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/browse?order=rating&page=$page")
|
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/browse?order=rating&page=$page", headers)
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
|
@ -108,18 +109,19 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||||
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
|
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
|
||||||
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers.newBuilder().add("Referer", "$baseUrl/").build()) }
|
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers.newBuilder().add("Referer", "$baseUrl/").build()) }
|
||||||
|
private val universalExtractor by lazy { UniversalExtractor(client) }
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val jsonString = document.selectFirst("script:containsData(var videos = {)")?.data() ?: return emptyList()
|
val jsonString = document.selectFirst("script:containsData(var videos = {)")?.data() ?: return emptyList()
|
||||||
val responseString = jsonString.substringAfter("var videos =").substringBefore(";").trim()
|
val responseString = jsonString.substringAfter("var videos =").substringBefore(";").trim()
|
||||||
return json.decodeFromString<ServerModel>(responseString).sub.parallelCatchingFlatMapBlocking {
|
return json.decodeFromString<ServerModel>(responseString).sub.parallelCatchingFlatMapBlocking { it ->
|
||||||
when (it.title) {
|
when (it.title) {
|
||||||
"Stape" -> listOf(streamTapeExtractor.videoFromUrl(it.url ?: it.code)!!)
|
"Stape" -> listOf(streamTapeExtractor.videoFromUrl(it.url ?: it.code)!!)
|
||||||
"Okru" -> okruExtractor.videosFromUrl(it.url ?: it.code)
|
"Okru" -> okruExtractor.videosFromUrl(it.url ?: it.code)
|
||||||
"YourUpload" -> yourUploadExtractor.videoFromUrl(it.url ?: it.code, headers = headers)
|
"YourUpload" -> yourUploadExtractor.videoFromUrl(it.url ?: it.code, headers = headers)
|
||||||
"SW" -> streamWishExtractor.videosFromUrl(it.url ?: it.code, videoNameGen = { "StreamWish:$it" })
|
"SW" -> streamWishExtractor.videosFromUrl(it.url ?: it.code, videoNameGen = { "StreamWish:$it" })
|
||||||
else -> emptyList()
|
else -> universalExtractor.videosFromUrl(it.url ?: it.code, headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +134,6 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val params = AnimeFlvFilters.getSearchParameters(filters)
|
val params = AnimeFlvFilters.getSearchParameters(filters)
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
query.isNotBlank() -> GET("$baseUrl/browse?q=$query&page=$page")
|
query.isNotBlank() -> GET("$baseUrl/browse?q=$query&page=$page")
|
||||||
params.filter.isNotBlank() -> GET("$baseUrl/browse${params.getQuery()}&page=$page")
|
params.filter.isNotBlank() -> GET("$baseUrl/browse${params.getQuery()}&page=$page")
|
||||||
|
@ -166,13 +167,19 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
override fun latestUpdatesNextPageSelector() = null
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/browse?order=added&page=$page")
|
override fun latestUpdatesSelector() = "div.Container ul.ListEpisodios li a.fa-play"
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularAnimeSelector()
|
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.setUrlWithoutDomain(element.select("a").attr("abs:href").replace("/ver/", "/anime/").substringBeforeLast("-"))
|
||||||
|
anime.title = element.select("strong.Title").text()
|
||||||
|
anime.thumbnail_url = element.select("span.Image img").attr("abs:src").replace("thumbs", "covers")
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
override fun List<Video>.sort(): List<Video> {
|
||||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeID'
|
extName = 'AnimeID'
|
||||||
extClass = '.AnimeID'
|
extClass = '.AnimeID'
|
||||||
extVersionCode = 10
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -9,4 +9,5 @@ apply from: "$rootDir/common.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':lib:streamtape-extractor'))
|
implementation(project(':lib:streamtape-extractor'))
|
||||||
implementation(project(':lib:streamwish-extractor'))
|
implementation(project(':lib:streamwish-extractor'))
|
||||||
|
implementation(project(':lib:universal-extractor'))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.universalextractor.UniversalExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
@ -120,6 +121,7 @@ class AnimeID : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||||
|
private val universalExtractor by lazy { UniversalExtractor(client) }
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
@ -128,11 +130,10 @@ class AnimeID : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
val jsonString = script.attr("data")
|
val jsonString = script.attr("data")
|
||||||
val jsonUnescape = unescapeJava(jsonString)!!.replace("\\", "")
|
val jsonUnescape = unescapeJava(jsonString)!!.replace("\\", "")
|
||||||
val url = fetchUrls(jsonUnescape).firstOrNull()?.replace("\\\\", "\\") ?: ""
|
val url = fetchUrls(jsonUnescape).firstOrNull()?.replace("\\\\", "\\") ?: ""
|
||||||
if (url.contains("streamtape") || url.contains("tape") || url.contains("stp")) {
|
return when {
|
||||||
streamtapeExtractor.videosFromUrl(url).also(videoList::addAll)
|
url.contains("streamtape") || url.contains("tape") || url.contains("stp") -> streamtapeExtractor.videosFromUrl(url)
|
||||||
}
|
url.contains("wish") || url.contains("fviplions") || url.contains("obeywish") -> streamwishExtractor.videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
|
||||||
if (url.contains("wish") || url.contains("fviplions") || url.contains("obeywish")) {
|
else -> universalExtractor.videosFromUrl(url, headers)
|
||||||
streamwishExtractor.videosFromUrl(url, videoNameGen = { "StreamWish:$it" }).also(videoList::addAll)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return videoList
|
return videoList
|
||||||
|
|
20
src/es/animejl/build.gradle
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Animejl'
|
||||||
|
extClass = '.Animejl'
|
||||||
|
extVersionCode = 4
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:yourupload-extractor'))
|
||||||
|
implementation(project(':lib:streamtape-extractor'))
|
||||||
|
implementation(project(':lib:okru-extractor'))
|
||||||
|
implementation(project(':lib:voe-extractor'))
|
||||||
|
implementation(project(':lib:streamwish-extractor'))
|
||||||
|
implementation(project(':lib:streamhidevid-extractor'))
|
||||||
|
implementation(project(':lib:universal-extractor'))
|
||||||
|
implementation(project(':lib:uqload-extractor'))
|
||||||
|
implementation(project(':lib:mp4upload-extractor'))
|
||||||
|
}
|
BIN
src/es/animejl/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/es/animejl/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
src/es/animejl/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/es/animejl/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
src/es/animejl/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 54 KiB |
|
@ -0,0 +1,244 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.es.animejl
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
|
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.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.universalextractor.UniversalExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.Exception
|
||||||
|
|
||||||
|
class Animejl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "Animejl"
|
||||||
|
|
||||||
|
override val baseUrl = "https://www.anime-jl.net"
|
||||||
|
|
||||||
|
override val lang = "es"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
|
private const val PREF_QUALITY_DEFAULT = "720"
|
||||||
|
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||||
|
|
||||||
|
private const val PREF_SERVER_KEY = "preferred_server"
|
||||||
|
private const val PREF_SERVER_DEFAULT = "StreamWish"
|
||||||
|
private val SERVER_LIST = arrayOf("StreamWish", "YourUpload", "Okru", "StreamTape", "StreamHideVid", "Voe", "Uqload", "Mp4upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeSelector(): String = "div.Container ul.ListAnimes li article"
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/animes?order=rating&page=$page")
|
||||||
|
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.setUrlWithoutDomain(element.select("div.Description a.Button").attr("abs:href"))
|
||||||
|
anime.title = element.select("a h3").text()
|
||||||
|
anime.thumbnail_url = element.select("a div.Image figure img").attr("src").replace("/storage", "$baseUrl/storage")
|
||||||
|
anime.description = element.select("div.Description p:eq(2)").text().removeSurrounding("\"")
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector(): String = "ul.pagination li a[rel=\"next\"]"
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val episodeList = mutableListOf<SEpisode>()
|
||||||
|
|
||||||
|
val script = document.select("script:containsData(var episodes =)").firstOrNull()?.data() ?: return emptyList()
|
||||||
|
|
||||||
|
val episodesPattern = Regex("var episodes = (\\[.*?\\]);", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
val episodesMatch = episodesPattern.find(script) ?: return emptyList()
|
||||||
|
val episodesString = episodesMatch.groupValues[1]
|
||||||
|
|
||||||
|
val animeInfoPattern = Regex("var anime_info = \\[(.*?)\\];")
|
||||||
|
val animeInfoMatch = animeInfoPattern.find(script) ?: return emptyList()
|
||||||
|
val animeInfo = animeInfoMatch.groupValues[1].split(",").map { it.trim('"') }
|
||||||
|
|
||||||
|
val animeSlug = animeInfo.getOrNull(2) ?: ""
|
||||||
|
val animeId = animeInfo.getOrNull(0) ?: ""
|
||||||
|
val episodePattern = Regex("\\[(\\d+),\"(.*?)\",\"(.*?)\",\"(.*?)\"\\]")
|
||||||
|
val episodeMatches = episodePattern.findAll(episodesString)
|
||||||
|
|
||||||
|
episodeMatches.forEach { match ->
|
||||||
|
try {
|
||||||
|
val episodeNumber = match.groupValues[1].toIntOrNull() ?: 0
|
||||||
|
val url = "$baseUrl/anime/$animeId/$animeSlug/episodio-$episodeNumber"
|
||||||
|
val episode = SEpisode.create()
|
||||||
|
episode.setUrlWithoutDomain(url)
|
||||||
|
episode.episode_number = episodeNumber.toFloat()
|
||||||
|
episode.name = "Episodio $episodeNumber"
|
||||||
|
episodeList.add(episode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Animejl", "Error processing episode: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodeList.sortedByDescending { it.episode_number }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListSelector() = "uwu"
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
/*--------------------------------Video extractors------------------------------------*/
|
||||||
|
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||||
|
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||||
|
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
|
||||||
|
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||||
|
private val universalExtractor by lazy { UniversalExtractor(client) }
|
||||||
|
private val streamHideVidExtractor by lazy { StreamHideVidExtractor(client, headers) }
|
||||||
|
private val voeExtractor by lazy { VoeExtractor(client) }
|
||||||
|
private val uqloadExtractor by lazy { UqloadExtractor(client) }
|
||||||
|
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val scriptContent = document.selectFirst("script:containsData(var video = [)")?.data()
|
||||||
|
?: return emptyList()
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val videoPattern = Regex("""video\[\d+\] = '<iframe src="(.*?)"""")
|
||||||
|
val matches = videoPattern.findAll(scriptContent)
|
||||||
|
matches.forEach { match ->
|
||||||
|
val url = match.groupValues[1]
|
||||||
|
val videos = when {
|
||||||
|
url.contains("streamtape") -> listOfNotNull(streamTapeExtractor.videoFromUrl(url))
|
||||||
|
url.contains("ok.ru") -> okruExtractor.videosFromUrl(url)
|
||||||
|
url.contains("yourupload") -> yourUploadExtractor.videoFromUrl(url, headers)
|
||||||
|
url.contains("streamwish") || url.contains("playerwish") -> streamWishExtractor.videosFromUrl(url)
|
||||||
|
url.contains("streamhidevid") -> streamHideVidExtractor.videosFromUrl(url)
|
||||||
|
url.contains("voe") -> voeExtractor.videosFromUrl(url)
|
||||||
|
url.contains("uqload") -> uqloadExtractor.videosFromUrl(url)
|
||||||
|
url.contains("mp4upload") -> mp4uploadExtractor.videosFromUrl(url, headers)
|
||||||
|
else -> universalExtractor.videosFromUrl(url, headers)
|
||||||
|
}
|
||||||
|
videoList.addAll(videos)
|
||||||
|
}
|
||||||
|
return videoList.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
val params = AnimejlFilters.getSearchParameters(filters)
|
||||||
|
return when {
|
||||||
|
query.isNotBlank() -> GET("$baseUrl/animes?q=$query&page=$page")
|
||||||
|
params.filter.isNotBlank() -> GET("$baseUrl/animes${params.getQuery()}&page=$page")
|
||||||
|
else -> popularAnimeRequest(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): AnimeFilterList = AnimejlFilters.FILTER_LIST
|
||||||
|
|
||||||
|
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||||
|
|
||||||
|
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||||
|
|
||||||
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
anime.thumbnail_url =
|
||||||
|
document.selectFirst("div.AnimeCover div.Image figure img")!!.attr("abs:src")
|
||||||
|
anime.title = document.selectFirst("div.Ficha.fchlt div.Container .Title")!!.text()
|
||||||
|
anime.description = document.selectFirst("div.Description")!!.text().removeSurrounding("\"")
|
||||||
|
anime.genre = document.select("nav.Nvgnrs a").joinToString { it.text() }
|
||||||
|
anime.status = parseStatus(document.select("span.fa-tv").text())
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(statusString: String): Int {
|
||||||
|
return when {
|
||||||
|
statusString.contains("En emision") -> SAnime.ONGOING
|
||||||
|
statusString.contains("Finalizado") -> SAnime.COMPLETED
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/animes?order=updated&page=$page")
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
|
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||||
|
return this.sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.quality.contains(server, true) },
|
||||||
|
{ it.quality.contains(quality) },
|
||||||
|
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||||
|
),
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_SERVER_KEY
|
||||||
|
title = "Preferred server"
|
||||||
|
entries = SERVER_LIST
|
||||||
|
entryValues = SERVER_LIST
|
||||||
|
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)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_QUALITY_KEY
|
||||||
|
title = "Preferred quality"
|
||||||
|
entries = QUALITY_LIST
|
||||||
|
entryValues = QUALITY_LIST
|
||||||
|
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(key, entry).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.es.animejl
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
object AnimejlFilters {
|
||||||
|
open class QueryPartFilter(displayName: String, val vals: Array<Pair<String, String>>) : AnimeFilter.Select<String>(
|
||||||
|
displayName,
|
||||||
|
vals.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
fun toQueryPart(name: String) = vals[state].second.takeIf { it.isNotEmpty() }?.let { "&$name=${vals[state].second}" } ?: run { "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
|
||||||
|
|
||||||
|
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
||||||
|
options: Array<Pair<String, String>>,
|
||||||
|
name: String,
|
||||||
|
): String {
|
||||||
|
return (this.getFirst<R>() as CheckBoxFilterList).state
|
||||||
|
.mapNotNull { checkbox ->
|
||||||
|
if (checkbox.state) {
|
||||||
|
options.find { it.first == checkbox.name }!!.second
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.joinToString("&$name[]=").let {
|
||||||
|
if (it.isBlank()) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"&$name[]=$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
|
||||||
|
return (this.getFirst<R>() as QueryPartFilter).toQueryPart(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||||
|
return this.filterIsInstance<R>().first()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.changePrefix() = this.takeIf { it.startsWith("&") }?.let { this.replaceFirst("&", "?") } ?: run { this }
|
||||||
|
|
||||||
|
data class FilterSearchParams(val filter: String = "") { fun getQuery() = filter.changePrefix() }
|
||||||
|
|
||||||
|
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||||
|
if (filters.isEmpty()) return FilterSearchParams()
|
||||||
|
return FilterSearchParams(
|
||||||
|
filters.parseCheckbox<GenresFilter>(AnimeFlvFiltersData.GENRES, "genre") +
|
||||||
|
filters.parseCheckbox<YearsFilter>(AnimeFlvFiltersData.YEARS, "year") +
|
||||||
|
filters.parseCheckbox<TypesFilter>(AnimeFlvFiltersData.TYPES, "type") +
|
||||||
|
filters.parseCheckbox<StateFilter>(AnimeFlvFiltersData.STATE, "estado") +
|
||||||
|
filters.asQueryPart<SortFilter>("order"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val FILTER_LIST get() = AnimeFilterList(
|
||||||
|
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||||
|
GenresFilter(),
|
||||||
|
YearsFilter(),
|
||||||
|
TypesFilter(),
|
||||||
|
StateFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
class GenresFilter : CheckBoxFilterList("Género", AnimeFlvFiltersData.GENRES.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class YearsFilter : CheckBoxFilterList("Año", AnimeFlvFiltersData.YEARS.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class TypesFilter : CheckBoxFilterList("Tipo", AnimeFlvFiltersData.TYPES.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class StateFilter : CheckBoxFilterList("Estado", AnimeFlvFiltersData.STATE.map { CheckBoxVal(it.first, false) })
|
||||||
|
|
||||||
|
class SortFilter : QueryPartFilter("Orden", AnimeFlvFiltersData.SORT)
|
||||||
|
|
||||||
|
private object AnimeFlvFiltersData {
|
||||||
|
val YEARS = (1990..Calendar.getInstance().get(Calendar.YEAR)).map { Pair("$it", "$it") }.reversed().toTypedArray()
|
||||||
|
|
||||||
|
val TYPES = arrayOf(
|
||||||
|
Pair("Anime", "1"),
|
||||||
|
Pair("Ova", "2"),
|
||||||
|
Pair("Pelicula", "3"),
|
||||||
|
Pair("Donghua", "7"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val STATE = arrayOf(
|
||||||
|
Pair("En emisión", "0"),
|
||||||
|
Pair("Finalizado", "1"),
|
||||||
|
Pair("Próximamente", "2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val SORT = arrayOf(
|
||||||
|
Pair("Por Defecto", "created"),
|
||||||
|
Pair("Recientemente Actualizados", "updated"),
|
||||||
|
Pair("Nombre A-Z", "titleaz"),
|
||||||
|
Pair("Nombre Z-A", "titleza"),
|
||||||
|
Pair("Calificación", "rating"),
|
||||||
|
Pair("Vistas", "views"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val GENRES = arrayOf(
|
||||||
|
Pair("Acción", "1"),
|
||||||
|
Pair("Artes Marciales", "2"),
|
||||||
|
Pair("Aventuras", "3"),
|
||||||
|
Pair("Ciencia Ficción", "33"),
|
||||||
|
Pair("Comedia", "9"),
|
||||||
|
Pair("Cultivación", "71"),
|
||||||
|
Pair("Demencia", "40"),
|
||||||
|
Pair("Demonios", "42"),
|
||||||
|
Pair("Deportes", "27"),
|
||||||
|
Pair("Donghua", "50"),
|
||||||
|
Pair("Drama", "10"),
|
||||||
|
Pair("Ecchi", "25"),
|
||||||
|
Pair("Escolares", "22"),
|
||||||
|
Pair("Espacial", "48"),
|
||||||
|
Pair("Fantasia", "6"),
|
||||||
|
Pair("Gore", "67"),
|
||||||
|
Pair("Harem", "32"),
|
||||||
|
Pair("Hentai", "31"),
|
||||||
|
Pair("Historico", "43"),
|
||||||
|
Pair("Horror", "39"),
|
||||||
|
Pair("Isekai", "45"),
|
||||||
|
Pair("Josei", "70"),
|
||||||
|
Pair("Juegos", "11"),
|
||||||
|
Pair("Latino / Castellano", "46"),
|
||||||
|
Pair("Magia", "38"),
|
||||||
|
Pair("Mecha", "41"),
|
||||||
|
Pair("Militar", "44"),
|
||||||
|
Pair("Misterio", "26"),
|
||||||
|
Pair("Mitología", "73"),
|
||||||
|
Pair("Musica", "28"),
|
||||||
|
Pair("Parodia", "13"),
|
||||||
|
Pair("Policía", "51"),
|
||||||
|
Pair("Psicologico", "29"),
|
||||||
|
Pair("Recuentos de la vida", "23"),
|
||||||
|
Pair("Reencarnación", "72"),
|
||||||
|
Pair("Romance", "12"),
|
||||||
|
Pair("Samurai", "69"),
|
||||||
|
Pair("Seinen", "24"),
|
||||||
|
Pair("Shoujo", "36"),
|
||||||
|
Pair("Shounen", "4"),
|
||||||
|
Pair("Sin Censura", "68"),
|
||||||
|
Pair("Sobrenatural", "7"),
|
||||||
|
Pair("Superpoderes", "5"),
|
||||||
|
Pair("Suspenso", "21"),
|
||||||
|
Pair("Terror", "20"),
|
||||||
|
Pair("Vampiros", "49"),
|
||||||
|
Pair("Venganza", "74"),
|
||||||
|
Pair("Yaoi", "53"),
|
||||||
|
Pair("Yuri", "52"),
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeLatinoHD'
|
extName = 'AnimeLatinoHD'
|
||||||
extClass = '.AnimeLatinoHD'
|
extClass = '.AnimeLatinoHD'
|
||||||
extVersionCode = 35
|
extVersionCode = 38
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -85,21 +85,21 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
if (url.contains("status=1")) {
|
if (url.contains("status=1")) {
|
||||||
val latestData = data["data"]!!.jsonArray
|
val latestData = data["data"]!!.jsonArray
|
||||||
latestData.forEach { item ->
|
latestData.forEach { item ->
|
||||||
val animeItem = item!!.jsonObject
|
val animeItem = item.jsonObject
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
|
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive.content}"))
|
||||||
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
|
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive.content}"
|
||||||
anime.title = animeItem["name"]!!.jsonPrimitive!!.content
|
anime.title = animeItem["name"]!!.jsonPrimitive.content
|
||||||
animeList.add(anime)
|
animeList.add(anime)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val popularToday = data["popular_today"]!!.jsonArray
|
val popularToday = data["popular_today"]!!.jsonArray
|
||||||
popularToday.forEach { item ->
|
popularToday.forEach { item ->
|
||||||
val animeItem = item!!.jsonObject
|
val animeItem = item.jsonObject
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
|
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive.content}"))
|
||||||
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
|
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive.content}"
|
||||||
anime.title = animeItem["name"]!!.jsonPrimitive!!.content
|
anime.title = animeItem["name"]!!.jsonPrimitive.content
|
||||||
animeList.add(anime)
|
animeList.add(anime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,12 +122,12 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val pageProps = props["pageProps"]!!.jsonObject
|
val pageProps = props["pageProps"]!!.jsonObject
|
||||||
val data = pageProps["data"]!!.jsonObject
|
val data = pageProps["data"]!!.jsonObject
|
||||||
|
|
||||||
newAnime.title = data["name"]!!.jsonPrimitive!!.content
|
newAnime.title = data["name"]!!.jsonPrimitive.content
|
||||||
newAnime.genre = data["genres"]!!.jsonPrimitive!!.content.split(",").joinToString()
|
newAnime.genre = data["genres"]!!.jsonPrimitive.content.split(",").joinToString()
|
||||||
newAnime.description = data["overview"]!!.jsonPrimitive!!.content
|
newAnime.description = data["overview"]!!.jsonPrimitive.content
|
||||||
newAnime.status = parseStatus(data["status"]!!.jsonPrimitive!!.content)
|
newAnime.status = parseStatus(data["status"]!!.jsonPrimitive.content)
|
||||||
newAnime.thumbnail_url = "https://image.tmdb.org/t/p/w600_and_h900_bestv2${data["poster"]!!.jsonPrimitive!!.content}"
|
newAnime.thumbnail_url = "https://image.tmdb.org/t/p/w600_and_h900_bestv2${data["poster"]!!.jsonPrimitive.content}"
|
||||||
newAnime.setUrlWithoutDomain(externalOrInternalImg("anime/${data["slug"]!!.jsonPrimitive!!.content}"))
|
newAnime.setUrlWithoutDomain(externalOrInternalImg("anime/${data["slug"]!!.jsonPrimitive.content}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newAnime
|
return newAnime
|
||||||
|
@ -144,11 +144,11 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val data = pageProps["data"]!!.jsonObject
|
val data = pageProps["data"]!!.jsonObject
|
||||||
val arrEpisode = data["episodes"]!!.jsonArray
|
val arrEpisode = data["episodes"]!!.jsonArray
|
||||||
arrEpisode.forEach { item ->
|
arrEpisode.forEach { item ->
|
||||||
val animeItem = item!!.jsonObject
|
val animeItem = item.jsonObject
|
||||||
val episode = SEpisode.create()
|
val episode = SEpisode.create()
|
||||||
episode.setUrlWithoutDomain(externalOrInternalImg("ver/${data["slug"]!!.jsonPrimitive!!.content}/${animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()}"))
|
episode.setUrlWithoutDomain(externalOrInternalImg("ver/${data["slug"]!!.jsonPrimitive.content}/${animeItem["number"]!!.jsonPrimitive.content.toFloat()}"))
|
||||||
episode.episode_number = animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()
|
episode.episode_number = animeItem["number"]!!.jsonPrimitive.content.toFloat()
|
||||||
episode.name = "Episodio ${animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()}"
|
episode.name = "Episodio ${animeItem["number"]!!.jsonPrimitive.content.toFloat()}"
|
||||||
episodeList.add(episode)
|
episodeList.add(episode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
private fun parseJsonArray(json: JsonElement?): List<JsonElement> {
|
private fun parseJsonArray(json: JsonElement?): List<JsonElement> {
|
||||||
val list = mutableListOf<JsonElement>()
|
val list = mutableListOf<JsonElement>()
|
||||||
json!!.jsonObject!!.entries!!.forEach { list.add(it.value) }
|
json!!.jsonObject.entries.forEach { list.add(it.value) }
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,11 +178,11 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val pageProps = props["pageProps"]!!.jsonObject
|
val pageProps = props["pageProps"]!!.jsonObject
|
||||||
val data = pageProps["data"]!!.jsonObject
|
val data = pageProps["data"]!!.jsonObject
|
||||||
val playersElement = data["players"]
|
val playersElement = data["players"]
|
||||||
val players = if (playersElement !is JsonArray) JsonArray(parseJsonArray(playersElement)) else playersElement!!.jsonArray
|
val players = if (playersElement !is JsonArray) JsonArray(parseJsonArray(playersElement)) else playersElement.jsonArray
|
||||||
players.forEach { player ->
|
players.forEach { player ->
|
||||||
val servers = player!!.jsonArray
|
val servers = player.jsonArray
|
||||||
servers.forEach { server ->
|
servers.forEach { server ->
|
||||||
val item = server!!.jsonObject
|
val item = server.jsonObject
|
||||||
val request = client.newCall(
|
val request = client.newCall(
|
||||||
GET(
|
GET(
|
||||||
url = "https://api.animelatinohd.com/stream/${item["id"]!!.jsonPrimitive.content}",
|
url = "https://api.animelatinohd.com/stream/${item["id"]!!.jsonPrimitive.content}",
|
||||||
|
@ -193,9 +193,9 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
.build(),
|
.build(),
|
||||||
),
|
),
|
||||||
).execute()
|
).execute()
|
||||||
val locationsDdh = request!!.networkResponse.toString()
|
val locationsDdh = request.networkResponse.toString()
|
||||||
fetchUrls(locationsDdh).map { url ->
|
fetchUrls(locationsDdh).map { url ->
|
||||||
val language = if (item["languaje"]!!.jsonPrimitive!!.content == "1") "[LAT]" else "[SUB]"
|
val language = if (item["languaje"]!!.jsonPrimitive.content == "1") "[LAT]" else "[SUB]"
|
||||||
val embedUrl = url.lowercase()
|
val embedUrl = url.lowercase()
|
||||||
if (embedUrl.contains("filemoon")) {
|
if (embedUrl.contains("filemoon")) {
|
||||||
val vidHeaders = headers.newBuilder()
|
val vidHeaders = headers.newBuilder()
|
||||||
|
@ -211,7 +211,7 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
StreamTapeExtractor(client).videoFromUrl(url, "$language Streamtape")?.let { videoList.add(it) }
|
StreamTapeExtractor(client).videoFromUrl(url, "$language Streamtape")?.let { videoList.add(it) }
|
||||||
}
|
}
|
||||||
if (embedUrl.contains("dood")) {
|
if (embedUrl.contains("dood")) {
|
||||||
DoodExtractor(client).videoFromUrl(url, "$language DoodStream")?.let { videoList.add(it) }
|
DoodExtractor(client).videoFromUrl(url, language)?.let { videoList.add(it) }
|
||||||
}
|
}
|
||||||
if (embedUrl.contains("okru") || embedUrl.contains("ok.ru")) {
|
if (embedUrl.contains("okru") || embedUrl.contains("ok.ru")) {
|
||||||
OkruExtractor(client).videosFromUrl(url, language).also(videoList::addAll)
|
OkruExtractor(client).videosFromUrl(url, language).also(videoList::addAll)
|
||||||
|
@ -281,11 +281,11 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val data = pageProps["data"]!!.jsonObject
|
val data = pageProps["data"]!!.jsonObject
|
||||||
val arrData = data["data"]!!.jsonArray
|
val arrData = data["data"]!!.jsonArray
|
||||||
arrData.forEach { item ->
|
arrData.forEach { item ->
|
||||||
val animeItem = item!!.jsonObject
|
val animeItem = item.jsonObject
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
|
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive.content}"))
|
||||||
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
|
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive.content}"
|
||||||
anime.title = animeItem["name"]!!.jsonPrimitive!!.content
|
anime.title = animeItem["name"]!!.jsonPrimitive.content
|
||||||
animeList.add(anime)
|
animeList.add(anime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|