From f107e1152859501bc432342d3bc9e86ce475860b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 24 Feb 2025 17:14:27 +0200 Subject: [PATCH] Refactoring --- .../koitharu/kotatsu/parsers/MangaParser.kt | 26 ++- .../kotatsu/parsers/PagedMangaParser.kt | 27 ++-- .../kotatsu/parsers/SinglePageMangaParser.kt | 8 +- .../kotatsu/parsers/model/MangaListFilter.kt | 46 +++--- .../parsers/model/search/MangaSearchQuery.kt | 110 ++++--------- .../search/MangaSearchQueryCapabilities.kt | 80 ++-------- .../parsers/model/search/QueryCriteria.kt | 72 ++++----- .../parsers/model/search/SearchableField.kt | 2 +- .../parsers/site/all/HitomiLaParser.kt | 88 +++++------ .../parsers/site/all/MangaDexParser.kt | 103 +++++++++--- .../kotatsu/parsers/site/en/WeebCentral.kt | 42 ++--- .../parsers/site/mangabox/MangaboxParser.kt | 52 ++++-- .../parsers/site/mangabox/en/Mangairo.kt | 39 +++-- .../parsers/site/mangabox/en/Mangakakalot.kt | 34 +++- .../site/mangabox/en/MangakakalotTv.kt | 33 +++- .../parsers/util/SearchQueryConverter.kt | 148 ++++++++++++++---- .../kotatsu/parsers/MangaParserTest.kt | 34 ++-- .../model/search/MangaSearchQueryTest.kt | 16 +- .../ListFilterToSearchQueryConverterTest.kt | 8 +- .../SearchQueryToListFilterConverterTest.kt | 18 ++- 20 files changed, 554 insertions(+), 432 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt index 3621d202..c11a99d9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt @@ -28,7 +28,7 @@ public abstract class MangaParser @InternalParsersApi constructor( public abstract val filterCapabilities: MangaListFilterCapabilities public open val searchQueryCapabilities: MangaSearchQueryCapabilities - get() = MangaSearchQueryCapabilities.from(filterCapabilities) + get() = filterCapabilities.toMangaSearchQueryCapabilities() public val config: MangaSourceConfig by lazy { context.getConfig(source) } @@ -68,26 +68,24 @@ public abstract class MangaParser @InternalParsersApi constructor( * * @param searchQuery searchQuery */ - public suspend fun searchManga(searchQuery: MangaSearchQuery, validateQuery: Boolean = true): List { - if (validateQuery) { + public suspend fun queryManga(searchQuery: MangaSearchQuery): List { + if (!searchQuery.skipValidation) { searchQueryCapabilities.validate(searchQuery) } - return validatedMangaSearch(searchQuery) + return getList(searchQuery) } /** * Search list of manga by specified searchQuery * - * @param searchQuery searchQuery + * @param query searchQuery */ - protected open suspend fun validatedMangaSearch(searchQuery: MangaSearchQuery): List { - return getList( - searchQuery.offset ?: 0, - searchQuery.order ?: defaultSortOrder, - convertToMangaListFilter(searchQuery), - ) - } + protected open suspend fun getList(query: MangaSearchQuery): List = getList( + offset = query.offset, + order = query.order ?: defaultSortOrder, + filter = convertToMangaListFilter(query), + ) /** * Parse list of manga by specified criteria @@ -97,9 +95,9 @@ public abstract class MangaParser @InternalParsersApi constructor( * @param order one of [availableSortOrders] or [defaultSortOrder] for default value * @param filter is a set of filter rules * - * @deprecated New [searchManga] should be preferred. + * @deprecated New [getList] should be preferred. */ - @Deprecated("New searchManga method should be preferred") + @Deprecated("New getList(query: MangaSearchQuery) method should be preferred") public abstract suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List /** diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/PagedMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/PagedMangaParser.kt index 23179207..e80a07cc 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/PagedMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/PagedMangaParser.kt @@ -24,9 +24,9 @@ public abstract class PagedMangaParser( @JvmField protected val searchPaginator: Paginator = Paginator(searchPageSize) - final override suspend fun validatedMangaSearch(searchQuery: MangaSearchQuery): List { + final override suspend fun getList(query: MangaSearchQuery): List { var containTitleNameCriteria = false - searchQuery.criteria.forEach { + query.criteria.forEach { if (it.field == SearchableField.TITLE_NAME) { containTitleNameCriteria = true } @@ -38,15 +38,15 @@ public abstract class PagedMangaParser( } else { searchPaginator }, - searchQuery = searchQuery, + query = query, ) } - public open suspend fun searchPageManga(searchQuery: MangaSearchQuery): List { - return getList( - searchQuery.offset ?: 0, - searchQuery.order ?: defaultSortOrder, - convertToMangaListFilter(searchQuery), + public open suspend fun getListPage(query: MangaSearchQuery, page: Int): List { + return getListPage( + page = page, + order = query.order ?: defaultSortOrder, + filter = convertToMangaListFilter(query), ) } @@ -81,16 +81,11 @@ public abstract class PagedMangaParser( private suspend fun searchManga( paginator: Paginator, - searchQuery: MangaSearchQuery, + query: MangaSearchQuery, ): List { - val offset: Int = searchQuery.offset ?: 0 + val offset: Int = query.offset val page = paginator.getPage(offset) - val list = searchPageManga( - MangaSearchQuery.builder() - .copy(searchQuery) - .offset(page) - .build(), - ) + val list = getListPage(query, page) paginator.onListReceived(offset, page, list.size) return list } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/SinglePageMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/SinglePageMangaParser.kt index a4e32f5e..406f2181 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/SinglePageMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/SinglePageMangaParser.kt @@ -14,16 +14,16 @@ public abstract class SinglePageMangaParser( ) : MangaParser(context, source) { - final override suspend fun validatedMangaSearch(searchQuery: MangaSearchQuery): List { - if (searchQuery.offset != null && searchQuery.offset > 0) { + final override suspend fun getList(query: MangaSearchQuery): List { + if (query.offset > 0) { return emptyList() } - return searchSinglePageManga(searchQuery) + return searchSinglePageManga(query) } public open suspend fun searchSinglePageManga(searchQuery: MangaSearchQuery): List { return getList( - searchQuery.offset ?: 0, + searchQuery.offset, searchQuery.order ?: defaultSortOrder, convertToMangaListFilter(searchQuery), ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt index febf96ee..771f2287 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt @@ -44,7 +44,7 @@ public data class MangaListFilter( public val EMPTY: MangaListFilter = MangaListFilter() } - public class Builder { + internal class Builder { private var query: String? = null private val tags: MutableSet = mutableSetOf() private val tagsExclude: MutableSet = mutableSetOf() @@ -58,28 +58,32 @@ public data class MangaListFilter( private var yearFrom: Int = YEAR_UNKNOWN private var yearTo: Int = YEAR_UNKNOWN - public fun query(query: String?): Builder = apply { this.query = query } - public fun addTag(tag: MangaTag): Builder = apply { tags.add(tag) } - public fun addTags(tags: Collection): Builder = apply { this.tags.addAll(tags) } - public fun excludeTag(tag: MangaTag): Builder = apply { tagsExclude.add(tag) } - public fun excludeTags(tags: Collection): Builder = apply { this.tagsExclude.addAll(tags) } - public fun locale(locale: Locale?): Builder = apply { this.locale = locale } - public fun originalLocale(locale: Locale?): Builder = apply { this.originalLocale = locale } - public fun addState(state: MangaState): Builder = apply { states.add(state) } - public fun addStates(states: Collection): Builder = apply { this.states.addAll(states) } - public fun addContentRating(rating: ContentRating): Builder = apply { contentRating.add(rating) } - public fun addContentRatings(ratings: Collection): Builder = apply { this.contentRating.addAll(ratings) } - public fun addType(type: ContentType): Builder = apply { types.add(type) } - public fun addTypes(types: Collection): Builder = apply { this.types.addAll(types) } - public fun addDemographic(demographic: Demographic): Builder = apply { demographics.add(demographic) } - public fun addDemographics(demographics: Collection): Builder = apply { this.demographics.addAll(demographics) } - public fun year(year: Int): Builder = apply { this.year = year } - public fun yearFrom(year: Int): Builder = apply { this.yearFrom = year } - public fun yearTo(year: Int): Builder = apply { this.yearTo = year } + fun query(query: String?): Builder = apply { this.query = query } + fun addTag(tag: MangaTag): Builder = apply { tags.add(tag) } + fun addTags(tags: Collection): Builder = apply { this.tags.addAll(tags) } + fun excludeTag(tag: MangaTag): Builder = apply { tagsExclude.add(tag) } + fun excludeTags(tags: Collection): Builder = apply { this.tagsExclude.addAll(tags) } + fun locale(locale: Locale?): Builder = apply { this.locale = locale } + fun originalLocale(locale: Locale?): Builder = apply { this.originalLocale = locale } + fun addState(state: MangaState): Builder = apply { states.add(state) } + fun addStates(states: Collection): Builder = apply { this.states.addAll(states) } + fun addContentRating(rating: ContentRating): Builder = apply { contentRating.add(rating) } + fun addContentRatings(ratings: Collection): Builder = + apply { this.contentRating.addAll(ratings) } - public fun build(): MangaListFilter = MangaListFilter( + fun addType(type: ContentType): Builder = apply { types.add(type) } + fun addTypes(types: Collection): Builder = apply { this.types.addAll(types) } + fun addDemographic(demographic: Demographic): Builder = apply { demographics.add(demographic) } + fun addDemographics(demographics: Collection): Builder = + apply { this.demographics.addAll(demographics) } + + fun year(year: Int): Builder = apply { this.year = year } + fun yearFrom(year: Int): Builder = apply { this.yearFrom = year } + fun yearTo(year: Int): Builder = apply { this.yearTo = year } + + fun build(): MangaListFilter = MangaListFilter( query, tags, tagsExclude, locale, originalLocale, states, - contentRating, types, demographics, year, yearFrom, yearTo + contentRating, types, demographics, year, yearFrom, yearTo, ) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt index dde23d10..628b13f3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQuery.kt @@ -1,5 +1,7 @@ package org.koitharu.kotatsu.parsers.model.search +import androidx.collection.ArrayMap +import androidx.collection.ArraySet import org.koitharu.kotatsu.parsers.model.SortOrder /** @@ -10,98 +12,48 @@ import org.koitharu.kotatsu.parsers.model.SortOrder * @property order The sorting order for the results (optional). * @property offset The offset number for paginated search results (optional). */ -public class MangaSearchQuery private constructor( - @JvmField public val criteria: Set> = emptySet(), - @JvmField public val order: SortOrder? = null, - @JvmField public val offset: Int? = null, -) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is MangaSearchQuery) return false - return criteria == other.criteria && - order == other.order && - offset == other.offset - } +@ConsistentCopyVisibility +public data class MangaSearchQuery private constructor( + @JvmField public val criteria: Set>, + @JvmField public val order: SortOrder?, + @JvmField public val offset: Int, + @JvmField public val skipValidation: Boolean, +) { - override fun hashCode(): Int { - var result = criteria.hashCode() - result = 31 * result + (order?.hashCode() ?: 0) - result = 31 * result + (offset ?: 0) - return result - } - - public companion object { - public fun builder(): Builder = Builder() - } + public fun newBuilder(): Builder = Builder(this) public class Builder { - private var criteria: MutableSet> = mutableSetOf() + + private val criteria = ArraySet>() private var order: SortOrder? = null - private var offset: Int? = null + private var offset: Int = 0 + private var skipValidation: Boolean = false - public fun copy(searchQuery: MangaSearchQuery): Builder = apply { - this.criteria = searchQuery.criteria as MutableSet> - this.order = searchQuery.order - this.offset = searchQuery.offset - } + public constructor() - @Throws(IllegalArgumentException::class) - public fun criterion(criterion: QueryCriteria<*>): Builder = apply { - validateCriterion(criterion) - this.criteria.add(criterion) + public constructor(query: MangaSearchQuery) : this() { + criteria.addAll(query.criteria) + order = query.order + offset = query.offset } + public fun criterion(criterion: QueryCriteria<*>): Builder = apply { criteria.add(criterion) } + public fun order(order: SortOrder?): Builder = apply { this.order = order } - public fun offset(offset: Int?): Builder = apply { this.offset = offset } + public fun offset(offset: Int): Builder = apply { this.offset = offset } + + public fun skipValidation(skip: Boolean): Builder = apply { this.skipValidation = skip } @Throws(IllegalArgumentException::class) public fun build(): MangaSearchQuery { - return MangaSearchQuery(deduplicateCriteria(criteria), order, offset) - } - - /** - * Validates the provided [QueryCriteria] to ensure type correctness. - * - * @param criterion The search criterion to validate. - * @throws IllegalArgumentException If the criterion type does not match the expected type. - */ - private fun validateCriterion(criterion: QueryCriteria<*>) { - try { - val expectedType = criterion.field.type - val actualType: Class<*>? = when (criterion) { - is QueryCriteria.Include<*> -> criterion.values.first().javaClass - is QueryCriteria.Exclude<*> -> criterion.values.first().javaClass - is QueryCriteria.Match<*> -> criterion.value.javaClass - is QueryCriteria.Range<*> -> { - if (criterion.from.javaClass != criterion.to.javaClass) { - throw IllegalArgumentException( - "Mismatched types for field '${criterion.field}'. 'from' and 'to' should have same types" - ) - } - criterion.from.javaClass - } - } - - val isCompatibleIntType = (expectedType == Int::class.java && actualType == Integer::class.java) || - (expectedType == Integer::class.java && actualType == Int::class.java) - - if (actualType != null && !expectedType.isAssignableFrom(actualType) && !isCompatibleIntType) { - throw IllegalArgumentException( - "Invalid type for ${criterion.field}. Expected: ${expectedType.simpleName}, but got: ${actualType.simpleName}" - ) - } - } catch (e: NoSuchElementException) { - throw IllegalArgumentException( - "QueryCriteria values should not be empty" - ) - } + return MangaSearchQuery(deduplicateCriteria(criteria), order, offset, skipValidation) } private fun deduplicateCriteria(criteria: Set>): Set> { - val uniqueCriteria = mutableMapOf>>, QueryCriteria<*>>() + val uniqueCriteria = + ArrayMap>>, QueryCriteria<*>>(criteria.size) for (criterion in criteria) { val key = criterion.field to criterion::class.java @@ -111,15 +63,17 @@ public class MangaSearchQuery private constructor( existing == null -> uniqueCriteria[key] = criterion existing is QueryCriteria.Include<*> && criterion is QueryCriteria.Include<*> -> { - uniqueCriteria[key] = QueryCriteria.Include(criterion.field,existing.values union criterion.values) + uniqueCriteria[key] = + QueryCriteria.Include(criterion.field, existing.values union criterion.values) } existing is QueryCriteria.Exclude<*> && criterion is QueryCriteria.Exclude<*> -> { - uniqueCriteria[key] = QueryCriteria.Exclude(criterion.field,existing.values union criterion.values) + uniqueCriteria[key] = + QueryCriteria.Exclude(criterion.field, existing.values union criterion.values) } else -> throw IllegalArgumentException( - "Match and Range have only one criterion per type, but found duplicates for: ${criterion.field} in ${criterion::class.simpleName}" + "Match and Range have only one criterion per type, but found duplicates for: ${criterion.field} in ${criterion::class.simpleName}", ) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt index b7152b39..8295d18b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryCapabilities.kt @@ -1,81 +1,25 @@ package org.koitharu.kotatsu.parsers.model.search +import androidx.collection.ArraySet import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* -import org.koitharu.kotatsu.parsers.model.search.SearchableField.* +import org.koitharu.kotatsu.parsers.util.mapToSet -public data class MangaSearchQueryCapabilities( - val capabilities: Set = emptySet(), +@ExposedCopyVisibility +public data class MangaSearchQueryCapabilities internal constructor( + val capabilities: Set, ) { - public companion object { - @InternalParsersApi - public fun from(filterCapabilities: MangaListFilterCapabilities): MangaSearchQueryCapabilities { - return MangaSearchQueryCapabilities( - capabilities = setOfNotNull( - filterCapabilities.isMultipleTagsSupported.takeIf { it }?.let { - SearchCapability( - field = TAG, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true - ) - }, - filterCapabilities.isTagsExclusionSupported.takeIf { it }?.let { - SearchCapability( - field = TAG, criteriaTypes = setOf(Exclude::class), multiValue = true, otherCriteria = true - ) - }, - filterCapabilities.isSearchSupported.takeIf { it }?.let { - SearchCapability( - field = TITLE_NAME, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = false - ) - }, - filterCapabilities.isSearchWithFiltersSupported.takeIf { it }?.let { - SearchCapability( - field = TITLE_NAME, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = true - ) - }, - filterCapabilities.isYearSupported.takeIf { it }?.let { - SearchCapability( - field = PUBLICATION_YEAR, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = true - ) - }, - filterCapabilities.isYearRangeSupported.takeIf { it }?.let { - SearchCapability( - field = PUBLICATION_YEAR, criteriaTypes = setOf(Range::class), multiValue = false, otherCriteria = true - ) - }, - filterCapabilities.isOriginalLocaleSupported.takeIf { it }?.let { - SearchCapability( - field = ORIGINAL_LANGUAGE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true - ) - }, - SearchCapability( - field = LANGUAGE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true - ), - SearchCapability( - field = STATE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true - ), - SearchCapability( - field = CONTENT_TYPE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true - ), - SearchCapability( - field = CONTENT_RATING, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true - ), - SearchCapability( - field = DEMOGRAPHIC, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true - ), - ), - ) - } - } + + public constructor(vararg capabilities: SearchCapability) : this(ArraySet(capabilities)) @InternalParsersApi public fun validate(query: MangaSearchQuery) { - val strictFields = capabilities.filter { !it.otherCriteria }.map { it.field }.toSet() - val usedStrictFields = query.criteria.map { it.field }.toSet().intersect(strictFields) + val strictFields = capabilities.filter { !it.otherCriteria }.mapToSet { it.field } + val usedStrictFields = query.criteria.mapToSet { it.field }.intersect(strictFields) if (usedStrictFields.isNotEmpty() && query.criteria.size > 1) { throw IllegalArgumentException( - "Query contains multiple criteria, but at least one field (${usedStrictFields.joinToString()}) does not support multiple criteria." + "Query contains multiple criteria, but at least one field (${usedStrictFields.joinToString()}) does not support multiple criteria.", ) } @@ -85,7 +29,7 @@ public data class MangaSearchQueryCapabilities( if (criterion::class !in capability.criteriaTypes) { throw IllegalArgumentException( - "Unsupported search criterion: ${criterion::class.simpleName} for field ${criterion.field}" + "Unsupported search criterion: ${criterion::class.simpleName} for field ${criterion.field}", ) } @@ -94,8 +38,10 @@ public data class MangaSearchQueryCapabilities( when (criterion) { is Include<*> -> if (criterion.values.size > 1) throw IllegalArgumentException("Multiple values are not allowed for field ${criterion.field}") + is Exclude<*> -> if (criterion.values.size > 1) throw IllegalArgumentException("Multiple values are not allowed for field ${criterion.field}") + is Range<*> -> {} // Range is always valid (from, to) is Match<*> -> {} // Match always has a single value } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt index 8294844d..ffaf5759 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/QueryCriteria.kt @@ -7,57 +7,53 @@ package org.koitharu.kotatsu.parsers.model.search * @param T The type of value associated with the search criterion. * @property field The field to which this search criterion applies. */ -public sealed class QueryCriteria( - @JvmField public val field: SearchableField -) { - public class Include(field: SearchableField, @JvmField public val values: Set) : QueryCriteria(field) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Include<*>) return false - return field == other.field && values == other.values - } +public sealed interface QueryCriteria { + + public val field: SearchableField + + override fun equals(other: Any?): Boolean + + override fun hashCode(): Int - override fun hashCode(): Int { - return 31 * field.hashCode() + values.hashCode() + public data class Include( + public override val field: SearchableField, + @JvmField public val values: Set, + ) : QueryCriteria { + + init { + check(values.all { x -> field.type.isInstance(x) }) } } - public class Exclude(field: SearchableField, @JvmField public val values: Set) : QueryCriteria(field) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Exclude<*>) return false - return field == other.field && values == other.values - } + public data class Exclude( + public override val field: SearchableField, + @JvmField public val values: Set, + ) : QueryCriteria { - override fun hashCode(): Int { - return 31 * field.hashCode() + values.hashCode() + init { + check(values.all { x -> field.type.isInstance(x) }) } } - public class Range>(field: SearchableField, @JvmField public val from: T, @JvmField public val to: T) : QueryCriteria(field) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Range<*>) return false - return field == other.field && from == other.from && to == other.to - } + public data class Range>( + public override val field: SearchableField, + @JvmField public val from: T, + @JvmField public val to: T, + ) : QueryCriteria { - override fun hashCode(): Int { - var result = field.hashCode() - result = 31 * result + from.hashCode() - result = 31 * result + to.hashCode() - return result + init { + check(field.type.isInstance(from)) + check(field.type.isInstance(to)) } } - public class Match(field: SearchableField, @JvmField public val value: T) : QueryCriteria(field) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Match<*>) return false - return field == other.field && value == other.value - } + public data class Match( + public override val field: SearchableField, + @JvmField public val value: T, + ) : QueryCriteria { - override fun hashCode(): Int { - return 31 * field.hashCode() + value.hashCode() + init { + check(field.type.isInstance(value)) } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt index e906713a..d14e5c16 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/search/SearchableField.kt @@ -1,7 +1,7 @@ package org.koitharu.kotatsu.parsers.model.search import org.koitharu.kotatsu.parsers.model.* -import java.util.Locale +import java.util.* /** * Represents the various fields that can be used for searching manga. diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt index 6d5b4193..24dd603e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/HitomiLaParser.kt @@ -516,14 +516,14 @@ internal class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context title = doc.selectFirstOrThrow("h1").text(), url = id.toString(), coverUrl = - "https:" + - doc.selectFirstOrThrow("picture > source") - .attr("data-srcset") - .substringBefore(" "), + "https:" + + doc.selectFirstOrThrow("picture > source") + .attr("data-srcset") + .substringBefore(" "), publicUrl = - doc.selectFirstOrThrow("h1 > a") - .attrAsRelativeUrl("href") - .toAbsoluteUrl(domain), + doc.selectFirstOrThrow("h1 > a") + .attrAsRelativeUrl("href") + .toAbsoluteUrl(domain), author = null, tags = emptySet(), isNsfw = true, @@ -546,37 +546,37 @@ internal class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context return manga.copy( title = json.getString("title"), largeCoverUrl = - json.getJSONArray("files").getJSONObject(0).let { - val hash = it.getString("hash") - val commonId = commonImageId() - val imageId = imageIdFromHash(hash) - val subDomain = 'a' + subdomainOffset(imageId) - - "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp" - }, + json.getJSONArray("files").getJSONObject(0).let { + val hash = it.getString("hash") + val commonId = commonImageId() + val imageId = imageIdFromHash(hash) + val subDomain = 'a' + subdomainOffset(imageId) + + "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp" + }, author = - json.optJSONArray("artists") - ?.mapJSON { it.getString("artist").toCamelCase() } - ?.joinToString(), + json.optJSONArray("artists") + ?.mapJSON { it.getString("artist").toCamelCase() } + ?.joinToString(), publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain), tags = - buildSet { - json.optJSONArray("characters") - ?.mapToTags("character") - ?.let(::addAll) - json.optJSONArray("tags") - ?.mapToTags("tag") - ?.let(::addAll) - json.optJSONArray("artists") - ?.mapToTags("artist") - ?.let(::addAll) - json.optJSONArray("parodys") - ?.mapToTags("parody") - ?.let(::addAll) - json.optJSONArray("groups") - ?.mapToTags("group") - ?.let(::addAll) - }, + buildSet { + json.optJSONArray("characters") + ?.mapToTags("character") + ?.let(::addAll) + json.optJSONArray("tags") + ?.mapToTags("tag") + ?.let(::addAll) + json.optJSONArray("artists") + ?.mapToTags("artist") + ?.let(::addAll) + json.optJSONArray("parodys") + ?.mapToTags("parody") + ?.let(::addAll) + json.optJSONArray("groups") + ?.mapToTags("group") + ?.let(::addAll) + }, chapters = listOf( MangaChapter( id = generateUid(manga.url), @@ -600,15 +600,15 @@ internal class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context mapJSON { MangaTag( title = - it.getString(key).toCamelCase().let { title -> - if (it.getStringOrNull("female")?.toIntOrNull() == 1) { - "$title ♀" - } else if (it.getStringOrNull("male")?.toIntOrNull() == 1) { - "$title ♂" - } else { - title - } - }, + it.getString(key).toCamelCase().let { title -> + if (it.getStringOrNull("female")?.toIntOrNull() == 1) { + "$title ♀" + } else if (it.getStringOrNull("male")?.toIntOrNull() == 1) { + "$title ♂" + } else { + title + } + }, key = it.getString("url").tagUrlToTag(), source = source, ).let(tags::add) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt index 99b5de22..1305e384 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt @@ -11,11 +11,13 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.search.* +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.model.search.SearchCapability +import org.koitharu.kotatsu.parsers.model.search.SearchableField import org.koitharu.kotatsu.parsers.model.search.SearchableField.* import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.json.* @@ -79,17 +81,65 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context override val searchQueryCapabilities: MangaSearchQueryCapabilities get() = MangaSearchQueryCapabilities( - capabilities = setOf( - SearchCapability(field = TAG, criteriaTypes = setOf(Include::class, Exclude::class), multiValue = true, otherCriteria = true), - SearchCapability(field = TITLE_NAME, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = true), - SearchCapability(field = STATE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = AUTHOR, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = CONTENT_TYPE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = CONTENT_RATING, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = DEMOGRAPHIC, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = ORIGINAL_LANGUAGE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = LANGUAGE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = PUBLICATION_YEAR, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = true), + SearchCapability( + field = TAG, + criteriaTypes = setOf(Include::class, Exclude::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = true, + ), + SearchCapability( + field = STATE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = AUTHOR, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = CONTENT_TYPE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = CONTENT_RATING, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = DEMOGRAPHIC, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = ORIGINAL_LANGUAGE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = LANGUAGE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = PUBLICATION_YEAR, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = true, ), ) @@ -127,7 +177,6 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context ORIGINAL_LANGUAGE -> "originalLanguage[]" LANGUAGE -> "availableTranslatedLanguage[]" PUBLICATION_YEAR -> "year" - else -> "" } private fun Any?.toQueryParam(): String = when (this) { @@ -141,13 +190,14 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context MangaState.PAUSED -> "hiatus" else -> "" } + is ContentRating -> when (this) { ContentRating.SAFE -> "safe" // quick fix for double value ContentRating.SUGGESTIVE -> "suggestive&contentRating[]=erotica" ContentRating.ADULT -> "pornographic" - else -> "" } + is Demographic -> when (this) { Demographic.SHOUNEN -> "shounen" Demographic.SHOUJO -> "shoujo" @@ -156,6 +206,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context Demographic.NONE -> "none" else -> "" } + is SortOrder -> when (this) { SortOrder.UPDATED -> "[latestUploadedChapter]=desc" SortOrder.UPDATED_ASC -> "[latestUploadedChapter]=asc" @@ -172,6 +223,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context SortOrder.RELEVANCE -> "&order[relevance]=desc" else -> "[latestUploadedChapter]=desc" } + else -> this.toString().urlEncoded() } @@ -183,14 +235,14 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context } } - override suspend fun validatedMangaSearch(searchQuery: MangaSearchQuery): List { + override suspend fun getList(query: MangaSearchQuery): List { val url = buildString { - append("https://api.$domain/manga?limit=$PAGE_SIZE&offset=${searchQuery.offset ?: 0}") + append("https://api.$domain/manga?limit=$PAGE_SIZE&offset=${query.offset}") .append("&includes[]=cover_art&includes[]=author&includes[]=artist&includedTagsMode=AND&excludedTagsMode=OR") var hasContentRating = false - searchQuery.criteria.forEach { criterion -> + query.criteria.forEach { criterion -> when (criterion) { is Include<*> -> { if (criterion.field == CONTENT_RATING) { @@ -198,12 +250,15 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context } criterion.values.forEach { appendCriterion(criterion.field, it) } } + is Exclude<*> -> { criterion.values.forEach { appendCriterion(criterion.field, it, "excludedTags[]") } } + is Match<*> -> { appendCriterion(criterion.field, criterion.value) } + else -> { // Not supported } @@ -216,7 +271,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context } append("&order") - append((searchQuery.order ?: defaultSortOrder).toQueryParam()) + append((query.order ?: defaultSortOrder).toQueryParam()) } val json = webClient.httpGet(url).parseJson().getJSONArray("data") @@ -224,7 +279,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context } override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { - return searchManga(convertToMangaSearchQuery(offset, order, filter)) + return queryManga(convertToMangaSearchQuery(offset, order, filter)) } override suspend fun getDetails(manga: Manga): Manga { @@ -304,15 +359,14 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context "https://uploads.$domain/covers/$id/$it" } val authors: Set = (relations["author"] ?: relations["artist"]) - ?.mapNotNull { + ?.mapNotNullToSet { val key = it.getStringOrNull("id") val title = it.getJSONObject("attributes")?.getStringOrNull("name") key?.let { k -> title?.let { t -> MangaTag(key = k, title = t, source = source) } } - } - ?.toSet() ?: emptySet() + }.orEmpty() return Manga( id = generateUid(id), @@ -452,7 +506,8 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context val locale = attrs.getStringOrNull("translatedLanguage")?.let { Locale.forLanguageTag(it) } val lc = locale?.getDisplayName(locale)?.toTitleCase(locale) val relations = jo.getJSONArray("relationships").associateByKey("type") - val team = relations["scanlation_group"]?.firstOrNull()?.optJSONObject("attributes")?.getStringOrNull("name") + val team = + relations["scanlation_group"]?.firstOrNull()?.optJSONObject("attributes")?.getStringOrNull("name") val branch = (list.indices).firstNotNullOf { i -> val b = if (i == 0) lc else "$lc ($i)" if (branchedChapters[b]?.get(volume to number) == null) b else null diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt index 26a8dcb3..d354fa55 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt @@ -64,27 +64,27 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M MangaTag( title = it.selectFirstOrThrow(".label-text").text(), key = it.selectFirstOrThrow("input[id$=value]").attr("value"), - source = source + source = source, ) } val states = EnumSet.of( - ONGOING, FINISHED, ABANDONED, PAUSED + ONGOING, FINISHED, ABANDONED, PAUSED, ) val types = EnumSet.of( - MANGA, MANHWA, MANHUA, COMICS + MANGA, MANHWA, MANHUA, COMICS, ) val rating = EnumSet.of( - SAFE, SUGGESTIVE + SAFE, SUGGESTIVE, ) return MangaListFilterOptions( availableTags = tags, availableStates = states, availableContentTypes = types, - availableContentRating = rating + availableContentRating = rating, ) } @@ -109,7 +109,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M ADDED, ADDED_ASC -> "Recently Added" UPDATED, UPDATED_ASC -> "Latest Updates" else -> throw UnsupportedOperationException("unsupported order: $order") - } + }, ) addQueryParameter( name = "order", @@ -117,11 +117,11 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M RELEVANCE, ALPHABETICAL, POPULARITY_ASC, RATING_ASC, ADDED_ASC, UPDATED_ASC -> "Ascending" ALPHABETICAL_DESC, POPULARITY, RATING, ADDED, UPDATED -> "Descending" else -> throw UnsupportedOperationException("unsupported order: $order") - } + }, ) addQueryParameter("official", "Any") addQueryParameter("anime", "Any") - with (filter.contentRating) { + with(filter.contentRating) { addQueryParameter( name = "adult", value = when { @@ -130,7 +130,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M SAFE in this -> "False" SUGGESTIVE in this -> "True" else -> throw UnsupportedOperationException("unsupported content rating: $this") - } + }, ) } filter.states.forEach { state -> @@ -142,7 +142,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M ABANDONED -> "Canceled" PAUSED -> "Hiatus" else -> throw UnsupportedOperationException("unsupported state: $state") - } + }, ) } filter.types.forEach { type -> @@ -154,7 +154,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M MANHUA -> "Manhua" COMICS -> "OEL" else -> throw UnsupportedOperationException("unsupported type: $type") - } + }, ) } filter.tags.forEach { tag -> @@ -193,11 +193,11 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M MangaTag( title = it, key = it, - source = source + source = source, ) } .orEmpty(), - state = when(document.selectFirst("div:contains(status) span")?.text()) { + state = when (document.selectFirst("div:contains(status) span")?.text()) { "Ongoing" -> ONGOING "Complete" -> FINISHED "Canceled" -> ABANDONED @@ -207,7 +207,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M author = document.select("div:contains(author) a").eachText().joinToString(), largeCoverUrl = null, chapters = null, - source = source + source = source, ) } } @@ -237,7 +237,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M MangaTag( title = it.text(), key = it.text(), - source = source + source = source, ) }, state = when (sectionLeft.selectFirst("ul > li:has(strong:contains(Status)) > a")?.text()) { @@ -259,12 +259,12 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M abbr.selectFirst("a")?.attr("href")?.let { url -> val a = Element("a") .text( - abbr.attr("title") + abbr.attr("title"), ) .attr("href", url) ul.appendChild( - Element("li").appendChild(a) + Element("li").appendChild(a), ) } } @@ -276,7 +276,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M }.outerHtml(), chapters = chapters.await(), - source = source + source = source, ) } @@ -308,10 +308,10 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M else -> null }, uploadDate = dateFormat.tryParse( - element.selectFirst("time[datetime]")?.attr("datetime") + element.selectFirst("time[datetime]")?.attr("datetime"), ), branch = null, - source = source + source = source, ) } } @@ -336,7 +336,7 @@ internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, M id = generateUid(pageUrl), url = pageUrl, preview = null, - source = source + source = source, ) } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt index e74d5e60..d5ad8753 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/MangaboxParser.kt @@ -46,11 +46,29 @@ internal abstract class MangaboxParser( override val searchQueryCapabilities: MangaSearchQueryCapabilities get() = MangaSearchQueryCapabilities( - capabilities = setOf( - SearchCapability(field = TAG, criteriaTypes = setOf(Include::class, Exclude::class), multiValue = true, otherCriteria = true), - SearchCapability(field = TITLE_NAME, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = true), - SearchCapability(field = STATE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true), - SearchCapability(field = AUTHOR, criteriaTypes = setOf(Include::class), multiValue = false, otherCriteria = false), + SearchCapability( + field = TAG, + criteriaTypes = setOf(Include::class, Exclude::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = true, + ), + SearchCapability( + field = STATE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = AUTHOR, + criteriaTypes = setOf(Include::class), + multiValue = false, + otherCriteria = false, ), ) @@ -94,12 +112,14 @@ internal abstract class MangaboxParser( MangaState.FINISHED -> "completed" else -> "" } + is SortOrder -> when (this) { SortOrder.ALPHABETICAL -> "az" SortOrder.NEWEST -> "newest" SortOrder.POPULARITY -> "topview" else -> "" } + else -> this.toString().replace(" ", "_").urlEncoded() } @@ -115,31 +135,35 @@ internal abstract class MangaboxParser( Regex("[^/]+\$", RegexOption.IGNORE_CASE) } - override suspend fun searchPageManga(searchQuery: MangaSearchQuery): List { + override suspend fun getListPage(query: MangaSearchQuery, page: Int): List { var authorSearchUrl: String? = null val url = buildString { - val pageQueryParameter = "page=${searchQuery.offset ?: 0}" + val pageQueryParameter = "page=$page" append("https://${domain}${listUrl}/?s=all") - searchQuery.criteria.forEach { criterion -> + query.criteria.forEach { criterion -> when (criterion) { is Include<*> -> { if (criterion.field == AUTHOR) { - criterion.values.firstOrNull()?.toQueryParam()?.takeIf { it.isNotBlank() }?.let { authorKey -> - authorSearchUrl = "https://${domain}${authorUrl}/${authorKey}/?$pageQueryParameter" - } + criterion.values.firstOrNull()?.toQueryParam()?.takeIf { it.isNotBlank() } + ?.let { authorKey -> + authorSearchUrl = "https://${domain}${authorUrl}/${authorKey}/?$pageQueryParameter" + } } criterion.field.toParamName().takeIf { it.isNotBlank() }?.let { param -> append("&$param=${criterion.values.joinToString("_") { it.toQueryParam() }}") } } + is Exclude<*> -> { append("&g_e=${criterion.values.joinToString("_") { it.toQueryParam() }}") } + is Match<*> -> { appendCriterion(criterion.field, criterion.value) } + else -> { // Not supported } @@ -147,7 +171,7 @@ internal abstract class MangaboxParser( } append("&${pageQueryParameter}") - append("&orby=${(searchQuery.order ?: defaultSortOrder).toQueryParam()}") + append("&orby=${(query.order ?: defaultSortOrder).toQueryParam()}") } val doc = webClient.httpGet(authorSearchUrl ?: url).parseHtml() @@ -174,7 +198,7 @@ internal abstract class MangaboxParser( } override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - return searchManga(convertToMangaSearchQuery(page, order, filter)) + return queryManga(convertToMangaSearchQuery(page, order, filter)) } protected open val selectTagMap = "div.panel-genres-list a:not(.genres-select)" @@ -215,7 +239,7 @@ internal abstract class MangaboxParser( val alt = doc.body().select(selectAlt).text().replace("Alternative : ", "").nullIfEmpty() val authors = doc.body().select(selectAut).mapToSet { MangaTag( - key = it.attribute("href").value.find(authorKeyRegex)?: it.text(), + key = it.attribute("href").value.find(authorKeyRegex) ?: it.text(), title = it.text(), source = source, ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt index 895ef473..7fb73556 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangairo.kt @@ -6,9 +6,11 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.search.* +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.Include import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.Match +import org.koitharu.kotatsu.parsers.model.search.SearchCapability import org.koitharu.kotatsu.parsers.model.search.SearchableField.* import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser import org.koitharu.kotatsu.parsers.util.* @@ -44,10 +46,23 @@ internal class Mangairo(context: MangaLoaderContext) : override val searchQueryCapabilities: MangaSearchQueryCapabilities get() = MangaSearchQueryCapabilities( - capabilities = setOf( - SearchCapability(field = TAG, criteriaTypes = setOf(Include::class), multiValue = false, otherCriteria = true), - SearchCapability(field = TITLE_NAME, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = false), - SearchCapability(field = STATE, criteriaTypes = setOf(Include::class), multiValue = false, otherCriteria = true), + SearchCapability( + field = TAG, + criteriaTypes = setOf(Include::class), + multiValue = false, + otherCriteria = true, + ), + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = false, + ), + SearchCapability( + field = STATE, + criteriaTypes = setOf(Include::class), + multiValue = false, + otherCriteria = true, ), ) @@ -59,25 +74,27 @@ internal class Mangairo(context: MangaLoaderContext) : MangaState.FINISHED -> "completed" else -> "all" } + is SortOrder -> when (this) { SortOrder.POPULARITY -> "topview" SortOrder.UPDATED -> "latest" SortOrder.NEWEST -> "newest" else -> "latest" } + else -> this.toString().urlEncoded() } - override suspend fun searchPageManga(searchQuery: MangaSearchQuery): List { + override suspend fun getListPage(query: MangaSearchQuery, page: Int): List { var titleSearchUrl: String? = null var category = "all" var state = "all" val url = buildString { append("https://${domain}${listUrl}") - append("/type-${(searchQuery.order ?: defaultSortOrder).toQueryParam()}") + append("/type-${(query.order ?: defaultSortOrder).toQueryParam()}") - searchQuery.criteria.forEach { criterion -> + query.criteria.forEach { criterion -> when (criterion) { is Include<*> -> { when (criterion.field) { @@ -86,14 +103,16 @@ internal class Mangairo(context: MangaLoaderContext) : else -> Unit } } + is Match<*> -> { if (criterion.field == TITLE_NAME) { criterion.value.toQueryParam().takeIf { it.isNotBlank() }?.let { titleName -> titleSearchUrl = "https://${domain}${searchUrl}${titleName}/" + - "?page=${searchQuery.offset ?: 0}" + "?page=${query.offset}" } } } + else -> { // Not supported } @@ -101,7 +120,7 @@ internal class Mangairo(context: MangaLoaderContext) : } append("/ctg-$category") append("/state-$state") - append("/page-${searchQuery.offset ?: 0}") + append("/page-$page") } val doc = webClient.httpGet(titleSearchUrl ?: url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt index 4ed13a69..81300ba2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt @@ -31,10 +31,23 @@ internal class Mangakakalot(context: MangaLoaderContext) : override val searchQueryCapabilities: MangaSearchQueryCapabilities get() = MangaSearchQueryCapabilities( - capabilities = setOf( - SearchCapability(field = TAG, criteriaTypes = setOf(Include::class), multiValue = false, otherCriteria = true), - SearchCapability(field = TITLE_NAME, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = false), - SearchCapability(field = STATE, criteriaTypes = setOf(Include::class), multiValue = false, otherCriteria = true), + SearchCapability( + field = TAG, + criteriaTypes = setOf(Include::class), + multiValue = false, + otherCriteria = true, + ), + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = false, + ), + SearchCapability( + field = STATE, + criteriaTypes = setOf(Include::class), + multiValue = false, + otherCriteria = true, ), ) @@ -53,18 +66,21 @@ internal class Mangakakalot(context: MangaLoaderContext) : .replace(" ", "_") .urlEncoded() } + is MangaTag -> key is MangaState -> when (this) { MangaState.ONGOING -> "ongoing" MangaState.FINISHED -> "completed" else -> "all" } + is SortOrder -> when (this) { SortOrder.POPULARITY -> "topview" SortOrder.UPDATED -> "latest" SortOrder.NEWEST -> "newest" else -> "" } + else -> this.toString().replace(" ", "_").urlEncoded() } @@ -80,19 +96,20 @@ internal class Mangakakalot(context: MangaLoaderContext) : Regex("[^A-Za-z0-9 ]") } - override suspend fun searchPageManga(searchQuery: MangaSearchQuery): List { + override suspend fun getListPage(query: MangaSearchQuery, page: Int): List { var titleSearchUrl: String? = null val url = buildString { - val pageQueryParameter = "page=${searchQuery.offset ?: 0}" + val pageQueryParameter = "page=$page" append("https://$domain/?") - searchQuery.criteria.forEach { criterion -> + query.criteria.forEach { criterion -> when (criterion) { is Include<*> -> { criterion.field.toParamName().takeIf { it.isNotBlank() }?.let { param -> append("&$param=${criterion.values.first().toQueryParam()}") } } + is Match<*> -> { if (criterion.field == TITLE_NAME) { criterion.value.toQueryParam().takeIf { it.isNotBlank() }?.let { titleName -> @@ -102,6 +119,7 @@ internal class Mangakakalot(context: MangaLoaderContext) : } appendCriterion(criterion.field, criterion.value) } + else -> { // Not supported } @@ -109,7 +127,7 @@ internal class Mangakakalot(context: MangaLoaderContext) : } append("&$pageQueryParameter") - append("&type=${(searchQuery.order ?: defaultSortOrder).toQueryParam()}") + append("&type=${(query.order ?: defaultSortOrder).toQueryParam()}") } val doc = webClient.httpGet(titleSearchUrl ?: url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt index 418f1e29..7cfda584 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/MangakakalotTv.kt @@ -35,10 +35,23 @@ internal class MangakakalotTv(context: MangaLoaderContext) : override val searchQueryCapabilities: MangaSearchQueryCapabilities get() = MangaSearchQueryCapabilities( - capabilities = setOf( - SearchCapability(field = TAG, criteriaTypes = setOf(Include::class), multiValue = false, otherCriteria = true), - SearchCapability(field = TITLE_NAME, criteriaTypes = setOf(Match::class), multiValue = false, otherCriteria = false), - SearchCapability(field = STATE, criteriaTypes = setOf(Include::class), multiValue = false, otherCriteria = true), + SearchCapability( + field = TAG, + criteriaTypes = setOf(Include::class), + multiValue = false, + otherCriteria = true, + ), + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = false, + ), + SearchCapability( + field = STATE, + criteriaTypes = setOf(Include::class), + multiValue = false, + otherCriteria = true, ), ) @@ -56,12 +69,14 @@ internal class MangakakalotTv(context: MangaLoaderContext) : MangaState.FINISHED -> "completed" else -> "all" } + is SortOrder -> when (this) { SortOrder.POPULARITY -> "topview" SortOrder.UPDATED -> "latest" SortOrder.NEWEST -> "newest" else -> "" } + else -> this.toString().urlEncoded() } @@ -73,19 +88,20 @@ internal class MangakakalotTv(context: MangaLoaderContext) : } } - override suspend fun searchPageManga(searchQuery: MangaSearchQuery): List { + override suspend fun getListPage(query: MangaSearchQuery, page: Int): List { var titleSearchUrl: String? = null val url = buildString { - val pageQueryParameter = "page=${searchQuery.offset ?: 0}" + val pageQueryParameter = "page=$page" append("https://$domain/?") - searchQuery.criteria.forEach { criterion -> + query.criteria.forEach { criterion -> when (criterion) { is Include<*> -> { criterion.field.toParamName().takeIf { it.isNotBlank() }?.let { param -> append("&$param=${criterion.values.first().toQueryParam()}") } } + is Match<*> -> { if (criterion.field == TITLE_NAME) { criterion.value.toQueryParam().takeIf { it.isNotBlank() }?.let { titleName -> @@ -95,6 +111,7 @@ internal class MangakakalotTv(context: MangaLoaderContext) : } appendCriterion(criterion.field, criterion.value) } + else -> { // Not supported } @@ -102,7 +119,7 @@ internal class MangakakalotTv(context: MangaLoaderContext) : } append("&$pageQueryParameter") - append("&type=${(searchQuery.order ?: defaultSortOrder).toQueryParam()}") + append("&type=${(query.order ?: defaultSortOrder).toQueryParam()}") } val doc = webClient.httpGet(titleSearchUrl ?: url).parseHtml() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt index 74680b6a..5fe41fc6 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryConverter.kt @@ -1,9 +1,15 @@ package org.koitharu.kotatsu.parsers.util import org.koitharu.kotatsu.parsers.InternalParsersApi -import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities import org.koitharu.kotatsu.parsers.model.search.QueryCriteria +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.model.search.SearchCapability import org.koitharu.kotatsu.parsers.model.search.SearchableField.* /** @@ -20,26 +26,26 @@ public fun convertToMangaSearchQuery(offset: Int, sortOrder: SortOrder, filter: return MangaSearchQuery.Builder().apply { offset(offset) order(sortOrder) - if (filter.tags.isNotEmpty()) criterion(QueryCriteria.Include(TAG, filter.tags)) - if (filter.tagsExclude.isNotEmpty()) criterion(QueryCriteria.Exclude(TAG, filter.tagsExclude)) - if (filter.states.isNotEmpty()) criterion(QueryCriteria.Include(STATE, filter.states)) - if (filter.types.isNotEmpty()) criterion(QueryCriteria.Include(CONTENT_TYPE, filter.types)) - if (filter.contentRating.isNotEmpty()) criterion(QueryCriteria.Include(CONTENT_RATING, filter.contentRating)) - if (filter.demographics.isNotEmpty()) criterion(QueryCriteria.Include(DEMOGRAPHIC, filter.demographics)) + if (filter.tags.isNotEmpty()) criterion(Include(TAG, filter.tags)) + if (filter.tagsExclude.isNotEmpty()) criterion(Exclude(TAG, filter.tagsExclude)) + if (filter.states.isNotEmpty()) criterion(Include(STATE, filter.states)) + if (filter.types.isNotEmpty()) criterion(Include(CONTENT_TYPE, filter.types)) + if (filter.contentRating.isNotEmpty()) criterion(Include(CONTENT_RATING, filter.contentRating)) + if (filter.demographics.isNotEmpty()) criterion(Include(DEMOGRAPHIC, filter.demographics)) if (validateYear(filter.yearFrom) || validateYear(filter.yearTo)) { criterion(QueryCriteria.Range(PUBLICATION_YEAR, filter.yearFrom, filter.yearTo)) } if (validateYear(filter.year)) { - criterion(QueryCriteria.Match(PUBLICATION_YEAR, filter.year)) + criterion(Match(PUBLICATION_YEAR, filter.year)) } - filter.locale?.takeIf { it != null }?.let { - criterion(QueryCriteria.Include(LANGUAGE, setOf(it))) + filter.locale?.let { + criterion(Include(LANGUAGE, setOf(it))) } - filter.originalLocale?.takeIf { it != null }?.let { - criterion(QueryCriteria.Include(ORIGINAL_LANGUAGE, setOf(it))) + filter.originalLocale?.let { + criterion(Include(ORIGINAL_LANGUAGE, setOf(it))) } filter.query?.takeIf { it.isNotBlank() }?.let { - criterion(QueryCriteria.Match(TITLE_NAME, it)) + criterion(Match(TITLE_NAME, it)) } }.build() } @@ -74,16 +80,99 @@ public fun convertToMangaListFilter(searchQuery: MangaSearchQuery): MangaListFil return MangaListFilter.Builder().apply { for (criterion in searchQuery.criteria) { when (criterion) { - is QueryCriteria.Include<*> -> handleInclude(this, criterion) - is QueryCriteria.Exclude<*> -> handleExclude(this, criterion) - is QueryCriteria.Range<*> -> handleBetween(this, criterion) - is QueryCriteria.Match<*> -> handleMatch(this, criterion) + is Include<*> -> handleInclude(this, criterion) + is Exclude<*> -> handleExclude(this, criterion) + is Range<*> -> handleBetween(this, criterion) + is Match<*> -> handleMatch(this, criterion) } } }.build() } -private fun handleInclude(builder: MangaListFilter.Builder, criterion: QueryCriteria.Include<*>) { +internal fun MangaListFilterCapabilities.toMangaSearchQueryCapabilities(): MangaSearchQueryCapabilities = + MangaSearchQueryCapabilities( + capabilities = setOfNotNull( + isMultipleTagsSupported.takeIf { it }?.let { + SearchCapability( + field = TAG, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true, + ) + }, + isTagsExclusionSupported.takeIf { it }?.let { + SearchCapability( + field = TAG, criteriaTypes = setOf(Exclude::class), multiValue = true, otherCriteria = true, + ) + }, + isSearchSupported.takeIf { it }?.let { + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = false, + ) + }, + isSearchWithFiltersSupported.takeIf { it }?.let { + SearchCapability( + field = TITLE_NAME, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = true, + ) + }, + isYearSupported.takeIf { it }?.let { + SearchCapability( + field = PUBLICATION_YEAR, + criteriaTypes = setOf(Match::class), + multiValue = false, + otherCriteria = true, + ) + }, + isYearRangeSupported.takeIf { it }?.let { + SearchCapability( + field = PUBLICATION_YEAR, + criteriaTypes = setOf(Range::class), + multiValue = false, + otherCriteria = true, + ) + }, + isOriginalLocaleSupported.takeIf { it }?.let { + SearchCapability( + field = ORIGINAL_LANGUAGE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ) + }, + SearchCapability( + field = LANGUAGE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = STATE, criteriaTypes = setOf(Include::class), multiValue = true, otherCriteria = true, + ), + SearchCapability( + field = CONTENT_TYPE, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = CONTENT_RATING, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + SearchCapability( + field = DEMOGRAPHIC, + criteriaTypes = setOf(Include::class), + multiValue = true, + otherCriteria = true, + ), + ), + ) + +private fun handleInclude(builder: MangaListFilter.Builder, criterion: Include<*>) { val type = criterion.field.type when (criterion.field) { @@ -94,20 +183,20 @@ private fun handleInclude(builder: MangaListFilter.Builder, criterion: QueryCrit DEMOGRAPHIC -> builder.addDemographics(filterValues(criterion, type)) LANGUAGE -> builder.locale(getFirstValue(criterion, type)) ORIGINAL_LANGUAGE -> builder.originalLocale(getFirstValue(criterion, type)) - else -> throw UnsupportedOperationException("Unsupported field for Include criterion: ${criterion.field}") + else -> throw IllegalArgumentException("Unsupported field for Include criterion: ${criterion.field}") } } -private fun handleExclude(builder: MangaListFilter.Builder, criterion: QueryCriteria.Exclude<*>) { +private fun handleExclude(builder: MangaListFilter.Builder, criterion: Exclude<*>) { val type = criterion.field.type when (criterion.field) { TAG -> builder.excludeTags(filterValues(criterion, type)) - else -> throw UnsupportedOperationException("Unsupported field for Exclude criterion: ${criterion.field}") + else -> throw IllegalArgumentException("Unsupported field for Exclude criterion: ${criterion.field}") } } -private fun handleBetween(builder: MangaListFilter.Builder, criterion: QueryCriteria.Range<*>) { +private fun handleBetween(builder: MangaListFilter.Builder, criterion: Range<*>) { val type = criterion.field.type when (criterion.field) { @@ -115,32 +204,33 @@ private fun handleBetween(builder: MangaListFilter.Builder, criterion: QueryCrit builder.yearFrom(getValue(criterion.from, type, YEAR_UNKNOWN)) builder.yearTo(getValue(criterion.to, type, YEAR_UNKNOWN)) } - else -> throw UnsupportedOperationException("Unsupported field for Between criterion: ${criterion.field}") + + else -> throw IllegalArgumentException("Unsupported field for Between criterion: ${criterion.field}") } } -private fun handleMatch(builder: MangaListFilter.Builder, criterion: QueryCriteria.Match<*>) { +private fun handleMatch(builder: MangaListFilter.Builder, criterion: Match<*>) { val type = criterion.field.type when (criterion.field) { TITLE_NAME -> builder.query(getValue(criterion.value, type, "")) PUBLICATION_YEAR -> builder.year(getValue(criterion.value, type, YEAR_UNKNOWN)) - else -> throw UnsupportedOperationException("Unsupported field for Match criterion: ${criterion.field}") + else -> throw IllegalArgumentException("Unsupported field for Match criterion: ${criterion.field}") } } @Suppress("UNCHECKED_CAST") -private fun filterValues(criterion: QueryCriteria.Include<*>, type: Class<*>): List { +private fun filterValues(criterion: Include<*>, type: Class<*>): List { return criterion.values.filter { type.isInstance(it) } as List } @Suppress("UNCHECKED_CAST") -private fun filterValues(criterion: QueryCriteria.Exclude<*>, type: Class<*>): List { +private fun filterValues(criterion: Exclude<*>, type: Class<*>): List { return criterion.values.filter { type.isInstance(it) } as List } @Suppress("UNCHECKED_CAST") -private fun getFirstValue(criterion: QueryCriteria.Include<*>, type: Class<*>): T? { +private fun getFirstValue(criterion: Include<*>, type: Class<*>): T? { return criterion.values.firstOrNull { type.isInstance(it) } as? T } @@ -151,4 +241,4 @@ private fun getValue(value: Any?, type: Class<*>, default: T): T { return if (type.isInstance(value) || isCompatibleIntType) value as T else default } -private fun validateYear(year: Int) = year != null && year != YEAR_UNKNOWN +private fun validateYear(year: Int) = year != YEAR_UNKNOWN diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt index d667cf90..8fad3243 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt @@ -38,8 +38,9 @@ internal class MangaParserTest { if (parser is SinglePageMangaParser) { return@runTest } - val page1 = parser.searchManga(MangaSearchQuery.builder().offset(0).order(parser.defaultSortOrder).build()) - val page2 = parser.searchManga(MangaSearchQuery.builder().offset(page1.size).order(parser.defaultSortOrder).build()) + val page1 = parser.queryManga(MangaSearchQuery.Builder().offset(0).order(parser.defaultSortOrder).build()) + val page2 = + parser.queryManga(MangaSearchQuery.Builder().offset(page1.size).order(parser.defaultSortOrder).build()) if (parser is PagedMangaParser) { assert(parser.pageSize >= page1.size) { "Page size is ${page1.size} but ${parser.pageSize} expected" @@ -58,19 +59,19 @@ internal class MangaParserTest { @MangaSources fun searchByTitleName(source: MangaParserSource) = runTest(timeout = timeout) { val parser = context.newParserInstance(source) - val subject = parser.searchManga( - MangaSearchQuery.builder() + val subject = parser.queryManga( + MangaSearchQuery.Builder() .offset(0) .order(parser.defaultSortOrder) - .build() + .build(), ).minByOrNull { it.title.length } ?: error("No manga found") val query = subject.title check(query.isNotBlank()) { "Manga title '$query' is blank" } - val list = parser.searchManga( - MangaSearchQuery.builder() + val list = parser.queryManga( + MangaSearchQuery.Builder() .order(SortOrder.RELEVANCE) .criterion(QueryCriteria.Match(TITLE_NAME, query)) .build(), @@ -102,8 +103,8 @@ internal class MangaParserTest { assert(tags.all { it.source == source }) val tag = tags.last() - val list = parser.searchManga( - MangaSearchQuery.builder() + val list = parser.queryManga( + MangaSearchQuery.Builder() .offset(0) .order(parser.defaultSortOrder) .criterion(Include(TAG, setOf(tag))) @@ -120,8 +121,8 @@ internal class MangaParserTest { if (!parser.filterCapabilities.isMultipleTagsSupported) return@runTest val tags = parser.getFilterOptions().availableTags.shuffled().take(2).toSet() - val list = parser.searchManga( - MangaSearchQuery.builder() + val list = parser.queryManga( + MangaSearchQuery.Builder() .offset(0) .order(parser.defaultSortOrder) .criterion(Include(TAG, tags)) @@ -141,8 +142,8 @@ internal class MangaParserTest { return@runTest } val locale = locales.random() - val list = parser.searchManga( - MangaSearchQuery.builder() + val list = parser.queryManga( + MangaSearchQuery.Builder() .offset(0) .order(parser.defaultSortOrder) .criterion(Include(LANGUAGE, setOf(locale))) @@ -159,7 +160,7 @@ internal class MangaParserTest { @MangaSources fun details(source: MangaParserSource) = runTest(timeout = timeout) { val parser = context.newParserInstance(source) - val list = parser.searchManga(MangaSearchQuery.builder().offset(0).order(parser.defaultSortOrder).build()) + val list = parser.queryManga(MangaSearchQuery.Builder().offset(0).order(parser.defaultSortOrder).build()) val manga = list[0] parser.getDetails(manga).apply { @@ -190,7 +191,7 @@ internal class MangaParserTest { @MangaSources fun pages(source: MangaParserSource) = runTest(timeout = timeout) { val parser = context.newParserInstance(source) - val list = parser.searchManga(MangaSearchQuery.builder().offset(0).order(parser.defaultSortOrder).build()) + val list = parser.queryManga(MangaSearchQuery.Builder().offset(0).order(parser.defaultSortOrder).build()) val manga = list.first() val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null at ${manga.publicUrl}") val pages = parser.getPages(chapter) @@ -245,7 +246,8 @@ internal class MangaParserTest { @MangaSources fun link(source: MangaParserSource) = runTest(timeout = timeout) { val parser = context.newParserInstance(source) - val manga = parser.searchManga(MangaSearchQuery.builder().offset(0).order(parser.defaultSortOrder).build()).first() + val manga = + parser.queryManga(MangaSearchQuery.Builder().offset(0).order(parser.defaultSortOrder).build()).first() val resolved = context.newLinkResolver(manga.publicUrl).getManga() Assertions.assertNotNull(resolved) resolved ?: return@runTest diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt index 9aaddec6..b7be3832 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt @@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* import org.koitharu.kotatsu.parsers.model.search.SearchableField.* -import java.util.Locale +import java.util.* class MangaSearchQueryCapabilitiesTest { @@ -18,12 +18,12 @@ class MangaSearchQueryCapabilitiesTest { SearchCapability(TAG, setOf(Include::class, Exclude::class), multiValue = true, otherCriteria = true), SearchCapability(PUBLICATION_YEAR, setOf(Range::class), multiValue = false, otherCriteria = true), SearchCapability(STATE, setOf(Include::class), multiValue = false, otherCriteria = true), - ) + ), ) @Test fun validateValidSingleCriterionQuery() { - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Match(TITLE_NAME, "title")) .build() @@ -32,7 +32,7 @@ class MangaSearchQueryCapabilitiesTest { @Test fun validateUnsupportedFieldThrowsException() { - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.ENGLISH))) .build() @@ -41,7 +41,7 @@ class MangaSearchQueryCapabilitiesTest { @Test fun validateUnsupportedMultiValueThrowsException() { - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Include(STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))) .build() @@ -50,7 +50,7 @@ class MangaSearchQueryCapabilitiesTest { @Test fun validateMultipleCriteriaWithOtherCriteriaAllowed() { - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Include(TAG, setOf(buildTag("tag1"), buildTag("tag2")))) .criterion(Exclude(TAG, setOf(buildTag("tag3")))) .build() @@ -60,7 +60,7 @@ class MangaSearchQueryCapabilitiesTest { @Test fun validateMultipleCriteriaWithStrictCapabilityThrowsException() { - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Match(TITLE_NAME, "title")) .criterion(Range(PUBLICATION_YEAR, 1990, 2000)) .build() @@ -68,5 +68,5 @@ class MangaSearchQueryCapabilitiesTest { assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } } - private fun buildTag(name: String) = MangaTag(title = name, key = "${name}Key", source = MangaParserSource.DUMMY) + private fun buildTag(name: String) = MangaTag(title = name, key = "${name}Key", source = MangaParserSource.DUMMY) } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt index d43db48e..04530bec 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt @@ -34,12 +34,12 @@ class ListFilterToSearchQueryConverterTest { demographics = demographics, year = 2020, yearFrom = 1997, - yearTo = 2024 + yearTo = 2024, ) val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) - val expectedQuery = MangaSearchQuery.builder() + val expectedQuery = MangaSearchQuery.Builder() .offset(0) .order(SortOrder.NEWEST) .criterion(Match(TITLE_NAME, "title_name")) @@ -64,14 +64,14 @@ class ListFilterToSearchQueryConverterTest { val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) - assertEquals(MangaSearchQuery.builder().offset(0).order(SortOrder.NEWEST).build(), searchQuery) + assertEquals(MangaSearchQuery.Builder().offset(0).order(SortOrder.NEWEST).build(), searchQuery) } private fun buildMangaTag(name: String): MangaTag { return MangaTag( key = "${name}Key", title = name, - source = MangaParserSource.DUMMY + source = MangaParserSource.DUMMY, ) } } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt index 1d7dfa7a..ef282378 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt @@ -3,10 +3,14 @@ package org.koitharu.kotatsu.parsers.util import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.model.ContentType.* +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.ContentType.MANGA +import org.koitharu.kotatsu.parsers.model.ContentType.MANHUA import org.koitharu.kotatsu.parsers.model.Demographic.SEINEN -import org.koitharu.kotatsu.parsers.model.search.* +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* import org.koitharu.kotatsu.parsers.model.search.SearchableField.* import java.util.* @@ -22,7 +26,7 @@ class ConvertToMangaListFilterTest { val contentTypes = setOf(MANGA, MANHUA) val demographics = setOf(SEINEN) - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Match(TITLE_NAME, "title_name")) .criterion(Include(TAG, tags)) .criterion(Exclude(TAG, excludedTags)) @@ -57,7 +61,7 @@ class ConvertToMangaListFilterTest { val tags1 = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) val tags2 = setOf(buildMangaTag("tag3"), buildMangaTag("tag4")) - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Include(TAG, tags1)) .criterion(Include(TAG, tags2)) .build() @@ -69,11 +73,11 @@ class ConvertToMangaListFilterTest { @Test fun convertToMangaListFilterWithUnsupportedFieldTest() { - val query = MangaSearchQuery.builder() + val query = MangaSearchQuery.Builder() .criterion(Include(AUTHOR, setOf(buildMangaTag("author")))) .build() - val exception = assertThrows { + val exception = assertThrows { convertToMangaListFilter(query) }