From 0f216c6de9d2011da394d3bc6085c56224fee604 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 25 Oct 2023 12:09:41 +0300 Subject: [PATCH 1/5] [Remanga] Fix loading large amount of chapters --- .../kotatsu/parsers/site/ru/RemangaParser.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt index 6208ee82..5aeb808a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt @@ -1,6 +1,9 @@ package org.koitharu.kotatsu.parsers.site.ru import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.internal.closeQuietly import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -23,11 +26,12 @@ import java.util.* private const val PAGE_SIZE = 30 private const val STATUS_ONGOING = 1 private const val STATUS_FINISHED = 0 +private const val TOO_MANY_REQUESTS = 429 @MangaSourceParser("REMANGA", "ReManga", "ru") internal class RemangaParser( context: MangaLoaderContext, -) : PagedMangaParser(context, MangaSource.REMANGA, PAGE_SIZE), MangaParserAuthProvider { +) : PagedMangaParser(context, MangaSource.REMANGA, PAGE_SIZE), MangaParserAuthProvider, Interceptor { private val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_MOBILE) @@ -59,6 +63,16 @@ internal class RemangaParser( private val regexLastUrlPath = Regex("/[^/]+/?$") + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + if (response.code == TOO_MANY_REQUESTS) { + response.closeQuietly() + Thread.sleep(1000) + return chain.proceed(chain.request().newBuilder().build()) + } + return response + } + override suspend fun getListPage( page: Int, query: String?, @@ -266,7 +280,7 @@ internal class RemangaParser( var page = 1 while (true) { val content = webClient.httpGet( - url = "https://api.$domain/api/titles/chapters/?branch_id=$branchId&page=$page&count=100", + url = "https://api.$domain/api/titles/chapters/?branch_id=$branchId&page=$page&count=500", ).parseJson().getJSONArray("content") val len = content.length() if (len == 0) { From ec11b79b186dffb136b273e662a4c292aa23c0a3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 25 Oct 2023 12:22:10 +0300 Subject: [PATCH 2/5] [MangaLib] Fix search result covers --- .../kotatsu/parsers/site/ru/rulib/MangaLibParser.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt index 334538df..34fab199 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt @@ -301,7 +301,7 @@ internal open class MangaLibParser( return json.mapJSON { jo -> val slug = jo.getString("slug") val url = "/$slug" - val covers = jo.getJSONObject("covers") + val cover = jo.getJSONObject("covers").getString("default").toAbsoluteUrl(domain) val title = jo.getString("rus_name").ifEmpty { jo.getString("name") } Manga( id = generateUid(url), @@ -315,8 +315,8 @@ internal open class MangaLibParser( state = null, isNsfw = false, source = source, - coverUrl = covers.getString("thumbnail").toAbsoluteUrl(domain), - largeCoverUrl = covers.getString("default").toAbsoluteUrl(domain), + coverUrl = cover, + largeCoverUrl = null, ) } } From d089fad6ceb31c5f5d79c7f4d20b595c370a6c21 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 25 Oct 2023 12:48:44 +0300 Subject: [PATCH 3/5] [MangaLib] Add branching support --- .../parsers/site/ru/rulib/MangaLibParser.kt | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt index 34fab199..722f2cca 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/MangaLibParser.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.parsers.site.ru.rulib +import androidx.collection.ArrayMap import androidx.collection.ArraySet import kotlinx.coroutines.withTimeoutOrNull import okhttp3.Response @@ -17,10 +18,7 @@ import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* -import org.koitharu.kotatsu.parsers.util.json.JSONIterator -import org.koitharu.kotatsu.parsers.util.json.getStringOrNull -import org.koitharu.kotatsu.parsers.util.json.mapJSON -import org.koitharu.kotatsu.parsers.util.json.values +import org.koitharu.kotatsu.parsers.util.json.* import java.text.SimpleDateFormat import java.util.* @@ -102,10 +100,15 @@ internal open class MangaLibParser( if (line.startsWith("window.__DATA__")) { val json = JSONObject(line.substringAfter('=').substringBeforeLast(';')) val list = json.getJSONObject("chapters").getJSONArray("list") + val branches = json.getJSONObject("chapters").getJSONArray("branches").toJSONList() + .associate { x -> + x.getInt("id") to x.getJSONArray("teams").toJSONList().joinToString { it.getString("name") } + } val id = json.optJSONObject("user")?.getLong("id")?.toString() ?: "not" val total = list.length() chapters = ChaptersListBuilder(total) - for (i in 0 until total) { + val counters = ArrayMap(branches.size) + for (i in (0 until total).reversed()) { val item = list.getJSONObject(i) val chapterId = item.getLong("chapter_id") val scanlator = item.getStringOrNull("username") @@ -130,22 +133,22 @@ internal open class MangaLibParser( val volume = item.getInt("chapter_volume") val number = item.getString("chapter_number") val fullNameChapter = "Том $volume. Глава $number" + val branchId = item.getIntOrDefault("branch_id", 0) chapters.add( MangaChapter( id = generateUid(chapterId), url = url, source = source, - number = total - i, + number = counters.incrementAndGet(branchId), uploadDate = dateFormat.tryParse( item.getString("chapter_created_at").substringBefore(" "), ), scanlator = scanlator, - branch = null, + branch = branches[branchId], name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter", ), ) } - chapters.reverse() break@scripts } } @@ -343,6 +346,13 @@ internal open class MangaLibParser( return isSuccessful && mimeType?.startsWith("image/") == true && headersContentLength() >= 1024L } + private fun MutableMap.incrementAndGet(key: Int): Int { + var v = getOrDefault(key, 0) + v++ + put(key, v) + return v + } + @MangaSourceParser("MANGALIB", "MangaLib", "ru") class Impl(context: MangaLoaderContext) : MangaLibParser(context, MangaSource.MANGALIB) From 17b8fd51b7e4d2dfe1ec09fab49ad67f38970328 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 25 Oct 2023 12:53:48 +0300 Subject: [PATCH 4/5] [ComicK] Fix chapters numbers --- .../org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt index 7bca28a6..4e15f7ef 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt @@ -188,7 +188,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex chap?.let { append("Chap ").append(it) } jo.getStringOrNull("title")?.let { append(": ").append(it) } }, - number = branchedChapters[branch]?.size?.plus(1) ?: 0, + number = branchedChapters[branch]?.size?.plus(1) ?: 1, url = jo.getString("hid"), scanlator = jo.optJSONArray("group_name")?.asIterable()?.joinToString() ?.takeUnless { it.isBlank() }, From 6bf0ae92e4309983430c1405f539e7c1bf63def2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 25 Oct 2023 13:36:42 +0300 Subject: [PATCH 5/5] [*chan] Fix sort orders --- .../parsers/site/ru/multichan/ChanParser.kt | 79 ++++++++++++------- .../site/ru/multichan/HenChanParser.kt | 34 ++++++++ .../site/ru/multichan/YaoiChanParser.kt | 8 +- 3 files changed, 90 insertions(+), 31 deletions(-) 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 39fd9297..58848ec8 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,5 +1,6 @@ package org.koitharu.kotatsu.parsers.site.ru.multichan +import okhttp3.HttpUrl import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParserAuthProvider @@ -18,6 +19,7 @@ internal abstract class ChanParser( SortOrder.NEWEST, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, + SortOrder.RATING, ) override val authUrl: String @@ -33,23 +35,7 @@ internal abstract class ChanParser( sortOrder: SortOrder, ): List { val domain = domain - val url = when { - !query.isNullOrEmpty() -> { - if (offset != 0) { - return emptyList() - } - "https://$domain/?do=search&subaction=search&story=${query.urlEncoded()}" - } - - !tags.isNullOrEmpty() -> tags.joinToString( - prefix = "https://$domain/tags/", - postfix = "&n=${getSortKey2(sortOrder)}?offset=$offset", - separator = "+", - ) { tag -> tag.key } - - else -> "https://$domain/${getSortKey(sortOrder)}?offset=$offset" - } - val doc = webClient.httpGet(url).parseHtml() + val doc = webClient.httpGet(buildUrl(offset, query, tags, sortOrder)).parseHtml() val root = doc.body().selectFirst("div.main_fon")?.getElementById("content") ?: doc.parseFailed("Cannot find root") return root.select("div.content_row").mapNotNull { row -> @@ -191,21 +177,54 @@ internal abstract class ChanParser( } } - private fun getSortKey(sortOrder: SortOrder) = - when (sortOrder) { - SortOrder.ALPHABETICAL -> "catalog" - SortOrder.POPULARITY -> "mostfavorites" - SortOrder.NEWEST -> "manga/new" - else -> "mostfavorites" - } + protected open fun buildUrl( + offset: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): HttpUrl { + val builder = urlBuilder() + builder.addQueryParameter("offset", offset.toString()) + when { + !query.isNullOrEmpty() -> { + 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("need_sort_date", "false") + } - private fun getSortKey2(sortOrder: SortOrder) = - when (sortOrder) { - SortOrder.ALPHABETICAL -> "abcasc" - SortOrder.POPULARITY -> "favdesc" - SortOrder.NEWEST -> "datedesc" - else -> "favdesc" + !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 + }, + ) + } + + 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") + } + } } + return builder.build() + } private fun String.toTagName() = replace('_', ' ').toTitleCase() 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 b6a6fc72..f3644236 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 @@ -1,15 +1,19 @@ package org.koitharu.kotatsu.parsers.site.ru.multichan +import okhttp3.HttpUrl import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* +import java.util.* @MangaSourceParser("HENCHAN", "Хентай-тян", "ru", type = ContentType.HENTAI) internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context, MangaSource.HENCHAN) { override val configKeyDomain = ConfigKey.Domain( + "x.henchan.pro", + "xxx.henchan.pro", "y.hentaichan.live", "xxx.hentaichan.live", "xx.hentaichan.live", @@ -17,6 +21,12 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context, "hentaichan.pro", ) + override val sortOrders: Set = EnumSet.of( + SortOrder.NEWEST, + SortOrder.POPULARITY, + SortOrder.RATING, + ) + override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val root = doc.body().requireElementById("dle-content") @@ -46,4 +56,28 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context, ), ) } + + override fun buildUrl(offset: Int, query: String?, tags: Set?, sortOrder: SortOrder): HttpUrl { + if (query.isNullOrEmpty() && tags.isNullOrEmpty()) { + val builder = urlBuilder().addQueryParameter("offset", offset.toString()) + when (sortOrder) { + SortOrder.POPULARITY -> { + builder.addPathSegment("mostviews") + builder.addQueryParameter("sort", "manga") + } + + SortOrder.RATING -> { + builder.addPathSegment("mostfavorites") + builder.addQueryParameter("sort", "manga") + } + + else -> { // SortOrder.NEWEST + builder.addPathSegment("manga") + builder.addPathSegment("newest") + } + } + return builder.build() + } + return super.buildUrl(offset, query, tags, sortOrder) + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/YaoiChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/YaoiChanParser.kt index 020824a7..f2fd2683 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/YaoiChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/YaoiChanParser.kt @@ -6,12 +6,18 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.* @MangaSourceParser("YAOICHAN", "Яой-тян", "ru") internal class YaoiChanParser(context: MangaLoaderContext) : ChanParser(context, MangaSource.YAOICHAN) { - override val configKeyDomain = ConfigKey.Domain("yaoi-chan.me") + override val configKeyDomain = ConfigKey.Domain( + "v1.yaoi-chan.me", + "yaoi-chan.me", + ) + + override val sortOrders: Set = setOf(SortOrder.NEWEST) override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()