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