Improve utilities

master
Koitharu 2 years ago
parent 7e95949ab7
commit f410df40f1
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -1,3 +1,3 @@
package org.koitharu.kotatsu.parsers.exception package org.koitharu.kotatsu.parsers.exception
class ContentUnavailableException(message: String) : RuntimeException(message) public class ContentUnavailableException(message: String) : RuntimeException(message)

@ -2,11 +2,12 @@ package org.koitharu.kotatsu.parsers.exception
import okio.IOException import okio.IOException
import org.json.JSONArray import org.json.JSONArray
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.json.mapJSON
class GraphQLException(private val errors: JSONArray) : IOException() { public class GraphQLException @InternalParsersApi constructor(private val errors: JSONArray) : IOException() {
val messages = errors.mapJSON { public val messages = errors.mapJSON {
it.getString("message") it.getString("message")
} }

@ -3,7 +3,7 @@ package org.koitharu.kotatsu.parsers.exception
import org.jsoup.HttpStatusException import org.jsoup.HttpStatusException
import java.net.HttpURLConnection import java.net.HttpURLConnection
class NotFoundException( public class NotFoundException(
message: String, message: String,
url: String, url: String,
) : HttpStatusException(message, HttpURLConnection.HTTP_NOT_FOUND, url) ) : HttpStatusException(message, HttpURLConnection.HTTP_NOT_FOUND, url)

@ -2,8 +2,8 @@ package org.koitharu.kotatsu.parsers.exception
import org.koitharu.kotatsu.parsers.InternalParsersApi import org.koitharu.kotatsu.parsers.InternalParsersApi
class ParseException @InternalParsersApi @JvmOverloads constructor( public class ParseException @InternalParsersApi @JvmOverloads constructor(
val shortMessage: String?, public val shortMessage: String?,
val url: String, public val url: String,
cause: Throwable? = null, cause: Throwable? = null,
) : RuntimeException("$shortMessage at $url", cause) ) : RuntimeException("$shortMessage at $url", cause)

@ -4,18 +4,18 @@ import okio.IOException
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
class TooManyRequestExceptions( public class TooManyRequestExceptions(
val url: String, public val url: String,
retryAfter: Long, retryAfter: Long,
) : IOException("Too man requests") { ) : IOException("Too man requests") {
val retryAt: Instant? = if (retryAfter > 0 && retryAfter < Long.MAX_VALUE) { public val retryAt: Instant? = if (retryAfter > 0 && retryAfter < Long.MAX_VALUE) {
Instant.now().plusMillis(retryAfter) Instant.now().plusMillis(retryAfter)
} else { } else {
null null
} }
fun getRetryDelay(): Long { public fun getRetryDelay(): Long {
if (retryAt == null) { if (retryAt == null) {
return -1L return -1L
} }

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
enum class ContentRating { public enum class ContentRating {
SAFE, SAFE,
SUGGESTIVE, SUGGESTIVE,
ADULT ADULT

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
enum class ContentType { public enum class ContentType {
/** /**
* Standard manga, manhua, webtoons, etc * Standard manga, manhua, webtoons, etc

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
enum class Demographic { public enum class Demographic {
SHOUNEN, SHOUNEN,
SHOUJO, SHOUJO,
SEINEN, SEINEN,

@ -2,14 +2,14 @@ package org.koitharu.kotatsu.parsers.model
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
class Favicon internal constructor( public class Favicon(
@JvmField val url: String, @JvmField public val url: String,
@JvmField val size: Int, @JvmField public val size: Int,
@JvmField internal val rel: String?, @JvmField internal val rel: String?,
) : Comparable<Favicon> { ) : Comparable<Favicon> {
@JvmField @JvmField
val type: String = url.toHttpUrl().pathSegments.last() public val type: String = url.toHttpUrl().pathSegments.last()
.substringAfterLast('.', "").lowercase() .substringAfterLast('.', "").lowercase()
override fun compareTo(other: Favicon): Int { override fun compareTo(other: Favicon): Int {

@ -1,8 +1,8 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
class Favicons internal constructor( public class Favicons(
favicons: Collection<Favicon>, favicons: Collection<Favicon>,
@JvmField val referer: String?, @JvmField public val referer: String?,
) : Collection<Favicon> { ) : Collection<Favicon> {
private val icons = favicons.sortedDescending() private val icons = favicons.sortedDescending()
@ -18,7 +18,7 @@ class Favicons internal constructor(
override fun iterator(): Iterator<Favicon> = icons.iterator() override fun iterator(): Iterator<Favicon> = icons.iterator()
operator fun minus(victim: Favicon): Favicons = Favicons( public operator fun minus(victim: Favicon): Favicons = Favicons(
favicons = icons.filterNot { it == victim }, favicons = icons.filterNot { it == victim },
referer = referer, referer = referer,
) )
@ -30,7 +30,7 @@ class Favicons internal constructor(
* @param types supported file types, e.g. png, svg, ico. May be null but not empty * @param types supported file types, e.g. png, svg, ico. May be null but not empty
*/ */
@JvmOverloads @JvmOverloads
fun find(size: Int, types: Set<String>? = null): Favicon? { public fun find(size: Int, types: Set<String>? = null): Favicon? {
if (icons.isEmpty()) { if (icons.isEmpty()) {
return null return null
} }
@ -48,11 +48,12 @@ class Favicons internal constructor(
return result return result
} }
companion object { public companion object {
@JvmStatic @JvmStatic
fun empty() = Favicons(emptySet(), null) public val EMPTY: Favicons = Favicons(emptySet(), null)
fun single(url: String) = Favicons(setOf(Favicon(url, 0, null)), null) @JvmStatic
public fun single(url: String): Favicons = Favicons(setOf(Favicon(url, 0, null)), null)
} }
} }

@ -17,19 +17,24 @@ public data class MangaListFilter(
@JvmField val yearTo: Int = YEAR_UNKNOWN, @JvmField val yearTo: Int = YEAR_UNKNOWN,
) { ) {
public fun isEmpty(): Boolean = tags.isEmpty() && private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() &&
tagsExclude.isEmpty() && tagsExclude.isEmpty() &&
locale == null && locale == null &&
originalLocale == null && originalLocale == null &&
states.isEmpty() && states.isEmpty() &&
contentRating.isEmpty() && contentRating.isEmpty() &&
query == null &&
year == YEAR_UNKNOWN && year == YEAR_UNKNOWN &&
yearFrom == YEAR_UNKNOWN && yearFrom == YEAR_UNKNOWN &&
yearTo == YEAR_UNKNOWN && yearTo == YEAR_UNKNOWN &&
types.isEmpty() && types.isEmpty() &&
demographics.isEmpty() demographics.isEmpty()
public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty()
public fun isNotEmpty(): Boolean = !isEmpty()
public fun hasNonSearchOptions(): Boolean = !isNonSearchOptionsEmpty()
public companion object { public companion object {
@JvmStatic @JvmStatic

@ -2,23 +2,23 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
class MangaPage( public class MangaPage(
/** /**
* Unique identifier for manga * Unique identifier for manga
*/ */
@JvmField val id: Long, @JvmField public val id: Long,
/** /**
* Relative url to page (**without** a domain) or any other uri. * Relative url to page (**without** a domain) or any other uri.
* Used principally in parsers. * Used principally in parsers.
* May contain link to image or html page. * May contain link to image or html page.
* @see MangaParser.getPageUrl * @see MangaParser.getPageUrl
*/ */
@JvmField val url: String, @JvmField public val url: String,
/** /**
* Absolute url of the small page image if exists, null otherwise * Absolute url of the small page image if exists, null otherwise
*/ */
@JvmField val preview: String?, @JvmField public val preview: String?,
@JvmField val source: MangaSource, @JvmField public val source: MangaSource,
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
enum class SortOrder { public enum class SortOrder {
UPDATED, UPDATED,
UPDATED_ASC, UPDATED_ASC,
POPULARITY, POPULARITY,

@ -13,7 +13,7 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.parseJson import org.koitharu.kotatsu.parsers.util.parseJson
import java.net.HttpURLConnection import java.net.HttpURLConnection
class OkHttpWebClient( public class OkHttpWebClient(
private val httpClient: OkHttpClient, private val httpClient: OkHttpClient,
private val mangaSource: MangaSource, private val mangaSource: MangaSource,
) : WebClient { ) : WebClient {

@ -1,16 +1,17 @@
package org.koitharu.kotatsu.parsers.network package org.koitharu.kotatsu.parsers.network
object UserAgents { public object UserAgents {
const val CHROME_MOBILE = public const val CHROME_MOBILE: String =
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 Mobile Safari/537.36" "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 Mobile Safari/537.36"
const val FIREFOX_MOBILE = "Mozilla/5.0 (Android 14; Mobile; LG-M255; rv:123.0) Gecko/123.0 Firefox/123.0" public const val FIREFOX_MOBILE: String =
"Mozilla/5.0 (Android 14; Mobile; LG-M255; rv:123.0) Gecko/123.0 Firefox/123.0"
const val CHROME_DESKTOP = public const val CHROME_DESKTOP: String =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
const val FIREFOX_DESKTOP = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0" public const val FIREFOX_DESKTOP: String = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"
const val KOTATSU = "Kotatsu/6.8 (Android 13;;; en)" public const val KOTATSU: String = "Kotatsu/6.8 (Android 13;;; en)"
} }

@ -6,54 +6,55 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response import okhttp3.Response
import org.json.JSONObject import org.json.JSONObject
interface WebClient { public interface WebClient {
/** /**
* Do a GET http request to specific url * Do a GET http request to specific url
* @param url * @param url
*/ */
suspend fun httpGet(url: String): Response = httpGet(url.toHttpUrl()) public suspend fun httpGet(url: String): Response = httpGet(url.toHttpUrl())
suspend fun httpGet(url: String, extraHeaders: Headers?): Response = httpGet(url.toHttpUrl(), extraHeaders) public suspend fun httpGet(url: String, extraHeaders: Headers?): Response = httpGet(url.toHttpUrl(), extraHeaders)
/** /**
* Do a GET http request to specific url * Do a GET http request to specific url
* @param url * @param url
*/ */
suspend fun httpGet(url: HttpUrl): Response = httpGet(url, null) public suspend fun httpGet(url: HttpUrl): Response = httpGet(url, null)
/** /**
* Do a GET http request to specific url * Do a GET http request to specific url
* @param url * @param url
* @param extraHeaders additional HTTP headers for request * @param extraHeaders additional HTTP headers for request
*/ */
suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response public suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response
/** /**
* Do a HEAD http request to specific url * Do a HEAD http request to specific url
* @param url * @param url
*/ */
suspend fun httpHead(url: String): Response = httpHead(url.toHttpUrl()) public suspend fun httpHead(url: String): Response = httpHead(url.toHttpUrl())
/** /**
* Do a HEAD http request to specific url * Do a HEAD http request to specific url
* @param url * @param url
*/ */
suspend fun httpHead(url: HttpUrl): Response public suspend fun httpHead(url: HttpUrl): Response
/** /**
* Do a POST http request to specific url with `multipart/form-data` payload * Do a POST http request to specific url with `multipart/form-data` payload
* @param url * @param url
* @param form payload as key=>value map * @param form payload as key=>value map
*/ */
suspend fun httpPost(url: String, form: Map<String, String>): Response = httpPost(url.toHttpUrl(), form, null) public suspend fun httpPost(url: String, form: Map<String, String>): Response =
httpPost(url.toHttpUrl(), form, null)
/** /**
* Do a POST http request to specific url with `multipart/form-data` payload * Do a POST http request to specific url with `multipart/form-data` payload
* @param url * @param url
* @param form payload as key=>value map * @param form payload as key=>value map
*/ */
suspend fun httpPost(url: HttpUrl, form: Map<String, String>): Response = httpPost(url, form, null) public suspend fun httpPost(url: HttpUrl, form: Map<String, String>): Response = httpPost(url, form, null)
/** /**
* Do a POST http request to specific url with `multipart/form-data` payload * Do a POST http request to specific url with `multipart/form-data` payload
@ -61,21 +62,21 @@ interface WebClient {
* @param form payload as key=>value map * @param form payload as key=>value map
* @param extraHeaders additional HTTP headers for request * @param extraHeaders additional HTTP headers for request
*/ */
suspend fun httpPost(url: HttpUrl, form: Map<String, String>, extraHeaders: Headers?): Response public suspend fun httpPost(url: HttpUrl, form: Map<String, String>, extraHeaders: Headers?): Response
/** /**
* Do a POST http request to specific url with `multipart/form-data` payload * Do a POST http request to specific url with `multipart/form-data` payload
* @param url * @param url
* @param payload payload as `key=value` string with `&` separator * @param payload payload as `key=value` string with `&` separator
*/ */
suspend fun httpPost(url: String, payload: String): Response = httpPost(url.toHttpUrl(), payload, null) public suspend fun httpPost(url: String, payload: String): Response = httpPost(url.toHttpUrl(), payload, null)
/** /**
* Do a POST http request to specific url with `multipart/form-data` payload * Do a POST http request to specific url with `multipart/form-data` payload
* @param url * @param url
* @param payload payload as `key=value` string with `&` separator * @param payload payload as `key=value` string with `&` separator
*/ */
suspend fun httpPost(url: HttpUrl, payload: String): Response = httpPost(url, payload, null) public suspend fun httpPost(url: HttpUrl, payload: String): Response = httpPost(url, payload, null)
/** /**
* Do a POST http request to specific url with `multipart/form-data` payload * Do a POST http request to specific url with `multipart/form-data` payload
@ -83,21 +84,21 @@ interface WebClient {
* @param payload payload as `key=value` string with `&` separator * @param payload payload as `key=value` string with `&` separator
* @param extraHeaders additional HTTP headers for request * @param extraHeaders additional HTTP headers for request
*/ */
suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response public suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response
/** /**
* Do a POST http request to specific url with json payload * Do a POST http request to specific url with json payload
* @param url * @param url
* @param body * @param body
*/ */
suspend fun httpPost(url: String, body: JSONObject): Response = httpPost(url.toHttpUrl(), body, null) public suspend fun httpPost(url: String, body: JSONObject): Response = httpPost(url.toHttpUrl(), body, null)
/** /**
* Do a POST http request to specific url with json payload * Do a POST http request to specific url with json payload
* @param url * @param url
* @param body * @param body
*/ */
suspend fun httpPost(url: HttpUrl, body: JSONObject): Response = httpPost(url, body, null) public suspend fun httpPost(url: HttpUrl, body: JSONObject): Response = httpPost(url, body, null)
/** /**
* Do a POST http request to specific url with json payload * Do a POST http request to specific url with json payload
@ -105,12 +106,12 @@ interface WebClient {
* @param body * @param body
* @param extraHeaders additional HTTP headers for request * @param extraHeaders additional HTTP headers for request
*/ */
suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response public suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response
/** /**
* Do a GraphQL request to specific url * Do a GraphQL request to specific url
* @param endpoint an url * @param endpoint an url
* @param query GraphQL request payload * @param query GraphQL request payload
*/ */
suspend fun graphQLQuery(endpoint: String, query: String): JSONObject public suspend fun graphQLQuery(endpoint: String, query: String): JSONObject
} }

@ -177,12 +177,12 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
append("&genres=") append("&genres=")
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
appendAll(filter.tags, ",") { it.key } filter.tags.joinTo(this, ",") { it.key }
} }
append("|") append("|")
if (filter.tagsExclude.isNotEmpty()) { if (filter.tagsExclude.isNotEmpty()) {
appendAll(filter.tagsExclude, ",") { it.key } filter.tagsExclude.joinTo(this, ",") { it.key }
} }
if (filter.contentRating.isNotEmpty()) { if (filter.contentRating.isNotEmpty()) {

@ -79,14 +79,10 @@ internal class MangaPark(context: MangaLoaderContext) :
} }
append("&genres=") append("&genres=")
if (filter.tags.isNotEmpty()) { filter.tags.joinTo(this, ",") { it.key }
appendAll(filter.tags, ",") { it.key }
}
append("|") append("|")
if (filter.tagsExclude.isNotEmpty()) { filter.tagsExclude.joinTo(this, ",") { it.key }
appendAll(filter.tagsExclude, ",") { it.key }
}
if (filter.contentRating.isNotEmpty()) { if (filter.contentRating.isNotEmpty()) {
filter.contentRating.oneOrThrowIfMany()?.let { filter.contentRating.oneOrThrowIfMany()?.let {

@ -61,7 +61,7 @@ internal abstract class IkenParser(
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&genreIds=") append("&genreIds=")
appendAll(filter.tags, ",") { it.key } filter.tags.joinTo(this, ",") { it.key }
} }
append("&seriesType=&seriesStatus=") append("&seriesType=&seriesStatus=")

@ -56,7 +56,7 @@ internal class DesuMeParser(context: MangaLoaderContext) : PagedMangaParser(cont
append(page) append(page)
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&genres=") append("&genres=")
appendAll(filter.tags, ",") { it.key } filter.tags.joinTo(this, ",") { it.key }
} }
if (!filter.query.isNullOrEmpty()) { if (!filter.query.isNullOrEmpty()) {
append("&search=") append("&search=")

@ -6,7 +6,7 @@ import androidx.collection.ArrayMap
import androidx.collection.ArraySet import androidx.collection.ArraySet
import java.util.* import java.util.*
fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) { public fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
clear() clear()
addAll(subject) addAll(subject)
} }
@ -23,7 +23,7 @@ fun <T> List<T>.medianOrNull(): T? = when {
else -> get((size / 2).coerceIn(indices)) else -> get((size / 2).coerceIn(indices))
} }
inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> { public inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> {
return mapTo(ArraySet(size), transform) return mapTo(ArraySet(size), transform)
} }
@ -35,13 +35,13 @@ inline fun <T, R> Collection<T>.mapNotNullToSet(transform: (T) -> R?): Set<R> {
return destination return destination
} }
inline fun <T, reified R> Array<T>.mapToArray(transform: (T) -> R): Array<R> = Array(size) { i -> public inline fun <T, reified R> Array<T>.mapToArray(transform: (T) -> R): Array<R> = Array(size) { i ->
transform(get(i)) transform(get(i))
} }
fun <K, V> List<Pair<K, V>>.toMutableMap(): MutableMap<K, V> = toMap(ArrayMap(size)) fun <K, V> List<Pair<K, V>>.toMutableMap(): MutableMap<K, V> = toMap(ArrayMap(size))
fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) { public fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
if (sourceIndex <= targetIndex) { if (sourceIndex <= targetIndex) {
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)
} else { } else {

@ -8,7 +8,7 @@ import okhttp3.HttpUrl
private const val SCHEME_HTTPS = "https" private const val SCHEME_HTTPS = "https"
fun CookieJar.insertCookies(domain: String, vararg cookies: String) { public fun CookieJar.insertCookies(domain: String, vararg cookies: String) {
val url = safeUrlOf(domain) ?: return val url = safeUrlOf(domain) ?: return
saveFromResponse( saveFromResponse(
url, url,
@ -18,17 +18,17 @@ fun CookieJar.insertCookies(domain: String, vararg cookies: String) {
) )
} }
fun CookieJar.insertCookie(domain: String, cookie: Cookie) { public fun CookieJar.insertCookie(domain: String, cookie: Cookie) {
val url = safeUrlOf(domain) ?: return val url = safeUrlOf(domain) ?: return
saveFromResponse(url, listOf(cookie)) saveFromResponse(url, listOf(cookie))
} }
fun CookieJar.getCookies(domain: String): List<Cookie> { public fun CookieJar.getCookies(domain: String): List<Cookie> {
val url = safeUrlOf(domain) ?: return emptyList() val url = safeUrlOf(domain) ?: return emptyList()
return loadForRequest(url) return loadForRequest(url)
} }
fun CookieJar.copyCookies(oldDomain: String, newDomain: String, names: Array<String>? = null) { public fun CookieJar.copyCookies(oldDomain: String, newDomain: String, names: Array<String>? = null) {
val url = HttpUrl.Builder() val url = HttpUrl.Builder()
.scheme(SCHEME_HTTPS) .scheme(SCHEME_HTTPS)
.host(oldDomain) .host(oldDomain)

@ -1,11 +1,13 @@
package org.koitharu.kotatsu.parsers.util package org.koitharu.kotatsu.parsers.util
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.Throws
private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING" private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING"
private const val AES = "AES" private const val AES = "AES"
@ -14,7 +16,8 @@ private const val KDF_DIGEST = "MD5"
/** /**
* Conforming with CryptoJS AES method * Conforming with CryptoJS AES method
*/ */
class CryptoAES( @InternalParsersApi
public class CryptoAES(
private val context: MangaLoaderContext, private val context: MangaLoaderContext,
) { ) {
@ -27,7 +30,7 @@ class CryptoAES(
* @param password passphrase * @param password passphrase
*/ */
@Throws(Exception::class) @Throws(Exception::class)
fun decrypt(cipherText: String, password: String): String { public fun decrypt(cipherText: String, password: String): String {
val ctBytes = context.decodeBase64(cipherText) val ctBytes = context.decodeBase64(cipherText)
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16) val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size) val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
@ -48,7 +51,7 @@ class CryptoAES(
* @param ivBytes iv as a bytearray * @param ivBytes iv as a bytearray
*/ */
@Throws(Exception::class) @Throws(Exception::class)
fun decrypt(cipherText: String, keyBytes: ByteArray, ivBytes: ByteArray): String { public fun decrypt(cipherText: String, keyBytes: ByteArray, ivBytes: ByteArray): String {
val cipherTextBytes = context.decodeBase64(cipherText) val cipherTextBytes = context.decodeBase64(cipherText)
return decryptAES(cipherTextBytes, keyBytes, ivBytes) return decryptAES(cipherTextBytes, keyBytes, ivBytes)
} }

@ -7,6 +7,7 @@ import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import org.jsoup.select.QueryParser import org.jsoup.select.QueryParser
import org.jsoup.select.Selector import org.jsoup.select.Selector
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
val Element.host: String? val Element.host: String?
@ -154,8 +155,22 @@ fun Element.attrOrNull(vararg names: String): String? {
return null return null
} }
@InternalParsersApi
@JvmOverloads @JvmOverloads
fun Element.src(names: Array<String> = arrayOf("data-src", "data-cfsrc", "data-original", "data-cdn", "data-sizes", "data-lazy-src", "data-srcset", "original-src", "data-wpfc-original-src", "src")): String? { public fun Element.src(
names: Array<String> = arrayOf(
"data-src",
"data-cfsrc",
"data-original",
"data-cdn",
"data-sizes",
"data-lazy-src",
"data-srcset",
"original-src",
"data-wpfc-original-src",
"src",
),
): String? {
for (name in names) { for (name in names) {
val value = attrAsAbsoluteUrlOrNull(name) val value = attrAsAbsoluteUrlOrNull(name)
if (value != null) { if (value != null) {

@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.model.*
* @see [MangaPage.id] * @see [MangaPage.id]
*/ */
@InternalParsersApi @InternalParsersApi
fun MangaParser.generateUid(url: String): Long { public fun MangaParser.generateUid(url: String): Long {
var h = 1125899906842597L var h = 1125899906842597L
source.name.forEach { c -> source.name.forEach { c ->
h = 31 * h + c.code h = 31 * h + c.code
@ -36,7 +36,7 @@ fun MangaParser.generateUid(url: String): Long {
* @see [MangaPage.id] * @see [MangaPage.id]
*/ */
@InternalParsersApi @InternalParsersApi
fun MangaParser.generateUid(id: Long): Long { public fun MangaParser.generateUid(id: Long): Long {
var h = 1125899906842597L var h = 1125899906842597L
source.name.forEach { c -> source.name.forEach { c ->
h = 31 * h + c.code h = 31 * h + c.code
@ -46,32 +46,32 @@ fun MangaParser.generateUid(id: Long): Long {
} }
@InternalParsersApi @InternalParsersApi
fun Element.parseFailed(message: String? = null): Nothing { public fun Element.parseFailed(message: String? = null): Nothing {
throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null) throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null)
} }
@InternalParsersApi @InternalParsersApi
fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? = oneOrThrowIfMany( public fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? = oneOrThrowIfMany(
ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED, ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED,
) )
@InternalParsersApi @InternalParsersApi
fun Set<MangaState>?.oneOrThrowIfMany(): MangaState? = oneOrThrowIfMany( public fun Set<MangaState>?.oneOrThrowIfMany(): MangaState? = oneOrThrowIfMany(
ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED, ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED,
) )
@InternalParsersApi @InternalParsersApi
fun Set<ContentType>?.oneOrThrowIfMany(): ContentType? = oneOrThrowIfMany( public fun Set<ContentType>?.oneOrThrowIfMany(): ContentType? = oneOrThrowIfMany(
ErrorMessages.FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED, ErrorMessages.FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED,
) )
@InternalParsersApi @InternalParsersApi
fun Set<Demographic>?.oneOrThrowIfMany(): Demographic? = oneOrThrowIfMany( public fun Set<Demographic>?.oneOrThrowIfMany(): Demographic? = oneOrThrowIfMany(
ErrorMessages.FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED, ErrorMessages.FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED,
) )
@InternalParsersApi @InternalParsersApi
fun Set<ContentRating>?.oneOrThrowIfMany(): ContentRating? = oneOrThrowIfMany( public fun Set<ContentRating>?.oneOrThrowIfMany(): ContentRating? = oneOrThrowIfMany(
ErrorMessages.FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED, ErrorMessages.FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED,
) )
@ -81,17 +81,19 @@ private fun <T> Set<T>?.oneOrThrowIfMany(msg: String): T? = when {
else -> throw IllegalArgumentException(msg) else -> throw IllegalArgumentException(msg)
} }
val MangaParser.domain: String public val MangaParser.domain: String
get() { get() {
return config[configKeyDomain] return config[configKeyDomain]
} }
fun MangaParser.getDomain(subdomain: String): String { @InternalParsersApi
public fun MangaParser.getDomain(subdomain: String): String {
val domain = domain val domain = domain
return subdomain + "." + domain.removePrefix("www.") return subdomain + "." + domain.removePrefix("www.")
} }
fun MangaParser.urlBuilder(subdomain: String? = null): HttpUrl.Builder { @InternalParsersApi
public fun MangaParser.urlBuilder(subdomain: String? = null): HttpUrl.Builder {
return HttpUrl.Builder() return HttpUrl.Builder()
.scheme("https") .scheme("https")
.host(if (subdomain == null) domain else "$subdomain.$domain") .host(if (subdomain == null) domain else "$subdomain.$domain")

@ -0,0 +1,13 @@
@file:JvmName("MangaParsersUtils")
package org.koitharu.kotatsu.parsers.util
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import kotlin.contracts.contract
public fun MangaListFilter?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.isEmpty()
}

@ -7,7 +7,7 @@ import java.text.NumberFormat
import java.util.* import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String { public fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String {
val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat
val symbols = formatter.decimalFormatSymbols val symbols = formatter.decimalFormatSymbols
if (thousandsSep != null) { if (thousandsSep != null) {
@ -29,7 +29,7 @@ fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? =
} }
} }
fun Float.toIntUp(): Int { public fun Float.toIntUp(): Int {
val intValue = toInt() val intValue = toInt()
return if ((this - intValue.toFloat()).absoluteValue <= 0.00001) { return if ((this - intValue.toFloat()).absoluteValue <= 0.00001) {
intValue intValue
@ -38,7 +38,7 @@ fun Float.toIntUp(): Int {
} }
} }
infix fun Int.upBy(step: Int): Int { public infix fun Int.upBy(step: Int): Int {
val mod = this % step val mod = this % step
return if (mod == 0) { return if (mod == 0) {
this this
@ -47,7 +47,7 @@ infix fun Int.upBy(step: Int): Int {
} }
} }
fun Number.formatSimple(): String { public fun Number.formatSimple(): String {
val raw = toString() val raw = toString()
return if (raw.endsWith(".0") || raw.endsWith(",0")) { return if (raw.endsWith(".0") || raw.endsWith(",0")) {
raw.dropLast(2) raw.dropLast(2)
@ -56,7 +56,7 @@ fun Number.formatSimple(): String {
} }
} }
inline fun Int.ifZero(defaultVale: () -> Int): Int = if (this == 0) { public inline fun Int.ifZero(defaultVale: () -> Int): Int = if (this == 0) {
defaultVale() defaultVale()
} else { } else {
this this

@ -7,19 +7,19 @@ import okhttp3.Call
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Response import okhttp3.Response
suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation -> public suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
val callback = ContinuationCallCallback(this, continuation) val callback = ContinuationCallCallback(this, continuation)
enqueue(callback) enqueue(callback)
continuation.invokeOnCancellation(callback) continuation.invokeOnCancellation(callback)
} }
val Response.mimeType: String? public val Response.mimeType: String?
get() = header("content-type")?.takeUnless { it.isEmpty() } get() = header("content-type")?.takeUnless { it.isEmpty() }
val Response.contentDisposition: String? public val Response.contentDisposition: String?
get() = header("Content-Disposition") get() = header("Content-Disposition")
fun Headers.Builder.mergeWith(other: Headers, replaceExisting: Boolean): Headers.Builder { public fun Headers.Builder.mergeWith(other: Headers, replaceExisting: Boolean): Headers.Builder {
for ((name, value) in other) { for ((name, value) in other) {
if (replaceExisting || this[name] == null) { if (replaceExisting || this[name] == null) {
this[name] = value this[name] = value
@ -28,11 +28,11 @@ fun Headers.Builder.mergeWith(other: Headers, replaceExisting: Boolean): Headers
return this return this
} }
fun Response.copy() = newBuilder() public fun Response.copy(): Response = newBuilder()
.body(peekBody(Long.MAX_VALUE)) .body(peekBody(Long.MAX_VALUE))
.build() .build()
fun Response.Builder.setHeader(name: String, value: String?) = if (value == null) { public fun Response.Builder.setHeader(name: String, value: String?): Response.Builder = if (value == null) {
removeHeader(name) removeHeader(name)
} else { } else {
header(name, value) header(name, value)

@ -3,12 +3,12 @@ package org.koitharu.kotatsu.parsers.util
import androidx.collection.SparseArrayCompat import androidx.collection.SparseArrayCompat
import androidx.collection.set import androidx.collection.set
class Paginator(private val initialPageSize: Int) { public class Paginator internal constructor(private val initialPageSize: Int) {
var firstPage = 1 public var firstPage: Int = 1
private var pages = SparseArrayCompat<Int>() private var pages = SparseArrayCompat<Int>()
fun getPage(offset: Int): Int { internal fun getPage(offset: Int): Int {
if (offset == 0) { // just an optimization if (offset == 0) { // just an optimization
return firstPage return firstPage
} }
@ -19,7 +19,7 @@ class Paginator(private val initialPageSize: Int) {
return intPage + firstPage + if (tail == 0) 0 else 1 return intPage + firstPage + if (tail == 0) 0 else 1
} }
fun onListReceived(offset: Int, page: Int, count: Int) { internal fun onListReceived(offset: Int, page: Int, count: Int) {
pages[offset + count] = if (count > 0) page + 1 else page pages[offset + count] = if (count > 0) page + 1 else page
} }
} }

@ -5,10 +5,12 @@ package org.koitharu.kotatsu.parsers.util
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import org.jetbrains.annotations.Blocking
import org.json.JSONArray 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.InternalParsersApi
import java.text.DateFormat import java.text.DateFormat
/** /**
@ -16,7 +18,8 @@ import java.text.DateFormat
* @see [parseJson] * @see [parseJson]
* @see [parseJsonArray] * @see [parseJsonArray]
*/ */
fun Response.parseHtml(): Document = try { @Blocking
public fun Response.parseHtml(): Document = try {
val body = requireBody() val body = requireBody()
val charset = body.contentType()?.charset()?.name() val charset = body.contentType()?.charset()?.name()
Jsoup.parse(body.byteStream(), charset, request.url.toString()) Jsoup.parse(body.byteStream(), charset, request.url.toString())
@ -29,7 +32,7 @@ fun Response.parseHtml(): Document = try {
* @see [parseJsonArray] * @see [parseJsonArray]
* @see [parseHtml] * @see [parseHtml]
*/ */
fun Response.parseJson(): JSONObject = try { public fun Response.parseJson(): JSONObject = try {
JSONObject(requireBody().string()) JSONObject(requireBody().string())
} finally { } finally {
closeQuietly() closeQuietly()
@ -40,19 +43,19 @@ fun Response.parseJson(): JSONObject = try {
* @see [parseJson] * @see [parseJson]
* @see [parseHtml] * @see [parseHtml]
*/ */
fun Response.parseJsonArray(): JSONArray = try { public fun Response.parseJsonArray(): JSONArray = try {
JSONArray(requireBody().string()) JSONArray(requireBody().string())
} finally { } finally {
closeQuietly() closeQuietly()
} }
fun Response.parseRaw(): String = try { public fun Response.parseRaw(): String = try {
requireBody().string() requireBody().string()
} finally { } finally {
closeQuietly() closeQuietly()
} }
fun Response.parseBytes(): ByteArray = try { public fun Response.parseBytes(): ByteArray = try {
requireBody().bytes() requireBody().bytes()
} finally { } finally {
closeQuietly() closeQuietly()
@ -62,7 +65,7 @@ fun Response.parseBytes(): ByteArray = try {
* Convert url to relative if it is on [domain] * Convert url to relative if it is on [domain]
* @return an url relative to the [domain] or absolute, if domain is mismatching * @return an url relative to the [domain] or absolute, if domain is mismatching
*/ */
fun String.toRelativeUrl(domain: String): String { public fun String.toRelativeUrl(domain: String): String {
if (isEmpty() || startsWith("/")) { if (isEmpty() || startsWith("/")) {
return this return this
} }
@ -73,13 +76,13 @@ fun String.toRelativeUrl(domain: String): String {
* Convert url to absolute with specified domain * Convert url to absolute with specified domain
* @return an absolute url with [domain] if this is relative * @return an absolute url with [domain] if this is relative
*/ */
fun String.toAbsoluteUrl(domain: String): String = when { public fun String.toAbsoluteUrl(domain: String): String = when {
this.startsWith("//") -> "https:$this" this.startsWith("//") -> "https:$this"
this.startsWith('/') -> "https://$domain$this" this.startsWith('/') -> "https://$domain$this"
else -> this else -> this
} }
fun concatUrl(host: String, path: String): String { public fun concatUrl(host: String, path: String): String {
val hostWithSlash = host.endsWith('/') val hostWithSlash = host.endsWith('/')
val pathWithSlash = path.startsWith('/') val pathWithSlash = path.startsWith('/')
val hostWithScheme = if (host.startsWith("//")) "https:$host" else host val hostWithScheme = if (host.startsWith("//")) "https:$host" else host
@ -90,7 +93,8 @@ fun concatUrl(host: String, path: String): String {
} }
} }
fun DateFormat.tryParse(str: String?): Long = if (str.isNullOrEmpty()) { @InternalParsersApi
public fun DateFormat.tryParse(str: String?): Long = if (str.isNullOrEmpty()) {
// assert(false) { "Date string is null or empty" } // assert(false) { "Date string is null or empty" }
0L 0L
} else { } else {

@ -9,13 +9,13 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
class RelatedMangaFinder( public class RelatedMangaFinder(
private val parsers: Collection<MangaParser>, private val parsers: Collection<MangaParser>,
) { ) {
private val regexWhitespace = Regex("\\s+") private val regexWhitespace = Regex("\\s+")
suspend operator fun invoke(seed: Manga): List<Manga> = coroutineScope { public suspend operator fun invoke(seed: Manga): List<Manga> = coroutineScope {
parsers.singleOrNull()?.let { parser -> parsers.singleOrNull()?.let { parser ->
findRelatedImpl(this, parser, seed) findRelatedImpl(this, parser, seed)
} ?: parsers.map { parser -> } ?: parsers.map { parser ->

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.util
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
inline fun <T, R> T.runCatchingCancellable(block: T.() -> R): Result<R> { public inline fun <T, R> T.runCatchingCancellable(block: T.() -> R): Result<R> {
return try { return try {
Result.success(block()) Result.success(block())
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
@ -14,14 +14,14 @@ inline fun <T, R> T.runCatchingCancellable(block: T.() -> R): Result<R> {
} }
} }
inline fun <R, T : R> Result<T>.recoverCatchingCancellable(transform: (exception: Throwable) -> R): Result<R> { public inline fun <R, T : R> Result<T>.recoverCatchingCancellable(transform: (exception: Throwable) -> R): Result<R> {
return when (val exception = exceptionOrNull()) { return when (val exception = exceptionOrNull()) {
null -> this null -> this
else -> runCatchingCancellable { transform(exception) } else -> runCatchingCancellable { transform(exception) }
} }
} }
inline fun <R : Any, T : R> Result<T>.recoverNotNull(transform: (exception: Throwable) -> R?): Result<R> { public inline fun <R : Any, T : R> Result<T>.recoverNotNull(transform: (exception: Throwable) -> R?): Result<R> {
return when (val exception = exceptionOrNull()) { return when (val exception = exceptionOrNull()) {
null -> this null -> this
else -> transform(exception)?.let(Result.Companion::success) ?: this else -> transform(exception)?.let(Result.Companion::success) ?: this

@ -7,14 +7,14 @@ import java.lang.ref.SoftReference
/** /**
* Like a [SuspendLazy] but with [SoftReference] under the hood * Like a [SuspendLazy] but with [SoftReference] under the hood
*/ */
class SoftSuspendLazy<T : Any>( public class SoftSuspendLazy<T : Any>(
private val initializer: suspend () -> T, private val initializer: suspend () -> T,
) { ) {
private val mutex = Mutex() private val mutex = Mutex()
private var cachedValue: SoftReference<T>? = null private var cachedValue: SoftReference<T>? = null
suspend fun get(): T { public suspend fun get(): T {
// fast way // fast way
cachedValue?.get()?.let { cachedValue?.get()?.let {
return it return it
@ -29,9 +29,9 @@ class SoftSuspendLazy<T : Any>(
} }
} }
suspend fun tryGet() = runCatchingCancellable { get() } public suspend fun tryGet(): Result<T> = runCatchingCancellable { get() }
fun peek(): T? { public fun peek(): T? {
return cachedValue?.get() return cachedValue?.get()
} }
} }

@ -12,7 +12,7 @@ import java.security.MessageDigest
import java.util.* import java.util.*
import kotlin.math.min import kotlin.math.min
fun String.removeSurrounding(vararg chars: Char): String { public fun String.removeSurrounding(vararg chars: Char): String {
if (isEmpty()) { if (isEmpty()) {
return this return this
} }
@ -24,7 +24,7 @@ fun String.removeSurrounding(vararg chars: Char): String {
return this return this
} }
fun String.toCamelCase(): String { public fun String.toCamelCase(): String {
if (isEmpty()) { if (isEmpty()) {
return this return this
} }
@ -43,15 +43,15 @@ fun String.toCamelCase(): String {
return result.toString() return result.toString()
} }
fun String.toTitleCase(): String { public fun String.toTitleCase(): String {
return replaceFirstChar { x -> x.uppercase() } return replaceFirstChar { x -> x.uppercase() }
} }
fun String.toTitleCase(locale: Locale): String { public fun String.toTitleCase(locale: Locale): String {
return replaceFirstChar { x -> x.uppercase(locale) } return replaceFirstChar { x -> x.uppercase(locale) }
} }
fun String.transliterate(skipMissing: Boolean): String { public fun String.transliterate(skipMissing: Boolean): String {
val cyr = charArrayOf( val cyr = charArrayOf(
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п',
'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я', 'ё', 'ў', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я', 'ё', 'ў',
@ -76,15 +76,15 @@ fun String.transliterate(skipMissing: Boolean): String {
} }
} }
fun String.toFileNameSafe() = this.transliterate(false) public fun String.toFileNameSafe() = this.transliterate(false)
.replace(Regex("[^a-z0-9_\\-]", arraySetOf(RegexOption.IGNORE_CASE)), " ") .replace(Regex("[^a-z0-9_\\-]", arraySetOf(RegexOption.IGNORE_CASE)), " ")
.replace(Regex("\\s+"), "_") .replace(Regex("\\s+"), "_")
fun String.ellipsize(maxLength: Int) = if (this.length > maxLength) { public fun String.ellipsize(maxLength: Int) = if (this.length > maxLength) {
this.take(maxLength - 1) + Typography.ellipsis this.take(maxLength - 1) + Typography.ellipsis
} else this } else this
fun String.splitTwoParts(delimiter: Char): Pair<String, String>? { public fun String.splitTwoParts(delimiter: Char): Pair<String, String>? {
val indices = MutableIntList(4) val indices = MutableIntList(4)
for ((i, c) in this.withIndex()) { for ((i, c) in this.withIndex()) {
if (c == delimiter) { if (c == delimiter) {
@ -98,13 +98,13 @@ fun String.splitTwoParts(delimiter: Char): Pair<String, String>? {
return substring(0, index) to substring(index + 1) return substring(0, index) to substring(index + 1)
} }
fun String.urlEncoded(): String = URLEncoder.encode(this, Charsets.UTF_8.name()) public fun String.urlEncoded(): String = URLEncoder.encode(this, Charsets.UTF_8.name())
fun String.urlDecode(): String = URLDecoder.decode(this, Charsets.UTF_8.name()) public fun String.urlDecode(): String = URLDecoder.decode(this, Charsets.UTF_8.name())
fun String.nl2br() = replace("\n", "<br>") public fun String.nl2br() = replace("\n", "<br>")
fun ByteArray.byte2HexFormatted(): String { public fun ByteArray.byte2HexFormatted(): String {
val str = StringBuilder(size * 2) val str = StringBuilder(size * 2)
for (i in indices) { for (i in indices) {
var h = Integer.toHexString(this[i].toInt()) var h = Integer.toHexString(this[i].toInt())
@ -123,14 +123,14 @@ fun ByteArray.byte2HexFormatted(): String {
return str.toString() return str.toString()
} }
fun String.md5(): String { public fun String.md5(): String {
val md = MessageDigest.getInstance("MD5") val md = MessageDigest.getInstance("MD5")
return BigInteger(1, md.digest(toByteArray())) return BigInteger(1, md.digest(toByteArray()))
.toString(16) .toString(16)
.padStart(32, '0') .padStart(32, '0')
} }
fun String.substringBetween(from: String, to: String, fallbackValue: String = this): String { public fun String.substringBetween(from: String, to: String, fallbackValue: String = this): String {
val fromIndex = indexOf(from) val fromIndex = indexOf(from)
if (fromIndex == -1) { if (fromIndex == -1) {
return fallbackValue return fallbackValue
@ -143,7 +143,7 @@ fun String.substringBetween(from: String, to: String, fallbackValue: String = th
} }
} }
fun String.substringBetweenFirst(from: String, to: String): String? { public fun String.substringBetweenFirst(from: String, to: String): String? {
val fromIndex = indexOf(from) val fromIndex = indexOf(from)
if (fromIndex == -1) { if (fromIndex == -1) {
return null return null
@ -156,7 +156,7 @@ fun String.substringBetweenFirst(from: String, to: String): String? {
} }
} }
fun String.substringBetweenLast(from: String, to: String, fallbackValue: String = this): String { public fun String.substringBetweenLast(from: String, to: String, fallbackValue: String = this): String {
val fromIndex = lastIndexOf(from) val fromIndex = lastIndexOf(from)
if (fromIndex == -1) { if (fromIndex == -1) {
return fallbackValue return fallbackValue
@ -169,16 +169,16 @@ fun String.substringBetweenLast(from: String, to: String, fallbackValue: String
} }
} }
fun String.find(regex: Regex) = regex.find(this)?.value public fun String.find(regex: Regex) = regex.find(this)?.value
fun String.removeSuffix(suffix: Char): String { public fun String.removeSuffix(suffix: Char): String {
if (lastOrNull() == suffix) { if (lastOrNull() == suffix) {
return substring(0, length - 1) return substring(0, length - 1)
} }
return this return this
} }
fun String.levenshteinDistance(other: String): Int { public fun String.levenshteinDistance(other: String): Int {
if (this == other) { if (this == other) {
return 0 return 0
} }
@ -219,7 +219,7 @@ fun String.levenshteinDistance(other: String): Int {
/** /**
* @param threshold 0 = exact match * @param threshold 0 = exact match
*/ */
fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float): Boolean { public fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float): Boolean {
if (threshold <= 0f) { if (threshold <= 0f) {
return equals(other, ignoreCase = true) return equals(other, ignoreCase = true)
} }
@ -227,24 +227,7 @@ fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float)
return diff < threshold return diff < threshold
} }
public fun String.isNumeric() = all { c -> c.isDigit() }
inline fun <T> Appendable.appendAll(
items: Iterable<T>,
separator: CharSequence,
transform: (T) -> CharSequence = { it.toString() },
) {
var isFirst = true
for (item in items) {
if (isFirst) {
isFirst = false
} else {
append(separator)
}
append(transform(item))
}
}
fun String.isNumeric() = all { c -> c.isDigit() }
internal fun StringBuilder.removeTrailingZero() { internal fun StringBuilder.removeTrailingZero() {
if (length > 2 && get(length - 1) == '0') { if (length > 2 && get(length - 1) == '0') {

@ -3,7 +3,7 @@ package org.koitharu.kotatsu.parsers.util
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
class SuspendLazy<T>( public class SuspendLazy<T>(
private val initializer: suspend () -> T, private val initializer: suspend () -> T,
) { ) {
@ -11,7 +11,7 @@ class SuspendLazy<T>(
private var cachedValue: Any? = Uninitialized private var cachedValue: Any? = Uninitialized
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
suspend fun get(): T { public suspend fun get(): T {
// fast way // fast way
cachedValue.let { cachedValue.let {
if (it !== Uninitialized) { if (it !== Uninitialized) {
@ -30,10 +30,10 @@ class SuspendLazy<T>(
} }
} }
suspend fun tryGet() = runCatchingCancellable { get() } public suspend fun tryGet(): Result<T> = runCatchingCancellable { get() }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun peek(): T? { public fun peek(): T? {
return cachedValue?.takeUnless { it === Uninitialized } as T? return cachedValue?.takeUnless { it === Uninitialized } as T?
} }

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.util.json package org.koitharu.kotatsu.parsers.util.json
fun String.unescapeJson(): String { public fun String.unescapeJson(): String {
val builder = StringBuilder() val builder = StringBuilder()
var i = 0 var i = 0
while (i < length) { while (i < length) {

@ -130,7 +130,7 @@ fun JSONArray.associateByKey(key: String): Map<String, JSONObject> {
return destination return destination
} }
fun JSONArray?.isNullOrEmpty(): Boolean { public fun JSONArray?.isNullOrEmpty(): Boolean {
contract { contract {
returns(false) implies (this@isNullOrEmpty != null) returns(false) implies (this@isNullOrEmpty != null)
} }

Loading…
Cancel
Save