Eine neue Quelle wirkt erst beim nächsten Holen der Metadaten. Das ging bisher nur je Mediathek: achthundert Titel, eine Viertelstunde — nur um an einem Film zu sehen, ob ein Schlüssel etwas bringt. Wer auf einer Detailseite bemerkt, dass IMDb und Rotten Tomatoes fehlen, hatte von dort keinen Weg dorthin. Der Server kann eine Auswahl längst; es fehlte bloss die Kennung der Mediathek in der Antwort, damit die Seite weiss, wohin ihr Titel gehört. Der runde Knopf holt diesen einen Titel, wartet den Lauf ab und zeichnet die Seite neu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
965 lines
41 KiB
C#
965 lines
41 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);
|
|
|
|
public sealed record Bildauswahl(
|
|
Bildstand Current, IReadOnlyList<Flaro.Core.Metadata.Bildkandidat> Candidates);
|
|
|
|
public sealed record BildWahlRequest(string Kind, string? Url);
|
|
|
|
/// <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>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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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>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);
|
|
|
|
/// <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,
|
|
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>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);
|