import Foundation /// Übersetzt die JSON-Zeilen des mediaremote-adapter. /// /// Der Adapter sendet im `stream`-Modus Zeilen der Form /// `{"type":"data","diff":false,"payload":{…}}`. Bei `diff = true` enthält /// `payload` **nur die geänderten Felder** — ein vollständiges Ersetzen würde /// dann Titel und Cover löschen, sobald sich nur die Position ändert. public enum AdapterPayload { public struct Envelope: Decodable { public let type: String public let diff: Bool? public let payload: [String: AnyDecodable]? } /// Wendet eine Zeile auf den bisherigen Stand an. /// /// - Returns: der neue Stand, oder `nil` wenn die Zeile nichts aussagt. public static func apply(line: String, to previous: NowPlaying?) -> NowPlaying? { guard let data = line.data(using: .utf8), let envelope = try? JSONDecoder().decode(Envelope.self, from: data), envelope.type == "data" else { return nil } guard let payload = envelope.payload, !payload.isEmpty else { // Ein leerer Payload bei einer Vollmeldung heißt: es läuft nichts. // Bei einer Teilmeldung heißt er gar nichts und wird verworfen. return envelope.diff == true ? previous : nil } // Bei einer Teilmeldung auf dem alten Stand aufbauen, sonst neu beginnen. var state = (envelope.diff == true ? previous : nil) ?? NowPlaying() state.origin = .adapter if let value = payload["title"]?.string { state.title = value } if let value = payload["artist"]?.string { state.artist = value } if let value = payload["album"]?.string { state.album = value } if let value = payload["playing"]?.bool { state.isPlaying = value } if let value = payload["duration"]?.double { state.duration = value > 0 ? value : nil } if let value = payload["elapsedTime"]?.double { state.elapsed = value } if let value = payload["playbackRate"]?.double { state.playbackRate = value } if let value = payload["bundleIdentifier"]?.string { state.bundleIdentifier = value } if let value = payload["artworkData"]?.string { state.artwork = Data(base64Encoded: value, options: .ignoreUnknownCharacters) } // `timestamp` ist der Zeitpunkt, zu dem `elapsedTime` galt — nicht der // Empfangszeitpunkt. Ohne ihn springt der Scrubber bei jedem Ereignis. if let raw = payload["timestamp"]?.string, let parsed = ISO8601DateFormatter().date(from: raw) { state.asOf = parsed } else { state.asOf = Date() } return state } } /// Minimaler dynamischer Wert — der Payload hat kein festes Schema, und ein /// vollständiger `Decodable`-Typ müsste bei jedem neuen Feld nachgezogen werden. public struct AnyDecodable: Decodable, Sendable { private enum Storage: Sendable { case string(String), double(Double), bool(Bool), other } private let storage: Storage public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() // Reihenfolge zählt: `Bool` vor `Double`, sonst wird `true` zu 1. if let value = try? container.decode(Bool.self) { storage = .bool(value) } else if let value = try? container.decode(Double.self) { storage = .double(value) } else if let value = try? container.decode(String.self) { storage = .string(value) } else { storage = .other } } public var string: String? { if case .string(let v) = storage { v } else { nil } } public var double: Double? { if case .double(let v) = storage { v } else { nil } } public var bool: Bool? { if case .bool(let v) = storage { v } else { nil } } }