Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/de/animebase/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ ext {
apply from: "$rootDir/common.gradle"

dependencies {
implementation(project(":lib:lulu-extractor"))
implementation(project(":lib:upstream-extractor"))
implementation(project(":lib:voe-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:vidguard-extractor"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package eu.kanade.tachiyomi.animeextension.de.animebase

import android.app.Application
import android.net.Uri
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.de.animebase.extractors.UnpackerExtractor
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.vidguardextractor.VidGuardExtractor
import eu.kanade.tachiyomi.lib.luluextractor.LuluExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
Expand All @@ -41,22 +46,44 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}

// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/favorites", headers)
val limit = 24

override fun popularAnimeRequest(page: Int) = GET("$baseUrl/api/top-on-animebase?offset=${limit * (page - 1)}&limit=$limit", headers)

override fun popularAnimeParse(response: Response): AnimesPage {
val json = Json.parseToJsonElement(response.body.string())
val animes = mutableListOf<SAnime>()
for (anime in json.jsonObject["data"]?.jsonArray!!.map { it.jsonObject }) {
try {
animes += SAnime.create().apply {
val cat = anime["category"]!!.jsonPrimitive.content
val slug = anime["nameSlug"]!!.jsonPrimitive.content
setUrlWithoutDomain("/$cat/$slug")
thumbnail_url = anime["image"]!!.jsonPrimitive.content
title = anime["name"]!!.jsonPrimitive.content
}
} catch (e: Exception) {
Log.e("animebase", "Error parsing anime from JSON: $anime", e)
}
}
val hasNext = json.jsonObject["meta"]!!.jsonObject["hasMore"]!!.jsonPrimitive.content.toBoolean()
return AnimesPage(animes, hasNext)
}

override fun popularAnimeSelector() = "div.table-responsive > a"
override fun popularAnimeSelector() = "div.grid > div > 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()
thumbnail_url = element.selectFirst("div img")?.absUrl("src")
title = element.selectFirst("div > div.font-medium, div 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 latestUpdatesSelector() = "div.hidden > div > a"

override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)

Expand All @@ -65,28 +92,20 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =============================== 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() -> {
if (query.length < 3) throw Exception("Please enter at least 3 characters for the search")
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)
.add("query", query)
.build()
// make a simple request to get the xsrf token
val token = client.cookieJar.loadForRequest(baseUrl.toHttpUrl()).find { it.name == "XSRF-TOKEN" }
if (token == null) throw Exception("XSRF token not found in cookies")
val headers = headers.newBuilder().add("X-XSRF-TOKEN", token.value).build()
POST("$baseUrl/api/search", headers, body)
}

else -> {
Expand All @@ -96,19 +115,21 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}

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)
val body = Json.parseToJsonElement(response.body.string()).jsonObject
if (body["success"]?.jsonPrimitive?.booleanOrNull != true) {
throw Exception("Search API returned success=false: ${body["message"]?.jsonPrimitive?.contentOrNull}")
}
val animes = mutableListOf<SAnime>()
for (anime in body["data"]!!.jsonObject["series"]!!.jsonArray.map { it.jsonObject }) {
animes += SAnime.create().apply {
val cat = anime["category"]!!.jsonPrimitive.content
val slug = anime["nameSlug"]!!.jsonPrimitive.content
setUrlWithoutDomain("/$cat/$slug")
thumbnail_url = anime["image"]!!.jsonPrimitive.content
title = anime["name"]!!.jsonPrimitive.content
}
}
return AnimesPage(animes, false)
}

override fun searchAnimeSelector() = "div.col-lg-9.col-md-8 div.box-body > a"
Expand All @@ -119,27 +140,20 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {

// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
// could also be done with data-page.value.props.serie...
setUrlWithoutDomain(document.location())

val boxBody = document.selectFirst("div.box-body.box-profile > center")!!
title = boxBody.selectFirst("h3")!!.text()
val boxBody = document.selectFirst(".border-zinc-700.border.p-6.shadow-lg.rounded-xl.to-zinc-950.from-zinc-800.bg-gradient-to-br")!!
title = boxBody.selectFirst("h2")!!.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)
val statusBadge = boxBody.selectFirst("div > span.inline-block")
status = parseStatus(statusBadge?.text())

infosDiv.getInfo("Originalname")?.also { append("\nOriginal name: $it") }
infosDiv.getInfo("Erscheinungsjahr")?.also { append("\nErscheinungsjahr: $it") }
}
description = boxBody.selectFirst("div > div > div > div.text-zinc-300")?.text()
}

private fun parseStatus(status: String?) = when (status?.orEmpty()) {
private fun parseStatus(status: String?) = when (status.orEmpty()) {
"Laufend" -> SAnime.ONGOING
"Abgeschlossen" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
Expand All @@ -149,83 +163,69 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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 episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
val data = doc.selectFirst("div#app")?.attr("data-page") ?: return emptyList()
val serie = Json.parseToJsonElement(data).jsonObject["props"]?.jsonObject?.get("serie") ?: return emptyList()
val episodes = serie.jsonObject["episodes"]?.jsonArray ?: return emptyList()
val episodeList = mutableListOf<SEpisode>()
for (episode in episodes) {
episodeList += SEpisode.create().apply {
name = episode.jsonObject["name"]?.jsonPrimitive?.content ?: "Unknown Episode"
scanlator = when (episode.jsonObject["dubsub"]?.jsonPrimitive?.intOrNull) {
0 -> "Subbed"
1 -> "Dubbed"
else -> "Unknown"
}

override fun episodeListSelector() = "div.tab-content > div > div.panel"
episode_number = episode.jsonObject["episode"]?.jsonPrimitive?.intOrNull?.toFloat() ?: 0.0f

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"
setUrlWithoutDomain(response.request.url.encodedPath + "?id=" + episode.jsonObject["id"]?.jsonPrimitive?.content)
}
}

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")
return episodeList.sortedByDescending { it.episode_number }
}

// ============================ 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 episodeListSelector() = throw UnsupportedOperationException("Not used")

override fun episodeFromElement(element: Element) = throw UnsupportedOperationException("Not used")

// ============================ Video Links =============================
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"
Log.d("animebase", "videoListParse called")
val data = doc.selectFirst("div#app")?.attr("data-page") ?: return emptyList()
val serie = Json.parseToJsonElement(data).jsonObject["props"]?.jsonObject?.get("serie") ?: return emptyList()
val episodes = serie.jsonObject["episodes"]?.jsonArray ?: return emptyList()
Log.d("animebase", "searching for ID")
val wantedId = response.request.url.queryParameter("id") ?: return emptyList()
val episode = episodes.find { it.jsonObject["id"]?.jsonPrimitive?.content == wantedId } ?: return emptyList()
Log.d("animebase", "found ID: $wantedId")
val videos = mutableListOf<Video>()
val links = episode.jsonObject.entries.filter { it.key.startsWith("link") }.map { it.value }
for (link in links) {
val url = link.jsonPrimitive.content
when (Uri.parse(url).host) {
"lulustream.com", "luluvid.com" -> {
videos += LuluExtractor(client, headers).videosFromUrl(url, "")
}
"voe.sx" -> {
videos += VoeExtractor(client, headers).videosFromUrl(url, "")
}
"byse.sx" -> {
videos += UpstreamExtractor(client).videosFromUrl(url, "")
}
null -> continue
else -> {
Log.w(
"animebase",
"Unknown host for video link: ${Uri.parse(link.jsonPrimitive.content).host}",
)
}

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, headers) }
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()
}
return videos
}

override fun List<Video>.sort(): List<Video> {
Expand Down
Loading