diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt index 21e6c6e6..b5aa4530 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaChapter.kt @@ -71,4 +71,15 @@ class MangaChapter( override fun toString(): String { return "MangaChapter($id - #$number [$url] - $source)" } + + internal fun copy(number: Int) = MangaChapter( + id = id, + name = name, + number = number, + url = url, + scanlator = scanlator, + uploadDate = uploadDate, + branch = branch, + source = source, + ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt index 1b134fcb..e0795e56 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt @@ -417,6 +417,4 @@ internal class ExHentaiParser( } return joiner.complete().takeUnless { it.isEmpty() } } - - private fun String.isNumeric() = all { c -> c.isDigit() } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt index 172be900..5808fadd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt @@ -155,28 +155,13 @@ internal abstract class GroupleParser( val scripts = doc.select("script") for (script in scripts) { val data = script.html() - val pos = data.indexOf("rm_h.readerInit( 0,") - if (pos == -1) { - continue + var pos = data.indexOf("rm_h.readerDoInit(") + if (pos != -1) { + parsePagesNew(data, pos)?.let { return it } ?: continue } - val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')') - if (json.isEmpty()) { - continue - } - val ja = JSONArray("[$json]") - val pages = ja.getJSONArray(1) - val servers = ja.getJSONArray(3).mapJSON { it.getString("path") } - val serversStr = servers.joinToString("|") - return (0 until pages.length()).map { i -> - val page = pages.getJSONArray(i) - val primaryServer = page.getString(0) - val url = page.getString(2) - MangaPage( - id = generateUid(url), - url = "$primaryServer|$serversStr|$url", - preview = null, - source = source, - ) + pos = data.indexOf("rm_h.readerInit( 0,") + if (pos != -1) { + parsePagesOld(data, pos)?.let { return it } ?: continue } } doc.parseFailed("Pages list not found at ${chapter.url}") @@ -373,4 +358,48 @@ internal abstract class GroupleParser( source = source, ) } + + private fun parsePagesNew(data: String, pos: Int): List? { + val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')') + if (json.isEmpty()) { + return null + } + val ja = JSONArray("[$json]") + val pages = ja.getJSONArray(0) + val servers = ja.getJSONArray(2).mapJSON { it.getString("path") } + val serversStr = servers.joinToString("|") + return (0 until pages.length()).map { i -> + val page = pages.getJSONArray(i) + val primaryServer = page.getString(2) + val url = page.getString(1) + MangaPage( + id = generateUid(url), + url = "$primaryServer|$serversStr|$url", + preview = null, + source = source, + ) + } + } + + private fun parsePagesOld(data: String, pos: Int): List? { + val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')') + if (json.isEmpty()) { + return null + } + val ja = JSONArray("[$json]") + val pages = ja.getJSONArray(1) + val servers = ja.getJSONArray(3).mapJSON { it.getString("path") } + val serversStr = servers.joinToString("|") + return (0 until pages.length()).map { i -> + val page = pages.getJSONArray(i) + val primaryServer = page.getString(0) + val url = page.getString(2) + MangaPage( + id = generateUid(url), + url = "$primaryServer|$serversStr|$url", + preview = null, + source = source, + ) + } + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/MintMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/MintMangaParser.kt index f8aed227..ed2a77d6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/MintMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/MintMangaParser.kt @@ -11,9 +11,8 @@ internal class MintMangaParser( ) : GroupleParser(context, MangaSource.MINTMANGA, 2) { override val configKeyDomain = ConfigKey.Domain( - "23.mintmanga.live", + "24.mintmanga.one", "mintmanga.live", "mintmanga.com", - "m.mintmanga.live", ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt index 66b1ba86..7cf677fe 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.parsers.site.ru.multichan import okhttp3.HttpUrl +import org.jsoup.internal.StringUtil import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParserAuthProvider @@ -22,21 +23,17 @@ internal abstract class ChanParser( SortOrder.RATING, ) + override val isTagsExclusionSupported: Boolean = true + override val authUrl: String get() = "https://${domain}" override val isAuthorized: Boolean get() = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" } - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - tagsExclude: Set?, - sortOrder: SortOrder, - ): List { + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { val domain = domain - val doc = webClient.httpGet(buildUrl(offset, query, tags, sortOrder)).parseHtml() + val doc = webClient.httpGet(buildUrl(offset, filter)).parseHtml() val root = doc.body().selectFirst("div.main_fon")?.getElementById("content") ?: doc.parseFailed("Cannot find root") return root.select("div.content_row").mapNotNull { row -> @@ -180,48 +177,56 @@ internal abstract class ChanParser( protected open fun buildUrl( offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder, + filter: MangaListFilter?, ): HttpUrl { val builder = urlBuilder() builder.addQueryParameter("offset", offset.toString()) - when { - !query.isNullOrEmpty() -> { + when (filter) { + is MangaListFilter.Search -> { builder.addQueryParameter("do", "search") builder.addQueryParameter("subaction", "search") builder.addQueryParameter("search_start", ((offset / 40) + 1).toString()) builder.addQueryParameter("full_search", "0") builder.addQueryParameter("result_from", (offset + 1).toString()) builder.addQueryParameter("result_num", "40") - builder.addQueryParameter("story", query) + builder.addQueryParameter("story", filter.query) builder.addQueryParameter("need_sort_date", "false") } - !tags.isNullOrEmpty() -> { - builder.addPathSegment("tags") - builder.addPathSegment(tags.joinToString("+") { it.key }) - builder.addQueryParameter( - "n", - when (sortOrder) { - SortOrder.RATING, - SortOrder.POPULARITY, - -> "favdesc" - - SortOrder.ALPHABETICAL -> "abcasc" - else -> "" // SortOrder.NEWEST - }, - ) + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty()) { + builder.addPathSegment("tags") + val joiner = StringUtil.StringJoiner("+") + filter.tags.forEach { joiner.add(it.key) } + filter.tagsExclude.forEach { joiner.add("-"); joiner.append(it.key) } + builder.addPathSegment(joiner.complete()) + builder.addQueryParameter( + "n", + when (filter.sortOrder) { + SortOrder.RATING, + SortOrder.POPULARITY, + -> "favdesc" + + SortOrder.ALPHABETICAL -> "abcasc" + else -> "" // SortOrder.NEWEST + }, + ) + } else { + when (filter.sortOrder) { + SortOrder.POPULARITY -> builder.addPathSegment("mostviews") + SortOrder.ALPHABETICAL -> builder.addPathSegment("catalog") + SortOrder.RATING -> builder.addPathSegment("mostfavorites") + else -> { // SortOrder.NEWEST + builder.addPathSegment("manga") + builder.addPathSegment("new") + } + } + } } - else -> when (sortOrder) { - SortOrder.POPULARITY -> builder.addPathSegment("mostviews") - SortOrder.ALPHABETICAL -> builder.addPathSegment("catalog") - SortOrder.RATING -> builder.addPathSegment("mostfavorites") - else -> { // SortOrder.NEWEST - builder.addPathSegment("manga") - builder.addPathSegment("new") - } + null -> { + builder.addPathSegment("manga") + builder.addPathSegment("new") } } return builder.build() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt index 9d25acb4..31be4354 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/HenChanParser.kt @@ -58,10 +58,10 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context, ) } - override fun buildUrl(offset: Int, query: String?, tags: Set?, sortOrder: SortOrder): HttpUrl { - if (query.isNullOrEmpty() && tags.isNullOrEmpty()) { + override fun buildUrl(offset: Int, filter: MangaListFilter?): HttpUrl = when { + filter is MangaListFilter.Advanced && filter.tags.isEmpty() && filter.tagsExclude.isEmpty() -> { val builder = urlBuilder().addQueryParameter("offset", offset.toString()) - when (sortOrder) { + when (filter.sortOrder) { SortOrder.POPULARITY -> { builder.addPathSegment("mostviews") builder.addQueryParameter("sort", "manga") @@ -77,8 +77,18 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context, builder.addPathSegment("newest") } } - return builder.build() + builder.build() + } + + filter == null -> { + val builder = urlBuilder().addQueryParameter("offset", offset.toString()) + builder.addPathSegment("manga") + builder.addPathSegment("newest") + builder.build() + } + + else -> { + super.buildUrl(offset, filter) } - return super.buildUrl(offset, query, tags, sortOrder) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt index b6c3c5b4..4c0b7354 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/WpComicsParser.kt @@ -178,9 +178,11 @@ internal abstract class WpComicsParser( val tagItems = doc.select("div.genre-item") val result = ArrayMap(tagItems.size) for (item in tagItems) { - val title = item.text().trim() + val title = item.text() val key = item.select("span[data-id]").attr("data-id") - result[title] = MangaTag(title = title, key = key, source = source) + if (key.isNotEmpty() && title.isNotEmpty()) { + result[title] = MangaTag(title = title, key = key, source = source) + } } tagCache = result result @@ -216,7 +218,7 @@ internal abstract class WpComicsParser( protected open val selectDate = "div.col-xs-4" - protected open val selectChapter = "div#nt_listchapter li:not(.heading)" + protected open val selectChapter = "div#nt_listchapter li .chapter" protected open suspend fun getChapters(doc: Document): List { return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li -> diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt index 047b3cbf..ac6080fa 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/en/XoxoComics.kt @@ -1,13 +1,15 @@ package org.koitharu.kotatsu.parsers.site.wpcomics.en import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser import org.koitharu.kotatsu.parsers.util.* -import java.util.EnumSet +import java.util.* @MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS) internal class XoxoComics(context: MangaLoaderContext) : @@ -146,6 +148,29 @@ internal class XoxoComics(context: MangaLoaderContext) : ) } + override suspend fun getChapters(doc: Document): List { + val pages = doc.select("ul.pagination > li:not(.active)") + return if (pages.size <= 1) { + super.getChapters(doc) + } else { + val list = coroutineScope { + pages.mapNotNull { page -> + val a = page.selectFirst("a") ?: return@mapNotNull null + if (a.text().isNumeric()) { + val href = a.attrAsAbsoluteUrl("href") + async { + super.getChapters(webClient.httpGet(href).parseHtml()).asReversed() + } + } else { + null // TODO support pagination with overflow + } + }.awaitAll().flattenTo(ArrayList()) + } + list.addAll(super.getChapters(doc).asReversed()) + list.reverse() + list.mapIndexed { i, x -> x.copy(number = i + 1) } + } + } override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) + "/all" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen.kt index 81691cf7..0a8d6e6c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyen.kt @@ -10,6 +10,7 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser internal class NetTruyen(context: MangaLoaderContext) : WpComicsParser(context, MangaSource.NETTRUYEN, "www.nettruyenlive.com", 36) { override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain( + "www.nettruyenaz.com", "www.nettruyenlive.com", "www.nettruyenio.com", "www.nettruyento.com", diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nhattruyenmin.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nhattruyenmin.kt index b471d526..2006b791 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nhattruyenmin.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nhattruyenmin.kt @@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser @MangaSourceParser("NHATTRUYENMIN", "NhattruyenPlus", "vi") internal class Nhattruyenmin(context: MangaLoaderContext) : - WpComicsParser(context, MangaSource.NHATTRUYENMIN, "nhattruyenplus.com") + WpComicsParser(context, MangaSource.NHATTRUYENMIN, "nhattruyenmax.com") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt index e6efece6..94baf822 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt @@ -240,3 +240,5 @@ inline fun Appendable.appendAll( append(transform(item)) } } + +fun String.isNumeric() = all { c -> c.isDigit() }