forked from Kohi-den/extensions-source
parent
45cff438ce
commit
821cbc1d59
89 changed files with 668 additions and 557 deletions
lib
buzzheavier-extractor
chillx-extractor/src/main/java/eu/kanade/tachiyomi/lib/chillxextractor
filemoon-extractor/src/main/java/eu/kanade/tachiyomi/lib/filemoonextractor
savefile-extractor
streamwish-extractor/src/main/java/eu/kanade/tachiyomi/lib/streamwishextractor
src
all
chineseanime
hikari
javgg
javguru
lmanime
supjav
ar
de
animebase
animetoast
cinemathek
einfach
kool
moflixstream
en
allanime
allanimechi
animekhor
animenosub
animetake
asiaflix
luciferdonghua
tokuzilla
es
animebum
animefenix
animeflv
animeid
animejl
animelatinohd
animemovil
animenix
animeonlineninja
animeytes
asialiveaction
cine24h
cinecalidad
cineplus123
cuevana
detodopeliculas
doramasflix
doramasyt
estrenosdoramas
flixlatam
gnula
hackstore
hentaila
hentaitk
homecine
jkanime
katanime
lacartoons
legionanime
metroseries
mhdflix
monoschinos
mundodonghua
otakuverso
pelisforte
pelisplushd
serieskao
sololatino
veranimes
verseriesonline
zeroanime
zonaleros
fr
hi/animesaga
id/otakudesu
pt/animesgratis
sr/animesrbija
tr
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 }
|
||||||
|
if (checked.isNotEmpty()) {
|
||||||
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
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())
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"),
|
|
||||||
)
|
)
|
||||||
|
}.toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
||||||
} else {
|
|
||||||
addPathSegment("ajax")
|
|
||||||
addPathSegment("getfilter")
|
|
||||||
filters.filterIsInstance<UriFilter>().forEach {
|
filters.filterIsInstance<UriFilter>().forEach {
|
||||||
it.addToUri(this)
|
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