import Foundation /// Woher die Angaben stammen. Bestimmt, was steuerbar ist. public enum MediaOrigin: String, Equatable, Sendable { /// mediaremote-adapter — kennt alle Quellen inkl. Browser. case adapter /// AppleScript für Spotify oder Musik. case appleScript /// Nur Medientasten: Steuerung ohne Angaben. case mediaKeys } /// Was gerade läuft. public struct NowPlaying: Equatable, Sendable { public var title: String public var artist: String public var album: String public var isPlaying: Bool /// Gesamtlänge in Sekunden. `nil` bei Livestreams. public var duration: TimeInterval? /// Position **zum Zeitpunkt `asOf`** — nicht jetzt. Siehe `elapsed(at:)`. public var elapsed: TimeInterval public var playbackRate: Double /// Wann die Angaben gültig waren. public var asOf: Date public var artwork: Data? public var bundleIdentifier: String? public var origin: MediaOrigin public init(title: String = "", artist: String = "", album: String = "", isPlaying: Bool = false, duration: TimeInterval? = nil, elapsed: TimeInterval = 0, playbackRate: Double = 1, asOf: Date = Date(), artwork: Data? = nil, bundleIdentifier: String? = nil, origin: MediaOrigin = .adapter) { self.title = title self.artist = artist self.album = album self.isPlaying = isPlaying self.duration = duration self.elapsed = elapsed self.playbackRate = playbackRate self.asOf = asOf self.artwork = artwork self.bundleIdentifier = bundleIdentifier self.origin = origin } /// Die Position zum angegebenen Zeitpunkt. /// /// Der Adapter liefert `elapsedTime` **nicht** laufend nach — bei /// unveränderter Wiedergabe kommt minutenlang kein Ereignis. Ein Scrubber, /// der stur `elapsed` anzeigt, steht deshalb still, obwohl der Titel läuft. /// Er muss zwischen den Ereignissen selbst hochzählen, ausgehend von /// `asOf` und `playbackRate`. public func elapsed(at date: Date) -> TimeInterval { guard isPlaying else { return clamp(elapsed) } let advanced = elapsed + date.timeIntervalSince(asOf) * playbackRate return clamp(advanced) } /// Fortschritt von 0 bis 1, oder `nil` ohne bekannte Länge. public func progress(at date: Date) -> Double? { guard let duration, duration > 0 else { return nil } return min(max(elapsed(at: date) / duration, 0), 1) } public var hasMetadata: Bool { !title.isEmpty || !artist.isEmpty } private func clamp(_ value: TimeInterval) -> TimeInterval { // Nie über die Länge hinaus: sonst zeigt die Anzeige nach dem Ende // eines Titels weiter steigende Zahlen, solange kein Ereignis kommt. guard let duration, duration > 0 else { return max(value, 0) } return min(max(value, 0), duration) } } /// Was das Widget auslösen kann. public enum MediaCommand: Equatable, Sendable { case playPause case next case previous case seek(to: TimeInterval) }