* RawZ

* nsfw flag

* webview urls
This commit is contained in:
AwkwardPeak7 2024-01-20 20:53:52 +05:00 committed by Draff
parent 846e783195
commit b49c4c5378
5 changed files with 388 additions and 0 deletions

View File

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

8
src/ja/rawz/build.gradle Normal file
View File

@ -0,0 +1,8 @@
ext {
extName = 'RawZ'
extClass = '.RawZ'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.extension.ja.rawz
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.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.UnsupportedOperationException
class RawZ : HttpSource() {
override val name = "RawZ"
override val baseUrl = "https://stmanga.com"
private val apiUrl = "https://api.rawz.org/api"
override val lang = "ja"
override val supportsLatest = true
private val json by injectLazy<Json>()
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/manga".toHttpUrl().newBuilder().apply {
addQueryParameter("name", query.trim())
filters.filterIsInstance<UriPartFilter>().forEach {
it.addQueryParameter(this)
}
addQueryParameter("page", page.toString())
addQueryParameter("limit", LIMIT.toString())
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
fetchGenres()
val result = response.parseAs<Data<List<Manga>>>()
val entries = result.data.map { it.toSManga() }
val hasNextPage = result.data.size == LIMIT
return MangasPage(entries, hasNextPage)
}
private var genreCache: List<Pair<String, String>> = emptyList()
private var genreFetchAttempts = 0
private var genreFetchFailed = false
private fun fetchGenres() {
if ((genreCache.isEmpty() || genreFetchFailed) && genreFetchAttempts < 3) {
val genres = runCatching {
client.newCall(
GET("$apiUrl/taxonomy-browse?type=genres&limit=100&page=1", headers),
)
.execute()
.parseAs<Data<Taxonomy>>()
.data.genres.map {
Pair(it.name, it.id.toString())
}
}
genreCache = genres.getOrNull().orEmpty()
genreFetchFailed = genres.isFailure
genreFetchAttempts++
}
}
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>(
SortFilter(),
TypeFilter(),
StatusFilter(),
ChapterNumFilter(),
)
filters += if (genreCache.isEmpty()) {
listOf(
Filter.Separator(),
Filter.Header("Press Reset to attempt to display genre"),
)
} else {
listOf(
GenreFilter(genreCache),
)
}
return FilterList(filters)
}
override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$apiUrl/manga/${manga.url.substringAfter(".")}")
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<Data<Manga>>().data.toSManga()
}
override fun chapterListRequest(manga: SManga): Request {
val slug = manga.url
.substringAfter("/manga/")
.substringBefore(".")
val id = manga.url.substringAfterLast(".")
return GET("$apiUrl/manga/$id/childs#$slug", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<Data<List<Chapter>>>()
val mangaSlug = response.request.url.fragment!!
return result.data.map {
SChapter.create().apply {
url = "/read/$mangaSlug.${it.id}/${it.slug}"
name = it.name
date_upload = runCatching {
dateFormat.parse(it.createdAt!!)!!.time
}.getOrDefault(0L)
}
}
}
override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
override fun pageListRequest(chapter: SChapter): Request {
val id = chapter.url
.substringBeforeLast("/")
.substringAfterLast(".")
return GET("$apiUrl/child-detail/$id", headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<Data<Pages>>()
return result.data.images.mapIndexed { idx, img ->
Page(idx, "", img.url)
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromString(body.string())
}
companion object {
private const val LIMIT = 30
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
}
}
}

View File

@ -0,0 +1,97 @@
package eu.kanade.tachiyomi.extension.ja.rawz
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
import java.util.Locale
@Serializable
data class Data<T>(
val data: T,
)
@Serializable
data class Manga(
val id: Int,
val name: String,
val slug: String,
val description: String? = null,
val status: String? = null,
val type: String? = null,
val image: String? = null,
@Serializable(with = EmptyArrayOrTaxonomySerializer::class)
val taxonomy: Taxonomy,
) {
fun toSManga() = SManga.create().apply {
url = "/manga/$slug.$id"
thumbnail_url = image
title = name
description = this@Manga.description
genre = (
taxonomy.genres.map {
it.name
}.let {
type?.run {
it.plus(
this.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
},
)
}
}
)?.joinToString()
status = when (this@Manga.status) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
initialized = true
}
}
@Serializable
data class Taxonomy(
val genres: List<Genre>,
)
object EmptyArrayOrTaxonomySerializer : JsonTransformingSerializer<Taxonomy>(Taxonomy.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return if (element is JsonArray) {
JsonObject(mapOf(Pair("genres", JsonArray(emptyList()))))
} else {
element
}
}
}
@Serializable
data class Genre(
val id: Int,
val name: String,
)
@Serializable
data class Chapter(
val id: Int,
val name: String,
val slug: String,
@SerialName("created_at") val createdAt: String? = null,
)
@Serializable
data class Pages(
val images: List<Url>,
)
@Serializable
data class Url(
val url: String,
)

View File

@ -0,0 +1,101 @@
package eu.kanade.tachiyomi.extension.ja.rawz
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl
interface UriPartFilter {
fun addQueryParameter(url: HttpUrl.Builder)
}
class CheckBoxFilter(
name: String,
val value: String,
) : Filter.CheckBox(name)
abstract class CheckBoxFilterGroup(
name: String,
genres: List<Pair<String, String>>,
) : UriPartFilter, Filter.Group<CheckBoxFilter>(
name,
genres.map { CheckBoxFilter(it.first, it.second) },
) {
abstract val queryParameter: String
override fun addQueryParameter(url: HttpUrl.Builder) {
state.filter { it.state }.forEach {
url.addQueryParameter(queryParameter, it.value)
}
}
}
abstract class SelectFilter(
name: String,
private val options: List<Pair<String, String>>,
defaultValue: String? = null,
) : UriPartFilter, Filter.Select<String>(
name,
options.map { it.first }.toTypedArray(),
options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
abstract val queryParameter: String
override fun addQueryParameter(url: HttpUrl.Builder) {
url.addQueryParameter(queryParameter, options[state].second)
}
}
class TypeFilter : CheckBoxFilterGroup("タイプ", types) {
override val queryParameter = "type[]"
companion object {
private val types = listOf(
Pair("Manga", "manga"),
Pair("Manhua", "manhua"),
Pair("Manhwa", "manhwa"),
Pair("Oneshot", "oneshot"),
Pair("Doujinshi", "doujinshi"),
)
}
}
class GenreFilter(genres: List<Pair<String, String>>) : CheckBoxFilterGroup("ジャンル", genres) {
override val queryParameter = "taxonomy[]"
}
class StatusFilter : CheckBoxFilterGroup("ステータス", status) {
override val queryParameter = "status[]"
companion object {
private val status = listOf(
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
)
}
}
class ChapterNumFilter : SelectFilter("最小章", minChapNum) {
override val queryParameter = "minchap"
companion object {
private val minChapNum = listOf(
Pair(">= 1 chapters", "1"),
Pair(">= 3 chapters", "3"),
Pair(">= 5 chapters", "5"),
Pair(">= 10 chapters", "10"),
Pair(">= 20 chapters", "20"),
Pair(">= 30 chapters", "30"),
Pair(">= 50 chapters", "50"),
)
}
}
class SortFilter(default: String? = null) : SelectFilter("並び替え", sorts, default) {
override val queryParameter = "order_by"
companion object {
private val sorts = listOf(
Pair("Recently updated", "updated_at"),
Pair("Recently added", "created_at"),
Pair("Trending", "views"),
Pair("Name A-Z", "name"),
)
val POPULAR = FilterList(SortFilter("views"))
val LATEST = FilterList(SortFilter("updated_at"))
}
}