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:
parent
dd554f20b7
commit
4aa0bb218a
|
@ -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>
|
|
@ -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 |
|
@ -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:"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue