Initial commit
This commit is contained in:
commit
98ed7e8839
2263 changed files with 108711 additions and 0 deletions
15
src/de/animebase/build.gradle
Normal file
15
src/de/animebase/build.gradle
Normal 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")
|
||||
}
|
BIN
src/de/animebase/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/de/animebase/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
BIN
src/de/animebase/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/de/animebase/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
BIN
src/de/animebase/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/de/animebase/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
src/de/animebase/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/de/animebase/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
src/de/animebase/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/de/animebase/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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" },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue