Initial commit
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
After Width: | Height: | Size: 9.6 KiB |
BIN
src/de/animebase/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/de/animebase/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/de/animebase/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src/de/animebase/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
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()
|
||||
}
|
||||
}
|
14
src/de/animeloads/build.gradle
Normal file
|
@ -0,0 +1,14 @@
|
|||
ext {
|
||||
extName = 'Anime-Loads'
|
||||
extClass = '.AnimeLoads'
|
||||
extVersionCode = 15
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
}
|
BIN
src/de/animeloads/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/de/animeloads/res/mipmap-hdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/de/animeloads/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/de/animeloads/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/de/animeloads/res/mipmap-mdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/de/animeloads/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/de/animeloads/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/de/animeloads/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/de/animeloads/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
src/de/animeloads/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 17 KiB |
BIN
src/de/animeloads/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,813 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.animeloads
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URLDecoder
|
||||
import kotlin.Exception
|
||||
|
||||
class AnimeLoads : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Anime-Loads"
|
||||
|
||||
override val baseUrl = "https://www.anime-loads.org"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override val id: Long = 655155856096L
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.addInterceptor(DdosGuardInterceptor(network.client))
|
||||
.build()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.row div.col-sm-6 div.panel-body"
|
||||
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime-series/page/$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("div.row a.cover-img").attr("href"))
|
||||
anime.thumbnail_url = element.select("div.row a.cover-img img").attr("src")
|
||||
anime.title = element.select("div.row h4.title-list a").text()
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "i.glyphicon-forward"
|
||||
|
||||
// episodes
|
||||
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val episode = SEpisode.create()
|
||||
val series = document.select("a[title=\"Anime Serien\"]")
|
||||
if (series.attr("title").contains("Anime Serien")) {
|
||||
val eplist = document.select("#streams_episodes_1 div.list-group")
|
||||
val url = document.select("meta[property=\"og:url\"]").attr("content")
|
||||
val ep = parseEpisodesFromSeries(eplist, url)
|
||||
episodeList.addAll(ep)
|
||||
} else {
|
||||
episode.name = document.select("div.page-header > h1").attr("title")
|
||||
episode.episode_number = 1F
|
||||
episode.setUrlWithoutDomain(document.select("meta[property=\"og:url\"]").attr("content"))
|
||||
episodeList.add(episode)
|
||||
}
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
private fun parseEpisodesFromSeries(element: Elements, url: String): List<SEpisode> {
|
||||
val episodeElement = element.select("a.list-group-item")
|
||||
return episodeElement.map { episodeFromElement(it, url) }
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
||||
|
||||
private fun episodeFromElement(element: Element, url: String): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
val id = element.attr("aria-controls")
|
||||
episode.setUrlWithoutDomain("$url#$id")
|
||||
episode.name = "Ep." + element.select("span:nth-child(1)").text()
|
||||
episode.episode_number = element.select("span strong").text().toFloat()
|
||||
return episode
|
||||
}
|
||||
|
||||
// Video Extractor
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val url = response.request.url.toString()
|
||||
val idep = url
|
||||
.substringAfter("#")
|
||||
return videosFromElement(document, idep, url)
|
||||
}
|
||||
|
||||
private fun videosFromElement(document: Document, idep: String, url: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val hosterSelection = preferences.getStringSet("hoster_selection", setOf("dood", "voe", "stape"))
|
||||
val subSelection = preferences.getStringSet("sub_selection", setOf("sub", "dub"))
|
||||
val lang = document.select("div#streams ul.nav li[role=\"presentation\"]")
|
||||
lang.forEach { langit ->
|
||||
Log.i("videosFromElement", "Langit: $langit")
|
||||
when {
|
||||
langit.select("a i.flag-de").attr("title").contains("Subtitles: German") || langit.select("a i.flag-de").attr("title").contains("Untertitel: Deutsch") && subSelection?.contains("sub") == true -> {
|
||||
val aria = langit.select("a").attr("aria-controls")
|
||||
val id = document.select("#$aria div.episodes").attr("id")
|
||||
val epnum = idep.substringAfter("streams_episodes_1")
|
||||
val element = document.select("div#$id$epnum")
|
||||
val enc = element.attr("data-enc")
|
||||
val capfiles = client.newCall(
|
||||
POST(
|
||||
"$baseUrl/files/captcha",
|
||||
body = "cID=0&rT=1".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"application/json, text/javascript, */*; q=0.01",
|
||||
"cache-control",
|
||||
"max-age=15",
|
||||
),
|
||||
),
|
||||
).execute().asJsoup()
|
||||
|
||||
val hashes = capfiles.toString().substringAfter("[").substringBefore("]").split(",")
|
||||
val hashlist = mutableListOf<String>()
|
||||
val pnglist = mutableListOf<String>()
|
||||
var max = "1"
|
||||
var min = "99999"
|
||||
hashes.forEach {
|
||||
val hash = it.replace("<body>", "")
|
||||
.replace("[", "")
|
||||
.replace("\"", "").replace("]", "")
|
||||
.replace("</body>", "").replace("%20", "")
|
||||
val png = client.newCall(
|
||||
GET(
|
||||
"$baseUrl/files/captcha?cid=0&hash=$hash",
|
||||
headers = Headers.headersOf(
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"image/avif,image/webp,*/*",
|
||||
"cache-control",
|
||||
"max-age=15",
|
||||
),
|
||||
),
|
||||
).execute().body.byteString()
|
||||
val size = png.toString()
|
||||
.substringAfter("[size=").substringBefore(" hex")
|
||||
pnglist.add("$size | $hash")
|
||||
hashlist.add(size)
|
||||
for (num in hashlist) {
|
||||
if (max < num) {
|
||||
max = num
|
||||
}
|
||||
}
|
||||
for (num in hashlist) {
|
||||
if (min > num) {
|
||||
min = num
|
||||
}
|
||||
}
|
||||
}
|
||||
var int = 0
|
||||
|
||||
pnglist.forEach { diffit ->
|
||||
if (int == 0) {
|
||||
if (diffit.substringBefore(" |").toInt() != max.toInt() && diffit.substringBefore(" |").toInt() != min.toInt()) {
|
||||
int = 1
|
||||
val hash = diffit.substringBefore(" |").toInt()
|
||||
val diffmax = max.toInt() - hash
|
||||
val diffmin = hash - min.toInt()
|
||||
if (diffmax > diffmin) {
|
||||
pnglist.forEach { itmax ->
|
||||
if (max.toInt() == itmax.substringBefore(" |").toInt()) {
|
||||
val maxhash = itmax.substringAfter("| ")
|
||||
network.client.newCall(
|
||||
POST(
|
||||
"$baseUrl/files/captcha",
|
||||
body = "cID=0&pC=$maxhash&rT=2".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#$id$epnum", ""), "Accept", "*/*", "cache-control", "max-age=15",
|
||||
),
|
||||
),
|
||||
).execute()
|
||||
val maxdoc = client.newCall(
|
||||
POST(
|
||||
"$baseUrl/ajax/captcha",
|
||||
body = "enc=${enc.replace("=", "%3D")}&response=captcha&captcha-idhf=0&captcha-hf=$maxhash".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#$id$epnum", ""),
|
||||
"Accept", "application/json, text/javascript, */*; q=0.01", "cache-control", "max-age=15",
|
||||
),
|
||||
),
|
||||
).execute().asJsoup().toString()
|
||||
if (maxdoc.substringAfter("\"code\":\"").substringBefore("\",").contains("error")) {
|
||||
throw Exception("Captcha bypass failed! Clear Cookies & Webview data. Or wait some time.")
|
||||
} else {
|
||||
val links = maxdoc.substringAfter("\"content\":").substringBefore("</body>").split("{\"links\":")
|
||||
links.forEach {
|
||||
if (it.contains("link")) {
|
||||
val hoster = it.substringAfter("\"hoster\":\"").substringBefore("\",\"")
|
||||
val linkpart = it.substringAfter("\"link\":\"").substringBefore("\"}]")
|
||||
val leaveurl = client.newCall(GET("$baseUrl/leave/$linkpart")).execute().request.url.toString()
|
||||
val decode = "https://www." + URLDecoder.decode(leaveurl.substringAfter("www."), "utf-8")
|
||||
if (decode.contains(baseUrl)) {
|
||||
val link = client.newCall(GET(decode)).execute().request.url.toString()
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(link, "(Deutsch Sub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Sub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstreams Deutsch Sub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(leaveurl, "(Deutsch Sub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Sub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstreams Deutsch Sub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pnglist.forEach { itmin ->
|
||||
if (min.toInt() == itmin.substringBefore(" |").toInt()) {
|
||||
val minhash = itmin.substringAfter("| ")
|
||||
network.client.newCall(
|
||||
POST(
|
||||
"$baseUrl/files/captcha",
|
||||
body = "cID=0&pC=$minhash&rT=2".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin",
|
||||
baseUrl,
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"*/*",
|
||||
),
|
||||
),
|
||||
).execute()
|
||||
val mindoc = client.newCall(
|
||||
POST(
|
||||
"$baseUrl/ajax/captcha",
|
||||
body = "enc=${enc.replace("=", "%3D")}&response=captcha&captcha-idhf=0&captcha-hf=$minhash".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin",
|
||||
baseUrl,
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"application/json, text/javascript, */*; q=0.01",
|
||||
),
|
||||
),
|
||||
).execute().asJsoup().toString()
|
||||
if (mindoc.substringAfter("\"code\":\"").substringBefore("\",").contains("error")) {
|
||||
throw Exception("Captcha bypass failed! Clear Cookies & Webview data. Or wait some time.")
|
||||
} else {
|
||||
val links = mindoc.substringAfter("\"content\":[").substringBefore("</body>").split("{\"links\":")
|
||||
links.forEach {
|
||||
if (it.contains("link")) {
|
||||
val hoster = it.substringAfter("\"hoster\":\"").substringBefore("\",\"")
|
||||
val linkpart = it.substringAfter("\"link\":\"").substringBefore("\"}]")
|
||||
val leaveurl = client.newCall(GET("$baseUrl/leave/$linkpart")).execute().request.url.toString()
|
||||
val decode = "https://www." + URLDecoder.decode(leaveurl.substringAfter("www."), "utf-8")
|
||||
if (decode.contains(baseUrl)) {
|
||||
val link = client.newCall(GET(decode)).execute().request.url.toString()
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(link, "(Deutsch Sub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Sub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstreams Deutsch Sub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(leaveurl, "(Deutsch Sub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Sub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstreams Deutsch Sub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
langit.select("a i.flag-de").attr("title").contains("Language: German") || langit.select("a i.flag-de").attr("title").contains("Sprache: Deutsch") && subSelection?.contains("dub") == true -> {
|
||||
val aria = langit.select("a").attr("aria-controls")
|
||||
val id = document.select("#$aria div.episodes").attr("id")
|
||||
val epnum = idep.substringAfter("streams_episodes_1")
|
||||
val element = document.select("div#$id$epnum")
|
||||
val enc = element.attr("data-enc")
|
||||
val capfiles = client.newCall(
|
||||
POST(
|
||||
"$baseUrl/files/captcha",
|
||||
body = "cID=0&rT=1".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"application/json, text/javascript, */*; q=0.01",
|
||||
"cache-control",
|
||||
"max-age=15",
|
||||
),
|
||||
),
|
||||
).execute().asJsoup()
|
||||
|
||||
val hashes = capfiles.toString().substringAfter("[").substringBefore("]").split(",")
|
||||
val hashlist = mutableListOf<String>()
|
||||
val pnglist = mutableListOf<String>()
|
||||
var max = "1"
|
||||
var min = "99999"
|
||||
hashes.forEach {
|
||||
val hash = it.replace("<body>", "")
|
||||
.replace("[", "")
|
||||
.replace("\"", "").replace("]", "")
|
||||
.replace("</body>", "").replace("%20", "")
|
||||
val png = client.newCall(
|
||||
GET(
|
||||
"$baseUrl/files/captcha?cid=0&hash=$hash",
|
||||
headers = Headers.headersOf(
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"image/avif,image/webp,*/*",
|
||||
"cache-control",
|
||||
"max-age=15",
|
||||
),
|
||||
),
|
||||
).execute().body.byteString()
|
||||
val size = png.toString()
|
||||
.substringAfter("[size=").substringBefore(" hex")
|
||||
pnglist.add("$size | $hash")
|
||||
hashlist.add(size)
|
||||
for (num in hashlist) {
|
||||
if (max < num) {
|
||||
max = num
|
||||
}
|
||||
}
|
||||
for (num in hashlist) {
|
||||
if (min > num) {
|
||||
min = num
|
||||
}
|
||||
}
|
||||
}
|
||||
var int = 0
|
||||
|
||||
pnglist.forEach { diffit ->
|
||||
if (int == 0) {
|
||||
if (diffit.substringBefore(" |").toInt() != max.toInt() && diffit.substringBefore(" |").toInt() != min.toInt()) {
|
||||
int = 1
|
||||
val hash = diffit.substringBefore(" |").toInt()
|
||||
val diffmax = max.toInt() - hash
|
||||
val diffmin = hash - min.toInt()
|
||||
if (diffmax > diffmin) {
|
||||
pnglist.forEach { itmax ->
|
||||
if (max.toInt() == itmax.substringBefore(" |").toInt()) {
|
||||
val maxhash = itmax.substringAfter("| ")
|
||||
network.client.newCall(
|
||||
POST(
|
||||
"$baseUrl/files/captcha",
|
||||
body = "cID=0&pC=$maxhash&rT=2".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#$id$epnum", ""), "Accept", "*/*", "cache-control", "max-age=15",
|
||||
),
|
||||
),
|
||||
).execute()
|
||||
val maxdoc = client.newCall(
|
||||
POST(
|
||||
"$baseUrl/ajax/captcha",
|
||||
body = "enc=${enc.replace("=", "%3D")}&response=captcha&captcha-idhf=0&captcha-hf=$maxhash".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#$id$epnum", ""),
|
||||
"Accept", "application/json, text/javascript, */*; q=0.01", "cache-control", "max-age=15",
|
||||
),
|
||||
),
|
||||
).execute().asJsoup().toString()
|
||||
if (maxdoc.substringAfter("\"code\":\"").substringBefore("\",").contains("error")) {
|
||||
throw Exception("Captcha bypass failed! Clear Cookies & Webview data. Or wait some time.")
|
||||
} else {
|
||||
val links = maxdoc.substringAfter("\"content\":").substringBefore("</body>").split("{\"links\":")
|
||||
links.forEach {
|
||||
if (it.contains("link")) {
|
||||
val hoster = it.substringAfter("\"hoster\":\"").substringBefore("\",\"")
|
||||
val linkpart = it.substringAfter("\"link\":\"").substringBefore("\"}]")
|
||||
val leaveurl = client.newCall(GET("$baseUrl/leave/$linkpart")).execute().request.url.toString()
|
||||
val decode = "https://www." + URLDecoder.decode(leaveurl.substringAfter("www."), "utf-8")
|
||||
if (decode.contains(baseUrl)) {
|
||||
val link = client.newCall(GET(decode)).execute().request.url.toString()
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(link, "(Deutsch Dub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Dub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstream Deutsch Dub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(leaveurl, "(Deutsch Dub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Dub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstream Deutsch Dub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pnglist.forEach { itmin ->
|
||||
if (min.toInt() == itmin.substringBefore(" |").toInt()) {
|
||||
val minhash = itmin.substringAfter("| ")
|
||||
network.client.newCall(
|
||||
POST(
|
||||
"$baseUrl/files/captcha",
|
||||
body = "cID=0&pC=$minhash&rT=2".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin",
|
||||
baseUrl,
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"*/*",
|
||||
),
|
||||
),
|
||||
).execute()
|
||||
val mindoc = client.newCall(
|
||||
POST(
|
||||
"$baseUrl/ajax/captcha",
|
||||
body = "enc=${enc.replace("=", "%3D")}&response=captcha&captcha-idhf=0&captcha-hf=$minhash".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
|
||||
headers = Headers.headersOf(
|
||||
"Origin",
|
||||
baseUrl,
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"Referer",
|
||||
url.replace("#$id$epnum", ""),
|
||||
"Accept",
|
||||
"application/json, text/javascript, */*; q=0.01",
|
||||
),
|
||||
),
|
||||
).execute().asJsoup().toString()
|
||||
if (mindoc.substringAfter("\"code\":\"").substringBefore("\",").contains("error")) {
|
||||
throw Exception("Captcha bypass failed! Clear Cookies & Webview data. Or wait some time.")
|
||||
} else {
|
||||
val links = mindoc.substringAfter("\"content\":[").substringBefore("</body>").split("{\"links\":")
|
||||
links.forEach {
|
||||
if (it.contains("link")) {
|
||||
val hoster = it.substringAfter("\"hoster\":\"").substringBefore("\",\"")
|
||||
val linkpart = it.substringAfter("\"link\":\"").substringBefore("\"}]")
|
||||
val leaveurl = client.newCall(GET("$baseUrl/leave/$linkpart")).execute().request.url.toString()
|
||||
val decode = "https://www." + URLDecoder.decode(leaveurl.substringAfter("www."), "utf-8")
|
||||
if (decode.contains(baseUrl)) {
|
||||
val link = client.newCall(GET(decode)).execute().request.url.toString()
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(link, "(Deutsch Dub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Dub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstream Deutsch Dub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(link, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(leaveurl, "(Deutsch Dub) "))
|
||||
}
|
||||
|
||||
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape Deutsch Dub"
|
||||
val video = try {
|
||||
StreamTapeExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
|
||||
val quality = "Doodstream Deutsch Dub"
|
||||
val video = try {
|
||||
DoodExtractor(client).videoFromUrl(leaveurl, quality)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList.reversed()
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val hoster = preferences.getString("preferred_hoster", null)
|
||||
if (hoster != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(hoster)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("div.row a.cover-img").attr("href"))
|
||||
anime.thumbnail_url = element.select("div.row a.cover-img img").attr("src")
|
||||
anime.title = element.select("div.row h4.title-list a").text()
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "i.glyphicon-forward"
|
||||
|
||||
override fun searchAnimeSelector(): String = "div.row div.col-sm-6 div.panel-body"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/search/page/$page?q=$query")
|
||||
|
||||
// Details
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.select("#description img.img-responsive").attr("src")
|
||||
anime.title = document.select("div.page-header > h1").attr("title")
|
||||
anime.genre = document.select("#description div.label-group a.label.label-info").joinToString(", ") { it.text() }
|
||||
anime.description = document.select("div.pt20").not("strong").text()
|
||||
anime.author = document.select("div.col-md-6.text-left p:nth-child(3) a").joinToString(", ") { it.text() }
|
||||
anime.status = SAnime.COMPLETED
|
||||
return anime
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// Preferences
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val hosterPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_hoster"
|
||||
title = "Standard-Hoster"
|
||||
entries = arrayOf("Doodstream", "Voe", "MIXdrop")
|
||||
entryValues = arrayOf("https://dood", "https://voe.sx", "https://streamtape.com")
|
||||
setDefaultValue("https://voe.sx")
|
||||
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()
|
||||
}
|
||||
}
|
||||
val hostSelection = MultiSelectListPreference(screen.context).apply {
|
||||
key = "hoster_selection"
|
||||
title = "Hoster auswählen"
|
||||
entries = arrayOf("Doodstream", "Voe", "Streamtape")
|
||||
entryValues = arrayOf("dood", "voe", "stape")
|
||||
setDefaultValue(setOf("dood", "voe", "stape"))
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}
|
||||
val subSelection = MultiSelectListPreference(screen.context).apply {
|
||||
key = "sub_selection"
|
||||
title = "Sprache auswählen"
|
||||
entries = arrayOf("SUB", "DUB")
|
||||
entryValues = arrayOf("sub", "dub")
|
||||
setDefaultValue(setOf("sub"))
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(hosterPref)
|
||||
screen.addPreference(hostSelection)
|
||||
screen.addPreference(subSelection)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.animeloads
|
||||
|
||||
import android.util.Log
|
||||
import android.webkit.CookieManager
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
|
||||
class DdosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
|
||||
|
||||
private val cookieManager by lazy { CookieManager.getInstance() }
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val response = chain.proceed(originalRequest)
|
||||
|
||||
// Check if DDos-GUARD is on
|
||||
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
|
||||
return response
|
||||
}
|
||||
|
||||
response.close()
|
||||
val cookies = cookieManager.getCookie(originalRequest.url.toString())
|
||||
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
Log.i("newCookie", "OldCookies: $oldCookie")
|
||||
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
|
||||
if (!ddg2Cookie?.value.isNullOrEmpty()) {
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
|
||||
val newCookie = getNewCookie(originalRequest.url) ?: return chain.proceed(originalRequest)
|
||||
val newCookieHeader = buildString {
|
||||
(oldCookie + newCookie).forEachIndexed { index, cookie ->
|
||||
if (index > 0) append("; ")
|
||||
append(cookie.name).append('=').append(cookie.value)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(originalRequest.newBuilder().addHeader("cookie", newCookieHeader).build())
|
||||
}
|
||||
|
||||
fun getNewCookie(url: HttpUrl): Cookie? {
|
||||
val cookies = cookieManager.getCookie(url.toString())
|
||||
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
|
||||
if (!ddg2Cookie?.value.isNullOrEmpty()) {
|
||||
return ddg2Cookie
|
||||
}
|
||||
val wellKnown = client.newCall(GET("https://check.ddos-guard.net/check.js"))
|
||||
.execute().body.string()
|
||||
.substringAfter("'", "")
|
||||
.substringBefore("'", "")
|
||||
val checkUrl = "${url.scheme}://${url.host + wellKnown}"
|
||||
return client.newCall(GET(checkUrl)).execute().header("set-cookie")?.let {
|
||||
Cookie.parse(url, it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ERROR_CODES = listOf(403)
|
||||
private val SERVER_CHECK = listOf("ddos-guard")
|
||||
}
|
||||
}
|
7
src/de/animestream/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime-Stream'
|
||||
extClass = '.AnimeStream'
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/de/animestream/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/de/animestream/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/de/animestream/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/de/animestream/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/de/animestream/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
|
@ -0,0 +1,138 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.animestream
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.de.animestream.extractors.MetaExtractor
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AnimeStream : ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Anime-Stream"
|
||||
|
||||
override val baseUrl = "https://anime-stream.to"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val id: Long = 314593699490737069
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.movies-list div.ml-item"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/series/page/$page/")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
anime.thumbnail_url = element.select("a img").attr("data-original")
|
||||
anime.title = element.select("a img").attr("alt")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "li.active ~ li"
|
||||
|
||||
// episodes
|
||||
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val episodeElement = document.select("div.les-content a")
|
||||
episodeElement.forEach {
|
||||
var num = 0
|
||||
val episode = nEpisodeFromElement(it, num)
|
||||
episodeList.add(episode)
|
||||
}
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
private fun nEpisodeFromElement(element: Element, num: Int): SEpisode {
|
||||
num + 1
|
||||
val episode = SEpisode.create()
|
||||
episode.episode_number = num.toFloat()
|
||||
episode.name = element.text()
|
||||
episode.setUrlWithoutDomain(element.attr("href"))
|
||||
return episode
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
||||
|
||||
// Video Extractor
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
return videosFromElement(document)
|
||||
}
|
||||
|
||||
private fun videosFromElement(document: Document): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val url = document.select("div a.lnk-lnk").attr("href")
|
||||
val quality = "Metastream"
|
||||
val video = MetaExtractor(client).videoFromUrl(url, quality)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
return videoList.reversed()
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
anime.thumbnail_url = element.select("a img").attr("data-original")
|
||||
anime.title = element.select("a img").attr("alt")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "li.active ~ li"
|
||||
|
||||
override fun searchAnimeSelector(): String = "div.movies-list div.ml-item"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/page/$page/?s=$query")
|
||||
|
||||
// Details
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.select("div.thumb img").attr("src")
|
||||
anime.title = document.select("div.thumb img").attr("alt")
|
||||
anime.description = document.select("div.desc p.f-desc").text()
|
||||
anime.status = parseStatus(document.select("div.mvici-right span[itemprop=\"duration\"]").text())
|
||||
anime.genre = document.select("div.mvici-left p a[rel=\"category tag\"]").joinToString(", ") { it.text() }
|
||||
return anime
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SAnime.UNKNOWN
|
||||
status.contains("Abgeschlossen", ignoreCase = true) -> SAnime.ONGOING
|
||||
else -> SAnime.COMPLETED
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.animestream.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class MetaExtractor(private val client: OkHttpClient) {
|
||||
|
||||
fun videoFromUrl(url: String, quality: String): Video? {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
val script = document.select("script:containsData(sources: [{src:)")
|
||||
.firstOrNull()?.data()?.substringAfter("sources: [{src: \"") ?: return null
|
||||
val videoUrl = script.substringAfter("sources: [{src: \"").substringBefore("\", type:")
|
||||
return Video(url, quality, videoUrl)
|
||||
}
|
||||
}
|
14
src/de/animetoast/build.gradle
Normal file
|
@ -0,0 +1,14 @@
|
|||
ext {
|
||||
extName = 'AnimeToast'
|
||||
extClass = '.AnimeToast'
|
||||
extVersionCode = 14
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
}
|
BIN
src/de/animetoast/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/de/animetoast/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/de/animetoast/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
src/de/animetoast/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/de/animetoast/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,355 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.animetoast
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.Exception
|
||||
|
||||
class AnimeToast : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeToast"
|
||||
|
||||
override val baseUrl = "https://www.animetoast.cc"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.row div.col-md-4 div.video-item"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("div.item-thumbnail a").attr("href"))
|
||||
anime.thumbnail_url = element.select("div.item-thumbnail a img").attr("src")
|
||||
anime.title = element.select("div.item-thumbnail a").attr("title")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
// episodes
|
||||
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val file = document.select("a[rel=\"category tag\"]").text()
|
||||
if (file.contains("Serie")) {
|
||||
if (document.select("#multi_link_tab0").attr("id").isNotEmpty()) {
|
||||
val elements = document.select("#multi_link_tab0")
|
||||
elements.forEach {
|
||||
val episode = parseEpisodesFromSeries(it)
|
||||
episodeList.addAll(episode)
|
||||
}
|
||||
} else {
|
||||
val elements = document.select("#multi_link_tab1")
|
||||
elements.forEach {
|
||||
val episode = parseEpisodesFromSeries(it)
|
||||
episodeList.addAll(episode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val episode = SEpisode.create()
|
||||
episode.setUrlWithoutDomain(document.select("link[rel=canonical]").attr("href"))
|
||||
episode.name = document.select("h1.light-title").text()
|
||||
episode.episode_number = 1F
|
||||
episodeList.add(episode)
|
||||
}
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
|
||||
val episodeElements = element.select("div.tab-pane a")
|
||||
val epT = episodeElements.text()
|
||||
if (epT.contains(":") || epT.contains("-")) {
|
||||
val url = episodeElements.attr("href")
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
val nUrl = document.select("#player-embed a").attr("href")
|
||||
val nDoc = client.newCall(GET(nUrl)).execute().asJsoup()
|
||||
val nEpEl = nDoc.select("div.tab-pane a")
|
||||
return nEpEl.map { episodeFromElement(it) }
|
||||
} else {
|
||||
return episodeElements.map { episodeFromElement(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
episode.episode_number = try {
|
||||
element.text().replace("Ep. ", "").toFloat()
|
||||
} catch (e: Exception) {
|
||||
100.0f
|
||||
}
|
||||
episode.name = element.text()
|
||||
episode.setUrlWithoutDomain(element.attr("href"))
|
||||
return episode
|
||||
}
|
||||
|
||||
// Video Extractor
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
return videosFromElement(document)
|
||||
}
|
||||
|
||||
private fun videosFromElement(document: Document): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val hosterSelection = preferences.getStringSet("hoster_selection", setOf("voe", "dood", "fmoon", "mp4u"))
|
||||
val fEp = document.select("div.tab-pane")
|
||||
if (fEp.text().contains(":") || fEp.text().contains("-")) {
|
||||
val tx = document.select("div.tab-pane")
|
||||
var here = false
|
||||
tx.forEach {
|
||||
if ((it.text().contains(":") || it.text().contains("-")) && !here) {
|
||||
here = true
|
||||
val sUrl = it.select("a").attr("href")
|
||||
val doc = client.newCall(GET(sUrl)).execute().asJsoup()
|
||||
val nUrl = doc.select("#player-embed a").attr("href")
|
||||
val nDoc = client.newCall(GET(nUrl)).execute().asJsoup()
|
||||
val nEpEl = nDoc.select("div.tab-pane a")
|
||||
val nEpcu = try {
|
||||
nDoc.select("div.tab-pane a.current-link").text()
|
||||
.substringAfter("Ep.").toFloat()
|
||||
} catch (e: Exception) {
|
||||
100.0f
|
||||
}
|
||||
nEpEl.forEach { tIt ->
|
||||
if (try { tIt.text().substringAfter("Ep.").toFloat() } catch (_: Exception) {} == nEpcu) {
|
||||
val url = tIt.attr("href")
|
||||
val newdoc = client.newCall(GET(url)).execute().asJsoup()
|
||||
val element = newdoc.select("#player-embed")
|
||||
for (elements in element) {
|
||||
val link = element.select("a").attr("abs:href")
|
||||
when {
|
||||
link.contains("https://voe.sx") && hosterSelection?.contains(
|
||||
"voe",
|
||||
) == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(link))
|
||||
}
|
||||
}
|
||||
}
|
||||
for (elements in element) {
|
||||
val link = element.select("iframe").attr("abs:src")
|
||||
when {
|
||||
(link.contains("https://dood") || link.contains("https://ds2play")) && hosterSelection?.contains(
|
||||
"dood",
|
||||
) == true -> {
|
||||
val quality = "DoodStream"
|
||||
val video =
|
||||
DoodExtractor(client).videoFromUrl(
|
||||
link,
|
||||
quality,
|
||||
false,
|
||||
)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
link.contains("https://filemoon.sx") && hosterSelection?.contains(
|
||||
"fmoon",
|
||||
) == true -> {
|
||||
val videos =
|
||||
FilemoonExtractor(client).videosFromUrl(link)
|
||||
videoList.addAll(videos)
|
||||
}
|
||||
|
||||
link.contains("mp4upload") && hosterSelection?.contains("mp4u") == true -> {
|
||||
val videos =
|
||||
Mp4uploadExtractor(client).videosFromUrl(
|
||||
link,
|
||||
headers,
|
||||
)
|
||||
videoList.addAll(videos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val epcu = try {
|
||||
document.select("div.tab-pane a.current-link").text().substringAfter("Ep.")
|
||||
.toFloat()
|
||||
} catch (e: Exception) {
|
||||
100.0f
|
||||
}
|
||||
val ep = document.select("div.tab-pane a")
|
||||
ep.forEach {
|
||||
if (try { it.text().substringAfter("Ep.").toFloat() } catch (_: Exception) {} == epcu) {
|
||||
val url = it.attr("href")
|
||||
val newdoc = client.newCall(GET(url)).execute().asJsoup()
|
||||
val element = newdoc.select("#player-embed")
|
||||
for (elements in element) {
|
||||
val link = element.select("a").attr("abs:href")
|
||||
when {
|
||||
link.contains("https://voe.sx") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(link))
|
||||
}
|
||||
}
|
||||
}
|
||||
for (elements in element) {
|
||||
val link = element.select("iframe").attr("abs:src")
|
||||
when {
|
||||
(link.contains("https://dood") || link.contains("https://ds2play")) && hosterSelection?.contains(
|
||||
"dood",
|
||||
) == true -> {
|
||||
val quality = "DoodStream"
|
||||
val video =
|
||||
DoodExtractor(client).videoFromUrl(link, quality, false)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
link.contains("https://filemoon.sx") && hosterSelection?.contains("fmoon") == true -> {
|
||||
val videos = FilemoonExtractor(client).videosFromUrl(link)
|
||||
videoList.addAll(videos)
|
||||
}
|
||||
|
||||
link.contains("mp4upload") && hosterSelection?.contains("mp4u") == true -> {
|
||||
val videos =
|
||||
Mp4uploadExtractor(client).videosFromUrl(link, headers)
|
||||
videoList.addAll(videos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList.reversed()
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val hoster = preferences.getString("preferred_hoster", null)
|
||||
if (hoster != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(hoster)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.attr("href"))
|
||||
anime.thumbnail_url = element.select("a img").attr("src")
|
||||
anime.title = element.attr("title")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = ".nextpostslink"
|
||||
|
||||
override fun searchAnimeSelector(): String = "div.item-thumbnail a[href]"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = "$baseUrl/page/$page/?s=$query"
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.select(".item-content p img").attr("src")
|
||||
anime.title = document.select("h1.light-title.entry-title").text()
|
||||
anime.genre = document.select("a[rel=tag]").joinToString(", ") { it.text() }
|
||||
val height = document.select("div.item-content p img").attr("height")
|
||||
anime.description = document.select("div.item-content div + p").text()
|
||||
anime.status = parseStatus(document.select("a[rel=\"category tag\"]").text())
|
||||
return anime
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SAnime.UNKNOWN
|
||||
status.contains("Airing", ignoreCase = true) -> SAnime.ONGOING
|
||||
else -> SAnime.COMPLETED
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// Preferences
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val hosterPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_hoster"
|
||||
title = "Standard-Hoster"
|
||||
entries = arrayOf("Voe", "DoodStream", "Filemoon", "Mp4upload")
|
||||
entryValues = arrayOf("https://voe.sx", "https://dood", "https://filemoon", "https://www.mp4upload")
|
||||
setDefaultValue("https://voe.sx")
|
||||
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()
|
||||
}
|
||||
}
|
||||
val subSelection = MultiSelectListPreference(screen.context).apply {
|
||||
key = "hoster_selection"
|
||||
title = "Hoster auswählen"
|
||||
entries = arrayOf("Voe", "DoodStream", "Filemoon", "Mp4upload")
|
||||
entryValues = arrayOf("voe", "dood", "fmoon", "mp4u")
|
||||
setDefaultValue(setOf("voe", "dood", "fmoon", "mp4u"))
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(hosterPref)
|
||||
screen.addPreference(subSelection)
|
||||
}
|
||||
}
|
13
src/de/aniworld/build.gradle
Normal file
|
@ -0,0 +1,13 @@
|
|||
ext {
|
||||
extName = 'AniWorld'
|
||||
extClass = '.AniWorld'
|
||||
extVersionCode = 23
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
}
|
BIN
src/de/aniworld/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/de/aniworld/res/mipmap-hdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/de/aniworld/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/de/aniworld/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/de/aniworld/res/mipmap-mdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/de/aniworld/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/de/aniworld/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
src/de/aniworld/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/de/aniworld/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/de/aniworld/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
src/de/aniworld/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
src/de/aniworld/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
src/de/aniworld/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/de/aniworld/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/de/aniworld/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 40 KiB |
|
@ -0,0 +1,30 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.aniworld
|
||||
|
||||
object AWConstants {
|
||||
const val NAME_DOOD = "Doodstream"
|
||||
const val NAME_STAPE = "Streamtape"
|
||||
const val NAME_VOE = "VOE"
|
||||
const val NAME_VIZ = "Vidoza"
|
||||
|
||||
const val URL_DOOD = "https://dood"
|
||||
const val URL_STAPE = "https://streamtape.com"
|
||||
const val URL_VOE = "https://voe"
|
||||
const val URL_VIZ = "https://vidoza"
|
||||
|
||||
val HOSTER_NAMES = arrayOf(NAME_VOE, NAME_DOOD, NAME_STAPE, NAME_VIZ)
|
||||
val HOSTER_URLS = arrayOf(URL_VOE, URL_DOOD, URL_STAPE, URL_VIZ)
|
||||
|
||||
const val KEY_GER_DUB = 1
|
||||
const val KEY_ENG_SUB = 2
|
||||
const val KEY_GER_SUB = 3
|
||||
|
||||
const val LANG_GER_SUB = "Deutscher Sub"
|
||||
const val LANG_GER_DUB = "Deutscher Dub"
|
||||
const val LANG_ENG_SUB = "Englischer Sub"
|
||||
|
||||
val LANGS = arrayOf(LANG_GER_SUB, LANG_GER_DUB, LANG_ENG_SUB)
|
||||
|
||||
const val PREFERRED_HOSTER = "preferred_hoster"
|
||||
const val PREFERRED_LANG = "preferred_lang"
|
||||
const val HOSTER_SELECTION = "hoster_selection"
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.aniworld
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.de.aniworld.extractors.VidozaExtractor
|
||||
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.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
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 kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AniWorld : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AniWorld"
|
||||
|
||||
override val baseUrl = "https://aniworld.to"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val id: Long = 8286900189409315836
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.addInterceptor(DdosGuardInterceptor(network.client))
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// ===== POPULAR ANIME =====
|
||||
override fun popularAnimeSelector(): String = "div.seriesListContainer div"
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
return GET("$baseUrl/beliebte-animes")
|
||||
}
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
val linkElement = element.selectFirst("a")!!
|
||||
anime.url = linkElement.attr("href")
|
||||
anime.thumbnail_url = baseUrl + linkElement.selectFirst("img")!!.attr("data-src")
|
||||
anime.title = element.selectFirst("h3")!!.text()
|
||||
return anime
|
||||
}
|
||||
|
||||
// ===== LATEST ANIME =====
|
||||
override fun latestUpdatesSelector(): String = "div.seriesListContainer div"
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/neu")
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
val linkElement = element.selectFirst("a")!!
|
||||
anime.url = linkElement.attr("href")
|
||||
anime.thumbnail_url = baseUrl + linkElement.selectFirst("img")!!.attr("data-src")
|
||||
anime.title = element.selectFirst("h3")!!.text()
|
||||
return anime
|
||||
}
|
||||
|
||||
// ===== SEARCH =====
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val headers = Headers.Builder()
|
||||
.add("Referer", "https://aniworld.to/search")
|
||||
.add("origin", baseUrl)
|
||||
.add("connection", "keep-alive")
|
||||
.add("user-agent", "Mozilla/5.0 (Linux; Android 12; Pixel 5 Build/SP2A.220405.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Safari/537.36")
|
||||
.add("Upgrade-Insecure-Requests", "1")
|
||||
.add("content-length", query.length.plus(8).toString())
|
||||
.add("cache-control", "")
|
||||
.add("accept", "*/*")
|
||||
.add("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("x-requested-with", "XMLHttpRequest")
|
||||
.build()
|
||||
return POST("$baseUrl/ajax/search", body = FormBody.Builder().add("keyword", query).build(), headers = headers)
|
||||
}
|
||||
override fun searchAnimeSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val body = response.body.string()
|
||||
val results = json.decodeFromString<JsonArray>(body)
|
||||
val animes = results.filter {
|
||||
val link = it.jsonObject["link"]!!.jsonPrimitive.content
|
||||
link.startsWith("/anime/stream/") &&
|
||||
link.count { c -> c == '/' } == 3
|
||||
}.map {
|
||||
animeFromSearch(it.jsonObject)
|
||||
}
|
||||
return AnimesPage(animes, false)
|
||||
}
|
||||
|
||||
private fun animeFromSearch(result: JsonObject): SAnime {
|
||||
val anime = SAnime.create()
|
||||
val title = result["title"]!!.jsonPrimitive.content
|
||||
val link = result["link"]!!.jsonPrimitive.content
|
||||
anime.title = title.replace("<em>", "").replace("</em>", "")
|
||||
val thumpage = client.newCall(GET("$baseUrl$link")).execute().asJsoup()
|
||||
anime.thumbnail_url = baseUrl +
|
||||
thumpage.selectFirst("div.seriesCoverBox img")!!.attr("data-src")
|
||||
anime.url = link
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
// ===== ANIME DETAILS =====
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = document.selectFirst("div.series-title h1 span")!!.text()
|
||||
anime.thumbnail_url = baseUrl +
|
||||
document.selectFirst("div.seriesCoverBox img")!!.attr("data-src")
|
||||
anime.genre = document.select("div.genres ul li").joinToString { it.text() }
|
||||
anime.description = document.selectFirst("p.seri_des")!!.attr("data-full-description")
|
||||
document.selectFirst("div.cast li:contains(Produzent:) ul")?.let {
|
||||
val author = it.select("li").joinToString { li -> li.text() }
|
||||
anime.author = author
|
||||
}
|
||||
anime.status = SAnime.UNKNOWN
|
||||
return anime
|
||||
}
|
||||
|
||||
// ===== EPISODE =====
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val seasonsElements = document.select("#stream > ul:nth-child(1) > li > a")
|
||||
if (seasonsElements.attr("href").contains("/filme")) {
|
||||
seasonsElements.forEach {
|
||||
val seasonEpList = parseMoviesFromSeries(it)
|
||||
episodeList.addAll(seasonEpList)
|
||||
}
|
||||
} else {
|
||||
seasonsElements.forEach {
|
||||
val seasonEpList = parseEpisodesFromSeries(it)
|
||||
episodeList.addAll(seasonEpList)
|
||||
}
|
||||
}
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
|
||||
val seasonId = element.attr("abs:href")
|
||||
val episodesHtml = client.newCall(GET(seasonId)).execute().asJsoup()
|
||||
val episodeElements = episodesHtml.select("table.seasonEpisodesList tbody tr")
|
||||
return episodeElements.map { episodeFromElement(it) }
|
||||
}
|
||||
|
||||
private fun parseMoviesFromSeries(element: Element): List<SEpisode> {
|
||||
val seasonId = element.attr("abs:href")
|
||||
val episodesHtml = client.newCall(GET(seasonId)).execute().asJsoup()
|
||||
val episodeElements = episodesHtml.select("table.seasonEpisodesList tbody tr")
|
||||
return episodeElements.map { episodeFromElement(it) }
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
if (element.select("td.seasonEpisodeTitle a").attr("href").contains("/film")) {
|
||||
val num = element.attr("data-episode-season-id")
|
||||
episode.name = "Film $num" + " : " + element.select("td.seasonEpisodeTitle a span").text()
|
||||
episode.episode_number = element.attr("data-episode-season-id").toFloat()
|
||||
episode.url = element.selectFirst("td.seasonEpisodeTitle a")!!.attr("href")
|
||||
} else {
|
||||
val season = element.select("td.seasonEpisodeTitle a").attr("href")
|
||||
.substringAfter("staffel-").substringBefore("/episode")
|
||||
val num = element.attr("data-episode-season-id")
|
||||
episode.name = "Staffel $season Folge $num" + " : " + element.select("td.seasonEpisodeTitle a span").text()
|
||||
episode.episode_number = element.select("td meta").attr("content").toFloat()
|
||||
episode.url = element.selectFirst("td.seasonEpisodeTitle a")!!.attr("href")
|
||||
}
|
||||
return episode
|
||||
}
|
||||
|
||||
// ===== VIDEO SOURCES =====
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val redirectlink = document.select("ul.row li")
|
||||
val videoList = mutableListOf<Video>()
|
||||
val hosterSelection = preferences.getStringSet(AWConstants.HOSTER_SELECTION, null)
|
||||
redirectlink.forEach {
|
||||
val langkey = it.attr("data-lang-key")
|
||||
val language = getlanguage(langkey)
|
||||
val redirectgs = baseUrl + it.selectFirst("a.watchEpisode")!!.attr("href")
|
||||
val hoster = it.select("a h4").text()
|
||||
if (hosterSelection != null) {
|
||||
when {
|
||||
hoster.contains("VOE") && hosterSelection.contains(AWConstants.NAME_VOE) -> {
|
||||
val url = client.newCall(GET(redirectgs)).execute().request.url.toString()
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(url, "($language) "))
|
||||
}
|
||||
|
||||
hoster.contains("Doodstream") && hosterSelection.contains(AWConstants.NAME_DOOD) -> {
|
||||
val quality = "Doodstream $language"
|
||||
val url = client.newCall(GET(redirectgs)).execute().request.url.toString()
|
||||
val video = DoodExtractor(client).videoFromUrl(url, quality)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
hoster.contains("Streamtape") && hosterSelection.contains(AWConstants.NAME_STAPE) -> {
|
||||
val quality = "Streamtape $language"
|
||||
val url = client.newCall(GET(redirectgs)).execute().request.url.toString()
|
||||
val video = StreamTapeExtractor(client).videoFromUrl(url, quality)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
hoster.contains("Vidoza") && hosterSelection.contains(AWConstants.NAME_VIZ) -> {
|
||||
val quality = "Vidoza $language"
|
||||
val url = client.newCall(GET(redirectgs)).execute().request.url.toString()
|
||||
val video = VidozaExtractor(client).videoFromUrl(url, quality)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun getlanguage(langkey: String): String? {
|
||||
when {
|
||||
langkey.contains("${AWConstants.KEY_GER_SUB}") -> {
|
||||
return "Deutscher Sub"
|
||||
}
|
||||
langkey.contains("${AWConstants.KEY_GER_DUB}") -> {
|
||||
return "Deutscher Dub"
|
||||
}
|
||||
langkey.contains("${AWConstants.KEY_ENG_SUB}") -> {
|
||||
return "Englischer Sub"
|
||||
}
|
||||
else -> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val hoster = preferences.getString(AWConstants.PREFERRED_HOSTER, null)
|
||||
val subPreference = preferences.getString(AWConstants.PREFERRED_LANG, "Sub")!!
|
||||
val hosterList = mutableListOf<Video>()
|
||||
val otherList = mutableListOf<Video>()
|
||||
if (hoster != null) {
|
||||
for (video in this) {
|
||||
if (video.url.contains(hoster)) {
|
||||
hosterList.add(video)
|
||||
} else {
|
||||
otherList.add(video)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
otherList += this
|
||||
}
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in hosterList) {
|
||||
if (video.quality.contains(subPreference)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
for (video in otherList) {
|
||||
if (video.quality.contains(subPreference)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
|
||||
return newList
|
||||
}
|
||||
|
||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
// ===== PREFERENCES ======
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val hosterPref = ListPreference(screen.context).apply {
|
||||
key = AWConstants.PREFERRED_HOSTER
|
||||
title = "Standard-Hoster"
|
||||
entries = AWConstants.HOSTER_NAMES
|
||||
entryValues = AWConstants.HOSTER_URLS
|
||||
setDefaultValue(AWConstants.URL_STAPE)
|
||||
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()
|
||||
}
|
||||
}
|
||||
val subPref = ListPreference(screen.context).apply {
|
||||
key = AWConstants.PREFERRED_LANG
|
||||
title = "Bevorzugte Sprache"
|
||||
entries = AWConstants.LANGS
|
||||
entryValues = AWConstants.LANGS
|
||||
setDefaultValue(AWConstants.LANG_GER_SUB)
|
||||
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()
|
||||
}
|
||||
}
|
||||
val hosterSelection = MultiSelectListPreference(screen.context).apply {
|
||||
key = AWConstants.HOSTER_SELECTION
|
||||
title = "Hoster auswählen"
|
||||
entries = AWConstants.HOSTER_NAMES
|
||||
entryValues = AWConstants.HOSTER_NAMES
|
||||
setDefaultValue(AWConstants.HOSTER_NAMES.toSet())
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(subPref)
|
||||
screen.addPreference(hosterPref)
|
||||
screen.addPreference(hosterSelection)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.aniworld
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
|
||||
class DdosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
|
||||
|
||||
private val cookieManager by lazy { CookieManager.getInstance() }
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val response = chain.proceed(originalRequest)
|
||||
|
||||
// Check if DDos-GUARD is on
|
||||
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
|
||||
return response
|
||||
}
|
||||
|
||||
response.close()
|
||||
val cookies = cookieManager.getCookie(originalRequest.url.toString())
|
||||
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
|
||||
if (!ddg2Cookie?.value.isNullOrEmpty()) {
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
|
||||
val newCookie = getNewCookie(originalRequest.url) ?: return chain.proceed(originalRequest)
|
||||
val newCookieHeader = buildString {
|
||||
(oldCookie + newCookie).forEachIndexed { index, cookie ->
|
||||
if (index > 0) append("; ")
|
||||
append(cookie.name).append('=').append(cookie.value)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(originalRequest.newBuilder().addHeader("cookie", newCookieHeader).build())
|
||||
}
|
||||
|
||||
fun getNewCookie(url: HttpUrl): Cookie? {
|
||||
val cookies = cookieManager.getCookie(url.toString())
|
||||
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
|
||||
if (!ddg2Cookie?.value.isNullOrEmpty()) {
|
||||
return ddg2Cookie
|
||||
}
|
||||
val wellKnown = client.newCall(GET("https://check.ddos-guard.net/check.js"))
|
||||
.execute().body.string()
|
||||
.substringAfter("'", "")
|
||||
.substringBefore("'", "")
|
||||
val checkUrl = "${url.scheme}://${url.host + wellKnown}"
|
||||
return client.newCall(GET(checkUrl)).execute().header("set-cookie")?.let {
|
||||
Cookie.parse(url, it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ERROR_CODES = listOf(403)
|
||||
private val SERVER_CHECK = listOf("ddos-guard")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.aniworld.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class VidozaExtractor(private val client: OkHttpClient) {
|
||||
|
||||
fun videoFromUrl(url: String, quality: String): Video? {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
val script = document.select("script:containsData(window.pData = {)")
|
||||
.firstOrNull()?.data()?.substringAfter("sourcesCode: [{ src: \"") ?: return null
|
||||
val videoUrl = script.substringAfter("sourcesCode: [{ src: \"").substringBefore("\", type:")
|
||||
return Video(url, quality, videoUrl)
|
||||
}
|
||||
}
|
16
src/de/cineclix/build.gradle
Normal file
|
@ -0,0 +1,16 @@
|
|||
ext {
|
||||
extName = 'CineClix'
|
||||
extClass = '.CineClix'
|
||||
extVersionCode = 14
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:mixdrop-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:playlist-utils'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/de/cineclix/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/de/cineclix/res/mipmap-hdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 842 B |
BIN
src/de/cineclix/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/de/cineclix/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/de/cineclix/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/de/cineclix/res/mipmap-mdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 448 B |
BIN
src/de/cineclix/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/de/cineclix/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/de/cineclix/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/de/cineclix/res/mipmap-xhdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/de/cineclix/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/de/cineclix/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/de/cineclix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
src/de/cineclix/res/mipmap-xxhdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/de/cineclix/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
src/de/cineclix/res/mipmap-xxhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
src/de/cineclix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
src/de/cineclix/res/mipmap-xxxhdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/de/cineclix/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/de/cineclix/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,364 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.cineclix
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.de.cineclix.extractors.StreamVidExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.de.cineclix.extractors.SuperVideoExtractor
|
||||
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.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class CineClix : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "CineClix"
|
||||
|
||||
override val baseUrl = "https://cineclix.de"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val json = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET(
|
||||
"$baseUrl/api/v1/channel/64?returnContentOnly=true&restriction=&order=rating:desc&paginate=simple&perPage=50&query=&page=$page",
|
||||
headers = Headers.headersOf("referer", "$baseUrl/movies?order=rating%3Adesc"),
|
||||
)
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val responseString = response.body.string()
|
||||
return parsePopularAnimeJson(responseString)
|
||||
}
|
||||
|
||||
private fun parsePopularAnimeJson(jsonLine: String?): AnimesPage {
|
||||
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val jO = jObject.jsonObject["pagination"]!!.jsonObject
|
||||
val nextPage = jO.jsonObject["next_page"]!!.jsonPrimitive.int
|
||||
// .substringAfter("page=").toInt()
|
||||
val page = jO.jsonObject["current_page"]!!.jsonPrimitive.int
|
||||
val hasNextPage = page < nextPage
|
||||
val array = jO["data"]!!.jsonArray
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
for (item in array) {
|
||||
val anime = SAnime.create()
|
||||
anime.title = item.jsonObject["name"]!!.jsonPrimitive.content
|
||||
val animeId = item.jsonObject["id"]!!.jsonPrimitive.content
|
||||
anime.setUrlWithoutDomain("$baseUrl/api/v1/titles/$animeId?load=images,genres,productionCountries,keywords,videos,primaryVideo,seasons,compactCredits")
|
||||
anime.thumbnail_url = item.jsonObject["poster"]?.jsonPrimitive?.content ?: item.jsonObject["backdrop"]?.jsonPrimitive?.content
|
||||
animeList.add(anime)
|
||||
}
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
// episodes
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request = GET(baseUrl + anime.url, headers = Headers.headersOf("referer", baseUrl))
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val responseString = response.body.string()
|
||||
val url = response.request.url.toString()
|
||||
return parseEpisodeAnimeJson(responseString, url)
|
||||
}
|
||||
|
||||
private fun parseEpisodeAnimeJson(jsonLine: String?, url: String): List<SEpisode> {
|
||||
val jsonData = jsonLine ?: return emptyList()
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val mId = jObject.jsonObject["title"]!!.jsonObject["id"]!!.jsonPrimitive.content
|
||||
val season = jObject.jsonObject["seasons"]?.jsonObject
|
||||
if (season != null) {
|
||||
val dataArray = season.jsonObject["data"]!!.jsonArray
|
||||
val next = season.jsonObject["next_page"]?.jsonPrimitive?.content
|
||||
if (next != null) {
|
||||
val seNextJsonData = client.newCall(GET("$baseUrl/api/v1/titles/$mId/seasons?perPage=8&query=&page=$next", headers = Headers.headersOf("referer", baseUrl))).execute().body.string()
|
||||
val seNextJObject = json.decodeFromString<JsonObject>(seNextJsonData)
|
||||
val seasonNext = seNextJObject.jsonObject["pagination"]!!.jsonObject
|
||||
val dataNextArray = seasonNext.jsonObject["data"]!!.jsonArray
|
||||
val dataAllArray = dataArray.plus(dataNextArray)
|
||||
for (item in dataAllArray) {
|
||||
val id = item.jsonObject["title_id"]!!.jsonPrimitive.content
|
||||
val num = item.jsonObject["number"]!!.jsonPrimitive.content
|
||||
val seUrl = "$baseUrl/api/v1/titles/$id/seasons/$num?load=episodes,primaryVideo"
|
||||
val seJsonData = client.newCall(GET(seUrl, headers = Headers.headersOf("referer", baseUrl))).execute().body.string()
|
||||
val seJObject = json.decodeFromString<JsonObject>(seJsonData)
|
||||
val epObject = seJObject.jsonObject["episodes"]!!.jsonObject
|
||||
val epDataArray = epObject.jsonObject["data"]!!.jsonArray.reversed()
|
||||
for (epItem in epDataArray) {
|
||||
val episode = SEpisode.create()
|
||||
val seNum = epItem.jsonObject["season_number"]!!.jsonPrimitive.content
|
||||
val epNum = epItem.jsonObject["episode_number"]!!.jsonPrimitive.content
|
||||
episode.name = "Staffel $seNum Folge $epNum : " + epItem.jsonObject["name"]!!.jsonPrimitive.content
|
||||
episode.episode_number = epNum.toFloat()
|
||||
val epId = epItem.jsonObject["title_id"]!!.jsonPrimitive.content
|
||||
episode.setUrlWithoutDomain("$baseUrl/api/v1/titles/$epId/seasons/$seNum/episodes/$epNum?load=videos,compactCredits,primaryVideo")
|
||||
episodeList.add(episode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (item in dataArray) {
|
||||
val id = item.jsonObject["title_id"]!!.jsonPrimitive.content
|
||||
val num = item.jsonObject["number"]!!.jsonPrimitive.content
|
||||
val seUrl = "$baseUrl/api/v1/titles/$id/seasons/$num?load=episodes,primaryVideo"
|
||||
val seJsonData = client.newCall(GET(seUrl, headers = Headers.headersOf("referer", baseUrl))).execute().body.string()
|
||||
val seJObject = json.decodeFromString<JsonObject>(seJsonData)
|
||||
val epObject = seJObject.jsonObject["episodes"]!!.jsonObject
|
||||
val epDataArray = epObject.jsonObject["data"]!!.jsonArray.reversed()
|
||||
for (epItem in epDataArray) {
|
||||
val episode = SEpisode.create()
|
||||
val seNum = epItem.jsonObject["season_number"]!!.jsonPrimitive.content
|
||||
val epNum = epItem.jsonObject["episode_number"]!!.jsonPrimitive.content
|
||||
episode.name = "Staffel $seNum Folge $epNum : " + epItem.jsonObject["name"]!!.jsonPrimitive.content
|
||||
episode.episode_number = epNum.toFloat()
|
||||
val epId = epItem.jsonObject["title_id"]!!.jsonPrimitive.content
|
||||
episode.setUrlWithoutDomain("$baseUrl/api/v1/titles/$epId/seasons/$seNum/episodes/$epNum?load=videos,compactCredits,primaryVideo")
|
||||
episodeList.add(episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val episode = SEpisode.create()
|
||||
episode.episode_number = 1F
|
||||
episode.name = "Film"
|
||||
episode.setUrlWithoutDomain(url)
|
||||
episodeList.add(episode)
|
||||
}
|
||||
return episodeList
|
||||
}
|
||||
|
||||
// Video Extractor
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
return GET(baseUrl + episode.url, headers = Headers.headersOf("referer", baseUrl))
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val responseString = response.body.string()
|
||||
val url = response.request.url.toString()
|
||||
return videosFromJson(responseString, url)
|
||||
}
|
||||
|
||||
private fun videosFromJson(jsonLine: String?, url: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val hosterSelection = preferences.getStringSet("hoster_selection", setOf("stape", "supv", "mix", "svid", "dood", "voe"))
|
||||
val jsonData = jsonLine ?: return emptyList()
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
if (url.contains("episodes")) {
|
||||
val epObject = jObject.jsonObject["episode"]!!.jsonObject
|
||||
val videoArray = epObject.jsonObject["videos"]!!.jsonArray
|
||||
for (item in videoArray) {
|
||||
val host = item.jsonObject["name"]!!.jsonPrimitive.content
|
||||
val eUrl = item.jsonObject["src"]!!.jsonPrimitive.content
|
||||
when {
|
||||
host.contains("streamtape") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape"
|
||||
val video = StreamTapeExtractor(client).videoFromUrl(eUrl, quality)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
host.contains("supervideo") && hosterSelection?.contains("supv") == true -> {
|
||||
val video = SuperVideoExtractor(client).videosFromUrl(eUrl)
|
||||
videoList.addAll(video)
|
||||
}
|
||||
host.contains("mixdrop") && hosterSelection?.contains("mix") == true -> {
|
||||
val video = MixDropExtractor(client).videoFromUrl(eUrl)
|
||||
videoList.addAll(video)
|
||||
}
|
||||
host.contains("streamvid") && hosterSelection?.contains("svid") == true -> {
|
||||
val video = StreamVidExtractor(client).videosFromUrl(eUrl)
|
||||
videoList.addAll(video)
|
||||
}
|
||||
host.contains("DoodStream") && hosterSelection?.contains("dood") == true -> {
|
||||
val video = DoodExtractor(client).videoFromUrl(eUrl)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
host.contains("VOE.SX") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(eUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val titleObject = jObject.jsonObject["title"]!!.jsonObject
|
||||
val videoArray = titleObject.jsonObject["videos"]!!.jsonArray
|
||||
for (item in videoArray) {
|
||||
val host = item.jsonObject["name"]!!.jsonPrimitive.content
|
||||
val fUrl = item.jsonObject["src"]!!.jsonPrimitive.content
|
||||
when {
|
||||
host.contains("streamtape") && hosterSelection?.contains("stape") == true -> {
|
||||
val quality = "Streamtape"
|
||||
val video = StreamTapeExtractor(client).videoFromUrl(fUrl, quality)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
host.contains("supervideo") && hosterSelection?.contains("supv") == true -> {
|
||||
val video = SuperVideoExtractor(client).videosFromUrl(fUrl)
|
||||
videoList.addAll(video)
|
||||
}
|
||||
host.contains("mixdrop") && hosterSelection?.contains("mix") == true -> {
|
||||
val video = MixDropExtractor(client).videoFromUrl(fUrl)
|
||||
videoList.addAll(video)
|
||||
}
|
||||
host.contains("streamvid") && hosterSelection?.contains("svid") == true -> {
|
||||
val video = StreamVidExtractor(client).videosFromUrl(fUrl)
|
||||
videoList.addAll(video)
|
||||
}
|
||||
host.contains("DoodStream") && hosterSelection?.contains("dood") == true -> {
|
||||
val video = DoodExtractor(client).videoFromUrl(fUrl)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
host.contains("VOE.SX") && hosterSelection?.contains("voe") == true -> {
|
||||
videoList.addAll(VoeExtractor(client).videosFromUrl(fUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val hoster = preferences.getString("preferred_hoster", null)
|
||||
if (hoster != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(hoster)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET(
|
||||
"$baseUrl/api/v1/search/$query?query=$query",
|
||||
headers = Headers.headersOf("referer", "$baseUrl/search/$query"),
|
||||
)
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val responseString = response.body.string()
|
||||
return parseSearchAnimeJson(responseString)
|
||||
}
|
||||
|
||||
private fun parseSearchAnimeJson(jsonLine: String?): AnimesPage {
|
||||
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val array = jObject["results"]!!.jsonArray
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
for (item in array) {
|
||||
val anime = SAnime.create()
|
||||
anime.title = item.jsonObject["name"]!!.jsonPrimitive.content
|
||||
val animeId = item.jsonObject["id"]!!.jsonPrimitive.content
|
||||
anime.setUrlWithoutDomain("$baseUrl/api/v1/titles/$animeId?load=images,genres,productionCountries,keywords,videos,primaryVideo,seasons,compactCredits")
|
||||
anime.thumbnail_url = item.jsonObject["poster"]?.jsonPrimitive?.content ?: item.jsonObject["backdrop"]?.jsonPrimitive?.content
|
||||
animeList.add(anime)
|
||||
}
|
||||
return AnimesPage(animeList, hasNextPage = false)
|
||||
}
|
||||
// Details
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request = GET(baseUrl + anime.url, headers = Headers.headersOf("referer", baseUrl))
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val responseString = response.body.string()
|
||||
return parseAnimeDetailsParseJson(responseString)
|
||||
}
|
||||
|
||||
private fun parseAnimeDetailsParseJson(jsonLine: String?): SAnime {
|
||||
val anime = SAnime.create()
|
||||
val jsonData = jsonLine ?: return anime
|
||||
val jObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
val jO = jObject.jsonObject["title"]!!.jsonObject
|
||||
anime.title = jO.jsonObject["name"]!!.jsonPrimitive.content
|
||||
anime.description = jO.jsonObject["description"]!!.jsonPrimitive.content
|
||||
val genArray = jO.jsonObject["genres"]!!.jsonArray
|
||||
val genres = mutableListOf<String>()
|
||||
for (item in genArray) {
|
||||
val genre = item.jsonObject["display_name"]!!.jsonPrimitive.content
|
||||
genres.add(genre)
|
||||
}
|
||||
anime.genre = genres.joinToString { it }
|
||||
anime.thumbnail_url = jO.jsonObject["poster"]?.jsonPrimitive?.content ?: jO.jsonObject["backdrop"]?.jsonPrimitive?.content
|
||||
return anime
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
// Preferences
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val hosterPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_hoster"
|
||||
title = "Standard-Hoster"
|
||||
entries = arrayOf("Streamtape", "SuperVideo", "MixDrop", "StreamVid", "DoodStream", "Voe")
|
||||
entryValues = arrayOf("https://streamtape", "https://supervideo", "https://mixdrop", "https://streamvid", "https://dood", "https://voe")
|
||||
setDefaultValue("https://streamtape")
|
||||
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()
|
||||
}
|
||||
}
|
||||
val subSelection = MultiSelectListPreference(screen.context).apply {
|
||||
key = "hoster_selection"
|
||||
title = "Hoster auswählen"
|
||||
entries = arrayOf("Streamtape", "SuperVideo", "MixDrop", "StreamVid", "DoodStream", "Voe")
|
||||
entryValues = arrayOf("stape", "supv", "mix", "svid", "dood", "voe")
|
||||
setDefaultValue(setOf("stape", "supv", "mix", "svid", "dood", "voe"))
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(hosterPref)
|
||||
screen.addPreference(subSelection)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.cineclix.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.OkHttpClient
|
||||
|
||||
class StreamVidExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
return runCatching {
|
||||
val doc = client.newCall(GET(url)).execute().asJsoup()
|
||||
|
||||
val script = doc.selectFirst("script:containsData(eval):containsData(p,a,c,k,e,d)")?.data()
|
||||
?.let(JsUnpacker::unpackAndCombine)
|
||||
?: return emptyList()
|
||||
val masterUrl = script.substringAfter("sources:[{src:\"").substringBefore("\",")
|
||||
PlaylistUtils(client).extractFromHls(masterUrl, videoNameGen = { "${prefix}StreamVid - $it" })
|
||||
}.getOrElse { emptyList() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package eu.kanade.tachiyomi.animeextension.de.cineclix.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.OkHttpClient
|
||||
|
||||
class SuperVideoExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
return runCatching {
|
||||
val doc = client.newCall(GET(url)).execute().asJsoup()
|
||||
|
||||
val script = doc.selectFirst("script:containsData(eval):containsData(p,a,c,k,e,d)")?.data()
|
||||
?.let(JsUnpacker::unpackAndCombine)
|
||||
?: return emptyList()
|
||||
val masterUrl = script.substringAfter("sources:[{file:\"").substringBefore("\"}]")
|
||||
PlaylistUtils(client).extractFromHls(masterUrl, videoNameGen = { "${prefix}SuperVideo - $it" })
|
||||
}.getOrElse { emptyList() }
|
||||
}
|
||||
}
|
18
src/de/cinemathek/build.gradle
Normal file
|
@ -0,0 +1,18 @@
|
|||
ext {
|
||||
extName = 'Cinemathek'
|
||||
extClass = '.Cinemathek'
|
||||
themePkg = 'dooplay'
|
||||
baseUrl = 'https://cinemathek.net'
|
||||
overrideVersionCode = 20
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
}
|
BIN
src/de/cinemathek/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
src/de/cinemathek/res/mipmap-hdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/de/cinemathek/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
src/de/cinemathek/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/de/cinemathek/res/mipmap-mdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/de/cinemathek/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/de/cinemathek/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/de/cinemathek/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/de/cinemathek/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
src/de/cinemathek/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 13 KiB |