import Foundation import Observation import FlixyPlayback import PlexKit /// Hält fest, was beim letzten Abspielversuch passiert ist. /// /// Der Player verschwindet, wenn man ihn schließt — und mit ihm jeder Hinweis /// darauf, woran es lag. Dieser Mitschnitt überlebt das und steht in den /// Einstellungen. Aus „es läuft nicht" wird damit „VLC, lokale Verbindung, /// hängt bei Öffnen" — und das ist ein Unterschied, der Stunden spart. @MainActor @Observable final class PlaybackDiagnostics { struct Attempt: Identifiable { let id = UUID() let startedAt: Date var title: String var method: String var connection: String /// Die Adresse ohne Abfrageteil. /// /// Der Token steckt in der Abfrage. Er hat in einem Protokoll nichts /// zu suchen — es landet in Screenshots, und wer ihn hat, kommt an die /// ganze Mediathek. var sourceDescription: String var steps: [String] = [] var outcome: String? } private(set) var lastAttempt: Attempt? func begin(title: String, connection: PlexConnection?) { lastAttempt = Attempt( startedAt: Date(), title: title, method: "—", connection: connection.map(Self.describe) ?? "—", sourceDescription: "—" ) note("Player geöffnet") } func record(decision: PlaybackDecision, source: URL) { lastAttempt?.method = Self.describe(decision) lastAttempt?.sourceDescription = Self.describe(source) note("Quelle gewählt") } /// Ein Zwischenschritt, mit Zeitabstand zum Beginn. /// /// Der Abstand ist die eigentliche Auskunft: steht zwischen „Quelle /// gewählt" und „spielt" eine Minute, liegt es an der Leitung; kommt /// „spielt" gar nicht, am Motor. func note(_ step: String) { guard var attempt = lastAttempt else { return } let elapsed = Date().timeIntervalSince(attempt.startedAt) attempt.steps.append(String(format: "%.1fs %@", elapsed, step)) lastAttempt = attempt } func finish(outcome: String) { lastAttempt?.outcome = outcome note(outcome) } private static func describe(_ decision: PlaybackDecision) -> String { switch decision.method { case .directPlay(let engine): switch engine { case .avPlayer: "Direkt · AVPlayer" case .vlc: "Direkt · VLC" } case .transcode(let limit): limit.map { "Transcode · max. \($0 / 1000) Mbit" } ?? "Transcode" } } private static func describe(_ connection: PlexConnection) -> String { if connection.local { return "Lokal · \(connection.uri)" } if connection.relay { return "Plex-Vermittlung (gedrosselt)" } return "Direkt von außen · \(connection.uri)" } private static func describe(_ url: URL) -> String { guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "—" } components.query = nil return components.string ?? "—" } }