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.
With "Partially Watched Videos" set to Ask, opening a live stream showed
the resume sheet with a playback timestamp. A live position is relative
to the live edge, not a fixed timeline, so it is meaningless as a
playhead.
Live views are still recorded in history, but carry no resume position:
- WatchEntry gains a persisted isLive flag plus recordLiveWatch(), which
stores no position and clears state a drifting HLS duration may have
left behind. progress is always 0 and toVideo() propagates the flag so
history rows render the LIVE badge.
- from(video:) clamps duration, keeping Piped's -1 live sentinel out of
storage.
- DataManager routes live saves through recordLiveWatch() and clears the
flag on the non-live path (before updateProgress, so the auto-finish
check is not suppressed by the hard-0 live progress), letting an entry
heal once the stream becomes a VOD.
- PlayerService skips completion saves, always starts at the live edge,
and drops any startTime passed for a live video.
- The resume prompt, the "Continue at" button label, thumbnail progress
bars, "X remaining", Continue Watching and tvOS Top Shelf all exclude
live entries.
- CloudKit syncs isLive with the rest of the watch state: the conflict
resolver carries it with watchedSeconds/isFinished and the sync engine
applies it when merging into an existing entry. It is read leniently,
so the schema version stays at 2 and older clients keep parsing these
records.
Surfaces Continue Watching, Recent Feed, and Recent Bookmarks in the
Apple TV Home top shelf when Yattee is focused. Tapping a tile opens
the video via the existing yattee://video/{id} deep link.
- New YatteeTopShelf app extension target (tvOS only). LD_ENTRY_POINT is
overridden to _NSExtensionMain; the tv-app-extension product type
defaults to _TVExtensionMain which is for the pre-tvOS-13 legacy API
and crashes modern TVTopShelfContentProvider subclasses at launch.
- Main app writes per-section JSON snapshots (capped at 10 items each)
to a shared App Group UserDefaults suite after bookmark, watch-history,
and feed-cache changes, plus an initial write on launch.
- Enabled-sections list is mirrored to the same App Group so the
extension can respect the user's selection without touching SwiftData.
- Settings → Top Shelf (tvOS only) lets the user toggle sections.
- Deep link playback shows a loading toast while video details are
fetched, and an error toast if no source is configured.