Das Belegen des Tuners klappt, und heraus kommt:
/livetv/sessions/1b5bb998…/DBACCA6D…/index.m3u8?offset=-1.000000
Eine fertige HLS-Abspielliste. Ich habe sie an den Transcoder gegeben —
also darum gebeten, etwas umzurechnen, das schon fertig ist. Daher die
nackte 400. Sie wird jetzt unmittelbar abgerufen, mit Token und
Gerätekennung in der Adresse, weil der Systemplayer keine Kopfzeilen
mitschickt. Die eigene Abfrage der Sitzung bleibt dabei erhalten.
Dazu das Gegenstück, ebenfalls aus dem Quelltext des Plex-Clients:
`DELETE /media/subscriptions/{id}` gibt den Tuner wieder frei. Er ist ein
Stück Hardware — vier Stück gibt es, und ein nicht freigegebener fehlt
beim nächsten Einschalten. Das passiert jetzt beim Schließen des Players
und auch dann, wenn das Belegen zwar klappt, aber nichts Abspielbares
dabei herauskommt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
3.4 KiB
Swift
97 lines
3.4 KiB
Swift
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?
|
|
/// Womit der Tuner nach dem Zusehen wieder freigegeben wird.
|
|
///
|
|
/// Bei vier Tunern ist der fünfte Sender sonst nicht mehr einzuschalten.
|
|
var liveSubscriptionKey: String?
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|