Die Ansichten sprechen nicht mehr mit Plex
Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit `MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre. Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle` wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und `TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten. `Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein zweites Mal falsch — zu schreiben. Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie waren bereits auseinandergelaufen: derselbe Fehler bekam je nach Bildschirm eine andere Erklärung. Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das jetzt ausdrücklich, damit es niemand später „aufräumt". Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit Plex. Sie folgen als Nächstes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
effdcd4532
commit
fe2d730c00
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import FlixyDownloads
|
||||
import MediaKit
|
||||
import PlexKit
|
||||
|
||||
/// Übersetzt Titel vom Server in Download-Aufträge.
|
||||
@@ -79,7 +80,7 @@ enum DownloadQuality: String, CaseIterable, Identifiable {
|
||||
|
||||
@MainActor
|
||||
struct DownloadService {
|
||||
let server: PlexServer?
|
||||
let backend: PlexBackend?
|
||||
let coordinator: DownloadCoordinator
|
||||
|
||||
/// Baut den Auftrag für einen einzelnen Titel.
|
||||
@@ -89,33 +90,33 @@ struct DownloadService {
|
||||
/// nichts, obwohl der Grund bekannt war — etwa ein Titel, den der Server
|
||||
/// noch nicht analysiert hat.
|
||||
func plan(
|
||||
for item: PlexItem,
|
||||
for item: MediaItem,
|
||||
mediaIndex: Int = 0,
|
||||
quality: DownloadQuality = .original
|
||||
) throws(DownloadRefusal) -> DownloadPlan {
|
||||
guard let server else {
|
||||
guard let backend else {
|
||||
throw .noDownloadableFile(reason: String(localized: "downloads.refused.noServer"))
|
||||
}
|
||||
let media = item.media.indices.contains(mediaIndex)
|
||||
? item.media[mediaIndex]
|
||||
: item.preferredMedia
|
||||
let media = item.versions.indices.contains(mediaIndex)
|
||||
? item.versions[mediaIndex]
|
||||
: item.preferredVersion
|
||||
guard let media else {
|
||||
throw .noDownloadableFile(reason: String(localized: "downloads.refused.noMedia"))
|
||||
}
|
||||
guard let part = media.parts.first else {
|
||||
guard let part = media.files.first else {
|
||||
throw .noDownloadableFile(reason: String(localized: "downloads.refused.noPart"))
|
||||
}
|
||||
let url: URL?
|
||||
switch quality {
|
||||
case .original:
|
||||
url = server.downloadURL(part: part)
|
||||
url = backend.downloadURL(for: part)
|
||||
case .fullHD, .hd, .low:
|
||||
// Über den Universal-Transcoder. NOCH NICHT AM SERVER GEPRÜFT:
|
||||
// offen ist, ob Plex eine Content-Length mitschickt — ohne sie
|
||||
// gibt es keinen Fortschrittsbalken und kein Fortsetzen.
|
||||
url = server.transcodedDownloadURL(
|
||||
ratingKey: item.ratingKey,
|
||||
mediaIndex: mediaIndex,
|
||||
url = backend.compressedDownloadURL(
|
||||
itemID: item.id,
|
||||
versionIndex: mediaIndex,
|
||||
maximumBitrate: quality.maximumBitrate ?? 8_000,
|
||||
resolution: quality.resolution
|
||||
)
|
||||
@@ -125,13 +126,13 @@ struct DownloadService {
|
||||
}
|
||||
|
||||
return DownloadPlan(
|
||||
ratingKey: item.ratingKey,
|
||||
ratingKey: item.id,
|
||||
title: item.rowTitle,
|
||||
subtitle: item.downloadSubtitle,
|
||||
sourceURL: url,
|
||||
seriesKey: item.grandparentRatingKey,
|
||||
seriesTitle: item.grandparentTitle,
|
||||
seasonKey: item.parentRatingKey,
|
||||
seriesKey: item.seriesID,
|
||||
seriesTitle: item.seriesTitle,
|
||||
seasonKey: item.parentID,
|
||||
seasonIndex: item.parentIndex,
|
||||
episodeIndex: item.index,
|
||||
durationMilliseconds: item.duration,
|
||||
@@ -146,7 +147,7 @@ struct DownloadService {
|
||||
/// Lädt einen einzelnen Titel.
|
||||
@discardableResult
|
||||
func download(
|
||||
_ item: PlexItem,
|
||||
_ item: MediaItem,
|
||||
mediaIndex: Int = 0,
|
||||
quality: DownloadQuality = .original
|
||||
) throws(DownloadRefusal) -> DownloadItem {
|
||||
@@ -159,30 +160,30 @@ struct DownloadService {
|
||||
/// keine Dateiangaben, und ohne Dateigröße ließe sich weder das
|
||||
/// Speicherlimit prüfen noch ein Fortschrittsbalken zeigen.
|
||||
func downloadAll(
|
||||
in container: PlexItem
|
||||
) async -> (accepted: Int, refused: [(PlexItem, DownloadRefusal)], unavailable: Int) {
|
||||
guard let server else { return (0, [], 0) }
|
||||
in container: MediaItem
|
||||
) async -> (accepted: Int, refused: [(MediaItem, DownloadRefusal)], unavailable: Int) {
|
||||
guard let backend else { return (0, [], 0) }
|
||||
|
||||
let episodes: [PlexItem]
|
||||
let episodes: [MediaItem]
|
||||
do {
|
||||
episodes = try await allEpisodes(in: container, server: server)
|
||||
episodes = try await allEpisodes(in: container, backend: backend)
|
||||
} catch {
|
||||
return (0, [], 0)
|
||||
}
|
||||
|
||||
var accepted = 0
|
||||
var refused: [(PlexItem, DownloadRefusal)] = []
|
||||
var refused: [(MediaItem, DownloadRefusal)] = []
|
||||
var unavailable = 0
|
||||
|
||||
for episode in episodes {
|
||||
// Folgen, die schon offline sind, werden übersprungen statt
|
||||
// abgelehnt — bei einer Serie, von der die Hälfte geladen ist,
|
||||
// wäre eine Liste aus zwanzig Fehlern keine Hilfe.
|
||||
if coordinator.items.contains(where: { $0.ratingKey == episode.ratingKey }) {
|
||||
if coordinator.items.contains(where: { $0.ratingKey == episode.id }) {
|
||||
continue
|
||||
}
|
||||
|
||||
let detailed = (try? await server.metadata(ratingKey: episode.ratingKey)) ?? episode
|
||||
let detailed = (try? await backend.item(id: episode.id)) ?? episode
|
||||
|
||||
do {
|
||||
_ = try coordinator.enqueue(try plan(for: detailed))
|
||||
@@ -191,7 +192,7 @@ struct DownloadService {
|
||||
// Einzelne Folgen ohne Datei stoppen den Stapel nicht — sie
|
||||
// werden am Ende nur gezählt.
|
||||
unavailable += 1
|
||||
} catch {
|
||||
} catch let error as DownloadRefusal {
|
||||
refused.append((episode, error))
|
||||
// Beim Speicherlimit hat es keinen Sinn, die übrigen Folgen
|
||||
// noch einzeln durchzuprobieren — sie scheitern alle gleich.
|
||||
@@ -203,14 +204,14 @@ struct DownloadService {
|
||||
return (accepted, refused, unavailable)
|
||||
}
|
||||
|
||||
private func allEpisodes(in container: PlexItem, server: PlexServer) async throws -> [PlexItem] {
|
||||
private func allEpisodes(in container: MediaItem, backend: PlexBackend) async throws -> [MediaItem] {
|
||||
switch container.kind {
|
||||
case .season:
|
||||
return try await server.children(of: container.ratingKey)
|
||||
return try await backend.children(of: container.id)
|
||||
case .show:
|
||||
var episodes: [PlexItem] = []
|
||||
for season in try await server.children(of: container.ratingKey) {
|
||||
episodes.append(contentsOf: try await server.children(of: season.ratingKey))
|
||||
var episodes: [MediaItem] = []
|
||||
for season in try await backend.children(of: container.id) {
|
||||
episodes.append(contentsOf: try await backend.children(of: season.id))
|
||||
}
|
||||
return episodes
|
||||
default:
|
||||
@@ -219,7 +220,7 @@ struct DownloadService {
|
||||
}
|
||||
|
||||
/// Der Abzug der Serverangaben, den der Player offline braucht.
|
||||
private func offlineMedia(for item: PlexItem, media: PlexMedia, part: PlexPart) -> OfflineMedia {
|
||||
private func offlineMedia(for item: MediaItem, media: MediaVersion, part: MediaFile) -> OfflineMedia {
|
||||
OfflineMedia(
|
||||
container: part.container ?? media.container,
|
||||
videoCodec: media.videoCodec,
|
||||
@@ -230,16 +231,16 @@ struct DownloadService {
|
||||
subtitleLanguages: part.subtitleStreams.compactMap(\.language),
|
||||
markers: item.markers.map {
|
||||
OfflineMarker(
|
||||
type: $0.type,
|
||||
startMilliseconds: $0.startTimeOffset,
|
||||
endMilliseconds: $0.endTimeOffset
|
||||
type: $0.kind.rawValue,
|
||||
startMilliseconds: $0.startMilliseconds,
|
||||
endMilliseconds: $0.endMilliseconds
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PlexItem {
|
||||
extension MediaItem {
|
||||
/// Die Zeile unter dem Titel in der Downloads-Ansicht.
|
||||
var downloadSubtitle: String? {
|
||||
if kind == .episode, let season = parentIndex, let episode = index {
|
||||
|
||||
Reference in New Issue
Block a user