Phase 1: Anmeldung, Bibliothek, Detailseiten, Hybrid-Player
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>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import PlexKit
|
||||
|
||||
@Suite("Mediatheks-Abfragen")
|
||||
struct LibraryQueryTests {
|
||||
private func value(_ items: [URLQueryItem], _ name: String) -> String? {
|
||||
items.first { $0.name == name }?.value
|
||||
}
|
||||
|
||||
@Test("Eine Serienmediathek liefert Serien, nicht einzelne Folgen")
|
||||
func showLibraryRequestsShows() {
|
||||
// Ohne `type=2` schüttet Plex alle Folgen einzeln aus — bei einer
|
||||
// Serienmediathek sind das schnell zehntausend Einträge statt dreißig
|
||||
// Serien.
|
||||
let items = LibraryQuery().queryItems(libraryKind: .show)
|
||||
#expect(value(items, "type") == "2")
|
||||
}
|
||||
|
||||
@Test("Eine Filmmediathek fragt nach Filmen")
|
||||
func movieLibraryRequestsMovies() {
|
||||
#expect(value(LibraryQuery().queryItems(libraryKind: .movie), "type") == "1")
|
||||
}
|
||||
|
||||
@Test("Unbekannte Mediathekenarten bekommen keine Artangabe")
|
||||
func unknownKindOmitsType() {
|
||||
#expect(value(LibraryQuery().queryItems(libraryKind: nil), "type") == nil)
|
||||
}
|
||||
|
||||
@Test("Sortierung landet immer in der Abfrage")
|
||||
func sortIsAlwaysPresent() {
|
||||
for sort in LibraryQuery.Sort.allCases {
|
||||
let items = LibraryQuery(sort: sort).queryItems(libraryKind: .movie)
|
||||
#expect(value(items, "sort") == sort.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Ohne Filter stehen keine Filterparameter in der Abfrage")
|
||||
func unsetFiltersAreOmitted() {
|
||||
// Ein leerer Parameter ist für Plex nicht dasselbe wie ein fehlender:
|
||||
// `genre=` filtert auf ein Genre ohne Namen und liefert nichts.
|
||||
let items = LibraryQuery().queryItems(libraryKind: .movie)
|
||||
#expect(value(items, "genre") == nil)
|
||||
#expect(value(items, "year") == nil)
|
||||
#expect(value(items, "unwatched") == nil)
|
||||
#expect(value(items, "resolution") == nil)
|
||||
}
|
||||
|
||||
@Test("Gesetzte Filter landen vollständig in der Abfrage")
|
||||
func setFiltersAppear() {
|
||||
let query = LibraryQuery(
|
||||
sort: .recentlyAdded,
|
||||
unwatchedOnly: true,
|
||||
genre: "18",
|
||||
year: 2017,
|
||||
resolution: "4k"
|
||||
)
|
||||
let items = query.queryItems(libraryKind: .movie)
|
||||
|
||||
#expect(value(items, "sort") == "addedAt:desc")
|
||||
#expect(value(items, "unwatched") == "1")
|
||||
#expect(value(items, "genre") == "18")
|
||||
#expect(value(items, "year") == "2017")
|
||||
#expect(value(items, "resolution") == "4k")
|
||||
}
|
||||
|
||||
@Test("Der Filterknopf weiß, ob gefiltert wird")
|
||||
func filteringFlagReflectsState() {
|
||||
#expect(!LibraryQuery().isFiltering)
|
||||
// Die Sortierung allein zählt nicht als Filter — sonst wäre der Knopf
|
||||
// dauerhaft hervorgehoben.
|
||||
#expect(!LibraryQuery(sort: .rating).isFiltering)
|
||||
#expect(LibraryQuery(unwatchedOnly: true).isFiltering)
|
||||
#expect(LibraryQuery(genre: "18").isFiltering)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Seitenweises Nachladen")
|
||||
struct PlexPageTests {
|
||||
@Test("Eine volle Seite kündigt Nachschub an")
|
||||
func fullPageHasMore() {
|
||||
let page = PlexPage(items: Array(0..<60), offset: 0, totalSize: 250)
|
||||
#expect(page.hasMore)
|
||||
#expect(page.nextOffset == 60)
|
||||
}
|
||||
|
||||
@Test("Die letzte Seite kündigt keinen Nachschub an")
|
||||
func lastPageHasNoMore() {
|
||||
// Genau die Bedingung, die sonst zu einer Endlosschleife führt: die
|
||||
// Ansicht lädt nach, bekommt nichts, lädt wieder nach.
|
||||
let page = PlexPage(items: Array(0..<10), offset: 240, totalSize: 250)
|
||||
#expect(!page.hasMore)
|
||||
}
|
||||
|
||||
@Test("Eine leere Mediathek verlangt kein Nachladen")
|
||||
func emptyLibraryHasNoMore() {
|
||||
let page = PlexPage(items: [Int](), offset: 0, totalSize: 0)
|
||||
#expect(!page.hasMore)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user