diff --git a/README.md b/README.md index b78985dc..34816705 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. set it to 0 for unlimited. 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: diff --git a/komf-app/src/main/kotlin/snd/komf/app/api/MetadataRoutes.kt b/komf-app/src/main/kotlin/snd/komf/app/api/MetadataRoutes.kt index 0d169b70..bf8d6c2e 100644 --- a/komf-app/src/main/kotlin/snd/komf/app/api/MetadataRoutes.kt +++ b/komf-app/src/main/kotlin/snd/komf/app/api/MetadataRoutes.kt @@ -49,6 +49,8 @@ class MetadataRoutes( resetSeriesRoute() resetLibraryRoute() + + clearSeriesCacheRoute() } } @@ -144,6 +146,34 @@ class MetadataRoutes( } } + private fun Route.clearSeriesCacheRoute() { + post("/cache/library/{libraryId}/series/{seriesId}/clear") { + val libraryId = call.parameters.getOrFail("libraryId") + val seriesId = MediaServerSeriesId(call.parameters.getOrFail("seriesId")) + val series = mediaServerClient + .first() + .getSeries(MediaServerSeriesId(seriesId.value)) + + series.metadata.links.forEach { + if (it.url.contains("comicvine.gamespot.com")) { + val providerSeriesId = it.url.trimEnd('/').substringAfterLast('-') + metadataServiceProvider + .first() + .metadataServiceFor(libraryId) + .clearSeriesCache( + libraryId, + CoreProviders.COMIC_VINE, + ProviderSeriesId(providerSeriesId), + ) + + call.respond(HttpStatusCode.Accepted, "") + } + } + + call.respond(HttpStatusCode.NoContent, "") + } + } + private fun Route.matchSeriesRoute() { post("/match/library/{libraryId}/series/{seriesId}") { @@ -190,4 +220,4 @@ class MetadataRoutes( } } -} \ No newline at end of file +} diff --git a/komf-app/src/main/kotlin/snd/komf/app/api/deprecated/DeprecatedMetadataRoutes.kt b/komf-app/src/main/kotlin/snd/komf/app/api/deprecated/DeprecatedMetadataRoutes.kt index 449da1b9..9fd6832e 100644 --- a/komf-app/src/main/kotlin/snd/komf/app/api/deprecated/DeprecatedMetadataRoutes.kt +++ b/komf-app/src/main/kotlin/snd/komf/app/api/deprecated/DeprecatedMetadataRoutes.kt @@ -41,6 +41,7 @@ class DeprecatedMetadataRoutes( matchLibraryRoute() resetSeriesRoute() resetLibraryRoute() + clearSeriesCacheRoute() } } } @@ -115,6 +116,34 @@ class DeprecatedMetadataRoutes( } } + private fun Route.clearSeriesCacheRoute() { + post("/cache/library/{libraryId}/series/{seriesId}/clear") { + val libraryId = call.parameters.getOrFail("libraryId") + val seriesId = MediaServerSeriesId(call.parameters.getOrFail("seriesId")) + val series = mediaServerClient + .first() + .getSeries(MediaServerSeriesId(seriesId.value)) + + series.metadata.links.forEach { + if (it.url.contains("comicvine.gamespot.com")) { + val providerSeriesId = it.url.trimEnd('/').substringAfterLast('-') + metadataServiceProvider + .first() + .metadataServiceFor(libraryId) + .clearSeriesCache( + libraryId, + CoreProviders.COMIC_VINE, + ProviderSeriesId(providerSeriesId), + ) + + call.respond(HttpStatusCode.Accepted, "") + } + } + + call.respond(HttpStatusCode.NoContent, "") + } + } + private fun Route.matchLibraryRoute() { post("/match/library/{libraryId}") { val libraryId = MediaServerLibraryId(call.parameters.getOrFail("libraryId")) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvider.kt index fdd30d35..46d58600 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/MetadataProvider.kt @@ -15,6 +15,8 @@ interface MetadataProvider { suspend fun getSeriesCover(seriesId: ProviderSeriesId): Image? + suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) + suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata suspend fun searchSeries(seriesName: String, limit: Int = 5): Collection 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..fdccd661 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,8 @@ data class MetadataProvidersConfig( val defaultProviders: ProvidersConfig = ProvidersConfig(), 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 a92e54d4..3205a00f 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,8 @@ class ProvidersModule( comicVineIssueName = config.comicVineIssueName, comicVineIdFormat = config.comicVineIdFormat, bangumiToken = config.bangumiToken, + cacheDatabaseFile = config.cacheDatabaseFile, + cacheDatabaseExpiry = config.cacheDatabaseExpiry, ) val libraryProviders = config.libraryProviders .map { (libraryId, libraryConfig) -> @@ -120,6 +122,8 @@ class ProvidersModule( comicVineIssueName = config.comicVineIssueName, comicVineIdFormat = config.comicVineIdFormat, bangumiToken = config.bangumiToken, + cacheDatabaseFile = config.cacheDatabaseFile, + cacheDatabaseExpiry = config.cacheDatabaseExpiry, ) } .toMap() @@ -333,6 +337,8 @@ class ProvidersModule( comicVineIssueName: String?, comicVineIdFormat: String?, bangumiToken: String?, + cacheDatabaseFile: String, + cacheDatabaseExpiry: Int, ): MetadataProvidersContainer { return MetadataProvidersContainer( mangaupdates = createMangaUpdatesMetadataProvider( @@ -403,6 +409,8 @@ class ProvidersModule( comicVineIdFormat = comicVineIdFormat, rateLimiter = comicVineRateLimiter, defaultNameMatcher = defaultNameMatcher, + cacheDatabaseFile = cacheDatabaseFile, + cacheDatabaseExpiry = cacheDatabaseExpiry, ), comicVinePriority = config.comicVine.priority, hentag = createHentagMetadataProvider( @@ -706,6 +714,8 @@ class ProvidersModule( comicVineIdFormat: String?, 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" } @@ -719,7 +729,9 @@ class ProvidersModule( }, apiKey = apiKey, comicVineSearchLimit = comicVineSearchLimit, - rateLimiter = rateLimiter + rateLimiter = rateLimiter, + cacheDatabaseFile = cacheDatabaseFile, + cacheDatabaseExpiry = cacheDatabaseExpiry, ) val metadataMapper = ComicVineMetadataMapper( seriesMetadataConfig = config.seriesMetadata, @@ -908,4 +920,4 @@ class ProvidersModule( } -} \ No newline at end of file +} diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/AniListMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/AniListMetadataProvider.kt index 924cedab..f4321ad7 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/AniListMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/anilist/AniListMetadataProvider.kt @@ -42,6 +42,10 @@ class AniListMetadataProvider( return client.getThumbnail(series) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { throw UnsupportedOperationException() } diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/BangumiMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/BangumiMetadataProvider.kt index 8ec07db1..de3d977b 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/BangumiMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/bangumi/BangumiMetadataProvider.kt @@ -46,6 +46,10 @@ class BangumiMetadataProvider( return client.getThumbnail(series) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val book = client.getSubject(bookId.id.toLong()) val thumbnail = if (fetchSeriesCovers) client.getThumbnail(book) else null diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/BookWalkerMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/BookWalkerMetadataProvider.kt index f9918f24..7215e3e7 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/BookWalkerMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/bookwalker/BookWalkerMetadataProvider.kt @@ -66,6 +66,10 @@ class BookWalkerMetadataProvider( return fetchCover(getFirstBook(books)) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val bookMetadata = bookCache.get(BookWalkerBookId(bookId.id)) { client.getBook(BookWalkerBookId(bookId.id)) } val bookCover = if (fetchBookCovers) fetchCover(bookMetadata) else null 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..22a5a659 --- /dev/null +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineCache.kt @@ -0,0 +1,108 @@ +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.deleteWhere +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 QueriesTable : Table("queries") { + val urlCol = text("url") + override val primaryKey = PrimaryKey(urlCol) + + val createdAtCol = timestamp("created_at") + + val responseCol = text("response") +} + +class ComicVineCache( + private val databaseFile: String, + private val expiry: Int, +) { + private val databasePath = Path.of(databaseFile) + private val database = Database.connect("jdbc:sqlite:$databasePath", driver = "org.sqlite.JDBC") + + init { + transaction(db = database) { + SchemaUtils.create(QueriesTable) + } + } + + 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) { + QueriesTable.upsert { + it[urlCol] = maskApiKey(url) + it[responseCol] = response + it[createdAtCol] = getExpiryTimestamp() + } + } + } + + fun removeEntry(url: String) { + transaction(db = database) { + QueriesTable.deleteWhere { + QueriesTable.urlCol eq maskApiKey(url) + } + } + } + + suspend fun getEntry(url: String): String? { + if (expiry == 0) { + return transaction(db = database) { + QueriesTable + .select(QueriesTable.responseCol).where { + QueriesTable.urlCol eq maskApiKey(url) + } + .firstOrNull() + ?.get(QueriesTable.responseCol) + } + } + + return transaction(db = database) { + QueriesTable + .select(QueriesTable.responseCol).where { + (QueriesTable.urlCol eq maskApiKey(url)) and + (QueriesTable.createdAtCol greater getNowTimestamp()) + } + .firstOrNull() + ?.get(QueriesTable.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..642d09c0 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,44 @@ class ComicVineClient( 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, cacheDatabaseExpiry) + + private fun buildUrlString( + url: String, + params: Map = mapOf(), + ): String { + val finalParams = sortedMapOf( + Pair("api_key", apiKey), + Pair("format", "json"), + ) + params + + 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" + } + + return "$url?$encodedParams" + } + + private suspend inline 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() @@ -37,18 +80,36 @@ 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() + return getCachedApi("$baseUrl/volume/${VOLUME.id}-${id.value}/") + } + + suspend fun clearVolumeCache(id: ComicVineVolumeId) { + val url = buildUrlString("$baseUrl/volume/${VOLUME.id}-${id.value}/") + + val cachedResult = cache.getEntry(url) + + if (cachedResult == null) { + return + } + + val response: ComicVineSearchResult = Json.decodeFromString(cachedResult); + + response.results.issues?.forEach { + val issueUrl = buildUrlString("$baseUrl/issue/${ISSUE.id}-${it.id}/") + + val cachedIssueResult = cache.getEntry(issueUrl) + + if (cachedIssueResult != null) { + cache.removeEntry(issueUrl) + } + } + + cache.removeEntry(url) } 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() + return getCachedApi("$baseUrl/issue/${ISSUE.id}-${id.value}/") } suspend fun getStoryArc(id: ComicVineStoryArcId): ComicVineSearchResult { @@ -57,7 +118,6 @@ class ComicVineClient( parameter("format", "json") parameter("api_key", apiKey) }.body() - } suspend fun getCover(url: String): Image { diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineMetadataProvider.kt index 0178ad94..15524ee1 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/comicvine/ComicVineMetadataProvider.kt @@ -63,6 +63,10 @@ class ComicVineMetadataProvider( return series.image?.let { getCover(it) } } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + client.clearVolumeCache(providerSeriesId.toComicVineVolumeId()) + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val issue = handleResult(client.getIssue(bookId.toComicVineIssueId())) val storyArcs = issue.storyArcCredits?.let { credits -> diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/hentag/HentagMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/hentag/HentagMetadataProvider.kt index 2844176d..115c0dbb 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/hentag/HentagMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/hentag/HentagMetadataProvider.kt @@ -37,6 +37,10 @@ class HentagMetadataProvider( return hentagClient.getCover(book) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { throw UnsupportedOperationException() } diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/KodanshaMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/KodanshaMetadataProvider.kt index 008bb585..6b58d2fe 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/KodanshaMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/kodansha/KodanshaMetadataProvider.kt @@ -38,6 +38,10 @@ class KodanshaMetadataProvider( return getThumbnail(series.thumbnails?.firstOrNull()?.url) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val bookMetadata = client.getBook(KodanshaBookId(bookId.id.toInt())).response val thumbnail = if (fetchBookCovers) getThumbnail(bookMetadata.thumbnails.firstOrNull()?.url) else null diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/mal/MalMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/mal/MalMetadataProvider.kt index b60b8e56..64888ce0 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/mal/MalMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/mal/MalMetadataProvider.kt @@ -55,6 +55,10 @@ class MalMetadataProvider( return malClient.getThumbnail(series) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { throw UnsupportedOperationException() } diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/mangabaka/MangaBakaMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/mangabaka/MangaBakaMetadataProvider.kt index 84623c6c..ee57f20d 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/mangabaka/MangaBakaMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/mangabaka/MangaBakaMetadataProvider.kt @@ -59,6 +59,10 @@ class MangaBakaMetadataProvider( return fetchCover(series) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata( seriesId: ProviderSeriesId, bookId: ProviderBookId diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/mangadex/MangaDexMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/mangadex/MangaDexMetadataProvider.kt index 7a68a925..a1be1fa4 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/mangadex/MangaDexMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/mangadex/MangaDexMetadataProvider.kt @@ -37,6 +37,10 @@ class MangaDexMetadataProvider( return series.getCoverArt()?.let { client.getCover(series.id, it.attributes.fileName) } } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val cover = if (fetchBookCovers) client.getCover(MangaDexMangaId(seriesId.value), bookId.id) else null return metadataMapper.toBookMetadata(bookId.id, cover) diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/mangaupdates/MangaUpdatesMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/mangaupdates/MangaUpdatesMetadataProvider.kt index bf04def3..dc4a76d1 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/mangaupdates/MangaUpdatesMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/mangaupdates/MangaUpdatesMetadataProvider.kt @@ -67,6 +67,10 @@ class MangaUpdatesMetadataProvider( return client.getThumbnail(series) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { throw UnsupportedOperationException() } diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/nautiljon/NautiljonMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/nautiljon/NautiljonMetadataProvider.kt index 8084f19b..56eefc07 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/nautiljon/NautiljonMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/nautiljon/NautiljonMetadataProvider.kt @@ -38,6 +38,10 @@ class NautiljonMetadataProvider( return client.getSeriesThumbnail(series) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val bookMetadata = client.getBook(NautiljonSeriesId(seriesId.value), NautiljonVolumeId(bookId.id)) val thumbnail = if (fetchBookCovers) client.getVolumeThumbnail(bookMetadata) else null diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/viz/VizMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/viz/VizMetadataProvider.kt index a8129ae3..7062687b 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/viz/VizMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/viz/VizMetadataProvider.kt @@ -45,6 +45,10 @@ class VizMetadataProvider( return getThumbnail(series.coverUrl) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val bookMetadata = getBook(VizBookId(bookId.id)) val thumbnail = if (fetchBookCovers) getThumbnail(bookMetadata.coverUrl) else null diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/webtoons/WebtoonsMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/webtoons/WebtoonsMetadataProvider.kt index 27e80d0d..a27630cf 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/webtoons/WebtoonsMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/webtoons/WebtoonsMetadataProvider.kt @@ -50,6 +50,10 @@ class WebtoonsMetadataProvider( return client.getSeriesThumbnail(series) } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata( seriesId: ProviderSeriesId, bookId: ProviderBookId ): ProviderBookMetadata { diff --git a/komf-core/src/commonMain/kotlin/snd/komf/providers/yenpress/YenPressMetadataProvider.kt b/komf-core/src/commonMain/kotlin/snd/komf/providers/yenpress/YenPressMetadataProvider.kt index 707a9ed8..d1400151 100644 --- a/komf-core/src/commonMain/kotlin/snd/komf/providers/yenpress/YenPressMetadataProvider.kt +++ b/komf-core/src/commonMain/kotlin/snd/komf/providers/yenpress/YenPressMetadataProvider.kt @@ -55,6 +55,10 @@ class YenPressMetadataProvider( } } + override suspend fun clearSeriesCache(providerSeriesId: ProviderSeriesId) { + throw UnsupportedOperationException() + } + override suspend fun getBookMetadata(seriesId: ProviderSeriesId, bookId: ProviderBookId): ProviderBookMetadata { val bookMetadata = client.getBook(YenPressBookId(bookId.id)) val thumbnail = if (fetchBookCovers) client.getBookThumbnail(bookMetadata) else null diff --git a/komf-mediaserver/src/commonMain/kotlin/snd/komf/mediaserver/metadata/MetadataService.kt b/komf-mediaserver/src/commonMain/kotlin/snd/komf/mediaserver/metadata/MetadataService.kt index ef1827a7..1878f3bb 100644 --- a/komf-mediaserver/src/commonMain/kotlin/snd/komf/mediaserver/metadata/MetadataService.kt +++ b/komf-mediaserver/src/commonMain/kotlin/snd/komf/mediaserver/metadata/MetadataService.kt @@ -134,6 +134,17 @@ class MetadataService( return jobId } + fun clearSeriesCache( + libraryId: String, + providerName: CoreProviders, + providerSeriesId: ProviderSeriesId, + ) { + coroutineScope.launch { + val provider = metadataProviders.provider(libraryId, providerName) ?: throw RuntimeException() + provider.clearSeriesCache(providerSeriesId) + } + } + fun matchLibraryMetadata(libraryId: MediaServerLibraryId) { coroutineScope.launch { var errorCount = 0