Add 18Kami (#4262)

* Add 18Kami

* Apply suggestions

- Apply vetleledaal's suggestions

* Apply suggestion

- Apply bapeey's suggestion

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Lint Fix

---------

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
This commit is contained in:
KenjieDec 2024-07-27 21:04:00 +07:00 committed by GitHub
parent 513a0f7e51
commit 4654f367c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 235 additions and 0 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.extension.en.kami18
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
Filter.Header("Filter is ignored when using text search"),
SortFilter("Sort", getSortsList),
TimelineFilter("Timeline", getTimelinesList),
TypeFilter("Type", getTypes),
Filter.Separator(),
TextFilter("Tags"),
)
}
/** Filters **/
internal open class TextFilter(name: String) : Filter.Text(name)
internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, sortList, state)
internal class TypeFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, sortList, state)
internal class TimelineFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, sortList, state)
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second
}
private val getTimelinesList: List<Pair<String, String>> = listOf(
Pair("All Time", "a"),
Pair("Added Today", "d"),
Pair("Added This Week", "w"),
Pair("Added This Month", "m"),
)
private val getTypes: List<Pair<String, String>> = listOf(
Pair("All", ""),
Pair("Other", "another"),
Pair("Comic", "comic"),
Pair("Cosplay", "cosplay"),
Pair("Image", "image"),
Pair("Manga", "manga"),
Pair("Manhwa", "manhwa"),
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Relevant", "mm"),
Pair("Most Recent", "mr"),
Pair("Most Viewed", "mv"),
Pair("Most Photos", "mp"),
Pair("Top Rated", "tr"),
Pair("Most Commented", "md"),
Pair("Most Liked", "tf"),
)

View File

@ -0,0 +1,164 @@
package eu.kanade.tachiyomi.extension.en.kami18
import eu.kanade.tachiyomi.network.GET
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
class Kami18() : HttpSource() {
override val name = "18Kami"
override val lang = "en"
override val baseUrl = "https://18kami.com"
private val baseImageUrl = "$baseUrl/media/photos"
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder().apply {
add("Referer", "$baseUrl/")
}
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/albums?o=mv&page=$page", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val entries = document.select(".image-container")
val hasNextPage = document.selectFirst(".prevnext") != null
return MangasPage(entries.map(::popularMangaFromElement), hasNextPage)
}
private fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a:has(button)")!!.absUrl("href"))
title = element.selectFirst("img")!!.attr("title")
thumbnail_url = element.selectFirst("img")?.let { img ->
img.absUrl("src").takeIf { !it.contains("blank") } ?: img.absUrl("data-original")
}
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/albums?o=mr&page=$page", headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/search/photos".toHttpUrl().newBuilder().apply {
addQueryParameter("main_tag", "5")
addQueryParameter("search_query", query)
}.build()
return GET(url, headers)
}
val url = baseUrl.toHttpUrl().newBuilder().apply {
var type = ""
var search = false
filters.forEach {
when (it) {
is TypeFilter -> {
type = it.getValue()
}
is SortFilter -> {
addQueryParameter("o", it.getValue())
}
is TimelineFilter -> {
addQueryParameter("t", it.getValue())
}
is TextFilter -> {
if (it.state.isNotBlank()) {
search = true
addQueryParameter("main_tag", "3")
addQueryParameter("search_query", it.state.replace(",", " "))
}
}
else -> {}
}
}
addPathSegments(if (search) "search/photos" else "albums")
if (type.isNotEmpty()) addPathSegment(type)
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun getFilterList() = getFilters()
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
description = buildString {
val desc = document.selectFirst("div[class*=p-t-5]:contains(description)")?.ownText()?.substringAfter("") ?: ""
append(desc)
append("\n\n", document.select("div[class\$=p-b-5]:contains(Pages)").text())
}
status = SManga.UNKNOWN
author = document.select("div[class*=p-t-5]:contains(Author) > div").eachText().joinToString()
genre = document.select("div[class*=p-t-5]:contains(Tags) > div:not(:contains(add))").eachText().joinToString()
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
override fun chapterListParse(response: Response): List<SChapter> {
val doc = response.asJsoup()
return doc.selectFirst(".episode")?.let {
it.select("ul > a").reversed().mapIndexed { index, element ->
SChapter.create().apply {
setUrlWithoutDomain("/photo/" + element.attr("data-album"))
name = "Chapter $index"
date_upload = try {
dateFormat.parse(element.selectFirst("span")!!.text())!!.time
} catch (_: Exception) {
0L
}
}
}
} ?: listOf(
SChapter.create().apply {
setUrlWithoutDomain("/photo/" + doc.selectFirst("[id=album_id]")!!.attr("value"))
name = "Chapter 1"
date_upload = try {
dateFormat.parse(doc.selectFirst("[itemprop=datePublished]")!!.text().substringAfter(": "))!!.time
} catch (_: Exception) {
0L
}
},
)
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val contents = document.select("[id*=pageselect] > option")
val id = response.request.url.toString().filter { it.isDigit() }
return contents.mapIndexed { idx, image ->
val filename = image.attr("data-page")
Page(idx, imageUrl = "$baseImageUrl/$id/$filename")
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
}