Improve utilities
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
package org.koitharu.kotatsu.parsers.exception
|
||||
|
||||
class ContentUnavailableException(message: String) : RuntimeException(message)
|
||||
public class ContentUnavailableException(message: String) : RuntimeException(message)
|
||||
|
||||
@@ -2,14 +2,15 @@ package org.koitharu.kotatsu.parsers.exception
|
||||
|
||||
import okio.IOException
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
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")
|
||||
}
|
||||
|
||||
override val message: String
|
||||
get() = messages.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.parsers.exception
|
||||
import org.jsoup.HttpStatusException
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class NotFoundException(
|
||||
public class NotFoundException(
|
||||
message: 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
|
||||
|
||||
class ParseException @InternalParsersApi @JvmOverloads constructor(
|
||||
val shortMessage: String?,
|
||||
val url: String,
|
||||
public class ParseException @InternalParsersApi @JvmOverloads constructor(
|
||||
public val shortMessage: String?,
|
||||
public val url: String,
|
||||
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.temporal.ChronoUnit
|
||||
|
||||
class TooManyRequestExceptions(
|
||||
val url: String,
|
||||
public class TooManyRequestExceptions(
|
||||
public val url: String,
|
||||
retryAfter: Long,
|
||||
) : 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)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun getRetryDelay(): Long {
|
||||
public fun getRetryDelay(): Long {
|
||||
if (retryAt == null) {
|
||||
return -1L
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
enum class ContentRating {
|
||||
public enum class ContentRating {
|
||||
SAFE,
|
||||
SUGGESTIVE,
|
||||
ADULT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
enum class ContentType {
|
||||
public enum class ContentType {
|
||||
|
||||
/**
|
||||
* Standard manga, manhua, webtoons, etc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
enum class Demographic {
|
||||
public enum class Demographic {
|
||||
SHOUNEN,
|
||||
SHOUJO,
|
||||
SEINEN,
|
||||
|
||||
@@ -2,14 +2,14 @@ package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
||||
class Favicon internal constructor(
|
||||
@JvmField val url: String,
|
||||
@JvmField val size: Int,
|
||||
public class Favicon(
|
||||
@JvmField public val url: String,
|
||||
@JvmField public val size: Int,
|
||||
@JvmField internal val rel: String?,
|
||||
) : Comparable<Favicon> {
|
||||
|
||||
@JvmField
|
||||
val type: String = url.toHttpUrl().pathSegments.last()
|
||||
public val type: String = url.toHttpUrl().pathSegments.last()
|
||||
.substringAfterLast('.', "").lowercase()
|
||||
|
||||
override fun compareTo(other: Favicon): Int {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
class Favicons internal constructor(
|
||||
public class Favicons(
|
||||
favicons: Collection<Favicon>,
|
||||
@JvmField val referer: String?,
|
||||
@JvmField public val referer: String?,
|
||||
) : Collection<Favicon> {
|
||||
|
||||
private val icons = favicons.sortedDescending()
|
||||
@@ -18,7 +18,7 @@ class Favicons internal constructor(
|
||||
|
||||
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 },
|
||||
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
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun find(size: Int, types: Set<String>? = null): Favicon? {
|
||||
public fun find(size: Int, types: Set<String>? = null): Favicon? {
|
||||
if (icons.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
@@ -48,11 +48,12 @@ class Favicons internal constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
public companion object {
|
||||
|
||||
@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,
|
||||
) {
|
||||
|
||||
public fun isEmpty(): Boolean = tags.isEmpty() &&
|
||||
private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() &&
|
||||
tagsExclude.isEmpty() &&
|
||||
locale == null &&
|
||||
originalLocale == null &&
|
||||
states.isEmpty() &&
|
||||
contentRating.isEmpty() &&
|
||||
query == null &&
|
||||
year == YEAR_UNKNOWN &&
|
||||
yearFrom == YEAR_UNKNOWN &&
|
||||
yearTo == YEAR_UNKNOWN &&
|
||||
types.isEmpty() &&
|
||||
demographics.isEmpty()
|
||||
|
||||
public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty()
|
||||
|
||||
public fun isNotEmpty(): Boolean = !isEmpty()
|
||||
|
||||
public fun hasNonSearchOptions(): Boolean = !isNonSearchOptionsEmpty()
|
||||
|
||||
public companion object {
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -2,23 +2,23 @@ package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
|
||||
class MangaPage(
|
||||
public class MangaPage(
|
||||
/**
|
||||
* Unique identifier for manga
|
||||
*/
|
||||
@JvmField val id: Long,
|
||||
@JvmField public val id: Long,
|
||||
/**
|
||||
* Relative url to page (**without** a domain) or any other uri.
|
||||
* Used principally in parsers.
|
||||
* May contain link to image or html page.
|
||||
* @see MangaParser.getPageUrl
|
||||
*/
|
||||
@JvmField val url: String,
|
||||
@JvmField public val url: String,
|
||||
/**
|
||||
* Absolute url of the small page image if exists, null otherwise
|
||||
*/
|
||||
@JvmField val preview: String?,
|
||||
@JvmField val source: MangaSource,
|
||||
@JvmField public val preview: String?,
|
||||
@JvmField public val source: MangaSource,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
enum class SortOrder {
|
||||
public enum class SortOrder {
|
||||
UPDATED,
|
||||
UPDATED_ASC,
|
||||
POPULARITY,
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class OkHttpWebClient(
|
||||
public class OkHttpWebClient(
|
||||
private val httpClient: OkHttpClient,
|
||||
private val mangaSource: MangaSource,
|
||||
) : WebClient {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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 org.json.JSONObject
|
||||
|
||||
interface WebClient {
|
||||
public interface WebClient {
|
||||
|
||||
/**
|
||||
* Do a GET http request to specific 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
|
||||
* @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
|
||||
* @param url
|
||||
* @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
|
||||
* @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
|
||||
* @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
|
||||
* @param url
|
||||
* @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
|
||||
* @param url
|
||||
* @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
|
||||
@@ -61,21 +62,21 @@ interface WebClient {
|
||||
* @param form payload as key=>value map
|
||||
* @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
|
||||
* @param url
|
||||
* @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
|
||||
* @param url
|
||||
* @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
|
||||
@@ -83,21 +84,21 @@ interface WebClient {
|
||||
* @param payload payload as `key=value` string with `&` separator
|
||||
* @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
|
||||
* @param url
|
||||
* @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
|
||||
* @param url
|
||||
* @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
|
||||
@@ -105,12 +106,12 @@ interface WebClient {
|
||||
* @param body
|
||||
* @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
|
||||
* @param endpoint an url
|
||||
* @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=")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
appendAll(filter.tags, ",") { it.key }
|
||||
filter.tags.joinTo(this, ",") { it.key }
|
||||
}
|
||||
|
||||
append("|")
|
||||
if (filter.tagsExclude.isNotEmpty()) {
|
||||
appendAll(filter.tagsExclude, ",") { it.key }
|
||||
filter.tagsExclude.joinTo(this, ",") { it.key }
|
||||
}
|
||||
|
||||
if (filter.contentRating.isNotEmpty()) {
|
||||
|
||||
@@ -79,14 +79,10 @@ internal class MangaPark(context: MangaLoaderContext) :
|
||||
}
|
||||
|
||||
append("&genres=")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
appendAll(filter.tags, ",") { it.key }
|
||||
}
|
||||
filter.tags.joinTo(this, ",") { it.key }
|
||||
|
||||
append("|")
|
||||
if (filter.tagsExclude.isNotEmpty()) {
|
||||
appendAll(filter.tagsExclude, ",") { it.key }
|
||||
}
|
||||
filter.tagsExclude.joinTo(this, ",") { it.key }
|
||||
|
||||
if (filter.contentRating.isNotEmpty()) {
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
|
||||
@@ -61,7 +61,7 @@ internal abstract class IkenParser(
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("&genreIds=")
|
||||
appendAll(filter.tags, ",") { it.key }
|
||||
filter.tags.joinTo(this, ",") { it.key }
|
||||
}
|
||||
|
||||
append("&seriesType=&seriesStatus=")
|
||||
|
||||
@@ -56,7 +56,7 @@ internal class DesuMeParser(context: MangaLoaderContext) : PagedMangaParser(cont
|
||||
append(page)
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("&genres=")
|
||||
appendAll(filter.tags, ",") { it.key }
|
||||
filter.tags.joinTo(this, ",") { it.key }
|
||||
}
|
||||
if (!filter.query.isNullOrEmpty()) {
|
||||
append("&search=")
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.collection.ArrayMap
|
||||
import androidx.collection.ArraySet
|
||||
import java.util.*
|
||||
|
||||
fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
|
||||
public fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
|
||||
clear()
|
||||
addAll(subject)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ fun <T> List<T>.medianOrNull(): T? = when {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ inline fun <T, R> Collection<T>.mapNotNullToSet(transform: (T) -> R?): Set<R> {
|
||||
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))
|
||||
}
|
||||
|
||||
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) {
|
||||
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ import okhttp3.HttpUrl
|
||||
|
||||
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
|
||||
saveFromResponse(
|
||||
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
|
||||
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()
|
||||
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()
|
||||
.scheme(SCHEME_HTTPS)
|
||||
.host(oldDomain)
|
||||
@@ -47,4 +47,4 @@ private fun safeUrlOf(domain: String): HttpUrl? = try {
|
||||
.build()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.Throws
|
||||
|
||||
private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING"
|
||||
private const val AES = "AES"
|
||||
@@ -14,7 +16,8 @@ private const val KDF_DIGEST = "MD5"
|
||||
/**
|
||||
* Conforming with CryptoJS AES method
|
||||
*/
|
||||
class CryptoAES(
|
||||
@InternalParsersApi
|
||||
public class CryptoAES(
|
||||
private val context: MangaLoaderContext,
|
||||
) {
|
||||
|
||||
@@ -27,7 +30,7 @@ class CryptoAES(
|
||||
* @param password passphrase
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun decrypt(cipherText: String, password: String): String {
|
||||
public fun decrypt(cipherText: String, password: String): String {
|
||||
val ctBytes = context.decodeBase64(cipherText)
|
||||
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
||||
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
||||
@@ -48,7 +51,7 @@ class CryptoAES(
|
||||
* @param ivBytes iv as a bytearray
|
||||
*/
|
||||
@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)
|
||||
return decryptAES(cipherTextBytes, keyBytes, ivBytes)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import org.jsoup.select.QueryParser
|
||||
import org.jsoup.select.Selector
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
|
||||
val Element.host: String?
|
||||
@@ -154,8 +155,22 @@ fun Element.attrOrNull(vararg names: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
@InternalParsersApi
|
||||
@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) {
|
||||
val value = attrAsAbsoluteUrlOrNull(name)
|
||||
if (value != null) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.model.*
|
||||
* @see [MangaPage.id]
|
||||
*/
|
||||
@InternalParsersApi
|
||||
fun MangaParser.generateUid(url: String): Long {
|
||||
public fun MangaParser.generateUid(url: String): Long {
|
||||
var h = 1125899906842597L
|
||||
source.name.forEach { c ->
|
||||
h = 31 * h + c.code
|
||||
@@ -36,7 +36,7 @@ fun MangaParser.generateUid(url: String): Long {
|
||||
* @see [MangaPage.id]
|
||||
*/
|
||||
@InternalParsersApi
|
||||
fun MangaParser.generateUid(id: Long): Long {
|
||||
public fun MangaParser.generateUid(id: Long): Long {
|
||||
var h = 1125899906842597L
|
||||
source.name.forEach { c ->
|
||||
h = 31 * h + c.code
|
||||
@@ -46,32 +46,32 @@ fun MangaParser.generateUid(id: Long): Long {
|
||||
}
|
||||
|
||||
@InternalParsersApi
|
||||
fun Element.parseFailed(message: String? = null): Nothing {
|
||||
public fun Element.parseFailed(message: String? = null): Nothing {
|
||||
throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null)
|
||||
}
|
||||
|
||||
@InternalParsersApi
|
||||
fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? = oneOrThrowIfMany(
|
||||
public fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? = oneOrThrowIfMany(
|
||||
ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED,
|
||||
)
|
||||
|
||||
@InternalParsersApi
|
||||
fun Set<MangaState>?.oneOrThrowIfMany(): MangaState? = oneOrThrowIfMany(
|
||||
public fun Set<MangaState>?.oneOrThrowIfMany(): MangaState? = oneOrThrowIfMany(
|
||||
ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED,
|
||||
)
|
||||
|
||||
@InternalParsersApi
|
||||
fun Set<ContentType>?.oneOrThrowIfMany(): ContentType? = oneOrThrowIfMany(
|
||||
public fun Set<ContentType>?.oneOrThrowIfMany(): ContentType? = oneOrThrowIfMany(
|
||||
ErrorMessages.FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED,
|
||||
)
|
||||
|
||||
@InternalParsersApi
|
||||
fun Set<Demographic>?.oneOrThrowIfMany(): Demographic? = oneOrThrowIfMany(
|
||||
public fun Set<Demographic>?.oneOrThrowIfMany(): Demographic? = oneOrThrowIfMany(
|
||||
ErrorMessages.FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED,
|
||||
)
|
||||
|
||||
@InternalParsersApi
|
||||
fun Set<ContentRating>?.oneOrThrowIfMany(): ContentRating? = oneOrThrowIfMany(
|
||||
public fun Set<ContentRating>?.oneOrThrowIfMany(): ContentRating? = oneOrThrowIfMany(
|
||||
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)
|
||||
}
|
||||
|
||||
val MangaParser.domain: String
|
||||
public val MangaParser.domain: String
|
||||
get() {
|
||||
return config[configKeyDomain]
|
||||
}
|
||||
|
||||
fun MangaParser.getDomain(subdomain: String): String {
|
||||
@InternalParsersApi
|
||||
public fun MangaParser.getDomain(subdomain: String): String {
|
||||
val domain = domain
|
||||
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()
|
||||
.scheme("https")
|
||||
.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 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 symbols = formatter.decimalFormatSymbols
|
||||
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()
|
||||
return if ((this - intValue.toFloat()).absoluteValue <= 0.00001) {
|
||||
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
|
||||
return if (mod == 0) {
|
||||
this
|
||||
@@ -47,7 +47,7 @@ infix fun Int.upBy(step: Int): Int {
|
||||
}
|
||||
}
|
||||
|
||||
fun Number.formatSimple(): String {
|
||||
public fun Number.formatSimple(): String {
|
||||
val raw = toString()
|
||||
return if (raw.endsWith(".0") || raw.endsWith(",0")) {
|
||||
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()
|
||||
} else {
|
||||
this
|
||||
|
||||
@@ -7,19 +7,19 @@ import okhttp3.Call
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Response
|
||||
|
||||
suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
|
||||
public suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
|
||||
val callback = ContinuationCallCallback(this, continuation)
|
||||
enqueue(callback)
|
||||
continuation.invokeOnCancellation(callback)
|
||||
}
|
||||
|
||||
val Response.mimeType: String?
|
||||
public val Response.mimeType: String?
|
||||
get() = header("content-type")?.takeUnless { it.isEmpty() }
|
||||
|
||||
val Response.contentDisposition: String?
|
||||
public val Response.contentDisposition: String?
|
||||
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) {
|
||||
if (replaceExisting || this[name] == null) {
|
||||
this[name] = value
|
||||
@@ -28,11 +28,11 @@ fun Headers.Builder.mergeWith(other: Headers, replaceExisting: Boolean): Headers
|
||||
return this
|
||||
}
|
||||
|
||||
fun Response.copy() = newBuilder()
|
||||
public fun Response.copy(): Response = newBuilder()
|
||||
.body(peekBody(Long.MAX_VALUE))
|
||||
.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)
|
||||
} else {
|
||||
header(name, value)
|
||||
|
||||
@@ -3,12 +3,12 @@ package org.koitharu.kotatsu.parsers.util
|
||||
import androidx.collection.SparseArrayCompat
|
||||
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>()
|
||||
|
||||
fun getPage(offset: Int): Int {
|
||||
internal fun getPage(offset: Int): Int {
|
||||
if (offset == 0) { // just an optimization
|
||||
return firstPage
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class Paginator(private val initialPageSize: Int) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ package org.koitharu.kotatsu.parsers.util
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import java.text.DateFormat
|
||||
|
||||
/**
|
||||
@@ -16,7 +18,8 @@ import java.text.DateFormat
|
||||
* @see [parseJson]
|
||||
* @see [parseJsonArray]
|
||||
*/
|
||||
fun Response.parseHtml(): Document = try {
|
||||
@Blocking
|
||||
public fun Response.parseHtml(): Document = try {
|
||||
val body = requireBody()
|
||||
val charset = body.contentType()?.charset()?.name()
|
||||
Jsoup.parse(body.byteStream(), charset, request.url.toString())
|
||||
@@ -29,7 +32,7 @@ fun Response.parseHtml(): Document = try {
|
||||
* @see [parseJsonArray]
|
||||
* @see [parseHtml]
|
||||
*/
|
||||
fun Response.parseJson(): JSONObject = try {
|
||||
public fun Response.parseJson(): JSONObject = try {
|
||||
JSONObject(requireBody().string())
|
||||
} finally {
|
||||
closeQuietly()
|
||||
@@ -40,19 +43,19 @@ fun Response.parseJson(): JSONObject = try {
|
||||
* @see [parseJson]
|
||||
* @see [parseHtml]
|
||||
*/
|
||||
fun Response.parseJsonArray(): JSONArray = try {
|
||||
public fun Response.parseJsonArray(): JSONArray = try {
|
||||
JSONArray(requireBody().string())
|
||||
} finally {
|
||||
closeQuietly()
|
||||
}
|
||||
|
||||
fun Response.parseRaw(): String = try {
|
||||
public fun Response.parseRaw(): String = try {
|
||||
requireBody().string()
|
||||
} finally {
|
||||
closeQuietly()
|
||||
}
|
||||
|
||||
fun Response.parseBytes(): ByteArray = try {
|
||||
public fun Response.parseBytes(): ByteArray = try {
|
||||
requireBody().bytes()
|
||||
} finally {
|
||||
closeQuietly()
|
||||
@@ -62,7 +65,7 @@ fun Response.parseBytes(): ByteArray = try {
|
||||
* Convert url to relative if it is on [domain]
|
||||
* @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("/")) {
|
||||
return this
|
||||
}
|
||||
@@ -73,13 +76,13 @@ fun String.toRelativeUrl(domain: String): String {
|
||||
* Convert url to absolute with specified domain
|
||||
* @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://$domain$this"
|
||||
else -> this
|
||||
}
|
||||
|
||||
fun concatUrl(host: String, path: String): String {
|
||||
public fun concatUrl(host: String, path: String): String {
|
||||
val hostWithSlash = host.endsWith('/')
|
||||
val pathWithSlash = path.startsWith('/')
|
||||
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" }
|
||||
0L
|
||||
} 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.SortOrder
|
||||
|
||||
class RelatedMangaFinder(
|
||||
public class RelatedMangaFinder(
|
||||
private val parsers: Collection<MangaParser>,
|
||||
) {
|
||||
|
||||
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 ->
|
||||
findRelatedImpl(this, parser, seed)
|
||||
} ?: parsers.map { parser ->
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.util
|
||||
|
||||
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 {
|
||||
Result.success(block())
|
||||
} 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()) {
|
||||
null -> this
|
||||
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()) {
|
||||
null -> 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
|
||||
*/
|
||||
class SoftSuspendLazy<T : Any>(
|
||||
public class SoftSuspendLazy<T : Any>(
|
||||
private val initializer: suspend () -> T,
|
||||
) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
private var cachedValue: SoftReference<T>? = null
|
||||
|
||||
suspend fun get(): T {
|
||||
public suspend fun get(): T {
|
||||
// fast way
|
||||
cachedValue?.get()?.let {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
fun String.removeSurrounding(vararg chars: Char): String {
|
||||
public fun String.removeSurrounding(vararg chars: Char): String {
|
||||
if (isEmpty()) {
|
||||
return this
|
||||
}
|
||||
@@ -24,7 +24,7 @@ fun String.removeSurrounding(vararg chars: Char): String {
|
||||
return this
|
||||
}
|
||||
|
||||
fun String.toCamelCase(): String {
|
||||
public fun String.toCamelCase(): String {
|
||||
if (isEmpty()) {
|
||||
return this
|
||||
}
|
||||
@@ -43,15 +43,15 @@ fun String.toCamelCase(): String {
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
fun String.toTitleCase(): String {
|
||||
public fun String.toTitleCase(): String {
|
||||
return replaceFirstChar { x -> x.uppercase() }
|
||||
}
|
||||
|
||||
fun String.toTitleCase(locale: Locale): String {
|
||||
public fun String.toTitleCase(locale: Locale): String {
|
||||
return replaceFirstChar { x -> x.uppercase(locale) }
|
||||
}
|
||||
|
||||
fun String.transliterate(skipMissing: Boolean): String {
|
||||
public fun String.transliterate(skipMissing: Boolean): String {
|
||||
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("\\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
|
||||
} else this
|
||||
|
||||
fun String.splitTwoParts(delimiter: Char): Pair<String, String>? {
|
||||
public fun String.splitTwoParts(delimiter: Char): Pair<String, String>? {
|
||||
val indices = MutableIntList(4)
|
||||
for ((i, c) in this.withIndex()) {
|
||||
if (c == delimiter) {
|
||||
@@ -98,13 +98,13 @@ fun String.splitTwoParts(delimiter: Char): Pair<String, String>? {
|
||||
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)
|
||||
for (i in indices) {
|
||||
var h = Integer.toHexString(this[i].toInt())
|
||||
@@ -123,14 +123,14 @@ fun ByteArray.byte2HexFormatted(): String {
|
||||
return str.toString()
|
||||
}
|
||||
|
||||
fun String.md5(): String {
|
||||
public fun String.md5(): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
return BigInteger(1, md.digest(toByteArray()))
|
||||
.toString(16)
|
||||
.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)
|
||||
if (fromIndex == -1) {
|
||||
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)
|
||||
if (fromIndex == -1) {
|
||||
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)
|
||||
if (fromIndex == -1) {
|
||||
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) {
|
||||
return substring(0, length - 1)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun String.levenshteinDistance(other: String): Int {
|
||||
public fun String.levenshteinDistance(other: String): Int {
|
||||
if (this == other) {
|
||||
return 0
|
||||
}
|
||||
@@ -219,7 +219,7 @@ fun String.levenshteinDistance(other: String): Int {
|
||||
/**
|
||||
* @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) {
|
||||
return equals(other, ignoreCase = true)
|
||||
}
|
||||
@@ -227,24 +227,7 @@ fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float)
|
||||
return diff < threshold
|
||||
}
|
||||
|
||||
|
||||
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() }
|
||||
public fun String.isNumeric() = all { c -> c.isDigit() }
|
||||
|
||||
internal fun StringBuilder.removeTrailingZero() {
|
||||
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.withLock
|
||||
|
||||
class SuspendLazy<T>(
|
||||
public class SuspendLazy<T>(
|
||||
private val initializer: suspend () -> T,
|
||||
) {
|
||||
|
||||
@@ -11,7 +11,7 @@ class SuspendLazy<T>(
|
||||
private var cachedValue: Any? = Uninitialized
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun get(): T {
|
||||
public suspend fun get(): T {
|
||||
// fast way
|
||||
cachedValue.let {
|
||||
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")
|
||||
fun peek(): T? {
|
||||
public fun peek(): T? {
|
||||
return cachedValue?.takeUnless { it === Uninitialized } as T?
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.util.json
|
||||
|
||||
fun String.unescapeJson(): String {
|
||||
public fun String.unescapeJson(): String {
|
||||
val builder = StringBuilder()
|
||||
var i = 0
|
||||
while (i < length) {
|
||||
|
||||
@@ -130,7 +130,7 @@ fun JSONArray.associateByKey(key: String): Map<String, JSONObject> {
|
||||
return destination
|
||||
}
|
||||
|
||||
fun JSONArray?.isNullOrEmpty(): Boolean {
|
||||
public fun JSONArray?.isNullOrEmpty(): Boolean {
|
||||
contract {
|
||||
returns(false) implies (this@isNullOrEmpty != null)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user