diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index 8047db1..f4e7c17 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -428,10 +428,21 @@ extension PlexServer { trace: trace ) } + // Belegt, aber nicht abspielbar: den Tuner sofort wieder // freigeben, statt ihn für nichts zu halten. if let subscription = outcome.subscription { await endLiveSession(subscriptionKey: subscription) + + // Und hier aufhören. Dass das Belegen klappt, beantwortet die + // Frage nach dem Weg — die übrigen zwanzig Abfragen sind dann + // nur noch Lärm im Bericht und Last für den Server. + return LiveTuneResult( + url: nil, + sessionIdentifier: session, + subscriptionKey: nil, + trace: trace + ) } } @@ -691,12 +702,17 @@ extension PlexServer { // angelegt hat, wird umgerechnet — nicht die Abspielliste darin und // nicht meine eigene Kennung. Ihr Schlüssel steht in der Antwort. if let sessionKey = Self.firstSessionKey(in: data) { - for withProfile in [true, false] { + // Voll, ohne Profil, und auf das Nötigste zusammengestrichen. + // Eine nackte 400 sagt nicht, welche Angabe sie stört; die drei + // Formen nebeneinander sagen es. + let variants: [(profile: Bool, minimal: Bool)] = [(true, false), (false, false), (true, true)] + for variant in variants { let outcome = await attemptStream( path: sessionKey, ratingKey: channel.id, mediaIndex: 0, - includeProfile: withProfile, + includeProfile: variant.profile, + minimal: variant.minimal, maximumBitrate: maximumBitrate, session: session ) @@ -706,11 +722,19 @@ extension PlexServer { } // Und die Abspielliste unmittelbar. Sie antwortete mit 404 statt 400: - // die Adresse stimmt, die Ressource steht nur nicht bereit. + // die Adresse stimmt, die Ressource steht nur nicht bereit. Ein + // Empfänger ist ein Stück Hardware — er braucht einen Moment, bis er + // abgestimmt hat. Deshalb ein zweiter Versuch nach kurzer Pause. if let direct = directStreamURL(forPath: part) { - let outcome = await probeStream(url: direct) - probes.append(ChannelProbe(path: "\(part) (unmittelbar)", result: outcome)) - if outcome.hasPrefix("HTTP 200") { return (direct, subscription, probes) } + for attempt in 0..<2 { + if attempt > 0 { try? await Task.sleep(for: .seconds(2)) } + let outcome = await probeStream(url: direct) + probes.append(ChannelProbe( + path: "\(part) (unmittelbar\(attempt > 0 ? ", nach 2 s" : ""))", + result: outcome + )) + if outcome.hasPrefix("HTTP 200") { return (direct, subscription, probes) } + } } let outcome = await attemptStream( @@ -825,6 +849,7 @@ extension PlexServer { mediaIndex: Int, includeProfile: Bool, keepPathEncoding: Bool = true, + minimal: Bool = false, maximumBitrate: Int?, session: String ) async -> (url: URL?, probe: ChannelProbe) { @@ -841,7 +866,8 @@ extension PlexServer { guard let url = transcodeURL( for: request, includeProfile: includeProfile, - keepPathEncoding: keepPathEncoding + keepPathEncoding: keepPathEncoding, + minimal: minimal ) else { return (nil, ChannelProbe(path: path, result: "Adresse nicht baubar")) } @@ -849,8 +875,16 @@ extension PlexServer { let outcome = await probeStream(url: url) let label = "\(path) (mediaIndex \(mediaIndex)" + "\(includeProfile ? "" : ", ohne Profil")" - + "\(keepPathEncoding ? "" : ", doppelt kodiert"))" - return (outcome.hasPrefix("HTTP 200") ? url : nil, ChannelProbe(path: label, result: outcome)) + + "\(keepPathEncoding ? "" : ", doppelt kodiert")" + + "\(minimal ? ", nur das Nötigste" : "") )" + + // Die gebaute Adresse mit in die Spur. Ohne sie diskutiert man über + // eine Anfrage, die niemand gesehen hat — und genau das habe ich + // mehrere Runden lang getan. + let detail = outcome.hasPrefix("HTTP 200") + ? outcome + : "\(outcome)\n \(redacted(url.absoluteString))" + return (outcome.hasPrefix("HTTP 200") ? url : nil, ChannelProbe(path: label, result: detail)) } /// Welche Adresse mit welchem Index umzurechnen ist. @@ -1320,7 +1354,7 @@ extension PlexServer { } } - private nonisolated func redacted(_ text: String) -> String { + nonisolated func redacted(_ text: String) -> String { text.replacingOccurrences(of: token, with: "«token»") } } diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift index e7e716d..0b92653 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+Playback.swift @@ -23,7 +23,8 @@ extension PlexServer { public nonisolated func transcodeURL( for request: TranscodeRequest, includeProfile: Bool = true, - keepPathEncoding: Bool = true + keepPathEncoding: Bool = true, + minimal: Bool = false ) -> URL? { guard let baseURL = connection.url, var components = URLComponents( @@ -32,9 +33,24 @@ extension PlexServer { ) else { return nil } - let items = request.queryItems + [ - URLQueryItem(name: "X-Plex-Token", value: token), - ] + (includeProfile ? clientProfileItems : []) + identity.queryItems + // Das Nötigste, um herauszufinden, an welcher Angabe der Server + // Anstoß nimmt. Eine nackte 400 sagt nicht, welcher Parameter ihr + // nicht passt — halbieren sagt es. + let items = minimal + ? [ + URLQueryItem(name: "path", value: request.percentEncodedPath), + URLQueryItem(name: "protocol", value: "hls"), + URLQueryItem(name: "hasMDE", value: "1"), + URLQueryItem(name: "mediaIndex", value: String(request.mediaIndex)), + URLQueryItem(name: "partIndex", value: String(request.partIndex)), + URLQueryItem(name: "session", value: request.sessionIdentifier), + URLQueryItem(name: "X-Plex-Session-Identifier", value: request.sessionIdentifier), + URLQueryItem(name: "X-Plex-Token", value: token), + URLQueryItem(name: "X-Plex-Client-Identifier", value: identity.clientIdentifier), + ] + : request.queryItems + [ + URLQueryItem(name: "X-Plex-Token", value: token), + ] + (includeProfile ? clientProfileItems : []) + identity.queryItems components.queryItems = items components.percentEncodedQueryItems = components.percentEncodedQueryItems?.map { item in