From 818bedc9552071722a5f1f316038b5ca236f538f Mon Sep 17 00:00:00 2001 From: ThePromidius Date: Wed, 23 Feb 2022 10:46:32 +0100 Subject: [PATCH] Kavita: Bug fixes and changes for tracking (#10904) * Fixed filters not populating if user was not admin * Updated search, changes needed for tracking and changes in login * Bump ext version Updated Changelog Updated Readme * changed url query to proper HttpUrl builder Co-authored-by: Vetle Ledaal * changed url query to proper HttpUrl builder Co-authored-by: Vetle Ledaal Co-authored-by: Vetle Ledaal --- src/all/kavita/CHANGELOG.md | 14 + src/all/kavita/README.md | 2 +- src/all/kavita/build.gradle | 2 +- .../tachiyomi/extension/all/kavita/Kavita.kt | 486 ++++++++++-------- .../extension/all/kavita/dto/MangaDto.kt | 7 +- .../extension/all/kavita/dto/MetadataDto.kt | 1 + .../extension/all/kavita/dto/Responses.kt | 9 + 7 files changed, 317 insertions(+), 204 deletions(-) diff --git a/src/all/kavita/CHANGELOG.md b/src/all/kavita/CHANGELOG.md index 3600e181b..906d30039 100644 --- a/src/all/kavita/CHANGELOG.md +++ b/src/all/kavita/CHANGELOG.md @@ -1,3 +1,17 @@ +## 1.2.3 + +### Fix + +* Fixed Rating filter +* Fixed Chapter list not sorting correctly +* Fixed search +* Fixed manga details not showing correctly +* Fixed filters not populating if account was not admin + +### Features +* The extension is now ready to implement tracking. +* Min required version for the extension to work properly: `v0.5.1.1` + ## 1.2.2 ### Features diff --git a/src/all/kavita/README.md b/src/all/kavita/README.md index a73efdf92..79f467edd 100644 --- a/src/all/kavita/README.md +++ b/src/all/kavita/README.md @@ -12,7 +12,7 @@ Table of Content Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation) -Kavita also has a documentation about the Tachiyomi Kavita extension at the [Kavita wiki](https://wiki.kavitareader.com/en/guides/plugins-and-3rd-party-scripts/tachiyomi). +Kavita also has a documentation about the Tachiyomi Kavita extension at the [Kavita wiki](https://wiki.kavitareader.com/en/guides/misc/tachiyomi). ## FAQ diff --git a/src/all/kavita/build.gradle b/src/all/kavita/build.gradle index a76875a7d..b6967e701 100644 --- a/src/all/kavita/build.gradle +++ b/src/all/kavita/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Kavita' pkgNameSuffix = 'all.kavita' extClass = '.KavitaFactory' - extVersionCode = 2 + extVersionCode = 3 } dependencies { diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt index bf4be2f84..8beaac961 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt @@ -10,7 +10,6 @@ import androidx.preference.MultiSelectListPreference import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.extension.all.kavita.dto.AuthenticationDto import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterDto -import eu.kanade.tachiyomi.extension.all.kavita.dto.KavitaComicsSearch import eu.kanade.tachiyomi.extension.all.kavita.dto.MangaFormat import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataAgeRatings import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataCollections @@ -22,8 +21,11 @@ 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 import eu.kanade.tachiyomi.network.POST @@ -44,6 +46,8 @@ import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Request import okhttp3.RequestBody @@ -56,9 +60,32 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.IOException +import java.net.ConnectException import java.security.MessageDigest -class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { +class Kavita(private val suffix: String = "") : ConfigurableSource, HttpSource() { + class CompareChapters { + companion object : Comparator { + override fun compare(a: SChapter, b: SChapter): Int { + if (a.chapter_number < 1.0 && b.chapter_number < 1.0) { + // Both are volumes, multiply by 100 and do normal sort + return if ((a.chapter_number * 100) < (b.chapter_number * 100)) { + 1 + } else -1 + } else { + if (a.chapter_number < 1.0 && b.chapter_number >= 1.0) { + // A is volume, b is not. A should sort first + return 1 + } else if (a.chapter_number >= 1.0 && b.chapter_number < 1.0) { + return -1 + } + } + if (a.chapter_number < b.chapter_number) return 1 + if (a.chapter_number > b.chapter_number) return -1 + return 0 + } + } + } override val id by lazy { val key = "${"kavita_$suffix"}/all/$versionId" @@ -75,13 +102,19 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { override val baseUrl by lazy { getPrefBaseUrl() } private val address by lazy { getPrefAddress() } // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory. - private val LOG_TAG = "extension.all.kavita_${preferences.getString(KavitaConstants.customSourceNamePref,suffix)!!.replace(' ','_')}" + private val LOG_TAG = """extension.all.kavita_${"[$suffix]_" + preferences.getString(KavitaConstants.customSourceNamePref,"[$suffix]")!!.replace(' ','_')}""" private var isLoged = false // Used to know if login was correct and not send login requests anymore private val json: Json by injectLazy() private val helper = KavitaHelper() private inline fun Response.parseAs(): T = use { + if (it.peekBody(Long.MAX_VALUE).string().isEmpty()) { + throw EmptyRequestBody( + "Body of the response is empty. RequestUrl=${it.request.url}\nPlease check your kavita instance is up to date", + Throwable("Empty Body of the response is empty. RequestUrl=${it.request.url}\n Please check your kavita instance is up to date") + ) + } json.decodeFromString(it.body?.string().orEmpty()) } private inline fun > safeValueOf(type: String): T { @@ -143,8 +176,8 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { if (filter.state != null) { toFilter.sorting = filter.state!!.index + 1 toFilter.sorting_asc = filter.state!!.ascending - // disabled till the search api is stable - // isFilterOn = true + // Disabled until search is stable +// isFilterOn = false } } is StatusFilterGroup -> { @@ -165,6 +198,7 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { } is UserRating -> { toFilter.userRating = filter.state + isFilterOn = true } is TagFilterGroup -> { filter.state.forEach { content -> @@ -305,13 +339,18 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { } } } + else -> isFilterOn = false } } - if (isFilterOn || query.isEmpty()) { + if (query.isEmpty()) { + isFilterOn = true return popularMangaRequest(page) } else { - return GET("$apiUrl/Library/search?queryString=$query", headers) + isFilterOn = false + val url = "$apiUrl/Library/search".toHttpUrl().newBuilder() + .addQueryParameter("queryString", query) + return GET(url.toString(), headers) } } @@ -322,13 +361,13 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { if (response.request.url.toString().contains("api/series/all")) return popularMangaParse(response) - val result = response.parseAs>() + val result = response.parseAs().series val mangaList = result.map(::searchMangaFromObject) return MangasPage(mangaList, false) } } - private fun searchMangaFromObject(obj: KavitaComicsSearch): SManga = SManga.create().apply { + private fun searchMangaFromObject(obj: SeriesSearchDto): SManga = SManga.create().apply { title = obj.name thumbnail_url = "$apiUrl/Image/series-cover?seriesId=${obj.seriesId}" description = "None" @@ -363,23 +402,25 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { val result = response.parseAs() val existingSeries = series.find { dto -> dto.id == result.seriesId } - + Log.d("[Kavita]", "old manga url:") if (existingSeries != null) { val manga = helper.createSeriesDto(existingSeries, apiUrl) + manga.url = "$apiUrl/Series/${result.seriesId}" manga.artist = result.coverArtists.joinToString { it.name } manga.description = result.summary manga.author = result.writers.joinToString { it.name } manga.genre = result.genres.joinToString { it.title } + manga.thumbnail_url = "$apiUrl/image/series-cover?seriesId=${result.seriesId}" return manga } return SManga.create().apply { url = "$apiUrl/Series/${result.seriesId}" - artist = result.coverArtists.joinToString { ", " } - author = result.writers.joinToString { ", " } - genre = result.genres.joinToString { ", " } - thumbnail_url = "$apiUrl/image/series-cover?seriesId=${result.seriesId}" + artist = result.coverArtists.joinToString { it.name } + description = result.summary + author = result.writers.joinToString { it.name } + genre = result.genres.joinToString { it.title } } } @@ -394,6 +435,7 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { private fun chapterFromObject(obj: ChapterDto): SChapter = SChapter.create().apply { url = obj.id.toString() if (obj.number == "0" && obj.isSpecial) { + // This is a special. Chapter name is special name name = obj.range } else { val cleanedName = obj.title.replaceFirst("^0+(?!$)".toRegex(), "") @@ -401,7 +443,7 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { } date_upload = helper.parseDate(obj.created) chapter_number = obj.number.toFloat() - scanlator = obj.pages.toString() + scanlator = "${obj.pages} pages" } private fun chapterFromVolume(obj: ChapterDto, volume: VolumeDto): SChapter = @@ -409,25 +451,31 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { // If there are multiple chapters to this volume, then prefix with Volume number if (volume.chapters.isNotEmpty() && obj.number != "0") { name = "Volume ${volume.number} Chapter ${obj.number}" + chapter_number = obj.number.toFloat() } else if (obj.number == "0") { // This chapter is solely on volume if (volume.number == 0) { // Treat as special if (obj.range == "") { name = "Chapter 0" + chapter_number = obj.number.toFloat() } else { name = obj.range + chapter_number = obj.number.toFloat() } } else { name = "Volume ${volume.number}" +// val newVolNumber: Float = (volume.number / 100).toFloat() +// chapter_number = newVolNumber.toString().padStart(3, '0').toFloat() + chapter_number = volume.number.toFloat() / 100 } } else { name = "Unhandled Else Volume ${volume.number}" } url = obj.id.toString() date_upload = helper.parseDate(obj.created) - chapter_number = obj.number.toFloat() - scanlator = "${obj.pages}" + + scanlator = "${obj.pages} pages" } override fun chapterListParse(response: Response): List { try { @@ -448,7 +496,8 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { } } } - allChapterList.reverse() + + allChapterList.sortWith(CompareChapters) return allChapterList } catch (e: Exception) { Log.e(LOG_TAG, "Unhandled exception parsing chapters. Send logs to kavita devs", e) @@ -465,7 +514,7 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { override fun fetchPageList(chapter: SChapter): Observable> { val chapterId = chapter.url - val numPages = chapter.scanlator?.toInt() + val numPages = chapter.scanlator?.replace(" pages", "")?.toInt() val numPages2 = "$numPages".toInt() - 1 val pages = mutableListOf() for (i in 0..numPages2) { @@ -797,8 +846,18 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { constructor(cause: Throwable) : this(null, cause) } + class OpdsurlExistsInPref(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { + constructor(cause: Throwable) : this(null, cause) + } + class EmptyRequestBody(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { + constructor(cause: Throwable) : this(null, cause) + } + class LoadingFilterFailed(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { + constructor(cause: Throwable) : this(null, cause) + } + override fun headersBuilder(): Headers.Builder { - if (jwtToken.isEmpty()) throw LoginErrorException("403 Error\nOPDS address got modified or is incorrect") + if (jwtToken.isEmpty()) throw LoginErrorException("401 Error\nOPDS address got modified or is incorrect") return Headers.Builder() .add("User-Agent", "Tachiyomi Kavita v${BuildConfig.VERSION_NAME}") .add("Content-Type", "application/json") @@ -833,13 +892,8 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { "readStatus", buildJsonObject { if (filter.readStatus.isNotEmpty()) { - filter.readStatus.forEach { status -> - if (status in listOf("notRead", "inProgress", "read")) { - put(status, JsonPrimitive(true)) - } else { - put(status, JsonPrimitive(false)) - } - } + filter.readStatusList + .forEach { status -> put(status, JsonPrimitive(status in filter.readStatus)) } } else { put("notRead", JsonPrimitive(true)) put("inProgress", JsonPrimitive(true)) @@ -882,7 +936,6 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { "", "The OPDS url copied from User Settings. This should include address and the api key on end." ) - val enabledFiltersPref = MultiSelectListPreference(screen.context).apply { key = KavitaConstants.toggledFiltersPref title = "Default filters shown" @@ -915,7 +968,6 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { res } } - screen.addPreference(customSourceNamePref) screen.addPreference(opdsAddressPref) screen.addPreference(enabledFiltersPref) @@ -944,6 +996,19 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { } setOnPreferenceChangeListener { _, newValue -> try { + val opdsUrlInPref = opdsUrlInPreferences(newValue.toString()) // We don't allow hot have multiple sources with same ip or domain + if (opdsUrlInPref.isNotEmpty()) { + // TODO("Add option to allow multiple sources with same url at the cost of tracking") + preferences.edit().putString(title, "").apply() + + Toast.makeText( + context, + "URL exists in a different source -> $opdsUrlInPref", + Toast.LENGTH_LONG + ).show() + throw OpdsurlExistsInPref("Url exists in a different source -> $opdsUrlInPref") + } + val res = preferences.edit().putString(title, newValue as String).commit() Toast.makeText( context, @@ -953,15 +1018,17 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { setupLogin(newValue) Log.v(LOG_TAG, "[Preferences] Successfully modified OPDS URL") res + } catch (e: OpdsurlExistsInPref) { + Log.e(LOG_TAG, "Url exists in a different sourcce") + false } catch (e: Exception) { - e.printStackTrace() + Log.e(LOG_TAG, "Unrecognised error", e) false } } } } - // private fun getPrefapiKey(): String = preferences.getString("APIKEY", "")!! private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!! private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!! private fun getPrefKey(key: String): String = preferences.getString(key, "")!! @@ -984,23 +1051,48 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { /** * LOGIN **/ + + private fun opdsUrlInPreferences(url: String): String { + fun getCleanedApiUrl(url: String): String = "${url.split("/api/").first()}/api" + /**Used to check if a url already exists in preference in any source + * This is a limitation needed for tracking.**/ + for (sourceId in 1..3) { // There's 3 sources so 3 preferences to check + val sourceSuffixID by lazy { + val key = "${"kavita_$sourceId"}/all/1" // Hardcoded versionID to 1 + val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) + (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } + .reduce(Long::or) and Long.MAX_VALUE + } + val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$sourceSuffixID", 0x0000) + } + val prefApiUrl = preferences.getString("APIURL", "")!! + + if (prefApiUrl.isNotEmpty()) { + if (prefApiUrl == getCleanedApiUrl(url)) { + if (sourceId.toString() != suffix) { + return preferences.getString(KavitaConstants.customSourceNamePref, sourceId.toString())!! + } + } + } + } + return "" + } + private fun setupLogin(addressFromPreference: String = "") { Log.v(LOG_TAG, "[Setup Login] Starting setup") - val validaddress = if (address.isEmpty()) addressFromPreference else address - val tokens = validaddress.split("/api/opds/") + val validAddress = address.ifEmpty { addressFromPreference } + val tokens = validAddress.split("/api/opds/") val apiKey = tokens[1] val baseUrlSetup = tokens[0].replace("\n", "\\n") - if (!baseUrlSetup.startsWith("http")) { - try { - throw Exception("""Url does not start with "http/s" but with ${baseUrlSetup.split("://")[0]} """) - } catch (e: Exception) { - throw Exception("""Malformed Url: $baseUrlSetup""") - } + if (baseUrlSetup.toHttpUrlOrNull() == null) { + Log.e(LOG_TAG, "Invalid URL $baseUrlSetup") + throw Exception("""Invalid URL: $baseUrlSetup""") } - preferences.edit().putString("BASEURL", baseUrlSetup).commit() - preferences.edit().putString("APIKEY", apiKey).commit() - preferences.edit().putString("APIURL", "$baseUrlSetup/api").commit() + preferences.edit().putString("BASEURL", baseUrlSetup).apply() + preferences.edit().putString("APIKEY", apiKey).apply() + preferences.edit().putString("APIURL", "$baseUrlSetup/api").apply() Log.v(LOG_TAG, "[Setup Login] Setup successful") } @@ -1020,6 +1112,8 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { setupLoginHeaders().build(), "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) ) client.newCall(request).execute().use { + val peekbody = it.peekBody(Long.MAX_VALUE).toString() + if (it.code == 200) { try { jwtToken = it.parseAs().token @@ -1029,8 +1123,13 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { throw IOException("Please check your kavita version.\nv0.5+ is required for the extension to work properly") } } else { - Log.e(LOG_TAG, "[LOGIN] login failed. Authentication was not successful -> Code: ${it.code}.Response message: ${it.message} Response body: ${it.body!!}.") - throw LoginErrorException("[LOGIN] login failed. Authentication was not successful") + if (it.code == 500) { + Log.e(LOG_TAG, "[LOGIN] login failed. There was some error -> Code: ${it.code}.Response message: ${it.message} Response body: $peekbody.") + throw LoginErrorException("[LOGIN] login failed. Something went wrong") + } else { + Log.e(LOG_TAG, "[LOGIN] login failed. Authentication was not successful -> Code: ${it.code}.Response message: ${it.message} Response body: $peekbody.") + throw LoginErrorException("[LOGIN] login failed. Something went wrong") + } } } Log.v(LOG_TAG, "[Login] Login successful") @@ -1040,69 +1139,66 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { if (apiUrl.isNotBlank()) { Single.fromCallable { // Login - var loginSuccesful = false - try { - doLogin() - loginSuccesful = true - } catch (e: LoginErrorException) { - Log.e(LOG_TAG, "Init login failed: $e") + doLogin() + try { // Get current version + val requestUrl = "$apiUrl/Server/server-info" + val serverInfoDto = client.newCall(GET(requestUrl, headersBuilder().build())) + .execute() + .parseAs() + Log.e( + LOG_TAG, + "Extension version: code=${BuildConfig.VERSION_CODE} name=${BuildConfig.VERSION_NAME}" + + " - - Kavita version: ${serverInfoDto.kavitaVersion}" + ) // this is not a real error. Using this so it gets printed in dump logs if there's any error + } catch (e: EmptyRequestBody) { + Log.e(LOG_TAG, "Extension version: code=${BuildConfig.VERSION_CODE} - name=${BuildConfig.VERSION_NAME}") + } catch (e: Exception) { + Log.e(LOG_TAG, "Tachiyomi version: code=${BuildConfig.VERSION_CODE} - name=${BuildConfig.VERSION_NAME}", e) } - if (loginSuccesful) { // doing this check to not clutter LOGS + try { // Load Filters // Genres Log.v(LOG_TAG, "[Filter] Fetching filters ") - try { - client.newCall(GET("$apiUrl/Metadata/genres", headersBuilder().build())) - .execute().use { response -> - genresListMeta = try { - val responseBody = response.body - if (responseBody != null) { - responseBody.use { json.decodeFromString(it.string()) } - } else { - Log.e( - LOG_TAG, - "[Filter] Error decoding JSON for genres filter: response body is null. Response code: ${response.code}" - ) - emptyList() - } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error decoding JSON for genres filter -> ${response.body!!}", e) + client.newCall(GET("$apiUrl/Metadata/genres", headersBuilder().build())) + .execute().use { response -> + + genresListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for genres filter: response body is null. Response code: ${response.code}" + ) emptyList() } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error decoding JSON for genres filter -> ${response.body!!}", e) + emptyList() } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading genres for filters", e) - } + } // tagsListMeta - try { - client.newCall(GET("$apiUrl/Metadata/tags", headersBuilder().build())) - .execute().use { response -> - tagsListMeta = try { - val responseBody = response.body - if (responseBody != null) { - responseBody.use { json.decodeFromString(it.string()) } - } else { - Log.e( - LOG_TAG, - "[Filter] Error decoding JSON for tagsList filter: response body is null. Response code: ${response.code}" - ) - emptyList() - } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error decoding JSON for tagsList filter", e) + client.newCall(GET("$apiUrl/Metadata/tags", headersBuilder().build())) + .execute().use { response -> + tagsListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for tagsList filter: response body is null. Response code: ${response.code}" + ) emptyList() } + } catch (e: Exception) { + Log.e(LOG_TAG, "[Filter] Error decoding JSON for tagsList filter", e) + emptyList() } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading tagsList for filters", e) - } + } // age-ratings - try { - client.newCall( - GET( - "$apiUrl/Metadata/age-ratings", - headersBuilder().build() - ) - ).execute().use { response -> + client.newCall(GET("$apiUrl/Metadata/age-ratings", headersBuilder().build())) + .execute().use { response -> ageRatingsListMeta = try { val responseBody = response.body if (responseBody != null) { @@ -1123,153 +1219,141 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { emptyList() } } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading age-ratings for age-ratings", e) - } // collectionsListMeta - try { - client.newCall(GET("$apiUrl/Collection", headersBuilder().build())) - .execute().use { response -> - collectionsListMeta = try { - val responseBody = response.body - if (responseBody != null) { - responseBody.use { json.decodeFromString(it.string()) } - } else { - Log.e( - LOG_TAG, - "[Filter] Error decoding JSON for collectionsListMeta filter: response body is null. Response code: ${response.code}" - ) - emptyList() - } - } catch (e: Exception) { + client.newCall(GET("$apiUrl/Collection", headersBuilder().build())) + .execute().use { response -> + collectionsListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { Log.e( LOG_TAG, - "[Filter] Error decoding JSON for collectionsListMeta filter", - e + "[Filter] Error decoding JSON for collectionsListMeta filter: response body is null. Response code: ${response.code}" ) emptyList() } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for collectionsListMeta filter", + e + ) + emptyList() } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading collectionsListMeta for collectionsListMeta", e) - } + } // languagesListMeta - try { - client.newCall(GET("$apiUrl/Metadata/languages", headersBuilder().build())) - .execute().use { response -> - languagesListMeta = try { - val responseBody = response.body - if (responseBody != null) { - responseBody.use { json.decodeFromString(it.string()) } - } else { - Log.e( - LOG_TAG, - "[Filter] Error decoding JSON for languagesListMeta filter: response body is null. Response code: ${response.code}" - ) - emptyList() - } - } catch (e: Exception) { + client.newCall(GET("$apiUrl/Metadata/languages", headersBuilder().build())) + .execute().use { response -> + languagesListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { Log.e( LOG_TAG, - "[Filter] Error decoding JSON for languagesListMeta filter", - e + "[Filter] Error decoding JSON for languagesListMeta filter: response body is null. Response code: ${response.code}" ) emptyList() } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for languagesListMeta filter", + e + ) + emptyList() } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading languagesListMeta for languagesListMeta", e) - } + } // libraries - try { - client.newCall(GET("$apiUrl/Library", headersBuilder().build())).execute() - .use { response -> - libraryListMeta = try { - val responseBody = response.body - if (responseBody != null) { - responseBody.use { json.decodeFromString(it.string()) } - } else { - Log.e( - LOG_TAG, - "[Filter] Error decoding JSON for libraries filter: response body is null. Response code: ${response.code}" - ) - emptyList() - } - } catch (e: Exception) { + client.newCall(GET("$apiUrl/Library", headersBuilder().build())) + .execute().use { response -> + libraryListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { Log.e( LOG_TAG, - "[Filter] Error decoding JSON for libraries filter", - e + "[Filter] Error decoding JSON for libraries filter: response body is null. Response code: ${response.code}" ) emptyList() } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "[Filter] Error decoding JSON for libraries filter", + e + ) + emptyList() } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading libraries for languagesListMeta", e) - } + } // peopleListMeta - try { - client.newCall(GET("$apiUrl/Metadata/people", headersBuilder().build())) - .execute().use { response -> - peopleListMeta = try { - val responseBody = response.body - if (responseBody != null) { - responseBody.use { json.decodeFromString(it.string()) } - } else { - Log.e( - LOG_TAG, - "error while decoding JSON for peopleListMeta filter: response body is null. Response code: ${response.code}" - ) - emptyList() - } - } catch (e: Exception) { + client.newCall(GET("$apiUrl/Metadata/people", headersBuilder().build())) + .execute().use { response -> + peopleListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { Log.e( LOG_TAG, - "error while decoding JSON for peopleListMeta filter", - e + "error while decoding JSON for peopleListMeta filter: response body is null. Response code: ${response.code}" ) emptyList() } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "error while decoding JSON for peopleListMeta filter", + e + ) + emptyList() } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading tagsList for peopleListMeta", e) - } - try { - client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build())) - .execute().use { response -> - pubStatusListMeta = try { - val responseBody = response.body - if (responseBody != null) { - responseBody.use { json.decodeFromString(it.string()) } - } else { - Log.e( - LOG_TAG, - "error while decoding JSON for publicationStatusListMeta filter: response body is null. Response code: ${response.code}" - ) - emptyList() - } - } catch (e: Exception) { + } + client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build())) + .execute().use { response -> + pubStatusListMeta = try { + val responseBody = response.body + if (responseBody != null) { + responseBody.use { json.decodeFromString(it.string()) } + } else { Log.e( LOG_TAG, - "error while decoding JSON for publicationStatusListMeta filter", - e + "error while decoding JSON for publicationStatusListMeta filter: response body is null. Response code: ${response.code}" ) emptyList() } + } catch (e: Exception) { + Log.e( + LOG_TAG, + "error while decoding JSON for publicationStatusListMeta filter", + e + ) + emptyList() } - } catch (e: Exception) { - Log.e(LOG_TAG, "[Filter] Error loading tagsList for peopleListMeta", e) - } - + } Log.v(LOG_TAG, "[Filter] Successfully loaded metadata tags from server") + } catch (e: Exception) { + throw LoadingFilterFailed("Failed Loading Filters", e.cause) } - Log.v(LOG_TAG, "Successfully ended init") } .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe( {}, { tr -> + /** + * Avoid polluting logs with traces of exception + * **/ + if (tr is EmptyRequestBody || tr is LoginErrorException) { + Log.e(LOG_TAG, "error while doing initial calls\n${tr.cause}") + return@subscribe + } + if (tr is ConnectException) { // avoid polluting logs with traces of exception + Log.e(LOG_TAG, "Error while doing initial calls\n${tr.cause}") + return@subscribe + } Log.e(LOG_TAG, "error while doing initial calls", tr) } ) diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt index e10d2087a..1e64ca053 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt @@ -98,7 +98,12 @@ data class ChapterDto( ) @Serializable -data class KavitaComicsSearch( +data class SearchResultsDto( + val series: List +) + +@Serializable +data class SeriesSearchDto( val seriesId: Int, val name: String, val originalName: String, diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt index b2b7c79fa..e291945a8 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MetadataDto.kt @@ -51,6 +51,7 @@ data class MetadataPayload( var sorting: Int = 1, var sorting_asc: Boolean = true, var readStatus: ArrayList = arrayListOf< String>(), + val readStatusList: List = listOf("notRead", "inProgress", "read"), var genres: ArrayList = arrayListOf(), var tags: ArrayList = arrayListOf(), var ageRating: ArrayList = arrayListOf(), diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt index a2120c9a7..8958eb769 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/Responses.kt @@ -16,3 +16,12 @@ data class PaginationInfo( val totalItems: Int, val totalPages: Int ) +@Serializable +data class ServerInfoDto( + val installId: String, + val os: String, + val isDocker: Boolean, + val dotnetVersion: String, + val kavitaVersion: String, + val numOfCores: Int +)