Die Datenschicht: - plex.tv-Anmeldung über das PIN-Verfahren, Token im Schlüsselbund - Serversuche und Verbindungsauswahl nach Rang (lokal vor direkt vor Vermittlung), mit Abgleich der Server-Kennung gegen veraltete plex.direct-Adressen - PlexServer als einzige Stelle, an der die App Endpunkte kennt: Mediatheken, Hubs, seitenweises Blättern mit Filtern, Metadaten, Staffeln/Folgen, Suche Die Oberfläche: - Startseite mit Aufmacher und den Reihen vom Server - Mediathek als Raster oder Liste, mit Filter- und Sortierblatt - Detailseiten für Filme, Folgen, Serien und Staffeln inkl. Besetzung, Fassungsauswahl und aufklappbaren technischen Angaben - Suche mit gemerkten Anfragen - Vollbildplayer mit eigenem Chrome Der Player: - AVPlayerEngine und VLCPlayerEngine hinter einem gemeinsamen Protokoll - VLCKit liegt in einem eigenen Package (FlixyVLC), damit `swift test` in FlixyKit nicht das 920-MB-Binärpaket mitzieht Am echten plex.tv geprüft: der Anmeldecode kommt an. Dabei fiel auf, dass `strong=true` einen dreißigstelligen Code liefert, der sich bei plex.tv/link nicht eintippen lässt — jetzt der kurze. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
3.1 KiB
Swift
86 lines
3.1 KiB
Swift
import Foundation
|
|
|
|
/// Filter und Sortierung einer Mediatheksansicht.
|
|
///
|
|
/// Wird zu Abfrageparametern für Plex — die Filterung passiert also auf dem
|
|
/// Server. Bei zehntausend Titeln wäre alles andere aussichtslos: die Liste
|
|
/// erst vollständig zu laden, um sie dann lokal zu durchsuchen, dauert
|
|
/// Minuten und belegt entsprechend Speicher.
|
|
public struct LibraryQuery: Sendable, Equatable {
|
|
public enum Sort: String, Sendable, Equatable, CaseIterable {
|
|
case titleAscending = "titleSort:asc"
|
|
case titleDescending = "titleSort:desc"
|
|
case recentlyAdded = "addedAt:desc"
|
|
case releaseDate = "originallyAvailableAt:desc"
|
|
case rating = "rating:desc"
|
|
case lastViewed = "lastViewedAt:desc"
|
|
}
|
|
|
|
public var sort: Sort
|
|
public var unwatchedOnly: Bool
|
|
public var genre: String?
|
|
public var year: Int?
|
|
public var contentRating: String?
|
|
public var resolution: String?
|
|
|
|
public init(
|
|
sort: Sort = .titleAscending,
|
|
unwatchedOnly: Bool = false,
|
|
genre: String? = nil,
|
|
year: Int? = nil,
|
|
contentRating: String? = nil,
|
|
resolution: String? = nil
|
|
) {
|
|
self.sort = sort
|
|
self.unwatchedOnly = unwatchedOnly
|
|
self.genre = genre
|
|
self.year = year
|
|
self.contentRating = contentRating
|
|
self.resolution = resolution
|
|
}
|
|
|
|
/// Ob überhaupt gefiltert wird — steuert, ob die Oberfläche den
|
|
/// Filterknopf hervorhebt.
|
|
public var isFiltering: Bool {
|
|
unwatchedOnly || genre != nil || year != nil || contentRating != nil || resolution != nil
|
|
}
|
|
|
|
func queryItems(libraryKind: PlexLibrary.Kind?) -> [URLQueryItem] {
|
|
var items: [URLQueryItem] = [URLQueryItem(name: "sort", value: sort.rawValue)]
|
|
|
|
// Plex nummeriert die Arten: 1 Film, 2 Serie, 4 Folge. Ohne diese
|
|
// Angabe liefert eine Serienmediathek alle Folgen einzeln statt der
|
|
// Serien.
|
|
switch libraryKind {
|
|
case .movie: items.append(URLQueryItem(name: "type", value: "1"))
|
|
case .show: items.append(URLQueryItem(name: "type", value: "2"))
|
|
default: break
|
|
}
|
|
|
|
if unwatchedOnly { items.append(URLQueryItem(name: "unwatched", value: "1")) }
|
|
if let genre { items.append(URLQueryItem(name: "genre", value: genre)) }
|
|
if let year { items.append(URLQueryItem(name: "year", value: String(year))) }
|
|
if let contentRating { items.append(URLQueryItem(name: "contentRating", value: contentRating)) }
|
|
if let resolution { items.append(URLQueryItem(name: "resolution", value: resolution)) }
|
|
|
|
return items
|
|
}
|
|
}
|
|
|
|
/// Ein Ausschnitt aus einer längeren Liste.
|
|
public struct PlexPage<Element: Sendable>: Sendable {
|
|
public let items: [Element]
|
|
public let offset: Int
|
|
/// Wie viele Einträge es insgesamt gibt.
|
|
public let totalSize: Int
|
|
|
|
public init(items: [Element], offset: Int, totalSize: Int) {
|
|
self.items = items
|
|
self.offset = offset
|
|
self.totalSize = totalSize
|
|
}
|
|
|
|
public var hasMore: Bool { offset + items.count < totalSize }
|
|
public var nextOffset: Int { offset + items.count }
|
|
}
|