Merge pull request #1063 from KotatsuApp/feature/advanced_filter
Advanced filter
This commit is contained in:
1
.gitignore
vendored
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()
|
||||
public abstract val filterCapabilities: MangaListFilterCapabilities
|
||||
|
||||
public val config: MangaSourceConfig by lazy { context.getConfig(source) }
|
||||
|
||||
open val availableContentRating: Set<ContentRating>
|
||||
get() = emptySet()
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param order one of [availableSortOrders] or [defaultSortOrder] for default value
|
||||
* @param filter is a set of filter rules
|
||||
*/
|
||||
@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
|
||||
*/
|
||||
@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)
|
||||
internal open suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
|
||||
|
||||
/**
|
||||
* Fetch available tags (genres) for source
|
||||
*/
|
||||
abstract suspend fun getAvailableTags(): Set<MangaTag>
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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,
|
||||
) {
|
||||
|
||||
fun isEmpty(): Boolean
|
||||
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()
|
||||
|
||||
val sortOrder: SortOrder?
|
||||
public companion object {
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
@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 filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isMultipleTagsSupported = true,
|
||||
isTagsExclusionSupported = true,
|
||||
isSearchSupported = true,
|
||||
)
|
||||
|
||||
override val isTagsExclusionSupported: Boolean = 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)
|
||||
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"
|
||||
},
|
||||
"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("<=1&pp=0")
|
||||
SortOrder.POPULARITY -> append("<=0&pp=1")
|
||||
SortOrder.RATING -> append("<=0&pp=0")
|
||||
else -> append("<=1&pp=0")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("<=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,117 +142,80 @@ 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")
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
|
||||
|
||||
val sortOrderStr = when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> "UPDATE"
|
||||
SortOrder.POPULARITY -> "READ_COUNT"
|
||||
SortOrder.RATING -> "LIKEIT"
|
||||
else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}")
|
||||
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 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
|
||||
|
||||
val sortOrderStr = when (order) {
|
||||
SortOrder.UPDATED -> "UPDATE"
|
||||
SortOrder.POPULARITY -> "READ_COUNT"
|
||||
SortOrder.RATING -> "LIKEIT"
|
||||
else -> throw IllegalArgumentException("Unsupported sort order: $order")
|
||||
}
|
||||
|
||||
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 filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isMultipleTagsSupported = true,
|
||||
isTagsExclusionSupported = true,
|
||||
isSearchSupported = true,
|
||||
isSearchWithFiltersSupported = true,
|
||||
isYearSupported = true,
|
||||
isYearRangeSupported = true,
|
||||
isOriginalLocaleSupported = true,
|
||||
)
|
||||
|
||||
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::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 val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
|
||||
|
||||
override val isTagsExclusionSupported: Boolean = true
|
||||
|
||||
|
||||
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,14 +19,45 @@ 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 isTagsExclusionSupported: Boolean = true
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("mangapark.net")
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isMultipleTagsSupported = true,
|
||||
isTagsExclusionSupported = true,
|
||||
isSearchSupported = true,
|
||||
)
|
||||
|
||||
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)
|
||||
keys.add(userAgentKey)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isSearchSupported = true,
|
||||
)
|
||||
|
||||
override suspend fun getFilterOptions() = MangaListFilterOptions()
|
||||
|
||||
private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
|
||||
|
||||
// no tags or tag search available
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
filter.tags.takeUnless { it.isEmpty() }?.joinToString(
|
||||
separator = ",",
|
||||
prefix = "genres: [",
|
||||
postfix = "]",
|
||||
) { "\"${it.key}\"" }.orEmpty()
|
||||
}
|
||||
|
||||
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 getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
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)
|
||||
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 suspend fun getFilterOptions() = MangaListFilterOptions(
|
||||
availableTags = fetchAvailableTags(),
|
||||
availableStates = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
),
|
||||
availableContentRating = emptySet(),
|
||||
)
|
||||
|
||||
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?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,16 +29,25 @@ class Manhwa18Parser(context: MangaLoaderContext) :
|
||||
SortOrder.RATING,
|
||||
)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
MangaState.PAUSED,
|
||||
)
|
||||
|
||||
override val isTagsExclusionSupported = true
|
||||
|
||||
private val tagsMap = SuspendLazy(::parseTags)
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isMultipleTagsSupported = true,
|
||||
isTagsExclusionSupported = true,
|
||||
isSearchSupported = true,
|
||||
)
|
||||
|
||||
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(
|
||||
listOf(
|
||||
@@ -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 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)
|
||||
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,68 +32,56 @@ 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.httpGet(url).parseHtml()
|
||||
|
||||
}
|
||||
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()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return doc.select("div.list div.group").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
@@ -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,59 +18,51 @@ 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()
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/manga/")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
append('/')
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
append('/')
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).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()
|
||||
}
|
||||
|
||||
else -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/manga/")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
append('/')
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
}
|
||||
}
|
||||
|
||||
return doc.select("div.sect__content.grid-items div.item-poster").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
@@ -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 getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
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,
|
||||
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("<=1&dl=0&pp=0&tr=0", "<=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,
|
||||
)
|
||||
|
||||
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
||||
availableTags = fetchAvailableTags(),
|
||||
)
|
||||
|
||||
protected open val tagPrefix = "tag"
|
||||
|
||||
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("/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
Reference in New Issue
Block a user