Partial migration to MangaListFilterV2

Koitharu 2 years ago
parent 9042074c50
commit 481ad02e01
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -169,7 +169,7 @@ abstract class MangaParser @InternalParsersApi constructor(
@Deprecated( @Deprecated(
"Use getList with filter instead", "Use getList with filter instead",
ReplaceWith( ReplaceWith(
"getList(offset, MangaListFilter.Search(query))", "getList(offset, SortOrder.RELEVANCE, MangaListFilterV2(query = query))",
"org.koitharu.kotatsu.parsers.model.MangaListFilter", "org.koitharu.kotatsu.parsers.model.MangaListFilter",
), ),
) )
@ -188,7 +188,7 @@ abstract class MangaParser @InternalParsersApi constructor(
@Deprecated( @Deprecated(
"Use getList with filter instead", "Use getList with filter instead",
ReplaceWith( ReplaceWith(
"getList(offset, MangaListFilter.Advanced(sortOrder, tags, null, emptySet()))", "getList(offset, sortOrder, MangaListFilterV2(tags = tags))",
"org.koitharu.kotatsu.parsers.model.MangaListFilter", "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<Manga> { open suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) { return when (filter) {
is MangaListFilter.Advanced -> getList( 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. * Parse details for [Manga]: chapters list, description, large cover, etc.
* Must return the same manga, may change any fields excepts id, url and source * Must return the same manga, may change any fields excepts id, url and source

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import java.util.* import java.util.*
@Deprecated("Use MangaListFilterV2 instead")
sealed interface MangaListFilter { sealed interface MangaListFilter {
fun isEmpty(): Boolean fun isEmpty(): Boolean

@ -2,121 +2,37 @@ package org.koitharu.kotatsu.parsers.model
import java.util.* import java.util.*
@Suppress("DataClassPrivateConstructor") data class MangaListFilterV2(
data class MangaListFilterV2 private constructor( @JvmField val query: String? = null,
@JvmField val sortOrder: SortOrder?, @JvmField val tags: Set<MangaTag> = emptySet(),
@JvmField val tags: Set<MangaTag>, @JvmField val tagsExclude: Set<MangaTag> = emptySet(),
@JvmField val tagsExclude: Set<MangaTag>, @JvmField val locale: Locale? = null,
@JvmField val locale: Locale?, @JvmField val sourceLocale: Locale? = null,
@JvmField val localeMangas: Locale?, @JvmField val states: Set<MangaState> = emptySet(),
@JvmField val states: Set<MangaState>, @JvmField val contentRating: Set<ContentRating> = emptySet(),
@JvmField val contentRating: Set<ContentRating>, @JvmField val types: Set<ContentType> = emptySet(),
@JvmField val query: String?, @JvmField val demographics: Set<Demographic> = emptySet(),
@JvmField val year: Int, @JvmField val year: Int = 0,
@JvmField val yearFrom: Int, @JvmField val yearFrom: Int = 0,
@JvmField val yearTo: Int, @JvmField val yearTo: Int = 0,
) { ) {
fun isEmpty(): Boolean = tags.isEmpty() && fun isEmpty(): Boolean = tags.isEmpty() &&
tagsExclude.isEmpty() && tagsExclude.isEmpty() &&
locale == null && locale == null &&
localeMangas == null && sourceLocale == null &&
states.isEmpty() && states.isEmpty() &&
contentRating.isEmpty() && contentRating.isEmpty() &&
query == null && query == null &&
year == 0 && year == 0 &&
yearFrom == 0 && yearFrom == 0 &&
yearTo == 0 yearTo == 0 &&
types.isEmpty() &&
fun newBuilder() = Builder() demographics.isEmpty()
.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<MangaTag>? = null
private var _tagsExclude: Set<MangaTag>? = null
private var _locale: Locale? = null
private var _localeMangas: Locale? = null
private var _states: Set<MangaState>? = null
private var _contentRating: Set<ContentRating>? = 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<MangaTag>?) = apply {
_tags = tags
}
fun tagsExclude(tags: Set<MangaTag>?) = apply {
_tagsExclude = tags
}
fun locale(locale: Locale?) = apply {
_locale = locale
}
fun localeMangas(localeMangas: Locale?) = apply {
_localeMangas = localeMangas
}
fun states(states: Set<MangaState>?) = apply {
_states = states
}
fun contentRatings(rating: Set<ContentRating>?) = 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,
)
}
companion object { companion object {
@JvmStatic @JvmStatic
val EMPTY = Builder().build() val EMPTY = MangaListFilterV2()
} }
} }

@ -47,7 +47,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
override val isSearchYearSupported: Boolean = true override val isSearchYearSupported: Boolean = true
override val isSearchOriginalLanguages: Boolean = true override val isSearchOriginalLanguages: Boolean = true
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
val domain = domain val domain = domain
val url = buildString { val url = buildString {
append("https://api.") append("https://api.")
@ -57,104 +57,90 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append("&offset=") append("&offset=")
append(offset) append(offset)
append("&includes[]=cover_art&includes[]=author&includes[]=artist") 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 { filter.query?.let {
append("&title=") append("&title=")
append(filter.query) append(filter.query.urlEncoded())
} }
filter.tags.forEach { filter.tags.forEach {
append("&includedTags[]=") append("&includedTags[]=")
append(it.key) append(it.key)
} }
filter.tagsExclude.forEach { filter.tagsExclude.forEach {
append("&excludedTags[]=") append("&excludedTags[]=")
append(it.key) append(it.key)
} }
if (filter.contentRating.isNotEmpty()) { if (filter.contentRating.isNotEmpty()) {
filter.contentRating.forEach { filter.contentRating.forEach {
when (it) { when (it) {
ContentRating.SAFE -> append("&contentRating[]=safe") ContentRating.SAFE -> append("&contentRating[]=safe")
ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica") ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica")
ContentRating.ADULT -> append("&contentRating[]=pornographic") 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"
},
)
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 { filter.states.forEach {
append("&publicationDemographic[]=") append("&status[]=")
append( when (it) {
when (it) { MangaState.ONGOING -> append("ongoing")
Demographic.SHOUNEN -> "shounen" MangaState.FINISHED -> append("completed")
Demographic.SHOUJO -> "shoujo" MangaState.ABANDONED -> append("cancelled")
Demographic.SEINEN -> "seinen" MangaState.PAUSED -> append("hiatus")
Demographic.JOSEI -> "josei" else -> append("")
Demographic.NONE -> "none" }
}, }
)
}
filter.locale?.let { filter.demographics.forEach {
append("&availableTranslatedLanguage[]=") append("&publicationDemographic[]=")
append(it.language) append(
} when (it) {
Demographic.SHOUNEN -> "shounen"
Demographic.SHOUJO -> "shoujo"
Demographic.SEINEN -> "seinen"
Demographic.JOSEI -> "josei"
Demographic.NONE -> "none"
},
)
}
filter.localeMangas?.let { filter.locale?.let {
append("&originalLanguage[]=") append("&availableTranslatedLanguage[]=")
append(it.language) append(it.language)
} }
filter.year?.let { filter.sourceLocale?.let {
append("&year=") append("&originalLanguage[]=")
append(filter.year) append(it.language)
} }
}
null -> { if (filter.year != 0) {
append("&order[latestUploadedChapter]=desc") append("&year=")
} append(filter.year)
} }
} }
val json = webClient.httpGet(url).parseJson().getJSONArray("data") val json = webClient.httpGet(url).parseJson().getJSONArray("data")

@ -52,27 +52,14 @@ internal class MangaParserTest {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val subject = parser.getList( val subject = parser.getList(
offset = 0, offset = 0,
filter = MangaListFilter.Advanced( order = SortOrder.POPULARITY,
sortOrder = SortOrder.POPULARITY, filter = MangaListFilterV2.EMPTY,
tags = emptySet(),
locale = null,
localeMangas = null,
states = emptySet(),
tagsExclude = emptySet(),
contentRating = emptySet(),
query = null,
year = null,
yearFrom = null,
yearTo = null,
types = emptySet(),
demographics = emptySet(),
),
).minByOrNull { ).minByOrNull {
it.title.length it.title.length
} ?: error("No manga found") } ?: error("No manga found")
val query = subject.title val query = subject.title
check(query.isNotBlank()) { "Manga title '$query' is blank" } 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.isNotEmpty()) { "Empty search results by \"$query\"" }
assert(list.singleOrNull { it.url == subject.url && it.id == subject.id } != null) { assert(list.singleOrNull { it.url == subject.url && it.id == subject.id } != null) {
"Single subject '${subject.title} (${subject.publicUrl})' not found in search results" "Single subject '${subject.title} (${subject.publicUrl})' not found in search results"
@ -102,9 +89,8 @@ internal class MangaParserTest {
val tag = tags.last() val tag = tags.last()
val list = parser.getList( val list = parser.getList(
offset = 0, offset = 0,
MangaListFilter.Advanced.Builder(parser.defaultSortOrder) order = parser.defaultSortOrder,
.tags(setOf(tag)) filter = MangaListFilterV2(tags = setOf(tag)),
.build(),
) )
checkMangaList(list, "${tag.title} (${tag.key})") checkMangaList(list, "${tag.title} (${tag.key})")
assert(list.all { it.source == source }) assert(list.all { it.source == source })
@ -117,10 +103,8 @@ internal class MangaParserTest {
if (!parser.isMultipleTagsSupported) return@runTest if (!parser.isMultipleTagsSupported) return@runTest
val tags = parser.getAvailableTags().shuffled().take(2).toSet() val tags = parser.getAvailableTags().shuffled().take(2).toSet()
val filter = MangaListFilter.Advanced.Builder(parser.availableSortOrders.first()) val filter = MangaListFilterV2(tags = tags)
.tags(tags) val list = parser.getList(0, parser.defaultSortOrder, filter)
.build()
val list = parser.getList(0, filter)
checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})") checkMangaList(list, "${tags.joinToString { it.title }} (${tags.joinToString { it.key }})")
assert(list.all { it.source == source }) assert(list.all { it.source == source })
} }
@ -133,22 +117,11 @@ internal class MangaParserTest {
if (locales.isEmpty()) { if (locales.isEmpty()) {
return@runTest return@runTest
} }
val filter = MangaListFilter.Advanced( val filter = MangaListFilterV2(
sortOrder = parser.availableSortOrders.first(),
tags = setOf(),
tagsExclude = setOf(),
locale = locales.random(), locale = locales.random(),
localeMangas = locales.random(), sourceLocale = locales.random(),
states = setOf(),
contentRating = setOf(),
query = null,
year = null,
yearFrom = null,
yearTo = null,
types = emptySet(),
demographics = emptySet(),
) )
val list = parser.getList(offset = 0, filter) val list = parser.getList(offset = 0, order = parser.defaultSortOrder, filter)
checkMangaList(list, filter.locale.toString()) checkMangaList(list, filter.locale.toString())
assert(list.all { it.source == source }) assert(list.all { it.source == source })
} }

Loading…
Cancel
Save