diff --git a/lib-multisrc/liliana/build.gradle.kts b/lib-multisrc/liliana/build.gradle.kts new file mode 100644 index 000000000..dc076cc37 --- /dev/null +++ b/lib-multisrc/liliana/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("lib-multisrc") +} + +baseVersionCode = 1 diff --git a/lib-multisrc/liliana/src/eu/kanade/tachiyomi/multisrc/liliana/Filters.kt b/lib-multisrc/liliana/src/eu/kanade/tachiyomi/multisrc/liliana/Filters.kt new file mode 100644 index 000000000..3b144382d --- /dev/null +++ b/lib-multisrc/liliana/src/eu/kanade/tachiyomi/multisrc/liliana/Filters.kt @@ -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>, + private val urlParameter: String, +) : UrlPartFilter, Filter.Select( + 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>, + private val includeUrlParameter: String, + private val excludeUrlParameter: String, +) : UrlPartFilter, Filter.Group( + 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>, +) : TriStateGroupFilter( + name, + options, + "genres", + "notGenres", +) + +class ChapterCountFilter( + name: String, + options: List>, +) : SelectFilter( + name, + options, + "chapter_count", +) + +class StatusFilter( + name: String, + options: List>, +) : SelectFilter( + name, + options, + "status", +) + +class GenderFilter( + name: String, + options: List>, +) : SelectFilter( + name, + options, + "sex", +) + +class SortFilter( + name: String, + options: List>, +) : SelectFilter( + name, + options, + "sort", +) diff --git a/lib-multisrc/liliana/src/eu/kanade/tachiyomi/multisrc/liliana/Liliana.kt b/lib-multisrc/liliana/src/eu/kanade/tachiyomi/multisrc/liliana/Liliana.kt new file mode 100644 index 000000000..fcd6fbb23 --- /dev/null +++ b/lib-multisrc/liliana/src/eu/kanade/tachiyomi/multisrc/liliana/Liliana.kt @@ -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().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().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, + ) { + @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>() + protected var chapterCountName = "" + protected var chapterCountData = listOf>() + protected var statusName = "" + protected var statusData = listOf>() + protected var genderName = "" + protected var genderData = listOf>() + protected var sortName = "" + protected var sortData = listOf>() + 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> { + return this.select("#$selectorId > option").map { + it.text() to it.attr("value") + } + } + + override fun getFilterList(): FilterList { + launchIO { fetchFilters() } + + val filters = mutableListOf>() + + 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 { + 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() + + if (!data.status) { + throw Exception(data.msg) + } + + return pageListParse( + Jsoup.parseBodyFragment( + data.html, + response.request.url.toString(), + ), + ) + } + + override fun pageListParse(document: Document): List { + 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 Response.parseAs(): T { + return json.decodeFromString(body.string()) + } +} diff --git a/src/en/comickiba/build.gradle b/src/en/comickiba/build.gradle index 8007c2652..c002f69f8 100644 --- a/src/en/comickiba/build.gradle +++ b/src/en/comickiba/build.gradle @@ -1,9 +1,9 @@ ext { extName = 'Manhuagold' extClass = '.Manhuagold' - themePkg = 'mangareader' - baseUrl = 'https://manhuagold.com' - overrideVersionCode = 33 + themePkg = 'liliana' + baseUrl = 'https://manhuagold.top' + overrideVersionCode = 34 isNsfw = true } diff --git a/src/en/comickiba/src/eu/kanade/tachiyomi/extension/en/comickiba/Manhuagold.kt b/src/en/comickiba/src/eu/kanade/tachiyomi/extension/en/comickiba/Manhuagold.kt index de17ec25b..b0ff1cd5c 100644 --- a/src/en/comickiba/src/eu/kanade/tachiyomi/extension/en/comickiba/Manhuagold.kt +++ b/src/en/comickiba/src/eu/kanade/tachiyomi/extension/en/comickiba/Manhuagold.kt @@ -1,233 +1,18 @@ package eu.kanade.tachiyomi.extension.en.comickiba -import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.multisrc.liliana.Liliana 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 lang = "en" - - override val baseUrl = "https://manhuagold.com" - - override val client = network.cloudflareClient.newBuilder() + override val client = super.client.newBuilder() .rateLimit(2) .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(count) - val artistList = ArrayList(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 { - TODO("Not yet implemented") - } - - override val chapterType = "" - override val volumeType = "" - - override fun fetchChapterList(manga: SManga): Observable> { - return client.newCall(chapterListRequest(manga)) - .asObservableSuccess() - .map(::parseChapterList) - } - - private fun parseChapterList(response: Response): List { - 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> = 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 { - 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) - } } diff --git a/src/en/manhuaplusorg/build.gradle b/src/en/manhuaplusorg/build.gradle index b996031e7..b1db2215e 100644 --- a/src/en/manhuaplusorg/build.gradle +++ b/src/en/manhuaplusorg/build.gradle @@ -1,7 +1,9 @@ ext { extName = 'ManhuaPlus (unoriginal)' extClass = '.ManhuaPlusOrg' - extVersionCode = 1 + themePkg = 'liliana' + baseUrl = 'https://manhuaplus.org' + overrideVersionCode = 1 } apply from: "$rootDir/common.gradle" diff --git a/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrg.kt b/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrg.kt index 5751e5cae..15c080da7 100644 --- a/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrg.kt +++ b/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrg.kt @@ -1,242 +1,9 @@ package eu.kanade.tachiyomi.extension.en.manhuaplusorg -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.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 +import eu.kanade.tachiyomi.multisrc.liliana.Liliana -class ManhuaPlusOrg : ParsedHttpSource() { - - override val name = "ManhuaPlus (Unoriginal)" - - 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 { - val data = response.parseAs().html - return pageListParse( - Jsoup.parseBodyFragment( - data, - response.request.header("Referer")!!, - ), - ) - } - - override fun pageListParse(document: Document): List { - 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 Response.parseAs(): T { - return json.decodeFromString(body.string()) - } -} +class ManhuaPlusOrg : Liliana( + "ManhuaPlus (Unoriginal)", + "https://manhuaplus.org", + "en", +) diff --git a/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrgFilters.kt b/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrgFilters.kt deleted file mode 100644 index ab7e83da2..000000000 --- a/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrgFilters.kt +++ /dev/null @@ -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>, -) : Filter.Select( - 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( - "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"), - ) - } -} diff --git a/src/ja/mangakoma/build.gradle b/src/ja/mangakoma/build.gradle new file mode 100644 index 000000000..baf270fa1 --- /dev/null +++ b/src/ja/mangakoma/build.gradle @@ -0,0 +1,9 @@ +ext { + extName = 'Manga Koma' + extClass = '.MangaKoma' + themePkg = 'liliana' + baseUrl = 'https://mangakoma01.net' + overrideVersionCode = 0 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/mangakoma/res/mipmap-hdpi/ic_launcher.png b/src/ja/mangakoma/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..cec4aad18 Binary files /dev/null and b/src/ja/mangakoma/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/mangakoma/res/mipmap-mdpi/ic_launcher.png b/src/ja/mangakoma/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8c68a7faf Binary files /dev/null and b/src/ja/mangakoma/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/mangakoma/res/mipmap-xhdpi/ic_launcher.png b/src/ja/mangakoma/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f34c8c641 Binary files /dev/null and b/src/ja/mangakoma/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/mangakoma/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/mangakoma/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d6b495502 Binary files /dev/null and b/src/ja/mangakoma/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/mangakoma/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/mangakoma/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..8c5772c08 Binary files /dev/null and b/src/ja/mangakoma/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/mangakoma/src/eu/kanade/tachiyomi/extension/ja/mangakoma/MangaKoma.kt b/src/ja/mangakoma/src/eu/kanade/tachiyomi/extension/ja/mangakoma/MangaKoma.kt new file mode 100644 index 000000000..d3c31cb15 --- /dev/null +++ b/src/ja/mangakoma/src/eu/kanade/tachiyomi/extension/ja/mangakoma/MangaKoma.kt @@ -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 { + 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 } + } +} diff --git a/src/ja/raw1001/build.gradle b/src/ja/raw1001/build.gradle new file mode 100644 index 000000000..ee7d78642 --- /dev/null +++ b/src/ja/raw1001/build.gradle @@ -0,0 +1,9 @@ +ext { + extName = 'Raw1001' + extClass = '.Raw1001' + themePkg = 'liliana' + baseUrl = 'https://raw1001.net' + overrideVersionCode = 0 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/raw1001/res/mipmap-hdpi/ic_launcher.png b/src/ja/raw1001/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..fdb5ec225 Binary files /dev/null and b/src/ja/raw1001/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/raw1001/res/mipmap-mdpi/ic_launcher.png b/src/ja/raw1001/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e79e76720 Binary files /dev/null and b/src/ja/raw1001/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/raw1001/res/mipmap-xhdpi/ic_launcher.png b/src/ja/raw1001/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..86ebb70fb Binary files /dev/null and b/src/ja/raw1001/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/raw1001/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/raw1001/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..e503b8fe6 Binary files /dev/null and b/src/ja/raw1001/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/raw1001/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/raw1001/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4e12d1ccb Binary files /dev/null and b/src/ja/raw1001/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/raw1001/src/eu/kanade/tachiyomi/extension/ja/raw1001/Raw1001.kt b/src/ja/raw1001/src/eu/kanade/tachiyomi/extension/ja/raw1001/Raw1001.kt new file mode 100644 index 000000000..551fcbbdd --- /dev/null +++ b/src/ja/raw1001/src/eu/kanade/tachiyomi/extension/ja/raw1001/Raw1001.kt @@ -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") diff --git a/src/vi/doctruyen5s/build.gradle b/src/vi/doctruyen5s/build.gradle index 20255d261..0c0111fed 100644 --- a/src/vi/doctruyen5s/build.gradle +++ b/src/vi/doctruyen5s/build.gradle @@ -1,7 +1,9 @@ ext { extName = 'DocTruyen5s' extClass = '.DocTruyen5s' - extVersionCode = 2 + themePkg = 'liliana' + baseUrl = 'https://manga.io.vn' + overrideVersionCode = 2 } apply from: "$rootDir/common.gradle" diff --git a/src/vi/doctruyen5s/src/eu/kanade/tachiyomi/extension/vi/doctruyen5s/DocTruyen5s.kt b/src/vi/doctruyen5s/src/eu/kanade/tachiyomi/extension/vi/doctruyen5s/DocTruyen5s.kt index 595b1888f..605a21951 100644 --- a/src/vi/doctruyen5s/src/eu/kanade/tachiyomi/extension/vi/doctruyen5s/DocTruyen5s.kt +++ b/src/vi/doctruyen5s/src/eu/kanade/tachiyomi/extension/vi/doctruyen5s/DocTruyen5s.kt @@ -1,377 +1,5 @@ package eu.kanade.tachiyomi.extension.vi.doctruyen5s -import eu.kanade.tachiyomi.network.GET -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 +import eu.kanade.tachiyomi.multisrc.liliana.Liliana -class DocTruyen5s : ParsedHttpSource() { - - 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() - .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 { - val data = json.decodeFromString(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>, - state: Int = 0, - ) : UriFilter, Filter.Select(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) : UriFilter, Filter.Group("Thể loại", state) { - override fun addToUri(builder: HttpUrl.Builder) { - val genres = mutableListOf() - val genresEx = mutableListOf() - - 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"), - ) -} +class DocTruyen5s : Liliana("DocTruyen5s", "https://manga.io.vn", "vi")