Merged with dark25 #636

Merged
almightyhak merged 2 commits from merge into main 2025-02-10 02:41:59 -06:00
350 changed files with 12176 additions and 1064 deletions

View file

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

View file

@ -117,7 +117,11 @@ abstract class AnimeStream(
}
protected open fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

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

View file

@ -155,7 +155,11 @@ abstract class DooPlay(
// =============================== Search ===============================
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.lib.chillxextractor
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
@ -51,6 +52,7 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
val subtitleList = buildList {
val subtitles = REGEX_SUBS.findAll(decryptedScript)
subtitles.forEach {
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
}
}

View file

@ -5,36 +5,50 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
import java.net.URI
class DoodExtractor(private val client: OkHttpClient) {
fun videoFromUrl(
url: String,
quality: String? = null,
prefix: String? = null,
redirect: Boolean = true,
externalSubs: List<Track> = emptyList(),
): Video? {
val newQuality = quality ?: ("Doodstream" + if (redirect) " mirror" else "")
return runCatching {
val response = client.newCall(GET(url)).execute()
val newUrl = if (redirect) response.request.url.toString() else url
val doodHost = Regex("https://(.*?)/").find(newUrl)!!.groupValues[1]
val doodHost = getBaseUrl(newUrl)
val content = response.body.string()
if (!content.contains("'/pass_md5/")) return null
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
// Obtener la calidad del título de la página
val extractedQuality = Regex("\\d{3,4}p")
.find(content.substringAfter("<title>").substringBefore("</title>"))
?.groupValues
?.getOrNull(0)
// Determinar la calidad a usar
val newQuality = extractedQuality ?: ( if (redirect) " mirror" else "")
// Obtener el hash MD5
val md5 = doodHost + (Regex("/pass_md5/[^']*").find(content)?.value ?: return null)
val token = md5.substringAfterLast("/")
val randomString = getRandomString()
val randomString = createHashTable()
val expiry = System.currentTimeMillis()
// Obtener la URL del video
val videoUrlStart = client.newCall(
GET(
"https://$doodHost/pass_md5/$md5",
md5,
Headers.headersOf("referer", newUrl),
),
).execute().body.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
Video(videoUrl, newQuality, videoUrl, headers = doodHeaders(doodHost), subtitleTracks = externalSubs)
val trueUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
Video(trueUrl, prefix + "Doodstream " + newQuality , trueUrl, headers = doodHeaders(doodHost), subtitleTracks = externalSubs)
}.getOrNull()
}
@ -44,16 +58,27 @@ class DoodExtractor(private val client: OkHttpClient) {
redirect: Boolean = true,
): List<Video> {
val video = videoFromUrl(url, quality, redirect)
return video?.let(::listOf) ?: emptyList<Video>()
return video?.let(::listOf) ?: emptyList()
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
// Método para generar una cadena aleatoria
private fun createHashTable(): String {
val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return buildString {
repeat(10) {
append(alphabet.random())
}
}
}
// Método para obtener la base de la URL
private fun getBaseUrl(url: String): String {
return URI(url).let {
"${it.scheme}://${it.host}"
}
}
// Método para obtener headers personalizados
private fun doodHeaders(host: String) = Headers.Builder().apply {
add("User-Agent", "Aniyomi")
add("Referer", "https://$host/")

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.OkHttpClient
import okhttp3.internal.commonEmptyHeaders
import kotlin.math.abs
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
@ -126,10 +127,16 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
}.toList()
return masterPlaylist.substringAfter(PLAYLIST_SEPARATOR).split(PLAYLIST_SEPARATOR).mapNotNull {
val codec = it.substringAfter("CODECS=\"", "").substringBefore("\"", "")
if (codec.isNotEmpty()) {
if (codec.startsWith("mp4a")) return@mapNotNull null
}
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
.substringBefore(",").let(::stnQuality)
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
@ -328,6 +335,13 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
// ============================= Utilities ==============================
private fun stnQuality(quality: String): String {
val intQuality = quality.toInt()
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
return "${result}p"
}
companion object {
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"

View file

@ -1,25 +1,48 @@
package eu.kanade.tachiyomi.lib.streamhidevidextractor
import android.util.Log
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class StreamHideVidExtractor(private val client: OkHttpClient) {
class StreamHideVidExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client) }
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val page = client.newCall(GET(url)).execute().body.string()
val playlistUrl = (JsUnpacker.unpackAndCombine(page) ?: page)
.substringAfter("sources:")
.substringAfter("file:\"") // StreamHide
.substringAfter("src:\"") // StreamVid
.substringBefore('"')
if (!playlistUrl.startsWith("http")) return emptyList()
return playlistUtils.extractFromHls(playlistUrl,
videoNameGen = { "${prefix}StreamHideVid - $it" }
)
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "StreamHideVid - $quality" }): List<Video> {
val doc = client.newCall(GET(getEmbedUrl(url), headers)).execute().asJsoup()
val scriptBody = doc.selectFirst("script:containsData(m3u8)")?.data()
?.let { script ->
if (script.contains("eval(function(p,a,c")) {
JsUnpacker.unpackAndCombine(script)
} else {
script
}
}
val masterUrl = scriptBody
?.substringAfter("source", "")
?.substringAfter("file:\"", "")
?.substringBefore("\"", "")
?.takeIf(String::isNotBlank)
?: return emptyList()
Log.d("StreamHideVidExtractor", "Playlist URL: $masterUrl")
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen)
}
private fun getEmbedUrl(url: String): String {
return when {
url.contains("/d/") -> url.replace("/d/", "/v/")
url.contains("/download/") -> url.replace("/download/", "/v/")
url.contains("/file/") -> url.replace("/file/", "/v/")
else -> url.replace("/f/", "/v/")
}
}
}

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") }
?: return emptyList()
val videoHeaders = Headers.headersOf("Referer", "https://uqload.co/")
val videoHeaders = Headers.headersOf("Referer", "https://uqload.ws/")
val quality = if (prefix.isNotBlank()) "$prefix Uqload" else "Uqload"
return listOf(Video(videoUrl, quality, videoUrl, videoHeaders))

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -166,7 +166,6 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
DoodExtractor(client).videoFromUrl(
link,
quality,
false,
)
if (video != null) {
videoList.add(video)
@ -224,7 +223,7 @@ class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
) == true -> {
val quality = "DoodStream"
val video =
DoodExtractor(client).videoFromUrl(link, quality, false)
DoodExtractor(client).videoFromUrl(link, quality)
if (video != null) {
videoList.add(video)
}

View file

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

View file

@ -37,7 +37,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Serienstream"
override val baseUrl = "https://s.to"
override val baseUrl = "http://186.2.175.5"
override val lang = "de"
@ -91,7 +91,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val headers = Headers.Builder()
.add("Referer", "https://s.to/search")
.add("Referer", "http://186.2.175.5/search")
.add("origin", baseUrl)
.add("connection", "keep-alive")
.add("user-agent", "Mozilla/5.0 (Linux; Android 12; Pixel 5 Build/SP2A.220405.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Safari/537.36")

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 {
extName = 'Wcofun'
extClass = '.Wcofun'
extVersionCode = 13
extVersionCode = 14
}
apply from: "$rootDir/common.gradle"

View file

@ -99,13 +99,13 @@ class Wcofun : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val epName = element.ownText()
val season = epName.substringAfter("Season ")
val ep = epName.substringAfter("Episode ")
val seasonNum = season.substringBefore(" ").toIntOrNull() ?: 1
val epNum = ep.substringBefore(" ").toIntOrNull() ?: 1
episode_number = (seasonNum * 100 + epNum).toFloat()
name = "Season $seasonNum - Episode $epNum"
val title = element.attr("title")
val season = title.substringAfter("Season ").substringBefore(" ")
val episode = title.substringAfter("Episode ").substringBefore(" ")
val seasonNum = season.toIntOrNull() ?: 1
val episodeNum = episode.toIntOrNull() ?: 1
episode_number = ((seasonNum - 1) * 100 + episodeNum).toFloat()
name = "Season $seasonNum - Episode $episodeNum"
}
// ============================ 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 {
extName = 'AnimeFLV'
extClass = '.AnimeFlv'
extVersionCode = 59
extVersionCode = 63
}
apply from: "$rootDir/common.gradle"
@ -11,4 +11,5 @@ dependencies {
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:okru-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.streamtapeextractor.StreamTapeExtractor
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.network.GET
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 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 {
val anime = SAnime.create()
@ -108,18 +109,19 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val okruExtractor by lazy { OkruExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
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> {
val document = response.asJsoup()
val jsonString = document.selectFirst("script:containsData(var videos = {)")?.data() ?: return emptyList()
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) {
"Stape" -> listOf(streamTapeExtractor.videoFromUrl(it.url ?: it.code)!!)
"Okru" -> okruExtractor.videosFromUrl(it.url ?: it.code)
"YourUpload" -> yourUploadExtractor.videoFromUrl(it.url ?: it.code, headers = headers)
"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 {
val params = AnimeFlvFilters.getSearchParameters(filters)
return when {
query.isNotBlank() -> GET("$baseUrl/browse?q=$query&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> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeID'
extClass = '.AnimeID'
extVersionCode = 10
extVersionCode = 15
}
apply from: "$rootDir/common.gradle"
@ -9,4 +9,5 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:streamtape-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.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.universalextractor.UniversalExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
@ -120,6 +121,7 @@ class AnimeID : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================ Video Links =============================
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val universalExtractor by lazy { UniversalExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
@ -128,11 +130,10 @@ class AnimeID : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val jsonString = script.attr("data")
val jsonUnescape = unescapeJava(jsonString)!!.replace("\\", "")
val url = fetchUrls(jsonUnescape).firstOrNull()?.replace("\\\\", "\\") ?: ""
if (url.contains("streamtape") || url.contains("tape") || url.contains("stp")) {
streamtapeExtractor.videosFromUrl(url).also(videoList::addAll)
}
if (url.contains("wish") || url.contains("fviplions") || url.contains("obeywish")) {
streamwishExtractor.videosFromUrl(url, videoNameGen = { "StreamWish:$it" }).also(videoList::addAll)
return when {
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" })
else -> universalExtractor.videosFromUrl(url, headers)
}
}
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 {
extName = 'AnimeLatinoHD'
extClass = '.AnimeLatinoHD'
extVersionCode = 35
extVersionCode = 38
}
apply from: "$rootDir/common.gradle"

View file

@ -85,21 +85,21 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
if (url.contains("status=1")) {
val latestData = data["data"]!!.jsonArray
latestData.forEach { item ->
val animeItem = item!!.jsonObject
val animeItem = item.jsonObject
val anime = SAnime.create()
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
anime.title = animeItem["name"]!!.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.title = animeItem["name"]!!.jsonPrimitive.content
animeList.add(anime)
}
} else {
val popularToday = data["popular_today"]!!.jsonArray
popularToday.forEach { item ->
val animeItem = item!!.jsonObject
val animeItem = item.jsonObject
val anime = SAnime.create()
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
anime.title = animeItem["name"]!!.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.title = animeItem["name"]!!.jsonPrimitive.content
animeList.add(anime)
}
}
@ -122,12 +122,12 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
val pageProps = props["pageProps"]!!.jsonObject
val data = pageProps["data"]!!.jsonObject
newAnime.title = data["name"]!!.jsonPrimitive!!.content
newAnime.genre = data["genres"]!!.jsonPrimitive!!.content.split(",").joinToString()
newAnime.description = data["overview"]!!.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.setUrlWithoutDomain(externalOrInternalImg("anime/${data["slug"]!!.jsonPrimitive!!.content}"))
newAnime.title = data["name"]!!.jsonPrimitive.content
newAnime.genre = data["genres"]!!.jsonPrimitive.content.split(",").joinToString()
newAnime.description = data["overview"]!!.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.setUrlWithoutDomain(externalOrInternalImg("anime/${data["slug"]!!.jsonPrimitive.content}"))
}
}
return newAnime
@ -144,11 +144,11 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
val data = pageProps["data"]!!.jsonObject
val arrEpisode = data["episodes"]!!.jsonArray
arrEpisode.forEach { item ->
val animeItem = item!!.jsonObject
val animeItem = item.jsonObject
val episode = SEpisode.create()
episode.setUrlWithoutDomain(externalOrInternalImg("ver/${data["slug"]!!.jsonPrimitive!!.content}/${animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()}"))
episode.episode_number = animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()
episode.name = "Episodio ${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.name = "Episodio ${animeItem["number"]!!.jsonPrimitive.content.toFloat()}"
episodeList.add(episode)
}
}
@ -158,7 +158,7 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
private fun parseJsonArray(json: JsonElement?): List<JsonElement> {
val list = mutableListOf<JsonElement>()
json!!.jsonObject!!.entries!!.forEach { list.add(it.value) }
json!!.jsonObject.entries.forEach { list.add(it.value) }
return list
}
@ -178,11 +178,11 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
val pageProps = props["pageProps"]!!.jsonObject
val data = pageProps["data"]!!.jsonObject
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 ->
val servers = player!!.jsonArray
val servers = player.jsonArray
servers.forEach { server ->
val item = server!!.jsonObject
val item = server.jsonObject
val request = client.newCall(
GET(
url = "https://api.animelatinohd.com/stream/${item["id"]!!.jsonPrimitive.content}",
@ -193,9 +193,9 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
.build(),
),
).execute()
val locationsDdh = request!!.networkResponse.toString()
val locationsDdh = request.networkResponse.toString()
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()
if (embedUrl.contains("filemoon")) {
val vidHeaders = headers.newBuilder()
@ -211,7 +211,7 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
StreamTapeExtractor(client).videoFromUrl(url, "$language Streamtape")?.let { videoList.add(it) }
}
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")) {
OkruExtractor(client).videosFromUrl(url, language).also(videoList::addAll)
@ -281,11 +281,11 @@ class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
val data = pageProps["data"]!!.jsonObject
val arrData = data["data"]!!.jsonArray
arrData.forEach { item ->
val animeItem = item!!.jsonObject
val animeItem = item.jsonObject
val anime = SAnime.create()
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
anime.title = animeItem["name"]!!.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.title = animeItem["name"]!!.jsonPrimitive.content
animeList.add(anime)
}
}

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