Live-TV: das Programm kam nie an, und die Einschalt-Adresse wurde weggeworfen

Drei Fehler, alle im eigenen Code.

Die Sendezeiten stehen beim Programmanbieter im Medien-Block, nicht am
Eintrag darüber. Ich habe sie oben gesucht, nicht gefunden und jede
Sendung verworfen — übrig blieb „Kein Programm verfügbar" auf jedem
Sender, und ohne Sendungen fiel auch die Senderliste auf die Tunerliste
zurück.

Die Senderspalte im Raster fehlte ganz. Die Zeitachse ist mehrere tausend
Punkte breit, und der HStack hat den Nachbarn dafür auf null gedrückt.
Jetzt hat die Spalte Vorrang, und die Achse liegt als Auflage über einer
durchsichtigen Fläche — eine Auflage zählt bei der Größenrechnung nicht
mit. Der Abstand der Achsenbeschriftung gehört in die Spaltenbreite
hinein statt daneben, sonst steht die ganze Achse acht Punkte neben dem
Programm darunter.

Und das Einschalten: die Adresse wurde berechnet und verworfen, dann
bekam der Player eine Senderkennung, zu der er einen Film in der
Mediathek suchte. Den gibt es nicht. Jetzt reicht die Anfrage die fertige
Adresse durch, der Player spielt sie ohne Umweg, und meldet weder
Fortschritt noch sucht er Metadaten. Scheitert es doch, steht ab jetzt
eine Meldung da statt gar nichts.

Geprüft wurde das Raster diesmal mit den Augen: ein Gerüst im Simulator
zeichnet es mit erfundenen Sendern. Dafür bekommt es die Bildadresse als
Abschluss übergeben, statt sie aus der Sitzung zu lesen.

Dazu: Downloads zeigen jetzt an, wenn sie auf WLAN warten. Die
Hintergrund-Sitzung wartet in dem Fall von sich aus — kein Fehler, nur
Stillstand, und das sah bisher aus wie ein hängender Download.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-12 13:34:48 +02:00
co-authored by Claude Opus 5
parent 2362865ba9
commit bbe47b83be
10 changed files with 2893 additions and 2565 deletions
@@ -0,0 +1,33 @@
import Foundation
/// Warum gerade nichts übertragen wird, obwohl etwas in der Schlange steht.
///
/// Die Hintergrund-Sitzung wartet in diesen Fällen von sich aus — sie meldet
/// keinen Fehler, sie tut nur nichts. Für den Betrachter sieht das aus wie
/// ein hängender Download: der Balken steht, und nichts sagt, worauf.
public enum TransferHold: Sendable, Equatable {
case none
/// „Nur über WLAN" ist an, und das Gerät hängt am Mobilfunk.
case waitingForWiFi
/// Gar keine Verbindung.
case offline
/// Was die Übertragung gerade aufhält.
///
/// `isExpensive` statt einer Abfrage auf den Schnittstellentyp: der Wert
/// deckt auch den persönlichen Hotspot ab — technisch WLAN, in der
/// Abrechnung Mobilfunk. Wer dort lädt, zahlt es trotzdem.
public static func current(
isWiFiOnly: Bool,
isReachable: Bool,
isExpensive: Bool
) -> TransferHold {
// Ohne Verbindung hilft auch ein ausgeschalteter WLAN-Zwang nicht —
// deshalb zuerst geprüft.
if !isReachable { return .offline }
if isWiFiOnly, isExpensive { return .waitingForWiFi }
return .none
}
public var isHolding: Bool { self != .none }
}
@@ -189,10 +189,18 @@ struct PlexGridMedia: Decodable {
let channelVcn: String?
/// Die laufende Nummer des Senders am Server.
let channelID: String?
/// Anfang und Ende der Sendung.
///
/// Sie stehen hier und **nicht** am Eintrag darüber. Genau daran ist das
/// Raster gescheitert: oben gesucht, nicht gefunden, jede Sendung
/// verworfen — und übrig blieb „Kein Programm verfügbar" auf jedem Sender.
let beginsAt: Int?
let endsAt: Int?
let parts: [PlexGridPart]
private enum CodingKeys: String, CodingKey {
case channelIdentifier, channelTitle, channelCallSign, channelThumb, channelVcn, channelID
case beginsAt, endsAt
case part = "Part"
}
@@ -217,6 +225,8 @@ struct PlexGridMedia: Decodable {
} else {
self.channelID = nil
}
self.beginsAt = try? values.decodeIfPresent(Int.self, forKey: .beginsAt)
self.endsAt = try? values.decodeIfPresent(Int.self, forKey: .endsAt)
self.parts = try values.decodeIfPresent([PlexGridPart].self, forKey: .part) ?? []
}
@@ -440,6 +450,11 @@ struct PlexGridEntry: Decodable {
var channel: PlexChannel? { media?.compactMap(\.channel).first }
func program(channelID: String?) -> PlexProgram? {
// Die Zeiten stehen beim Programmanbieter im Medien-Block, nicht am
// Eintrag. Beide Stellen lesen, damit es auf beiden Serverfassungen
// trägt.
let beginsAt = self.beginsAt ?? media?.first?.beginsAt
let endsAt = self.endsAt ?? media?.first?.endsAt
guard let beginsAt, let endsAt, let title else { return nil }
return PlexProgram(
id: ratingKey ?? "\(channelID ?? "")-\(beginsAt)",
@@ -0,0 +1,45 @@
import Testing
@testable import FlixyDownloads
/// Warum gerade nichts übertragen wird.
@Suite("Übertragungssperre")
struct TransferHoldTests {
@Test("Mobilfunk hält auf, solange nur WLAN erlaubt ist")
func cellularHolds() {
#expect(
TransferHold.current(isWiFiOnly: true, isReachable: true, isExpensive: true)
== .waitingForWiFi
)
}
@Test("Im WLAN läuft es")
func wifiRuns() {
#expect(
TransferHold.current(isWiFiOnly: true, isReachable: true, isExpensive: false)
== .none
)
}
@Test("Ohne den WLAN-Zwang hält Mobilfunk nicht auf")
func cellularAllowed() {
#expect(
TransferHold.current(isWiFiOnly: false, isReachable: true, isExpensive: true)
== .none
)
}
@Test("Ohne Verbindung hilft auch ein ausgeschalteter Zwang nichts")
func offlineWins() {
// Sonst hieße es „wartet auf WLAN", während schlicht gar nichts geht —
// und der Betrachter sucht den Fehler an der falschen Stelle.
#expect(
TransferHold.current(isWiFiOnly: false, isReachable: false, isExpensive: false)
== .offline
)
#expect(
TransferHold.current(isWiFiOnly: true, isReachable: false, isExpensive: true)
== .offline
)
}
}
@@ -8,8 +8,6 @@
"title": "Endlich Witwer",
"summary": "Komödie",
"type": "movie",
"beginsAt": 1757682000,
"endsAt": 1757685600,
"Media": [
{
"channelCallSign": "ZDFGERH",
@@ -18,7 +16,9 @@
"channelThumb": "/library/metadata/5/thumb",
"channelTitle": "006 ZDFGERH (ZDF HD)",
"channelVcn": "006",
"onAir": true
"onAir": true,
"beginsAt": 1757682000,
"endsAt": 1757685600
}
]
},
@@ -27,14 +27,14 @@
"key": "/tv.plex.providers.epg.cloud:2/metadata/plex%3A%2F%2Fepisode%2Fabc",
"title": "heute journal",
"type": "episode",
"beginsAt": 1757685600,
"endsAt": 1757687400,
"Media": [
{
"channelID": 5,
"channelIdentifier": "6228f7f2364e02fa5ec8e51c-5fc705f9c8d56d002e06f849",
"channelTitle": "006 ZDFGERH (ZDF HD)",
"channelVcn": "006"
"channelVcn": "006",
"beginsAt": 1757685600,
"endsAt": 1757687400
}
]
},
@@ -42,17 +42,17 @@
"ratingKey": "5f3",
"key": "/tv.plex.providers.epg.cloud:2/metadata/plex%3A%2F%2Fshow%2Fxyz",
"title": "Sport aktuell",
"beginsAt": 1757682000,
"endsAt": 1757685600,
"Media": [
{
"channelCallSign": "SRF2",
"channelID": 12,
"channelIdentifier": "6228f7f2364e02fa5ec8e51c-12",
"channelVcn": "002"
"channelVcn": "002",
"beginsAt": 1757682000,
"endsAt": 1757685600
}
]
}
]
}
}
}