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.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.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") } }