Renames Sources/PlumTV→TVAnarchy and PlumTVCore→TVAnarchyCore (the rename the auto-commit service couldn't stage — it git-add'd the old, now-gone paths and aborted every cycle), and commits the accumulated work: - Library: black-built index fast path (LibraryIndex + scanFromIndex) with NFS-walk fallback; incremental --add on download-complete; mtime staleness gate; loose-file series-collapse fix; determinate scan/index progress. - Cover art: keyless TVmaze cartoon-vs-live-action disambiguation (type/year). - Player: sleep timer (timed + end-of-episode); visibility-gated polling. - Home: Continue Watching cover art + live refresh; Recently Added; adult gate. - Logs: multi-line selection + copy; truncated giant tx-list errors. - Hover previews (opt-in) via black ffmpeg + scp. Also gitignores foreign project trees (governor/mcp/fleet/recommender) that sit in this directory but belong to their own repos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.7 KiB
Swift
62 lines
2.7 KiB
Swift
import Foundation
|
|
|
|
/// Live connection state of a target, kept separate from playback so a single
|
|
/// dropped poll renders as "unreachable (last known …)" rather than wiping state.
|
|
public enum ConnectionState: String, Sendable, Equatable {
|
|
case checking, connected, unreachable
|
|
}
|
|
|
|
/// Unified playback state across every target. Volume is normalized to a
|
|
/// percentage (100 = normal) regardless of the backend's native scale.
|
|
public struct PlaybackStatus: Equatable, Sendable, Codable {
|
|
public var playing: Bool
|
|
public var paused: Bool?
|
|
public var title: String?
|
|
public var volume: Double? // percent (100 = normal)
|
|
public var position: Double? // seconds
|
|
public var duration: Double? // seconds
|
|
public var playlistPos: Int?
|
|
public var playlistCount: Int?
|
|
|
|
public init(playing: Bool, paused: Bool? = nil, title: String? = nil,
|
|
volume: Double? = nil, position: Double? = nil, duration: Double? = nil,
|
|
playlistPos: Int? = nil, playlistCount: Int? = nil) {
|
|
self.playing = playing; self.paused = paused; self.title = title
|
|
self.volume = volume; self.position = position; self.duration = duration
|
|
self.playlistPos = playlistPos; self.playlistCount = playlistCount
|
|
}
|
|
|
|
public static let idle = PlaybackStatus(playing: false)
|
|
}
|
|
|
|
/// Result of polling a target: reachability and (if reachable) the playback
|
|
/// state. `status == nil` while reachable means "reachable but no clear state".
|
|
public struct PollResult: Sendable {
|
|
public var reachable: Bool
|
|
public var status: PlaybackStatus?
|
|
public init(reachable: Bool, status: PlaybackStatus?) {
|
|
self.reachable = reachable; self.status = status
|
|
}
|
|
public static let unreachable = PollResult(reachable: false, status: nil)
|
|
}
|
|
|
|
/// A place we can send playback to. Reference type so an implementation can hold
|
|
/// connection state (e.g. a pinned SSH endpoint). Verbs translate onto an
|
|
/// existing backend — the playback intelligence lives there, not here.
|
|
public protocol PlayerTarget: AnyObject {
|
|
var id: String { get }
|
|
var name: String { get }
|
|
var kind: HostKind { get }
|
|
var detail: String { get } // human-readable endpoint, for the Hosts view
|
|
var volumeScale: Int { get } // max value for the volume slider (percent)
|
|
|
|
func poll() async -> PollResult
|
|
func playPause() async // toggle
|
|
func resume() async // ensure playing
|
|
func setVolume(_ percent: Int) async
|
|
func seek(relative seconds: Int) async
|
|
func seek(toSeconds seconds: Int) async // absolute — for the scrubber + resume
|
|
func next() async
|
|
func previous() async
|
|
func stop() async
|
|
}
|