New multisrc theme: Liliana (#2413)
* new multisrc theme: liliana * dont specify type * suggestions * add raw1001
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 1
|
|
@ -0,0 +1,90 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.liliana
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
interface UrlPartFilter {
|
||||||
|
fun addUrlParameter(url: HttpUrl.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<Pair<String, String>>,
|
||||||
|
private val urlParameter: String,
|
||||||
|
) : UrlPartFilter, Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||||
|
url.addQueryParameter(urlParameter, options[state].second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriStateFilter(name: String, val id: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
abstract class TriStateGroupFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
private val includeUrlParameter: String,
|
||||||
|
private val excludeUrlParameter: String,
|
||||||
|
) : UrlPartFilter, Filter.Group<TriStateFilter>(
|
||||||
|
name,
|
||||||
|
options.map { TriStateFilter(it.first, it.second) },
|
||||||
|
) {
|
||||||
|
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||||
|
url.addQueryParameter(
|
||||||
|
includeUrlParameter,
|
||||||
|
state.filter { it.isIncluded() }.joinToString(",") { it.id },
|
||||||
|
)
|
||||||
|
url.addQueryParameter(
|
||||||
|
excludeUrlParameter,
|
||||||
|
state.filter { it.isExcluded() }.joinToString(",") { it.id },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenreFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : TriStateGroupFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"genres",
|
||||||
|
"notGenres",
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChapterCountFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"chapter_count",
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"status",
|
||||||
|
)
|
||||||
|
|
||||||
|
class GenderFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"sex",
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"sort",
|
||||||
|
)
|
|
@ -0,0 +1,353 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.liliana
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
abstract class Liliana(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
final override val lang: String,
|
||||||
|
private val usesPostSearch: Boolean = false,
|
||||||
|
) : ParsedHttpSource() {
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector(): String = "div#main div.grid > div"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
|
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
||||||
|
with(element.selectFirst(".text-center a")!!) {
|
||||||
|
title = text()
|
||||||
|
setUrlWithoutDomain(attr("abs:href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/all-manga/$page/?sort=last_update&status=0", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||||
|
popularMangaParse(response)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query.isNotBlank() && usesPostSearch) {
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("search", query)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val formHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
add("Host", baseUrl.toHttpUrl().host)
|
||||||
|
add("Origin", baseUrl)
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return POST("$baseUrl/ajax/search", formHeaders, formBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
addPathSegment("search")
|
||||||
|
addQueryParameter("keyword", query)
|
||||||
|
} else {
|
||||||
|
addPathSegment("filter")
|
||||||
|
filters.filterIsInstance<UrlPartFilter>().forEach {
|
||||||
|
it.addUrlParameter(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addPathSegment(page.toString())
|
||||||
|
addPathSegment("")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
if (response.request.method == "GET") {
|
||||||
|
return popularMangaParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaList = response.parseAs<SearchResponseDto>().list.map { manga ->
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(manga.url)
|
||||||
|
title = manga.name
|
||||||
|
thumbnail_url = baseUrl + manga.cover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangaList, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchResponseDto(
|
||||||
|
val list: List<MangaDto>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class MangaDto(
|
||||||
|
val cover: String,
|
||||||
|
val name: String,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Filters ==============================
|
||||||
|
|
||||||
|
protected var genreName = ""
|
||||||
|
protected var genreData = listOf<Pair<String, String>>()
|
||||||
|
protected var chapterCountName = ""
|
||||||
|
protected var chapterCountData = listOf<Pair<String, String>>()
|
||||||
|
protected var statusName = ""
|
||||||
|
protected var statusData = listOf<Pair<String, String>>()
|
||||||
|
protected var genderName = ""
|
||||||
|
protected var genderData = listOf<Pair<String, String>>()
|
||||||
|
protected var sortName = ""
|
||||||
|
protected var sortData = listOf<Pair<String, String>>()
|
||||||
|
private var fetchFilterAttempts = 0
|
||||||
|
|
||||||
|
protected suspend fun fetchFilters() {
|
||||||
|
if (
|
||||||
|
fetchFilterAttempts < 3 &&
|
||||||
|
arrayOf(genreData, chapterCountData, statusData, genderData, sortData).any { it.isEmpty() }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val doc = client.newCall(filtersRequest())
|
||||||
|
.await()
|
||||||
|
.asJsoup()
|
||||||
|
|
||||||
|
parseFilters(doc)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("$name: Filters", e.stackTraceToString())
|
||||||
|
}
|
||||||
|
fetchFilterAttempts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun filtersRequest() = GET("$baseUrl/filter", headers)
|
||||||
|
|
||||||
|
protected open fun parseFilters(document: Document) {
|
||||||
|
genreName = document.selectFirst("div.advanced-genres > h3")?.text() ?: ""
|
||||||
|
genreData = document.select("div.advanced-genres > div > .advance-item").map {
|
||||||
|
it.text() to it.selectFirst("span")!!.attr("data-genre")
|
||||||
|
}
|
||||||
|
|
||||||
|
chapterCountName = document.getSelectName("select-count")
|
||||||
|
chapterCountData = document.getSelectData("select-count")
|
||||||
|
|
||||||
|
statusName = document.getSelectName("select-status")
|
||||||
|
statusData = document.getSelectData("select-status")
|
||||||
|
|
||||||
|
genderName = document.getSelectName("select-gender")
|
||||||
|
genderData = document.getSelectData("select-gender")
|
||||||
|
|
||||||
|
sortName = document.getSelectName("select-sort")
|
||||||
|
sortData = document.getSelectData("select-sort")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Document.getSelectName(selectorClass: String): String {
|
||||||
|
return this.selectFirst(".select-div > label.$selectorClass")?.text() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Document.getSelectData(selectorId: String): List<Pair<String, String>> {
|
||||||
|
return this.select("#$selectorId > option").map {
|
||||||
|
it.text() to it.attr("value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
launchIO { fetchFilters() }
|
||||||
|
|
||||||
|
val filters = mutableListOf<Filter<*>>()
|
||||||
|
|
||||||
|
if (genreData.isNotEmpty()) {
|
||||||
|
filters.add(GenreFilter(genreName, genreData))
|
||||||
|
}
|
||||||
|
if (chapterCountData.isNotEmpty()) {
|
||||||
|
filters.add(ChapterCountFilter(chapterCountName, chapterCountData))
|
||||||
|
}
|
||||||
|
if (statusData.isNotEmpty()) {
|
||||||
|
filters.add(StatusFilter(statusName, statusData))
|
||||||
|
}
|
||||||
|
if (genderData.isNotEmpty()) {
|
||||||
|
filters.add(GenderFilter(genderName, genderData))
|
||||||
|
}
|
||||||
|
if (sortData.isNotEmpty()) {
|
||||||
|
filters.add(SortFilter(sortName, sortData))
|
||||||
|
}
|
||||||
|
if (filters.size < 5) {
|
||||||
|
filters.add(0, Filter.Header("Press 'reset' to load more filters"))
|
||||||
|
} else {
|
||||||
|
filters.add(0, Filter.Header("NOTE: Ignored if using text search!"))
|
||||||
|
filters.add(1, Filter.Separator())
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
protected fun launchIO(block: suspend () -> Unit) = scope.launch { block() }
|
||||||
|
|
||||||
|
// =========================== Manga Details ============================
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
|
description = document.selectFirst("div#syn-target")?.text()
|
||||||
|
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
|
||||||
|
title = document.selectFirst(".a2 header h1")!!.text()
|
||||||
|
genre = document.select(".a2 div > a[rel='tag'].label").joinToString { it.text() }
|
||||||
|
author = document.selectFirst("div.y6x11p i.fas.fa-user + span.dt")?.text()?.takeUnless {
|
||||||
|
it.equals("updating", true)
|
||||||
|
}
|
||||||
|
status = document.selectFirst("div.y6x11p i.fas.fa-rss + span.dt").parseStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||||
|
"ongoing", "đang tiến hành", "進行中" -> SManga.ONGOING
|
||||||
|
"completed", "hoàn thành", "完了" -> SManga.COMPLETED
|
||||||
|
"on-hold", "tạm ngưng", "保留" -> SManga.ON_HIATUS
|
||||||
|
"canceled", "đã huỷ", "キャンセル" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Chapters ==============================
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "ul > li.chapter"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
|
element.selectFirst("time[datetime]")?.also {
|
||||||
|
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
|
||||||
|
}
|
||||||
|
with(element.selectFirst("a")!!) {
|
||||||
|
name = text()
|
||||||
|
setUrlWithoutDomain(attr("abs:href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Pages ================================
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageListResponseDto(
|
||||||
|
val status: Boolean = false,
|
||||||
|
val msg: String? = null,
|
||||||
|
val html: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")?.data()
|
||||||
|
?: throw Exception("Failed to get chapter id")
|
||||||
|
|
||||||
|
val chapterId = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
||||||
|
|
||||||
|
val pageHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "application/json, text/javascript, *//*; q=0.01")
|
||||||
|
add("Host", baseUrl.toHttpUrl().host)
|
||||||
|
set("Referer", response.request.url.toString())
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val ajaxResponse = client.newCall(
|
||||||
|
GET("$baseUrl/ajax/image/list/chap/$chapterId", pageHeaders),
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
val data = ajaxResponse.parseAs<PageListResponseDto>()
|
||||||
|
|
||||||
|
if (!data.status) {
|
||||||
|
throw Exception(data.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageListParse(
|
||||||
|
Jsoup.parseBodyFragment(
|
||||||
|
data.html,
|
||||||
|
response.request.url.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select("div.separator").mapIndexed { i, page ->
|
||||||
|
val url = page.selectFirst("a")!!.attr("abs:href")
|
||||||
|
Page(i, document.location(), url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val imgHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "image/avif,image/webp,*/*")
|
||||||
|
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||||
|
}.build()
|
||||||
|
return GET(page.imageUrl!!, imgHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
// From mangathemesia
|
||||||
|
private fun Element.imgAttr(): String = when {
|
||||||
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
|
else -> attr("abs:src")
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
return json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manhuagold'
|
extName = 'Manhuagold'
|
||||||
extClass = '.Manhuagold'
|
extClass = '.Manhuagold'
|
||||||
themePkg = 'mangareader'
|
themePkg = 'liliana'
|
||||||
baseUrl = 'https://manhuagold.com'
|
baseUrl = 'https://manhuagold.top'
|
||||||
overrideVersionCode = 33
|
overrideVersionCode = 34
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,233 +1,18 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.comickiba
|
package eu.kanade.tachiyomi.extension.en.comickiba
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
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 eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.jsoup.nodes.TextNode
|
|
||||||
import org.jsoup.select.Evaluator
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
class Manhuagold : MangaReader() {
|
class Manhuagold : Liliana(
|
||||||
|
"Manhuagold",
|
||||||
|
"https://manhuagold.top",
|
||||||
|
"en",
|
||||||
|
usesPostSearch = true,
|
||||||
|
) {
|
||||||
|
// MangaReader -> Liliana
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
override val name = "Manhuagold"
|
override val client = super.client.newBuilder()
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val baseUrl = "https://manhuagold.com"
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) =
|
|
||||||
GET("$baseUrl/filter/$page/?sort=views&sex=All&chapter_count=0", headers)
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) =
|
|
||||||
GET("$baseUrl/filter/$page/?sort=latest-updated&sex=All&chapter_count=0", headers)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
urlBuilder.addPathSegment("search").apply {
|
|
||||||
addQueryParameter("keyword", query)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
urlBuilder.addPathSegment("filter").apply {
|
|
||||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is Select -> {
|
|
||||||
addQueryParameter(filter.param, filter.selection)
|
|
||||||
}
|
|
||||||
is GenresFilter -> {
|
|
||||||
addQueryParameter(filter.param, filter.selection)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
urlBuilder.addPathSegment(page.toString())
|
|
||||||
urlBuilder.addPathSegment("")
|
|
||||||
|
|
||||||
return GET(urlBuilder.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) =
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
|
||||||
title = it.attr("alt")
|
|
||||||
thumbnail_url = it.imgAttr()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "ul.pagination > li.active + li"
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
override fun getFilterList() =
|
|
||||||
FilterList(
|
|
||||||
Note,
|
|
||||||
StatusFilter(),
|
|
||||||
SortFilter(),
|
|
||||||
GenresFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
|
|
||||||
val mangaTitle = root.selectFirst(Evaluator.Class("manga-name"))!!.ownText()
|
|
||||||
title = mangaTitle
|
|
||||||
description = root.run {
|
|
||||||
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
|
||||||
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
|
|
||||||
"", mangaTitle -> description
|
|
||||||
else -> "$description\n\nAlternative Title: $altTitle"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.imgAttr()
|
|
||||||
genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() }
|
|
||||||
for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) {
|
|
||||||
if (item.hasClass("item").not()) continue
|
|
||||||
when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) {
|
|
||||||
"Authors:" -> item.parseAuthorsTo(this)
|
|
||||||
"Status:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText().lowercase()) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
"on-hold" -> SManga.ON_HIATUS
|
|
||||||
"canceled" -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Element.parseAuthorsTo(manga: SManga) {
|
|
||||||
val authors = select(Evaluator.Tag("a"))
|
|
||||||
val text = authors.map { it.ownText().replace(",", "") }
|
|
||||||
val count = authors.size
|
|
||||||
when (count) {
|
|
||||||
0 -> return
|
|
||||||
1 -> {
|
|
||||||
manga.author = text[0]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val authorList = ArrayList<String>(count)
|
|
||||||
val artistList = ArrayList<String>(count)
|
|
||||||
for ((index, author) in authors.withIndex()) {
|
|
||||||
val textNode = author.nextSibling() as? TextNode
|
|
||||||
val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList
|
|
||||||
list.add(text[index])
|
|
||||||
}
|
|
||||||
if (authorList.isEmpty().not()) manga.author = authorList.joinToString()
|
|
||||||
if (artistList.isEmpty().not()) manga.artist = artistList.joinToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListRequest(mangaUrl: String, type: String): Request =
|
|
||||||
GET(baseUrl + mangaUrl, headers)
|
|
||||||
|
|
||||||
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override val chapterType = ""
|
|
||||||
override val volumeType = ""
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
return client.newCall(chapterListRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map(::parseChapterList)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterList(response: Response): List<SChapter> {
|
|
||||||
val document = response.use { it.asJsoup() }
|
|
||||||
|
|
||||||
return document.select(chapterListSelector())
|
|
||||||
.map(::chapterFromElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun chapterListSelector(): String = "#chapters-list > li"
|
|
||||||
|
|
||||||
private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
element.selectFirst("a")!!.run {
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
name = selectFirst(".name")?.text() ?: text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Images
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable {
|
|
||||||
val document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
|
|
||||||
|
|
||||||
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
|
|
||||||
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
|
||||||
|
|
||||||
val ajaxHeaders = super.headersBuilder().apply {
|
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
|
||||||
add("Referer", baseUrl + chapter.url)
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val ajaxUrl = "$baseUrl/ajax/image/list/chap/$id"
|
|
||||||
client.newCall(GET(ajaxUrl, ajaxHeaders)).execute().let(::pageListParse)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.use { it.parseHtmlProperty() }
|
|
||||||
|
|
||||||
val pageList = document.select("div").map {
|
|
||||||
val index = it.attr("data-number").toInt()
|
|
||||||
val imgUrl = it.imgAttr().ifEmpty { it.selectFirst("img")!!.imgAttr() }
|
|
||||||
|
|
||||||
Page(index, "", imgUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageList
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
|
|
||||||
// From mangathemesia
|
|
||||||
private fun Element.imgAttr(): String = when {
|
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
|
||||||
else -> attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.parseHtmlProperty(): Document {
|
|
||||||
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
|
|
||||||
return Jsoup.parseBodyFragment(html)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'ManhuaPlus (unoriginal)'
|
extName = 'ManhuaPlus (unoriginal)'
|
||||||
extClass = '.ManhuaPlusOrg'
|
extClass = '.ManhuaPlusOrg'
|
||||||
extVersionCode = 1
|
themePkg = 'liliana'
|
||||||
|
baseUrl = 'https://manhuaplus.org'
|
||||||
|
overrideVersionCode = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,242 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||||
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.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class ManhuaPlusOrg : ParsedHttpSource() {
|
class ManhuaPlusOrg : Liliana(
|
||||||
|
"ManhuaPlus (Unoriginal)",
|
||||||
override val name = "ManhuaPlus (Unoriginal)"
|
"https://manhuaplus.org",
|
||||||
|
"en",
|
||||||
override val baseUrl = "https://manhuaplus.org"
|
)
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(1)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String = "div#main div.grid > div"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
|
||||||
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
|
||||||
element.selectFirst(".text-center a")!!.run {
|
|
||||||
title = text().trim()
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/all-manga/$page/?sort=1", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
addPathSegment("search")
|
|
||||||
addQueryParameter("keyword", query)
|
|
||||||
} else {
|
|
||||||
addPathSegment("filter")
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is GenreFilter -> {
|
|
||||||
if (filter.checked.isNotEmpty()) {
|
|
||||||
addQueryParameter("genres", filter.checked.joinToString(","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is StatusFilter -> {
|
|
||||||
if (filter.selected.isNotBlank()) {
|
|
||||||
addQueryParameter("status", filter.selected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is SortFilter -> {
|
|
||||||
addQueryParameter("sort", filter.selected)
|
|
||||||
}
|
|
||||||
is ChapterCountFilter -> {
|
|
||||||
addQueryParameter("chapter_count", filter.selected)
|
|
||||||
}
|
|
||||||
is GenderFilter -> {
|
|
||||||
addQueryParameter("sex", filter.selected)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addPathSegment(page.toString())
|
|
||||||
addPathSegment("")
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
|
||||||
|
|
||||||
override fun searchMangaSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList(
|
|
||||||
Filter.Header("Ignored when using text search"),
|
|
||||||
Filter.Separator(),
|
|
||||||
GenreFilter(),
|
|
||||||
ChapterCountFilter(),
|
|
||||||
GenderFilter(),
|
|
||||||
StatusFilter(),
|
|
||||||
SortFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
|
||||||
description = document.selectFirst("div#syn-target")?.text()
|
|
||||||
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
|
|
||||||
title = document.selectFirst(".a2 header h1")?.text()?.trim() ?: "N/A"
|
|
||||||
genre = document.select(".a2 div > a[rel='tag'].label").joinToString(", ") { it.text() }
|
|
||||||
|
|
||||||
document.selectFirst(".a1 > aside")?.run {
|
|
||||||
author = select("div:contains(Authors) > span a")
|
|
||||||
.joinToString(", ") { it.text().trim() }
|
|
||||||
.takeUnless { it.isBlank() || it.equals("Updating", true) }
|
|
||||||
status = selectFirst("div:contains(Status) > span")?.text().let(::parseStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String?): Int = when {
|
|
||||||
status.equals("ongoing", true) -> SManga.ONGOING
|
|
||||||
status.equals("completed", true) -> SManga.COMPLETED
|
|
||||||
status.equals("on-hold", true) -> SManga.ON_HIATUS
|
|
||||||
status.equals("canceled", true) -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "ul > li.chapter"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
element.selectFirst("time[datetime]")?.also {
|
|
||||||
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
|
|
||||||
}
|
|
||||||
element.selectFirst("a")!!.run {
|
|
||||||
text().trim().also {
|
|
||||||
name = it
|
|
||||||
chapter_number = it.substringAfter("hapter ").toFloatOrNull() ?: 0F
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
|
|
||||||
|
|
||||||
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
|
|
||||||
|
|
||||||
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
|
||||||
|
|
||||||
val pageHeaders = headersBuilder().apply {
|
|
||||||
add("Accept", "application/json, text/javascript, *//*; q=0.01")
|
|
||||||
add("Host", baseUrl.toHttpUrl().host)
|
|
||||||
add("Referer", baseUrl + chapter.url)
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET("$baseUrl/ajax/image/list/chap/$id", pageHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PageListResponseDto(val html: String)
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val data = response.parseAs<PageListResponseDto>().html
|
|
||||||
return pageListParse(
|
|
||||||
Jsoup.parseBodyFragment(
|
|
||||||
data,
|
|
||||||
response.request.header("Referer")!!,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
return document.select("div.separator").map { page ->
|
|
||||||
val index = page.selectFirst("img")!!.attr("alt").substringAfterLast(" ").toInt()
|
|
||||||
val url = page.selectFirst("a")!!.attr("abs:href")
|
|
||||||
Page(index, document.location(), url)
|
|
||||||
}.sortedBy { it.index }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
val imgHeaders = headersBuilder().apply {
|
|
||||||
add("Accept", "image/avif,image/webp,*/*")
|
|
||||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
|
||||||
}.build()
|
|
||||||
return GET(page.imageUrl!!, imgHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
|
|
||||||
// From mangathemesia
|
|
||||||
private fun Element.imgAttr(): String = when {
|
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
|
||||||
else -> attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
class CheckBoxFilter(
|
|
||||||
name: String,
|
|
||||||
val value: String,
|
|
||||||
) : Filter.CheckBox(name)
|
|
||||||
|
|
||||||
class ChapterCountFilter : SelectFilter("Chapter count", chapterCount) {
|
|
||||||
companion object {
|
|
||||||
private val chapterCount = listOf(
|
|
||||||
Pair(">= 0", "0"),
|
|
||||||
Pair(">= 10", "10"),
|
|
||||||
Pair(">= 30", "30"),
|
|
||||||
Pair(">= 50", "50"),
|
|
||||||
Pair(">= 100", "100"),
|
|
||||||
Pair(">= 200", "200"),
|
|
||||||
Pair(">= 300", "300"),
|
|
||||||
Pair(">= 400", "400"),
|
|
||||||
Pair(">= 500", "500"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenderFilter : SelectFilter("Manga Gender", gender) {
|
|
||||||
companion object {
|
|
||||||
private val gender = listOf(
|
|
||||||
Pair("All", "All"),
|
|
||||||
Pair("Boy", "Boy"),
|
|
||||||
Pair("Girl", "Girl"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusFilter : SelectFilter("Status", status) {
|
|
||||||
companion object {
|
|
||||||
private val status = listOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Completed", "completed"),
|
|
||||||
Pair("OnGoing", "on-going"),
|
|
||||||
Pair("On-Hold", "on-hold"),
|
|
||||||
Pair("Canceled", "canceled"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SortFilter : SelectFilter("Sort", sort) {
|
|
||||||
companion object {
|
|
||||||
private val sort = listOf(
|
|
||||||
Pair("Default", "default"),
|
|
||||||
Pair("Latest Updated", "latest-updated"),
|
|
||||||
Pair("Most Viewed", "views"),
|
|
||||||
Pair("Most Viewed Month", "views_month"),
|
|
||||||
Pair("Most Viewed Week", "views_week"),
|
|
||||||
Pair("Most Viewed Day", "views_day"),
|
|
||||||
Pair("Score", "score"),
|
|
||||||
Pair("Name A-Z", "az"),
|
|
||||||
Pair("Name Z-A", "za"),
|
|
||||||
Pair("Newest", "new"),
|
|
||||||
Pair("Oldest", "old"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenreFilter : Filter.Group<CheckBoxFilter>(
|
|
||||||
"Genre",
|
|
||||||
genres.map { CheckBoxFilter(it.first, it.second) },
|
|
||||||
) {
|
|
||||||
val checked get() = state.filter { it.state }.map { it.value }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val genres = listOf(
|
|
||||||
Pair("Action", "4"),
|
|
||||||
Pair("Adaptation", "87"),
|
|
||||||
Pair("Adult", "31"),
|
|
||||||
Pair("Adventure", "5"),
|
|
||||||
Pair("Animals", "1657"),
|
|
||||||
Pair("Cartoon", "46"),
|
|
||||||
Pair("Comedy", "14"),
|
|
||||||
Pair("Demons", "284"),
|
|
||||||
Pair("Drama", "59"),
|
|
||||||
Pair("Ecchi", "67"),
|
|
||||||
Pair("Fantasy", "6"),
|
|
||||||
Pair("Full Color", "89"),
|
|
||||||
Pair("Genderswap", "2409"),
|
|
||||||
Pair("Ghosts", "2253"),
|
|
||||||
Pair("Gore", "1182"),
|
|
||||||
Pair("Harem", "17"),
|
|
||||||
Pair("Historical", "642"),
|
|
||||||
Pair("Horror", "797"),
|
|
||||||
Pair("Isekai", "239"),
|
|
||||||
Pair("Live action", "11"),
|
|
||||||
Pair("Long Strip", "86"),
|
|
||||||
Pair("Magic", "90"),
|
|
||||||
Pair("Magical Girls", "1470"),
|
|
||||||
Pair("Manhua", "7"),
|
|
||||||
Pair("Manhwa", "70"),
|
|
||||||
Pair("Martial Arts", "8"),
|
|
||||||
Pair("Mature", "12"),
|
|
||||||
Pair("Mecha", "786"),
|
|
||||||
Pair("Medical", "1443"),
|
|
||||||
Pair("Monsters", "138"),
|
|
||||||
Pair("Mystery", "9"),
|
|
||||||
Pair("Post-Apocalyptic", "285"),
|
|
||||||
Pair("Psychological", "798"),
|
|
||||||
Pair("Reincarnation", "139"),
|
|
||||||
Pair("Romance", "987"),
|
|
||||||
Pair("School Life", "10"),
|
|
||||||
Pair("Sci-fi", "135"),
|
|
||||||
Pair("Seinen", "196"),
|
|
||||||
Pair("Shounen", "26"),
|
|
||||||
Pair("Shounen ai", "64"),
|
|
||||||
Pair("Slice of Life", "197"),
|
|
||||||
Pair("Superhero", "136"),
|
|
||||||
Pair("Supernatural", "13"),
|
|
||||||
Pair("Survival", "140"),
|
|
||||||
Pair("Thriller", "137"),
|
|
||||||
Pair("Time travel", "231"),
|
|
||||||
Pair("Tragedy", "15"),
|
|
||||||
Pair("Video Games", "283"),
|
|
||||||
Pair("Villainess", "676"),
|
|
||||||
Pair("Virtual Reality", "611"),
|
|
||||||
Pair("Web comic", "88"),
|
|
||||||
Pair("Webtoon", "18"),
|
|
||||||
Pair("Wuxia", "239"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Manga Koma'
|
||||||
|
extClass = '.MangaKoma'
|
||||||
|
themePkg = 'liliana'
|
||||||
|
baseUrl = 'https://mangakoma01.net'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 8.6 KiB |
|
@ -0,0 +1,15 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ja.mangakoma
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
|
class MangaKoma : Liliana("Manga Koma", "https://mangakoma01.net", "ja") {
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select("div.separator[data-index]").map { page ->
|
||||||
|
val index = page.attr("data-index").toInt()
|
||||||
|
val url = page.selectFirst("a")!!.attr("abs:href")
|
||||||
|
Page(index, document.location(), url)
|
||||||
|
}.sortedBy { it.index }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Raw1001'
|
||||||
|
extClass = '.Raw1001'
|
||||||
|
themePkg = 'liliana'
|
||||||
|
baseUrl = 'https://raw1001.net'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 9.1 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ja.raw1001
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||||
|
|
||||||
|
class Raw1001 : Liliana("Raw1001", "https://raw1001.net", "ja")
|
|
@ -1,7 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'DocTruyen5s'
|
extName = 'DocTruyen5s'
|
||||||
extClass = '.DocTruyen5s'
|
extClass = '.DocTruyen5s'
|
||||||
extVersionCode = 2
|
themePkg = 'liliana'
|
||||||
|
baseUrl = 'https://manga.io.vn'
|
||||||
|
overrideVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,377 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.extension.vi.doctruyen5s
|
package eu.kanade.tachiyomi.extension.vi.doctruyen5s
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
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 eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class DocTruyen5s : ParsedHttpSource() {
|
class DocTruyen5s : Liliana("DocTruyen5s", "https://manga.io.vn", "vi")
|
||||||
|
|
||||||
override val name = "DocTruyen5s"
|
|
||||||
|
|
||||||
override val lang = "vi"
|
|
||||||
|
|
||||||
override val baseUrl = "https://manga.io.vn"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) =
|
|
||||||
GET("$baseUrl/filter/$page/?sort=views_day&chapter_count=0&sex=All", headers)
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.Blog section div.grid > div"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
val anchor = element.selectFirst("div.text-center a")!!
|
|
||||||
|
|
||||||
setUrlWithoutDomain(anchor.attr("abs:href"))
|
|
||||||
title = anchor.text()
|
|
||||||
thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "span.pagecurrent:not(:last-child)"
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) =
|
|
||||||
GET("$baseUrl/filter/$page/?sort=latest-updated&chapter_count=0&sex=All", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = if (query.isNotBlank()) {
|
|
||||||
"$baseUrl/search/$page/".toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("keyword", query)
|
|
||||||
}.build()
|
|
||||||
} else {
|
|
||||||
val builder = "$baseUrl/filter/$page/".toHttpUrl().newBuilder()
|
|
||||||
|
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).filterIsInstance<UriFilter>()
|
|
||||||
.forEach { it.addToUri(builder) }
|
|
||||||
|
|
||||||
builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
title = document.selectFirst("article header h1")!!.text()
|
|
||||||
author = document.selectFirst("div.y6x11p i.fas.fa-user + span.dt")?.text()
|
|
||||||
description = document.selectFirst("div#syn-target")?.text()
|
|
||||||
genre = document.select("a.label[rel=tag]").joinToString { it.text() }
|
|
||||||
status = when (document.selectFirst("div.y6x11p i.fas.fa-rss + span.dt")?.text()) {
|
|
||||||
"Đang tiến hành" -> SManga.ONGOING
|
|
||||||
"Hoàn thành" -> SManga.COMPLETED
|
|
||||||
"Tạm ngưng" -> SManga.ON_HIATUS
|
|
||||||
"Đã huỷ" -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
thumbnail_url = document.selectFirst("figure img")?.attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "li.chapter"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
val anchor = element.selectFirst("a")!!
|
|
||||||
|
|
||||||
setUrlWithoutDomain(anchor.attr("abs:href"))
|
|
||||||
name = anchor.text()
|
|
||||||
date_upload = element
|
|
||||||
.selectFirst("time")
|
|
||||||
?.attr("datetime")
|
|
||||||
?.toLongOrNull()
|
|
||||||
?.times(1000L) ?: 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mangaIdRegex = Regex("""const MANGA_ID = (\d+);""")
|
|
||||||
private val chapterIdRegex = Regex("""const CHAPTER_ID = (\d+);""")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PageAjaxResponse(
|
|
||||||
val status: Boolean = false,
|
|
||||||
val msg: String? = null,
|
|
||||||
val html: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val html = client.newCall(GET("$baseUrl${chapter.url}")).execute().body.string()
|
|
||||||
val chapterId = chapterIdRegex.find(html)?.groupValues?.get(1)
|
|
||||||
?: throw Exception("Không tìm thấy ID của chương truyện.")
|
|
||||||
val mangaId = mangaIdRegex.find(html)?.groupValues?.get(1)
|
|
||||||
|
|
||||||
if (mangaId != null) {
|
|
||||||
countViews(mangaId, chapterId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return POST("https://manga.io.vn/ajax/image/list/chap/$chapterId", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val data = json.decodeFromString<PageAjaxResponse>(response.body.string())
|
|
||||||
|
|
||||||
if (!data.status) {
|
|
||||||
throw Exception(data.msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageListParse(Jsoup.parse(data.html))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document) =
|
|
||||||
document.select("a.readImg img").mapIndexed { i, it ->
|
|
||||||
Page(i, imageUrl = it.attr("abs:src"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private fun countViews(mangaId: String, chapterId: String) {
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("manga", mangaId)
|
|
||||||
.add("chapter", chapterId)
|
|
||||||
.build()
|
|
||||||
val request = POST(
|
|
||||||
"$baseUrl/ajax/manga/view",
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
|
|
||||||
runCatching { client.newCall(request).execute().close() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
Filter.Header("Không dùng chung với tìm kiếm bằng tên"),
|
|
||||||
ChapterCountFilter(),
|
|
||||||
StatusFilter(),
|
|
||||||
GenderFilter(),
|
|
||||||
OrderByFilter(),
|
|
||||||
GenreList(getGenresList()),
|
|
||||||
)
|
|
||||||
|
|
||||||
interface UriFilter {
|
|
||||||
fun addToUri(builder: HttpUrl.Builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
open class UriPartFilter(
|
|
||||||
name: String,
|
|
||||||
private val query: String,
|
|
||||||
private val vals: Array<Pair<String, String>>,
|
|
||||||
state: Int = 0,
|
|
||||||
) : UriFilter, Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
builder.addQueryParameter(query, vals[state].second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChapterCountFilter : UriPartFilter(
|
|
||||||
"Số chương",
|
|
||||||
"chapter_count",
|
|
||||||
arrayOf(
|
|
||||||
">= 0" to "0",
|
|
||||||
">= 10" to "10",
|
|
||||||
">= 30" to "30",
|
|
||||||
">= 50" to "50",
|
|
||||||
">= 100" to "100",
|
|
||||||
">= 200" to "200",
|
|
||||||
">= 300" to "300",
|
|
||||||
">= 400" to "400",
|
|
||||||
">= 500" to "500",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class GenderFilter : UriPartFilter(
|
|
||||||
"Giới tính",
|
|
||||||
"sex",
|
|
||||||
arrayOf(
|
|
||||||
"Tất cả" to "All",
|
|
||||||
"Con trai" to "Boy",
|
|
||||||
"Con gái" to "Girl",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class StatusFilter : UriPartFilter(
|
|
||||||
"Trạng thái",
|
|
||||||
"status",
|
|
||||||
arrayOf(
|
|
||||||
"Tất cả" to "",
|
|
||||||
"Hoàn thành" to "completed",
|
|
||||||
"Đang tiến hành" to "on-going",
|
|
||||||
"Tạm ngưng" to "on-hold",
|
|
||||||
"Đã huỷ" to "canceled",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class OrderByFilter : UriPartFilter(
|
|
||||||
"Sắp xếp",
|
|
||||||
"sort",
|
|
||||||
arrayOf(
|
|
||||||
"Mặc định" to "default",
|
|
||||||
"Mới cập nhật" to "latest-updated",
|
|
||||||
"Xem nhiều" to "views",
|
|
||||||
"Xem nhiều nhất tháng" to "views_month",
|
|
||||||
"Xem nhiều nhất tuần" to "views_week",
|
|
||||||
"Xem nhiều nhất hôm nay" to "views_day",
|
|
||||||
"Đánh giá cao" to "score",
|
|
||||||
"Từ A-Z" to "az",
|
|
||||||
"Từ Z-A" to "za",
|
|
||||||
"Số chương nhiều nhất" to "chapters",
|
|
||||||
"Mới nhất" to "new",
|
|
||||||
"Cũ nhất" to "old",
|
|
||||||
),
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Genre(name: String, val id: String) : Filter.TriState(name)
|
|
||||||
|
|
||||||
class GenreList(state: List<Genre>) : UriFilter, Filter.Group<Genre>("Thể loại", state) {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
val genres = mutableListOf<String>()
|
|
||||||
val genresEx = mutableListOf<String>()
|
|
||||||
|
|
||||||
state.forEach {
|
|
||||||
when (it.state) {
|
|
||||||
TriState.STATE_INCLUDE -> genres.add(it.id)
|
|
||||||
TriState.STATE_EXCLUDE -> genresEx.add(it.id)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (genres.size > 0) {
|
|
||||||
builder.addQueryParameter("genres", genres.joinToString(","))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (genresEx.size > 0) {
|
|
||||||
builder.addQueryParameter("notGenres", genresEx.joinToString(","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Get the list by navigating to https://manga.io.vn/filter/1 and paste in the code below
|
|
||||||
```
|
|
||||||
copy([...document.querySelectorAll("div.advanced-genres div.advance-item")].map((e) => {
|
|
||||||
const genreId = e.querySelector("span").dataset.genre;
|
|
||||||
const genreName = e.querySelector("label").textContent;
|
|
||||||
return `Genre("${genreName}", "${genreId}"),`
|
|
||||||
}).join("\n"))
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
private fun getGenresList() = listOf(
|
|
||||||
Genre("16+", "788"),
|
|
||||||
Genre("Action", "129"),
|
|
||||||
Genre("Adult", "837"),
|
|
||||||
Genre("Adventure", "810"),
|
|
||||||
Genre("Bi Kịch", "393"),
|
|
||||||
Genre("Cải Biên Tiểu Thuyết", "771"),
|
|
||||||
Genre("Chuyển sinh", "287"),
|
|
||||||
Genre("Chuyển Thể", "803"),
|
|
||||||
Genre("Cổ Đại", "809"),
|
|
||||||
Genre("Cổ Trang", "340"),
|
|
||||||
Genre("Comedy", "131"),
|
|
||||||
Genre("Comic", "828"),
|
|
||||||
Genre("Cooking", "834"),
|
|
||||||
Genre("Doujinshi", "201"),
|
|
||||||
Genre("Drama", "149"),
|
|
||||||
Genre("Ecchi", "300"),
|
|
||||||
Genre("Fantasy", "132"),
|
|
||||||
Genre("Full màu", "189"),
|
|
||||||
Genre("Game", "38"),
|
|
||||||
Genre("Gender Bender", "133"),
|
|
||||||
Genre("gender_bender", "832"),
|
|
||||||
Genre("Girls Love", "815"),
|
|
||||||
Genre("Hài Hước", "791"),
|
|
||||||
Genre("Hào Môn", "779"),
|
|
||||||
Genre("Harem", "187"),
|
|
||||||
Genre("Hiện đại", "285"),
|
|
||||||
Genre("Historical", "836"),
|
|
||||||
Genre("Hoạt Hình", "497"),
|
|
||||||
Genre("Horror", "191"),
|
|
||||||
Genre("Huyền Huyễn", "475"),
|
|
||||||
Genre("Isekai", "811"),
|
|
||||||
Genre("Josei", "395"),
|
|
||||||
Genre("Lịch Sử", "561"),
|
|
||||||
Genre("Ma Mị", "764"),
|
|
||||||
Genre("Magic", "160"),
|
|
||||||
Genre("Main Mạnh", "763"),
|
|
||||||
Genre("Manga", "151"),
|
|
||||||
Genre("Manh Bảo", "807"),
|
|
||||||
Genre("Mạnh Mẽ", "818"),
|
|
||||||
Genre("Manhua", "153"),
|
|
||||||
Genre("Manhwa", "193"),
|
|
||||||
Genre("Martial Arts", "614"),
|
|
||||||
Genre("Mystery", "155"),
|
|
||||||
Genre("Ngôn Tình", "156"),
|
|
||||||
Genre("Ngọt Sủng", "799"),
|
|
||||||
Genre("Nữ Cường", "819"),
|
|
||||||
Genre("Oneshot", "65"),
|
|
||||||
Genre("Phép Thuật", "808"),
|
|
||||||
Genre("Phiêu Lưu", "478"),
|
|
||||||
Genre("Psychological", "180"),
|
|
||||||
Genre("Quái Vật", "758"),
|
|
||||||
Genre("Romance", "756"),
|
|
||||||
Genre("School Life", "31"),
|
|
||||||
Genre("school_life", "833"),
|
|
||||||
Genre("Sci-Fi", "812"),
|
|
||||||
Genre("Seinen", "172"),
|
|
||||||
Genre("Shoujo", "68"),
|
|
||||||
Genre("Shoujo Ai", "136"),
|
|
||||||
Genre("Shounen", "140"),
|
|
||||||
Genre("Shounen Ai", "203"),
|
|
||||||
Genre("Showbiz", "436"),
|
|
||||||
Genre("siêu nhiên", "765"),
|
|
||||||
Genre("Slice Of Life", "8"),
|
|
||||||
Genre("Sports", "167"),
|
|
||||||
Genre("Sư Tôn", "794"),
|
|
||||||
Genre("Sủng", "820"),
|
|
||||||
Genre("Sủng Nịch", "806"),
|
|
||||||
Genre("Supernatural", "150"),
|
|
||||||
Genre("Tận Thế", "759"),
|
|
||||||
Genre("Thú Thê", "800"),
|
|
||||||
Genre("Tiên Hiệp", "773"),
|
|
||||||
Genre("Tình cảm", "814"),
|
|
||||||
Genre("Tragedy", "822"),
|
|
||||||
Genre("Tranh Sủng", "805"),
|
|
||||||
Genre("Trap (Crossdressing)", "147"),
|
|
||||||
Genre("Trinh Thám", "336"),
|
|
||||||
Genre("Trọng Sinh", "398"),
|
|
||||||
Genre("Trùng Sinh", "392"),
|
|
||||||
Genre("Truy Thê", "780"),
|
|
||||||
Genre("Truyện Màu", "154"),
|
|
||||||
Genre("Truyện Nam", "761"),
|
|
||||||
Genre("Truyện Nữ", "776"),
|
|
||||||
Genre("Tu Tiên", "477"),
|
|
||||||
Genre("Viễn Tưởng", "438"),
|
|
||||||
Genre("VNComic", "787"),
|
|
||||||
Genre("Vườn Trường", "813"),
|
|
||||||
Genre("Webtoon", "198"),
|
|
||||||
Genre("Xuyên Không", "157"),
|
|
||||||
Genre("Yaoi", "593"),
|
|
||||||
Genre("Yuri", "137"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|