Global refactoring: migrate getList to new filter

master
Koitharu 2 years ago
parent ae9a7c6090
commit c5d3a7b0c1
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.parsers package org.koitharu.kotatsu.parsers
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import okhttp3.Headers import okhttp3.Headers
@ -32,15 +31,27 @@ abstract class MangaParser @InternalParsersApi constructor(
* *
* For better performance use [EnumSet] for more than one item. * For better performance use [EnumSet] for more than one item.
*/ */
@Deprecated("")
open val availableStates: Set<MangaState> open val availableStates: Set<MangaState>
get() = emptySet() get() = emptySet()
open val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = isMultipleTagsSupported,
isTagsExclusionSupported = isTagsExclusionSupported,
isSearchSupported = isSearchSupported,
isSearchWithFiltersSupported = searchSupportedWithMultipleFilters,
isYearSupported = isSearchYearSupported,
isYearRangeSupported = isSearchYearRangeSupported,
isSourceLocaleSupported = isSearchOriginalLanguages,
)
/** /**
* Supported [ContentRating] variants for filtering. May be empty. * Supported [ContentRating] variants for filtering. May be empty.
* *
* For better performance use [EnumSet] for more than one item. * For better performance use [EnumSet] for more than one item.
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val availableContentRating: Set<ContentRating> open val availableContentRating: Set<ContentRating>
get() = emptySet() get() = emptySet()
@ -49,6 +60,7 @@ abstract class MangaParser @InternalParsersApi constructor(
* *
* For better performance use [EnumSet] for more than one item. * For better performance use [EnumSet] for more than one item.
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val availableContentTypes: Set<ContentType> open val availableContentTypes: Set<ContentType>
get() = emptySet() get() = emptySet()
@ -57,44 +69,50 @@ abstract class MangaParser @InternalParsersApi constructor(
* *
* For better performance use [EnumSet] for more than one item. * For better performance use [EnumSet] for more than one item.
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val availableDemographics: Set<Demographic> open val availableDemographics: Set<Demographic>
get() = emptySet() get() = emptySet()
/** /**
* Whether parser supports filtering by more than one tag * Whether parser supports filtering by more than one tag
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val isMultipleTagsSupported: Boolean = true open val isMultipleTagsSupported: Boolean = true
/** /**
* Whether parser supports tagsExclude field in filter * Whether parser supports tagsExclude field in filter
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val isTagsExclusionSupported: Boolean = false open val isTagsExclusionSupported: Boolean = false
/** /**
* Whether parser supports searching by string query using [MangaListFilter.Search] * Whether parser supports searching by string query using [MangaListFilter.Search]
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val isSearchSupported: Boolean = true open val isSearchSupported: Boolean = true
/** /**
* Whether parser supports searching by string query using [MangaListFilter.Advanced] * Whether parser supports searching by string query using [MangaListFilter.Advanced]
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val searchSupportedWithMultipleFilters: Boolean = false open val searchSupportedWithMultipleFilters: Boolean = false
/** /**
* Whether parser supports searching by year * Whether parser supports searching by year
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val isSearchYearSupported: Boolean = false open val isSearchYearSupported: Boolean = false
/** /**
* Whether parser supports searching by year range * Whether parser supports searching by year range
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val isSearchYearRangeSupported: Boolean = false open val isSearchYearRangeSupported: Boolean = false
/** /**
* Whether parser supports searching Original Languages * Whether parser supports searching Original Languages
*/ */
@Deprecated("Use getListFilterCapabilities instead")
open val isSearchOriginalLanguages: Boolean = false open val isSearchOriginalLanguages: Boolean = false
@ -129,7 +147,6 @@ abstract class MangaParser @InternalParsersApi constructor(
/** /**
* Used as fallback if value of `sortOrder` passed to [getList] is null * Used as fallback if value of `sortOrder` passed to [getList] is null
*/ */
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
open val defaultSortOrder: SortOrder open val defaultSortOrder: SortOrder
get() { get() {
val supported = availableSortOrders val supported = availableSortOrders
@ -144,141 +161,10 @@ abstract class MangaParser @InternalParsersApi constructor(
* *
* @param offset starting from 0 and used for pagination. * @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. * 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 order one of [availableSortOrders] or null for default value
* @param tags genres for filtering, values from [getAvailableTags] and [Manga.tags]. May be null or empty * @param filter is a set of filter rules
* @param sortOrder one of [availableSortOrders] or null for default value
*/
@JvmSynthetic
@InternalParsersApi
@Deprecated(
"Use getList with filter instead",
replaceWith = ReplaceWith("getList(offset, filter)"),
)
open suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getList(offset, filter) instead")
/**
* Parse list of manga with search by text query
*
* @param offset starting from 0 and used for pagination.
* @param query search query
*/
@Deprecated(
"Use getList with filter instead",
ReplaceWith(
"getList(offset, SortOrder.RELEVANCE, MangaListFilterV2(query = 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( abstract suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga>
"Use getList with filter instead",
ReplaceWith(
"getList(offset, sortOrder, MangaListFilterV2(tags = tags))",
"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,
localeMangas = null,
states = emptySet(),
contentRating = emptySet(),
query = null,
year = null,
yearFrom = null,
yearTo = null,
types = emptySet(),
demographics = emptySet(),
),
)
}
@Deprecated(
"Use getList with filter instead",
ReplaceWith(
"getList(offset, filter.sortOrder, filter)",
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
),
)
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,
)
}
}
open suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2) = getList(
offset = offset,
filter = when {
filter.query.isNullOrEmpty() -> MangaListFilter.Advanced(
sortOrder = order,
tags = filter.tags,
tagsExclude = filter.tagsExclude,
locale = filter.locale,
localeMangas = filter.sourceLocale,
states = filter.states,
contentRating = filter.contentRating,
query = filter.query,
year = filter.year,
yearFrom = filter.yearFrom,
yearTo = filter.yearTo,
types = filter.types,
demographics = filter.demographics,
)
else -> MangaListFilter.Search(
query = filter.query,
)
},
)
/** /**
* Parse details for [Manga]: chapters list, description, large cover, etc. * Parse details for [Manga]: chapters list, description, large cover, etc.
@ -301,32 +187,26 @@ abstract class MangaParser @InternalParsersApi constructor(
/** /**
* Fetch available tags (genres) for source * Fetch available tags (genres) for source
*/ */
abstract suspend fun getAvailableTags(): Set<MangaTag> @Deprecated("Use getListFilterDatasets instead")
open suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
open suspend fun getListFilterCapabilities(): MangaListFilterCapabilities = coroutineScope { open suspend fun getFilterOptions(): MangaListFilterOptions = coroutineScope {
val tagsDeferred = async { getAvailableTags() } val tagsDeferred = async { getAvailableTags() }
val localesDeferred = async { getAvailableLocales() } val localesDeferred = async { getAvailableLocales() }
MangaListFilterCapabilities( MangaListFilterOptions(
availableSortOrders = availableSortOrders,
availableTags = tagsDeferred.await(), availableTags = tagsDeferred.await(),
availableStates = availableStates, availableStates = availableStates,
availableContentRating = availableContentRating, availableContentRating = availableContentRating,
availableContentTypes = availableContentTypes, availableContentTypes = availableContentTypes,
availableDemographics = availableDemographics, availableDemographics = availableDemographics,
availableLocales = localesDeferred.await(), availableLocales = localesDeferred.await(),
isMultipleTagsSupported = isMultipleTagsSupported,
isTagsExclusionSupported = isTagsExclusionSupported,
isSearchSupported = isSearchSupported,
searchSupportedWithMultipleFilters = searchSupportedWithMultipleFilters,
isSearchYearSupported = isSearchYearSupported,
isSearchYearRangeSupported = isSearchYearRangeSupported,
isSearchOriginalLanguages = isSearchOriginalLanguages,
) )
} }
/** /**
* Fetch available locales for multilingual sources * Fetch available locales for multilingual sources
*/ */
@Deprecated("Use getListFilterDatasets instead")
open suspend fun getAvailableLocales(): Set<Locale> = emptySet() open suspend fun getAvailableLocales(): Set<Locale> = emptySet()
@Deprecated( @Deprecated(

@ -1,7 +1,10 @@
package org.koitharu.kotatsu.parsers package org.koitharu.kotatsu.parsers
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilterV2
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.Paginator import org.koitharu.kotatsu.parsers.util.Paginator
@InternalParsersApi @InternalParsersApi
@ -18,72 +21,29 @@ abstract class PagedMangaParser(
@JvmField @JvmField
protected val searchPaginator = Paginator(searchPageSize) protected val searchPaginator = Paginator(searchPageSize)
final override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
return getList( return getList(
paginator = if (filter is MangaListFilter.Search) { paginator = if (filter.query.isNullOrEmpty()) {
searchPaginator
} else {
paginator paginator
} else {
searchPaginator
}, },
offset = offset, offset = offset,
order = order,
filter = filter, filter = filter,
) )
} }
@InternalParsersApi abstract suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga>
@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,
)
}
}
private suspend fun getList( private suspend fun getList(
paginator: Paginator, paginator: Paginator,
offset: Int, offset: Int,
filter: MangaListFilter?, order: SortOrder,
filter: MangaListFilterV2,
): List<Manga> { ): List<Manga> {
val page = paginator.getPage(offset) val page = paginator.getPage(offset)
val list = getListPage(page, filter) val list = getListPage(page, order, filter)
paginator.onListReceived(offset, page, list.size) paginator.onListReceived(offset, page, list.size)
return list return list
} }

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

@ -1,21 +1,13 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi import org.koitharu.kotatsu.parsers.InternalParsersApi
import java.util.*
data class MangaListFilterCapabilities @InternalParsersApi constructor( data class MangaListFilterCapabilities @InternalParsersApi constructor(
val availableSortOrders: Set<SortOrder>,
val availableTags: Set<MangaTag>,
val availableStates: Set<MangaState>,
val availableContentRating: Set<ContentRating>,
val availableContentTypes: Set<ContentType>,
val availableDemographics: Set<Demographic>,
val availableLocales: Set<Locale>,
val isMultipleTagsSupported: Boolean, val isMultipleTagsSupported: Boolean,
val isTagsExclusionSupported: Boolean, val isTagsExclusionSupported: Boolean,
val isSearchSupported: Boolean, val isSearchSupported: Boolean,
val searchSupportedWithMultipleFilters: Boolean, val isSearchWithFiltersSupported: Boolean,
val isSearchYearSupported: Boolean, val isYearSupported: Boolean = false,
val isSearchYearRangeSupported: Boolean, val isYearRangeSupported: Boolean = false,
val isSearchOriginalLanguages: Boolean, val isSourceLocaleSupported: Boolean = false,
) )

@ -0,0 +1,13 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
import java.util.*
data class MangaListFilterOptions @InternalParsersApi constructor(
val availableTags: Set<MangaTag>,
val availableStates: Set<MangaState> = emptySet(),
val availableContentRating: Set<ContentRating> = emptySet(),
val availableContentTypes: Set<ContentType> = emptySet(),
val availableDemographics: Set<Demographic> = emptySet(),
val availableLocales: Set<Locale> = emptySet(),
)

@ -60,11 +60,39 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.POPULARITY_HOUR, SortOrder.POPULARITY_HOUR,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
override val isTagsExclusionSupported: Boolean = true isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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( override val configKeyDomain = ConfigKey.Domain(
"bato.to", "bato.to",
@ -94,21 +122,20 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
"zbato.org", "zbato.org",
) )
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
when {
when (filter) { !filter.query.isNullOrEmpty() -> {
is MangaListFilter.Search -> {
return search(page, filter.query) return search(page, filter.query)
} }
is MangaListFilter.Advanced -> { else -> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/browse?sort=") append("/browse?sort=")
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> append("update.za") SortOrder.UPDATED -> append("update.za")
SortOrder.POPULARITY -> append("views_a.za") SortOrder.POPULARITY -> append("views_a.za")
SortOrder.NEWEST -> append("create.za") SortOrder.NEWEST -> append("create.za")
@ -171,17 +198,6 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
return parseList(url, page) 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)
}
} }
} }
@ -252,7 +268,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
throw ParseException("Cannot find images list", fullUrl) throw ParseException("Cannot find images list", fullUrl)
} }
override suspend fun getAvailableTags(): Set<MangaTag> { private suspend fun fetchAvailableTags(): Set<MangaTag> {
val scripts = webClient.httpGet( val scripts = webClient.httpGet(
"https://${domain}/browse", "https://${domain}/browse",
).parseHtml().selectOrThrow("script") ).parseHtml().selectOrThrow("script")
@ -273,22 +289,6 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
throw ParseException("Cannot find gernes list", scripts[0].baseUri()) 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> { private suspend fun search(page: Int, query: String): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")

@ -27,10 +27,6 @@ internal class ComickFunParser(context: MangaLoaderContext) :
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override val isTagsExclusionSupported = true
override val isSearchYearRangeSupported = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY, SortOrder.POPULARITY,
SortOrder.UPDATED, SortOrder.UPDATED,
@ -38,21 +34,34 @@ internal class ComickFunParser(context: MangaLoaderContext) :
SortOrder.NEWEST, SortOrder.NEWEST,
) )
override val availableContentTypes: Set<ContentType> = EnumSet.of( private val tagsArray = SuspendLazy(::loadTags)
ContentType.MANGA,
ContentType.MANHWA,
ContentType.MANHUA,
ContentType.OTHER,
)
override val availableDemographics: Set<Demographic> = EnumSet.allOf(Demographic::class.java)
override val availableStates: Set<MangaState> = override val filterCapabilities: MangaListFilterCapabilities
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED) get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = true,
isSourceLocaleSupported = false,
)
private val tagsArray = SuspendLazy(::loadTags) 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, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val domain = domain val domain = domain
val url = urlBuilder() val url = urlBuilder()
.host("api.$domain") .host("api.$domain")
@ -62,12 +71,12 @@ internal class ComickFunParser(context: MangaLoaderContext) :
.addQueryParameter("tachiyomi", "true") .addQueryParameter("tachiyomi", "true")
.addQueryParameter("limit", pageSize.toString()) .addQueryParameter("limit", pageSize.toString())
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
url.addQueryParameter("q", filter.query) url.addQueryParameter("q", filter.query)
} }
is MangaListFilter.Advanced -> { else -> {
filter.tags.forEach { filter.tags.forEach {
url.addQueryParameter("genres", it.key) url.addQueryParameter("genres", it.key)
@ -79,7 +88,7 @@ internal class ComickFunParser(context: MangaLoaderContext) :
url.addQueryParameter( url.addQueryParameter(
"sort", "sort",
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> "view" SortOrder.POPULARITY -> "view"
SortOrder.UPDATED -> "uploaded" SortOrder.UPDATED -> "uploaded"
SortOrder.NEWEST -> "created_at" SortOrder.NEWEST -> "created_at"
@ -101,11 +110,11 @@ internal class ComickFunParser(context: MangaLoaderContext) :
) )
} }
filter.yearFrom?.let { if (filter.yearFrom != 0) {
url.addQueryParameter("from", filter.yearFrom.toString()) url.addQueryParameter("from", filter.yearFrom.toString())
} }
filter.yearTo?.let { if (filter.yearTo != 0) {
url.addQueryParameter("to", filter.yearTo.toString()) url.addQueryParameter("to", filter.yearTo.toString())
} }
@ -135,10 +144,6 @@ internal class ComickFunParser(context: MangaLoaderContext) :
) )
} }
} }
null -> {
url.addQueryParameter("sort", "uploaded")
}
} }
val ja = webClient.httpGet(url.build()).parseJsonArray() val ja = webClient.httpGet(url.build()).parseJsonArray()
val tagsMap = tagsArray.get() val tagsMap = tagsArray.get()
@ -208,7 +213,7 @@ internal class ComickFunParser(context: MangaLoaderContext) :
} }
} }
override suspend fun getAvailableTags(): Set<MangaTag> { private suspend fun fetchAvailableTags(): Set<MangaTag> {
val sparseArray = tagsArray.get() val sparseArray = tagsArray.get()
val set = ArraySet<MangaTag>(sparseArray.size()) val set = ArraySet<MangaTag>(sparseArray.size())
for (i in 0 until sparseArray.size()) { for (i in 0 until sparseArray.size()) {

@ -33,7 +33,6 @@ internal class ExHentaiParser(
) : PagedMangaParser(context, MangaParserSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider, Interceptor { ) : PagedMangaParser(context, MangaParserSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider, Interceptor {
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST) override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val isTagsExclusionSupported: Boolean = true
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain( get() = ConfigKey.Domain(
@ -51,6 +50,17 @@ internal class ExHentaiParser(
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false) private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
private val tagsMap = SuspendLazy(::fetchTags) private val tagsMap = SuspendLazy(::fetchTags)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
override val isAuthorized: Boolean override val isAuthorized: Boolean
get() { get() {
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED) val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
@ -75,7 +85,32 @@ internal class ExHentaiParser(
searchPaginator.firstPage = 0 searchPaginator.firstPage = 0
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tagsMap.get().values.toSet(),
availableStates = emptySet(),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
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: MangaListFilterV2): List<Manga> {
val next = nextPages.get(page, 0L) val next = nextPages.get(page, 0L)
if (page > 0 && next == 0L) { if (page > 0 && next == 0L) {
@ -90,15 +125,15 @@ internal class ExHentaiParser(
append(domain) append(domain)
append("/?next=") append("/?next=")
append(next) append(next)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
search += filter.query.urlEncoded() search += filter.query.urlEncoded()
append("&f_search=") append("&f_search=")
append(search.trim().replace(' ', '+')) append(search.trim().replace(' ', '+'))
} }
is MangaListFilter.Advanced -> { else -> {
filter.toSearchQuery()?.let { sq -> filter.toSearchQuery()?.let { sq ->
append("&f_search=") append("&f_search=")
@ -121,8 +156,6 @@ internal class ExHentaiParser(
append(fCats) append(fCats)
} }
} }
null -> {}
} }
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again // by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
if (updateDm) { if (updateDm) {
@ -141,7 +174,7 @@ internal class ExHentaiParser(
body.parseFailed("Cannot find root") body.parseFailed("Cannot find root")
} else { } else {
updateDm = true updateDm = true
return getListPage(page, filter) return getListPage(page, order, filter)
} }
updateDm = false updateDm = false
nextPages[page + 1] = getNextTimestamp(body) nextPages[page + 1] = getNextTimestamp(body)
@ -267,10 +300,6 @@ internal class ExHentaiParser(
"unusual pupils,urination,vore,vtuber,widow,wings,witch,wolf girl,x-ray,yuri,zombie,sole male,males only,yaoi," + "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" "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> { private suspend fun fetchTags(): Map<String, MangaTag> {
val tagMap = ArrayMap<String, MangaTag>() val tagMap = ArrayMap<String, MangaTag>()
val tagElements = tags.split(",") val tagElements = tags.split(",")
@ -297,24 +326,6 @@ internal class ExHentaiParser(
return tagMap 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 { override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request()) val response = chain.proceed(chain.request())
if (response.headersContentLength() <= 256) { if (response.headersContentLength() <= 256) {
@ -420,7 +431,7 @@ internal class ExHentaiParser(
?.toLongOrNull() ?: 1 ?.toLongOrNull() ?: 1
} }
private fun MangaListFilter.Advanced.toSearchQuery(): String? { private fun MangaListFilterV2.toSearchQuery(): String? {
val joiner = StringUtil.StringJoiner(" ") val joiner = StringUtil.StringJoiner(" ")
for (tag in tags) { for (tag in tags) {
if (tag.key.isNumeric()) { if (tag.key.isNumeric()) {

@ -109,13 +109,10 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
private var cachedSearchIds: List<Int> = emptyList() private var cachedSearchIds: List<Int> = emptyList()
override suspend fun getList( override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> = when {
offset: Int, filter.query.isNullOrEmpty() -> {
filter: MangaListFilter?,
): List<Manga> = when (filter) {
is MangaListFilter.Advanced -> {
if (filter.tags.isEmpty()) { if (filter.tags.isEmpty()) {
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> { SortOrder.POPULARITY -> {
getGalleryIDsFromNozomi( getGalleryIDsFromNozomi(
"popular", "popular",
@ -134,7 +131,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
cachedSearchIds = cachedSearchIds =
hitomiSearch( hitomiSearch(
filter.tags.joinToString(" ") { it.key }, filter.tags.joinToString(" ") { it.key },
filter.sortOrder == SortOrder.POPULARITY, order == SortOrder.POPULARITY,
filter.locale.getSiteLang(), filter.locale.getSiteLang(),
).toList() ).toList()
} }
@ -142,14 +139,12 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
} }
} }
is MangaListFilter.Search -> { else -> {
if (offset == 0) { 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)) cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
} }
else -> getGalleryIDsFromNozomi(null, "popular", "all", offset.nextOffsetRange())
}.toMangaList() }.toMangaList()
private fun Int.nextOffsetRange(): LongRange { private fun Int.nextOffsetRange(): LongRange {
@ -478,14 +473,14 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
title = doc.selectFirstOrThrow("h1").text(), title = doc.selectFirstOrThrow("h1").text(),
url = id.toString(), url = id.toString(),
coverUrl = coverUrl =
"https:" + "https:" +
doc.selectFirstOrThrow("picture > source") doc.selectFirstOrThrow("picture > source")
.attr("data-srcset") .attr("data-srcset")
.substringBefore(" "), .substringBefore(" "),
publicUrl = publicUrl =
doc.selectFirstOrThrow("h1 > a") doc.selectFirstOrThrow("h1 > a")
.attrAsRelativeUrl("href") .attrAsRelativeUrl("href")
.toAbsoluteUrl(domain), .toAbsoluteUrl(domain),
author = null, author = null,
tags = emptySet(), tags = emptySet(),
isNsfw = true, isNsfw = true,
@ -508,37 +503,37 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
return manga.copy( return manga.copy(
title = json.getString("title"), title = json.getString("title"),
largeCoverUrl = largeCoverUrl =
json.getJSONArray("files").getJSONObject(0).let { json.getJSONArray("files").getJSONObject(0).let {
val hash = it.getString("hash") val hash = it.getString("hash")
val commonId = commonImageId() val commonId = commonImageId()
val imageId = imageIdFromHash(hash) val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId) val subDomain = 'a' + subdomainOffset(imageId)
"https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp" "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
}, },
author = author =
json.optJSONArray("artists") json.optJSONArray("artists")
?.mapJSON { it.getString("artist").toCamelCase() } ?.mapJSON { it.getString("artist").toCamelCase() }
?.joinToString(), ?.joinToString(),
publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain), publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain),
tags = tags =
buildSet { buildSet {
json.optJSONArray("characters") json.optJSONArray("characters")
?.mapToTags("character") ?.mapToTags("character")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("tags") json.optJSONArray("tags")
?.mapToTags("tag") ?.mapToTags("tag")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("artists") json.optJSONArray("artists")
?.mapToTags("artist") ?.mapToTags("artist")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("parodys") json.optJSONArray("parodys")
?.mapToTags("parody") ?.mapToTags("parody")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("groups") json.optJSONArray("groups")
?.mapToTags("group") ?.mapToTags("group")
?.let(::addAll) ?.let(::addAll)
}, },
chapters = listOf( chapters = listOf(
MangaChapter( MangaChapter(
id = generateUid(manga.url), id = generateUid(manga.url),
@ -562,15 +557,15 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaPa
mapJSON { mapJSON {
MangaTag( MangaTag(
title = title =
it.getString(key).toCamelCase().let { title -> it.getString(key).toCamelCase().let { title ->
if (it.getStringOrNull("female")?.toIntOrNull() == 1) { if (it.getStringOrNull("female")?.toIntOrNull() == 1) {
"$title" "$title"
} else if (it.getStringOrNull("male")?.toIntOrNull() == 1) { } else if (it.getStringOrNull("male")?.toIntOrNull() == 1) {
"$title" "$title"
} else { } else {
title title
} }
}, },
key = it.getString("url").tagUrlToTag(), key = it.getString("url").tagUrlToTag(),
source = source, source = source,
).let(tags::add) ).let(tags::add)

@ -27,19 +27,23 @@ internal class ImHentai(context: MangaLoaderContext) :
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(
page: Int,
order: SortOrder,
filter: MangaListFilterV2,
): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/search/?page=") append("/search/?page=")
append(page.toString()) append(page.toString())
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&key=") append("&key=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&key=") append("&key=")
@ -53,17 +57,13 @@ internal class ImHentai(context: MangaLoaderContext) :
} }
append(lang) append(lang)
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> append("&lt=1&pp=0") SortOrder.UPDATED -> append("&lt=1&pp=0")
SortOrder.POPULARITY -> append("&lt=0&pp=1") SortOrder.POPULARITY -> append("&lt=0&pp=1")
SortOrder.RATING -> append("&lt=0&pp=0") SortOrder.RATING -> append("&lt=0&pp=0")
else -> append("&lt=1&pp=0") else -> append("&lt=1&pp=0")
} }
} }
null -> {
append("&lt=1&pp=0")
}
} }
} }

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

@ -33,19 +33,36 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java) override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java) isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
override val availableDemographics: Set<Demographic> = EnumSet.allOf(Demographic::class.java) isSearchSupported = true,
isSearchWithFiltersSupported = true,
isYearSupported = true,
isYearRangeSupported = true,
isSourceLocaleSupported = true,
)
override val availableStates: Set<MangaState> = override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isTagsExclusionSupported: Boolean = true override suspend fun getFilterOptions(): MangaListFilterOptions = coroutineScope {
override val searchSupportedWithMultipleFilters: Boolean = true val localesDeferred = async { fetchAvailableLocales() }
override val isSearchYearSupported: Boolean = true val tagsDeferred = async { fetchAvailableTags() }
override val isSearchOriginalLanguages: Boolean = true MangaListFilterOptions(
availableTags = tagsDeferred.await(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
MangaState.ABANDONED,
),
availableContentRating = EnumSet.allOf(ContentRating::class.java),
availableContentTypes = emptySet(),
availableDemographics = EnumSet.allOf(Demographic::class.java),
availableLocales = localesDeferred.await(),
)
}
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> { override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val domain = domain val domain = domain
@ -231,7 +248,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() val tags = webClient.httpGet("https://api.${domain}/manga/tag").parseJson()
.getJSONArray("data") .getJSONArray("data")
return tags.mapJSONToSet { jo -> return tags.mapJSONToSet { jo ->
@ -245,7 +262,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() val head = webClient.httpGet("https://$domain/").parseHtml().head()
return head.getElementsByAttributeValue("property", "og:locale:alternate") return head.getElementsByAttributeValue("property", "og:locale:alternate")
.mapNotNullToSet { meta -> .mapNotNullToSet { meta ->

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

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

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

@ -55,8 +55,6 @@ class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(contex
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
val tags = SoftSuspendLazy { val tags = SoftSuspendLazy {
val document = webClient.httpGet("https://$domain/filter").parseHtml() val document = webClient.httpGet("https://$domain/filter").parseHtml()
@ -69,27 +67,41 @@ class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(contex
}.associateBy { it.title } }.associateBy { it.title }
} }
override suspend fun getAvailableTags(): Set<MangaTag> { override val filterCapabilities: MangaListFilterCapabilities
return tags.get().values.toSet() get() = MangaListFilterCapabilities(
} isMultipleTagsSupported = true,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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: MangaListFilterV2): List<Manga> {
val url = "https://$domain".toHttpUrl().newBuilder().apply { val url = "https://$domain".toHttpUrl().newBuilder().apply {
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
addPathSegment("search") addPathSegment("search")
addQueryParameter("keyword", filter.query) addQueryParameter("keyword", filter.query)
addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
addPathSegment("filter") addPathSegment("filter")
addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
addQueryParameter( addQueryParameter(
name = "sort", name = "sort",
value = when (filter.sortOrder) { value = when (order) {
SortOrder.POPULARITY -> "most-viewed" SortOrder.POPULARITY -> "most-viewed"
SortOrder.RATING -> "score" SortOrder.RATING -> "score"
SortOrder.UPDATED -> "latest-updated" SortOrder.UPDATED -> "latest-updated"
@ -111,11 +123,6 @@ class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(contex
}, },
) )
} }
null -> {
addPathSegment("filter")
addQueryParameter("page", page.toString())
}
} }
}.build() }.build()

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

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

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

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

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

@ -19,19 +19,36 @@ import java.util.*
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.FLIXSCANS, 18) { internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.FLIXSCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED) 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 configKeyDomain = ConfigKey.Domain("flixscans.net")
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys) super.onCreateConfig(keys)
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val json = when {
val json = when (filter) { !filter.query.isNullOrEmpty() -> {
is MangaListFilter.Search -> {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
@ -41,7 +58,7 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
webClient.httpPost(url, body).parseJson().getJSONArray("data") webClient.httpPost(url, body).parseJson().getJSONArray("data")
} }
is MangaListFilter.Advanced -> { else -> {
val url = buildString { val url = buildString {
append("https://api.") append("https://api.")
append(domain) append(domain)
@ -90,11 +107,6 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
webClient.httpGet(url).parseJson().getJSONArray("data") 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 -> return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}" val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
@ -122,7 +134,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 doc = webClient.httpGet("https://$domain/search/advance").parseHtml()
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data()) val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
val tagsList = json.getJSONArray(3).toString().replace("[", "").replace("]", "").split(",") val tagsList = json.getJSONArray(3).toString().replace("[", "").replace("]", "").split(",")

@ -25,19 +25,19 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/mangas?page=") append("/mangas?page=")
append(page) append(page)
append("&query=") append("&query=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
@ -46,7 +46,7 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
append("?page=") append("?page=")
append(page) append(page)
} else { } else {
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("/mangas?page=") append("/mangas?page=")
append(page) append(page)
} else { } else {
@ -56,11 +56,6 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
} }
} }
} }
null -> {
append("/mangas?page=")
append(page)
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -17,8 +17,6 @@ import java.util.*
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.TEAMXNOVEL, 10) { internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.TEAMXNOVEL, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) 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") override val configKeyDomain = ConfigKey.Domain("teamoney.site")
@ -27,16 +25,33 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override val isMultipleTagsSupported = false override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = false,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/?search=") append("/?search=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
if (page > 1) { if (page > 1) {
@ -45,7 +60,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
} }
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
append("/series?genre=") append("/series?genre=")
@ -56,7 +71,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
} }
append("&") append("&")
} else { } else {
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("/series") SortOrder.POPULARITY -> append("/series")
SortOrder.UPDATED -> append("/") SortOrder.UPDATED -> append("/")
else -> append("/") else -> append("/")
@ -70,7 +85,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 { filter.states.oneOrThrowIfMany()?.let {
append("status=") append("status=")
append( append(
@ -84,8 +99,6 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
} }
} }
} }
null -> append("/?page=$page")
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
@ -115,7 +128,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() val doc = webClient.httpGet("https://$domain/series").parseHtml()
return doc.requireElementById("select_genre").select("option").mapNotNullToSet { return doc.requireElementById("select_genre").select("option").mapNotNullToSet {
MangaTag( MangaTag(

@ -34,31 +34,26 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
SortOrder.NEWEST, SortOrder.NEWEST,
) )
override suspend fun getList( override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
offset: Int, val filters = when {
filter: MangaListFilter?, !filter.query.isNullOrEmpty() -> {
): List<Manga> { return if (offset == 0) {
val filters = search(filter.query)
when (filter) { } else {
is MangaListFilter.Search -> { emptyList()
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( val array = apiCall(
""" """
getMediaList(offset: $offset, limit: 20, mediaType: manga, filters: {$filters}) { getMediaList(offset: $offset, limit: 20, mediaType: manga, filters: {$filters}) {

@ -27,26 +27,42 @@ internal abstract class CupFoxParser(
SortOrder.UPDATED, SortOrder.UPDATED,
) )
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = false,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search/") append("/search/")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append('/') append('/')
append(page) append(page)
} }
is MangaListFilter.Advanced -> { else -> {
append("/category/") append("/category/")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("order/hits/") SortOrder.POPULARITY -> append("order/hits/")
SortOrder.UPDATED -> append("order/addtime/") SortOrder.UPDATED -> append("order/addtime/")
else -> append("order/addtime/") else -> append("order/addtime/")
@ -73,11 +89,6 @@ internal abstract class CupFoxParser(
append("page/") append("page/")
append(page) append(page)
} }
null -> {
append("/category/order/addtime/page/")
append(page)
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
@ -193,7 +204,8 @@ internal abstract class CupFoxParser(
} }
protected open val selectAvailableTags = "div.swiper-wrapper a[href*=tags], ul.stui-screen__list li a[href*=tags]" 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() val doc = webClient.httpGet("https://$domain/category/").parseHtml()
return doc.select(selectAvailableTags) return doc.select(selectAvailableTags)
.mapNotNullToSet { a -> .mapNotNullToSet { a ->

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

@ -22,14 +22,32 @@ internal class BeeToon(context: MangaLoaderContext) :
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override val isMultipleTagsSupported = false override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = false,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = emptySet(),
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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
@ -37,7 +55,7 @@ internal class BeeToon(context: MangaLoaderContext) :
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
@ -47,7 +65,7 @@ internal class BeeToon(context: MangaLoaderContext) :
append(page) append(page)
append("/") append("/")
} else { } else {
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> append("/latest-update/") SortOrder.UPDATED -> append("/latest-update/")
SortOrder.POPULARITY -> append("/popular-manga/") SortOrder.POPULARITY -> append("/popular-manga/")
else -> append("/latest-update/") else -> append("/latest-update/")
@ -57,8 +75,6 @@ internal class BeeToon(context: MangaLoaderContext) :
append("/") append("/")
} }
} }
null -> append("/latest-update/page-$page/")
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
@ -85,7 +101,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() val doc = webClient.httpGet("https://$domain/").parseHtml()
return doc.requireElementById("menu-item-3").select("ul.sub-menu li a").mapNotNullToSet { return doc.requireElementById("menu-item-3").select("ul.sub-menu li a").mapNotNullToSet {
MangaTag( MangaTag(

@ -1,16 +1,16 @@
package org.koitharu.kotatsu.parsers.site.en package org.koitharu.kotatsu.parsers.site.en
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.* import java.util.*
@MangaSourceParser("CLONEMANGA", "CloneManga", "en") @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( override val availableSortOrders: Set<SortOrder> = Collections.singleton(
SortOrder.POPULARITY, SortOrder.POPULARITY,
@ -23,27 +23,11 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte
keys.add(userAgentKey) keys.add(userAgentKey)
} }
@InternalParsersApi override suspend fun getList(order: SortOrder, filter: MangaListFilterV2): List<Manga> {
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { if (!filter.query.isNullOrEmpty()) {
return emptyList()
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"
} }
val doc = webClient.httpGet("https://$domain/viewer_landing.php").parseHtml()
val doc = webClient.httpGet(link).parseHtml()
val mangas = doc.getElementsByClass("comicPreviewContainer") val mangas = doc.getElementsByClass("comicPreviewContainer")
return mangas.mapNotNull { item -> return mangas.mapNotNull { item ->
val background = item.selectFirstOrThrow(".comicPreview").styleValueOrNull("background") val background = item.selectFirstOrThrow(".comicPreview").styleValueOrNull("background")

@ -30,14 +30,13 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/") append("/")
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("search?keyword=") append("search?keyword=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
if (page > 1) { if (page > 1) {
@ -46,7 +45,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
} }
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty() && filter.states.isEmpty()) { if (filter.tags.isNotEmpty() && filter.states.isEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append(it.key) append(it.key)
@ -65,7 +64,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
} else if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) { } else if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED) throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED)
} else { } else {
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("popular-comic") SortOrder.POPULARITY -> append("popular-comic")
SortOrder.UPDATED -> append("new-comic") SortOrder.UPDATED -> append("new-comic")
SortOrder.NEWEST -> append("recent-comic") SortOrder.NEWEST -> append("recent-comic")
@ -78,15 +77,6 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
append(page.toString()) append(page.toString())
} }
} }
null -> {
append("popular-comic")
if (page > 1) {
append("/")
append(page.toString())
}
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -32,9 +32,9 @@ internal class DynastyScans(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
@ -47,7 +47,7 @@ internal class DynastyScans(context: MangaLoaderContext) :
return parseMangaListQuery(webClient.httpGet(url).parseHtml()) return parseMangaListQuery(webClient.httpGet(url).parseHtml())
} }
is MangaListFilter.Advanced -> { else -> {
val url = buildString { val url = buildString {
append("https://") append("https://")
@ -68,16 +68,6 @@ internal class DynastyScans(context: MangaLoaderContext) :
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) 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())
}
} }
} }

@ -31,14 +31,13 @@ internal class FlixScansOrg(context: MangaLoaderContext) :
override val isSearchSupported = false override val isSearchSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val json = when {
val json = when (filter) { !filter.query.isNullOrEmpty() -> {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
} }
is MangaListFilter.Advanced -> { else -> {
val url = buildString { val url = buildString {
append("https://api.") append("https://api.")
@ -75,11 +74,6 @@ internal class FlixScansOrg(context: MangaLoaderContext) :
} }
webClient.httpGet(url).parseJson().getJSONArray("data") 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 -> return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}" val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"

@ -26,13 +26,12 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
@ -40,13 +39,13 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("/browse-comics/?results=") append("/browse-comics/?results=")
append(page) append(page)
append("&filter=") append("&filter=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("Updated") SortOrder.UPDATED -> append("Updated")
SortOrder.NEWEST -> append("New") SortOrder.NEWEST -> append("New")
@ -60,12 +59,6 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
} }
} }
} }
null -> {
append("/browse-comics/?results=")
append(page)
append("&filter=Updated")
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -31,26 +31,25 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?query=") append("/search?query=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&search_type=manga&page=") append("&search_type=manga&page=")
append(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") throw IllegalArgumentException("Filter part tag is not available with sort not updated")
} }
if (filter.sortOrder == SortOrder.ALPHABETICAL) { if (order == SortOrder.ALPHABETICAL) {
append("/manga-list") append("/manga-list")
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("/category/") append("/category/")
@ -62,12 +61,6 @@ internal class MangaKawaiiEn(context: MangaLoaderContext) :
return emptyList() return emptyList()
} }
} }
null -> {
if (page > 1) {
return emptyList()
}
}
} }
} }

@ -35,19 +35,19 @@ internal class MangaTownParser(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?name=") append("/search?name=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
append("/directory/") append("/directory/")
append("0-") append("0-")
@ -79,7 +79,7 @@ internal class MangaTownParser(context: MangaLoaderContext) :
append(".htm") append(".htm")
append( append(
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> "" SortOrder.POPULARITY -> ""
SortOrder.UPDATED -> "?last_chapter_time.za" SortOrder.UPDATED -> "?last_chapter_time.za"
SortOrder.ALPHABETICAL -> "?name.az" SortOrder.ALPHABETICAL -> "?name.az"
@ -88,8 +88,6 @@ internal class MangaTownParser(context: MangaLoaderContext) :
}, },
) )
} }
null -> append("/directory/$page.htm?last_chapter_time.za")
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -25,32 +25,53 @@ internal class Mangaowl(context: MangaLoaderContext) :
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.RATING, SortOrder.RATING,
) )
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("mangaowl.to") override val configKeyDomain = ConfigKey.Domain("mangaowl.to")
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys) super.onCreateConfig(keys)
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override suspend fun getListPage(
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { page: Int,
order: SortOrder,
filter: MangaListFilterV2,
): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/10-search?q=") append("/10-search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
append("/10-comics") append("/10-comics")
append("?page=") append("?page=")
@ -74,7 +95,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
append("&ordering=") append("&ordering=")
append( append(
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> "view_count" SortOrder.POPULARITY -> "view_count"
SortOrder.UPDATED -> "-modified_at" SortOrder.UPDATED -> "-modified_at"
SortOrder.NEWEST -> "created_at" SortOrder.NEWEST -> "created_at"
@ -83,11 +104,6 @@ internal class Mangaowl(context: MangaLoaderContext) :
}, },
) )
} }
null -> {
append("/10-comics?ordering=-modified_at&page=")
append(page.toString())
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
@ -110,7 +126,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() val doc = webClient.httpGet("https://$domain/10-genres").parseHtml()
return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a -> return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a ->
val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-") val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-")

@ -48,21 +48,20 @@ class Manhwa18Parser(context: MangaLoaderContext) :
) )
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/tim-kiem?page=") append("/tim-kiem?page=")
append(page.toString()) append(page.toString())
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&q=") append("&q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("&accept_genres=") append("&accept_genres=")
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -80,7 +79,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
append("&sort=") append("&sort=")
append( append(
when (filter.sortOrder) { when (order) {
SortOrder.ALPHABETICAL -> "az" SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za" SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top" SortOrder.POPULARITY -> "top"
@ -103,8 +102,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
) )
} }
} }
null -> append("&sort=update")
} }
} }

@ -28,21 +28,24 @@ class ManhwasMen(context: MangaLoaderContext) :
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(
page: Int,
order: SortOrder,
filter: MangaListFilterV2,
): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/manga-list") append("/manga-list")
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&search=") append("&search=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("&genero=") append("&genero=")
@ -60,8 +63,6 @@ class ManhwasMen(context: MangaLoaderContext) :
) )
} }
} }
null -> {}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
@ -10,7 +11,7 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
@MangaSourceParser("PO2SCANS", "Po2Scans", "en") @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 availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("po2scans.com") override val configKeyDomain = ConfigKey.Domain("po2scans.com")
@ -20,23 +21,14 @@ internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, Mang
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getList(order: SortOrder, filter: MangaListFilterV2): List<Manga> {
if (offset > 0) {
return emptyList()
}
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/series") append("/series")
when (filter) { if (!filter.query.isNullOrEmpty()) {
is MangaListFilter.Search -> { append("?search=")
append("?search=") append(filter.query.urlEncoded())
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {}
null -> {}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -29,19 +29,19 @@ internal class Pururin(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?q=") append("/search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
append("/browse") append("/browse")
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
@ -54,7 +54,7 @@ internal class Pururin(context: MangaLoaderContext) :
append(page) append(page)
append("&sort=") append("&sort=")
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> append("") SortOrder.UPDATED -> append("")
SortOrder.POPULARITY -> append("most-viewed") SortOrder.POPULARITY -> append("most-viewed")
SortOrder.RATING -> append("highest-rated") SortOrder.RATING -> append("highest-rated")
@ -62,11 +62,6 @@ internal class Pururin(context: MangaLoaderContext) :
else -> append("") else -> append("")
} }
} }
null -> {
append("/browse?page=")
append(page)
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -36,20 +36,19 @@ class VyManga(context: MangaLoaderContext) :
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?search_po=0&q=") append("/search?search_po=0&q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&author_po=0&author=&completed=2&sort=updated_at&sort_type=desc&page=") append("&author_po=0&author=&completed=2&sort=updated_at&sort_type=desc&page=")
append(page) append(page)
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isEmpty()) { if (filter.tags.isEmpty()) {
@ -84,7 +83,7 @@ class VyManga(context: MangaLoaderContext) :
} }
append("&sort=") append("&sort=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("viewed&sort_type=desc") SortOrder.POPULARITY -> append("viewed&sort_type=desc")
SortOrder.POPULARITY_ASC -> append("viewed&sort_type=asc") SortOrder.POPULARITY_ASC -> append("viewed&sort_type=asc")
SortOrder.RATING -> append("scored&sort_type=desc") SortOrder.RATING -> append("scored&sort_type=desc")
@ -99,11 +98,6 @@ class VyManga(context: MangaLoaderContext) :
append("&page=") append("&page=")
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() val doc = webClient.httpGet(url).parseHtml()

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

@ -40,22 +40,22 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.RATING, SortOrder.RATING,
) )
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/library") append("/library")
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("?title=") append("?title=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("?order_item=") append("?order_item=")
append( append(
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> "likes_count&order_dir=desc" SortOrder.POPULARITY -> "likes_count&order_dir=desc"
SortOrder.POPULARITY_ASC -> "likes_count&order_dir=asc" SortOrder.POPULARITY_ASC -> "likes_count&order_dir=asc"
SortOrder.UPDATED -> "release_date&order_dir=desc" SortOrder.UPDATED -> "release_date&order_dir=desc"
@ -88,10 +88,6 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
) )
} }
} }
null -> {
append("?order_item=release_date&order_dir=desc&filter_by=title")
}
} }
append("&_pg=1&page=") append("&_pg=1&page=")
append(page.toString()) append(page.toString())

@ -35,18 +35,21 @@ internal abstract class FmreaderParser(
SortOrder.ALPHABETICAL_DESC, 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 listUrl = "/manga-list.html"
protected open val datePattern = "MMMM d, yyyy" protected open val datePattern = "MMMM d, yyyy"
protected open val tagPrefix = "manga-list-genre-" protected open val tagPrefix = "manga-list-genre-"
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
init { init {
paginator.firstPage = 1 paginator.firstPage = 1
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
@ -72,20 +75,33 @@ internal abstract class FmreaderParser(
"drop", "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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) append(listUrl)
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&name=") append("&name=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("&genre=") append("&genre=")
append(filter.tags.joinToString(",") { it.key }) append(filter.tags.joinToString(",") { it.key })
@ -95,7 +111,7 @@ internal abstract class FmreaderParser(
append("&sort=") append("&sort=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views&sort_type=DESC") SortOrder.POPULARITY -> append("views&sort_type=DESC")
SortOrder.POPULARITY_ASC -> append("views&sort_type=ASC") SortOrder.POPULARITY_ASC -> append("views&sort_type=ASC")
SortOrder.UPDATED -> append("last_update&sort_type=DESC") SortOrder.UPDATED -> append("last_update&sort_type=DESC")
@ -118,8 +134,6 @@ internal abstract class FmreaderParser(
} }
} }
null -> append("&sort=last_update")
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
@ -150,7 +164,7 @@ internal abstract class FmreaderParser(
protected open val selectBodyTag = "ul.filter-type li a" 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() val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectBodyTag).mapNotNullToSet { a -> return doc.select(selectBodyTag).mapNotNullToSet { a ->
val href = a.attr("href").substringAfter(tagPrefix).substringBeforeLast(".html") 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 selectPage = "div#chapter-content img"
override val selectBodyTag = "div.advanced-wrapper .genre_label" 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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/tim-kiem?page=") append("/tim-kiem?page=")
append(page.toString()) append(page.toString())
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&q=") append("&q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("&accept_genres=") append("&accept_genres=")
append(filter.tags.joinToString(",") { it.key }) append(filter.tags.joinToString(",") { it.key })
@ -45,7 +45,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
append("&sort=") append("&sort=")
append( append(
when (filter.sortOrder) { when (order) {
SortOrder.ALPHABETICAL -> "az" SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za" SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top" SortOrder.POPULARITY -> "top"
@ -68,8 +68,6 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
) )
} }
} }
null -> append("&sort=update")
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -17,12 +17,12 @@ internal class OlimpoScans(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override val isTagsExclusionSupported = false override val isTagsExclusionSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append(listUrl) append(listUrl)
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
@ -30,7 +30,7 @@ internal class OlimpoScans(context: MangaLoaderContext) :
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("/lista-de-comics-genero-") append("/lista-de-comics-genero-")
@ -42,7 +42,7 @@ internal class OlimpoScans(context: MangaLoaderContext) :
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
append("&sort=") append("&sort=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views&sort_type=DESC") SortOrder.POPULARITY -> append("views&sort_type=DESC")
SortOrder.POPULARITY_ASC -> append("views&sort_type=ASC") SortOrder.POPULARITY_ASC -> append("views&sort_type=ASC")
SortOrder.UPDATED -> append("last_update&sort_type=DESC") SortOrder.UPDATED -> append("last_update&sort_type=DESC")
@ -65,13 +65,6 @@ internal class OlimpoScans(context: MangaLoaderContext) :
) )
} }
} }
null -> {
append(listUrl)
append("?page=")
append(page.toString())
append("&sort=last_update")
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

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

@ -47,19 +47,19 @@ internal class BentomangaParser(context: MangaLoaderContext) :
searchPaginator.firstPage = 0 searchPaginator.firstPage = 0
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = urlBuilder() val url = urlBuilder()
.host(domain) .host(domain)
.addPathSegment("manga_list") .addPathSegment("manga_list")
.addQueryParameter("limit", page.toString()) .addQueryParameter("limit", page.toString())
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
url.addQueryParameter("search", filter.query) url.addQueryParameter("search", filter.query)
} }
is MangaListFilter.Advanced -> { else -> {
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> url.addQueryParameter("order_by", "update") SortOrder.UPDATED -> url.addQueryParameter("order_by", "update")
.addQueryParameter("order", "desc") .addQueryParameter("order", "desc")
@ -104,8 +104,6 @@ internal class BentomangaParser(context: MangaLoaderContext) :
} }
} }
null -> url.addQueryParameter("order_by", "update")
} }
val root = webClient.httpGet(url.build()).parseHtml().requireElementById("mangas_content") val root = webClient.httpGet(url.build()).parseHtml().requireElementById("mangas_content")
return root.select(".manga[data-manga]").map { div -> return root.select(".manga[data-manga]").map { div ->

@ -6,6 +6,7 @@ import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
@ -16,7 +17,7 @@ import java.util.*
@MangaSourceParser("FURYOSOCIETY", "FuryoSociety", "fr") @MangaSourceParser("FURYOSOCIETY", "FuryoSociety", "fr")
internal class FuryoSociety(context: MangaLoaderContext) : 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) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.UPDATED)
@ -40,27 +41,20 @@ internal class FuryoSociety(context: MangaLoaderContext) :
) )
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getList(order: SortOrder, filter: MangaListFilterV2): List<Manga> {
if (page > 1) {
return emptyList()
}
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
} }
is MangaListFilter.Advanced -> { else -> {
if (order == SortOrder.ALPHABETICAL) {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
append("/mangas") append("/mangas")
} }
} }
null -> {}
} }
} }

@ -27,12 +27,12 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
keys.add(userAgentKey) keys.add(userAgentKey)
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val end = page * pageSize val end = page * pageSize
val start = end - (pageSize - 1) val start = end - (pageSize - 1)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
@ -43,7 +43,7 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
return parseMangaListQuery(webClient.httpGet(url).parseJson()) return parseMangaListQuery(webClient.httpGet(url).parseJson())
} }
is MangaListFilter.Advanced -> { else -> {
val url = buildString { val url = buildString {
append("https://api.") append("https://api.")
append(domain) append(domain)
@ -68,18 +68,6 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
} }
return parseMangaList(webClient.httpGet(url).parseJson()) 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())
}
} }
} }

@ -25,51 +25,36 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context,
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val doc = when {
val doc = !filter.query.isNullOrEmpty() -> {
when (filter) { if (page > 1) {
is MangaListFilter.Search -> { return emptyList()
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()
} }
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 -> { else -> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
filter.tags.oneOrThrowIfMany()?.let {
append("/manga/")
append(it.key)
}
if (page > 1) { filter.tags.oneOrThrowIfMany()?.let {
append("/page/") append("/manga/")
append(page) append(it.key)
append('/')
}
} }
webClient.httpGet(url).parseHtml()
}
null -> { if (page > 1) {
val url = buildString { append("/page/")
append("https://") append(page)
append(domain) append('/')
if (page > 1) {
append("/page/")
append(page)
append('/')
}
} }
webClient.httpGet(url).parseHtml()
} }
webClient.httpGet(url).parseHtml()
} }
}
return doc.select("div.sect__content.grid-items div.item-poster").map { div -> return doc.select("div.sect__content.grid-items div.item-poster").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")

@ -23,15 +23,30 @@ internal class LugnicaScans(context: MangaLoaderContext) :
SortOrder.UPDATED, 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 configKeyDomain = ConfigKey.Domain("lugnica-scans.com")
override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = false,
isSearchSupported = false,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys) super.onCreateConfig(keys)
keys.add(userAgentKey) keys.add(userAgentKey)
@ -53,15 +68,15 @@ internal class LugnicaScans(context: MangaLoaderContext) :
) )
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.sortOrder == SortOrder.ALPHABETICAL) { if (order == SortOrder.ALPHABETICAL) {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
@ -92,16 +107,6 @@ internal class LugnicaScans(context: MangaLoaderContext) :
return parseMangaList(webClient.httpGet(url).parseJsonArray()) 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 +225,4 @@ internal class LugnicaScans(context: MangaLoaderContext) :
} }
return pages return pages
} }
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
} }

@ -30,26 +30,25 @@ internal class MangaKawaii(context: MangaLoaderContext) : PagedMangaParser(conte
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?query=") append("/search?query=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&search_type=manga&page=") append("&search_type=manga&page=")
append(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") 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") append("/manga-list")
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("/category/") append("/category/")
@ -61,12 +60,6 @@ internal class MangaKawaii(context: MangaLoaderContext) : PagedMangaParser(conte
return emptyList() return emptyList()
} }
} }
null -> {
if (page > 1) {
return emptyList()
}
}
} }
} }

@ -44,13 +44,12 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val postData = buildString { val postData = buildString {
append("page=") append("page=")
append(page) append(page)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (page > 1) { if (page > 1) {
return emptyList() return emptyList()
} }
@ -88,9 +87,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()) { 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.") throw IllegalArgumentException("Le filtrage par « tri par : mis à jour » avec les genres ou les statuts n'est pas pris en charge par cette source.")
@ -140,7 +139,7 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
} }
append("&sort_by=") append("&sort_by=")
when (filter.sortOrder) { when (order) {
SortOrder.RATING -> append("score&sort_dir=desc") SortOrder.RATING -> append("score&sort_dir=desc")
SortOrder.NEWEST -> append("updated_at&sort_dir=desc") SortOrder.NEWEST -> append("updated_at&sort_dir=desc")
SortOrder.ALPHABETICAL -> append("name&sort_dir=asc") SortOrder.ALPHABETICAL -> append("name&sort_dir=asc")
@ -149,8 +148,6 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
} }
} }
} }
null -> append("&sort_by=updated_at&sort_dir=desc")
} }
} }

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

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

@ -73,21 +73,21 @@ internal abstract class FuzzyDoodleParser(
protected open val pausedValue = "haitus" protected open val pausedValue = "haitus"
protected open val abandonedValue = "dropped" protected open val abandonedValue = "dropped"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/manga?page=") append("/manga?page=")
append(page) append(page)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&title=") append("&title=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("&type=") append("&type=")
append("&status=") append("&status=")
@ -110,8 +110,6 @@ internal abstract class FuzzyDoodleParser(
append(it.key) append(it.key)
} }
} }
null -> {}
} }
} }

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

@ -37,24 +37,24 @@ internal class Hentai3(context: MangaLoaderContext) :
Locale("pt"), Locale("pt"),
) )
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?q=") append("/search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search?q=") append("/search?q=")
append(buildQuery(filter.tags, filter.locale)) append(buildQuery(filter.tags, filter.locale))
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("&sort=popular") append("&sort=popular")
} }
append("&page=") append("&page=")
@ -64,7 +64,7 @@ internal class Hentai3(context: MangaLoaderContext) :
append(filter.locale.toLanguagePath()) append(filter.locale.toLanguagePath())
append("/") append("/")
append(page.toString()) append(page.toString())
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("?sort=popular") append("?sort=popular")
} }
} else if (filter.tags.isNotEmpty()) { } else if (filter.tags.isNotEmpty()) {
@ -74,7 +74,7 @@ internal class Hentai3(context: MangaLoaderContext) :
} }
append("/") append("/")
append(page.toString()) append(page.toString())
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("?sort=popular") append("?sort=popular")
} }
} else { } else {
@ -82,11 +82,6 @@ internal class Hentai3(context: MangaLoaderContext) :
append(page) append(page)
} }
} }
null -> {
append("/")
append(page)
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -24,18 +24,18 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) 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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search/?s_key=") append("/search/?s_key=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&") append("&")
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
if (filter.locale != null) { if (filter.locale != null) {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED) throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED)
@ -43,7 +43,7 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("/tag/") append("/tag/")
append(it.key) append(it.key)
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("/popular") append("/popular")
} }
append("/?") append("/?")
@ -56,8 +56,6 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
append("/?") append("/?")
} }
} }
null -> append("/?")
} }
append("page=") append("page=")
append(page) append(page)

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

@ -46,22 +46,22 @@ internal class HentaiForce(context: MangaLoaderContext) :
return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found") 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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?q=") append("/search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search?q=") append("/search?q=")
append(buildQuery(filter.tags, filter.locale)) append(buildQuery(filter.tags, filter.locale))
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("&sort=popular") append("&sort=popular")
} }
append("&page=") append("&page=")
@ -72,7 +72,7 @@ internal class HentaiForce(context: MangaLoaderContext) :
} }
append("/") append("/")
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("popular/") append("popular/")
} }
append("?") append("?")
@ -81,7 +81,7 @@ internal class HentaiForce(context: MangaLoaderContext) :
append(filter.locale.toLanguagePath()) append(filter.locale.toLanguagePath())
append("/") append("/")
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("popular/") append("popular/")
} }
append("?") append("?")
@ -89,8 +89,6 @@ internal class HentaiForce(context: MangaLoaderContext) :
append("/page/") append("/page/")
} }
} }
null -> append("/page/")
} }
append(page.toString()) append(page.toString())
} }

@ -22,12 +22,12 @@ internal class HentaiFox(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY) 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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search/?q=") append("/search/?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
if (page > 1) { if (page > 1) {
@ -36,7 +36,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
} }
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) { if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search/?q=") append("/search/?q=")
append(buildQuery(filter.tags, filter.locale)) append(buildQuery(filter.tags, filter.locale))
@ -45,7 +45,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
append(page.toString()) append(page.toString())
} }
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("&sort=popular") append("&sort=popular")
} }
} else if (filter.tags.isNotEmpty()) { } else if (filter.tags.isNotEmpty()) {
@ -54,7 +54,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
append(it.key) append(it.key)
} }
append("/") append("/")
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("popular/") append("popular/")
} }
@ -67,7 +67,7 @@ internal class HentaiFox(context: MangaLoaderContext) :
append("/language/") append("/language/")
append(filter.locale.toLanguagePath()) append(filter.locale.toLanguagePath())
append("/") append("/")
if (filter.sortOrder == SortOrder.POPULARITY) { if (order == SortOrder.POPULARITY) {
append("popular/") append("popular/")
} }
@ -88,18 +88,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("/")
}
}
} }
} }

@ -26,22 +26,28 @@ internal class NHentaiParser(context: MangaLoaderContext) :
".tag-container:contains(Languages:) span.tags a:not(.tag-17249) span.name" // tag-17249 = translated ".tag-container:contains(Languages:) span.tags a:not(.tag-17249) span.name" // tag-17249 = translated
override val idImg = "image-container" override val idImg = "image-container"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.POPULARITY_TODAY, SortOrder.POPULARITY_WEEK) 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() override fun getRequestHeaders(): Headers = super.getRequestHeaders().newBuilder()
.set("User-Agent", config[userAgentKey]) .set("User-Agent", config[userAgentKey])
.build() .build()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
!filter.query.isNullOrEmpty() -> {
is MangaListFilter.Search -> {
// Check if the query is all numbers // Check if the query is all numbers
val numericQuery = filter.query.trim() val numericQuery = filter.query.trim()
if (numericQuery.matches("\\d+".toRegex())) { if (numericQuery.matches("\\d+".toRegex())) {
@ -56,13 +62,13 @@ internal class NHentaiParser(context: MangaLoaderContext) :
} }
} }
is MangaListFilter.Advanced -> { else -> {
append("/search/?q=pages:>0 ") append("/search/?q=pages:>0 ")
// for Search with query // for Search with query
// append(filter.query.urlEncoded()) // append(filter.query.urlEncoded())
// append(' ') // append(' ')
append(buildQuery(filter.tags, filter.locale).urlEncoded()) append(buildQuery(filter.tags, filter.locale).urlEncoded())
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("&sort=popular") SortOrder.POPULARITY -> append("&sort=popular")
SortOrder.POPULARITY_TODAY -> append("&sort=popular-today") SortOrder.POPULARITY_TODAY -> append("&sort=popular-today")
SortOrder.POPULARITY_WEEK -> append("&sort=popular-week") SortOrder.POPULARITY_WEEK -> append("&sort=popular-week")
@ -70,8 +76,6 @@ internal class NHentaiParser(context: MangaLoaderContext) :
else -> {} else -> {}
} }
} }
null -> append("/search/?q=pages:>0 ")
} }
if (page > 1) { if (page > 1) {
append("&page=") append("&page=")
@ -122,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<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys) super.onCreateConfig(keys)
keys.add(userAgentKey) keys.add(userAgentKey)

@ -29,21 +29,20 @@ internal abstract class GattsuParser(
protected open val tagPrefix = "tag" protected open val tagPrefix = "tag"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/page/") append("/page/")
append(page.toString()) append(page.toString())
append("/?s=") append("/?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("/$tagPrefix/") append("/$tagPrefix/")
@ -54,11 +53,6 @@ internal abstract class GattsuParser(
append(page.toString()) append(page.toString())
} }
null -> {
append("/page/")
append(page.toString())
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

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

@ -39,27 +39,43 @@ internal abstract class HeanCms(
SortOrder.POPULARITY_ASC, 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 pathManga = "series"
protected open val apiPath protected open val apiPath
get() = getDomain("api") get() = getDomain("api")
protected open val paramsUpdated = "latest" protected open val paramsUpdated = "latest"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(apiPath) append(apiPath)
append("/query?query_string=") append("/query?query_string=")
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
filter.states.oneOrThrowIfMany()?.let { filter.states.oneOrThrowIfMany()?.let {
append("&status=") append("&status=")
@ -75,7 +91,7 @@ internal abstract class HeanCms(
} }
append("&orderBy=") append("&orderBy=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("total_views&order=desc") SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.POPULARITY_ASC -> append("total_views&order=asc") SortOrder.POPULARITY_ASC -> append("total_views&order=asc")
SortOrder.UPDATED -> append("$paramsUpdated&order=desc") SortOrder.UPDATED -> append("$paramsUpdated&order=desc")
@ -94,8 +110,6 @@ internal abstract class HeanCms(
append("]".urlEncoded()) append("]".urlEncoded())
} }
null -> append("&status=All&orderBy=$paramsUpdated&order=desc&series_type=Comic&perPage=20")
} }
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
@ -184,7 +198,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 doc = webClient.httpGet("https://$domain/comics").parseHtml()
val regex = Regex("\"tags\\\\.*?(\\[.+?])") val regex = Regex("\"tags\\\\.*?(\\[.+?])")
val tags = doc.select("script").firstNotNullOf { script -> val tags = doc.select("script").firstNotNullOf { script ->

@ -41,20 +41,13 @@ internal abstract class HeanCmsAlt(
protected open val selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)" protected open val selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)"
protected open val selectMangaTitle = "h5" protected open val selectMangaTitle = "h5"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) append(listUrl)
when (filter) { if (!filter.query.isNullOrEmpty()) {
is MangaListFilter.Search -> { throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
}
null -> {}
} }
if (page > 1) { if (page > 1) {
append("?page=") append("?page=")

@ -31,17 +31,35 @@ internal abstract class HotComicsParser(
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST)
override val isMultipleTagsSupported = false
protected open val mangasUrl = "/genres" protected open val mangasUrl = "/genres"
protected open val onePage = false protected open val onePage = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = false,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = emptySet(),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override fun getRequestHeaders(): Headers = Headers.Builder() override fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
if (onePage && page > 1) { if (onePage && page > 1) {
return emptyList() return emptyList()
} }
@ -49,16 +67,16 @@ internal abstract class HotComicsParser(
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?keyword=") append("/search?keyword=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page) append(page)
} }
is MangaListFilter.Advanced -> { else -> {
append(mangasUrl) append(mangasUrl)
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append('/') append('/')
@ -70,11 +88,6 @@ internal abstract class HotComicsParser(
append(page) append(page)
} }
} }
null -> {
append("/genres?page=")
append(page)
}
} }
} }
val tagMap = getOrCreateTagMap() val tagMap = getOrCreateTagMap()
@ -174,7 +187,7 @@ internal abstract class HotComicsParser(
} }
} }
override suspend fun getAvailableTags(): Set<MangaTag> { private suspend fun fetchAvailableTags(): Set<MangaTag> {
val map = getOrCreateTagMap() val map = getOrCreateTagMap()
val tagSet = ArraySet<MangaTag>(map.size) val tagSet = ArraySet<MangaTag>(map.size)
for (entry in map) { for (entry in map) {

@ -33,22 +33,22 @@ class DoujinDesuParser(context: MangaLoaderContext) :
.add("Referer", "https://$domain/") .add("Referer", "https://$domain/")
.build() .build()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = urlBuilder().apply { val url = urlBuilder().apply {
addPathSegment("manga") addPathSegment("manga")
addPathSegment("page") addPathSegment("page")
addPathSegment("$page/") addPathSegment("$page/")
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
addQueryParameter("title", filter.query) addQueryParameter("title", filter.query)
} }
is MangaListFilter.Advanced -> { else -> {
addQueryParameter("title", "") addQueryParameter("title", "")
addQueryParameter( addQueryParameter(
"order", "order",
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> "update" SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "popular" SortOrder.POPULARITY -> "popular"
SortOrder.ALPHABETICAL -> "title" SortOrder.ALPHABETICAL -> "title"
@ -72,8 +72,6 @@ class DoujinDesuParser(context: MangaLoaderContext) :
) )
} }
} }
null -> addQueryParameter("order", "update")
} }
}.build() }.build()

@ -24,20 +24,19 @@ internal class HentaiCrot(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/page/") append("/page/")
append(page) append(page)
append("/?s=") append("/?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("/category/") append("/category/")
@ -49,12 +48,6 @@ internal class HentaiCrot(context: MangaLoaderContext) :
append(page) append(page)
append('/') append('/')
} }
null -> {
append("/page/")
append(page)
append('/')
}
} }
} }

@ -24,20 +24,19 @@ internal class PixHentai(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/page/") append("/page/")
append(page) append(page)
append("/?s=") append("/?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append("/genre/") append("/genre/")
@ -49,12 +48,6 @@ internal class PixHentai(context: MangaLoaderContext) :
append(page) append(page)
append('/') append('/')
} }
null -> {
append("/page/")
append(page)
append('/')
}
} }
} }

@ -28,25 +28,45 @@ internal abstract class IkenParser(
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override val availableStates: Set<MangaState> = override val filterCapabilities: MangaListFilterCapabilities
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.UPCOMING) get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
override val isMultipleTagsSupported = true isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
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,
MangaState.UPCOMING,
),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/api/query?page=") append("/api/query?page=")
append(page) append(page)
append("&perPage=18&searchTerm=") append("&perPage=18&searchTerm=")
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&genreIds=") append("&genreIds=")
@ -66,8 +86,6 @@ internal abstract class IkenParser(
) )
} }
} }
null -> {}
} }
} }
return parseMangaList(webClient.httpGet(url).parseJson()) return parseMangaList(webClient.httpGet(url).parseJson())
@ -145,7 +163,7 @@ internal abstract class IkenParser(
} }
} }
override suspend fun getAvailableTags(): Set<MangaTag> { protected open suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/series").parseHtml() val doc = webClient.httpGet("https://$domain/series").parseHtml()
return doc.selectLastOrThrow("select").select("option[value]").mapNotNullToSet { return doc.selectLastOrThrow("select").select("option[value]").mapNotNullToSet {
val key = it.attr("value") ?: return@mapNotNullToSet null val key = it.attr("value") ?: return@mapNotNullToSet null

@ -45,32 +45,28 @@ class NicovideoSeigaParser(context: MangaLoaderContext) :
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nicovideo.jp") override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nicovideo.jp")
@InternalParsersApi override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val page = (offset / 20f).toIntUp().inc() val page = (offset / 20f).toIntUp().inc()
val domain = getDomain("seiga") val domain = getDomain("seiga")
val url = val url = when {
when (filter) { !filter.query.isNullOrEmpty() -> {
is MangaListFilter.Search -> { return if (offset == 0) getSearchList(filter.query, page) else emptyList()
return if (offset == 0) getSearchList(filter.query, page) else emptyList() }
}
is MangaListFilter.Advanced -> {
else -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany().let {
"https://$domain/manga/list?category=${it?.key}&page=$page&sort=${getSortKey(filter.sortOrder)}"
}
} else { if (filter.tags.isNotEmpty()) {
"https://$domain/manga/list?page=$page&sort=${getSortKey(filter.sortOrder)}" filter.tags.oneOrThrowIfMany().let {
"https://$domain/manga/list?category=${it?.key}&page=$page&sort=${getSortKey(order)}"
} }
} else {
"https://$domain/manga/list?page=$page&sort=${getSortKey(order)}"
} }
null -> "https://$domain/manga/list?page=$page"
} }
}
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
val comicList = doc.body().select("#comic_list > ul > li") ?: doc.parseFailed("Container not found") val comicList = doc.body().select("#comic_list > ul > li") ?: doc.parseFailed("Container not found")

@ -5,7 +5,7 @@ import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext 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.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
@ -17,8 +17,7 @@ internal abstract class KeyoappParser(
context: MangaLoaderContext, context: MangaLoaderContext,
source: MangaParserSource, source: MangaParserSource,
domain: String, domain: String,
pageSize: Int = 24, ) : SinglePageMangaParser(context, source) {
) : PagedMangaParser(context, source, pageSize) {
override val configKeyDomain = ConfigKey.Domain(domain) override val configKeyDomain = ConfigKey.Domain(domain)
@ -58,29 +57,20 @@ internal abstract class KeyoappParser(
"dropped", "dropped",
) )
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getList(order: SortOrder, filter: MangaListFilterV2): List<Manga> {
var query = "" var query = ""
var tag = "" var tag = ""
if (page > 1) {
return emptyList()
}
val url = urlBuilder().apply { val url = urlBuilder().apply {
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
addPathSegment("series") addPathSegment("series")
query = filter.query query = filter.query
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
@ -88,15 +78,13 @@ internal abstract class KeyoappParser(
} }
} }
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> addPathSegment("latest") SortOrder.UPDATED -> addPathSegment("latest")
SortOrder.NEWEST -> addPathSegment("series") SortOrder.NEWEST -> addPathSegment("series")
else -> addPathSegment("latest") else -> addPathSegment("latest")
} }
} }
null -> addPathSegment("latest")
} }
}.build() }.build()

@ -37,25 +37,25 @@ internal abstract class LikeMangaParser(
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/?act=search") append("/?act=search")
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&f") append("&f")
append("[keyword]".urlEncoded()) append("[keyword]".urlEncoded())
append("=") append("=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("&f") append("&f")
append("[sortby]".urlEncoded()) append("[sortby]".urlEncoded())
append("=") append("=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("hot") SortOrder.POPULARITY -> append("hot")
SortOrder.UPDATED -> append("lastest-chap") SortOrder.UPDATED -> append("lastest-chap")
SortOrder.NEWEST -> append("lastest-manga") SortOrder.NEWEST -> append("lastest-manga")
@ -85,12 +85,6 @@ internal abstract class LikeMangaParser(
) )
} }
} }
null -> {
append("&f")
append("[sortby]".urlEncoded())
append("=lastest-chap")
}
} }
if (page > 1) { if (page > 1) {

@ -27,6 +27,26 @@ internal abstract class MadaraParser(
override val configKeyDomain = ConfigKey.Domain(domain) override val configKeyDomain = ConfigKey.Domain(domain)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = false,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys) super.onCreateConfig(keys)
keys.add(userAgentKey) keys.add(userAgentKey)
@ -54,8 +74,6 @@ internal abstract class MadaraParser(
} }
} }
override val isMultipleTagsSupported = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.UPDATED_ASC, SortOrder.UPDATED_ASC,
@ -69,12 +87,6 @@ internal abstract class MadaraParser(
SortOrder.RATING_ASC, SortOrder.RATING_ASC,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT)
override val isTagsExclusionSupported = true
protected open val tagPrefix = "manga-genre/" protected open val tagPrefix = "manga-genre/"
protected open val datePattern = "MMMM d, yyyy" protected open val datePattern = "MMMM d, yyyy"
protected open val stylePage = "?style=list" protected open val stylePage = "?style=list"
@ -196,7 +208,7 @@ internal abstract class MadaraParser(
// can be changed to retrieve tags see getTags // can be changed to retrieve tags see getTags
protected open val listUrl = "manga/" protected open val listUrl = "manga/"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
if (withoutAjax) { if (withoutAjax) {
val pages = page + 1 val pages = page + 1
@ -204,9 +216,9 @@ internal abstract class MadaraParser(
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (pages > 1) { if (pages > 1) {
append("/page/") append("/page/")
append(pages.toString()) append(pages.toString())
@ -216,7 +228,7 @@ internal abstract class MadaraParser(
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
if (pages > 1) { if (pages > 1) {
append("/page/") append("/page/")
append(pages.toString()) append(pages.toString())
@ -278,7 +290,7 @@ internal abstract class MadaraParser(
append("&m_orderby=") append("&m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -288,10 +300,6 @@ internal abstract class MadaraParser(
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append("/?s=&post_type=wp-manga&m_orderby=latest")
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
@ -301,13 +309,13 @@ internal abstract class MadaraParser(
payload["page"] = page.toString() payload["page"] = page.toString()
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
payload["vars[s]"] = filter.query.urlEncoded() payload["vars[s]"] = filter.query.urlEncoded()
} }
is MangaListFilter.Advanced -> { else -> {
// Support query // Support query
@ -362,7 +370,7 @@ internal abstract class MadaraParser(
payload["vars[tax_query][relation]"] = "AND" payload["vars[tax_query][relation]"] = "AND"
} }
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> { SortOrder.POPULARITY -> {
payload["vars[meta_key]"] = "_wp_manga_views" payload["vars[meta_key]"] = "_wp_manga_views"
payload["vars[orderby]"] = "meta_value_num" payload["vars[orderby]"] = "meta_value_num"
@ -453,10 +461,6 @@ internal abstract class MadaraParser(
} }
} }
} }
null -> {
payload["vars[meta_key]"] = "_latest_update"
}
} }
return parseMangaList( return parseMangaList(
@ -510,7 +514,7 @@ internal abstract class MadaraParser(
} }
} }
override suspend fun getAvailableTags(): Set<MangaTag> { protected open suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
val body = doc.body() val body = doc.body()
val root1 = body.selectFirst("header")?.selectFirst("ul.second-menu") val root1 = body.selectFirst("header")?.selectFirst("ul.second-menu")

@ -29,20 +29,19 @@ internal class Manga18Fx(context: MangaLoaderContext) :
override val availableStates: Set<MangaState> get() = emptySet() override val availableStates: Set<MangaState> get() = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?q=") append("/search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -59,13 +58,6 @@ internal class Manga18Fx(context: MangaLoaderContext) :
} }
} }
} }
null -> {
if (page > 1) {
append("/page/")
append(page)
}
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -32,20 +32,20 @@ internal class Manhwa18Cc(context: MangaLoaderContext) :
override val availableStates: Set<MangaState> get() = emptySet() override val availableStates: Set<MangaState> get() = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?q=") append("/search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -60,7 +60,7 @@ internal class Manhwa18Cc(context: MangaLoaderContext) :
} }
append("?orderby=") append("?orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("trending") SortOrder.POPULARITY -> append("trending")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.ALPHABETICAL -> append("alphabet") SortOrder.ALPHABETICAL -> append("alphabet")
@ -68,10 +68,6 @@ internal class Manhwa18Cc(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append("?s&post_type=wp-manga&m_orderby=latest")
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -25,22 +25,27 @@ internal class AdultWebtoon(context: MangaLoaderContext) :
override val listUrl = "adult-webtoon/" override val listUrl = "adult-webtoon/"
override val postReq = true override val postReq = true
override val withoutAjax = true override val withoutAjax = true
override val isTagsExclusionSupported = false
override val availableSortOrders: Set<SortOrder> = override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING) EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING)
override val availableStates: Set<MangaState> = emptySet() override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isTagsExclusionSupported = false,
)
override val availableContentRating: Set<ContentRating> = emptySet() override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableStates = emptySet(),
availableContentRating = emptySet(),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val pages = page + 1 val pages = page + 1
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (pages > 1) { if (pages > 1) {
append("/page/") append("/page/")
append(pages.toString()) append(pages.toString())
@ -50,7 +55,7 @@ internal class AdultWebtoon(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
@ -71,7 +76,7 @@ internal class AdultWebtoon(context: MangaLoaderContext) :
} }
append("?m_orderby=") append("?m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -80,16 +85,6 @@ internal class AdultWebtoon(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append('/')
append(listUrl)
if (pages > 1) {
append("page/")
append(pages)
append("/?m_orderby=latest")
}
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -26,12 +26,12 @@ internal class Hentai4Free(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/page/") append("/page/")
append(page.toString()) append(page.toString())
append("/?s=") append("/?s=")
@ -39,7 +39,7 @@ internal class Hentai4Free(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -85,7 +85,7 @@ internal class Hentai4Free(context: MangaLoaderContext) :
} }
append("m_orderby=") append("m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -96,14 +96,6 @@ internal class Hentai4Free(context: MangaLoaderContext) :
} }
null -> {
if (page > 1) {
append("/page/")
append(page.toString())
}
append("/?m_orderby=latest")
}
} }
} }

@ -29,14 +29,14 @@ internal class HentaiManga(context: MangaLoaderContext) :
override val availableStates: Set<MangaState> = emptySet() override val availableStates: Set<MangaState> = emptySet()
override val availableContentRating: Set<ContentRating> = emptySet() override val availableContentRating: Set<ContentRating> = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val pages = page + 1 val pages = page + 1
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (pages > 1) { if (pages > 1) {
append("/page/") append("/page/")
append(pages.toString()) append(pages.toString())
@ -46,7 +46,7 @@ internal class HentaiManga(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
@ -67,7 +67,7 @@ internal class HentaiManga(context: MangaLoaderContext) :
} }
append("?m_orderby=") append("?m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -76,16 +76,6 @@ internal class HentaiManga(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append('/')
append(listUrl)
if (pages > 1) {
append("page/")
append(pages)
append("/?m_orderby=latest")
}
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -23,20 +23,27 @@ internal class HentaiWebtoon(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.HENTAIWEBTOON, "hentaiwebtoon.com") { MadaraParser(context, MangaParserSource.HENTAIWEBTOON, "hentaiwebtoon.com") {
override val postReq = true override val postReq = true
override val withoutAjax = true override val withoutAjax = true
override val isTagsExclusionSupported = false
override val availableSortOrders: Set<SortOrder> = override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING) EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING)
override val availableStates: Set<MangaState> = emptySet()
override val availableContentRating: Set<ContentRating> = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isMultipleTagsSupported = false,
)
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableContentRating = emptySet(),
availableStates = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val pages = page + 1 val pages = page + 1
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (pages > 1) { if (pages > 1) {
append("/page/") append("/page/")
append(pages.toString()) append(pages.toString())
@ -46,7 +53,7 @@ internal class HentaiWebtoon(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
@ -67,7 +74,7 @@ internal class HentaiWebtoon(context: MangaLoaderContext) :
} }
append("?m_orderby=") append("?m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -76,16 +83,6 @@ internal class HentaiWebtoon(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append('/')
append(listUrl)
if (pages > 1) {
append("page/")
append(pages)
append("/?m_orderby=latest")
}
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -28,13 +28,12 @@ internal class IsekaiScan(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/?search=") append("/?search=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
@ -42,14 +41,14 @@ internal class IsekaiScan(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("/$tagPrefix") append("/$tagPrefix")
append(tag?.key.orEmpty()) append(tag?.key.orEmpty())
append("?orderby=") append("?orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("2") SortOrder.POPULARITY -> append("2")
SortOrder.UPDATED -> append("3") SortOrder.UPDATED -> append("3")
else -> append("3") else -> append("3")
@ -57,7 +56,7 @@ internal class IsekaiScan(context: MangaLoaderContext) :
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} else { } else {
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("/popular-manga") SortOrder.POPULARITY -> append("/popular-manga")
SortOrder.UPDATED -> append("/latest-manga") SortOrder.UPDATED -> append("/latest-manga")
else -> append("/latest-manga") else -> append("/latest-manga")
@ -66,11 +65,6 @@ internal class IsekaiScan(context: MangaLoaderContext) :
append(page.toString()) append(page.toString())
} }
} }
null -> {
append("/latest-manga?page=")
append(page.toString())
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -23,15 +23,14 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (page > 1) { if (page > 1) {
append("/page/") append("/page/")
append(page.toString()) append(page.toString())
@ -41,7 +40,7 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -84,7 +83,7 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
} }
append("m_orderby=") append("m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -93,10 +92,6 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append("/?s&post_type=wp-manga&m_orderby=latest")
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -32,20 +32,19 @@ internal class MangaDass(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?q=") append("/search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -62,7 +61,7 @@ internal class MangaDass(context: MangaLoaderContext) :
} }
append("orderby=") append("orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -71,13 +70,6 @@ internal class MangaDass(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append("/$listUrl")
append("/")
append(page.toString())
append("?orderby=latest")
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -25,19 +25,19 @@ internal class MangaDna(context: MangaLoaderContext) :
override val availableStates: Set<MangaState> = emptySet() override val availableStates: Set<MangaState> = emptySet()
override val availableContentRating: Set<ContentRating> = emptySet() override val availableContentRating: Set<ContentRating> = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?q=") append("/search?q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -52,7 +52,7 @@ internal class MangaDna(context: MangaLoaderContext) :
} }
append("?orderby=") append("?orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("trending") SortOrder.POPULARITY -> append("trending")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.ALPHABETICAL -> append("alphabet") SortOrder.ALPHABETICAL -> append("alphabet")
@ -60,13 +60,6 @@ internal class MangaDna(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append("/$listUrl")
append("/page/")
append(page.toString())
append("?orderby=latest")
}
} }
} }

@ -32,12 +32,12 @@ internal class MangaPure(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?s=") append("/search?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
@ -45,14 +45,14 @@ internal class MangaPure(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("/$tagPrefix") append("/$tagPrefix")
append(tag?.key.orEmpty()) append(tag?.key.orEmpty())
append("?orderby=") append("?orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("2") SortOrder.POPULARITY -> append("2")
SortOrder.UPDATED -> append("3") SortOrder.UPDATED -> append("3")
else -> append("3") else -> append("3")
@ -60,7 +60,7 @@ internal class MangaPure(context: MangaLoaderContext) :
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} else { } else {
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("/popular-manga") SortOrder.POPULARITY -> append("/popular-manga")
SortOrder.UPDATED -> append("/latest-manga") SortOrder.UPDATED -> append("/latest-manga")
else -> append("/latest-manga") else -> append("/latest-manga")
@ -69,11 +69,6 @@ internal class MangaPure(context: MangaLoaderContext) :
append(page.toString()) append(page.toString())
} }
} }
null -> {
append("/latest-manga?page=")
append(page.toString())
}
} }
} }

@ -33,19 +33,19 @@ internal class Manhwaz(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?s=") append("/search?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -62,7 +62,7 @@ internal class Manhwaz(context: MangaLoaderContext) :
} }
append("m_orderby=") append("m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new") SortOrder.NEWEST -> append("new")
@ -70,13 +70,6 @@ internal class Manhwaz(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append("/$listUrl")
append("?page=")
append(page.toString())
append("&m_orderby=latest")
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()

@ -32,14 +32,14 @@ internal class ManyToon(context: MangaLoaderContext) :
override val availableContentRating: Set<ContentRating> = emptySet() override val availableContentRating: Set<ContentRating> = emptySet()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val pages = page + 1 val pages = page + 1
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
if (pages > 1) { if (pages > 1) {
append("/page/") append("/page/")
append(pages.toString()) append(pages.toString())
@ -49,7 +49,7 @@ internal class ManyToon(context: MangaLoaderContext) :
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
@ -70,7 +70,7 @@ internal class ManyToon(context: MangaLoaderContext) :
} }
append("?m_orderby=") append("?m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
@ -79,16 +79,6 @@ internal class ManyToon(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append('/')
append(listUrl)
if (pages > 1) {
append("page/")
append(pages)
append("/?m_orderby=latest")
}
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -20,19 +20,19 @@ internal class DragonTranslationParser(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/mangas?buscar=") append("/mangas?buscar=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
append("/mangas?page=") append("/mangas?page=")
append(page.toString()) append(page.toString())
@ -43,11 +43,6 @@ internal class DragonTranslationParser(context: MangaLoaderContext) :
append(tag?.key.orEmpty()) append(tag?.key.orEmpty())
} }
} }
null -> {
append("/mangas?page=")
append(page.toString())
}
} }
} }

@ -26,12 +26,12 @@ internal class TmoManga(context: MangaLoaderContext) :
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/$listUrl") append("/$listUrl")
append("?search=") append("?search=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
@ -41,7 +41,7 @@ internal class TmoManga(context: MangaLoaderContext) :
} }
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -59,14 +59,6 @@ internal class TmoManga(context: MangaLoaderContext) :
} }
} }
} }
null -> {
append("/$listUrl")
if (page > 1) {
append("?page=")
append(page)
}
}
} }
} }

@ -15,31 +15,38 @@ internal class ManhwaHub(context: MangaLoaderContext) :
override val datePattern = "MMMM d, yyyy" override val datePattern = "MMMM d, yyyy"
override val sourceLocale: Locale = Locale.ENGLISH override val sourceLocale: Locale = Locale.ENGLISH
override val withoutAjax = true override val withoutAjax = true
override val isTagsExclusionSupported = false
override val listUrl = "genre/manhwa" override val listUrl = "genre/manhwa"
override val selectTestAsync = "ul.box-list-chapter" override val selectTestAsync = "ul.box-list-chapter"
override val availableStates: Set<MangaState> = emptySet()
override val availableContentRating: Set<ContentRating> = emptySet()
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isTagsExclusionSupported = false,
)
init { init {
paginator.firstPage = 1 paginator.firstPage = 1
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableStates = emptySet(),
availableContentRating = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?s=") append("/search?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -55,11 +62,6 @@ internal class ManhwaHub(context: MangaLoaderContext) :
} }
null -> {
append("/?page=")
append(page.toString())
}
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
@ -98,7 +100,7 @@ internal class ManhwaHub(context: MangaLoaderContext) :
} }
} }
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain").parseHtml() val doc = webClient.httpGet("https://$domain").parseHtml()
return doc.select("div.genres li").mapNotNullToSet { li -> return doc.select("div.genres li").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null val a = li.selectFirst("a") ?: return@mapNotNullToSet null

@ -6,9 +6,11 @@ import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.* import java.util.*
import kotlin.collections.Set
@Broken @Broken
@MangaSourceParser("SAYTRUYENHAY", "PheTruyen", "vi") @MangaSourceParser("SAYTRUYENHAY", "PheTruyen", "vi")
@ -17,31 +19,38 @@ internal class Saytruyenhay(context: MangaLoaderContext) :
override val tagPrefix = "genre/" override val tagPrefix = "genre/"
override val withoutAjax = true override val withoutAjax = true
override val isTagsExclusionSupported = false
override val listUrl = "public/genre/manga/" override val listUrl = "public/genre/manga/"
override val availableStates: Set<MangaState> = emptySet()
override val availableContentRating: Set<ContentRating> = emptySet()
override val availableSortOrders: Set<SortOrder> = override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.RATING, SortOrder.NEWEST) EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.RATING, SortOrder.NEWEST)
override val filterCapabilities: MangaListFilterCapabilities
get() = super.filterCapabilities.copy(
isTagsExclusionSupported = false,
)
init { init {
paginator.firstPage = 1 paginator.firstPage = 1
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableStates = emptySet(),
availableContentRating = emptySet(),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("/search?s=") append("/search?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
is MangaListFilter.Advanced -> { else -> {
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -54,7 +63,7 @@ internal class Saytruyenhay(context: MangaLoaderContext) :
append("?page=") append("?page=")
append(page.toString()) append(page.toString())
append("&m_orderby=") append("&m_orderby=")
when (filter.sortOrder) { when (order) {
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
@ -62,12 +71,6 @@ internal class Saytruyenhay(context: MangaLoaderContext) :
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append("/$listUrl")
append("?page=")
append(page.toString())
}
} }
} }

@ -34,8 +34,6 @@ internal abstract class MadthemeParser(
SortOrder.RATING, SortOrder.RATING,
) )
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
protected open val listUrl = "search/" protected open val listUrl = "search/"
protected open val datePattern = "MMM dd, yyyy" protected open val datePattern = "MMM dd, yyyy"
@ -59,23 +57,43 @@ internal abstract class MadthemeParser(
"COMPLETED", "COMPLETED",
) )
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = false,
isSearchSupported = true,
isSearchWithFiltersSupported = false,
isYearSupported = false,
isYearRangeSupported = false,
isSourceLocaleSupported = 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, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append('/') append('/')
append(listUrl) append(listUrl)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("?sort=updated_at&q=") append("?sort=updated_at&q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("?sort=") append("?sort=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("updated_at") SortOrder.UPDATED -> append("updated_at")
SortOrder.ALPHABETICAL -> append("name") // On some sites without tags or searches, the alphabetical option is empty. SortOrder.ALPHABETICAL -> append("name") // On some sites without tags or searches, the alphabetical option is empty.
@ -104,8 +122,6 @@ internal abstract class MadthemeParser(
} }
} }
null -> append("?sort=updated_at")
} }
append("&page=") append("&page=")
@ -140,7 +156,7 @@ internal abstract class MadthemeParser(
} }
} }
override suspend fun getAvailableTags(): Set<MangaTag> { private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select("div.genres .checkbox").mapNotNullToSet { checkbox -> return doc.select("div.genres .checkbox").mapNotNullToSet { checkbox ->
val key = checkbox.selectFirstOrThrow("input").attr("value") ?: return@mapNotNullToSet null val key = checkbox.selectFirstOrThrow("input").attr("value") ?: return@mapNotNullToSet null

@ -17,23 +17,23 @@ internal class ManhuaScan(context: MangaLoaderContext) :
override val sourceLocale: Locale = Locale.ENGLISH override val sourceLocale: Locale = Locale.ENGLISH
override val listUrl = "search" override val listUrl = "search"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append('/') append('/')
append(listUrl) append(listUrl)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("?sort=updated_at&q=") append("?sort=updated_at&q=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
append("?sort=") append("?sort=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("updated_at") SortOrder.UPDATED -> append("updated_at")
SortOrder.ALPHABETICAL -> append("name") SortOrder.ALPHABETICAL -> append("name")
@ -62,8 +62,6 @@ internal class ManhuaScan(context: MangaLoaderContext) :
} }
} }
null -> append("?sort=updated_at")
} }
append("&page=") append("&page=")

@ -54,14 +54,14 @@ internal abstract class Manga18Parser(
"Completed", "Completed",
) )
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append('/') append('/')
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append(listUrl) append(listUrl)
append(page.toString()) append(page.toString())
append("?search=") append("?search=")
@ -69,7 +69,7 @@ internal abstract class Manga18Parser(
append("&order_by=latest") append("&order_by=latest")
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append(tagUrl) append(tagUrl)
@ -82,19 +82,13 @@ internal abstract class Manga18Parser(
append(page.toString()) append(page.toString())
append("?order_by=") append("?order_by=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("lastest") SortOrder.UPDATED -> append("lastest")
SortOrder.ALPHABETICAL -> append("name") SortOrder.ALPHABETICAL -> append("name")
else -> append("latest") else -> append("latest")
} }
} }
null -> {
append(listUrl)
append(page.toString())
append("?order_by=latest")
}
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())

@ -55,20 +55,20 @@ internal abstract class MangaboxParser(
"completed", "completed",
) )
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append(listUrl) append(listUrl)
append("/?s=all") append("/?s=all")
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append("&keyw=") append("&keyw=")
append(filter.query.replace(" ", "_").urlEncoded()) append(filter.query.replace(" ", "_").urlEncoded())
} }
is MangaListFilter.Advanced -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&g_i=") append("&g_i=")
@ -100,7 +100,7 @@ internal abstract class MangaboxParser(
} }
append("&orby=") append("&orby=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("topview") SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("") SortOrder.UPDATED -> append("")
SortOrder.NEWEST -> append("newest") SortOrder.NEWEST -> append("newest")
@ -108,8 +108,6 @@ internal abstract class MangaboxParser(
else -> append("") else -> append("")
} }
} }
null -> {}
} }
append("&page=") append("&page=")

@ -33,22 +33,23 @@ internal class Mangairo(context: MangaLoaderContext) :
) )
override val isTagsExclusionSupported = false override val isTagsExclusionSupported = false
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append(searchUrl) append(searchUrl)
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("?page=") append("?page=")
} }
is MangaListFilter.Advanced -> { else -> {
append(listUrl) append(listUrl)
append("/type-") append("/type-")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("topview") SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("newest") SortOrder.NEWEST -> append("newest")
@ -81,11 +82,6 @@ internal class Mangairo(context: MangaLoaderContext) :
append("/page-") append("/page-")
} }
null -> {
append(listUrl)
append("/type-latest/ctg-all/state-all/page-")
}
} }
append(page.toString()) append(page.toString())
} }

@ -24,13 +24,13 @@ internal class Mangakakalot(context: MangaLoaderContext) :
override val otherDomain = "chapmanganato.com" override val otherDomain = "chapmanganato.com"
override val listUrl = "/manga_list" override val listUrl = "/manga_list"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when (filter) { when {
is MangaListFilter.Search -> { !filter.query.isNullOrEmpty() -> {
append(searchUrl) append(searchUrl)
val regex = Regex("[^A-Za-z0-9 ]") val regex = Regex("[^A-Za-z0-9 ]")
val q = regex.replace(filter.query, "") val q = regex.replace(filter.query, "")
@ -38,10 +38,10 @@ internal class Mangakakalot(context: MangaLoaderContext) :
append("?page=") append("?page=")
} }
is MangaListFilter.Advanced -> { else -> {
append(listUrl) append(listUrl)
append("?type=") append("?type=")
when (filter.sortOrder) { when (order) {
SortOrder.POPULARITY -> append("topview") SortOrder.POPULARITY -> append("topview")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("newest") SortOrder.NEWEST -> append("newest")
@ -67,11 +67,6 @@ internal class Mangakakalot(context: MangaLoaderContext) :
append("&page=") append("&page=")
} }
null -> {
append(listUrl)
append("?type=latest&page=")
}
} }
append(page.toString()) append(page.toString())
} }

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

Loading…
Cancel
Save