mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 10:55:03 +00:00
Stabilize Nuke cache key across rotating thumbnail URL tokens
This commit is contained in:
@@ -13,8 +13,24 @@ import Nuke
|
|||||||
final class ImageLoadingService: Sendable {
|
final class ImageLoadingService: Sendable {
|
||||||
static let shared = ImageLoadingService()
|
static let shared = ImageLoadingService()
|
||||||
|
|
||||||
|
private let pipelineDelegate = TokenStrippingPipelineDelegate()
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
|
/// Returns a cache-stable key for an image URL by stripping query params
|
||||||
|
/// that rotate per-request (e.g. Yattee-server signed `token`). Images
|
||||||
|
/// whose path matches but token differs share a cache entry.
|
||||||
|
nonisolated static func cacheKey(for url: URL) -> String {
|
||||||
|
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||||
|
return url.absoluteString
|
||||||
|
}
|
||||||
|
if let items = components.queryItems {
|
||||||
|
let filtered = items.filter { $0.name != "token" }
|
||||||
|
components.queryItems = filtered.isEmpty ? nil : filtered
|
||||||
|
}
|
||||||
|
return components.url?.absoluteString ?? url.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
/// Configure the shared ImagePipeline with app-specific settings.
|
/// Configure the shared ImagePipeline with app-specific settings.
|
||||||
/// Call this once at app launch.
|
/// Call this once at app launch.
|
||||||
func configure() {
|
func configure() {
|
||||||
@@ -53,8 +69,10 @@ final class ImageLoadingService: Sendable {
|
|||||||
// Use default URLSession-based data loader
|
// Use default URLSession-based data loader
|
||||||
config.dataLoader = DataLoader(configuration: .default)
|
config.dataLoader = DataLoader(configuration: .default)
|
||||||
|
|
||||||
// Set as shared pipeline
|
// Set as shared pipeline with a delegate that normalizes cache keys
|
||||||
ImagePipeline.shared = ImagePipeline(configuration: config)
|
// so signed thumbnail URLs whose only difference is a rotating `token`
|
||||||
|
// query param share a single cache entry.
|
||||||
|
ImagePipeline.shared = ImagePipeline(configuration: config, delegate: pipelineDelegate)
|
||||||
|
|
||||||
LoggingService.shared.info(
|
LoggingService.shared.info(
|
||||||
"Image pipeline configured",
|
"Image pipeline configured",
|
||||||
@@ -99,3 +117,12 @@ final class ImageLoadingService: Sendable {
|
|||||||
return ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file)
|
return ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pipeline delegate that normalizes Nuke's cache key by stripping per-request
|
||||||
|
/// query params (`token`). Prevents churn when Yattee-server re-signs URLs.
|
||||||
|
private final class TokenStrippingPipelineDelegate: ImagePipelineDelegate {
|
||||||
|
func cacheKey(for request: ImageRequest, pipeline _: ImagePipeline) -> String? {
|
||||||
|
guard let url = request.url else { return nil }
|
||||||
|
return ImageLoadingService.cacheKey(for: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2206,8 +2206,10 @@ struct VideoInfoView: View {
|
|||||||
/// payloads from Nuke's cache.
|
/// payloads from Nuke's cache.
|
||||||
@MainActor
|
@MainActor
|
||||||
private func invalidateStaleThumbnails(old: Video, new: Video) {
|
private func invalidateStaleThumbnails(old: Video, new: Video) {
|
||||||
let newURLs = Set(new.thumbnails.map(\.url))
|
// Compare by normalized cache key so URLs that only differ in a
|
||||||
for thumb in old.thumbnails where !newURLs.contains(thumb.url) {
|
// rotating signing `token` aren't considered stale and evicted.
|
||||||
|
let newKeys = Set(new.thumbnails.map { ImageLoadingService.cacheKey(for: $0.url) })
|
||||||
|
for thumb in old.thumbnails where !newKeys.contains(ImageLoadingService.cacheKey(for: thumb.url)) {
|
||||||
ImageLoadingService.shared.removeCachedImage(for: thumb.url)
|
ImageLoadingService.shared.removeCachedImage(for: thumb.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user