import Foundation import MediaKit /// Ein Abschnitt, den man überspringen können soll. /// /// Jellyfin nennt sie seit 10.10 „Media Segments" und liefert sie über einen /// eigenen Weg — **nicht** als Feld am Titel. Deshalb steht das hier und nicht /// in `JellyfinItem`. /// /// Gefüllt werden sie von einem Anbieter im Server, etwa dem Intro-Skipper. /// Hat der Server keinen, kommt eine leere Liste — oder gar nichts, wenn er /// älter ist als 10.10. Beides ist kein Fehler, sondern heisst schlicht: es /// gibt hier nichts zu überspringen. struct JellyfinSegment: Decodable, Sendable { let type: String? /// In Hundertnanosekunden, wie alles Zeitliche bei Jellyfin. let startTicks: Int64? let endTicks: Int64? enum CodingKeys: String, CodingKey { case type = "Type" case startTicks = "StartTicks" case endTicks = "EndTicks" } /// `nil`, wenn der Abschnitt keiner ist, mit dem die App etwas anfangen /// kann — etwa eine Vorschau oder ein Rückblick. Lieber nichts anbieten /// als einen Knopf, der an einer unerwarteten Stelle auftaucht. var asMediaMarker: MediaMarker? { guard let startTicks, let endTicks, endTicks > startTicks else { return nil } let kind: MediaMarker.Kind switch type?.lowercased() { case "intro": kind = .intro case "outro": kind = .credits case "commercial": kind = .commercial default: return nil } return MediaMarker( kind: kind, startMilliseconds: Int(startTicks / 10_000), endMilliseconds: Int(endTicks / 10_000) ) } } /// Die Antwort ist eine Seite wie jede andere bei Jellyfin. private struct JellyfinSegmentPage: Decodable { let items: [JellyfinSegment] enum CodingKeys: String, CodingKey { case items = "Items" } } extension JellyfinBackend { /// Holt Intro und Abspann zu einem Titel. /// /// **Wirft nicht.** Ein Server ohne Anbieter antwortet mit einer leeren /// Liste, einer vor 10.10 mit einem 404 — und ein Film ohne Intro ist /// kein Fehler, sondern der Normalfall. Daran die Detailseite scheitern /// zu lassen, wäre die schlechtere Hälfte des Tauschs. /// /// **`includeSegmentTypes` zweimal, nicht einmal mit Komma.** Als /// `Intro,Outro` beantwortet Jellyfin dieselbe Frage mit null Treffern; /// als zwei Angaben kommen sie. Das steht nirgends in der Dokumentation /// und kostet sonst eine Stunde Suche am falschen Ende. func segments(forItem id: String) async -> [MediaMarker] { do { let page = try await client.send( JellyfinRequest( path: "/MediaSegments/\(id)", query: [ URLQueryItem(name: "includeSegmentTypes", value: "Intro"), URLQueryItem(name: "includeSegmentTypes", value: "Outro"), ] ), as: JellyfinSegmentPage.self ) return page.items.compactMap(\.asMediaMarker).sorted { $0.startMilliseconds < $1.startMilliseconds } } catch { return [] } } }