From 10f7841215220dccbf851a16ad44a1fb1a071e03 Mon Sep 17 00:00:00 2001 From: Claudemirovsky <63046606+Claudemirovsky@users.noreply.github.com> Date: Mon, 25 Sep 2023 08:30:44 -0300 Subject: [PATCH] fix(en/oppaistream): Fix episode list and video extractor (#2247) Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- src/en/oppaistream/build.gradle | 3 +- .../en/oppaistream/OppaiStream.kt | 383 +++++++++--------- .../en/oppaistream/OppaiStreamFilters.kt | 133 +++--- .../en/oppaistream/dto/OppaiStreamDto.kt | 24 ++ 4 files changed, 293 insertions(+), 250 deletions(-) create mode 100644 src/en/oppaistream/src/eu/kanade/tachiyomi/animeextension/en/oppaistream/dto/OppaiStreamDto.kt diff --git a/src/en/oppaistream/build.gradle b/src/en/oppaistream/build.gradle index ab42a4cb0..88dfd93d5 100644 --- a/src/en/oppaistream/build.gradle +++ b/src/en/oppaistream/build.gradle @@ -1,13 +1,14 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) } ext { extName = 'Oppai Stream' pkgNameSuffix = 'en.oppaistream' extClass = '.OppaiStream' - extVersionCode = 2 + extVersionCode = 3 libVersion = '13' containsNsfw = true } diff --git a/src/en/oppaistream/src/eu/kanade/tachiyomi/animeextension/en/oppaistream/OppaiStream.kt b/src/en/oppaistream/src/eu/kanade/tachiyomi/animeextension/en/oppaistream/OppaiStream.kt index 2be4b943f..34dadfb30 100644 --- a/src/en/oppaistream/src/eu/kanade/tachiyomi/animeextension/en/oppaistream/OppaiStream.kt +++ b/src/en/oppaistream/src/eu/kanade/tachiyomi/animeextension/en/oppaistream/OppaiStream.kt @@ -1,9 +1,10 @@ package eu.kanade.tachiyomi.animeextension.en.oppaistream import android.app.Application -import android.content.SharedPreferences import androidx.preference.ListPreference import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.animeextension.en.oppaistream.dto.AnilistResponseDto import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState.Companion.STATE_EXCLUDE import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState.Companion.STATE_INCLUDE @@ -15,22 +16,19 @@ import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive import okhttp3.FormBody -import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.OkHttpClient 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 OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource { @@ -42,44 +40,43 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource { override val supportsLatest = true - override val client: OkHttpClient = network.cloudflareClient + override val client = network.cloudflareClient - override fun headersBuilder(): Headers.Builder = super.headersBuilder() - .add("Referer", baseUrl) + override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl) - private val preferences: SharedPreferences by lazy { + private val preferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } - // popular - override fun popularAnimeRequest(page: Int): Request { - return searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("views"))) - } + private val json: Json by injectLazy() - override fun popularAnimeSelector() = searchAnimeSelector() - - override fun popularAnimeNextPageSelector() = searchAnimeNextPageSelector() + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET("$baseUrl/$SEARCH_PATH?order=views&page=$page&limit=$SEARCH_LIMIT") override fun popularAnimeParse(response: Response) = searchAnimeParse(response) + override fun popularAnimeSelector() = searchAnimeSelector() + override fun popularAnimeFromElement(element: Element) = searchAnimeFromElement(element) - // latest - override fun latestUpdatesRequest(page: Int): Request { - return searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("uploaded"))) - } + override fun popularAnimeNextPageSelector() = null - override fun latestUpdatesSelector() = searchAnimeSelector() - - override fun latestUpdatesNextPageSelector() = searchAnimeNextPageSelector() + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$SEARCH_PATH?order=uploaded&page=$page&limit=$SEARCH_LIMIT") override fun latestUpdatesParse(response: Response) = searchAnimeParse(response) + override fun latestUpdatesSelector() = searchAnimeSelector() + override fun latestUpdatesFromElement(element: Element) = searchAnimeFromElement(element) - // search + override fun latestUpdatesNextPageSelector() = null + + // =============================== Search =============================== + override fun getFilterList() = FILTERS + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { - val url = "$baseUrl/actions/search.php".toHttpUrl().newBuilder().apply { + val url = "$baseUrl/$SEARCH_PATH".toHttpUrl().newBuilder().apply { addQueryParameter("text", query.trim()) filters.forEach { filter -> when (filter) { @@ -95,8 +92,8 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource { STATE_EXCLUDE -> genresExclude.add(genreState.value) } } - addQueryParameter("genres", genresInclude.joinToString(",") { it }) - addQueryParameter("blacklist", genresExclude.joinToString(",") { it }) + addQueryParameter("genres", genresInclude.joinToString(",")) + addQueryParameter("blacklist", genresExclude.joinToString(",")) } is StudioListFilter -> { addQueryParameter("studio", filter.state.filter { it.state }.joinToString(",") { it.value }) @@ -111,37 +108,173 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource { return GET(url, headers) } - override fun searchAnimeSelector() = "div.episode-shown" + override fun searchAnimeSelector() = "div.episode-shown > div > a" override fun searchAnimeNextPageSelector() = null override fun searchAnimeParse(response: Response): AnimesPage { - val document = response.asJsoup() + val document = response.use { it.asJsoup() } val elements = document.select(searchAnimeSelector()) - val mangas = elements.map { element -> - searchAnimeFromElement(element) - }.distinctBy { it.title } + val anime = elements.map(::searchAnimeFromElement).distinctBy { it.title } val hasNextPage = elements.size >= SEARCH_LIMIT - return AnimesPage(mangas, hasNextPage) + return AnimesPage(anime, hasNextPage) } - override fun searchAnimeFromElement(element: Element): SAnime { - return SAnime.create().apply { - thumbnail_url = element.select("img.cover-img-in").attr("abs:src") - title = element.select(".title-ep").text() - .replace(TITLE_CLEANUP_REGEX, "") - setUrlWithoutDomain(element.selectFirst("a")!!.attr("href")) + override fun searchAnimeFromElement(element: Element) = SAnime.create().apply { + thumbnail_url = element.selectFirst("img.cover-img-in")?.attr("abs:src") + title = element.selectFirst(".title-ep")!!.text().replace(TITLE_CLEANUP_REGEX, "") + setUrlWithoutDomain(element.attr("href")) + } + + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document) = SAnime.create().apply { + // Fetch from from Anilist when "Anilist Cover" is selected in settings + val name = document.selectFirst("div.episode-info > h1")!!.text().substringBefore(" Ep ") + title = name + description = document.selectFirst("div.description")?.text()?.substringBeforeLast(" Watch ") + genre = document.select("div.tags a").joinToString { it.text() } + val studios = document.select("div.episode-info a.red").eachText() + artist = studios.joinToString() + + val useAnilistCover = preferences.getBoolean(PREF_ANILIST_COVER_KEY, PREF_ANILIST_COVER_DEFAULT) + val thumbnailUrl = if (useAnilistCover) { + val newTitle = name.replace(Regex("[^a-zA-Z0-9\\s!.:\"]"), " ") + runCatching { fetchThumbnailUrlByTitle(newTitle) }.getOrNull() + } else { + null // Use default cover (episode preview) } + + // Match local studios with anilist studios to increase the accuracy of the poster + val matchedStudio = thumbnailUrl?.second?.find { it in studios } + + thumbnail_url = matchedStudio?.let { thumbnailUrl.first } + ?: document.selectFirst("video#episode")?.attr("poster") + } + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response): List { + val doc = response.use { it.asJsoup() } + return buildList { + doc.select(episodeListSelector()) + .map(::episodeFromElement) + .let(::addAll) + + add( + SEpisode.create().apply { + setUrlWithoutDomain(doc.location()) + val num = doc.selectFirst("div.episode-info > h1")!!.text().substringAfter(" Ep ") + name = "Episode $num" + episode_number = num.toFloatOrNull() ?: 1F + scanlator = doc.selectFirst("div.episode-info a.red")?.text() + }, + ) + }.sortedByDescending { it.episode_number } + } + + override fun episodeListSelector() = "div.more-same-eps > div > div > a" + + override fun episodeFromElement(element: Element) = SEpisode.create().apply { + setUrlWithoutDomain(element.attr("href")) + val num = element.selectFirst("font.ep")?.text() ?: "1" + name = "Episode $num" + episode_number = num.toFloatOrNull() ?: 1F + scanlator = element.selectFirst("h6 > a")?.text() + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List