From adf6fea4acdf62d8ed752ecad567150edc2138b7 Mon Sep 17 00:00:00 2001 From: seew3l Date: Sat, 8 Jul 2023 23:07:25 -0500 Subject: [PATCH 1/9] Add TMO --- .../parsers/site/TuMangaOnlineParser.kt | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt new file mode 100644 index 00000000..821b52ed --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -0,0 +1,392 @@ +package org.koitharu.kotatsu.parsers.site + +import android.os.SystemClock +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.json.JSONObject +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.PagedMangaParser +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.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrlOrNull +import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl +import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.generateUid +import org.koitharu.kotatsu.parsers.util.host +import org.koitharu.kotatsu.parsers.util.parseFailed +import org.koitharu.kotatsu.parsers.util.parseHtml +import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow +import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.EnumSet +import java.util.Locale +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit + +/** + * This parser is just for parser development, it should not be used in releases + */ +@MangaSourceParser("TUMANGAONLINE", "TuMangaOnline", "es") +class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( + context, + source = MangaSource.TUMANGAONLINE, + pageSize = 24, +), Interceptor { + + private val client = OkHttpClient.Builder().build() + + override val headers: Headers + get() = super.headers.newBuilder() + .set("Referer","https://$domain/") + .build() + + override val configKeyDomain: ConfigKey.Domain + get() = ConfigKey.Domain("lectortmo.com") + + override val sortOrders: Set + get() = EnumSet.of( + SortOrder.NEWEST, + SortOrder.POPULARITY, + ) + + private val host = "lectortmo.com" + private val permits = 10 + private val period = 60L + private val unit = TimeUnit.SECONDS + + private val requestQueue = ArrayDeque(permits) + private val rateLimitMillis = unit.toMillis(period) + private val fairLock = Semaphore(1, true) + + // RateLimit + override fun intercept(chain: Interceptor.Chain): Response { + val call = chain.call() + if (call.isCanceled()) throw IOException("Canceled") + + val request = chain.request() + when (host) { + request.url.host -> {} + else -> return chain.proceed(request) + } + + try { + fairLock.acquire() + } catch (e: InterruptedException) { + throw IOException(e) + } + + val requestQueue = this.requestQueue + val timestamp: Long + + try { + synchronized(requestQueue) { + while (requestQueue.size >= permits) { // queue is full, remove expired entries + val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis + var hasRemovedExpired = false + while (requestQueue.isEmpty().not() && requestQueue.first() <= periodStart) { + requestQueue.removeFirst() + hasRemovedExpired = true + } + if (call.isCanceled()) { + throw IOException("Canceled") + } else if (hasRemovedExpired) { + break + } else { + try { + (requestQueue as Object).wait(requestQueue.first() - periodStart) + } catch (_: InterruptedException) { + continue + } + } + } + + timestamp = SystemClock.elapsedRealtime() + requestQueue.addLast(timestamp) + } + } finally { + fairLock.release() + } + + val response = chain.proceed(request) + if (response.networkResponse == null) { + synchronized(requestQueue) { + if (requestQueue.isEmpty() || timestamp < requestQueue.first()) return@synchronized + val iterator = requestQueue.iterator() + while (iterator.hasNext()) { + if (iterator.next() == timestamp) { + iterator.remove() + break + } + } + (requestQueue as Object).notifyAll() + } + } + + return response + } + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder + ): List { + val url = buildString { + append("/library") + if(query.isNullOrEmpty()){ + append("?order_item=") + if (sortOrder == SortOrder.POPULARITY) { + append("likes_count") + } + if (sortOrder == SortOrder.NEWEST) { + append("creation") + } + append("&order_dir=desc") + append("&filter_by=title") + if (tags != null) { + for(tag in tags){ + append("&genders[]=${tag.key}") + } + } + } else { + append("?title=$query") + } + append("&_pg=1") + append("&page=$page") + }.toAbsoluteUrl(domain) + + val doc = webClient.httpGet(url, headers).parseHtml() + val items = doc.body().select("div.element") + return items.mapNotNull { item -> + val href = item.selectFirst("a")?.attrAsRelativeUrl("href")?.substringAfter(" ") ?: return@mapNotNull null + Manga( + id = generateUid(href), + title = item.selectFirst("h4.text-truncate")?.text() ?: return@mapNotNull null, + coverUrl = item.select("style").toString().substringAfter("('").substringBeforeLast("')"), + altTitle = null, + author = null, + rating = item.selectFirst("span.score") + ?.text() + ?.toFloatOrNull() + ?.div(10F) ?: RATING_UNKNOWN, + url = href, + isNsfw = item.select("i").hasClass("fas fa-heartbeat fa-2x"), + tags = emptySet(), + state = null, + publicUrl = href.toAbsoluteUrl(doc.host ?: domain), + source = source, + ) + } + + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val contents = doc.body().selectFirstOrThrow("section.element-header-content") + return manga.copy( + description = contents.selectFirst("p.element-description")?.html(), + largeCoverUrl = contents.selectFirst(".book-thumbnail")?.attrAsAbsoluteUrlOrNull("src"), + state = parseStatus(contents.select("span.book-status").text().orEmpty()), + author = contents.selectFirst("h5.card-title")?.attr("title")?.substringAfter(", "), + chapters = if(doc.select("div.chapters").isEmpty()){ + doc.select(oneShotChapterListSelector()).reversed().map { oneShotChapterFromElement(it) } + } else { + val chapters = mutableListOf() + doc.select(regularChapterListSelector()).reversed().forEachIndexed{ i, chapelement -> + val chaptername = chapelement.select("div.col-10.text-truncate").text().replace(" ", " ").trim() + val scanelement = chapelement.select("ul.chapter-list > li") + scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) } + } + chapters + } + ) + } + + private fun oneShotChapterListSelector() = "div.chapter-list-element > ul.list-group li.list-group-item" + private fun oneShotChapterFromElement(element: Element): MangaChapter { + val href = element.selectFirst("div.row > .text-right > a") + ?.attrAsRelativeUrl("href") ?: element.parseFailed() + return MangaChapter( + id = generateUid(href), + name = "One Shot", + number = 1, + url = href, + scanlator = element.select("div.col-md-6.text-truncate").text(), + branch = null, + uploadDate = element.select("span.badge.badge-primary.p-2").first()?.text() + ?.let { parseChapterDate(it) } + ?: 0, + source = source, + ) + } + + private fun regularChapterListSelector() = "div.chapters > ul.list-group li.p-0.list-group-item" + private fun regularChapterFromElement(element: Element, chName: String, number: Int): MangaChapter { + val href = element.selectFirst("div.row > .text-right > a") + ?.attrAsRelativeUrl("href") ?: element.parseFailed() + return MangaChapter( + id = generateUid(href), + name = chName, + number = number + 1, + url = href, + scanlator = element.select("div.col-md-6.text-truncate").text(), + branch = null, + uploadDate = element.select("span.badge.badge-primary.p-2").first()?.text() + ?.let { parseChapterDate(it) } + ?: 0, + source = source, + ) + } + + + override suspend fun getPages(chapter: MangaChapter): List { + val redirectDoc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + var doc = redirectToReadingPage(redirectDoc) + val currentUrl = doc.location() + val newUrl = if (!currentUrl.contains("cascade")) { + currentUrl.substringBefore("paginated") + "cascade" + } else { + currentUrl + } + + if (currentUrl != newUrl) { + doc = webClient.httpGet(newUrl, headers).parseHtml() + } + + return doc.select("div.viewer-container img:not(noscript img)").map{ + val href = if (it.hasAttr("data-src")) { + it.attr("abs:data-src") + } else { + it.attr("abs:src") + } + MangaPage( + id = generateUid(href), + url = href, + preview = null, + source = source, + ) + } + } + + // Some chapters uses JavaScript to redirect to read page + private suspend fun redirectToReadingPage(document: Document): Document { + val script1 = document.selectFirst("script:containsData(uniqid)") + val script2 = document.selectFirst("script:containsData(window.location.replace)") + + val redirectHeaders = Headers.Builder() + .set("Referer", document.baseUri()) + .build() + + if (script1 != null) { + val data = script1.data() + val regexParams = """\{uniqid:'(.+)',cascade:(.+)\}""".toRegex() + val regexAction = """form\.action\s?=\s?'(.+)'""".toRegex() + val params = regexParams.find(data)!! + val action = regexAction.find(data)!!.groupValues[1] + + val body = JSONObject() + body.put("uniqid",params.groupValues[1]) + body.put("cascade",params.groupValues[2]) + + val formBody = FormBody.Builder() + .add("uniqid", params.groupValues[1]) + .add("cascade", params.groupValues[2]) + .build() + + val postRequest = Request.Builder() + .url(action) + .headers(redirectHeaders) + .post(formBody) + .build() + + return redirectToReadingPage(client.newCall(postRequest).execute().parseHtml()) + } + + if (script2 != null) { + val data = script2.data() + val regexRedirect = """window\.location\.replace\('(.+)'\)""".toRegex() + val url = regexRedirect.find(data)!!.groupValues[1] + + return redirectToReadingPage(webClient.httpGet(url, redirectHeaders).parseHtml()) + } + + return document + } + + override suspend fun getTags() = setOf( + MangaTag("Acción", "1", source), + MangaTag("Aventura", "2", source), + MangaTag("Comedia", "3", source), + MangaTag("Drama", "4", source), + MangaTag("Recuentos de la vida", "5", source), + MangaTag("Ecchi", "6", source), + MangaTag("Fantasia", "7", source), + MangaTag("Magia", "8", source), + MangaTag("Sobrenatural", "9", source), + MangaTag("Horror", "10", source), + MangaTag("Misterio", "11", source), + MangaTag("Psicológico", "12", source), + MangaTag("Romance", "13", source), + MangaTag("Ciencia Ficción", "14", source), + MangaTag("Thriller", "15", source), + MangaTag("Deporte", "16", source), + MangaTag("Girls Love", "17", source), + MangaTag("Boys Love", "18", source), + MangaTag("Harem", "19", source), + MangaTag("Mecha", "20", source), + MangaTag("Supervivencia", "21", source), + MangaTag("Reencarnación", "22", source), + MangaTag("Gore", "23", source), + MangaTag("Apocalíptico", "24", source), + MangaTag("Tragedia", "25", source), + MangaTag("Vida Escolar", "26", source), + MangaTag("Historia", "27", source), + MangaTag("Militar", "28", source), + MangaTag("Policiaco", "29", source), + MangaTag("Crimen", "30", source), + MangaTag("Superpoderes", "31", source), + MangaTag("Vampiros", "32", source), + MangaTag("Artes Marciales", "33", source), + MangaTag("Samurái", "34", source), + MangaTag("Género Bender", "35", source), + MangaTag("Realidad Virtual", "36", source), + MangaTag("Ciberpunk", "37", source), + MangaTag("Musica", "38", source), + MangaTag("Parodia", "39", source), + MangaTag("Animación", "40", source), + MangaTag("Demonios", "41", source), + MangaTag("Familia", "42", source), + MangaTag("Extranjero", "43", source), + MangaTag("Niños", "44", source), + MangaTag("Realidad", "45", source), + MangaTag("Telenovela", "46", source), + MangaTag("Guerra", "47", source), + MangaTag("Oeste", "48", source), + ) + + + private fun parseStatus(status: String) = when { + status.contains("Publicándose") -> MangaState.ONGOING + status.contains("Finalizado") -> MangaState.FINISHED + else -> null + } + + private fun parseChapterDate(date: String): Long = + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date)?.time ?: 0 + +} From a3fa04bcb7d4541cb864c4eace25029a34a45451 Mon Sep 17 00:00:00 2001 From: seew3l Date: Sat, 8 Jul 2023 23:27:48 -0500 Subject: [PATCH 2/9] Remove comments --- .../org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index 821b52ed..9432daee 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -73,7 +73,6 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( private val rateLimitMillis = unit.toMillis(period) private val fairLock = Semaphore(1, true) - // RateLimit override fun intercept(chain: Interceptor.Chain): Response { val call = chain.call() if (call.isCanceled()) throw IOException("Canceled") @@ -95,7 +94,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( try { synchronized(requestQueue) { - while (requestQueue.size >= permits) { // queue is full, remove expired entries + while (requestQueue.size >= permits) { val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis var hasRemovedExpired = false while (requestQueue.isEmpty().not() && requestQueue.first() <= periodStart) { @@ -283,7 +282,6 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( } } - // Some chapters uses JavaScript to redirect to read page private suspend fun redirectToReadingPage(document: Document): Document { val script1 = document.selectFirst("script:containsData(uniqid)") val script2 = document.selectFirst("script:containsData(window.location.replace)") From af8fb952df5d699362e6fc3352dfe9da995666ee Mon Sep 17 00:00:00 2001 From: seew3l Date: Sat, 8 Jul 2023 23:28:31 -0500 Subject: [PATCH 3/9] Remove comments --- .../org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index 9432daee..a6affbfd 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -38,9 +38,6 @@ import java.util.Locale import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit -/** - * This parser is just for parser development, it should not be used in releases - */ @MangaSourceParser("TUMANGAONLINE", "TuMangaOnline", "es") class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( context, From e462b7df9733128cfe150ef2512d2e247764d2ca Mon Sep 17 00:00:00 2001 From: seew3l Date: Sun, 9 Jul 2023 10:07:25 -0500 Subject: [PATCH 4/9] Remove unused val --- .../org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index a6affbfd..291ac2e0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -7,7 +7,6 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import org.json.JSONObject import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.koitharu.kotatsu.parsers.MangaLoaderContext @@ -294,10 +293,6 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( val params = regexParams.find(data)!! val action = regexAction.find(data)!!.groupValues[1] - val body = JSONObject() - body.put("uniqid",params.groupValues[1]) - body.put("cascade",params.groupValues[2]) - val formBody = FormBody.Builder() .add("uniqid", params.groupValues[1]) .add("cascade", params.groupValues[2]) From 22d59e46c8ef1652e7ebab2c863e5b9f8f33e25e Mon Sep 17 00:00:00 2001 From: seew3l Date: Sun, 9 Jul 2023 10:18:20 -0500 Subject: [PATCH 5/9] Get domain from ConfigKey --- .../org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index 291ac2e0..c37d811b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -60,7 +60,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( SortOrder.POPULARITY, ) - private val host = "lectortmo.com" + private val host = domain private val permits = 10 private val period = 60L private val unit = TimeUnit.SECONDS From 23a7b945af4e3d6b010424c4fcc7e0ac13034d8b Mon Sep 17 00:00:00 2001 From: seew3l Date: Sun, 9 Jul 2023 13:34:15 -0500 Subject: [PATCH 6/9] Add headers to getPages request --- .../org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index c37d811b..e30df1af 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -250,7 +250,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( override suspend fun getPages(chapter: MangaChapter): List { - val redirectDoc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + val redirectDoc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain), headers).parseHtml() var doc = redirectToReadingPage(redirectDoc) val currentUrl = doc.location() val newUrl = if (!currentUrl.contains("cascade")) { From 46535f82afecd6d4914165e0c8121560675a1f36 Mon Sep 17 00:00:00 2001 From: seew3l Date: Mon, 10 Jul 2023 09:35:26 -0500 Subject: [PATCH 7/9] Apply requested changes --- .../parsers/site/TuMangaOnlineParser.kt | 141 ++++++------------ 1 file changed, 49 insertions(+), 92 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index e30df1af..35642562 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -4,11 +4,11 @@ import android.os.SystemClock import okhttp3.FormBody import okhttp3.Headers import okhttp3.Interceptor -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser @@ -21,19 +21,23 @@ import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.network.WebClient import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrlOrNull import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl +import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrlOrNull +import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.generateUid import org.koitharu.kotatsu.parsers.util.host -import org.koitharu.kotatsu.parsers.util.parseFailed +import org.koitharu.kotatsu.parsers.util.mapChapters +import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl +import org.koitharu.kotatsu.parsers.util.tryParse import java.io.IOException import java.text.SimpleDateFormat import java.util.EnumSet -import java.util.Locale import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit @@ -44,27 +48,17 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( pageSize = 24, ), Interceptor { - private val client = OkHttpClient.Builder().build() - - override val headers: Headers - get() = super.headers.newBuilder() - .set("Referer","https://$domain/") - .build() - override val configKeyDomain: ConfigKey.Domain get() = ConfigKey.Domain("lectortmo.com") + private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale) + override val sortOrders: Set get() = EnumSet.of( SortOrder.NEWEST, SortOrder.POPULARITY, ) - private val host = domain - private val permits = 10 - private val period = 60L - private val unit = TimeUnit.SECONDS - private val requestQueue = ArrayDeque(permits) private val rateLimitMillis = unit.toMillis(period) private val fairLock = Semaphore(1, true) @@ -74,7 +68,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( if (call.isCanceled()) throw IOException("Canceled") val request = chain.request() - when (host) { + when (domain) { request.url.host -> {} else -> return chain.proceed(request) } @@ -168,7 +162,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( val doc = webClient.httpGet(url, headers).parseHtml() val items = doc.body().select("div.element") return items.mapNotNull { item -> - val href = item.selectFirst("a")?.attrAsRelativeUrl("href")?.substringAfter(" ") ?: return@mapNotNull null + val href = item.selectFirst("a")?.attrAsRelativeUrlOrNull("href")?.substringAfter(' ') ?: return@mapNotNull null Manga( id = generateUid(href), title = item.selectFirst("h4.text-truncate")?.text() ?: return@mapNotNull null, @@ -199,13 +193,15 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( state = parseStatus(contents.select("span.book-status").text().orEmpty()), author = contents.selectFirst("h5.card-title")?.attr("title")?.substringAfter(", "), chapters = if(doc.select("div.chapters").isEmpty()){ - doc.select(oneShotChapterListSelector()).reversed().map { oneShotChapterFromElement(it) } + doc.select(oneShotChapterListSelector()).mapChapters(reversed = true) { i, item -> + oneShotChapterFromElement(item) + } } else { val chapters = mutableListOf() - doc.select(regularChapterListSelector()).reversed().forEachIndexed{ i, chapelement -> - val chaptername = chapelement.select("div.col-10.text-truncate").text().replace(" ", " ").trim() - val scanelement = chapelement.select("ul.chapter-list > li") - scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) } + doc.select(regularChapterListSelector()).reversed().forEachIndexed { i, item -> + val chaptername = item.select("div.col-10.text-truncate").text().replace(" ", " ").trim() + val scanelement = item.select("ul.chapter-list > li") + scanelement.forEach { chapters.add(regularChapterFromElement(item, chaptername, i)) } } chapters } @@ -214,8 +210,8 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( private fun oneShotChapterListSelector() = "div.chapter-list-element > ul.list-group li.list-group-item" private fun oneShotChapterFromElement(element: Element): MangaChapter { - val href = element.selectFirst("div.row > .text-right > a") - ?.attrAsRelativeUrl("href") ?: element.parseFailed() + val href = element.selectFirstOrThrow("div.row > .text-right > a") + .attrAsRelativeUrl("href") return MangaChapter( id = generateUid(href), name = "One Shot", @@ -223,17 +219,15 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( url = href, scanlator = element.select("div.col-md-6.text-truncate").text(), branch = null, - uploadDate = element.select("span.badge.badge-primary.p-2").first()?.text() - ?.let { parseChapterDate(it) } - ?: 0, + uploadDate = chapterDateFormat.tryParse(element.select("span.badge.badge-primary.p-2").first()?.text()), source = source, ) } private fun regularChapterListSelector() = "div.chapters > ul.list-group li.p-0.list-group-item" private fun regularChapterFromElement(element: Element, chName: String, number: Int): MangaChapter { - val href = element.selectFirst("div.row > .text-right > a") - ?.attrAsRelativeUrl("href") ?: element.parseFailed() + val href = element.selectFirstOrThrow("div.row > .text-right > a") + .attrAsRelativeUrl("href") return MangaChapter( id = generateUid(href), name = chName, @@ -241,9 +235,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( url = href, scanlator = element.select("div.col-md-6.text-truncate").text(), branch = null, - uploadDate = element.select("span.badge.badge-primary.p-2").first()?.text() - ?.let { parseChapterDate(it) } - ?: 0, + uploadDate = chapterDateFormat.tryParse(element.select("span.badge.badge-primary.p-2").first()?.text()), source = source, ) } @@ -298,13 +290,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( .add("cascade", params.groupValues[2]) .build() - val postRequest = Request.Builder() - .url(action) - .headers(redirectHeaders) - .post(formBody) - .build() - - return redirectToReadingPage(client.newCall(postRequest).execute().parseHtml()) + return redirectToReadingPage(webClient.httpPost(action,redirectHeaders,formBody).parseHtml()) } if (script2 != null) { @@ -318,57 +304,17 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( return document } - override suspend fun getTags() = setOf( - MangaTag("Acción", "1", source), - MangaTag("Aventura", "2", source), - MangaTag("Comedia", "3", source), - MangaTag("Drama", "4", source), - MangaTag("Recuentos de la vida", "5", source), - MangaTag("Ecchi", "6", source), - MangaTag("Fantasia", "7", source), - MangaTag("Magia", "8", source), - MangaTag("Sobrenatural", "9", source), - MangaTag("Horror", "10", source), - MangaTag("Misterio", "11", source), - MangaTag("Psicológico", "12", source), - MangaTag("Romance", "13", source), - MangaTag("Ciencia Ficción", "14", source), - MangaTag("Thriller", "15", source), - MangaTag("Deporte", "16", source), - MangaTag("Girls Love", "17", source), - MangaTag("Boys Love", "18", source), - MangaTag("Harem", "19", source), - MangaTag("Mecha", "20", source), - MangaTag("Supervivencia", "21", source), - MangaTag("Reencarnación", "22", source), - MangaTag("Gore", "23", source), - MangaTag("Apocalíptico", "24", source), - MangaTag("Tragedia", "25", source), - MangaTag("Vida Escolar", "26", source), - MangaTag("Historia", "27", source), - MangaTag("Militar", "28", source), - MangaTag("Policiaco", "29", source), - MangaTag("Crimen", "30", source), - MangaTag("Superpoderes", "31", source), - MangaTag("Vampiros", "32", source), - MangaTag("Artes Marciales", "33", source), - MangaTag("Samurái", "34", source), - MangaTag("Género Bender", "35", source), - MangaTag("Realidad Virtual", "36", source), - MangaTag("Ciberpunk", "37", source), - MangaTag("Musica", "38", source), - MangaTag("Parodia", "39", source), - MangaTag("Animación", "40", source), - MangaTag("Demonios", "41", source), - MangaTag("Familia", "42", source), - MangaTag("Extranjero", "43", source), - MangaTag("Niños", "44", source), - MangaTag("Realidad", "45", source), - MangaTag("Telenovela", "46", source), - MangaTag("Guerra", "47", source), - MangaTag("Oeste", "48", source), - ) - + override suspend fun getTags(): Set { + val doc = webClient.httpGet("https://$domain/library", headers).parseHtml() + val elements = doc.body().select("div#books-genders > div > div") + return elements.mapNotNullToSet { element -> + MangaTag( + title = element.select("label").text(), + key = element.select("input").attr("value"), + source = source + ) + } + } private fun parseStatus(status: String) = when { status.contains("Publicándose") -> MangaState.ONGOING @@ -376,7 +322,18 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( else -> null } - private fun parseChapterDate(date: String): Long = - SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date)?.time ?: 0 + private suspend fun WebClient.httpPost(url: String, headers: Headers, body: FormBody): Response { + val client = context.httpClient + val request = Request.Builder() + .post(body) + .headers(headers) + .url(url) + return client.newCall(request.build()).await().ensureSuccess() + } + companion object { + private const val permits = 10 + private const val period = 60L + private val unit = TimeUnit.SECONDS + } } From cc26897fb944e2ed80254e9bb4972554742e0044 Mon Sep 17 00:00:00 2001 From: seew3l Date: Mon, 10 Jul 2023 09:37:35 -0500 Subject: [PATCH 8/9] Opps --- .../org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index 35642562..4445841f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -201,7 +201,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( doc.select(regularChapterListSelector()).reversed().forEachIndexed { i, item -> val chaptername = item.select("div.col-10.text-truncate").text().replace(" ", " ").trim() val scanelement = item.select("ul.chapter-list > li") - scanelement.forEach { chapters.add(regularChapterFromElement(item, chaptername, i)) } + scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) } } chapters } From 3874fe31d5069e3ce85dc93823c51fe4a18fc9b6 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 11 Jul 2023 10:58:51 +0300 Subject: [PATCH 9/9] [TuMangaOnline] Fixes --- .../parsers/network/RateLimitInterceptor.kt | 79 ++++++++ .../parsers/site/TuMangaOnlineParser.kt | 186 ++++-------------- .../koitharu/kotatsu/parsers/MangaSources.kt | 2 +- 3 files changed, 113 insertions(+), 154 deletions(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitInterceptor.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitInterceptor.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitInterceptor.kt new file mode 100644 index 00000000..dfc126fb --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/network/RateLimitInterceptor.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.parsers.network + +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.internal.notifyAll +import okio.IOException +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit + +// TODO rewrite this +class RateLimitInterceptor : Interceptor { + + private val requestQueue = ArrayDeque(10) + private val rateLimitMillis = TimeUnit.SECONDS.toMillis(60L) + private val fairLock = Semaphore(1, true) + + override fun intercept(chain: Interceptor.Chain): Response { + val call = chain.call() + val request = chain.request() + + try { + fairLock.acquire() + } catch (e: InterruptedException) { + throw IOException(e) + } + + val requestQueue = this.requestQueue + val timestamp: Long + + try { + synchronized(requestQueue) { + while (requestQueue.size >= 10) { + val periodStart = System.currentTimeMillis() - rateLimitMillis + var hasRemovedExpired = false + while (requestQueue.isEmpty().not() && requestQueue.first() <= periodStart) { + requestQueue.removeFirst() + hasRemovedExpired = true + } + if (call.isCanceled()) { + throw IOException("Canceled") + } else if (hasRemovedExpired) { + break + } else { + try { + requestQueue.wait(requestQueue.first() - periodStart) + } catch (_: InterruptedException) { + continue + } + } + } + + timestamp = System.currentTimeMillis() + requestQueue.addLast(timestamp) + } + } finally { + fairLock.release() + } + + val response = chain.proceed(request) + if (response.networkResponse == null) { + synchronized(requestQueue) { + if (requestQueue.isEmpty() || timestamp < requestQueue.first()) return@synchronized + val iterator = requestQueue.iterator() + while (iterator.hasNext()) { + if (iterator.next() == timestamp) { + iterator.remove() + break + } + } + requestQueue.notifyAll() + } + } + + return response + } + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE") + private inline fun Any.wait(timeout: Long) = (this as Object).wait(timeout) +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt index 4445841f..d8fe1a9a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/TuMangaOnlineParser.kt @@ -1,143 +1,43 @@ package org.koitharu.kotatsu.parsers.site -import android.os.SystemClock -import okhttp3.FormBody import okhttp3.Headers -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response +import okhttp3.HttpUrl.Companion.toHttpUrl import org.jsoup.nodes.Document import org.jsoup.nodes.Element -import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.PagedMangaParser 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.MangaPage -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaState -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN -import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.network.WebClient -import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrlOrNull -import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl -import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrlOrNull -import org.koitharu.kotatsu.parsers.util.await -import org.koitharu.kotatsu.parsers.util.domain -import org.koitharu.kotatsu.parsers.util.generateUid -import org.koitharu.kotatsu.parsers.util.host -import org.koitharu.kotatsu.parsers.util.mapChapters -import org.koitharu.kotatsu.parsers.util.mapNotNullToSet -import org.koitharu.kotatsu.parsers.util.parseHtml -import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow -import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl -import org.koitharu.kotatsu.parsers.util.tryParse -import java.io.IOException +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.util.* import java.text.SimpleDateFormat -import java.util.EnumSet -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit +import java.util.* @MangaSourceParser("TUMANGAONLINE", "TuMangaOnline", "es") -class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( +class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser( context, source = MangaSource.TUMANGAONLINE, pageSize = 24, -), Interceptor { +) { - override val configKeyDomain: ConfigKey.Domain - get() = ConfigKey.Domain("lectortmo.com") + override val configKeyDomain = ConfigKey.Domain("lectortmo.com") private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale) - override val sortOrders: Set - get() = EnumSet.of( - SortOrder.NEWEST, - SortOrder.POPULARITY, - ) - - private val requestQueue = ArrayDeque(permits) - private val rateLimitMillis = unit.toMillis(period) - private val fairLock = Semaphore(1, true) - - override fun intercept(chain: Interceptor.Chain): Response { - val call = chain.call() - if (call.isCanceled()) throw IOException("Canceled") - - val request = chain.request() - when (domain) { - request.url.host -> {} - else -> return chain.proceed(request) - } - - try { - fairLock.acquire() - } catch (e: InterruptedException) { - throw IOException(e) - } - - val requestQueue = this.requestQueue - val timestamp: Long - - try { - synchronized(requestQueue) { - while (requestQueue.size >= permits) { - val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis - var hasRemovedExpired = false - while (requestQueue.isEmpty().not() && requestQueue.first() <= periodStart) { - requestQueue.removeFirst() - hasRemovedExpired = true - } - if (call.isCanceled()) { - throw IOException("Canceled") - } else if (hasRemovedExpired) { - break - } else { - try { - (requestQueue as Object).wait(requestQueue.first() - periodStart) - } catch (_: InterruptedException) { - continue - } - } - } - - timestamp = SystemClock.elapsedRealtime() - requestQueue.addLast(timestamp) - } - } finally { - fairLock.release() - } - - val response = chain.proceed(request) - if (response.networkResponse == null) { - synchronized(requestQueue) { - if (requestQueue.isEmpty() || timestamp < requestQueue.first()) return@synchronized - val iterator = requestQueue.iterator() - while (iterator.hasNext()) { - if (iterator.next() == timestamp) { - iterator.remove() - break - } - } - (requestQueue as Object).notifyAll() - } - } - - return response - } + override val sortOrders = EnumSet.of( + SortOrder.NEWEST, + SortOrder.POPULARITY, + ) override suspend fun getListPage( page: Int, query: String?, tags: Set?, - sortOrder: SortOrder + sortOrder: SortOrder, ): List { val url = buildString { append("/library") - if(query.isNullOrEmpty()){ + if (query.isNullOrEmpty()) { append("?order_item=") if (sortOrder == SortOrder.POPULARITY) { append("likes_count") @@ -148,7 +48,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( append("&order_dir=desc") append("&filter_by=title") if (tags != null) { - for(tag in tags){ + for (tag in tags) { append("&genders[]=${tag.key}") } } @@ -162,17 +62,15 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( val doc = webClient.httpGet(url, headers).parseHtml() val items = doc.body().select("div.element") return items.mapNotNull { item -> - val href = item.selectFirst("a")?.attrAsRelativeUrlOrNull("href")?.substringAfter(' ') ?: return@mapNotNull null + val href = + item.selectFirst("a")?.attrAsRelativeUrlOrNull("href")?.substringAfter(' ') ?: return@mapNotNull null Manga( id = generateUid(href), title = item.selectFirst("h4.text-truncate")?.text() ?: return@mapNotNull null, coverUrl = item.select("style").toString().substringAfter("('").substringBeforeLast("')"), altTitle = null, author = null, - rating = item.selectFirst("span.score") - ?.text() - ?.toFloatOrNull() - ?.div(10F) ?: RATING_UNKNOWN, + rating = item.selectFirst("span.score")?.text()?.toFloatOrNull()?.div(10F) ?: RATING_UNKNOWN, url = href, isNsfw = item.select("i").hasClass("fas fa-heartbeat fa-2x"), tags = emptySet(), @@ -192,7 +90,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( largeCoverUrl = contents.selectFirst(".book-thumbnail")?.attrAsAbsoluteUrlOrNull("src"), state = parseStatus(contents.select("span.book-status").text().orEmpty()), author = contents.selectFirst("h5.card-title")?.attr("title")?.substringAfter(", "), - chapters = if(doc.select("div.chapters").isEmpty()){ + chapters = if (doc.select("div.chapters").isEmpty()) { doc.select(oneShotChapterListSelector()).mapChapters(reversed = true) { i, item -> oneShotChapterFromElement(item) } @@ -204,14 +102,14 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) } } chapters - } + }, ) } private fun oneShotChapterListSelector() = "div.chapter-list-element > ul.list-group li.list-group-item" + private fun oneShotChapterFromElement(element: Element): MangaChapter { - val href = element.selectFirstOrThrow("div.row > .text-right > a") - .attrAsRelativeUrl("href") + val href = element.selectFirstOrThrow("div.row > .text-right > a").attrAsRelativeUrl("href") return MangaChapter( id = generateUid(href), name = "One Shot", @@ -225,9 +123,9 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( } private fun regularChapterListSelector() = "div.chapters > ul.list-group li.p-0.list-group-item" + private fun regularChapterFromElement(element: Element, chName: String, number: Int): MangaChapter { - val href = element.selectFirstOrThrow("div.row > .text-right > a") - .attrAsRelativeUrl("href") + val href = element.selectFirstOrThrow("div.row > .text-right > a").attrAsRelativeUrl("href") return MangaChapter( id = generateUid(href), name = chName, @@ -255,7 +153,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( doc = webClient.httpGet(newUrl, headers).parseHtml() } - return doc.select("div.viewer-container img:not(noscript img)").map{ + return doc.select("div.viewer-container img:not(noscript img)").map { val href = if (it.hasAttr("data-src")) { it.attr("abs:data-src") } else { @@ -274,23 +172,20 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( val script1 = document.selectFirst("script:containsData(uniqid)") val script2 = document.selectFirst("script:containsData(window.location.replace)") - val redirectHeaders = Headers.Builder() - .set("Referer", document.baseUri()) - .build() + val redirectHeaders = Headers.Builder().set("Referer", document.baseUri()).build() if (script1 != null) { val data = script1.data() - val regexParams = """\{uniqid:'(.+)',cascade:(.+)\}""".toRegex() + val regexParams = """\{uniqid:'(.+)',cascade:(.+)}""".toRegex() val regexAction = """form\.action\s?=\s?'(.+)'""".toRegex() val params = regexParams.find(data)!! - val action = regexAction.find(data)!!.groupValues[1] - - val formBody = FormBody.Builder() - .add("uniqid", params.groupValues[1]) - .add("cascade", params.groupValues[2]) - .build() + val action = regexAction.find(data)!!.groupValues[1].toHttpUrl() - return redirectToReadingPage(webClient.httpPost(action,redirectHeaders,formBody).parseHtml()) + val formBody = mapOf( + "uniqid" to params.groupValues[1], + "cascade" to params.groupValues[2], + ) + return redirectToReadingPage(webClient.httpPost(action, formBody, redirectHeaders).parseHtml()) } if (script2 != null) { @@ -311,7 +206,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( MangaTag( title = element.select("label").text(), key = element.select("input").attr("value"), - source = source + source = source, ) } } @@ -321,19 +216,4 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser ( status.contains("Finalizado") -> MangaState.FINISHED else -> null } - - private suspend fun WebClient.httpPost(url: String, headers: Headers, body: FormBody): Response { - val client = context.httpClient - val request = Request.Builder() - .post(body) - .headers(headers) - .url(url) - return client.newCall(request.build()).await().ensureSuccess() - } - - companion object { - private const val permits = 10 - private const val period = 60L - private val unit = TimeUnit.SECONDS - } } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt index ef660faa..095f1682 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt @@ -3,5 +3,5 @@ package org.koitharu.kotatsu.parsers import org.junit.jupiter.params.provider.EnumSource import org.koitharu.kotatsu.parsers.model.MangaSource -@EnumSource(MangaSource::class, names = ["LOCAL", "DUMMY"], mode = EnumSource.Mode.EXCLUDE) +@EnumSource(MangaSource::class, names = ["TUMANGAONLINE"], mode = EnumSource.Mode.INCLUDE) internal annotation class MangaSources