Refresh expired thumbnail URLs for downloads and video info

Proxied thumbnail URLs from Invidious/Piped/Yattee server expire over
time. Two paths were left holding stale URLs: the Video Info carousel
kept the original list copy even after fresh details arrived, and
downloaded videos rendered from the remote URL snapshot taken at
download time while the local thumbnail on disk was ignored.

Evict stale URLs from the Nuke cache when fresh video details load,
pass the fresh details through to the videoCard thumbnail, and resolve
downloads' thumbnails from the local file when localThumbnailPath is
set.
This commit is contained in:
Arkadiusz Fal
2026-04-17 06:47:14 +02:00
parent abd432fd0e
commit 2efa0708c8
7 changed files with 67 additions and 17 deletions

View File

@@ -298,9 +298,29 @@ struct Download: Identifiable, Codable, Sendable, Equatable {
self.audioBitrate = audioBitrate
}
/// Resolves the local thumbnail file URL, if the thumbnail has been
/// downloaded to disk. Remote `thumbnailURL`s for proxied sources
/// (Invidious / Piped / Yattee server) expire, so prefer the local copy
/// whenever it is available.
func localThumbnailURL(in downloadsDirectory: URL) -> URL? {
guard let localThumbnailPath else { return nil }
return downloadsDirectory.appendingPathComponent(localThumbnailPath)
}
/// Convert to a Video model for display purposes.
func toVideo() -> Video {
Video(
/// - Parameter downloadsDirectory: When provided and a local thumbnail
/// exists on disk, the resulting `Video` uses a `file://` URL pointing
/// at the local thumbnail instead of the (potentially expired) remote
/// `thumbnailURL` captured at download time.
func toVideo(downloadsDirectory: URL? = nil) -> Video {
let resolvedThumbnailURL: URL? = {
if let downloadsDirectory, let localURL = localThumbnailURL(in: downloadsDirectory) {
return localURL
}
return thumbnailURL
}()
return Video(
id: videoID,
title: title,
description: description,
@@ -316,7 +336,7 @@ struct Download: Identifiable, Codable, Sendable, Equatable {
publishedText: publishedText,
viewCount: viewCount,
likeCount: likeCount,
thumbnails: thumbnailURL.map { [Thumbnail(url: $0, quality: .medium)] } ?? [],
thumbnails: resolvedThumbnailURL.map { [Thumbnail(url: $0, quality: .medium)] } ?? [],
isLive: false,
isUpcoming: false,
scheduledStartTime: nil

View File

@@ -63,6 +63,15 @@ final class ImageLoadingService: Sendable {
)
}
/// Remove a specific URL from both the memory and disk image caches.
/// Use when a previously cached URL is known to return stale or broken
/// data (e.g. an expired proxied thumbnail URL).
func removeCachedImage(for url: URL) {
let request = ImageRequest(url: url)
ImagePipeline.shared.cache.removeCachedImage(for: request)
ImagePipeline.shared.cache.removeCachedData(for: request)
}
/// Clear all image caches (memory and disk).
func clearCache() {
// Clear memory cache