Die Markensuche lief stundenlang über 95 Serien, 4213 Folgen und 453
Filme und schrieb dabei fünf Zeilen, in denen Kennungen standen statt
Namen. „Markenerkennung für 4f3a…: 12 Marken." beantwortet keine der
Fragen, die in den letzten Tagen tatsächlich aufkamen — welche Staffel
nichts bekommen hat, warum, und ob die Serie überhaupt dran war.
Jetzt nennt jede Zeile den Titel, und es gibt eine je Serie, je Staffel
und je Folge:
„Biohackers": 2 Staffel(n), 18 Folge(n).
„Biohackers", Staffel 2: Vorspann in 8 von 8 Folge(n), Abspann in 1 von 8.
Biohackers S02E01 · Neuanfang: Vorspann 0:31–1:24, Abspann ab 41:12 (über das Bild).
Biohackers S02E07 · Letzte Fahrt: Vorspann 0:31–1:24, kein Abspann.
„Biohackers" fertig: 14 Marke(n).
Die Folgenzeilen werden **aus der Datenbank gelesen, nicht mitgezählt**:
so steht dort, was danach wirklich an der Folge hängt, und nicht, was
dieser eine Lauf geschrieben hat. Nur das beantwortet „warum kommt bei
dieser Folge kein Knopf?".
**Drei Gründe statt eines `false`.** `FilmAsync` gab für „keine Datei",
„schon durchsucht" und „nichts gefunden" dasselbe zurück, und von aussen
waren sie nicht zu unterscheiden. Jetzt sagt jeder seinen Namen.
**Und eine Live-Zeile bei der Mediathek**, wie der Scan sie hat. Sie
benutzt denselben Mechanismus — `Scanschritt` mit einer neuen Phase,
derselbe Endpunkt, ein eigenes Feld. Ein eigenes, weil der Scan sich auf
„fertig" setzt, sobald er die Markensuche angestossen hat; in sein Feld
geschrieben sähe er wieder laufend aus.
**Zwei Fehler sind dabei herausgefallen.** In `NachScanAsync` stand
`if (serien.Count == 0) return;` vor dem Filmteil — eine reine
Filmmediathek bekam nach einem Scan also nie eine Abspannsuche. Und die
fpcalc-Prüfung stand ganz oben und galt für beides, obwohl der Bildweg
fpcalc gar nicht braucht: ohne fpcalc fand der Server keinen einzigen
Filmabspann.
Dazu ein Suchfeld im Protokoll und tausend Anfangszeilen statt
dreihundert. Ohne das wäre eine Zeile je Folge keine Auskunft, sondern
eine Mauer — genau der Fall, gegen den sich `ScanCoordinator` für den
Scan schon einmal entschieden hat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1080 lines
46 KiB
C#
1080 lines
46 KiB
C#
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<AttributionResponse> Attributions,
|
|
/// <summary>Wie hoch der Aufmacher steht: „niedrig", „mittel", „hoch", „sehrhoch".</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Hier und nicht in einer eigenen Abfrage.</b> 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.
|
|
/// </remarks>
|
|
string HeroHeight = "mittel");
|
|
|
|
/// <summary>Ein Ablageort des Servers, mitsamt Belegung.</summary>
|
|
///
|
|
/// <param name="Variable">
|
|
/// Die Umgebungsvariable, über die er gesetzt wird. <b>Sie wird mit
|
|
/// angezeigt</b>, 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.
|
|
/// </param>
|
|
/// <param name="Bilder">Wie viel davon auf den Bildspeicher entfällt; sonst <c>null</c>.</param>
|
|
/// <summary>Wie viel auf dem Kratzplatz liegen bleiben darf.</summary>
|
|
///
|
|
/// <param name="CapGb">Obergrenze in Gigabyte; <c>0</c> heisst unbegrenzt.</param>
|
|
/// <param name="UnusedDays">Ab wann Unbenutztes auch unterhalb der Grenze geht; <c>0</c> heisst nie.</param>
|
|
/// <param name="GraceMinutes">Wie lange frisch Gebrauchtes in jedem Fall bleibt.</param>
|
|
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);
|
|
|
|
/// <param name="Teile">
|
|
/// Die Unterordner, wo es welche gibt — heute nur beim Kratzplatz.
|
|
/// <b>Mit Dateizahl</b>, denn bei den Untertiteln ist die Anzahl das
|
|
/// Problem und nicht die Grösse, und das sieht man an Bytes nicht.
|
|
/// Additiv: <c>Bilder</c> bleibt stehen, damit kein Client bricht.
|
|
/// </param>
|
|
public sealed record Speicherort(
|
|
string Name, string Variable, string Pfad, long Belegt, long? Frei, long? Bilder,
|
|
IReadOnlyList<Teilbericht>? Teile = null);
|
|
|
|
/// <summary>Dateien, die zu keinem Titel gehören — mit Begründung.</summary>
|
|
public sealed record Unzugeordnet(int Gesamt, IReadOnlyList<UnzugeordneteDatei> Dateien);
|
|
|
|
/// <summary>Ein Titel, zu dem die Metadatenquelle nichts hergab.</summary>
|
|
///
|
|
/// <param name="Art">„movie" oder „series" — damit der Client weiss, wohin.</param>
|
|
/// <param name="Dateien">Wie viele Dateien daran hängen.</param>
|
|
/// <param name="Beispiel">Ein Dateiname, damit man den Titel wiedererkennt.</param>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Weil die Zahl allein nichts erklärt.</b> 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.
|
|
/// </remarks>
|
|
public sealed record OhneZuordnung(
|
|
Guid Id, string Art, string Titel, int? Jahr, int Dateien, string? Beispiel);
|
|
|
|
/// <summary>Welche Mehrfachfassungen abgehakt oder wieder hervorgeholt werden.</summary>
|
|
///
|
|
/// <param name="Ignorieren">
|
|
/// <c>true</c> hakt ab, <c>false</c> holt zurück. Ein Weg statt zweier
|
|
/// Endpunkte: es ist dieselbe Spalte, nur einmal gesetzt und einmal geleert.
|
|
/// </param>
|
|
public sealed record FassungenRequest(IReadOnlyList<Guid> Ids, bool Ignorieren = true);
|
|
|
|
/// <param name="Titel">Titel mit mehr als einer Datei — dort versteckt sich Falsches.</param>
|
|
public sealed record Mehrfachbericht(
|
|
int Gesamt, IReadOnlyList<Mehrfachtitel> Titel);
|
|
|
|
/// <param name="Dateien">Die Dateien, die an diesem einen Titel hängen.</param>
|
|
public sealed record Mehrfachtitel(
|
|
Guid Id, string Art, string Titel, int? Jahr, IReadOnlyList<Fassungsdatei> Dateien);
|
|
|
|
/// <summary>Eine Datei an einem Titel — mit Kennung, damit sie sich herauslösen lässt.</summary>
|
|
public sealed record Fassungsdatei(Guid Id, string Name);
|
|
|
|
public sealed record UnzugeordneteDatei(Guid Id, string Pfad, string Grund);
|
|
|
|
/// <summary>Wie viele Titel in welcher Sprache vorliegen.</summary>
|
|
///
|
|
/// <param name="Sprache"><c>null</c> heisst: vor der Einführung dieser Spalte geholt.</param>
|
|
public sealed record Sprachanteil(string? Sprache, int Anzahl);
|
|
|
|
/// <param name="Abweichend">Wie viele nicht in der Serversprache vorliegen.</param>
|
|
public sealed record Sprachverteilung(
|
|
string Serversprache, IReadOnlyList<Sprachanteil> Anteile, int Abweichend);
|
|
|
|
public sealed record TitelRequest(string? Title);
|
|
|
|
public sealed record ServerNameRequest(string Name);
|
|
|
|
public sealed record SetupRequest(string UserName, string Password);
|
|
|
|
/// <param name="StaySignedIn">
|
|
/// <b>Ob die Anmeldung einen geschlossenen Browser überlebt.</b> 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.
|
|
/// </param>
|
|
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,
|
|
/// <summary>Die bevorzugte Sprache — <c>null</c> heisst: die des Servers.</summary>
|
|
string? Language = null,
|
|
|
|
/// <summary>Das eigene Bild, als Schlüssel in die Bildablage.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Hier und nicht in einer eigenen Abfrage.</b> 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.
|
|
/// </remarks>
|
|
string? AvatarImage = null,
|
|
|
|
/// <summary>Der Besitzer — ihm nimmt niemand das Adminrecht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
bool IsOwner = false);
|
|
|
|
public sealed record LoginResponse(string Token, AuthenticatedUser User);
|
|
|
|
public sealed record CodeRequest(string DeviceName, string DeviceId);
|
|
|
|
/// <summary>Was der Fernseher nach dem Anfordern bekommt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Der <c>Handle</c> ist das Geheimnis und wird <b>nicht</b> angezeigt — der
|
|
/// <c>Code</c> ist das, was auf dem Bildschirm steht.
|
|
/// </remarks>
|
|
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);
|
|
|
|
/// <summary>Adminrecht geben oder nehmen.</summary>
|
|
public sealed record RechtRequest(bool IsAdministrator);
|
|
|
|
/// <summary>Eine der vier Stufen für den Aufmacher.</summary>
|
|
public sealed record HoeheRequest(string Height);
|
|
|
|
/// <summary>Eine Bibliothek, wie die Oberfläche sie zeigt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <c>ItemCount</c> ist <c>null</c>, wenn er nicht mitgezählt wurde — etwa
|
|
/// direkt nach dem Anlegen oder Umbenennen. Nicht <c>0</c>: „keine Titel" und
|
|
/// „nicht gezählt" sind verschiedene Auskünfte, und eine frisch angelegte
|
|
/// Bibliothek als leer auszugeben wäre die falsche.
|
|
/// </remarks>
|
|
public sealed record LibraryResponse(
|
|
Guid Id, string Name, string Kind, string Path, int? ItemCount,
|
|
/// <summary>Ob neue Funde erst geprüft werden.</summary>
|
|
bool ReviewNewItems = false,
|
|
/// <summary>Wie viele Titel dieser Mediathek noch auf Freigabe warten.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Hier und nicht nur in der Prüfliste.</b> Wer eine Mediathek
|
|
/// scannt, sieht das Ergebnis an ihr — dass zwanzig Titel irgendwo anders
|
|
/// warten, erfährt er sonst nur, wenn er weiterscrollt.
|
|
/// </remarks>
|
|
int PendingCount = 0,
|
|
|
|
/// <summary>Wie viele Mediendateien in dieser Mediathek liegen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Weil „455 Filme" und „461 Dateien" beide stimmen können.</b> 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.
|
|
/// </remarks>
|
|
int FileCount = 0,
|
|
|
|
/// <summary>Wie viele Folgen — nur bei Serien, sonst <c>null</c>.</summary>
|
|
int? EpisodeCount = null,
|
|
|
|
/// <summary>Dateien, die an keinem Titel hängen.</summary>
|
|
int UnassignedCount = 0,
|
|
|
|
/// <summary>Dateien über die erste je Titel hinaus — zweite Fassungen.</summary>
|
|
int ExtraVersionCount = 0);
|
|
|
|
public sealed record RenameLibraryRequest(string Name);
|
|
|
|
public sealed record CreateLibraryRequest(
|
|
string Name, string Kind, string Path,
|
|
/// <summary>Ob neue Funde erst geprüft werden. Vorgabe: nein.</summary>
|
|
bool ReviewNewItems = false);
|
|
|
|
public sealed record PruefungRequest(bool ReviewNewItems);
|
|
|
|
/// <summary>Die Reihenfolge der Mediatheken, von oben nach unten.</summary>
|
|
public sealed record ReihenfolgeRequest(IReadOnlyList<Guid>? Ids);
|
|
|
|
public sealed record SprachRequest(string? Language);
|
|
|
|
/// <summary>Welche Sprache gilt — und wie weit sie abgedeckt ist.</summary>
|
|
public sealed record SprachStand(string? Language, int Translated, int Total);
|
|
|
|
/// <summary>Welche Bilder gerade gesetzt sind — und welche von Hand.</summary>
|
|
public sealed record Bildstand(
|
|
string? Poster, string? Backdrop, string? Logo,
|
|
bool PosterPinned, bool BackdropPinned, bool LogoPinned,
|
|
/// <summary>Die für den Aufmacher gewählten Bilder, höchstens fünf.</summary>
|
|
IReadOnlyList<string>? Heroes = null);
|
|
|
|
public sealed record Bildauswahl(
|
|
Bildstand Current, IReadOnlyList<Flaro.Core.Metadata.Bildkandidat> Candidates);
|
|
|
|
public sealed record BildWahlRequest(string Kind, string? Url);
|
|
|
|
/// <summary>Die Mehrfachauswahl fürs Aufmacher-Karussell.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Eine Liste und kein einzelner Wert: hier wird nicht ein Bild gesetzt,
|
|
/// sondern die ganze Auswahl ersetzt. Eine leere Liste nimmt sie zurück.
|
|
/// </remarks>
|
|
public sealed record AufmacherWahlRequest(IReadOnlyList<string> Urls);
|
|
|
|
/// <summary>Ein Titel, der noch wartet oder geplant ist.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Eine Zeile je Serie, nicht je Folge.</b> Bei zwanzig neuen Folgen
|
|
/// derselben Serie wären zwanzig Zeilen zwanzig Entscheidungen für eine
|
|
/// Sache. Die Folgen stecken in <see cref="Ids"/>; freigegeben werden sie
|
|
/// gemeinsam.
|
|
/// </remarks>
|
|
public sealed record Pruefeintrag(
|
|
/// <summary><c>movie</c> oder <c>series</c>.</summary>
|
|
string Kind,
|
|
Guid Id, string Title, int? Year, string? PosterImage,
|
|
/// <summary><c>null</c> heisst: wartet. Ein Zeitpunkt in der Zukunft heisst: geplant.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
DateTimeOffset? PublishAt,
|
|
string? Path,
|
|
/// <summary>Ob eine Zuordnung besteht — ohne sie fehlen Titel und Bilder.</summary>
|
|
bool Matched,
|
|
/// <summary>Wie viele Titel dahinterstecken — bei einer Serie die Zahl der Folgen.</summary>
|
|
int Count,
|
|
/// <summary>Worauf eine Freigabe wirkt.</summary>
|
|
IReadOnlyList<Guid> Ids,
|
|
|
|
/// <summary>Warum der Eintrag hier steht: <c>neu</c> oder <c>geaendert</c>.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Zwei verschiedene Dinge in einer Liste, und das mit Absicht.</b> 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.
|
|
/// </remarks>
|
|
string Grund = "neu");
|
|
|
|
public sealed record FreigabeRequest(
|
|
IReadOnlyList<Guid>? Ids,
|
|
DateTimeOffset? PublishAt,
|
|
/// <summary>Statt einzelner Kennungen: alles Wartende dieser Mediathek.</summary>
|
|
Guid? LibraryId = null);
|
|
|
|
/// <summary>Ein Protokolltag.</summary>
|
|
public sealed record Protokolltag(string Date, long Bytes);
|
|
|
|
/// <summary>Eine Datei, die der letzte Scan nicht mehr gefunden hat.</summary>
|
|
public sealed record VermissteDatei(
|
|
Guid FileId,
|
|
string Path,
|
|
string? VersionLabel,
|
|
DateTimeOffset MissingSince,
|
|
string LibraryName,
|
|
/// <summary>Zu welchem Titel sie gehört — damit klar ist, was fehlen würde.</summary>
|
|
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<ScanProblem>? Problems,
|
|
string? Error,
|
|
MetadataProgress? Metadata,
|
|
/// <summary>Wie viele Dateien dieser Lauf als verschwunden gemeldet hat.</summary>
|
|
int? Missing = null,
|
|
/// <summary>Woran der Lauf gerade ist, in einem Satz.</summary>
|
|
string? Step = null,
|
|
/// <summary>Woran die Markensuche gerade ist — <c>null</c>, wenn keine läuft.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Ein eigenes Feld, nicht <see cref="Step"/>.</b> Die Markensuche
|
|
/// hängt sich hinter den Scan und läuft weiter, wenn der längst
|
|
/// „fertig" meldet. In dasselbe Feld geschrieben sähe der Scan wieder
|
|
/// laufend aus, und die Verwaltung käme nie zur Ruhe.
|
|
/// </remarks>
|
|
string? Marken = null);
|
|
|
|
/// <summary>Was das Nachschlagen bei der Metadatenquelle ergeben hat.</summary>
|
|
public sealed record MetadataProgress(
|
|
int MoviesMatched, int SeriesMatched, int EpisodesMatched, bool Skipped, string? Error);
|
|
|
|
/// <summary>Eine Spur in einer Datei.</summary>
|
|
public sealed record MediaStreamResponse(
|
|
int Index, string Kind, string? Codec, string? Language, string? Title,
|
|
bool IsDefault, bool IsForced, int? Channels, int? Width, int? Height,
|
|
/// <summary>Die Zeile, wie sie angezeigt werden soll.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Vom Server formuliert, nicht vom Client.</b> „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.
|
|
/// </remarks>
|
|
string? Display = null,
|
|
/// <summary>Die Auflösung als Wort — „4K", „Full HD", „HD", „SD".</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Eigenes Feld, damit der Client nichts zerlegen muss.</b> Die
|
|
/// Oberfläche las die Stufe bisher mit <c>display.split(' · ')[0]</c>
|
|
/// heraus — richtig, solange vor der Auflösung nie etwas anderes steht,
|
|
/// und falsch an dem Tag, an dem doch.
|
|
/// </remarks>
|
|
string? ResolutionLabel = null,
|
|
/// <summary>Dieselbe Auflösung genau — „1080p". Gehört in den Tooltip.</summary>
|
|
string? ResolutionExact = null,
|
|
|
|
/// <summary>Die Sprache als Wort — „Deutsch" statt „deu".</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit eine Liste von Sprachen ohne Rätselraten entsteht.</b> Die
|
|
/// Detailansicht nennt, in welchen Sprachen ein Film vorliegt; sie dafür
|
|
/// aus <see cref="Display"/> herauszuschneiden hiesse, sich auf dessen
|
|
/// Satzbau zu verlassen.
|
|
/// </remarks>
|
|
string? LanguageName = null);
|
|
|
|
/// <summary>Eine Datei hinter einem Titel — eine Fassung also.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Nur der Dateiname, nicht der Pfad.</b> 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.
|
|
/// </remarks>
|
|
/// <param name="VersionLabel">
|
|
/// <b>Wie diese Fassung heisst</b> — „Mobil", „4K", „Director's Cut" — oder
|
|
/// <c>null</c>, wenn es nur eine gibt. Ohne diese Angabe standen zwei Dateien
|
|
/// derselben Folge als zwei gleich aussehende Knopfreihen untereinander, und
|
|
/// die Folge wirkte doppelt.
|
|
/// </param>
|
|
/// <param name="Folder">
|
|
/// <b>Der Ordner unterhalb der Mediathek</b> — <c>null</c> für alle, die nicht
|
|
/// verwalten dürfen.
|
|
///
|
|
/// <para>
|
|
/// <b>Weil man einer falschen Zuordnung am Ordner sofort ansieht, dass sie
|
|
/// falsch ist.</b> Steht über der Beschreibung „Hanni & Nanni 3" und der
|
|
/// Ordner heisst <c>Hanni und Nanni</c>, ist die Sache in einer Sekunde klar
|
|
/// — ohne Umweg über den Zuordnungsdialog.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Nur der Teil unterhalb der Mediathek, und nur für Administratoren.</b>
|
|
/// Der volle Pfad verriete jedem Angemeldeten den Aufbau des Servers; der
|
|
/// relative sagt genau das, was zum Beurteilen nötig ist.
|
|
/// </para>
|
|
/// </param>
|
|
public sealed record MediaFileResponse(
|
|
Guid Id, string FileName, long SizeBytes, string? Container, double? DurationSeconds,
|
|
IReadOnlyList<MediaStreamResponse> Streams, string? VersionLabel = null,
|
|
string? Folder = null);
|
|
|
|
public sealed record MovieResponse(
|
|
Guid Id, string Title, int? Year, string? Overview,
|
|
string? PosterImage, string? BackdropImage,
|
|
IReadOnlyList<MediaFileResponse> Files,
|
|
ItemStateResponse? State,
|
|
/// <summary>Der Schriftzug des Titels — fehlt oft, und das ist in Ordnung.</summary>
|
|
string? LogoImage = null,
|
|
int? RuntimeMinutes = null,
|
|
double? Rating = null,
|
|
string? Tagline = null,
|
|
string? ContentRating = null,
|
|
/// <summary>Leer in Listen, gefüllt beim Einzelabruf.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Eine Seite mit fünfzig Titeln braucht keine Besetzungslisten.</b>
|
|
/// Sie mitzuschicken hiesse, tausend Namen über die Leitung zu schicken,
|
|
/// von denen keiner angezeigt wird.
|
|
/// </remarks>
|
|
IReadOnlyList<string>? Genres = null,
|
|
IReadOnlyList<CreditResponse>? Credits = null,
|
|
|
|
/// <summary>Der Titel in der Sprache des Originals, wenn er abweicht.</summary>
|
|
string? OriginalTitle = null,
|
|
|
|
/// <summary>Ab wann sichtbar — <c>null</c> heisst: wartet auf Freigabe.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit die Mediathek es zeigen kann.</b> 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.
|
|
/// </remarks>
|
|
DateTimeOffset? PublishAt = null,
|
|
|
|
/// <summary>Ob der Titel von Hand gesetzt wurde.</summary>
|
|
bool TitlePinned = false,
|
|
|
|
/// <summary>Wonach einsortiert wird, wenn nicht nach dem Titel.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit der Client weiss, wohin ein Titel gehört.</b> 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.
|
|
/// </remarks>
|
|
string? SortTitle = null,
|
|
|
|
/// <summary>Ob der Sortiertitel von Hand gesetzt wurde.</summary>
|
|
bool SortTitlePinned = false,
|
|
|
|
/// <summary>Bewertungen von anderswo — leer, wenn keine Quelle eingerichtet ist.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Neben <c>Rating</c>, nicht statt dessen.</b> Das Feld ist die
|
|
/// Wertung von TMDB und bleibt, wo es ist; ein Client, der nur das
|
|
/// kennt, zeigt weiter dasselbe wie bisher.
|
|
/// </remarks>
|
|
IReadOnlyList<Fremdwertungsantwort>? ExternalRatings = null,
|
|
|
|
/// <summary>Zu welcher Mediathek der Titel gehört.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit die Detailseite einen einzelnen Titel auffrischen kann.</b>
|
|
/// Das Auffrischen hängt an der Mediathek, nicht am Titel; ohne diese
|
|
/// Kennung müsste der Client erst alle Mediatheken durchgehen, um zu
|
|
/// erfahren, wohin dieser eine Film gehört.
|
|
/// </remarks>
|
|
Guid? LibraryId = null,
|
|
/// <summary>Das Bild, das jetzt im Aufmacher gilt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Vom Server gewählt, nicht vom Client.</b> Wer mehrere Aufnahmen
|
|
/// ausgesucht hat, bekommt alle fünf Stunden eine andere — und zwar
|
|
/// überall dieselbe: im Web, in der App, nach einem Neustart. Siehe
|
|
/// <c>Aufmacherwahl</c>. <c>null</c> heisst „keine ausgesucht"; dann
|
|
/// gilt wie bisher der Hintergrund.
|
|
/// </remarks>
|
|
string? HeroImage = null,
|
|
|
|
/// <summary>Der Abspann, sofern er im Bild gefunden wurde.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Bei einem Film gibt es keinen Vorspann zu finden — erkannt wird er
|
|
/// daran, dass er sich über die Folgen einer Staffel wiederholt, und
|
|
/// ein Film hat nichts, womit er sich vergleichen liesse.
|
|
/// </remarks>
|
|
IReadOnlyList<MarkerResponse>? Markers = 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<int> Seasons,
|
|
/// <summary>Wie viele Folgen dieser Benutzer noch nicht gesehen hat.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
int UnwatchedCount,
|
|
string? LogoImage = null,
|
|
int? RuntimeMinutes = null,
|
|
double? Rating = null,
|
|
string? Tagline = null,
|
|
string? ContentRating = null,
|
|
IReadOnlyList<string>? Genres = null,
|
|
IReadOnlyList<CreditResponse>? Credits = null,
|
|
|
|
/// <summary>Der Titel in der Sprache des Originals, wenn er abweicht.</summary>
|
|
string? OriginalTitle = null,
|
|
|
|
/// <summary>Ab wann sichtbar — <c>null</c> heisst: wartet auf Freigabe.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit die Mediathek es zeigen kann.</b> 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.
|
|
/// </remarks>
|
|
DateTimeOffset? PublishAt = null,
|
|
|
|
/// <summary>Favorit, Bewertung, Häkchen — zur Serie als Ganzem.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Neben <see cref="UnwatchedCount"/>, nicht an dessen Stelle.</b> 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.
|
|
/// </remarks>
|
|
ItemStateResponse? State = null,
|
|
|
|
/// <summary>Ob der Titel von Hand gesetzt wurde.</summary>
|
|
bool TitlePinned = false,
|
|
|
|
/// <summary>Wonach einsortiert wird, wenn nicht nach dem Titel.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit der Client weiss, wohin ein Titel gehört.</b> 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.
|
|
/// </remarks>
|
|
string? SortTitle = null,
|
|
|
|
/// <summary>Ob der Sortiertitel von Hand gesetzt wurde.</summary>
|
|
bool SortTitlePinned = false,
|
|
|
|
/// <summary>Bewertungen von anderswo — leer, wenn keine Quelle eingerichtet ist.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Neben <c>Rating</c>, nicht statt dessen.</b> Das Feld ist die
|
|
/// Wertung von TMDB und bleibt, wo es ist; ein Client, der nur das
|
|
/// kennt, zeigt weiter dasselbe wie bisher.
|
|
/// </remarks>
|
|
IReadOnlyList<Fremdwertungsantwort>? ExternalRatings = null,
|
|
|
|
/// <summary>Zu welcher Mediathek der Titel gehört.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit die Detailseite einen einzelnen Titel auffrischen kann.</b>
|
|
/// Das Auffrischen hängt an der Mediathek, nicht am Titel; ohne diese
|
|
/// Kennung müsste der Client erst alle Mediatheken durchgehen, um zu
|
|
/// erfahren, wohin dieser eine Film gehört.
|
|
/// </remarks>
|
|
Guid? LibraryId = null,
|
|
/// <summary>Das Bild, das jetzt im Aufmacher gilt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Vom Server gewählt, nicht vom Client.</b> Wer mehrere Aufnahmen
|
|
/// ausgesucht hat, bekommt alle fünf Stunden eine andere — und zwar
|
|
/// überall dieselbe: im Web, in der App, nach einem Neustart. Siehe
|
|
/// <c>Aufmacherwahl</c>. <c>null</c> heisst „keine ausgesucht"; dann
|
|
/// gilt wie bisher der Hintergrund.
|
|
/// </remarks>
|
|
string? HeroImage = null);
|
|
|
|
/// <summary>Ein überspringbarer Abschnitt — Vorspann oder Abspann.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <c>Kind</c> ist <c>"intro"</c> oder <c>"credits"</c>: dieselben Wörter,
|
|
/// die Jellyfin benutzt. Die Apps kennen sie bereits, und zwei Namen für
|
|
/// dieselbe Sache wären zwei Übersetzungen an der Grenze jeder App.
|
|
/// </remarks>
|
|
public sealed record MarkerResponse(string Kind, double StartSeconds, double EndSeconds);
|
|
|
|
public sealed record EpisodeResponse(
|
|
Guid Id, int Season, int Episode, string? Title, string? Overview, string? StillImage,
|
|
IReadOnlyList<MediaFileResponse> Files,
|
|
ItemStateResponse? State,
|
|
DateTimeOffset? PublishAt = null,
|
|
|
|
/// <summary>Zu welcher Serie sie gehört.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit eine Folge für sich geöffnet werden kann.</b> 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.
|
|
/// </remarks>
|
|
Guid? SeriesId = null,
|
|
|
|
/// <summary>Vorspann und Abspann, sofern erkannt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Leer heisst „nicht erkannt", nicht „gibt es nicht". Wer sie sucht,
|
|
/// stösst den Lauf an; bis er durch ist, bleibt die Liste leer und die
|
|
/// Spieler zeigen schlicht keinen Knopf.
|
|
/// </remarks>
|
|
IReadOnlyList<MarkerResponse>? Markers = null);
|
|
|
|
public sealed record Page<T>(IReadOnlyList<T> Items, int Total);
|
|
|
|
/// <summary>Die saisonale Reihe — mit ihrem Namen, weil er wechselt.</summary>
|
|
///
|
|
/// <param name="Occasion">
|
|
/// Was über der Reihe steht. <c>null</c> heisst: heute hat nichts Saison,
|
|
/// oder der Anlass findet nichts — dann zeigt der Client gar keine Reihe.
|
|
/// </param>
|
|
public sealed record Saisonreihe(string? Occasion, Page<MovieResponse> Items);
|
|
|
|
/// <param name="HasFanartKey">
|
|
/// Ob eine zweite Bildquelle eingerichtet ist. Auch hier kommt der Schlüssel
|
|
/// selbst nicht zurück — zum Anzeigen genügt, dass einer da ist.
|
|
/// </param>
|
|
public sealed record MetadataSettingsResponse(
|
|
bool HasApiKey, string Language, bool UsingProjectKey,
|
|
bool HasFanartKey = false, bool HasOmdbKey = false,
|
|
|
|
/// <summary>Ob die Seite von Rotten Tomatoes gelesen werden darf.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Kein Schlüssel, sondern ein Schalter.</b> Dort gibt es keine
|
|
/// Zugänge zu vergeben; eingeschaltet wird das Auslesen der Seite.
|
|
/// </remarks>
|
|
bool RottenTomatoes = false);
|
|
|
|
/// <summary>Was ein Client über der Mediathek anzeigen muss.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Bedingung, nicht Höflichkeit.</b> 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.
|
|
/// </remarks>
|
|
public sealed record AttributionResponse(string Text, string LogoUrl, string Url);
|
|
|
|
public sealed record MetadataSettingsRequest(
|
|
string? ApiKey, string? Language, string? FanartApiKey = null, string? OmdbApiKey = null,
|
|
bool? RottenTomatoes = null);
|
|
|
|
/// <summary>Was der Server über die Wiedergabe entscheidet.</summary>
|
|
///
|
|
/// <param name="ResumeAfterSeconds">
|
|
/// Wie weit man gekommen sein muss, damit ein Titel in „Weiterschauen" steht.
|
|
/// </param>
|
|
/// <param name="WatchedFraction">Ab welchem Anteil er als gesehen gilt, 0 bis 1.</param>
|
|
/// <param name="Encoder">„qualitaet", „ausgewogen" oder „tempo".</param>
|
|
public sealed record PlaybackSettingsResponse(
|
|
double ResumeAfterSeconds, double WatchedFraction, string Encoder);
|
|
|
|
/// <summary>Was geändert werden soll — <c>null</c> heisst: unverändert.</summary>
|
|
public sealed record PlaybackSettingsRequest(
|
|
double? ResumeAfterSeconds, double? WatchedFraction, string? Encoder);
|
|
|
|
/// <summary>Was danach gilt, und wie viele Zeilen das umgeworfen hat.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Die Zahl gehört in die Antwort.</b> Eine neue Schwelle wirkt rückwirkend
|
|
/// auf alles, was schon in „Weiterschauen" steht; ohne diese Rückmeldung
|
|
/// änderte sich die Startseite scheinbar von selbst.
|
|
/// </remarks>
|
|
public sealed record PlaybackSettingsApplied(
|
|
double ResumeAfterSeconds, double WatchedFraction, string Encoder, int Reevaluated);
|
|
|
|
// --- Stufe 3: Ausliefern -------------------------------------------------
|
|
|
|
/// <summary>Was mit einer Datei geschehen wird, bevor sie hinausgeht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit vorher feststeht, was man bekommt.</b> 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.
|
|
/// </remarks>
|
|
public sealed record DeliveryInfo(
|
|
string Method,
|
|
IReadOnlyList<string> Reasons,
|
|
IReadOnlyList<int> AudioToEncode,
|
|
IReadOnlyList<int> SubtitlesDropped,
|
|
string PackageState,
|
|
double Progress,
|
|
string? Error);
|
|
|
|
// --- Zuordnung korrigieren -----------------------------------------------
|
|
|
|
/// <summary>Ein Vorschlag, aus dem gewählt werden kann.</summary>
|
|
public sealed record MatchCandidate(
|
|
int TmdbId, string Title, int? Year, string? Overview, string? PosterUrl);
|
|
|
|
public sealed record ApplyMatchRequest(int TmdbId);
|
|
|
|
/// <summary>Wie die Zuordnung jetzt aussieht.</summary>
|
|
///
|
|
/// <param name="Id">
|
|
/// <b>Welcher Eintrag das jetzt ist.</b> Meist der, den man korrigiert hat —
|
|
/// nach einem Zusammenlegen aber der überlebende, und der hat eine andere
|
|
/// Kennung.
|
|
/// </param>
|
|
/// <param name="Merged">
|
|
/// <b>Ob dabei zwei Einträge zu einem wurden.</b>
|
|
///
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </param>
|
|
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 -------------------------------------
|
|
|
|
/// <summary>Eine Fortschrittsmeldung.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>In Sekunden, nicht in Ticks.</b> Jellyfin rechnet in Hundertnanosekunden,
|
|
/// und jeder Client muss das wissen. Sekunden als Gleitkommazahl sind in jeder
|
|
/// Sprache dasselbe und in der Entwicklerkonsole lesbar.
|
|
///
|
|
/// <c>RuntimeSeconds</c> 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.
|
|
/// </remarks>
|
|
public sealed record ProgressRequest(
|
|
double PositionSeconds, double? RuntimeSeconds,
|
|
/// <summary>Die letzte Meldung einer Wiedergabe.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
bool? Stopped = null);
|
|
|
|
public sealed record WatchedRequest(bool Watched);
|
|
public sealed record FavouriteRequest(bool Favourite);
|
|
|
|
/// <summary>Mehrere Titel auf einmal abhaken — oder das Häkchen wegnehmen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Eine Anfrage statt zweihundert.</b> „Alle als gesehen markieren" über
|
|
/// eine Mediathek hinweg wäre sonst ein Anfragensturm, bei dem mittendrin
|
|
/// etwas scheitert und niemand weiss, was durchging.
|
|
/// </remarks>
|
|
public sealed record BulkWatchedRequest(IReadOnlyList<Guid> Ids, bool Watched);
|
|
|
|
/// <param name="Changed">Wie viele Titel sich dadurch geändert haben.</param>
|
|
/// <param name="Unknown">
|
|
/// Kennungen, zu denen es nichts gibt — stillschweigend zu übergehen hiesse,
|
|
/// einen Teilerfolg als vollen auszugeben.
|
|
/// </param>
|
|
public sealed record BulkWatchedResponse(int Changed, IReadOnlyList<Guid> Unknown);
|
|
|
|
/// <summary>Welche Titel neu geholt werden sollen — leer heisst: alle.</summary>
|
|
public sealed record RefreshRequest(IReadOnlyList<Guid>? Ids);
|
|
|
|
/// <summary>Was ein Spieler kann und was er sehen will.</summary>
|
|
///
|
|
/// <param name="Containers">Schachteln, die das Gerät öffnet — „mp4", „webm".</param>
|
|
/// <param name="Height">
|
|
/// Die gewünschte Stufe: 1080, 720 oder 480. <c>null</c> heisst Originalauflösung.
|
|
/// </param>
|
|
/// <param name="AudioStream">
|
|
/// Welche Tonspur — die Nummer aus der Datei. <c>null</c> nimmt die erste.
|
|
/// </param>
|
|
/// <param name="StartSeconds">Ab welcher Sekunde des Films.</param>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Das Gerät sagt, was es kann, statt dass der Server es rät.</b> Vorher
|
|
/// bekam jeder Browser Apples Profil — und mit ihm HEVC, AC3 und EAC3, von
|
|
/// denen er keines öffnen kann.
|
|
/// </remarks>
|
|
public sealed record StreamRequest(
|
|
IReadOnlyList<string>? Containers,
|
|
IReadOnlyList<string>? VideoCodecs,
|
|
IReadOnlyList<string>? AudioCodecs,
|
|
int? Height = null,
|
|
int? AudioStream = null,
|
|
double? StartSeconds = null,
|
|
|
|
/// <summary>Umkodieren, egal was das Profil sagt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Für den zweiten Versuch.</b> Der Spieler schickt sein Profil aus
|
|
/// <c>canPlayType</c> — 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.
|
|
/// </remarks>
|
|
bool Transcode = false,
|
|
|
|
/// <summary>Um wie viele Millisekunden der Ton verschoben wird.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Für Dateien, bei denen es ab Werk nicht stimmt.</b> Positiv heisst
|
|
/// später, negativ früher. Der Server schiebt dabei nicht am Bild, sondern
|
|
/// liest die Datei ein zweites Mal mit <c>-itsoffset</c> und nimmt von dort
|
|
/// nur den Ton — mit einer Eingabe verschöbe sich beides gemeinsam und
|
|
/// nichts wäre gewonnen.
|
|
/// </remarks>
|
|
int AudioOffsetMs = 0);
|
|
|
|
/// <summary>Eine wählbare Spur, so wie sie im Menü steht.</summary>
|
|
public sealed record SpurAuskunft(int Index, string Label, string? Language, bool IsDefault);
|
|
|
|
/// <param name="StartSeconds">
|
|
/// Ab welcher Sekunde des Films dieser Strom beginnt. <b>Der Spieler zählt ab
|
|
/// null</b> — ohne diesen Wert ginge jeder gemeldete Stand um den Vorlauf daneben.
|
|
/// </param>
|
|
public sealed record StreamResponse(
|
|
Guid SessionId,
|
|
string Url,
|
|
|
|
/// <summary>Dieselbe Wiedergabe als Hauptliste, mit Untertitelspuren.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Für Abspieler, die ihre Untertitel aus HLS beziehen.</b> Der
|
|
/// Browser hängt sie als eigene Spur ans Videoelement und braucht nur
|
|
/// <see cref="Url"/>; AVPlayer kennt diesen Weg nicht — dort gibt es
|
|
/// keine Möglichkeit, einer laufenden Wiedergabe eine Textspur von
|
|
/// aussen beizulegen. Bei einer Direktwiedergabe steht hier nichts: dort
|
|
/// liegen die Spuren in der Datei selbst.
|
|
/// </remarks>
|
|
string? MasterUrl,
|
|
string Method,
|
|
double StartSeconds,
|
|
int? Height,
|
|
int? AudioStream,
|
|
IReadOnlyList<SpurAuskunft> AudioStreams,
|
|
IReadOnlyList<SpurAuskunft> SubtitleStreams);
|
|
/// <summary>Eine Bewertung von anderswo, fertig zum Hinschreiben.</summary>
|
|
///
|
|
/// <param name="Source">„imdb", „rottenTomatoes", „metacritic".</param>
|
|
/// <param name="Display">Was dastehen soll — in der Form, die man von der Quelle kennt.</param>
|
|
public sealed record Fremdwertungsantwort(string Source, double Value, string Display);
|
|
|
|
public sealed record RatingRequest(int? Rating);
|
|
|
|
/// <summary>Was ein Benutzer mit einem Titel zu tun hatte.</summary>
|
|
public sealed record ItemStateResponse(
|
|
double PositionSeconds, double? RuntimeSeconds,
|
|
bool Watched, int PlayCount, bool Favourite, int? Rating, bool Dismissed,
|
|
DateTimeOffset? LastPlayedAt,
|
|
|
|
/// <summary>Ob der Titel in „Weiterschauen" steht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit die Beschriftung dasselbe sagt wie die Reihe.</b> 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.
|
|
/// </remarks>
|
|
bool InProgress = false);
|
|
|
|
/// <summary>Eine Zeile in „Weiterschauen".</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Film und Folge in einer Form, weil die Reihe beide mischt. Bei einer Folge
|
|
/// steht in <c>Title</c> die Serie und in <c>SubTitle</c> die Folge — so liest
|
|
/// sich die Kachel wie bei Plex: oben, worum es geht, darunter, wo man ist.
|
|
/// </remarks>
|
|
/// <param name="Id">
|
|
/// Die Kennung des Titels selbst — bei einer Folge also die der Folge. Dorthin
|
|
/// geht der Fortschritt.
|
|
/// </param>
|
|
/// <param name="SeriesId">
|
|
/// <b>Zu welcher Serie die Folge gehört</b> — <c>null</c> bei einem Film.
|
|
///
|
|
/// <para>
|
|
/// <b>Ohne diese Angabe führte die Kachel ins Leere.</b> 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.
|
|
/// </para>
|
|
/// </param>
|
|
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,
|
|
|
|
/// <summary>Die Datei, die abgespielt würde.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit „Abspielen" abspielt.</b> 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.
|
|
/// </remarks>
|
|
Guid? FileId = null,
|
|
double? DurationSeconds = null,
|
|
|
|
/// <summary>Ab wann abspielbar. In der Zukunft heisst: noch nicht.</summary>
|
|
DateTimeOffset? PublishAt = null,
|
|
|
|
/// <summary>Das breite Bild der Serie, zu der die Folge gehört.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Neben dem Folgenbild, nicht statt seiner.</b> 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.
|
|
/// </remarks>
|
|
string? SeriesBackdropImage = null,
|
|
|
|
/// <summary>Das Bild, das jetzt im Aufmacher gilt — siehe <c>Aufmacherwahl</c>.</summary>
|
|
string? HeroImage = null);
|
|
|
|
/// <summary>Was jemand geschaut hat — nur für ihn selbst.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Zwei Arten Zahl, und der Unterschied gehört dazu.</b>
|
|
/// <c>LoggedSeconds</c> kommt aus dem Sehprotokoll und fängt an dem Tag an,
|
|
/// an dem es eingebaut wurde. <c>WatchedTitles</c> und
|
|
/// <c>WatchedRuntimeSeconds</c> kommen aus den Sehständen und sind für die
|
|
/// ganze Vergangenheit vollständig. Beides als eine Zahl auszugeben hiesse,
|
|
/// eine davon zu behaupten.
|
|
/// </remarks>
|
|
public sealed record StatistikResponse(
|
|
double LoggedSeconds,
|
|
int WatchedTitles,
|
|
double WatchedRuntimeSeconds,
|
|
DateTimeOffset? Since,
|
|
IReadOnlyList<Wochenwert> Weeks,
|
|
IReadOnlyList<Zuletztgesehen> Recent,
|
|
IReadOnlyList<Genrewert> Genres);
|
|
|
|
/// <param name="Start">Der Montag dieser Woche.</param>
|
|
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);
|
|
|
|
/// <summary>Eine laufende Wiedergabe — für das Dashboard.</summary>
|
|
public sealed record SessionResponse(
|
|
string UserName, string DeviceName,
|
|
Guid ItemId, string Title, string? SubTitle, string? PosterImage,
|
|
double PositionSeconds, double? RuntimeSeconds);
|
|
|
|
/// <summary>Was der Server gerade tut.</summary>
|
|
public sealed record ServerStatusResponse(
|
|
int Playing, int Packaging, int Scanning, double Uptime);
|
|
|
|
/* ---------- Meldungen ---------- */
|
|
|
|
/// <summary>Was jemand an einem Titel gemeldet hat.</summary>
|
|
public sealed record MeldungRequest(
|
|
/// <summary><c>bild</c>, <c>ton</c>, <c>untertitel</c>, <c>daten</c> oder <c>sonstiges</c>.</summary>
|
|
string Kind,
|
|
string Comment,
|
|
/// <summary>Eine Stelle wie <c>0:34:12</c> — oder nichts.</summary>
|
|
string? Position = null,
|
|
/// <summary>Nur bei einer Serie: eine einzelne Staffel statt der ganzen.</summary>
|
|
int? SeasonNumber = null);
|
|
|
|
/// <summary>Eine Meldung, wie sie gelesen wird.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b><c>ItemId</c> ist die Kennung der Detailseite, nicht die des
|
|
/// Gemeldeten.</b> 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 <c>Target</c> und <c>SubTitle</c>.
|
|
///
|
|
/// <b><c>Title</c> ist leer, wenn der Titel nicht mehr sichtbar ist.</b> 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.
|
|
/// </remarks>
|
|
public sealed record Meldungseintrag(
|
|
Guid Id,
|
|
/// <summary><c>movie</c>, <c>series</c>, <c>season</c> oder <c>episode</c>.</summary>
|
|
string Target,
|
|
Guid ItemId,
|
|
string Title,
|
|
/// <summary>„Staffel 2" oder „S2 · F5 — Der Pakt" — sonst <c>null</c>.</summary>
|
|
string? SubTitle,
|
|
string? PosterImage,
|
|
string Kind,
|
|
double? PositionSeconds,
|
|
string Comment,
|
|
DateTimeOffset ReportedAt,
|
|
string ReportedBy,
|
|
/// <summary><c>null</c> heisst: offen.</summary>
|
|
DateTimeOffset? ResolvedAt);
|
|
|
|
/* ---------- Einladungen ---------- */
|
|
|
|
/// <summary>Was beim Erstellen eines Einladungslinks gewählt wird.</summary>
|
|
public sealed record EinladungRequest(
|
|
/// <summary>Wie viele Tage er gilt.</summary>
|
|
int Days = 7,
|
|
/// <summary>Wie oft er sich einlösen lässt. <c>null</c> heisst: unbegrenzt.</summary>
|
|
int? MaxUses = 1);
|
|
|
|
/// <summary>Der frisch erstellte Link — der Token steht hier zum einzigen Mal.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Die Adresse baut der Client.</b> 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.
|
|
/// </remarks>
|
|
public sealed record EinladungErstellt(Guid Id, string Token, DateTimeOffset ExpiresAt, int? MaxUses);
|
|
|
|
/// <summary>Eine Einladung in der Verwaltungsliste — ohne Token.</summary>
|
|
public sealed record Einladungseintrag(
|
|
Guid Id,
|
|
string CreatedBy,
|
|
DateTimeOffset CreatedAt,
|
|
DateTimeOffset ExpiresAt,
|
|
int? MaxUses,
|
|
int UsedCount,
|
|
DateTimeOffset? RevokedAt,
|
|
/// <summary>Ob sich damit jetzt noch ein Konto anlegen lässt.</summary>
|
|
bool Valid);
|
|
|
|
/// <summary>Was die Einladungsseite zeigt, bevor jemand etwas eintippt.</summary>
|
|
public sealed record Einladungsschau(string ServerName, string InvitedBy, DateTimeOffset ExpiresAt);
|
|
|
|
/// <summary>Namen und Passwort, die sich der Eingeladene selbst gibt.</summary>
|
|
public sealed record EinloesenRequest(string UserName, string Password);
|
|
|
|
/// <summary>Eine Sprache, wie sie in einem Menü steht.</summary>
|
|
///
|
|
/// <param name="Code">Das Kürzel, unter dem die Einstellung abgelegt wird.</param>
|
|
/// <param name="Name">Wie sie dasteht — „Deutsch", „English".</param>
|
|
public sealed record Sprachangabe(string Code, string Name);
|
|
|
|
/// <summary>Welche Sprachen eine Serie durchgehend führt.</summary>
|
|
///
|
|
/// <param name="Audio">Tonsprachen, die in jeder Folge vorkommen.</param>
|
|
/// <param name="Subtitles">Textsprachen, die in jeder Folge vorkommen.</param>
|
|
/// <param name="Partial">
|
|
/// Wie viele Sprachen es nur in einem Teil der Folgen gibt.
|
|
///
|
|
/// <remarks>
|
|
/// <b>Damit die Oberfläche es sagen kann, statt es zu verschweigen.</b> Ohne
|
|
/// diese Zahl sähe eine gekürzte Liste aus wie die ganze Wahrheit — und wer
|
|
/// eine Sprache vermisst, die er in Staffel 1 gesehen hat, hielte es für
|
|
/// einen Fehler.
|
|
/// </remarks>
|
|
/// </param>
|
|
public sealed record Sprachauswahl(
|
|
IReadOnlyList<Sprachangabe> Audio,
|
|
IReadOnlyList<Sprachangabe> Subtitles,
|
|
int Partial);
|