Tachiyomi-Extensions/multisrc/overrides/mangathemesia/komikcast/src/KomikCast.kt

254 lines
10 KiB
Kotlin

package eu.kanade.tachiyomi.extension.id.komikcast
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.GET
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.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar
import java.util.Locale
class KomikCast : MangaThemesia("Komik Cast", "https://komikcast.lol", "id", "/daftar-komik") {
// Formerly "Komik Cast (WP Manga Stream)"
override val id = 972717448578983812
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(3)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.add("Accept-language", "en-US,en;q=0.9,id;q=0.8")
override fun imageRequest(page: Page): Request {
val newHeaders = headersBuilder()
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.set("Referer", "$baseUrl/")
.build()
return GET(page.imageUrl!!, newHeaders)
}
override fun popularMangaRequest(page: Int) = customPageRequest(page, "orderby", "popular")
override fun latestUpdatesRequest(page: Int) = customPageRequest(page, "sortby", "update")
private fun customPageRequest(page: Int, filterKey: String, filterValue: String): Request {
val pagePath = if (page > 1) "page/$page/" else ""
return GET("$baseUrl$mangaUrlDirectory/$pagePath?$filterKey=$filterValue", headers)
}
override fun searchMangaSelector() = "div.list-update_item"
override fun searchMangaFromElement(element: Element) = super.searchMangaFromElement(element).apply {
title = element.selectFirst("h3.title")!!.ownText()
}
override val seriesDetailsSelector = "div.komik_info:has(.komik_info-content)"
override val seriesTitleSelector = "h1.komik_info-content-body-title"
override val seriesDescriptionSelector = ".komik_info-description-sinopsis"
override val seriesAltNameSelector = ".komik_info-content-native"
override val seriesGenreSelector = ".komik_info-content-genre a"
override val seriesThumbnailSelector = ".komik_info-content-thumbnail img"
override val seriesStatusSelector = ".komik_info-content-info:contains(Status)"
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
title = seriesDetails.selectFirst(seriesTitleSelector)?.text()
?.replace("bahasa indonesia", "", ignoreCase = true)?.trim().orEmpty()
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
// Add alternative name to manga description
val altName = seriesDetails.selectFirst(seriesAltNameSelector)?.ownText().takeIf { it.isNullOrBlank().not() }
altName?.let {
description = "$description\n\n$altNamePrefix$altName".trim()
}
val genres = seriesDetails.select(seriesGenreSelector).map { it.text() }.toMutableList()
// Add series type (manga/manhwa/manhua/other) to genre
seriesDetails.selectFirst(seriesTypeSelector)?.ownText().takeIf { it.isNullOrBlank().not() }?.let { genres.add(it) }
genre = genres.map { genre ->
genre.lowercase(Locale.forLanguageTag(lang)).replaceFirstChar { char ->
if (char.isLowerCase()) {
char.titlecase(Locale.forLanguageTag(lang))
} else {
char.toString()
}
}
}
.joinToString { it.trim() }
status = seriesDetails.selectFirst(seriesStatusSelector)?.text().parseStatus()
thumbnail_url = seriesDetails.select(seriesThumbnailSelector).imgAttr()
}
}
override fun chapterListSelector() = "div.komik_info-chapters li"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
val urlElements = element.select("a")
setUrlWithoutDomain(urlElements.attr("href"))
name = element.select(".chapter-link-item").text()
date_upload = parseChapterDate2(element.select(".chapter-link-time").text())
}
private fun parseChapterDate2(date: String): Long {
return if (date.endsWith("ago")) {
val value = date.split(' ')[0].toInt()
when {
"min" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
}.timeInMillis
"hour" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis
"day" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
}.timeInMillis
"week" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
}.timeInMillis
"month" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
}.timeInMillis
"year" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
}.timeInMillis
else -> {
0L
}
}
} else {
try {
dateFormat.parse(date)?.time ?: 0
} catch (_: Exception) {
0L
}
}
}
override fun pageListParse(document: Document): List<Page> {
var doc = document
var cssQuery = "div#chapter_body .main-reading-area img"
val imageListRegex = Regex("chapterImages = (.*) \\|\\|")
val imageListMatchResult = imageListRegex.find(document.toString())
if (imageListMatchResult != null) {
val imageListJson = imageListMatchResult.destructured.toList()[0]
val imageList = json.parseToJsonElement(imageListJson).jsonObject
var imageServer = "cdn"
if (!imageList.containsKey(imageServer)) imageServer = imageList.keys.first()
val imageElement = imageList[imageServer]!!.jsonArray.joinToString("")
doc = Jsoup.parse(imageElement)
cssQuery = "img.size-full"
}
return doc.select(cssQuery)
.mapIndexed { i, img -> Page(i, document.location(), img.imgAttr()) }
}
override val hasProjectPage: Boolean = true
override val projectPageString = "/project-list"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
if (query.isNotEmpty()) {
url.addPathSegments("page/$page/").addQueryParameter("s", query)
} else {
url.addPathSegment(mangaUrlDirectory.substring(1)).addPathSegments("page/$page/")
}
filters.forEach { filter ->
when (filter) {
is StatusFilter -> {
url.addQueryParameter("status", filter.selectedValue())
}
is TypeFilter -> {
url.addQueryParameter("type", filter.selectedValue())
}
is OrderByFilter -> {
url.addQueryParameter("orderby", filter.selectedValue())
}
is GenreListFilter -> {
filter.state
.filter { it.state != Filter.TriState.STATE_IGNORE }
.forEach {
val value = if (it.state == Filter.TriState.STATE_EXCLUDE) "-${it.value}" else it.value
url.addQueryParameter("genre[]", value)
}
}
// if site has project page, default value "hasProjectPage" = false
is ProjectFilter -> {
if (filter.selectedValue() == "project-filter-on") {
url.setPathSegment(0, projectPageString.substring(1))
}
}
else -> { /* Do Nothing */ }
}
}
return GET(url.build())
}
private class StatusFilter : SelectFilter(
"Status",
arrayOf(
Pair("All", ""),
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
),
)
private class TypeFilter : SelectFilter(
"Type",
arrayOf(
Pair("All", ""),
Pair("Manga", "manga"),
Pair("Manhwa", "manhwa"),
Pair("Manhua", "manhua"),
),
)
private class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
"Sort By",
arrayOf(
Pair("Default", ""),
Pair("A-Z", "titleasc"),
Pair("Z-A", "titledesc"),
Pair("Update", "update"),
Pair("Popular", "popular"),
),
defaultOrder,
)
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>(
Filter.Separator(),
StatusFilter(),
TypeFilter(),
OrderByFilter(),
Filter.Header("Genre exclusion is not available for all sources"),
GenreListFilter(getGenreList()),
Filter.Separator(),
Filter.Header("NOTE: Can't be used with other filter!"),
Filter.Header("$name Project List page"),
ProjectFilter(),
)
return FilterList(filters)
}
}