Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -0,0 +1,299 @@
package eu.kanade.tachiyomi.animeextension.de.animebase
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.de.animebase.extractors.UnpackerExtractor
import eu.kanade.tachiyomi.animeextension.de.animebase.extractors.VidGuardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Anime-Base"
override val baseUrl = "https://anime-base.net"
override val lang = "de"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/favorites", headers)
override fun popularAnimeSelector() = "div.table-responsive > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href").replace("/link/", "/anime/"))
thumbnail_url = element.selectFirst("div.thumbnail img")?.absUrl("src")
title = element.selectFirst("div.caption h3")!!.text()
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/updates", headers)
override fun latestUpdatesSelector() = "div.box-header + div.box-body > a"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = null
// =============================== Search ===============================
override fun getFilterList() = AnimeBaseFilters.FILTER_LIST
private val searchToken by lazy {
client.newCall(GET("$baseUrl/searching", headers)).execute()
.asJsoup()
.selectFirst("form > input[name=_token]")!!
.attr("value")
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimeBaseFilters.getSearchParameters(filters)
return when {
params.list.isEmpty() -> {
val body = FormBody.Builder()
.add("_token", searchToken)
.add("_token", searchToken)
.add("name_serie", query)
.add("jahr", params.year.toIntOrNull()?.toString() ?: "")
.apply {
params.languages.forEach { add("dubsub[]", it) }
params.genres.forEach { add("genre[]", it) }
}.build()
POST("$baseUrl/searching", headers, body)
}
else -> {
GET("$baseUrl/${params.list}${params.letter}?page=$page", headers)
}
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
val doc = response.asJsoup()
return when {
doc.location().contains("/searching") -> {
val animes = doc.select(searchAnimeSelector()).map(::searchAnimeFromElement)
AnimesPage(animes, false)
}
else -> { // pages like filmlist or animelist
val animes = doc.select(popularAnimeSelector()).map(::popularAnimeFromElement)
val hasNext = doc.selectFirst(searchAnimeNextPageSelector()) != null
AnimesPage(animes, hasNext)
}
}
}
override fun searchAnimeSelector() = "div.col-lg-9.col-md-8 div.box-body > a"
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = "ul.pagination li > a[rel=next]"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
val boxBody = document.selectFirst("div.box-body.box-profile > center")!!
title = boxBody.selectFirst("h3")!!.text()
thumbnail_url = boxBody.selectFirst("img")!!.absUrl("src")
val infosDiv = document.selectFirst("div.box-body > div.col-md-9")!!
status = parseStatus(infosDiv.getInfo("Status"))
genre = infosDiv.select("strong:contains(Genre) + p > a").eachText()
.joinToString()
.takeIf(String::isNotBlank)
description = buildString {
infosDiv.getInfo("Beschreibung")?.also(::append)
infosDiv.getInfo("Originalname")?.also { append("\nOriginal name: $it") }
infosDiv.getInfo("Erscheinungsjahr")?.also { append("\nErscheinungsjahr: $it") }
}
}
private fun parseStatus(status: String?) = when (status?.orEmpty()) {
"Laufend" -> SAnime.ONGOING
"Abgeschlossen" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
private fun Element.getInfo(selector: String) =
selectFirst("strong:contains($selector) + p")?.text()?.trim()
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) =
super.episodeListParse(response).sortedWith(
compareBy(
{ it.name.startsWith("Film ") },
{ it.name.startsWith("Special ") },
{ it.episode_number },
),
).reversed()
override fun episodeListSelector() = "div.tab-content > div > div.panel"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val epname = element.selectFirst("h3")?.text() ?: "Episode 1"
val language = when (element.selectFirst("button")?.attr("data-dubbed").orEmpty()) {
"0" -> "Subbed"
else -> "Dubbed"
}
name = epname
scanlator = language
episode_number = epname.substringBefore(":").substringAfter(" ").toFloatOrNull() ?: 0F
val selectorClass = element.classNames().first { it.startsWith("episode-div") }
setUrlWithoutDomain(element.baseUri() + "?selector=div.panel.$selectorClass")
}
// ============================ Video Links =============================
private val hosterSettings by lazy {
mapOf(
"Streamwish" to "https://streamwish.to/e/",
"Voe.SX" to "https://voe.sx/e/",
"Lulustream" to "https://lulustream.com/e/",
"VTube" to "https://vtbe.to/embed-",
"VidGuard" to "https://vembed.net/e/",
)
}
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val selector = response.request.url.queryParameter("selector")
?: return emptyList()
return doc.select("$selector div.panel-body > button").toList()
.filter { it.text() in hosterSettings.keys }
.parallelCatchingFlatMapBlocking {
val language = when (it.attr("data-dubbed")) {
"0" -> "SUB"
else -> "DUB"
}
getVideosFromHoster(it.text(), it.attr("data-streamlink"))
.map { video ->
Video(
video.url,
"$language ${video.quality}",
video.videoUrl,
video.headers,
video.subtitleTracks,
video.audioTracks,
)
}
}
}
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val unpackerExtractor by lazy { UnpackerExtractor(client, headers) }
private val vidguardExtractor by lazy { VidGuardExtractor(client) }
private fun getVideosFromHoster(hoster: String, urlpart: String): List<Video> {
val url = hosterSettings.get(hoster)!! + urlpart
return when (hoster) {
"Streamwish" -> streamWishExtractor.videosFromUrl(url)
"Voe.SX" -> voeExtractor.videosFromUrl(url)
"VTube", "Lulustream" -> unpackerExtractor.videosFromUrl(url, hoster)
"VidGuard" -> vidguardExtractor.videosFromUrl(url)
else -> null
} ?: emptyList()
}
override fun List<Video>.sort(): List<Video> {
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(lang) },
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// =============================== Search ===============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_ENTRIES
entryValues = PREF_LANG_VALUES
setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
companion object {
private const val PREF_LANG_KEY = "preferred_sub"
private const val PREF_LANG_TITLE = "Standardmäßig Sub oder Dub?"
private const val PREF_LANG_DEFAULT = "SUB"
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
private val PREF_LANG_VALUES = arrayOf("SUB", "DUB")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080p", "720p", "480p", "360p")
}
}

View file

@ -0,0 +1,285 @@
package eu.kanade.tachiyomi.animeextension.de.animebase
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimeBaseFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
}
class YearFilter : AnimeFilter.Text("Erscheinungsjahr")
class LanguagesFilter : CheckBoxFilterList("Sprache", AnimeBaseFiltersData.LANGUAGES)
class GenresFilter : CheckBoxFilterList("Genre", AnimeBaseFiltersData.GENRES)
class ListFilter : QueryPartFilter("Liste der Konten", AnimeBaseFiltersData.LISTS)
class LetterFilter : QueryPartFilter("Schreiben", AnimeBaseFiltersData.LETTERS)
val FILTER_LIST get() = AnimeFilterList(
YearFilter(),
LanguagesFilter(),
GenresFilter(),
AnimeFilter.Separator(),
// >imagine using deepL
AnimeFilter.Header("Die untenstehenden Filter ignorieren die textsuche!"),
ListFilter(),
LetterFilter(),
)
data class FilterSearchParams(
val year: String = "",
val languages: List<String> = emptyList(),
val genres: List<String> = emptyList(),
val list: String = "",
val letter: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.getFirst<YearFilter>().state,
filters.parseCheckbox<LanguagesFilter>(AnimeBaseFiltersData.LANGUAGES),
filters.parseCheckbox<GenresFilter>(AnimeBaseFiltersData.GENRES),
filters.asQueryPart<ListFilter>(),
filters.asQueryPart<LetterFilter>(),
)
}
private object AnimeBaseFiltersData {
val LANGUAGES = arrayOf(
Pair("German Sub", "0"), // Literally Jmir
Pair("German Dub", "1"),
Pair("English Sub", "2"), // Average bri'ish
Pair("English Dub", "3"),
)
val GENRES = arrayOf(
Pair("Abenteuer", "1"),
Pair("Abenteuerkomödie", "261"),
Pair("Action", "2"),
Pair("Actiondrama", "3"),
Pair("Actionkomödie", "4"),
Pair("Adeliger", "258"),
Pair("Airing", "59"),
Pair("Alltagsdrama", "6"),
Pair("Alltagsleben", "7"),
Pair("Ältere Frau, jüngerer Mann", "210"),
Pair("Älterer Mann, jüngere Frau", "222"),
Pair("Alternative Welt", "53"),
Pair("Altes Asien", "187"),
Pair("Animation", "193"),
Pair("Anime & Film", "209"),
Pair("Anthologie", "260"),
Pair("Auftragsmörder / Attentäter", "265"),
Pair("Außerirdische", "204"),
Pair("Badminton", "259"),
Pair("Band", "121"),
Pair("Baseball", "234"),
Pair("Basketball", "239"),
Pair("Bionische Kräfte", "57"),
Pair("Boxen", "218"),
Pair("Boys Love", "226"),
Pair("Büroangestellter", "248"),
Pair("CG-Anime", "81"),
Pair("Charakterschwache Heldin", "102"),
Pair("Charakterschwacher Held", "101"),
Pair("Charakterstarke Heldin", "100"),
Pair("Charakterstarker Held", "88"),
Pair("Cyberpunk", "60"),
Pair("Cyborg", "109"),
Pair("Dämon", "58"),
Pair("Delinquent", "114"),
Pair("Denk- und Glücksspiele", "227"),
Pair("Detektiv", "91"),
Pair("Dialogwitz", "93"),
Pair("Dieb", "245"),
Pair("Diva", "112"),
Pair("Donghua", "257"),
Pair("Drache", "263"),
Pair("Drama", "8"),
Pair("Dunkle Fantasy", "90"),
Pair("Ecchi", "9"),
Pair("Elf", "89"),
Pair("Endzeit", "61"),
Pair("Epische Fantasy", "95"),
Pair("Episodisch", "92"),
Pair("Erotik", "186"),
Pair("Erwachsen", "70"),
Pair("Erwachsenwerden", "125"),
Pair("Essenszubereitung", "206"),
Pair("Familie", "63"),
Pair("Fantasy", "11"),
Pair("Fee", "264"),
Pair("Fighting-Shounen", "12"),
Pair("Football", "241"),
Pair("Frühe Neuzeit", "113"),
Pair("Fußball", "220"),
Pair("Gaming Kartenspiele", "250"),
Pair("Ganbatte", "13"),
Pair("Gedächtnisverlust", "115"),
Pair("Gegenwart", "46"),
Pair("Geist", "75"),
Pair("Geistergeschichten", "14"),
Pair("Gender Bender", "216"),
Pair("Genie", "116"),
Pair("Girls Love", "201"),
Pair("Grundschule", "103"),
Pair("Harem", "15"),
Pair("Hentai", "16"),
Pair("Hexe", "97"),
Pair("Himmlische Wesen", "105"),
Pair("Historisch", "49"),
Pair("Horror", "17"),
Pair("Host-Club", "247"),
Pair("Idol", "122"),
Pair("In einem Raumschiff", "208"),
Pair("Independent Anime", "251"),
Pair("Industrialisierung", "230"),
Pair("Isekai", "120"),
Pair("Kami", "98"),
Pair("Kampfkunst", "246"),
Pair("Kampfsport", "79"),
Pair("Kemonomimi", "106"),
Pair("Kinder", "41"),
Pair("Kindergarten", "243"),
Pair("Klubs", "189"),
Pair("Kodomo", "40"),
Pair("Komödie", "18"),
Pair("Kopfgeldjäger", "211"),
Pair("Krieg", "68"),
Pair("Krimi", "19"),
Pair("Liebesdrama", "20"),
Pair("Mafia", "127"),
Pair("Magical Girl", "21"),
Pair("Magie", "52"),
Pair("Maid", "244"),
Pair("Malerei", "231"),
Pair("Manga & Doujinshi", "217"),
Pair("Mannschaftssport", "262"),
Pair("Martial Arts", "64"),
Pair("Mecha", "22"),
Pair("Mediziner", "238"),
Pair("Mediziner", "254"),
Pair("Meiji-Ära", "242"),
Pair("Militär", "62"),
Pair("Mittelalter", "76"),
Pair("Mittelschule", "190"),
Pair("Moe", "43"),
Pair("Monster", "54"),
Pair("Musik", "69"),
Pair("Mystery", "23"),
Pair("Ninja", "55"),
Pair("Nonsense-Komödie", "24"),
Pair("Oberschule", "83"),
Pair("Otaku", "215"),
Pair("Parodie", "94"),
Pair("Pirat", "252"),
Pair("Polizist", "84"),
Pair("PSI-Kräfte", "78"),
Pair("Psychodrama", "25"),
Pair("Real Robots", "212"),
Pair("Rennsport", "207"),
Pair("Ritter", "50"),
Pair("Roboter ", "73"),
Pair("Roboter & Android", "110"),
Pair("Romantische Komödie", "26"),
Pair("Romanze", "27"),
Pair("Samurai", "47"),
Pair("Satire", "232"),
Pair("Schule", "119"),
Pair("Schusswaffen", "82"),
Pair("Schwerter & Co", "51"),
Pair("Schwimmen", "223"),
Pair("Scifi", "28"),
Pair("Seinen", "39"),
Pair("Sentimentales Drama", "29"),
Pair("Shounen", "37"),
Pair("Slapstick", "56"),
Pair("Slice of Life", "5"),
Pair("Solosänger", "219"),
Pair("Space Opera", "253"),
Pair("Splatter", "36"),
Pair("Sport", "30"),
Pair("Stoische Heldin", "123"),
Pair("Stoischer Held", "85"),
Pair("Super Robots", "203"),
Pair("Super-Power", "71"),
Pair("Superhelden", "256"),
Pair("Supernatural", "225"),
Pair("Tanzen", "249"),
Pair("Tennis", "233"),
Pair("Theater", "224"),
Pair("Thriller", "31"),
Pair("Tiermensch", "111"),
Pair("Tomboy", "104"),
Pair("Tragödie", "86"),
Pair("Tsundere", "107"),
Pair("Überlebenskampf", "117"),
Pair("Übermäßige Gewaltdarstellung", "34"),
Pair("Unbestimmt", "205"),
Pair("Universität", "214"),
Pair("Vampir", "35"),
Pair("Verworrene Handlung", "126"),
Pair("Virtuelle Welt", "108"),
Pair("Volleyball", "191"),
Pair("Volljährig", "67"),
Pair("Wassersport", "266"),
Pair("Weiblich", "45"),
Pair("Weltkriege", "128"),
Pair("Weltraum", "74"),
Pair("Widerwillige Heldin", "124"),
Pair("Widerwilliger Held", "188"),
Pair("Yandere", "213"),
Pair("Yaoi", "32"),
Pair("Youkai", "99"),
Pair("Yuri", "33"),
Pair("Zeichentrick", "77"),
Pair("Zeichentrick", "255"),
Pair("Zeitgenössische Fantasy", "80"),
Pair("Zeitsprung", "240"),
Pair("Zombie", "87"),
)
val LISTS = arrayOf(
Pair("Keine", ""),
Pair("Anime", "animelist"),
Pair("Film", "filmlist"),
Pair("Hentai", "hentailist"),
Pair("Sonstiges", "misclist"),
)
val LETTERS = arrayOf(Pair("Jede", "")) + ('A'..'Z').map {
Pair(it.toString(), "/$it")
}.toTypedArray()
}
}

View file

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.animeextension.de.animebase.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class UnpackerExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, hoster: String): List<Video> {
val doc = client.newCall(GET(url, headers)).execute()
.asJsoup()
val script = doc.selectFirst("script:containsData(eval)")
?.data()
?.let(JsUnpacker::unpackAndCombine)
?: return emptyList()
val playlistUrl = script.substringAfter("file:\"").substringBefore('"')
return playlistUtils.extractFromHls(
playlistUrl,
referer = playlistUrl,
videoNameGen = { "$hoster - $it" },
)
}
}

View file

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