|
|
|
@ -1,9 +1,15 @@
|
|
|
|
package org.koitharu.kotatsu.parsers.util
|
|
|
|
package org.koitharu.kotatsu.parsers.util
|
|
|
|
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
|
|
|
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.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.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.*
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -20,26 +26,26 @@ public fun convertToMangaSearchQuery(offset: Int, sortOrder: SortOrder, filter:
|
|
|
|
return MangaSearchQuery.Builder().apply {
|
|
|
|
return MangaSearchQuery.Builder().apply {
|
|
|
|
offset(offset)
|
|
|
|
offset(offset)
|
|
|
|
order(sortOrder)
|
|
|
|
order(sortOrder)
|
|
|
|
if (filter.tags.isNotEmpty()) criterion(QueryCriteria.Include(TAG, filter.tags))
|
|
|
|
if (filter.tags.isNotEmpty()) criterion(Include(TAG, filter.tags))
|
|
|
|
if (filter.tagsExclude.isNotEmpty()) criterion(QueryCriteria.Exclude(TAG, filter.tagsExclude))
|
|
|
|
if (filter.tagsExclude.isNotEmpty()) criterion(Exclude(TAG, filter.tagsExclude))
|
|
|
|
if (filter.states.isNotEmpty()) criterion(QueryCriteria.Include(STATE, filter.states))
|
|
|
|
if (filter.states.isNotEmpty()) criterion(Include(STATE, filter.states))
|
|
|
|
if (filter.types.isNotEmpty()) criterion(QueryCriteria.Include(CONTENT_TYPE, filter.types))
|
|
|
|
if (filter.types.isNotEmpty()) criterion(Include(CONTENT_TYPE, filter.types))
|
|
|
|
if (filter.contentRating.isNotEmpty()) criterion(QueryCriteria.Include(CONTENT_RATING, filter.contentRating))
|
|
|
|
if (filter.contentRating.isNotEmpty()) criterion(Include(CONTENT_RATING, filter.contentRating))
|
|
|
|
if (filter.demographics.isNotEmpty()) criterion(QueryCriteria.Include(DEMOGRAPHIC, filter.demographics))
|
|
|
|
if (filter.demographics.isNotEmpty()) criterion(Include(DEMOGRAPHIC, filter.demographics))
|
|
|
|
if (validateYear(filter.yearFrom) || validateYear(filter.yearTo)) {
|
|
|
|
if (validateYear(filter.yearFrom) || validateYear(filter.yearTo)) {
|
|
|
|
criterion(QueryCriteria.Range(PUBLICATION_YEAR, filter.yearFrom, filter.yearTo))
|
|
|
|
criterion(QueryCriteria.Range(PUBLICATION_YEAR, filter.yearFrom, filter.yearTo))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (validateYear(filter.year)) {
|
|
|
|
if (validateYear(filter.year)) {
|
|
|
|
criterion(QueryCriteria.Match(PUBLICATION_YEAR, filter.year))
|
|
|
|
criterion(Match(PUBLICATION_YEAR, filter.year))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
filter.locale?.takeIf { it != null }?.let {
|
|
|
|
filter.locale?.let {
|
|
|
|
criterion(QueryCriteria.Include(LANGUAGE, setOf(it)))
|
|
|
|
criterion(Include(LANGUAGE, setOf(it)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
filter.originalLocale?.takeIf { it != null }?.let {
|
|
|
|
filter.originalLocale?.let {
|
|
|
|
criterion(QueryCriteria.Include(ORIGINAL_LANGUAGE, setOf(it)))
|
|
|
|
criterion(Include(ORIGINAL_LANGUAGE, setOf(it)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
filter.query?.takeIf { it.isNotBlank() }?.let {
|
|
|
|
filter.query?.takeIf { it.isNotBlank() }?.let {
|
|
|
|
criterion(QueryCriteria.Match(TITLE_NAME, it))
|
|
|
|
criterion(Match(TITLE_NAME, it))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}.build()
|
|
|
|
}.build()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -74,16 +80,99 @@ public fun convertToMangaListFilter(searchQuery: MangaSearchQuery): MangaListFil
|
|
|
|
return MangaListFilter.Builder().apply {
|
|
|
|
return MangaListFilter.Builder().apply {
|
|
|
|
for (criterion in searchQuery.criteria) {
|
|
|
|
for (criterion in searchQuery.criteria) {
|
|
|
|
when (criterion) {
|
|
|
|
when (criterion) {
|
|
|
|
is QueryCriteria.Include<*> -> handleInclude(this, criterion)
|
|
|
|
is Include<*> -> handleInclude(this, criterion)
|
|
|
|
is QueryCriteria.Exclude<*> -> handleExclude(this, criterion)
|
|
|
|
is Exclude<*> -> handleExclude(this, criterion)
|
|
|
|
is QueryCriteria.Range<*> -> handleBetween(this, criterion)
|
|
|
|
is Range<*> -> handleBetween(this, criterion)
|
|
|
|
is QueryCriteria.Match<*> -> handleMatch(this, criterion)
|
|
|
|
is Match<*> -> handleMatch(this, criterion)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}.build()
|
|
|
|
}.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
|
|
|
|
val type = criterion.field.type
|
|
|
|
|
|
|
|
|
|
|
|
when (criterion.field) {
|
|
|
|
when (criterion.field) {
|
|
|
|
@ -94,20 +183,20 @@ private fun handleInclude(builder: MangaListFilter.Builder, criterion: QueryCrit
|
|
|
|
DEMOGRAPHIC -> builder.addDemographics(filterValues(criterion, type))
|
|
|
|
DEMOGRAPHIC -> builder.addDemographics(filterValues(criterion, type))
|
|
|
|
LANGUAGE -> builder.locale(getFirstValue(criterion, type))
|
|
|
|
LANGUAGE -> builder.locale(getFirstValue(criterion, type))
|
|
|
|
ORIGINAL_LANGUAGE -> builder.originalLocale(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
|
|
|
|
val type = criterion.field.type
|
|
|
|
|
|
|
|
|
|
|
|
when (criterion.field) {
|
|
|
|
when (criterion.field) {
|
|
|
|
TAG -> builder.excludeTags(filterValues(criterion, type))
|
|
|
|
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
|
|
|
|
val type = criterion.field.type
|
|
|
|
|
|
|
|
|
|
|
|
when (criterion.field) {
|
|
|
|
when (criterion.field) {
|
|
|
|
@ -115,32 +204,33 @@ private fun handleBetween(builder: MangaListFilter.Builder, criterion: QueryCrit
|
|
|
|
builder.yearFrom(getValue(criterion.from, type, YEAR_UNKNOWN))
|
|
|
|
builder.yearFrom(getValue(criterion.from, type, YEAR_UNKNOWN))
|
|
|
|
builder.yearTo(getValue(criterion.to, 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
|
|
|
|
val type = criterion.field.type
|
|
|
|
|
|
|
|
|
|
|
|
when (criterion.field) {
|
|
|
|
when (criterion.field) {
|
|
|
|
TITLE_NAME -> builder.query(getValue(criterion.value, type, ""))
|
|
|
|
TITLE_NAME -> builder.query(getValue(criterion.value, type, ""))
|
|
|
|
PUBLICATION_YEAR -> builder.year(getValue(criterion.value, type, YEAR_UNKNOWN))
|
|
|
|
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")
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
private fun <T> filterValues(criterion: QueryCriteria.Include<*>, type: Class<*>): List<T> {
|
|
|
|
private fun <T> filterValues(criterion: Include<*>, type: Class<*>): List<T> {
|
|
|
|
return criterion.values.filter { type.isInstance(it) } as List<T>
|
|
|
|
return criterion.values.filter { type.isInstance(it) } as List<T>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
private fun <T> filterValues(criterion: QueryCriteria.Exclude<*>, type: Class<*>): List<T> {
|
|
|
|
private fun <T> filterValues(criterion: Exclude<*>, type: Class<*>): List<T> {
|
|
|
|
return criterion.values.filter { type.isInstance(it) } as List<T>
|
|
|
|
return criterion.values.filter { type.isInstance(it) } as List<T>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
private fun <T> getFirstValue(criterion: QueryCriteria.Include<*>, type: Class<*>): T? {
|
|
|
|
private fun <T> getFirstValue(criterion: Include<*>, type: Class<*>): T? {
|
|
|
|
return criterion.values.firstOrNull { type.isInstance(it) } as? T
|
|
|
|
return criterion.values.firstOrNull { type.isInstance(it) } as? T
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -151,4 +241,4 @@ private fun <T> getValue(value: Any?, type: Class<*>, default: T): T {
|
|
|
|
return if (type.isInstance(value) || isCompatibleIntType) value as T else default
|
|
|
|
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
|
|
|
|
|