Files
Flaro/Packages/FlixyKit/Sources/PlexKit/Discovery/PlexConnectionSelector.swift
T
ScarriffleandClaude Opus 5 5b2b86b66b MediaKit: die Grundschicht, auf der Plex und Jellyfin dasselbe meinen
Erster Schritt zu Jellyfin, und er besteht vor allem aus Wegnehmen.

`PlaybackDecider` nahm bisher Plex-Modelle entgegen und las daraus sechs
Felder. Das band die Entscheidung an einen bestimmten Server, obwohl an
der Frage „kann iOS das öffnen?" nichts serverspezifisch ist. Jetzt nimmt
er ein `MediaProfile` — Container, Codecs, Bitrate, ob die Datei
untersucht ist.

Wie ernst die Kopplung war, zeigte der Player selbst: für die
Offline-Wiedergabe baute er sich **erfundene** Plex-Werte zusammen — eine
Media mit Kennung 0, ein Part mit dem Dateinamen als Schlüssel,
zusammengesetzte Spurlisten —, nur damit die Signatur passte. Für eine
Datei, die längst auf dem Gerät lag und mit Plex nichts mehr zu tun
hatte. Dieser Hack ist weg.

Neu ist das Target `MediaKit`: Typen und Rechnungen, die für jeden Server
gelten, ohne eine einzige Abhängigkeit. Darin `MediaProfile` und die
`Duration`-Brücke, die bisher in PlexKit stand — „weil es die unterste
Schicht ist". Das stimmte, solange es nur einen Server gab.

Damit kennen Wiedergabe und Downloads Plex nicht mehr. Zwei der
Abhängigkeiten waren ohnehin tot; die dritte trug nur diese Brücke.

Die wichtigste Testtabelle des Projekts ist mitgewandert und dabei
kürzer geworden: dort steht jetzt, worauf es ankommt, statt vollständiger
Modelle mit Kennungen und Dateipfaden, von denen niemand etwas las.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:26:19 +02:00

129 lines
4.8 KiB
Swift

import Foundation
import MediaKit
/// Sucht den besten erreichbaren Weg zu einem Server.
///
/// Alle Adressen werden **gleichzeitig** geprüft, nicht Rang für Rang. Der
/// Unterschied ist erheblich: geht man die Ränge nacheinander durch, kostet
/// eine nicht erreichbare lokale Adresse erst die volle Zeitgrenze, bevor der
/// direkte Weg überhaupt beginnt. Aus zwei Wartezeiten wird so eine.
///
/// Die Rangordnung bleibt trotzdem: gewinnt eine lokale Adresse, wird sofort
/// abgebrochen — besser kann es nicht werden. Sonst wird gesammelt und am Ende
/// die beste erreichbare genommen. Ein reines „wer zuerst antwortet" wäre
/// falsch, weil die Vermittlung von Plex sich oft am schnellsten meldet und
/// zugleich die langsamste Leitung hat.
public struct PlexConnectionSelector: Sendable {
private let identity: PlexClientIdentity
private let transport: any PlexTransport
private let probeTimeout: Duration
public init(
identity: PlexClientIdentity,
transport: any PlexTransport = URLSession.shared,
probeTimeout: Duration = .seconds(4)
) {
self.identity = identity
self.transport = transport
self.probeTimeout = probeTimeout
}
public func selectConnection(for resource: PlexResource) async -> PlexConnection? {
guard let token = resource.accessToken, !resource.connections.isEmpty else { return nil }
return await withTaskGroup(of: PlexConnection?.self) { group in
for connection in resource.connections {
group.addTask {
await reachable(connection, token: token, expecting: resource.clientIdentifier)
? connection
: nil
}
}
var best: PlexConnection?
for await candidate in group {
guard let candidate else { continue }
if best.map({ candidate.tier < $0.tier }) ?? true {
best = candidate
}
// Eine lokale Adresse ist nicht zu schlagen. Auf die übrigen
// zu warten würde die Anmeldung nur verzögern.
if best?.tier == 0 {
group.cancelAll()
break
}
}
return best
}
}
/// Fragt `/identity` ab und gleicht die Server-Kennung ab.
private func reachable(
_ connection: PlexConnection,
token: String,
expecting clientIdentifier: String
) async -> Bool {
guard let baseURL = connection.url else { return false }
let client = PlexHTTPClient(
baseURL: baseURL,
identity: identity,
token: token,
transport: transport
)
do {
let response = try await withTimeout(probeTimeout) {
try await client.send(
PlexRequest(path: "/identity", timeout: probeTimeout),
as: PlexIdentity.self
)
}
// Der Abgleich ist keine Förmlichkeit: eine veraltete
// plex.direct-Adresse kann auf einen fremden Server zeigen, und
// ohne Prüfung würde die App fremde Mediatheken anzeigen.
return response?.machineIdentifier == clientIdentifier
} catch {
return false
}
}
/// Bricht ab, wenn die Arbeit zu lange dauert.
///
/// Zusätzlich zur Zeitgrenze der Anfrage selbst, damit die Grenze auch bei
/// einem Transport greift, der sie nicht beachtet — in den Tests etwa.
/// Liefert `nil`, wenn die Zeit abgelaufen ist.
private func withTimeout<T: Sendable>(
_ duration: Duration,
_ operation: @escaping @Sendable () async throws -> T
) async throws -> T? {
try await withThrowingTaskGroup(of: T?.self) { group in
group.addTask { try await operation() }
group.addTask {
try? await Task.sleep(for: duration)
return nil
}
let first = try await group.next() ?? nil
group.cancelAll()
return first
}
}
}
/// Die Antwort von `/identity`.
public struct PlexIdentity: Sendable, Decodable {
public let machineIdentifier: String
public let version: String?
private enum OuterKeys: String, CodingKey { case mediaContainer = "MediaContainer" }
private enum InnerKeys: String, CodingKey { case machineIdentifier, version }
public init(from decoder: any Decoder) throws {
let outer = try decoder.container(keyedBy: OuterKeys.self)
let inner = try outer.nestedContainer(keyedBy: InnerKeys.self, forKey: .mediaContainer)
self.machineIdentifier = try inner.decode(String.self, forKey: .machineIdentifier)
self.version = try inner.decodeIfPresent(String.self, forKey: .version)
}
}