Phase 2: Transcoding, adaptive Bitrate, Spuren, Marker, Sync
Der Decider brauchte keine Änderung — nur eine andere Richtlinie. Genau dafür war er gebaut. Transcoding - TranscodeRequest baut die Parameter für /video/:/transcode/universal. directStream=1 lässt Plex nur umpacken, wenn der Bildcodec passt, statt pauschal neu zu kodieren. - Vor dem Start wird der Server gefragt (decision); sagt er nein, steht der Grund im Klartext da statt eines schwarzen Bildes. - Beim Verlassen wird der Transcoder angehalten. Ohne dieses Signal rechnet der Server minutenlang für niemanden weiter. - Die adaptive Bitrate kommt mit HLS: AVPlayer wechselt sie selbst. Qualitätseinstellungen - Getrennt für WLAN und Mobilfunk, fünf spürbare Stufen. Voreinstellung: zuhause Original, unterwegs 4 Mbit. - NetworkConditions erkennt Mobilfunk über isExpensive — das deckt auch den Hotspot ab, der technisch WLAN ist und trotzdem Volumen kostet. Spuren und Marken - Ton- und Untertitelwahl im Player, bevorzugte Sprache in den Einstellungen, nachsichtiger Sprachvergleich (de/deu/ger/de-DE). - Intro- und Abspann-Marken mit Überspringen-Knopf, optional automatisch. Bereits übersprungene Marken werden gemerkt, sonst käme man nach einem Rücksprung nie ins Intro zurück. - Nächste Folge inkl. Staffelwechsel; Specials in Staffel 0 werden nicht vorgezogen. Fortschritt - Alle zehn Sekunden an den Server, plus Schlussmeldung beim Verlassen. Erst die nimmt den Titel aus „wird gerade abgespielt" heraus. - markWatched/markUnwatched/rate/refreshLibrary stehen bereit. 86 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import PlexKit
|
||||
|
||||
/// Die Suche nach der nächsten Folge.
|
||||
///
|
||||
/// Plex kennt dafür keinen Endpunkt — der Weg führt über die Geschwister, und
|
||||
/// am Staffelende über die nächste Staffel. Genau dort bricht es ohne diese
|
||||
/// Logik ab, und zwar an der Stelle, an der man es am wenigsten gebrauchen
|
||||
/// kann: mitten in einer Serie.
|
||||
@Suite("Nächste Folge")
|
||||
struct NextEpisodeTests {
|
||||
|
||||
private func episode(
|
||||
ratingKey: String,
|
||||
index: Int,
|
||||
season: Int,
|
||||
seasonKey: String,
|
||||
showKey: String = "show-1"
|
||||
) -> String {
|
||||
"""
|
||||
{
|
||||
"ratingKey": "\(ratingKey)", "type": "episode", "title": "Folge \(index)",
|
||||
"index": \(index), "parentIndex": \(season),
|
||||
"parentRatingKey": "\(seasonKey)", "grandparentRatingKey": "\(showKey)",
|
||||
"grandparentTitle": "Eine Serie"
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private func container(_ items: [String]) -> String {
|
||||
"""
|
||||
{"MediaContainer":{"size":\(items.count),"Metadata":[\(items.joined(separator: ","))]}}
|
||||
"""
|
||||
}
|
||||
|
||||
private func season(ratingKey: String, index: Int) -> String {
|
||||
"""
|
||||
{"ratingKey":"\(ratingKey)","type":"season","title":"Staffel \(index)","index":\(index)}
|
||||
"""
|
||||
}
|
||||
|
||||
private func server(routes: [String: String]) -> PlexServer {
|
||||
PlexServer(
|
||||
resource: PlexResource(
|
||||
name: "Test",
|
||||
clientIdentifier: "srv",
|
||||
accessToken: "token",
|
||||
connections: [PlexConnection(uri: "http://server:32400", local: true)]
|
||||
),
|
||||
connection: PlexConnection(uri: "http://server:32400", local: true),
|
||||
identity: .test,
|
||||
transport: FakeTransport.routes(routes)
|
||||
)!
|
||||
}
|
||||
|
||||
private func current(index: Int, season: Int = 1, seasonKey: String = "season-1") throws -> PlexItem {
|
||||
let data = Data(container([episode(ratingKey: "e\(index)", index: index, season: season, seasonKey: seasonKey)]).utf8)
|
||||
let decoded = try PlexResponse.decode(PlexMediaContainer<PlexItem>.self, from: data)
|
||||
return try #require(decoded.content.first)
|
||||
}
|
||||
|
||||
@Test("Innerhalb einer Staffel kommt die nächsthöhere Folge")
|
||||
func nextWithinSeason() async throws {
|
||||
let server = server(routes: [
|
||||
"/library/metadata/season-1/children": container([
|
||||
episode(ratingKey: "e1", index: 1, season: 1, seasonKey: "season-1"),
|
||||
episode(ratingKey: "e2", index: 2, season: 1, seasonKey: "season-1"),
|
||||
episode(ratingKey: "e3", index: 3, season: 1, seasonKey: "season-1"),
|
||||
]),
|
||||
])
|
||||
|
||||
let next = try await server.nextEpisode(after: try current(index: 1))
|
||||
#expect(next?.ratingKey == "e2")
|
||||
}
|
||||
|
||||
@Test("Lücken in der Nummerierung überspringen nichts Falsches")
|
||||
func handlesGapsInNumbering() async throws {
|
||||
// Fehlt eine Folge auf der Platte, darf nicht die Folge mit der
|
||||
// höchsten Nummer kommen, sondern die nächsterreichbare.
|
||||
let server = server(routes: [
|
||||
"/library/metadata/season-1/children": container([
|
||||
episode(ratingKey: "e1", index: 1, season: 1, seasonKey: "season-1"),
|
||||
episode(ratingKey: "e5", index: 5, season: 1, seasonKey: "season-1"),
|
||||
episode(ratingKey: "e9", index: 9, season: 1, seasonKey: "season-1"),
|
||||
]),
|
||||
])
|
||||
|
||||
let next = try await server.nextEpisode(after: try current(index: 1))
|
||||
#expect(next?.ratingKey == "e5")
|
||||
}
|
||||
|
||||
@Test("Am Staffelende geht es in der nächsten Staffel weiter")
|
||||
func rollsOverToNextSeason() async throws {
|
||||
let server = server(routes: [
|
||||
"/library/metadata/season-1/children": container([
|
||||
episode(ratingKey: "e1", index: 1, season: 1, seasonKey: "season-1"),
|
||||
episode(ratingKey: "e2", index: 2, season: 1, seasonKey: "season-1"),
|
||||
]),
|
||||
"/library/metadata/show-1/children": container([
|
||||
season(ratingKey: "season-1", index: 1),
|
||||
season(ratingKey: "season-2", index: 2),
|
||||
]),
|
||||
"/library/metadata/season-2/children": container([
|
||||
episode(ratingKey: "s2e1", index: 1, season: 2, seasonKey: "season-2"),
|
||||
episode(ratingKey: "s2e2", index: 2, season: 2, seasonKey: "season-2"),
|
||||
]),
|
||||
])
|
||||
|
||||
let next = try await server.nextEpisode(after: try current(index: 2))
|
||||
#expect(next?.ratingKey == "s2e1")
|
||||
}
|
||||
|
||||
@Test("Nach der letzten Folge der letzten Staffel kommt nichts mehr")
|
||||
func stopsAtTheEnd() async throws {
|
||||
let server = server(routes: [
|
||||
"/library/metadata/season-1/children": container([
|
||||
episode(ratingKey: "e1", index: 1, season: 1, seasonKey: "season-1"),
|
||||
]),
|
||||
"/library/metadata/show-1/children": container([season(ratingKey: "season-1", index: 1)]),
|
||||
])
|
||||
|
||||
let next = try await server.nextEpisode(after: try current(index: 1))
|
||||
#expect(next == nil)
|
||||
}
|
||||
|
||||
@Test("Ein Film hat keine nächste Folge")
|
||||
func moviesHaveNoNext() async throws {
|
||||
let data = Data(#"{"MediaContainer":{"Metadata":[{"ratingKey":"m1","type":"movie","title":"Ein Film"}]}}"#.utf8)
|
||||
let movie = try #require(try PlexResponse.decode(PlexMediaContainer<PlexItem>.self, from: data).content.first)
|
||||
|
||||
let next = try await server(routes: [:]).nextEpisode(after: movie)
|
||||
#expect(next == nil)
|
||||
}
|
||||
|
||||
@Test("Staffel 0 mit Specials wird nicht vorgezogen")
|
||||
func specialsDoNotJumpAhead() async throws {
|
||||
// Plex führt Specials als Staffel 0. Sie stehen in der Liste vor der
|
||||
// ersten regulären Staffel — würde einfach die erste genommen, landete
|
||||
// man nach dem Staffelfinale bei einem Weihnachtsspecial.
|
||||
let server = server(routes: [
|
||||
"/library/metadata/season-1/children": container([
|
||||
episode(ratingKey: "e1", index: 1, season: 1, seasonKey: "season-1"),
|
||||
]),
|
||||
"/library/metadata/show-1/children": container([
|
||||
season(ratingKey: "season-0", index: 0),
|
||||
season(ratingKey: "season-1", index: 1),
|
||||
season(ratingKey: "season-2", index: 2),
|
||||
]),
|
||||
"/library/metadata/season-2/children": container([
|
||||
episode(ratingKey: "s2e1", index: 1, season: 2, seasonKey: "season-2"),
|
||||
]),
|
||||
])
|
||||
|
||||
let next = try await server.nextEpisode(after: try current(index: 1))
|
||||
#expect(next?.ratingKey == "s2e1")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user