Update dependencies

master
Koitharu 8 months ago
parent 4a854c7a23
commit 19567f9642
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -1,5 +1,5 @@
plugins { plugins {
kotlin("jvm") version "2.0.20" kotlin("jvm") version "2.2.10"
} }
repositories { repositories {
@ -14,5 +14,5 @@ dependencies {
implementation(gradleApi()) implementation(gradleApi())
implementation("org.simpleframework:simple-xml:2.7.1") implementation("org.simpleframework:simple-xml:2.7.1")
implementation("com.soywiz.korlibs.korte:korte-jvm:4.0.10") implementation("com.soywiz.korlibs.korte:korte-jvm:4.0.10")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
} }

@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

@ -1,13 +1,13 @@
[versions] [versions]
kotlin = "2.0.20" kotlin = "2.2.10"
ksp = "2.0.20-1.0.25" ksp = "2.2.10-2.0.2"
coroutines = "1.10.2" coroutines = "1.10.2"
junit = "5.10.1" junit = "5.10.1"
okhttp = "4.12.0" okhttp = "5.1.0"
okio = "3.11.0" okio = "3.16.0"
json = "20240303" json = "20240303"
androidx-collection = "1.5.0" androidx-collection = "1.5.0"
jsoup = "1.19.1" jsoup = "1.21.2"
quickjs = "1.1.0" quickjs = "1.1.0"
[plugins] [plugins]

@ -1,7 +1,7 @@
#Wed Aug 27 01:56:37 ICT 2025 #Wed Aug 27 01:56:37 ICT 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78 distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

@ -181,7 +181,7 @@ class ParserProcessor(
} }
val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty() val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty()
sourcesWriter?.write( sourcesWriter?.write(
"\t$deprecationString$name(\"$title\", $localeString$localeComment, ContentType.$type, $isBroken),\n", "\t$deprecationString$name(\"$title\", $localeString$localeComment, $type, $isBroken),\n",
) )
} }
} }

@ -15,5 +15,4 @@ public object ErrorMessages {
public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String = public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String =
"Filtering by both genres and states is not supported by this source" "Filtering by both genres and states is not supported by this source"
public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source" public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source"
public const val RESPONSE_NULL_BODY: String = "Response has no body"
} }

@ -19,11 +19,11 @@ public object CloudFlareHelper {
if (response.code != HTTP_FORBIDDEN && response.code != HTTP_UNAVAILABLE) { if (response.code != HTTP_FORBIDDEN && response.code != HTTP_UNAVAILABLE) {
return PROTECTION_NOT_DETECTED return PROTECTION_NOT_DETECTED
} }
val content = if (response.body != null) { val content = try {
response.peekBody(Long.MAX_VALUE).use { response.peekBody(Long.MAX_VALUE).use {
Jsoup.parse(it.byteStream(), Charsets.UTF_8.name(), response.request.url.toString()) Jsoup.parse(it.byteStream(), Charsets.UTF_8.name(), response.request.url.toString())
} }
} else { } catch (_: IllegalStateException) {
return PROTECTION_NOT_DETECTED return PROTECTION_NOT_DETECTED
} }
return when { return when {

@ -7,7 +7,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okhttp3.internal.headersContentLength
import org.jsoup.internal.StringUtil import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
@ -28,6 +27,7 @@ import java.util.concurrent.TimeUnit
private const val DOMAIN_UNAUTHORIZED = "e-hentai.org" private const val DOMAIN_UNAUTHORIZED = "e-hentai.org"
private const val DOMAIN_AUTHORIZED = "exhentai.org" private const val DOMAIN_AUTHORIZED = "exhentai.org"
private val TAG_PREFIXES = arrayOf("male:", "female:", "other:") private val TAG_PREFIXES = arrayOf("male:", "female:", "other:")
private const val BANNED_RESPONSE_LENGTH = 256L
@MangaSourceParser("EXHENTAI", "ExHentai", type = ContentType.HENTAI) @MangaSourceParser("EXHENTAI", "ExHentai", type = ContentType.HENTAI)
internal class ExHentaiParser( internal class ExHentaiParser(
@ -301,8 +301,8 @@ internal class ExHentaiParser(
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request()) val response = chain.proceed(chain.request())
if (response.headersContentLength() <= 256) { if (response.headersContentLength(BANNED_RESPONSE_LENGTH) <= BANNED_RESPONSE_LENGTH) {
val text = response.peekBody(256).use { it.string() } val text = response.peekBody(BANNED_RESPONSE_LENGTH).use { it.string() }
if (text.contains("IP address has been temporarily banned", ignoreCase = true)) { if (text.contains("IP address has been temporarily banned", ignoreCase = true)) {
val hours = Regex("([0-9]+) hours?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0 val hours = Regex("([0-9]+) hours?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0
val minutes = Regex("([0-9]+) minutes?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0 val minutes = Regex("([0-9]+) minutes?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0

@ -19,6 +19,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
import org.koitharu.kotatsu.parsers.util.attrOrNull
import org.koitharu.kotatsu.parsers.util.generateUid import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.mapChapters import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
@ -28,6 +29,7 @@ import org.koitharu.kotatsu.parsers.util.parseFailed
import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.src import org.koitharu.kotatsu.parsers.util.src
import org.koitharu.kotatsu.parsers.util.textOrNull
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.parsers.util.urlEncoded import org.koitharu.kotatsu.parsers.util.urlEncoded
@ -255,8 +257,8 @@ internal class KdtScans(context: MangaLoaderContext) :
private suspend fun fetchAvailableTags(): Set<MangaTag> { private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/manga/").parseHtml() val doc = webClient.httpGet("https://$domain/manga/").parseHtml()
return doc.select("ul.genrez li").mapNotNullToSet { li -> return doc.select("ul.genrez li").mapNotNullToSet { li ->
val key = li.selectFirst("input").attr("value") ?: return@mapNotNullToSet null val key = li.selectFirst("input")?.attrOrNull("value") ?: return@mapNotNullToSet null
val title = li.selectFirst("label").text().toTitleCase() val title = li.selectFirst("label")?.textOrNull()?.toTitleCase() ?: return@mapNotNullToSet null
MangaTag( MangaTag(
key = key, key = key,
title = title, title = title,

@ -1,17 +1,18 @@
package org.koitharu.kotatsu.parsers.site.es package org.koitharu.kotatsu.parsers.site.es
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.Broken
import java.util.* import java.util.*
@Broken // Website closed @Broken // Website closed
@MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es") @MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es")
internal class DragonTranslationParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.DRAGONTRANSLATION, 30) { internal class DragonTranslationParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.DRAGONTRANSLATION, 30) {
override val configKeyDomain = ConfigKey.Domain("dragontranslation.net") override val configKeyDomain = ConfigKey.Domain("dragontranslation.net")
@ -60,12 +61,14 @@ internal class DragonTranslationParser(context: MangaLoaderContext) : PagedManga
if (filter.types.isNotEmpty()) { if (filter.types.isNotEmpty()) {
append("&type=") append("&type=")
append(
when (filter.types.oneOrThrowIfMany()) { when (filter.types.oneOrThrowIfMany()) {
ContentType.MANGA -> "manga" ContentType.MANGA -> "manga"
ContentType.MANHWA -> "manhwa" ContentType.MANHWA -> "manhwa"
ContentType.MANHUA -> "manhua" ContentType.MANHUA -> "manhua"
else -> "" else -> ""
} },
)
} }
} }
} }
@ -81,7 +84,7 @@ internal class DragonTranslationParser(context: MangaLoaderContext) : PagedManga
url = href, url = href,
publicUrl = href, publicUrl = href,
coverUrl = coverUrl, coverUrl = coverUrl,
title = div.selectFirst("h2.card-title.fs-6.entry-title").text(), title = div.selectFirst("h2.card-title.fs-6.entry-title")?.text().orEmpty(),
altTitles = emptySet(), altTitles = emptySet(),
rating = RATING_UNKNOWN, rating = RATING_UNKNOWN,
tags = emptySet(), tags = emptySet(),
@ -150,24 +153,31 @@ internal class DragonTranslationParser(context: MangaLoaderContext) : PagedManga
dateText.contains("minutos") -> { dateText.contains("minutos") -> {
now - (number * 60 * 1000L) now - (number * 60 * 1000L)
} }
dateText.contains("horas") -> { dateText.contains("horas") -> {
now - (number * 60 * 60 * 1000L) now - (number * 60 * 60 * 1000L)
} }
dateText.contains("días") -> { dateText.contains("días") -> {
now - (number * 24 * 60 * 60 * 1000L) now - (number * 24 * 60 * 60 * 1000L)
} }
dateText.contains("día") -> { dateText.contains("día") -> {
now - (number * 24 * 60 * 60 * 1000L) now - (number * 24 * 60 * 60 * 1000L)
} }
dateText.contains("semanas") -> { dateText.contains("semanas") -> {
now - (number * 7 * 24 * 60 * 60 * 1000L) now - (number * 7 * 24 * 60 * 60 * 1000L)
} }
dateText.contains("meses") -> { dateText.contains("meses") -> {
now - (number * 30 * 24 * 60 * 60 * 1000L) now - (number * 30 * 24 * 60 * 60 * 1000L)
} }
dateText.contains("años") -> { dateText.contains("años") -> {
now - (number * 365 * 24 * 60 * 60 * 1000L) now - (number * 365 * 24 * 60 * 60 * 1000L)
} }
else -> 0L else -> 0L
} }
} }

@ -136,7 +136,7 @@ internal class LuratoonScansParser(context: MangaLoaderContext) :
val response = chain.proceed(chain.request()) val response = chain.proceed(chain.request())
if (response.mimeType == "application/octet-stream") { if (response.mimeType == "application/octet-stream") {
val (bytes, name) = response.use { resp -> val (bytes, name) = response.use { resp ->
ZipInputStream(resp.requireBody().byteStream()).use { ZipInputStream(resp.body.byteStream()).use {
val entry = it.nextEntry val entry = it.nextEntry
it.readBytes() to entry?.name it.readBytes() to entry?.name
} }

@ -12,7 +12,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okhttp3.internal.headersContentLength
import okio.IOException import okio.IOException
import org.json.JSONArray import org.json.JSONArray
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -102,9 +101,10 @@ internal abstract class GroupleParser(
} else { } else {
advancedSearch(offset, order, filter).parseHtml() advancedSearch(offset, order, filter).parseHtml()
} }
checkNotNull(root) { "Root not found" }
val tiles = root.selectFirst("div.tiles.row") val tiles = root.selectFirst("div.tiles.row")
if (tiles == null) { if (tiles == null) {
if (!root.getElementsContainingOwnText(NOTHING_FOUND).isNullOrEmpty()) { if (root.getElementsContainingOwnText(NOTHING_FOUND).isNotEmpty()) {
return emptyList() return emptyList()
} }
root.parseFailed("No tiles found") root.parseFailed("No tiles found")
@ -180,7 +180,7 @@ internal abstract class GroupleParser(
if (translations.isNullOrEmpty() || a.attr("data-translations").isEmpty()) { if (translations.isNullOrEmpty() || a.attr("data-translations").isEmpty()) {
var translators = "" var translators = ""
val translatorElement = a.attr("title") val translatorElement = a.attr("title")
if (!translatorElement.isNullOrBlank()) { if (translatorElement.isNotBlank()) {
translators = translatorElement.replace("(Переводчик),", "&").removeSuffix(" (Переводчик)") translators = translatorElement.replace("(Переводчик),", "&").removeSuffix(" (Переводчик)")
} }
listOf( listOf(

@ -4,6 +4,7 @@ package org.koitharu.kotatsu.parsers.util
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.*
import okhttp3.internal.toLongOrDefault
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@ -42,29 +43,13 @@ public inline fun Response.map(mapper: (ResponseBody) -> ResponseBody): Response
contract { contract {
callsInPlace(mapper, InvocationKind.AT_MOST_ONCE) callsInPlace(mapper, InvocationKind.AT_MOST_ONCE)
} }
return body?.use { responseBody -> return body.use { responseBody ->
newBuilder() newBuilder()
.body(mapper(responseBody)) .body(mapper(responseBody))
.build() .build()
} ?: this
}
public fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
c.name(name)
c.value(value)
if (persistent) {
c.expiresAt(expiresAt)
}
if (hostOnly) {
c.hostOnlyDomain(domain)
} else {
c.domain(domain)
}
c.path(path)
if (secure) {
c.secure()
}
if (httpOnly) {
c.httpOnly()
} }
} }
public fun Response.headersContentLength(
defaultValue: Long = -1,
): Long = headers["Content-Length"]?.toLongOrDefault(defaultValue) ?: defaultValue

@ -8,7 +8,6 @@ import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.InternalParsersApi import org.koitharu.kotatsu.parsers.InternalParsersApi
import java.text.DateFormat import java.text.DateFormat
@ -22,7 +21,7 @@ internal const val SCHEME_HTTPS = "https"
*/ */
// TODO suspend // TODO suspend
public fun Response.parseHtml(): Document = use { response -> public fun Response.parseHtml(): Document = use { response ->
val body = response.requireBody() val body = response.body
val charset = body.contentType()?.charset()?.name() val charset = body.contentType()?.charset()?.name()
Jsoup.parse(body.byteStream(), charset, response.request.url.toString()) Jsoup.parse(body.byteStream(), charset, response.request.url.toString())
} }
@ -33,7 +32,7 @@ public fun Response.parseHtml(): Document = use { response ->
* @see [parseHtml] * @see [parseHtml]
*/ */
public fun Response.parseJson(): JSONObject = use { response -> public fun Response.parseJson(): JSONObject = use { response ->
JSONObject(response.requireBody().string()) JSONObject(response.body.string())
} }
/** /**
@ -42,15 +41,15 @@ public fun Response.parseJson(): JSONObject = use { response ->
* @see [parseHtml] * @see [parseHtml]
*/ */
public fun Response.parseJsonArray(): JSONArray = use { response -> public fun Response.parseJsonArray(): JSONArray = use { response ->
JSONArray(response.requireBody().string()) JSONArray(response.body.string())
} }
public fun Response.parseRaw(): String = use { response -> public fun Response.parseRaw(): String = use { response ->
response.requireBody().string() response.body.string()
} }
public fun Response.parseBytes(): ByteArray = use { response -> public fun Response.parseBytes(): ByteArray = use { response ->
response.requireBody().bytes() response.body.bytes()
} }
/** /**
@ -99,6 +98,5 @@ public fun DateFormat.parseSafe(str: String?): Long = if (str.isNullOrEmpty()) {
}.getOrDefault(0L) }.getOrDefault(0L)
} }
public fun Response.requireBody(): ResponseBody = requireNotNull(body) { @Deprecated("Useless since OkHttp 5.0", replaceWith = ReplaceWith("body"))
ErrorMessages.RESPONSE_NULL_BODY public fun Response.requireBody(): ResponseBody = body
}

@ -3,10 +3,8 @@ package org.koitharu.kotatsu.parsers.util
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria
import org.koitharu.kotatsu.parsers.model.search.SearchableField
public class RelatedMangaFinder( public class RelatedMangaFinder(
private val parsers: Collection<MangaParser>, private val parsers: Collection<MangaParser>,
@ -36,10 +34,15 @@ public class RelatedMangaFinder(
val results = words.map { keyword -> val results = words.map { keyword ->
scope.async { scope.async {
val result = parser.getList( val result = parser.getList(
MangaSearchQuery.Builder() 0,
.order(SortOrder.RELEVANCE) if (SortOrder.RELEVANCE in parser.availableSortOrders) {
.criterion(QueryCriteria.Match(SearchableField.TITLE_NAME, keyword)) SortOrder.RELEVANCE
.build(), } else {
parser.availableSortOrders.first()
},
MangaListFilter(
query = keyword,
),
) )
result.filter { it.id != seed.id && it.containKeyword(keyword) } result.filter { it.id != seed.id && it.containKeyword(keyword) }
} }
@ -48,6 +51,7 @@ public class RelatedMangaFinder(
} }
private fun Manga.containKeyword(keyword: String): Boolean { private fun Manga.containKeyword(keyword: String): Boolean {
return title.contains(keyword, ignoreCase = true) || altTitle?.contains(keyword, ignoreCase = true) == true return title.contains(keyword, ignoreCase = true)
|| altTitles.any { it.contains(keyword, ignoreCase = true) }
} }
} }

Loading…
Cancel
Save