diff --git a/src/de/animebase/build.gradle b/src/de/animebase/build.gradle index d26db0008a..70bcb50eb4 100644 --- a/src/de/animebase/build.gradle +++ b/src/de/animebase/build.gradle @@ -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")) diff --git a/src/de/animebase/src/eu/kanade/tachiyomi/animeextension/de/animebase/AnimeBase.kt b/src/de/animebase/src/eu/kanade/tachiyomi/animeextension/de/animebase/AnimeBase.kt index 7c6a21c866..96b4c90b89 100644 --- a/src/de/animebase/src/eu/kanade/tachiyomi/animeextension/de/animebase/AnimeBase.kt +++ b/src/de/animebase/src/eu/kanade/tachiyomi/animeextension/de/animebase/AnimeBase.kt @@ -1,9 +1,10 @@ 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 @@ -11,13 +12,17 @@ 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 @@ -41,14 +46,36 @@ 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() + 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 @@ -56,7 +83,7 @@ class AnimeBase : ConfigurableAnimeSource, ParsedAnimeHttpSource() { // =============================== 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) @@ -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 -> { @@ -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() + 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" @@ -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 @@ -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 { + 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() + 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