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