import Foundation import MediaKit /// Aus Flaros Antworten werden neutrale Typen. /// /// Die einzige Stelle, an der Flaros Schreibweise in die der App übersetzt /// wird. Alles darüber kennt nur noch `MediaItem`. extension Wire.Movie { func neutral() -> MediaItem { MediaItem( id: id, kind: .movie, title: title, summary: overview, year: year, duration: files.first?.durationSeconds.map { Int($0 * 1000) }, thumb: posterImage, art: backdropImage, playCount: state?.playCount, resumeMilliseconds: state.map { Int($0.positionSeconds * 1000) }, lastPlayedAt: state?.lastPlayedAt, isFavourite: state?.favourite ?? false, isInProgress: state?.inProgress, availableFrom: publishAt, versions: [Wire.fassung(id: id, files: files)] ) } } extension Wire.Series { func neutral() -> MediaItem { MediaItem( id: id, kind: .show, title: title, summary: overview, year: year, thumb: posterImage, art: backdropImage, // **Gesehene Folgen, nicht ungesehene.** Der Server zählt, was // offen ist — die App zeigt daraus die Plakette, rechnet aber // intern mit der Gegenzahl. Beides anzubieten hiesse, zwei Wege // zu derselben Aussage zu haben. availableFrom: publishAt, episodeCount: episodeCount, playedEpisodeCount: episodeCount - unwatchedCount, childCount: seasons.count ) } } extension Wire.Episode { func neutral(series: Wire.Series? = nil) -> MediaItem { MediaItem( id: id, kind: .episode, title: title ?? "Folge \(episode)", summary: overview, duration: files.first?.durationSeconds.map { Int($0 * 1000) }, thumb: stillImage, seriesThumb: series?.posterImage, seriesArt: series?.backdropImage, index: episode, parentIndex: season, seriesTitle: series?.title, // Die Serie aus dem Vertrag, wenn sie nicht mitgegeben wurde: // ohne sie führte der Weg zurück zur Serie ins Leere. seriesID: series?.id ?? seriesId, playCount: state?.playCount, resumeMilliseconds: state.map { Int($0.positionSeconds * 1000) }, lastPlayedAt: state?.lastPlayedAt, isFavourite: state?.favourite ?? false, isInProgress: state?.inProgress, availableFrom: publishAt, versions: [Wire.fassung(id: id, files: files)] ) } } extension Wire.Started { /// Die Kachel aus „Weiterschauen" als Titel. /// /// **Ohne Fassungen.** Die Reihe nennt keine Dateien, und das ist /// richtig so: eine Kachel zeigt Plakat, Titel und Balken. Wer sie /// antippt, holt den Titel einzeln — und bekommt dann alles. func neutral() -> MediaItem { let folge = kind == "episode" return MediaItem( id: id, kind: folge ? .episode : .movie, title: folge ? (subTitle ?? "Folge \(episode ?? 0)") : title, year: year, duration: state.runtimeSeconds.map { Int($0 * 1000) }, thumb: folge ? backdropImage : posterImage, // **Für die Fläche über den halben Bildschirm das Bild der // Serie.** `backdropImage` ist bei einer Folge ihr Standbild — // richtig für die Kachel, falsch für den Aufmacher: wer der // Serie ein Bild ausgesucht hatte, fand dort ein anderes vor. art: folge ? (seriesBackdropImage ?? backdropImage) : backdropImage, seriesThumb: folge ? posterImage : nil, index: episode, parentIndex: season, seriesTitle: folge ? title : nil, seriesID: folge ? seriesId : nil, playCount: state.playCount, resumeMilliseconds: Int(state.positionSeconds * 1000), lastPlayedAt: state.lastPlayedAt, isFavourite: state.favourite, isInProgress: state.inProgress, availableFrom: publishAt ) } } extension Wire.Library { func neutral() -> MediaLibrary { MediaLibrary( id: id, title: name, // Flaro kennt genau zwei Arten. Was es nicht gibt, als `.other` // durchzulassen wäre eine Vorsorge für einen Fall, den der // Server gar nicht erzeugen kann. kind: kind.lowercased() == "shows" ? .show : .movie) } } extension Wire.File { func neutral(itemID: String) -> MediaFile { MediaFile( id: id, key: "/api/v1/files/\(id)/download", itemID: itemID, container: container, size: sizeBytes, duration: durationSeconds.map { Int($0 * 1000) }, file: fileName, streams: streams.map { $0.neutral() }) } } extension Wire.Stream { func neutral() -> MediaStream { let art: MediaStream.Kind = switch kind.lowercased() { case "video": .video case "subtitle": .subtitle default: .audio } return MediaStream( // **Die Spurnummer ist die Kennung.** `MediaStream.index` liest // sie aus `id` zurück, und daran hängt die Spurwahl beim // Umpacken. Eine eigene Kennung wäre hier eine, die niemand // zurückrechnen kann. id: String(index), kind: art, codec: codec, language: language, languageCode: language, displayTitle: title ?? language, channels: channels, isSelected: isDefault, isForced: isForced, title: title, isDefault: isDefault) } } extension Wire { /// Eine Fassung aus den Dateien eines Titels. /// /// **Alle Dateien in einer Fassung**, nicht eine Fassung je Datei: Flaro /// führt mehrere Dateien zu einem Titel als Fassungen desselben Films — /// 4K und 1080p nebeneinander — und `MediaVersion.files` ist genau dafür /// da. Die Eckdaten kommen aus der ersten Datei; welche gespielt wird, /// entscheidet die App. static func fassung(id: String, files: [Wire.File]) -> MediaVersion { let erste = files.first let bild = erste?.streams.first { $0.kind.lowercased() == "video" } let ton = erste?.streams.first { $0.kind.lowercased() == "audio" } return MediaVersion( id: erste?.id ?? id, container: erste?.container, videoCodec: bild?.codec, audioCodec: ton?.codec, videoResolution: bild?.height.map(String.init), width: bild?.width, height: bild?.height, audioChannels: ton?.channels, duration: erste?.durationSeconds.map { Int($0 * 1000) }, files: files.map { $0.neutral(itemID: id) }) } }