diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt index b9966944..a7cc961b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt @@ -169,7 +169,7 @@ abstract class MangaParser @InternalParsersApi constructor( @Deprecated( "Use getList with filter instead", ReplaceWith( - "getList(offset, MangaListFilter.Search(query))", + "getList(offset, SortOrder.RELEVANCE, MangaListFilterV2(query = query))", "org.koitharu.kotatsu.parsers.model.MangaListFilter", ), ) @@ -188,7 +188,7 @@ abstract class MangaParser @InternalParsersApi constructor( @Deprecated( "Use getList with filter instead", ReplaceWith( - "getList(offset, MangaListFilter.Advanced(sortOrder, tags, null, emptySet()))", + "getList(offset, sortOrder, MangaListFilterV2(tags = tags))", "org.koitharu.kotatsu.parsers.model.MangaListFilter", ), ) @@ -218,7 +218,13 @@ abstract class MangaParser @InternalParsersApi constructor( ) } - @Suppress("DEPRECATION") + @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 { return when (filter) { is MangaListFilter.Advanced -> getList( @@ -247,6 +253,31 @@ abstract class MangaParser @InternalParsersApi constructor( } } + 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. * Must return the same manga, may change any fields excepts id, url and source 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 6ff8dda8..f95a903e 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilter.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.model import org.koitharu.kotatsu.parsers.MangaParser import java.util.* +@Deprecated("Use MangaListFilterV2 instead") sealed interface MangaListFilter { fun isEmpty(): Boolean diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterV2.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterV2.kt index 5562a960..f0c9abd8 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterV2.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/MangaListFilterV2.kt @@ -2,121 +2,37 @@ package org.koitharu.kotatsu.parsers.model import java.util.* -@Suppress("DataClassPrivateConstructor") -data class MangaListFilterV2 private constructor( - @JvmField val sortOrder: SortOrder?, - @JvmField val tags: Set, - @JvmField val tagsExclude: Set, - @JvmField val locale: Locale?, - @JvmField val localeMangas: Locale?, - @JvmField val states: Set, - @JvmField val contentRating: Set, - @JvmField val query: String?, - @JvmField val year: Int, - @JvmField val yearFrom: Int, - @JvmField val yearTo: Int, +data class MangaListFilterV2( + @JvmField val query: String? = null, + @JvmField val tags: Set = emptySet(), + @JvmField val tagsExclude: Set = emptySet(), + @JvmField val locale: Locale? = null, + @JvmField val sourceLocale: Locale? = null, + @JvmField val states: Set = emptySet(), + @JvmField val contentRating: Set = emptySet(), + @JvmField val types: Set = emptySet(), + @JvmField val demographics: Set = emptySet(), + @JvmField val year: Int = 0, + @JvmField val yearFrom: Int = 0, + @JvmField val yearTo: Int = 0, ) { fun isEmpty(): Boolean = tags.isEmpty() && tagsExclude.isEmpty() && locale == null && - localeMangas == null && + sourceLocale == null && states.isEmpty() && contentRating.isEmpty() && query == null && year == 0 && yearFrom == 0 && - yearTo == 0 - - fun newBuilder() = Builder() - .sortOrder(sortOrder) - .tags(tags) - .tagsExclude(tagsExclude) - .locale(locale) - .localeMangas(localeMangas) - .states(states) - .contentRatings(contentRating) - .searchQuery(query) - .year(year) - .yearFrom(yearFrom) - .yearTo(yearTo) - - class Builder { - - private var _sortOrder: SortOrder? = null - private var _tags: Set? = null - private var _tagsExclude: Set? = null - private var _locale: Locale? = null - private var _localeMangas: Locale? = null - private var _states: Set? = null - private var _contentRating: Set? = null - private var _query: String? = null - private var _year: Int = 0 - private var _yearFrom: Int = 0 - private var _yearTo: Int = 0 - - fun sortOrder(order: SortOrder?) = apply { - _sortOrder = order - } - - fun tags(tags: Set?) = apply { - _tags = tags - } - - fun tagsExclude(tags: Set?) = apply { - _tagsExclude = tags - } - - fun locale(locale: Locale?) = apply { - _locale = locale - } - - fun localeMangas(localeMangas: Locale?) = apply { - _localeMangas = localeMangas - } - - fun states(states: Set?) = apply { - _states = states - } - - fun contentRatings(rating: Set?) = apply { - _contentRating = rating - } - - fun searchQuery(query: String?) = apply { - _query = query - } - - fun year(year: Int) = apply { - _year = year - } - - fun yearFrom(yearFrom: Int) = apply { - _yearFrom = yearFrom - } - - fun yearTo(yearTo: Int) = apply { - _yearTo = yearTo - } - - fun build() = MangaListFilterV2( - sortOrder = _sortOrder, - tags = _tags.orEmpty(), - tagsExclude = _tagsExclude.orEmpty(), - locale = _locale, - localeMangas = _localeMangas, - states = _states.orEmpty(), - contentRating = _contentRating.orEmpty(), - query = _query, - year = _year, - yearFrom = _yearFrom, - yearTo = _yearTo, - ) - } + yearTo == 0 && + types.isEmpty() && + demographics.isEmpty() companion object { @JvmStatic - val EMPTY = Builder().build() + val EMPTY = MangaListFilterV2() } } 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 d670a34e..ae464ee1 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 @@ -47,7 +47,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context override val isSearchYearSupported: Boolean = true override val isSearchOriginalLanguages: Boolean = true - override suspend fun getList(offset: Int, filter: MangaListFilter?): List { + override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List { val domain = domain val url = buildString { append("https://api.") @@ -57,104 +57,90 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context append("&offset=") append(offset) append("&includes[]=cover_art&includes[]=author&includes[]=artist") - when (filter) { - is MangaListFilter.Search -> { - append("&title=") - append(filter.query) - append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic&order[relevance]=desc") - } - - is MangaListFilter.Advanced -> { - filter.query.let { - append("&title=") - append(filter.query) - } + filter.query?.let { + append("&title=") + append(filter.query.urlEncoded()) + } - filter.tags.forEach { - append("&includedTags[]=") - append(it.key) - } + filter.tags.forEach { + append("&includedTags[]=") + append(it.key) + } - filter.tagsExclude.forEach { - append("&excludedTags[]=") - append(it.key) - } + filter.tagsExclude.forEach { + append("&excludedTags[]=") + append(it.key) + } - if (filter.contentRating.isNotEmpty()) { - filter.contentRating.forEach { - when (it) { - ContentRating.SAFE -> append("&contentRating[]=safe") - ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica") - ContentRating.ADULT -> append("&contentRating[]=pornographic") - - } - } - } else append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic") - - append("&order") - append( - when (filter.sortOrder) { - SortOrder.UPDATED -> "[latestUploadedChapter]=desc" - SortOrder.UPDATED_ASC -> "[latestUploadedChapter]=asc" - SortOrder.RATING -> "[rating]=desc" - SortOrder.RATING_ASC -> "[rating]=asc" - SortOrder.ALPHABETICAL -> "[title]=asc" - SortOrder.ALPHABETICAL_DESC -> "[title]=desc" - SortOrder.NEWEST -> "[year]=desc" - SortOrder.NEWEST_ASC -> "[year]=asc" - SortOrder.POPULARITY -> "[followedCount]=desc" - SortOrder.POPULARITY_ASC -> "[followedCount]=asc" - SortOrder.ADDED -> "[createdAt]=desc" - SortOrder.ADDED_ASC -> "[createdAt]=asc" - SortOrder.RELEVANCE -> "&order[relevance]=desc" - else -> "[latestUploadedChapter]=desc" - }, - ) + if (filter.contentRating.isNotEmpty()) { + filter.contentRating.forEach { + when (it) { + ContentRating.SAFE -> append("&contentRating[]=safe") + ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica") + ContentRating.ADULT -> append("&contentRating[]=pornographic") - filter.states.forEach { - append("&status[]=") - when (it) { - MangaState.ONGOING -> append("ongoing") - MangaState.FINISHED -> append("completed") - MangaState.ABANDONED -> append("cancelled") - MangaState.PAUSED -> append("hiatus") - else -> append("") - } } + } + } else append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic") + + append("&order") + append( + when (order) { + SortOrder.UPDATED -> "[latestUploadedChapter]=desc" + SortOrder.UPDATED_ASC -> "[latestUploadedChapter]=asc" + SortOrder.RATING -> "[rating]=desc" + SortOrder.RATING_ASC -> "[rating]=asc" + SortOrder.ALPHABETICAL -> "[title]=asc" + SortOrder.ALPHABETICAL_DESC -> "[title]=desc" + SortOrder.NEWEST -> "[year]=desc" + SortOrder.NEWEST_ASC -> "[year]=asc" + SortOrder.POPULARITY -> "[followedCount]=desc" + SortOrder.POPULARITY_ASC -> "[followedCount]=asc" + SortOrder.ADDED -> "[createdAt]=desc" + SortOrder.ADDED_ASC -> "[createdAt]=asc" + SortOrder.RELEVANCE -> "&order[relevance]=desc" + else -> "[latestUploadedChapter]=desc" + }, + ) - filter.demographics.forEach { - append("&publicationDemographic[]=") - append( - when (it) { - Demographic.SHOUNEN -> "shounen" - Demographic.SHOUJO -> "shoujo" - Demographic.SEINEN -> "seinen" - Demographic.JOSEI -> "josei" - Demographic.NONE -> "none" - }, - ) - } + filter.states.forEach { + append("&status[]=") + when (it) { + MangaState.ONGOING -> append("ongoing") + MangaState.FINISHED -> append("completed") + MangaState.ABANDONED -> append("cancelled") + MangaState.PAUSED -> append("hiatus") + else -> append("") + } + } - filter.locale?.let { - append("&availableTranslatedLanguage[]=") - append(it.language) - } + filter.demographics.forEach { + append("&publicationDemographic[]=") + append( + when (it) { + Demographic.SHOUNEN -> "shounen" + Demographic.SHOUJO -> "shoujo" + Demographic.SEINEN -> "seinen" + Demographic.JOSEI -> "josei" + Demographic.NONE -> "none" + }, + ) + } - filter.localeMangas?.let { - append("&originalLanguage[]=") - append(it.language) - } + filter.locale?.let { + append("&availableTranslatedLanguage[]=") + append(it.language) + } - filter.year?.let { - append("&year=") - append(filter.year) - } - } + filter.sourceLocale?.let { + append("&originalLanguage[]=") + append(it.language) + } - null -> { - append("&order[latestUploadedChapter]=desc") - } + if (filter.year != 0) { + append("&year=") + append(filter.year) } } val json = webClient.httpGet(url).parseJson().getJSONArray("data") diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt index 5b698f46..8794393b 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaParserTest.kt @@ -52,27 +52,14 @@ internal class MangaParserTest { val parser = context.newParserInstance(source) val subject = parser.getList( offset = 0, - filter = MangaListFilter.Advanced( - sortOrder = SortOrder.POPULARITY, - tags = emptySet(), - locale = null, - localeMangas = null, - states = emptySet(), - tagsExclude = emptySet(), - contentRating = emptySet(), - query = null, - year = null, - yearFrom = null, - yearTo = null, - types = emptySet(), - demographics = emptySet(), - ), + order = SortOrder.POPULARITY, + filter = MangaListFilterV2.EMPTY, ).minByOrNull { it.title.length } ?: error("No manga found") val query = subject.title check(query.isNotBlank()) { "Manga title '$query' is blank" } - val list = parser.getList(0, MangaListFilter.Search(query)) + val list = parser.getList(0, SortOrder.RELEVANCE, MangaListFilterV2(query = query)) assert(list.isNotEmpty()) { "Empty search results by \"$query\"" } assert(list.singleOrNull { it.url == subject.url && it.id == subject.id } != null) { "Single subject '${subject.title} (${subject.publicUrl})' not found in search results" @@ -102,9 +89,8 @@ internal class MangaParserTest { val tag = tags.last() val list = parser.getList( offset = 0, - MangaListFilter.Advanced.Builder(parser.defaultSortOrder) - .tags(setOf(tag)) - .build(), + order = parser.defaultSortOrder, + filter = MangaListFilterV2(tags = setOf(tag)), ) checkMangaList(list, "${tag.title} (${tag.key})") assert(list.all { it.source == source }) @@ -117,10 +103,8 @@ internal class MangaParserTest { if (!parser.isMultipleTagsSupported) return@runTest val tags = parser.getAvailableTags().shuffled().take(2).toSet() - val filter = MangaListFilter.Advanced.Builder(parser.availableSortOrders.first()) - .tags(tags) - .build() - val list = parser.getList(0, filter) + val filter = MangaListFilterV2(tags = tags) + val list = parser.getList(0, parser.defaultSortOrder, filter) checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})") assert(list.all { it.source == source }) } @@ -133,22 +117,11 @@ internal class MangaParserTest { if (locales.isEmpty()) { return@runTest } - val filter = MangaListFilter.Advanced( - sortOrder = parser.availableSortOrders.first(), - tags = setOf(), - tagsExclude = setOf(), + val filter = MangaListFilterV2( locale = locales.random(), - localeMangas = locales.random(), - states = setOf(), - contentRating = setOf(), - query = null, - year = null, - yearFrom = null, - yearTo = null, - types = emptySet(), - demographics = emptySet(), + sourceLocale = locales.random(), ) - val list = parser.getList(offset = 0, filter) + val list = parser.getList(offset = 0, order = parser.defaultSortOrder, filter) checkMangaList(list, filter.locale.toString()) assert(list.all { it.source == source }) }