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
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 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")
}

@ -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)

@ -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)

@ -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)

@ -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)
}

Loading…
Cancel
Save