Files
Flaro/Packages/FlaroKit/Sources/JellyfinKit/JellyfinItem.swift
T
ScarriffleandClaude Opus 5 a7cf6c5def Halo heisst ab hier Flaro — die Marke stand im Weg
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>
2026-10-03 19:38:27 +02:00

222 lines
8.6 KiB
Swift

import Foundation
import MediaKit
/// Ein Eintrag, wie Jellyfin ihn über die Leitung schickt.
///
/// Die Felder heißen groß und die Zeiten sind Ticks zu hundert Nanosekunden —
/// beides bleibt hier unten. Nach oben geht nur `MediaItem`.
public struct JellyfinItem: Decodable, Sendable {
public let id: String
public let name: String?
public let type: String?
public let overview: String?
public let productionYear: Int?
/// Laufzeit in Ticks zu 100 ns.
public let runTimeTicks: Int64?
public let indexNumber: Int?
public let parentIndexNumber: Int?
public let seriesName: String?
public let seriesID: String?
public let seasonID: String?
public let seasonName: String?
public let officialRating: String?
public let communityRating: Double?
public let criticRating: Double?
public let dateCreated: Date?
public let childCount: Int?
public let recursiveItemCount: Int?
public let genres: [String]?
public let people: [Person]?
public let userData: UserData?
public let imageTags: [String: String]?
public let backdropImageTags: [String]?
public let parentBackdropItemID: String?
public let seriesPrimaryImageTag: String?
public let mediaSources: [JellyfinMediaSource]?
public let collectionType: String?
// MARK: - Live-TV
//
// Sender **und** Sendungen kommen bei Jellyfin als derselbe Typ wie
// Filme und Folgen. Deshalb stehen ihre Felder hier und nicht in einem
// eigenen Modell: ein zweites, das zu neunzig Prozent dasselbe trägt,
// wäre eine Kopie mit Pflegeaufwand.
/// Zu welchem Sender eine Sendung gehört.
public let channelID: String?
public let channelName: String?
/// Der Folgentitel einer Sendung — der Reihentitel steht in `name`.
public let episodeTitle: String?
/// Die Nummer, unter der ein Sender im Programm steht — „3" oder „11.1".
public let channelNumber: String?
/// Dasselbe, aber am Sender selbst statt an der Sendung.
public let number: String?
public let startDate: Date?
public let endDate: Date?
/// Was auf diesem Sender gerade läuft.
///
/// Kommt nur, wenn die Senderliste mit `addCurrentProgram` geholt wurde —
/// dann aber gleich mit, und die Liste braucht keinen zweiten Aufruf.
public let currentProgram: JellyfinCurrentProgram?
/// Ob die Sendung zu einem laufenden Serien-Timer gehört.
public let seriesTimerID: String?
public let isMovie: Bool?
public let isSeries: Bool?
public let isNews: Bool?
public let isSports: Bool?
public let isKids: Bool?
public let isLive: Bool?
public struct Person: Decodable, Sendable {
public let id: String?
public let name: String?
public let role: String?
public let type: String?
public let primaryImageTag: String?
private enum CodingKeys: String, CodingKey {
case id = "Id", name = "Name", role = "Role", type = "Type"
case primaryImageTag = "PrimaryImageTag"
}
}
public struct UserData: Decodable, Sendable {
public let playbackPositionTicks: Int64?
public let playCount: Int?
public let played: Bool?
public let isFavorite: Bool?
public let unplayedItemCount: Int?
private enum CodingKeys: String, CodingKey {
case playbackPositionTicks = "PlaybackPositionTicks"
case playCount = "PlayCount"
case played = "Played"
case isFavorite = "IsFavorite"
case unplayedItemCount = "UnplayedItemCount"
}
}
private enum CodingKeys: String, CodingKey {
case id = "Id", name = "Name", type = "Type", overview = "Overview"
case productionYear = "ProductionYear", runTimeTicks = "RunTimeTicks"
case indexNumber = "IndexNumber", parentIndexNumber = "ParentIndexNumber"
case seriesName = "SeriesName", seriesID = "SeriesId"
case seasonID = "SeasonId", seasonName = "SeasonName"
case officialRating = "OfficialRating", communityRating = "CommunityRating"
case criticRating = "CriticRating", dateCreated = "DateCreated"
case childCount = "ChildCount", recursiveItemCount = "RecursiveItemCount"
case genres = "Genres", people = "People", userData = "UserData"
case imageTags = "ImageTags", backdropImageTags = "BackdropImageTags"
case parentBackdropItemID = "ParentBackdropItemId"
case seriesPrimaryImageTag = "SeriesPrimaryImageTag"
case mediaSources = "MediaSources", collectionType = "CollectionType"
case channelID = "ChannelId", channelName = "ChannelName"
case episodeTitle = "EpisodeTitle"
case channelNumber = "ChannelNumber", number = "Number"
case startDate = "StartDate", endDate = "EndDate"
case currentProgram = "CurrentProgram", seriesTimerID = "SeriesTimerId"
case isMovie = "IsMovie", isSeries = "IsSeries", isNews = "IsNews"
case isSports = "IsSports", isKids = "IsKids", isLive = "IsLive"
}
}
/// Was auf einem Sender gerade läuft.
///
/// Ein eigenes, schmales Modell statt eines verschachtelten `JellyfinItem`:
/// ein Struct darf sich nicht selbst enthalten — seine Größe wäre unendlich.
/// Gebraucht werden hier ohnehin nur fünf Felder.
public struct JellyfinCurrentProgram: Decodable, Sendable {
public let id: String
public let name: String?
public let overview: String?
public let startDate: Date?
public let endDate: Date?
private enum CodingKeys: String, CodingKey {
case id = "Id", name = "Name", overview = "Overview"
case startDate = "StartDate", endDate = "EndDate"
}
}
/// Eine Fassung, wie Jellyfin sie meldet.
public struct JellyfinMediaSource: Decodable, Sendable {
public let id: String?
public let container: String?
public let size: Int64?
public let bitrate: Int?
public let runTimeTicks: Int64?
public let mediaStreams: [JellyfinMediaStream]?
public let supportsDirectPlay: Bool?
public let supportsDirectStream: Bool?
public let supportsTranscoding: Bool?
public let transcodingURL: String?
private enum CodingKeys: String, CodingKey {
case id = "Id", container = "Container", size = "Size", bitrate = "Bitrate"
case runTimeTicks = "RunTimeTicks", mediaStreams = "MediaStreams"
case supportsDirectPlay = "SupportsDirectPlay"
case supportsDirectStream = "SupportsDirectStream"
case supportsTranscoding = "SupportsTranscoding"
case transcodingURL = "TranscodingUrl"
}
}
/// Eine Spur, wie Jellyfin sie meldet.
public struct JellyfinMediaStream: Decodable, Sendable {
public let index: Int?
public let type: String?
public let codec: String?
public let language: String?
public let displayTitle: String?
/// Was **in der Datei** steht — im Gegensatz zu `displayTitle`, das
/// Jellyfin aus Sprache, Codec und Kennzeichen zusammensetzt.
///
/// Das zweite Signal neben dem Sprachkürzel, und manchmal das einzige
/// richtige: wo „eng" steht, kann im Titel „Deutsch" stehen.
public let title: String?
/// Für Hörgeschädigte. **Nicht** Audiodeskription — dafür hat Jellyfin
/// kein Kennzeichen.
public let isHearingImpaired: Bool?
public let isOriginal: Bool?
public let channels: Int?
public let isDefault: Bool?
public let isForced: Bool?
public let height: Int?
public let width: Int?
private enum CodingKeys: String, CodingKey {
case index = "Index", type = "Type", codec = "Codec", language = "Language"
case displayTitle = "DisplayTitle", channels = "Channels"
case title = "Title"
case isHearingImpaired = "IsHearingImpaired", isOriginal = "IsOriginal"
case isDefault = "IsDefault", isForced = "IsForced"
case height = "Height", width = "Width"
}
}
/// Die Hülle, in der Jellyfin Listen schickt.
public struct JellyfinItemPage: Decodable, Sendable {
public let items: [JellyfinItem]
public let totalRecordCount: Int?
public let startIndex: Int?
private enum CodingKeys: String, CodingKey {
case items = "Items"
case totalRecordCount = "TotalRecordCount"
case startIndex = "StartIndex"
}
}
extension Int64 {
/// Ticks zu hundert Nanosekunden in Millisekunden.
///
/// Jellyfins Einheit. Zehntausend Ticks sind eine Millisekunde — eine
/// Zahl, die man beim Lesen zweimal prüft und deshalb genau einmal
/// aufschreibt.
var millisecondsFromTicks: Int { Int(self / 10_000) }
}
extension Int {
var ticksFromMilliseconds: Int64 { Int64(self) * 10_000 }
}