Zwei Titel je Film, und die Sprache lässt sich wählen

Die Mediathek sah zweisprachig aus — "Despicable Me" neben "Ich einfach
unverbesserlich". Es gab nur ein Titelfeld, und welche Sprache darin
landete, entschied die Einstellung zum Zeitpunkt des Scans.

Movie und Series tragen jetzt zusätzlich OriginalTitle. Gesucht wird über
beide: wer "Despicable Me" tippt, findet "Ich einfach unverbesserlich".
Auf der Detailseite steht der Originaltitel unter dem übersetzten, wenn er
abweicht.

Die Sprache der Metadaten ist in der Verwaltung eine Auswahl statt eines
Textfelds. Vorher musste man "de-DE" wissen, und ein Tippfehler darin nahm
TMDB stillschweigend als Englisch an.

Dazu eine eigene Sprache je Benutzer. Das Ehrliche daran steht im Dialog:
die Daten liegen in einer Sprache vor, und wer eine andere wählt, bekommt,
was übersetzt vorliegt — für den Rest die Serversprache, nicht ein leeres
Feld. ItemTranslation hält die zweite Sprache, gefüllt über eine
Sammelaktion. Nicht nebenbei beim Öffnen: das verteilte die Wartezeit über
Wochen und träfe immer den, der gerade etwas sucht.

Alle Sprachen für alle Titel vorzuhalten wäre der Bestand mal Sprachen an
Abrufen und Speicher, für einen Haushalt mit zweien. Deshalb auf Ansage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 12:23:47 +02:00
co-authored by Claude Opus 5
parent f359807561
commit 921533050b
20 changed files with 1533 additions and 26 deletions
@@ -0,0 +1,35 @@
namespace Flaro.Core.Library;
/// <summary>Ein Titel in einer anderen Sprache als der des Servers.</summary>
///
/// <remarks>
/// <para>
/// <b>Warum nicht einfach alles in allen Sprachen holen.</b> Das hiesse, den
/// ganzen Bestand je Sprache abzurufen und zu speichern — für einen Haushalt
/// mit zwei Sprachen ein Vielfaches an Abrufen und Speicher für etwas, das
/// fast niemand ansieht.
/// </para>
/// <para>
/// <b>Also auf Abruf.</b> Öffnet jemand mit abweichender Sprache einen Titel,
/// wird die Übersetzung einmal geholt und bleibt liegen. Was fehlt, erscheint
/// in der Sprache des Servers — nicht leer.
/// </para>
/// </remarks>
public sealed class ItemTranslation
{
public Guid Id { get; init; } = Guid.CreateVersion7();
public Guid? MovieId { get; init; }
public Guid? SeriesId { get; init; }
public Guid? EpisodeId { get; init; }
/// <summary>Das Sprachkürzel, so wie es beim Abruf verwendet wurde.</summary>
public required string Language { get; init; }
public string? Title { get; set; }
public string? Overview { get; set; }
public string? Tagline { get; set; }
/// <summary>Wann sie geholt wurde — für ein späteres Auffrischen.</summary>
public DateTimeOffset FetchedAt { get; set; }
}
@@ -110,6 +110,17 @@ public sealed class Movie
/// <summary>Ohne führenden Artikel — siehe <see cref="SortTitle"/>.</summary>
public required string SortTitleValue { get; set; }
/// <summary>Der Titel in der Sprache des Originals.</summary>
///
/// <remarks>
/// <b>Damit die Mediathek nicht zweisprachig aussieht.</b> Vorher stand
/// nur ein Titel da, und welcher es wurde, entschied die Sprache zum
/// Zeitpunkt des Scans — „Despicable Me" neben „Ich einfach
/// unverbesserlich". Mit beiden lässt sich zeigen, was passt, und über
/// beide suchen.
/// </remarks>
public string? OriginalTitle { get; set; }
public int? Year { get; set; }
public string? Overview { get; set; }
@@ -203,6 +214,17 @@ public sealed class Series
public required string Title { get; set; }
public required string SortTitleValue { get; set; }
/// <summary>Der Titel in der Sprache des Originals.</summary>
///
/// <remarks>
/// <b>Damit die Mediathek nicht zweisprachig aussieht.</b> Vorher stand
/// nur ein Titel da, und welcher es wurde, entschied die Sprache zum
/// Zeitpunkt des Scans — „Despicable Me" neben „Ich einfach
/// unverbesserlich". Mit beiden lässt sich zeigen, was passt, und über
/// beide suchen.
/// </remarks>
public string? OriginalTitle { get; set; }
public int? Year { get; set; }
public string? Overview { get; set; }
public int? TmdbId { get; set; }
@@ -53,7 +53,10 @@ public sealed record MovieMetadata(
///
/// Nicht jeder Titel hat eines. Fehlt es, steht der Titel als Text da.
/// </remarks>
RemoteImage? Logo = null);
RemoteImage? Logo = null,
/// <summary>Der Titel in der Sprache des Originals.</summary>
string? OriginalTitle = null);
public sealed record SeriesMetadata(
int ProviderId, string Title, int? Year, string? Overview,
@@ -74,7 +77,10 @@ public sealed record SeriesMetadata(
///
/// Nicht jeder Titel hat eines. Fehlt es, steht der Titel als Text da.
/// </remarks>
RemoteImage? Logo = null);
RemoteImage? Logo = null,
/// <summary>Der Titel in der Sprache des Originals.</summary>
string? OriginalTitle = null);
public sealed record EpisodeMetadata(string? Title, string? Overview, RemoteImage? Still);
@@ -119,8 +125,28 @@ public interface IMetadataProvider
/// </remarks>
Task<MovieMetadata?> GetMovieAsync(int providerId, CancellationToken ct = default);
/// <summary>Holt einen Titel in einer bestimmten Sprache.</summary>
///
/// <remarks>
/// <b>Für die Übersetzungstabelle, nicht für den Scan.</b> Der Scan nimmt
/// die Sprache des Servers; dies hier füllt nach, was ein Benutzer mit
/// abweichender Sprache sehen will.
/// </remarks>
/// <remarks>
/// <b>Mit Vorgabe.</b> Ein Anbieter, der keine Sprachen kennt, liefert
/// schlicht die des Servers — das ist ehrlicher als eine leere Antwort und
/// erspart jedem Test-Doppelgänger eine Pflichtmethode, die er nicht
/// braucht.
/// </remarks>
Task<MovieMetadata?> GetMovieAsync(int providerId, string sprache, CancellationToken ct = default) =>
GetMovieAsync(providerId, ct);
Task<SeriesMetadata?> GetSeriesAsync(int providerId, CancellationToken ct = default);
/// <summary>Holt eine Serie in einer bestimmten Sprache.</summary>
Task<SeriesMetadata?> GetSeriesAsync(int providerId, string sprache, CancellationToken ct = default) =>
GetSeriesAsync(providerId, ct);
/// <summary>Mehrere Vorschläge zur Auswahl — für das Korrigieren von Hand.</summary>
Task<IReadOnlyList<MovieMetadata>> SearchMoviesAsync(string title, int? year, CancellationToken ct = default);
+11
View File
@@ -19,5 +19,16 @@ public sealed class User
public bool IsAdministrator { get; set; }
/// <summary>Die bevorzugte Sprache — <c>null</c> heisst: die des Servers.</summary>
///
/// <remarks>
/// <b>Und sie ist nicht kostenlos.</b> Metadaten werden in der Sprache des
/// Servers geholt und gespeichert. Wer eine andere wählt, bekommt, was in
/// der Übersetzungstabelle liegt — und für den Rest die Serversprache.
/// Darauf wird beim Umstellen deutlich hingewiesen; es stillschweigend
/// halb zu übersetzen wäre schlechter als gar nicht.
/// </remarks>
public string? Language { get; set; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
}