From 93f4dfd8e925bafd8f7188d6107923e3ad4e1f5b Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Wed, 14 Oct 2026 08:39:31 +0200 Subject: [PATCH] Ein frischer JSONDecoder, drei Zeilen neben der Warnung davor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Im Web stand am Abspann „Nächste Folge", in der App nur „Abspann überspringen". Der Server war es nicht: beide fragen denselben Endpunkt, und der ordnet nach Staffel und dann nach Nummer — er findet also auch die erste Folge der nächsten Staffel. Es war `nachbar`. Die Stelle baute sich einen frischen `JSONDecoder()`, und der kennt das Datumsformat des Servers nicht. Solange die Nachbarfolge weder `publishAt` noch `lastPlayedAt` mitbrachte, ging es gut — mit eingeschalteter Prüfung bringt sie ein `publishAt`. Das `try?` schluckte den Fehler, und von aussen sah es aus, als gäbe es keine nächste Folge: kein Knopf, kein Hinweis, nichts. In `FlaroClient` steht seit je ein eingerichteter Dekoder, ausdrücklich nicht privat, „damit die Prüfungen denselben Dekoder benutzen" — und direkt darunter die Warnung, dass ein frischer an genau diesem Format scheitert. Diese eine Stelle las sie nicht. Vier Prüfungen dazu, darunter die mit Datumsangaben: mit dem alten Dekoder ist sie rot, mit dem neuen grün. Gegengeprüft, nicht behauptet. Die übrigen frischen Dekoder im Quelltext lesen nur, was dieselbe App vorher selbst geschrieben hat — Zwischenspeicher, Abzüge, Offline- Angaben. Die laufen rund und bleiben. Co-Authored-By: Claude Opus 5 --- .../Sources/FlaroServerKit/FlaroBackend.swift | 9 +- .../NachbarfolgeTests.swift | 100 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 Packages/FlaroKit/Tests/FlaroServerKitTests/NachbarfolgeTests.swift diff --git a/Packages/FlaroKit/Sources/FlaroServerKit/FlaroBackend.swift b/Packages/FlaroKit/Sources/FlaroServerKit/FlaroBackend.swift index 2924beb..5110f97 100644 --- a/Packages/FlaroKit/Sources/FlaroServerKit/FlaroBackend.swift +++ b/Packages/FlaroKit/Sources/FlaroServerKit/FlaroBackend.swift @@ -254,7 +254,14 @@ public actor FlaroBackend: MediaBackend { guard item.kind == .episode else { return nil } let daten = try await client.send(FlaroRequest(path: "/api/v1/episodes/\(item.id)/\(weg)")) guard !daten.isEmpty else { return nil } - guard let folge = try? JSONDecoder().decode(Wire.Episode.self, from: daten) else { return nil } + // **Der eingerichtete Dekoder, nicht ein frischer.** Ein frischer + // kennt das Datumsformat des Servers nicht; sobald die Nachbarfolge + // ein `publishAt` oder ein `lastPlayedAt` mitbringt — und mit + // eingeschalteter Prüfung bringt sie es —, scheiterte das Lesen. + // Das `try?` schluckte den Fehler, und von aussen sah es aus, als + // gäbe es keine nächste Folge: der Knopf blieb weg. + guard let folge = try? FlaroClient.decoder.decode( + Wire.Episode.self, from: daten) else { return nil } return folge.neutral() } diff --git a/Packages/FlaroKit/Tests/FlaroServerKitTests/NachbarfolgeTests.swift b/Packages/FlaroKit/Tests/FlaroServerKitTests/NachbarfolgeTests.swift new file mode 100644 index 0000000..16c976b --- /dev/null +++ b/Packages/FlaroKit/Tests/FlaroServerKitTests/NachbarfolgeTests.swift @@ -0,0 +1,100 @@ +import Foundation +import Testing +import MediaKit +@testable import FlaroServerKit + +/// Die Folge nach dieser — über die Staffelgrenze hinweg. +/// +/// **Der Anlass.** Im Web stand am Abspann „Nächste Folge", in der App nur +/// „Abspann überspringen". Der Server war es nicht: beide fragen denselben +/// Endpunkt, und er ordnet nach Staffel und dann nach Nummer, findet also +/// auch die erste Folge der nächsten Staffel. +/// +/// **Es war der Dekoder.** `nachbar` baute sich einen frischen +/// `JSONDecoder()`, und der kennt das Datumsformat des Servers nicht. +/// Solange die Nachbarfolge weder `publishAt` noch `lastPlayedAt` hatte, +/// ging es gut — mit eingeschalteter Prüfung hat sie ein `publishAt`. Das +/// `try?` schluckte den Fehler, und von aussen sah es aus, als gäbe es +/// keine nächste Folge. +@Suite("Flaro: Nachbarfolge") +struct NachbarfolgeTests { + private func backend(_ transport: any FlaroTransport) -> FlaroBackend { + FlaroBackend( + client: FlaroClient( + baseURL: Proben.adresse, identity: Proben.kennung, token: "t", + transport: transport, cookieJar: nil), + account: FlaroAccount( + baseURL: Proben.adresse, serverName: "Wohnzimmer", + userID: "u1", userName: "guido", isAdministrator: false)) + } + + private static func folge( + id: String, staffel: Int, nummer: Int, + publishAt: String = "null", zuletzt: String = "null" + ) -> String { + """ + { + "id": "\(id)", "season": \(staffel), "episode": \(nummer), + "title": "Neuanfang", "overview": null, "stillImage": null, + "files": [{ + "id": "d-\(id)", "fileName": "F.mkv", "sizeBytes": 1, + "container": "mkv", "durationSeconds": 2700, "streams": [] + }], + "state": { "positionSeconds": 0, "runtimeSeconds": 2700, + "watched": false, "playCount": 0, "favourite": false, + "rating": null, "dismissed": false, + "lastPlayedAt": \(zuletzt), "inProgress": false }, + "publishAt": \(publishAt), "seriesId": "serie-1", "markers": [] + } + """ + } + + private static let folgeJetzt = MediaItem( + id: "s1e10", kind: .episode, title: "Finale", index: 10, parentIndex: 1) + + /// Das Staffelfinale: die nächste Folge ist die erste der nächsten Staffel. + @Test("Über die Staffelgrenze hinweg") + func ueberDieStaffelgrenze() async throws { + let backend = backend(Attrappe(rumpf: Self.folge(id: "s2e1", staffel: 2, nummer: 1))) + + let naechste = try #require(await backend.nextEpisode(after: Self.folgeJetzt)) + + #expect(naechste.id == "s2e1") + #expect(naechste.parentIndex == 2) + #expect(naechste.index == 1) + } + + /// **Der Fehler selbst.** Mit `publishAt` und `lastPlayedAt` scheiterte + /// das Lesen stillschweigend, und der Knopf blieb weg. Mit dem frischen + /// `JSONDecoder()` von vorher ist diese Prüfung rot. + @Test("Auch mit Datumsangaben — daran scheiterte es") + func mitDatumsangaben() async throws { + let backend = backend(Attrappe(rumpf: Self.folge( + id: "s2e1", staffel: 2, nummer: 1, + publishAt: "\"2026-10-01T18:00:00Z\"", + zuletzt: "\"2026-10-11T21:14:03.482Z\""))) + + let naechste = try #require(await backend.nextEpisode(after: Self.folgeJetzt)) + + #expect(naechste.id == "s2e1") + #expect(naechste.availableFrom != nil) + } + + /// Nach der letzten Folge kommt keine — 204, und das ist kein Fehler. + @Test("Hinter der letzten Folge kommt nichts") + func hinterDerLetzten() async throws { + let backend = backend(Attrappe(rumpf: "", status: 204)) + + #expect(try await backend.nextEpisode(after: Self.folgeJetzt) == nil) + } + + /// Ein Film hat keine Nachbarn — und fragt gar nicht erst. + @Test("Ein Film fragt nicht nach Nachbarn") + func filmFragtNicht() async throws { + let attrappe = Attrappe(rumpf: Self.folge(id: "x", staffel: 1, nummer: 1)) + let film = MediaItem(id: "f1", kind: .movie, title: "Arrival") + + #expect(try await backend(attrappe).nextEpisode(after: film) == nil) + #expect(attrappe.anfragen.isEmpty) + } +}