diff --git a/src/en/oppaistream/AndroidManifest.xml b/src/en/oppaistream/AndroidManifest.xml
new file mode 100644
index 000000000..568741e54
--- /dev/null
+++ b/src/en/oppaistream/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/en/oppaistream/build.gradle b/src/en/oppaistream/build.gradle
new file mode 100644
index 000000000..1d8687878
--- /dev/null
+++ b/src/en/oppaistream/build.gradle
@@ -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"
\ No newline at end of file
diff --git a/src/en/oppaistream/res/mipmap-hdpi/ic_launcher.png b/src/en/oppaistream/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..4a626ef63
Binary files /dev/null and b/src/en/oppaistream/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/oppaistream/res/mipmap-mdpi/ic_launcher.png b/src/en/oppaistream/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..33a2663a0
Binary files /dev/null and b/src/en/oppaistream/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/oppaistream/res/mipmap-xhdpi/ic_launcher.png b/src/en/oppaistream/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..78c7836c8
Binary files /dev/null and b/src/en/oppaistream/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/oppaistream/res/mipmap-xxhdpi/ic_launcher.png b/src/en/oppaistream/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f96f14795
Binary files /dev/null and b/src/en/oppaistream/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/oppaistream/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/oppaistream/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..dae6df97d
Binary files /dev/null and b/src/en/oppaistream/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/oppaistream/res/web_hi_res_512.png b/src/en/oppaistream/res/web_hi_res_512.png
new file mode 100644
index 000000000..9a13c4f4d
Binary files /dev/null and b/src/en/oppaistream/res/web_hi_res_512.png differ
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
new file mode 100644
index 000000000..90dd8e041
--- /dev/null
+++ b/src/en/oppaistream/src/eu/kanade/tachiyomi/animeextension/en/oppaistream/OppaiStream.kt
@@ -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().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()
+ val genresExclude = mutableListOf()
+ 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 {
+ 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