Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
4.7 KiB
Swift
111 lines
4.7 KiB
Swift
import Foundation
|
|
|
|
/// Welche Folge dran ist, wenn jemand eine Serie oder Staffel startet.
|
|
///
|
|
/// **Warum als reiner Typ.** Liste hinein, eine Folge heraus. Kein Netz, keine
|
|
/// Einstellungen, keine Ansicht — dieselbe Bauform wie `HeroSelection` und
|
|
/// `AudioTrackChooser`, und aus demselben Grund: im App-Ziel gibt es kein
|
|
/// Unit-Test-Ziel, also muss alles, worin eine Entscheidung steckt, hierher.
|
|
/// „Er hat die falsche Folge gestartet" ist sonst ein Befund, den man nicht
|
|
/// nachstellen kann.
|
|
///
|
|
/// **Die Regel in einem Satz:** die älteste Folge, die noch nicht als gesehen
|
|
/// gilt; wurde sie angefangen, dort weiter; ist alles gesehen, von vorn.
|
|
public enum EpisodeSelection {
|
|
/// Ob die Liste eine ganze Serie ist oder genau eine Staffel.
|
|
///
|
|
/// Sie entscheidet nur zweierlei: ob Specials wegfallen und was „von vorn"
|
|
/// bedeutet. Alles Übrige ist gleich.
|
|
public enum Scope: Sendable, Equatable {
|
|
case series
|
|
case season
|
|
}
|
|
|
|
/// Was gespielt werden soll — und ob an der gemerkten Stelle.
|
|
public struct Choice: Sendable, Equatable {
|
|
public let episode: MediaItem
|
|
/// Ob an der gemerkten Stelle fortgesetzt wird.
|
|
public let resumes: Bool
|
|
|
|
public init(episode: MediaItem, resumes: Bool) {
|
|
self.episode = episode
|
|
self.resumes = resumes
|
|
}
|
|
}
|
|
|
|
/// Ab wo eine angefangene Folge **nicht** mehr fortgesetzt wird.
|
|
///
|
|
/// Ein Server, der einen Fortschritt vier Sekunden vor Schluss noch nicht
|
|
/// gelöscht hat, öffnete den Player sonst mitten im Abspann — und das
|
|
/// sieht aus wie „er hat die Folge übersprungen".
|
|
static let resumeCeiling = 0.95
|
|
|
|
/// Die Folge, mit der es weitergeht.
|
|
///
|
|
/// `nil`, wenn nach dem Aussortieren nichts übrig ist. Dann sagt der
|
|
/// Aufrufer das, statt lautlos nichts zu tun.
|
|
public static func nextUp(in items: [MediaItem], scope: Scope) -> Choice? {
|
|
let sortiert = ordered(items, scope: scope)
|
|
guard !sortiert.isEmpty else { return nil }
|
|
|
|
// **Die älteste ungesehene, nicht die am weitesten fortgeschrittene.**
|
|
//
|
|
// Bei S1E1 gesehen, S1E2 unberührt und S1E5 halb gesehen kommt S1E2.
|
|
// Wer sich eine Folge für später aufgehoben hat, soll sie nicht
|
|
// dadurch verlieren, dass er irgendwo weiter vorn hineingeschaut hat.
|
|
guard let offen = sortiert.first(where: { !$0.isWatched }) else {
|
|
// Alles gesehen: von vorn. Bei einer Serie ist das die erste Folge
|
|
// der ersten Staffel, bei einer Staffel deren erste — derselbe
|
|
// Code, weil `scope` längst entschieden hat, was in der Liste
|
|
// liegt.
|
|
return Choice(episode: sortiert[0], resumes: false)
|
|
}
|
|
|
|
let angefangen = (offen.watchProgress ?? 0) > 0
|
|
&& (offen.watchProgress ?? 0) < resumeCeiling
|
|
return Choice(episode: offen, resumes: angefangen)
|
|
}
|
|
|
|
/// Die Sendereihenfolge — **nie** die des Servers.
|
|
///
|
|
/// Jellyfins `children(of:)` sortiert nach `SortName`, und das stellt
|
|
/// „Folge 10" vor „Folge 2". Sich darauf zu verlassen ginge genau so
|
|
/// lange gut, bis eine Staffel zehn Folgen hat.
|
|
static func ordered(_ items: [MediaItem], scope: Scope) -> [MediaItem] {
|
|
var übrig = items.filter { eintrag in
|
|
switch eintrag.kind {
|
|
// Behälter sind keine Folgen. `nil` bleibt dagegen: Jellyfin
|
|
// liefert in Listen nicht immer eine brauchbare Art, und
|
|
// Wegwerfen leerte die Liste.
|
|
case .season, .show, .collection: false
|
|
default: true
|
|
}
|
|
}
|
|
|
|
// **Specials nur bei der Serie aussortieren.** Staffel 0 vorzuziehen
|
|
// wäre falsch — sie steht vor Staffel 1 und ist trotzdem nicht der
|
|
// Anfang. Wer dagegen die Specials-Kachel angeklickt hat, meint die
|
|
// Specials.
|
|
if scope == .series {
|
|
übrig = übrig.filter { ($0.parentIndex ?? 1) != 0 }
|
|
}
|
|
|
|
// Fehlt eine Nummer, ist das keine Null, sondern „der Server hat
|
|
// nichts gesagt". Solche Einträge gehören ans Ende, nicht an den
|
|
// Anfang. Die Kennung zuletzt, damit dieselbe Eingabe dasselbe ergibt
|
|
// — sonst ist ein Fehler nicht wiederholbar.
|
|
übrig.sort { links, rechts in
|
|
let staffelL = links.parentIndex ?? .max
|
|
let staffelR = rechts.parentIndex ?? .max
|
|
if staffelL != staffelR { return staffelL < staffelR }
|
|
|
|
let folgeL = links.index ?? .max
|
|
let folgeR = rechts.index ?? .max
|
|
if folgeL != folgeR { return folgeL < folgeR }
|
|
|
|
return links.id < rechts.id
|
|
}
|
|
return übrig
|
|
}
|
|
}
|