fix(en/oppaistream): Fix episode list and video extractor (#2247)

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
Claudemirovsky
2023-09-25 08:30:44 -03:00
committed by GitHub
parent 280de3fa38
commit 10f7841215
4 changed files with 293 additions and 250 deletions

View File

@ -1,13 +1,14 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
} }
ext { ext {
extName = 'Oppai Stream' extName = 'Oppai Stream'
pkgNameSuffix = 'en.oppaistream' pkgNameSuffix = 'en.oppaistream'
extClass = '.OppaiStream' extClass = '.OppaiStream'
extVersionCode = 2 extVersionCode = 3
libVersion = '13' libVersion = '13'
containsNsfw = true containsNsfw = true
} }

View File

@ -1,9 +1,10 @@
package eu.kanade.tachiyomi.animeextension.en.oppaistream package eu.kanade.tachiyomi.animeextension.en.oppaistream
import android.app.Application import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen 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.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState.Companion.STATE_EXCLUDE import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState.Companion.STATE_EXCLUDE
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState.Companion.STATE_INCLUDE 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.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json 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.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource { class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
@ -42,44 +40,43 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client = network.cloudflareClient
override fun headersBuilder(): Headers.Builder = super.headersBuilder() override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
.add("Referer", baseUrl)
private val preferences: SharedPreferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// popular private val json: Json by injectLazy()
override fun popularAnimeRequest(page: Int): Request {
return searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("views")))
}
override fun popularAnimeSelector() = searchAnimeSelector() // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/$SEARCH_PATH?order=views&page=$page&limit=$SEARCH_LIMIT")
override fun popularAnimeNextPageSelector() = searchAnimeNextPageSelector()
override fun popularAnimeParse(response: Response) = searchAnimeParse(response) override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
override fun popularAnimeSelector() = searchAnimeSelector()
override fun popularAnimeFromElement(element: Element) = searchAnimeFromElement(element) override fun popularAnimeFromElement(element: Element) = searchAnimeFromElement(element)
// latest override fun popularAnimeNextPageSelector() = null
override fun latestUpdatesRequest(page: Int): Request {
return searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("uploaded")))
}
override fun latestUpdatesSelector() = searchAnimeSelector() // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$SEARCH_PATH?order=uploaded&page=$page&limit=$SEARCH_LIMIT")
override fun latestUpdatesNextPageSelector() = searchAnimeNextPageSelector()
override fun latestUpdatesParse(response: Response) = searchAnimeParse(response) override fun latestUpdatesParse(response: Response) = searchAnimeParse(response)
override fun latestUpdatesSelector() = searchAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = searchAnimeFromElement(element) 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 { 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()) addQueryParameter("text", query.trim())
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
@ -95,8 +92,8 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
STATE_EXCLUDE -> genresExclude.add(genreState.value) STATE_EXCLUDE -> genresExclude.add(genreState.value)
} }
} }
addQueryParameter("genres", genresInclude.joinToString(",") { it }) addQueryParameter("genres", genresInclude.joinToString(","))
addQueryParameter("blacklist", genresExclude.joinToString(",") { it }) addQueryParameter("blacklist", genresExclude.joinToString(","))
} }
is StudioListFilter -> { is StudioListFilter -> {
addQueryParameter("studio", filter.state.filter { it.state }.joinToString(",") { it.value }) addQueryParameter("studio", filter.state.filter { it.state }.joinToString(",") { it.value })
@ -111,37 +108,173 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
return GET(url, headers) return GET(url, headers)
} }
override fun searchAnimeSelector() = "div.episode-shown" override fun searchAnimeSelector() = "div.episode-shown > div > a"
override fun searchAnimeNextPageSelector() = null override fun searchAnimeNextPageSelector() = null
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.use { it.asJsoup() }
val elements = document.select(searchAnimeSelector()) val elements = document.select(searchAnimeSelector())
val mangas = elements.map { element -> val anime = elements.map(::searchAnimeFromElement).distinctBy { it.title }
searchAnimeFromElement(element)
}.distinctBy { it.title }
val hasNextPage = elements.size >= SEARCH_LIMIT val hasNextPage = elements.size >= SEARCH_LIMIT
return AnimesPage(mangas, hasNextPage) return AnimesPage(anime, hasNextPage)
} }
override fun searchAnimeFromElement(element: Element): SAnime { override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
return SAnime.create().apply { thumbnail_url = element.selectFirst("img.cover-img-in")?.attr("abs:src")
thumbnail_url = element.select("img.cover-img-in").attr("abs:src") title = element.selectFirst(".title-ep")!!.text().replace(TITLE_CLEANUP_REGEX, "")
title = element.select(".title-ep").text() setUrlWithoutDomain(element.attr("href"))
.replace(TITLE_CLEANUP_REGEX, "") }
setUrlWithoutDomain(element.selectFirst("a")!!.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<SEpisode> {
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<Video> {
val doc = response.use { it.asJsoup() }
val script = doc.selectFirst("script:containsData(var availableres)")!!.data()
val subtitles = doc.select("track[kind=captions]").map {
Track(it.attr("src"), it.attr("label"))
}
return script.substringAfter("var availableres = {").substringBefore('}')
.split(',')
.map {
val (resolution, url) = it.replace("\"", "").replace("\\", "").split(':', limit = 2)
val fixedResolution = when (resolution) {
"4k" -> "2160p"
else -> "${resolution}p"
}
Video(url, fixedResolution, url, headers, subtitles)
}
}
override fun videoListSelector() = throw Exception("Not used")
override fun videoFromElement(element: Element) = throw Exception("Not used")
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
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)
SwitchPreferenceCompat(screen.context).apply {
key = PREF_ANILIST_COVER_KEY
title = PREF_ANILIST_COVER_TITLE
summary = PREF_ANILIST_COVER_SUMMARY
setDefaultValue(PREF_ANILIST_COVER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_COVER_QUALITY_KEY
title = PREF_COVER_QUALITY_TITLE
entries = PREF_COVER_QUALITY_ENTRIES
entryValues = PREF_COVER_QUALITY_VALUES
setDefaultValue(PREF_COVER_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 ==============================
private inline fun <reified T> Response.parseAs(): T {
return use { it.body.string() }.let(json::decodeFromString)
} }
// Function to fetch thumbnail URL using AniList GraphQL API // Function to fetch thumbnail URL using AniList GraphQL API
// Only use in animeDetailsParse. // Only use in animeDetailsParse.
private fun fetchThumbnailUrlByTitle(title: String): Pair<String?, MutableList<String>>? { private fun fetchThumbnailUrlByTitle(title: String): Pair<String, List<String>>? {
val client = OkHttpClient()
val query = """ val query = """
query { query {
Media(search: "$title", type: ANIME, isAdult: true) { Media(search: "$title", type: ANIME, isAdult: true) {
@ -162,161 +295,49 @@ class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
.add("query", query) .add("query", query)
.build() .build()
val request = Request.Builder() val request = POST("https://graphql.anilist.co", body = requestBody)
.url("https://graphql.anilist.co")
.post(requestBody)
.build()
val response = client.newCall(request).execute() val response = client.newCall(request).execute()
val responseString = response.body?.string()
return parseThumbnailUrlFromResponse(responseString) return parseThumbnailUrlFromObject(response.parseAs<AnilistResponseDto>())
} }
private fun parseThumbnailUrlFromResponse(responseString: String?): Pair<String?, MutableList<String>>? { private fun parseThumbnailUrlFromObject(obj: AnilistResponseDto): Pair<String, List<String>>? {
val responseJson = Json.parseToJsonElement(responseString ?: "") as? JsonObject ?: return null val media = obj.data.media ?: return null
val data = responseJson["data"] as? JsonObject ?: return null
val media = data["Media"] as? JsonObject ?: return null
val coverImage = media["coverImage"] as? JsonObject ?: return null
val coverURL = when (preferences.getString(PREF_COVER_QUALITY, "large")) { val coverURL = when (preferences.getString(PREF_COVER_QUALITY_KEY, PREF_COVER_QUALITY_DEFAULT)) {
"extraLarge" -> coverImage["extraLarge"]?.jsonPrimitive?.content "extraLarge" -> media.coverImage.extraLarge
"large" -> coverImage["large"]?.jsonPrimitive?.content else -> media.coverImage.large
else -> null
} }
val studiosList = mutableListOf<String>() val studiosList = media.studios.names
val studios = media["studios"]?.jsonObject?.get("nodes")?.jsonArray
studios?.forEach { studio ->
val name = studio.jsonObject["name"]?.jsonPrimitive?.content
if (!name.isNullOrEmpty()) {
studiosList.add(name)
}
}
return Pair(coverURL, studiosList) return Pair(coverURL, studiosList)
} }
override fun getFilterList() = FILTERS
// details
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
// Fetch from from Anilist when "Anilist Cover" is selected in settings
val selectedCoverSource = preferences.getString(PREF_COVER_SOURCE, "Anilist-Cover")
val newTitle = document.select("div.effect-title").text().replace(Regex("[^a-zA-Z0-9\\s!.:\"]"), " ")
val thumbnailUrl = if (selectedCoverSource == "Anilist-Cover") {
fetchThumbnailUrlByTitle(newTitle)
} else {
null // Use default cover
}
title = document.select("div.effect-title").text()
description = document.select("div.description").text()
genre = document.select("div.tags a").joinToString { it.text() }
author = document.select("div.content a.red").joinToString { it.text() }
// thumbnail_url = document.select("#player").attr("data-poster")
// Match local studios with anilist studios to increase the accuracy of the poster
val matchingStudios = document.select("div.content a.red").map { it.text() }
val matchedStudio = thumbnailUrl?.second?.find { it in matchingStudios }
thumbnail_url = if (matchedStudio != null) thumbnailUrl.first else document.select("#player").attr("data-poster")
}
}
// episodes
override fun episodeListSelector() = "div.ep-swap a"
override fun episodeListParse(response: Response): List<SEpisode> {
return super.episodeListParse(response).reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = "Episode " + element.text()
}
}
override fun videoListSelector() = "#player source"
override fun videoFromElement(element: Element): Video {
val url = element.attr("src")
val quality = element.attr("size") + "p"
val subtitles = element.parent()!!.select("track").map {
Track(it.attr("src"), it.attr("label"))
}
return Video(
url = url,
quality = quality,
videoUrl = url,
subtitleTracks = subtitles,
)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY, "720")!!
return this.sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used")
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY
title = PREF_QUALITY_TITLE
entries = arrayOf("2160p", "1080p", "720p")
entryValues = arrayOf("2160", "1080", "720")
setDefaultValue("720")
summary = "%s"
}.let {
screen.addPreference(it)
}
// Add cover source preference
ListPreference(screen.context).apply {
key = PREF_COVER_SOURCE
title = PREF_COVER_SOURCE_TITLE
entries = arrayOf("Default Cover", "Anilist Cover")
entryValues = arrayOf("Default-Cover", "Anilist-Cover")
summary = "This feature is experimental. It uses a covers for Anilist. If you see the default cover after switching to AniList cover, try clearing the cache in Settings > Advanced > Clear Anime Database > Oppai Steam. It only fetch Anilist covers in anime details page."
setDefaultValue("Anilist-Cover")
}.let {
screen.addPreference(it)
}
// Add cover source preference
ListPreference(screen.context).apply {
key = PREF_COVER_QUALITY
title = PREF_COVER_QUALITY_TITLE
entries = arrayOf("Extra Large", "Large")
entryValues = arrayOf("extraLarge", "large")
summary = "%s"
setDefaultValue("large")
}.let {
screen.addPreference(it)
}
}
companion object { companion object {
private const val SEARCH_PATH = "actions/search.php"
private const val SEARCH_LIMIT = 36 private const val SEARCH_LIMIT = 36
private val TITLE_CLEANUP_REGEX = Regex("""\s+\d+$""") private val TITLE_CLEANUP_REGEX = Regex("""\s+\d+$""")
private const val PREF_QUALITY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality" private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("2160p", "1080p", "720p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
private const val PREF_COVER_SOURCE = "preferred_cover_source" private const val PREF_ANILIST_COVER_KEY = "preferred_anilist_cover"
private const val PREF_COVER_SOURCE_TITLE = "Preferred cover source - Beta" private const val PREF_ANILIST_COVER_TITLE = "Use Anilist as cover source - Beta"
private const val PREF_ANILIST_COVER_DEFAULT = true
private const val PREF_ANILIST_COVER_SUMMARY = "This feature is experimental. " +
"It enables fetching covers from Anilist. If you see the default cover " +
"after switching to AniList cover, try clearing the cache in " +
"Settings > Advanced > Clear Anime Database > Oppai Steam. It only fetch Anilist covers in anime details page."
private const val PREF_COVER_QUALITY = "preferred_cover_quality" private const val PREF_COVER_QUALITY_KEY = "preferred_cover_quality"
private const val PREF_COVER_QUALITY_TITLE = "Preferred cover quality - Beta" private const val PREF_COVER_QUALITY_TITLE = "Preferred Anilist cover quality - Beta"
private const val PREF_COVER_QUALITY_DEFAULT = "large"
private val PREF_COVER_QUALITY_ENTRIES = arrayOf("Extra Large", "Large")
private val PREF_COVER_QUALITY_VALUES = arrayOf("extraLarge", "large")
} }
} }

View File

@ -6,16 +6,14 @@ import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
open class SelectFilter( open class SelectFilter(
displayName: String, displayName: String,
private val vals: Array<Pair<String, String>>, private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>( ) : AnimeFilter.Select<String>(
displayName, displayName,
vals.map { it.first }.toTypedArray(), vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) { ) {
fun selectedValue() = vals[state].second fun selectedValue() = vals[state].second
} }
class OrderByFilter(defaultOrder: String? = null) : SelectFilter( class OrderByFilter : SelectFilter(
"Sort By", "Sort By",
arrayOf( arrayOf(
Pair("A-Z", "az"), Pair("A-Z", "az"),
@ -27,7 +25,6 @@ class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
Pair("Recently Uploaded", "uploaded"), Pair("Recently Uploaded", "uploaded"),
Pair("Randomize", "random"), Pair("Randomize", "random"),
), ),
defaultOrder,
) )
class TriFilter(name: String, val value: String) : AnimeFilter.TriState(name) class TriFilter(name: String, val value: String) : AnimeFilter.TriState(name)
@ -35,96 +32,96 @@ class TriFilter(name: String, val value: String) : AnimeFilter.TriState(name)
private fun getGenreList(): List<TriFilter> = listOf( private fun getGenreList(): List<TriFilter> = listOf(
TriFilter("4k", "4k"), TriFilter("4k", "4k"),
TriFilter("Ahegao", "ahegao"), TriFilter("Ahegao", "ahegao"),
TriFilter("Anal", "anal"),
TriFilter("Armpit Masturbation", "armpitmasturbation"),
TriFilter("BDSM", "bdsm"),
TriFilter("Beach", "beach"), TriFilter("Beach", "beach"),
TriFilter("Big Boobs", "bigboobs"),
TriFilter("Black Hair", "blackhair"),
TriFilter("Blonde Hair", "blondehair"),
TriFilter("BlowJob", "blowjob"),
TriFilter("Blue Hair", "bluehair"),
TriFilter("Bondage", "bondage"),
TriFilter("BoobJob", "boobjob"),
TriFilter("Brown Hair", "brownhair"),
TriFilter("Censored", "censored"), TriFilter("Censored", "censored"),
TriFilter("Comedy", "comedy"), TriFilter("Comedy", "comedy"),
TriFilter("Cosplay", "cosplay"),
TriFilter("Cowgirl", "cowgirl"),
TriFilter("Creampie", "creampie"),
TriFilter("Dark Skin", "darkskin"),
TriFilter("Demon", "demon"),
TriFilter("Doggy", "doggy"),
TriFilter("Dominant Girl", "dominantgirl"),
TriFilter("Double Penetration", "doublepenetration"),
TriFilter("Elf", "elf"),
TriFilter("Facial", "facial"),
TriFilter("Fantasy", "fantasy"), TriFilter("Fantasy", "fantasy"),
TriFilter("Filmed", "filmed"), TriFilter("Filmed", "filmed"),
TriFilter("FootJob", "footjob"),
TriFilter("Futanari", "futanari"),
TriFilter("Gangbang", "gangbang"),
TriFilter("Girls Only", "girlsonly"),
TriFilter("Glasses", "glasses"),
TriFilter("Green Hair", "greenhair"),
TriFilter("Gyaru", "gyaru"),
TriFilter("HD", "hd"), TriFilter("HD", "hd"),
TriFilter("HandJob", "handjob"),
TriFilter("Harem", "harem"), TriFilter("Harem", "harem"),
TriFilter("Horror", "horror"), TriFilter("Horror", "horror"),
TriFilter("Incest", "incest"), TriFilter("Incest", "incest"),
TriFilter("Inflation", "inflation"), TriFilter("Inflation", "inflation"),
TriFilter("Inverted Nipples", "invertednipples"),
TriFilter("Lactation", "lactation"), TriFilter("Lactation", "lactation"),
TriFilter("Loli", "loli"),
TriFilter("Maid", "maid"),
TriFilter("Masturbation", "masturbation"),
TriFilter("Milf", "milf"),
TriFilter("Mind Break", "mindbreak"), TriFilter("Mind Break", "mindbreak"),
TriFilter("Mind Control", "mindcontrol"), TriFilter("Mind Control", "mindcontrol"),
TriFilter("Monster", "monster"),
TriFilter("POV", "pov"),
TriFilter("Plot", "plot"),
TriFilter("Scat", "scat"),
TriFilter("Softcore", "softcore"),
TriFilter("Tentacle", "tentacle"),
TriFilter("Uncensored", "uncensored"),
TriFilter("Vanilla", "vanilla"),
TriFilter("Watersports", "watersports"),
TriFilter("X-Ray", "x-ray"),
TriFilter("Yaoi", "yaoi"),
TriFilter("Yuri", "yuri"),
TriFilter("Anal", "anal"),
TriFilter("Armpit Masturbation", "armpitmasturbation"),
TriFilter("BDSM", "bdsm"),
TriFilter("BlowJob", "blowjob"),
TriFilter("Bondage", "bondage"),
TriFilter("BoobJob", "boobjob"),
TriFilter("Cowgirl", "cowgirl"),
TriFilter("Creampie", "creampie"),
TriFilter("Doggy", "doggy"),
TriFilter("Double Penetration", "doublepenetration"),
TriFilter("Facial", "facial"),
TriFilter("FootJob", "footjob"),
TriFilter("Gangbang", "gangbang"),
TriFilter("Girls Only", "girlsonly"),
TriFilter("HandJob", "handjob"),
TriFilter("Masturbation", "masturbation"),
TriFilter("Missionary", "missionary"), TriFilter("Missionary", "missionary"),
TriFilter("Monster", "monster"),
TriFilter("NTR", "ntr"), TriFilter("NTR", "ntr"),
TriFilter("Nekomimi", "nekomimi"),
TriFilter("Nurse", "nurse"),
TriFilter("Old", "old"),
TriFilter("Orgy", "orgy"), TriFilter("Orgy", "orgy"),
TriFilter("POV", "pov"),
TriFilter("Pink Hair", "pinkhair"),
TriFilter("Plot", "plot"),
TriFilter("Pregnant", "pregnant"),
TriFilter("Public Sex", "publicsex"), TriFilter("Public Sex", "publicsex"),
TriFilter("Purple Hair", "purplehair"),
TriFilter("Rape", "rape"), TriFilter("Rape", "rape"),
TriFilter("Red Hair", "redhair"),
TriFilter("Reverse Gangbang", "reversegangbang"), TriFilter("Reverse Gangbang", "reversegangbang"),
TriFilter("Reverse Rape", "reverserape"), TriFilter("Reverse Rape", "reverserape"),
TriFilter("Rimjob", "rimjob"), TriFilter("Rimjob", "rimjob"),
TriFilter("Threesome", "threesome"), TriFilter("Scat", "scat"),
TriFilter("Toys", "toys"),
TriFilter("Tripple Penetration", "tripplepenetration"),
TriFilter("Big Boobs", "bigboobs"),
TriFilter("Black Hair", "blackhair"),
TriFilter("Blonde Hair", "blondehair"),
TriFilter("Blue Hair", "bluehair"),
TriFilter("Brown Hair", "brownhair"),
TriFilter("Cosplay", "cosplay"),
TriFilter("Dark Skin", "darkskin"),
TriFilter("Demon", "demon"),
TriFilter("Dominant Girl", "dominantgirl"),
TriFilter("Elf", "elf"),
TriFilter("Futanari", "futanari"),
TriFilter("Glasses", "glasses"),
TriFilter("Green Hair", "greenhair"),
TriFilter("Gyaru", "gyaru"),
TriFilter("Inverted Nipples", "invertednipples"),
TriFilter("Loli", "loli"),
TriFilter("Maid", "maid"),
TriFilter("Milf", "milf"),
TriFilter("Nekomimi", "nekomimi"),
TriFilter("Nurse", "nurse"),
TriFilter("Pink Hair", "pinkhair"),
TriFilter("Pregnant", "pregnant"),
TriFilter("Purple Hair", "purplehair"),
TriFilter("Red Hair", "redhair"),
TriFilter("School Girl", "schoolgirl"), TriFilter("School Girl", "schoolgirl"),
TriFilter("Short Hair", "shorthair"), TriFilter("Short Hair", "shorthair"),
TriFilter("Shota", "shota"),
TriFilter("Small Boobs", "smallboobs"), TriFilter("Small Boobs", "smallboobs"),
TriFilter("Softcore", "softcore"),
TriFilter("Succubus", "succubus"), TriFilter("Succubus", "succubus"),
TriFilter("Swimsuit", "swimsuit"), TriFilter("Swimsuit", "swimsuit"),
TriFilter("Teacher", "teacher"), TriFilter("Teacher", "teacher"),
TriFilter("Tsundere", "tsundere"), TriFilter("Tentacle", "tentacle"),
TriFilter("Vampire", "vampire"), TriFilter("Threesome", "threesome"),
TriFilter("Virgin", "virgin"), TriFilter("Toys", "toys"),
TriFilter("White Hair", "whitehair"),
TriFilter("Old", "old"),
TriFilter("Shota", "shota"),
TriFilter("Trap", "trap"), TriFilter("Trap", "trap"),
TriFilter("Tripple Penetration", "tripplepenetration"),
TriFilter("Tsundere", "tsundere"),
TriFilter("Ugly Bastard", "uglybastard"), TriFilter("Ugly Bastard", "uglybastard"),
TriFilter("Uncensored", "uncensored"),
TriFilter("Vampire", "vampire"),
TriFilter("Vanilla", "vanilla"),
TriFilter("Virgin", "virgin"),
TriFilter("Watersports", "watersports"),
TriFilter("White Hair", "whitehair"),
TriFilter("X-Ray", "x-ray"),
TriFilter("Yaoi", "yaoi"),
TriFilter("Yuri", "yuri"),
) )
class GenreListFilter(genres: List<TriFilter>) : AnimeFilter.Group<TriFilter>("Genre", genres) class GenreListFilter(genres: List<TriFilter>) : AnimeFilter.Group<TriFilter>("Genre", genres)
@ -157,9 +154,9 @@ private fun getStudioList(): List<CheckFilter> = listOf(
CheckFilter("MS Pictures", "MS Pictures"), CheckFilter("MS Pictures", "MS Pictures"),
CheckFilter("Magic Bus", "Magic Bus"), CheckFilter("Magic Bus", "Magic Bus"),
CheckFilter("Magin Label", "Magin Label"), CheckFilter("Magin Label", "Magin Label"),
CheckFilter("Majin", "Majin"),
CheckFilter("Majin Petit", "Majin Petit"), CheckFilter("Majin Petit", "Majin Petit"),
CheckFilter("Majin petit", "Majin petit"), CheckFilter("Majin petit", "Majin petit"),
CheckFilter("Majin", "Majin"),
CheckFilter("Mary Jane", "Mary Jane"), CheckFilter("Mary Jane", "Mary Jane"),
CheckFilter("Mediabank", "Mediabank"), CheckFilter("Mediabank", "Mediabank"),
CheckFilter("Milky Animation Label", "Milky Animation Label"), CheckFilter("Milky Animation Label", "Milky Animation Label"),
@ -175,8 +172,8 @@ private fun getStudioList(): List<CheckFilter> = listOf(
CheckFilter("Passione", "Passione"), CheckFilter("Passione", "Passione"),
CheckFilter("Peak Hunt", "Peak Hunt"), CheckFilter("Peak Hunt", "Peak Hunt"),
CheckFilter("Pink Pineapple", "Pink Pineapple"), CheckFilter("Pink Pineapple", "Pink Pineapple"),
CheckFilter("PoRO", "PoRO"),
CheckFilter("PoRO petit", "PoRO petit"), CheckFilter("PoRO petit", "PoRO petit"),
CheckFilter("PoRO", "PoRO"),
CheckFilter("Queen Bee", "Queen Bee"), CheckFilter("Queen Bee", "Queen Bee"),
CheckFilter("Rabbit Gate", "Rabbit Gate"), CheckFilter("Rabbit Gate", "Rabbit Gate"),
CheckFilter("Seven", "Seven"), CheckFilter("Seven", "Seven"),
@ -201,7 +198,7 @@ private fun getStudioList(): List<CheckFilter> = listOf(
class StudioListFilter(studios: List<CheckFilter>) : AnimeFilter.Group<CheckFilter>("Studio", studios) class StudioListFilter(studios: List<CheckFilter>) : AnimeFilter.Group<CheckFilter>("Studio", studios)
val FILTERS = AnimeFilterList( val FILTERS: AnimeFilterList get() = AnimeFilterList(
OrderByFilter(), OrderByFilter(),
GenreListFilter(getGenreList()), GenreListFilter(getGenreList()),
StudioListFilter(getStudioList()), StudioListFilter(getStudioList()),

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.animeextension.en.oppaistream.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class AnilistResponseDto(val data: DataDto)
@Serializable
data class DataDto(@SerialName("Media") val media: MediaDto?)
@Serializable
data class MediaDto(val coverImage: CoverDto, val studios: StudiosDto)
@Serializable
data class CoverDto(val extraLarge: String, val large: String)
@Serializable
data class StudiosDto(val nodes: List<NodeDto>) {
@Serializable
data class NodeDto(val name: String)
val names by lazy { nodes.map { it.name } }
}