Ein frischer JSONDecoder, drei Zeilen neben der Warnung davor
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ce4da992ea
commit
93f4dfd8e9
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user