From da208e322f2c19fa7d5d589eebd82ddfadce3e44 Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 16:30:32 -0500 Subject: [PATCH 1/8] feat: init Comicvine cache --- .../komf/providers/MetadataProvidersConfig.kt | 1 + .../snd/komf/providers/ProvidersModule.kt | 10 ++- .../providers/comicvine/ComicVineCache.kt | 89 +++++++++++++++++++ .../providers/comicvine/ComicVineClient.kt | 78 +++++++++++++--- 4 files changed, 164 insertions(+), 14 deletions(-) create mode 100644 komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt index 7c229e9a..6388ff5b 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt @@ -25,6 +25,7 @@ data class MetadataProvidersConfig( val defaultProviders: ProvidersConfig = ProvidersConfig(), val libraryProviders: Map = emptyMap(), val mangabakaDatabaseDir: String = "./mangabaka", + val cacheDatabaseFile: String = "./cv_cache.db", ) @Serializable diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt index a92e54d4..87714464 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt @@ -108,6 +108,7 @@ class ProvidersModule( comicVineIssueName = config.comicVineIssueName, comicVineIdFormat = config.comicVineIdFormat, bangumiToken = config.bangumiToken, + cacheDatabaseFile = config.cacheDatabaseFile, ) val libraryProviders = config.libraryProviders .map { (libraryId, libraryConfig) -> @@ -120,6 +121,7 @@ class ProvidersModule( comicVineIssueName = config.comicVineIssueName, comicVineIdFormat = config.comicVineIdFormat, bangumiToken = config.bangumiToken, + cacheDatabaseFile = config.cacheDatabaseFile, ) } .toMap() @@ -333,6 +335,7 @@ class ProvidersModule( comicVineIssueName: String?, comicVineIdFormat: String?, bangumiToken: String?, + cacheDatabaseFile: String, ): MetadataProvidersContainer { return MetadataProvidersContainer( mangaupdates = createMangaUpdatesMetadataProvider( @@ -403,6 +406,7 @@ class ProvidersModule( comicVineIdFormat = comicVineIdFormat, rateLimiter = comicVineRateLimiter, defaultNameMatcher = defaultNameMatcher, + cacheDatabaseFile = cacheDatabaseFile, ), comicVinePriority = config.comicVine.priority, hentag = createHentagMetadataProvider( @@ -706,6 +710,7 @@ class ProvidersModule( comicVineIdFormat: String?, rateLimiter: ComicVineRateLimiter, defaultNameMatcher: NameSimilarityMatcher, + cacheDatabaseFile: String, ): ComicVineMetadataProvider? { if (config.enabled.not()) return null requireNotNull(apiKey) { "Api key is not configured for ComicVine provider" } @@ -719,7 +724,8 @@ class ProvidersModule( }, apiKey = apiKey, comicVineSearchLimit = comicVineSearchLimit, - rateLimiter = rateLimiter + rateLimiter = rateLimiter, + cacheDatabaseFile = cacheDatabaseFile ) val metadataMapper = ComicVineMetadataMapper( seriesMetadataConfig = config.seriesMetadata, @@ -908,4 +914,4 @@ class ProvidersModule( } -} \ No newline at end of file +} diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt new file mode 100644 index 00000000..7c62f7e8 --- /dev/null +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt @@ -0,0 +1,89 @@ +package snd.komf.providers.comicvine + +import org.jetbrains.exposed.v1.jdbc.Database +import org.jetbrains.exposed.v1.jdbc.SchemaUtils +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.greater +import org.jetbrains.exposed.v1.datetime.* +import org.jetbrains.exposed.v1.jdbc.select +import org.jetbrains.exposed.v1.jdbc.upsert +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import java.nio.file.Path +import java.io.File +import java.time.temporal.ChronoUnit +import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.plus +import kotlinx.datetime.DateTimeUnit + +object CacheTable : Table("cache") { + val queryCol = text("query") + override val primaryKey = PrimaryKey(queryCol) + + val timestampCol = timestamp("timestamp") + + val responseCol = text("response") +} + +class ComicVineCache( + private val databaseFile: String, + private val expiry: Int = 30, +) { + private val databasePath = Path.of(databaseFile) + private val database = Database.connect("jdbc:sqlite:$databasePath", driver = "org.sqlite.JDBC") + + init { + transaction(db = database) { + SchemaUtils.create(CacheTable) + } + } + + private fun getExpiryTimestamp(): Instant { + return Clock.System.now() + .toLocalDateTime(TimeZone.UTC) + .toInstant(TimeZone.UTC) + .plus(value = expiry * 24, DateTimeUnit.HOUR) + } + + private fun getNowTimestamp(): Instant { + return Clock.System.now() + .toLocalDateTime(TimeZone.UTC) + .toInstant(TimeZone.UTC) + } + + private fun maskApiKey(url: String): String { + return url.replace( + Regex("""api_key=[^&]+"""), + "api_key=*****" + ) + } + + fun addEntry(url: String, response: String) { + transaction(db = database) { + CacheTable.upsert { + it[queryCol] = maskApiKey(url) + it[responseCol] = response + it[timestampCol] = getExpiryTimestamp() + } + } + } + + suspend fun getEntry(url: String): String? { + return transaction(db = database) { + CacheTable + .select(CacheTable.responseCol).where { + (CacheTable.queryCol eq maskApiKey(url)) and + @OptIn(kotlin.time.ExperimentalTime::class) + (CacheTable.timestampCol greater getNowTimestamp()) + } + .firstOrNull() + ?.get(CacheTable.responseCol) + } + } +} diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt index 27ea8833..4130d0e4 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt @@ -3,6 +3,12 @@ package snd.komf.providers.comicvine import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* +import io.ktor.http.URLBuilder +import io.ktor.http.ParametersBuilder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import kotlin.text.Regex +import kotlinx.serialization.json.Json import snd.komf.model.Image import snd.komf.providers.comicvine.ComicVineClient.ComicVineTypeId.ISSUE import snd.komf.providers.comicvine.ComicVineClient.ComicVineTypeId.VOLUME @@ -22,7 +28,26 @@ class ComicVineClient( private val apiKey: String, private val comicVineSearchLimit: Int? = 10, private val rateLimiter: ComicVineRateLimiter, + private val cacheDatabaseFile: String, ) { + val cache = ComicVineCache(cacheDatabaseFile) + + fun buildUrlString( + url: String, + ): String { + val params = sortedMapOf( + Pair("api_key", apiKey), + Pair("format", "json"), + ) + + val encodedParams = params.entries.joinToString("&") { (key, value) -> + val k = URLEncoder.encode(key, StandardCharsets.UTF_8) + val v = URLEncoder.encode(value, StandardCharsets.UTF_8) + "$k=$v" + } + + return "$url?$encodedParams" + } suspend fun searchVolume(name: String): ComicVineSearchResult> { rateLimiter.searchAcquire() @@ -37,27 +62,56 @@ class ComicVineClient( suspend fun getVolume(id: ComicVineVolumeId): ComicVineSearchResult { rateLimiter.volumeAcquire() - return ktor.get("$baseUrl/volume/${VOLUME.id}-${id.value}/") { - parameter("format", "json") - parameter("api_key", apiKey) - }.body() + + val url = buildUrlString("$baseUrl/volume/${VOLUME.id}-${id.value}/") + + val cachedResult = cache.getEntry(url) + + if (cachedResult != null) { + return Json.decodeFromString(cachedResult); + } + + val response: ComicVineSearchResult = ktor.get(url).body() + + cache.addEntry(url, Json.encodeToString(response)) + + return response } suspend fun getIssue(id: ComicVineIssueId): ComicVineSearchResult { rateLimiter.issueAcquire() - return ktor.get("$baseUrl/issue/${ISSUE.id}-${id.value}/") { - parameter("format", "json") - parameter("api_key", apiKey) - }.body() + + val url = buildUrlString("$baseUrl/issue/${ISSUE.id}-${id.value}/") + + val cachedResult = cache.getEntry(url) + + if (cachedResult != null) { + return Json.decodeFromString(cachedResult); + } + + val response: ComicVineSearchResult = ktor.get(url).body() + + cache.addEntry(url, Json.encodeToString(response)) + + return response } suspend fun getStoryArc(id: ComicVineStoryArcId): ComicVineSearchResult { rateLimiter.storyArcAcquire() - return ktor.get("$baseUrl/story_arc/${ComicVineTypeId.STORY_ARC.id}-${id.value}/") { - parameter("format", "json") - parameter("api_key", apiKey) - }.body() + val url = buildUrlString("$baseUrl/story_arc/${ComicVineTypeId.STORY_ARC.id}-${id.value}/") + + val cachedResult = cache.getEntry(url) + + if (cachedResult != null) { + return Json.decodeFromString(cachedResult); + } + + val response: ComicVineSearchResult = ktor.get(url).body() + + cache.addEntry(url, Json.encodeToString(response)) + + return response } suspend fun getCover(url: String): Image { From 56c24c25e48319344e8a92586c51dcbcedb5a7f1 Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 16:37:15 -0500 Subject: [PATCH 2/8] chore: remove unneeded OptIn --- .../kotlin/snd/komf/providers/comicvine/ComicVineCache.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt index 7c62f7e8..aa1b94a2 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt @@ -79,7 +79,6 @@ class ComicVineCache( CacheTable .select(CacheTable.responseCol).where { (CacheTable.queryCol eq maskApiKey(url)) and - @OptIn(kotlin.time.ExperimentalTime::class) (CacheTable.timestampCol greater getNowTimestamp()) } .firstOrNull() From 87bb6f96aee271b386e87e5b31dcec8ca300dd2d Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 17:03:59 -0500 Subject: [PATCH 3/8] refactor: clean up ComicVineClient code --- .../providers/comicvine/ComicVineClient.kt | 65 ++++++------------- 1 file changed, 21 insertions(+), 44 deletions(-) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt index 4130d0e4..81baa58d 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt @@ -30,9 +30,9 @@ class ComicVineClient( private val rateLimiter: ComicVineRateLimiter, private val cacheDatabaseFile: String, ) { - val cache = ComicVineCache(cacheDatabaseFile) + private val cache = ComicVineCache(cacheDatabaseFile) - fun buildUrlString( + private fun buildUrlString( url: String, ): String { val params = sortedMapOf( @@ -49,6 +49,22 @@ class ComicVineClient( return "$url?$encodedParams" } + private suspend fun getCachedApi(url: String): ComicVineSearchResult { + val fullUrl = buildUrlString(url) + + val cachedResult = cache.getEntry(fullUrl) + + if (cachedResult != null) { + return Json.decodeFromString(cachedResult); + } + + val response: ComicVineSearchResult = ktor.get(fullUrl).body() + + cache.addEntry(fullUrl, Json.encodeToString(response)) + + return response + } + suspend fun searchVolume(name: String): ComicVineSearchResult> { rateLimiter.searchAcquire() return ktor.get("$baseUrl/search/") { @@ -62,56 +78,17 @@ class ComicVineClient( suspend fun getVolume(id: ComicVineVolumeId): ComicVineSearchResult { rateLimiter.volumeAcquire() - - val url = buildUrlString("$baseUrl/volume/${VOLUME.id}-${id.value}/") - - val cachedResult = cache.getEntry(url) - - if (cachedResult != null) { - return Json.decodeFromString(cachedResult); - } - - val response: ComicVineSearchResult = ktor.get(url).body() - - cache.addEntry(url, Json.encodeToString(response)) - - return response + return getCachedApi("$baseUrl/volume/${VOLUME.id}-${id.value}/") } suspend fun getIssue(id: ComicVineIssueId): ComicVineSearchResult { rateLimiter.issueAcquire() - - val url = buildUrlString("$baseUrl/issue/${ISSUE.id}-${id.value}/") - - val cachedResult = cache.getEntry(url) - - if (cachedResult != null) { - return Json.decodeFromString(cachedResult); - } - - val response: ComicVineSearchResult = ktor.get(url).body() - - cache.addEntry(url, Json.encodeToString(response)) - - return response + return getCachedApi("$baseUrl/issue/${ISSUE.id}-${id.value}/") } suspend fun getStoryArc(id: ComicVineStoryArcId): ComicVineSearchResult { rateLimiter.storyArcAcquire() - - val url = buildUrlString("$baseUrl/story_arc/${ComicVineTypeId.STORY_ARC.id}-${id.value}/") - - val cachedResult = cache.getEntry(url) - - if (cachedResult != null) { - return Json.decodeFromString(cachedResult); - } - - val response: ComicVineSearchResult = ktor.get(url).body() - - cache.addEntry(url, Json.encodeToString(response)) - - return response + return getCachedApi("$baseUrl/story_arc/${ComicVineTypeId.STORY_ARC.id}-${id.value}/") } suspend fun getCover(url: String): Image { From cb2264018c04569a39234331489799ef7d07de15 Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 18:07:40 -0500 Subject: [PATCH 4/8] feat: add cacheDatabaseExpiry setting --- .../kotlin/snd/komf/providers/MetadataProvidersConfig.kt | 1 + .../kotlin/snd/komf/providers/ProvidersModule.kt | 8 +++++++- .../kotlin/snd/komf/providers/comicvine/ComicVineCache.kt | 2 +- .../snd/komf/providers/comicvine/ComicVineClient.kt | 6 +++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt index 6388ff5b..fdccd661 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvidersConfig.kt @@ -26,6 +26,7 @@ data class MetadataProvidersConfig( val libraryProviders: Map = emptyMap(), val mangabakaDatabaseDir: String = "./mangabaka", val cacheDatabaseFile: String = "./cv_cache.db", + val cacheDatabaseExpiry: Int = 14, ) @Serializable diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt index 87714464..3205a00f 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/ProvidersModule.kt @@ -109,6 +109,7 @@ class ProvidersModule( comicVineIdFormat = config.comicVineIdFormat, bangumiToken = config.bangumiToken, cacheDatabaseFile = config.cacheDatabaseFile, + cacheDatabaseExpiry = config.cacheDatabaseExpiry, ) val libraryProviders = config.libraryProviders .map { (libraryId, libraryConfig) -> @@ -122,6 +123,7 @@ class ProvidersModule( comicVineIdFormat = config.comicVineIdFormat, bangumiToken = config.bangumiToken, cacheDatabaseFile = config.cacheDatabaseFile, + cacheDatabaseExpiry = config.cacheDatabaseExpiry, ) } .toMap() @@ -336,6 +338,7 @@ class ProvidersModule( comicVineIdFormat: String?, bangumiToken: String?, cacheDatabaseFile: String, + cacheDatabaseExpiry: Int, ): MetadataProvidersContainer { return MetadataProvidersContainer( mangaupdates = createMangaUpdatesMetadataProvider( @@ -407,6 +410,7 @@ class ProvidersModule( rateLimiter = comicVineRateLimiter, defaultNameMatcher = defaultNameMatcher, cacheDatabaseFile = cacheDatabaseFile, + cacheDatabaseExpiry = cacheDatabaseExpiry, ), comicVinePriority = config.comicVine.priority, hentag = createHentagMetadataProvider( @@ -711,6 +715,7 @@ class ProvidersModule( rateLimiter: ComicVineRateLimiter, defaultNameMatcher: NameSimilarityMatcher, cacheDatabaseFile: String, + cacheDatabaseExpiry: Int, ): ComicVineMetadataProvider? { if (config.enabled.not()) return null requireNotNull(apiKey) { "Api key is not configured for ComicVine provider" } @@ -725,7 +730,8 @@ class ProvidersModule( apiKey = apiKey, comicVineSearchLimit = comicVineSearchLimit, rateLimiter = rateLimiter, - cacheDatabaseFile = cacheDatabaseFile + cacheDatabaseFile = cacheDatabaseFile, + cacheDatabaseExpiry = cacheDatabaseExpiry, ) val metadataMapper = ComicVineMetadataMapper( seriesMetadataConfig = config.seriesMetadata, diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt index aa1b94a2..6f428339 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt @@ -33,7 +33,7 @@ object CacheTable : Table("cache") { class ComicVineCache( private val databaseFile: String, - private val expiry: Int = 30, + private val expiry: Int, ) { private val databasePath = Path.of(databaseFile) private val database = Database.connect("jdbc:sqlite:$databasePath", driver = "org.sqlite.JDBC") diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt index 81baa58d..d3918e85 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt @@ -23,14 +23,18 @@ import snd.komf.providers.comicvine.model.ComicVineVolumeSearch private const val baseUrl = "https://comicvine.gamespot.com/api" +// TODO: handle when cacheDatabaseExpiry is 0 +// TODO: allow passing more parameters to buildUrlString + class ComicVineClient( private val ktor: HttpClient, private val apiKey: String, private val comicVineSearchLimit: Int? = 10, private val rateLimiter: ComicVineRateLimiter, private val cacheDatabaseFile: String, + private val cacheDatabaseExpiry: Int, ) { - private val cache = ComicVineCache(cacheDatabaseFile) + private val cache = ComicVineCache(cacheDatabaseFile, cacheDatabaseExpiry) private fun buildUrlString( url: String, From 27ba090992dee7c1793bcc76ef8cb7a8a0e3bf27 Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 18:30:05 -0500 Subject: [PATCH 5/8] fix(cvCache): inline and reify getCachedApi --- .../kotlin/snd/komf/providers/comicvine/ComicVineClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt index d3918e85..b17dc33e 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt @@ -53,7 +53,7 @@ class ComicVineClient( return "$url?$encodedParams" } - private suspend fun getCachedApi(url: String): ComicVineSearchResult { + private suspend inline fun getCachedApi(url: String): ComicVineSearchResult { val fullUrl = buildUrlString(url) val cachedResult = cache.getEntry(fullUrl) From 9d6abf4897ef215a47feab0a86a8a551f5c59aed Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 19:30:15 -0500 Subject: [PATCH 6/8] feat: allow passing more parameters to buildUrlString --- .../snd/komf/providers/comicvine/ComicVineClient.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt index b17dc33e..13078604 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt @@ -24,7 +24,6 @@ import snd.komf.providers.comicvine.model.ComicVineVolumeSearch private const val baseUrl = "https://comicvine.gamespot.com/api" // TODO: handle when cacheDatabaseExpiry is 0 -// TODO: allow passing more parameters to buildUrlString class ComicVineClient( private val ktor: HttpClient, @@ -38,13 +37,14 @@ class ComicVineClient( private fun buildUrlString( url: String, + params: Map = mapOf(), ): String { - val params = sortedMapOf( + val finalParams = sortedMapOf( Pair("api_key", apiKey), Pair("format", "json"), - ) + ) + params - val encodedParams = params.entries.joinToString("&") { (key, value) -> + val encodedParams = finalParams.entries.joinToString("&") { (key, value) -> val k = URLEncoder.encode(key, StandardCharsets.UTF_8) val v = URLEncoder.encode(value, StandardCharsets.UTF_8) "$k=$v" From e49a4c29945a72841089ac271944e6b550ec7744 Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 19:40:19 -0500 Subject: [PATCH 7/8] feat(cvCache): don't expire when expiry is set to 0 --- .../snd/komf/providers/comicvine/ComicVineCache.kt | 11 +++++++++++ .../snd/komf/providers/comicvine/ComicVineClient.kt | 2 -- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt index 6f428339..398f193c 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt @@ -75,6 +75,17 @@ class ComicVineCache( } suspend fun getEntry(url: String): String? { + if (expiry == 0) { + return transaction(db = database) { + CacheTable + .select(CacheTable.responseCol).where { + CacheTable.queryCol eq maskApiKey(url) + } + .firstOrNull() + ?.get(CacheTable.responseCol) + } + } + return transaction(db = database) { CacheTable .select(CacheTable.responseCol).where { diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt index 13078604..fe3a0d23 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineClient.kt @@ -23,8 +23,6 @@ import snd.komf.providers.comicvine.model.ComicVineVolumeSearch private const val baseUrl = "https://comicvine.gamespot.com/api" -// TODO: handle when cacheDatabaseExpiry is 0 - class ComicVineClient( private val ktor: HttpClient, private val apiKey: String, From 4e2eb19438ed707d7648985ebdbda9d5c685be8c Mon Sep 17 00:00:00 2001 From: matt1432 Date: Sat, 29 Nov 2025 20:29:25 -0500 Subject: [PATCH 8/8] docs: add explanation for new options in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b78985dc..b6132d9c 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,8 @@ metadataProviders: comicVineApiKey: # required for comicVine provider https://comicvine.gamespot.com/api/ env:KOMF_METADATA_PROVIDERS_COMIC_VINE_API_KEY comicVineSearchLimit: # define ComicVine search result Limit, default is 10 comicVineIssueName: # string that contains "{number}" which will be replaced by the issue number ie. "Issue #{number}". Used when an issue has no name on ComicVine, default is null + cacheDatabaseFile: # cache database file location. default is "./cv_cache.db" + cacheDatabaseExpiry: # number of days after which an entry in the cache is considered expired. default is 14 comicVineIdFormat: # string that contains "{id}" which will serve to parse the ComicVine volume of a given book from its title or folder name ie. "[cv-{id}]" which will correctly identify '.../Uncanny X-Men Omnibus (2006) [cv-27512]' as being [4050-27512](https://comicvine.gamespot.com/uncanny-x-men-omnibus/4050-27512/) bangumiToken: # bangumi provider require a token to show nsfw items https://next.bgm.tv/demo/access-token env:KOMF_METADATA_PROVIDERS_BANGUMI_TOKEN defaultProviders: