import Foundation import Observation import MediaKit /// „Spiel das jetzt" — von einer Kachel aus, ohne Umweg über die Detailseite. /// /// **Warum ein eigener Typ.** Bei einem Film ist nichts nachzuschlagen: er /// läuft sofort. Bei einer Serie schon — es muss erst feststehen, welche /// Folge dran ist, und das ist eine Serveranfrage. Diese Anfrage gehört /// weder in `PlaybackCoordinator`, der keinen Serverzugang hat und dessen /// Aufgabe die Darstellung ist, noch in die Kacheln, wo sie dreimal stünde. /// /// **Die Regel selbst steht nicht hier**, sondern in `EpisodeSelection` — als /// reiner Typ mit Tests. Hier steht nur, woher die Liste kommt. @MainActor @Observable final class QuickPlay { /// Welche Kachel gerade nachschlägt. /// /// Eine Kennung, kein Wahrheitswert: der Kreisel soll an **dieser** einen /// Kachel stehen und nicht an allen zwanzig. private(set) var resolvingID: String? /// Warum es nicht ging — einmal für die ganze App. /// /// Ein Hinweis je Kachel wäre bei einem Serverausfall ein Stapel aus /// zwanzig identischen Meldungen. var failure: String? private var task: Task? /// Startet, was hinter dieser Kachel steckt. /// /// `backend` und `coordinator` kommen als Argumente statt als gespeicherte /// Werte: so bleibt der Typ ohne Abhängigkeiten baubar, und die Sitzung /// kann den Server wechseln, ohne dass hier etwas veraltet. Dasselbe /// Muster wie bei `DownloadRefiller`. func start( _ item: MediaItem, backend: (any MediaBackend)?, coordinator: PlaybackCoordinator ) { // Der zweite Klick gewinnt. Sonst liefen zwei Abfragen, und die // langsamere überschriebe am Ende die gewollte. task?.cancel() switch item.kind { case .show, .season: resolve(item, backend: backend, coordinator: coordinator) default: // **Sofort, ohne Aufgabe und ohne Kreisel.** Der häufige Fall // soll nicht aufblitzen, nur weil der seltene eine Anfrage // braucht. resolvingID = nil coordinator.play(item) } } func cancel() { task?.cancel() task = nil resolvingID = nil } private func resolve( _ item: MediaItem, backend: (any MediaBackend)?, coordinator: PlaybackCoordinator ) { guard let backend else { failure = String(localized: "quickPlay.noServer") return } resolvingID = item.id task = Task { [weak self] in let folgen: [MediaItem] let bereich: EpisodeSelection.Scope do { if item.kind == .show { folgen = try await backend.episodes(inSeries: item.id) bereich = .series } else { // Eine Staffel ist schon eine Anfrage — `children(of:)` // genügt, und der Server muss nichts Besonderes können. folgen = try await backend.children(of: item.id) bereich = .season } } catch { guard !Task.isCancelled, let self else { return } resolvingID = nil failure = ErrorText.describe(error) return } guard !Task.isCancelled, let self else { return } resolvingID = nil guard let wahl = EpisodeSelection.nextUp(in: folgen, scope: bereich) else { failure = String(localized: "quickPlay.nothingToPlay") return } coordinator.play(wahl.episode, resuming: wahl.resumes) } } }