Rizz Comic (#82)
This commit is contained in:
parent
c1079bfb43
commit
23d0a158a0
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Rizz Comic'
|
||||
pkgNameSuffix = 'en.rizzcomic'
|
||||
extClass = '.RizzComic'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
After Width: | Height: | Size: 230 KiB |
|
@ -0,0 +1,281 @@
|
|||
package eu.kanade.tachiyomi.extension.en.rizzcomic
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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 eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class RizzComic : HttpSource() {
|
||||
|
||||
override val name = "Rizz Comic"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val baseUrl = "https://rizzcomic.com"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(1)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
private val apiHeaders by lazy {
|
||||
headersBuilder()
|
||||
.set("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
}
|
||||
|
||||
private var urlPrefix: String? = null
|
||||
private var genreCache: List<Pair<String, String>> = emptyList()
|
||||
private var attempts = 0
|
||||
|
||||
private fun updateCache() {
|
||||
if ((urlPrefix.isNullOrEmpty() || genreCache.isEmpty()) && attempts < 3) {
|
||||
runCatching {
|
||||
val document = client.newCall(GET("$baseUrl/series", headers))
|
||||
.execute().use { it.asJsoup() }
|
||||
|
||||
urlPrefix = document.selectFirst(".listupd a")
|
||||
?.attr("href")
|
||||
?.substringAfter("/series/")
|
||||
?.substringBefore("-")
|
||||
|
||||
genreCache = document.selectFirst(".filter .genrez")
|
||||
?.select("li")
|
||||
.orEmpty()
|
||||
.map {
|
||||
val name = it.select("label").text()
|
||||
val id = it.select("input").attr("value")
|
||||
|
||||
Pair(name, id)
|
||||
}
|
||||
}
|
||||
|
||||
attempts++
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrlPrefix(): String {
|
||||
if (urlPrefix.isNullOrEmpty()) {
|
||||
updateCache()
|
||||
}
|
||||
|
||||
return urlPrefix!!
|
||||
}
|
||||
|
||||
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 {
|
||||
if (query.isNotEmpty()) {
|
||||
val form = FormBody.Builder()
|
||||
.add("search_value", query.trim())
|
||||
.build()
|
||||
|
||||
return POST("$baseUrl/Index/live_search", apiHeaders, form)
|
||||
}
|
||||
|
||||
val form = FormBody.Builder().apply {
|
||||
filters.filterIsInstance<FormBodyFilter>().forEach {
|
||||
it.addFormParameter(this)
|
||||
}
|
||||
}.build()
|
||||
|
||||
return POST("$baseUrl/Index/filter_series", apiHeaders, form)
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters: MutableList<Filter<*>> = mutableListOf(
|
||||
Filter.Header("Filters don't work with text search"),
|
||||
SortFilter(),
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
)
|
||||
|
||||
filters += if (genreCache.isEmpty()) {
|
||||
listOf(
|
||||
Filter.Separator(),
|
||||
Filter.Header("Press reset to attempt to load genres"),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
GenreFilter(genreCache),
|
||||
)
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
updateCache()
|
||||
|
||||
val result = response.parseAs<List<Comic>>()
|
||||
|
||||
val entries = result.map { comic ->
|
||||
SManga.create().apply {
|
||||
url = "${comic.slug}#${comic.id}"
|
||||
title = comic.title
|
||||
description = comic.synopsis
|
||||
author = listOfNotNull(comic.author, comic.serialization).joinToString()
|
||||
artist = comic.artist
|
||||
status = comic.status.parseStatus()
|
||||
thumbnail_url = comic.cover?.let { "$baseUrl/assets/images/$it" }
|
||||
genre = buildList {
|
||||
add(comic.type?.capitalize())
|
||||
comic.genreIds?.onEach { gId ->
|
||||
add(genreCache.firstOrNull { it.second == gId }?.first)
|
||||
}
|
||||
}.filterNotNull().joinToString()
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(entries, false)
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { mangaDetailsParse(it, manga) }
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val slug = manga.url.substringBefore("#")
|
||||
val randomPart = getUrlPrefix()
|
||||
|
||||
return GET("$baseUrl/series/$randomPart-$slug", headers)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
val slug = manga.url.substringBefore("#")
|
||||
|
||||
val urlPart = urlPrefix?.let { "$it-" } ?: ""
|
||||
|
||||
return "$baseUrl/series/$urlPart$slug"
|
||||
}
|
||||
|
||||
private fun mangaDetailsParse(response: Response, manga: SManga) = manga.apply {
|
||||
val document = response.use { it.asJsoup() }
|
||||
|
||||
title = document.selectFirst("h1.entry-title")?.text().orEmpty()
|
||||
artist = document.selectFirst(".tsinfo .imptdt:contains(artist) i")?.ownText()
|
||||
author = listOfNotNull(
|
||||
document.selectFirst(".tsinfo .imptdt:contains(author) i")?.ownText(),
|
||||
document.selectFirst(".tsinfo .imptdt:contains(serialization) i")?.ownText(),
|
||||
).joinToString()
|
||||
genre = buildList {
|
||||
add(
|
||||
document.selectFirst(".tsinfo .imptdt:contains(type) a")
|
||||
?.ownText()
|
||||
?.capitalize(),
|
||||
)
|
||||
document.select(".mgen a").eachText().onEach { add(it) }
|
||||
}.filterNotNull().joinToString()
|
||||
status = document.selectFirst(".tsinfo .imptdt:contains(status) i")?.text().parseStatus()
|
||||
thumbnail_url = document.selectFirst(".infomanga > div[itemprop=image] img, .thumb img")?.absUrl("src")
|
||||
}
|
||||
|
||||
private fun String?.parseStatus(): Int = when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
listOf("ongoing", "publishing").any { contains(it, ignoreCase = true) } -> SManga.ONGOING
|
||||
contains("hiatus", ignoreCase = true) -> SManga.ON_HIATUS
|
||||
contains("completed", ignoreCase = true) -> SManga.COMPLETED
|
||||
listOf("dropped", "cancelled").any { contains(it, ignoreCase = true) } -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val id = manga.url.substringAfter("#")
|
||||
val slug = manga.url.substringBefore("#")
|
||||
|
||||
return GET("$baseUrl/index/search_chapters/$id#$slug", apiHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val result = response.parseAs<List<Chapter>>()
|
||||
val slug = response.request.url.fragment!!
|
||||
|
||||
return result.map {
|
||||
SChapter.create().apply {
|
||||
url = "$slug-chapter-${it.name}"
|
||||
name = "Chapter ${it.name}"
|
||||
date_upload = runCatching {
|
||||
dateFormat.parse(it.time!!)!!.time
|
||||
}.getOrDefault(0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET("$baseUrl/chapter/${getUrlPrefix()}-${chapter.url}", headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.use { it.asJsoup() }
|
||||
val chapterUrl = response.request.url.toString()
|
||||
|
||||
return document.select("div#readerarea img")
|
||||
.mapIndexed { i, img ->
|
||||
Page(i, chapterUrl, img.absUrl("src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Accept", "image/avif,image/webp,image/png,image/jpeg,*/*")
|
||||
.set("Referer", page.url)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, newHeaders)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
throw UnsupportedOperationException("Not Used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException("Not Used")
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
use { it.body.string() }.let(json::decodeFromString)
|
||||
|
||||
companion object {
|
||||
private fun String.capitalize() = replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(Locale.ROOT)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package eu.kanade.tachiyomi.extension.en.rizzcomic
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Comic(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
@SerialName("image_url") val cover: String? = null,
|
||||
@SerialName("long_description") val synopsis: String? = null,
|
||||
val status: String? = null,
|
||||
val type: String? = null,
|
||||
val artist: String? = null,
|
||||
val author: String? = null,
|
||||
val serialization: String? = null,
|
||||
@SerialName("genre_id") val genres: String? = null,
|
||||
) {
|
||||
val slug get() = title.trim().lowercase()
|
||||
.replace(slugRegex, "-")
|
||||
.replace("-s-", "s-")
|
||||
.replace("-ll-", "ll-")
|
||||
|
||||
val genreIds get() = genres?.split(",")?.map(String::trim)
|
||||
|
||||
companion object {
|
||||
private val slugRegex = Regex("""[^a-z0-9]+""")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Chapter(
|
||||
@SerialName("chapter_time") val time: String? = null,
|
||||
@SerialName("chapter_title") val name: String,
|
||||
)
|
|
@ -0,0 +1,84 @@
|
|||
package eu.kanade.tachiyomi.extension.en.rizzcomic
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.FormBody
|
||||
|
||||
interface FormBodyFilter {
|
||||
fun addFormParameter(form: FormBody.Builder)
|
||||
}
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
defaultValue: String? = null,
|
||||
) : FormBodyFilter, Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||
) {
|
||||
abstract val formParameter: String
|
||||
override fun addFormParameter(form: FormBody.Builder) {
|
||||
form.add(formParameter, options[state].second)
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter(defaultOrder: String? = null) : SelectFilter("Sort By", sort, defaultOrder) {
|
||||
override val formParameter = "OrderValue"
|
||||
companion object {
|
||||
private val sort = listOf(
|
||||
Pair("Default", "all"),
|
||||
Pair("A-Z", "title"),
|
||||
Pair("Z-A", "titlereverse"),
|
||||
Pair("Latest Update", "update"),
|
||||
Pair("Latest Added", "latest"),
|
||||
Pair("Popular", "popular"),
|
||||
)
|
||||
|
||||
val POPULAR = FilterList(StatusFilter(), TypeFilter(), SortFilter("popular"))
|
||||
val LATEST = FilterList(StatusFilter(), TypeFilter(), SortFilter("update"))
|
||||
}
|
||||
}
|
||||
|
||||
class StatusFilter : SelectFilter("Status", status) {
|
||||
override val formParameter = "StatusValue"
|
||||
companion object {
|
||||
private val status = listOf(
|
||||
Pair("All", "all"),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Complete", "completed"),
|
||||
Pair("Hiatus", "hiatus"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilter : SelectFilter("Type", type) {
|
||||
override val formParameter = "TypeValue"
|
||||
companion object {
|
||||
private val type = listOf(
|
||||
Pair("All", "all"),
|
||||
Pair("Manga", "Manga"),
|
||||
Pair("Manhwa", "Manhwa"),
|
||||
Pair("Manhua", "Manhua"),
|
||||
Pair("Comic", "Comic"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CheckBoxFilter(
|
||||
name: String,
|
||||
val value: String,
|
||||
) : Filter.CheckBox(name)
|
||||
|
||||
class GenreFilter(
|
||||
genres: List<Pair<String, String>>,
|
||||
) : FormBodyFilter, Filter.Group<CheckBoxFilter>(
|
||||
"Genre",
|
||||
genres.map { CheckBoxFilter(it.first, it.second) },
|
||||
) {
|
||||
override fun addFormParameter(form: FormBody.Builder) {
|
||||
state.filter { it.state }.forEach {
|
||||
form.add("genres_checked[]", it.value)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue