Merged with dark25 (#636)

* merge

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

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

View file

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3

View file

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

View file

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 1 baseVersionCode = 2

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
plugins {
id("lib-android")
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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)(\\?.*)?$") }
}
}

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,15 +25,19 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.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",

View file

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

View file

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

View file

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

View file

@ -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"
@ -10,4 +10,4 @@ dependencies {
implementation(project(':lib:voe-extractor')) implementation(project(':lib:voe-extractor'))
implementation(project(':lib:streamtape-extractor')) implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:dood-extractor')) implementation(project(':lib:dood-extractor'))
} }

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

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

View 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'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".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>

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -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:"
}
}

View file

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

View 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'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

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

View 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'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

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

View file

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

View file

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

View file

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

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