Merge branch 'Kohi-den:main' into main

This commit is contained in:
Dark25 2024-09-18 19:45:36 +01:00 committed by GitHub
commit 44158e73d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
118 changed files with 2086 additions and 3414 deletions

View file

@ -1,5 +1,5 @@
name: 🐞 Issue report
description: Report a source issue in Aniyomi
name: 🐞 Bug report
description: Report a bug
labels: [Bug]
body:
@ -57,11 +57,22 @@ body:
required: true
- type: input
id: aniyomi-version
id: which-app
attributes:
label: Aniyomi version
label: Which app are you using?
placeholder: |
Example: Aniyomi, Kuukiyomi, Animiru, Animetail
description: |
Self explanatory
validations:
required: true
- type: input
id: app-version
attributes:
label: App version
description: |
You can find your Aniyomi version in **More → About**.
You can find your app version in **More → About**.
placeholder: |
Example: "0.15.2.4" or "Preview r7473"
validations:
@ -95,13 +106,9 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true
- label: I have tried the [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/aniyomiorg/aniyomi/issues/new/choose).
required: true
- label: I will fill out all of the requested information in this form.
required: true

View file

@ -1,5 +1,5 @@
name: 🌐 Source request
description: Suggest a new source for Aniyomi
description: Suggest a new source
labels: [Source request]
body:

View file

@ -53,7 +53,5 @@ body:
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/aniyomiorg/aniyomi/issues/new/choose).
required: true
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View file

@ -1,41 +0,0 @@
name: 🧠 Meta request
description: Suggest improvements to the project
labels: [Meta request]
body:
- type: textarea
id: feature-description
attributes:
label: Describe why this should be added
description: How can the project be improved?
placeholder: |
Example:
"It should work like this..."
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open (or closed) issue.
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/aniyomiorg/aniyomi/issues/new/choose).
required: true
- label: I have updated the app to version **[0.15.2.4](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View file

@ -25,7 +25,7 @@ body:
label: Requirements
description: Your request will be denied if you don't meet these requirements.
options:
- label: Proof of ownership/intent to remove sent to a Aniyomi Discord server mod via DM
- label: Proof of ownership/intent to remove sent to a Kohi den Discord server mod via DM
required: true
- label: Site only hosts content scanlated by the group and not stolen from other scanlators or official releases (i.e., not an aggregator site)
required: true

View file

@ -1,14 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: ⚠️ Application issue
url: https://github.com/aniyomiorg/aniyomi/issues/new/choose
about: Issues and requests about the app itself should be opened in the tachiyomi repository instead
- name: 📦 Aniyomi extensions
url: https://aniyomi.org/extensions
about: List of all available extensions with download links
- name: 🖥️ Aniyomi website
url: https://aniyomi.org/help/
about: Guides, troubleshooting, and answers to common questions
- name: Aniyomi app GitHub repository
url: https://github.com/aniyomiorg/aniyomi
about: Issues about the app itself should be opened here instead.

View file

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

View file

@ -0,0 +1,86 @@
package eu.kanade.tachiyomi.lib.streamsilkextractor
import java.util.regex.Matcher
import java.util.regex.Pattern
import kotlin.math.pow
class JsHunter(private val hunterJS: String) {
/**
* Detects whether the javascript is H.U.N.T.E.R coded.
*
* @return true if it's H.U.N.T.E.R coded.
*/
fun detect(): Boolean {
val p = Pattern.compile("eval\\(function\\(h,u,n,t,e,r\\)")
val searchResults = p.matcher(hunterJS)
return searchResults.find()
}
/**
* Unpack the javascript
*
* @return the javascript unhunt or null.
*/
fun dehunt(): String? {
try {
val p: Pattern =
Pattern.compile(
"""\}\("([^"]+)",[^,]+,\s*"([^"]+)",\s*(\d+),\s*(\d+)""",
Pattern.DOTALL
)
val searchResults: Matcher = p.matcher(hunterJS)
if (searchResults.find() && searchResults.groupCount() == 4) {
val h = searchResults.group(1)!!.toString()
val n = searchResults.group(2)!!.toString()
val t = searchResults.group(3)!!.toInt()
val e = searchResults.group(4)!!.toInt()
return hunter(h, n, t, e)
}
} catch (e: Exception) {
return null
}
return null
}
private fun duf(d: String, e: Int, f: Int = 10): Int {
val str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
val g = str.toList()
val h = g.take(e)
val i = g.take(f)
val dList = d.reversed().toList()
var j = 0.0
for ((c, b) in dList.withIndex()) {
if (b in h) {
j += h.indexOf(b) * e.toDouble().pow(c)
}
}
var k = ""
while (j > 0) {
k = i[(j % f).toInt()] + k
j = (j - j % f) / f
}
return k.toIntOrNull() ?: 0
}
private fun hunter(h: String, n: String, t: Int, e: Int): String {
var result = ""
var i = 0
while (i < h.length) {
var j = 0
var s = ""
while (h[i] != n[e]) {
s += h[i]
i++
}
while (j < n.length) {
s = s.replace(n[j], j.digitToChar())
j++
}
result += (duf(s, e) - t).toChar()
i++
}
return result
}
}

View file

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.lib.streamsilkextractor
import eu.kanade.tachiyomi.animesource.model.Track
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 kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.internal.commonEmptyHeaders
import uy.kohesive.injekt.injectLazy
class StreamSilkExtractor(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
private val srcRegex = Regex("var urlPlay =\\s*\"(.*?m3u8.*?)\"")
private val subsRegex = Regex("jsonUrl = `([^`]*)`")
private val videoHeaders by lazy {
headers.newBuilder()
.set("Referer", "$STREAM_SILK_URL/")
.set("Origin", STREAM_SILK_URL)
.build()
}
private val json: Json by injectLazy()
private val playlistUtils by lazy { PlaylistUtils(client, videoHeaders) }
fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "${prefix}StreamSilk:$it" }
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "StreamSilk:$quality" }): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val scriptData = document.select("script").firstOrNull { it.html().contains("h,u,n,t,e,r") }?.data() ?: return emptyList()
val deHunt = JsHunter(scriptData).dehunt() ?: return emptyList()
val link = extractLink(deHunt) ?: return emptyList()
val subs = buildList {
val subUrl = extractSubs(deHunt)
if (!subUrl.isNullOrEmpty()) {
runCatching {
client.newCall(GET(subUrl, videoHeaders)).execute().body.string()
.let { json.decodeFromString<List<SubtitleDto>>(it) }
.forEach { add(Track(it.file, it.label)) }
}
}
}
return playlistUtils.extractFromHls(link, videoNameGen = videoNameGen, subtitleList = subs)
}
private fun extractLink(script: String) = srcRegex.find(script)?.groupValues?.get(1)?.trim()
private fun extractSubs(script: String) = subsRegex.find(script)?.groupValues?.get(1)?.trim()
@Serializable
data class SubtitleDto(val file: String, val label: String)
}
private const val STREAM_SILK_URL = "https://streamsilk.com"

View file

@ -0,0 +1,8 @@
plugins {
id("lib-android")
}
dependencies {
implementation(project(":lib:playlist-utils"))
implementation("org.mozilla:rhino:1.7.14")
}

View file

@ -0,0 +1,104 @@
package eu.kanade.tachiyomi.lib.vidguardextractor
import android.util.Base64
import android.util.Log
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 kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.mozilla.javascript.Context
import org.mozilla.javascript.NativeJSON
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.Scriptable
import uy.kohesive.injekt.injectLazy
class VidGuardExtractor(private val client: OkHttpClient) {
private val playlistUtils by lazy { PlaylistUtils(client) }
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "${prefix}VidGuard:$it" }
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "VidGuard:$quality" }): List<Video> {
val res = client.newCall(GET(url)).execute().asJsoup()
val scriptData = res.selectFirst("script:containsData(eval)")?.data() ?: return emptyList()
val jsonStr2 = json.decodeFromString<SvgObject>(runJS2(scriptData))
val playlistUrl = sigDecode(jsonStr2.stream)
return playlistUtils.extractFromHls(playlistUrl, videoNameGen = videoNameGen)
}
private fun sigDecode(url: String): String {
val sig = url.split("sig=")[1].split("&")[0]
val t = sig.chunked(2)
.joinToString("") { (Integer.parseInt(it, 16) xor 2).toChar().toString() }
.let {
val padding = when (it.length % 4) {
2 -> "=="
3 -> "="
else -> ""
}
String(Base64.decode((it + padding).toByteArray(Charsets.UTF_8), Base64.DEFAULT))
}
.dropLast(5)
.reversed()
.toCharArray()
.apply {
for (i in indices step 2) {
if (i + 1 < size) {
this[i] = this[i + 1].also { this[i + 1] = this[i] }
}
}
}
.concatToString()
.dropLast(5)
return url.replace(sig, t)
}
private fun runJS2(hideMyHtmlContent: String): String {
var result = ""
val r = Runnable {
val rhino = Context.enter()
rhino.initSafeStandardObjects()
rhino.optimizationLevel = -1
val scope: Scriptable = rhino.initSafeStandardObjects()
scope.put("window", scope, scope)
try {
rhino.evaluateString(
scope,
hideMyHtmlContent,
"JavaScript",
1,
null,
)
val svgObject = scope.get("svg", scope)
result = if (svgObject is NativeObject) {
NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null)
.toString()
} else {
Context.toString(svgObject)
}
} catch (e: Exception) {
Log.i("Error", e.toString())
} finally {
Context.exit()
}
}
val t = Thread(ThreadGroup("A"), r, "thread_rhino", 2000000) // StackSize 2Mb: Run in a thread because rhino requires more stack size for large scripts.
t.start()
t.join()
t.interrupt()
return result
}
}
@Serializable
data class SvgObject(
val stream: String,
val hash: String,
)

View file

@ -20,14 +20,15 @@ class VkExtractor(private val client: OkHttpClient, private val headers: Headers
.build()
}
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val data = client.newCall(GET(url, documentHeaders)).execute()
.body.string()
fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "${prefix}Vk:$it" }
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "Vk:$quality" }): List<Video> {
val data = client.newCall(GET(url, documentHeaders)).execute().body.string()
return REGEX_VIDEO.findAll(data).map {
val quality = it.groupValues[1]
val videoUrl = it.groupValues[2].replace("\\/", "/")
Video(videoUrl, "${prefix}vk.com - ${quality}p", videoUrl, videoHeaders)
Video(videoUrl, videoNameGen("${quality}p"), videoUrl, videoHeaders)
}.toList()
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime-Base'
extClass = '.AnimeBase'
extVersionCode = 25
extVersionCode = 26
isNsfw = true
}
@ -10,6 +10,7 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:voe-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:vidguard-extractor"))
implementation(project(":lib:playlist-utils"))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}

View file

@ -4,7 +4,6 @@ import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.de.animebase.extractors.UnpackerExtractor
import eu.kanade.tachiyomi.animeextension.de.animebase.extractors.VidGuardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -13,6 +12,7 @@ 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.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST

View file

@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.animeextension.de.animebase.extractors
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VidGuardExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch) {
var payload: String = ""
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
?.absUrl("src")
?: return emptyList()
val headers = Headers.headersOf("Referer", url)
val script = client.newCall(GET(scriptUrl, headers)).execute()
.body.string()
val sources = getSourcesFromScript(script, url)
.takeIf { it.isNotBlank() && it != "undefined" }
?: return emptyList()
return sources.substringAfter("stream:[").substringBefore("}]")
.split('{')
.drop(1)
.mapNotNull { line ->
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let(::fixUrl)
?: return@mapNotNull null
Video(videoUrl, "VidGuard - $resolution", videoUrl, headers)
}
}
private fun getSourcesFromScript(script: String, url: String): String {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsinterface = JsObject(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
cacheMode = WebSettings.LOAD_NO_CACHE
}
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
view?.evaluateJavascript(script) {}
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
}
}
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
}
latch.await(5, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsinterface.payload
}
private fun fixUrl(url: String): String {
val httpUrl = url.toHttpUrl()
val originalSign = httpUrl.queryParameter("sig")!!
val newSign = originalSign.chunked(2).joinToString("") {
Char(it.toInt(16) xor 2).toString()
}
.let { String(Base64.decode(it, Base64.DEFAULT)) }
.substring(5)
.chunked(2)
.reversed()
.joinToString("")
.substring(5)
return httpUrl.newBuilder()
.removeAllQueryParameters("sig")
.addQueryParameter("sig", newSign)
.build()
.toString()
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Moflix-Stream'
extClass = '.MoflixStream'
extVersionCode = 9
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"
@ -10,6 +10,7 @@ dependencies {
implementation(project(':lib:streamvid-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:vidguard-extractor'))
implementation(project(':lib:playlist-utils'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

View file

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.SearchDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.SeasonPaginationDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.dto.VideoResponseDto
import eu.kanade.tachiyomi.animeextension.de.moflixstream.extractors.UnpackerExtractor
import eu.kanade.tachiyomi.animeextension.de.moflixstream.extractors.VidGuardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamvidextractor.StreamVidExtractor
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.parseAs
import kotlinx.serialization.json.Json

View file

@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.animeextension.de.moflixstream.extractors
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VidGuardExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch) {
var payload: String = ""
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
?.absUrl("src")
?: return emptyList()
val headers = Headers.headersOf("Referer", url)
val script = client.newCall(GET(scriptUrl, headers)).execute()
.body.string()
val sources = getSourcesFromScript(script, url)
.takeIf { it.isNotBlank() && it != "undefined" }
?: return emptyList()
return sources.substringAfter("stream:[").substringBefore("}]")
.split('{')
.drop(1)
.mapNotNull { line ->
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let(::fixUrl)
?: return@mapNotNull null
Video(videoUrl, "VidGuard - $resolution", videoUrl, headers)
}
}
private fun getSourcesFromScript(script: String, url: String): String {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsinterface = JsObject(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
cacheMode = WebSettings.LOAD_NO_CACHE
}
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
view?.evaluateJavascript(script) {}
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
}
}
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
}
latch.await(5, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsinterface.payload
}
private fun fixUrl(url: String): String {
val httpUrl = url.toHttpUrl()
val originalSign = httpUrl.queryParameter("sig")!!
val newSign = originalSign.chunked(2).joinToString("") {
Char(it.toInt(16) xor 2).toString()
}
.let { String(Base64.decode(it, Base64.DEFAULT)) }
.substring(5)
.chunked(2)
.reversed()
.joinToString("")
.substring(5)
return httpUrl.newBuilder()
.removeAllQueryParameters("sig")
.addQueryParameter("sig", newSign)
.build()
.toString()
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AllAnime'
extClass = '.AllAnime'
extVersionCode = 31
extVersionCode = 32
}
apply from: "$rootDir/common.gradle"
@ -12,5 +12,7 @@ dependencies {
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:gogostream-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

View file

@ -14,10 +14,12 @@ 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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.gogostreamextractor.GogoStreamExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
@ -271,6 +273,8 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
private val okruExtractor by lazy { OkruExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val response = client.newCall(videoListRequest(episode)).await()
@ -284,11 +288,13 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
// list of alternative hosters
val mappings = listOf(
"vidstreaming" to listOf("vidstreaming", "https://gogo", "playgo1.cc", "playtaku"),
"vidstreaming" to listOf("vidstreaming", "https://gogo", "playgo1.cc", "playtaku", "vidcloud"),
"doodstream" to listOf("dood"),
"okru" to listOf("ok.ru"),
"okru" to listOf("ok.ru", "okru"),
"mp4upload" to listOf("mp4upload.com"),
"streamlare" to listOf("streamlare.com"),
"filemoon" to listOf("filemoon", "moonplayer"),
"streamwish" to listOf("wish"),
)
videoJson.data.episode.sourceUrls.forEach { video ->
@ -356,6 +362,12 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
sName == "streamlare" -> {
streamlareExtractor.videosFromUrl(server.sourceUrl)
}
sName == "filemoon" -> {
filemoonExtractor.videosFromUrl(server.sourceUrl, prefix = "Filemoon:")
}
sName == "streamwish" -> {
streamwishExtractor.videosFromUrl(server.sourceUrl, videoNameGen = { "StreamWish:$it" })
}
else -> emptyList()
}.let { it.map { v -> Pair(v, server.priority) } }
},
@ -367,15 +379,13 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ==============================
private fun String.decryptSource(): String {
return if (this.startsWith("-")) {
this.substringAfterLast('-').chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray().map {
(it.toInt() xor 56).toChar()
}.joinToString("")
} else {
this
}
if (!this.startsWith("-")) return this
return this.substringAfterLast('-').chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray().map {
(it.toInt() xor 56).toChar()
}.joinToString("")
}
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
@ -466,6 +476,8 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
"mp4upload",
"streamlare",
"doodstream",
"filemoon",
"streamwish",
)
private const val PREF_SITE_DOMAIN_KEY = "preferred_site_domain"

View file

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

View file

@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelFlatMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
@ -35,12 +34,11 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.math.ceil
@ExperimentalSerializationApi
class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeOwl"
override val baseUrl = "https://animeowl.us"
override val baseUrl = "https://animeowl.live"
override val lang = "en"
@ -109,6 +107,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
genre = document.select("div.genre > a").joinToString { it.text() }
author = document.select("div.type > a").text()
status = parseStatus(document.select("div.status > span").text())
thumbnail_url = document.selectFirst(".cover-img-container > img")?.attr("abs:src")
description = buildString {
document.select("div.anime-desc.desc-content").text()
.takeIf { it.isNotBlank() }
@ -127,32 +126,33 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val animeId = response.asJsoup().select("div#unq-anime-id").attr("animeId")
val episodes = client.newCall(
GET("$baseUrl/api/anime/$animeId/episodes"),
).execute()
.parseAs<EpisodeResponse>()
val document = response.asJsoup()
val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it ->
EpisodeResponse.Episode(
id = it.text().toDouble(),
episodeIndex = idx.toString(),
name = it.text(),
lang = "Sub",
href = it.attr("abs:href"),
)
}
val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it ->
EpisodeResponse.Episode(
id = it.text().toDouble(),
episodeIndex = idx.toString(),
name = it.text(),
lang = "Dub",
href = it.attr("abs:href"),
)
}
return listOf(
episodes.sub.map { it.copy(lang = "Sub") },
episodes.dub.map { it.copy(lang = "Dub") },
).flatten()
.groupBy { it.name }
.map { (epNum, epList) ->
SEpisode.create().apply {
url = LinkData(
epList.map { ep ->
Link(
ep.buildUrl(episodes.subSlug, episodes.dubSlug),
ep.lang!!,
)
},
).toJsonString()
episode_number = epNum.toFloatOrNull() ?: 0F
name = "Episode $epNum"
}
return listOf(sub, dub).flatten().groupBy { it.name }.map { (epNum, epList) ->
SEpisode.create().apply {
url = LinkData(epList.map { ep -> Link(ep.href!!, ep.lang!!) }).toJsonString()
episode_number = epNum.toFloatOrNull() ?: 0F
name = "Episode $epNum"
}
.sortedByDescending { it.episode_number }
}.sortedByDescending { it.episode_number }
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
@ -160,9 +160,9 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> =
json.decodeFromString<LinkData>(episode.url)
.links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
override suspend fun getVideoList(episode: SEpisode): List<Video> {
return json.decodeFromString<LinkData>(episode.url).links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
@ -228,15 +228,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
)
}
private fun LinkData.toJsonString(): String {
return json.encodeToString(this)
}
private fun EpisodeResponse.Episode.buildUrl(subSlug: String, dubSlug: String): String =
when (lang) {
"dub" -> dubSlug
else -> subSlug
}.let { "$baseUrl/watch/$it/$episodeIndex" }
private fun LinkData.toJsonString(): String = json.encodeToString(this)
private fun parseStatus(statusString: String): Int =
when (statusString) {

View file

@ -31,11 +31,12 @@ data class EpisodeResponse(
) {
@Serializable
data class Episode(
val id: Int,
val id: Double? = null,
val name: String,
val lang: String? = null,
@SerialName("episode_index")
val episodeIndex: String,
val href: String? = null,
)
}

View file

@ -36,18 +36,18 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
}
.let(Deobfuscator::deobfuscateScript)
?: throw Exception("Unable to get clean JS")
val jwt = JWT_REGEX.find(epJS)?.groupValues?.get(1) ?: throw Exception("Unable to get jwt")
val jwt = findFirstJwt(epJS) ?: throw Exception("Unable to get jwt")
val videoList = mutableListOf<Video>()
val servers = client.newCall(GET("$baseUrl$dataSrc")).execute()
.parseAs<OwlServers>()
coroutineScope {
val lufDeferred = async {
servers.luffy?.let { luffy ->
noRedirectClient.newCall(GET("${luffy}$jwt")).execute()
.use { it.headers["Location"] }
?.let { videoList.add(Video(it, "Luffy - ${link.lang} - 1080p", it)) }
?.let { videoList.add(Video(it, "${link.lang} Luffy:1080p", it)) }
}
}
val kaiDeferred = async {
@ -70,18 +70,20 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
return videoList
}
private fun getHLS(url: String, server: String, lang: String): List<Video> =
client.newCall(GET(url)).execute()
.parseAs<Stream>()
.url
.let {
playlistUtils.extractFromHls(
it,
videoNameGen = { qty -> "$server - $lang - $qty" },
)
private fun getHLS(url: String, server: String, lang: String): List<Video> {
return client.newCall(GET(url)).execute().let {
if (it.isSuccessful) {
it.parseAs<Stream>().url.let {
playlistUtils.extractFromHls(it, videoNameGen = { qty -> "$lang $server:$qty" })
}
} else {
emptyList()
}
}
}
companion object {
private val JWT_REGEX by lazy { "const\\s+(?:[A-Za-z0-9_]*)\\s*=\\s*'([^']+)'".toRegex() }
private fun findFirstJwt(text: String): String? {
val jwtPattern = Regex("['\"]([A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+)['\"]")
return jwtPattern.find(text)?.groupValues?.get(1)
}
}

View file

@ -1,14 +0,0 @@
ext {
extName = 'Aniwave'
extClass = '.Aniwave'
extVersionCode = 75
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:vidsrc-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:streamtape-extractor'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,538 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.nineanime
import android.app.Application
import android.content.SharedPreferences
import android.webkit.URLUtil
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
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.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.vidsrcextractor.VidsrcExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapBlocking
import eu.kanade.tachiyomi.util.parseAs
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 java.text.SimpleDateFormat
import java.util.Locale
class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Aniwave"
override val id: Long = 98855593379717478
override val baseUrl by lazy {
val customDomain = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
if (customDomain.isNullOrBlank()) {
preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
} else {
customDomain
}
}
override val lang = "en"
override val supportsLatest = true
private val utils by lazy { AniwaveUtils() }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val refererHeaders = headers.newBuilder().apply {
add("Referer", "$baseUrl/")
}.build()
private val markFiller by lazy { preferences.getBoolean(PREF_MARK_FILLERS_KEY, PREF_MARK_FILLERS_DEFAULT) }
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/filter?sort=trending&page=$page", refererHeaders)
override fun popularAnimeSelector(): String = "div.ani.items > div.item"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
element.select("a.name").let { a ->
setUrlWithoutDomain(a.attr("href").substringBefore("?"))
title = a.text()
}
thumbnail_url = element.select("div.poster img").attr("src")
}
override fun popularAnimeNextPageSelector(): String =
"nav > ul.pagination > li.active ~ li"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/filter?sort=recently_updated&page=$page", refererHeaders)
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filters = AniwaveFilters.getSearchParameters(filters)
val vrf = if (query.isNotBlank()) utils.vrfEncrypt(query) else ""
var url = "$baseUrl/filter?keyword=$query"
if (filters.genre.isNotBlank()) url += filters.genre
if (filters.country.isNotBlank()) url += filters.country
if (filters.season.isNotBlank()) url += filters.season
if (filters.year.isNotBlank()) url += filters.year
if (filters.type.isNotBlank()) url += filters.type
if (filters.status.isNotBlank()) url += filters.status
if (filters.language.isNotBlank()) url += filters.language
if (filters.rating.isNotBlank()) url += filters.rating
return GET("$url&sort=${filters.sort}&page=$page&vrf=$vrf", refererHeaders)
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AniwaveFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val newDocument = resolveSearchAnime(anime, document)
anime.apply {
title = newDocument.select("h1.title").text()
genre = newDocument.select("div:contains(Genre) > span > a").joinToString { it.text() }
description = newDocument.select("div.synopsis > div.shorting > div.content").text()
author = newDocument.select("div:contains(Studio) > span > a").text()
status = parseStatus(newDocument.select("div:contains(Status) > span").text())
val altName = "Other name(s): "
newDocument.select("h1.title").attr("data-jp").let {
if (it.isNotBlank()) {
description = when {
description.isNullOrBlank() -> altName + it
else -> description + "\n\n$altName" + it
}
}
}
}
return anime
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val response = client.newCall(GET(baseUrl + anime.url)).execute()
var document = response.asJsoup()
document = resolveSearchAnime(anime, document)
val id = document.selectFirst("div[data-id]")?.attr("data-id") ?: throw Exception("ID not found")
val vrf = utils.vrfEncrypt(id)
val listHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", baseUrl + anime.url)
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET("$baseUrl/ajax/episode/list/$id?vrf=$vrf#${anime.url}", listHeaders)
}
override fun episodeListSelector() = "div.episodes ul > li > a"
override fun episodeListParse(response: Response): List<SEpisode> {
val animeUrl = response.request.url.fragment!!
val document = response.parseAs<ResultResponse>().toDocument()
val episodeElements = document.select(episodeListSelector())
return episodeElements.parallelMapBlocking { episodeFromElements(it, animeUrl) }.reversed()
}
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
private fun episodeFromElements(element: Element, url: String): SEpisode {
val title = element.parent()?.attr("title") ?: ""
val epNum = element.attr("data-num")
val ids = element.attr("data-ids")
val sub = if (element.attr("data-sub").toInt().toBoolean()) "Sub" else ""
val dub = if (element.attr("data-dub").toInt().toBoolean()) "Dub" else ""
val softSub = if (SOFTSUB_REGEX.find(title) != null) "SoftSub" else ""
val extraInfo = if (element.hasClass("filler") && markFiller) {
" • Filler Episode"
} else {
""
}
val name = element.parent()?.select("span.d-title")?.text().orEmpty()
val namePrefix = "Episode $epNum"
return SEpisode.create().apply {
this.name = "Episode $epNum" +
if (name.isNotEmpty() && name != namePrefix) ": $name" else ""
this.url = "$ids&epurl=$url/ep-$epNum"
episode_number = epNum.toFloat()
date_upload = RELEASE_REGEX.find(title)?.let {
parseDate(it.groupValues[1])
} ?: 0L
scanlator = arrayOf(sub, softSub, dub).filter(String::isNotBlank).joinToString(", ") + extraInfo
}
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val ids = episode.url.substringBefore("&")
val vrf = utils.vrfEncrypt(ids)
val url = "/ajax/server/list/$ids?vrf=$vrf"
val epurl = episode.url.substringAfter("epurl=")
val listHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", baseUrl + epurl)
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET("$baseUrl$url#$epurl", listHeaders)
}
data class VideoData(
val type: String,
val serverId: String,
val serverName: String,
)
override fun videoListParse(response: Response): List<Video> {
val epurl = response.request.url.fragment!!
val document = response.parseAs<ResultResponse>().toDocument()
val hosterSelection = getHosters()
val typeSelection = preferences.getStringSet(PREF_TYPE_TOGGLE_KEY, PREF_TYPES_TOGGLE_DEFAULT)!!
return document.select("div.servers > div").parallelFlatMapBlocking { elem ->
val type = elem.attr("data-type").replaceFirstChar { it.uppercase() }
elem.select("li").mapNotNull { serverElement ->
val serverId = serverElement.attr("data-link-id")
val serverName = serverElement.text().lowercase()
if (hosterSelection.contains(serverName, true).not()) return@mapNotNull null
if (typeSelection.contains(type, true).not()) return@mapNotNull null
VideoData(type, serverId, serverName)
}
}
.parallelFlatMapBlocking { extractVideo(it, epurl) }
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================= Utilities ==============================
private val vidsrcExtractor by lazy { VidsrcExtractor(client, headers) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private fun extractVideo(server: VideoData, epUrl: String): List<Video> {
val vrf = utils.vrfEncrypt(server.serverId)
val listHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", baseUrl + epUrl)
add("X-Requested-With", "XMLHttpRequest")
}.build()
val response = client.newCall(
GET("$baseUrl/ajax/server/${server.serverId}?vrf=$vrf", listHeaders),
).execute()
if (response.code != 200) return emptyList()
return runCatching {
val parsed = response.parseAs<ServerResponse>()
val embedLink = utils.vrfDecrypt(parsed.result.url)
when (server.serverName) {
"vidstream" -> vidsrcExtractor.videosFromUrl(embedLink, "Vidstream", server.type)
"megaf" -> vidsrcExtractor.videosFromUrl(embedLink, "MegaF", server.type)
"moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ")
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
"mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")
else -> emptyList()
}
}.getOrElse { emptyList() }
}
private fun Int.toBoolean() = this == 1
private fun Set<String>.contains(s: String, ignoreCase: Boolean): Boolean {
return any { it.equals(s, ignoreCase) }
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
compareByDescending<Video> { it.quality.contains(lang) }
.thenByDescending { it.quality.contains(quality) }
.thenByDescending { it.quality.contains(server, true) },
)
}
@Synchronized
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Releasing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
private fun resolveSearchAnime(anime: SAnime, document: Document): Document {
if (document.location().startsWith("$baseUrl/filter?keyword=")) { // redirected to search
val element = document.selectFirst(searchAnimeSelector())
val foundAnimePath = element?.selectFirst("a[href]")?.attr("href")
?: throw Exception("Search element not found (resolveSearch)")
anime.url = foundAnimePath // probably doesn't work as intended
return client.newCall(GET(baseUrl + foundAnimePath)).execute().asJsoup()
}
return document
}
private fun getHosters(): Set<String> {
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
var invalidRecord = false
hosterSelection.forEach { str ->
val index = HOSTERS_NAMES.indexOf(str)
if (index == -1) {
invalidRecord = true
}
}
// found invalid record, reset to defaults
if (invalidRecord) {
preferences.edit().putStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT).apply()
return PREF_HOSTER_DEFAULT.toSet()
}
return hosterSelection.toSet()
}
companion object {
private val SOFTSUB_REGEX by lazy { Regex("""\bsoftsub\b""", RegexOption.IGNORE_CASE) }
private val RELEASE_REGEX by lazy { Regex("""Release: (\d+\/\d+\/\d+ \d+:\d+)""") }
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.ENGLISH)
}
private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_DEFAULT = "https://aniwave.to"
private const val PREF_CUSTOM_DOMAIN_KEY = "custom_domain"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_LANG_KEY = "preferred_language"
private const val PREF_LANG_DEFAULT = "Sub"
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Vidstream"
private const val PREF_MARK_FILLERS_KEY = "mark_fillers"
private const val PREF_MARK_FILLERS_DEFAULT = true
private const val PREF_HOSTER_KEY = "hoster_selection"
private val HOSTERS = arrayOf(
"Vidstream",
"Megaf",
"MoonF",
"StreamTape",
"MP4u",
)
private val HOSTERS_NAMES = arrayOf(
"vidstream",
"megaf",
"moonf",
"streamtape",
"mp4u",
)
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
private const val PREF_TYPE_TOGGLE_KEY = "type_selection"
private val TYPES = arrayOf("Sub", "Softsub", "Dub")
private val PREF_TYPES_TOGGLE_DEFAULT = TYPES.toSet()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// validate hosters preferences and if invalid reset
try {
getHosters()
} catch (e: Exception) {
Toast.makeText(screen.context, e.toString(), Toast.LENGTH_LONG).show()
}
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain"
entries = arrayOf("aniwave.to", "aniwavetv.to (unofficial)")
entryValues = arrayOf("https://aniwave.to", "https://aniwavetv.to")
setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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_LANG_KEY
title = "Preferred Type"
entries = TYPES
entryValues = TYPES
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_SERVER_KEY
title = "Preferred Server"
entries = HOSTERS
entryValues = HOSTERS_NAMES
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)
SwitchPreferenceCompat(screen.context).apply {
key = PREF_MARK_FILLERS_KEY
title = "Mark filler episodes"
setDefaultValue(PREF_MARK_FILLERS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = "Enable/Disable Hosts"
entries = HOSTERS
entryValues = HOSTERS_NAMES
setDefaultValue(PREF_HOSTER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_TYPE_TOGGLE_KEY
title = "Enable/Disable Types"
entries = TYPES
entryValues = TYPES
setDefaultValue(PREF_TYPES_TOGGLE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_CUSTOM_DOMAIN_KEY
title = "Custom domain"
setDefaultValue(null)
val currentValue = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
summary = if (currentValue.isNullOrBlank()) {
"Custom domain of your choosing"
} else {
"Domain: \"$currentValue\". \nLeave blank to disable. Overrides any domain preferences!"
}
setOnPreferenceChangeListener { _, newValue ->
val newDomain = newValue as String
if (newDomain.isBlank() || URLUtil.isValidUrl(newDomain)) {
summary = "Restart to apply changes"
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, newDomain).apply()
true
} else {
Toast.makeText(screen.context, "Invalid url. Url example: https://aniwave.to", Toast.LENGTH_LONG).show()
false
}
}
}.also(screen::addPreference)
}
}

View file

@ -1,49 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.nineanime
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
@Serializable
data class ServerResponse(
val result: Result,
) {
@Serializable
data class Result(
val url: String,
)
}
@Serializable
data class MediaResponseBody(
val status: Int,
val result: Result,
) {
@Serializable
data class Result(
val sources: ArrayList<Source>,
val tracks: ArrayList<SubTrack> = ArrayList(),
) {
@Serializable
data class Source(
val file: String,
)
@Serializable
data class SubTrack(
val file: String,
val label: String = "",
val kind: String,
)
}
}
@Serializable
data class ResultResponse(
val result: String,
) {
fun toDocument(): Document {
return Jsoup.parseBodyFragment(result)
}
}

View file

@ -1,270 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.nineanime
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniwaveFilters {
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, 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.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
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"
}
}
}
class SortFilter : QueryPartFilter("Sort order", NineAnimeFiltersData.SORT)
class GenreFilter : CheckBoxFilterList(
"Genre",
NineAnimeFiltersData.GENRE.map { CheckBoxVal(it.first, false) },
)
class CountryFilter : CheckBoxFilterList(
"Country",
NineAnimeFiltersData.COUNTRY.map { CheckBoxVal(it.first, false) },
)
class SeasonFilter : CheckBoxFilterList(
"Season",
NineAnimeFiltersData.SEASON.map { CheckBoxVal(it.first, false) },
)
class YearFilter : CheckBoxFilterList(
"Year",
NineAnimeFiltersData.YEAR.map { CheckBoxVal(it.first, false) },
)
class TypeFilter : CheckBoxFilterList(
"Type",
NineAnimeFiltersData.TYPE.map { CheckBoxVal(it.first, false) },
)
class StatusFilter : CheckBoxFilterList(
"Status",
NineAnimeFiltersData.STATUS.map { CheckBoxVal(it.first, false) },
)
class LanguageFilter : CheckBoxFilterList(
"Language",
NineAnimeFiltersData.LANGUAGE.map { CheckBoxVal(it.first, false) },
)
class RatingFilter : CheckBoxFilterList(
"Rating",
NineAnimeFiltersData.RATING.map { CheckBoxVal(it.first, false) },
)
val FILTER_LIST get() = AnimeFilterList(
SortFilter(),
AnimeFilter.Separator(),
GenreFilter(),
CountryFilter(),
SeasonFilter(),
YearFilter(),
TypeFilter(),
StatusFilter(),
LanguageFilter(),
RatingFilter(),
)
data class FilterSearchParams(
val sort: String = "",
val genre: String = "",
val country: String = "",
val season: String = "",
val year: String = "",
val type: String = "",
val status: String = "",
val language: String = "",
val rating: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<SortFilter>(),
filters.parseCheckbox<GenreFilter>(NineAnimeFiltersData.GENRE, "genre"),
filters.parseCheckbox<CountryFilter>(NineAnimeFiltersData.COUNTRY, "country"),
filters.parseCheckbox<SeasonFilter>(NineAnimeFiltersData.SEASON, "season"),
filters.parseCheckbox<YearFilter>(NineAnimeFiltersData.YEAR, "year"),
filters.parseCheckbox<TypeFilter>(NineAnimeFiltersData.TYPE, "type"),
filters.parseCheckbox<StatusFilter>(NineAnimeFiltersData.STATUS, "status"),
filters.parseCheckbox<LanguageFilter>(NineAnimeFiltersData.LANGUAGE, "language"),
filters.parseCheckbox<RatingFilter>(NineAnimeFiltersData.RATING, "rating"),
)
}
private object NineAnimeFiltersData {
val SORT = arrayOf(
Pair("Most relevance", "most_relevance"),
Pair("Recently updated", "recently_updated"),
Pair("Recently added", "recently_added"),
Pair("Release date", "release_date"),
Pair("Trending", "trending"),
Pair("Name A-Z", "title_az"),
Pair("Scores", "scores"),
Pair("MAL scores", "mal_scores"),
Pair("Most watched", "most_watched"),
Pair("Most favourited", "most_favourited"),
Pair("Number of episodes", "number_of_episodes"),
)
val GENRE = arrayOf(
Pair("Action", "1"),
Pair("Adventure", "2"),
Pair("Avant Garde", "2262888"),
Pair("Boys Love", "2262603"),
Pair("Comedy", "4"),
Pair("Demons", "4424081"),
Pair("Drama", "7"),
Pair("Ecchi", "8"),
Pair("Fantasy", "9"),
Pair("Girls Love", "2263743"),
Pair("Gourmet", "2263289"),
Pair("Harem", "11"),
Pair("Horror", "14"),
Pair("Isekai", "3457284"),
Pair("Iyashikei", "4398552"),
Pair("Josei", "15"),
Pair("Kids", "16"),
Pair("Magic", "4424082"),
Pair("Mahou Shoujo", "3457321"),
Pair("Martial Arts", "18"),
Pair("Mecha", "19"),
Pair("Military", "20"),
Pair("Music", "21"),
Pair("Mystery", "22"),
Pair("Parody", "23"),
Pair("Psychological", "25"),
Pair("Reverse Harem", "4398403"),
Pair("Romance", "26"),
Pair("School", "28"),
Pair("Sci-Fi", "29"),
Pair("Seinen", "30"),
Pair("Shoujo", "31"),
Pair("Shounen", "33"),
Pair("Slice of Life", "35"),
Pair("Space", "36"),
Pair("Sports", "37"),
Pair("Super Power", "38"),
Pair("Supernatural", "39"),
Pair("Suspense", "2262590"),
Pair("Thriller", "40"),
Pair("Vampire", "41"),
)
val COUNTRY = arrayOf(
Pair("China", "120823"),
Pair("Japan", "120822"),
)
val SEASON = arrayOf(
Pair("Fall", "fall"),
Pair("Summer", "summer"),
Pair("Spring", "spring"),
Pair("Winter", "winter"),
Pair("Unknown", "unknown"),
)
val YEAR = arrayOf(
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015", "2015"),
Pair("2014", "2014"),
Pair("2013", "2013"),
Pair("2012", "2012"),
Pair("2011", "2011"),
Pair("2010", "2010"),
Pair("2009", "2009"),
Pair("2008", "2008"),
Pair("2007", "2007"),
Pair("2006", "2006"),
Pair("2005", "2005"),
Pair("2004", "2004"),
Pair("2003", "2003"),
Pair("2000s", "2000s"),
Pair("1990s", "1990s"),
Pair("1980s", "1980s"),
Pair("1970s", "1970s"),
Pair("1960s", "1960s"),
Pair("1950s", "1950s"),
Pair("1940s", "1940s"),
Pair("1930s", "1930s"),
Pair("1920s", "1920s"),
Pair("1910s", "1910s"),
)
val TYPE = arrayOf(
Pair("Movie", "movie"),
Pair("TV", "tv"),
Pair("OVA", "ova"),
Pair("ONA", "ona"),
Pair("Special", "special"),
Pair("Music", "music"),
)
val STATUS = arrayOf(
Pair("Not Yet Aired", "info"),
Pair("Releasing", "releasing"),
Pair("Completed", "completed"),
)
val LANGUAGE = arrayOf(
Pair("Sub and Dub", "subdub"),
Pair("Sub", "sub"),
Pair("Dub", "dub"),
)
val RATING = arrayOf(
Pair("G - All Ages", "g"),
Pair("PG - Children", "pg"),
Pair("PG 13 - Teens 13 and Older", "pg_13"),
Pair("R - 17+, Violence & Profanity", "r"),
Pair("R+ - Profanity & Mild Nudity", "r+"),
Pair("Rx - Hentai", "rx"),
)
}
}

View file

@ -1,117 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.nineanime
import android.util.Base64
import java.net.URLDecoder
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class AniwaveUtils {
fun vrfEncrypt(input: String): String {
var vrf = input
ORDER.sortedBy {
it.first
}.forEach { item ->
when (item.second) {
"exchange" -> vrf = exchange(vrf, item.third)
"rc4" -> vrf = rc4Encrypt(item.third.get(0), vrf)
"reverse" -> vrf = vrf.reversed()
"base64" -> vrf = Base64.encode(vrf.toByteArray(), Base64.URL_SAFE or Base64.NO_WRAP).toString(Charsets.UTF_8)
else -> {}
}
}
return java.net.URLEncoder.encode(vrf, "utf-8")
}
fun vrfDecrypt(input: String): String {
var vrf = input
ORDER.sortedByDescending {
it.first
}.forEach { item ->
when (item.second) {
"exchange" -> vrf = exchange(vrf, item.third.reversed())
"rc4" -> vrf = rc4Decrypt(item.third.get(0), vrf)
"reverse" -> vrf = vrf.reversed()
"base64" -> vrf = Base64.decode(vrf, Base64.URL_SAFE).toString(Charsets.UTF_8)
else -> {}
}
}
return URLDecoder.decode(vrf, "utf-8")
}
private fun rc4Encrypt(key: String, input: String): String {
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
var output = cipher.doFinal(input.toByteArray())
output = Base64.encode(output, Base64.URL_SAFE or Base64.NO_WRAP)
return output.toString(Charsets.UTF_8)
}
private fun rc4Decrypt(key: String, input: String): String {
var vrf = input.toByteArray()
vrf = Base64.decode(vrf, Base64.URL_SAFE)
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
vrf = cipher.doFinal(vrf)
return vrf.toString(Charsets.UTF_8)
}
private fun exchange(input: String, keys: List<String>): String {
val key1 = keys.get(0)
val key2 = keys.get(1)
return input.map { i ->
val index = key1.indexOf(i)
if (index != -1) {
key2[index]
} else {
i
}
}.joinToString("")
}
private fun rot13(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val byte = vrf[i]
if (byte in 'A'.code..'Z'.code) {
vrf[i] = ((byte - 'A'.code + 13) % 26 + 'A'.code).toByte()
} else if (byte in 'a'.code..'z'.code) {
vrf[i] = ((byte - 'a'.code + 13) % 26 + 'a'.code).toByte()
}
}
return vrf
}
private fun vrfShift(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val shift = arrayOf(-2, -4, -5, 6, 2, -3, 3, 6)[i % 8]
vrf[i] = vrf[i].plus(shift).toByte()
}
return vrf
}
companion object {
private val EXCHANGE_KEY_1 = listOf("AP6GeR8H0lwUz1", "UAz8Gwl10P6ReH")
private val KEY_1 = "ItFKjuWokn4ZpB"
private val KEY_2 = "fOyt97QWFB3"
private val EXCHANGE_KEY_2 = listOf("1majSlPQd2M5", "da1l2jSmP5QM")
private val EXCHANGE_KEY_3 = listOf("CPYvHj09Au3", "0jHA9CPYu3v")
private val KEY_3 = "736y1uTJpBLUX"
private val ORDER = listOf(
Triple(1, "exchange", EXCHANGE_KEY_1),
Triple(2, "rc4", listOf(KEY_1)),
Triple(3, "rc4", listOf(KEY_2)),
Triple(4, "exchange", EXCHANGE_KEY_2),
Triple(5, "exchange", EXCHANGE_KEY_3),
Triple(5, "reverse", emptyList()),
Triple(6, "rc4", listOf(KEY_3)),
Triple(7, "base64", emptyList()),
)
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Oppai Stream'
extClass = '.OppaiStream'
extVersionCode = 4
extVersionCode = 6
isNsfw = true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View file

@ -14,23 +14,21 @@ 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.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
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.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
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 uy.kohesive.injekt.injectLazy
import java.net.URLEncoder.encode
class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
class OppaiStream : AnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Oppai Stream"
@ -46,30 +44,18 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
private val searchAnimeSelector = "div.episode-shown > div > a"
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/$SEARCH_PATH?order=views&page=$page&limit=$SEARCH_LIMIT")
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
override fun popularAnimeSelector() = searchAnimeSelector()
override fun popularAnimeFromElement(element: Element) = searchAnimeFromElement(element)
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$SEARCH_PATH?order=uploaded&page=$page&limit=$SEARCH_LIMIT")
override fun latestUpdatesParse(response: Response) = searchAnimeParse(response)
override fun latestUpdatesSelector() = searchAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = searchAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = null
// =============================== Search ===============================
override fun getFilterList() = FILTERS
@ -106,33 +92,23 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
return GET(url, headers)
}
override fun searchAnimeSelector() = "div.episode-shown > div > a"
override fun searchAnimeNextPageSelector() = null
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(searchAnimeSelector())
val elements = document.select(searchAnimeSelector)
val anime = elements.map(::searchAnimeFromElement).distinctBy { it.title }
val hasNextPage = elements.size >= SEARCH_LIMIT
return AnimesPage(anime, hasNextPage)
}
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
private fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
thumbnail_url = element.selectFirst("img.cover-img-in")?.attr("abs:src")
title = element.selectFirst(".title-ep")!!.text().replace(TITLE_CLEANUP_REGEX, "")
setUrlWithoutDomain(
element.attr("href").replace(Regex("(?<=\\?e=)(.*?)(?=&f=)")) {
java.net.URLEncoder.encode(it.groupValues[1], "UTF-8")
},
)
setUrlWithoutDomain(element.attr("exur").ifEmpty { element.attr("href") }.fixLink())
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val document = response.asJsoup()
// Fetch from from Anilist when "Anilist Cover" is selected in settings
val name = document.selectFirst("div.episode-info > h1")!!.text().substringBefore(" Ep ")
title = name
@ -159,35 +135,11 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
return buildList {
doc.select(episodeListSelector())
.map(::episodeFromElement)
.let(::addAll)
add(
SEpisode.create().apply {
setUrlWithoutDomain(
doc.location().replace(Regex("(?<=\\?e=)(.*?)(?=&f=)")) {
java.net.URLEncoder.encode(it.groupValues[1], "UTF-8")
},
)
val num = doc.selectFirst("div.episode-info > h1")!!.text().substringAfter(" Ep ")
name = "Episode $num"
episode_number = num.toFloatOrNull() ?: 1F
scanlator = doc.selectFirst("div.episode-info a.red")?.text()
},
)
}.sortedByDescending { it.episode_number }
return doc.select("div.more-same-eps .in-main-gr > a").map(::episodeFromElement).reversed()
}
override fun episodeListSelector() = "div.more-same-eps > div > div > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(
element.attr("href").replace(Regex("(?<=\\?e=)(.*?)(?=&f=)")) {
java.net.URLEncoder.encode(it.groupValues[1], "UTF-8")
},
)
private fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("exur").ifEmpty { element.attr("href") }.fixLink())
val num = element.selectFirst("font.ep")?.text() ?: "1"
name = "Episode $num"
episode_number = num.toFloatOrNull() ?: 1F
@ -198,7 +150,7 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val script = doc.selectFirst("script:containsData(var availableres)")!!.data()
val subtitles = doc.select("track[kind=captions]").map {
val subtitles = doc.select("track[kind=captions], track[kind=subtitles]").map {
Track(it.attr("src"), it.attr("label"))
}
@ -214,10 +166,6 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
}
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
@ -226,10 +174,6 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
).reversed()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
@ -322,6 +266,9 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
return Pair(coverURL, studiosList)
}
private fun String.fixLink(): String =
this.replace(Regex("(?<=\\?e=)(.*?)(?=&f=)")) { encode(it.groupValues[1], "UTF-8") }
companion object {
private const val SEARCH_PATH = "actions/search.php"
private const val SEARCH_LIMIT = 36
@ -329,7 +276,7 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_ENTRIES = arrayOf("2160p", "1080p", "720p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
@ -343,7 +290,7 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
private const val PREF_COVER_QUALITY_KEY = "preferred_cover_quality"
private const val PREF_COVER_QUALITY_TITLE = "Preferred Anilist cover quality - Beta"
private const val PREF_COVER_QUALITY_DEFAULT = "large"
private const val PREF_COVER_QUALITY_DEFAULT = "extraLarge"
private val PREF_COVER_QUALITY_ENTRIES = arrayOf("Extra Large", "Large")
private val PREF_COVER_QUALITY_VALUES = arrayOf("extraLarge", "large")
}

View file

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

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

@ -5,7 +5,6 @@ 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
@ -84,38 +83,12 @@ class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val yearFilter = filters.find { it is YearFilter } as YearFilter
val stateFilter = filters.find { it is StateFilter } as StateFilter
val typeFilter = filters.find { it is TypeFilter } as TypeFilter
val orderByFilter = filters.find { it is OrderByFilter } as OrderByFilter
val genreFilter = (filters.find { it is TagFilter } as TagFilter).state.filter { it.state }
var filterUrl = "$baseUrl/animes?"
if (query.isNotBlank()) {
filterUrl += "&q=$query"
} // search by name
if (genreFilter.isNotEmpty()) {
genreFilter.forEach {
filterUrl += "&genero[]=${it.name}"
}
} // search by genre
if (yearFilter.state.isNotBlank()) {
filterUrl += "&year[]=${yearFilter.state}"
} // search by year
if (stateFilter.state != 0) {
filterUrl += "&estado[]=${stateFilter.toUriPart()}"
} // search by state
if (typeFilter.state != 0) {
filterUrl += "&type[]=${typeFilter.toUriPart()}"
} // search by type
filterUrl += "&order=${orderByFilter.toUriPart()}"
filterUrl += "&page=$page" // add page
val params = AnimeFenixFilters.getSearchParameters(filters)
return when {
genreFilter.isEmpty() || yearFilter.state.isNotBlank() ||
stateFilter.state != 0 || typeFilter.state != 0 || query.isNotBlank() -> GET(filterUrl, headers)
else -> GET("$baseUrl/animes?order=likes&page=$page ")
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")
}
}
@ -273,110 +246,7 @@ class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
}
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
TagFilter("Generos", checkboxesFrom(genreList)),
StateFilter(),
TypeFilter(),
OrderByFilter(),
YearFilter(),
)
private val genreList = arrayOf(
Pair("Acción", "acción"),
Pair("Aventura", "aventura"),
Pair("Angeles", "angeles"),
Pair("Artes Marciales", "artes-marciales"),
Pair("Ciencia Ficcion", "ciencia-ficcion"),
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", "fantasía"),
Pair("Gore", "gore"),
Pair("Harem", "harem"),
Pair("Historico", "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", "música"),
Pair("Ninjas", "ninjas"),
Pair("Parodias", "parodias"),
Pair("Policia", "policia"),
Pair("Psicológico", "psicológico"),
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("Shonen", "shonen"),
Pair("Slice of life", "slice-of-life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Space", "space"),
Pair("Spokon", "spokon"),
Pair("SteamPunk", "steampunk"),
Pair("SuperPoder", "superpoder"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
private fun checkboxesFrom(tagArray: Array<Pair<String, String>>): List<TagCheckBox> = tagArray.map { TagCheckBox(it.second) }
class TagCheckBox(tag: String) : AnimeFilter.CheckBox(tag, false)
class TagFilter(name: String, checkBoxes: List<TagCheckBox>) : AnimeFilter.Group<TagCheckBox>(name, checkBoxes)
private class YearFilter : AnimeFilter.Text("Año")
private class StateFilter : UriPartFilter(
"Estado",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("Emision", "1"),
Pair("Finalizado", "2"),
Pair("Proximamente", "3"),
Pair("En Cuarentena", "4"),
),
)
private class TypeFilter : UriPartFilter(
"Tipo",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("TV", "tv"),
Pair("Pelicula", "movie"),
Pair("Especial", "special"),
Pair("OVA", "ova"),
),
)
private class OrderByFilter : UriPartFilter(
"Ordenar Por",
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"),
),
)
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
}
override fun getFilterList(): AnimeFilterList = AnimeFenixFilters.FILTER_LIST
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {

View file

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

View file

@ -5,7 +5,6 @@ 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.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
@ -132,116 +131,16 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
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 stateFilter = filterList.find { it is StateFilter } as StateFilter
val typeFilter = filterList.find { it is TypeFilter } as TypeFilter
val orderByFilter = filterList.find { it is OrderByFilter } as OrderByFilter
var uri = "$baseUrl/browse?"
uri += if (query.isNotBlank()) "&q=$query" else ""
uri += if (genreFilter.state != 0) "&genre[]=${genreFilter.toUriPart()}" else ""
uri += if (stateFilter.state != 0) "&status[]=${stateFilter.toUriPart()}" else ""
uri += if (typeFilter.state != 0) "&type[]=${typeFilter.toUriPart()}" else ""
uri += "&order=${orderByFilter.toUriPart()}"
uri += "&page=$page"
val params = AnimeFlvFilters.getSearchParameters(filters)
return when {
query.isNotBlank() || genreFilter.state != 0 || stateFilter.state != 0 || orderByFilter.state != 0 || typeFilter.state != 0 -> GET(uri)
else -> GET("$baseUrl/browse?page=$page&order=rating")
query.isNotBlank() -> GET("$baseUrl/browse?q=$query&page=$page")
params.filter.isNotBlank() -> GET("$baseUrl/browse${params.getQuery()}&page=$page")
else -> popularAnimeRequest(page)
}
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
StateFilter(),
TypeFilter(),
OrderByFilter(),
)
private class GenreFilter : UriPartFilter(
"Géneros",
arrayOf(
Pair("<Selecionar>", "all"),
Pair("Todo", "all"),
Pair("Acción", "accion"),
Pair("Artes Marciales", "artes_marciales"),
Pair("Aventuras", "aventura"),
Pair("Carreras", "carreras"),
Pair("Ciencia Ficción", "ciencia_ficcion"),
Pair("Comedia", "comedia"),
Pair("Demencia", "demencia"),
Pair("Demonios", "demonios"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Espacial", "espacial"),
Pair("Fantasía", "fantasia"),
Pair("Harem", "harem"),
Pair("Historico", "historico"),
Pair("Infantil", "infantil"),
Pair("Josei", "josei"),
Pair("Juegos", "juegos"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "musica"),
Pair("Parodia", "parodia"),
Pair("Policía", "policia"),
Pair("Psicológico", "psicologico"),
Pair("Recuentos de la vida", "recuentos_de_la_vida"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoderes", "superpoderes"),
Pair("Suspenso", "suspenso"),
Pair("Terror", "terror"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
),
)
private class StateFilter : UriPartFilter(
"Estado",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("En emisión", "1"),
Pair("Finalizado", "2"),
Pair("Próximamente", "3"),
),
)
private class TypeFilter : UriPartFilter(
"Tipo",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("TV", "tv"),
Pair("Película", "movie"),
Pair("Especial", "special"),
Pair("OVA", "ova"),
),
)
private class OrderByFilter : UriPartFilter(
"Ordenar Por",
arrayOf(
Pair("Por defecto", "default"),
Pair("Recientemente Actualizados", "updated"),
Pair("Recientemente Agregados", "added"),
Pair("Nombre A-Z", "title"),
Pair("Calificación", "rating"),
),
)
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
}
override fun getFilterList(): AnimeFilterList = AnimeFlvFilters.FILTER_LIST
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)

View file

@ -0,0 +1,148 @@
package eu.kanade.tachiyomi.animeextension.es.animeflv
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import java.util.Calendar
object AnimeFlvFilters {
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, "status") +
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("TV", "tv"),
Pair("Película", "movie"),
Pair("Especial", "special"),
Pair("OVA", "ova"),
)
val STATE = arrayOf(
Pair("En emisión", "1"),
Pair("Finalizado", "2"),
Pair("Próximamente", "3"),
)
val SORT = arrayOf(
Pair("Por Defecto", "default"),
Pair("Recientemente Actualizados", "updated"),
Pair("Recientemente Agregados", "added"),
Pair("Nombre A-Z", "title"),
Pair("Calificación", "rating"),
)
val GENRES = arrayOf(
Pair("Acción", "accion"),
Pair("Artes Marciales", "artes-marciales"),
Pair("Aventuras", "aventura"),
Pair("Carreras", "carreras"),
Pair("Ciencia Ficción", "ciencia-ficcion"),
Pair("Comedia", "comedia"),
Pair("Demencia", "demencia"),
Pair("Demonios", "demonios"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Espacial", "espacial"),
Pair("Fantasía", "fantasia"),
Pair("Harem", "harem"),
Pair("Historico", "historico"),
Pair("Infantil", "infantil"),
Pair("Josei", "josei"),
Pair("Juegos", "juegos"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "musica"),
Pair("Parodia", "parodia"),
Pair("Policía", "policia"),
Pair("Psicológico", "psicologico"),
Pair("Recuentos de la vida", "recuentos-de-la-vida"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoderes", "superpoderes"),
Pair("Suspenso", "suspenso"),
Pair("Terror", "terror"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
}
}

View file

@ -1,27 +1,15 @@
ext {
extName = 'AsiaLiveAction'
extClass = '.AsiaLiveAction'
extVersionCode = 31
extVersionCode = 32
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:vudeo-extractor'))
implementation(project(':lib:uqload-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamlare-extractor'))
implementation(project(':lib:yourupload-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:mixdrop-extractor'))
implementation(project(':lib:burstcloud-extractor'))
implementation(project(':lib:fastream-extractor'))
implementation(project(':lib:upstream-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:vk-extractor'))
implementation(project(':lib:vidguard-extractor'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -4,7 +4,6 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.asialiveaction.extractors.VidGuardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -12,31 +11,20 @@ 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.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.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
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.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
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 uy.kohesive.injekt.injectLazy
import java.util.Calendar
class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -47,9 +35,7 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val lang = "es"
override val supportsLatest = true
private val json: Json by injectLazy()
override val supportsLatest = false
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
@ -61,13 +47,16 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "FileLions"
private const val PREF_SERVER_DEFAULT = "Vk"
private val SERVER_LIST = arrayOf(
"YourUpload", "Voe", "Mp4Upload", "Doodstream",
"Upload", "BurstCloud", "Upstream", "StreamTape",
"Fastream", "Filemoon", "StreamWish", "VidGuard",
"Amazon", "AmazonES", "Fireload", "FileLions",
"vk.com",
"Filemoon",
"StreamWish",
"VidGuard",
"Amazon",
"AmazonES",
"FileLions",
"Vk",
"Okru",
)
}
@ -86,24 +75,23 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeNextPageSelector(): String = "div.TpRwCont main div a.next.page-numbers"
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = document.selectFirst("header div.Image figure img")!!.attr("src").trim().replace("//", "https://")
anime.title = document.selectFirst("header div.asia-post-header h1.Title")!!.text()
anime.description = document.selectFirst("header div.asia-post-main div.Description p:nth-child(2), header div.asia-post-main div.Description p")!!.text().removeSurrounding("\"")
anime.genre = document.select("div.asia-post-main p.Info span.tags a").joinToString { it.text() }
val year = document.select("header div.asia-post-main p.Info span.Date a").text().toInt()
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
anime.status = when {
year < currentYear -> SAnime.COMPLETED
year == currentYear -> SAnime.ONGOING
else -> SAnime.UNKNOWN
return SAnime.create().apply {
thumbnail_url = document.selectFirst("header div.Image figure img")?.attr("abs:src")?.getHdImg()
title = document.selectFirst("header div.asia-post-header h1.Title")!!.text()
description = document.selectFirst("header div.asia-post-main div.Description p:nth-child(2), header div.asia-post-main div.Description p")!!.text().removeSurrounding("\"")
genre = document.select("div.asia-post-main p.Info span.tags a").joinToString { it.text() }
artist = document.selectFirst("#elenco a span")?.text()
val year = document.select("header div.asia-post-main p.Info span.Date a").text().toInt()
status = when {
year < currentYear -> SAnime.COMPLETED
year == currentYear -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
return anime
}
override fun episodeListParse(response: Response): List<SEpisode> {
return super.episodeListParse(response).reversed()
}
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeListSelector() = "#ep-list div.TPTblCn span a, #ep-list div.TPTblCn .accordion"
@ -131,9 +119,7 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
private fun getNumberFromEpsString(epsStr: String): String = epsStr.filter { it.isDigit() }
private fun fetchUrls(text: String?): List<String> {
if (text.isNullOrEmpty()) return listOf()
@ -143,27 +129,30 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
document.select("script:containsData(var videos)").forEach { script ->
fetchUrls(script.data()).map { url ->
try {
serverVideoResolver(url).also(videoList::addAll)
} catch (_: Exception) {}
}
}
return videoList
return document.select("script:containsData(var videos)")
.flatMap { fetchUrls(it.data()) }
.parallelCatchingFlatMapBlocking { serverVideoResolver(it) }
}
/*--------------------------------Video extractors------------------------------------*/
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
private val vkExtractor by lazy { VkExtractor(client, headers) }
private val okruExtractor by lazy { OkruExtractor(client) }
private fun serverVideoResolver(url: String): List<Video> {
val videoList = mutableListOf<Video>()
val embedUrl = url.lowercase()
try {
if (embedUrl.contains("voe")) {
VoeExtractor(client).videosFromUrl(url).also(videoList::addAll)
}
if ((embedUrl.contains("amazon") || embedUrl.contains("amz")) && !embedUrl.contains("disable")) {
return when {
arrayOf("vk").any(url) -> vkExtractor.videosFromUrl(url)
arrayOf("ok.ru", "okru").any(url) -> okruExtractor.videosFromUrl(url)
arrayOf("wishembed", "streamwish", "strwish", "wish").any(url) -> streamWishExtractor.videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
arrayOf("filemoon", "moonplayer").any(url) -> filemoonExtractor.videosFromUrl(url, prefix = "Filemoon:")
arrayOf("vembed", "guard", "listeamed", "bembed", "vgfplay").any(url) -> vidGuardExtractor.videosFromUrl(url)
arrayOf("filelions", "lion", "fviplions").any(url) -> streamWishExtractor.videosFromUrl(url, videoNameGen = { "FileLions:$it" })
!url.contains("disable") && (arrayOf("amazon", "amz").any(url)) -> {
val body = client.newCall(GET(url)).execute().asJsoup()
if (body.select("script:containsData(var shareId)").toString().isNotBlank()) {
return if (body.select("script:containsData(var shareId)").toString().isNotBlank()) {
val shareId = body.selectFirst("script:containsData(var shareId)")!!.data()
.substringAfter("shareId = \"").substringBefore("\"")
val amazonApiJson = client.newCall(GET("https://www.amazon.com/drive/v1/shares/$shareId?resourceVersion=V2&ContentType=JSON&asset=ALL"))
@ -173,70 +162,13 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
client.newCall(GET("https://www.amazon.com/drive/v1/nodes/$epId/children?resourceVersion=V2&ContentType=JSON&limit=200&sort=%5B%22kind+DESC%22%2C+%22modifiedDate+DESC%22%5D&asset=ALL&tempLink=true&shareId=$shareId"))
.execute().asJsoup()
val videoUrl = amazonApi.toString().substringAfter("\"FOLDER\":").substringAfter("tempLink\":\"").substringBefore("\"")
videoList.add(Video(videoUrl, "Amazon", videoUrl))
listOf(Video(videoUrl, "Amazon", videoUrl))
} else {
emptyList()
}
}
if (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)
}
if (embedUrl.contains("uqload")) {
UqloadExtractor(client).videosFromUrl(url).also(videoList::addAll)
}
if (embedUrl.contains("mp4upload")) {
Mp4uploadExtractor(client).videosFromUrl(url, headers).let { videoList.addAll(it) }
}
if (embedUrl.contains("wishembed") ||
embedUrl.contains("streamwish") ||
embedUrl.contains("strwish") ||
embedUrl.contains("wish") ||
embedUrl.contains("sfastwish")
) {
val docHeaders = headers.newBuilder()
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" }).also(videoList::addAll)
}
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
val url2 = url.replace("https://doodstream.com/e/", "https://dood.to/e/")
DoodExtractor(client).videoFromUrl(url2, "DoodStream", false)?.let { videoList.add(it) }
}
if (embedUrl.contains("streamlare")) {
StreamlareExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
}
if (embedUrl.contains("yourupload") || embedUrl.contains("upload")) {
YourUploadExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
}
if (embedUrl.contains("burstcloud") || embedUrl.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
}
if (embedUrl.contains("fastream")) {
FastreamExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
}
if (embedUrl.contains("upstream")) {
UpstreamExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
}
if (embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape")) {
StreamTapeExtractor(client).videoFromUrl(url)?.let { videoList.add(it) }
}
if (embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("hide")) {
StreamHideVidExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
}
if (embedUrl.contains("filelions") || embedUrl.contains("lion") || embedUrl.contains("fviplions")) {
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "FileLions:$it" }).also(videoList::addAll)
}
if (embedUrl.contains("vembed") || embedUrl.contains("guard")) {
VidGuardExtractor(client).videosFromUrl(url).also(videoList::addAll)
}
if (embedUrl.contains("vk")) {
VkExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
}
} catch (_: Exception) { }
return videoList
else -> emptyList()
}
}
override fun videoListSelector() = throw UnsupportedOperationException()
@ -305,9 +237,7 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
fun toUriPart() = vals[state].second
}
override fun searchAnimeFromElement(element: Element): SAnime {
return popularAnimeFromElement(element)
}
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
@ -321,6 +251,17 @@ class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun latestUpdatesSelector() = popularAnimeSelector()
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
private fun String?.getHdImg(): String? {
if (this.isNullOrEmpty() || !this.contains("tmdb")) return this
val pattern = """(https:\/\/image\.tmdb\.org\/t\/p\/)([\w_]+)(\/[^\s]*)""".toRegex()
return pattern.replace(this) { matchResult ->
"${matchResult.groupValues[1]}w500${matchResult.groupValues[3]}"
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY

View file

@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.asialiveaction.extractors
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VidGuardExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch) {
var payload: String = ""
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
?.absUrl("src")
?: return emptyList()
val headers = Headers.headersOf("Referer", url)
val script = client.newCall(GET(scriptUrl, headers)).execute()
.body.string()
val sources = getSourcesFromScript(script, url)
.takeIf { it.isNotBlank() && it != "undefined" }
?: return emptyList()
return sources.substringAfter("stream:[").substringBefore("}]")
.split('{')
.drop(1)
.mapNotNull { line ->
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let(::fixUrl)
?: return@mapNotNull null
Video(videoUrl, "VidGuard:$resolution", videoUrl, headers)
}
}
private fun getSourcesFromScript(script: String, url: String): String {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsinterface = JsObject(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
cacheMode = WebSettings.LOAD_NO_CACHE
}
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
view?.evaluateJavascript(script) {}
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
}
}
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
}
latch.await(5, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsinterface.payload
}
private fun fixUrl(url: String): String {
val httpUrl = url.toHttpUrl()
val originalSign = httpUrl.queryParameter("sig")!!
val newSign = originalSign.chunked(2).joinToString("") {
Char(it.toInt(16) xor 2).toString()
}
.let { String(Base64.decode(it, Base64.DEFAULT)) }
.substring(5)
.chunked(2)
.reversed()
.joinToString("")
.substring(5)
return httpUrl.newBuilder()
.removeAllQueryParameters("sig")
.addQueryParameter("sig", newSign)
.build()
.toString()
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Cine24h'
extClass = '.Cine24h'
extVersionCode = 1
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"
@ -11,4 +11,5 @@ dependencies {
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:vidguard-extractor'))
}

View file

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
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.vidguardextractor.VidGuardExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
@ -153,6 +154,7 @@ open class Cine24h : ConfigurableAnimeSource, AnimeHttpSource() {
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
private fun serverVideoResolver(url: String): List<Video> {
val embedUrl = url.lowercase()
@ -161,9 +163,10 @@ open class Cine24h : ConfigurableAnimeSource, AnimeHttpSource() {
val link = if (url.contains("emb.html")) "https://fastream.to/embed-${url.split("/").last()}.html" else url
FastreamExtractor(client, headers).videosFromUrl(link)
}
embedUrl.contains("filemoon") || embedUrl.contains("moonplayer") -> filemoonExtractor.videosFromUrl(url, prefix = "Filemoon:")
embedUrl.contains("voe") -> voeExtractor.videosFromUrl(url)
embedUrl.contains("dood") -> doodExtractor.videosFromUrl(url)
arrayOf("filemoon", "moonplayer").any(url) -> filemoonExtractor.videosFromUrl(url, prefix = "Filemoon:")
arrayOf("voe").any(url) -> voeExtractor.videosFromUrl(url)
arrayOf("doodstream", "dood.", "ds2play", "doods.").any(url) -> doodExtractor.videosFromUrl(url)
arrayOf("vembed", "guard", "listeamed", "bembed", "vgfplay").any(url) -> vidGuardExtractor.videosFromUrl(url)
else -> emptyList()
}
}
@ -236,6 +239,8 @@ open class Cine24h : ConfigurableAnimeSource, AnimeHttpSource() {
return !attr(attrName).contains("data:image/")
}
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY

View file

@ -1,7 +1,7 @@
ext {
extName = 'Doramasyt'
extClass = '.Doramasyt'
extVersionCode = 13
extVersionCode = 14
}
apply from: "$rootDir/common.gradle"
@ -10,4 +10,9 @@ dependencies {
implementation(project(':lib:uqload-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:mixdrop-extractor'))
}

View file

@ -2,34 +2,41 @@ package eu.kanade.tachiyomi.animeextension.es.doramasyt
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.doramasyt.extractors.SolidFilesExtractor
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.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
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.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
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 okhttp3.FormBody
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.math.ceil
class Doramasyt : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
class Doramasyt : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Doramasyt"
override val baseUrl = "https://doramasyt.com"
override val baseUrl = "https://www.doramasyt.com"
override val lang = "es"
@ -39,241 +46,203 @@ class Doramasyt : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector(): String = "div.col-lg-2.col-md-4.col-6 div.animes"
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")
override fun popularAnimeRequest(page: Int): Request = GET("https://doramasyt.com/doramas/?p=$page")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(
element.select("div.anithumb a").attr("href"),
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Filemoon"
private val SERVER_LIST = arrayOf(
"Voe",
"StreamWish",
"Okru",
"Upload",
"FileLions",
"Filemoon",
"DoodStream",
"MixDrop",
"Streamtape",
)
anime.title = element.select("div.animedtls p").text()
anime.thumbnail_url = element.select(" div.anithumb a img").attr("src")
anime.description = element.select("div.animedtls p").text()
return anime
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination li:last-child a"
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val animeDetails = SAnime.create().apply {
title = document.selectFirst(".flex-column h1.text-capitalize")?.text() ?: ""
description = document.selectFirst(".h-100 .mb-3 p")?.text()
genre = document.select(".lh-lg span").joinToString { it.text() }
thumbnail_url = document.selectFirst(".gap-3 img")?.getImageUrl()
status = document.select(".lh-sm .ms-2").eachText().let { items ->
when {
items.any { it.contains("Finalizado") } -> SAnime.COMPLETED
items.any { it.contains("Estreno") } -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
}
return animeDetails
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/doramas?p=$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(".ficha_efecto a")
val nextPage = document.select(".pagination [rel=\"next\"]").any()
val animeList = elements.map { element ->
SAnime.create().apply {
title = element.selectFirst(".title_cap")!!.text()
thumbnail_url = element.selectFirst("img")?.getImageUrl()
setUrlWithoutDomain(element.attr("abs:href"))
}
}
return AnimesPage(animeList, nextPage)
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/emision?p=$page", headers)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = DoramasytFilters.getSearchParameters(filters)
return when {
query.isNotBlank() -> GET("$baseUrl/buscar?q=$query", headers)
params.filter.isNotBlank() -> GET("$baseUrl/doramas${params.getQuery()}&p=$page", headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun episodeListParse(response: Response): List<SEpisode> {
return super.episodeListParse(response).reversed()
val document = response.asJsoup()
val token = document.select("meta[name='csrf-token']").attr("content")
val capListLink = document.select(".caplist").attr("data-ajax")
val referer = document.location()
val detail = getEpisodeDetails(capListLink, token, referer)
val total = detail.eps.size
val perPage = detail.perpage ?: return emptyList()
val pages = (total / perPage).ceilPage()
return (1..pages).parallelCatchingFlatMapBlocking {
getEpisodePage(detail.paginateUrl ?: "", it, token, referer).caps.mapIndexed { idx, ep ->
val episodeNumber = (ep.episodio ?: (idx + 1))
SEpisode.create().apply {
name = "Capítulo $episodeNumber"
episode_number = episodeNumber.toFloat()
setUrlWithoutDomain(ep.url ?: "")
}
}
}.reversed()
}
override fun episodeListSelector(): String = "div.mainrowdiv.pagesdiv div.jpage div.col-item"
private fun getEpisodeDetails(capListLink: String, token: String, referer: String): EpisodesDto {
val formBody = FormBody.Builder().add("_token", token).build()
val request = Request.Builder()
.url(capListLink)
.post(formBody)
.header("accept", "application/json, text/javascript, */*; q=0.01")
.header("accept-language", "es-419,es;q=0.8")
.header("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
.header("origin", baseUrl)
.header("referer", referer)
.header("x-requested-with", "XMLHttpRequest")
.build()
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val epNum = getNumberFromEpsString(element.select("a div.flimss div.dtlsflim p").text())
Log.i("bruh ep", element.select("a").attr("href"))
val formatedEp = when {
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
else -> 1F
}
episode.setUrlWithoutDomain(element.select("a").attr("href"))
episode.episode_number = formatedEp
episode.name = "Episodio $formatedEp"
return episode
return client.newCall(request).execute().parseAs<EpisodesDto>()
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
private fun getEpisodePage(paginateUrl: String, page: Int, token: String, referer: String): EpisodeInfoDto {
val formBodyEp = FormBody.Builder()
.add("_token", token)
.add("p", "$page")
.build()
val requestEp = Request.Builder()
.url(paginateUrl)
.post(formBodyEp)
.header("accept", "application/json, text/javascript, */*; q=0.01")
.header("accept-language", "es-419,es;q=0.8")
.header("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
.header("origin", baseUrl)
.header("referer", referer)
.header("x-requested-with", "XMLHttpRequest")
.build()
return client.newCall(requestEp).execute().parseAs<EpisodeInfoDto>()
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
document.select("div.playermain ul.dropcaps li#play-video a.cap").forEach { players ->
val server = players.text()
val urlEncoded = players.attr("data-player")
val byte = android.util.Base64.decode(urlEncoded, android.util.Base64.DEFAULT)
val url = String(byte, charset("UTF-8")).substringAfter("?url=")
if (server.contains("streamtape")) {
val video = StreamTapeExtractor(client).videoFromUrl(url)
if (video != null) {
videoList.add(video)
}
}
if (server.contains("ok")) {
val videos = OkruExtractor(client).videosFromUrl(url)
videoList.addAll(videos)
}
if (server.contains("zeus")) {
val videos = SolidFilesExtractor(client).videosFromUrl(url)
videoList.addAll(videos)
}
if (server.contains("uqload") || server.contains("upload")) {
videoList.addAll(UqloadExtractor(client).videosFromUrl(url))
}
}
return videoList
return document.select("[data-player]")
.map { String(Base64.decode(it.attr("data-player"), Base64.DEFAULT)) }
.parallelCatchingFlatMapBlocking { serverVideoResolver(it) }
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun getFilterList(): AnimeFilterList = DoramasytFilters.FILTER_LIST
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
private val voeExtractor by lazy { VoeExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val mixdropExtractor by lazy { MixDropExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
private fun serverVideoResolver(url: String): List<Video> {
val embedUrl = url.lowercase()
return when {
embedUrl.contains("voe") -> voeExtractor.videosFromUrl(url)
embedUrl.contains("uqload") -> uqloadExtractor.videosFromUrl(url)
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> okruExtractor.videosFromUrl(url)
embedUrl.contains("filemoon") || embedUrl.contains("moonplayer") -> filemoonExtractor.videosFromUrl(url, prefix = "Filemoon:")
embedUrl.contains("wishembed") || embedUrl.contains("streamwish") || embedUrl.contains("strwish") || embedUrl.contains("wish") || embedUrl.contains("wishfast") -> streamwishExtractor.videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> streamTapeExtractor.videosFromUrl(url)
embedUrl.contains("doodstream") || embedUrl.contains("dood.") || embedUrl.contains("ds2play") || embedUrl.contains("doods.") -> doodExtractor.videosFromUrl(url, "DoodStream", false)
embedUrl.contains("filelions") || embedUrl.contains("lion") -> streamwishExtractor.videosFromUrl(url, videoNameGen = { "FileLions:$it" })
embedUrl.contains("mix") -> mixdropExtractor.videosFromUrl(url)
else -> emptyList()
}
}
override fun List<Video>.sort(): List<Video> {
return try {
val videoSorted = this.sortedWith(
compareBy<Video> { it.quality.replace("[0-9]".toRegex(), "") }.thenByDescending { getNumberFromString(it.quality) },
).toTypedArray()
val userPreferredQuality = preferences.getString("preferred_quality", "Okru:720p")
val preferredIdx = videoSorted.indexOfFirst { x -> x.quality == userPreferredQuality }
if (preferredIdx != -1) {
videoSorted.drop(preferredIdx + 1)
videoSorted[0] = videoSorted[preferredIdx]
}
videoSorted.toList()
} catch (e: Exception) {
this
}
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()
}
private fun getNumberFromString(epsStr: String): String {
return epsStr.filter { it.isDigit() }.ifEmpty { "0" }
}
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
private fun Element.getImageUrl(): String? {
return when {
query.isNotBlank() -> GET("$baseUrl/buscar?q=$query&p=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/doramas?categoria=false&genero=${genreFilter.toUriPart()}&fecha=false&letra=false&p=$page")
else -> GET("$baseUrl/doramas/?p=$page")
isValidUrl("data-src") -> attr("abs:data-src")
isValidUrl("data-lazy-src") -> attr("abs:data-lazy-src")
isValidUrl("srcset") -> attr("abs:srcset").substringBefore(" ")
isValidUrl("src") -> attr("abs:src")
else -> ""
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(
element.selectFirst("a")!!.attr("href"),
)
anime.title = element.select("div.animedtls p").text()
anime.thumbnail_url = element.select("a img").attr("src")
anime.description = element.select("div.animedtls p").text()
return anime
private fun Element.isValidUrl(attrName: String): Boolean {
if (!hasAttr(attrName)) return false
return !attr(attrName).contains("anime.png")
}
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.herohead div.heroheadmain")!!.attr("style").substringAfter(",url(").substringBefore(") no-repeat;")
val sub = document.selectFirst("div.herohead div.heroheadmain strong")!!.text()
val title = document.selectFirst("div.herohead div.heroheadmain h1")!!.text().trim()
anime.title = title + if (sub.isNotEmpty()) " ($sub)" else ""
anime.description = document.selectFirst("div.herohead div.heroheadmain div.flimdtls p.textComplete")!!
.text().removeSurrounding("\"").replace("Ver menos", "")
anime.genre = document.select("div.herohead div.heroheadmain div.writersdiv div.nobel h6 a").joinToString { it.text() }
anime.status = parseStatus(document.select("div.herohead div.heroheadmain div.writersdiv div.state h6").text())
return anime
}
private fun parseStatus(statusString: String): Int {
return when {
statusString.contains("Estreno") -> SAnime.ONGOING
statusString.contains("Finalizado") -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun latestUpdatesNextPageSelector() = "" // popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(
element.selectFirst("a")!!.attr("href"),
)
anime.title = element.selectFirst("a div.chapter p")!!.text()
anime.thumbnail_url = element.select("a div.chapter img").attr("src")
anime.description = element.select("div.animedtls p").text()
return anime
}
override fun latestUpdatesRequest(page: Int) = GET(baseUrl)
override fun latestUpdatesSelector() = "div.heroarea div.heromain div.chapters div.row div.chaps" // popularAnimeSelector()
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Generos",
arrayOf(
Pair("<selecionar>", "false"),
Pair("Acción", "accion"),
Pair("Amistad", "amistad"),
Pair("Artes marciales", "artes-marciales"),
Pair("Aventuras", "aventuras"),
Pair("Bélico", "belico"),
Pair("C-Drama", "c-drama"),
Pair("Ciencia Ficción", "ciencia-ficcion"),
Pair("Comedia", "comedia"),
Pair("Comida", "comida"),
Pair("Crimen ", "crimen"),
Pair("Deporte", "deporte"),
Pair("Documental", "documental"),
Pair("Drama", "drama"),
Pair("Escolar", "escolar"),
Pair("Familiar", "familiar"),
Pair("Fantasia", "fantasia"),
Pair("Histórico", "historico"),
Pair("HK-Drama", "hk-drama"),
Pair("Horror", "horror"),
Pair("Idols", "idols"),
Pair("J-Drama", "j-drama"),
Pair("Juvenil", "juvenil"),
Pair("K-Drama", "k-drama"),
Pair("Legal", "legal"),
Pair("Médico", "medico"),
Pair("Melodrama", "melodrama"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Musical", "musical"),
Pair("Negocios", "negocios"),
Pair("Policial", "policial"),
Pair("Política", "politica"),
Pair("Psicológico", "psicologico"),
Pair("Reality Show", "reality-show"),
Pair("Recuentos de la vida", "recuentos-de-la-vida"),
Pair("Romance", "romance"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Supervivencia", "supervivencia"),
Pair("Suspenso", "suspenso"),
Pair("Thai-Drama", "thai-drama"),
Pair("Thriller", "thriller"),
Pair("Time Travel", "time-travel"),
Pair("TW-Drama", "tw-drama"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
),
)
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 fun Double.ceilPage(): Int = if (this % 1 == 0.0) this.toInt() else ceil(this).toInt()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val qualities = arrayOf(
"Okru:1080p", "Okru:720p", "Okru:480p", "Okru:360p", "Okru:240p", "Okru:144p", // Okru
"Uqload", "SolidFiles", "StreamTape", // video servers without resolution
)
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = qualities
entryValues = qualities
setDefaultValue("Okru:720p")
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 ->
@ -282,7 +251,22 @@ class Doramasyt : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}.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,29 @@
package eu.kanade.tachiyomi.animeextension.es.doramasyt
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class EpisodesDto(
@SerialName("paginate_url") var paginateUrl: String? = null,
@SerialName("perpage") var perpage: Double? = null,
@SerialName("eps") var eps: ArrayList<Eps> = arrayListOf(),
)
@Serializable
data class Eps(
@SerialName("num") var num: Int? = null,
)
@Serializable
data class EpisodeInfoDto(
@SerialName("default") var default: String? = null,
@SerialName("caps") var caps: ArrayList<Caps> = arrayListOf(),
)
@Serializable
data class Caps(
@SerialName("episodio") var episodio: Int? = null,
@SerialName("url") var url: String? = null,
@SerialName("thumb") var thumb: String? = null,
)

View file

@ -0,0 +1,117 @@
package eu.kanade.tachiyomi.animeextension.es.doramasyt
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object DoramasytFilters {
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 { "" }
}
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.asQueryPart<CategoriesFilter>("categoria") +
filters.asQueryPart<GenresFilter>("genero") +
filters.asQueryPart<YearsFilter>("fecha") +
filters.asQueryPart<LettersFilter>("letra"),
)
}
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
CategoriesFilter(),
GenresFilter(),
YearsFilter(),
LettersFilter(),
)
class CategoriesFilter : QueryPartFilter("Categoría", DoramasytFiltersData.CATEGORIES)
class GenresFilter : QueryPartFilter("Género", DoramasytFiltersData.GENRES)
class YearsFilter : QueryPartFilter("Año", DoramasytFiltersData.YEARS)
class LettersFilter : QueryPartFilter("Letra", DoramasytFiltersData.LETTER)
private object DoramasytFiltersData {
val CATEGORIES = arrayOf(
Pair("<Selecionar>", ""),
Pair("Dorama", "dorama"),
Pair("Live Action", "live-action"),
Pair("Pelicula", "pelicula"),
Pair("Series Turcas", "serie-turcas"),
)
val YEARS = arrayOf(Pair("<Seleccionar>", "")) + (1982..2024).map { Pair("$it", "$it") }.reversed().toTypedArray()
val LETTER = arrayOf(Pair("<Seleccionar>", "")) + ('A'..'Z').map { Pair("$it", "$it") }.toTypedArray()
val GENRES = arrayOf(
Pair("<Selecionar>", ""),
Pair("Policial", "policial"),
Pair("Romance", "romance"),
Pair("Comedia", "comedia"),
Pair("Escolar", "escolar"),
Pair("Acción", "accion"),
Pair("Thriller", "thriller"),
Pair("Drama", "drama"),
Pair("Misterio", "misterio"),
Pair("Fantasia", "fantasia"),
Pair("Histórico", "historico"),
Pair("Bélico", "belico"),
Pair("Militar", "militar"),
Pair("Médico", "medico"),
Pair("Ciencia Ficción", "ciencia-ficcion"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Horror", "horror"),
Pair("Política", "politica"),
Pair("Familiar", "familiar"),
Pair("Melodrama", "melodrama"),
Pair("Deporte", "deporte"),
Pair("Comida", "comida"),
Pair("Supervivencia", "supervivencia"),
Pair("Aventuras", "aventuras"),
Pair("Artes marciales", "artes-marciales"),
Pair("Recuentos de la vida", "recuentos-de-la-vida"),
Pair("Amistad", "amistad"),
Pair("Psicológico", "psicologico"),
Pair("Yuri", "yuri"),
Pair("K-Drama", "k-drama"),
Pair("J-Drama", "j-drama"),
Pair("C-Drama", "c-drama"),
Pair("HK-Drama", "hk-drama"),
Pair("TW-Drama", "tw-drama"),
Pair("Thai-Drama", "thai-drama"),
Pair("Idols", "idols"),
Pair("Suspenso", "suspenso"),
Pair("Negocios", "negocios"),
Pair("Time Travel", "time-travel"),
Pair("Crimen ", "crimen"),
Pair("Yaoi", "yaoi"),
Pair("Legal", "legal"),
Pair("Juvenil", "juvenil"),
Pair("Musical", "musical"),
Pair("Reality Show", "reality-show"),
Pair("Documental", "documental"),
Pair("Turcas", "turcas"),
)
}
}

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.doramasyt.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 = 'Hackstore'
extClass = '.Hackstore'
extVersionCode = 12
extVersionCode = 13
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

@ -83,6 +83,27 @@ class Hackstore : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Pair("Peliculas", "peliculas"),
Pair("Series", "series"),
Pair("Animes", "animes"),
Pair("Acción", "genero/accion"),
Pair("Action & Adventure", "genero/action-adventure"),
Pair("Animación", "genero/animacion"),
Pair("Aventura", "genero/aventura"),
Pair("Bélica", "genero/belica"),
Pair("Ciencia ficción", "genero/ciencia-ficcion"),
Pair("Comedia", "genero/comedia"),
Pair("Crimen", "genero/crimen"),
Pair("Documental", "genero/documental"),
Pair("Drama", "genero/drama"),
Pair("Familia", "genero/familia"),
Pair("Fantasía", "genero/fantasia"),
Pair("Historia", "genero/historia"),
Pair("Misterio", "genero/misterio"),
Pair("Música", "genero/musica"),
Pair("Occidental", "genero/occidental"),
Pair("Película de TV", "genero/pelicula-de-tv"),
Pair("Romance", "genero/romance"),
Pair("Suspense", "genero/suspense"),
Pair("Suspenso", "genero/suspenso"),
Pair("Terror", "genero/terror"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :

View file

@ -1,7 +1,7 @@
ext {
extName = 'MonosChinos'
extClass = '.MonosChinos'
extVersionCode = 27
extVersionCode = 28
}
apply from: "$rootDir/common.gradle"
@ -12,4 +12,8 @@ dependencies {
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:mixdrop-extractor'))
implementation(project(':lib:dood-extractor'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

View file

@ -5,275 +5,250 @@ import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.monoschinos.extractors.SolidFilesExtractor
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.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
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.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
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 okhttp3.FormBody
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.math.ceil
class MonosChinos : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
class MonosChinos : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "MonosChinos"
override val baseUrl = "https://monoschinos2.com"
override val id = 6957694006954649296
override val lang = "es"
override val supportsLatest = false
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector(): String = "div.heromain div.row div.col-md-4"
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")
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes?p=$page")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Filemoon"
private val SERVER_LIST = arrayOf(
"Voe",
"StreamWish",
"Okru",
"Upload",
"FileLions",
"Filemoon",
"DoodStream",
"MixDrop",
"Streamtape",
"Mp4Upload",
)
}
override fun popularAnimeFromElement(element: Element): SAnime {
val thumbDiv = element.select("a div.series div.seriesimg img")
return SAnime.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
title = element.select("a div.series div.seriesdetails h3").text()
thumbnail_url = if (thumbDiv.attr("src").contains("/public/img")) {
thumbDiv.attr("data-src")
} else {
thumbDiv.attr("src")
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val animeDetails = SAnime.create().apply {
title = document.selectFirst(".flex-column h1.text-capitalize")?.text() ?: ""
description = document.selectFirst(".h-100 .mb-3 p")?.text()
genre = document.select(".lh-lg span").joinToString { it.text() }
thumbnail_url = document.selectFirst(".gap-3 img")?.getImageUrl()
status = document.select(".lh-sm .ms-2").eachText().let { items ->
when {
items.any { it.contains("Finalizado") } -> SAnime.COMPLETED
items.any { it.contains("Estreno") } -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
}
return animeDetails
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes?p=$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(".ficha_efecto a")
val nextPage = document.select(".pagination [rel=\"next\"]").any()
val animeList = elements.map { element ->
SAnime.create().apply {
title = element.selectFirst(".title_cap")!!.text()
thumbnail_url = element.selectFirst("img")?.getImageUrl()
setUrlWithoutDomain(element.attr("abs:href"))
}
}
return AnimesPage(animeList, nextPage)
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/emision?p=$page", headers)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = MonosChinosFilters.getSearchParameters(filters)
return when {
query.isNotBlank() -> GET("$baseUrl/buscar?q=$query", headers)
params.filter.isNotBlank() -> GET("$baseUrl/animes${params.getQuery()}&p=$page", headers)
else -> popularAnimeRequest(page)
}
}
override fun popularAnimeNextPageSelector(): String = "li.page-item a.page-link"
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun episodeListParse(response: Response): List<SEpisode> {
val jsoup = response.asJsoup()
val animeId = response.request.url.pathSegments.last().replace("-sub-espanol", "").replace("-080p", "-1080p")
return jsoup.select("div.col-item").map { it ->
val epNum = it.attr("data-episode")
SEpisode.create().apply {
episode_number = epNum.toFloat()
name = "Episodio $epNum"
url = "/ver/$animeId-episodio-$epNum"
val document = response.asJsoup()
val token = document.select("meta[name='csrf-token']").attr("content")
val capListLink = document.select(".caplist").attr("data-ajax")
val referer = document.location()
val detail = getEpisodeDetails(capListLink, token, referer)
val total = detail.eps.size
val perPage = detail.perpage ?: return emptyList()
val pages = (total / perPage).ceilPage()
return (1..pages).parallelCatchingFlatMapBlocking {
getEpisodePage(detail.paginateUrl ?: "", it, token, referer).caps.mapIndexed { idx, ep ->
val episodeNumber = (ep.episodio ?: (idx + 1))
SEpisode.create().apply {
name = "Capítulo $episodeNumber"
episode_number = episodeNumber.toFloat()
setUrlWithoutDomain(ep.url ?: "")
}
}
}.reversed()
}
override fun episodeListSelector() = throw UnsupportedOperationException()
private fun getEpisodeDetails(capListLink: String, token: String, referer: String): EpisodesDto {
val formBody = FormBody.Builder().add("_token", token).build()
val request = Request.Builder()
.url(capListLink)
.post(formBody)
.header("accept", "application/json, text/javascript, */*; q=0.01")
.header("accept-language", "es-419,es;q=0.8")
.header("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
.header("origin", baseUrl)
.header("referer", referer)
.header("x-requested-with", "XMLHttpRequest")
.build()
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
return client.newCall(request).execute().parseAs<EpisodesDto>()
}
private fun getEpisodePage(paginateUrl: String, page: Int, token: String, referer: String): EpisodeInfoDto {
val formBodyEp = FormBody.Builder()
.add("_token", token)
.add("p", "$page")
.build()
val requestEp = Request.Builder()
.url(paginateUrl)
.post(formBodyEp)
.header("accept", "application/json, text/javascript, */*; q=0.01")
.header("accept-language", "es-419,es;q=0.8")
.header("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
.header("origin", baseUrl)
.header("referer", referer)
.header("x-requested-with", "XMLHttpRequest")
.build()
return client.newCall(requestEp).execute().parseAs<EpisodeInfoDto>()
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
document.select("div.heroarea div.row div.col-md-12 ul.dropcaps li").forEach { it ->
// val server = it.select("a").text()
val urlBase64 = it.select("a").attr("data-player")
val url = Base64.decode(urlBase64, Base64.DEFAULT).toString(Charsets.UTF_8).substringAfter("=")
when {
url.contains("ok") -> if (!url.contains("streamcherry")) videoList.addAll(OkruExtractor(client).videosFromUrl(url))
url.contains("solidfiles") -> videoList.addAll(SolidFilesExtractor(client).videosFromUrl(url))
url.contains("uqload") -> {
videoList.addAll(UqloadExtractor(client).videosFromUrl(url))
}
url.contains("mp4upload") -> {
val videos = Mp4uploadExtractor(client).videosFromUrl(url, headers)
videoList.addAll(videos)
}
url.contains("streamtape") -> {
val videos = StreamTapeExtractor(client).videosFromUrl(url)
videoList.addAll(videos)
}
url.contains("filemoon") -> {
val videos = FilemoonExtractor(client).videosFromUrl(url)
videoList.addAll(videos)
}
}
}
return videoList.filter { video -> video.url.contains("http") }
return document.select("[data-player]")
.map { String(Base64.decode(it.attr("data-player"), Base64.DEFAULT)) }
.parallelCatchingFlatMapBlocking { serverVideoResolver(it) }
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun getFilterList(): AnimeFilterList = MonosChinosFilters.FILTER_LIST
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
private val voeExtractor by lazy { VoeExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val mixdropExtractor by lazy { MixDropExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
private fun serverVideoResolver(url: String): List<Video> {
val embedUrl = url.lowercase()
return when {
embedUrl.contains("voe") -> voeExtractor.videosFromUrl(url)
embedUrl.contains("uqload") -> uqloadExtractor.videosFromUrl(url)
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> okruExtractor.videosFromUrl(url)
embedUrl.contains("filemoon") || embedUrl.contains("moonplayer") -> filemoonExtractor.videosFromUrl(url, prefix = "Filemoon:")
embedUrl.contains("wishembed") || embedUrl.contains("streamwish") || embedUrl.contains("strwish") || embedUrl.contains("wish") || embedUrl.contains("wishfast") -> streamwishExtractor.videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> streamTapeExtractor.videosFromUrl(url)
embedUrl.contains("doodstream") || embedUrl.contains("dood.") || embedUrl.contains("ds2play") || embedUrl.contains("doods.") -> doodExtractor.videosFromUrl(url, "DoodStream", false)
embedUrl.contains("filelions") || embedUrl.contains("lion") -> streamwishExtractor.videosFromUrl(url, videoNameGen = { "FileLions:$it" })
embedUrl.contains("mp4upload") || embedUrl.contains("mp4") -> mp4uploadExtractor.videosFromUrl(url, headers)
embedUrl.contains("mix") -> mixdropExtractor.videosFromUrl(url)
else -> emptyList()
}
}
override fun List<Video>.sort(): List<Video> {
return try {
val videoSorted = this.sortedWith(
compareBy<Video> { it.quality.replace("[0-9]".toRegex(), "") }.thenByDescending { getNumberFromString(it.quality) },
).toTypedArray()
val userPreferredQuality = preferences.getString("preferred_quality", "Okru:720p")
val preferredIdx = videoSorted.indexOfFirst { x -> x.quality == userPreferredQuality }
if (preferredIdx != -1) {
videoSorted.drop(preferredIdx + 1)
videoSorted[0] = videoSorted[preferredIdx]
}
videoSorted.toList()
} catch (e: Exception) {
this
}
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()
}
private fun getNumberFromString(epsStr: String): String {
return epsStr.filter { it.isDigit() }.ifEmpty { "0" }
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val genreFilter = filters.filterIsInstance<GenreFilter>().firstOrNull() ?: GenreFilter()
val yearFilter = try {
(filters.find { it is YearFilter } as YearFilter).state.toInt()
} catch (e: Exception) {
"false"
}
val letterFilter = try {
(filters.find { it is LetterFilter } as LetterFilter).state.first().uppercase()
} catch (e: Exception) {
"false"
}
private fun Element.getImageUrl(): String? {
return when {
query.isNotBlank() -> GET("$baseUrl/buscar?q=$query&p=$page")
else -> GET("$baseUrl/animes?categoria=false&genero=${genreFilter.toUriPart()}&fecha=$yearFilter&letra=$letterFilter&p=$page")
isValidUrl("data-src") -> attr("abs:data-src")
isValidUrl("data-lazy-src") -> attr("abs:data-lazy-src")
isValidUrl("srcset") -> attr("abs:srcset").substringBefore(" ")
isValidUrl("src") -> attr("abs:src")
else -> ""
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
return popularAnimeFromElement(element)
private fun Element.isValidUrl(attrName: String): Boolean {
if (!hasAttr(attrName)) return false
return !attr(attrName).contains("anime.png")
}
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
thumbnail_url = document.selectFirst("div.chapterpic img")!!.attr("src")
title = document.selectFirst("div.chapterdetails h1")!!.text()
description = document.selectFirst("p.textShort")!!.ownText()
genre = document.select("ol.breadcrumb li.breadcrumb-item a").joinToString { it.text() }
status = parseStatus(document.select("div.butns button.btn1").text())
}
}
private fun parseStatus(statusString: String): Int {
return when {
statusString.contains("Estreno") -> SAnime.ONGOING
statusString.contains("Finalizado") -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
AnimeFilter.Separator(),
YearFilter(),
LetterFilter(),
)
private class YearFilter : AnimeFilter.Text("Año", "2022")
private class LetterFilter : AnimeFilter.Text("Letra", "")
private class GenreFilter : UriPartFilter(
"Generos",
arrayOf(
Pair("<selecionar>", ""),
Pair("Latino", "latino"),
Pair("Castellano", "castellano"),
Pair("Acción", "acción"),
Pair("Aventura", "aventura"),
Pair("Carreras", "carreras"),
Pair("Comedia", "comedia"),
Pair("Cyberpunk", "cyberpunk"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Fantasía", "fantasía"),
Pair("Gore", "gore"),
Pair("Harem", "harem"),
Pair("Horror", "horror"),
Pair("Josei", "josei"),
Pair("Lucha", "lucha"),
Pair("Magia", "magia"),
Pair("Josei", "josei"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "música"),
Pair("Parodias", "parodias"),
Pair("Psicológico", "psicológico"),
Pair("Recuerdos de la vida", "recuerdos-de-la-vida"),
Pair("Seinen", "seinen"),
Pair("Shojo", "shojo"),
Pair("Shonen", "shonen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Espacial", "espacial"),
Pair("Histórico", "histórico"),
Pair("Samurai", "samurai"),
Pair("Artes Marciales", "artes-marciales"),
Pair("Demonios", "demonios"),
Pair("Romance", "romance"),
Pair("Policía", " policía"),
Pair("Historia paralela", "historia-paralela"),
Pair("Aenime", "aenime"),
Pair("Donghua", "donghua"),
Pair("Blu-ray", "blu-ray"),
Pair("Monogatari", "monogatari"),
),
)
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 fun Double.ceilPage(): Int = if (this % 1 == 0.0) this.toInt() else ceil(this).toInt()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val qualities = arrayOf(
"Okru:1080p",
"Okru:720p",
"Okru:480p",
"Okru:360p",
"Okru:240p", // Okru
"SolidFiles",
"Upload", // video servers without resolution
"StreamTape",
"FileMoon",
)
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = qualities
entryValues = qualities
setDefaultValue("Okru:720p")
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 ->
@ -282,7 +257,22 @@ class MonosChinos : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}.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,29 @@
package eu.kanade.tachiyomi.animeextension.es.monoschinos
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class EpisodesDto(
@SerialName("paginate_url") var paginateUrl: String? = null,
@SerialName("perpage") var perpage: Double? = null,
@SerialName("eps") var eps: ArrayList<Eps> = arrayListOf(),
)
@Serializable
data class Eps(
@SerialName("num") var num: Int? = null,
)
@Serializable
data class EpisodeInfoDto(
@SerialName("default") var default: String? = null,
@SerialName("caps") var caps: ArrayList<Caps> = arrayListOf(),
)
@Serializable
data class Caps(
@SerialName("episodio") var episodio: Int? = null,
@SerialName("url") var url: String? = null,
@SerialName("thumb") var thumb: String? = null,
)

View file

@ -0,0 +1,139 @@
package eu.kanade.tachiyomi.animeextension.es.monoschinos
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import java.util.Calendar
object MonosChinosFilters {
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(",").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>(DoramasytFiltersData.GENRES, "genero") +
filters.parseCheckbox<TypesFilter>(DoramasytFiltersData.TYPES, "tipo") +
filters.asQueryPart<YearsFilter>("fecha") +
filters.asQueryPart<LettersFilter>("letra"),
)
}
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenresFilter(),
TypesFilter(),
YearsFilter(),
LettersFilter(),
)
class GenresFilter : CheckBoxFilterList("Género", DoramasytFiltersData.GENRES.map { CheckBoxVal(it.first, false) })
class TypesFilter : CheckBoxFilterList("Tipo", DoramasytFiltersData.TYPES.map { CheckBoxVal(it.first, false) })
class YearsFilter : QueryPartFilter("Año", DoramasytFiltersData.YEARS)
class LettersFilter : QueryPartFilter("Letra", DoramasytFiltersData.LETTER)
private object DoramasytFiltersData {
val TYPES = arrayOf(
Pair("<Selecionar>", ""),
Pair("Pelicula", "pelicula"),
Pair("Anime", "anime"),
)
val YEARS = arrayOf(Pair("<Seleccionar>", "")) + (1982..Calendar.getInstance().get(Calendar.YEAR)).map { Pair("$it", "$it") }.reversed().toTypedArray()
val LETTER = arrayOf(Pair("<Seleccionar>", "")) + ('A'..'Z').map { Pair("$it", "$it") }.toTypedArray()
val GENRES = arrayOf(
Pair("<Selecionar>", ""),
Pair("Acción", "accion"),
Pair("Aventura", "aventura"),
Pair("Carreras", "carreras"),
Pair("Ciencia Ficción", "ciencia-ficcion"),
Pair("Comedia", "comedia"),
Pair("Cyberpunk", "cyberpunk"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Fantasía", "fantasia"),
Pair("Gore", "gore"),
Pair("Harem", "harem"),
Pair("Horror", "horror"),
Pair("Josei", "josei"),
Pair("Lucha", "lucha"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "musica"),
Pair("Parodias", "parodias"),
Pair("Psicológico", "psicologico"),
Pair("Recuerdos de la vida", "recuerdos-de-la-vida"),
Pair("Seinen", "seinen"),
Pair("Shojo", "shojo"),
Pair("Shonen", "shonen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Latino", "latino"),
Pair("Espacial", "espacial"),
Pair("Histórico", "historico"),
Pair("Samurai", "samurai"),
Pair("Artes Marciales", "artes-marciales"),
Pair("Demonios", "demonios"),
Pair("Romance", "romance"),
Pair("Dementia", "dementia"),
Pair(" Policía", "policia"),
Pair("Castellano", "castellano"),
Pair("Historia paralela", "historia-paralela"),
Pair("Aenime", "aenime"),
Pair("Blu-ray", "blu-ray"),
Pair("Monogatari", "monogatari"),
)
}
}

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.monoschinos.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 = 'Pelisplushd'
extClass = '.PelisplushdFactory'
extVersionCode = 57
extVersionCode = 58
}
apply from: "$rootDir/common.gradle"
@ -23,5 +23,7 @@ dependencies {
implementation(project(':lib:fastream-extractor'))
implementation(project(':lib:upstream-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
implementation(project(':lib:streamsilk-extractor'))
implementation(project(':lib:vidguard-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

View file

@ -20,15 +20,16 @@ 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.streamsilkextractor.StreamSilkExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
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.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -57,6 +58,7 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
"YourUpload", "BurstCloud", "Voe", "Mp4Upload", "Doodstream",
"Upload", "BurstCloud", "Upstream", "StreamTape", "Amazon",
"Fastream", "Filemoon", "StreamWish", "Okru", "Streamlare",
"VidGuard",
)
private val REGEX_LINK = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)".toRegex()
@ -115,15 +117,15 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
if (apiResponse.isSuccessful) {
val encryptedList = if (docResponse.select("iframe").any()) {
listOf(docResponse.select("iframe").attr("src"))
listOf("" to docResponse.select("iframe").attr("src"))
} else {
docResponse.select("#PlayerDisplay div[class*=\"OptionsLangDisp\"] div[class*=\"ODDIV\"] div[class*=\"OD\"] li")
.map { it.attr("onclick") }
.map { it.attr("data-lang").getLang() to it.attr("onclick") }
}
encryptedList.flatMap {
runCatching {
val url = it.substringAfter("go_to_player('")
val url = it.second.substringAfter("go_to_player('")
.substringAfter("go_to_playerVast('")
.substringBefore("?cover_url=")
.substringBefore("')")
@ -142,7 +144,7 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
url
}
serverVideoResolver(realUrl)
serverVideoResolver(realUrl, it.first)
}.getOrNull() ?: emptyList()
}.also(videoList::addAll)
}
@ -150,20 +152,31 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
return videoList
}
private fun serverVideoResolver(url: String): List<Video> {
val embedUrl = url.lowercase()
/*--------------------------------Video extractors------------------------------------*/
private val voeExtractor by lazy { VoeExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private val burstCloudExtractor by lazy { BurstCloudExtractor(client) }
private val fastreamExtractor by lazy { FastreamExtractor(client, headers) }
private val upstreamExtractor by lazy { UpstreamExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamHideVidExtractor by lazy { StreamHideVidExtractor(client) }
private val streamSilkExtractor by lazy { StreamSilkExtractor(client) }
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
fun serverVideoResolver(url: String, prefix: String = ""): List<Video> {
return runCatching {
when {
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> OkruExtractor(client).videosFromUrl(url)
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)
}
!embedUrl.contains("disable") && (embedUrl.contains("amazon") || embedUrl.contains("amz")) -> {
arrayOf("voe").any(url) -> voeExtractor.videosFromUrl(url, "$prefix ")
arrayOf("ok.ru", "okru").any(url) -> okruExtractor.videosFromUrl(url, prefix)
arrayOf("filemoon", "moonplayer").any(url) -> filemoonExtractor.videosFromUrl(url, prefix = "$prefix Filemoon:")
!url.contains("disable") && (arrayOf("amazon", "amz").any(url)) -> {
val body = client.newCall(GET(url)).execute().asJsoup()
return if (body.select("script:containsData(var shareId)").toString().isNotBlank()) {
val shareId = body.selectFirst("script:containsData(var shareId)")!!.data()
@ -175,32 +188,29 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
client.newCall(GET("https://www.amazon.com/drive/v1/nodes/$epId/children?resourceVersion=V2&ContentType=JSON&limit=200&sort=%5B%22kind+DESC%22%2C+%22modifiedDate+DESC%22%5D&asset=ALL&tempLink=true&shareId=$shareId"))
.execute().asJsoup()
val videoUrl = amazonApi.toString().substringAfter("\"FOLDER\":").substringAfter("tempLink\":\"").substringBefore("\"")
listOf(Video(videoUrl, "Amazon", videoUrl))
listOf(Video(videoUrl, "$prefix Amazon", videoUrl))
} else {
emptyList()
}
}
embedUrl.contains("uqload") -> UqloadExtractor(client).videosFromUrl(url)
embedUrl.contains("mp4upload") -> Mp4uploadExtractor(client).videosFromUrl(url, headers)
embedUrl.contains("wishembed") || 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" })
arrayOf("uqload").any(url) -> uqloadExtractor.videosFromUrl(url, prefix)
arrayOf("mp4upload").any(url) -> mp4uploadExtractor.videosFromUrl(url, headers, prefix = "$prefix ")
arrayOf("wishembed", "streamwish", "strwish", "wish").any(url) -> {
streamWishExtractor.videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" })
}
embedUrl.contains("doodstream") || embedUrl.contains("dood.") || embedUrl.contains("ds2play") || embedUrl.contains("doods.") -> {
arrayOf("doodstream", "dood.", "ds2play", "doods.").any(url) -> {
val url2 = url.replace("https://doodstream.com/e/", "https://d0000d.com/e/")
listOf(DoodExtractor(client).videoFromUrl(url2, "DoodStream")!!)
doodExtractor.videosFromUrl(url2, "$prefix DoodStream")
}
embedUrl.contains("streamlare") -> StreamlareExtractor(client).videosFromUrl(url)
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
embedUrl.contains("burstcloud") || embedUrl.contains("burst") -> BurstCloudExtractor(client).videoFromUrl(url, headers = headers)
embedUrl.contains("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "Fastream:")
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url)
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "StreamTape")!!)
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") ||
embedUrl.contains("streamvid") || embedUrl.contains("vidhide") -> StreamHideVidExtractor(client).videosFromUrl(url)
arrayOf("streamlare").any(url) -> streamlareExtractor.videosFromUrl(url, prefix)
arrayOf("yourupload", "upload").any(url) -> yourUploadExtractor.videoFromUrl(url, headers = headers, prefix = "$prefix ")
arrayOf("burstcloud", "burst").any(url) -> burstCloudExtractor.videoFromUrl(url, headers = headers, prefix = "$prefix ")
arrayOf("fastream").any(url) -> fastreamExtractor.videosFromUrl(url, prefix = "$prefix Fastream:")
arrayOf("upstream").any(url) -> upstreamExtractor.videosFromUrl(url, prefix = "$prefix ")
arrayOf("streamsilk").any(url) -> streamSilkExtractor.videosFromUrl(url, videoNameGen = { "$prefix StreamSilk:$it" })
arrayOf("streamtape", "stp", "stape").any(url) -> streamTapeExtractor.videosFromUrl(url, quality = "$prefix StreamTape")
arrayOf("ahvsh", "streamhide", "guccihide", "streamvid", "vidhide").any(url) -> streamHideVidExtractor.videosFromUrl(url, prefix = "$prefix ")
arrayOf("vembed", "guard", "listeamed", "bembed", "vgfplay").any(url) -> vidGuardExtractor.videosFromUrl(url, prefix = "$prefix ")
else -> emptyList()
}
}.getOrNull() ?: emptyList()
@ -306,6 +316,17 @@ open class Pelisplushd(override val name: String, override val baseUrl: String)
fun toUriPart() = vals[state].second
}
fun String.getLang(): String {
return when {
arrayOf("0", "lat").any(this) -> "[LAT]"
arrayOf("1", "cast").any(this) -> "[CAST]"
arrayOf("2", "eng", "sub").any(this) -> "[SUB]"
else -> ""
}
}
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY

View file

@ -7,23 +7,8 @@ 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.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.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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -141,45 +126,6 @@ class Pelisplusph(override val name: String, override val baseUrl: String) : Pel
}
}
private fun serverVideoResolver(url: String, prefix: String = ""): List<Video> {
val embedUrl = url.lowercase()
return runCatching {
when {
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url, prefix)
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> OkruExtractor(client).videosFromUrl(url, prefix)
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 = "$prefix Filemoon:", headers = vidHeaders)
}
embedUrl.contains("uqload") -> UqloadExtractor(client).videosFromUrl(url, prefix = prefix)
embedUrl.contains("mp4upload") -> Mp4uploadExtractor(client).videosFromUrl(url, headers, prefix = prefix)
embedUrl.contains("wishembed") || 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 = { "$prefix StreamWish:$it" })
}
embedUrl.contains("doodstream") || embedUrl.contains("dood.") || embedUrl.contains("ds2play") || embedUrl.contains("doods.") -> {
val url2 = url.replace("https://doodstream.com/e/", "https://dood.to/e/")
listOf(DoodExtractor(client).videoFromUrl(url2, "$prefix DoodStream")!!)
}
embedUrl.contains("streamlare") -> StreamlareExtractor(client).videosFromUrl(url, prefix = prefix)
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix)
embedUrl.contains("burstcloud") || embedUrl.contains("burst") -> BurstCloudExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix)
embedUrl.contains("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "$prefix Fastream:")
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url, prefix = prefix)
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "$prefix StreamTape")!!)
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") ||
embedUrl.contains("streamvid") || embedUrl.contains("vidhide") -> StreamHideVidExtractor(client).videosFromUrl(url, "$prefix ")
else -> emptyList()
}
}.getOrNull() ?: emptyList()
}
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)!!

View file

@ -8,20 +8,6 @@ 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.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.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.serialization.json.Json
@ -29,7 +15,6 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -129,6 +114,7 @@ class Pelisplusto(override val name: String, override val baseUrl: String) : Pel
val document = response.asJsoup()
val regIsUrl = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)".toRegex()
return document.select(".bg-tabs ul li").flatMap {
val prefix = it.parent()?.parent()?.selectFirst("button")?.ownText()?.lowercase()?.getLang()
val decode = String(Base64.decode(it.attr("data-server"), Base64.DEFAULT))
val url = if (!regIsUrl.containsMatchIn(decode)) {
@ -143,7 +129,7 @@ class Pelisplusto(override val name: String, override val baseUrl: String) : Pel
}.replace("https://sblanh.com", "https://lvturbo.com")
.replace(Regex("([a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)=https:\\/\\/ww3.pelisplus.to.*"), "")
serverVideoResolver(videoUrl)
serverVideoResolver(videoUrl, prefix ?: "")
}
}
@ -159,62 +145,6 @@ class Pelisplusto(override val name: String, override val baseUrl: String) : Pel
).reversed()
}
private fun serverVideoResolver(url: String): List<Video> {
val embedUrl = url.lowercase()
return runCatching {
when {
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> OkruExtractor(client).videosFromUrl(url)
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)
}
!embedUrl.contains("disable") && (embedUrl.contains("amazon") || embedUrl.contains("amz")) -> {
val body = client.newCall(GET(url)).execute().asJsoup()
return if (body.select("script:containsData(var shareId)").toString().isNotBlank()) {
val shareId = body.selectFirst("script:containsData(var shareId)")!!.data()
.substringAfter("shareId = \"").substringBefore("\"")
val amazonApiJson = client.newCall(GET("https://www.amazon.com/drive/v1/shares/$shareId?resourceVersion=V2&ContentType=JSON&asset=ALL"))
.execute().asJsoup()
val epId = amazonApiJson.toString().substringAfter("\"id\":\"").substringBefore("\"")
val amazonApi =
client.newCall(GET("https://www.amazon.com/drive/v1/nodes/$epId/children?resourceVersion=V2&ContentType=JSON&limit=200&sort=%5B%22kind+DESC%22%2C+%22modifiedDate+DESC%22%5D&asset=ALL&tempLink=true&shareId=$shareId"))
.execute().asJsoup()
val videoUrl = amazonApi.toString().substringAfter("\"FOLDER\":").substringAfter("tempLink\":\"").substringBefore("\"")
listOf(Video(videoUrl, "Amazon", videoUrl))
} else {
emptyList()
}
}
embedUrl.contains("uqload") -> UqloadExtractor(client).videosFromUrl(url)
embedUrl.contains("mp4upload") -> Mp4uploadExtractor(client).videosFromUrl(url, headers)
embedUrl.contains("wishembed") || 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" })
}
embedUrl.contains("doodstream") || embedUrl.contains("dood.") || embedUrl.contains("ds2play") || embedUrl.contains("doods.") -> {
val url2 = url.replace("https://doodstream.com/e/", "https://dood.to/e/")
listOf(DoodExtractor(client).videoFromUrl(url2, "DoodStream")!!)
}
embedUrl.contains("streamlare") -> StreamlareExtractor(client).videosFromUrl(url)
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
embedUrl.contains("burstcloud") || embedUrl.contains("burst") -> BurstCloudExtractor(client).videoFromUrl(url, headers = headers)
embedUrl.contains("fastream") -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "Fastream:")
embedUrl.contains("upstream") -> UpstreamExtractor(client).videosFromUrl(url)
embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape") -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "StreamTape")!!)
embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("guccihide") ||
embedUrl.contains("streamvid") || embedUrl.contains("vidhide") -> StreamHideVidExtractor(client).videosFromUrl(url)
else -> emptyList()
}
}.getOrNull() ?: emptyList()
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por genero ignora los otros filtros"),
GenreFilter(),

View file

@ -1,7 +1,7 @@
ext {
extName = 'TioanimeH'
extClass = '.TioanimeHFactory'
extVersionCode = 18
extVersionCode = 19
}
apply from: "$rootDir/common.gradle"
@ -10,4 +10,6 @@ dependencies {
implementation(project(':lib:yourupload-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:mixdrop-extractor'))
implementation(project(':lib:vidguard-extractor'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,208 @@
package eu.kanade.tachiyomi.animeextension.es.tioanimeh
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.CheckBox
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import java.util.Calendar
object TioAnimeHFilters {
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() }
private fun Array<Pair<String, String>>.toCheckBoxVal(): List<CheckBox> = map { CheckBoxVal(it.first, false) }
private fun String.addSuffix(): String = takeIf { it.isNotBlank() }?.let { "$it,${Calendar.getInstance().get(Calendar.YEAR)}" } ?: this
internal fun getSearchParameters(filters: AnimeFilterList, origen: TioAnimeHFiltersData.ORIGEN): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val strFilter = buildString {
when (origen) {
TioAnimeHFiltersData.ORIGEN.ANIME -> {
append(filters.parseCheckbox<TypesFilter>(TioAnimeHFiltersData.TYPES, "type"))
append(filters.parseCheckbox<AnimeGenresFilter>(TioAnimeHFiltersData.ANIME_GENRES, "genero"))
append(filters.asQueryPart<YearsFilter>("year").addSuffix())
append(filters.asQueryPart<StatesFilter>("status"))
append(filters.asQueryPart<SortFilter>("sort"))
}
else -> {
append(filters.asQueryPart<HentaiGenresFilter>("genero"))
}
}
}
return FilterSearchParams(strFilter)
}
private val ANIME_FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
TypesFilter(),
AnimeGenresFilter(),
YearsFilter(),
StatesFilter(),
SortFilter(),
)
private val HENTAI_FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
HentaiGenresFilter(),
)
fun getFilterList(origen: TioAnimeHFiltersData.ORIGEN) = when (origen) {
TioAnimeHFiltersData.ORIGEN.ANIME -> ANIME_FILTER_LIST
else -> HENTAI_FILTER_LIST
}
class TypesFilter : CheckBoxFilterList("Tipo", TioAnimeHFiltersData.TYPES.toCheckBoxVal())
class AnimeGenresFilter : CheckBoxFilterList("Género", TioAnimeHFiltersData.ANIME_GENRES.toCheckBoxVal())
class HentaiGenresFilter : QueryPartFilter("Género", TioAnimeHFiltersData.HENTAI_GENRES)
class YearsFilter : QueryPartFilter("Año", TioAnimeHFiltersData.YEARS)
class StatesFilter : QueryPartFilter("Estado", TioAnimeHFiltersData.STATES)
class SortFilter : QueryPartFilter("Orden", TioAnimeHFiltersData.SORT)
object TioAnimeHFiltersData {
val YEARS = arrayOf(Pair("Todos", "")) + (1950..Calendar.getInstance().get(Calendar.YEAR)).map { Pair("$it", "$it") }.reversed().toTypedArray()
val TYPES = arrayOf(
Pair("TV", "0"),
Pair("Película", "1"),
Pair("OVA", "2"),
Pair("Especial", "3"),
)
val ANIME_GENRES = arrayOf(
Pair("Acción", "accion"),
Pair("Artes Marciales", "artes-marciales"),
Pair("Aventuras", "aventura"),
Pair("Carreras", "carreras"),
Pair("Ciencia Ficción", "ciencia-ficcion"),
Pair("Comedia", "comedia"),
Pair("Demencia", "demencia"),
Pair("Demonios", "demonios"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Espacial", "espacial"),
Pair("Fantasía", "fantasia"),
Pair("Harem", "harem"),
Pair("Historico", "historico"),
Pair("Infantil", "infantil"),
Pair("Josei", "josei"),
Pair("Juegos", "juegos"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "musica"),
Pair("Parodia", "parodia"),
Pair("Policía", "policia"),
Pair("Psicológico", "psicologico"),
Pair("Recuentos de la vida", "recuentos-de-la-vida"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoderes", "superpoderes"),
Pair("Suspenso", "suspenso"),
Pair("Terror", "terror"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
val HENTAI_GENRES = arrayOf(
Pair("Ahegao", "ahegao"),
Pair("Anal", "anal"),
Pair("Bestialidad", "bestialidad"),
Pair("Bondage", "bondage"),
Pair("Chikan", "chikan"),
Pair("Comedia", "comedia"),
Pair("Enfermeras", "enfermeras"),
Pair("Escolar", "escolar"),
Pair("Fantasia", "fantasia"),
Pair("Futanari", "futanari"),
Pair("Gangbang", "gangbang"),
Pair("Harem", "harem"),
Pair("Incesto", "incesto"),
Pair("Vamp", "vamp"),
Pair("Maids", "maids"),
Pair("Milf", "milf"),
Pair("Netorare", "netorare"),
Pair("Masturbacion", "masturbacion"),
Pair("Romance", "romance"),
Pair("Shota", "shota"),
Pair("Tentaculos", "tentaculos"),
Pair("Tetonas", "tetonas"),
Pair("Violacion", "violacion"),
Pair("Virgenes", "virgenes"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Demonios", "demonios"),
Pair("Felacion", "felacion"),
)
val STATES = arrayOf(
Pair("Finalizado", "2"),
Pair("En emisión", "1"),
Pair("Próximamente", "3"),
)
val SORT = arrayOf(
Pair("Mas Reciente", "recent"),
Pair("Menos Reciente", "-recent"),
)
enum class ORIGEN {
ANIME,
HENTAI,
}
}
}

View file

@ -4,19 +4,20 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.tioanimeh.extractors.VidGuardExtractor
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.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
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 eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -54,21 +55,21 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/directorio?p=$page")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.url = element.select("article a").attr("href")
anime.title = element.select("article a h3").text()
anime.thumbnail_url = baseUrl + element.select("article a div figure img").attr("src")
return anime
return SAnime.create().apply {
url = element.select("article a").attr("href")
title = element.select("article a h3").text()
thumbnail_url = baseUrl + element.select("article a div figure img").attr("src")
}
}
override fun popularAnimeNextPageSelector(): String = "nav ul.pagination.d-inline-flex li.page-item a.page-link"
override fun popularAnimeNextPageSelector(): String = ".pagination .active ~ li:not(.disabled)"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val epInfoScript = document.selectFirst("script:containsData(var episodes = )")!!.data()
if (epInfoScript.substringAfter("episodes = [").substringBefore("];").isEmpty()) {
return listOf<SEpisode>()
return emptyList()
}
val epNumList = epInfoScript.substringAfter("episodes = [").substringBefore("];").split(",")
@ -84,27 +85,36 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
}
override fun episodeListSelector() = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
/*--------------------------------Video extractors------------------------------------*/
private val voeExtractor by lazy { VoeExtractor(client) }
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private val mixDropExtractor by lazy { MixDropExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
val serverList = document.selectFirst("script:containsData(var videos =)")!!.data().substringAfter("var videos = [[").substringBefore("]];")
.replace("\"", "").split("],[")
serverList.forEach {
val serverList = document.selectFirst("script:containsData(var videos =)")
?.data()?.substringAfter("var videos = [[")?.substringBefore("]];")
?.replace("\"", "")?.split("],[") ?: return emptyList()
return serverList.parallelCatchingFlatMapBlocking {
val servers = it.split(",")
val serverName = servers[0]
val serverUrl = servers[1].replace("\\/", "/")
when (serverName.lowercase()) {
"voe" -> VoeExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll)
"vidguard" -> VidGuardExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll)
"okru" -> OkruExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll)
"yourupload" -> YourUploadExtractor(client).videoFromUrl(serverUrl, headers = headers).let(videoList::addAll)
"voe" -> voeExtractor.videosFromUrl(serverUrl)
"vidguard" -> vidGuardExtractor.videosFromUrl(serverUrl)
"okru" -> okruExtractor.videosFromUrl(serverUrl)
"yourupload" -> yourUploadExtractor.videoFromUrl(serverUrl, headers = headers)
"mixdrop" -> mixDropExtractor.videosFromUrl(serverUrl)
else -> emptyList()
}
}
return videoList
}
override fun videoListSelector() = throw UnsupportedOperationException()
@ -126,32 +136,29 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = if (filterList.isNotEmpty())filterList.find { it is GenreFilter } as GenreFilter else { GenreFilter().apply { state = 0 } }
val params = TioAnimeHFilters.getSearchParameters(filters, TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.HENTAI)
return when {
query.isNotBlank() -> GET("$baseUrl/directorio?q=$query&p=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/directorio?genero=${genreFilter.toUriPart()}&p=$page")
else -> GET("$baseUrl/directorio?p=$page ")
params.filter.isNotBlank() -> GET("$baseUrl/directorio${params.getQuery()}&p=$page")
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
return popularAnimeFromElement(element)
}
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("h1.title").text()
anime.description = document.selectFirst("p.sinopsis")!!.ownText()
anime.genre = document.select("p.genres span.btn.btn-sm.btn-primary.rounded-pill a").joinToString { it.text() }
anime.thumbnail_url = document.select(".thumb img").attr("abs:src")
anime.status = parseStatus(document.select("a.btn.btn-success.btn-block.status").text())
return anime
return SAnime.create().apply {
title = document.select("h1.title").text()
description = document.selectFirst("p.sinopsis")!!.ownText()
genre = document.select("p.genres span.btn.btn-sm.btn-primary.rounded-pill a").joinToString { it.text() }
thumbnail_url = document.select(".thumb img").attr("abs:src")
status = parseStatus(document.select("a.btn.btn-success.btn-block.status").text())
}
}
private fun parseStatus(statusString: String): Int {
@ -165,59 +172,21 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.title = element.select("article a h3").text()
anime.thumbnail_url = baseUrl + element.select("article a div figure img").attr("src")
val slug = if (baseUrl.contains("hentai")) "/hentai/" else "/anime/"
val fixUrl = element.select("article a").attr("href").split("-").toTypedArray()
val realUrl = fixUrl.copyOf(fixUrl.size - 1).joinToString("-").replace("/ver/", slug)
anime.setUrlWithoutDomain(realUrl)
return anime
return SAnime.create().apply {
title = element.select("article a h3").text()
thumbnail_url = baseUrl + element.select("article a div figure img").attr("src")
val slug = if (baseUrl.contains("hentai")) "/hentai/" else "/anime/"
val fixUrl = element.select("article a").attr("href").split("-").toTypedArray()
val realUrl = fixUrl.copyOf(fixUrl.size - 1).joinToString("-").replace("/ver/", slug)
setUrlWithoutDomain(realUrl)
}
}
override fun latestUpdatesRequest(page: Int) = GET(baseUrl)
override fun latestUpdatesSelector() = ".episodes li"
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
)
class GenreFilter : UriPartFilter(
"Generos",
arrayOf(
Pair("<selecionar>", ""),
Pair("Ahegao", "ahegao"),
Pair("Anal", "anal"),
Pair("Casadas", "casadas"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Enfermeras", "enfermeras"),
Pair("Futanari", "futanari"),
Pair("Harem", "Harem"),
Pair("Gore", "gore"),
Pair("Hardcore", "hardcore"),
Pair("Incesto", "incesto"),
Pair("Juegos Sexuales", "juegos-sexuales"),
Pair("Milfs", "milf"),
Pair("Orgia", "orgia"),
Pair("Romance", "romance"),
Pair("Shota", "shota"),
Pair("Succubus", "succubus"),
Pair("Tetonas", "tetonas"),
Pair("Violacion", "violacion"),
Pair("Virgenes(como tu)", "virgenes"),
Pair("Yaoi", "Yaoi"),
Pair("Yuri", "yuri"),
),
)
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
}
override fun getFilterList(): AnimeFilterList = TioAnimeHFilters.getFilterList(TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.HENTAI)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {

View file

@ -15,65 +15,16 @@ class TioanimeHFactory : AnimeSourceFactory {
class TioAnime : TioanimeH("TioAnime", "https://tioanime.com") {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = if (filterList.isNotEmpty())filterList.find { it is GenreFilter } as GenreFilter else { GenreFilter().apply { state = 0 } }
val params = TioAnimeHFilters.getSearchParameters(filters, TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.ANIME)
return when {
query.isNotBlank() -> GET("$baseUrl/directorio?q=$query&p=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/directorio?genero=${genreFilter.toUriPart()}&p=$page")
else -> GET("$baseUrl/directorio?p=$page ")
params.filter.isNotBlank() -> GET("$baseUrl/directorio${params.getQuery()}&p=$page")
else -> popularAnimeRequest(page)
}
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Géneros",
arrayOf(
Pair("<Selecionar>", "all"),
Pair("Acción", "accion"),
Pair("Artes Marciales", "artes_marciales"),
Pair("Aventuras", "aventura"),
Pair("Carreras", "carreras"),
Pair("Ciencia Ficción", "ciencia_ficcion"),
Pair("Comedia", "comedia"),
Pair("Demencia", "demencia"),
Pair("Demonios", "demonios"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Espacial", "espacial"),
Pair("Fantasía", "fantasia"),
Pair("Harem", "harem"),
Pair("Historico", "historico"),
Pair("Infantil", "infantil"),
Pair("Josei", "josei"),
Pair("Juegos", "juegos"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "musica"),
Pair("Parodia", "parodia"),
Pair("Policía", "policia"),
Pair("Psicológico", "psicologico"),
Pair("Recuentos de la vida", "recuentos_de_la_vida"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoderes", "superpoderes"),
Pair("Suspenso", "suspenso"),
Pair("Terror", "terror"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
),
)
override fun getFilterList(): AnimeFilterList = TioAnimeHFilters.getFilterList(TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.ANIME)
}
class TioHentai : TioanimeH("TioHentai", "https://tiohentai.com")

View file

@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.tioanimeh.extractors
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VidGuardExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch) {
var payload: String = ""
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
?.absUrl("src")
?: return emptyList()
val headers = Headers.headersOf("Referer", url)
val script = client.newCall(GET(scriptUrl, headers)).execute()
.body.string()
val sources = getSourcesFromScript(script, url)
.takeIf { it.isNotBlank() && it != "undefined" }
?: return emptyList()
return sources.substringAfter("stream:[").substringBefore("}]")
.split('{')
.drop(1)
.mapNotNull { line ->
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let(::fixUrl)
?: return@mapNotNull null
Video(videoUrl, "VidGuard:$resolution", videoUrl, headers)
}
}
private fun getSourcesFromScript(script: String, url: String): String {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsinterface = JsObject(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
cacheMode = WebSettings.LOAD_NO_CACHE
}
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
view?.evaluateJavascript(script) {}
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
}
}
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
}
latch.await(5, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsinterface.payload
}
private fun fixUrl(url: String): String {
val httpUrl = url.toHttpUrl()
val originalSign = httpUrl.queryParameter("sig")!!
val newSign = originalSign.chunked(2).joinToString("") {
Char(it.toInt(16) xor 2).toString()
}
.let { String(Base64.decode(it, Base64.DEFAULT)) }
.substring(5)
.chunked(2)
.reversed()
.joinToString("")
.substring(5)
return httpUrl.newBuilder()
.removeAllQueryParameters("sig")
.addQueryParameter("sig", newSign)
.build()
.toString()
}
}

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