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.
Previously CKSyncEngine was created with nil state serialization on
every launch, replaying the entire zone change history (hundreds of
merge/delete log lines and re-downloads each start). Now the saved
state is loaded so launches only fetch changes since the last session.
To make incremental sync safe:
- Clear persisted state once when the record schema version increases,
so records previously skipped as unsupported get re-delivered after
an app update.
- Handle remote zone deletion in fetchedDatabaseChanges by recreating
the zone and re-uploading local data, instead of leaving sync dead.
Account change and manual Refresh Sync still clear the state and force
a full fetch as before.
CloudKitRecordMapper was a standalone actor, so 'await recordMapper.toCKRecord'
from the @MainActor sync engine hopped off the main actor and read live
SwiftData @Model properties (e.g. LocalPlaylistItem.authorName) from the wrong
executor. SwiftData models are bound to the main ModelContext and are not
thread-safe, so this raced the backing store and crashed with EXC_BAD_ACCESS.
Make the mapper @MainActor (it must touch main-isolated models regardless) and
drop the now-redundant awaits. Fixes the crash for all synced model types.
Fixes an EXC_BAD_ACCESS seen in TestFlight build 261.