add Manga18 multisrc (#1135)
* add Manga18 multisrc 18 Porn Comic Comic1000 HANMAN18 Hentai3z.CC Manga18.Club TuManhwas.Club * tag filters * empty alt name * lint
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,8 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.hanman18
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.manga18.Manga18
|
||||
|
||||
class HANMAN18 : Manga18("HANMAN18", "https://hanman18.com", "zh") {
|
||||
// tag filter doesn't work on site
|
||||
override val getAvailableTags = false
|
||||
}
|
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 53 KiB |
|
@ -0,0 +1,16 @@
|
|||
package eu.kanade.tachiyomi.extension.en.hentai3zcc
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.manga18.Manga18
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class Hentai3zCC : Manga18("Hentai3z.CC", "https://hentai3z.cc", "en") {
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
title = element.selectFirst("div.mg_info > div.mg_name a")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
?.replace("cover_thumb_2.webp", "cover_250x350.jpg")
|
||||
?.replace("admin.manga18.us", "bk.18porncomic.com")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
package eu.kanade.tachiyomi.multisrc.manga18
|
||||
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.lang.Exception
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
abstract class Manga18(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list-manga/$page?order_by=views", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
getTags(document)
|
||||
|
||||
val entries = document.select(popularMangaSelector()).map(::popularMangaFromElement)
|
||||
val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null
|
||||
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.story_item"
|
||||
override fun popularMangaNextPageSelector() = ".pagination a[rel=next]"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
title = element.selectFirst("div.mg_info > div.mg_name a")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list-manga/$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
val tag = filters.filterIsInstance<TagFilter>().firstOrNull()
|
||||
if (query.isNotEmpty() || tag?.selected.isNullOrEmpty()) {
|
||||
addPathSegment("list-manga")
|
||||
addPathSegment(page.toString())
|
||||
addQueryParameter("search", query.trim())
|
||||
} else {
|
||||
addPathSegment("manga-list")
|
||||
addPathSegment(tag!!.selected!!)
|
||||
addPathSegment(page.toString())
|
||||
filters.filterIsInstance<SortFilter>().firstOrNull()?.selected?.let {
|
||||
addQueryParameter("order_by", it)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
protected open val getAvailableTags = true
|
||||
protected open val tagsSelector = "div.grid_cate li > a"
|
||||
|
||||
private var tags = listOf<Pair<String, String>>()
|
||||
private var getTagsAttempts = 0
|
||||
|
||||
protected open fun getTags(document: Document) {
|
||||
if (getAvailableTags && tags.isEmpty() && getTagsAttempts < 3) {
|
||||
try {
|
||||
tags = document.select(tagsSelector).map {
|
||||
Pair(
|
||||
it.text().trim(),
|
||||
it.attr("href")
|
||||
.removeSuffix("/")
|
||||
.substringAfterLast("/"),
|
||||
)
|
||||
}.let {
|
||||
listOf(Pair("", "")) + it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(name, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
if (!getAvailableTags) return FilterList()
|
||||
|
||||
return if (tags.isEmpty()) {
|
||||
FilterList(
|
||||
Filter.Header("Press 'reset' to attempt to load genres"),
|
||||
)
|
||||
} else {
|
||||
FilterList(
|
||||
Filter.Header("Ignored with text search"),
|
||||
Filter.Separator(),
|
||||
SortFilter(),
|
||||
TagFilter(tags),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected open val infoElementSelector = "div.detail_listInfo"
|
||||
protected open val titleSelector = "div.detail_name > h1"
|
||||
protected open val descriptionSelector = "div.detail_reviewContent"
|
||||
protected open val statusSelector = "div.item:contains(Status) div.info_value"
|
||||
protected open val altNameSelector = "div.item:contains(Other name) div.info_value"
|
||||
protected open val genreSelector = "div.info_value > a[href*=/manga-list/]"
|
||||
protected open val authorSelector = "div.info_label:contains(author) + div.info_value, div.info_label:contains(autor) + div.info_value"
|
||||
protected open val artistSelector = "div.info_label:contains(artist) + div.info_value"
|
||||
protected open val thumbnailSelector = "div.detail_avatar > img"
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val info = document.selectFirst(infoElementSelector)!!
|
||||
|
||||
title = document.select(titleSelector).text()
|
||||
description = buildString {
|
||||
document.select(descriptionSelector)
|
||||
.eachText().onEach {
|
||||
append(it.trim())
|
||||
append("\n\n")
|
||||
}
|
||||
|
||||
info.selectFirst(altNameSelector)
|
||||
?.text()
|
||||
?.takeIf { it != "Updating" && it.isNotEmpty() }
|
||||
?.let {
|
||||
append("Alternative Names:\n")
|
||||
append(it.trim())
|
||||
}
|
||||
}
|
||||
status = when (info.select(statusSelector).text()) {
|
||||
"On Going" -> SManga.ONGOING
|
||||
"Completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
author = info.selectFirst(authorSelector)?.text()?.takeIf { it != "Updating" }
|
||||
artist = info.selectFirst(artistSelector)?.text()?.takeIf { it != "Updating" }
|
||||
genre = info.select(genreSelector).eachText().joinToString()
|
||||
thumbnail_url = document.selectFirst(thumbnailSelector)?.absUrl("src")
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapter_box .item"
|
||||
|
||||
protected open val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH)
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
element.selectFirst("a")!!.run {
|
||||
setUrlWithoutDomain(absUrl("href"))
|
||||
name = text()
|
||||
}
|
||||
date_upload = try {
|
||||
dateFormat.parse(element.selectFirst("p")!!.text())!!.time
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val script = document.selectFirst("script:containsData(slides_p_path)")
|
||||
?: throw Exception("Unable to find script with image data")
|
||||
|
||||
val encodedImages = script.data()
|
||||
.substringAfter('[')
|
||||
.substringBefore(",]")
|
||||
.replace("\"", "")
|
||||
.split(",")
|
||||
|
||||
return encodedImages.mapIndexed { idx, encoded ->
|
||||
val url = Base64.decode(encoded, Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||
val imageUrl = when {
|
||||
url.startsWith("/") -> "$baseUrl$url"
|
||||
else -> url
|
||||
}
|
||||
Page(idx, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package eu.kanade.tachiyomi.multisrc.manga18
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
val selected get() = options[state].second.takeUnless { it.isEmpty() }
|
||||
}
|
||||
|
||||
class TagFilter(tags: List<Pair<String, String>>) : SelectFilter("Tags", tags)
|
||||
|
||||
class SortFilter : SelectFilter("Sort", sortValues)
|
||||
|
||||
private val sortValues = listOf(
|
||||
Pair("Latest", ""),
|
||||
Pair("Views", "views"),
|
||||
Pair("A-Z", "name"),
|
||||
)
|
|
@ -0,0 +1,29 @@
|
|||
package eu.kanade.tachiyomi.multisrc.manga18
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class Manga18Generator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "manga18"
|
||||
|
||||
override val themeClass = "Manga18"
|
||||
|
||||
override val baseVersionCode = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("18 Porn Comic", "https://18porncomic.com", "en", isNsfw = true, className = "EighteenPornComic"),
|
||||
SingleLang("Comic1000", "https://comic1000.com", "en", isNsfw = true),
|
||||
SingleLang("HANMAN18", "https://hanman18.com", "zh", isNsfw = true),
|
||||
SingleLang("Hentai3z.CC", "https://hentai3z.cc", "en", isNsfw = true, className = "Hentai3zCC"),
|
||||
SingleLang("Manga18.Club", "https://manga18.club", "en", isNsfw = true, className = "Manga18Club"),
|
||||
SingleLang("TuManhwas.Club", "https://tumanhwas.club", "es", isNsfw = true, className = "TuManhwasClub"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
Manga18Generator().createAll()
|
||||
}
|
||||
}
|
||||
}
|