parent
45cff438ce
commit
821cbc1d59
89 changed files with 668 additions and 557 deletions
3
lib/buzzheavier-extractor/build.gradle.kts
Normal file
3
lib/buzzheavier-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-android")
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.buzzheavierextractor
|
||||||
|
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.internal.EMPTY_HEADERS
|
||||||
|
|
||||||
|
class BuzzheavierExtractor(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val headers: Headers,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
val id = httpUrl.pathSegments.first()
|
||||||
|
|
||||||
|
val dlHeaders = headers.newBuilder().apply {
|
||||||
|
add("Accept", "*/*")
|
||||||
|
add("Host", httpUrl.host)
|
||||||
|
add("HX-Current-URL", url)
|
||||||
|
add("HX-Request", "true")
|
||||||
|
add("Referer", url)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val videoHeaders = headers.newBuilder().apply {
|
||||||
|
add("Referer", url)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val path = client.newCall(GET("$url/download", dlHeaders)).execute().headers["hx-redirect"].orEmpty()
|
||||||
|
|
||||||
|
return if (path.isNotEmpty()) {
|
||||||
|
val videoUrl = if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
|
||||||
|
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders))
|
||||||
|
} else if (proxyUrl?.isNotEmpty() == true) {
|
||||||
|
val videoUrl = client.newCall(GET(proxyUrl + id)).execute().parseAs<UrlDto>().url
|
||||||
|
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders))
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UrlDto(val url: String)
|
||||||
|
}
|
|
@ -1,73 +1,53 @@
|
||||||
package eu.kanade.tachiyomi.lib.chillxextractor
|
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
private val json: Json by injectLazy()
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
private val webViewResolver by lazy { WebViewResolver(client, headers) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val REGEX_MASTER_JS = Regex("""\s*=\s*'([^']+)""")
|
|
||||||
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
|
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
|
||||||
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
|
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
|
||||||
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
|
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
|
||||||
private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""")
|
private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""")
|
||||||
private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
|
fun videoFromUrl(url: String, prefix: String = "Chillx - "): List<Video> {
|
||||||
val newHeaders = headers.newBuilder()
|
val data = webViewResolver.getDecryptedData(url) ?: return emptyList()
|
||||||
.set("Referer", "$referer/")
|
|
||||||
.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
|
||||||
.set("Accept-Language", "en-US,en;q=0.5")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val body = client.newCall(GET(url, newHeaders)).execute().body.string()
|
val masterUrl = REGEX_SOURCES.find(data)?.groupValues?.get(1)
|
||||||
|
?: REGEX_FILE.find(data)?.groupValues?.get(1)
|
||||||
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
|
?: REGEX_SOURCE.find(data)?.groupValues?.get(1)
|
||||||
val aesJson = json.decodeFromString<CryptoInfo>(master)
|
|
||||||
val key = fetchKey() ?: throw ErrorLoadingException("Unable to get key")
|
|
||||||
val decryptedScript = CryptoAES.decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
|
|
||||||
.replace("\\n", "\n")
|
|
||||||
.replace("\\", "")
|
|
||||||
|
|
||||||
val masterUrl = REGEX_SOURCES.find(decryptedScript)?.groupValues?.get(1)
|
|
||||||
?: REGEX_FILE.find(decryptedScript)?.groupValues?.get(1)
|
|
||||||
?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1)
|
|
||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
|
|
||||||
val subtitleList = buildList {
|
val subtitleList = buildList {
|
||||||
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
val subtitles = REGEX_SUBS.findAll(data)
|
||||||
subtitles.forEach {
|
subtitles.forEach {
|
||||||
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
|
|
||||||
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
val videoList = playlistUtils.extractFromHls(
|
||||||
playlistUrl = masterUrl,
|
playlistUrl = masterUrl,
|
||||||
referer = url,
|
referer = url,
|
||||||
videoNameGen = { "$prefix$it" },
|
videoNameGen = { "$prefix$it" },
|
||||||
subtitleList = subtitleList,
|
subtitleList = subtitleList,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
return videoList.map {
|
||||||
private fun fetchKey(): String? {
|
Video(
|
||||||
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
|
url = it.url,
|
||||||
|
quality = it.quality,
|
||||||
|
videoUrl = it.videoUrl,
|
||||||
|
audioTracks = it.audioTracks,
|
||||||
|
subtitleTracks = playlistUtils.fixSubtitles(it.subtitleTracks),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeUnicodeEscape(input: String): String {
|
private fun decodeUnicodeEscape(input: String): String {
|
||||||
|
@ -76,16 +56,4 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
it.groupValues[1].toInt(16).toChar().toString()
|
it.groupValues[1].toInt(16).toChar().toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CryptoInfo(
|
|
||||||
@SerialName("ct") val ciphertext: String,
|
|
||||||
@SerialName("s") val salt: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class KeysData(
|
|
||||||
@SerialName("chillx") val keys: List<String>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
class ErrorLoadingException(message: String) : Exception(message)
|
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class WebViewResolver(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val globalHeaders: Headers,
|
||||||
|
) {
|
||||||
|
private val context: Application by injectLazy()
|
||||||
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
|
|
||||||
|
class JsInterface(private val latch: CountDownLatch) {
|
||||||
|
var result: String? = null
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun passPayload(payload: String) {
|
||||||
|
result = payload
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
fun getDecryptedData(embedUrl: String): String? {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
var webView: WebView? = null
|
||||||
|
val jsi = JsInterface(latch)
|
||||||
|
val interfaceName = randomString()
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
val webview = WebView(context)
|
||||||
|
webView = webview
|
||||||
|
|
||||||
|
with(webview.settings) {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
databaseEnabled = true
|
||||||
|
useWideViewPort = false
|
||||||
|
loadWithOverviewMode = false
|
||||||
|
userAgentString = globalHeaders["User-Agent"]
|
||||||
|
}
|
||||||
|
|
||||||
|
webview.addJavascriptInterface(jsi, interfaceName)
|
||||||
|
webview.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldInterceptRequest(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?
|
||||||
|
): WebResourceResponse? {
|
||||||
|
if (request?.url.toString().contains("assets/js/library")) {
|
||||||
|
return patchScript(request!!.url.toString(), interfaceName)
|
||||||
|
?: super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webView?.loadUrl(embedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
webView?.stopLoading()
|
||||||
|
webView?.destroy()
|
||||||
|
webView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsi.result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TIMEOUT_SEC: Long = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomString(length: Int = 10): String {
|
||||||
|
val charPool = ('a'..'z') + ('A'..'Z')
|
||||||
|
return List(length) { charPool.random() }.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patchScript(scriptUrl: String, interfaceName: String): WebResourceResponse? {
|
||||||
|
val scriptBody = client.newCall(GET(scriptUrl)).execute().body.string()
|
||||||
|
|
||||||
|
val oldFunc = randomString()
|
||||||
|
val newBody = buildString {
|
||||||
|
append(
|
||||||
|
"""
|
||||||
|
const $oldFunc = Function;
|
||||||
|
window.Function = function (...args) {
|
||||||
|
if (args.length == 1) {
|
||||||
|
window.$interfaceName.passPayload(args[0]);
|
||||||
|
}
|
||||||
|
return $oldFunc(...args);
|
||||||
|
};
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
append(scriptBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return WebResourceResponse(
|
||||||
|
"application/javascript",
|
||||||
|
"utf-8",
|
||||||
|
200,
|
||||||
|
"ok",
|
||||||
|
mapOf("server" to "cloudflare"),
|
||||||
|
ByteArrayInputStream(newBody.toByteArray()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
package eu.kanade.tachiyomi.lib.filemoonextractor
|
package eu.kanade.tachiyomi.lib.filemoonextractor
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
import dev.datlag.jsunpacker.JsUnpacker
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
@ -13,20 +16,28 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class FilemoonExtractor(private val client: OkHttpClient) {
|
class FilemoonExtractor(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val preferences: SharedPreferences? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
|
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
|
||||||
val httpUrl = url.toHttpUrl()
|
var httpUrl = url.toHttpUrl()
|
||||||
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
||||||
.set("Referer", url)
|
.set("Referer", url)
|
||||||
.set("Origin", "https://${httpUrl.host}")
|
.set("Origin", "https://${httpUrl.host}")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
||||||
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")?.data() ?: run {
|
||||||
|
val iframeUrl = doc.selectFirst("iframe[src]")!!.attr("src")
|
||||||
|
httpUrl = iframeUrl.toHttpUrl()
|
||||||
|
val iframeDoc = client.newCall(GET(iframeUrl, videoHeaders)).execute().asJsoup()
|
||||||
|
iframeDoc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
||||||
|
}
|
||||||
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
||||||
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
||||||
?.substringAfter("{file:\"", "")
|
?.substringAfter("{file:\"", "")
|
||||||
|
@ -50,14 +61,39 @@ class FilemoonExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
val videoList = playlistUtils.extractFromHls(
|
||||||
masterUrl,
|
masterUrl,
|
||||||
subtitleList = subtitleTracks,
|
subtitleList = subtitleTracks,
|
||||||
referer = "https://${httpUrl.host}/",
|
referer = "https://${httpUrl.host}/",
|
||||||
videoNameGen = { "$prefix$it" },
|
videoNameGen = { "$prefix$it" },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val subPref = preferences?.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
||||||
|
return videoList.map {
|
||||||
|
Video(
|
||||||
|
url = it.url,
|
||||||
|
quality = it.quality,
|
||||||
|
videoUrl = it.videoUrl,
|
||||||
|
audioTracks = it.audioTracks,
|
||||||
|
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SubtitleDto(val file: String, val label: String)
|
data class SubtitleDto(val file: String, val label: String)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun addSubtitlePref(screen: PreferenceScreen) {
|
||||||
|
EditTextPreference(screen.context).apply {
|
||||||
|
key = PREF_SUBTITLE_KEY
|
||||||
|
title = "Filemoon subtitle preference"
|
||||||
|
summary = "Leave blank to use all subs"
|
||||||
|
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PREF_SUBTITLE_KEY = "pref_filemoon_sub_lang_key"
|
||||||
|
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
7
lib/savefile-extractor/build.gradle.kts
Normal file
7
lib/savefile-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:playlist-utils"))
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.savefileextractor
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class SavefileExtractor(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val preferences: SharedPreferences,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String = "Savefile - ", headers: Headers? = null): List<Video> {
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
||||||
|
.set("Referer", url)
|
||||||
|
.set("Origin", "https://${httpUrl.host}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
||||||
|
val js = doc.selectFirst("script:containsData(m3u8)")!!.data()
|
||||||
|
val masterUrl = js.takeIf(String::isNotBlank)
|
||||||
|
?.substringAfter("{file:\"", "")
|
||||||
|
?.substringBefore("\"}", "")
|
||||||
|
?.takeIf(String::isNotBlank)
|
||||||
|
?: return emptyList()
|
||||||
|
|
||||||
|
val videoList = playlistUtils.extractFromHls(
|
||||||
|
masterUrl,
|
||||||
|
referer = "https://${httpUrl.host}/",
|
||||||
|
videoNameGen = { "$prefix$it" },
|
||||||
|
)
|
||||||
|
|
||||||
|
val subPref = preferences.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
||||||
|
return videoList.map {
|
||||||
|
Video(
|
||||||
|
url = it.url,
|
||||||
|
quality = it.quality,
|
||||||
|
videoUrl = it.videoUrl,
|
||||||
|
audioTracks = it.audioTracks,
|
||||||
|
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun addSubtitlePref(screen: PreferenceScreen) {
|
||||||
|
EditTextPreference(screen.context).apply {
|
||||||
|
key = PREF_SUBTITLE_KEY
|
||||||
|
title = "Savefile subtitle preference"
|
||||||
|
summary = "Leave blank to use all subs"
|
||||||
|
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PREF_SUBTITLE_KEY = "pref_savefile_sub_lang_key"
|
||||||
|
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
@ -30,16 +31,19 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
script
|
script
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val masterUrl = scriptBody
|
val masterUrl = scriptBody?.let {
|
||||||
?.substringAfter("source", "")
|
M3U8_REGEX.find(it)?.value
|
||||||
?.substringAfter("file:\"", "")
|
}
|
||||||
?.substringBefore("\"", "")
|
|
||||||
?.takeIf(String::isNotBlank)
|
|
||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
|
|
||||||
val subtitleList = extractSubtitles(scriptBody)
|
val subtitleList = extractSubtitles(scriptBody)
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen, subtitleList = subtitleList)
|
return playlistUtils.extractFromHls(
|
||||||
|
playlistUrl = masterUrl,
|
||||||
|
referer = "https://${url.toHttpUrl().host}/",
|
||||||
|
videoNameGen = videoNameGen,
|
||||||
|
subtitleList = playlistUtils.fixSubtitles(subtitleList),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbedUrl(url: String): String {
|
private fun getEmbedUrl(url: String): String {
|
||||||
|
@ -57,7 +61,11 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
.substringAfter("tracks")
|
.substringAfter("tracks")
|
||||||
.substringAfter("[")
|
.substringAfter("[")
|
||||||
.substringBefore("]")
|
.substringBefore("]")
|
||||||
json.decodeFromString<List<TrackDto>>("[$subtitleStr]")
|
val fixedSubtitleStr = FIX_TRACKS_REGEX.replace(subtitleStr) { match ->
|
||||||
|
"\"${match.value}\""
|
||||||
|
}
|
||||||
|
|
||||||
|
json.decodeFromString<List<TrackDto>>("[$fixedSubtitleStr]")
|
||||||
.filter { it.kind.equals("captions", true) }
|
.filter { it.kind.equals("captions", true) }
|
||||||
.map { Track(it.file, it.label ?: "") }
|
.map { Track(it.file, it.label ?: "") }
|
||||||
} catch (e: SerializationException) {
|
} catch (e: SerializationException) {
|
||||||
|
@ -67,4 +75,7 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class TrackDto(val file: String, val kind: String, val label: String? = null)
|
private data class TrackDto(val file: String, val kind: String, val label: String? = null)
|
||||||
|
|
||||||
|
private val M3U8_REGEX = Regex("""https[^"]*m3u8[^"]*""")
|
||||||
|
private val FIX_TRACKS_REGEX = Regex("""(?<!["])(file|kind|label)(?!["])""")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.ChineseAnime'
|
extClass = '.ChineseAnime'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://www.chineseanime.vip'
|
baseUrl = 'https://www.chineseanime.vip'
|
||||||
overrideVersionCode = 13
|
overrideVersionCode = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hikari'
|
extName = 'Hikari'
|
||||||
extClass = '.Hikari'
|
extClass = '.Hikari'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -9,6 +9,7 @@ apply from: "$rootDir/common.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':lib:chillx-extractor'))
|
implementation(project(':lib:chillx-extractor'))
|
||||||
implementation(project(':lib:filemoon-extractor'))
|
implementation(project(':lib:filemoon-extractor'))
|
||||||
|
implementation(project(':lib:savefile-extractor'))
|
||||||
|
implementation(project(':lib:buzzheavier-extractor'))
|
||||||
implementation(project(':lib:streamwish-extractor'))
|
implementation(project(':lib:streamwish-extractor'))
|
||||||
implementation(project(':lib:vidhide-extractor'))
|
|
||||||
}
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.all.hikari
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CatalogResponseDto<T>(
|
||||||
|
val next: String? = null,
|
||||||
|
val results: List<T>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AnimeDto(
|
||||||
|
val uid: String,
|
||||||
|
|
||||||
|
@SerialName("ani_ename")
|
||||||
|
val aniEName: String? = null,
|
||||||
|
@SerialName("ani_name")
|
||||||
|
val aniName: String,
|
||||||
|
@SerialName("ani_poster")
|
||||||
|
val aniPoster: String? = null,
|
||||||
|
@SerialName("ani_synopsis")
|
||||||
|
val aniSynopsis: String? = null,
|
||||||
|
@SerialName("ani_synonyms")
|
||||||
|
val aniSynonyms: String? = null,
|
||||||
|
@SerialName("ani_genre")
|
||||||
|
val aniGenre: String? = null,
|
||||||
|
@SerialName("ani_studio")
|
||||||
|
val aniStudio: String? = null,
|
||||||
|
@SerialName("ani_stats")
|
||||||
|
val aniStats: Int? = null,
|
||||||
|
) {
|
||||||
|
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
|
||||||
|
url = uid
|
||||||
|
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
|
||||||
|
thumbnail_url = aniPoster
|
||||||
|
genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
|
||||||
|
author = aniStudio
|
||||||
|
description = buildString {
|
||||||
|
aniSynopsis?.trim()?.let(::append)
|
||||||
|
append("\n\n")
|
||||||
|
aniSynonyms?.let { append("Synonyms: $it") }
|
||||||
|
}.trim()
|
||||||
|
|
||||||
|
status = when (aniStats) {
|
||||||
|
1 -> SAnime.ONGOING
|
||||||
|
2 -> SAnime.COMPLETED
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LatestEpisodeDto(
|
||||||
|
val uid: Int,
|
||||||
|
val title: String,
|
||||||
|
@SerialName("title_en")
|
||||||
|
val titleEn: String? = null,
|
||||||
|
val imageUrl: String,
|
||||||
|
) {
|
||||||
|
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
|
||||||
|
url = uid.toString()
|
||||||
|
title = if (preferEnglish) titleEn?.takeUnless(String::isBlank) ?: this@LatestEpisodeDto.title else this@LatestEpisodeDto.title
|
||||||
|
thumbnail_url = imageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EpisodeDto(
|
||||||
|
@SerialName("ep_id_name")
|
||||||
|
val epId: String,
|
||||||
|
@SerialName("ep_name")
|
||||||
|
val epName: String? = null,
|
||||||
|
) {
|
||||||
|
fun toSEpisode(uid: String): SEpisode = SEpisode.create().apply {
|
||||||
|
url = "$uid-$epId"
|
||||||
|
name = epName?.let { "Ep. $epId - $it" } ?: "Episode $epId"
|
||||||
|
episode_number = epId.toFloatOrNull() ?: 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmbedDto(
|
||||||
|
@SerialName("embed_type")
|
||||||
|
val embedType: String,
|
||||||
|
@SerialName("embed_name")
|
||||||
|
val embedName: String,
|
||||||
|
@SerialName("embed_frame")
|
||||||
|
val embedFrame: String,
|
||||||
|
)
|
|
@ -5,7 +5,7 @@ import okhttp3.HttpUrl
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
interface UriFilter {
|
interface UriFilter {
|
||||||
fun addToUri(url: HttpUrl.Builder)
|
fun addToUri(builder: HttpUrl.Builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class UriPartFilter(
|
sealed class UriPartFilter(
|
||||||
|
@ -20,7 +20,10 @@ sealed class UriPartFilter(
|
||||||
),
|
),
|
||||||
UriFilter {
|
UriFilter {
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
builder.addQueryParameter(param, vals[state].second)
|
val value = vals[state].second
|
||||||
|
if (value.isNotEmpty()) {
|
||||||
|
builder.addQueryParameter(param, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,13 +36,15 @@ sealed class UriMultiSelectFilter(
|
||||||
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
val checked = state.filter { it.state }
|
val checked = state.filter { it.state }
|
||||||
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
if (checked.isNotEmpty()) {
|
||||||
|
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TypeFilter : UriPartFilter(
|
class TypeFilter : UriPartFilter(
|
||||||
"Type",
|
"Type",
|
||||||
"type",
|
"ani_type",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("TV", "1"),
|
Pair("TV", "1"),
|
||||||
|
@ -50,165 +55,53 @@ class TypeFilter : UriPartFilter(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class CountryFilter : UriPartFilter(
|
|
||||||
"Country",
|
|
||||||
"country",
|
|
||||||
arrayOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Japanese", "1"),
|
|
||||||
Pair("Chinese", "2"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class StatusFilter : UriPartFilter(
|
class StatusFilter : UriPartFilter(
|
||||||
"Status",
|
"Status",
|
||||||
"stats",
|
"ani_stats",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("Currently Airing", "1"),
|
Pair("Ongoing", "1"),
|
||||||
Pair("Finished Airing", "2"),
|
Pair("Completed", "2"),
|
||||||
Pair("Not yet Aired", "3"),
|
Pair("Upcoming", "3"),
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class RatingFilter : UriPartFilter(
|
|
||||||
"Rating",
|
|
||||||
"rate",
|
|
||||||
arrayOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("G", "1"),
|
|
||||||
Pair("PG", "2"),
|
|
||||||
Pair("PG-13", "3"),
|
|
||||||
Pair("R-17+", "4"),
|
|
||||||
Pair("R+", "5"),
|
|
||||||
Pair("Rx", "6"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class SourceFilter : UriPartFilter(
|
|
||||||
"Source",
|
|
||||||
"source",
|
|
||||||
arrayOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("LightNovel", "1"),
|
|
||||||
Pair("Manga", "2"),
|
|
||||||
Pair("Original", "3"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class SeasonFilter : UriPartFilter(
|
class SeasonFilter : UriPartFilter(
|
||||||
"Season",
|
"Season",
|
||||||
"season",
|
"ani_release_season",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("Spring", "1"),
|
Pair("Winter", "1"),
|
||||||
Pair("Summer", "2"),
|
Pair("Spring", "2"),
|
||||||
Pair("Fall", "3"),
|
Pair("Summer", "3"),
|
||||||
Pair("Winter", "4"),
|
Pair("Fall", "4"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class LanguageFilter : UriPartFilter(
|
class YearFilter : UriPartFilter(
|
||||||
"Language",
|
"Release Year",
|
||||||
"language",
|
"ani_release",
|
||||||
arrayOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Raw", "1"),
|
|
||||||
Pair("Sub", "2"),
|
|
||||||
Pair("Dub", "3"),
|
|
||||||
Pair("Turk", "4"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class SortFilter : UriPartFilter(
|
|
||||||
"Sort",
|
|
||||||
"sort",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Default", "default"),
|
|
||||||
Pair("Recently Added", "recently_added"),
|
|
||||||
Pair("Recently Updated", "recently_updated"),
|
|
||||||
Pair("Score", "score"),
|
|
||||||
Pair("Name A-Z", "name_az"),
|
|
||||||
Pair("Released Date", "released_date"),
|
|
||||||
Pair("Most Watched", "most_watched"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class YearFilter(name: String, param: String) : UriPartFilter(
|
|
||||||
name,
|
|
||||||
param,
|
|
||||||
YEARS,
|
YEARS,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private val NEXT_YEAR by lazy {
|
private val CURRENT_YEAR by lazy {
|
||||||
Calendar.getInstance()[Calendar.YEAR] + 1
|
Calendar.getInstance()[Calendar.YEAR]
|
||||||
}
|
}
|
||||||
|
|
||||||
private val YEARS = Array(NEXT_YEAR - 1917) { year ->
|
private val YEARS = buildList {
|
||||||
if (year == 0) {
|
add(Pair("Any", ""))
|
||||||
Pair("Any", "")
|
addAll(
|
||||||
} else {
|
(1990..CURRENT_YEAR).map {
|
||||||
(NEXT_YEAR - year).toString().let { Pair(it, it) }
|
Pair(it.toString(), it.toString())
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
}
|
}.toTypedArray()
|
||||||
}
|
|
||||||
|
|
||||||
class MonthFilter(name: String, param: String) : UriPartFilter(
|
|
||||||
name,
|
|
||||||
param,
|
|
||||||
MONTHS,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
private val MONTHS = Array(13) { months ->
|
|
||||||
if (months == 0) {
|
|
||||||
Pair("Any", "")
|
|
||||||
} else {
|
|
||||||
val monthStr = "%02d".format(months)
|
|
||||||
Pair(monthStr, monthStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DayFilter(name: String, param: String) : UriPartFilter(
|
|
||||||
name,
|
|
||||||
param,
|
|
||||||
DAYS,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
private val DAYS = Array(32) { day ->
|
|
||||||
if (day == 0) {
|
|
||||||
Pair("Any", "")
|
|
||||||
} else {
|
|
||||||
val dayStr = "%02d".format(day)
|
|
||||||
Pair(dayStr, dayStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AiringDateFilter(
|
|
||||||
private val values: List<UriPartFilter> = PARTS,
|
|
||||||
) : AnimeFilter.Group<UriPartFilter>("Airing Date", values), UriFilter {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
values.forEach {
|
|
||||||
it.addToUri(builder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val PARTS = listOf(
|
|
||||||
YearFilter("Year", "aired_year"),
|
|
||||||
MonthFilter("Month", "aired_month"),
|
|
||||||
DayFilter("Day", "aired_day"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreFilter : UriMultiSelectFilter(
|
class GenreFilter : UriMultiSelectFilter(
|
||||||
"Genre",
|
"Genre",
|
||||||
"genres",
|
"ani_genre",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("Action", "Action"),
|
Pair("Action", "Action"),
|
||||||
Pair("Adventure", "Adventure"),
|
Pair("Adventure", "Adventure"),
|
||||||
|
@ -233,7 +126,7 @@ class GenreFilter : UriMultiSelectFilter(
|
||||||
Pair("Music", "Music"),
|
Pair("Music", "Music"),
|
||||||
Pair("Mystery", "Mystery"),
|
Pair("Mystery", "Mystery"),
|
||||||
Pair("Parody", "Parody"),
|
Pair("Parody", "Parody"),
|
||||||
Pair("Police", "Police"),
|
Pair("Policy", "Policy"),
|
||||||
Pair("Psychological", "Psychological"),
|
Pair("Psychological", "Psychological"),
|
||||||
Pair("Romance", "Romance"),
|
Pair("Romance", "Romance"),
|
||||||
Pair("Samurai", "Samurai"),
|
Pair("Samurai", "Samurai"),
|
||||||
|
@ -253,3 +146,12 @@ class GenreFilter : UriMultiSelectFilter(
|
||||||
Pair("Vampire", "Vampire"),
|
Pair("Vampire", "Vampire"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class LanguageFilter : UriPartFilter(
|
||||||
|
"Language",
|
||||||
|
"ani_genre",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", ""),
|
||||||
|
Pair("Portuguese", "Portuguese"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
@ -1,42 +1,43 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.all.hikari
|
package eu.kanade.tachiyomi.animeextension.all.hikari
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
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.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.buzzheavierextractor.BuzzheavierExtractor
|
||||||
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
|
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
|
||||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.savefileextractor.SavefileExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
override val name = "Hikari"
|
override val name = "Hikari"
|
||||||
|
|
||||||
override val baseUrl = "https://watch.hikaritv.xyz"
|
private val proxyUrl = "https://hikari.gg/hiki-proxy/extract/"
|
||||||
|
private val apiUrl = "https://api.hikari.gg/api"
|
||||||
|
override val baseUrl = "https://hikari.gg"
|
||||||
|
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
|
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder().apply {
|
override fun headersBuilder() = super.headersBuilder().apply {
|
||||||
|
@ -50,75 +51,40 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
override fun popularAnimeRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList())
|
||||||
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=score&genres=&page=$page"
|
|
||||||
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
|
||||||
val parsed = response.parseAs<HtmlResponseDto>()
|
|
||||||
|
|
||||||
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < parsed.page!!.totalPages
|
|
||||||
val animeList = parsed.toHtml(baseUrl).select(popularAnimeSelector())
|
|
||||||
.map(::popularAnimeFromElement)
|
|
||||||
|
|
||||||
return AnimesPage(animeList, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeSelector(): String = ".flw-item"
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a[data-id]")!!.attr("abs:href"))
|
|
||||||
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
|
|
||||||
title = element.selectFirst(".film-name")!!.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=recently_updated&genres=&page=$page"
|
val url = "$apiUrl/episode/new/".toHttpUrl().newBuilder().apply {
|
||||||
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
|
addQueryParameter("limit", "100")
|
||||||
|
addQueryParameter("language", "EN")
|
||||||
|
}.build()
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage =
|
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||||
popularAnimeParse(response)
|
val data = response.parseAs<CatalogResponseDto<LatestEpisodeDto>>()
|
||||||
|
val preferEnglish = preferences.getTitleLang
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String =
|
val animeList = data.results.distinctBy { it.uid }.map { it.toSAnime(preferEnglish) }
|
||||||
throw UnsupportedOperationException()
|
return AnimesPage(animeList, false)
|
||||||
|
}
|
||||||
override fun latestUpdatesFromElement(element: Element): SAnime =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
val url = "$apiUrl/anime/".toHttpUrl().newBuilder().apply {
|
||||||
if (query.isNotEmpty()) {
|
addQueryParameter("sort", "created_at")
|
||||||
addPathSegment("search")
|
addQueryParameter("order", "asc")
|
||||||
addQueryParameter("keyword", query)
|
addQueryParameter("page", page.toString())
|
||||||
addQueryParameter("page", page.toString())
|
filters.filterIsInstance<UriFilter>().forEach {
|
||||||
} else {
|
it.addToUri(this)
|
||||||
addPathSegment("ajax")
|
|
||||||
addPathSegment("getfilter")
|
|
||||||
filters.filterIsInstance<UriFilter>().forEach {
|
|
||||||
it.addToUri(this)
|
|
||||||
}
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
}
|
}
|
||||||
}.build()
|
|
||||||
|
|
||||||
val headers = headersBuilder().apply {
|
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
set("Referer", url.toString().substringBeforeLast("&page"))
|
addQueryParameter("search", query)
|
||||||
} else {
|
|
||||||
set("Referer", "$baseUrl/filter")
|
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
@ -126,280 +92,148 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
return if (response.request.url.encodedPath.startsWith("/search")) {
|
val data = response.parseAs<CatalogResponseDto<AnimeDto>>()
|
||||||
super.searchAnimeParse(response)
|
val preferEnglish = preferences.getTitleLang
|
||||||
} else {
|
|
||||||
popularAnimeParse(response)
|
val animeList = data.results.map { it.toSAnime(preferEnglish) }
|
||||||
}
|
val hasNextPage = data.next != null
|
||||||
|
|
||||||
|
return AnimesPage(animeList, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
|
||||||
|
|
||||||
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
|
|
||||||
|
|
||||||
// ============================== Filters ===============================
|
// ============================== Filters ===============================
|
||||||
|
|
||||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||||
AnimeFilter.Header("Note: text search ignores filters"),
|
|
||||||
AnimeFilter.Separator(),
|
|
||||||
TypeFilter(),
|
TypeFilter(),
|
||||||
CountryFilter(),
|
|
||||||
StatusFilter(),
|
StatusFilter(),
|
||||||
RatingFilter(),
|
|
||||||
SourceFilter(),
|
|
||||||
SeasonFilter(),
|
SeasonFilter(),
|
||||||
LanguageFilter(),
|
YearFilter(),
|
||||||
SortFilter(),
|
|
||||||
AiringDateFilter(),
|
|
||||||
GenreFilter(),
|
GenreFilter(),
|
||||||
|
LanguageFilter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
override fun getAnimeUrl(anime: SAnime): String {
|
||||||
with(document.selectFirst("#ani_detail")!!) {
|
return "$baseUrl/info/${anime.url}"
|
||||||
title = selectFirst(".film-name")!!.text()
|
|
||||||
thumbnail_url = selectFirst(".film-poster img")!!.attr("abs:src")
|
|
||||||
description = selectFirst(".film-description > .text")?.text()
|
|
||||||
genre = select(".item-list:has(span:contains(Genres)) > a").joinToString { it.text() }
|
|
||||||
author = select(".item:has(span:contains(Studio)) > a").joinToString { it.text() }
|
|
||||||
status = selectFirst(".item:has(span:contains(Status)) > .name").parseStatus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||||
"currently airing" -> SAnime.ONGOING
|
return GET("$apiUrl/anime/uid/${anime.url}/", headers)
|
||||||
"finished" -> SAnime.COMPLETED
|
}
|
||||||
else -> SAnime.UNKNOWN
|
|
||||||
|
override fun animeDetailsParse(response: Response): SAnime {
|
||||||
|
return response.parseAs<AnimeDto>().toSAnime(preferences.getTitleLang)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
private val specialCharRegex = Regex("""(?![\-_])\W{1,}""")
|
|
||||||
|
|
||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
val animeId = anime.url.split("/")[2]
|
return GET("$apiUrl/episode/uid/${anime.url}/", headers)
|
||||||
|
|
||||||
val sanitized = anime.title.replace(" ", "_")
|
|
||||||
|
|
||||||
val refererUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("watch")
|
|
||||||
addQueryParameter("anime", specialCharRegex.replace(sanitized, ""))
|
|
||||||
addQueryParameter("uid", animeId)
|
|
||||||
addQueryParameter("eps", "1")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val headers = headersBuilder()
|
|
||||||
.set("Referer", refererUrl.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET("$baseUrl/ajax/episodelist/$animeId", headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
return response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
val guid = response.request.url.pathSegments[3]
|
||||||
.select(episodeListSelector())
|
|
||||||
.map(::episodeFromElement)
|
|
||||||
.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListSelector() = "a[class~=ep-item]"
|
return response.parseAs<List<EpisodeDto>>().map { it.toSEpisode(guid) }.reversed()
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode {
|
|
||||||
val epText = element.selectFirst(".ssli-order")?.text()?.trim()
|
|
||||||
?: element.attr("data-number").trim()
|
|
||||||
val ep = epText.toFloatOrNull() ?: 0F
|
|
||||||
|
|
||||||
val nameText = element.selectFirst(".ep-name")?.text()?.trim()
|
|
||||||
?: element.attr("title").replace("Episode-", "Ep. ") ?: ""
|
|
||||||
|
|
||||||
return SEpisode.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("abs:href"))
|
|
||||||
episode_number = ep
|
|
||||||
name = "Ep. $ep - $nameText"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
private val filemoonExtractor by lazy { FilemoonExtractor(client, preferences) }
|
||||||
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
|
private val savefileExtractor by lazy { SavefileExtractor(client, preferences) }
|
||||||
|
private val buzzheavierExtractor by lazy { BuzzheavierExtractor(client, headers) }
|
||||||
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
|
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
|
||||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||||
private val embedRegex = Regex("""getEmbed\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\d+)'""")
|
|
||||||
|
private fun getEmbedTypeName(type: String): String {
|
||||||
|
return when (type) {
|
||||||
|
"2" -> "[SUB] "
|
||||||
|
"3" -> "[DUB] "
|
||||||
|
"4" -> "[MULTI AUDIO] "
|
||||||
|
"8" -> "[HARD-SUB] "
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
val url = (baseUrl + episode.url).toHttpUrl()
|
val (guid, epId) = episode.url.split("-")
|
||||||
val animeId = url.queryParameter("uid")!!
|
return GET("$apiUrl/embed/$guid/$epId/", headers)
|
||||||
val episodeNum = url.queryParameter("eps")!!
|
|
||||||
|
|
||||||
val headers = headersBuilder()
|
|
||||||
.set("Referer", baseUrl + episode.url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET("$baseUrl/ajax/embedserver/$animeId/$episodeNum", headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val html = response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
val data = response.parseAs<List<EmbedDto>>()
|
||||||
Log.d("Hikari", html.toString())
|
|
||||||
|
|
||||||
val headers = headersBuilder()
|
return data.parallelCatchingFlatMapBlocking { embed ->
|
||||||
.set("Referer", response.request.url.toString())
|
val prefix = getEmbedTypeName(embed.embedType) + embed.embedName
|
||||||
.build()
|
val embedName = embed.embedName.lowercase()
|
||||||
|
|
||||||
val subEmbedUrls = html.select(".servers-sub div.item.server-item").flatMap { item ->
|
when (embedName) {
|
||||||
val name = item.text().trim() + " (Sub)"
|
"streamwish" -> streamwishExtractor.videosFromUrl(embed.embedFrame, videoNameGen = { "$prefix - $it" })
|
||||||
val onClick = item.selectFirst("a")?.attr("onclick")
|
"filemoon" -> filemoonExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
||||||
if (onClick == null) {
|
"sv" -> savefileExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
||||||
Log.e("Hikari", "onClick attribute is null for item: $item")
|
"playerx" -> chillxExtractor.videoFromUrl(embed.embedFrame, "$prefix - ")
|
||||||
return@flatMap emptyList<Pair<String, String>>()
|
"hiki" -> buzzheavierExtractor.videosFromUrl(embed.embedFrame, "$prefix - ", proxyUrl)
|
||||||
}
|
else -> emptyList()
|
||||||
|
|
||||||
val match = embedRegex.find(onClick)?.groupValues
|
|
||||||
if (match == null) {
|
|
||||||
Log.e("Hikari", "No match found for onClick: $onClick")
|
|
||||||
return@flatMap emptyList<Pair<String, String>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
|
|
||||||
val iframeList = client.newCall(
|
|
||||||
GET(url, headers),
|
|
||||||
).execute().parseAs<List<String>>()
|
|
||||||
|
|
||||||
iframeList.map {
|
|
||||||
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
|
|
||||||
if (iframeSrc == null) {
|
|
||||||
Log.e("Hikari", "iframe src is null for URL: $url")
|
|
||||||
return@map Pair("", "")
|
|
||||||
}
|
|
||||||
Pair(iframeSrc, name)
|
|
||||||
}.filter { it.first.isNotEmpty() }
|
|
||||||
}
|
|
||||||
val dubEmbedUrls = html.select(".servers-dub div.item.server-item").flatMap { item ->
|
|
||||||
val name = item.text().trim() + " (Dub)"
|
|
||||||
val onClick = item.selectFirst("a")?.attr("onclick")
|
|
||||||
if (onClick == null) {
|
|
||||||
Log.e("Hikari", "onClick attribute is null for item: $item")
|
|
||||||
return@flatMap emptyList<Pair<String, String>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val match = embedRegex.find(onClick)?.groupValues
|
|
||||||
if (match == null) {
|
|
||||||
Log.e("Hikari", "No match found for onClick: $onClick")
|
|
||||||
return@flatMap emptyList<Pair<String, String>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
|
|
||||||
val iframeList = client.newCall(
|
|
||||||
GET(url, headers),
|
|
||||||
).execute().parseAs<List<String>>()
|
|
||||||
|
|
||||||
iframeList.map {
|
|
||||||
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
|
|
||||||
if (iframeSrc == null) {
|
|
||||||
Log.e("Hikari", "iframe src is null for URL: $url")
|
|
||||||
return@map Pair("", "")
|
|
||||||
}
|
|
||||||
Pair(iframeSrc, name)
|
|
||||||
}.filter { it.first.isNotEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
val sdEmbedUrls = html.select(".servers-sub.\\&.dub div.item.server-item").flatMap { item ->
|
|
||||||
val name = item.text().trim() + " (Sub + Dub)"
|
|
||||||
val onClick = item.selectFirst("a")?.attr("onclick")
|
|
||||||
if (onClick == null) {
|
|
||||||
Log.e("Hikari", "onClick attribute is null for item: $item")
|
|
||||||
return@flatMap emptyList<Pair<String, String>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val match = embedRegex.find(onClick)?.groupValues
|
|
||||||
if (match == null) {
|
|
||||||
Log.e("Hikari", "No match found for onClick: $onClick")
|
|
||||||
return@flatMap emptyList<Pair<String, String>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
|
|
||||||
val iframeList = client.newCall(
|
|
||||||
GET(url, headers),
|
|
||||||
).execute().parseAs<List<String>>()
|
|
||||||
|
|
||||||
iframeList.map {
|
|
||||||
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
|
|
||||||
if (iframeSrc == null) {
|
|
||||||
Log.e("Hikari", "iframe src is null for URL: $url")
|
|
||||||
return@map Pair("", "")
|
|
||||||
}
|
|
||||||
Pair(iframeSrc, name)
|
|
||||||
}.filter { it.first.isNotEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
return sdEmbedUrls.parallelCatchingFlatMapBlocking {
|
|
||||||
getVideosFromEmbed(it.first, it.second)
|
|
||||||
}.ifEmpty {
|
|
||||||
(subEmbedUrls + dubEmbedUrls).parallelCatchingFlatMapBlocking {
|
|
||||||
getVideosFromEmbed(it.first, it.second)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
|
|
||||||
name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl, videoNameGen = { s -> "$name - $s" })
|
|
||||||
embedUrl.contains("filemoon", true) -> filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
|
|
||||||
name.contains("streamwish", true) -> streamwishExtractor.videosFromUrl(embedUrl, prefix = "$name - ")
|
|
||||||
else -> chillxExtractor.videoFromUrl(embedUrl, referer = baseUrl, prefix = "$name - ")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"
|
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
override fun List<Video>.sort(): List<Video> {
|
||||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
|
val type = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!
|
||||||
|
val hoster = preferences.getString(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
||||||
|
|
||||||
return sortedWith(
|
return sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
|
{ it.quality.startsWith(type) },
|
||||||
{ it.quality.contains(quality) },
|
{ it.quality.contains(quality) },
|
||||||
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||||
|
{ it.quality.contains(hoster, true) },
|
||||||
),
|
),
|
||||||
).reversed()
|
).reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun videoFromElement(element: Element): Video =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class HtmlResponseDto(
|
|
||||||
val html: String,
|
|
||||||
val page: PageDto? = null,
|
|
||||||
) {
|
|
||||||
fun toHtml(baseUrl: String): Document = Jsoup.parseBodyFragment(html, baseUrl)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class PageDto(
|
|
||||||
val totalPages: Int,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val QUALITY_REGEX = Regex("""(\d+)p""")
|
private val QUALITY_REGEX = Regex("""(\d+)p""")
|
||||||
|
|
||||||
|
private const val PREF_ENGLISH_TITLE_KEY = "preferred_title_lang"
|
||||||
|
private const val PREF_ENGLISH_TITLE_DEFAULT = true
|
||||||
|
|
||||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||||
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
|
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||||
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
|
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
|
||||||
"${it}p"
|
"${it}p"
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
|
private val TYPE_LIST = arrayOf("[SUB] ", "[DUB] ", "[MULTI AUDIO] ", "[HARD-SUB] ")
|
||||||
|
private const val PREF_TYPE_KEY = "pref_type"
|
||||||
|
private const val PREF_TYPE_DEFAULT = ""
|
||||||
|
private val PREF_TYPE_VALUES = arrayOf("") + TYPE_LIST
|
||||||
|
private val PREF_TYPE_ENTRIES = arrayOf("Any") + TYPE_LIST
|
||||||
|
|
||||||
|
private val HOSTER_LIST = arrayOf("Streamwish", "Filemoon", "SV", "PlayerX", "Hiki")
|
||||||
|
private const val PREF_HOSTER_KEY = "pref_hoster"
|
||||||
|
private const val PREF_HOSTER_DEFAULT = ""
|
||||||
|
private val PREF_HOSTER_VALUES = arrayOf("") + HOSTER_LIST
|
||||||
|
private val PREF_HOSTER_ENTRIES = arrayOf("Any") + HOSTER_LIST
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
// ============================== Settings ==============================
|
||||||
|
|
||||||
|
private val SharedPreferences.getTitleLang
|
||||||
|
get() = getBoolean(PREF_ENGLISH_TITLE_KEY, PREF_ENGLISH_TITLE_DEFAULT)
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = PREF_ENGLISH_TITLE_KEY
|
||||||
|
title = "Prefer english titles"
|
||||||
|
setDefaultValue(PREF_ENGLISH_TITLE_DEFAULT)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_QUALITY_KEY
|
key = PREF_QUALITY_KEY
|
||||||
title = "Preferred quality"
|
title = "Preferred quality"
|
||||||
|
@ -407,13 +241,27 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
entryValues = PREF_QUALITY_VALUES
|
entryValues = PREF_QUALITY_VALUES
|
||||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||||
summary = "%s"
|
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)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_TYPE_KEY
|
||||||
|
title = "Preferred type"
|
||||||
|
entries = PREF_TYPE_ENTRIES
|
||||||
|
entryValues = PREF_TYPE_VALUES
|
||||||
|
setDefaultValue(PREF_TYPE_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_HOSTER_KEY
|
||||||
|
title = "Preferred hoster"
|
||||||
|
entries = PREF_HOSTER_ENTRIES
|
||||||
|
entryValues = PREF_HOSTER_VALUES
|
||||||
|
setDefaultValue(PREF_HOSTER_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
FilemoonExtractor.addSubtitlePref(screen)
|
||||||
|
SavefileExtractor.addSubtitlePref(screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'JavGG'
|
extName = 'JavGG'
|
||||||
extClass = '.Javgg'
|
extClass = '.Javgg'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Jav Guru'
|
extName = 'Jav Guru'
|
||||||
extClass = '.JavGuru'
|
extClass = '.JavGuru'
|
||||||
extVersionCode = 26
|
extVersionCode = 27
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.LMAnime'
|
extClass = '.LMAnime'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://lmanime.com'
|
baseUrl = 'https://lmanime.com'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'SupJav'
|
extName = 'SupJav'
|
||||||
extClass = '.SupJavFactory'
|
extClass = '.SupJavFactory'
|
||||||
extVersionCode = 14
|
extVersionCode = 15
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime4up'
|
extName = 'Anime4up'
|
||||||
extClass = '.Anime4Up'
|
extClass = '.Anime4Up'
|
||||||
extVersionCode = 62
|
extVersionCode = 63
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animerco'
|
extName = 'Animerco'
|
||||||
extClass = '.Animerco'
|
extClass = '.Animerco'
|
||||||
extVersionCode = 41
|
extVersionCode = 42
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Arab Seed'
|
extName = 'Arab Seed'
|
||||||
extClass = '.ArabSeed'
|
extClass = '.ArabSeed'
|
||||||
extVersionCode = 17
|
extVersionCode = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'asia2tv'
|
extName = 'asia2tv'
|
||||||
extClass = '.Asia2TV'
|
extClass = '.Asia2TV'
|
||||||
extVersionCode = 22
|
extVersionCode = 23
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Egy Dead'
|
extName = 'Egy Dead'
|
||||||
extClass = '.EgyDead'
|
extClass = '.EgyDead'
|
||||||
extVersionCode = 17
|
extVersionCode = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime-Base'
|
extName = 'Anime-Base'
|
||||||
extClass = '.AnimeBase'
|
extClass = '.AnimeBase'
|
||||||
extVersionCode = 31
|
extVersionCode = 32
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeToast'
|
extName = 'AnimeToast'
|
||||||
extClass = '.AnimeToast'
|
extClass = '.AnimeToast'
|
||||||
extVersionCode = 21
|
extVersionCode = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Cinemathek'
|
extClass = '.Cinemathek'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://cinemathek.net'
|
baseUrl = 'https://cinemathek.net'
|
||||||
overrideVersionCode = 24
|
overrideVersionCode = 25
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Einfach'
|
extName = 'Einfach'
|
||||||
extClass = '.Einfach'
|
extClass = '.Einfach'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Kool'
|
extName = 'Kool'
|
||||||
extClass = '.Kool'
|
extClass = '.Kool'
|
||||||
extVersionCode = 13
|
extVersionCode = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Moflix-Stream'
|
extName = 'Moflix-Stream'
|
||||||
extClass = '.MoflixStream'
|
extClass = '.MoflixStream'
|
||||||
extVersionCode = 15
|
extVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AllAnime'
|
extName = 'AllAnime'
|
||||||
extClass = '.AllAnime'
|
extClass = '.AllAnime'
|
||||||
extVersionCode = 35
|
extVersionCode = 36
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AllAnimeChi'
|
extName = 'AllAnimeChi'
|
||||||
extClass = '.AllAnimeChi'
|
extClass = '.AllAnimeChi'
|
||||||
extVersionCode = 12
|
extVersionCode = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.AnimeKhor'
|
extClass = '.AnimeKhor'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animekhor.org'
|
baseUrl = 'https://animekhor.org'
|
||||||
overrideVersionCode = 7
|
overrideVersionCode = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Animenosub'
|
extClass = '.Animenosub'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animenosub.com'
|
baseUrl = 'https://animenosub.com'
|
||||||
overrideVersionCode = 8
|
overrideVersionCode = 9
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeTake'
|
extName = 'AnimeTake'
|
||||||
extClass = '.AnimeTake'
|
extClass = '.AnimeTake'
|
||||||
extVersionCode = 7
|
extVersionCode = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AsiaFlix'
|
extName = 'AsiaFlix'
|
||||||
extClass = '.AsiaFlix'
|
extClass = '.AsiaFlix'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.LuciferDonghua'
|
extClass = '.LuciferDonghua'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://luciferdonghua.in'
|
baseUrl = 'https://luciferdonghua.in'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Tokuzilla'
|
extName = 'Tokuzilla'
|
||||||
extClass = '.Tokuzilla'
|
extClass = '.Tokuzilla'
|
||||||
extVersionCode = 21
|
extVersionCode = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeBum'
|
extName = 'AnimeBum'
|
||||||
extClass = '.AnimeBum'
|
extClass = '.AnimeBum'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animefenix'
|
extName = 'Animefenix'
|
||||||
extClass = '.Animefenix'
|
extClass = '.Animefenix'
|
||||||
extVersionCode = 57
|
extVersionCode = 58
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeFLV'
|
extName = 'AnimeFLV'
|
||||||
extClass = '.AnimeFlv'
|
extClass = '.AnimeFlv'
|
||||||
extVersionCode = 64
|
extVersionCode = 65
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeID'
|
extName = 'AnimeID'
|
||||||
extClass = '.AnimeID'
|
extClass = '.AnimeID'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animejl'
|
extName = 'Animejl'
|
||||||
extClass = '.Animejl'
|
extClass = '.Animejl'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeLatinoHD'
|
extName = 'AnimeLatinoHD'
|
||||||
extClass = '.AnimeLatinoHD'
|
extClass = '.AnimeLatinoHD'
|
||||||
extVersionCode = 39
|
extVersionCode = 40
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeMovil'
|
extName = 'AnimeMovil'
|
||||||
extClass = '.AnimeMovil'
|
extClass = '.AnimeMovil'
|
||||||
extVersionCode = 29
|
extVersionCode = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Animenix'
|
extClass = '.Animenix'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://animenix.com'
|
baseUrl = 'https://animenix.com'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.AnimeOnlineNinja'
|
extClass = '.AnimeOnlineNinja'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://ww3.animeonline.ninja'
|
baseUrl = 'https://ww3.animeonline.ninja'
|
||||||
overrideVersionCode = 44
|
overrideVersionCode = 45
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.AnimeYTES'
|
extClass = '.AnimeYTES'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animeyt.pro'
|
baseUrl = 'https://animeyt.pro'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AsiaLiveAction'
|
extName = 'AsiaLiveAction'
|
||||||
extClass = '.AsiaLiveAction'
|
extClass = '.AsiaLiveAction'
|
||||||
extVersionCode = 36
|
extVersionCode = 37
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Cine24h'
|
extName = 'Cine24h'
|
||||||
extClass = '.Cine24h'
|
extClass = '.Cine24h'
|
||||||
extVersionCode = 11
|
extVersionCode = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'CineCalidad'
|
extName = 'CineCalidad'
|
||||||
extClass = '.CineCalidad'
|
extClass = '.CineCalidad'
|
||||||
extVersionCode = 17
|
extVersionCode = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Cineplus123'
|
extClass = '.Cineplus123'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://cineplus123.org'
|
baseUrl = 'https://cineplus123.org'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Cuevana'
|
extName = 'Cuevana'
|
||||||
extClass = '.CuevanaFactory'
|
extClass = '.CuevanaFactory'
|
||||||
extVersionCode = 46
|
extVersionCode = 47
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.DeTodoPeliculas'
|
extClass = '.DeTodoPeliculas'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://detodopeliculas.nu'
|
baseUrl = 'https://detodopeliculas.nu'
|
||||||
overrideVersionCode = 3
|
overrideVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Doramasflix'
|
extName = 'Doramasflix'
|
||||||
extClass = '.Doramasflix'
|
extClass = '.Doramasflix'
|
||||||
extVersionCode = 33
|
extVersionCode = 34
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Doramasyt'
|
extName = 'Doramasyt'
|
||||||
extClass = '.Doramasyt'
|
extClass = '.Doramasyt'
|
||||||
extVersionCode = 20
|
extVersionCode = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'EstrenosDoramas'
|
extName = 'EstrenosDoramas'
|
||||||
extClass = '.EstrenosDoramas'
|
extClass = '.EstrenosDoramas'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.FlixLatam'
|
extClass = '.FlixLatam'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://flixlatam.com'
|
baseUrl = 'https://flixlatam.com'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Gnula'
|
extName = 'Gnula'
|
||||||
extClass = '.Gnula'
|
extClass = '.Gnula'
|
||||||
extVersionCode = 31
|
extVersionCode = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hackstore'
|
extName = 'Hackstore'
|
||||||
extClass = '.Hackstore'
|
extClass = '.Hackstore'
|
||||||
extVersionCode = 25
|
extVersionCode = 26
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'HentaiLA'
|
extName = 'HentaiLA'
|
||||||
extClass = '.Hentaila'
|
extClass = '.Hentaila'
|
||||||
extVersionCode = 34
|
extVersionCode = 35
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'HentaiTk'
|
extName = 'HentaiTk'
|
||||||
extClass = '.Hentaitk'
|
extClass = '.Hentaitk'
|
||||||
extVersionCode = 13
|
extVersionCode = 14
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'HomeCine'
|
extName = 'HomeCine'
|
||||||
extClass = '.HomeCine'
|
extClass = '.HomeCine'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Jkanime'
|
extName = 'Jkanime'
|
||||||
extClass = '.Jkanime'
|
extClass = '.Jkanime'
|
||||||
extVersionCode = 35
|
extVersionCode = 36
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Katanime'
|
extName = 'Katanime'
|
||||||
extClass = '.Katanime'
|
extClass = '.Katanime'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'LACartoons'
|
extName = 'LACartoons'
|
||||||
extClass = '.Lacartoons'
|
extClass = '.Lacartoons'
|
||||||
extVersionCode = 11
|
extVersionCode = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'LegionAnime'
|
extName = 'LegionAnime'
|
||||||
extClass = '.LegionAnime'
|
extClass = '.LegionAnime'
|
||||||
extVersionCode = 35
|
extVersionCode = 36
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MetroSeries'
|
extName = 'MetroSeries'
|
||||||
extClass = '.MetroSeries'
|
extClass = '.MetroSeries'
|
||||||
extVersionCode = 18
|
extVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MhdFlix'
|
extName = 'MhdFlix'
|
||||||
extClass = '.MhdFlix'
|
extClass = '.MhdFlix'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MonosChinos'
|
extName = 'MonosChinos'
|
||||||
extClass = '.MonosChinos'
|
extClass = '.MonosChinos'
|
||||||
extVersionCode = 35
|
extVersionCode = 36
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MundoDonghua'
|
extName = 'MundoDonghua'
|
||||||
extClass = '.MundoDonghua'
|
extClass = '.MundoDonghua'
|
||||||
extVersionCode = 29
|
extVersionCode = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Otakuverso'
|
extName = 'Otakuverso'
|
||||||
extClass = '.Otakuverso'
|
extClass = '.Otakuverso'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'PelisForte'
|
extName = 'PelisForte'
|
||||||
extClass = '.PelisForte'
|
extClass = '.PelisForte'
|
||||||
extVersionCode = 30
|
extVersionCode = 31
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Pelisplushd'
|
extName = 'Pelisplushd'
|
||||||
extClass = '.PelisplushdFactory'
|
extClass = '.PelisplushdFactory'
|
||||||
extVersionCode = 73
|
extVersionCode = 74
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Serieskao'
|
extName = 'Serieskao'
|
||||||
extClass = '.Serieskao'
|
extClass = '.Serieskao'
|
||||||
extVersionCode = 4
|
extVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.SoloLatino'
|
extClass = '.SoloLatino'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://sololatino.net'
|
baseUrl = 'https://sololatino.net'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'VerAnimes'
|
extName = 'VerAnimes'
|
||||||
extClass = '.VerAnimes'
|
extClass = '.VerAnimes'
|
||||||
extVersionCode = 10
|
extVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'VerSeriesOnline'
|
extName = 'VerSeriesOnline'
|
||||||
extClass = '.VerSeriesOnline'
|
extClass = '.VerSeriesOnline'
|
||||||
extVersionCode = 4
|
extVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'zeroanime'
|
extName = 'zeroanime'
|
||||||
extClass = '.Zeroanime'
|
extClass = '.Zeroanime'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Zonaleros'
|
extName = 'Zonaleros'
|
||||||
extClass = '.Zonaleros'
|
extClass = '.Zonaleros'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AniSama'
|
extName = 'AniSama'
|
||||||
extClass = '.AniSama'
|
extClass = '.AniSama'
|
||||||
extVersionCode = 11
|
extVersionCode = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Hds'
|
extClass = '.Hds'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://www.hds.quest'
|
baseUrl = 'https://www.hds.quest'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'OtakuFR'
|
extName = 'OtakuFR'
|
||||||
extClass = '.OtakuFR'
|
extClass = '.OtakuFR'
|
||||||
extVersionCode = 25
|
extVersionCode = 26
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.AniSAGA'
|
extClass = '.AniSAGA'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://www.anisaga.org'
|
baseUrl = 'https://www.anisaga.org'
|
||||||
overrideVersionCode = 17
|
overrideVersionCode = 18
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'OtakuDesu'
|
extName = 'OtakuDesu'
|
||||||
extClass = '.OtakuDesu'
|
extClass = '.OtakuDesu'
|
||||||
extVersionCode = 31
|
extVersionCode = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Q1N'
|
extClass = '.Q1N'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://q1n.net'
|
baseUrl = 'https://q1n.net'
|
||||||
overrideVersionCode = 18
|
overrideVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime Srbija'
|
extName = 'Anime Srbija'
|
||||||
extClass = '.AnimeSrbija'
|
extClass = '.AnimeSrbija'
|
||||||
extVersionCode = 11
|
extVersionCode = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animeler'
|
extName = 'Animeler'
|
||||||
extClass = '.Animeler'
|
extClass = '.Animeler'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anizm'
|
extName = 'Anizm'
|
||||||
extClass = '.Anizm'
|
extClass = '.Anizm'
|
||||||
extVersionCode = 25
|
extVersionCode = 26
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'TR Anime Izle'
|
extName = 'TR Anime Izle'
|
||||||
extClass = '.TRAnimeIzle'
|
extClass = '.TRAnimeIzle'
|
||||||
extVersionCode = 21
|
extVersionCode = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Türk Anime TV'
|
extName = 'Türk Anime TV'
|
||||||
extClass = '.TurkAnime'
|
extClass = '.TurkAnime'
|
||||||
extVersionCode = 34
|
extVersionCode = 35
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue