Rizz Comic (#82)

This commit is contained in:
AwkwardPeak7 2024-01-10 13:32:15 +05:00 committed by Draff
parent c1079bfb43
commit 23d0a158a0
11 changed files with 414 additions and 0 deletions

View File

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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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,
)

View File

@ -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)
}
}
}