Files
Flaro/Flixy/Session/DownloadRefiller.swift
T
GuidoandClaude Opus 5 bf0a63a924 Vier Punkte aus dem Praxistest
Download-Knopf tat nichts
  DownloadService.plan gab still nil zurück, wenn es keinen Auftrag
  bauen konnte, und download() reichte das als nil weiter. Der Knopf sah
  aus, als wäre er kaputt, dabei war der Grund jedes Mal bekannt. Es
  gibt jetzt einen eigenen Ablehnungsgrund mit vier unterschiedlichen
  Begründungen — keine Verbindung, keine Datei, kein abrufbarer Teil,
  keine Adresse. Eine Funktion, die kommentarlos nichts tut, ist selbst
  der Fehler.

Live-TV galt als „nicht eingerichtet"
  Plex legt die Empfängerliste unter dem Schlüssel `Dvr` ab, die Hülle
  kannte aber nur Metadata, Directory und Hub. Sie las eine leere Liste
  und zog daraus die falsche Schlussfolgerung — auf einem Server, auf
  dem ein Empfänger eingerichtet war. Dasselbe galt für die
  Anbieterliste unter `MediaProvider`, ohne die es keinen
  Programmführer gibt.

  Beim Umbau wäre beinahe die Fehlerdiagnose verloren gegangen: mit
  `try?` über alle Schlüssel verschwände auch ein echter Dekodierfehler
  und ergäbe stillschweigend eine leere Liste. Aus „ein Feld heißt jetzt
  anders" würde „die Mediathek ist leer". Jetzt wird erst geprüft, ob
  der Schlüssel da ist, und dann ohne try? dekodiert. Fünf Tests halten
  beides fest.

Qualität im Player wählbar
  Automatisch, Original, 1080p, 720p, 480p. Der Eingriff wird genau dann
  gebraucht, wenn es ruckelt — dafür will niemand den Film verlassen.
  Ein Wechsel baut den Strom neu auf (die Stufe steckt in der Adresse),
  merkt sich die Stelle und fährt sie wieder an. Der alte Transcoder
  wird vorher angehalten, sonst rechnet der Server zwei Ströme für einen
  Zuschauer.

AVPlayer-Anzeige entfernt
  Sie war eine Prüfhilfe aus Phase 1. Dieselbe Angabe steht weiterhin
  unter Einstellungen → Diagnose, wo sie hingehört.

137 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:44:20 +02:00

80 lines
3.3 KiB
Swift

import Foundation
import FlixyDownloads
import PlexKit
/// Füllt den Vorrat an ungesehenen Folgen wieder auf.
///
/// Die Regel-Engine rechnet aus, *wie viele* Folgen je Serie fehlen; hier wird
/// entschieden, *welche*. Die Trennung ist Absicht: das Rechnen braucht keinen
/// Server und ist deshalb prüfbar, das Auswählen braucht ihn zwangsläufig.
@MainActor
struct DownloadRefiller {
let server: PlexServer?
let coordinator: DownloadCoordinator
/// Lädt nach, was den Regeln nach fehlt.
///
/// Läuft nach dem Ansehen einer Folge und beim Start — nicht auf einem
/// Zeitgeber. Eine App, die im Hintergrund unaufgefordert Gigabytes lädt,
/// wäre eine unangenehme Überraschung auf der Mobilfunkrechnung.
@discardableResult
func refill() async -> Int {
guard let server, coordinator.rules.keepUnwatchedEpisodes != nil else { return 0 }
let needed = coordinator.rules.seriesNeedingRefill(in: coordinator.items)
guard !needed.isEmpty else { return 0 }
var added = 0
let service = DownloadService(server: server, coordinator: coordinator)
for (seriesKey, count) in needed {
let candidates = await nextUnwatchedEpisodes(
inSeries: seriesKey,
count: count,
server: server
)
for episode in candidates {
// Die Kinderliste bringt keine Dateiangaben mit — ohne sie
// ließe sich weder das Speicherlimit prüfen noch ein
// Fortschritt anzeigen.
let detailed = (try? await server.metadata(ratingKey: episode.ratingKey)) ?? episode
// Folgen ohne abrufbare Datei still überspringen: das Nachladen
// läuft im Hintergrund, da hilft keine Fehlermeldung.
guard let plan = try? service.plan(for: detailed) else { continue }
if (try? coordinator.enqueue(plan)) != nil { added += 1 }
}
}
return added
}
/// Die nächsten ungesehenen Folgen einer Serie, die noch nicht offline sind.
///
/// In Ausstrahlungsreihenfolge, nicht nach Beliebtheit oder Zufall: wer
/// drei Folgen vorhalten lässt, will die drei, die als Nächstes dran sind.
private func nextUnwatchedEpisodes(
inSeries seriesKey: String,
count: Int,
server: PlexServer
) async -> [PlexItem] {
guard let seasons = try? await server.children(of: seriesKey) else { return [] }
let offlineKeys = Set(coordinator.items.map(\.ratingKey))
var result: [PlexItem] = []
for season in seasons.sorted(by: { ($0.index ?? 0) < ($1.index ?? 0) }) {
// Specials führt Plex als Staffel 0. Sie gehören nicht in den
// Vorrat für „was kommt als Nächstes".
guard (season.index ?? 0) > 0 else { continue }
guard let episodes = try? await server.children(of: season.ratingKey) else { continue }
for episode in episodes.sorted(by: { ($0.index ?? 0) < ($1.index ?? 0) }) {
guard !episode.isWatched, !offlineKeys.contains(episode.ratingKey) else { continue }
result.append(episode)
if result.count >= count { return result }
}
}
return result
}
}