Kavita: Filter and search update (#13526)

* Refactored code and removed hack to make search work
- Added queryfield to MetadataDto.kt
- Changed how we encode volumes in chapter_number in chapterFromVolume (tracking)

* Implemented query in filter
Removed PDFs, Epubs from format filter
Added Year Release filter
Added time to read Sort
Refactored some variables
Bumped version

* Changelog and bump version
This commit is contained in:
ThePromidius 2022-09-20 21:44:46 +02:00 committed by GitHub
parent 3c64171991
commit a7c19b4959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 93 additions and 118 deletions

View File

@ -1,3 +1,14 @@
## 1.3.7
### Features
* New Sort filter: Time to read
* New Filter: Year release filter
### Fix
* Filters can now be used together with search
* Epub and pdfs no longer show in format filter (currently not supported)
## 1.3.6
### Fix

View File

@ -6,7 +6,7 @@ ext {
extName = 'Kavita'
pkgNameSuffix = 'all.kavita'
extClass = '.KavitaFactory'
extVersionCode = 6
extVersionCode = 7
}
dependencies {

View File

@ -21,10 +21,8 @@ import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPeople
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataPubStatus
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataTag
import eu.kanade.tachiyomi.extension.all.kavita.dto.PersonRole
import eu.kanade.tachiyomi.extension.all.kavita.dto.SearchResultsDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesMetadataDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesSearchDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.ServerInfoDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.VolumeDto
import eu.kanade.tachiyomi.network.GET
@ -48,7 +46,6 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Dns
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
@ -142,7 +139,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
return POST(
"$apiUrl/series/all?pageNumber=$page&libraryId=0&pageSize=20",
headersBuilder().build(),
buildFilterBody()
buildFilterBody(currentFilter)
)
}
@ -176,71 +173,71 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
/**
* SEARCH MANGA
* **/
private var isFilterOn = false // If any filter option is enabled this is true
private var toFilter = MetadataPayload()
private var currentFilter: MetadataPayload = MetadataPayload()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
toFilter = MetadataPayload() // need to reset it or will double
isFilterOn = false
val newFilter = MetadataPayload() // need to reset it or will double
filters.forEach { filter ->
when (filter) {
is SortFilter -> {
if (filter.state != null) {
toFilter.sorting = filter.state!!.index + 1
toFilter.sorting_asc = filter.state!!.ascending
// Disabled until search is stable
// isFilterOn = false
newFilter.sorting = filter.state!!.index + 1
newFilter.sorting_asc = filter.state!!.ascending
}
}
is StatusFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.readStatus.add(content.name)
isFilterOn = true
newFilter.readStatus.add(content.name)
}
}
}
is ReleaseYearRangeGroup -> {
filter.state.forEach { content ->
if (content.state.isNotEmpty()) {
if (content.name == "Min")
newFilter.releaseYearRangeMin = content.state.toInt()
if (content.name == "Max")
newFilter.releaseYearRangeMax = content.state.toInt()
}
}
}
is GenreFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.genres.add(genresListMeta.find { it.title == content.name }!!.id)
isFilterOn = true
newFilter.genres.add(genresListMeta.find { it.title == content.name }!!.id)
}
}
}
is UserRating -> {
toFilter.userRating = filter.state
isFilterOn = true
newFilter.userRating = filter.state
}
is TagFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.tags.add(tagsListMeta.find { it.title == content.name }!!.id)
isFilterOn = true
newFilter.tags.add(tagsListMeta.find { it.title == content.name }!!.id)
}
}
}
is AgeRatingFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.ageRating.add(ageRatingsListMeta.find { it.title == content.name }!!.value)
isFilterOn = true
newFilter.ageRating.add(ageRatingsListMeta.find { it.title == content.name }!!.value)
}
}
}
is FormatsFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.formats.add(MangaFormat.valueOf(content.name).ordinal)
isFilterOn = true
newFilter.formats.add(MangaFormat.valueOf(content.name).ordinal)
}
}
}
is CollectionFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.collections.add(collectionsListMeta.find { it.title == content.name }!!.id)
isFilterOn = true
newFilter.collections.add(collectionsListMeta.find { it.title == content.name }!!.id)
}
}
}
@ -248,16 +245,14 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
is LanguageFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.language.add(languagesListMeta.find { it.title == content.name }!!.isoCode)
isFilterOn = true
newFilter.language.add(languagesListMeta.find { it.title == content.name }!!.isoCode)
}
}
}
is LibrariesFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.libraries.add(libraryListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.libraries.add(libraryListMeta.find { it.name == content.name }!!.id)
}
}
}
@ -265,8 +260,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
is PubStatusFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.pubStatus.add(pubStatusListMeta.find { it.title == content.name }!!.value)
isFilterOn = true
newFilter.pubStatus.add(pubStatusListMeta.find { it.title == content.name }!!.value)
}
}
}
@ -274,116 +268,84 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
is WriterPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peopleWriters.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peopleWriters.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is PencillerPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peoplePenciller.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peoplePenciller.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is InkerPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peopleInker.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peopleInker.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is ColoristPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peoplePeoplecolorist.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peoplePeoplecolorist.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is LettererPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peopleLetterer.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peopleLetterer.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is CoverArtistPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peopleCoverArtist.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peopleCoverArtist.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is EditorPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peopleEditor.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peopleEditor.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is PublisherPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peoplePublisher.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peoplePublisher.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is CharacterPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peopleCharacter.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peopleCharacter.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
is TranslatorPeopleFilterGroup -> {
filter.state.forEach { content ->
if (content.state) {
toFilter.peopleTranslator.add(peopleListMeta.find { it.name == content.name }!!.id)
isFilterOn = true
newFilter.peopleTranslator.add(peopleListMeta.find { it.name == content.name }!!.id)
}
}
}
else -> isFilterOn = false
else -> {}
}
}
if (query.isEmpty()) {
isFilterOn = true
return popularMangaRequest(page)
} else {
isFilterOn = false
val url = "$apiUrl/Library/search".toHttpUrl().newBuilder()
.addQueryParameter("queryString", query)
return GET(url.toString(), headers)
}
newFilter.seriesNameQuery = query
currentFilter = newFilter
return popularMangaRequest(page)
}
override fun searchMangaParse(response: Response): MangasPage {
if (isFilterOn) {
return popularMangaParse(response)
} else {
if (response.request.url.toString().contains("api/series/all"))
return popularMangaParse(response)
val result = response.parseAs<SearchResultsDto>().series
val mangaList = result.map(::searchMangaFromObject)
return MangasPage(mangaList, false)
}
}
private fun searchMangaFromObject(obj: SeriesSearchDto): SManga = SManga.create().apply {
title = obj.name
thumbnail_url = "$apiUrl/Image/series-cover?seriesId=${obj.seriesId}"
description = "None"
url = "$apiUrl/Series/${obj.seriesId}"
return popularMangaParse(response)
}
/**
@ -448,12 +410,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private fun chapterFromObject(obj: ChapterDto): SChapter = SChapter.create().apply {
url = obj.id.toString()
if (obj.number == "0" && obj.isSpecial) {
name = if (obj.number == "0" && obj.isSpecial) {
// This is a special. Chapter name is special name
name = obj.range
obj.range
} else {
val cleanedName = obj.title.replaceFirst("^0+(?!$)".toRegex(), "")
name = "Chapter $cleanedName"
"Chapter $cleanedName"
}
date_upload = helper.parseDate(obj.created)
chapter_number = obj.number.toFloat()
@ -487,7 +449,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
// Is a single-file volume
// We encode the chapter number to support tracking
name = "Volume ${volume.number}"
chapter_number = volume.number.toFloat() / 100
chapter_number = volume.number.toFloat() / 10000
}
} else {
name = "Unhandled Else Volume ${volume.number}"
@ -579,7 +541,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
"Translator"
)
private class UserRating() :
private class UserRating :
Filter.Select<String>(
"Minimum Rating",
arrayOf(
@ -594,16 +556,21 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private class SortFilter(sortables: Array<String>) : Filter.Sort("Sort by", sortables, Selection(0, true))
val sortableList = listOf(
private val sortableList = listOf(
Pair("Sort name", 1),
Pair("Created", 2),
Pair("Last modified", 3),
Pair("Item added", 4),
Pair("Time to Read", 5)
)
private class StatusFilter(name: String) : Filter.CheckBox(name, false)
private class StatusFilterGroup(filters: List<StatusFilter>) :
Filter.Group<StatusFilter>("Status", filters)
private class ReleaseYearRange(name: String) : Filter.Text(name)
private class ReleaseYearRangeGroup(filters: List<ReleaseYearRange>) :
Filter.Group<ReleaseYearRange>("Release Year", filters)
private class GenreFilter(name: String) : Filter.CheckBox(name, false)
private class GenreFilterGroup(genres: List<GenreFilter>) :
Filter.Group<GenreFilter>("Genres", genres)
@ -637,7 +604,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private class PeopleHeaderFilter(name: String) :
Filter.Header(name)
private class PeopleSeparatorFilter() :
private class PeopleSeparatorFilter :
Filter.Separator()
private class WriterPeopleFilter(name: String) : Filter.CheckBox(name, false)
@ -713,6 +680,13 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
)
)
}
if (toggledFilters.contains("ReleaseYearRange")) {
filtersLoaded.add(
ReleaseYearRangeGroup(
listOf("Min", "Max").map { ReleaseYearRange(it) }
)
)
}
if (genresListMeta.isNotEmpty() and toggledFilters.contains("Genres")) {
filtersLoaded.add(
@ -736,8 +710,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
"Image",
"Archive",
"Unknown",
"Epub",
"Pdf"
).map { FormatFilter(it) }
)
)
@ -890,11 +862,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
.add("Content-Type", "application/json")
.add("Authorization", "Bearer $jwtToken")
}
private fun buildFilterBody(filter: MetadataPayload = toFilter): RequestBody {
var filter = filter
if (!isFilterOn and !filter.forceUseMetadataPayload) {
filter = MetadataPayload()
}
private fun buildFilterBody(filter: MetadataPayload): RequestBody {
val formats = if (filter.formats.isEmpty()) {
buildJsonArray {
@ -946,6 +914,14 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
put("isAscending", JsonPrimitive(filter.sorting_asc))
}
)
put("seriesNameQuery", filter.seriesNameQuery)
put(
"releaseYearRange",
buildJsonObject {
put("min", filter.releaseYearRangeMin)
put("max", filter.releaseYearRangeMax)
}
)
}
return payload.toString().toRequestBody(JSON_MEDIA_TYPE)
}
@ -1052,7 +1028,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!!
private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!!
private fun getPrefKey(key: String): String = preferences.getString(key, "")!!
private fun getPrefKey(): String = preferences.getString("APIKEY", "")!!
private fun getToggledFilters() = preferences.getStringSet(KavitaConstants.toggledFiltersPref, KavitaConstants.defaultFilterPrefEntries)!!
// We strip the last slash since we will append it above
@ -1129,7 +1105,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
if (jwtToken.isEmpty()) setupLogin()
Log.v(LOG_TAG, "[Login] Starting login")
val request = POST(
"$apiUrl/Plugin/authenticate?apiKey=${getPrefKey("APIKEY")}&pluginName=Tachiyomi-Kavita",
"$apiUrl/Plugin/authenticate?apiKey=${getPrefKey()}&pluginName=Tachiyomi-Kavita",
setupLoginHeaders().build(), "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
)
client.newCall(request).execute().use {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.extension.all.kavita
object KavitaConstants {
// togle filters
// toggle filters
const val toggledFiltersPref = "toggledFilters"
val filterPrefEntries = arrayOf(
"Sort Options",
@ -24,7 +24,8 @@ object KavitaConstants {
"Editor",
"Publisher",
"Character",
"Translators"
"Translators",
"ReleaseYearRange"
)
val filterPrefEntriesValue = arrayOf(
"Sort Options",
@ -47,7 +48,8 @@ object KavitaConstants {
"Editor",
"Publisher",
"Character",
"Translators"
"Translators",
"ReleaseYearRange"
)
val defaultFilterPrefEntries = setOf(
"Sort Options",
@ -70,7 +72,8 @@ object KavitaConstants {
"Editor",
"Publisher",
"Character",
"Translators"
"Translators",
"ReleaseYearRange"
)
const val customSourceNamePref = "customSourceName"

View File

@ -96,20 +96,3 @@ data class ChapterDto(
val volumeId: Int,
val created: String
)
@Serializable
data class SearchResultsDto(
val series: List<SeriesSearchDto>
)
@Serializable
data class SeriesSearchDto(
val seriesId: Int,
val name: String,
val originalName: String,
val sortName: String,
val localizedName: String,
val format: Int,
val libraryName: String,
val libraryId: Int
)

View File

@ -63,6 +63,9 @@ data class MetadataPayload(
var language: ArrayList<String> = arrayListOf<String>(),
var libraries: ArrayList<Int> = arrayListOf<Int>(),
var pubStatus: ArrayList<Int> = arrayListOf<Int>(),
var seriesNameQuery: String = "",
var releaseYearRangeMin: Int = 0,
var releaseYearRangeMax: Int = 0,
var peopleWriters: ArrayList<Int> = arrayListOf<Int>(),
var peoplePenciller: ArrayList<Int> = arrayListOf<Int>(),
@ -74,5 +77,4 @@ data class MetadataPayload(
var peoplePublisher: ArrayList<Int> = arrayListOf<Int>(),
var peopleCharacter: ArrayList<Int> = arrayListOf<Int>(),
var peopleTranslator: ArrayList<Int> = arrayListOf<Int>(),
)