Merge pull request #1063 from KotatsuApp/feature/advanced_filter

Advanced filter
master
Koitharu 2 years ago committed by GitHub
commit 5269659c73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

1
.gitignore vendored

@ -82,3 +82,4 @@ bin/
src/test/resources/cookies.txt
local.properties
.kotlin/

@ -64,6 +64,8 @@ All functions in `MangaParser` class are documented. Pay attention to some pecul
find issues during unit testing.
- If your source website (or it's api) uses pages for pagination instead of offset you should extend `PagedMangaParser`
instead of `MangaParser`.
- If your source website (or it's api) do not provide pagination (has only one page of content) you should extend
`SinglePageMangaParser` instead of `MangaParser` nor `PagedMangaParser.
- Your parser may also implement the `Interceptor` interface for additional manipulation of all network requests and/or
responses, including image loading.

@ -2,8 +2,8 @@ import tasks.ReportGenerateTask
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm' version '2.0.10-RC'
id 'com.google.devtools.ksp' version '2.0.10-RC-1.0.23'
id 'org.jetbrains.kotlin.jvm' version '2.0.20'
id 'com.google.devtools.ksp' version '2.0.20-1.0.25'
id 'maven-publish'
}
@ -37,6 +37,7 @@ compileTestKotlin {
kotlin {
jvmToolchain(8)
explicitApi = 'warning'
sourceSets {
main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin'
}
@ -53,7 +54,7 @@ afterEvaluate {
}
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okio:okio:3.9.0'
api 'org.jsoup:jsoup:1.18.1'

@ -1,5 +1,5 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.0.10-RC'
id 'org.jetbrains.kotlin.jvm' version '2.0.20'
}
repositories {
@ -14,5 +14,5 @@ dependencies {
implementation gradleApi()
implementation 'org.simpleframework:simple-xml:2.7.1'
implementation 'com.soywiz.korlibs.korte:korte-jvm:4.0.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0'
}

@ -7,5 +7,5 @@ kotlin {
}
dependencies {
implementation 'com.google.devtools.ksp:symbol-processing-api:2.0.10-RC-1.0.23'
implementation 'com.google.devtools.ksp:symbol-processing-api:2.0.20-1.0.25'
}

@ -80,11 +80,11 @@ class ParserProcessor(
"""
package org.koitharu.kotatsu.parsers.model
enum class MangaParserSource(
val title: String,
val locale: String,
val contentType: ContentType,
val isBroken: Boolean,
public enum class MangaParserSource(
public val title: String,
public val locale: String,
public val contentType: ContentType,
public val isBroken: Boolean,
): MangaSource {
""".trimIndent(),

@ -5,4 +5,4 @@ package org.koitharu.kotatsu.parsers
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Broken
internal annotation class Broken

@ -1,14 +1,18 @@
package org.koitharu.kotatsu.parsers
object ErrorMessages {
public object ErrorMessages {
const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED = "Multiple states are not supported by this source"
const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED = "Multiple genres are not supported by this source"
const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED =
"Multiple Content Rating are not supported by this source"
const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED =
public const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED: String = "Multiple states are not supported by this source"
public const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED: String = "Multiple genres are not supported by this source"
public const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED: String =
"Multiple Content ratings are not supported by this source"
public const val FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED: String =
"Multiple Content types are not supported by this source"
public const val FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED: String =
"Multiple Demographics are not supported by this source"
public const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED: String =
"Filtering by both genres and locale is not supported by this source"
const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED =
public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String =
"Filtering by both genres and states is not supported by this source"
const val SEARCH_NOT_SUPPORTED = "Search is not supported by this source"
public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source"
}

@ -11,4 +11,4 @@ package org.koitharu.kotatsu.parsers
@SinceKotlin("1.3")
@RequiresOptIn
@MustBeDocumented
annotation class InternalParsersApi
public annotation class InternalParsersApi

@ -9,30 +9,30 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.*
abstract class MangaLoaderContext {
public abstract class MangaLoaderContext {
abstract val httpClient: OkHttpClient
public abstract val httpClient: OkHttpClient
abstract val cookieJar: CookieJar
public abstract val cookieJar: CookieJar
fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this)
public fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this)
open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
public open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
public open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
public open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
/**
* Execute JavaScript code and return result
* @param script JavaScript source code
* @return execution result as string, may be null
*/
abstract suspend fun evaluateJs(script: String): String?
public abstract suspend fun evaluateJs(script: String): String?
abstract fun getConfig(source: MangaSource): MangaSourceConfig
public abstract fun getConfig(source: MangaSource): MangaSourceConfig
abstract fun getDefaultUserAgent(): String
public abstract fun getDefaultUserAgent(): String
/**
* Helper function to be used in an interceptor
@ -40,7 +40,7 @@ abstract class MangaLoaderContext {
* @param response Image response
* @param redraw lambda function to implement descrambling logic
*/
abstract fun redrawImageResponse(
public abstract fun redrawImageResponse(
response: Response,
redraw: (image: Bitmap) -> Bitmap,
): Response
@ -48,7 +48,7 @@ abstract class MangaLoaderContext {
/**
* create a new empty Bitmap with given dimensions
*/
abstract fun createBitmap(
public abstract fun createBitmap(
width: Int,
height: Int,
): Bitmap

@ -1,9 +1,9 @@
package org.koitharu.kotatsu.parsers
import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
import org.koitharu.kotatsu.parsers.network.WebClient
@ -13,9 +13,9 @@ import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import java.util.*
abstract class MangaParser @InternalParsersApi constructor(
@property:InternalParsersApi val context: MangaLoaderContext,
val source: MangaParserSource,
public abstract class MangaParser @InternalParsersApi constructor(
@property:InternalParsersApi public val context: MangaLoaderContext,
public val source: MangaParserSource,
) {
/**
@ -23,48 +23,16 @@ abstract class MangaParser @InternalParsersApi constructor(
*
* For better performance use [EnumSet] for more than one item.
*/
abstract val availableSortOrders: Set<SortOrder>
public abstract val availableSortOrders: Set<SortOrder>
/**
* Supported [MangaState] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
open val availableStates: Set<MangaState>
get() = emptySet()
open val availableContentRating: Set<ContentRating>
get() = emptySet()
public abstract val filterCapabilities: MangaListFilterCapabilities
/**
* Whether parser supports filtering by more than one tag
*/
open val isMultipleTagsSupported: Boolean = true
/**
* Whether parser supports tagsExclude field in filter
*/
open val isTagsExclusionSupported: Boolean = false
public val config: MangaSourceConfig by lazy { context.getConfig(source) }
/**
* Whether parser supports searching by string query using [MangaListFilter.Search]
*/
open val isSearchSupported: Boolean = true
@Deprecated(
message = "Use availableSortOrders instead",
replaceWith = ReplaceWith("availableSortOrders"),
)
open val sortOrders: Set<SortOrder>
get() = availableSortOrders
val config by lazy { context.getConfig(source) }
open val sourceLocale: Locale
public open val sourceLocale: Locale
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
val isNsfwSource = source.contentType == ContentType.HENTAI
protected val isNsfwSource: Boolean = source.contentType == ContentType.HENTAI
/**
* Provide default domain and available alternatives, if any.
@ -72,19 +40,18 @@ abstract class MangaParser @InternalParsersApi constructor(
* Never hardcode domain in requests, use [domain] instead.
*/
@InternalParsersApi
abstract val configKeyDomain: ConfigKey.Domain
public abstract val configKeyDomain: ConfigKey.Domain
protected open val userAgentKey = ConfigKey.UserAgent(context.getDefaultUserAgent())
protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent())
open fun getRequestHeaders(): Headers = Headers.Builder()
public open fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", config[userAgentKey])
.build()
/**
* Used as fallback if value of `sortOrder` passed to [getList] is null
* Used as fallback if value of `order` passed to [getList] is null
*/
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
open val defaultSortOrder: SortOrder
public open val defaultSortOrder: SortOrder
get() {
val supported = availableSortOrders
return SortOrder.entries.first { it in supported }
@ -98,157 +65,44 @@ abstract class MangaParser @InternalParsersApi constructor(
*
* @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 [getAvailableTags] and [Manga.tags]. May be null or empty
* @param sortOrder one of [availableSortOrders] or null for default value
*/
@JvmSynthetic
@InternalParsersApi
@Deprecated(
"Use getList with filter instead",
replaceWith = ReplaceWith("getList(offset, filter)"),
)
open suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getList(offset, filter) instead")
/**
* Parse list of manga with search by text query
*
* @param offset starting from 0 and used for pagination.
* @param query search query
*/
@Deprecated(
"Use getList with filter instead",
ReplaceWith(
"getList(offset, MangaListFilter.Search(query))",
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
),
)
open suspend fun getList(offset: Int, query: String): List<Manga> {
return getList(offset, MangaListFilter.Search(query))
}
/**
* 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 [getAvailableTags] and [Manga.tags]. May be null or empty
* @param sortOrder one of [availableSortOrders] or null for default value
* @param order one of [availableSortOrders] or [defaultSortOrder] for default value
* @param filter is a set of filter rules
*/
@Deprecated(
"Use getList with filter instead",
ReplaceWith(
"getList(offset, MangaListFilter.Advanced(sortOrder, tags, null, emptySet()))",
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
),
)
open suspend fun getList(
offset: Int,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder?,
): List<Manga> {
return getList(
offset,
MangaListFilter.Advanced(
sortOrder = sortOrder ?: defaultSortOrder,
tags = tags.orEmpty(),
tagsExclude = tagsExclude.orEmpty(),
locale = null,
states = emptySet(),
contentRating = emptySet(),
),
)
}
@Suppress("DEPRECATION")
open suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> getList(
offset = offset,
query = null,
tags = filter.tags,
tagsExclude = filter.tagsExclude,
sortOrder = filter.sortOrder,
)
is MangaListFilter.Search -> getList(
offset = offset,
query = filter.query,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
null -> getList(
offset = offset,
query = null,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
}
}
public abstract suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
/**
* 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
public abstract suspend fun getDetails(manga: Manga): Manga
/**
* Parse pages list for specified chapter.
* @see MangaPage for details
*/
abstract suspend fun getPages(chapter: MangaChapter): List<MangaPage>
public abstract suspend fun getPages(chapter: MangaChapter): List<MangaPage>
/**
* Fetch direct link to the page image.
*/
open suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
/**
* Fetch available tags (genres) for source
*/
abstract suspend fun getAvailableTags(): Set<MangaTag>
internal open suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
/**
* Fetch available locales for multilingual sources
*/
open suspend fun getAvailableLocales(): Set<Locale> = emptySet()
@Deprecated(
message = "Use getAvailableTags instead",
replaceWith = ReplaceWith("getAvailableTags()"),
)
suspend fun getTags(): Set<MangaTag> = getAvailableTags()
public abstract suspend fun getFilterOptions(): MangaListFilterOptions
/**
* Parse favicons from the main page of the source`s website
*/
open suspend fun getFavicons(): Favicons {
public open suspend fun getFavicons(): Favicons {
return FaviconParser(webClient, domain).parseFavicons()
}
@CallSuper
open fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
public open fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
keys.add(configKeyDomain)
}
open suspend fun getRelatedManga(seed: Manga): List<Manga> {
public open suspend fun getRelatedManga(seed: Manga): List<Manga> {
return RelatedMangaFinder(listOf(this)).invoke(seed)
}
protected fun getParser(source: MangaParserSource) = if (this.source == source) {
this
} else {
context.newParserInstance(source)
}
}

@ -6,19 +6,19 @@ import org.koitharu.kotatsu.parsers.exception.ParseException
/**
* Implement this in your parser for authorization support
*/
interface MangaParserAuthProvider {
public interface MangaParserAuthProvider {
/**
* Return link to the login page, which will be opened in browser.
* Must be an absolute url
*/
val authUrl: String
public val authUrl: String
/**
* Quick check if user is logged in.
* In most case you should check for cookies in [MangaLoaderContext.cookieJar].
*/
val isAuthorized: Boolean
public val isAuthorized: Boolean
/**
* Fetch and return current user`s name or login.
@ -26,5 +26,5 @@ interface MangaParserAuthProvider {
* @throws [AuthRequiredException] if user is not logged in or authorization is expired
* @throws [ParseException] on parsing error
*/
suspend fun getUsername(): String
}
public suspend fun getUsername(): String
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.ContentType
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class MangaSourceParser(
internal annotation class MangaSourceParser(
/**
* Name of manga source. Used as an Enum value, must be UPPER_CASE and unique.
*/

@ -1,11 +1,14 @@
package org.koitharu.kotatsu.parsers
import androidx.annotation.VisibleForTesting
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.Paginator
@InternalParsersApi
abstract class PagedMangaParser(
public abstract class PagedMangaParser(
context: MangaLoaderContext,
source: MangaParserSource,
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField internal val pageSize: Int,
@ -13,77 +16,34 @@ abstract class PagedMangaParser(
) : MangaParser(context, source) {
@JvmField
protected val paginator = Paginator(pageSize)
protected val paginator: Paginator = Paginator(pageSize)
@JvmField
protected val searchPaginator = Paginator(searchPageSize)
protected val searchPaginator: Paginator = Paginator(searchPageSize)
final override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
return getList(
paginator = if (filter is MangaListFilter.Search) {
searchPaginator
} else {
paginator = if (filter.query.isNullOrEmpty()) {
paginator
} else {
searchPaginator
},
offset = offset,
order = order,
filter = filter,
)
}
@InternalParsersApi
@Deprecated("You should use getListPage for PagedMangaParser", level = DeprecationLevel.HIDDEN)
final override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
@Deprecated("")
open suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead")
open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> getListPage(
page = page,
query = null,
tags = filter.tags,
tagsExclude = filter.tagsExclude,
sortOrder = filter.sortOrder,
)
is MangaListFilter.Search -> getListPage(
page = page,
query = filter.query,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
null -> getListPage(
page = page,
query = null,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
}
}
public abstract suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
private suspend fun getList(
paginator: Paginator,
offset: Int,
filter: MangaListFilter?,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> {
val page = paginator.getPage(offset)
val list = getListPage(page, filter)
val list = getListPage(page, order, filter)
paginator.onListReceived(offset, page, list.size)
return list
}

@ -0,0 +1,22 @@
package org.koitharu.kotatsu.parsers
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder
@InternalParsersApi
public abstract class SinglePageMangaParser(
context: MangaLoaderContext,
source: MangaParserSource,
) : MangaParser(context, source) {
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
if (offset > 0) {
return emptyList()
}
return getList(order, filter)
}
public abstract suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga>
}

@ -1,9 +1,9 @@
package org.koitharu.kotatsu.parsers.bitmap
interface Bitmap {
public interface Bitmap {
val width: Int
val height: Int
public val width: Int
public val height: Int
fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect)
public fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect)
}

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.bitmap
data class Rect(
public data class Rect(
val left: Int = 0,
val top: Int = 0,
val right: Int = 0,

@ -1,13 +1,13 @@
package org.koitharu.kotatsu.parsers.config
sealed class ConfigKey<T>(
@JvmField val key: String,
public sealed class ConfigKey<T>(
@JvmField public val key: String,
) {
abstract val defaultValue: T
public abstract val defaultValue: T
class Domain(
@JvmField @JvmSuppressWildcards vararg val presetValues: String,
public class Domain(
@JvmField @JvmSuppressWildcards public vararg val presetValues: String,
) : ConfigKey<String>("domain") {
init {
@ -18,20 +18,20 @@ sealed class ConfigKey<T>(
get() = presetValues.first()
}
class ShowSuspiciousContent(
public class ShowSuspiciousContent(
override val defaultValue: Boolean,
) : ConfigKey<Boolean>("show_suspicious")
class UserAgent(
public class UserAgent(
override val defaultValue: String,
) : ConfigKey<String>("user_agent")
class SplitByTranslations(
public class SplitByTranslations(
override val defaultValue: Boolean,
) : ConfigKey<Boolean>("split_translations")
class PreferredImageServer(
val presetValues: Map<String?, String?>,
public class PreferredImageServer(
public val presetValues: Map<String?, String?>,
override val defaultValue: String?,
) : ConfigKey<String?>("img_server")
}

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.config
interface MangaSourceConfig {
public interface MangaSourceConfig {
operator fun <T> get(key: ConfigKey<T>): T
}
public operator fun <T> get(key: ConfigKey<T>): T
}

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
/**
* Authorization is required for access to the requested content
*/
class AuthRequiredException @InternalParsersApi @JvmOverloads constructor(
val source: MangaSource,
public class AuthRequiredException @InternalParsersApi @JvmOverloads constructor(
public val source: MangaSource,
cause: Throwable? = null,
) : RuntimeException("Authorization required", cause)

@ -7,6 +7,10 @@ enum class ContentType {
*/
MANGA,
MANHWA,
MANHUA,
/**
* Use this if the source provides mostly nsfw content.
*/
@ -17,6 +21,8 @@ enum class ContentType {
*/
COMICS,
NOVEL,
/**
* Use this type if no other suits your needs. For example, for an indie manga
*/

@ -0,0 +1,9 @@
package org.koitharu.kotatsu.parsers.model
enum class Demographic {
SHOUNEN,
SHOUJO,
SEINEN,
JOSEI,
NONE,
}

@ -2,86 +2,86 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
class Manga(
public class Manga(
/**
* Unique identifier for manga
*/
@JvmField val id: Long,
@JvmField public val id: Long,
/**
* Manga title, human-readable
*/
@JvmField val title: String,
@JvmField public val title: String,
/**
* Alternative title (for example on other language), may be null
*/
@JvmField val altTitle: String?,
@JvmField public val altTitle: String?,
/**
* Relative url to manga (**without** a domain) or any other uri.
* Used principally in parsers
*/
@JvmField val url: String,
@JvmField public val url: String,
/**
* Absolute url to manga, must be ready to open in browser
*/
@JvmField val publicUrl: String,
@JvmField public val publicUrl: String,
/**
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
* @see hasRating
*/
@JvmField val rating: Float,
@JvmField public val rating: Float,
/**
* Indicates that manga may contain sensitive information (18+, NSFW)
*/
@JvmField val isNsfw: Boolean,
@JvmField public val isNsfw: Boolean,
/**
* Absolute link to the cover
* @see largeCoverUrl
*/
@JvmField val coverUrl: String,
@JvmField public val coverUrl: String,
/**
* Tags (genres) of the manga
*/
@JvmField val tags: Set<MangaTag>,
@JvmField public val tags: Set<MangaTag>,
/**
* Manga status (ongoing, finished) or null if unknown
*/
@JvmField val state: MangaState?,
@JvmField public val state: MangaState?,
/**
* Author of the manga, may be null
*/
@JvmField val author: String?,
@JvmField public val author: String?,
/**
* Large cover url (absolute), null if is no large cover
* @see coverUrl
*/
@JvmField val largeCoverUrl: String? = null,
@JvmField public val largeCoverUrl: String? = null,
/**
* Manga description, may be html or null
*/
@JvmField val description: String? = null,
@JvmField public val description: String? = null,
/**
* List of chapters
*/
@JvmField val chapters: List<MangaChapter>? = null,
@JvmField public val chapters: List<MangaChapter>? = null,
/**
* Manga source
*/
@JvmField val source: MangaSource,
@JvmField public val source: MangaSource,
) {
/**
* Return if manga has a specified rating
* @see rating
*/
val hasRating: Boolean
public val hasRating: Boolean
get() = rating > 0f && rating <= 1f
fun getChapters(branch: String?): List<MangaChapter>? {
public fun getChapters(branch: String?): List<MangaChapter>? {
return chapters?.filter { x -> x.branch == branch }
}
@InternalParsersApi
fun copy(
public fun copy(
title: String = this.title,
altTitle: String? = this.altTitle,
publicUrl: String = this.publicUrl,
@ -95,7 +95,7 @@ class Manga(
description: String? = this.description,
chapters: List<MangaChapter>? = this.chapters,
source: MangaSource = this.source,
) = Manga(
): Manga = Manga(
id = id,
title = title,
altTitle = altTitle,

@ -1,45 +1,45 @@
package org.koitharu.kotatsu.parsers.model
class MangaChapter(
public class MangaChapter(
/**
* An unique id of chapter
*/
@JvmField val id: Long,
@JvmField public val id: Long,
/**
* User-readable name of chapter
*/
@JvmField val name: String,
@JvmField public val name: String,
/**
* Chapter number starting from 1, 0 if unknown
*/
@JvmField val number: Float,
@JvmField public val number: Float,
/**
* Volume number starting from 1, 0 if unknown
*/
@JvmField val volume: Int,
@JvmField public val volume: Int,
/**
* Relative url to chapter (**without** a domain) or any other uri.
* Used principally in parsers
*/
@JvmField val url: String,
@JvmField public val url: String,
/**
* User-readable name of scanlator (releaser) or null if unknown
*/
@JvmField val scanlator: String?,
@JvmField public val scanlator: String?,
/**
* Chapter upload date in milliseconds
*/
@JvmField val uploadDate: Long,
@JvmField public val uploadDate: Long,
/**
* User-readable name of branch.
* A branch is a group of chapters that overlap (e.g. different languages)
*/
@JvmField val branch: String?,
@JvmField val source: MangaSource,
@JvmField public val branch: String?,
@JvmField public val source: MangaSource,
) {
@Deprecated(message = "Consider using constructor with volume value")
constructor(
internal constructor(
id: Long,
name: String,
number: Int,

@ -1,93 +1,38 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
import java.util.*
sealed interface MangaListFilter {
fun isEmpty(): Boolean
val sortOrder: SortOrder?
fun isValid(parser: MangaParser): Boolean = when (this) {
is Advanced -> (sortOrder in parser.availableSortOrders) &&
(tags.size <= 1 || parser.isMultipleTagsSupported) &&
(tagsExclude.isEmpty() || parser.isTagsExclusionSupported) &&
(contentRating.isEmpty() || parser.availableContentRating.containsAll(contentRating)) &&
(states.isEmpty() || parser.availableStates.containsAll(states))
is Search -> parser.isSearchSupported
}
data class Search(
@JvmField val query: String,
) : MangaListFilter {
override val sortOrder: SortOrder? = null
override fun isEmpty() = query.isBlank()
}
data class Advanced(
override val sortOrder: SortOrder,
@JvmField val tags: Set<MangaTag>,
@JvmField val tagsExclude: Set<MangaTag>,
@JvmField val locale: Locale?,
@JvmField val states: Set<MangaState>,
@JvmField val contentRating: Set<ContentRating>,
) : MangaListFilter {
override fun isEmpty(): Boolean =
tags.isEmpty() && tagsExclude.isEmpty() && locale == null && states.isEmpty() && contentRating.isEmpty()
fun newBuilder() = Builder(sortOrder)
.tags(tags)
.tagsExclude(tagsExclude)
.locale(locale)
.states(states)
.contentRatings(contentRating)
class Builder(sortOrder: SortOrder) {
private var _sortOrder: SortOrder = sortOrder
private var _tags: Set<MangaTag>? = null
private var _tagsExclude: Set<MangaTag>? = null
private var _locale: Locale? = null
private var _states: Set<MangaState>? = null
private var _contentRating: Set<ContentRating>? = null
fun sortOrder(order: SortOrder) = apply {
_sortOrder = order
}
fun tags(tags: Set<MangaTag>?) = apply {
_tags = tags
}
fun tagsExclude(tags: Set<MangaTag>?) = apply {
_tagsExclude = tags
}
fun locale(locale: Locale?) = apply {
_locale = locale
}
fun states(states: Set<MangaState>?) = apply {
_states = states
}
fun contentRatings(rating: Set<ContentRating>?) = apply {
_contentRating = rating
}
fun build() = Advanced(
sortOrder = _sortOrder,
tags = _tags.orEmpty(),
tagsExclude = _tagsExclude.orEmpty(),
locale = _locale,
states = _states.orEmpty(),
contentRating = _contentRating.orEmpty(),
)
}
public data class MangaListFilter(
@JvmField val query: String? = null,
@JvmField val tags: Set<MangaTag> = emptySet(),
@JvmField val tagsExclude: Set<MangaTag> = emptySet(),
@JvmField val locale: Locale? = null,
@JvmField val originalLocale: Locale? = null,
@JvmField val states: Set<MangaState> = emptySet(),
@JvmField val contentRating: Set<ContentRating> = emptySet(),
@JvmField val types: Set<ContentType> = emptySet(),
@JvmField val demographics: Set<Demographic> = emptySet(),
@JvmField val year: Int = 0,
@JvmField val yearFrom: Int = 0,
@JvmField val yearTo: Int = 0,
) {
public fun isEmpty(): Boolean = tags.isEmpty() &&
tagsExclude.isEmpty() &&
locale == null &&
originalLocale == null &&
states.isEmpty() &&
contentRating.isEmpty() &&
query == null &&
year == 0 &&
yearFrom == 0 &&
yearTo == 0 &&
types.isEmpty() &&
demographics.isEmpty()
public companion object {
@JvmStatic
public val EMPTY: MangaListFilter = MangaListFilter()
}
}

@ -0,0 +1,50 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
public data class MangaListFilterCapabilities @InternalParsersApi constructor(
/**
* Whether parser supports filtering by more than one tag
* @see [MangaListFilter.tags]
* @see [MangaListFilterOptions.availableTags]
*/
val isMultipleTagsSupported: Boolean = false,
/**
* Whether parser supports tagsExclude field in filter
* @see [MangaListFilter.tagsExclude]
* @see [MangaListFilterOptions.availableTags]
*/
val isTagsExclusionSupported: Boolean = false,
/**
* Whether parser supports searching by string query
* @see [MangaListFilter.query]
*/
val isSearchSupported: Boolean = false,
/**
* Whether parser supports searching by string query combined within other filters
*/
val isSearchWithFiltersSupported: Boolean = false,
/**
* Whether parser supports searching/filtering by year
* @see [MangaListFilter.year]
*/
val isYearSupported: Boolean = false,
/**
* Whether parser supports searching by year range
* @see [MangaListFilter.yearFrom] and [MangaListFilter.yearTo]
*/
val isYearRangeSupported: Boolean = false,
/**
* Whether parser supports searching Original Languages
* @see [MangaListFilter.originalLocale]
* @see [MangaListFilterOptions.availableLocales]
*/
val isOriginalLocaleSupported: Boolean = false,
)

@ -0,0 +1,45 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
import java.util.*
public data class MangaListFilterOptions @InternalParsersApi constructor(
/**
* Available tags (genres)
*/
public val availableTags: Set<MangaTag> = emptySet(),
/**
* Supported [MangaState] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableStates: Set<MangaState> = emptySet(),
/**
* Supported [ContentRating] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableContentRating: Set<ContentRating> = emptySet(),
/**
* Supported [ContentType] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableContentTypes: Set<ContentType> = emptySet(),
/**
* Supported [Demographic] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableDemographics: Set<Demographic> = emptySet(),
/**
* Supported content locales for multilingual sources
*/
public val availableLocales: Set<Locale> = emptySet(),
)

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.model
interface MangaSource {
public interface MangaSource {
val name: String
public val name: String
}

@ -1,5 +1,5 @@
package org.koitharu.kotatsu.parsers.model
enum class MangaState {
public enum class MangaState {
ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING
}

@ -2,17 +2,17 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
class MangaTag(
public class MangaTag(
/**
* User-readable tag title, should be in Title case
*/
@JvmField val title: String,
@JvmField public val title: String,
/**
* Identifier of a tag, must be unique among the source.
* @see MangaParser.getList
*/
@JvmField val key: String,
@JvmField val source: MangaSource,
@JvmField public val key: String,
@JvmField public val source: MangaSource,
) {
override fun equals(other: Any?): Boolean {

@ -11,4 +11,12 @@ enum class SortOrder {
NEWEST_ASC,
ALPHABETICAL,
ALPHABETICAL_DESC,
ADDED,
ADDED_ASC,
RELEVANCE,
POPULARITY_HOUR,
POPULARITY_TODAY,
POPULARITY_WEEK,
POPULARITY_MONTH,
POPULARITY_YEAR,
}

@ -3,9 +3,9 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
@InternalParsersApi
class WordSet(private vararg val words: String) {
public class WordSet(private vararg val words: String) {
fun anyWordIn(dateString: String): Boolean = words.any {
public fun anyWordIn(dateString: String): Boolean = words.any {
dateString.contains(it, ignoreCase = true)
}
}
}

@ -53,13 +53,42 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.POPULARITY_YEAR,
SortOrder.POPULARITY_MONTH,
SortOrder.POPULARITY_WEEK,
SortOrder.POPULARITY_TODAY,
SortOrder.POPULARITY_HOUR,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val isTagsExclusionSupported: Boolean = true
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.SAFE),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = setOf(
Locale.CHINESE, Locale.ENGLISH, Locale.US, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN, Locale.JAPANESE,
Locale("af"), Locale("ar"), Locale("az"), Locale("eu"), Locale("be"),
Locale("bn"), Locale("bs"), Locale("bg"), Locale("my"), Locale("km"),
Locale("ceb"), Locale("zh_hk"), Locale("zh_tw"), Locale("hr"), Locale("cs"),
Locale("da"), Locale("nl"), Locale("eo"), Locale("et"), Locale("fil"),
Locale("fi"), Locale("ka"), Locale("el"), Locale("ht"), Locale("he"),
Locale("hi"), Locale("hu"), Locale("id"), Locale("kk"), Locale("ko"),
Locale("lv"), Locale("ms"), Locale("ml"), Locale("mo"), Locale("mn"),
Locale("ne"), Locale("no"), Locale("fa"), Locale("pl"), Locale("pt"),
Locale("pt_br"), Locale("pt_pt"), Locale("ro"), Locale("ru"), Locale("sr"),
Locale("si"), Locale("sk"), Locale("es"), Locale("es_419"), Locale("ta"),
Locale("te"), Locale("th"), Locale("ti"), Locale("tr"), Locale("uk"),
Locale("vi"), Locale("zu"),
),
)
override val configKeyDomain = ConfigKey.Domain(
"bato.to",
@ -89,25 +118,29 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
"zbato.org",
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
when {
!filter.query.isNullOrEmpty() -> {
return search(page, filter.query)
}
is MangaListFilter.Advanced -> {
else -> {
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=")
when (filter.sortOrder) {
when (order) {
SortOrder.UPDATED -> append("update.za")
SortOrder.POPULARITY -> append("views_a.za")
SortOrder.NEWEST -> append("create.za")
SortOrder.ALPHABETICAL -> append("title.az")
SortOrder.POPULARITY_YEAR -> append("views_y.za")
SortOrder.POPULARITY_MONTH -> append("views_m.za")
SortOrder.POPULARITY_WEEK -> append("views_w.za")
SortOrder.POPULARITY_TODAY -> append("views_d.za")
SortOrder.POPULARITY_HOUR -> append("views_h.za")
else -> append("update.za")
}
@ -161,17 +194,6 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
return parseList(url, page)
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=update.za")
append("&page=")
append(page.toString())
}
return parseList(url, page)
}
}
}
@ -242,7 +264,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
throw ParseException("Cannot find images list", fullUrl)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val scripts = webClient.httpGet(
"https://${domain}/browse",
).parseHtml().selectOrThrow("script")
@ -263,22 +285,6 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
throw ParseException("Cannot find gernes list", scripts[0].baseUri())
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.CHINESE, Locale.ENGLISH, Locale.US, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN, Locale.JAPANESE,
Locale("af"), Locale("ar"), Locale("az"), Locale("eu"), Locale("be"),
Locale("bn"), Locale("bs"), Locale("bg"), Locale("my"), Locale("km"),
Locale("ceb"), Locale("zh_hk"), Locale("zh_tw"), Locale("hr"), Locale("cs"),
Locale("da"), Locale("nl"), Locale("eo"), Locale("et"), Locale("fil"),
Locale("fi"), Locale("ka"), Locale("el"), Locale("ht"), Locale("he"),
Locale("hi"), Locale("hu"), Locale("id"), Locale("kk"), Locale("ko"),
Locale("lv"), Locale("ms"), Locale("ml"), Locale("mo"), Locale("mn"),
Locale("ne"), Locale("no"), Locale("fa"), Locale("pl"), Locale("pt"),
Locale("pt_br"), Locale("pt_pt"), Locale("ro"), Locale("ru"), Locale("sr"),
Locale("si"), Locale("sk"), Locale("es"), Locale("es_419"), Locale("ta"),
Locale("te"), Locale("th"), Locale("ti"), Locale("tr"), Locale("uk"),
Locale("vi"), Locale("zu"),
)
private suspend fun search(page: Int, query: String): List<Manga> {
val url = buildString {
append("https://")

@ -27,8 +27,6 @@ internal class ComickFunParser(context: MangaLoaderContext) :
keys.add(userAgentKey)
}
override val isTagsExclusionSupported = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.UPDATED,
@ -36,12 +34,31 @@ internal class ComickFunParser(context: MangaLoaderContext) :
SortOrder.NEWEST,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
private val tagsArray = SuspendLazy(::loadTags)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isYearRangeSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = EnumSet.of(
ContentType.MANGA,
ContentType.MANHWA,
ContentType.MANHUA,
ContentType.OTHER,
),
availableDemographics = EnumSet.allOf(Demographic::class.java),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val domain = domain
val url = urlBuilder()
.host("api.$domain")
@ -51,16 +68,12 @@ internal class ComickFunParser(context: MangaLoaderContext) :
.addQueryParameter("tachiyomi", "true")
.addQueryParameter("limit", pageSize.toString())
.addQueryParameter("page", page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
url.addQueryParameter("q", filter.query)
}
null -> {
url.addQueryParameter("sort", "view")
}
is MangaListFilter.Advanced -> {
else -> {
filter.tags.forEach {
url.addQueryParameter("genres", it.key)
@ -72,7 +85,7 @@ internal class ComickFunParser(context: MangaLoaderContext) :
url.addQueryParameter(
"sort",
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> "view"
SortOrder.UPDATED -> "uploaded"
SortOrder.NEWEST -> "created_at"
@ -93,6 +106,40 @@ internal class ComickFunParser(context: MangaLoaderContext) :
},
)
}
if (filter.yearFrom != 0) {
url.addQueryParameter("from", filter.yearFrom.toString())
}
if (filter.yearTo != 0) {
url.addQueryParameter("to", filter.yearTo.toString())
}
filter.types.forEach {
url.addQueryParameter(
"country",
when (it) {
ContentType.MANGA -> "jp"
ContentType.MANHWA -> "kr"
ContentType.MANHUA -> "cn"
ContentType.OTHER -> "others"
else -> ""
},
)
}
filter.demographics.forEach {
url.addQueryParameter(
"demographic",
when (it) {
Demographic.SHOUNEN -> "1"
Demographic.SHOUJO -> "2"
Demographic.SEINEN -> "3"
Demographic.JOSEI -> "4"
Demographic.NONE -> "5"
},
)
}
}
}
val ja = webClient.httpGet(url.build()).parseJsonArray()
@ -163,7 +210,7 @@ internal class ComickFunParser(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val sparseArray = tagsArray.get()
val set = ArraySet<MangaTag>(sparseArray.size())
for (i in 0 until sparseArray.size()) {

@ -33,7 +33,6 @@ internal class ExHentaiParser(
) : PagedMangaParser(context, MangaParserSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider, Interceptor {
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val isTagsExclusionSupported: Boolean = true
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain(
@ -51,6 +50,13 @@ internal class ExHentaiParser(
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
private val tagsMap = SuspendLazy(::fetchTags)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
override val isAuthorized: Boolean
get() {
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
@ -75,7 +81,28 @@ internal class ExHentaiParser(
searchPaginator.firstPage = 0
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tagsMap.get().values.toSet(),
availableLocales = setOf(
Locale.JAPANESE,
Locale.ENGLISH,
Locale.CHINESE,
Locale("nl"),
Locale.FRENCH,
Locale.GERMAN,
Locale("hu"),
Locale.ITALIAN,
Locale("kr"),
Locale("pl"),
Locale("pt"),
Locale("ru"),
Locale("es"),
Locale("th"),
Locale("vi"),
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val next = nextPages.get(page, 0L)
if (page > 0 && next == 0L) {
@ -90,15 +117,15 @@ internal class ExHentaiParser(
append(domain)
append("/?next=")
append(next)
when (filter) {
when {
is MangaListFilter.Search -> {
!filter.query.isNullOrEmpty() -> {
search += filter.query.urlEncoded()
append("&f_search=")
append(search.trim().replace(' ', '+'))
}
is MangaListFilter.Advanced -> {
else -> {
filter.toSearchQuery()?.let { sq ->
append("&f_search=")
@ -121,8 +148,6 @@ internal class ExHentaiParser(
append(fCats)
}
}
null -> {}
}
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
if (updateDm) {
@ -141,7 +166,7 @@ internal class ExHentaiParser(
body.parseFailed("Cannot find root")
} else {
updateDm = true
return getListPage(page, filter)
return getListPage(page, order, filter)
}
updateDm = false
nextPages[page + 1] = getNextTimestamp(body)
@ -267,10 +292,6 @@ internal class ExHentaiParser(
"unusual pupils,urination,vore,vtuber,widow,wings,witch,wolf girl,x-ray,yuri,zombie,sole male,males only,yaoi," +
"tomgirl,tall man,oni,shotacon,prostate massage,policeman,males only,huge penis,fox boy,feminization,dog boy,dickgirl on male,big penis"
override suspend fun getAvailableTags(): Set<MangaTag> {
return tagsMap.get().values.toSet()
}
private suspend fun fetchTags(): Map<String, MangaTag> {
val tagMap = ArrayMap<String, MangaTag>()
val tagElements = tags.split(",")
@ -297,24 +318,6 @@ internal class ExHentaiParser(
return tagMap
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.JAPANESE,
Locale.ENGLISH,
Locale.CHINESE,
Locale("nl"),
Locale.FRENCH,
Locale.GERMAN,
Locale("hu"),
Locale.ITALIAN,
Locale("kr"),
Locale("pl"),
Locale("pt"),
Locale("ru"),
Locale("es"),
Locale("th"),
Locale("vi"),
)
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.headersContentLength() <= 256) {
@ -420,7 +423,7 @@ internal class ExHentaiParser(
?.toLongOrNull() ?: 1
}
private fun MangaListFilter.Advanced.toSearchQuery(): String? {
private fun MangaListFilter.toSearchQuery(): String? {
val joiner = StringUtil.StringJoiner(" ")
for (tag in tags) {
if (tag.key.isNumeric()) {

@ -27,7 +27,7 @@ import kotlin.math.min
@OptIn(ExperimentalUnsignedTypes::class)
@MangaSourceParser("HITOMILA", "Hitomi.La", type = ContentType.HENTAI)
class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.HITOMILA) {
internal class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.HITOMILA) {
override val configKeyDomain = ConfigKey.Domain("hitomi.la")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
@ -70,14 +70,23 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
Locale.JAPANESE to "japanese",
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableLocales = localeMap.keys,
)
private fun Locale?.getSiteLang(): String = when (this) {
null -> "all"
else -> localeMap[this] ?: "all"
}
override suspend fun getAvailableLocales(): Set<Locale> = localeMap.keys
override suspend fun getAvailableTags(): Set<MangaTag> = coroutineScope {
private suspend fun fetchAvailableTags(): Set<MangaTag> = coroutineScope {
('a'..'z').map { alphabet ->
async {
val doc = webClient.httpGet("https://$domain/alltags-$alphabet.html").parseHtml()
@ -109,13 +118,10 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
private var cachedSearchIds: List<Int> = emptyList()
override suspend fun getList(
offset: Int,
filter: MangaListFilter?,
): List<Manga> = when (filter) {
is MangaListFilter.Advanced -> {
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> = when {
filter.query.isNullOrEmpty() -> {
if (filter.tags.isEmpty()) {
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> {
getGalleryIDsFromNozomi(
"popular",
@ -134,7 +140,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
cachedSearchIds =
hitomiSearch(
filter.tags.joinToString(" ") { it.key },
filter.sortOrder == SortOrder.POPULARITY,
order == SortOrder.POPULARITY,
filter.locale.getSiteLang(),
).toList()
}
@ -142,14 +148,12 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
}
}
is MangaListFilter.Search -> {
else -> {
if (offset == 0) {
cachedSearchIds = hitomiSearch(filter.query, filter.sortOrder == SortOrder.POPULARITY).toList()
cachedSearchIds = hitomiSearch(filter.query, order == SortOrder.POPULARITY).toList()
}
cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
}
else -> getGalleryIDsFromNozomi(null, "popular", "all", offset.nextOffsetRange())
}.toMangaList()
private fun Int.nextOffsetRange(): LongRange {
@ -478,14 +482,14 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
title = doc.selectFirstOrThrow("h1").text(),
url = id.toString(),
coverUrl =
"https:" +
doc.selectFirstOrThrow("picture > source")
.attr("data-srcset")
.substringBefore(" "),
"https:" +
doc.selectFirstOrThrow("picture > source")
.attr("data-srcset")
.substringBefore(" "),
publicUrl =
doc.selectFirstOrThrow("h1 > a")
.attrAsRelativeUrl("href")
.toAbsoluteUrl(domain),
doc.selectFirstOrThrow("h1 > a")
.attrAsRelativeUrl("href")
.toAbsoluteUrl(domain),
author = null,
tags = emptySet(),
isNsfw = true,
@ -508,37 +512,37 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
return manga.copy(
title = json.getString("title"),
largeCoverUrl =
json.getJSONArray("files").getJSONObject(0).let {
val hash = it.getString("hash")
val commonId = commonImageId()
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
},
json.getJSONArray("files").getJSONObject(0).let {
val hash = it.getString("hash")
val commonId = commonImageId()
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
},
author =
json.optJSONArray("artists")
?.mapJSON { it.getString("artist").toCamelCase() }
?.joinToString(),
json.optJSONArray("artists")
?.mapJSON { it.getString("artist").toCamelCase() }
?.joinToString(),
publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain),
tags =
buildSet {
json.optJSONArray("characters")
?.mapToTags("character")
?.let(::addAll)
json.optJSONArray("tags")
?.mapToTags("tag")
?.let(::addAll)
json.optJSONArray("artists")
?.mapToTags("artist")
?.let(::addAll)
json.optJSONArray("parodys")
?.mapToTags("parody")
?.let(::addAll)
json.optJSONArray("groups")
?.mapToTags("group")
?.let(::addAll)
},
buildSet {
json.optJSONArray("characters")
?.mapToTags("character")
?.let(::addAll)
json.optJSONArray("tags")
?.mapToTags("tag")
?.let(::addAll)
json.optJSONArray("artists")
?.mapToTags("artist")
?.let(::addAll)
json.optJSONArray("parodys")
?.mapToTags("parody")
?.let(::addAll)
json.optJSONArray("groups")
?.mapToTags("group")
?.let(::addAll)
},
chapters = listOf(
MangaChapter(
id = generateUid(manga.url),
@ -562,15 +566,15 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
mapJSON {
MangaTag(
title =
it.getString(key).toCamelCase().let { title ->
if (it.getStringOrNull("female")?.toIntOrNull() == 1) {
"$title"
} else if (it.getStringOrNull("male")?.toIntOrNull() == 1) {
"$title"
} else {
title
}
},
it.getString(key).toCamelCase().let { title ->
if (it.getStringOrNull("female")?.toIntOrNull() == 1) {
"$title"
} else if (it.getStringOrNull("male")?.toIntOrNull() == 1) {
"$title"
} else {
title
}
},
key = it.getString("url").tagUrlToTag(),
source = source,
).let(tags::add)

@ -22,24 +22,41 @@ internal class ImHentai(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("imhentai.xxx")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableLocales = setOf(
Locale.ENGLISH, Locale.JAPANESE, Locale("es"), Locale.FRENCH, Locale("kr"), Locale.GERMAN, Locale("ru"),
),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(
page: Int,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/search/?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&key=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
append("&key=")
@ -53,17 +70,13 @@ internal class ImHentai(context: MangaLoaderContext) :
}
append(lang)
when (filter.sortOrder) {
when (order) {
SortOrder.UPDATED -> append("&lt=1&pp=0")
SortOrder.POPULARITY -> append("&lt=0&pp=1")
SortOrder.RATING -> append("&lt=0&pp=0")
else -> append("&lt=1&pp=0")
}
}
null -> {
append("&lt=1&pp=0")
}
}
}
@ -90,7 +103,7 @@ internal class ImHentai(context: MangaLoaderContext) :
//Tags are deliberately reduced because there are too many and this slows down the application.
//only the most popular ones are taken.
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..3).map { page ->
async { getTags(page) }
@ -113,10 +126,6 @@ internal class ImHentai(context: MangaLoaderContext) :
)
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH, Locale.JAPANESE, Locale("es"), Locale.FRENCH, Locale("kr"), Locale.GERMAN, Locale("ru"),
)
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()

@ -23,7 +23,14 @@ internal abstract class LineWebtoonsParser(
source: MangaParserSource,
) : MangaParser(context, source) {
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
private val signer by lazy {
WebtoonsUrlSigner("gUtPzJFZch4ZyAGviiyH94P99lQ3pFdRTwpJWDlSGFfwgpr6ses5ALOxWHOIT7R1")
@ -135,116 +142,79 @@ internal abstract class LineWebtoonsParser(
}
}
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val manga =
when (filter) {
is MangaListFilter.Search -> {
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
.getJSONObject("challengeSearch")
.getJSONArray("titleList")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = RATING_UNKNOWN,
isNsfw = isNsfwSource,
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = null,
tags = emptySet(),
author = jo.getStringOrNull("writingAuthorName"),
description = null,
state = null,
source = source,
)
}
}
is MangaListFilter.Advanced -> {
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val manga = when {
!filter.query.isNullOrEmpty() -> {
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
.getJSONObject("challengeSearch")
.getJSONArray("titleList")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = RATING_UNKNOWN,
isNsfw = isNsfwSource,
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = null,
tags = emptySet(),
author = jo.getStringOrNull("writingAuthorName"),
description = null,
state = null,
source = source,
)
}
}
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
else -> {
val sortOrderStr = when (filter.sortOrder) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}")
}
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
author = jo.getStringOrNull("writingAuthorName"),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
val sortOrderStr = when (order) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: $order")
}
null -> {
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=ALL&sortOrder=UPDATE&startIndex=${offset + 1}&pageSize=20")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
author = jo.getStringOrNull("writingAuthorName"),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
}
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
author = jo.getStringOrNull("writingAuthorName"),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
}
}
return manga
@ -274,7 +244,7 @@ internal abstract class LineWebtoonsParser(
)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
return makeRequest("/lineWebtoon/webtoon/challengeGenreList.json")
.getJSONObject("genreList")
.getJSONArray("challengeGenres")

@ -44,17 +44,38 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
keys.add(preferredServerKey)
}
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = true,
isYearSupported = true,
isYearRangeSupported = true,
isOriginalLocaleSupported = true,
)
override val isTagsExclusionSupported: Boolean = true
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override suspend fun getFilterOptions(): MangaListFilterOptions = coroutineScope {
val localesDeferred = async { fetchAvailableLocales() }
val tagsDeferred = async { fetchAvailableTags() }
MangaListFilterOptions(
availableTags = tagsDeferred.await(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
MangaState.ABANDONED,
),
availableContentRating = EnumSet.allOf(ContentRating::class.java),
availableContentTypes = emptySet(),
availableDemographics = EnumSet.allOf(Demographic::class.java),
availableLocales = localesDeferred.await(),
)
}
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val domain = domain
val url = buildString {
append("https://api.")
@ -64,69 +85,91 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append("&offset=")
append(offset)
append("&includes[]=cover_art&includes[]=author&includes[]=artist")
when (filter) {
is MangaListFilter.Search -> {
append("&title=")
append(filter.query)
append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
}
is MangaListFilter.Advanced -> {
filter.tags.forEach {
append("&includedTags[]=")
append(it.key)
}
filter.query?.let {
append("&title=")
append(filter.query.urlEncoded())
}
filter.tagsExclude.forEach {
append("&excludedTags[]=")
append(it.key)
}
filter.tags.forEach {
append("&includedTags[]=")
append(it.key)
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.forEach {
when (it) {
ContentRating.SAFE -> append("&contentRating[]=safe")
ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica")
ContentRating.ADULT -> append("&contentRating[]=pornographic")
}
}
}
filter.tagsExclude.forEach {
append("&excludedTags[]=")
append(it.key)
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.forEach {
when (it) {
ContentRating.SAFE -> append("&contentRating[]=safe")
ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica")
ContentRating.ADULT -> append("&contentRating[]=pornographic")
append("&order")
append(
when (filter.sortOrder) {
SortOrder.UPDATED -> "[latestUploadedChapter]=desc"
SortOrder.UPDATED_ASC -> "[latestUploadedChapter]=asc"
SortOrder.RATING -> "[rating]=desc"
SortOrder.RATING_ASC -> "[rating]=asc"
SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.ALPHABETICAL_DESC -> "[title]=desc"
SortOrder.NEWEST -> "[createdAt]=desc"
SortOrder.NEWEST_ASC -> "[createdAt]=asc"
SortOrder.POPULARITY -> "[followedCount]=desc"
SortOrder.POPULARITY_ASC -> "[followedCount]=asc"
},
)
filter.states.forEach {
append("&status[]=")
when (it) {
MangaState.ONGOING -> append("ongoing")
MangaState.FINISHED -> append("completed")
MangaState.ABANDONED -> append("cancelled")
MangaState.PAUSED -> append("hiatus")
else -> append("")
}
}
filter.locale?.let {
append("&availableTranslatedLanguage[]=")
append(it.language)
}
}
} else append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
null -> {
append("&order[latestUploadedChapter]=desc")
append("&order")
append(
when (order) {
SortOrder.UPDATED -> "[latestUploadedChapter]=desc"
SortOrder.UPDATED_ASC -> "[latestUploadedChapter]=asc"
SortOrder.RATING -> "[rating]=desc"
SortOrder.RATING_ASC -> "[rating]=asc"
SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.ALPHABETICAL_DESC -> "[title]=desc"
SortOrder.NEWEST -> "[year]=desc"
SortOrder.NEWEST_ASC -> "[year]=asc"
SortOrder.POPULARITY -> "[followedCount]=desc"
SortOrder.POPULARITY_ASC -> "[followedCount]=asc"
SortOrder.ADDED -> "[createdAt]=desc"
SortOrder.ADDED_ASC -> "[createdAt]=asc"
SortOrder.RELEVANCE -> "&order[relevance]=desc"
else -> "[latestUploadedChapter]=desc"
},
)
filter.states.forEach {
append("&status[]=")
when (it) {
MangaState.ONGOING -> append("ongoing")
MangaState.FINISHED -> append("completed")
MangaState.ABANDONED -> append("cancelled")
MangaState.PAUSED -> append("hiatus")
else -> append("")
}
}
filter.demographics.forEach {
append("&publicationDemographic[]=")
append(
when (it) {
Demographic.SHOUNEN -> "shounen"
Demographic.SHOUJO -> "shoujo"
Demographic.SEINEN -> "seinen"
Demographic.JOSEI -> "josei"
Demographic.NONE -> "none"
},
)
}
filter.locale?.let {
append("&availableTranslatedLanguage[]=")
append(it.language)
}
filter.originalLocale?.let {
append("&originalLanguage[]=")
append(it.language)
}
if (filter.year != 0) {
append("&year=")
append(filter.year)
}
}
val json = webClient.httpGet(url).parseJson().getJSONArray("data")
return json.mapJSON { jo ->
@ -219,7 +262,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val tags = webClient.httpGet("https://api.${domain}/manga/tag").parseJson()
.getJSONArray("data")
return tags.mapJSONToSet { jo ->
@ -233,7 +276,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
}
}
override suspend fun getAvailableLocales(): Set<Locale> {
private suspend fun fetchAvailableLocales(): Set<Locale> {
val head = webClient.httpGet("https://$domain/").parseHtml().head()
return head.getElementsByAttributeValue("property", "og:locale:alternate")
.mapNotNullToSet { meta ->

@ -60,8 +60,6 @@ internal abstract class MangaFireParser(
?: body.parseFailed("Cannot find username")
}
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
private val tags = SoftSuspendLazy {
webClient.httpGet("https://$domain/filter").parseHtml()
.select(".genres > li").map {
@ -73,36 +71,44 @@ internal abstract class MangaFireParser(
}.associateBy { it.title }
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return tags.get().values.toSet()
}
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
override val isTagsExclusionSupported = true
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tags.get().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = "https://$domain/filter".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("language[]", siteLang)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
addQueryParameter("keyword", filter.query)
filter.sortOrder?.let {
addQueryParameter(
name = "sort",
value = when (it) {
SortOrder.UPDATED -> "recently_updated"
SortOrder.POPULARITY -> "most_viewed"
SortOrder.RATING -> "scores"
SortOrder.NEWEST -> "release_date"
SortOrder.ALPHABETICAL -> "title_az"
else -> ""
},
)
}
addQueryParameter(
name = "sort",
value = when (order) {
SortOrder.UPDATED -> "recently_updated"
SortOrder.POPULARITY -> "most_viewed"
SortOrder.RATING -> "scores"
SortOrder.NEWEST -> "release_date"
SortOrder.ALPHABETICAL -> "title_az"
else -> ""
},
)
}
is MangaListFilter.Advanced -> {
else -> {
filter.tagsExclude.forEach { tag ->
addQueryParameter("genre[]", "-${tag.key}")
}
@ -126,7 +132,7 @@ internal abstract class MangaFireParser(
}
addQueryParameter(
name = "sort",
value = when (filter.sortOrder) {
value = when (order) {
SortOrder.UPDATED -> "recently_updated"
SortOrder.POPULARITY -> "most_viewed"
SortOrder.RATING -> "scores"
@ -136,8 +142,6 @@ internal abstract class MangaFireParser(
},
)
}
null -> {}
}
}.build()

@ -19,13 +19,44 @@ internal class MangaPark(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
override val configKeyDomain = ConfigKey.Domain("mangapark.net")
override val isTagsExclusionSupported: Boolean = true
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
override val configKeyDomain = ConfigKey.Domain("mangapark.net")
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tagsMap.get().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.SAFE),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = setOf(
Locale("af"), Locale("sq"), Locale("am"), Locale("ar"), Locale("hy"),
Locale("az"), Locale("be"), Locale("bn"), Locale("zh_hk"), Locale("zh_tw"),
Locale.CHINESE, Locale("ceb"), Locale("ca"), Locale("km"), Locale("my"),
Locale("bg"), Locale("bs"), Locale("hr"), Locale("cs"), Locale("da"),
Locale("nl"), Locale.ENGLISH, Locale("et"), Locale("fo"), Locale("fil"),
Locale("fi"), Locale("he"), Locale("ha"), Locale("jv"), Locale("lb"),
Locale("mn"), Locale("ro"), Locale("si"), Locale("ta"), Locale("uz"),
Locale("ur"), Locale("tg"), Locale("sd"), Locale("pt_br"), Locale("mo"),
Locale("lt"), Locale.JAPANESE, Locale.ITALIAN, Locale("ht"), Locale("lv"),
Locale("mr"), Locale("pt"), Locale("sn"), Locale("sv"), Locale("uk"),
Locale("tk"), Locale("sw"), Locale("st"), Locale("pl"), Locale("mi"),
Locale("lo"), Locale("ga"), Locale("gu"), Locale("gn"), Locale("id"),
Locale("ky"), Locale("mt"), Locale("fa"), Locale("sh"), Locale("es_419"),
Locale("tr"), Locale("to"), Locale("vi"), Locale("es"), Locale("sr"),
Locale("ps"), Locale("ml"), Locale("ku"), Locale("ig"), Locale("el"),
Locale.GERMAN, Locale("is"), Locale.KOREAN, Locale("ms"), Locale("ny"), Locale("sm"),
Locale("so"), Locale("ti"), Locale("zu"), Locale("yo"), Locale("th"),
Locale("sl"), Locale("ru"), Locale("no"), Locale("mg"), Locale("kk"),
Locale("hu"), Locale("ka"), Locale.FRENCH, Locale("hi"), Locale("kn"),
Locale("mk"), Locale("ne"), Locale("rm"), Locale("sk"), Locale("te"),
),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
@ -38,19 +69,19 @@ internal class MangaPark(context: MangaLoaderContext) :
context.cookieJar.insertCookies(domain, "nsfw", "2")
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/search?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&word=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
append("&genres=")
if (filter.tags.isNotEmpty()) {
@ -88,7 +119,7 @@ internal class MangaPark(context: MangaLoaderContext) :
append("&sortby=")
append(
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> "views_d000"
SortOrder.UPDATED -> "field_update"
SortOrder.NEWEST -> "field_create"
@ -104,8 +135,6 @@ internal class MangaPark(context: MangaLoaderContext) :
append(it.language)
}
}
null -> append("&sortby=field_update")
}
}
@ -129,10 +158,6 @@ internal class MangaPark(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return tagsMap.get().values.toSet()
}
private suspend fun parseTags(): Map<String, MangaTag> {
val tagElements = webClient.httpGet("https://$domain/search").parseHtml()
.select("div.flex-col:contains(Genres) div.whitespace-nowrap")
@ -149,29 +174,6 @@ internal class MangaPark(context: MangaLoaderContext) :
return tagMap
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale("af"), Locale("sq"), Locale("am"), Locale("ar"), Locale("hy"),
Locale("az"), Locale("be"), Locale("bn"), Locale("zh_hk"), Locale("zh_tw"),
Locale.CHINESE, Locale("ceb"), Locale("ca"), Locale("km"), Locale("my"),
Locale("bg"), Locale("bs"), Locale("hr"), Locale("cs"), Locale("da"),
Locale("nl"), Locale.ENGLISH, Locale("et"), Locale("fo"), Locale("fil"),
Locale("fi"), Locale("he"), Locale("ha"), Locale("jv"), Locale("lb"),
Locale("mn"), Locale("ro"), Locale("si"), Locale("ta"), Locale("uz"),
Locale("ur"), Locale("tg"), Locale("sd"), Locale("pt_br"), Locale("mo"),
Locale("lt"), Locale.JAPANESE, Locale.ITALIAN, Locale("ht"), Locale("lv"),
Locale("mr"), Locale("pt"), Locale("sn"), Locale("sv"), Locale("uk"),
Locale("tk"), Locale("sw"), Locale("st"), Locale("pl"), Locale("mi"),
Locale("lo"), Locale("ga"), Locale("gu"), Locale("gn"), Locale("id"),
Locale("ky"), Locale("mt"), Locale("fa"), Locale("sh"), Locale("es_419"),
Locale("tr"), Locale("to"), Locale("vi"), Locale("es"), Locale("sr"),
Locale("ps"), Locale("ml"), Locale("ku"), Locale("ig"), Locale("el"),
Locale.GERMAN, Locale("is"), Locale.KOREAN, Locale("ms"), Locale("ny"), Locale("sm"),
Locale("so"), Locale("ti"), Locale("zu"), Locale("yo"), Locale("th"),
Locale("sl"), Locale("ru"), Locale("no"), Locale("mg"), Locale("kk"),
Locale("hu"), Locale("ka"), Locale.FRENCH, Locale("hi"), Locale("kn"),
Locale("mk"), Locale("ne"), Locale("rm"), Locale("sk"), Locale("te"),
)
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val tagMap = tagsMap.get()

@ -9,8 +9,8 @@ import okhttp3.ResponseBody.Companion.toResponseBody
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
@ -24,7 +24,7 @@ internal abstract class MangaPlusParser(
context: MangaLoaderContext,
source: MangaParserSource,
private val sourceLang: String,
) : MangaParser(context, source), Interceptor {
) : SinglePageMangaParser(context, source), Interceptor {
private val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp")
@ -40,27 +40,26 @@ internal abstract class MangaPlusParser(
SortOrder.ALPHABETICAL,
)
private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
// no tags or tag search available
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getFilterOptions() = MangaListFilterOptions()
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (offset > 0) {
return emptyList()
}
private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
return when (filter) {
is MangaListFilter.Advanced -> {
when (filter.sortOrder) {
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
return when {
filter.query.isNullOrEmpty() -> {
when (order) {
SortOrder.POPULARITY -> getPopularList()
SortOrder.UPDATED -> getLatestList()
else -> getAllTitleList()
}
}
is MangaListFilter.Search -> getAllTitleList(filter.query)
else -> getAllTitleList()
else -> getAllTitleList(filter.query)
}
}

@ -21,7 +21,8 @@ import javax.crypto.spec.SecretKeySpec
import kotlin.math.min
@MangaSourceParser("MANGAREADERTO", "MangaReader.To")
class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANGAREADERTO, 16),
internal class MangaReaderToParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MANGAREADERTO, 16),
Interceptor, MangaParserAuthProvider {
override val configKeyDomain = ConfigKey.Domain("mangareader.to")
@ -55,8 +56,6 @@ class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(contex
SortOrder.ALPHABETICAL,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
val tags = SoftSuspendLazy {
val document = webClient.httpGet("https://$domain/filter").parseHtml()
@ -69,27 +68,36 @@ class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(contex
}.associateBy { it.title }
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return tags.get().values.toSet()
}
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override val isTagsExclusionSupported = false
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tags.get().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = "https://$domain".toHttpUrl().newBuilder().apply {
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
addPathSegment("search")
addQueryParameter("keyword", filter.query)
addQueryParameter("page", page.toString())
}
is MangaListFilter.Advanced -> {
else -> {
addPathSegment("filter")
addQueryParameter("page", page.toString())
addQueryParameter(
name = "sort",
value = when (filter.sortOrder) {
value = when (order) {
SortOrder.POPULARITY -> "most-viewed"
SortOrder.RATING -> "score"
SortOrder.UPDATED -> "latest-updated"
@ -111,11 +119,6 @@ class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(contex
},
)
}
null -> {
addPathSegment("filter")
addQueryParameter("page", page.toString())
}
}
}.build()

@ -40,12 +40,24 @@ internal abstract class NineMangaParser(
SortOrder.POPULARITY,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
override val isTagsExclusionSupported: Boolean = true
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = getOrCreateTagMap().values.toSet(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
@ -57,12 +69,12 @@ internal abstract class NineMangaParser(
return chain.proceed(newRequest)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search/?name_sel=&wd=")
append(filter.query.urlEncoded())
append("&page=")
@ -70,7 +82,7 @@ internal abstract class NineMangaParser(
append(".html")
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty()) {
append("/search/?category_id=")
@ -94,12 +106,6 @@ internal abstract class NineMangaParser(
append(page.toString())
append(".html")
}
null -> {
append("/category/index_")
append(page)
append(".html")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -184,10 +190,6 @@ internal abstract class NineMangaParser(
private var tagCache: ArrayMap<String, MangaTag>? = null
private val mutex = Mutex()
override suspend fun getAvailableTags(): Set<MangaTag> {
return getOrCreateTagMap().values.toSet()
}
private suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>()

@ -34,13 +34,19 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
SortOrder.NEWEST,
)
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getAvailableLocales() = setOf(
Locale.ENGLISH,
Locale.CHINESE,
Locale.JAPANESE,
Locale("es"),
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableLocales = setOf(
Locale.ENGLISH,
Locale.CHINESE,
Locale.JAPANESE,
Locale("es"),
),
)
private fun Locale?.getSiteLang(): String {
@ -75,7 +81,7 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
return cdn?.toHttpUrlOrNull()?.host ?: "edge.fast4speed.rsvp"
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val query = """
queryTags(
search: {format:"tagchapter",sortBy:Popular}
@ -102,23 +108,15 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
}
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> {
if (filter.tags.isEmpty() && filter.sortOrder == SortOrder.POPULARITY) {
getPopularList(page, filter.locale)
} else {
getSearchList(page, null, filter.locale, filter.tags, filter.sortOrder)
}
}
is MangaListFilter.Search -> {
getSearchList(page, filter.query, null, null, filter.sortOrder)
}
else -> {
getPopularList(page, null)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
return if (filter.query.isNullOrEmpty()) {
if (filter.tags.isEmpty() && order == SortOrder.POPULARITY) {
getPopularList(page, filter.locale)
} else {
getSearchList(page, null, filter.locale, filter.tags, order)
}
} else {
getSearchList(page, filter.query, null, null, order)
}
}

@ -25,8 +25,6 @@ internal abstract class WebtoonsParser(
source: MangaParserSource,
) : MangaParser(context, source) {
override val isMultipleTagsSupported = false
private val signer by lazy {
WebtoonsUrlSigner("gUtPzJFZch4ZyAGviiyH94P99lQ3pFdRTwpJWDlSGFfwgpr6ses5ALOxWHOIT7R1")
}
@ -49,8 +47,17 @@ internal abstract class WebtoonsParser(
SortOrder.UPDATED,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override val userAgentKey = ConfigKey.UserAgent("nApps (Android 12;; linewebtoon; 3.1.0)")
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = getAllGenreList().values.toSet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -182,10 +189,9 @@ internal abstract class WebtoonsParser(
}
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val webtoons = when (filter) {
is MangaListFilter.Search -> {
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val webtoons = when {
!filter.query.isNullOrEmpty() -> {
makeRequest("/lineWebtoon/webtoon/searchWebtoon?query=${filter.query.urlEncoded()}").getJSONObject("webtoonSearch")
.getJSONArray("titleList").mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
@ -210,7 +216,7 @@ internal abstract class WebtoonsParser(
}
}
is MangaListFilter.Advanced -> {
else -> {
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
val genres = getAllGenreList()
@ -220,17 +226,14 @@ internal abstract class WebtoonsParser(
result = result.filter { it.manga.tags.contains(genres[genre]) }
}
when (filter.sortOrder) {
when (order) {
SortOrder.UPDATED -> result.sortedByDescending { it.date }
SortOrder.POPULARITY -> result.sortedByDescending { it.readCount }
SortOrder.RATING -> result.sortedByDescending { it.manga.rating }
//SortOrder.LIKE -> result.sortedBy { it.likeitCount }
else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}")
else -> throw IllegalArgumentException("Unsupported sort order: $order")
}
}
else -> getAllTitleList()
}
return webtoons.map { it.manga }.subList(offset, (offset + 20).coerceAtMost(webtoons.size))
}
@ -257,10 +260,6 @@ internal abstract class WebtoonsParser(
)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return getAllGenreList().values.toSet()
}
private suspend fun makeRequest(url: String): JSONObject {
val resp = webClient.httpGet(finalizeUrl(url))
val message: JSONObject? = resp.parseJson().optJSONObject("message")

@ -25,8 +25,6 @@ internal abstract class AnimeBootstrapParser(
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
@ -37,13 +35,21 @@ internal abstract class AnimeBootstrapParser(
protected open val listUrl = "/manga"
protected open val datePattern = "dd MMM. yyyy"
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
@ -52,13 +58,13 @@ internal abstract class AnimeBootstrapParser(
append(page.toString())
append("&type=all")
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&search=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&categorie=")
@ -66,7 +72,7 @@ internal abstract class AnimeBootstrapParser(
}
append("&sort=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("view")
SortOrder.UPDATED -> append("updated")
SortOrder.ALPHABETICAL -> append("default")
@ -75,8 +81,6 @@ internal abstract class AnimeBootstrapParser(
}
}
null -> append("&sort=updated")
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -100,7 +104,7 @@ internal abstract class AnimeBootstrapParser(
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
protected open suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
return doc.select("div.product__page__filter div:contains(Genre:) option ").mapNotNullToSet { option ->
val key = option.attr("value") ?: return@mapNotNullToSet null

@ -17,7 +17,6 @@ import java.util.*
internal class PapScan(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaParserSource.PAPSCAN, "papscan.com") {
override val sourceLocale: Locale = Locale.ENGLISH
override val isMultipleTagsSupported = false
override val listUrl = "/liste-manga"
override val selectState = "div.anime__details__widget li:contains(En cours)"
override val selectTag = "div.anime__details__widget li:contains(Genre) a"
@ -29,20 +28,20 @@ internal class PapScan(context: MangaLoaderContext) :
SortOrder.ALPHABETICAL_DESC,
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/filterList")
append("?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&alpha=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&cat=")
@ -50,7 +49,7 @@ internal class PapScan(context: MangaLoaderContext) :
}
append("&sortBy=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("views")
SortOrder.ALPHABETICAL_DESC -> append("name&asc=false")
SortOrder.ALPHABETICAL -> append("name&asc=true")
@ -58,8 +57,6 @@ internal class PapScan(context: MangaLoaderContext) :
}
}
null -> append("&sortBy=updated")
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -82,7 +79,7 @@ internal class PapScan(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
override suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
return doc.select("a.category ").mapNotNullToSet { a ->
val key = a.attr("href").substringAfterLast('=')

@ -19,19 +19,31 @@ import java.util.*
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.FLIXSCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.ADULT)
override val configKeyDomain = ConfigKey.Domain("flixscans.net")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.ADULT),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val json = when (filter) {
is MangaListFilter.Search -> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val json = when {
!filter.query.isNullOrEmpty() -> {
if (page > 1) {
return emptyList()
}
@ -41,7 +53,7 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
webClient.httpPost(url, body).parseJson().getJSONArray("data")
}
is MangaListFilter.Advanced -> {
else -> {
val url = buildString {
append("https://api.")
append(domain)
@ -90,11 +102,6 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
webClient.httpGet(url).parseJson().getJSONArray("data")
}
null -> {
val url = "https://api.$domain/api/v1/webtoon/pages/latest/romance?page=$page"
webClient.httpGet(url).parseJson().getJSONArray("data")
}
}
return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
@ -122,7 +129,7 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/search/advance").parseHtml()
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
val tagsList = json.getJSONArray(3).toString().replace("[", "").replace("]", "").split(",")

@ -16,7 +16,13 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("mangastorm.org")
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions()
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
@ -25,19 +31,19 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/mangas?page=")
append(page)
append("&query=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
@ -46,7 +52,7 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
append("?page=")
append(page)
} else {
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("/mangas?page=")
append(page)
} else {
@ -56,11 +62,6 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
}
null -> {
append("/mangas?page=")
append(page)
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -83,8 +84,6 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.selectFirstOrThrow(".card-body .col-lg-9")

@ -17,8 +17,6 @@ import java.util.*
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.TEAMXNOVEL, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("teamoney.site")
@ -27,16 +25,27 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
when {
is MangaListFilter.Search -> {
!filter.query.isNullOrEmpty() -> {
append("/?search=")
append(filter.query.urlEncoded())
if (page > 1) {
@ -45,7 +54,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
append("/series?genre=")
@ -56,7 +65,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
}
append("&")
} else {
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("/series")
SortOrder.UPDATED -> append("/")
else -> append("/")
@ -70,7 +79,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
if (filter.sortOrder == SortOrder.POPULARITY || filter.tags.isNotEmpty()) {
if (order == SortOrder.POPULARITY || filter.tags.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("status=")
append(
@ -84,8 +93,6 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
}
null -> append("/?page=$page")
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -115,7 +122,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/series").parseHtml()
return doc.requireElementById("select_genre").select("option").mapNotNullToSet {
MangaTag(

@ -25,6 +25,16 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
override val configKeyDomain = ConfigKey.Domain("anibel.net")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -34,31 +44,26 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
SortOrder.NEWEST,
)
override suspend fun getList(
offset: Int,
filter: MangaListFilter?,
): List<Manga> {
val filters =
when (filter) {
is MangaListFilter.Search -> {
return if (offset == 0) {
search(filter.query)
} else {
emptyList()
}
}
is MangaListFilter.Advanced -> {
filter.tags.takeUnless { it.isEmpty() }?.joinToString(
separator = ",",
prefix = "genres: [",
postfix = "]",
) { "\"${it.key}\"" }.orEmpty()
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val filters = when {
!filter.query.isNullOrEmpty() -> {
return if (offset == 0) {
search(filter.query)
} else {
emptyList()
}
}
null -> ""
else -> {
filter.tags.takeUnless { it.isEmpty() }?.joinToString(
separator = ",",
prefix = "genres: [",
postfix = "]",
) { "\"${it.key}\"" }.orEmpty()
}
}
val array = apiCall(
"""
getMediaList(offset: $offset, limit: 20, mediaType: manga, filters: {$filters}) {
@ -191,7 +196,7 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val json = apiCall(
"""
getFilters(mediaType: manga) {

@ -27,26 +27,36 @@ internal abstract class CupFoxParser(
SortOrder.UPDATED,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override val isMultipleTagsSupported = false
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search/")
append(filter.query.urlEncoded())
append('/')
append(page)
}
is MangaListFilter.Advanced -> {
else -> {
append("/category/")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("order/hits/")
SortOrder.UPDATED -> append("order/addtime/")
else -> append("order/addtime/")
@ -73,11 +83,6 @@ internal abstract class CupFoxParser(
append("page/")
append(page)
}
null -> {
append("/category/order/addtime/page/")
append(page)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
@ -193,7 +198,8 @@ internal abstract class CupFoxParser(
}
protected open val selectAvailableTags = "div.swiper-wrapper a[href*=tags], ul.stui-screen__list li a[href*=tags]"
override suspend fun getAvailableTags(): Set<MangaTag> {
protected open suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/category/").parseHtml()
return doc.select(selectAvailableTags)
.mapNotNullToSet { a ->

@ -25,31 +25,42 @@ internal class AsuraScansParser(context: MangaLoaderContext) :
SortOrder.ALPHABETICAL,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val configKeyDomain = ConfigKey.Domain("asuracomic.net")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = getOrCreateTagMap().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = true
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/series?page=")
append(page)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&name=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
append("&genres=")
@ -70,7 +81,7 @@ internal class AsuraScansParser(context: MangaLoaderContext) :
}
append("&types=-1&order=")
when (filter.sortOrder) {
when (order) {
SortOrder.RATING -> append("rating")
SortOrder.UPDATED -> append("update")
SortOrder.NEWEST -> append("latest")
@ -79,8 +90,6 @@ internal class AsuraScansParser(context: MangaLoaderContext) :
else -> append("update")
}
}
null -> append("&genres=&status=-1&order=update&types=-1")
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -113,10 +122,6 @@ internal class AsuraScansParser(context: MangaLoaderContext) :
private var tagCache: ArrayMap<String, MangaTag>? = null
private val mutex = Mutex()
override suspend fun getAvailableTags(): Set<MangaTag> {
return getOrCreateTagMap().values.toSet()
}
private suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>()

@ -22,14 +22,21 @@ internal class BeeToon(context: MangaLoaderContext) :
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
if (page > 1) {
return emptyList()
}
@ -37,7 +44,7 @@ internal class BeeToon(context: MangaLoaderContext) :
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
@ -47,7 +54,7 @@ internal class BeeToon(context: MangaLoaderContext) :
append(page)
append("/")
} else {
when (filter.sortOrder) {
when (order) {
SortOrder.UPDATED -> append("/latest-update/")
SortOrder.POPULARITY -> append("/popular-manga/")
else -> append("/latest-update/")
@ -57,8 +64,6 @@ internal class BeeToon(context: MangaLoaderContext) :
append("/")
}
}
null -> append("/latest-update/page-$page/")
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -85,7 +90,7 @@ internal class BeeToon(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
return doc.requireElementById("menu-item-3").select("ul.sub-menu li a").mapNotNullToSet {
MangaTag(

@ -1,16 +1,16 @@
package org.koitharu.kotatsu.parsers.site.en
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("CLONEMANGA", "CloneManga", "en")
internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.CLONEMANGA) {
internal class CloneMangaParser(context: MangaLoaderContext) :
SinglePageMangaParser(context, MangaParserSource.CLONEMANGA) {
override val availableSortOrders: Set<SortOrder> = Collections.singleton(
SortOrder.POPULARITY,
@ -18,32 +18,23 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte
override val configKeyDomain = ConfigKey.Domain("manga.clone-army.org")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions()
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
@InternalParsersApi
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val link = when (filter) {
is MangaListFilter.Search -> {
return emptyList()
}
is MangaListFilter.Advanced -> {
if (offset > 0) {
return emptyList()
}
"https://$domain/viewer_landing.php"
}
null -> "https://$domain/viewer_landing.php"
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
if (!filter.query.isNullOrEmpty()) {
return emptyList()
}
val doc = webClient.httpGet(link).parseHtml()
val doc = webClient.httpGet("https://$domain/viewer_landing.php").parseHtml()
val mangas = doc.getElementsByClass("comicPreviewContainer")
return mangas.mapNotNull { item ->
val background = item.selectFirstOrThrow(".comicPreview").styleValueOrNull("background")
@ -108,6 +99,4 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte
),
)
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
}

@ -17,27 +17,33 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("comixextra.com")
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/")
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("search?keyword=")
append(filter.query.urlEncoded())
if (page > 1) {
@ -46,7 +52,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty() && filter.states.isEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
@ -65,7 +71,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
} else if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED)
} else {
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("popular-comic")
SortOrder.UPDATED -> append("new-comic")
SortOrder.NEWEST -> append("recent-comic")
@ -78,15 +84,6 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
append(page.toString())
}
}
null -> {
append("popular-comic")
if (page > 1) {
append("/")
append(page.toString())
}
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -113,7 +110,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/popular-comic").parseHtml()
return doc.select("li.tag-item a").mapNotNullToSet { a ->
MangaTag(

@ -25,16 +25,23 @@ internal class DynastyScans(context: MangaLoaderContext) :
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
when {
!filter.query.isNullOrEmpty() -> {
val url = buildString {
append("https://")
append(domain)
@ -47,7 +54,7 @@ internal class DynastyScans(context: MangaLoaderContext) :
return parseMangaListQuery(webClient.httpGet(url).parseHtml())
}
is MangaListFilter.Advanced -> {
else -> {
val url = buildString {
append("https://")
@ -68,16 +75,6 @@ internal class DynastyScans(context: MangaLoaderContext) :
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/series?view=cover&page=")
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
}
}
@ -130,7 +127,7 @@ internal class DynastyScans(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..3).map { page ->
async { getTags(page) }

@ -20,25 +20,31 @@ internal class FlixScansOrg(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.FLIXSCANSORG, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.ADULT)
override val configKeyDomain = ConfigKey.Domain("flixscans.org")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.ADULT),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isSearchSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val json = when (filter) {
is MangaListFilter.Search -> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val json = when {
!filter.query.isNullOrEmpty() -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
else -> {
val url = buildString {
append("https://api.")
@ -75,11 +81,6 @@ internal class FlixScansOrg(context: MangaLoaderContext) :
}
webClient.httpGet(url).parseJson().getJSONArray("data")
}
null -> {
val url = "https://api.$domain/api/v1/search/advance?=&serie_type=webtoon&page=$page"
webClient.httpGet(url).parseJson().getJSONArray("data")
}
}
return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
@ -107,7 +108,7 @@ internal class FlixScansOrg(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/search/advance").parseHtml()
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
val tagsList = json.getJSONArray(3).toString().replace("[", "").replace("]", "").split(",")

@ -19,20 +19,26 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
override val configKeyDomain = ConfigKey.Domain("www.mgeko.cc", "www.mgeko.com", "www.mangageko.com")
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
if (page > 1) {
return emptyList()
}
@ -40,13 +46,13 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
append("/browse-comics/?results=")
append(page)
append("&filter=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("Updated")
SortOrder.NEWEST -> append("New")
@ -60,12 +66,6 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
}
}
}
null -> {
append("/browse-comics/?results=")
append(page)
append("&filter=Updated")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -88,7 +88,7 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/browse-comics/").parseHtml()
return doc.select("label.checkbox-inline").mapNotNullToSet { label ->
MangaTag(

@ -20,6 +20,15 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("www.mangakawaii.io")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -29,28 +38,25 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) :
.add("Accept-Language", "en")
.build()
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search?query=")
append(filter.query.urlEncoded())
append("&search_type=manga&page=")
append(page)
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.sortOrder == SortOrder.UPDATED && filter.tags.isNotEmpty()) {
if (order == SortOrder.UPDATED && filter.tags.isNotEmpty()) {
throw IllegalArgumentException("Filter part tag is not available with sort not updated")
}
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
if (order == SortOrder.ALPHABETICAL) {
append("/manga-list")
filter.tags.oneOrThrowIfMany()?.let {
append("/category/")
@ -62,12 +68,6 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) :
return emptyList()
}
}
null -> {
if (page > 1) {
return emptyList()
}
}
}
}
@ -162,7 +162,7 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/manga-list/").parseHtml()
return doc.select("ul li a.category").mapNotNullToSet { a ->
val name = a.text()

@ -28,26 +28,33 @@ internal class MangaTownParser(context: MangaLoaderContext) :
SortOrder.UPDATED,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override val isMultipleTagsSupported = false
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
),
availableContentRating = emptySet(),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search?name=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
is MangaListFilter.Advanced -> {
else -> {
append("/directory/")
append("0-")
@ -79,7 +86,7 @@ internal class MangaTownParser(context: MangaLoaderContext) :
append(".htm")
append(
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> ""
SortOrder.UPDATED -> "?last_chapter_time.za"
SortOrder.ALPHABETICAL -> "?name.az"
@ -88,8 +95,6 @@ internal class MangaTownParser(context: MangaLoaderContext) :
},
)
}
null -> append("/directory/$page.htm?last_chapter_time.za")
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -222,7 +227,7 @@ internal class MangaTownParser(context: MangaLoaderContext) :
return doc.requireElementById("image").attrAsAbsoluteUrl("src")
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("/directory/".toAbsoluteUrl(domain)).parseHtml()
val root = doc.body().selectFirst("aside.right")
?.getElementsContainingOwnText("Genres")

@ -25,32 +25,48 @@ internal class Mangaowl(context: MangaLoaderContext) :
SortOrder.UPDATED,
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("mangaowl.to")
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(
page: Int,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/10-search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
is MangaListFilter.Advanced -> {
else -> {
append("/10-comics")
append("?page=")
@ -74,7 +90,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
append("&ordering=")
append(
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> "view_count"
SortOrder.UPDATED -> "-modified_at"
SortOrder.NEWEST -> "created_at"
@ -83,11 +99,6 @@ internal class Mangaowl(context: MangaLoaderContext) :
},
)
}
null -> {
append("/10-comics?ordering=-modified_at&page=")
append(page.toString())
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -110,7 +121,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/10-genres").parseHtml()
return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a ->
val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-")

@ -10,7 +10,7 @@ import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("MANHWA18", "Manhwa18.net", "en", type = ContentType.HENTAI)
class Manhwa18Parser(context: MangaLoaderContext) :
internal class Manhwa18Parser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MANHWA18, pageSize = 18, searchPageSize = 18) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net")
@ -29,15 +29,24 @@ class Manhwa18Parser(context: MangaLoaderContext) :
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
)
private val tagsMap = SuspendLazy(::parseTags)
override val isTagsExclusionSupported = true
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
private val tagsMap = SuspendLazy(::parseTags)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tagsMap.get().values.toSet(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
),
availableContentRating = emptySet(),
)
override suspend fun getFavicons(): Favicons {
return Favicons(
@ -48,21 +57,20 @@ class Manhwa18Parser(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/tim-kiem?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&q=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
append("&accept_genres=")
if (filter.tags.isNotEmpty()) {
@ -80,7 +88,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
append("&sort=")
append(
when (filter.sortOrder) {
when (order) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top"
@ -103,8 +111,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
)
}
}
null -> append("&sort=update")
}
}
@ -221,10 +227,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return tagsMap.get().values.toSet()
}
private suspend fun parseTags(): Map<String, MangaTag> {
val doc = webClient.httpGet("https://$domain/tim-kiem?q=").parseHtml()
val list = doc.getElementsByAttribute("data-genre-id")

@ -11,7 +11,7 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MANHWASMEN", "ManhwasMen", "en", type = ContentType.HENTAI)
class ManhwasMen(context: MangaLoaderContext) :
internal class ManhwasMen(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MANHWASMEN, pageSize = 30, searchPageSize = 30) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwas.men")
@ -21,28 +21,38 @@ class ManhwasMen(context: MangaLoaderContext) :
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.POPULARITY)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
)
override suspend fun getListPage(
page: Int,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/manga-list")
append("?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&search=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&genero=")
@ -60,8 +70,6 @@ class ManhwasMen(context: MangaLoaderContext) :
)
}
}
null -> {}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -84,7 +92,7 @@ class ManhwasMen(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val tags = webClient.httpGet("https://$domain/manga-list").parseHtml()
.selectLastOrThrow(".filter-bx .form-group select.custom-select").select("option").drop(1)
return tags.mapNotNullToSet { option ->

@ -1,8 +1,8 @@
package org.koitharu.kotatsu.parsers.site.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
@ -10,33 +10,31 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("PO2SCANS", "Po2Scans", "en")
internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.PO2SCANS) {
internal class Po2Scans(context: MangaLoaderContext) : SinglePageMangaParser(context, MangaParserSource.PO2SCANS) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("po2scans.com")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions()
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (offset > 0) {
return emptyList()
}
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/series")
when (filter) {
is MangaListFilter.Search -> {
append("?search=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {}
null -> {}
if (!filter.query.isNullOrEmpty()) {
append("?search=")
append(filter.query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -59,8 +57,6 @@ internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, Mang
}
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("dd MMM, yy", Locale.ENGLISH)

@ -22,26 +22,33 @@ internal class Pururin(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("pururin.to")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
is MangaListFilter.Advanced -> {
else -> {
append("/browse")
filter.tags.oneOrThrowIfMany()?.let {
@ -54,7 +61,7 @@ internal class Pururin(context: MangaLoaderContext) :
append(page)
append("&sort=")
when (filter.sortOrder) {
when (order) {
SortOrder.UPDATED -> append("")
SortOrder.POPULARITY -> append("most-viewed")
SortOrder.RATING -> append("highest-rated")
@ -62,11 +69,6 @@ internal class Pururin(context: MangaLoaderContext) :
else -> append("")
}
}
null -> {
append("/browse?page=")
append(page)
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -89,7 +91,7 @@ internal class Pururin(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..4).map { page ->
async { getTags(page) }

@ -11,7 +11,7 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("VYMANGA", "VyManga", "en")
class VyManga(context: MangaLoaderContext) :
internal class VyManga(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.VYMANGA, pageSize = 36) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("vymanga.net")
@ -21,8 +21,6 @@ class VyManga(context: MangaLoaderContext) :
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.POPULARITY_ASC,
@ -34,22 +32,30 @@ class VyManga(context: MangaLoaderContext) :
SortOrder.UPDATED_ASC,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search?search_po=0&q=")
append(filter.query.urlEncoded())
append("&author_po=0&author=&completed=2&sort=updated_at&sort_type=desc&page=")
append(page)
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isEmpty()) {
@ -84,7 +90,7 @@ class VyManga(context: MangaLoaderContext) :
}
append("&sort=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("viewed&sort_type=desc")
SortOrder.POPULARITY_ASC -> append("viewed&sort_type=asc")
SortOrder.RATING -> append("scored&sort_type=desc")
@ -99,11 +105,6 @@ class VyManga(context: MangaLoaderContext) :
append("&page=")
append(page)
}
null -> {
append("/search?search_po=0&q=&author_po=0&author=&completed=2&sort=updated_at&sort_type=desc&page=")
append(page)
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -126,7 +127,7 @@ class VyManga(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
return doc.select("div.dropdown-menu.custom-menu ul li a[href*=genre]").mapNotNullToSet {
MangaTag(

@ -2,7 +2,6 @@ package org.koitharu.kotatsu.parsers.site.es
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@ -27,32 +26,21 @@ internal class TempleScanEsp(context: MangaLoaderContext) :
keys.add(userAgentKey)
}
override val isSearchSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities()
override suspend fun getFilterOptions() = MangaListFilterOptions()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.NEWEST) {
append("/comics?page=")
append(page.toString())
} else {
if (page > 1) {
return emptyList()
}
}
}
null -> {
append("/comics?page=")
append(page.toString())
if (order == SortOrder.NEWEST) {
append("/comics?page=")
append(page)
} else {
if (page > 1) {
return emptyList()
}
}
}
@ -79,8 +67,6 @@ internal class TempleScanEsp(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()

@ -14,7 +14,7 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("TUMANGAONLINE", "TuMangaOnline", "es")
class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
internal class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
context,
source = MangaParserSource.TUMANGAONLINE,
pageSize = 24,
@ -29,8 +29,6 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
@ -40,22 +38,33 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.RATING,
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableContentRating = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/library")
when (filter) {
when {
is MangaListFilter.Search -> {
!filter.query.isNullOrEmpty() -> {
append("?title=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
append("?order_item=")
append(
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> "likes_count&order_dir=desc"
SortOrder.POPULARITY_ASC -> "likes_count&order_dir=asc"
SortOrder.UPDATED -> "release_date&order_dir=desc"
@ -66,6 +75,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.ALPHABETICAL_DESC -> "alphabetically&order_dir=desc"
SortOrder.RATING -> "score&order_dir=desc"
SortOrder.RATING_ASC -> "score&order_dir=asc"
else -> "release_date&order_dir=desc"
},
)
append("&filter_by=title")
@ -87,10 +97,6 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
)
}
}
null -> {
append("?order_item=release_date&order_dir=desc&filter_by=title")
}
}
append("&_pg=1&page=")
append(page.toString())
@ -286,7 +292,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/library", getRequestHeaders()).parseHtml()
val elements = doc.body().select("div#books-genders > div > div")
return elements.mapNotNullToSet { element ->

@ -35,18 +35,17 @@ internal abstract class FmreaderParser(
SortOrder.ALPHABETICAL_DESC,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
)
override val isTagsExclusionSupported = true
protected open val listUrl = "/manga-list.html"
protected open val datePattern = "MMMM d, yyyy"
protected open val tagPrefix = "manga-list-genre-"
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
@ -72,20 +71,33 @@ internal abstract class FmreaderParser(
"drop",
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&name=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
append("&genre=")
append(filter.tags.joinToString(",") { it.key })
@ -95,7 +107,7 @@ internal abstract class FmreaderParser(
append("&sort=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("views&sort_type=DESC")
SortOrder.POPULARITY_ASC -> append("views&sort_type=ASC")
SortOrder.UPDATED -> append("last_update&sort_type=DESC")
@ -118,8 +130,6 @@ internal abstract class FmreaderParser(
}
}
null -> append("&sort=last_update")
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
@ -150,7 +160,7 @@ internal abstract class FmreaderParser(
protected open val selectBodyTag = "ul.filter-type li a"
override suspend fun getAvailableTags(): Set<MangaTag> {
protected open suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectBodyTag).mapNotNullToSet { a ->
val href = a.attr("href").substringAfter(tagPrefix).substringBeforeLast(".html")

@ -22,20 +22,20 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
override val selectPage = "div#chapter-content img"
override val selectBodyTag = "div.advanced-wrapper .genre_label"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/tim-kiem?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("&q=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
append("&accept_genres=")
append(filter.tags.joinToString(",") { it.key })
@ -45,7 +45,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
append("&sort=")
append(
when (filter.sortOrder) {
when (order) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top"
@ -68,14 +68,12 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
)
}
}
null -> append("&sort=update")
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getAvailableTags(): Set<MangaTag> {
override suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectBodyTag).mapNotNullToSet { label ->
val key = label.attr("data-genre-id")

@ -14,15 +14,19 @@ internal class OlimpoScans(context: MangaLoaderContext) :
override val selectAlt = "ul.manga-info li:contains(Otros nombres)"
override val selectTag = "ul.manga-info li:contains(Género) a"
override val tagPrefix = "lista-de-comics-genero-"
override val isMultipleTagsSupported = false
override val isTagsExclusionSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isMultipleTagsSupported = false,
isTagsExclusionSupported = false,
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append(listUrl)
append("?page=")
append(page.toString())
@ -30,7 +34,7 @@ internal class OlimpoScans(context: MangaLoaderContext) :
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/lista-de-comics-genero-")
@ -42,7 +46,7 @@ internal class OlimpoScans(context: MangaLoaderContext) :
append("?page=")
append(page.toString())
append("&sort=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("views&sort_type=DESC")
SortOrder.POPULARITY_ASC -> append("views&sort_type=ASC")
SortOrder.UPDATED -> append("last_update&sort_type=DESC")
@ -65,13 +69,6 @@ internal class OlimpoScans(context: MangaLoaderContext) :
)
}
}
null -> {
append(listUrl)
append("?page=")
append(page.toString())
append("&sort=last_update")
}
}
}
val doc = webClient.httpGet(url).parseHtml()

@ -32,67 +32,55 @@ internal abstract class FoolSlideParser(
protected open val pagination = true // false if the manga list has no pages
protected open val datePattern = "yyyy.MM.dd"
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val doc =
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
override suspend fun getFilterOptions() = MangaListFilterOptions()
val url = buildString {
append("https://")
append(domain)
append("/")
append(searchUrl)
}
webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml()
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val doc = when {
!filter.query.isNullOrEmpty() -> {
if (page > 1) {
return emptyList()
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
append("/")
append(listUrl)
// For some sites that don't have enough manga and page 2 links to page 1
if (!pagination) {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
}
}
webClient.httpGet(url).parseHtml()
val url = buildString {
append("https://")
append(domain)
append("/")
append(searchUrl)
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/")
append(listUrl)
if (!pagination) {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml()
}
else -> {
val url = buildString {
append("https://")
append(domain)
append("/")
append(listUrl)
// For some sites that don't have enough manga and page 2 links to page 1
if (!pagination) {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
}
webClient.httpGet(url).parseHtml()
}
webClient.httpGet(url).parseHtml()
}
}
return doc.select("div.list div.group").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
@ -114,8 +102,6 @@ internal abstract class FoolSlideParser(
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
protected open val selectInfo = "div.info"
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {

@ -37,29 +37,37 @@ internal class BentomangaParser(context: MangaLoaderContext) :
keys.add(userAgentKey)
}
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isTagsExclusionSupported: Boolean = true
init {
paginator.firstPage = 0
searchPaginator.firstPage = 0
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = urlBuilder()
.host(domain)
.addPathSegment("manga_list")
.addQueryParameter("limit", page.toString())
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
url.addQueryParameter("search", filter.query)
}
is MangaListFilter.Advanced -> {
else -> {
when (filter.sortOrder) {
when (order) {
SortOrder.UPDATED -> url.addQueryParameter("order_by", "update")
.addQueryParameter("order", "desc")
@ -104,8 +112,6 @@ internal class BentomangaParser(context: MangaLoaderContext) :
}
}
null -> url.addQueryParameter("order_by", "update")
}
val root = webClient.httpGet(url.build()).parseHtml().requireElementById("mangas_content")
return root.select(".manga[data-manga]").map { div ->
@ -208,7 +214,7 @@ internal class BentomangaParser(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val root = webClient.httpGet(urlBuilder().addPathSegment("manga_list").build())
.parseHtml()
.requireElementById("search_options-form")

@ -5,7 +5,7 @@ import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
@ -16,7 +16,7 @@ import java.util.*
@MangaSourceParser("FURYOSOCIETY", "FuryoSociety", "fr")
internal class FuryoSociety(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.FURYOSOCIETY, 0) {
SinglePageMangaParser(context, MangaParserSource.FURYOSOCIETY) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.UPDATED)
@ -24,13 +24,16 @@ internal class FuryoSociety(context: MangaLoaderContext) :
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities()
override suspend fun getFilterOptions() = MangaListFilterOptions()
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isSearchSupported = false
override suspend fun getFavicons(): Favicons {
return Favicons(
listOf(
@ -40,27 +43,20 @@ internal class FuryoSociety(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
else -> {
if (order == SortOrder.ALPHABETICAL) {
append("/mangas")
}
}
null -> {}
}
}
@ -86,10 +82,6 @@ internal class FuryoSociety(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
@ -101,7 +93,6 @@ internal class FuryoSociety(context: MangaLoaderContext) :
)
}
private fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().select("div.list.fs-chapter-list div.element").mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("div.title a")

@ -17,22 +17,31 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.PAUSED)
override val configKeyDomain = ConfigKey.Domain("legacy-scans.com")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.PAUSED),
availableContentRating = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val end = page * pageSize
val start = end - (pageSize - 1)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
if (page > 1) {
return emptyList()
}
@ -43,7 +52,7 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
return parseMangaListQuery(webClient.httpGet(url).parseJson())
}
is MangaListFilter.Advanced -> {
else -> {
val url = buildString {
append("https://api.")
append(domain)
@ -68,18 +77,6 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
}
return parseMangaList(webClient.httpGet(url).parseJson())
}
null -> {
val url = buildString {
append("https://api.")
append(domain)
append("/misc/comic/search/query?status=&order=&genreNames=&type=&start=")
append(start)
append("&end=")
append(end)
}
return parseMangaList(webClient.httpGet(url).parseJson())
}
}
}
@ -175,7 +172,7 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
val script = doc.requireElementById("__NUXT_DATA__").data()
.substringAfterLast("\"genres\"").substringBeforeLast("\"comics\"")

@ -18,58 +18,50 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context,
override val configKeyDomain = ConfigKey.Domain("lire-scan.me")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val doc =
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
val q = filter.query.urlEncoded().replace("%20", "+")
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val doc = when {
!filter.query.isNullOrEmpty() -> {
if (page > 1) {
return emptyList()
}
val q = filter.query.urlEncoded().replace("%20", "+")
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
filter.tags.oneOrThrowIfMany()?.let {
append("/manga/")
append(it.key)
}
else -> {
val url = buildString {
append("https://")
append(domain)
if (page > 1) {
append("/page/")
append(page)
append('/')
}
filter.tags.oneOrThrowIfMany()?.let {
append("/manga/")
append(it.key)
}
webClient.httpGet(url).parseHtml()
}
null -> {
val url = buildString {
append("https://")
append(domain)
if (page > 1) {
append("/page/")
append(page)
append('/')
}
if (page > 1) {
append("/page/")
append(page)
append('/')
}
webClient.httpGet(url).parseHtml()
}
webClient.httpGet(url).parseHtml()
}
}
return doc.select("div.sect__content.grid-items div.item-poster").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
@ -148,7 +140,7 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context,
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
return doc.select(".nav-menu li a").mapNotNullToSet { a ->
val key = a.attr("href").removeSuffix('/').substringAfterLast("manga/", "")

@ -23,15 +23,24 @@ internal class LugnicaScans(context: MangaLoaderContext) :
SortOrder.UPDATED,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isSearchSupported = false
override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com")
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = emptySet(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -53,15 +62,15 @@ internal class LugnicaScans(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
when {
!filter.query.isNullOrEmpty() -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
if (order == SortOrder.ALPHABETICAL) {
if (page > 1) {
return emptyList()
}
@ -92,16 +101,6 @@ internal class LugnicaScans(context: MangaLoaderContext) :
return parseMangaList(webClient.httpGet(url).parseJsonArray())
}
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/api/get/homegrid/")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseJsonArray())
}
}
}
@ -220,7 +219,4 @@ internal class LugnicaScans(context: MangaLoaderContext) :
}
return pages
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
}

@ -19,6 +19,15 @@ internal class MangaKawaii(context: MangaLoaderContext) : PagedMangaParser(conte
override val configKeyDomain = ConfigKey.Domain("www.mangakawaii.io")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -28,28 +37,25 @@ internal class MangaKawaii(context: MangaLoaderContext) : PagedMangaParser(conte
.add("Accept-Language", "fr")
.build()
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search?query=")
append(filter.query.urlEncoded())
append("&search_type=manga&page=")
append(page)
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.sortOrder == SortOrder.UPDATED && filter.tags.isNotEmpty()) {
if (order == SortOrder.UPDATED && filter.tags.isNotEmpty()) {
throw IllegalArgumentException("Filtrer part tag n'est pas disponible avec le tri pas mis à jour")
}
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
if (order == SortOrder.ALPHABETICAL) {
append("/manga-list")
filter.tags.oneOrThrowIfMany()?.let {
append("/category/")
@ -61,12 +67,6 @@ internal class MangaKawaii(context: MangaLoaderContext) : PagedMangaParser(conte
return emptyList()
}
}
null -> {
if (page > 1) {
return emptyList()
}
}
}
}
@ -161,7 +161,7 @@ internal class MangaKawaii(context: MangaLoaderContext) : PagedMangaParser(conte
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/manga-list/").parseHtml()
return doc.select("ul li a.category").mapNotNullToSet { a ->
val name = a.text()

@ -32,25 +32,30 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
SortOrder.NEWEST,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("www.manga-mana.com")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED),
availableContentRating = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val postData = buildString {
append("page=")
append(page)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
if (page > 1) {
return emptyList()
}
@ -88,9 +93,9 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.sortOrder == SortOrder.UPDATED) {
if (order == SortOrder.UPDATED) {
if (filter.tags.isNotEmpty() or filter.states.isNotEmpty()) {
throw IllegalArgumentException("Le filtrage par « tri par : mis à jour » avec les genres ou les statuts n'est pas pris en charge par cette source.")
@ -99,7 +104,7 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
val doc = webClient.httpGet("https://$domain/?page=$page").parseHtml()
return doc.select("div.row div.col_home").map { div ->
val href = div.selectFirstOrThrow("h4 a").attrAsRelativeUrl("href")
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() ?: false
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() == true
val img = if (isNsfw) {
div.selectFirst("img")?.attr("data-adult")
} else {
@ -140,7 +145,7 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
}
append("&sort_by=")
when (filter.sortOrder) {
when (order) {
SortOrder.RATING -> append("score&sort_dir=desc")
SortOrder.NEWEST -> append("updated_at&sort_dir=desc")
SortOrder.ALPHABETICAL -> append("name&sort_dir=asc")
@ -149,8 +154,6 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
}
}
}
null -> append("&sort_by=updated_at&sort_dir=desc")
}
}
@ -162,7 +165,7 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
return doc.select("div.p-2 div.col").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() ?: false
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() == true
val img = if (isNsfw) {
div.selectFirst("img")?.attr("data-adult")
} else {
@ -323,7 +326,7 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
return pages
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/liste-mangas").parseHtml()
return doc.select("select.selectpicker option").drop(1).mapNotNullToSet {
MangaTag(

@ -5,7 +5,7 @@ import org.json.JSONArray
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
@ -14,7 +14,7 @@ import java.util.*
@MangaSourceParser("SCANS_MANGAS_ME", "ScansMangas.me", "fr")
internal class ScansMangasMe(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.SCANS_MANGAS_ME, 0) {
SinglePageMangaParser(context, MangaParserSource.SCANS_MANGAS_ME) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,
@ -27,28 +27,32 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/?s=")
append(filter.query.urlEncoded())
append("&post_type=manga")
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
append("/genres/")
filter.tags.oneOrThrowIfMany()?.let {
@ -56,7 +60,7 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
} else {
append("/tous-nos-mangas/?order=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("popular")
SortOrder.UPDATED -> append("update")
SortOrder.ALPHABETICAL -> append("title")
@ -65,8 +69,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
}
}
null -> append("/tous-nos-mangas/?order=update")
}
}
@ -91,7 +93,7 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/tous-nos-mangas/").parseHtml()
return doc.select("ul.genre li").mapNotNullToSet { li ->
val key = li.selectFirstOrThrow("a").attr("href").removeSuffix('/').substringAfterLast('/')

@ -20,30 +20,37 @@ internal class ScantradUnion(context: MangaLoaderContext) :
SortOrder.UPDATED,
)
override val isMultipleTagsSupported = false
override val configKeyDomain = ConfigKey.Domain("scantrad-union.com")
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/page/")
append(page.toString())
append("/?s=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
@ -53,25 +60,19 @@ internal class ScantradUnion(context: MangaLoaderContext) :
append("/")
}
} else {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
if (order == SortOrder.ALPHABETICAL) {
append("/manga/page/")
append(page.toString())
append("/")
}
if (filter.sortOrder == SortOrder.UPDATED && page > 1) {
if (order == SortOrder.UPDATED && page > 1) {
return emptyList()
}
}
}
null -> {
append("/manga/page/")
append(page.toString())
append("/")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -183,7 +184,7 @@ internal class ScantradUnion(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
val body = doc.body()
val list = body.select(".asp_gochosen")[1].select("option").orEmpty()

@ -31,12 +31,6 @@ internal abstract class FuzzyDoodleParser(
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isMultipleTagsSupported = true
@JvmField
protected val ongoing = scatterSetOf(
"en cours",
@ -73,21 +67,33 @@ internal abstract class FuzzyDoodleParser(
protected open val pausedValue = "haitus"
protected open val abandonedValue = "dropped"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/manga?page=")
append(page)
when (filter) {
when {
is MangaListFilter.Search -> {
!filter.query.isNullOrEmpty() -> {
append("&title=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
append("&type=")
append("&status=")
@ -110,8 +116,6 @@ internal abstract class FuzzyDoodleParser(
append(it.key)
}
}
null -> {}
}
}
@ -252,7 +256,7 @@ internal abstract class FuzzyDoodleParser(
protected open val selectTagsList = "div.mt-1 div.items-center:has(label)"
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/manga").parseHtml()
return doc.select(selectTagsList).mapNotNullToSet {
val key = it.selectFirst("input")?.attr("value") ?: return@mapNotNullToSet null

@ -6,14 +6,17 @@ import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser
import java.util.EnumSet
import java.util.*
@MangaSourceParser("HENTAISLAYER", "HentaiSlayer", "ar", ContentType.HENTAI)
internal class HentaiSlayer(context: MangaLoaderContext) :
FuzzyDoodleParser(context, MangaParserSource.HENTAISLAYER, "hentaislayer.net") {
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val ongoingValue = "مستمر"
override val finishedValue = "مكتمل"
override val abandonedValue = "متوقف"
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED),
)
}

@ -5,12 +5,16 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser
import java.util.EnumSet
import java.util.*
@MangaSourceParser("LELSCANVF", "LelScanFr", "fr")
internal class LelScanVf(context: MangaLoaderContext) :
FuzzyDoodleParser(context, MangaParserSource.LELSCANVF, "lelscanfr.com") {
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val ongoingValue = "en-cours"
override val finishedValue = "termin"
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
)
}

@ -30,20 +30,47 @@ internal abstract class GalleryAdultsParser(
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableLocales = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("id"),
Locale.ITALIAN,
Locale("pt"),
Locale("tr"),
Locale("th"),
Locale("vi"),
),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(
page: Int,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search/?q=")
append(filter.query.urlEncoded())
append("&")
}
is MangaListFilter.Advanced -> {
else -> {
val tag = filter.tags.oneOrThrowIfMany()
val lang = filter.locale
if (tag != null && lang != null) {
@ -61,8 +88,6 @@ internal abstract class GalleryAdultsParser(
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
@ -102,7 +127,7 @@ internal abstract class GalleryAdultsParser(
//Tags are deliberately reduced because there are too many and this slows down the application.
//only the most popular ones are taken.
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..3).map { page ->
async { getTags(page) }
@ -110,23 +135,6 @@ internal abstract class GalleryAdultsParser(
}.awaitAll().flattenTo(ArraySet(360))
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("id"),
Locale.ITALIAN,
Locale("pt"),
Locale("tr"),
Locale("th"),
Locale("vi"),
)
protected open val pathTagUrl = "/tags/popular/?page="
protected open val selectTags = ".tags_page ul.tags li"

@ -14,6 +14,7 @@ import java.util.*
@MangaSourceParser("ASMHENTAI", "AsmHentai", type = ContentType.HENTAI)
internal class AsmHentai(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaParserSource.ASMHENTAI, "asmhentai.com") {
override val selectGallery = ".preview_item"
override val selectGalleryLink = ".image a"
override val selectGalleryImg = ".image img"
@ -21,11 +22,13 @@ internal class AsmHentai(context: MangaLoaderContext) :
override val selectAuthor = "div.tags:contains(Artists:) .tag_list a span.tag"
override val idImg = "fimg"
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("tr"),
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableLocales = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("tr"),
),
)
override fun Element.parseTags() = select("a").mapToSet {

@ -21,10 +21,12 @@ internal class DoujinDesuUk(context: MangaLoaderContext) :
override val selectLanguageChapter = "div.tag-container:contains(Languages) a"
override val idImg = "image-container"
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableLocales = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
),
)
override fun parseMangaList(doc: Document): List<Manga> {

@ -23,38 +23,43 @@ internal class Hentai3(context: MangaLoaderContext) :
override val selectUrlChapter = "#main-cover a"
override val idImg = ".js-main-img"
override val isMultipleTagsSupported = true
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isMultipleTagsSupported = true,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale.ITALIAN,
Locale("pt"),
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableLocales = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale.ITALIAN,
Locale("pt"),
),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
when {
is MangaListFilter.Search -> {
!filter.query.isNullOrEmpty() -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search?q=")
append(buildQuery(filter.tags, filter.locale))
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&page=")
@ -64,7 +69,7 @@ internal class Hentai3(context: MangaLoaderContext) :
append(filter.locale.toLanguagePath())
append("/")
append(page.toString())
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("?sort=popular")
}
} else if (filter.tags.isNotEmpty()) {
@ -74,7 +79,7 @@ internal class Hentai3(context: MangaLoaderContext) :
}
append("/")
append(page.toString())
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("?sort=popular")
}
} else {
@ -82,11 +87,6 @@ internal class Hentai3(context: MangaLoaderContext) :
append(page)
}
}
null -> {
append("/")
append(page)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())

@ -24,18 +24,32 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableLocales = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("pt"),
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search/?s_key=")
append(filter.query.urlEncoded())
append("&")
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
if (filter.locale != null) {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED)
@ -43,7 +57,7 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("/popular")
}
append("/?")
@ -56,8 +70,6 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
@ -65,16 +77,4 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("pt"),
)
}

@ -18,17 +18,21 @@ internal class HentaiEra(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isMultipleTagsSupported = true,
)
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableLocales = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
),
)
override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {
@ -41,22 +45,22 @@ internal class HentaiEra(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
when {
is MangaListFilter.Search -> {
!filter.query.isNullOrEmpty() -> {
append("/search/?key=")
append(filter.query.urlEncoded())
append("&")
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search/?key=")
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append(
buildQuery(filter.tags, filter.locale)
.replace("&lt=1&dl=0&pp=0&tr=0", "&lt=0&dl=0&pp=1&tr=0"),
@ -72,7 +76,7 @@ internal class HentaiEra(context: MangaLoaderContext) :
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
@ -81,7 +85,7 @@ internal class HentaiEra(context: MangaLoaderContext) :
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
@ -89,8 +93,6 @@ internal class HentaiEra(context: MangaLoaderContext) :
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page.toString())

@ -21,47 +21,54 @@ internal class HentaiForce(context: MangaLoaderContext) :
override val selectLanguageChapter = "div.tag-container:contains(Languages:) a"
override val idImg = ".gallery-reader-img-wrapper img"
override val isMultipleTagsSupported = true
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isMultipleTagsSupported = true,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("id"),
Locale.ITALIAN,
Locale("pt"),
Locale("th"),
Locale("vi"),
)
override suspend fun getFilterOptions(): MangaListFilterOptions {
return super.getFilterOptions().copy(
availableLocales = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("id"),
Locale.ITALIAN,
Locale("pt"),
Locale("th"),
Locale("vi"),
),
)
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found")
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search?q=")
append(buildQuery(filter.tags, filter.locale))
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&page=")
@ -72,7 +79,7 @@ internal class HentaiForce(context: MangaLoaderContext) :
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
@ -81,7 +88,7 @@ internal class HentaiForce(context: MangaLoaderContext) :
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
@ -89,8 +96,6 @@ internal class HentaiForce(context: MangaLoaderContext) :
append("/page/")
}
}
null -> append("/page/")
}
append(page.toString())
}

@ -18,16 +18,19 @@ internal class HentaiFox(context: MangaLoaderContext) :
override val selectTag = "ul.tags"
override val selectLanguageChapter = "ul.languages a.tag_btn"
override val isMultipleTagsSupported = true
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isMultipleTagsSupported = true,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append("/search/?q=")
append(filter.query.urlEncoded())
if (page > 1) {
@ -36,7 +39,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
}
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search/?q=")
append(buildQuery(filter.tags, filter.locale))
@ -45,7 +48,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
append(page.toString())
}
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("&sort=popular")
}
} else if (filter.tags.isNotEmpty()) {
@ -54,7 +57,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
append(it.key)
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("popular/")
}
@ -67,7 +70,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
append("/language/")
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
if (order == SortOrder.POPULARITY) {
append("popular/")
}
@ -88,18 +91,6 @@ internal class HentaiFox(context: MangaLoaderContext) :
}
}
}
null -> {
if (page > 2) {
append("/pag/")
append(page.toString())
append("/")
} else if (page > 1) {
append("/page/")
append(page.toString())
append("/")
}
}
}
}

@ -4,6 +4,7 @@ import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
@ -20,15 +21,19 @@ internal class HentaiRox(context: MangaLoaderContext) :
override val selectAuthor = "li:contains(Artists:) span.item_name"
override val selectLanguageChapter = "li:contains(Languages:) .item_name"
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
)
override suspend fun getFilterOptions(): MangaListFilterOptions {
return super.getFilterOptions().copy(
availableLocales = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
),
)
}
override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')

@ -26,22 +26,28 @@ internal class NHentaiParser(context: MangaLoaderContext) :
".tag-container:contains(Languages:) span.tags a:not(.tag-17249) span.name" // tag-17249 = translated
override val idImg = "image-container"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.POPULARITY_TODAY, SortOrder.POPULARITY_WEEK)
override val isMultipleTagsSupported = true
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isMultipleTagsSupported = true,
)
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableLocales = setOf(Locale.ENGLISH, Locale.JAPANESE, Locale.CHINESE),
)
override fun getRequestHeaders(): Headers = super.getRequestHeaders().newBuilder()
.set("User-Agent", config[userAgentKey])
.build()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
// Check if the query is all numbers
val numericQuery = filter.query.trim()
if (numericQuery.matches("\\d+".toRegex())) {
@ -56,49 +62,23 @@ internal class NHentaiParser(context: MangaLoaderContext) :
}
}
is MangaListFilter.Advanced -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search/?q=")
append(buildQuery(filter.tags, filter.locale).urlEncoded())
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&")
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular")
}
if (page > 1) {
append("?")
}
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular")
}
if (page > 1) {
append("?")
}
} else {
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/?sort=popular&")
} else {
append("/?")
}
else -> {
append("/search/?q=pages:>0 ")
// for Search with query
// append(filter.query.urlEncoded())
// append(' ')
append(buildQuery(filter.tags, filter.locale).urlEncoded())
when (order) {
SortOrder.POPULARITY -> append("&sort=popular")
SortOrder.POPULARITY_TODAY -> append("&sort=popular-today")
SortOrder.POPULARITY_WEEK -> append("&sort=popular-week")
SortOrder.UPDATED -> {}
else -> {}
}
}
null -> append("/?")
}
if (page > 1) {
append("page=")
append("&page=")
append(page.toString())
}
}
@ -146,12 +126,6 @@ internal class NHentaiParser(context: MangaLoaderContext) :
)
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)

@ -25,25 +25,31 @@ internal abstract class GattsuParser(
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
protected open val tagPrefix = "tag"
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
protected open val tagPrefix = "tag"
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
when {
is MangaListFilter.Search -> {
!filter.query.isNullOrEmpty() -> {
append("/page/")
append(page.toString())
append("/?s=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
filter.tags.oneOrThrowIfMany()?.let {
append("/$tagPrefix/")
@ -54,11 +60,6 @@ internal abstract class GattsuParser(
append(page.toString())
}
null -> {
append("/page/")
append(page.toString())
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
@ -92,7 +93,7 @@ internal abstract class GattsuParser(
protected open val tagUrl = "generos"
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$tagUrl/").parseHtml()
return doc.selectLastOrThrow(".meio-conteudo p, div.lista-tags ul").parseTags()
}

@ -13,7 +13,17 @@ internal class UniversoHentai(context: MangaLoaderContext) :
override val tagPrefix = "category"
override suspend fun getAvailableTags(): Set<MangaTag> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/tags/").parseHtml()
return doc.requireElementById("menu-topo").parseTags()
}

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.site.guya
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
@ -12,11 +12,17 @@ internal abstract class GuyaParser(
context: MangaLoaderContext,
source: MangaParserSource,
domain: String,
pageSize: Int = 0,
) : PagedMangaParser(context, source, pageSize) {
) : SinglePageMangaParser(context, source) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions()
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -24,25 +30,17 @@ internal abstract class GuyaParser(
override val configKeyDomain = ConfigKey.Domain(domain)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) return emptyList()
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/api/get_all_series/")
}
when (filter) {
is MangaListFilter.Search -> {
return parseMangaList(webClient.httpGet(url).parseJson(), filter.query)
}
is MangaListFilter.Advanced -> {}
null -> {}
return if (!filter.query.isNullOrEmpty()) {
parseMangaList(webClient.httpGet(url).parseJson(), filter.query)
} else {
parseMangaList(webClient.httpGet(url).parseJson(), "")
}
return parseMangaList(webClient.httpGet(url).parseJson(), "")
}
protected open fun parseMangaList(json: JSONObject, query: String): List<Manga> {
@ -79,8 +77,6 @@ internal abstract class GuyaParser(
)
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga {
val json = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseJson().getJSONObject("chapters")
val slug = manga.url.removeSuffix('/').substringAfterLast('/')

@ -39,27 +39,38 @@ internal abstract class HeanCms(
SortOrder.POPULARITY_ASC,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
protected open val pathManga = "series"
protected open val apiPath
get() = getDomain("api")
protected open val paramsUpdated = "latest"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(apiPath)
append("/query?query_string=&series_type=Comic&perPage=$pageSize")
when (filter) {
is MangaListFilter.Search -> {
when {
!filter.query.isNullOrEmpty() -> {
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
else -> {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
@ -75,7 +86,7 @@ internal abstract class HeanCms(
}
append("&orderBy=")
when (filter.sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.POPULARITY_ASC -> append("total_views&order=asc")
SortOrder.UPDATED -> append("$paramsUpdated&order=desc")
@ -91,7 +102,6 @@ internal abstract class HeanCms(
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
}
null -> append("&status=All&orderBy=$paramsUpdated&order=desc")
}
append("&page=")
append(page.toString())
@ -180,7 +190,7 @@ internal abstract class HeanCms(
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
val regex = Regex("\"tags\\\\?\":\\s*\\[(.+?)]\\s*[},]")
val tags = doc.select("script").joinToString("") { it.html() }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save