add Oppai Stream (#15683)

* add Oppai Stream

* add Url activity

* AndroidManifest is hard

* add exception for safety

* remove space from search genres
This commit is contained in:
mobi2002 2023-03-13 19:45:07 +05:00 committed by GitHub
parent dd554f20b7
commit 4aa0bb218a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 415 additions and 0 deletions

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".en.oppaistream.OppaiStreamUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="read.oppai.stream" />
<data
android:pathPattern="/manhwa"
android:scheme="https" />
<data
android:pathPattern="/page"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</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
isNsfw = 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,340 @@
package eu.kanade.tachiyomi.extension.en.oppaistream
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
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 rx.Observable
import java.util.Calendar
class OppaiStream : ParsedHttpSource() {
override val name = "Oppai Stream"
override val baseUrl = "https://read.oppai.stream"
private val cdnUrl = "https://myspacecat.pictures"
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", baseUrl)
// popular
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(page, "", FilterList(OrderByFilter("views")))
}
override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
// latest
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(page, "", FilterList(OrderByFilter("uploaded")))
}
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
// search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (!query.startsWith(SLUG_SEARCH_PREFIX)) {
return super.fetchSearchManga(page, query, filters)
}
val url = "/manhwa?m=${query.substringAfter(SLUG_SEARCH_PREFIX)}"
return fetchMangaDetails(SManga.create().apply { this.url = url }).map {
it.url = url
MangasPage(listOf(it), false)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api-search.php".toHttpUrl().newBuilder().apply {
addQueryParameter("text", query)
filters.forEach { filter ->
when (filter) {
is OrderByFilter -> {
addQueryParameter("order", filter.selectedValue())
}
is GenreListFilter -> {
val genresInclude = filter.state.filter { it.state == Filter.TriState.STATE_INCLUDE }.map { genre -> genre.value }
val genresExclude = filter.state.filter { it.state == Filter.TriState.STATE_EXCLUDE }.map { genre -> genre.value }
addQueryParameter("genres", genresInclude.joinToString(",") { it })
addQueryParameter("blacklist", genresExclude.joinToString(",") { it })
}
else -> {}
}
}
addQueryParameter("page", "$page")
addQueryParameter("limit", "$searchLimit")
}.build()
return GET(url, headers)
}
override fun searchMangaSelector() = "div.in-grid"
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val elements = document.select(searchMangaSelector())
val mangas = elements.map { element ->
searchMangaFromElement(element)
}
val hasNextPage = elements.size >= searchLimit
return MangasPage(mangas, hasNextPage)
}
override fun searchMangaFromElement(element: Element): SManga {
return SManga.create().apply {
thumbnail_url = element.select(".split-1 img").attr("src")
title = element.select("a div h3").text()
setUrlWithoutDomain(element.select("a").attr("href"))
}
}
override fun searchMangaNextPageSelector() = null
// manga details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
thumbnail_url = document.select(".cover-img").attr("src")
document.select(".manhwa-info-in").let { it ->
it.select("h1").text().let {
title = it.substringBeforeLast("By").trim()
author = it.substringAfterLast("By").trim()
artist = author
}
genre = it.select(".genres h5").joinToString { it.text() }
description = it.select(".description").text()
}
}
}
// chapter list
override fun chapterListSelector() = ".sort-chapters > a"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.select("div > h4").text()
date_upload = element.select("div > h6").text().parseRelativeDate()
}
}
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
// page list
override fun pageListRequest(chapter: SChapter): Request {
val chapterUrl = "$baseUrl${chapter.url}".toHttpUrl()
val slug = chapterUrl.queryParameter("m")
val chapNo = chapterUrl.queryParameter("c")
return GET("$cdnUrl/manhwa/im.php?f-m=$slug&c=$chapNo", headers)
}
override fun pageListParse(document: Document): List<Page> {
return document.select("img").mapIndexed { index, img ->
Page(index = index, imageUrl = img.attr("src"))
}
}
// filters
open class SelectFilter(
displayName: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun selectedValue() = vals[state].second
}
private 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"),
),
defaultOrder,
)
internal class TriState(name: String, val value: String) : Filter.TriState(name)
private fun getGenreList(): List<TriState> = listOf(
TriState("Adventure", "adventure"),
TriState("Beach", "beach"),
TriState("Blackmail", "blackmail"),
TriState("Cheating", "cheating"),
TriState("Comedy", "comedy"),
TriState("Cooking", "cooking"),
TriState("Drama", "drama"),
TriState("Fantasy", "fantasy"),
TriState("Harem", "harem"),
TriState("Historical", "historical"),
TriState("Horror", "horror"),
TriState("Incest", "incest"),
TriState("Mind Break", "mindbreak"),
TriState("Mind Control", "mindcontrol"),
TriState("Monster", "monster"),
TriState("Mystery", "mystery"),
TriState("NTR", "ntr"),
TriState("Psychological", "psychological"),
TriState("Rape", "rape"),
TriState("Reverse Rape", "reverserape"),
TriState("Romance", "romance"),
TriState("School Life", "schoollife"),
TriState("Sci-fi", "sci-fi"),
TriState("Secret Relationship", "secretrelationship"),
TriState("Slice of Life", "sliceoflife"),
TriState("Smut", "smut"),
TriState("Sports", "sports"),
TriState("Supernatural", "supernatural"),
TriState("Tragedy", "tragedy"),
TriState("Yaoi", "yaoi"),
TriState("Yuri", "yuri"),
TriState("Big Boobs", "bigboobs"),
TriState("Black Hair", "blackhair"),
TriState("Blonde Hair", "blondehair"),
TriState("Blue Hair", "bluehair"),
TriState("Brown Hair", "brownhair"),
TriState("Cosplay", "cosplay"),
TriState("Dark Skin", "darkskin"),
TriState("Demon", "demon"),
TriState("Dominant Girl", "dominantgirl"),
TriState("Elf", "elf"),
TriState("Futanari", "futanari"),
TriState("Glasses", "glasses"),
TriState("Green Hair", "greenhair"),
TriState("Gyaru", "gyaru"),
TriState("Inverted Nipples", "invertednipples"),
TriState("Loli", "loli"),
TriState("Maid", "maid"),
TriState("Milf", "milf"),
TriState("Nekomimi", "nekomimi"),
TriState("Nurse", "nurse"),
TriState("Pink Hair", "pinkhair"),
TriState("Pregnant", "pregnant"),
TriState("Purple Hair", "purplehair"),
TriState("Red Hair", "redhair"),
TriState("School Girl", "schoolgirl"),
TriState("Short Hair", "shorthair"),
TriState("Small Boobs", "smallboobs"),
TriState("Succubus", "succubus"),
TriState("Swimsuit", "swimsuit"),
TriState("Teacher", "teacher"),
TriState("Tsundere", "tsundere"),
TriState("Vampire", "vampire"),
TriState("Virgin", "virgin"),
TriState("White Hair", "whitehair"),
TriState("Old", "old"),
TriState("Shota", "shota"),
TriState("Trap", "trap"),
TriState("Ugly Bastard", "uglybastard"),
)
private class GenreListFilter(genres: List<OppaiStream.TriState>) : Filter.Group<TriState>("Genre", genres)
override fun getFilterList() = FilterList(
OrderByFilter(),
GenreListFilter(getGenreList()),
)
// Unused
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used")
}
// helpers
private fun String.parseRelativeDate(): Long {
val now = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
var parsedDate = 0L
val relativeDate = try {
this.split(" ")[0].trim().toInt()
} catch (e: NumberFormatException) {
return 0L
}
when {
// parse: 30 seconds ago
"second" in this -> {
parsedDate = now.apply { add(Calendar.SECOND, -relativeDate) }.timeInMillis
}
// parses: "42 minutes ago"
"minute" in this -> {
parsedDate = now.apply { add(Calendar.MINUTE, -relativeDate) }.timeInMillis
}
// parses: "1 hour ago" and "2 hours ago"
"hour" in this -> {
parsedDate = now.apply { add(Calendar.HOUR, -relativeDate) }.timeInMillis
}
// parses: "2 days ago"
"day" in this -> {
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -relativeDate) }.timeInMillis
}
// parses: "2 weeks ago"
"week" in this -> {
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -relativeDate) }.timeInMillis
}
// parses: "2 months ago"
"month" in this -> {
parsedDate = now.apply { add(Calendar.MONTH, -relativeDate) }.timeInMillis
}
// parse: "2 years ago"
"year" in this -> {
parsedDate = now.apply { add(Calendar.YEAR, -relativeDate) }.timeInMillis
}
}
return parsedDate
}
companion object {
const val searchLimit = 36
const val SLUG_SEARCH_PREFIX = "slug:"
}
}

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.extension.en.oppaistream
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class OppaiStreamUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent?.data
val slug = uri?.getQueryParameter("m")
if (slug != null) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${OppaiStream.SLUG_SEARCH_PREFIX}$slug")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("OppaiStreamUrlActivity", e.toString())
}
} else {
Log.e("OppaiStreamUrlActivity", "slug not found in uri $uri")
}
finish()
exitProcess(0)
}
}