import Foundation /// How much of a show (or one episode) has been watched, derived from the set of /// episode paths that carry a "play" finish marker in the unified watch history. /// (resume positions are separate and drive mid-ep resume + Netflix bars; a resume /// alone does not mark an episode "started" for badges/next, to ignore fly-bys). /// We surface three coarse states for list indicators. public enum WatchState: String, Sendable, Equatable { case unwatched // nothing started case inProgress // some episodes started, not all case watched // every episode started public var icon: String { switch self { case .unwatched: "circle" case .inProgress: "circle.lefthalf.filled" case .watched: "checkmark.circle.fill" } } } public extension CachedShow { /// True if this episode has been finished (has a "play" marker in watch history). /// (Mid-ep resume positions are tracked separately for resume targets and progress bars.) func isWatched(_ episode: CachedEpisode, watchedPaths: Set) -> Bool { watchedPaths.contains(MediaPaths.toRemote(episode.path)) } /// The show's overall watch state from the watched-path set. func watchState(watchedPaths: Set) -> WatchState { let eps = orderedEpisodes guard !eps.isEmpty else { return .unwatched } let started = eps.reduce(into: 0) { n, e in if watchedPaths.contains(MediaPaths.toRemote(e.path)) { n += 1 } } if started == 0 { return .unwatched } return started == eps.count ? .watched : .inProgress } /// The episode to play for "watch next": the first un-started episode in play /// order (specials/movies last). `nil` when everything is watched — the caller /// then offers "rewatch from the start". func nextUnwatched(watchedPaths: Set) -> CachedEpisode? { orderedEpisodes.first { !watchedPaths.contains(MediaPaths.toRemote($0.path)) } } }