diff --git a/src/all/kavita/build.gradle b/src/all/kavita/build.gradle index 67d73428..f18f2af9 100644 --- a/src/all/kavita/build.gradle +++ b/src/all/kavita/build.gradle @@ -2,7 +2,7 @@ ext { kmkVersionCode = 1 extName = 'Kavita' extClass = '.KavitaFactory' - extVersionCode = 25 + extVersionCode = 26 isNsfw = false } 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 2cb1d1c7..26a940c0 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 @@ -980,9 +980,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou title = (if (readingList.promoted) "🔺 " else "") + readingList.title artist = "${readingList.itemCount} items" thumbnail_url = if (!readingList.coverImage.isNullOrBlank()) { - "$apiUrl/image/${readingList.coverImage}?apiKey=$apiKey" + KavitaImageUrls.imagePath(apiUrl, readingList.coverImage, apiKey) } else { - "$apiUrl/image/readinglist-cover?readingListId=$readingListId&apiKey=$apiKey" + KavitaImageUrls.cover(apiUrl, "readinglist-cover", "readingListId", readingListId ?: 0, apiKey) } description = readingList.summary ?: "Reading List" genre = genreString @@ -1110,7 +1110,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou if (isComicLibrary && volume.minNumber.toInt() == KavitaConstants.UNNUMBERED_VOLUME) { for (chapter in volume.chapters) { if (chapter.coverImage.isNotBlank()) { - val url = "$apiUrl/Image/chapter-cover?chapterId=${chapter.id}&apiKey=$apiKey" + val url = KavitaImageUrls.cover(apiUrl, "chapter-cover", "chapterId", chapter.id, apiKey) coverCandidates.add( Triple(url, chapter.pagesRead < chapter.pages, chapter.number.toFloatOrNull() ?: 0f), ) @@ -1124,7 +1124,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou ChapterType.of(chapter, volume) == ChapterType.SingleFileVolume } if (hasSingleFile && volume.coverImage.isNotBlank()) { - val url = "$apiUrl/Image/volume-cover?volumeId=${volume.id}&apiKey=$apiKey" + val url = KavitaImageUrls.cover(apiUrl, "volume-cover", "volumeId", volume.id, apiKey) val isUnread = volume.pagesRead < volume.pages val number = volume.minNumber.toFloat() coverCandidates.add(Triple(url, isUnread, number)) @@ -1155,14 +1155,14 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } ?: System.currentTimeMillis().toString() // Append cache-busting timestamp to always get the latest cover - "$baseUrl&ts=$timestamp" - } ?: "$apiUrl/image/series-cover?seriesId=$seriesId&apiKey=$apiKey" + KavitaImageUrls.withTimestamp(baseUrl, timestamp) + } ?: KavitaImageUrls.cover(apiUrl, "series-cover", "seriesId", seriesId, apiKey) } catch (e: Exception) { Log.e(LOG_TAG, "Error fetching volumes for cover selection", e) - "$apiUrl/image/series-cover?seriesId=${result.seriesId ?: 0}&apiKey=$apiKey" + KavitaImageUrls.cover(apiUrl, "series-cover", "seriesId", result.seriesId ?: 0, apiKey) } } else { - "$apiUrl/image/series-cover?seriesId=${result.seriesId ?: 0}&apiKey=$apiKey" + KavitaImageUrls.cover(apiUrl, "series-cover", "seriesId", result.seriesId ?: 0, apiKey) } manga.status = when (result.publicationStatus) { @@ -1252,9 +1252,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou artist = "${list.itemCount} items" author = list.ownerUserName thumbnail_url = if (!list.coverImage.isNullOrBlank()) { - "$apiUrl/image/${list.coverImage}?apiKey=$apiKey" + KavitaImageUrls.imagePath(apiUrl, list.coverImage, apiKey) } else { - "$apiUrl/Image/readinglist-cover?readingListId=${list.id}&apiKey=$apiKey" + KavitaImageUrls.cover(apiUrl, "readinglist-cover", "readingListId", list.id, apiKey) } description = list.summary ?: "Reading List" url = "$baseUrl/ReadingList/items?readingListId=${list.id}&source=readinglist" diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt index ccd71a64..8cf3fcc4 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt @@ -73,7 +73,7 @@ class KavitaHelper { SManga.create().apply { url = "$baseUrl/Series/${obj.id}" title = obj.name - thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey" + thumbnail_url = KavitaImageUrls.cover(baseUrl, "series-cover", "seriesId", obj.id, apiKey) } fun chapterFromVolume( diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaImageUrls.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaImageUrls.kt new file mode 100644 index 00000000..eded43f7 --- /dev/null +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaImageUrls.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.extension.all.kavita + +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.net.URLEncoder + +internal object KavitaImageUrls { + fun cover( + apiUrl: String, + endpoint: String, + idParam: String, + id: Int, + apiKey: String, + timestamp: String? = null, + ): String { + val builder = "$apiUrl/image/$endpoint".toHttpUrlOrNull()?.newBuilder() + ?.addQueryParameter(idParam, id.toString()) + ?.addQueryParameter("apiKey", apiKey) + + if (!timestamp.isNullOrBlank()) { + builder?.addQueryParameter("ts", timestamp) + } + + return builder?.build()?.toString() + ?: "$apiUrl/image/$endpoint?$idParam=$id&apiKey=${encodeQuery(apiKey)}${timestampQuery(timestamp)}" + } + + fun imagePath(apiUrl: String, path: String, apiKey: String): String { + val separator = if (path.startsWith("/")) "" else "/" + val url = "$apiUrl/image$separator$path" + return url.toHttpUrlOrNull()?.newBuilder() + ?.addQueryParameter("apiKey", apiKey) + ?.build() + ?.toString() + ?: "$url?apiKey=${encodeQuery(apiKey)}" + } + + fun withTimestamp(url: String, timestamp: String): String = + url.toHttpUrlOrNull()?.newBuilder() + ?.addQueryParameter("ts", timestamp) + ?.build() + ?.toString() + ?: "$url${if (url.contains("?")) "&" else "?"}ts=${encodeQuery(timestamp)}" + + private fun timestampQuery(timestamp: String?): String = + if (timestamp.isNullOrBlank()) "" else "&ts=${encodeQuery(timestamp)}" + + private fun encodeQuery(value: String): String = + URLEncoder.encode(value, "UTF-8").replace("+", "%20") +} 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 16ebcdb4..e374ad86 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 @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.extension.all.kavita.dto import eu.kanade.tachiyomi.extension.all.kavita.KavitaConstants +import eu.kanade.tachiyomi.extension.all.kavita.KavitaImageUrls import eu.kanade.tachiyomi.source.model.SManga import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -169,7 +170,7 @@ data class RelatedSeriesItem( url = "$baseUrl/Series/$id" thumbnail_url = when { !coverImage.isNullOrBlank() && (coverImage.startsWith("http://") || coverImage.startsWith("https://")) -> coverImage - else -> "$apiUrl/image/series-cover?seriesId=$id&apiKey=$apiKey" + else -> KavitaImageUrls.cover(apiUrl, "series-cover", "seriesId", id, apiKey) } initialized = true }