Nachladen - DownloadRefiller wählt aus, WELCHE Folgen fehlen; wie viele, rechnet die Regel-Engine. Getrennt, weil das Rechnen ohne Server auskommt und damit prüfbar ist. - In Ausstrahlungsreihenfolge, Specials in Staffel 0 bleiben außen vor. - Läuft nach dem Ansehen und beim Start, nicht auf einem Zeitgeber: eine App, die im Hintergrund unaufgefordert Gigabytes lädt, wäre eine unangenehme Überraschung auf der Mobilfunkrechnung. Startseiten-Layout - Reihen ein-/ausblenden und per Bearbeiten-Modus umsortieren. - Die Anordnungslogik liegt als HubArrangement in PlexKit statt im App-Target — dort ist sie ohne Simulator testbar, und dort sitzen die unangenehmen Fälle: neue Reihen vom Server landen am Ende statt zu verschwinden, verschwundene aus der gespeicherten Liste stören nicht, ein doppelter Eintrag stürzt nicht ab. 11 Tests. 126 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
3.1 KiB
Swift
78 lines
3.1 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
|
|
guard let plan = 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
|
|
}
|
|
}
|