Stop tracking resume progress for live streams

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.
This commit is contained in:
Arkadiusz Fal
2026-07-30 23:59:57 +02:00
parent 8faf20c9e9
commit aa6e7fbce9
16 changed files with 392 additions and 28 deletions

View File

@@ -51,8 +51,9 @@ struct ContinueWatchingGridCard: View {
VideoCardView(
video: video,
watchProgress: entry.progress,
customMetadata: entry.isFinished ? nil : String(localized: "home.history.remaining \(entry.remainingTime)"),
customDuration: entry.remainingTime
customMetadata: entry.isFinished || entry.isLive ? nil : String(localized: "home.history.remaining \(entry.remainingTime)"),
// Live streams have no remaining time - fall back to the LIVE badge
customDuration: entry.isLive ? nil : entry.remainingTime
)
}
}

View File

@@ -33,7 +33,7 @@ struct ContinueWatchingView: View {
/// Filtered to only show in-progress videos.
private var inProgressEntries: [WatchEntry] {
watchHistory.filter { !$0.isFinished && $0.watchedSeconds > 10 }
watchHistory.filter { !$0.isFinished && !$0.isLive && $0.watchedSeconds > 10 }
}
// Grid layout configuration
@@ -237,7 +237,7 @@ struct ContinueWatchingView: View {
video: entry.toVideo(),
style: rowStyle,
watchProgress: entry.progress,
customMetadata: entry.isFinished ? nil : String(localized: "home.history.remaining \(entry.remainingTime)")
customMetadata: entry.isFinished || entry.isLive ? nil : String(localized: "home.history.remaining \(entry.remainingTime)")
)
.tappableVideo(
entry.toVideo(),

View File

@@ -305,7 +305,7 @@ struct HistoryListView: View {
video: video,
style: rowStyle,
watchProgress: watchProgress(for: entry),
customMetadata: entry.isFinished ? nil : String(localized: "home.history.remaining \(entry.remainingTime)")
customMetadata: entry.isFinished || entry.isLive ? nil : String(localized: "home.history.remaining \(entry.remainingTime)")
)
.tappableVideo(
video,

View File

@@ -1463,7 +1463,7 @@ struct HomeView: View {
private func loadContinueWatchingData() {
let allHistory = dataManager?.watchHistory(limit: 100) ?? []
// Filter to in-progress only (same logic as ContinueWatchingView)
recentContinueWatching = allHistory.filter { !$0.isFinished && $0.watchedSeconds > 10 }
recentContinueWatching = allHistory.filter { !$0.isFinished && !$0.isLive && $0.watchedSeconds > 10 }
continueWatchingCount = recentContinueWatching.count
}