namespace Flaro.Api.Endpoints; // Was über die Leitung geht. Bewusst eigene Typen und nicht die der Domäne: // sonst wandert jede Umbenennung im Datenmodell ungefragt in die Schnittstelle // und bricht Clients, die niemand mehr aktualisiert. public sealed record SystemInfo( string Version, string Build, string ServerName, bool NeedsSetup, IReadOnlyList Attributions, /// Wie hoch der Aufmacher steht: „niedrig", „mittel", „hoch", „sehrhoch". /// /// /// Hier und nicht in einer eigenen Abfrage. Diese Auskunft holt /// der Client ohnehin als Erstes, noch vor der Anmeldung. Käme die Höhe /// später, stünde der Aufmacher einen Augenblick in der Vorgabe und /// spränge dann — genau das, was eine einstellbare Höhe vermeiden soll. /// string HeroHeight = "mittel"); /// Ein Ablageort des Servers, mitsamt Belegung. /// /// /// Die Umgebungsvariable, über die er gesetzt wird. Sie wird mit /// angezeigt, weil sie die Antwort auf die nächste Frage ist: „und wie /// ändere ich das?" Im Betrieb umbiegen lässt sich ein Pfad nicht — dann /// lägen die Dateien woanders als die Datenbank sie vermutet. /// /// Wie viel davon auf den Bildspeicher entfällt; sonst null. /// Wie viel auf dem Kratzplatz liegen bleiben darf. /// /// Obergrenze in Gigabyte; 0 heisst unbegrenzt. /// Ab wann Unbenutztes auch unterhalb der Grenze geht; 0 heisst nie. /// Wie lange frisch Gebrauchtes in jedem Fall bleibt. public sealed record MaintenanceSettingsRequest( double? CapGb, int? UnusedDays, int? GraceMinutes, int? BackupDays = null, int? BackupKeep = null, string? BackupFolder = null, int? OptimizeDays = null, int? ImageDays = null); public sealed record MaintenanceSettingsResponse( double CapGb, int UnusedDays, int GraceMinutes, int BackupDays, int BackupKeep, string BackupFolder, int OptimizeDays, int ImageDays); /// /// Die Unterordner, wo es welche gibt — heute nur beim Kratzplatz. /// Mit Dateizahl, denn bei den Untertiteln ist die Anzahl das /// Problem und nicht die Grösse, und das sieht man an Bytes nicht. /// Additiv: Bilder bleibt stehen, damit kein Client bricht. /// public sealed record Speicherort( string Name, string Variable, string Pfad, long Belegt, long? Frei, long? Bilder, IReadOnlyList? Teile = null); /// Dateien, die zu keinem Titel gehören — mit Begründung. public sealed record Unzugeordnet(int Gesamt, IReadOnlyList Dateien); /// Ein Titel, zu dem die Metadatenquelle nichts hergab. /// /// „movie" oder „series" — damit der Client weiss, wohin. /// Wie viele Dateien daran hängen. /// Ein Dateiname, damit man den Titel wiedererkennt. /// /// /// Weil die Zahl allein nichts erklärt. In der Übersicht standen 448 /// Filme, in Plex 451 — und nirgends stand, welche drei fehlen oder warum. /// Eine Mediathek, die etwas nicht zuordnen konnte, muss sagen was. /// public sealed record OhneZuordnung( Guid Id, string Art, string Titel, int? Jahr, int Dateien, string? Beispiel); /// Welche Mehrfachfassungen abgehakt oder wieder hervorgeholt werden. /// /// /// true hakt ab, false holt zurück. Ein Weg statt zweier /// Endpunkte: es ist dieselbe Spalte, nur einmal gesetzt und einmal geleert. /// public sealed record FassungenRequest(IReadOnlyList Ids, bool Ignorieren = true); /// Titel mit mehr als einer Datei — dort versteckt sich Falsches. public sealed record Mehrfachbericht( int Gesamt, IReadOnlyList Titel); /// Die Dateien, die an diesem einen Titel hängen. public sealed record Mehrfachtitel( Guid Id, string Art, string Titel, int? Jahr, IReadOnlyList Dateien); /// Eine Datei an einem Titel — mit Kennung, damit sie sich herauslösen lässt. public sealed record Fassungsdatei(Guid Id, string Name); public sealed record UnzugeordneteDatei(Guid Id, string Pfad, string Grund); /// Wie viele Titel in welcher Sprache vorliegen. /// /// null heisst: vor der Einführung dieser Spalte geholt. public sealed record Sprachanteil(string? Sprache, int Anzahl); /// Wie viele nicht in der Serversprache vorliegen. public sealed record Sprachverteilung( string Serversprache, IReadOnlyList Anteile, int Abweichend); public sealed record TitelRequest(string? Title); public sealed record ServerNameRequest(string Name); public sealed record SetupRequest(string UserName, string Password); /// /// Ob die Anmeldung einen geschlossenen Browser überlebt. Vorgabe ist /// ja — am eigenen Rechner will niemand sich täglich neu anmelden. Wer an /// einem fremden sitzt, schaltet es ab, und das Cookie verfällt mit dem /// Fenster. /// /// Das Token selbst läuft in keinem Fall ab; es wird widerrufen, nicht /// vergessen. Was hier entschieden wird, ist allein die Lebensdauer des /// Cookies im Browser. /// public sealed record LoginRequest( string UserName, string Password, string DeviceName, string DeviceId, bool StaySignedIn = true); public sealed record AuthenticatedUser( Guid Id, string UserName, bool IsAdministrator, /// Die bevorzugte Sprache — null heisst: die des Servers. string? Language = null, /// Das eigene Bild, als Schlüssel in die Bildablage. /// /// /// Hier und nicht in einer eigenen Abfrage. Die Kopfleiste zeigt es /// auf jeder Seite; es nach der Anmeldung noch einmal holen zu müssen /// hiesse, dass der Buchstabenkreis kurz aufblitzt und dann wechselt. /// string? AvatarImage = null, /// Der Besitzer — ihm nimmt niemand das Adminrecht. /// /// /// Hinten angehängt, wie jedes neue Feld in dieser Datei: in der Mitte /// eingeschoben bricht es jede Aufrufstelle, die die Werte der Reihe nach /// übergibt — und davon gibt es hier drei. /// bool IsOwner = false); public sealed record LoginResponse(string Token, AuthenticatedUser User); public sealed record CodeRequest(string DeviceName, string DeviceId); /// Was der Fernseher nach dem Anfordern bekommt. /// /// /// Der Handle ist das Geheimnis und wird nicht angezeigt — der /// Code ist das, was auf dem Bildschirm steht. /// public sealed record CodeResponse(string Code, string Handle, DateTimeOffset ExpiresAt); public sealed record CodeState(string State, LoginResponse? Login); public sealed record CreateUserRequest(string UserName, string Password, bool IsAdministrator); /// Adminrecht geben oder nehmen. public sealed record RechtRequest(bool IsAdministrator); /// Eine der vier Stufen für den Aufmacher. public sealed record HoeheRequest(string Height); /// Eine Bibliothek, wie die Oberfläche sie zeigt. /// /// /// ItemCount ist null, wenn er nicht mitgezählt wurde — etwa /// direkt nach dem Anlegen oder Umbenennen. Nicht 0: „keine Titel" und /// „nicht gezählt" sind verschiedene Auskünfte, und eine frisch angelegte /// Bibliothek als leer auszugeben wäre die falsche. /// public sealed record LibraryResponse( Guid Id, string Name, string Kind, string Path, int? ItemCount, /// Ob neue Funde erst geprüft werden. bool ReviewNewItems = false, /// Wie viele Titel dieser Mediathek noch auf Freigabe warten. /// /// /// Hier und nicht nur in der Prüfliste. Wer eine Mediathek /// scannt, sieht das Ergebnis an ihr — dass zwanzig Titel irgendwo anders /// warten, erfährt er sonst nur, wenn er weiterscrollt. /// int PendingCount = 0, /// Wie viele Mediendateien in dieser Mediathek liegen. /// /// /// Weil „455 Filme" und „461 Dateien" beide stimmen können. Plex /// zählte 451, Flaro 449 — und der Scan lief über 461 Dateien. Drei /// Zahlen für drei verschiedene Dinge, und keine davon falsch. Nebeneinander /// zu sehen, wie sie sich aufteilen, beendet die Frage; eine davon allein /// zu zeigen hält sie offen. /// int FileCount = 0, /// Wie viele Folgen — nur bei Serien, sonst null. int? EpisodeCount = null, /// Dateien, die an keinem Titel hängen. int UnassignedCount = 0, /// Dateien über die erste je Titel hinaus — zweite Fassungen. int ExtraVersionCount = 0); public sealed record RenameLibraryRequest(string Name); public sealed record CreateLibraryRequest( string Name, string Kind, string Path, /// Ob neue Funde erst geprüft werden. Vorgabe: nein. bool ReviewNewItems = false); public sealed record PruefungRequest(bool ReviewNewItems); /// Die Reihenfolge der Mediatheken, von oben nach unten. public sealed record ReihenfolgeRequest(IReadOnlyList? Ids); public sealed record SprachRequest(string? Language); /// Welche Sprache gilt — und wie weit sie abgedeckt ist. public sealed record SprachStand(string? Language, int Translated, int Total); /// Welche Bilder gerade gesetzt sind — und welche von Hand. public sealed record Bildstand( string? Poster, string? Backdrop, string? Logo, bool PosterPinned, bool BackdropPinned, bool LogoPinned); public sealed record Bildauswahl( Bildstand Current, IReadOnlyList Candidates); public sealed record BildWahlRequest(string Kind, string? Url); /// Ein Titel, der noch wartet oder geplant ist. /// /// /// Eine Zeile je Serie, nicht je Folge. Bei zwanzig neuen Folgen /// derselben Serie wären zwanzig Zeilen zwanzig Entscheidungen für eine /// Sache. Die Folgen stecken in ; freigegeben werden sie /// gemeinsam. /// public sealed record Pruefeintrag( /// movie oder series. string Kind, Guid Id, string Title, int? Year, string? PosterImage, /// null heisst: wartet. Ein Zeitpunkt in der Zukunft heisst: geplant. /// /// /// Bei einer Serie nur gesetzt, wenn alle Folgen denselben Termin tragen — /// sonst wäre es eine Behauptung über zwanzig Folgen, von denen eine /// anders liegt. /// DateTimeOffset? PublishAt, string? Path, /// Ob eine Zuordnung besteht — ohne sie fehlen Titel und Bilder. bool Matched, /// Wie viele Titel dahinterstecken — bei einer Serie die Zahl der Folgen. int Count, /// Worauf eine Freigabe wirkt. IReadOnlyList Ids, /// Warum der Eintrag hier steht: neu oder geaendert. /// /// /// Zwei verschiedene Dinge in einer Liste, und das mit Absicht. Ein /// neuer Fund wartet auf Freigabe und ist bis dahin unsichtbar. Ein /// geänderter Titel steht längst in der Mediathek — er soll nur nicht /// unbemerkt anders heissen oder ein anderes Plakat tragen. Beides gehört /// vor dieselben Augen, aber nicht unter denselben Knopf: das eine wird /// veröffentlicht, das andere nur zur Kenntnis genommen. /// string Grund = "neu"); public sealed record FreigabeRequest( IReadOnlyList? Ids, DateTimeOffset? PublishAt, /// Statt einzelner Kennungen: alles Wartende dieser Mediathek. Guid? LibraryId = null); /// Ein Protokolltag. public sealed record Protokolltag(string Date, long Bytes); /// Eine Datei, die der letzte Scan nicht mehr gefunden hat. public sealed record VermissteDatei( Guid FileId, string Path, string? VersionLabel, DateTimeOffset MissingSince, string LibraryName, /// Zu welchem Titel sie gehört — damit klar ist, was fehlen würde. string? Title, Guid? MovieId, Guid? EpisodeId); // --- Stufe 2: Bibliothek ------------------------------------------------- public sealed record ScanProblem(string Path, string Reason); public sealed record ScanStatus( string State, DateTimeOffset? StartedAt, DateTimeOffset? FinishedAt, int? FilesFound, int? Added, int? Updated, int? Removed, int? Unrecognised, int? Probed, IReadOnlyList? Problems, string? Error, MetadataProgress? Metadata, /// Wie viele Dateien dieser Lauf als verschwunden gemeldet hat. int? Missing = null, /// Woran der Lauf gerade ist, in einem Satz. string? Step = null); /// Was das Nachschlagen bei der Metadatenquelle ergeben hat. public sealed record MetadataProgress( int MoviesMatched, int SeriesMatched, int EpisodesMatched, bool Skipped, string? Error); /// Eine Spur in einer Datei. public sealed record MediaStreamResponse( int Index, string Kind, string? Codec, string? Language, string? Title, bool IsDefault, bool IsForced, int? Channels, int? Width, int? Height, /// Die Zeile, wie sie angezeigt werden soll. /// /// /// Vom Server formuliert, nicht vom Client. „DEU EAC3 6.0" wird /// „Deutsch · Dolby Digital Plus · 5.1" — und die Apps brauchen denselben /// Satz. Zweimal formuliert driftet er auseinander, und dann heisst /// dieselbe Tonspur am Fernseher anders als im Browser. Die Rohwerte /// stehen weiterhin daneben; wer sie braucht, hat sie. /// string? Display = null, /// Die Auflösung als Wort — „4K", „Full HD", „HD", „SD". /// /// /// Eigenes Feld, damit der Client nichts zerlegen muss. Die /// Oberfläche las die Stufe bisher mit display.split(' · ')[0] /// heraus — richtig, solange vor der Auflösung nie etwas anderes steht, /// und falsch an dem Tag, an dem doch. /// string? ResolutionLabel = null, /// Dieselbe Auflösung genau — „1080p". Gehört in den Tooltip. string? ResolutionExact = null, /// Die Sprache als Wort — „Deutsch" statt „deu". /// /// /// Damit eine Liste von Sprachen ohne Rätselraten entsteht. Die /// Detailansicht nennt, in welchen Sprachen ein Film vorliegt; sie dafür /// aus herauszuschneiden hiesse, sich auf dessen /// Satzbau zu verlassen. /// string? LanguageName = null); /// Eine Datei hinter einem Titel — eine Fassung also. /// /// /// Nur der Dateiname, nicht der Pfad. Der Pfad verriete jedem /// Angemeldeten den Aufbau des Servers, und gebraucht wird er nirgends: die /// App spricht Dateien über ihre Kennung an. Zum Unterscheiden zweier /// Fassungen genügt der Name. /// /// /// Wie diese Fassung heisst — „Mobil", „4K", „Director's Cut" — oder /// null, wenn es nur eine gibt. Ohne diese Angabe standen zwei Dateien /// derselben Folge als zwei gleich aussehende Knopfreihen untereinander, und /// die Folge wirkte doppelt. /// /// /// Der Ordner unterhalb der Mediathek — null für alle, die nicht /// verwalten dürfen. /// /// /// Weil man einer falschen Zuordnung am Ordner sofort ansieht, dass sie /// falsch ist. Steht über der Beschreibung „Hanni & Nanni 3" und der /// Ordner heisst Hanni und Nanni, ist die Sache in einer Sekunde klar /// — ohne Umweg über den Zuordnungsdialog. /// /// /// Nur der Teil unterhalb der Mediathek, und nur für Administratoren. /// Der volle Pfad verriete jedem Angemeldeten den Aufbau des Servers; der /// relative sagt genau das, was zum Beurteilen nötig ist. /// /// public sealed record MediaFileResponse( Guid Id, string FileName, long SizeBytes, string? Container, double? DurationSeconds, IReadOnlyList Streams, string? VersionLabel = null, string? Folder = null); public sealed record MovieResponse( Guid Id, string Title, int? Year, string? Overview, string? PosterImage, string? BackdropImage, IReadOnlyList Files, ItemStateResponse? State, /// Der Schriftzug des Titels — fehlt oft, und das ist in Ordnung. string? LogoImage = null, int? RuntimeMinutes = null, double? Rating = null, string? Tagline = null, string? ContentRating = null, /// Leer in Listen, gefüllt beim Einzelabruf. /// /// /// Eine Seite mit fünfzig Titeln braucht keine Besetzungslisten. /// Sie mitzuschicken hiesse, tausend Namen über die Leitung zu schicken, /// von denen keiner angezeigt wird. /// IReadOnlyList? Genres = null, IReadOnlyList? Credits = null, /// Der Titel in der Sprache des Originals, wenn er abweicht. string? OriginalTitle = null, /// Ab wann sichtbar — null heisst: wartet auf Freigabe. /// /// /// Damit die Mediathek es zeigen kann. Für einen Administrator /// stehen wartende Titel mitten zwischen den freigegebenen, ohne dass /// etwas sie unterscheidet — und wer einen davon sucht, findet ihn nur /// über die Prüfliste. /// DateTimeOffset? PublishAt = null, /// Ob der Titel von Hand gesetzt wurde. bool TitlePinned = false, /// Wonach einsortiert wird, wenn nicht nach dem Titel. /// /// /// Damit der Client weiss, wohin ein Titel gehört. Nach einer /// Korrektur stand „WALL·E" weiter unter E, bis man die Seite neu lud — /// die Kachel wusste schlicht nicht, dass sie woandershin gehört. /// string? SortTitle = null, /// Ob der Sortiertitel von Hand gesetzt wurde. bool SortTitlePinned = false, /// Bewertungen von anderswo — leer, wenn keine Quelle eingerichtet ist. /// /// /// Neben Rating, nicht statt dessen. Das Feld ist die /// Wertung von TMDB und bleibt, wo es ist; ein Client, der nur das /// kennt, zeigt weiter dasselbe wie bisher. /// IReadOnlyList? ExternalRatings = null); public sealed record CreditResponse( Guid Id, string Name, string Role, string? Character, string? Image); public sealed record SeriesResponse( Guid Id, string Title, int? Year, string? Overview, string? PosterImage, string? BackdropImage, int EpisodeCount, IReadOnlyList Seasons, /// Wie viele Folgen dieser Benutzer noch nicht gesehen hat. /// /// /// Statt eines Zustands je Serie: eine Serie wird nicht angeschaut, /// ihre Folgen werden es. Was man an der Kachel sehen will, ist die Zahl /// — bei Plex steht sie als Plakette in der Ecke. /// int UnwatchedCount, string? LogoImage = null, int? RuntimeMinutes = null, double? Rating = null, string? Tagline = null, string? ContentRating = null, IReadOnlyList? Genres = null, IReadOnlyList? Credits = null, /// Der Titel in der Sprache des Originals, wenn er abweicht. string? OriginalTitle = null, /// Ab wann sichtbar — null heisst: wartet auf Freigabe. /// /// /// Damit die Mediathek es zeigen kann. Für einen Administrator /// stehen wartende Titel mitten zwischen den freigegebenen, ohne dass /// etwas sie unterscheidet — und wer einen davon sucht, findet ihn nur /// über die Prüfliste. /// DateTimeOffset? PublishAt = null, /// Favorit, Bewertung, Häkchen — zur Serie als Ganzem. /// /// /// Neben , nicht an dessen Stelle. Die /// Zahl sagt, wie viel noch aussteht; dieser Zustand sagt, was der /// Benutzer über die Serie selbst festgehalten hat. Ohne ihn konnte eine /// Kachel nicht wissen, ob „Als gesehen markieren" oder „Als ungesehen /// markieren" darauf gehört — sie bot immer das erste an. /// ItemStateResponse? State = null, /// Ob der Titel von Hand gesetzt wurde. bool TitlePinned = false, /// Wonach einsortiert wird, wenn nicht nach dem Titel. /// /// /// Damit der Client weiss, wohin ein Titel gehört. Nach einer /// Korrektur stand „WALL·E" weiter unter E, bis man die Seite neu lud — /// die Kachel wusste schlicht nicht, dass sie woandershin gehört. /// string? SortTitle = null, /// Ob der Sortiertitel von Hand gesetzt wurde. bool SortTitlePinned = false, /// Bewertungen von anderswo — leer, wenn keine Quelle eingerichtet ist. /// /// /// Neben Rating, nicht statt dessen. Das Feld ist die /// Wertung von TMDB und bleibt, wo es ist; ein Client, der nur das /// kennt, zeigt weiter dasselbe wie bisher. /// IReadOnlyList? ExternalRatings = null); public sealed record EpisodeResponse( Guid Id, int Season, int Episode, string? Title, string? Overview, string? StillImage, IReadOnlyList Files, ItemStateResponse? State, DateTimeOffset? PublishAt = null, /// Zu welcher Serie sie gehört. /// /// /// Damit eine Folge für sich geöffnet werden kann. Die Apps /// reichen eine Kennung durch, ohne zu wissen, was dahintersteckt. War es /// eine Folge, fand sich weder ein Film noch eine Serie, und die /// Startseite meldete „Dieser Titel ist auf dem Server nicht mehr /// vorhanden" — über einen Titel, den sie selbst gerade angezeigt hatte. /// Guid? SeriesId = null); public sealed record Page(IReadOnlyList Items, int Total); /// Die saisonale Reihe — mit ihrem Namen, weil er wechselt. /// /// /// Was über der Reihe steht. null heisst: heute hat nichts Saison, /// oder der Anlass findet nichts — dann zeigt der Client gar keine Reihe. /// public sealed record Saisonreihe(string? Occasion, Page Items); /// /// Ob eine zweite Bildquelle eingerichtet ist. Auch hier kommt der Schlüssel /// selbst nicht zurück — zum Anzeigen genügt, dass einer da ist. /// public sealed record MetadataSettingsResponse( bool HasApiKey, string Language, bool UsingProjectKey, bool HasFanartKey = false, bool HasOmdbKey = false); /// Was ein Client über der Mediathek anzeigen muss. /// /// /// Bedingung, nicht Höflichkeit. TMDB verlangt in seinen /// Nutzungsbedingungen, dass das Logo gezeigt wird und dieser Satz dasteht. /// Der Server liefert beides, damit kein Client es abtippen muss — und keiner /// es vergisst. /// public sealed record AttributionResponse(string Text, string LogoUrl, string Url); public sealed record MetadataSettingsRequest( string? ApiKey, string? Language, string? FanartApiKey = null, string? OmdbApiKey = null); /// Was der Server über die Wiedergabe entscheidet. /// /// /// Wie weit man gekommen sein muss, damit ein Titel in „Weiterschauen" steht. /// /// Ab welchem Anteil er als gesehen gilt, 0 bis 1. /// „qualitaet", „ausgewogen" oder „tempo". public sealed record PlaybackSettingsResponse( double ResumeAfterSeconds, double WatchedFraction, string Encoder); /// Was geändert werden soll — null heisst: unverändert. public sealed record PlaybackSettingsRequest( double? ResumeAfterSeconds, double? WatchedFraction, string? Encoder); /// Was danach gilt, und wie viele Zeilen das umgeworfen hat. /// /// /// Die Zahl gehört in die Antwort. Eine neue Schwelle wirkt rückwirkend /// auf alles, was schon in „Weiterschauen" steht; ohne diese Rückmeldung /// änderte sich die Startseite scheinbar von selbst. /// public sealed record PlaybackSettingsApplied( double ResumeAfterSeconds, double WatchedFraction, string Encoder, int Reevaluated); // --- Stufe 3: Ausliefern ------------------------------------------------- /// Was mit einer Datei geschehen wird, bevor sie hinausgeht. /// /// /// Damit vorher feststeht, was man bekommt. Drei Gigabyte /// herunterzuladen und erst danach zu merken, dass die Untertitel fehlen oder /// die falsche Tonspur drin ist, war der Anlass für dieses ganze Projekt. /// public sealed record DeliveryInfo( string Method, IReadOnlyList Reasons, IReadOnlyList AudioToEncode, IReadOnlyList SubtitlesDropped, string PackageState, double Progress, string? Error); // --- Zuordnung korrigieren ----------------------------------------------- /// Ein Vorschlag, aus dem gewählt werden kann. public sealed record MatchCandidate( int TmdbId, string Title, int? Year, string? Overview, string? PosterUrl); public sealed record ApplyMatchRequest(int TmdbId); /// Wie die Zuordnung jetzt aussieht. /// /// /// Welcher Eintrag das jetzt ist. Meist der, den man korrigiert hat — /// nach einem Zusammenlegen aber der überlebende, und der hat eine andere /// Kennung. /// /// /// Ob dabei zwei Einträge zu einem wurden. /// /// /// Ohne diese Auskunft stand der korrigierte Titel weiter in der Liste, /// obwohl es ihn nicht mehr gab: wer ihn dann bearbeiten wollte, bekam eine /// 404 — und zwar als englisches „Not Found", weil auch das nicht stimmte. /// Der Client kann die Kachel jetzt wegnehmen, statt auf einen gelöschten /// Eintrag zu zeigen. /// /// public sealed record MatchState( int? TmdbId, string? ImdbId, bool Locked, string Title, int? Year, Guid Id = default, bool Merged = false); // --- Stufe 4: Fortschritt und Zustand ------------------------------------- /// Eine Fortschrittsmeldung. /// /// /// In Sekunden, nicht in Ticks. Jellyfin rechnet in Hundertnanosekunden, /// und jeder Client muss das wissen. Sekunden als Gleitkommazahl sind in jeder /// Sprache dasselbe und in der Entwicklerkonsole lesbar. /// /// RuntimeSeconds ist freiwillig: kennt der Client die Laufzeit der /// Fassung, die er gerade abspielt, ist seine Angabe genauer als die des /// Servers. Fehlt sie, nimmt der Server die aus der Datei. /// public sealed record ProgressRequest( double PositionSeconds, double? RuntimeSeconds, /// Die letzte Meldung einer Wiedergabe. /// /// /// Nimmt den Eintrag sofort aus „läuft gerade". Ohne sie stünde ein /// beendeter Film dort noch eine Minute, bis das Lebenszeichen ausbleibt /// — und wer gerade den Spieler geschlossen hat, sieht sich selbst noch /// beim Schauen zu. /// bool? Stopped = null); public sealed record WatchedRequest(bool Watched); public sealed record FavouriteRequest(bool Favourite); /// Mehrere Titel auf einmal abhaken — oder das Häkchen wegnehmen. /// /// /// Eine Anfrage statt zweihundert. „Alle als gesehen markieren" über /// eine Mediathek hinweg wäre sonst ein Anfragensturm, bei dem mittendrin /// etwas scheitert und niemand weiss, was durchging. /// public sealed record BulkWatchedRequest(IReadOnlyList Ids, bool Watched); /// Wie viele Titel sich dadurch geändert haben. /// /// Kennungen, zu denen es nichts gibt — stillschweigend zu übergehen hiesse, /// einen Teilerfolg als vollen auszugeben. /// public sealed record BulkWatchedResponse(int Changed, IReadOnlyList Unknown); /// Welche Titel neu geholt werden sollen — leer heisst: alle. public sealed record RefreshRequest(IReadOnlyList? Ids); /// Was ein Spieler kann und was er sehen will. /// /// Schachteln, die das Gerät öffnet — „mp4", „webm". /// /// Die gewünschte Stufe: 1080, 720 oder 480. null heisst Originalauflösung. /// /// /// Welche Tonspur — die Nummer aus der Datei. null nimmt die erste. /// /// Ab welcher Sekunde des Films. /// /// /// Das Gerät sagt, was es kann, statt dass der Server es rät. Vorher /// bekam jeder Browser Apples Profil — und mit ihm HEVC, AC3 und EAC3, von /// denen er keines öffnen kann. /// public sealed record StreamRequest( IReadOnlyList? Containers, IReadOnlyList? VideoCodecs, IReadOnlyList? AudioCodecs, int? Height = null, int? AudioStream = null, double? StartSeconds = null, /// Umkodieren, egal was das Profil sagt. /// /// /// Für den zweiten Versuch. Der Spieler schickt sein Profil aus /// canPlayType — und das ist eine Behauptung: „kenne ich" heisst /// nicht „kann ich diese Datei". Scheitert die Wiedergabe trotzdem, weiss /// der Browser mehr als jede Abfrage vorher, und sein Urteil schlägt /// seine eigene Auskunft. /// bool Transcode = false, /// Um wie viele Millisekunden der Ton verschoben wird. /// /// /// Für Dateien, bei denen es ab Werk nicht stimmt. Positiv heisst /// später, negativ früher. Der Server schiebt dabei nicht am Bild, sondern /// liest die Datei ein zweites Mal mit -itsoffset und nimmt von dort /// nur den Ton — mit einer Eingabe verschöbe sich beides gemeinsam und /// nichts wäre gewonnen. /// int AudioOffsetMs = 0); /// Eine wählbare Spur, so wie sie im Menü steht. public sealed record SpurAuskunft(int Index, string Label, string? Language, bool IsDefault); /// /// Ab welcher Sekunde des Films dieser Strom beginnt. Der Spieler zählt ab /// null — ohne diesen Wert ginge jeder gemeldete Stand um den Vorlauf daneben. /// public sealed record StreamResponse( Guid SessionId, string Url, string Method, double StartSeconds, int? Height, int? AudioStream, IReadOnlyList AudioStreams, IReadOnlyList SubtitleStreams); /// Eine Bewertung von anderswo, fertig zum Hinschreiben. /// /// „imdb", „rottenTomatoes", „metacritic". /// Was dastehen soll — in der Form, die man von der Quelle kennt. public sealed record Fremdwertungsantwort(string Source, double Value, string Display); public sealed record RatingRequest(int? Rating); /// Was ein Benutzer mit einem Titel zu tun hatte. public sealed record ItemStateResponse( double PositionSeconds, double? RuntimeSeconds, bool Watched, int PlayCount, bool Favourite, int? Rating, bool Dismissed, DateTimeOffset? LastPlayedAt, /// Ob der Titel in „Weiterschauen" steht. /// /// /// Damit die Beschriftung dasselbe sagt wie die Reihe. Web und Apps /// entschieden „Weiterschauen" gegen „Abspielen" selbst, allein daran, ob /// eine Stelle über null steht. Seit es eine einstellbare Untergrenze gibt, /// lief das auseinander: ein Titel, in den man eine Sekunde hineingeschaut /// hat, stand nicht mehr in der Reihe — hiess aber weiter „Weiterschauen". /// /// Der Server hat diese Frage ohnehin beantwortet; sie noch einmal zu /// stellen hiesse, dieselbe Regel an vier Stellen zu pflegen. /// bool InProgress = false); /// Eine Zeile in „Weiterschauen". /// /// /// Film und Folge in einer Form, weil die Reihe beide mischt. Bei einer Folge /// steht in Title die Serie und in SubTitle die Folge — so liest /// sich die Kachel wie bei Plex: oben, worum es geht, darunter, wo man ist. /// /// /// Die Kennung des Titels selbst — bei einer Folge also die der Folge. Dorthin /// geht der Fortschritt. /// /// /// Zu welcher Serie die Folge gehört — null bei einem Film. /// /// /// Ohne diese Angabe führte die Kachel ins Leere. Sie trug die /// Folgenkennung und sollte damit eine Serienseite öffnen; die gibt es unter /// dieser Kennung nie, und heraus kam „Diese Serie gibt es nicht mehr" — /// ausgerechnet unter „Nächste Folge", wo man gerade weiterschauen wollte. /// /// public sealed record ContinueEntry( Guid Id, string Kind, string Title, string? SubTitle, int? Season, int? Episode, int? Year, string? PosterImage, string? BackdropImage, ItemStateResponse State, Guid? SeriesId = null, /// Die Datei, die abgespielt würde. /// /// /// Damit „Abspielen" abspielt. Der Aufmacher der Startseite hatte /// keine Datei zur Hand und konnte deshalb nur die Detailseite öffnen — /// ein Knopf, auf dem „Abspielen" steht und der etwas anderes tut. /// Guid? FileId = null, double? DurationSeconds = null, /// Ab wann abspielbar. In der Zukunft heisst: noch nicht. DateTimeOffset? PublishAt = null, /// Das breite Bild der Serie, zu der die Folge gehört. /// /// /// Neben dem Folgenbild, nicht statt seiner. Die Kachel will das /// Bild der Folge — es zeigt, wo man steht. Der Aufmacher füllt die halbe /// Seite und will das Bild der Serie; dort stand bisher das Folgenbild, /// und wer das Serienbild selbst ausgesucht hatte, fand ein anderes vor. /// string? SeriesBackdropImage = null); /// Was jemand geschaut hat — nur für ihn selbst. /// /// /// Zwei Arten Zahl, und der Unterschied gehört dazu. /// LoggedSeconds kommt aus dem Sehprotokoll und fängt an dem Tag an, /// an dem es eingebaut wurde. WatchedTitles und /// WatchedRuntimeSeconds kommen aus den Sehständen und sind für die /// ganze Vergangenheit vollständig. Beides als eine Zahl auszugeben hiesse, /// eine davon zu behaupten. /// public sealed record StatistikResponse( double LoggedSeconds, int WatchedTitles, double WatchedRuntimeSeconds, DateTimeOffset? Since, IReadOnlyList Weeks, IReadOnlyList Recent, IReadOnlyList Genres); /// Der Montag dieser Woche. public sealed record Wochenwert(DateOnly Start, double Seconds); public sealed record Zuletztgesehen( Guid Id, string Kind, string Title, string? SubTitle, string? PosterImage, DateTimeOffset At, bool Watched); public sealed record Genrewert(string Name, double Seconds); /// Eine laufende Wiedergabe — für das Dashboard. public sealed record SessionResponse( string UserName, string DeviceName, Guid ItemId, string Title, string? SubTitle, string? PosterImage, double PositionSeconds, double? RuntimeSeconds); /// Was der Server gerade tut. public sealed record ServerStatusResponse( int Playing, int Packaging, int Scanning, double Uptime); /* ---------- Meldungen ---------- */ /// Was jemand an einem Titel gemeldet hat. public sealed record MeldungRequest( /// bild, ton, untertitel, daten oder sonstiges. string Kind, string Comment, /// Eine Stelle wie 0:34:12 — oder nichts. string? Position = null, /// Nur bei einer Serie: eine einzelne Staffel statt der ganzen. int? SeasonNumber = null); /// Eine Meldung, wie sie gelesen wird. /// /// /// ItemId ist die Kennung der Detailseite, nicht die des /// Gemeldeten. Bei einer Folge und bei einer Staffel steht hier die Serie — /// die Detailseite gehört ihr, wie bei „Weiterschauen" auch. Was genau gemeint /// ist, steht in Target und SubTitle. /// /// Title ist leer, wenn der Titel nicht mehr sichtbar ist. Ein /// Administrator kann einen Titel zurückziehen, nachdem jemand ihn gemeldet /// hat; die Meldung bleibt dann in der eigenen Liste des Melders stehen, nur /// ohne Namen. Sie verschwinden zu lassen hiesse, jemandem seine eigene Eingabe /// wegen einer fremden Entscheidung wegzunehmen. /// public sealed record Meldungseintrag( Guid Id, /// movie, series, season oder episode. string Target, Guid ItemId, string Title, /// „Staffel 2" oder „S2 · F5 — Der Pakt" — sonst null. string? SubTitle, string? PosterImage, string Kind, double? PositionSeconds, string Comment, DateTimeOffset ReportedAt, string ReportedBy, /// null heisst: offen. DateTimeOffset? ResolvedAt); /* ---------- Einladungen ---------- */ /// Was beim Erstellen eines Einladungslinks gewählt wird. public sealed record EinladungRequest( /// Wie viele Tage er gilt. int Days = 7, /// Wie oft er sich einlösen lässt. null heisst: unbegrenzt. int? MaxUses = 1); /// Der frisch erstellte Link — der Token steht hier zum einzigen Mal. /// /// /// Die Adresse baut der Client. Der Server weiss nicht, unter welchem /// Namen er von aussen erreichbar ist; hinter einem Reverse Proxy rät er /// falsch, und ein falscher Einladungslink ist schlimmer als keiner. /// public sealed record EinladungErstellt(Guid Id, string Token, DateTimeOffset ExpiresAt, int? MaxUses); /// Eine Einladung in der Verwaltungsliste — ohne Token. public sealed record Einladungseintrag( Guid Id, string CreatedBy, DateTimeOffset CreatedAt, DateTimeOffset ExpiresAt, int? MaxUses, int UsedCount, DateTimeOffset? RevokedAt, /// Ob sich damit jetzt noch ein Konto anlegen lässt. bool Valid); /// Was die Einladungsseite zeigt, bevor jemand etwas eintippt. public sealed record Einladungsschau(string ServerName, string InvitedBy, DateTimeOffset ExpiresAt); /// Namen und Passwort, die sich der Eingeladene selbst gibt. public sealed record EinloesenRequest(string UserName, string Password);