Apply thumbnail fallback everywhere a single URL was rendered

The previous fix rebuilt the quality chain when converting persisted
models back to videos, but many consumers discarded it again by
rendering bestThumbnail (usually a maxresdefault.jpg that 404s for
older videos) as a single URL with no fallback.

Extract the retry logic from VideoThumbnailView into FallbackLazyImage
and use it at every in-app site that can iterate: player thumbnails
(mini bar, expanded sheet loaders, autoplay previews, tvOS audio-mode
artwork - previously a silent black screen), video info card and tvOS
header, and the tvOS playlist cover.

Sites that fetch or send exactly one URL are rewritten to the
always-available hqdefault variant via Thumbnail.reliableURL (exposed
as Video.reliableThumbnailURL): Now Playing artwork, Top Shelf
snapshots, remote control state, the frozen transition thumbnail,
blurred info background, navigation covers, and playlist covers
derived from a video's first thumbnail in Invidious/Yattee Server
responses.

Also invert RecentPlaylist's upgrade helper, which rewrote covers *to*
maxresdefault, walk the quality chain when caching download thumbnails
for offline artwork instead of giving up after one 404, and expand the
remaining single-thumbnail Piped conversions into full chains.
This commit is contained in:
Arkadiusz Fal
2026-07-31 23:21:38 +02:00
parent fd1029d3e0
commit 2c0c2e853a
19 changed files with 126 additions and 88 deletions

View File

@@ -346,12 +346,17 @@ extension DownloadManager {
var thumbnailPath: String?
var channelThumbnailPath: String?
// Download video thumbnail (best quality) - best-effort, ignore failures
// Download video thumbnail - best-effort, ignore failures. The stored URL
// is the best advertised variant (often maxresdefault, which 404s for
// older videos), so walk the quality chain until one succeeds.
if let thumbnailURL = download.thumbnailURL {
thumbnailPath = await downloadThumbnail(
from: thumbnailURL,
filename: "\(videoID)_thumbnail.jpg"
)
for candidate in Thumbnail.fallbackChain(for: thumbnailURL) {
thumbnailPath = await downloadThumbnail(
from: candidate.url,
filename: "\(videoID)_thumbnail.jpg"
)
if thumbnailPath != nil { break }
}
}
// Download channel thumbnail - best-effort, ignore failures