Files
Flaro/server/src/Flaro.Api/MatchApplier.cs
T
ScarriffleandClaude Opus 5 fcf40e4394 Der Orden des Phönix stand vor dem Stein der Weisen
Alphabetisch stimmt das, nur sieht man sie andersherum. Es gibt jetzt einen
Sortiertitel: „Harry Potter 1", „Harry Potter 2" — und die Reihe steht in
der Ordnung, in der man sie schaut. Leer gelassen bleibt alles wie bisher,
beim Titel ohne Artikel davor.

Er hat einen eigenen Merker, nicht den des Titels. Wer die Reihenfolge
festlegt, soll den Titel trotzdem weiter automatisch bekommen; beides an
einem Schalter hiesse, für das eine auf das andere zu verzichten. Eine
Korrektur der Zuordnung lässt ihn ebenfalls stehen.

Und WALL·E bleibt nicht mehr unter E. Die Kachel wurde nach einer Korrektur
zwar neu beschriftet, blieb aber an ihrem Platz — jetzt rutscht sie dorthin,
wo sie hingehört. Liegt der Platz im noch nicht geladenen Teil der Liste,
geht sie weg und sagt wohin, statt falsch stehenzubleiben.

Dazu ein Dialog, in dem alles zu einem Titel beieinander steht: Titel,
Sortiertitel, Bilder, Zuordnung. Die vier Menüpunkte gab es schon, jeder für
sich richtig — zusammen aber kein Ort. Wer einen Film geraderückt, macht
selten nur eines davon. Die Abkürzungen bleiben.

Beim Bauen gefunden: das Zurücksetzen des Sortiertitels endete in einer 500.
SortTitle.For ist eine C#-Methode, und EF Core übersetzt sie nicht nach SQL.
Es geht um eine Zeile — die wird jetzt geladen und zurückgeschrieben, statt
die Regel ein zweites Mal als SQL zu schreiben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 22:25:31 +02:00

130 lines
5.9 KiB
C#

using Flaro.Core.Library;
using Flaro.Core.Metadata;
using Flaro.Data;
using Microsoft.EntityFrameworkCore;
namespace Flaro.Api;
/// <summary>Schreibt eine korrigierte Zuordnung überallhin, wo sie hingehört.</summary>
///
/// <remarks>
/// <b>An drei Stellen, und alle drei sind nötig.</b> In die Datenbank, damit
/// es sofort stimmt. In die NFO-Datei, damit es einen Neuaufbau und einen
/// Umzug übersteht. Und als Sperre, damit kein automatischer Lauf die
/// Entscheidung wieder überstimmt.
/// </remarks>
public sealed class MatchApplier(
FlaroContext db,
IMetadataProvider anbieter,
IImageStore bilder,
IImageReader bildLeser,
ISidecarStore beiwerk,
ILogger<MatchApplier> log)
{
public async Task ApplyAsync(Movie film, MovieMetadata gefunden, CancellationToken ct = default)
{
film.TmdbId = gefunden.ProviderId;
film.ImdbId = gefunden.ImdbId;
film.Title = gefunden.Title;
// Ein von Hand gesetzter Sortiertitel überlebt auch eine Korrektur der
// Zuordnung — sonst wäre die Reihenfolge nach jedem Geradeziehen hin.
if (!film.SortTitlePinned) film.SortTitleValue = SortTitle.For(gefunden.Title);
film.Year = gefunden.Year ?? film.Year;
film.Overview = gefunden.Overview;
film.PosterImage = await Ablegen(gefunden.Poster, ct);
film.BackdropImage = await Ablegen(gefunden.Backdrop, ct);
film.MatchLocked = true;
await db.SaveChangesAsync(ct);
var datei = await db.MediaFiles.AsNoTracking().FirstOrDefaultAsync(f => f.MovieId == film.Id, ct);
if (datei is null || !beiwerk.Enabled) return;
var allein = beiwerk.IsAloneInFolder(datei.Path);
await BildNeben(datei.Path, Name(allein, datei.Path, SidecarNames.MoviePoster), film.PosterImage, ct);
await BildNeben(datei.Path, Name(allein, datei.Path, SidecarNames.MovieBackdrop), film.BackdropImage, ct);
await beiwerk.WriteTextAsync(datei.Path, SidecarNames.MovieNfo, Nfo.Write(new MovieNfo(
film.Title, film.Year, film.Overview, new ProviderIds(film.TmdbId, film.ImdbId), true)), ct);
log.LogInformation("Zuordnung von Hand gesetzt: „{Titel}“ → TMDB {Kennung}", film.Title, film.TmdbId);
}
public async Task ApplyAsync(Series serie, SeriesMetadata gefunden, CancellationToken ct = default)
{
serie.TmdbId = gefunden.ProviderId;
serie.ImdbId = gefunden.ImdbId;
serie.Title = gefunden.Title;
if (!serie.SortTitlePinned) serie.SortTitleValue = SortTitle.For(gefunden.Title);
serie.Year = gefunden.Year ?? serie.Year;
serie.Overview = gefunden.Overview;
serie.PosterImage = await Ablegen(gefunden.Poster, ct);
serie.BackdropImage = await Ablegen(gefunden.Backdrop, ct);
serie.MatchLocked = true;
// **Die Folgen gehörten zur alten Serie.** Ihre Beschreibungen und
// Standbilder sind jetzt falsch; sie bleiben stehen, bis sie durch die
// der richtigen Serie ersetzt sind.
var folgen = await db.Episodes.Where(f => f.SeriesId == serie.Id)
.OrderBy(f => f.SeasonNumber).ThenBy(f => f.EpisodeNumber).ToListAsync(ct);
foreach (var folge in folgen)
{
ct.ThrowIfCancellationRequested();
var neu = await anbieter.FindEpisodeAsync(gefunden.ProviderId, folge.SeasonNumber, folge.EpisodeNumber, ct);
folge.Title = neu?.Title;
folge.Overview = neu?.Overview;
folge.StillImage = await Ablegen(neu?.Still, ct);
}
await db.SaveChangesAsync(ct);
if (!beiwerk.Enabled) return;
await SerienBeiwerk(serie, folgen, ct);
log.LogInformation("Zuordnung von Hand gesetzt: „{Titel}“ → TMDB {Kennung}, {Anzahl} Folgen neu geholt",
serie.Title, serie.TmdbId, folgen.Count);
}
private async Task SerienBeiwerk(Series serie, List<Episode> folgen, CancellationToken ct)
{
var folgenKennungen = folgen.Select(f => f.Id).ToList();
var dateien = await db.MediaFiles.AsNoTracking()
.Where(d => d.EpisodeId != null && folgenKennungen.Contains(d.EpisodeId!.Value))
.ToListAsync(ct);
if (dateien.Count == 0) return;
var staffel = Path.GetDirectoryName(dateien[0].Path);
var serienOrdner = staffel is null ? null : Path.GetDirectoryName(staffel);
if (serienOrdner is not null)
{
var anker = Path.Combine(serienOrdner, "egal.mkv");
await BildNeben(anker, SidecarNames.SeriesPoster, serie.PosterImage, ct);
await BildNeben(anker, SidecarNames.SeriesBackdrop, serie.BackdropImage, ct);
await beiwerk.WriteTextAsync(anker, SidecarNames.SeriesNfo, Nfo.Write(new SeriesNfo(
serie.Title, serie.Year, serie.Overview, new ProviderIds(serie.TmdbId, serie.ImdbId), true)), ct);
}
foreach (var datei in dateien)
{
var folge = folgen.First(f => f.Id == datei.EpisodeId);
await BildNeben(datei.Path, SidecarNames.EpisodeThumb(datei.Path), folge.StillImage, ct);
await beiwerk.WriteTextAsync(datei.Path, SidecarNames.EpisodeNfo(datei.Path), Nfo.Write(
new EpisodeNfo(folge.Title, folge.SeasonNumber, folge.EpisodeNumber, folge.Overview)), ct);
}
}
private async Task<string?> Ablegen(RemoteImage? bild, CancellationToken ct) =>
bild is null ? null : await bilder.StoreAsync(bild, ct);
private async Task BildNeben(string anker, string name, string? schlüssel, CancellationToken ct)
{
if (schlüssel is null) return;
await using var strom = await bildLeser.OpenAsync(schlüssel, ct);
if (strom is null) return;
await beiwerk.WriteStreamAsync(anker, name, strom, ct);
}
private static string Name(bool allein, string mediendatei, string beiwerkName) =>
allein ? beiwerkName : SidecarNames.Prefixed(mediendatei, beiwerkName);
}