package org.koitharu.kotatsu.parsers import androidx.annotation.CallSuper import androidx.annotation.VisibleForTesting import okhttp3.Headers import okhttp3.HttpUrl import org.jsoup.nodes.Element import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.FaviconParser import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl import java.util.* abstract class MangaParser @InternalParsersApi constructor(val source: MangaSource) { protected abstract val context: MangaLoaderContext /** * Supported [SortOrder] variants. Must not be empty. * * For better performance use [EnumSet] for more than one item. */ abstract val sortOrders: Set val config by lazy { context.getConfig(source) } open val sourceLocale: Locale? get() = source.locale?.let { Locale(it) } /** * Provide default domain and available alternatives, if any. * * Never hardcode domain in requests, use [getDomain] instead. */ protected abstract val configKeyDomain: ConfigKey.Domain @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) internal open val headers: Headers? = null /** * Used as fallback if value of `sortOrder` passed to [getList] is null */ protected open val defaultSortOrder: SortOrder get() { val supported = sortOrders return SortOrder.values().first { it in supported } } /** * Parse list of manga by specified criteria * * @param offset starting from 0 and used for pagination. * Note than passed value may not be divisible by internal page size, so you should adjust it manually. * @param query search query, may be null or empty if no search needed * @param tags genres for filtering, values from [getTags] and [Manga.tags]. May be null or empty * @param sortOrder one of [sortOrders] or null for default value */ @JvmSynthetic @InternalParsersApi abstract suspend fun getList( offset: Int, query: String?, tags: Set?, sortOrder: SortOrder, ): List /** * Parse list of manga with search by text query * * @param offset starting from 0 and used for pagination. * @param query search query */ open suspend fun getList(offset: Int, query: String): List { return getList(offset, query, null, defaultSortOrder) } /** * Parse list of manga by specified criteria * * @param offset starting from 0 and used for pagination. * Note than passed value may not be divisible by internal page size, so you should adjust it manually. * @param tags genres for filtering, values from [getTags] and [Manga.tags]. May be null or empty * @param sortOrder one of [sortOrders] or null for default value */ open suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { return getList(offset, null, tags, sortOrder ?: defaultSortOrder) } /** * Parse details for [Manga]: chapters list, description, large cover, etc. * Must return the same manga, may change any fields excepts id, url and source * @see Manga.copy */ abstract suspend fun getDetails(manga: Manga): Manga /** * Parse pages list for specified chapter. * @see MangaPage for details */ abstract suspend fun getPages(chapter: MangaChapter): List /** * Fetch direct link to the page image. */ open suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(getDomain()) /** * Fetch available tags (genres) for source */ abstract suspend fun getTags(): Set /** * Returns direct link to the website favicon */ @Deprecated( message = "Use parseFavicons() to get multiple favicons with different size", replaceWith = ReplaceWith("parseFavicons()"), ) open fun getFaviconUrl() = "https://${getDomain()}/favicon.ico" /** * Parse favicons from the main page of the source`s website */ open suspend fun getFavicons(): Favicons { return FaviconParser(context, getDomain(), headers).parseFavicons() } @CallSuper open fun onCreateConfig(keys: MutableCollection>) { keys.add(configKeyDomain) } /* Utils */ fun getDomain(): String { return config[configKeyDomain] } fun getDomain(subdomain: String): String { val domain = getDomain() return subdomain + "." + domain.removePrefix("www.") } fun urlBuilder(): HttpUrl.Builder { return HttpUrl.Builder() .scheme("https") .host(getDomain()) } /** * Create a unique id for [Manga]/[MangaChapter]/[MangaPage]. * @param url must be relative url, without a domain * @see [Manga.id] * @see [MangaChapter.id] * @see [MangaPage.id] */ @InternalParsersApi protected fun generateUid(url: String): Long { var h = 1125899906842597L source.name.forEach { c -> h = 31 * h + c.code } url.forEach { c -> h = 31 * h + c.code } return h } /** * Create a unique id for [Manga]/[MangaChapter]/[MangaPage]. * @param id an internal identifier * @see [Manga.id] * @see [MangaChapter.id] * @see [MangaPage.id] */ @InternalParsersApi protected fun generateUid(id: Long): Long { var h = 1125899906842597L source.name.forEach { c -> h = 31 * h + c.code } h = 31 * h + id return h } @InternalParsersApi protected fun Element.parseFailed(message: String? = null): Nothing { throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null) } @InternalParsersApi protected fun Set?.oneOrThrowIfMany(): MangaTag? { return when { isNullOrEmpty() -> null size == 1 -> first() else -> throw IllegalArgumentException("Multiple genres are not supported by this source") } } }