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 <vetle.ledaal@gmail.com>

* changed url query to proper HttpUrl builder

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
This commit is contained in:
ThePromidius 2022-02-23 10:46:32 +01:00 committed by GitHub
parent 79e3a20a37
commit 818bedc955
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 317 additions and 204 deletions

View File

@ -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 ## 1.2.2
### Features ### Features

View File

@ -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) 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 ## FAQ

View File

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

View File

@ -10,7 +10,6 @@ import androidx.preference.MultiSelectListPreference
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.all.kavita.dto.AuthenticationDto 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.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.MangaFormat
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataAgeRatings import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataAgeRatings
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataCollections 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.MetadataPubStatus
import eu.kanade.tachiyomi.extension.all.kavita.dto.MetadataTag 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.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.SeriesDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesMetadataDto 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.extension.all.kavita.dto.VolumeDto
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
@ -44,6 +46,8 @@ import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
@ -56,9 +60,32 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.net.ConnectException
import java.security.MessageDigest import java.security.MessageDigest
class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() { class Kavita(private val suffix: String = "") : ConfigurableSource, HttpSource() {
class CompareChapters {
companion object : Comparator<SChapter> {
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 { override val id by lazy {
val key = "${"kavita_$suffix"}/all/$versionId" val key = "${"kavita_$suffix"}/all/$versionId"
@ -75,13 +102,19 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
override val baseUrl by lazy { getPrefBaseUrl() } 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 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 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 var isLoged = false // Used to know if login was correct and not send login requests anymore
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val helper = KavitaHelper() private val helper = KavitaHelper()
private inline fun <reified T> Response.parseAs(): T = private inline fun <reified T> Response.parseAs(): T =
use { 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()) json.decodeFromString(it.body?.string().orEmpty())
} }
private inline fun <reified T : Enum<T>> safeValueOf(type: String): T { private inline fun <reified T : Enum<T>> safeValueOf(type: String): T {
@ -143,8 +176,8 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
if (filter.state != null) { if (filter.state != null) {
toFilter.sorting = filter.state!!.index + 1 toFilter.sorting = filter.state!!.index + 1
toFilter.sorting_asc = filter.state!!.ascending toFilter.sorting_asc = filter.state!!.ascending
// disabled till the search api is stable // Disabled until search is stable
// isFilterOn = true // isFilterOn = false
} }
} }
is StatusFilterGroup -> { is StatusFilterGroup -> {
@ -165,6 +198,7 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
} }
is UserRating -> { is UserRating -> {
toFilter.userRating = filter.state toFilter.userRating = filter.state
isFilterOn = true
} }
is TagFilterGroup -> { is TagFilterGroup -> {
filter.state.forEach { content -> 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) return popularMangaRequest(page)
} else { } 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")) if (response.request.url.toString().contains("api/series/all"))
return popularMangaParse(response) return popularMangaParse(response)
val result = response.parseAs<List<KavitaComicsSearch>>() val result = response.parseAs<SearchResultsDto>().series
val mangaList = result.map(::searchMangaFromObject) val mangaList = result.map(::searchMangaFromObject)
return MangasPage(mangaList, false) 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 title = obj.name
thumbnail_url = "$apiUrl/Image/series-cover?seriesId=${obj.seriesId}" thumbnail_url = "$apiUrl/Image/series-cover?seriesId=${obj.seriesId}"
description = "None" description = "None"
@ -363,23 +402,25 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
val result = response.parseAs<SeriesMetadataDto>() val result = response.parseAs<SeriesMetadataDto>()
val existingSeries = series.find { dto -> dto.id == result.seriesId } val existingSeries = series.find { dto -> dto.id == result.seriesId }
Log.d("[Kavita]", "old manga url:")
if (existingSeries != null) { if (existingSeries != null) {
val manga = helper.createSeriesDto(existingSeries, apiUrl) val manga = helper.createSeriesDto(existingSeries, apiUrl)
manga.url = "$apiUrl/Series/${result.seriesId}"
manga.artist = result.coverArtists.joinToString { it.name } manga.artist = result.coverArtists.joinToString { it.name }
manga.description = result.summary manga.description = result.summary
manga.author = result.writers.joinToString { it.name } manga.author = result.writers.joinToString { it.name }
manga.genre = result.genres.joinToString { it.title } manga.genre = result.genres.joinToString { it.title }
manga.thumbnail_url = "$apiUrl/image/series-cover?seriesId=${result.seriesId}"
return manga return manga
} }
return SManga.create().apply { return SManga.create().apply {
url = "$apiUrl/Series/${result.seriesId}" url = "$apiUrl/Series/${result.seriesId}"
artist = result.coverArtists.joinToString { ", " } artist = result.coverArtists.joinToString { it.name }
author = result.writers.joinToString { ", " } description = result.summary
genre = result.genres.joinToString { ", " } author = result.writers.joinToString { it.name }
thumbnail_url = "$apiUrl/image/series-cover?seriesId=${result.seriesId}" 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 { private fun chapterFromObject(obj: ChapterDto): SChapter = SChapter.create().apply {
url = obj.id.toString() url = obj.id.toString()
if (obj.number == "0" && obj.isSpecial) { if (obj.number == "0" && obj.isSpecial) {
// This is a special. Chapter name is special name
name = obj.range name = obj.range
} else { } else {
val cleanedName = obj.title.replaceFirst("^0+(?!$)".toRegex(), "") val cleanedName = obj.title.replaceFirst("^0+(?!$)".toRegex(), "")
@ -401,7 +443,7 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
} }
date_upload = helper.parseDate(obj.created) date_upload = helper.parseDate(obj.created)
chapter_number = obj.number.toFloat() chapter_number = obj.number.toFloat()
scanlator = obj.pages.toString() scanlator = "${obj.pages} pages"
} }
private fun chapterFromVolume(obj: ChapterDto, volume: VolumeDto): SChapter = 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 there are multiple chapters to this volume, then prefix with Volume number
if (volume.chapters.isNotEmpty() && obj.number != "0") { if (volume.chapters.isNotEmpty() && obj.number != "0") {
name = "Volume ${volume.number} Chapter ${obj.number}" name = "Volume ${volume.number} Chapter ${obj.number}"
chapter_number = obj.number.toFloat()
} else if (obj.number == "0") { } else if (obj.number == "0") {
// This chapter is solely on volume // This chapter is solely on volume
if (volume.number == 0) { if (volume.number == 0) {
// Treat as special // Treat as special
if (obj.range == "") { if (obj.range == "") {
name = "Chapter 0" name = "Chapter 0"
chapter_number = obj.number.toFloat()
} else { } else {
name = obj.range name = obj.range
chapter_number = obj.number.toFloat()
} }
} else { } else {
name = "Volume ${volume.number}" 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 { } else {
name = "Unhandled Else Volume ${volume.number}" name = "Unhandled Else Volume ${volume.number}"
} }
url = obj.id.toString() url = obj.id.toString()
date_upload = helper.parseDate(obj.created) date_upload = helper.parseDate(obj.created)
chapter_number = obj.number.toFloat()
scanlator = "${obj.pages}" scanlator = "${obj.pages} pages"
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
try { try {
@ -448,7 +496,8 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
} }
} }
} }
allChapterList.reverse()
allChapterList.sortWith(CompareChapters)
return allChapterList return allChapterList
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "Unhandled exception parsing chapters. Send logs to kavita devs", e) 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<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val chapterId = chapter.url val chapterId = chapter.url
val numPages = chapter.scanlator?.toInt() val numPages = chapter.scanlator?.replace(" pages", "")?.toInt()
val numPages2 = "$numPages".toInt() - 1 val numPages2 = "$numPages".toInt() - 1
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
for (i in 0..numPages2) { 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) { class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
constructor(cause: Throwable) : this(null, 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 { 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() return Headers.Builder()
.add("User-Agent", "Tachiyomi Kavita v${BuildConfig.VERSION_NAME}") .add("User-Agent", "Tachiyomi Kavita v${BuildConfig.VERSION_NAME}")
.add("Content-Type", "application/json") .add("Content-Type", "application/json")
@ -833,13 +892,8 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
"readStatus", "readStatus",
buildJsonObject { buildJsonObject {
if (filter.readStatus.isNotEmpty()) { if (filter.readStatus.isNotEmpty()) {
filter.readStatus.forEach { status -> filter.readStatusList
if (status in listOf("notRead", "inProgress", "read")) { .forEach { status -> put(status, JsonPrimitive(status in filter.readStatus)) }
put(status, JsonPrimitive(true))
} else {
put(status, JsonPrimitive(false))
}
}
} else { } else {
put("notRead", JsonPrimitive(true)) put("notRead", JsonPrimitive(true))
put("inProgress", 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." "The OPDS url copied from User Settings. This should include address and the api key on end."
) )
val enabledFiltersPref = MultiSelectListPreference(screen.context).apply { val enabledFiltersPref = MultiSelectListPreference(screen.context).apply {
key = KavitaConstants.toggledFiltersPref key = KavitaConstants.toggledFiltersPref
title = "Default filters shown" title = "Default filters shown"
@ -915,7 +968,6 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
res res
} }
} }
screen.addPreference(customSourceNamePref) screen.addPreference(customSourceNamePref)
screen.addPreference(opdsAddressPref) screen.addPreference(opdsAddressPref)
screen.addPreference(enabledFiltersPref) screen.addPreference(enabledFiltersPref)
@ -944,6 +996,19 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
} }
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
try { 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() val res = preferences.edit().putString(title, newValue as String).commit()
Toast.makeText( Toast.makeText(
context, context,
@ -953,15 +1018,17 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
setupLogin(newValue) setupLogin(newValue)
Log.v(LOG_TAG, "[Preferences] Successfully modified OPDS URL") Log.v(LOG_TAG, "[Preferences] Successfully modified OPDS URL")
res res
} catch (e: OpdsurlExistsInPref) {
Log.e(LOG_TAG, "Url exists in a different sourcce")
false
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(LOG_TAG, "Unrecognised error", e)
false false
} }
} }
} }
} }
// private fun getPrefapiKey(): String = preferences.getString("APIKEY", "")!!
private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!! private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!!
private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!! private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!!
private fun getPrefKey(key: String): String = preferences.getString(key, "")!! private fun getPrefKey(key: String): String = preferences.getString(key, "")!!
@ -984,23 +1051,48 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
/** /**
* LOGIN * 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<Application>().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 = "") { private fun setupLogin(addressFromPreference: String = "") {
Log.v(LOG_TAG, "[Setup Login] Starting setup") Log.v(LOG_TAG, "[Setup Login] Starting setup")
val validaddress = if (address.isEmpty()) addressFromPreference else address val validAddress = address.ifEmpty { addressFromPreference }
val tokens = validaddress.split("/api/opds/") val tokens = validAddress.split("/api/opds/")
val apiKey = tokens[1] val apiKey = tokens[1]
val baseUrlSetup = tokens[0].replace("\n", "\\n") val baseUrlSetup = tokens[0].replace("\n", "\\n")
if (!baseUrlSetup.startsWith("http")) { if (baseUrlSetup.toHttpUrlOrNull() == null) {
try { Log.e(LOG_TAG, "Invalid URL $baseUrlSetup")
throw Exception("""Url does not start with "http/s" but with ${baseUrlSetup.split("://")[0]} """) throw Exception("""Invalid URL: $baseUrlSetup""")
} catch (e: Exception) {
throw Exception("""Malformed Url: $baseUrlSetup""")
}
} }
preferences.edit().putString("BASEURL", baseUrlSetup).commit() preferences.edit().putString("BASEURL", baseUrlSetup).apply()
preferences.edit().putString("APIKEY", apiKey).commit() preferences.edit().putString("APIKEY", apiKey).apply()
preferences.edit().putString("APIURL", "$baseUrlSetup/api").commit() preferences.edit().putString("APIURL", "$baseUrlSetup/api").apply()
Log.v(LOG_TAG, "[Setup Login] Setup successful") 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()) setupLoginHeaders().build(), "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
) )
client.newCall(request).execute().use { client.newCall(request).execute().use {
val peekbody = it.peekBody(Long.MAX_VALUE).toString()
if (it.code == 200) { if (it.code == 200) {
try { try {
jwtToken = it.parseAs<AuthenticationDto>().token jwtToken = it.parseAs<AuthenticationDto>().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") throw IOException("Please check your kavita version.\nv0.5+ is required for the extension to work properly")
} }
} else { } else {
Log.e(LOG_TAG, "[LOGIN] login failed. Authentication was not successful -> Code: ${it.code}.Response message: ${it.message} Response body: ${it.body!!}.") if (it.code == 500) {
throw LoginErrorException("[LOGIN] login failed. Authentication was not successful") 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") Log.v(LOG_TAG, "[Login] Login successful")
@ -1040,69 +1139,66 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
if (apiUrl.isNotBlank()) { if (apiUrl.isNotBlank()) {
Single.fromCallable { Single.fromCallable {
// Login // Login
var loginSuccesful = false doLogin()
try { try { // Get current version
doLogin() val requestUrl = "$apiUrl/Server/server-info"
loginSuccesful = true val serverInfoDto = client.newCall(GET(requestUrl, headersBuilder().build()))
} catch (e: LoginErrorException) { .execute()
Log.e(LOG_TAG, "Init login failed: $e") .parseAs<ServerInfoDto>()
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 // Genres
Log.v(LOG_TAG, "[Filter] Fetching filters ") Log.v(LOG_TAG, "[Filter] Fetching filters ")
try { client.newCall(GET("$apiUrl/Metadata/genres", headersBuilder().build()))
client.newCall(GET("$apiUrl/Metadata/genres", headersBuilder().build())) .execute().use { response ->
.execute().use { response ->
genresListMeta = try { genresListMeta = try {
val responseBody = response.body val responseBody = response.body
if (responseBody != null) { if (responseBody != null) {
responseBody.use { json.decodeFromString(it.string()) } responseBody.use { json.decodeFromString(it.string()) }
} else { } else {
Log.e( Log.e(
LOG_TAG, LOG_TAG,
"[Filter] Error decoding JSON for genres filter: response body is null. Response code: ${response.code}" "[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() 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 // tagsListMeta
try { client.newCall(GET("$apiUrl/Metadata/tags", headersBuilder().build()))
client.newCall(GET("$apiUrl/Metadata/tags", headersBuilder().build())) .execute().use { response ->
.execute().use { response -> tagsListMeta = try {
tagsListMeta = try { val responseBody = response.body
val responseBody = response.body if (responseBody != null) {
if (responseBody != null) { responseBody.use { json.decodeFromString(it.string()) }
responseBody.use { json.decodeFromString(it.string()) } } else {
} else { Log.e(
Log.e( LOG_TAG,
LOG_TAG, "[Filter] Error decoding JSON for tagsList filter: response body is null. Response code: ${response.code}"
"[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() 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 // age-ratings
try { client.newCall(GET("$apiUrl/Metadata/age-ratings", headersBuilder().build()))
client.newCall( .execute().use { response ->
GET(
"$apiUrl/Metadata/age-ratings",
headersBuilder().build()
)
).execute().use { response ->
ageRatingsListMeta = try { ageRatingsListMeta = try {
val responseBody = response.body val responseBody = response.body
if (responseBody != null) { if (responseBody != null) {
@ -1123,153 +1219,141 @@ class Kavita(suffix: String = "") : ConfigurableSource, HttpSource() {
emptyList() emptyList()
} }
} }
} catch (e: Exception) {
Log.e(LOG_TAG, "[Filter] Error loading age-ratings for age-ratings", e)
}
// collectionsListMeta // collectionsListMeta
try { client.newCall(GET("$apiUrl/Collection", headersBuilder().build()))
client.newCall(GET("$apiUrl/Collection", headersBuilder().build())) .execute().use { response ->
.execute().use { response -> collectionsListMeta = try {
collectionsListMeta = try { val responseBody = response.body
val responseBody = response.body if (responseBody != null) {
if (responseBody != null) { responseBody.use { json.decodeFromString(it.string()) }
responseBody.use { json.decodeFromString(it.string()) } } else {
} else {
Log.e(
LOG_TAG,
"[Filter] Error decoding JSON for collectionsListMeta filter: response body is null. Response code: ${response.code}"
)
emptyList()
}
} catch (e: Exception) {
Log.e( Log.e(
LOG_TAG, LOG_TAG,
"[Filter] Error decoding JSON for collectionsListMeta filter", "[Filter] Error decoding JSON for collectionsListMeta filter: response body is null. Response code: ${response.code}"
e
) )
emptyList() 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 // languagesListMeta
try { client.newCall(GET("$apiUrl/Metadata/languages", headersBuilder().build()))
client.newCall(GET("$apiUrl/Metadata/languages", headersBuilder().build())) .execute().use { response ->
.execute().use { response -> languagesListMeta = try {
languagesListMeta = try { val responseBody = response.body
val responseBody = response.body if (responseBody != null) {
if (responseBody != null) { responseBody.use { json.decodeFromString(it.string()) }
responseBody.use { json.decodeFromString(it.string()) } } else {
} else {
Log.e(
LOG_TAG,
"[Filter] Error decoding JSON for languagesListMeta filter: response body is null. Response code: ${response.code}"
)
emptyList()
}
} catch (e: Exception) {
Log.e( Log.e(
LOG_TAG, LOG_TAG,
"[Filter] Error decoding JSON for languagesListMeta filter", "[Filter] Error decoding JSON for languagesListMeta filter: response body is null. Response code: ${response.code}"
e
) )
emptyList() 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 // libraries
try { client.newCall(GET("$apiUrl/Library", headersBuilder().build()))
client.newCall(GET("$apiUrl/Library", headersBuilder().build())).execute() .execute().use { response ->
.use { response -> libraryListMeta = try {
libraryListMeta = try { val responseBody = response.body
val responseBody = response.body if (responseBody != null) {
if (responseBody != null) { responseBody.use { json.decodeFromString(it.string()) }
responseBody.use { json.decodeFromString(it.string()) } } else {
} else {
Log.e(
LOG_TAG,
"[Filter] Error decoding JSON for libraries filter: response body is null. Response code: ${response.code}"
)
emptyList()
}
} catch (e: Exception) {
Log.e( Log.e(
LOG_TAG, LOG_TAG,
"[Filter] Error decoding JSON for libraries filter", "[Filter] Error decoding JSON for libraries filter: response body is null. Response code: ${response.code}"
e
) )
emptyList() 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 // peopleListMeta
try { client.newCall(GET("$apiUrl/Metadata/people", headersBuilder().build()))
client.newCall(GET("$apiUrl/Metadata/people", headersBuilder().build())) .execute().use { response ->
.execute().use { response -> peopleListMeta = try {
peopleListMeta = try { val responseBody = response.body
val responseBody = response.body if (responseBody != null) {
if (responseBody != null) { responseBody.use { json.decodeFromString(it.string()) }
responseBody.use { json.decodeFromString(it.string()) } } else {
} else {
Log.e(
LOG_TAG,
"error while decoding JSON for peopleListMeta filter: response body is null. Response code: ${response.code}"
)
emptyList()
}
} catch (e: Exception) {
Log.e( Log.e(
LOG_TAG, LOG_TAG,
"error while decoding JSON for peopleListMeta filter", "error while decoding JSON for peopleListMeta filter: response body is null. Response code: ${response.code}"
e
) )
emptyList() 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) client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build()))
} .execute().use { response ->
try { pubStatusListMeta = try {
client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build())) val responseBody = response.body
.execute().use { response -> if (responseBody != null) {
pubStatusListMeta = try { responseBody.use { json.decodeFromString(it.string()) }
val responseBody = response.body } else {
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) {
Log.e( Log.e(
LOG_TAG, LOG_TAG,
"error while decoding JSON for publicationStatusListMeta filter", "error while decoding JSON for publicationStatusListMeta filter: response body is null. Response code: ${response.code}"
e
) )
emptyList() 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") 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()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.subscribe( .subscribe(
{}, {},
{ tr -> { 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) Log.e(LOG_TAG, "error while doing initial calls", tr)
} }
) )

View File

@ -98,7 +98,12 @@ data class ChapterDto(
) )
@Serializable @Serializable
data class KavitaComicsSearch( data class SearchResultsDto(
val series: List<SeriesSearchDto>
)
@Serializable
data class SeriesSearchDto(
val seriesId: Int, val seriesId: Int,
val name: String, val name: String,
val originalName: String, val originalName: String,

View File

@ -51,6 +51,7 @@ data class MetadataPayload(
var sorting: Int = 1, var sorting: Int = 1,
var sorting_asc: Boolean = true, var sorting_asc: Boolean = true,
var readStatus: ArrayList<String> = arrayListOf< String>(), var readStatus: ArrayList<String> = arrayListOf< String>(),
val readStatusList: List<String> = listOf("notRead", "inProgress", "read"),
var genres: ArrayList<Int> = arrayListOf<Int>(), var genres: ArrayList<Int> = arrayListOf<Int>(),
var tags: ArrayList<Int> = arrayListOf<Int>(), var tags: ArrayList<Int> = arrayListOf<Int>(),
var ageRating: ArrayList<Int> = arrayListOf<Int>(), var ageRating: ArrayList<Int> = arrayListOf<Int>(),

View File

@ -16,3 +16,12 @@ data class PaginationInfo(
val totalItems: Int, val totalItems: Int,
val totalPages: 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
)