Add oppai stream (#1620)

This commit is contained in:
AwkwardPeak7 2023-05-19 13:08:57 +05:00 committed by GitHub
parent fec0924640
commit c55f7c62ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 432 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Oppai Stream'
pkgNameSuffix = 'en.oppaistream'
extClass = '.OppaiStream'
extVersionCode = 1
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -0,0 +1,210 @@
package eu.kanade.tachiyomi.animeextension.en.oppaistream
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
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
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.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.util.asJsoup
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
class OppaiStream : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Oppai Stream"
override val lang = "en"
override val baseUrl = "https://oppai.stream"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", baseUrl)
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// popular
override fun popularAnimeRequest(page: Int): Request {
return searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("views")))
}
override fun popularAnimeSelector() = searchAnimeSelector()
override fun popularAnimeNextPageSelector() = searchAnimeNextPageSelector()
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
override fun popularAnimeFromElement(element: Element) = searchAnimeFromElement(element)
// latest
override fun latestUpdatesRequest(page: Int): Request {
return searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("uploaded")))
}
override fun latestUpdatesSelector() = searchAnimeSelector()
override fun latestUpdatesNextPageSelector() = searchAnimeNextPageSelector()
override fun latestUpdatesParse(response: Response) = searchAnimeParse(response)
override fun latestUpdatesFromElement(element: Element) = searchAnimeFromElement(element)
// search
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = "$baseUrl/actions/search.php".toHttpUrl().newBuilder().apply {
addQueryParameter("text", query.trim())
filters.forEach { filter ->
when (filter) {
is OrderByFilter -> {
addQueryParameter("order", filter.selectedValue())
}
is GenreListFilter -> {
val genresInclude = mutableListOf<String>()
val genresExclude = mutableListOf<String>()
filter.state.forEach { genreState ->
when (genreState.state) {
STATE_INCLUDE -> genresInclude.add(genreState.value)
STATE_EXCLUDE -> genresExclude.add(genreState.value)
}
}
addQueryParameter("genres", genresInclude.joinToString(",") { it })
addQueryParameter("blacklist", genresExclude.joinToString(",") { it })
}
is StudioListFilter -> {
addQueryParameter("studio", filter.state.filter { it.state }.joinToString(",") { it.value })
}
else -> {}
}
addQueryParameter("page", page.toString())
addQueryParameter("limit", searchLimit.toString())
}
}.build().toString()
return GET(url, headers)
}
override fun searchAnimeSelector() = "div.episode-shown"
override fun searchAnimeNextPageSelector() = null
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(searchAnimeSelector())
val mangas = elements.map { element ->
searchAnimeFromElement(element)
}.distinctBy { it.title }
val hasNextPage = elements.size >= searchLimit
return AnimesPage(mangas, 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(titleCleanupRegex, "")
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
}
}
override fun getFilterList() = filters
// details
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
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")
}
}
// 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)
}
}
companion object {
private const val searchLimit = 36
private val titleCleanupRegex = Regex("""\s+\d+$""")
private const val PREF_QUALITY = "preferred_quality"
private const val PREF_QUALITY_title = "Preferred quality"
}
}

View File

@ -0,0 +1,208 @@
package eu.kanade.tachiyomi.animeextension.en.oppaistream
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
open class SelectFilter(
displayName: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun selectedValue() = vals[state].second
}
class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
"Sort By",
arrayOf(
Pair("A-Z", "az"),
Pair("Z-A", "za"),
Pair("Recently Released", "recent"),
Pair("Oldest Releases", "old"),
Pair("Most Views", "views"),
Pair("Highest Rated", "rating"),
Pair("Recently Uploaded", "uploaded"),
Pair("Randomize", "random"),
),
defaultOrder,
)
class TriFilter(name: String, val value: String) : AnimeFilter.TriState(name)
private fun getGenreList(): List<TriFilter> = listOf(
TriFilter("4k", "4k"),
TriFilter("Ahegao", "ahegao"),
TriFilter("Beach", "beach"),
TriFilter("Censored", "censored"),
TriFilter("Comedy", "comedy"),
TriFilter("Fantasy", "fantasy"),
TriFilter("Filmed", "filmed"),
TriFilter("HD", "hd"),
TriFilter("Harem", "harem"),
TriFilter("Horror", "horror"),
TriFilter("Incest", "incest"),
TriFilter("Inflation", "inflation"),
TriFilter("Lactation", "lactation"),
TriFilter("Mind Break", "mindbreak"),
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("NTR", "ntr"),
TriFilter("Orgy", "orgy"),
TriFilter("Public Sex", "publicsex"),
TriFilter("Rape", "rape"),
TriFilter("Reverse Gangbang", "reversegangbang"),
TriFilter("Reverse Rape", "reverserape"),
TriFilter("Rimjob", "rimjob"),
TriFilter("Threesome", "threesome"),
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("Short Hair", "shorthair"),
TriFilter("Small Boobs", "smallboobs"),
TriFilter("Succubus", "succubus"),
TriFilter("Swimsuit", "swimsuit"),
TriFilter("Teacher", "teacher"),
TriFilter("Tsundere", "tsundere"),
TriFilter("Vampire", "vampire"),
TriFilter("Virgin", "virgin"),
TriFilter("White Hair", "whitehair"),
TriFilter("Old", "old"),
TriFilter("Shota", "shota"),
TriFilter("Trap", "trap"),
TriFilter("Ugly Bastard", "uglybastard"),
)
class GenreListFilter(genres: List<TriFilter>) : AnimeFilter.Group<TriFilter>("Genre", genres)
class CheckFilter(name: String, val value: String) : AnimeFilter.CheckBox(name)
private fun getStudioList(): List<CheckFilter> = listOf(
CheckFilter("44℃ Baidoku", "44℃ Baidoku"),
CheckFilter("AT-X", "AT-X"),
CheckFilter("AXsiZ", "AXsiZ"),
CheckFilter("Alice Soft", "Alice Soft"),
CheckFilter("Antechinus", "Antechinus"),
CheckFilter("An♥Tekinus", "An♥Tekinus"),
CheckFilter("BOOTLEG", "BOOTLEG"),
CheckFilter("BREAKBOTTLE", "BREAKBOTTLE"),
CheckFilter("Bomb! Cute! Bomb!", "Bomb! Cute! Bomb!"),
CheckFilter("Breakbottle", "Breakbottle"),
CheckFilter("Bunny Walker", "Bunny Walker"),
CheckFilter("ChuChu", "ChuChu"),
CheckFilter("Collaboration Works", "Collaboration Works"),
CheckFilter("Cotton Doll", "Cotton Doll"),
CheckFilter("Digital Works", "Digital Works"),
CheckFilter("Global Solutions", "Global Solutions"),
CheckFilter("HiLLS", "HiLLS"),
CheckFilter("Himajin Planning", "Himajin Planning"),
CheckFilter("JapanAnime", "JapanAnime"),
CheckFilter("Jumondou", "Jumondou"),
CheckFilter("Kitty Media", "Kitty Media"),
CheckFilter("Lune Pictures", "Lune Pictures"),
CheckFilter("MS Pictures", "MS Pictures"),
CheckFilter("Magic Bus", "Magic Bus"),
CheckFilter("Magin Label", "Magin Label"),
CheckFilter("Majin", "Majin"),
CheckFilter("Majin Petit", "Majin Petit"),
CheckFilter("Majin petit", "Majin petit"),
CheckFilter("Mary Jane", "Mary Jane"),
CheckFilter("Mediabank", "Mediabank"),
CheckFilter("Milky Animation Label", "Milky Animation Label"),
CheckFilter("Mirai Koujou", "Mirai Koujou"),
CheckFilter("NBCUniversal Entertainment Japan", "NBCUniversal Entertainment Japan"),
CheckFilter("Natural High", "Natural High"),
CheckFilter("NewGeneration", "NewGeneration"),
CheckFilter("Nippon Columbia", "Nippon Columbia"),
CheckFilter("Nur", "Nur"),
CheckFilter("Office Nobu", "Office Nobu"),
CheckFilter("Pashima", "Pashima"),
CheckFilter("Pashmina", "Pashmina"),
CheckFilter("Passione", "Passione"),
CheckFilter("Peak Hunt", "Peak Hunt"),
CheckFilter("Pink Pineapple", "Pink Pineapple"),
CheckFilter("PoRO", "PoRO"),
CheckFilter("PoRO petit", "PoRO petit"),
CheckFilter("Queen Bee", "Queen Bee"),
CheckFilter("Rabbit Gate", "Rabbit Gate"),
CheckFilter("Seven", "Seven"),
CheckFilter("Shion", "Shion"),
CheckFilter("Show-Ten", "Show-Ten"),
CheckFilter("Shueisha", "Shueisha"),
CheckFilter("Studio 1st", "Studio 1st"),
CheckFilter("Studio Gokumi", "Studio Gokumi"),
CheckFilter("Studio Houkiboshi", "Studio Houkiboshi"),
CheckFilter("Suzuki Mirano", "Suzuki Mirano"),
CheckFilter("T-Rex", "T-Rex"),
CheckFilter("TEATRO Nishi Tokyo Studio", "TEATRO Nishi Tokyo Studio"),
CheckFilter("TNK", "TNK"),
CheckFilter("Toranoana", "Toranoana"),
CheckFilter("WHITE BEAR", "WHITE BEAR"),
CheckFilter("Y.O.U.C", "Y.O.U.C"),
CheckFilter("YTV", "YTV"),
CheckFilter("Yomiuri TV Enterprise", "Yomiuri TV Enterprise"),
CheckFilter("ZIZ Entertainment", "ZIZ Entertainment"),
CheckFilter("erozuki", "erozuki"),
)
class StudioListFilter(studios: List<CheckFilter>) : AnimeFilter.Group<CheckFilter>("Studio", studios)
val filters = AnimeFilterList(
OrderByFilter(),
GenreListFilter(getGenreList()),
StudioListFilter(getStudioList()),
)