mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 18:35:05 +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 {
|
||||
static let shared = ImageLoadingService()
|
||||
|
||||
private let pipelineDelegate = TokenStrippingPipelineDelegate()
|
||||
|
||||
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.
|
||||
/// Call this once at app launch.
|
||||
func configure() {
|
||||
@@ -53,8 +69,10 @@ final class ImageLoadingService: Sendable {
|
||||
// Use default URLSession-based data loader
|
||||
config.dataLoader = DataLoader(configuration: .default)
|
||||
|
||||
// Set as shared pipeline
|
||||
ImagePipeline.shared = ImagePipeline(configuration: config)
|
||||
// Set as shared pipeline with a delegate that normalizes cache keys
|
||||
// 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(
|
||||
"Image pipeline configured",
|
||||
@@ -99,3 +117,12 @@ final class ImageLoadingService: Sendable {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2136,7 +2136,7 @@ struct VideoInfoView: View {
|
||||
return
|
||||
}
|
||||
let videoID = base.id.videoID
|
||||
|
||||
|
||||
// Skip if already loaded
|
||||
guard loadedVideoDetails[videoID] == nil else {
|
||||
isLoadingVideoDetails = false
|
||||
@@ -2206,8 +2206,10 @@ struct VideoInfoView: View {
|
||||
/// payloads from Nuke's cache.
|
||||
@MainActor
|
||||
private func invalidateStaleThumbnails(old: Video, new: Video) {
|
||||
let newURLs = Set(new.thumbnails.map(\.url))
|
||||
for thumb in old.thumbnails where !newURLs.contains(thumb.url) {
|
||||
// Compare by normalized cache key so URLs that only differ in a
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user