Files
yattee/Yattee/Data/RecentPlaylist.swift
Arkadiusz Fal 2c0c2e853a 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.
2026-08-03 23:34:06 +02:00

83 lines
2.6 KiB
Swift

//
// RecentPlaylist.swift
// Yattee
//
// SwiftData model for recent playlist visits (remote playlists only).
//
import Foundation
import SwiftData
@Model
final class RecentPlaylist {
var id: UUID
var playlistID: String
var sourceRawValue: String
var instanceURLString: String?
var title: String
var authorName: String
var videoCount: Int
var thumbnailURLString: String?
var visitedAt: Date
init(
id: UUID = UUID(),
playlistID: String,
sourceRawValue: String,
instanceURLString: String? = nil,
title: String,
authorName: String = "",
videoCount: Int = 0,
thumbnailURLString: String? = nil,
visitedAt: Date = Date()
) {
self.id = id
self.playlistID = playlistID
self.sourceRawValue = sourceRawValue
self.instanceURLString = instanceURLString
self.title = title
self.authorName = authorName
self.videoCount = videoCount
self.thumbnailURLString = thumbnailURLString
self.visitedAt = visitedAt
}
/// Creates a RecentPlaylist from a Playlist model
/// Returns nil for local playlists (we only track remote ones)
static func from(playlist: Playlist) -> RecentPlaylist? {
guard !playlist.isLocal, let source = playlist.id.source else {
return nil
}
let (sourceRaw, instanceURL) = extractSourceInfo(from: source)
return RecentPlaylist(
playlistID: playlist.id.playlistID,
sourceRawValue: sourceRaw,
instanceURLString: instanceURL,
title: playlist.title,
authorName: playlist.authorName,
videoCount: playlist.videoCount,
thumbnailURLString: reliableThumbnailURLString(playlist.thumbnailURL)
)
}
/// Rewrites YouTube `/vi/ID/...` thumbnails to the always-available `hqdefault.jpg`
/// variant: recent playlist cards render a single URL without fallback, and
/// higher-quality variants (`maxresdefault`/`sddefault`) 404 for many older videos.
static func reliableThumbnailURLString(_ url: URL?) -> String? {
guard let url else { return nil }
return (Thumbnail.reliableURL(for: url) ?? url).absoluteString
}
private static func extractSourceInfo(from source: ContentSource) -> (String, String?) {
switch source {
case .global:
return ("global", nil)
case .federated(_, let instance):
return ("federated", instance.absoluteString)
case .extracted:
return ("extracted", nil)
}
}
}