From b0a1cc48a6f4ce6ffe37a8c620086711586f587d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 19 Jan 2025 11:07:48 +0200 Subject: [PATCH] Update utils --- .../kotatsu/parsers/util/Collection.kt | 21 ++++++++ .../kotatsu/parsers/util/CookieJar.kt | 2 - .../kotatsu/parsers/util/MangaParserEnv.kt | 10 ++-- .../koitharu/kotatsu/parsers/util/Number.kt | 17 +++++-- .../koitharu/kotatsu/parsers/util/OkHttp.kt | 30 ++++++++--- .../koitharu/kotatsu/parsers/util/Parse.kt | 2 +- .../parsers/util/RelatedMangaFinder.kt | 6 +-- .../koitharu/kotatsu/parsers/util/String.kt | 51 ++++++++----------- .../parsers/util/suspendlazy/SuspendLazy.kt | 8 ++- .../org/koitharu/kotatsu/test_util/Util.kt | 3 +- 10 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt index 2b63f76a..68bd660e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Collection.kt @@ -6,6 +6,8 @@ import androidx.collection.ArrayMap import androidx.collection.ArraySet import java.util.* +public fun Collection<*>?.sizeOrZero(): Int = this?.size ?: 0 + public fun MutableCollection.replaceWith(subject: Iterable) { clear() addAll(subject) @@ -66,3 +68,22 @@ public fun MutableMap.incrementAndGet(key: K): Int { put(key, value) return value } + +public inline fun MutableSet(size: Int, init: (index: Int) -> T): MutableSet { + val set = ArraySet(size) + repeat(size) { index -> set.add(init(index)) } + return set +} + +public inline fun Set(size: Int, init: (index: Int) -> T): Set = when (size) { + 0 -> emptySet() + 1 -> Collections.singleton(init(0)) + else -> MutableSet(size, init) +} + +@Suppress("UNCHECKED_CAST") +public inline fun Collection.mapToArray(transform: (T) -> R): Array { + val result = arrayOfNulls(size) + forEachIndexed { index, t -> result[index] = transform(t) } + return result as Array +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt index 79258841..b3534c88 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CookieJar.kt @@ -6,8 +6,6 @@ import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl -private const val SCHEME_HTTPS = "https" - public fun CookieJar.insertCookies(domain: String, vararg cookies: String) { val url = safeUrlOf(domain) ?: return saveFromResponse( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt index 8f1e4155..9a73a837 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/MangaParserEnv.kt @@ -18,7 +18,7 @@ import org.koitharu.kotatsu.parsers.model.* */ @InternalParsersApi public fun MangaParser.generateUid(url: String): Long { - var h = 1125899906842597L + var h = LONG_HASH_SEED source.name.forEach { c -> h = 31 * h + c.code } @@ -37,7 +37,7 @@ public fun MangaParser.generateUid(url: String): Long { */ @InternalParsersApi public fun MangaParser.generateUid(id: Long): Long { - var h = 1125899906842597L + var h = LONG_HASH_SEED source.name.forEach { c -> h = 31 * h + c.code } @@ -82,9 +82,7 @@ private fun Set?.oneOrThrowIfMany(msg: String): T? = when { } public val MangaParser.domain: String - get() { - return config[configKeyDomain] - } + get() = config[configKeyDomain] @InternalParsersApi public fun MangaParser.getDomain(subdomain: String): String { @@ -95,6 +93,6 @@ public fun MangaParser.getDomain(subdomain: String): String { @InternalParsersApi public fun MangaParser.urlBuilder(subdomain: String? = null): HttpUrl.Builder { return HttpUrl.Builder() - .scheme("https") + .scheme(SCHEME_HTTPS) .host(if (subdomain == null) domain else "$subdomain.$domain") } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt index 6d4d2232..53227fec 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Number.kt @@ -58,12 +58,23 @@ public fun Number.formatSimple(): String { } } -public inline fun Int.ifZero(defaultVale: () -> Int): Int { +public inline fun Int.ifZero(defaultValue: () -> Int): Int { contract { - callsInPlace(defaultVale, InvocationKind.AT_MOST_ONCE) + callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) } return if (this == 0) { - defaultVale() + defaultValue() + } else { + this + } +} + +public inline fun Long.ifZero(defaultValue: () -> Long): Long { + contract { + callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) + } + return if (this == 0L) { + defaultValue() } else { this } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt index 259158f7..7151352c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/OkHttp.kt @@ -3,10 +3,7 @@ package org.koitharu.kotatsu.parsers.util import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.Call -import okhttp3.Headers -import okhttp3.Response -import okhttp3.ResponseBody +import okhttp3.* import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -19,9 +16,8 @@ public suspend fun Call.await(): Response = suspendCancellableCoroutine { contin public val Response.mimeType: String? get() = header("content-type")?.substringBefore(';')?.trim()?.nullIfEmpty()?.lowercase() -@Deprecated("") -public val Response.contentDisposition: String? - get() = header("Content-Disposition") +public val HttpUrl.isHttpOrHttps: Boolean + get() = scheme.equals("https", ignoreCase = true) || scheme.equals("http", ignoreCase = true) public fun Headers.Builder.mergeWith(other: Headers, replaceExisting: Boolean): Headers.Builder { for ((name, value) in other) { @@ -52,3 +48,23 @@ public inline fun Response.map(mapper: (ResponseBody) -> ResponseBody): Response .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() + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt index 1e96b1aa..857b2882 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/Parse.kt @@ -13,7 +13,7 @@ import org.koitharu.kotatsu.parsers.InternalParsersApi import java.text.DateFormat private val REGEX_SCHEME_PREFIX = Regex("^\\w{2,6}://", RegexOption.IGNORE_CASE) -private const val SCHEME_HTTPS = "https" +internal const val SCHEME_HTTPS = "https" /** * Parse [Response] body as html document using Jsoup diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt index 27aa4d33..43bc9d3f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/RelatedMangaFinder.kt @@ -13,8 +13,6 @@ public class RelatedMangaFinder( private val parsers: Collection, ) { - private val regexWhitespace = Regex("\\s+") - public suspend operator fun invoke(seed: Manga): List = coroutineScope { parsers.singleOrNull()?.let { parser -> findRelatedImpl(this, parser, seed) @@ -27,9 +25,9 @@ public class RelatedMangaFinder( private suspend fun findRelatedImpl(scope: CoroutineScope, parser: MangaParser, seed: Manga): List { val words = HashSet() - words += seed.title.split(regexWhitespace) + words += seed.title.splitByWhitespace() seed.altTitle?.let { - words += it.split(regexWhitespace) + words += it.splitByWhitespace() } if (words.isEmpty()) { return emptyList() 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 06bf02b1..7a9afc93 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/String.kt @@ -3,15 +3,17 @@ package org.koitharu.kotatsu.parsers.util import androidx.collection.MutableIntList -import androidx.collection.arraySetOf import java.math.BigInteger import java.net.URLDecoder import java.net.URLEncoder import java.security.MessageDigest import java.util.* +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.math.min private val REGEX_WHITESPACE = Regex("\\s+") +internal const val LONG_HASH_SEED = 1125899906842597L public fun String.removeSurrounding(vararg chars: Char): String { if (isEmpty()) { @@ -25,6 +27,22 @@ public fun String.removeSurrounding(vararg chars: Char): String { return this } +public inline fun C?.ifNullOrEmpty(defaultValue: () -> R): R { + contract { + callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) + } + return if (this.isNullOrEmpty()) defaultValue() else this +} + +public fun String.longHashCode(): Long { + var h = LONG_HASH_SEED + val len: Int = this.length + for (i in 0 until len) { + h = 31 * h + this[i].code + } + return h +} + public fun String.toCamelCase(): String { if (isEmpty()) { return this @@ -44,6 +62,8 @@ public fun String.toCamelCase(): String { return result.toString() } +public fun String.digits(): String = filter { it.isDigit() } + public fun String.toTitleCase(): String { return replaceFirstChar { x -> x.uppercase() } } @@ -52,35 +72,6 @@ public fun String.toTitleCase(locale: Locale): String { return replaceFirstChar { x -> x.uppercase(locale) } } -public fun String.transliterate(skipMissing: Boolean): String { - val cyr = charArrayOf( - 'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', - 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я', 'ё', 'ў', - ) - val lat = arrayOf( - "a", "b", "v", "g", "d", "e", "zh", "z", "i", "y", "k", "l", "m", "n", "o", "p", - "r", "s", "t", "u", "f", "h", "ts", "ch", "sh", "sch", "", "i", "", "e", "ju", "ja", "jo", "w", - ) - return buildString(length + 5) { - for (c in this@transliterate) { - val p = cyr.binarySearch(c.lowercaseChar()) - if (p in lat.indices) { - if (c.isUpperCase()) { - append(lat[p].uppercase()) - } else { - append(lat[p]) - } - } else if (!skipMissing) { - append(c) - } - } - } -} - -public fun String.toFileNameSafe(): String = this.transliterate(false) - .replace(Regex("[^a-z0-9_\\-]", arraySetOf(RegexOption.IGNORE_CASE)), " ") - .replace(REGEX_WHITESPACE, "_") - public fun String.ellipsize(maxLength: Int): String = if (this.length > maxLength) { this.take(maxLength - 1) + Typography.ellipsis } else this diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt index 549c8ac8..a923f71f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/suspendlazy/SuspendLazy.kt @@ -15,7 +15,13 @@ public interface SuspendLazy { public fun peek(): T? } -public suspend fun SuspendLazy.getOrNull(): T? = runCatchingCancellable { get() }.getOrNull() +public suspend fun SuspendLazy.getOrNull(): T? = runCatchingCancellable { + get() +}.getOrNull() + +public suspend fun SuspendLazy.getOrDefault(defaultValue: R): R = runCatchingCancellable { + get() +}.getOrDefault(defaultValue) public fun suspendLazy( context: CoroutineContext = EmptyCoroutineContext, diff --git a/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt b/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt index 986336ef..0aec98dc 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/test_util/Util.kt @@ -5,6 +5,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN +import org.koitharu.kotatsu.parsers.util.LONG_HASH_SEED import org.koitharu.kotatsu.parsers.util.toRelativeUrl private val PATTERN_URL_ABSOLUTE = Regex("^https?://[\\s\\S]+", setOf(RegexOption.IGNORE_CASE)) @@ -55,7 +56,7 @@ inline operator fun List.component7(): T = get(6) fun mangaOf(source: MangaParserSource, url: String): Manga { val httpUrl = url.toHttpUrlOrNull() - var id = 1125899906842597L + var id = LONG_HASH_SEED source.name.forEach { c -> id = 31 * id + c.code } url.forEach { c -> id = 31 * id + c.code } return Manga(