import Foundation import Observation import PlexKit /// Was abgespielt werden soll. struct PlaybackRequest: Identifiable, Hashable { let id = UUID() let ratingKey: String /// Für die Titelzeile, solange die Metadaten noch geladen werden. let title: String /// Welche Fassung, wenn ein Titel in mehreren vorliegt. let mediaIndex: Int /// Ab wo — aus dem gemeldeten Fortschritt. let startTime: Duration /// Eine bereits feststehende Adresse. /// /// Für Live-TV. Ein Sender liegt nicht in der Mediathek, und der übliche /// Weg — Kennung nachschlagen, Datei suchen — findet für ihn nichts. /// Daran ist das Einschalten gescheitert: die Adresse war längst /// berechnet und wurde weggeworfen. var streamURL: URL? static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } } /// Nimmt Abspielwünsche von überall entgegen. /// /// Ohne diese Stelle bräuchte jeder Bildschirm, von dem aus sich etwas starten /// lässt — Aufmacher, Reihe, Detailseite, Suche — seinen eigenen /// Vollbild-Übergang. Vier Kopien desselben Ablaufs, die auseinanderdriften. @MainActor @Observable final class PlaybackCoordinator { var request: PlaybackRequest? func play(_ item: PlexItem, mediaIndex: Int = 0, resuming: Bool = true) { request = PlaybackRequest( ratingKey: item.ratingKey, title: item.playbackTitle, mediaIndex: mediaIndex, startTime: resuming ? item.resumeTime : .zero ) } func dismiss() { request = nil } } extension PlexItem { /// Wie der Titel im Player heißt. /// /// Bei Folgen ist der eigene Titel allein nutzlos — „Die erste Folge" /// sagt nichts darüber, welche Serie läuft. var playbackTitle: String { if kind == .episode, let series = grandparentTitle { return "\(series) · \(title)" } return title } /// Ab welcher Stelle fortgesetzt wird. var resumeTime: Duration { guard let viewOffset, viewOffset > 0 else { return .zero } return .milliseconds(viewOffset) } /// Die Beschriftung unter einer Kachel. /// /// Sie ordnet ein, was darüber steht: zu einer Folge gehört die Serie, zu /// einer Staffel ihre Nummer. Die Reihenfolge ist Absicht — wird der /// Platz eng, bleibt der Serienname stehen und die Folgennummer fällt weg. var cardSubtitle: String? { switch kind { case .episode: var code: String? if let season = parentIndex, let episode = index { code = String(localized: "episode.short \(season) \(episode)") } guard let series = grandparentTitle else { return code } return code.map { "\(series) · \($0)" } ?? series case .season: // Ohne die Serie darüber wäre „Staffel 10" beliebig — deshalb // steht die Nummer hier und der Serienname als Titel. if let index { return String(localized: "season.short \(index)") } return parentTitle == nil ? nil : title default: return year.map(String.init) } } }