diff --git a/scratchpad/neu/data/flaro.db b/scratchpad/neu/data/flaro.db index 719a7a9..7ad9942 100644 Binary files a/scratchpad/neu/data/flaro.db and b/scratchpad/neu/data/flaro.db differ diff --git a/scratchpad/neu/data/logs/flaro-2026-10-07.log b/scratchpad/neu/data/logs/flaro-2026-10-07.log index 816b658..98157d7 100644 --- a/scratchpad/neu/data/logs/flaro-2026-10-07.log +++ b/scratchpad/neu/data/logs/flaro-2026-10-07.log @@ -1,2 +1,5 @@ 21:35:00 INFO Server gestartet — 1.0.0 · DEV 21:55:29 INFO Server fährt herunter. +22:00:35 INFO Server gestartet — 1.0.0 · DEV +22:11:06 INFO Server fährt herunter. +22:14:53 INFO Server gestartet — 1.0.0 · DEV diff --git a/scratchpad/neu/data/logs/flaro-2026-10-08.log b/scratchpad/neu/data/logs/flaro-2026-10-08.log new file mode 100644 index 0000000..e9bfa51 --- /dev/null +++ b/scratchpad/neu/data/logs/flaro-2026-10-08.log @@ -0,0 +1,17 @@ +06:38:47 INFO Server fährt herunter. +06:44:02 INFO Server gestartet — 1.0.0 · DEV +06:57:23 INFO Server fährt herunter. +06:59:45 INFO Server gestartet — 1.0.0 · DEV +17:38:34 INFO Server fährt herunter. +17:57:11 INFO Server gestartet — 1.0.0 · DEV +19:05:40 INFO Server fährt herunter. +19:08:22 INFO Server gestartet — 1.0.0 · DEV +21:03:32 INFO Server fährt herunter. +21:08:04 INFO Server gestartet — 1.0.0 · DEV +21:10:04 INFO Wartung — Sicherung: flaro-2026-10-08-1910.db, 0 MB +21:10:04 INFO Wartung — Optimieren: nichts zusammenzupacken +21:10:04 INFO Wartung — Bilder: kein Bildspeicher da +21:45:40 INFO Server fährt herunter. +21:45:44 INFO Server gestartet — 1.0.0 · DEV +22:06:03 INFO Server fährt herunter. +22:12:27 INFO Server gestartet — 1.0.0 · DEV diff --git a/scratchpad/neu/data/logs/flaro-2026-10-11.log b/scratchpad/neu/data/logs/flaro-2026-10-11.log new file mode 100644 index 0000000..4999dc0 --- /dev/null +++ b/scratchpad/neu/data/logs/flaro-2026-10-11.log @@ -0,0 +1 @@ +21:17:00 INFO Wartung — Sicherung: flaro-2026-10-11-1917.db, 0 MB diff --git a/scratchpad/neu/data/logs/flaro-2026-10-14.log b/scratchpad/neu/data/logs/flaro-2026-10-14.log new file mode 100644 index 0000000..9add8a7 --- /dev/null +++ b/scratchpad/neu/data/logs/flaro-2026-10-14.log @@ -0,0 +1 @@ +21:26:01 INFO Wartung — Sicherung: flaro-2026-10-14-1926.db, 0 MB diff --git a/scratchpad/neu/data/logs/flaro-2026-10-15.log b/scratchpad/neu/data/logs/flaro-2026-10-15.log new file mode 100644 index 0000000..a33e816 --- /dev/null +++ b/scratchpad/neu/data/logs/flaro-2026-10-15.log @@ -0,0 +1,2 @@ +21:10:21 INFO Wartung — Optimieren: nichts zusammenzupacken +21:10:21 INFO Wartung — Bilder: kein Bildspeicher da diff --git a/scratchpad/neu/data/sicherungen/flaro-2026-10-08-1910.db b/scratchpad/neu/data/sicherungen/flaro-2026-10-08-1910.db new file mode 100644 index 0000000..22cf373 Binary files /dev/null and b/scratchpad/neu/data/sicherungen/flaro-2026-10-08-1910.db differ diff --git a/scratchpad/neu/data/sicherungen/flaro-2026-10-11-1917.db b/scratchpad/neu/data/sicherungen/flaro-2026-10-11-1917.db new file mode 100644 index 0000000..6be24d0 Binary files /dev/null and b/scratchpad/neu/data/sicherungen/flaro-2026-10-11-1917.db differ diff --git a/scratchpad/neu/data/sicherungen/flaro-2026-10-14-1926.db b/scratchpad/neu/data/sicherungen/flaro-2026-10-14-1926.db new file mode 100644 index 0000000..2432423 Binary files /dev/null and b/scratchpad/neu/data/sicherungen/flaro-2026-10-14-1926.db differ diff --git a/server/src/Flaro.Api/Auth/TokenAuthentication.cs b/server/src/Flaro.Api/Auth/TokenAuthentication.cs index 3eb8ea6..d8bdc4e 100644 --- a/server/src/Flaro.Api/Auth/TokenAuthentication.cs +++ b/server/src/Flaro.Api/Auth/TokenAuthentication.cs @@ -13,7 +13,7 @@ namespace Flaro.Api.Auth; /// /// /// Authorization: Bearer <token>. Der Server hasht das Vorgezeigte -/// und sucht den Fingerabdruck — das Token selbst steht nirgends in der +/// und sucht den Fingerfingerprint — das Token selbst steht nirgends in der /// Datenbank und ist aus ihr auch nicht zu gewinnen. /// /// @@ -69,8 +69,8 @@ public sealed class TokenAuthenticationHandler( var token = AusKopfzeile() ?? Request.Cookies[CookieName]; if (string.IsNullOrEmpty(token)) return AuthenticateResult.NoResult(); - var fingerabdruck = Secrets.Fingerprint(token); - var eintrag = await db.DeviceTokens.FirstOrDefaultAsync(t => t.TokenFingerprint == fingerabdruck); + var fingerfingerprint = Secrets.Fingerprint(token); + var eintrag = await db.DeviceTokens.FirstOrDefaultAsync(t => t.TokenFingerprint == fingerfingerprint); var jetzt = clock.GetUtcNow(); if (eintrag is null || !eintrag.IsValid(jetzt)) return AuthenticateResult.Fail("Token gilt nicht."); diff --git a/server/src/Flaro.Api/Endpoints/AuthEndpoints.cs b/server/src/Flaro.Api/Endpoints/AuthEndpoints.cs index 5c10712..a00e679 100644 --- a/server/src/Flaro.Api/Endpoints/AuthEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/AuthEndpoints.cs @@ -128,8 +128,8 @@ public static class AuthEndpoints string handle, FlaroContext db, TimeProvider clock) => { var jetzt = clock.GetUtcNow(); - var fingerabdruck = Secrets.Fingerprint(handle); - var code = await db.AuthCodes.FirstOrDefaultAsync(c => c.HandleFingerprint == fingerabdruck); + var fingerfingerprint = Secrets.Fingerprint(handle); + var code = await db.AuthCodes.FirstOrDefaultAsync(c => c.HandleFingerprint == fingerfingerprint); if (code is null || code.IsExpired(jetzt) || code.ConsumedAt is not null) return Results.Ok(new CodeState("expired", null)); diff --git a/server/src/Flaro.Api/Endpoints/Contracts.cs b/server/src/Flaro.Api/Endpoints/Contracts.cs index 1d6b379..e36d76f 100644 --- a/server/src/Flaro.Api/Endpoints/Contracts.cs +++ b/server/src/Flaro.Api/Endpoints/Contracts.cs @@ -26,7 +26,7 @@ public sealed record SystemInfo( /// lägen die Dateien woanders als die Datenbank sie vermutet. /// /// Wie viel davon auf den Bildspeicher entfällt; sonst null. -/// Wie viel auf dem Kratzplatz liegen bleiben darf. +/// Wie viel auf dem Transcode-Cache liegen bleiben darf. /// /// Obergrenze in Gigabyte; 0 heisst unbegrenzt. /// Ab wann Unbenutztes auch unterhalb der Grenze geht; 0 heisst nie. @@ -42,7 +42,7 @@ public sealed record MaintenanceSettingsResponse( int OptimizeDays, int ImageDays); /// -/// Die Unterordner, wo es welche gibt — heute nur beim Kratzplatz. +/// Die Unterordner, wo es welche gibt — heute nur beim Transcode-Cache. /// Mit Dateizahl, denn bei den Untertiteln ist die Anzahl das /// Problem und nicht die Grösse, und das sieht man an Bytes nicht. /// Additiv: Bilder bleibt stehen, damit kein Client bricht. @@ -323,15 +323,15 @@ public sealed record ScanStatus( int? Missing = null, /// Woran der Lauf gerade ist, in einem Satz. string? Step = null, - /// Woran die Markensuche gerade ist — null, wenn keine läuft. + /// Woran die Segmentsuche gerade ist — null, wenn keine läuft. /// /// - /// Ein eigenes Feld, nicht . Die Markensuche + /// Ein eigenes Feld, nicht . Die Segmentsuche /// 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. /// - string? Marken = null); + string? Segmente = null); /// Was das Nachschlagen bei der Metadatenquelle ergeben hat. public sealed record MetadataProgress( diff --git a/server/src/Flaro.Api/Endpoints/EinladungEndpoints.cs b/server/src/Flaro.Api/Endpoints/EinladungEndpoints.cs index c77cc2c..95ffb70 100644 --- a/server/src/Flaro.Api/Endpoints/EinladungEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/EinladungEndpoints.cs @@ -43,7 +43,7 @@ public static class EinladungEndpoints return Einwand.Bitte("Ein Link, der nullmal gilt, ist keiner."); // **Das Geheimnis verlässt den Server genau hier.** Gespeichert - // wird nur sein Fingerabdruck; aus der Datenbank lässt sich kein + // wird nur sein Fingerfingerprint; aus der Datenbank lässt sich kein // Link mehr bauen, und eine verlorene Einladung wird deshalb nicht // wiedergefunden, sondern neu gemacht. var token = Secrets.Create(); diff --git a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs index 19aed8e..a7f7b2a 100644 --- a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs @@ -121,7 +121,7 @@ public static class LibraryContentEndpoints .ToListAsync(); var spuren = await SpurenZu(db, dateien.Select(d => d.Id).ToList()); var stände = await ZuständeZu(db, wer, folgen: [nachbar.Id]); - var marken = await MarkenZu(db, [nachbar.Id]); + var segmente = await SegmenteZu(db, [nachbar.Id]); // **Ohne eigenes Bild das der Serie.** Eine graue Fläche sagt // nichts; das Plakat der Serie sagt wenigstens, worum es geht. @@ -138,24 +138,24 @@ public static class LibraryContentEndpoints stände.GetValueOrDefault(nachbar.Id), nachbar.PublishAt, SeriesId: diese.SeriesId, - Markers: marken[nachbar.Id].ToList())); + Markers: segmente[nachbar.Id].ToList())); } - /// Die Marken zu einer Reihe von Folgen. + /// Die Segmente zu einer Reihe von Folgen. /// /// /// Als Nachschlagewerk und nicht je Folge einzeln: eine Staffel mit /// zwanzig Folgen wären sonst zwanzig Abfragen für zwanzig kurze Zeilen. /// - private static async Task> MarkenZu( + private static async Task> SegmenteZu( FlaroContext db, IReadOnlyCollection folgen) { if (folgen.Count == 0) return Array.Empty<(Guid, MarkerResponse)>().ToLookup(x => x.Item1, x => x.Item2); - var roh = await db.Marken.AsNoTracking() + var roh = await db.Segmente.AsNoTracking() .Where(m => m.EpisodeId != null && folgen.Contains(m.EpisodeId!.Value)) - // In Zeitfolge: ein geteilter Abspann kommt als zwei Marken, und + // In Zeitfolge: ein geteilter Abspann kommt als zwei Segmente, und // die Spieler nehmen die erste passende. .OrderBy(m => m.VonSekunden) .Select(m => new { Id = m.EpisodeId!.Value, m.Art, m.VonSekunden, m.BisSekunden }) @@ -164,24 +164,24 @@ public static class LibraryContentEndpoints return roh.ToLookup( m => m.Id, m => new MarkerResponse( - m.Art == Markenart.Intro ? "intro" : "credits", m.VonSekunden, m.BisSekunden)); + m.Art == Segmentart.Intro ? "intro" : "credits", m.VonSekunden, m.BisSekunden)); } /// Dasselbe für Filme. /// /// - /// Getrennt und nicht über eine gemeinsame Kennung: eine Marke gehört + /// Getrennt und nicht über eine gemeinsame Kennung: ein Segment gehört /// entweder zu einer Folge oder zu einem Film, und eine Abfrage, die /// beides zugleich sucht, fände zu einer Filmkennung auch eine Folge /// mit derselben — unmöglich heute, aber nichts hält es fest. /// - private static async Task> FilmmarkenZu( + private static async Task> FilmsegmenteZu( FlaroContext db, IReadOnlyCollection filme) { if (filme.Count == 0) return Array.Empty<(Guid, MarkerResponse)>().ToLookup(x => x.Item1, x => x.Item2); - var roh = await db.Marken.AsNoTracking() + var roh = await db.Segmente.AsNoTracking() .Where(m => m.MovieId != null && filme.Contains(m.MovieId!.Value)) .OrderBy(m => m.VonSekunden) .Select(m => new { Id = m.MovieId!.Value, m.Art, m.VonSekunden, m.BisSekunden }) @@ -190,7 +190,7 @@ public static class LibraryContentEndpoints return roh.ToLookup( m => m.Id, m => new MarkerResponse( - m.Art == Markenart.Intro ? "intro" : "credits", m.VonSekunden, m.BisSekunden)); + m.Art == Segmentart.Intro ? "intro" : "credits", m.VonSekunden, m.BisSekunden)); } private static async Task> SpurenZu(FlaroContext db, List dateien) @@ -424,20 +424,20 @@ public static class LibraryContentEndpoints }).RequireAuthorization(FlaroPolicies.Administrator); app.MapGet("/api/v1/libraries/{id:guid}/scan", - (Guid id, ScanCoordinator koordinator, Markenerkennung erkennung) => + (Guid id, ScanCoordinator koordinator, Segmenterkennung erkennung) => { - // **Auch ohne Scan kann etwas laufen.** Die Markensuche lässt + // **Auch ohne Scan kann etwas laufen.** Die Segmentsuche lässt // sich von Hand anstossen; ohne diese Zeile stünde die // Verwaltung dann auf „idle", während der Server stundenlang // arbeitet. - var marken = erkennung.Stand(id)?.Text(); + var segmente = erkennung.Stand(id)?.Text(); var zustand = koordinator.State(id); if (zustand is null) return Results.Ok(new ScanStatus( - marken is null ? "idle" : "marken", + segmente is null ? "idle" : "marken", null, null, null, null, null, null, null, null, null, null, null, - Marken: marken)); + Segmente: segmente)); var bericht = zustand.Report; var metadaten = zustand.Enrichment; @@ -452,7 +452,7 @@ public static class LibraryContentEndpoints metadaten.Skipped, metadaten.Error), bericht?.Missing, zustand.Schritt?.Text(), - marken)); + segmente)); }).WithName("Scanstand").RequireAuthorization(); } @@ -592,7 +592,7 @@ public static class LibraryContentEndpoints .ToListAsync(); var spuren = await SpurenZu(db, dateien.Select(f => f.Id).ToList()); var stände = await ZuständeZu(db, wer, filme: ids); - var marken = await FilmmarkenZu(db, ids); + var segmente = await FilmsegmenteZu(db, ids); var items = kennungen.Select(m => new MovieResponse( m.Id, m.Title, m.Year, m.Overview, m.PosterImage, m.BackdropImage, @@ -606,7 +606,7 @@ public static class LibraryContentEndpoints SortTitle: m.SortTitleValue, SortTitlePinned: m.SortTitlePinned, HeroImage: Aufmacherwahl.Jetziges(m.Aufmacherbilder, m.Id, DateTimeOffset.UtcNow), - Markers: marken[m.Id].ToList())) + Markers: segmente[m.Id].ToList())) .ToList(); return Results.Ok(new Page(items, gesamt)); @@ -802,7 +802,7 @@ public static class LibraryContentEndpoints ExternalRatings: await FremdwertungenZu(db, film: id, serie: null), LibraryId: film.LibraryId, HeroImage: Aufmacherwahl.Jetziges(film.Aufmacherbilder, id, DateTimeOffset.UtcNow), - Markers: (await FilmmarkenZu(db, [id]))[id].ToList())); + Markers: (await FilmsegmenteZu(db, [id]))[id].ToList())); }).RequireAuthorization(); app.MapGet("/api/v1/series/{id:guid}", async ( @@ -870,7 +870,7 @@ public static class LibraryContentEndpoints .ToListAsync(); var spuren = await SpurenZu(db, dateien.Select(d => d.Id).ToList()); var stände = await ZuständeZu(db, wer, folgen: [id]); - var marken = await MarkenZu(db, [id]); + var segmente = await SegmenteZu(db, [id]); var wurzel = await MediathekspfadFuer(db, wer, await db.Series.AsNoTracking().Where(x => x.Id == folge.SeriesId) @@ -883,7 +883,7 @@ public static class LibraryContentEndpoints stände.GetValueOrDefault(id), folge.PublishAt, SeriesId: folge.SeriesId, - Markers: marken[id].ToList())); + Markers: segmente[id].ToList())); }).RequireAuthorization(); // **Die Nachbarfolge — vorwärts wie rückwärts.** Reihenfolge ist @@ -900,8 +900,8 @@ public static class LibraryContentEndpoints // Hintergrundlauf statt einer Antwort, auf die jemand wartet. // Dasselbe für einen Film — dort sucht der Server im Bild. app.MapPost("/api/v1/movies/{id:guid}/marken", async ( - Guid id, FlaroContext db, Markenerkennung erkennung, - Markenumfang umfang = Markenumfang.Alles) => + Guid id, FlaroContext db, Segmenterkennung erkennung, + Segmentumfang umfang = Segmentumfang.Alles) => { if (!await db.Movies.AsNoTracking().AnyAsync(x => x.Id == id)) return Results.NotFound(); @@ -917,7 +917,7 @@ public static class LibraryContentEndpoints // Frage, ob bei Outtakes im Abspann wenigstens die Ränder dunkel // bleiben. app.MapGet("/api/v1/movies/{id:guid}/markenkurve", async ( - Guid id, FlaroContext db, Bildabtaster abtaster, CancellationToken ct) => + Guid id, FlaroContext db, Frameabtaster abtaster, CancellationToken ct) => { var film = await db.Movies.AsNoTracking() .Where(m => m.Id == id) @@ -928,7 +928,7 @@ public static class LibraryContentEndpoints if (film?.Duration is not { TotalSeconds: > 0 } laufzeit) return Einwand.Bitte("Zu diesem Film gibt es keine Datei mit Laufzeit."); - var ab = Math.Max(0, laufzeit.TotalSeconds - Markenerkennung.Suchbereich); + var ab = Math.Max(0, laufzeit.TotalSeconds - Segmenterkennung.Suchbereich); var kurve = await abtaster.KurveAsync(film.Path, ab, laufzeit.TotalSeconds - ab, ct); var text = new StringBuilder("sekunde;gesamt;links;rechts\n"); @@ -945,16 +945,16 @@ public static class LibraryContentEndpoints "text/csv", $"markenkurve-{name}.csv"); }).RequireAuthorization(FlaroPolicies.Administrator); - // **Eine Marke wieder wegnehmen.** + // **Ein Segment wieder wegnehmen.** // // Ohne diesen Weg wäre die Erkennung eine Blackbox, die manchmal // recht hat: ein Fehlgriff bliebe stehen, und niemand könnte etwas // dagegen tun ausser die Datenbank anzufassen. app.MapDelete("/api/v1/movies/{id:guid}/marken", async (Guid id, FlaroContext db) => { - var weg = await db.Marken.Where(m => m.MovieId == id).ExecuteDeleteAsync(); + var weg = await db.Segmente.Where(m => m.MovieId == id).ExecuteDeleteAsync(); // Der Vermerk bleibt: „gesucht" stimmt weiterhin, und ohne ihn - // legte der nächste Scan dieselbe falsche Marke wieder an. + // legte der nächste Scan dieselbe falsches Segment wieder an. return weg > 0 ? Results.NoContent() : Results.NotFound(); }).RequireAuthorization(FlaroPolicies.Administrator); @@ -963,14 +963,14 @@ public static class LibraryContentEndpoints // unzufrieden; ihn dann nach Lücken suchen zu lassen hiesse, ihm // die Antwort zu verweigern, die er haben will. app.MapPost("/api/v1/series/{id:guid}/marken", async ( - Guid id, FlaroContext db, Markenerkennung erkennung, - Markenumfang umfang = Markenumfang.Alles) => + Guid id, FlaroContext db, Segmenterkennung erkennung, + Segmentumfang umfang = Segmentumfang.Alles) => { if (!await db.Series.AsNoTracking().AnyAsync(x => x.Id == id)) return Results.NotFound(); return erkennung.Starten(id, umfang) ? Results.Accepted() - : Einwand.Belegt("Für diese Serie werden gerade Marken gesucht."); + : Einwand.Belegt("Für diese Serie werden gerade Segmente gesucht."); }).RequireAuthorization(FlaroPolicies.Administrator); // **Eine ganze Mediathek auf einmal.** Je Serie einen Knopf zu @@ -979,8 +979,8 @@ public static class LibraryContentEndpoints // einmal. Der Lauf kann Stunden dauern und läuft deshalb im // Hintergrund weiter; die Antwort kommt sofort. app.MapPost("/api/v1/libraries/{id:guid}/marken", async ( - Guid id, FlaroContext db, Markenerkennung erkennung, - Markenumfang umfang = Markenumfang.Neue) => + Guid id, FlaroContext db, Segmenterkennung erkennung, + Segmentumfang umfang = Segmentumfang.Neue) => { if (!await db.Libraries.AsNoTracking().AnyAsync(x => x.Id == id)) return Results.NotFound(); @@ -1077,7 +1077,7 @@ public static class LibraryContentEndpoints .ToListAsync(); var spuren = await SpurenZu(db, dateien.Select(d => d.Id).ToList()); var stände = await ZuständeZu(db, wer, folgen: ids); - var marken = await MarkenZu(db, ids); + var segmente = await SegmenteZu(db, ids); var serienBild = await db.Series.AsNoTracking() .Where(x => x.Id == id) @@ -1091,7 +1091,7 @@ public static class LibraryContentEndpoints stände.GetValueOrDefault(f.Id), f.PublishAt, SeriesId: id, - Markers: marken[f.Id].ToList())).ToList(); + Markers: segmente[f.Id].ToList())).ToList(); return Results.Ok(new Page(items, items.Count)); }).RequireAuthorization(); diff --git a/server/src/Flaro.Api/Endpoints/PlaybackEndpoints.cs b/server/src/Flaro.Api/Endpoints/PlaybackEndpoints.cs index 51cf585..260c6ac 100644 --- a/server/src/Flaro.Api/Endpoints/PlaybackEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/PlaybackEndpoints.cs @@ -106,14 +106,14 @@ public static class PlaybackEndpoints // `OrderBy` entschied die Datenbank, welche gilt. Gemeint ist // immer die erste — dort ist die Handlung vorbei. var abspannAb = zustand.EpisodeId is { } folge - ? await db.Marken.AsNoTracking() - .Where(m => m.EpisodeId == folge && m.Art == Markenart.Abspann) + ? await db.Segmente.AsNoTracking() + .Where(m => m.EpisodeId == folge && m.Art == Segmentart.Abspann) .OrderBy(m => m.VonSekunden) .Select(m => (double?)m.VonSekunden) .FirstOrDefaultAsync() : zustand.MovieId is { } film - ? await db.Marken.AsNoTracking() - .Where(m => m.MovieId == film && m.Art == Markenart.Abspann) + ? await db.Segmente.AsNoTracking() + .Where(m => m.MovieId == film && m.Art == Segmentart.Abspann) .OrderBy(m => m.VonSekunden) .Select(m => (double?)m.VonSekunden) .FirstOrDefaultAsync() diff --git a/server/src/Flaro.Api/Endpoints/SettingsEndpoints.cs b/server/src/Flaro.Api/Endpoints/SettingsEndpoints.cs index 9a19462..f15e91d 100644 --- a/server/src/Flaro.Api/Endpoints/SettingsEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/SettingsEndpoints.cs @@ -213,10 +213,10 @@ public static class SettingsEndpoints return Results.Ok(anlaesse); }).RequireAuthorization(FlaroPolicies.Administrator); - /* ---------- Wartung: was auf dem Kratzplatz liegen bleiben darf ---------- */ + /* ---------- Wartung: was auf dem Transcode-Cache liegen bleiben darf ---------- */ app.MapGet("/api/v1/settings/maintenance", async ( - Kratzplatzeinstellungen kratz, Wartungsplaneinstellungen plan) => + Transcodeeinstellungen kratz, Wartungsplaneinstellungen plan) => { var regeln = await kratz.RegelnAsync(); var w = await plan.RegelnAsync(); @@ -230,23 +230,23 @@ public static class SettingsEndpoints app.MapPut("/api/v1/settings/maintenance", async ( MaintenanceSettingsRequest anfrage, FlaroContext db, - Kratzplatzeinstellungen einstellungen, Wartungsplaneinstellungen plan) => + Transcodeeinstellungen einstellungen, Wartungsplaneinstellungen plan) => { var vorher = await einstellungen.RegelnAsync(); // Was nicht geschickt wird, bleibt — ein Client, der nur ein Feld // kennt, soll die anderen nicht zurücksetzen. - var nachher = Kratzplatzregeln.Geprueft( + var nachher = Transcoderegeln.Geprueft( anfrage.CapGb ?? vorher.ObergrenzeBytes / 1024d / 1024 / 1024, anfrage.UnusedDays ?? (int)vorher.Unbenutzt.TotalDays, anfrage.GraceMinutes ?? (int)vorher.Schonfrist.TotalMinutes); var zahl = System.Globalization.CultureInfo.InvariantCulture; - await Setzen(db, Kratzplatzeinstellungen.ObergrenzeSchluessel, + await Setzen(db, Transcodeeinstellungen.ObergrenzeSchluessel, (nachher.ObergrenzeBytes / 1024d / 1024 / 1024).ToString("0.###", zahl)); - await Setzen(db, Kratzplatzeinstellungen.UnbenutztSchluessel, + await Setzen(db, Transcodeeinstellungen.UnbenutztSchluessel, ((int)nachher.Unbenutzt.TotalDays).ToString(zahl)); - await Setzen(db, Kratzplatzeinstellungen.SchonfristSchluessel, + await Setzen(db, Transcodeeinstellungen.SchonfristSchluessel, ((int)nachher.Schonfrist.TotalMinutes).ToString(zahl)); var wVorher = await plan.RegelnAsync(); var wNachher = Wartungsregeln.Geprueft( diff --git a/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs b/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs index 7b3a847..cd81424 100644 --- a/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/StreamEndpoints.cs @@ -93,7 +93,7 @@ public static class StreamEndpoints // **Was der Browser ohnehin öffnet, geht unverändert hinaus.** // // Eine MP4 mit H.264 und AAC durch ffmpeg zu schicken wäre ein - // Prozess und ein Kratzplatzordner für nichts — und der + // Prozess und ein Transcodeordner für nichts — und der // Browser verlöre dabei das, was er von sich aus am besten // kann: springen über Bereichsanfragen, ohne dass irgendwo // etwas neu gerechnet wird. @@ -368,7 +368,7 @@ public static class StreamEndpoints // **Schliessen heisst aufräumen.** Ohne das liefe ffmpeg weiter, bis // die Geduld der Sitzung abgelaufen ist — und schriebe solange den - // ganzen Film auf den Kratzplatz. + // ganzen Film auf den Transcode-Cache. app.MapDelete("/api/v1/stream/{sitzung:guid}", (Guid sitzung, Streamkoordinator koordinator) => { koordinator.Beenden(sitzung); diff --git a/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs b/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs index c95d6b7..497a560 100644 --- a/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/SystemEndpoints.cs @@ -251,12 +251,12 @@ public static class SystemEndpoints // wohin die Bilder wandern, musste die Compose-Datei lesen. Und wenn // das `cache`-Volume vollläuft, ist das der erste Ort, an dem man // nachsieht — nicht der letzte. - app.MapGet("/api/v1/system/storage", (FlaroPaths pfade, Kratzplatzpflege pflege) => + app.MapGet("/api/v1/system/storage", (FlaroPaths pfade, Transcodepflege pflege) => { Speicherort Messen(string name, string variable, string pfad) => new(name, variable, pfad, Belegt(pfad), Frei(pfad), name == "Bilder" ? Belegt(Path.Combine(pfad, "images")) : null, - // **Beim Kratzplatz aufgeschlüsselt.** „39 GB belegt" sagt + // **Beim Transcode-Cache aufgeschlüsselt.** „39 GB belegt" sagt // nicht, ob das Segmente, umgepackte Filme oder Untertitel // sind — und davon hängt ab, ob es ein Problem ist. name == "Umpacken" ? pflege.Belegung() : null); @@ -269,7 +269,7 @@ public static class SystemEndpoints }); }).RequireAuthorization(FlaroPolicies.Administrator); - // **Den Kratzplatz aufräumen.** + // **Den Transcode-Cache aufräumen.** // // POST und nicht DELETE wie beim Bildspeicher darüber: dort wird ein // Ordner geleert, hier eine Regel angewandt. Etwas, das Verwaistes @@ -301,8 +301,8 @@ public static class SystemEndpoints }).RequireAuthorization(FlaroPolicies.Administrator); app.MapPost("/api/v1/system/storage/transcode/cleanup", async ( - Kratzplatzpflege pflege, CancellationToken ct) => - Results.Ok(await pflege.LaufenAsync(Kratzplatzpflege.Anlass.VonHand, ct))) + Transcodepflege pflege, CancellationToken ct) => + Results.Ok(await pflege.LaufenAsync(Transcodepflege.Anlass.VonHand, ct))) .RequireAuthorization(FlaroPolicies.Administrator); // **Den Bildspeicher leeren.** diff --git a/server/src/Flaro.Api/PackageCoordinator.cs b/server/src/Flaro.Api/PackageCoordinator.cs index 0c43330..381e82d 100644 --- a/server/src/Flaro.Api/PackageCoordinator.cs +++ b/server/src/Flaro.Api/PackageCoordinator.cs @@ -177,8 +177,8 @@ public sealed class PackageCoordinator( // Umpacken ist der eine, in dem der Platz wirklich gebraucht // wird — und `PlatzPruefen` sagt danach die Wahrheit statt // „kein Platz", obwohl zwanzig Gigabyte Altlast herumliegen. - await bereich.ServiceProvider.GetRequiredService() - .LaufenAsync(Kratzplatzpflege.Anlass.VorDemUmpacken); + await bereich.ServiceProvider.GetRequiredService() + .LaufenAsync(Transcodepflege.Anlass.VorDemUmpacken); PlatzPruefen(datei.SizeBytes); @@ -237,7 +237,7 @@ public sealed class PackageCoordinator( if (platte.AvailableFreeSpace >= gebraucht) return; throw new InvalidOperationException( - $"Auf dem Kratzplatz ({pfade.Transcode}) sind noch " + + $"Auf dem Transcode-Cache ({pfade.Transcode}) sind noch " + $"{platte.AvailableFreeSpace / (1024 * 1024)} MB frei, gebraucht werden etwa " + $"{gebraucht / (1024 * 1024)} MB. Räume dort auf oder hänge ein grösseres Volume ein."); } diff --git a/server/src/Flaro.Api/Program.cs b/server/src/Flaro.Api/Program.cs index 379f9d6..975e9b6 100644 --- a/server/src/Flaro.Api/Program.cs +++ b/server/src/Flaro.Api/Program.cs @@ -75,7 +75,7 @@ builder.Services.AddRateLimiter(o => // und lässt einen Absturz dort den Server nicht mitnehmen. builder.Services.AddSingleton(FfmpegOptions.FromEnvironment()); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Beiwerk im Medienordner — standardmässig aus. In fremde Medienordner zu @@ -172,11 +172,11 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); // Vorspann und Abspann: der Leser ist zustandslos, die Erkennung hält den // Vermerk, welche Serie gerade abgetastet wird. -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); // Bei Filmen führt kein Ton zum Abspann — nur das Bild. -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); // **Neben dem Umpacker, nicht an seiner Stelle.** Der baut eine fertige Datei @@ -184,9 +184,9 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -// Der Kratzplatz räumt sich auf — kein Dienst, kein Takt, vier Anlässe. -builder.Services.AddScoped(); -builder.Services.AddSingleton(); +// Der Transcode-Cache räumt sich auf — kein Dienst, kein Takt, vier Anlässe. +builder.Services.AddScoped(); +builder.Services.AddSingleton(); // **Der einzige Hintergrunddienst, und mit Bedacht.** Eine Sicherung hängt // an keinem Aufruf: sie soll alle drei Tage geschehen, auch wenn niemand @@ -449,8 +449,8 @@ if (Directory.Exists(oberfläche)) // Stromordner noch da und jedes Teilstück eines abgebrochenen // Umpackens — und beim Start ist sicher, dass niemand davon noch // gebraucht wird. - _ = app.Services.GetRequiredService() - .LaufenAsync(Kratzplatzpflege.Anlass.Start, leben.ApplicationStopping); + _ = app.Services.GetRequiredService() + .LaufenAsync(Transcodepflege.Anlass.Start, leben.ApplicationStopping); _ = app.Services.GetRequiredService() .NachUpdateAsync(leben.ApplicationStopping); diff --git a/server/src/Flaro.Api/ScanCoordinator.cs b/server/src/Flaro.Api/ScanCoordinator.cs index ee5c946..4dc3e0d 100644 --- a/server/src/Flaro.Api/ScanCoordinator.cs +++ b/server/src/Flaro.Api/ScanCoordinator.cs @@ -172,15 +172,15 @@ public sealed class ScanCoordinator( // Verwaiste entstehen: Dateien verschwinden, Änderungszeiten // wandern, und die dazugehörigen Pakete und Untertitel gehören // zu einem Stand, den es nicht mehr gibt. - _ = bereich.ServiceProvider.GetRequiredService() - .LaufenAsync(Kratzplatzpflege.Anlass.NachDemScan); + _ = bereich.ServiceProvider.GetRequiredService() + .LaufenAsync(Transcodepflege.Anlass.NachDemScan); - // **Und zuletzt die Marken.** Ein Scan ist der Augenblick, in + // **Und zuletzt die Segmente.** Ein Scan ist der Augenblick, in // dem neue Folgen dazukommen — und eine Folge ohne Vorspann- - // Marke bekommt nie eine, wenn niemand danach fragt. Der Lauf + // Segment bekommt nie eine, wenn niemand danach fragt. Der Lauf // hängt sich hinten an und nimmt sich Serie für Serie vor; // gerechnet wird nur, wo noch nichts steht. - _ = bereich.ServiceProvider.GetRequiredService() + _ = bereich.ServiceProvider.GetRequiredService() .NachScanAsync(libraryId); _zustand[libraryId] = _zustand[libraryId] with diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Segmenterkennung.cs similarity index 81% rename from server/src/Flaro.Api/Markenerkennung.cs rename to server/src/Flaro.Api/Segmenterkennung.cs index efc6e63..98a85ca 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Segmenterkennung.cs @@ -7,14 +7,14 @@ using Microsoft.EntityFrameworkCore; namespace Flaro.Api; -/// Wie weit ein Markenlauf greift. +/// Wie weit ein Segmentlauf greift. /// /// /// -/// „Schon durchsucht" und „hat eine Marke" sind zwei verschiedene +/// „Schon durchsucht" und „hat ein Segment" sind zwei verschiedene /// Dinge, und genau dazwischen fehlte bisher die Unterscheidung. Ein /// Film, bei dem die Suche einmal nichts fand, trägt einen Vermerk und -/// keine Marke: der nächste Lauf übersprang ihn, obwohl er der +/// kein Segment: der nächste Lauf übersprang ihn, obwohl er der /// interessanteste Fall ist. /// /// @@ -23,18 +23,18 @@ namespace Flaro.Api; /// steht. /// /// -public enum Markenumfang +public enum Segmentumfang { /// Nur, wo noch nie gesucht wurde. Der Lauf nach einem Scan. Neue, /// /// Auch, wo gesucht und nichts gefunden wurde — aber nicht, wo schon - /// eine Marke steht. + /// ein Segment steht. /// Luecken, - /// Alles, auch was längst eine Marke hat. + /// Alles, auch was längst ein Segment hat. Alles, } @@ -43,7 +43,7 @@ public enum Markenumfang /// /// /// Beides mit derselben Rechnung, nur in einem anderen Fenster. -/// sucht das längste Stück, das zwei Tonabdrücke +/// sucht das längste Stück, das zwei Tonabdrücke /// teilen — was dabei herauskommt, entscheidet allein, welchen Ausschnitt /// der Folge man ihm gibt. Vorn ist es der Vorspann, hinten der Abspann. /// Eine zweite Erkennung dafür zu schreiben hiesse, dieselbe Suche zweimal @@ -60,10 +60,10 @@ public enum Markenumfang /// Zeitlimit zu laufen und am Ende nichts zu haben. /// /// -public sealed class Markenerkennung( +public sealed class Segmenterkennung( IServiceScopeFactory scopes, TimeProvider clock, - ILogger log) + ILogger log) { /// Wie viel vom Anfang nach einem Vorspann abgesucht wird. /// @@ -105,7 +105,7 @@ public sealed class Markenerkennung( /// gleich wieder weggeworfen — gefunden, verworfen, und am Ende stand /// kein Knopf da, obwohl die Erkennung gearbeitet hatte. /// - /// Weiter als das Suchfenster kann eine Marke ohnehin nicht reichen; + /// Weiter als das Suchfenster kann ein Segment ohnehin nicht reichen; /// mehr als dieses Fenster zu erlauben hiesse nichts zu erlauben. /// public const double Hoechstabspann = Abspannfenster; @@ -121,7 +121,7 @@ public sealed class Markenerkennung( /// /// Ein eigener Zustand, nicht der des Scans. Der /// ScanCoordinator setzt seinen Lauf auf „fertig", sobald er die - /// Markenerkennung angestossen hat — in dasselbe Feld zu schreiben + /// Segmenterkennung angestossen hat — in dasselbe Feld zu schreiben /// liesse einen beendeten Scan wieder laufend aussehen. /// /// @@ -136,7 +136,7 @@ public sealed class Markenerkennung( _stand.TryGetValue(mediathek, out var schritt) ? schritt : null; /// Startet den Lauf für eine Serie — oder sagt, dass schon einer läuft. - public bool Starten(Guid serieId, Markenumfang umfang = Markenumfang.Neue) + public bool Starten(Guid serieId, Segmentumfang umfang = Segmentumfang.Neue) { if (!_laufend.TryAdd(serieId, true)) return false; @@ -148,7 +148,7 @@ public sealed class Markenerkennung( } catch (Exception fehler) { - log.LogWarning(fehler, "Markensuche für {Serie} abgebrochen: {Grund}", + log.LogWarning(fehler, "Segmentsuche für {Serie} abgebrochen: {Grund}", serieId, Fehlertext.FuerMenschen(fehler)); } finally @@ -159,12 +159,12 @@ public sealed class Markenerkennung( return true; } - /// Nach einem Scan: jede Serie der Mediathek, der noch Marken fehlen. + /// Nach einem Scan: jede Serie der Mediathek, der noch Segmente fehlen. /// /// /// /// Hier, weil es sonst nie passiert. Eine neue Folge bekommt - /// keine Marke, wenn niemand danach fragt — und niemand fragt, weil man + /// kein Segment, wenn niemand danach fragt — und niemand fragt, weil man /// nicht weiss, dass es etwas zu fragen gibt. Der Scan ist der /// Augenblick, in dem neue Folgen entstehen. /// @@ -177,12 +177,12 @@ public sealed class Markenerkennung( /// /// public async Task NachScanAsync( - Guid mediathek, Markenumfang umfang = Markenumfang.Neue, + Guid mediathek, Segmentumfang umfang = Segmentumfang.Neue, CancellationToken ct = default) { using var raum = scopes.CreateScope(); var db = raum.ServiceProvider.GetRequiredService(); - var leser = raum.ServiceProvider.GetRequiredService(); + var leser = raum.ServiceProvider.GetRequiredService(); var mediathekname = await db.Libraries.AsNoTracking() .Where(x => x.Id == mediathek).Select(x => x.Name).FirstOrDefaultAsync(ct) @@ -228,16 +228,16 @@ public sealed class Markenerkennung( /// Alle Serien einer Mediathek, eine nach der anderen. private async Task SerienAsync( FlaroContext db, string mediathekname, IReadOnlyList<(Guid Id, string Titel)> serien, - Guid mediathek, Markenumfang umfang, CancellationToken ct) + Guid mediathek, Segmentumfang umfang, CancellationToken ct) { var folgen = await db.Episodes.AsNoTracking() .CountAsync(f => db.Series.Any(s => s.Id == f.SeriesId && s.LibraryId == mediathek), ct); log.LogInformation( - "Markensuche in „{Mediathek}“: {Serien} Serie(n), {Folgen} Folge(n). " + "Segmentsuche in „{Mediathek}“: {Serien} Serie(n), {Folgen} Folge(n). " + "Das kann Stunden dauern.", mediathekname, serien.Count, folgen); - var marken = 0; + var segmente = 0; var ohneFund = 0; var angefasst = 0; @@ -253,16 +253,16 @@ public sealed class Markenerkennung( void Melden(Scanschritt schritt) => _stand[mediathek] = schritt with { Erledigt = nummer, Gesamt = serien.Count }; - Melden(new Scanschritt(Scanphase.Marken, titel)); + Melden(new Scanschritt(Scanphase.Segmente, titel)); if (!_laufend.TryAdd(id, true)) continue; try { var stand = await LaufenAsync(id, umfang, ct, Melden); - marken += stand.Marken; + segmente += stand.Segmente; if (!stand.Angefasst) continue; angefasst++; - if (stand.Marken == 0) ohneFund++; + if (stand.Segmente == 0) ohneFund++; } catch (Exception fehler) { @@ -270,16 +270,16 @@ public sealed class Markenerkennung( // `Message` an, und bei EF Core steht dort „See the inner // exception for details." — eine Zeile, die ausdrücklich // sagt, dass sie die Auskunft nicht enthält. - log.LogWarning(fehler, "Markensuche für „{Serie}“ abgebrochen: {Grund}", + log.LogWarning(fehler, "Segmentsuche für „{Serie}“ abgebrochen: {Grund}", titel, Fehlertext.FuerMenschen(fehler)); } finally { _laufend.TryRemove(id, out _); } } log.LogInformation( - "Markensuche in „{Mediathek}“ durch: {Marken} Marke(n) in {Angefasst} von " + "Segmentsuche in „{Mediathek}“ durch: {Segmente} Segment(n) in {Angefasst} von " + "{Serien} Serie(n) gesucht, {Ohne} ohne Fund.", - mediathekname, marken, angefasst, serien.Count, ohneFund); + mediathekname, segmente, angefasst, serien.Count, ohneFund); } /// Startet den Lauf für eine ganze Mediathek — im Hintergrund. @@ -289,16 +289,16 @@ public sealed class Markenerkennung( /// dauert das Stunden; eine Anfrage so lange offen zu halten hiesse, /// sie in ein Zeitlimit laufen zu lassen. Wie es steht, sieht man im /// Tagesprotokoll — und an den Folgen selbst, bei denen danach eine - /// Marke steht. + /// Segment steht. /// - public void MediathekStarten(Guid mediathek, Markenumfang umfang) + public void MediathekStarten(Guid mediathek, Segmentumfang umfang) { _ = Task.Run(async () => { try { await NachScanAsync(mediathek, umfang, CancellationToken.None); } catch (Exception fehler) { - log.LogWarning(fehler, "Markenlauf für Mediathek {Kennung} abgebrochen: {Grund}", + log.LogWarning(fehler, "Segmentlauf für Mediathek {Kennung} abgebrochen: {Grund}", mediathek, Fehlertext.FuerMenschen(fehler)); } }); @@ -312,7 +312,7 @@ public sealed class Markenerkennung( /// läuft dann ins Leere, und das kostet eine Abfrage. /// private async Task FilmeAsync( - FlaroContext db, string mediathekname, Guid mediathek, Markenumfang umfang, + FlaroContext db, string mediathekname, Guid mediathek, Segmentumfang umfang, CancellationToken ct, Action melden) { // **In C# verzweigt, nicht im Ausdruck.** Drei Bedingungen in einem @@ -322,12 +322,12 @@ public sealed class Markenerkennung( var abfrage = db.Movies.AsNoTracking().Where(m => m.LibraryId == mediathek); abfrage = umfang switch { - Markenumfang.Alles => abfrage, + Segmentumfang.Alles => abfrage, // Die Lücke: gesucht wurde vielleicht, gefunden nichts. - Markenumfang.Luecken => abfrage.Where(m => - !db.Marken.Any(x => x.MovieId == m.Id && x.Art == Markenart.Abspann)), + Segmentumfang.Luecken => abfrage.Where(m => + !db.Segmente.Any(x => x.MovieId == m.Id && x.Art == Segmentart.Abspann)), _ => abfrage.Where(m => - !db.Markensuchen.Any(x => x.MovieId == m.Id && x.Art == Markenart.Abspann)), + !db.Segmentsuchen.Any(x => x.MovieId == m.Id && x.Art == Segmentart.Abspann)), }; var offen = await abfrage @@ -346,7 +346,7 @@ public sealed class Markenerkennung( var film = offen[i]; var titel = Filmname(film.Title, film.Year); - melden(new Scanschritt(Scanphase.Marken, titel, i + 1, offen.Count)); + melden(new Scanschritt(Scanphase.Segmente, titel, i + 1, offen.Count)); try { if (await FilmAsync(film.Id, umfang, ct)) gefunden++; } catch (Exception fehler) @@ -380,16 +380,16 @@ public sealed class Markenerkennung( /// /// Erst grob, dann zielen. Ein Überblick über die letzten zwanzig /// Minuten sagt, ob es eine dunkle Strecke gibt; er kommt aus - /// Schlüsselbildern und ist deshalb billig, aber auf fünf bis zehn + /// Keyframesn und ist deshalb billig, aber auf fünf bis zehn /// Sekunden ungenau. Die Kante holt danach eine Handvoll kurzer Sonden. /// /// public async Task FilmAsync( - Guid filmId, Markenumfang umfang, CancellationToken ct = default) + Guid filmId, Segmentumfang umfang, CancellationToken ct = default) { using var raum = scopes.CreateScope(); var db = raum.ServiceProvider.GetRequiredService(); - var abtaster = raum.ServiceProvider.GetRequiredService(); + var abtaster = raum.ServiceProvider.GetRequiredService(); var kapitelleser = raum.ServiceProvider.GetRequiredService(); var film = await db.Movies.AsNoTracking() @@ -416,21 +416,21 @@ public sealed class Markenerkennung( return false; } - if (umfang == Markenumfang.Luecken && await db.Marken.AsNoTracking() - .AnyAsync(m => m.MovieId == filmId && m.Art == Markenart.Abspann, ct)) + if (umfang == Segmentumfang.Luecken && await db.Segmente.AsNoTracking() + .AnyAsync(m => m.MovieId == filmId && m.Art == Segmentart.Abspann, ct)) { log.LogInformation("„{Film}“: hat schon einen Abspann — übersprungen.", titel); return false; } - if (umfang == Markenumfang.Neue && await db.Markensuchen.AsNoTracking() - .AnyAsync(x => x.MovieId == filmId && x.Art == Markenart.Abspann, ct)) + if (umfang == Segmentumfang.Neue && await db.Segmentsuchen.AsNoTracking() + .AnyAsync(x => x.MovieId == filmId && x.Art == Segmentart.Abspann, ct)) { log.LogInformation("„{Film}“: schon durchsucht — übersprungen.", titel); return false; } - await VermerkenAsync(db, filmId, Markenart.Abspann, ct); + await VermerkenAsync(db, filmId, Segmentart.Abspann, ct); var gefunden = await BildabspannAsync( db, abtaster, kapitelleser, filmId, istFilm: true, titel, film.Path, @@ -438,12 +438,12 @@ public sealed class Markenerkennung( if (!gefunden) { - // Nichts gefunden ist ein Ergebnis. Eine alte Marke, die jetzt + // Nichts gefunden ist ein Ergebnis. Eine altes Segment, die jetzt // nicht mehr bestätigt wird, gehört weg — sonst bliebe ein // Fehlgriff für immer stehen. - var alt = await db.Marken - .FirstOrDefaultAsync(m => m.MovieId == filmId && m.Art == Markenart.Abspann, ct); - if (alt is not null) db.Marken.Remove(alt); + var alt = await db.Segmente + .FirstOrDefaultAsync(m => m.MovieId == filmId && m.Art == Segmentart.Abspann, ct); + if (alt is not null) db.Segmente.Remove(alt); } await db.SaveChangesAsync(ct); @@ -456,12 +456,12 @@ public sealed class Markenerkennung( /// /// Dunkel und bis zum Ende durchhalten — mehr braucht es nicht, /// und mehr hat ein Abspann auch nicht gemeinsam. Erst ein grober - /// Überblick über die letzten zwanzig Minuten aus Schlüsselbildern + /// Überblick über die letzten zwanzig Minuten aus Keyframesn /// (billig, aber auf fünf bis zehn Sekunden ungenau), dann eine Handvoll /// kurzer Sonden für die Kante. /// /// - /// Warum das auch bei Folgen gebraucht wird. Der Tonabgleich lebt + /// Warum das auch bei Folgen gebraucht wird. Der Fingerprintabgleich lebt /// davon, dass der Abspann in jeder Folge derselbe ist — und genau das /// stimmt beim Staffelfinale oft nicht: anderer Schluss, andere Musik, /// kein Treffer. Das Bild weiss davon nichts; dort sieht das Finale aus @@ -474,7 +474,7 @@ public sealed class Markenerkennung( /// ein Pfad beantwortet die Frage „welche Folge war das?" nicht. /// private async Task BildabspannAsync( - FlaroContext db, Bildabtaster abtaster, Kapitelleser kapitelleser, + FlaroContext db, Frameabtaster abtaster, Kapitelleser kapitelleser, Guid titelId, bool istFilm, string name, string pfad, double laufzeit, CancellationToken ct) { @@ -482,7 +482,7 @@ public sealed class Markenerkennung( // **Erst fragen, ob die Datei es selbst weiss.** Ein Kapitel kostet // Millisekunden — ffprobe liest nur den Kopf —, die Helligkeit - // zwanzig Minuten Schlüsselbilder. Und es findet, woran die + // zwanzig Minuten Keyframes. Und es findet, woran die // Helligkeit scheitert: einen Abspann über laufendem Bild, wie bei // „Alice im Wunderland" oder den Outtakes in „(T)Raumschiff // Surprise". @@ -492,7 +492,7 @@ public sealed class Markenerkennung( log.LogInformation("„{Titel}“: Abspann laut Kapitel ab {Stelle:h\\:mm\\:ss}.", name, TimeSpan.FromSeconds(ausKapitel)); - return await SchreibenAsync(db, titelId, istFilm, Markenart.Abspann, + return await SchreibenAsync(db, titelId, istFilm, Segmentart.Abspann, ausKapitel, laufzeit, ct); } @@ -500,7 +500,7 @@ public sealed class Markenerkennung( var proben = await abtaster.UeberblickAsync(pfad, ab, laufzeit - ab, ct); // **Ein Lauf ohne Fund soll sagen, warum.** „Kein Abspann erkannt" - // allein ist keine Auskunft: ob die Datei zu wenige Schlüsselbilder + // allein ist keine Auskunft: ob die Datei zu wenige Keyframes // hat, ob der Abspann zu hell war oder ob am Schluss eine Tafel // steht, sieht man nur an diesen drei Zahlen — und sonst gar nicht. var stücke = Abspannkante.Suchen(proben, regeln, out var grund); @@ -520,7 +520,7 @@ public sealed class Markenerkennung( // **Nur die erste Kante wird nachgemessen.** Sie ist die, die der // Zuschauer zuerst sieht, und die, an der „gilt als gesehen" hängt. - // Die übrigen Grenzen liegen an Schlüsselbildern und sind damit auf + // Die übrigen Grenzen liegen an Keyframesn und sind damit auf // fünf bis zehn Sekunden genau — dafür lohnt keine Sonde. var hell = Abspannkante.HellDavor(proben, stücke[0].Von, regeln) ?? ab; var kante = await Abspannkante.VerfeinernAsync( @@ -545,28 +545,28 @@ public sealed class Markenerkennung( (a, b) => $"{Zeit(a.Bis)}–{Zeit(b.Von)}"))); } - return await SchreibenAsync(db, titelId, istFilm, Markenart.Abspann, fein, ct); + return await SchreibenAsync(db, titelId, istFilm, Segmentart.Abspann, fein, ct); } /// Was ein Serienlauf ergeben hat — für die Zusammenfassung. /// - /// Wie viele Marken geschrieben wurden. + /// Wie viele Segmente geschrieben wurden. /// /// Ob überhaupt gesucht wurde. Eine Serie, deren Staffeln alle schon /// durchsucht sind, zählt nicht als „ohne Fund" — sie wurde gar nicht /// erst gefragt, und beides in einen Topf zu werfen machte die /// Zusammenfassung wertlos. /// - private readonly record struct Serienstand(int Marken, bool Angefasst); + private readonly record struct Serienstand(int Segmente, bool Angefasst); private async Task LaufenAsync( - Guid serieId, Markenumfang umfang, CancellationToken ct, + Guid serieId, Segmentumfang umfang, CancellationToken ct, Action? melden = null) { using var raum = scopes.CreateScope(); var db = raum.ServiceProvider.GetRequiredService(); - var leser = raum.ServiceProvider.GetRequiredService(); - var abtaster = raum.ServiceProvider.GetRequiredService(); + var leser = raum.ServiceProvider.GetRequiredService(); + var abtaster = raum.ServiceProvider.GetRequiredService(); var kapitelleser = raum.ServiceProvider.GetRequiredService(); var serienname = await db.Series.AsNoTracking() @@ -595,14 +595,14 @@ public sealed class Markenerkennung( // **Was schon einmal gesucht wurde, und mit wie vielen Folgen.** // Nicht „was gefunden wurde": eine Staffel ohne Abspann sähe sonst // bei jedem Scan aus wie eine, in der noch niemand nachgesehen hat. - var gesucht = await db.Markensuchen.AsNoTracking() + var gesucht = await db.Segmentsuchen.AsNoTracking() .Where(x => x.SeriesId == serieId) .ToListAsync(ct); // **Und was tatsächlich an den Folgen hängt.** Für „nur die // Lücken" reicht der Vermerk nicht: er sagt, dass gesucht wurde, // nicht ob etwas herauskam. - var vorhanden = await db.Marken.AsNoTracking() + var vorhanden = await db.Segmente.AsNoTracking() .Where(m => m.EpisodeId != null && db.Episodes.Any(f => f.Id == m.EpisodeId && f.SeriesId == serieId)) .Select(m => new { Folge = m.EpisodeId!.Value, m.Art }) @@ -613,7 +613,7 @@ public sealed class Markenerkennung( log.LogInformation("„{Serie}“: {Staffeln} Staffel(n), {Folgen} Folge(n).", serienname, staffeln.Count, folgen.Count); - // Getrennt gezählt: „14 Marken" beantwortet nicht, ob diese Serie + // Getrennt gezählt: „14 Segmente" beantwortet nicht, ob diese Serie // ihre Vorspänne bekommen hat — und das ist die häufigere Frage. var gefundenIntro = 0; var gefundenAbspann = 0; @@ -629,25 +629,25 @@ public sealed class Markenerkennung( // **Einmal lesen, zweimal schneiden.** fpcalc kann nicht // springen — es liest immer von vorn. Also die ganze Spur je // Folge, und Vorspann wie Abspann sind danach zwei Ausschnitte - // aus demselben Abdruck. Das ist nicht nur richtig, sondern + // aus demselben Fingerprint. Das ist nicht nur richtig, sondern // billiger als zweimal lesen. // Eine Art fehlt dieser Staffel, sobald irgendeine ihrer Folgen - // sie nicht hat. Der Tonabgleich ist Sache der ganzen Staffel, + // sie nicht hat. Der Fingerprintabgleich ist Sache der ganzen Staffel, // nicht der einzelnen Folge — für eine Lücke läuft sie ganz. - bool Luecke(Markenart art) => geordnet.Exists( + bool Luecke(Segmentart art) => geordnet.Exists( f => !vorhanden.Exists(v => v.Folge == f.Id && v.Art == art)); - var fehlt = new[] { Markenart.Intro, Markenart.Abspann } - .Where(art => umfang == Markenumfang.Alles + var fehlt = new[] { Segmentart.Intro, Segmentart.Abspann } + .Where(art => umfang == Segmentumfang.Alles || gesucht.FirstOrDefault( x => x.Staffel == staffel.Key && x.Art == art) is not { } lauf || lauf.Folgen != geordnet.Count - || (umfang == Markenumfang.Luecken && Luecke(art))) + || (umfang == Segmentumfang.Luecken && Luecke(art))) .ToList(); if (fehlt.Count == 0) continue; angefasst = true; - // **Der Tonabgleich braucht etwas zum Vergleichen.** Eine + // **Der Fingerprintabgleich braucht etwas zum Vergleichen.** Eine // Staffel mit einer einzigen Folge — eine laufende Serie, von // der erst die Premiere da ist — hat das nicht. Früher wurde // sie hier übersprungen, und zwar ganz; seit es den Bildweg @@ -657,15 +657,15 @@ public sealed class Markenerkennung( { for (var i = 0; i < geordnet.Count; i++) { - melden?.Invoke(new Scanschritt(Scanphase.Marken, Name(i))); - var abdruck = await leser.LesenAsync(geordnet[i].Path, ct); - if (abdruck.Count > 0) ganze[geordnet[i].Id] = abdruck; + melden?.Invoke(new Scanschritt(Scanphase.Segmente, Name(i))); + var fingerprint = await leser.LesenAsync(geordnet[i].Path, ct); + if (fingerprint.Count > 0) ganze[geordnet[i].Id] = fingerprint; } } else { log.LogInformation( - "„{Serie}“, Staffel {Staffel}: nur eine Folge — Tonabgleich entfällt, " + "„{Serie}“, Staffel {Staffel}: nur eine Folge — Fingerprintabgleich entfällt, " + "es bleibt das Bild.", serienname, staffel.Key); } @@ -675,7 +675,7 @@ public sealed class Markenerkennung( foreach (var art in tonarten) { - var abdruecke = new List<(Guid Schlüssel, IReadOnlyList Abdruck)>(); + var fingerprints = new List<(Guid Schlüssel, IReadOnlyList Fingerprint)>(); var fensteranfang = new Dictionary(); foreach (var folge in geordnet) @@ -685,21 +685,21 @@ public sealed class Markenerkennung( var (vonIndex, anzahl) = Ausschnitt(art, ganz.Count); if (anzahl < 2) continue; - abdruecke.Add((folge.Id, ganz.Skip(vonIndex).Take(anzahl).ToList())); - fensteranfang[folge.Id] = vonIndex * Tonabdruckleser.SekundenJeAbdruck; + fingerprints.Add((folge.Id, ganz.Skip(vonIndex).Take(anzahl).ToList())); + fensteranfang[folge.Id] = vonIndex * Fingerprintleser.SekundenJeFingerprint; } - if (abdruecke.Count < 2) continue; + if (fingerprints.Count < 2) continue; - var treffer = Tonabgleich.ReiheAbgleichen( - abdruecke, Tonabdruckleser.SekundenJeAbdruck); + var treffer = Fingerprintabgleich.ReiheAbgleichen( + fingerprints, Fingerprintleser.SekundenJeFingerprint); // **Vermerken, dass gesucht wurde — vor dem Ergebnis.** // Auch ein Lauf ohne Fund ist ein Lauf; ihn nicht zu // vermerken hiesse, ihn beim nächsten Scan zu wiederholen. await VermerkenAsync(db, serieId, staffel.Key, art, geordnet.Count, ct); - var grenze = art == Markenart.Intro ? Hoechstlaenge : Hoechstabspann; + var grenze = art == Segmentart.Intro ? Hoechstlaenge : Hoechstabspann; foreach (var (folgenId, stelle) in treffer) { @@ -713,18 +713,18 @@ public sealed class Markenerkennung( if (!await SchreibenAsync(db, folgenId, istFilm: false, art, stelle.Von + versatz, stelle.Bis + versatz, ct)) continue; - if (art == Markenart.Intro) gefundenIntro++; else gefundenAbspann++; + if (art == Segmentart.Intro) gefundenIntro++; else gefundenAbspann++; } } var nummern = geordnet.Select(f => f.Id).ToList(); var ueberBild = new HashSet(); - if (fehlt.Contains(Markenart.Abspann)) + if (fehlt.Contains(Segmentart.Abspann)) { - // Auch wenn der Tonabgleich gar nicht erst lief: gesucht wurde, + // Auch wenn der Fingerprintabgleich gar nicht erst lief: gesucht wurde, // und ohne Vermerk liefe der Bildweg bei jedem Scan von Neuem. - await VermerkenAsync(db, serieId, staffel.Key, Markenart.Abspann, + await VermerkenAsync(db, serieId, staffel.Key, Segmentart.Abspann, geordnet.Count, ct); // **Was der Ton nicht fand, sucht das Bild.** @@ -737,13 +737,13 @@ public sealed class Markenerkennung( // andere Folge. // // **Nur für die Lücken.** Der Überblick dekodiert zwanzig - // Minuten Schlüsselbilder; bei einer Staffel ohne Lücke läuft + // Minuten Keyframes; bei einer Staffel ohne Lücke läuft // er gar nicht, mit Lücke ein- bis zweimal. Erst speichern, - // sonst sieht die Abfrage die eben geschriebenen Marken nicht. + // sonst sieht die Abfrage die eben geschriebenen Segmente nicht. await db.SaveChangesAsync(ct); - var mitAbspann = await db.Marken.AsNoTracking() - .Where(m => m.Art == Markenart.Abspann + var mitAbspann = await db.Segmente.AsNoTracking() + .Where(m => m.Art == Segmentart.Abspann && m.EpisodeId != null && nummern.Contains(m.EpisodeId.Value)) .Select(m => m.EpisodeId!.Value) .ToListAsync(ct); @@ -754,7 +754,7 @@ public sealed class Markenerkennung( if (mitAbspann.Contains(folge.Id)) continue; if (folge.Duration is not { TotalSeconds: > 0 } dauer) continue; - melden?.Invoke(new Scanschritt(Scanphase.Marken, Name(i))); + melden?.Invoke(new Scanschritt(Scanphase.Segmente, Name(i))); if (await BildabspannAsync(db, abtaster, kapitelleser, folge.Id, istFilm: false, Name(i), folge.Path, dauer.TotalSeconds, ct)) @@ -805,13 +805,13 @@ public sealed class Markenerkennung( CancellationToken ct) { var nummern = folgen.Select(f => f.Id).ToList(); - var marken = await db.Marken.AsNoTracking() + var segmente = await db.Segmente.AsNoTracking() .Where(m => m.EpisodeId != null && nummern.Contains(m.EpisodeId.Value)) .Select(m => new { Folge = m.EpisodeId!.Value, m.Art, m.VonSekunden, m.BisSekunden }) .ToListAsync(ct); - var mitIntro = marken.Count(m => m.Art == Markenart.Intro); - var mitAbspann = marken.Count(m => m.Art == Markenart.Abspann); + var mitIntro = segmente.Count(m => m.Art == Segmentart.Intro); + var mitAbspann = segmente.Count(m => m.Art == Segmentart.Abspann); log.LogInformation( // **Jeder Platzhalter verbraucht ein Argument, auch ein @@ -825,8 +825,8 @@ public sealed class Markenerkennung( foreach (var (id, name) in folgen) { - var intro = marken.FirstOrDefault(m => m.Folge == id && m.Art == Markenart.Intro); - var abspann = marken.FirstOrDefault(m => m.Folge == id && m.Art == Markenart.Abspann); + var intro = segmente.FirstOrDefault(m => m.Folge == id && m.Art == Segmentart.Intro); + var abspann = segmente.FirstOrDefault(m => m.Folge == id && m.Art == Segmentart.Abspann); log.LogInformation("{Folge}: {Vorspann}, {Abspann}.", name, @@ -863,35 +863,35 @@ public sealed class Markenerkennung( return string.IsNullOrWhiteSpace(titel) ? kopf : $"{kopf} · {titel}"; } - /// Welcher Ausschnitt des Abdrucks abgetastet wird. + /// Welcher Ausschnitt des Fingerprints abgetastet wird. /// /// - /// In Werten, nicht in Sekunden: der Abdruck ist eine Reihe, und + /// In Werten, nicht in Sekunden: der Fingerprint ist eine Reihe, und /// geschnitten wird an einer Stelle darin. Die Laufzeit steht nicht mehr - /// in der Rechnung — die Länge des Abdrucks ist sie. + /// in der Rechnung — die Länge des Fingerprints ist sie. /// - private static (int Von, int Anzahl) Ausschnitt(Markenart art, int werte) + private static (int Von, int Anzahl) Ausschnitt(Segmentart art, int werte) { var fenster = (int)Math.Round( - (art == Markenart.Intro ? Introfenster : Abspannfenster) - / Tonabdruckleser.SekundenJeAbdruck); + (art == Segmentart.Intro ? Introfenster : Abspannfenster) + / Fingerprintleser.SekundenJeFingerprint); if (fenster >= werte) return (0, werte); - return art == Markenart.Intro ? (0, fenster) : (werte - fenster, fenster); + return art == Segmentart.Intro ? (0, fenster) : (werte - fenster, fenster); } /// Hält fest, dass bei diesem Film gesucht wurde. private async Task VermerkenAsync( - FlaroContext db, Guid filmId, Markenart art, CancellationToken ct) + FlaroContext db, Guid filmId, Segmentart art, CancellationToken ct) { - var alt = db.Markensuchen.Local + var alt = db.Segmentsuchen.Local .FirstOrDefault(x => x.MovieId == filmId && x.Art == art) - ?? await db.Markensuchen + ?? await db.Segmentsuchen .FirstOrDefaultAsync(x => x.MovieId == filmId && x.Art == art, ct); if (alt is not null) { alt.GelaufenAm = clock.GetUtcNow(); return; } - db.Markensuchen.Add(new Markensuche + db.Segmentsuchen.Add(new Segmentsuche { MovieId = filmId, Art = art, @@ -902,20 +902,20 @@ public sealed class Markenerkennung( /// Hält fest, dass in dieser Staffel gesucht wurde. private async Task VermerkenAsync( - FlaroContext db, Guid serieId, int staffel, Markenart art, int folgen, CancellationToken ct) + FlaroContext db, Guid serieId, int staffel, Segmentart art, int folgen, CancellationToken ct) { // **Erst im Änderungsspeicher nachsehen, dann in der Datenbank.** // // Eine Abfrage sieht nicht, was derselbe Lauf eben erst hinzugefügt // und noch nicht gespeichert hat. Seit der Bildweg dazukam, wird // für dieselbe Staffel zweimal vermerkt — einmal nach dem - // Tonabgleich, einmal vor dem Bild —, und die zweite Abfrage fand + // Fingerprintabgleich, einmal vor dem Bild —, und die zweite Abfrage fand // nichts, legte eine zweite Zeile an und brach beim Speichern am // eindeutigen Schlüssel ab. Die Serie verlor dabei alles, was in // diesem Lauf gefunden worden war. - var alt = db.Markensuchen.Local + var alt = db.Segmentsuchen.Local .FirstOrDefault(x => x.SeriesId == serieId && x.Staffel == staffel && x.Art == art) - ?? await db.Markensuchen + ?? await db.Segmentsuchen .FirstOrDefaultAsync( x => x.SeriesId == serieId && x.Staffel == staffel && x.Art == art, ct); @@ -926,7 +926,7 @@ public sealed class Markenerkennung( return; } - db.Markensuchen.Add(new Markensuche + db.Segmentsuchen.Add(new Segmentsuche { SeriesId = serieId, Staffel = staffel, @@ -937,11 +937,11 @@ public sealed class Markenerkennung( } private Task SchreibenAsync( - FlaroContext db, Guid titelId, bool istFilm, Markenart art, + FlaroContext db, Guid titelId, bool istFilm, Segmentart art, double von, double bis, CancellationToken ct) => SchreibenAsync(db, titelId, istFilm, art, [new Abspannstück(von, bis)], ct); - /// Setzt die Marken einer Art auf genau diese Stücke. + /// Setzt die Segmente einer Art auf genau diese Stücke. /// /// /// @@ -955,24 +955,24 @@ public sealed class Markenerkennung( /// /// In derselben Transaktion — gespeichert wird weiter oben, und /// bis dahin stehen Löschung und Anlage zusammen. Dazwischen gibt es - /// keinen Augenblick, in dem der Titel gar keine Marke hätte. + /// keinen Augenblick, in dem der Titel gar kein Segment hätte. /// /// private async Task SchreibenAsync( - FlaroContext db, Guid titelId, bool istFilm, Markenart art, + FlaroContext db, Guid titelId, bool istFilm, Segmentart art, IReadOnlyList stücke, CancellationToken ct) { if (stücke.Count == 0) return false; var alte = istFilm - ? await db.Marken.Where(m => m.MovieId == titelId && m.Art == art).ToListAsync(ct) - : await db.Marken.Where(m => m.EpisodeId == titelId && m.Art == art).ToListAsync(ct); + ? await db.Segmente.Where(m => m.MovieId == titelId && m.Art == art).ToListAsync(ct) + : await db.Segmente.Where(m => m.EpisodeId == titelId && m.Art == art).ToListAsync(ct); - db.Marken.RemoveRange(alte); + db.Segmente.RemoveRange(alte); foreach (var stück in stücke) { - db.Marken.Add(new Marke + db.Segmente.Add(new Segment { EpisodeId = istFilm ? null : titelId, MovieId = istFilm ? titelId : null, diff --git a/server/src/Flaro.Api/Streamkoordinator.cs b/server/src/Flaro.Api/Streamkoordinator.cs index 32438a5..3340013 100644 --- a/server/src/Flaro.Api/Streamkoordinator.cs +++ b/server/src/Flaro.Api/Streamkoordinator.cs @@ -32,7 +32,7 @@ public sealed record Stromsitzung( /// /// Daran hängt das Aufräumen. Ein geschlossener Browser sagt /// niemandem Bescheid; bliebe die Sitzung stehen, liefe ffmpeg weiter und - /// schriebe den ganzen Film auf den Kratzplatz. + /// schriebe den ganzen Film auf den Transcode-Cache. /// public DateTimeOffset Gesehen { get; set; } = Begonnen; @@ -61,7 +61,7 @@ public sealed class Streamkoordinator( IServiceScopeFactory scopes, FlaroPaths pfade, FfmpegOptions ffmpeg, - Schluesselbilder schluesselbilder, + Keyframes schluesselbilder, TimeProvider clock, ILogger log) : IDisposable { @@ -140,22 +140,22 @@ public sealed class Streamkoordinator( var stufe = await bereich.ServiceProvider .GetRequiredService().StufeAsync(ct); - // **Beim Kopieren wird auf das Schlüsselbild gerundet.** + // **Beim Kopieren wird auf das Keyframe gerundet.** // // Ein kopierter Bildstrom lässt sich nicht mitten in einer Bildgruppe - // aufschneiden; ffmpeg beginnt beim Schlüsselbild davor, während der + // aufschneiden; ffmpeg beginnt beim Keyframe davor, während der // Ton neu gerechnet wird und genau beim Wunsch anfängt. Gemessen an - // einer Quelle mit Schlüsselbildern alle fünf Sekunden, Sprung auf + // einer Quelle mit Keyframesn alle fünf Sekunden, Sprung auf // Sekunde 12: Bild ab 0, Ton erst ab 1,98 — zwei Sekunden stummes // Bild, und die Uhr des Spielers zwei Sekunden daneben. // // Beim Umkodieren entfällt das: dort wird das Bild neu gerechnet und // schneidet auf die Sekunde genau. Dann vorzurunden hiesse, ohne Not // ein Stück zurückzuspringen. - // **Gesprungen wird auf den Wunsch, gemeldet wird das Schlüsselbild.** + // **Gesprungen wird auf den Wunsch, gemeldet wird das Keyframe.** // // Beim Kopieren springt ffmpeg ungenau — siehe `HlsArgumente` — und - // landet damit auf dem Schlüsselbild vor dem Wunsch. Bild und Ton + // landet damit auf dem Keyframe vor dem Wunsch. Bild und Ton // fangen dort gemeinsam an; das ist die Zusage, und sie hängt an // keiner Rechnung hier. // @@ -188,14 +188,14 @@ public sealed class Streamkoordinator( // **Mit Zeiten, und auch im Tagesprotokoll.** // // „Es dauert fünfzehn Sekunden" liess sich nicht beantworten: zwischen - // dem Antippen und dem ersten Bild liegen die Schlüsselbildsuche, der + // dem Antippen und dem ersten Bild liegen die Keyframesuche, der // Start von ffmpeg und das erste Segment, und keine dieser Strecken // war irgendwo abzulesen. Eine Zeile je Wiedergabe ist kein Lärm — // sie ist genau das, wonach man im Protokoll sucht. var meldung = $"Wiedergabe: {plan.Art} von {Path.GetFileName(datei.Path)} ab {sitzung.Ab:0.#}s" + (plan.Hoehe is { } h ? $", auf {h}p" : "") - + $" — Schlüsselbildsuche {suche} ms, Vorbereitung {uhr.ElapsedMilliseconds} ms."; + + $" — Keyframesuche {suche} ms, Vorbereitung {uhr.ElapsedMilliseconds} ms."; log.LogInformation("{Meldung}", meldung); @@ -472,7 +472,7 @@ public sealed class Streamkoordinator( /// Löschen in Abraeumen an einem IOException, ist die /// Sitzung aus dem Verzeichnis heraus und der Ordner bleibt für immer /// liegen. Die Altersprüfung nimmt dabei den Fall heraus, dass ein - /// zweiter Prozess denselben Kratzplatz benutzt: ein aktiver Ordner + /// zweiter Prozess denselben Transcode-Cache benutzt: ein aktiver Ordner /// bekommt alle paar Sekunden ein Segment geschrieben. /// public long VerwaisteOrdnerWegraeumen(DateTimeOffset jetzt) @@ -521,7 +521,7 @@ public sealed class Streamkoordinator( // **Auch, was ein Absturz hinterlassen hat.** Nach einem harten Ende // steht der Ordner noch da; ihn beim nächsten Herunterfahren zu - // übersehen hiesse, dass der Kratzplatz nur wächst. + // übersehen hiesse, dass der Transcode-Cache nur wächst. WurzelLeeren(); } } diff --git a/server/src/Flaro.Api/Kratzplatzeinstellungen.cs b/server/src/Flaro.Api/Transcodeeinstellungen.cs similarity index 80% rename from server/src/Flaro.Api/Kratzplatzeinstellungen.cs rename to server/src/Flaro.Api/Transcodeeinstellungen.cs index b318cb9..9d1c902 100644 --- a/server/src/Flaro.Api/Kratzplatzeinstellungen.cs +++ b/server/src/Flaro.Api/Transcodeeinstellungen.cs @@ -4,22 +4,22 @@ using Microsoft.EntityFrameworkCore; namespace Flaro.Api; -/// Die Regeln fürs Aufräumen des Kratzplatzes, aus den Einstellungen. +/// Die Regeln fürs Aufräumen des Transcodes, aus den Einstellungen. /// /// /// Nach dem Vorbild von : einmal je /// Anfrage gelesen und gemerkt. Die Tabelle ist generisch, es braucht keine /// Migration. /// -public sealed class Kratzplatzeinstellungen(FlaroContext db) +public sealed class Transcodeeinstellungen(FlaroContext db) { public const string ObergrenzeSchluessel = "wartung.kratzplatz.paket.obergrenzeGb"; public const string UnbenutztSchluessel = "wartung.kratzplatz.paket.unbenutztTage"; public const string SchonfristSchluessel = "wartung.kratzplatz.paket.schonfristMinuten"; - private Kratzplatzregeln? _gemerkt; + private Transcoderegeln? _gemerkt; - public async Task RegelnAsync(CancellationToken ct = default) + public async Task RegelnAsync(CancellationToken ct = default) { if (_gemerkt is { } fertig) return fertig; @@ -29,9 +29,9 @@ public sealed class Kratzplatzeinstellungen(FlaroContext db) || e.Key == SchonfristSchluessel) .ToDictionaryAsync(e => e.Key, e => e.Value, ct); - var vorgabe = Kratzplatzregeln.Vorgabe; + var vorgabe = Transcoderegeln.Vorgabe; - _gemerkt = Kratzplatzregeln.Geprueft( + _gemerkt = Transcoderegeln.Geprueft( Zahl(werte, ObergrenzeSchluessel, vorgabe.ObergrenzeBytes / 1024d / 1024 / 1024), (int)Zahl(werte, UnbenutztSchluessel, vorgabe.Unbenutzt.TotalDays), (int)Zahl(werte, SchonfristSchluessel, vorgabe.Schonfrist.TotalMinutes)); diff --git a/server/src/Flaro.Api/Kratzplatzpflege.cs b/server/src/Flaro.Api/Transcodepflege.cs similarity index 96% rename from server/src/Flaro.Api/Kratzplatzpflege.cs rename to server/src/Flaro.Api/Transcodepflege.cs index 0ff931b..cb2c6be 100644 --- a/server/src/Flaro.Api/Kratzplatzpflege.cs +++ b/server/src/Flaro.Api/Transcodepflege.cs @@ -11,7 +11,7 @@ public sealed record Aufraeumbericht( /// Der Unterordner — stroeme, packages, untertitel. public sealed record Teilbericht(string Name, long Frei, int Dateien); -/// Räumt den Kratzplatz auf. +/// Räumt den Transcode-Cache auf. /// /// /// @@ -31,13 +31,13 @@ public sealed record Teilbericht(string Name, long Frei, int Dateien); /// nebeneinander löschten einander die Dateien unter den Händen weg. /// /// -public sealed class Kratzplatzpflege( +public sealed class Transcodepflege( IServiceScopeFactory scopes, FlaroPaths pfade, PackageCoordinator pakete, Streamkoordinator stroeme, TimeProvider uhr, - ILogger log) + ILogger log) { private readonly SemaphoreSlim _einerZurZeit = new(1, 1); @@ -78,7 +78,7 @@ public sealed class Kratzplatzpflege( if (bericht.Frei > 0) { log.LogInformation( - "Kratzplatz aufgeräumt ({Anlass}): {Mb} MB frei, {Dateien} Datei(en).", + "Transcode-Cache aufgeräumt ({Anlass}): {Mb} MB frei, {Dateien} Datei(en).", anlass, bericht.Frei / (1024 * 1024), bericht.Dateien); } @@ -100,7 +100,7 @@ public sealed class Kratzplatzpflege( using var raum = scopes.CreateScope(); var db = raum.ServiceProvider.GetRequiredService(); var regeln = await raum.ServiceProvider - .GetRequiredService().RegelnAsync(ct); + .GetRequiredService().RegelnAsync(ct); // **Teilstücke gehen bedingungslos.** Sie entstehen nur beim Bauen, // und wer gerade baut, hat die Datei noch offen — eine liegengebliebene @@ -137,7 +137,7 @@ public sealed class Kratzplatzpflege( .Select(f => new { f.Id, f.ModifiedAt }) .ToDictionaryAsync(f => f.Id, f => f.ModifiedAt.ToUnixTimeMilliseconds(), ct); - foreach (var a in Kratzplatzauswahl.Wegzuwerfen( + foreach (var a in Transcodeauswahl.Wegzuwerfen( akten, bekannt, PackageCoordinator.Bauart, regeln, jetzt, pakete.IstInBenutzung)) { frei += Loeschen(a.Pfad); diff --git a/server/src/Flaro.Core/Auth/AuthCode.cs b/server/src/Flaro.Core/Auth/AuthCode.cs index d2744dd..88e5353 100644 --- a/server/src/Flaro.Core/Auth/AuthCode.cs +++ b/server/src/Flaro.Core/Auth/AuthCode.cs @@ -12,7 +12,7 @@ namespace Flaro.Core.Auth; /// /// /// Der Code allein holt nie ein Token ab. Dafür gibt es den Abholschein, -/// den nur das anfragende Gerät kennt und der hier nur als Fingerabdruck +/// den nur das anfragende Gerät kennt und der hier nur als Fingerfingerprint /// liegt. Ohne ihn könnte jemand sechsstellige Zahlen durchprobieren — eine /// Million Versuche sind nichts, und am Ende stünde ein fremdes Gerät in der /// Mediathek. diff --git a/server/src/Flaro.Core/Auth/Einladung.cs b/server/src/Flaro.Core/Auth/Einladung.cs index f75939a..3e37b60 100644 --- a/server/src/Flaro.Core/Auth/Einladung.cs +++ b/server/src/Flaro.Core/Auth/Einladung.cs @@ -14,7 +14,7 @@ namespace Flaro.Core.Auth; /// /// /// Übernommen ist die Geheimnisdisziplin. Wie bei -/// liegt hier nur der Fingerabdruck; den Link +/// liegt hier nur der Fingerfingerprint; den Link /// selbst bekommt man genau einmal zu sehen, beim Erstellen. Wer die /// Datenbank liest, kann daraus keine Einladung bauen. /// diff --git a/server/src/Flaro.Core/Auth/Secrets.cs b/server/src/Flaro.Core/Auth/Secrets.cs index ddecaf3..a4209c2 100644 --- a/server/src/Flaro.Core/Auth/Secrets.cs +++ b/server/src/Flaro.Core/Auth/Secrets.cs @@ -6,7 +6,7 @@ namespace Flaro.Core.Auth; /// Erzeugt und verfingert Geheimnisse, die Clients vorzeigen. /// /// -/// Abgelegt wird nur der Fingerabdruck, nie das Geheimnis. Wer die +/// Abgelegt wird nur der Fingerfingerprint, nie das Geheimnis. Wer die /// Datenbank erbeutet, hat damit Hashes und keine Zugänge. Dass das überhaupt /// erwähnt werden muss: bei Medienservern liegen Token regelmässig im /// Klartext, und ein einziges Datenbankleck gibt dann jedem alles. @@ -23,7 +23,7 @@ public static class Secrets public static string Create() => Base64UrlEncode(RandomNumberGenerator.GetBytes(Bytes)); - /// Der Fingerabdruck, wie er in der Datenbank steht. + /// Der Fingerfingerprint, wie er in der Datenbank steht. public static string Fingerprint(string secret) => Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(secret))); diff --git a/server/src/Flaro.Core/Delivery/Paketname.cs b/server/src/Flaro.Core/Delivery/Paketname.cs index a744d5f..75d6ded 100644 --- a/server/src/Flaro.Core/Delivery/Paketname.cs +++ b/server/src/Flaro.Core/Delivery/Paketname.cs @@ -6,7 +6,7 @@ namespace Flaro.Core.Delivery; /// /// /// -/// Warum der Name alles trägt. Im Kratzplatz liegen Dateien, zu denen +/// Warum der Name alles trägt. Im Transcode-Cache liegen Dateien, zu denen /// es keine Tabelle gibt. Ein Aufräumer muss allein am Namen erkennen können, /// wozu eine Datei gehört und ob sie noch gilt — sonst bliebe ihm nur, /// alles zu behalten oder alles wegzuwerfen. diff --git a/server/src/Flaro.Core/Delivery/Kratzplatzregeln.cs b/server/src/Flaro.Core/Delivery/Transcoderegeln.cs similarity index 93% rename from server/src/Flaro.Core/Delivery/Kratzplatzregeln.cs rename to server/src/Flaro.Core/Delivery/Transcoderegeln.cs index ddb97fb..4e73355 100644 --- a/server/src/Flaro.Core/Delivery/Kratzplatzregeln.cs +++ b/server/src/Flaro.Core/Delivery/Transcoderegeln.cs @@ -21,7 +21,7 @@ namespace Flaro.Core.Delivery; /// Wie viel insgesamt liegen bleiben darf; 0 heisst unbegrenzt. /// Ab wann eine unbenutzte Datei auch unterhalb der Grenze geht; Zero heisst nie. /// Wie lange eine frisch gebrauchte Datei in jedem Fall bleibt. -public sealed record Kratzplatzregeln( +public sealed record Transcoderegeln( long ObergrenzeBytes, TimeSpan Unbenutzt, TimeSpan Schonfrist) @@ -34,7 +34,7 @@ public sealed record Kratzplatzregeln( /// Schonfrist decken die Lücke zwischen zwei Teilanfragen und einen /// Neustart, der den Benutzungszähler verliert. /// - public static Kratzplatzregeln Vorgabe { get; } = + public static Transcoderegeln Vorgabe { get; } = new(20L * 1024 * 1024 * 1024, TimeSpan.FromDays(7), TimeSpan.FromMinutes(30)); /// Nimmt entgegen, was eingestellt wurde, und biegt Unsinn zurecht. @@ -45,13 +45,13 @@ public sealed record Kratzplatzregeln( /// Datei weg — deshalb eine Untergrenze von fünf Minuten. Negative Werte /// gelten als „aus". /// - public static Kratzplatzregeln Geprueft(double obergrenzeGb, int unbenutztTage, int schonfristMinuten) => + public static Transcoderegeln Geprueft(double obergrenzeGb, int unbenutztTage, int schonfristMinuten) => new(obergrenzeGb <= 0 ? 0 : (long)(obergrenzeGb * 1024 * 1024 * 1024), unbenutztTage <= 0 ? TimeSpan.Zero : TimeSpan.FromDays(Math.Min(unbenutztTage, 3650)), TimeSpan.FromMinutes(Math.Clamp(schonfristMinuten, 5, 1440))); } -/// Eine Datei im Kratzplatz, so wie sie dort liegt. +/// Eine Datei im Transcode-Cache, so wie sie dort liegt. /// /// /// Wann sie zuletzt gebraucht wurde — die Änderungszeit der Datei selbst. @@ -66,7 +66,7 @@ public readonly record struct Paketakte( Paketname.Teile? Name); /// Welche Dateien weggeräumt werden — reine Rechnung, ohne Platte. -public static class Kratzplatzauswahl +public static class Transcodeauswahl { /// Kennung → Änderungszeitpunkt der Quelle, aus der Mediathek. /// Wie der Server heute baut. @@ -75,7 +75,7 @@ public static class Kratzplatzauswahl IReadOnlyList akten, IReadOnlyDictionary bekannt, int bauart, - Kratzplatzregeln regeln, + Transcoderegeln regeln, DateTimeOffset jetzt, Func inBenutzung) { diff --git a/server/src/Flaro.Core/Library/CatalogEntities.cs b/server/src/Flaro.Core/Library/CatalogEntities.cs index 77c07f0..7119acf 100644 --- a/server/src/Flaro.Core/Library/CatalogEntities.cs +++ b/server/src/Flaro.Core/Library/CatalogEntities.cs @@ -124,8 +124,8 @@ public sealed class Stichwortlink public Guid? SeriesId { get; init; } } -/// Was für ein Abschnitt eine Marke bezeichnet. -public enum Markenart +/// Was für ein Abschnitt ein Segment bezeichnet. +public enum Segmentart { /// Der Vorspann. Intro, @@ -148,7 +148,7 @@ public enum Markenart /// im Film ist dieselbe. /// /// -public sealed class Marke +public sealed class Segment { public Guid Id { get; init; } = Guid.CreateVersion7(); /// Zu welcher Folge sie gehört — oder zu welchem Film. @@ -162,7 +162,7 @@ public sealed class Marke /// public Guid? EpisodeId { get; init; } public Guid? MovieId { get; init; } - public required Markenart Art { get; init; } + public required Segmentart Art { get; init; } public required double VonSekunden { get; set; } public required double BisSekunden { get; set; } @@ -195,21 +195,21 @@ public sealed class Marke /// Lauf. /// /// -public sealed class Markensuche +public sealed class Segmentsuche { public Guid Id { get; init; } = Guid.CreateVersion7(); /// Eine Staffel einer Serie — oder ein Film. /// /// - /// Genau eines von beiden, wie bei . Bei einer + /// Genau eines von beiden, wie bei . Bei einer /// Staffel steht die Nummer daneben; ein Film hat keine. /// public Guid? SeriesId { get; init; } public int? Staffel { get; init; } public Guid? MovieId { get; init; } - public required Markenart Art { get; init; } + public required Segmentart Art { get; init; } /// Wie viele Folgen die Staffel hatte, als gesucht wurde. /// diff --git a/server/src/Flaro.Core/Library/Scanschritt.cs b/server/src/Flaro.Core/Library/Scanschritt.cs index 6a9b481..a09e710 100644 --- a/server/src/Flaro.Core/Library/Scanschritt.cs +++ b/server/src/Flaro.Core/Library/Scanschritt.cs @@ -28,7 +28,7 @@ public enum Scanphase /// über Stunden, und ohne eine Zeile, die mitzählt, sieht die /// Verwaltung minutenlang so aus, als täte sich nichts. /// - Marken, + Segmente, /// Eine Notiz fürs Protokoll — nicht für die Fortschrittszeile. /// @@ -79,7 +79,7 @@ public readonly record struct Scanschritt(Scanphase Phase, string Was, int Erled Scanphase.Holen => $"Hole Metadaten zu {Was}{zaehler}", Scanphase.Bilder => $"Lade Bilder zu {Was}{zaehler}", Scanphase.Aufraeumen => "Räume auf und lege Doppeltes zusammen", - Scanphase.Marken => $"Suche Vorspann und Abspann: {Was}{zaehler}", + Scanphase.Segmente => $"Suche Vorspann und Abspann: {Was}{zaehler}", Scanphase.Vermerk => Was, _ => Was, }; diff --git a/server/src/Flaro.Core/Library/SidecarNames.cs b/server/src/Flaro.Core/Library/SidecarNames.cs index 54fdf35..c512fde 100644 --- a/server/src/Flaro.Core/Library/SidecarNames.cs +++ b/server/src/Flaro.Core/Library/SidecarNames.cs @@ -18,7 +18,7 @@ namespace Flaro.Core.Library; /// /// Transcode-Zwischenstände gehören ausdrücklich nicht hierher. Die /// liegen unter FLARO_TRANSCODE, auf einem eigenen Volume. Ein -/// Medienordner ist kein Kratzplatz. +/// Medienordner ist kein Transcode-Cache. /// /// public static class SidecarNames diff --git a/server/src/Flaro.Core/Media/Abspannkante.cs b/server/src/Flaro.Core/Media/Abspannkante.cs index d6d0de3..4d6f16b 100644 --- a/server/src/Flaro.Core/Media/Abspannkante.cs +++ b/server/src/Flaro.Core/Media/Abspannkante.cs @@ -20,7 +20,7 @@ namespace Flaro.Core.Media; /// /// Die Stelle im Film. /// Wie viel Prozent des Bildes dunkel sind, 0 bis 100. -public readonly record struct Bildprobe(double Sekunde, double Dunkelanteil); +public readonly record struct Frameprobe(double Sekunde, double Dunkelanteil); /// Ein Stück Abspann: von wann bis wann. /// @@ -80,10 +80,10 @@ public readonly record struct Abspannregeln /// In Sekunden, nicht in Proben — dieselbe Lehre wie bei /// , hier eine Zeile höher übersehen. Als /// Probenzahl hing die Nachsicht daran, wie dicht die Datei ihre - /// Schlüsselbilder setzt: gemessen an „(T)Raumschiff Surprise" kommen + /// Keyframes setzt: gemessen an „(T)Raumschiff Surprise" kommen /// 1318 Proben auf zwanzig Minuten, also eine alle 0,9 Sekunden — /// zwei Proben waren dort 1,8 Sekunden Nachsicht. Bei einer Datei mit - /// einem Schlüsselbild alle zehn Sekunden waren es zwanzig. Dasselbe + /// einem Keyframe alle zehn Sekunden waren es zwanzig. Dasselbe /// Zugeständnis, zehnfach verschieden. /// /// @@ -94,7 +94,7 @@ public readonly record struct Abspannregeln /// /// In Sekunden, nicht in Proben. Eine Logotafel dauert, was sie /// dauert; wie viele Messpunkte darauf fallen, hängt allein daran, wie - /// dicht die Datei ihre Schlüsselbilder setzt. In Proben gezählt wäre + /// dicht die Datei ihre Keyframes setzt. In Proben gezählt wäre /// dieselbe Tafel bei der einen Datei verziehen und bei der nächsten /// ein Abbruch. /// @@ -156,7 +156,7 @@ public readonly record struct Abspannregeln /// Reine Rechnung, ohne Datei. Das Abtasten braucht ffmpeg; das /// Beurteilen braucht nichts. Getrennt lässt sich das Schwierige prüfen, /// ohne einen Film vorzuhalten — dieselbe Naht wie bei -/// . +/// . /// /// public static class Abspannkante @@ -165,10 +165,10 @@ public static class Abspannkante /// /// /// Messpunkte über das Ende des Films, aufsteigend nach Zeit. Sie dürfen - /// ungleichmässig liegen: Schlüsselbilder tun das. + /// ungleichmässig liegen: Keyframes tun das. /// public static IReadOnlyList Suchen( - IReadOnlyList proben, Abspannregeln regeln) => + IReadOnlyList proben, Abspannregeln regeln) => Suchen(proben, regeln, out _); /// @@ -178,7 +178,7 @@ public static class Abspannkante /// und ohne diese Zeile beginnt jede Nachfrage wieder beim Raten. /// public static IReadOnlyList Suchen( - IReadOnlyList proben, Abspannregeln regeln, out string grund) + IReadOnlyList proben, Abspannregeln regeln, out string grund) { grund = "gefunden"; if (proben.Count < 2) @@ -244,11 +244,11 @@ public static class Abspannkante /// /// Die Nachsicht zählt Sekunden, nicht Proben. Eine Schrifttafel /// blitzt auf, ein Logo steht zwei Sekunden im Bild; wie viele - /// Messpunkte darauf fallen, hängt allein an der Schlüsselbilddichte + /// Messpunkte darauf fallen, hängt allein an der Keyframedichte /// der Datei und schwankt um eine Zehnerpotenz. /// private static List Läufe( - IReadOnlyList proben, Abspannregeln regeln) + IReadOnlyList proben, Abspannregeln regeln) { var läufe = new List(); double? von = null; @@ -281,7 +281,7 @@ public static class Abspannkante /// Die letzte helle Probe vor dem Fund — der Suchbereich fürs Nachmessen. public static double? HellDavor( - IReadOnlyList proben, double dunkelAb, Abspannregeln regeln) + IReadOnlyList proben, double dunkelAb, Abspannregeln regeln) { double? letzteHelle = null; foreach (var p in proben) @@ -297,7 +297,7 @@ public static class Abspannkante /// /// /// Binäre Suche mit kurzen Sonden, nicht noch ein Durchlauf. Die - /// grobe Reihe stammt aus Schlüsselbildern, und die liegen je nach + /// grobe Reihe stammt aus Keyframesn, und die liegen je nach /// Aufnahme fünf bis zehn Sekunden auseinander. Den ganzen Bereich fein /// abzutasten hiesse, ihn zu dekodieren; zehn Sonden von je einer /// Sekunde kosten einen Bruchteil davon. Denselben Weg geht Jellyfins diff --git a/server/src/Flaro.Core/Media/Tonabgleich.cs b/server/src/Flaro.Core/Media/Fingerprintabgleich.cs similarity index 88% rename from server/src/Flaro.Core/Media/Tonabgleich.cs rename to server/src/Flaro.Core/Media/Fingerprintabgleich.cs index f879c68..f56f121 100644 --- a/server/src/Flaro.Core/Media/Tonabgleich.cs +++ b/server/src/Flaro.Core/Media/Fingerprintabgleich.cs @@ -24,7 +24,7 @@ public sealed record Gemeinsamkeit(double AVon, double ABis, double BVon, double /// /// /// Verglichen werden Tonabdrücke, keine Töne. Was hereinkommt, ist je -/// Folge eine Reihe von 32-Bit-Zahlen, eine je Zeitscheibe — der Abdruck, +/// Folge eine Reihe von 32-Bit-Zahlen, eine je Zeitscheibe — der Fingerprint, /// den Chromaprint aus dem Ton rechnet. Zwei Aufnahmen desselben Stücks /// ergeben darin fast dieselben Zahlen, auch bei anderer Lautstärke, anderem /// Codec und anderer Bitrate. Deshalb wird nicht auf Gleichheit geprüft, @@ -36,7 +36,7 @@ public sealed record Gemeinsamkeit(double AVon, double ABis, double BVon, double /// sich das Schwierige prüfen, ohne eine Mediendatei vorzuhalten. /// /// -public static class Tonabgleich +public static class Fingerprintabgleich { /// Wie viele Bits zweier Abdrücke sich unterscheiden dürfen. /// @@ -67,7 +67,7 @@ public static class Tonabgleich /// Sucht das längste Stück, das und teilen. /// - /// + /// /// Wie viel Zeit eine Zeitscheibe abdeckt. Chromaprint rechnet mit /// 11025 Hz und 4096 Abtastwerten bei zwei Dritteln Überlappung — das /// sind rund 0,1238 Sekunden. @@ -75,14 +75,14 @@ public static class Tonabgleich public static Gemeinsamkeit? Finden( IReadOnlyList a, IReadOnlyList b, - double sekundenJeAbdruck, + double sekundenJeFingerprint, double mindestlänge = StandardMindestlänge, int bitabstand = StandardBitabstand, int lücke = StandardLücke) { - if (a.Count == 0 || b.Count == 0 || sekundenJeAbdruck <= 0) return null; + if (a.Count == 0 || b.Count == 0 || sekundenJeFingerprint <= 0) return null; - var mindestens = (int)Math.Ceiling(mindestlänge / sekundenJeAbdruck); + var mindestens = (int)Math.Ceiling(mindestlänge / sekundenJeFingerprint); if (mindestens <= 0) mindestens = 1; Gemeinsamkeit? beste = null; @@ -90,7 +90,7 @@ public static class Tonabgleich // **Jede Verschiebung durchprobieren.** Das Intro steht in beiden // Folgen an einer anderen Stelle; gesucht ist die Verschiebung, bei // der die meisten Zeitscheiben aufeinanderpassen. Bei zwei Minuten - // Abdruck sind das ein paar Millionen Vergleiche — ein Wimpernschlag + // Fingerprint sind das ein paar Millionen Vergleiche — ein Wimpernschlag // gegen das Lesen der Datei, aus der sie stammen. for (var versatz = -(b.Count - 1); versatz < a.Count; versatz++) { @@ -102,13 +102,13 @@ public static class Tonabgleich var lauf = LängsterLauf(a, b, vonA, vonB, länge, bitabstand, lücke); if (lauf is not var (start, dauer) || dauer < mindestens) continue; - if (beste is not null && dauer * sekundenJeAbdruck <= beste.Laenge) continue; + if (beste is not null && dauer * sekundenJeFingerprint <= beste.Laenge) continue; beste = new Gemeinsamkeit( - (vonA + start) * sekundenJeAbdruck, - (vonA + start + dauer) * sekundenJeAbdruck, - (vonB + start) * sekundenJeAbdruck, - (vonB + start + dauer) * sekundenJeAbdruck); + (vonA + start) * sekundenJeFingerprint, + (vonA + start + dauer) * sekundenJeFingerprint, + (vonB + start) * sekundenJeFingerprint, + (vonB + start + dauer) * sekundenJeFingerprint); } return beste is not null && beste.Laenge >= mindestlänge ? beste : null; @@ -176,8 +176,8 @@ public static class Tonabgleich /// /// public static IReadOnlyDictionary ReiheAbgleichen( - IReadOnlyList<(TSchlüssel Schlüssel, IReadOnlyList Abdruck)> folgen, - double sekundenJeAbdruck, + IReadOnlyList<(TSchlüssel Schlüssel, IReadOnlyList Fingerprint)> folgen, + double sekundenJeFingerprint, double mindestlänge = StandardMindestlänge) where TSchlüssel : notnull { @@ -186,10 +186,10 @@ public static class Tonabgleich for (var i = 0; i + 1 < folgen.Count; i++) { - var (schlüsselA, abdruckA) = folgen[i]; - var (schlüsselB, abdruckB) = folgen[i + 1]; + var (schlüsselA, fingerprintA) = folgen[i]; + var (schlüsselB, fingerprintB) = folgen[i + 1]; - var treffer = Finden(abdruckA, abdruckB, sekundenJeAbdruck, mindestlänge); + var treffer = Finden(fingerprintA, fingerprintB, sekundenJeFingerprint, mindestlänge); if (treffer is null) continue; // Der erste Fund je Folge gewinnt: er stammt vom Vergleich mit diff --git a/server/src/Flaro.Core/Playback/ProgressRule.cs b/server/src/Flaro.Core/Playback/ProgressRule.cs index 97270fc..372e89a 100644 --- a/server/src/Flaro.Core/Playback/ProgressRule.cs +++ b/server/src/Flaro.Core/Playback/ProgressRule.cs @@ -79,8 +79,8 @@ public static class ProgressRule var schwelle = laenge * regeln.GesehenAnteil; // Der Abspann zieht die Schwelle nach vorn, nie nach hinten: eine - // falsch erkannte Marke bei Minute vierzig darf nicht dazu führen, - // dass eine Folge erst nach der Marke als gesehen gilt. + // falsch erkannte Segment bei Minute vierzig darf nicht dazu führen, + // dass eine Folge erst nach dem Segment als gesehen gilt. if (abspannAb is { } ab && ab > TimeSpan.Zero && ab < schwelle) schwelle = ab; return new Outcome(stelle, Watched: stelle >= schwelle); diff --git a/server/src/Flaro.Data/FlaroContext.cs b/server/src/Flaro.Data/FlaroContext.cs index bc11140..06ca0ef 100644 --- a/server/src/Flaro.Data/FlaroContext.cs +++ b/server/src/Flaro.Data/FlaroContext.cs @@ -76,8 +76,8 @@ public sealed class FlaroContext(DbContextOptions options) : DbCon public DbSet Genres => Set(); public DbSet GenreLinks => Set(); public DbSet Fremdbewertungen => Set(); - public DbSet Marken => Set(); - public DbSet Markensuchen => Set(); + public DbSet Segmente => Set(); + public DbSet Segmentsuchen => Set(); public DbSet Stichworte => Set(); public DbSet Stichwortlinks => Set(); public DbSet People => Set(); @@ -141,7 +141,7 @@ public sealed class FlaroContext(DbContextOptions options) : DbCon { e.HasKey(x => x.Id); - // Gesucht wird nur über den Fingerabdruck — und eindeutig, damit + // Gesucht wird nur über den Fingerfingerprint — und eindeutig, damit // zwei Links niemals dasselbe Konto füttern können. e.HasIndex(x => x.TokenFingerprint).IsUnique(); e.Property(x => x.TokenFingerprint).HasMaxLength(64); @@ -308,7 +308,7 @@ public sealed class FlaroContext(DbContextOptions options) : DbCon e.Property(x => x.Anzeige).HasMaxLength(32); }); - b.Entity(e => + b.Entity(e => { e.HasKey(x => x.Id); // **Mehrere Stücke je Art, und das ist der ganze Zweck.** Hier @@ -326,7 +326,7 @@ public sealed class FlaroContext(DbContextOptions options) : DbCon e.Property(x => x.Art).HasConversion(); }); - b.Entity(e => + b.Entity(e => { e.HasKey(x => x.Id); e.HasIndex(x => new { x.SeriesId, x.Staffel, x.Art }).IsUnique() diff --git a/server/src/Flaro.Data/Migrations/20261016141454_Segmente.Designer.cs b/server/src/Flaro.Data/Migrations/20261016141454_Segmente.Designer.cs new file mode 100644 index 0000000..143507e --- /dev/null +++ b/server/src/Flaro.Data/Migrations/20261016141454_Segmente.Designer.cs @@ -0,0 +1,1357 @@ +// +using System; +using Flaro.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flaro.Data.Migrations +{ + [DbContext(typeof(FlaroContext))] + [Migration("20261016141454_Segmente")] + partial class Segmente + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("Flaro.Core.Auth.AuthCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApprovedByUserId") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("ConsumedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("INTEGER"); + + b.Property("HandleFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("HandleFingerprint") + .IsUnique(); + + b.ToTable("AuthCodes"); + }); + + modelBuilder.Entity("Flaro.Core.Auth.DeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("INTEGER"); + + b.Property("RevokedAt") + .HasColumnType("INTEGER"); + + b.Property("TokenFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TokenFingerprint") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("DeviceTokens"); + }); + + modelBuilder.Entity("Flaro.Core.Auth.Einladung", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedByUserId") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("INTEGER"); + + b.Property("MaxUses") + .HasColumnType("INTEGER"); + + b.Property("RevokedAt") + .HasColumnType("INTEGER"); + + b.Property("TokenFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UsedCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("TokenFingerprint") + .IsUnique(); + + b.ToTable("Einladungen"); + }); + + modelBuilder.Entity("Flaro.Core.Libraries.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ReviewNewItems") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Credit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Character") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("PersonId"); + + b.HasIndex("SeriesId"); + + b.ToTable("Credits"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Episode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AddedAt") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("MetadataLanguage") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("PublishAt") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("StillImage") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId", "SeasonNumber", "EpisodeNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Fremdbewertung", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Anzeige") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Quelle") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Wert") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("MovieId", "Quelle") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("SeriesId", "Quelle") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Fremdbewertungen"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Flaro.Core.Library.GenreLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("GenreId") + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("GenreId", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("GenreId", "SeriesId") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("GenreLinks"); + }); + + modelBuilder.Entity("Flaro.Core.Library.ItemTranslation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("FetchedAt") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId", "Language") + .IsUnique() + .HasFilter("\"EpisodeId\" IS NOT NULL"); + + b.HasIndex("MovieId", "Language") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("SeriesId", "Language") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Translations"); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("Container") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("TEXT"); + + b.Property("MissingSince") + .HasColumnType("INTEGER"); + + b.Property("ModifiedAt") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ProbedAt") + .HasColumnType("INTEGER"); + + b.Property("SeenAt") + .HasColumnType("INTEGER"); + + b.Property("SizeBytes") + .HasColumnType("INTEGER"); + + b.Property("VersionLabel") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("LibraryId", "Path") + .IsUnique(); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("MediaFileId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileId", "Index") + .IsUnique(); + + b.ToTable("MediaStreams"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Meldung", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("ErledigtAm") + .HasColumnType("INTEGER"); + + b.Property("ErledigtVon") + .HasColumnType("TEXT"); + + b.Property("GemeldetAm") + .HasColumnType("INTEGER"); + + b.Property("Kommentar") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Stelle") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("ErledigtAm", "GemeldetAm"); + + b.HasIndex("UserId", "GemeldetAm"); + + b.ToTable("Meldungen"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AddedAt") + .HasColumnType("INTEGER"); + + b.Property("Aufmacherbilder") + .HasColumnType("TEXT"); + + b.Property("BackdropImage") + .HasColumnType("TEXT"); + + b.Property("BackdropPinned") + .HasColumnType("INTEGER"); + + b.Property("ChangedSince") + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("FassungenGeprueftBei") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("TEXT"); + + b.Property("LogoImage") + .HasColumnType("TEXT"); + + b.Property("LogoPinned") + .HasColumnType("INTEGER"); + + b.Property("MatchLocked") + .HasColumnType("INTEGER"); + + b.Property("MetadataLanguage") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("ParsedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParsedYear") + .HasColumnType("INTEGER"); + + b.Property("PosterImage") + .HasColumnType("TEXT"); + + b.Property("PosterPinned") + .HasColumnType("INTEGER"); + + b.Property("PublishAt") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("RuntimeMinutes") + .HasColumnType("INTEGER"); + + b.Property("SortTitlePinned") + .HasColumnType("INTEGER"); + + b.Property("SortTitleValue") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("TitlePinned") + .HasColumnType("INTEGER"); + + b.Property("TmdbId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId", "MetadataLanguage"); + + b.HasIndex("LibraryId", "SortTitleValue"); + + b.HasIndex("LibraryId", "TmdbId") + .IsUnique() + .HasFilter("\"TmdbId\" IS NOT NULL"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Image") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TmdbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TmdbId") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Segment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .HasColumnType("INTEGER"); + + b.Property("BisSekunden") + .HasColumnType("REAL"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("ErkanntAm") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("VonSekunden") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId", "Art"); + + b.HasIndex("MovieId", "Art"); + + b.ToTable("Segmente"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Segmentsuche", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .HasColumnType("INTEGER"); + + b.Property("Folgen") + .HasColumnType("INTEGER"); + + b.Property("GelaufenAm") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Staffel") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId", "Art") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("SeriesId", "Staffel", "Art") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Segmentsuchen"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AddedAt") + .HasColumnType("INTEGER"); + + b.Property("Aufmacherbilder") + .HasColumnType("TEXT"); + + b.Property("BackdropImage") + .HasColumnType("TEXT"); + + b.Property("BackdropPinned") + .HasColumnType("INTEGER"); + + b.Property("ChangedSince") + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("TEXT"); + + b.Property("LogoImage") + .HasColumnType("TEXT"); + + b.Property("LogoPinned") + .HasColumnType("INTEGER"); + + b.Property("MatchLocked") + .HasColumnType("INTEGER"); + + b.Property("MetadataLanguage") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("ParsedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParsedYear") + .HasColumnType("INTEGER"); + + b.Property("PosterImage") + .HasColumnType("TEXT"); + + b.Property("PosterPinned") + .HasColumnType("INTEGER"); + + b.Property("PublishAt") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("RuntimeMinutes") + .HasColumnType("INTEGER"); + + b.Property("SortTitlePinned") + .HasColumnType("INTEGER"); + + b.Property("SortTitleValue") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("TitlePinned") + .HasColumnType("INTEGER"); + + b.Property("TmdbId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId", "MetadataLanguage"); + + b.HasIndex("LibraryId", "SortTitleValue"); + + b.HasIndex("LibraryId", "TmdbId") + .IsUnique() + .HasFilter("\"TmdbId\" IS NOT NULL"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Stichwort", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Stichworte"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Stichwortlink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("StichwortId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("StichwortId", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("StichwortId", "SeriesId") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Stichwortlinks"); + }); + + modelBuilder.Entity("Flaro.Core.Playback.Sehtag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Sekunden") + .HasColumnType("REAL"); + + b.Property("Tag") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("UserId", "Tag"); + + b.HasIndex("UserId", "Tag", "EpisodeId") + .IsUnique() + .HasFilter("\"EpisodeId\" IS NOT NULL"); + + b.HasIndex("UserId", "Tag", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.ToTable("Sehtage"); + }); + + modelBuilder.Entity("Flaro.Core.Playback.UserItemState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Dismissed") + .HasColumnType("INTEGER"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("Favourite") + .HasColumnType("INTEGER"); + + b.Property("InProgress") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedAt") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Runtime") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Watched") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("UserId", "EpisodeId") + .IsUnique() + .HasFilter("\"EpisodeId\" IS NOT NULL"); + + b.HasIndex("UserId", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("UserId", "SeriesId") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.HasIndex("UserId", "InProgress", "UpdatedAt"); + + b.ToTable("UserItemStates"); + }); + + modelBuilder.Entity("Flaro.Core.ServerSetting", b => + { + b.Property("Key") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Flaro.Core.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarImage") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("IsAdministrator") + .HasColumnType("INTEGER"); + + b.Property("IsOwner") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Flaro.Core.Auth.DeviceToken", b => + { + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Auth.Einladung", b => + { + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Credit", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.Episode", b => + { + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Fremdbewertung", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.GenreLink", b => + { + b.HasOne("Flaro.Core.Library.Genre", null) + .WithMany() + .HasForeignKey("GenreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => + { + b.HasOne("Flaro.Core.Libraries.Library", null) + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaStream", b => + { + b.HasOne("Flaro.Core.Library.MediaFile", null) + .WithMany() + .HasForeignKey("MediaFileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Meldung", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Movie", b => + { + b.HasOne("Flaro.Core.Libraries.Library", null) + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Segment", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.Segmentsuche", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.Series", b => + { + b.HasOne("Flaro.Core.Libraries.Library", null) + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Stichwortlink", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Stichwort", null) + .WithMany() + .HasForeignKey("StichwortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Playback.Sehtag", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Playback.UserItemState", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Flaro.Data/Migrations/20261016141454_Segmente.cs b/server/src/Flaro.Data/Migrations/20261016141454_Segmente.cs new file mode 100644 index 0000000..16bd98d --- /dev/null +++ b/server/src/Flaro.Data/Migrations/20261016141454_Segmente.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flaro.Data.Migrations; + +/// Aus „Marke" wird „Segment" — nur der Name, nicht der Inhalt. +/// +/// +/// +/// Von Hand geschrieben, und zwar mit Absicht. Das Gerüst von +/// dotnet ef hätte beide Tabellen gelöscht und neu angelegt — es +/// sieht eine verschwundene und eine neue Tabelle, nicht eine umbenannte. +/// Damit wären jeder erkannte Vorspann, jeder Abspann und jeder Vermerk +/// „hier wurde schon gesucht" weg gewesen, und ein Bestand von +/// viertausend Folgen müsste stundenlang neu durchhört werden. +/// +/// +/// RenameTable und RenameIndex tun dasselbe, ohne eine Zeile +/// anzufassen. +/// +/// +public partial class Segmente : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameTable(name: "Marken", newName: "Segmente"); + migrationBuilder.RenameTable(name: "Markensuchen", newName: "Segmentsuchen"); + + migrationBuilder.RenameIndex( + table: "Segmente", name: "IX_Marken_EpisodeId_Art", + newName: "IX_Segmente_EpisodeId_Art"); + migrationBuilder.RenameIndex( + table: "Segmente", name: "IX_Marken_MovieId_Art", + newName: "IX_Segmente_MovieId_Art"); + migrationBuilder.RenameIndex( + table: "Segmentsuchen", name: "IX_Markensuchen_MovieId_Art", + newName: "IX_Segmentsuchen_MovieId_Art"); + migrationBuilder.RenameIndex( + table: "Segmentsuchen", name: "IX_Markensuchen_SeriesId_Staffel_Art", + newName: "IX_Segmentsuchen_SeriesId_Staffel_Art"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameIndex( + table: "Segmente", name: "IX_Segmente_EpisodeId_Art", + newName: "IX_Marken_EpisodeId_Art"); + migrationBuilder.RenameIndex( + table: "Segmente", name: "IX_Segmente_MovieId_Art", + newName: "IX_Marken_MovieId_Art"); + migrationBuilder.RenameIndex( + table: "Segmentsuchen", name: "IX_Segmentsuchen_MovieId_Art", + newName: "IX_Markensuchen_MovieId_Art"); + migrationBuilder.RenameIndex( + table: "Segmentsuchen", name: "IX_Segmentsuchen_SeriesId_Staffel_Art", + newName: "IX_Markensuchen_SeriesId_Staffel_Art"); + + migrationBuilder.RenameTable(name: "Segmente", newName: "Marken"); + migrationBuilder.RenameTable(name: "Segmentsuchen", newName: "Markensuchen"); + } +} diff --git a/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs b/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs index 4753f62..3aad9a4 100644 --- a/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs +++ b/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs @@ -406,76 +406,6 @@ namespace Flaro.Data.Migrations b.ToTable("Translations"); }); - modelBuilder.Entity("Flaro.Core.Library.Marke", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Art") - .HasColumnType("INTEGER"); - - b.Property("BisSekunden") - .HasColumnType("REAL"); - - b.Property("EpisodeId") - .HasColumnType("TEXT"); - - b.Property("ErkanntAm") - .HasColumnType("INTEGER"); - - b.Property("MovieId") - .HasColumnType("TEXT"); - - b.Property("VonSekunden") - .HasColumnType("REAL"); - - b.HasKey("Id"); - - b.HasIndex("EpisodeId", "Art"); - - b.HasIndex("MovieId", "Art"); - - b.ToTable("Marken"); - }); - - modelBuilder.Entity("Flaro.Core.Library.Markensuche", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Art") - .HasColumnType("INTEGER"); - - b.Property("Folgen") - .HasColumnType("INTEGER"); - - b.Property("GelaufenAm") - .HasColumnType("INTEGER"); - - b.Property("MovieId") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("TEXT"); - - b.Property("Staffel") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("MovieId", "Art") - .IsUnique() - .HasFilter("\"MovieId\" IS NOT NULL"); - - b.HasIndex("SeriesId", "Staffel", "Art") - .IsUnique() - .HasFilter("\"SeriesId\" IS NOT NULL"); - - b.ToTable("Markensuchen"); - }); - modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => { b.Property("Id") @@ -788,6 +718,76 @@ namespace Flaro.Data.Migrations b.ToTable("People"); }); + modelBuilder.Entity("Flaro.Core.Library.Segment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .HasColumnType("INTEGER"); + + b.Property("BisSekunden") + .HasColumnType("REAL"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("ErkanntAm") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("VonSekunden") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId", "Art"); + + b.HasIndex("MovieId", "Art"); + + b.ToTable("Segmente"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Segmentsuche", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .HasColumnType("INTEGER"); + + b.Property("Folgen") + .HasColumnType("INTEGER"); + + b.Property("GelaufenAm") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Staffel") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId", "Art") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("SeriesId", "Staffel", "Art") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Segmentsuchen"); + }); + modelBuilder.Entity("Flaro.Core.Library.Series", b => { b.Property("Id") @@ -1201,32 +1201,6 @@ namespace Flaro.Data.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("Flaro.Core.Library.Marke", b => - { - b.HasOne("Flaro.Core.Library.Episode", null) - .WithMany() - .HasForeignKey("EpisodeId") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Flaro.Core.Library.Movie", null) - .WithMany() - .HasForeignKey("MovieId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Flaro.Core.Library.Markensuche", b => - { - b.HasOne("Flaro.Core.Library.Movie", null) - .WithMany() - .HasForeignKey("MovieId") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Flaro.Core.Library.Series", null) - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade); - }); - modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => { b.HasOne("Flaro.Core.Libraries.Library", null) @@ -1278,6 +1252,32 @@ namespace Flaro.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("Flaro.Core.Library.Segment", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.Segmentsuche", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Flaro.Core.Library.Series", b => { b.HasOne("Flaro.Core.Libraries.Library", null) diff --git a/server/src/Flaro.Media/Tonabdruckleser.cs b/server/src/Flaro.Media/Fingerprintleser.cs similarity index 89% rename from server/src/Flaro.Media/Tonabdruckleser.cs rename to server/src/Flaro.Media/Fingerprintleser.cs index 9381ffe..17315d2 100644 --- a/server/src/Flaro.Media/Tonabdruckleser.cs +++ b/server/src/Flaro.Media/Fingerprintleser.cs @@ -4,12 +4,12 @@ using Microsoft.Extensions.Logging; namespace Flaro.Media; -/// Holt den Tonabdruck eines Dateiabschnitts. +/// Holt den Fingerprint eines Dateiabschnitts. /// /// /// /// Mit fpcalc aus Chromaprint, nicht von Hand. Was hier -/// gebraucht wird, ist ein Abdruck, der denselben Ton in zwei Dateien +/// gebraucht wird, ist ein Fingerprint, der denselben Ton in zwei Dateien /// wiedererkennt, auch bei anderem Codec und anderer Lautstärke. Das ist /// Signalverarbeitung mit einer Fourier-Transformation darin; sie noch /// einmal zu schreiben hiesse, einen seit zwanzig Jahren erprobten Algorith- @@ -17,9 +17,9 @@ namespace Flaro.Media; /// trifft Jellyfins Intro Skipper, und aus demselben Grund. /// /// -/// Fehlt das Programm, gibt es eben keine Marken. Es liegt im Abbild +/// Fehlt das Programm, gibt es eben kein Segmente. Es liegt im Abbild /// (libchromaprint-tools), aber nicht auf jedem Rechner, auf dem -/// jemand den Server baut. Darüber zu stolpern wäre falsch: Marken sind eine +/// jemand den Server baut. Darüber zu stolpern wäre falsch: Segmente sind eine /// Zugabe, kein Teil der Wiedergabe. /// /// @@ -29,20 +29,20 @@ namespace Flaro.Media; /// Gelegenheiten für falsche Funde. /// /// -public sealed class Tonabdruckleser(ILogger log) +public sealed class Fingerprintleser(ILogger log) { - /// Wie viel Zeit eine Zahl des Abdrucks abdeckt. + /// Wie viel Zeit eine Zahl des Fingerprints abdeckt. /// /// /// Chromaprint rechnet mit 11025 Hz, 4096 Abtastwerten je Scheibe und /// zwei Dritteln Überlappung: 4096/3 / 11025 Sekunden. Die Zahl steht - /// hier, weil nur mit ihr aus einem Abdruckbereich eine Zeitangabe wird. + /// hier, weil nur mit ihr aus einem Fingerprintbereich eine Zeitangabe wird. /// /// /// Nachgerechnet an einer Datei von 2700 Sekunden: 21 786 Werte, also /// 0,12393 Sekunden je Wert. Die Formel trifft es auf vier Stellen. /// - public const double SekundenJeAbdruck = 4096.0 / 3.0 / 11025.0; + public const double SekundenJeFingerprint = 4096.0 / 3.0 / 11025.0; /// Wo fpcalc liegt — überschreibbar wie ffmpeg selbst. public static string Programm => @@ -79,7 +79,7 @@ public sealed class Tonabdruckleser(ILogger log) } } - /// Der Abdruck der ganzen Tonspur, oder eine leere Reihe. + /// Der Fingerprint der ganzen Tonspur, oder eine leere Reihe. /// /// /// @@ -104,7 +104,7 @@ public sealed class Tonabdruckleser(ILogger log) { if (!await VorhandenAsync(ct)) { - log.LogDebug("Kein fpcalc vorhanden — keine Marken für {Pfad}.", pfad); + log.LogDebug("Kein fpcalc vorhanden — kein Segmente für {Pfad}.", pfad); return []; } @@ -127,7 +127,7 @@ public sealed class Tonabdruckleser(ILogger log) using var frist = CancellationTokenSource.CreateLinkedTokenSource(ct); // Grosszügig: bei einem Film auf einer schlafenden Platte dauert // allein das Anlaufen Sekunden. Knapp bemessen hiesse, bei jedem - // langsamen Datenträger die Marken zu verlieren. + // langsamen Datenträger die Segmente zu verlieren. frist.CancelAfter(TimeSpan.FromMinutes(3)); try diff --git a/server/src/Flaro.Media/Bildabtaster.cs b/server/src/Flaro.Media/Frameabtaster.cs similarity index 86% rename from server/src/Flaro.Media/Bildabtaster.cs rename to server/src/Flaro.Media/Frameabtaster.cs index 985fa85..b624597 100644 --- a/server/src/Flaro.Media/Bildabtaster.cs +++ b/server/src/Flaro.Media/Frameabtaster.cs @@ -12,7 +12,7 @@ namespace Flaro.Media; /// /// Der einzige Teil der Abspannsuche, der ffmpeg anfasst. Was /// daraus folgt, rechnet ohne Datei — dieselbe -/// Naht wie zwischen Tonabdruckleser und Tonabgleich. +/// Naht wie zwischen Fingerprintleser und Fingerprintabgleich. /// /// /// Zwei Griffe, weil zwei Fragen. „Gibt es am Ende überhaupt eine @@ -23,7 +23,7 @@ namespace Flaro.Media; /// heisst zehn Sekunden Ungenauigkeit. /// /// -public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger log) +public sealed partial class Frameabtaster(FfmpegOptions optionen, ILogger log) { /// Auf welche Breite verkleinert wird, bevor gerechnet wird. /// @@ -53,21 +53,21 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger private const string Dunkelschwelle = "24"; - /// Der Dunkelanteil an den Schlüsselbildern eines Abschnitts. + /// Der Dunkelanteil an den Keyframesn eines Abschnitts. /// /// - /// Nur Schlüsselbilder, und das ist der ganze Trick. Gemessen an + /// Nur Keyframes, und das ist der ganze Trick. Gemessen an /// 180 Sekunden 1080p: jedes zweite Bild zu dekodieren kostet 2,89 - /// Sekunden, nur die Schlüsselbilder 0,24. Dafür liegen die Messpunkte + /// Sekunden, nur die Keyframes 0,24. Dafür liegen die Messpunkte /// fünf bis zehn Sekunden auseinander — genug, um eine dunkle Strecke /// zu finden, zu wenig für ihre Kante. Die holt /// nach. /// - public async Task> UeberblickAsync( + public async Task> UeberblickAsync( string pfad, double ab, double dauer, CancellationToken ct = default) { var ausgabe = await LaufenAsync( - pfad, ab, dauer, nurSchluesselbilder: true, TimeSpan.FromMinutes(5), ct); + pfad, ab, dauer, nurKeyframes: true, TimeSpan.FromMinutes(5), ct); return Lesen(ausgabe, ab); } @@ -75,8 +75,8 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILoggerDer mittlere Dunkelanteil einer einzelnen Sekunde. /// /// - /// Alle Bilder, nicht nur Schlüsselbilder: eine Sekunde enthält sonst - /// vielleicht gar keines. Dass ffmpeg dafür vom vorigen Schlüsselbild + /// Alle Bilder, nicht nur Keyframes: eine Sekunde enthält sonst + /// vielleicht gar keines. Dass ffmpeg dafür vom vorigen Keyframe /// an dekodieren muss, ist der Preis — bei zehn Sekunden Abstand sind /// das zehn Sekunden Bildmaterial, also Millisekunden. /// @@ -84,7 +84,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILoggersplit ginge es, kostete aber drei Ausgabewege /// und eine Zusammenführung. Für etwas, das jemand einmal je Film von - /// Hand anstösst, sind drei Läufe über Schlüsselbilder das günstigere + /// Hand anstösst, sind drei Läufe über Keyframes das günstigere /// Geschäft. /// /// public async Task> KurveAsync( string pfad, double ab, double dauer, CancellationToken ct = default) { - async Task> Lauf(string? zuschnitt) => Lesen( - await LaufenAsync(pfad, ab, dauer, nurSchluesselbilder: true, + async Task> Lauf(string? zuschnitt) => Lesen( + await LaufenAsync(pfad, ab, dauer, nurKeyframes: true, TimeSpan.FromMinutes(10), ct, zuschnitt), ab); @@ -128,8 +128,8 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger reihe, double sekunde) => + // Keyframes, aber darauf zu bauen wäre eine Annahme. + double BeiZeit(IReadOnlyList reihe, double sekunde) => reihe.Count == 0 ? double.NaN : reihe.MinBy(p => Math.Abs(p.Sekunde - sekunde)).Dunkelanteil; @@ -140,7 +140,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger LaufenAsync( - string pfad, double ab, double dauer, bool nurSchluesselbilder, + string pfad, double ab, double dauer, bool nurKeyframes, TimeSpan frist, CancellationToken ct, string? zuschnitt = null) { var start = new ProcessStartInfo(optionen.Ffmpeg) @@ -157,7 +157,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger - public static IReadOnlyList Lesen(string ausgabe, double versatz) + public static IReadOnlyList Lesen(string ausgabe, double versatz) { - var proben = new List(); + var proben = new List(); double? zeit = null; foreach (var zeile in ausgabe.Split('\n')) @@ -251,7 +251,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger public const int SegmentSekunden = 6; - /// Wie viele Bilder zwischen zwei Schlüsselbildern liegen. + /// Wie viele Bilder zwischen zwei Keyframesn liegen. /// /// - /// Ein Segment muss mit einem Schlüsselbild anfangen. Ohne feste + /// Ein Segment muss mit einem Keyframe anfangen. Ohne feste /// Abstände schneidet ffmpeg dort, wo zufällig eines liegt — und die /// Segmente werden unterschiedlich lang, was jeden Sprung ungenau macht. /// @@ -109,17 +109,17 @@ public static class HlsArgumente // **Beim Kopieren wird ungenau gesprungen, und das mit Absicht.** // // Genau gesprungen schneidet ffmpeg den Ton auf die Sekunde, das - // kopierte Bild aber nicht — es kann erst beim Schlüsselbild davor + // kopierte Bild aber nicht — es kann erst beim Keyframe davor // anfangen. Dazwischen liegt Bild ohne Ton, und jeder Spieler darf // diese Lücke deuten, wie er will; hls.js schiebt den Ton nach - // vorn. Gemessen an einer Datei mit Schlüsselbildern alle fünf + // vorn. Gemessen an einer Datei mit Keyframesn alle fünf // Sekunden: bei einem Sprung auf 17 begann der Ton erst bei 1,96, // bei 15,014 sogar erst bei 4,98. // - // `-noaccurate_seek` lässt auch den Ton beim Schlüsselbild + // `-noaccurate_seek` lässt auch den Ton beim Keyframe // anfangen. Dieselben Sprünge, gemessen: 0,004 und 0,015 — und // zwar bei *jedem* Wert, nicht nur bei gut getroffenen. Das ist - // der Unterschied zum Runden auf das Schlüsselbild: das muss auf + // der Unterschied zum Runden auf das Keyframe: das muss auf // die Millisekunde stimmen, sonst wird es schlimmer als vorher. // // Beim Umkodieren nicht: dort wird das Bild neu gerechnet und @@ -222,7 +222,7 @@ public static class HlsArgumente /// /// Sechs Nachkommastellen, nicht drei. Auf drei gerundet kann ein /// Sprungpunkt um eine Millisekunde zu klein werden, und eine Millisekunde - /// entscheidet, ob ffmpeg das gemeinte Schlüsselbild erwischt oder das + /// entscheidet, ob ffmpeg das gemeinte Keyframe erwischt oder das /// davor. Gemessen: bei 15,015 begann der Ton nach 0,004 s, bei 15,014 /// nach 4,98 s. /// @@ -286,7 +286,7 @@ public static class HlsArgumente "-level", "4.1", "-pix_fmt", "yuv420p", - // Feste Schlüsselbildabstände, damit jedes Segment gleich lang wird. + // Feste Keyframeabstände, damit jedes Segment gleich lang wird. "-g", schluesselabstand, "-keyint_min", schluesselabstand, "-sc_threshold", "0", diff --git a/server/src/Flaro.Media/Kapitelleser.cs b/server/src/Flaro.Media/Kapitelleser.cs index bbb9cba..ed681dd 100644 --- a/server/src/Flaro.Media/Kapitelleser.cs +++ b/server/src/Flaro.Media/Kapitelleser.cs @@ -7,7 +7,7 @@ using Flaro.Core.Media; namespace Flaro.Media; -/// Liest die Kapitelmarken einer Datei. +/// Liest die Kapitelsegmente einer Datei. /// /// /// @@ -25,7 +25,7 @@ namespace Flaro.Media; /// /// /// Billig. ffprobe liest dafür nur den Kopf der Datei, kein Bild. -/// Millisekunden gegen zwanzig Minuten Schlüsselbilder. +/// Millisekunden gegen zwanzig Minuten Keyframes. /// /// public sealed class Kapitelleser(FfmpegOptions optionen, ILogger log) @@ -75,7 +75,7 @@ public sealed class Kapitelleser(FfmpegOptions optionen, ILogger l /// /// /// Getrennt vom Aufruf, damit es sich ohne ffprobe prüfen lässt — wie - /// bei Bildabtaster.Lesen und Tonabdruckleser.Lesen. + /// bei Frameabtaster.Lesen und Fingerprintleser.Lesen. /// public static IReadOnlyList Lesen(string json) { @@ -96,8 +96,8 @@ public sealed class Kapitelleser(FfmpegOptions optionen, ILogger l if (!Zeit(eintrag, "start_time", out var von)) continue; if (!Zeit(eintrag, "end_time", out var bis)) bis = von; - var titel = eintrag.TryGetProperty("tags", out var marken) - && marken.TryGetProperty("title", out var name) + var titel = eintrag.TryGetProperty("tags", out var segmente) + && segmente.TryGetProperty("title", out var name) ? name.GetString() ?? "" : ""; diff --git a/server/src/Flaro.Media/Schluesselbilder.cs b/server/src/Flaro.Media/Keyframes.cs similarity index 84% rename from server/src/Flaro.Media/Schluesselbilder.cs rename to server/src/Flaro.Media/Keyframes.cs index 0193e3d..c0fe108 100644 --- a/server/src/Flaro.Media/Schluesselbilder.cs +++ b/server/src/Flaro.Media/Keyframes.cs @@ -4,18 +4,18 @@ using Microsoft.Extensions.Logging; namespace Flaro.Media; -/// Findet das Schlüsselbild, bei dem ein Strom wirklich anfängt. +/// Findet das Keyframe, bei dem ein Strom wirklich anfängt. /// /// /// /// Warum das überhaupt nötig ist. Beim Umpacken wird das Bild Byte für /// Byte übernommen, und ein kopierter Strom lässt sich nicht mitten in einer /// Bildgruppe aufschneiden. Wer bei Sekunde 754 einsteigt, bekommt deshalb -/// Bild ab dem Schlüsselbild davor — der Ton dagegen wird neu gerechnet und +/// Bild ab dem Keyframe davor — der Ton dagegen wird neu gerechnet und /// fängt genau bei 754 an. /// /// -/// Gemessen, nicht vermutet. An einer Quelle mit Schlüsselbildern alle +/// Gemessen, nicht vermutet. An einer Quelle mit Keyframesn alle /// fünf Sekunden, Sprung auf Sekunde 12: das Bild begann bei 0, der Ton erst /// bei 1,98 — zwei Sekunden stummes Bild am Anfang. Aneinander ausgerichtet /// waren beide dabei richtig; falsch war, was der Server dem Spieler über den @@ -24,13 +24,13 @@ namespace Flaro.Media; /// /// /// Also wird gerundet statt geraten. Der Sprung geht auf das -/// Schlüsselbild, und derselbe Wert geht an den Spieler zurück. Dann fangen +/// Keyframe, und derselbe Wert geht an den Spieler zurück. Dann fangen /// Bild und Ton an derselben Stelle an, es gibt keine Stille davor, und die /// Uhr stimmt. Dass man ein paar Sekunden früher einsteigt als gewünscht, /// fällt beim Weiterschauen niemandem auf — ein Tonversatz schon. /// /// -public sealed class Schluesselbilder(FfmpegOptions optionen, ILogger log) +public sealed class Keyframes(FfmpegOptions optionen, ILogger log) { /// Wie weit vor dem Wunsch gesucht wird. /// @@ -46,14 +46,14 @@ public sealed class Schluesselbilder(FfmpegOptions optionen, ILogger /// - /// Ohne diesen Nachlauf fehlte das letzte Schlüsselbild. Hier + /// Ohne diesen Nachlauf fehlte das letzte Keyframe. Hier /// stand eine halbe Sekunde, und ffprobe hörte damit regelmässig eine /// ganze Bildgruppe zu früh auf: -read_intervals bricht am Ende /// des Fensters auf Paketebene ab, und das letzte Bild darin kam gar /// nicht mehr heraus. /// /// - /// Nachgemessen an einer Datei mit Schlüsselbildern alle vier Sekunden, + /// Nachgemessen an einer Datei mit Keyframesn alle vier Sekunden, /// gegen die Stelle, an der ffmpeg tatsächlich anfängt: /// /// @@ -72,7 +72,7 @@ public sealed class Schluesselbilder(FfmpegOptions optionen, ILogger private const double Nachlauf = 10; - /// Das Schlüsselbild bei oder vor . + /// Das Keyframe bei oder vor . public async Task DavorAsync(string pfad, double ab, CancellationToken ct = default) { if (ab <= 0) return 0; @@ -86,7 +86,7 @@ public sealed class Schluesselbilder(FfmpegOptions optionen, ILoggerEin Tippfehler im Namen darf den Link nicht verbrennen. diff --git a/server/tests/Flaro.Api.Tests/FlaroServer.cs b/server/tests/Flaro.Api.Tests/FlaroServer.cs index adac444..2c66604 100644 --- a/server/tests/Flaro.Api.Tests/FlaroServer.cs +++ b/server/tests/Flaro.Api.Tests/FlaroServer.cs @@ -30,8 +30,8 @@ public sealed class FlaroServer : IDisposable /// /// /// Sparsam benutzen. Ein Test, der am Server vorbei schreibt, - /// prüft nicht mehr, ob der Server es könnte. Für Marken ist es nötig: - /// sie entstehen aus einem Tonabgleich, und der braucht Mediendateien + /// prüft nicht mehr, ob der Server es könnte. Für Segmente ist es nötig: + /// sie entstehen aus einem Fingerprintabgleich, und der braucht Mediendateien /// mit echtem Ton, die es hier nicht gibt. /// public async Task InDerDatenbank(Func tun) diff --git a/server/tests/Flaro.Api.Tests/MarkenTests.cs b/server/tests/Flaro.Api.Tests/SegmentTests.cs similarity index 83% rename from server/tests/Flaro.Api.Tests/MarkenTests.cs rename to server/tests/Flaro.Api.Tests/SegmentTests.cs index 10fdca5..4e3b0f2 100644 --- a/server/tests/Flaro.Api.Tests/MarkenTests.cs +++ b/server/tests/Flaro.Api.Tests/SegmentTests.cs @@ -9,12 +9,12 @@ namespace Flaro.Api.Tests; /// Vorspann und Abspann auf dem Weg zu den Spielern. /// /// -/// Ohne Tonabgleich. Ob er findet, was er finden soll, steht in -/// TonabgleichTests — mit erfundenen Abdrücken und ohne Mediendateien. -/// Hier geht es um das Stück danach: dass eine gefundene Marke auch bei dem +/// Ohne Fingerprintabgleich. Ob er findet, was er finden soll, steht in +/// FingerprintabgleichTests — mit erfundenen Abdrücken und ohne Mediendateien. +/// Hier geht es um das Stück danach: dass eine gefundene Segment auch bei dem /// ankommt, der den Knopf zeichnen soll. /// -public class MarkenTests +public class SegmentTests { private string _bestand = ""; @@ -67,7 +67,7 @@ public class MarkenTests } [Test] - public async Task Eine_Marke_kommt_bei_der_Folge_an() + public async Task Eine_Segment_kommt_bei_der_Folge_an() { Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv"); @@ -77,14 +77,14 @@ public class MarkenTests await server.InDerDatenbank(async db => { - db.Marken.Add(new Marke + db.Segmente.Add(new Segment { - EpisodeId = folge, Art = Markenart.Intro, + EpisodeId = folge, Art = Segmentart.Intro, VonSekunden = 30, BisSekunden = 65, ErkanntAm = DateTimeOffset.UtcNow, }); - db.Marken.Add(new Marke + db.Segmente.Add(new Segment { - EpisodeId = folge, Art = Markenart.Abspann, + EpisodeId = folge, Art = Segmentart.Abspann, VonSekunden = 2500, BisSekunden = 2600, ErkanntAm = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); @@ -105,7 +105,7 @@ public class MarkenTests /// Auch in der Folgenliste — dort holt die App sie beim Blättern. [Test] - public async Task Die_Folgenliste_bringt_die_Marken_mit() + public async Task Die_Folgenliste_bringt_die_Segmente_mit() { Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv"); Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv"); @@ -116,9 +116,9 @@ public class MarkenTests await server.InDerDatenbank(async db => { - db.Marken.Add(new Marke + db.Segmente.Add(new Segment { - EpisodeId = folge, Art = Markenart.Intro, + EpisodeId = folge, Art = Segmentart.Intro, VonSekunden = 10, BisSekunden = 40, ErkanntAm = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); @@ -131,11 +131,11 @@ public class MarkenTests { Assert.That(liste!.Items.Single(f => f.Id == folge).Markers, Has.Count.EqualTo(1)); Assert.That(liste.Items.Single(f => f.Id != folge).Markers, Is.Empty, - "eine Folge ohne Marke bekommt eine leere Liste, nicht die der anderen"); + "eine Folge ohne Segment bekommt eine leere Liste, nicht die der anderen"); }); } - /// Ohne erkannte Marken bleibt die Liste leer — kein Knopf, kein Fehler. + /// Ohne erkannte Segmente bleibt die Liste leer — kein Knopf, kein Fehler. [Test] public async Task Ohne_Erkennung_ist_die_Liste_leer() { @@ -196,11 +196,11 @@ public class MarkenTests (await client.PostAsync($"/api/v1/series/{serie}/marken", null)).EnsureSuccessStatusCode(); // Der Lauf hängt im Hintergrund; ohne fpcalc ist er sofort durch. - List vermerke = []; + List vermerke = []; for (var versuch = 0; versuch < 40; versuch++) { await server.InDerDatenbank(async db => - vermerke = await db.Markensuchen.AsNoTracking() + vermerke = await db.Segmentsuchen.AsNoTracking() .Where(x => x.SeriesId == serie).ToListAsync()); if (vermerke.Count > 0) break; await Task.Delay(50); @@ -217,9 +217,9 @@ public class MarkenTests } } - /// Ein Film trägt seine Abspannmarke genauso. + /// Ein Film trägt seine Abspannsegment genauso. [Test] - public async Task Ein_Film_bekommt_seine_Marke() + public async Task Ein_Film_bekommt_seine_Segment() { Datei("Matrix (1999)/Matrix (1999).mkv"); @@ -230,9 +230,9 @@ public class MarkenTests await server.InDerDatenbank(async db => { - db.Marken.Add(new Marke + db.Segmente.Add(new Segment { - MovieId = film, Art = Markenart.Abspann, + MovieId = film, Art = Segmentart.Abspann, VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); @@ -249,7 +249,7 @@ public class MarkenTests }); } - /// Eine falsche Marke lässt sich wieder wegnehmen. + /// Eine falsches Segment lässt sich wieder wegnehmen. /// /// /// Ohne diesen Weg wäre die Erkennung eine Blackbox, die manchmal recht @@ -257,7 +257,7 @@ public class MarkenTests /// tun ausser die Datenbank anzufassen. /// [Test] - public async Task Eine_falsche_Filmmarke_laesst_sich_verwerfen() + public async Task Eine_falsche_Filmsegment_laesst_sich_verwerfen() { Datei("Matrix (1999)/Matrix (1999).mkv"); @@ -268,14 +268,14 @@ public class MarkenTests await server.InDerDatenbank(async db => { - db.Marken.Add(new Marke + db.Segmente.Add(new Segment { - MovieId = film, Art = Markenart.Abspann, + MovieId = film, Art = Segmentart.Abspann, VonSekunden = 60, BisSekunden = 7200, ErkanntAm = DateTimeOffset.UtcNow, }); - db.Markensuchen.Add(new Markensuche + db.Segmentsuchen.Add(new Segmentsuche { - MovieId = film, Art = Markenart.Abspann, Folgen = 1, + MovieId = film, Art = Segmentart.Abspann, Folgen = 1, GelaufenAm = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); @@ -286,14 +286,14 @@ public class MarkenTests var vermerke = 0; await server.InDerDatenbank(async db => - vermerke = await db.Markensuchen.CountAsync(x => x.MovieId == film)); + vermerke = await db.Segmentsuchen.CountAsync(x => x.MovieId == film)); Assert.Multiple(() => { Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); Assert.That(danach!.Markers, Is.Empty); Assert.That(vermerke, Is.EqualTo(1), - "der Vermerk bleibt, sonst legte der nächste Scan dieselbe Marke wieder an"); + "der Vermerk bleibt, sonst legte der nächste Scan dieselbe Segment wieder an"); }); } @@ -301,9 +301,9 @@ public class MarkenTests /// /// /// - /// „Schon durchsucht" und „hat eine Marke" sind zwei verschiedene + /// „Schon durchsucht" und „hat ein Segment" sind zwei verschiedene /// Dinge. Ein Film, bei dem die Suche einmal nichts fand, trägt - /// einen Vermerk und keine Marke — und war damit für jeden weiteren + /// einen Vermerk und kein Segment — und war damit für jeden weiteren /// Lauf erledigt, obwohl er der interessanteste Fall ist. /// /// @@ -313,9 +313,9 @@ public class MarkenTests /// /// [TestCase("Neue", false, TestName = "Neue überspringt, was schon durchsucht ist")] - [TestCase("Luecken", true, TestName = "Luecken nimmt sich den Film ohne Marke vor")] + [TestCase("Luecken", true, TestName = "Luecken nimmt sich den Film ohne Segment vor")] [TestCase("Alles", true, TestName = "Alles nimmt sich alles vor")] - public async Task Der_Umfang_entscheidet_bei_einem_Film_ohne_Marke( + public async Task Der_Umfang_entscheidet_bei_einem_Film_ohne_Segment( string umfang, bool erwartetAngefasst) { Datei("Matrix (1999)/Matrix (1999).mkv"); @@ -325,7 +325,7 @@ public class MarkenTests var filme = await client.GetFromJsonAsync>("/api/v1/movies"); var film = filme!.Items[0].Id; - // Gesucht wurde schon, gefunden nichts: Vermerk ja, Marke nein. + // Gesucht wurde schon, gefunden nichts: Vermerk ja, Segment nein. var vorher = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); await server.InDerDatenbank(async db => { @@ -334,9 +334,9 @@ public class MarkenTests var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film); datei.Duration = TimeSpan.FromMinutes(120); - db.Markensuchen.Add(new Markensuche + db.Segmentsuchen.Add(new Segmentsuche { - MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher, + MovieId = film, Art = Segmentart.Abspann, Folgen = 1, GelaufenAm = vorher, }); await db.SaveChangesAsync(); }); @@ -348,7 +348,7 @@ public class MarkenTests for (var versuch = 0; versuch < 40; versuch++) { await server.InDerDatenbank(async db => - gelaufen = await db.Markensuchen.AsNoTracking() + gelaufen = await db.Segmentsuchen.AsNoTracking() .Where(x => x.MovieId == film).Select(x => x.GelaufenAm).FirstAsync()); if (gelaufen != vorher) break; await Task.Delay(50); @@ -360,9 +360,9 @@ public class MarkenTests : "dieser Umfang muss ihn überspringen"); } - /// Ein Film mit Marke bleibt bei „nur Lücken" unangetastet. + /// Ein Film mit Segment bleibt bei „nur Lücken" unangetastet. [Test] - public async Task Luecken_laesst_einen_Film_mit_Marke_in_Ruhe() + public async Task Luecken_laesst_einen_Film_mit_Segment_in_Ruhe() { Datei("Matrix (1999)/Matrix (1999).mkv"); @@ -377,14 +377,14 @@ public class MarkenTests var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film); datei.Duration = TimeSpan.FromMinutes(120); - db.Marken.Add(new Marke + db.Segmente.Add(new Segment { - MovieId = film, Art = Markenart.Abspann, + MovieId = film, Art = Segmentart.Abspann, VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = vorher, }); - db.Markensuchen.Add(new Markensuche + db.Segmentsuchen.Add(new Segmentsuche { - MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher, + MovieId = film, Art = Segmentart.Abspann, Folgen = 1, GelaufenAm = vorher, }); await db.SaveChangesAsync(); }); @@ -395,10 +395,10 @@ public class MarkenTests var gelaufen = DateTimeOffset.MinValue; await server.InDerDatenbank(async db => - gelaufen = await db.Markensuchen.AsNoTracking() + gelaufen = await db.Segmentsuchen.AsNoTracking() .Where(x => x.MovieId == film).Select(x => x.GelaufenAm).FirstAsync()); Assert.That(gelaufen, Is.EqualTo(vorher), - "wo schon eine Marke steht, ist nichts zu füllen"); + "wo schon ein Segment steht, ist nichts zu füllen"); } } diff --git a/server/tests/Flaro.Api.Tests/StreamTests.cs b/server/tests/Flaro.Api.Tests/StreamTests.cs index 8ff9f8f..4a31eef 100644 --- a/server/tests/Flaro.Api.Tests/StreamTests.cs +++ b/server/tests/Flaro.Api.Tests/StreamTests.cs @@ -333,12 +333,12 @@ public class StreamTests /// Strom fängt bei null an, der Film bei Minute 30. /// /// - /// Und „echt" heisst: beim Schlüsselbild. Dieser Test stand vorher + /// Und „echt" heisst: beim Keyframe. Dieser Test stand vorher /// auf „genau zwei Sekunden" und hielt damit einen Fehler fest. Ein /// kopierter Bildstrom lässt sich nicht mitten in einer Bildgruppe - /// aufschneiden: der Strom fängt beim Schlüsselbild davor an, und wer + /// aufschneiden: der Strom fängt beim Keyframe davor an, und wer /// trotzdem „zwei" zurückmeldet, verschiebt die Uhr des Spielers um den - /// Unterschied. Diese Testdatei hat genau ein Schlüsselbild, bei null — + /// Unterschied. Diese Testdatei hat genau ein Keyframe, bei null — /// also ist null die richtige Antwort. /// /// @@ -353,10 +353,10 @@ public class StreamTests Assert.That(strom.StartSeconds, Is.LessThanOrEqualTo(2), "Weiter als gewünscht darf der Strom nie anfangen — das überspränge Film."); Assert.That(strom.StartSeconds, Is.EqualTo(0), - "Diese Datei hat nur ein Schlüsselbild, bei null."); + "Diese Datei hat nur ein Keyframe, bei null."); } - /// Schliessen räumt auf — sonst füllt sich der Kratzplatz. + /// Schliessen räumt auf — sonst füllt sich der Transcode-Cache. [Test] public async Task Schliessen_raeumt_die_Sitzung_weg() { @@ -501,7 +501,7 @@ public class StreamTests /// /// Verglichen wird mit dem gemeldeten Vorlauf, nicht mit dem /// gewünschten — und das ist keine Bequemlichkeit. Beim Kopieren - /// beginnt ffmpeg am Schlüsselbild vor dem Wunsch; in einem Prüfclip von + /// beginnt ffmpeg am Keyframe vor dem Wunsch; in einem Prüfclip von /// vier Sekunden ist das die Null. Der Untertitel muss zu dem passen, was /// wirklich läuft. /// diff --git a/server/tests/Flaro.Api.Tests/KratzplatzTests.cs b/server/tests/Flaro.Api.Tests/TranscodeTests.cs similarity index 94% rename from server/tests/Flaro.Api.Tests/KratzplatzTests.cs rename to server/tests/Flaro.Api.Tests/TranscodeTests.cs index ce93bd6..4a5ba60 100644 --- a/server/tests/Flaro.Api.Tests/KratzplatzTests.cs +++ b/server/tests/Flaro.Api.Tests/TranscodeTests.cs @@ -6,7 +6,7 @@ using Flaro.Core.Delivery; namespace Flaro.Api.Tests; -/// Was der Server auf dem Kratzplatz wegräumt — und was nicht. +/// Was der Server auf dem Transcode-Cache wegräumt — und was nicht. /// /// /// @@ -17,13 +17,13 @@ namespace Flaro.Api.Tests; /// /// Hier wird die Verdrahtung geprüft, nicht die Regel — die steht /// ohne Platte und ohne Server in -/// Flaro.Core.Tests/KratzplatzregelTests. Was hier zählt: findet der +/// Flaro.Core.Tests/TranscoderegelTests. Was hier zählt: findet der /// Aufräumer die Dateien, erkennt er, was zur Mediathek gehört, und lässt /// er das Gültige stehen. /// /// [TestFixture] -public sealed class KratzplatzTests +public sealed class TranscodeTests { private string _bestand = ""; @@ -41,8 +41,8 @@ public sealed class KratzplatzTests try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { /* Kür */ } } - /// Ein Server mit einem eingelesenen Film und bekanntem Kratzplatz. - private async Task<(HttpClient Client, string Kratzplatz, Guid DateiId)> Aufgesetzt(FlaroServer server) + /// Ein Server mit einem eingelesenen Film und bekanntem Transcode-Cache. + private async Task<(HttpClient Client, string Ordner, Guid DateiId)> Aufgesetzt(FlaroServer server) { var voll = Path.Combine(_bestand, "Probe (2026)", "Probe (2026).mkv"); Directory.CreateDirectory(Path.GetDirectoryName(voll)!); @@ -244,18 +244,18 @@ public sealed class KratzplatzTests Paketname.Bauen(dateiId, Gestempelt, PackageCoordinator.Bauart), kilobyte: 8); var orte = await client.GetFromJsonAsync>("/api/v1/system/storage"); - var kratzplatz = orte!.First(o => o.Variable == "FLARO_TRANSCODE"); + var transcode = orte!.First(o => o.Variable == "FLARO_TRANSCODE"); Assert.Multiple(() => { - Assert.That(kratzplatz.Teile, Is.Not.Null); - Assert.That(kratzplatz.Teile!.Select(t => t.Name), + Assert.That(transcode.Teile, Is.Not.Null); + Assert.That(transcode.Teile!.Select(t => t.Name), Is.EquivalentTo(new[] { "stroeme", "packages", "untertitel" })); - var pakete = kratzplatz.Teile!.First(t => t.Name == "packages"); + var pakete = transcode.Teile!.First(t => t.Name == "packages"); Assert.That(pakete.Dateien, Is.EqualTo(1)); Assert.That(pakete.Frei, Is.EqualTo(8 * 1024), "das Feld Frei trägt hier die Belegung des Ordners"); Assert.That(orte!.First(o => o.Variable == "FLARO_DATA").Teile, Is.Null, - "nur der Kratzplatz wird aufgeschlüsselt"); + "nur der Transcode-Cache wird aufgeschlüsselt"); }); } diff --git a/server/tests/Flaro.Api.Tests/WartungsplanTests.cs b/server/tests/Flaro.Api.Tests/WartungsplanTests.cs index bfc94ce..c150f85 100644 --- a/server/tests/Flaro.Api.Tests/WartungsplanTests.cs +++ b/server/tests/Flaro.Api.Tests/WartungsplanTests.cs @@ -261,7 +261,7 @@ public sealed class WartungsplanTests Assert.That(nachher!.BackupDays, Is.EqualTo(1)); Assert.That(nachher.BackupKeep, Is.EqualTo(10)); Assert.That(nachher.OptimizeDays, Is.EqualTo(7), "was nicht geschickt wurde, bleibt"); - Assert.That(nachher.CapGb, Is.EqualTo(20).Within(0.01), "und der Kratzplatz auch"); + Assert.That(nachher.CapGb, Is.EqualTo(20).Within(0.01), "und der Transcode-Cache auch"); }); } } diff --git a/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs b/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs index b2775b5..fc477eb 100644 --- a/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs +++ b/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs @@ -28,11 +28,11 @@ public class AbspannkanteTests /// Eine Reihe von Messpunkten im Abstand von zehn Sekunden. /// /// Ab wann dunkel. null heisst: nie. - private static List Reihe(double von, double bis, double? abspannAb) + private static List Reihe(double von, double bis, double? abspannAb) { - var proben = new List(); + var proben = new List(); for (var t = von; t <= bis; t += 10) - proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0)); + proben.Add(new Frameprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0)); return proben; } @@ -71,7 +71,7 @@ public class AbspannkanteTests /// Eine Szene nach dem Abspann nimmt den Fund weg. /// /// - /// Lieber keine Marke als eine, die über die Szene hinwegspringt. + /// Lieber kein Segment als eine, die über die Szene hinwegspringt. /// [Test] public void Eine_Szene_nach_dem_Abspann_nimmt_den_Fund_weg() @@ -223,14 +223,14 @@ public class AbspannAnteilTests /// /// /// Zwanzig Minuten vor dem Ende einer Folge von fünfundvierzig, an den - /// Schlüsselbildern abgetastet — also alle fünf Sekunden eine Probe. + /// Keyframesn abgetastet — also alle fünf Sekunden eine Probe. /// - private static List Fenster( + private static List Fenster( double abspannAb, double imAbspann, double imInhalt = 0) { - var proben = new List(); + var proben = new List(); for (var t = 1500.0; t <= 2700; t += 5) - proben.Add(new Bildprobe(t, t < abspannAb ? imInhalt : imAbspann)); + proben.Add(new Frameprobe(t, t < abspannAb ? imInhalt : imAbspann)); return proben; } @@ -300,13 +300,13 @@ public class AbspannSchlusstafelTests private static double? Anfang(IReadOnlyList stücke) => stücke.Count == 0 ? null : stücke[0].Von; - private static List Fenster(double abspannAb, double tafelAb = double.MaxValue) + private static List Fenster(double abspannAb, double tafelAb = double.MaxValue) { - var proben = new List(); + var proben = new List(); for (var t = 1500.0; t <= 2700; t += 5) { var dunkel = t >= tafelAb ? 0 : t < abspannAb ? 0 : 85; - proben.Add(new Bildprobe(t, dunkel)); + proben.Add(new Frameprobe(t, dunkel)); } return proben; } @@ -336,20 +336,20 @@ public class AbspannSchlusstafelTests } } -/// Dieselbe Szene, zweierlei Schlüsselbilddichte. +/// Dieselbe Szene, zweierlei Keyframedichte. /// /// /// /// Der Fehler, für den diese Prüfungen stehen. Die Nachsicht /// gegenüber hellen Stellen mitten im Abspann war in Proben /// angegeben, nicht in Sekunden. Was das bedeutet, hängt allein daran, -/// wie dicht eine Datei ihre Schlüsselbilder setzt — und das schwankt um +/// wie dicht eine Datei ihre Keyframes setzt — und das schwankt um /// eine Zehnerpotenz. /// /// /// Gemessen am Bestand: „(T)Raumschiff Surprise" liefert 1318 Proben auf /// zwanzig Minuten, also eine alle 0,9 Sekunden. Zwei Proben Nachsicht -/// waren dort 1,8 Sekunden; bei einer Datei mit einem Schlüsselbild alle +/// waren dort 1,8 Sekunden; bei einer Datei mit einem Keyframe alle /// zehn Sekunden waren dieselben zwei Proben zwanzig Sekunden. Ein /// fünfsekündiges Logo mitten im Abspann zerriss die Strecke deshalb nur /// bei der dicht abgetasteten Datei — und genau die fiel durch. @@ -370,13 +370,13 @@ public class AbspannNachsichtTests stücke.Count == 0 ? null : stücke[0].Von; /// Abspann ab 2340 s, darin fünf Sekunden helles Logo. - private static List Fenster(double takt) + private static List Fenster(double takt) { - var proben = new List(); + var proben = new List(); for (var t = 1500.0; t <= 2700; t += takt) { var imLogo = t >= 2500 && t < 2505; - proben.Add(new Bildprobe(t, t < 2340 ? 0 : imLogo ? 5 : 85)); + proben.Add(new Frameprobe(t, t < 2340 ? 0 : imLogo ? 5 : 85)); } return proben; } @@ -408,12 +408,12 @@ public class AbspannNachsichtTests [Test] public void Eine_Minute_hell_mittendrin_teilt_den_Abspann_in_zwei_Stuecke() { - var proben = new List(); + var proben = new List(); for (var t = 1500.0; t <= 2700; t += 1) { // Dunkel ab 2340, aber von 2400 bis 2460 eine Minute hell. var hell = t < 2340 || (t >= 2400 && t < 2460); - proben.Add(new Bildprobe(t, hell ? 0 : 85)); + proben.Add(new Frameprobe(t, hell ? 0 : 85)); } var stücke = Abspannkante.Suchen(proben, Regeln); @@ -433,15 +433,15 @@ public class AbspannNachsichtTests [Test] public void Der_Grund_wird_benannt() { - var hell = new List(); - for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Bildprobe(t, 0)); + var hell = new List(); + for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Frameprobe(t, 0)); Abspannkante.Suchen(hell, Regeln, out var grund); Assert.That(grund, Does.Contain("keine dunkle Strecke")); - var kurz = new List(); + var kurz = new List(); for (var t = 1500.0; t <= 2700; t += 5) - kurz.Add(new Bildprobe(t, t < 2680 ? 0 : 85)); + kurz.Add(new Frameprobe(t, t < 2680 ? 0 : 85)); Abspannkante.Suchen(kurz, Regeln, out var zuKurz); Assert.That(zuKurz, Does.Contain("zusammen nur")); @@ -469,13 +469,13 @@ public class AbspannstueckeTests private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe; /// Eine Reihe im Sekundentakt, dunkel in den angegebenen Spannen. - private static List Reihe(double bis, params (double Von, double Bis)[] dunkel) + private static List Reihe(double bis, params (double Von, double Bis)[] dunkel) { - var proben = new List(); + var proben = new List(); for (var t = 0.0; t <= bis; t += 1) { var istDunkel = Array.Exists(dunkel, d => t >= d.Von && t < d.Bis); - proben.Add(new Bildprobe(t, istDunkel ? 85 : 0)); + proben.Add(new Frameprobe(t, istDunkel ? 85 : 0)); } return proben; } diff --git a/server/tests/Flaro.Core.Tests/AuthCodeTests.cs b/server/tests/Flaro.Core.Tests/AuthCodeTests.cs index bde65c1..b59d140 100644 --- a/server/tests/Flaro.Core.Tests/AuthCodeTests.cs +++ b/server/tests/Flaro.Core.Tests/AuthCodeTests.cs @@ -22,7 +22,7 @@ public class SecretsTests } [Test] - public void Der_Fingerabdruck_ist_gleichbleibend_und_unterscheidet() + public void Der_Fingerfingerprint_ist_gleichbleibend_und_unterscheidet() { var a = Secrets.Create(); var b = Secrets.Create(); @@ -35,9 +35,9 @@ public class SecretsTests Assert.That(ersterLauf, Has.Length.EqualTo(64)); } - /// Der Fingerabdruck darf das Geheimnis nicht enthalten. + /// Der Fingerfingerprint darf das Geheimnis nicht enthalten. [Test] - public void Der_Fingerabdruck_verraet_das_Geheimnis_nicht() + public void Der_Fingerfingerprint_verraet_das_Geheimnis_nicht() { var s = Secrets.Create(); Assert.That(Secrets.Fingerprint(s), Does.Not.Contain(s)); diff --git a/server/tests/Flaro.Core.Tests/EchteAbdrueckeTests.cs b/server/tests/Flaro.Core.Tests/EchteFingerprintsTests.cs similarity index 90% rename from server/tests/Flaro.Core.Tests/EchteAbdrueckeTests.cs rename to server/tests/Flaro.Core.Tests/EchteFingerprintsTests.cs index dff193b..f3135ef 100644 --- a/server/tests/Flaro.Core.Tests/EchteAbdrueckeTests.cs +++ b/server/tests/Flaro.Core.Tests/EchteFingerprintsTests.cs @@ -16,13 +16,13 @@ namespace Flaro.Core.Tests; /// verschiedener Bitrate, durch fpcalc gedreht. Zwanzig Kilobyte /// Text — der Preis dafür, dass diese Zusage nicht bloss behauptet ist. /// -public class EchteAbdrueckeTests +public class EchteFingerprintsTests { - private const double JeAbdruck = 4096.0 / 3.0 / 11025.0; + private const double JeFingerprint = 4096.0 / 3.0 / 11025.0; private static IReadOnlyList? Lesen(string name) { - var pfad = Path.Combine(TestContext.CurrentContext.TestDirectory, "Abdruecke", name); + var pfad = Path.Combine(TestContext.CurrentContext.TestDirectory, "Fingerprints", name); if (!File.Exists(pfad)) return null; return File.ReadAllText(pfad) @@ -38,7 +38,7 @@ public class EchteAbdrueckeTests var b = Lesen("folgeB.txt"); if (a is null || b is null) Assert.Ignore("Keine Abdrücke zum Prüfen da."); - var treffer = Tonabgleich.Finden(a, b, JeAbdruck); + var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint); TestContext.Out.WriteLine($"A {a.Count} Werte, B {b.Count} Werte"); TestContext.Out.WriteLine(treffer is null diff --git a/server/tests/Flaro.Core.Tests/TonabgleichTests.cs b/server/tests/Flaro.Core.Tests/FingerprintabgleichTests.cs similarity index 89% rename from server/tests/Flaro.Core.Tests/TonabgleichTests.cs rename to server/tests/Flaro.Core.Tests/FingerprintabgleichTests.cs index 37e4a9e..569e00d 100644 --- a/server/tests/Flaro.Core.Tests/TonabgleichTests.cs +++ b/server/tests/Flaro.Core.Tests/FingerprintabgleichTests.cs @@ -10,10 +10,10 @@ namespace Flaro.Core.Tests; /// rechnen kann. Eine Prüfung, die eine Serie mit echten Folgen braucht, /// liefe nirgends ausser auf dem Rechner dessen, der sie geschrieben hat. /// -public class TonabgleichTests +public class FingerprintabgleichTests { /// Chromaprint mit Standardeinstellungen. - private const double JeAbdruck = 0.1238; + private const double JeFingerprint = 0.1238; /// Eine Folge von Abdrücken, die sich nicht wiederholt. /// @@ -40,7 +40,7 @@ public class TonabgleichTests teile.SelectMany(t => t).ToArray(); /// Wie viele Zeitscheiben so viele Sekunden sind. - private static int Scheiben(double sekunden) => (int)Math.Round(sekunden / JeAbdruck); + private static int Scheiben(double sekunden) => (int)Math.Round(sekunden / JeFingerprint); [Test] public void Ein_gemeinsamer_Vorspann_wird_gefunden() @@ -52,7 +52,7 @@ public class TonabgleichTests // Folge 2: der Vorspann liegt viel später. var b = Aneinander(Rauschen(Scheiben(70), 4), vorspann, Rauschen(Scheiben(20), 5)); - var treffer = Tonabgleich.Finden(a, b, JeAbdruck); + var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint); Assert.That(treffer, Is.Not.Null); Assert.Multiple(() => @@ -77,7 +77,7 @@ public class TonabgleichTests var a = Rauschen(Scheiben(120), 11); var b = Rauschen(Scheiben(120), 12); - Assert.That(Tonabgleich.Finden(a, b, JeAbdruck), Is.Null); + Assert.That(Fingerprintabgleich.Finden(a, b, JeFingerprint), Is.Null); } /// Ein zu kurzes gemeinsames Stück zählt nicht. @@ -88,7 +88,7 @@ public class TonabgleichTests var a = Aneinander(Rauschen(Scheiben(30), 22), kurz, Rauschen(Scheiben(30), 23)); var b = Aneinander(Rauschen(Scheiben(50), 24), kurz, Rauschen(Scheiben(10), 25)); - Assert.That(Tonabgleich.Finden(a, b, JeAbdruck), Is.Null); + Assert.That(Fingerprintabgleich.Finden(a, b, JeFingerprint), Is.Null); } /// Ein paar abweichende Bits machen den Fund nicht kaputt. @@ -107,7 +107,7 @@ public class TonabgleichTests var a = Aneinander(Rauschen(Scheiben(5), 32), vorspann, Rauschen(Scheiben(40), 33)); var b = Aneinander(Rauschen(Scheiben(20), 34), leichtAnders, Rauschen(Scheiben(40), 35)); - var treffer = Tonabgleich.Finden(a, b, JeAbdruck); + var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint); Assert.That(treffer, Is.Not.Null); Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0)); @@ -126,7 +126,7 @@ public class TonabgleichTests var a = Aneinander(Rauschen(Scheiben(5), 42), vorspann, Rauschen(Scheiben(30), 43)); var b = Aneinander(Rauschen(Scheiben(15), 44), mitAnsage, Rauschen(Scheiben(30), 45)); - var treffer = Tonabgleich.Finden(a, b, JeAbdruck); + var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint); Assert.That(treffer, Is.Not.Null); Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0), @@ -145,7 +145,7 @@ public class TonabgleichTests Rauschen(Scheiben(5 + i * 10), 60 + i), vorspann, Rauschen(Scheiben(30), 70 + i)))); } - var gefunden = Tonabgleich.ReiheAbgleichen(folgen, JeAbdruck); + var gefunden = Fingerprintabgleich.ReiheAbgleichen(folgen, JeFingerprint); Assert.That(gefunden, Has.Count.EqualTo(5), "auch die letzte Folge"); for (var i = 0; i < 5; i++) @@ -160,6 +160,6 @@ public class TonabgleichTests public void Eine_Folge_allein_ergibt_nichts() { var eine = new List<(int, IReadOnlyList)> { (0, Rauschen(Scheiben(60), 81)) }; - Assert.That(Tonabgleich.ReiheAbgleichen(eine, JeAbdruck), Is.Empty); + Assert.That(Fingerprintabgleich.ReiheAbgleichen(eine, JeFingerprint), Is.Empty); } } diff --git a/server/tests/Flaro.Core.Tests/Abdruecke/folgeA.txt b/server/tests/Flaro.Core.Tests/Fingerprints/folgeA.txt similarity index 100% rename from server/tests/Flaro.Core.Tests/Abdruecke/folgeA.txt rename to server/tests/Flaro.Core.Tests/Fingerprints/folgeA.txt diff --git a/server/tests/Flaro.Core.Tests/Abdruecke/folgeB.txt b/server/tests/Flaro.Core.Tests/Fingerprints/folgeB.txt similarity index 100% rename from server/tests/Flaro.Core.Tests/Abdruecke/folgeB.txt rename to server/tests/Flaro.Core.Tests/Fingerprints/folgeB.txt diff --git a/server/tests/Flaro.Core.Tests/Flaro.Core.Tests.csproj b/server/tests/Flaro.Core.Tests/Flaro.Core.Tests.csproj index d8ca55e..df190ad 100644 --- a/server/tests/Flaro.Core.Tests/Flaro.Core.Tests.csproj +++ b/server/tests/Flaro.Core.Tests/Flaro.Core.Tests.csproj @@ -25,7 +25,7 @@ - + diff --git a/server/tests/Flaro.Core.Tests/ProgressRuleTests.cs b/server/tests/Flaro.Core.Tests/ProgressRuleTests.cs index da7756d..77b4e65 100644 --- a/server/tests/Flaro.Core.Tests/ProgressRuleTests.cs +++ b/server/tests/Flaro.Core.Tests/ProgressRuleTests.cs @@ -295,15 +295,15 @@ public sealed class ProgressRuleTests }); } - /// Eine Marke hinter der Schwelle verschiebt nichts nach hinten. + /// Ein Segment hinter der Schwelle verschiebt nichts nach hinten. /// /// - /// Sonst machte eine falsch erkannte Marke aus einem Titel, der bei + /// Sonst machte eine falsch erkannte Segment aus einem Titel, der bei /// neunzig Prozent fertig war, einen, der es erst bei fünfundneunzig /// ist — und niemand sähe, warum. /// [Test] - public void Eine_spaete_Marke_schiebt_nichts_nach_hinten() + public void Eine_spaete_Segment_schiebt_nichts_nach_hinten() { var laenge = TimeSpan.FromMinutes(45); var regeln = new Wiedergaberegeln { GesehenAnteil = 0.9 }; @@ -314,7 +314,7 @@ public sealed class ProgressRuleTests Assert.That(ergebnis.Watched, Is.True); } - /// Ohne Marke bleibt alles, wie es war. + /// Ohne Segment bleibt alles, wie es war. [Test] public void Ohne_Abspann_gilt_der_Anteil() { diff --git a/server/tests/Flaro.Core.Tests/ScanschrittTests.cs b/server/tests/Flaro.Core.Tests/ScanschrittTests.cs index 3c4427e..d698dcd 100644 --- a/server/tests/Flaro.Core.Tests/ScanschrittTests.cs +++ b/server/tests/Flaro.Core.Tests/ScanschrittTests.cs @@ -5,7 +5,7 @@ namespace Flaro.Core.Tests; /// Der Satz, der in der Verwaltung mitläuft. /// /// -/// Warum das geprüft wird. Die Markensuche läuft über Stunden und +/// Warum das geprüft wird. Die Segmentsuche läuft über Stunden und /// hat bisher gar nichts gemeldet — in der Verwaltung stand das Ergebnis /// des längst beendeten Scans, während der Server noch jede Folge /// durchhörte. Sie benutzt jetzt denselben Weg wie der Scan, und der Satz @@ -15,10 +15,10 @@ public class ScanschrittTests { /// Mit Zähler, wenn die Gesamtzahl bekannt ist. [Test] - public void Die_Markensuche_nennt_Titel_und_Zaehler() + public void Die_Segmentsuche_nennt_Titel_und_Zaehler() { var schritt = new Scanschritt( - Scanphase.Marken, "Biohackers S02E01 · Neuanfang", 12, 95); + Scanphase.Segmente, "Biohackers S02E01 · Neuanfang", 12, 95); Assert.That(schritt.Text(), Is.EqualTo("Suche Vorspann und Abspann: Biohackers S02E01 · Neuanfang — 12 von 95")); @@ -28,7 +28,7 @@ public class ScanschrittTests [Test] public void Ohne_Gesamtzahl_steht_nur_der_Titel() { - var schritt = new Scanschritt(Scanphase.Marken, "Arrival (2016)"); + var schritt = new Scanschritt(Scanphase.Segmente, "Arrival (2016)"); Assert.That(schritt.Text(), Is.EqualTo("Suche Vorspann und Abspann: Arrival (2016)")); } diff --git a/server/tests/Flaro.Core.Tests/KratzplatzregelTests.cs b/server/tests/Flaro.Core.Tests/TranscoderegelTests.cs similarity index 92% rename from server/tests/Flaro.Core.Tests/KratzplatzregelTests.cs rename to server/tests/Flaro.Core.Tests/TranscoderegelTests.cs index e635b02..7424892 100644 --- a/server/tests/Flaro.Core.Tests/KratzplatzregelTests.cs +++ b/server/tests/Flaro.Core.Tests/TranscoderegelTests.cs @@ -5,12 +5,12 @@ namespace Flaro.Core.Tests; /// Welche umgepackten Dateien weggeräumt werden. /// /// -/// Der Anlass: 39 Gigabyte Kratzplatz. Für jede Mitnahme baut der +/// Der Anlass: 39 Gigabyte Transcode-Cache. Für jede Mitnahme baut der /// Server eine MP4 in Quellgrösse und löschte sie nie. Hier steht die Regel, /// die das beendet — ohne Platte, ohne Server, damit die Entscheidung selbst /// nachlesbar ist und nicht nur ihr Ergebnis. /// -public class KratzplatzregelTests +public class TranscoderegelTests { private static readonly DateTimeOffset Jetzt = new(2026, 10, 8, 12, 0, 0, TimeSpan.Zero); private const int Bauart = 2; @@ -20,7 +20,7 @@ public class KratzplatzregelTests private const long Gigabyte = 1024L * 1024 * 1024; - /// Eine Datei, wie sie im Kratzplatz läge. + /// Eine Datei, wie sie im Transcode-Cache läge. private static Paketakte Akte( Guid id, long gigabyte, TimeSpan vorWieLange, long? geaendertMs = null, int bauart = Bauart) { @@ -36,12 +36,12 @@ public class KratzplatzregelTests private static IReadOnlyList Weg( IReadOnlyList akten, - Kratzplatzregeln? regeln = null, + Transcoderegeln? regeln = null, IReadOnlyDictionary? bekannt = null, Func? inBenutzung = null) => - Kratzplatzauswahl.Wegzuwerfen( + Transcodeauswahl.Wegzuwerfen( akten, bekannt ?? Bekannt, Bauart, - regeln ?? Kratzplatzregeln.Vorgabe, Jetzt, + regeln ?? Transcoderegeln.Vorgabe, Jetzt, inBenutzung ?? (_ => false)) .Select(a => a.Pfad).ToList(); @@ -161,7 +161,7 @@ public class KratzplatzregelTests [Test] public void Eine_Obergrenze_von_null_heisst_unbegrenzt() { - var regeln = Kratzplatzregeln.Vorgabe with { ObergrenzeBytes = 0 }; + var regeln = Transcoderegeln.Vorgabe with { ObergrenzeBytes = 0 }; var akten = new[] { Akte(EinsId, 500, TimeSpan.FromHours(2)), @@ -174,7 +174,7 @@ public class KratzplatzregelTests [Test] public void Ohne_Altersabbau_bleibt_auch_sehr_Altes_liegen() { - var regeln = Kratzplatzregeln.Vorgabe with { Unbenutzt = TimeSpan.Zero }; + var regeln = Transcoderegeln.Vorgabe with { Unbenutzt = TimeSpan.Zero }; Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromDays(400))], regeln), Is.Empty); } @@ -187,7 +187,7 @@ public class KratzplatzregelTests [Test] public void Verwaistes_geht_auch_bei_abgeschalteten_Grenzen() { - var regeln = new Kratzplatzregeln(0, TimeSpan.Zero, TimeSpan.FromMinutes(30)); + var regeln = new Transcoderegeln(0, TimeSpan.Zero, TimeSpan.FromMinutes(30)); Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromHours(2), bauart: 1)], regeln), Has.Count.EqualTo(1)); } @@ -195,7 +195,7 @@ public class KratzplatzregelTests [Test] public void Eine_zu_kurze_Schonfrist_wird_hochgesetzt() { - var regeln = Kratzplatzregeln.Geprueft(20, 7, 1); + var regeln = Transcoderegeln.Geprueft(20, 7, 1); Assert.That(regeln.Schonfrist, Is.EqualTo(TimeSpan.FromMinutes(5)), "eine Minute löschte die Datei mitten im Herunterladen weg"); @@ -204,7 +204,7 @@ public class KratzplatzregelTests [Test] public void Null_und_negative_Werte_heissen_abgeschaltet() { - var regeln = Kratzplatzregeln.Geprueft(0, -3, 30); + var regeln = Transcoderegeln.Geprueft(0, -3, 30); Assert.Multiple(() => { diff --git a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs b/server/tests/Flaro.Media.Tests/FingerprintleserTests.cs similarity index 61% rename from server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs rename to server/tests/Flaro.Media.Tests/FingerprintleserTests.cs index 258e12c..3d50dfe 100644 --- a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs +++ b/server/tests/Flaro.Media.Tests/FingerprintleserTests.cs @@ -8,41 +8,41 @@ namespace Flaro.Media.Tests; /// Ohne das Programm selbst. Ob Chromaprint rechnen kann, ist nicht /// unsere Frage; ob wir seine Ausgabe richtig verstehen, schon. Die Zahlen /// kommen mit Vorzeichen, gemeint sind die Bitmuster — wer das verwechselt, -/// bekommt einen Abdruck, der nie zu einem zweiten passt. +/// bekommt einen Fingerprint, der nie zu einem zweiten passt. /// -public class TonabdruckleserTests +public class FingerprintleserTests { [Test] public void Die_Zahlen_werden_als_Bitmuster_gelesen() { - var abdruck = Tonabdruckleser.Lesen("DURATION=120\nFINGERPRINT=1,-1,2147483647,-2147483648\n"); + var fingerprint = Fingerprintleser.Lesen("DURATION=120\nFINGERPRINT=1,-1,2147483647,-2147483648\n"); - Assert.That(abdruck, Is.EqualTo(new uint[] { 1u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0x80000000u })); + Assert.That(fingerprint, Is.EqualTo(new uint[] { 1u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0x80000000u })); } [Test] - public void Ohne_Abdruckzeile_kommt_nichts() + public void Ohne_Fingerprintzeile_kommt_nichts() { - Assert.That(Tonabdruckleser.Lesen("DURATION=120\n"), Is.Empty); + Assert.That(Fingerprintleser.Lesen("DURATION=120\n"), Is.Empty); } [Test] - public void Eine_leere_Zeile_ergibt_keinen_Abdruck() + public void Eine_leere_Zeile_ergibt_keinen_Fingerprint() { - Assert.That(Tonabdruckleser.Lesen("FINGERPRINT=\n"), Is.Empty); + Assert.That(Fingerprintleser.Lesen("FINGERPRINT=\n"), Is.Empty); } /// Eine Zeitscheibe ist rund 124 Millisekunden. /// /// - /// Daran hängt jede Zeitangabe, die aus einem Abdruck entsteht. Stimmt - /// die Zahl nicht, liegt jede Marke um denselben Faktor daneben — und + /// Daran hängt jede Zeitangabe, die aus einem Fingerprint entsteht. Stimmt + /// die Zahl nicht, liegt jedes Segment um denselben Faktor daneben — und /// das sähe aus wie ein Fehler in der Erkennung. /// [Test] public void Eine_Zeitscheibe_dauert_124_Millisekunden() { - Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(0.1238).Within(0.0005)); + Assert.That(Fingerprintleser.SekundenJeFingerprint, Is.EqualTo(0.1238).Within(0.0005)); } } @@ -59,27 +59,27 @@ public class TonabdruckleserTests /// Sekunden: -length 0 ergibt 21 786 Werte, also 0,1239 Sekunden je /// Wert; -length 120 bricht nach 948 Werten ab. /// -public class TonabdruckRateTests +public class FingerprintRateTests { [Test] public void Die_Rate_stimmt_mit_der_Messung_ueberein() { // 21 786 Werte für 2700 Sekunden. var gemessen = 2700.0 / 21786; - Assert.That(Math.Abs(gemessen - Tonabdruckleser.SekundenJeAbdruck), Is.LessThan(0.0005)); + Assert.That(Math.Abs(gemessen - Fingerprintleser.SekundenJeFingerprint), Is.LessThan(0.0005)); } - /// Eine Stunde Abdruck passt in eine überschaubare Reihe. + /// Eine Stunde Fingerprint passt in eine überschaubare Reihe. /// /// - /// Damit niemand auf die Idee kommt, den Abdruck einer ganzen Folge für + /// Damit niemand auf die Idee kommt, den Fingerprint einer ganzen Folge für /// zu gross zu halten und wieder in Ausschnitten zu lesen: eine Stunde /// sind knapp 29 000 Werte, also 116 Kilobyte. /// [Test] public void Eine_Stunde_sind_rund_29000_Werte() { - var werte = (int)(3600 / Tonabdruckleser.SekundenJeAbdruck); + var werte = (int)(3600 / Fingerprintleser.SekundenJeFingerprint); Assert.That(werte, Is.InRange(28500, 29500)); } } diff --git a/server/tests/Flaro.Media.Tests/BildabtasterTests.cs b/server/tests/Flaro.Media.Tests/FrameabtasterTests.cs similarity index 93% rename from server/tests/Flaro.Media.Tests/BildabtasterTests.cs rename to server/tests/Flaro.Media.Tests/FrameabtasterTests.cs index c0af0df..ccdc390 100644 --- a/server/tests/Flaro.Media.Tests/BildabtasterTests.cs +++ b/server/tests/Flaro.Media.Tests/FrameabtasterTests.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions; namespace Flaro.Media.Tests; /// Die Ausgabe des Abtasters lesen — ohne ffmpeg. -public class BildabtasterLesenTests +public class FrameabtasterLesenTests { [Test] public void Zeit_und_Wert_werden_zu_Proben() @@ -19,12 +19,12 @@ public class BildabtasterLesenTests """; - var proben = Bildabtaster.Lesen(ausgabe, versatz: 60); + var proben = Frameabtaster.Lesen(ausgabe, versatz: 60); Assert.That(proben, Is.EqualTo(new[] { - new Bildprobe(62.5, 3), - new Bildprobe(65, 84), + new Frameprobe(62.5, 3), + new Frameprobe(65, 84), })); } @@ -38,13 +38,13 @@ public class BildabtasterLesenTests [Test] public void Ein_Wert_ohne_Zeit_faellt_heraus() { - Assert.That(Bildabtaster.Lesen("lavfi.blackframe.pblack=84\n", 0), Is.Empty); + Assert.That(Frameabtaster.Lesen("lavfi.blackframe.pblack=84\n", 0), Is.Empty); } [Test] public void Leere_Ausgabe_ergibt_nichts() { - Assert.That(Bildabtaster.Lesen("", 0), Is.Empty); + Assert.That(Frameabtaster.Lesen("", 0), Is.Empty); } } @@ -63,7 +63,7 @@ public class BildabtasterLesenTests /// rotgemeldet — ein fehlendes Werkzeug ist kein Fehler im Quelltext. /// /// -public class BildabtasterLaufTests +public class FrameabtasterLaufTests { private string _ordner = ""; private string _datei = ""; @@ -117,8 +117,8 @@ public class BildabtasterLaufTests try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { } } - private static Bildabtaster Abtaster() => - new(FfmpegOptions.FromEnvironment(), NullLogger.Instance); + private static Frameabtaster Abtaster() => + new(FfmpegOptions.FromEnvironment(), NullLogger.Instance); /// Die Kante liegt da, wo sie erzeugt wurde: bei 90 Sekunden. [Test] @@ -198,7 +198,7 @@ public class BildabtasterLaufTests /// 450 gewesen. /// /// -public class BildabtasterBittiefeTests +public class FrameabtasterBittiefeTests { private string _ordner = ""; @@ -246,8 +246,8 @@ public class BildabtasterBittiefeTests [Test] public async Task Acht_und_zehn_Bit_ergeben_dieselbe_Zahl() { - var abtaster = new Bildabtaster( - FfmpegOptions.FromEnvironment(), NullLogger.Instance); + var abtaster = new Frameabtaster( + FfmpegOptions.FromEnvironment(), NullLogger.Instance); var acht = await abtaster.DunkelanteilAsync(Erzeugen("yuv420p"), bei: 2); var zehn = await abtaster.DunkelanteilAsync(Erzeugen("yuv420p10le"), bei: 2); diff --git a/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs b/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs index f98f3c9..1d2c8a0 100644 --- a/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs +++ b/server/tests/Flaro.Media.Tests/HlsArgumenteTests.cs @@ -241,7 +241,7 @@ public class HlsArgumenteTests /// Beim Rechnen wird jedes Segment gleich lang. [Test] - public void Beim_Rechnen_stehen_die_Schluesselbilder_fest() + public void Beim_Rechnen_stehen_die_Keyframes_fest() { var zeile = Zeile([Bild("hevc"), Ton(1)]); diff --git a/server/tests/Flaro.Media.Tests/HlsLaufTests.cs b/server/tests/Flaro.Media.Tests/HlsLaufTests.cs index f4e446b..6bb43e9 100644 --- a/server/tests/Flaro.Media.Tests/HlsLaufTests.cs +++ b/server/tests/Flaro.Media.Tests/HlsLaufTests.cs @@ -197,15 +197,15 @@ public class HlsLaufTests /// /// /// Das ist der Test, der gefehlt hat. Die Quelle hat genau ein - /// Schlüsselbild, bei null; jeder Sprung landet also mitten in der + /// Keyframe, bei null; jeder Sprung landet also mitten in der /// Bildgruppe — der schlimmste Fall. Genau gesprungen schnitte ffmpeg den /// Ton auf die Sekunde, das kopierte Bild aber nicht, und dazwischen läge /// Bild ohne Ton. /// /// - /// Zuerst hatte ich dafür den Sprung auf das Schlüsselbild gerundet. Das + /// Zuerst hatte ich dafür den Sprung auf das Keyframe gerundet. Das /// war zu scharf geschliffen: eine Millisekunde zu klein, und ffmpeg nimmt - /// das Schlüsselbild davor — gemessen wurden aus 0,004 s Abstand dann + /// das Keyframe davor — gemessen wurden aus 0,004 s Abstand dann /// 4,98. `-noaccurate_seek` braucht diese Genauigkeit nicht. /// /// @@ -229,15 +229,15 @@ public class HlsLaufTests /// /// /// - /// Die Quelle hat genau ein Schlüsselbild, bei null. Ein Sprung auf + /// Die Quelle hat genau ein Keyframe, bei null. Ein Sprung auf /// Sekunde 4 ist damit der schlimmste Fall: das kopierte Bild muss ganz /// vorn anfangen, der neu gerechnete Ton könnte bei 4 anfangen — vier /// Sekunden stummes Bild, und eine Uhr, die um vier Sekunden danebenliegt. /// /// - /// Gemessen an einer Quelle mit Schlüsselbildern alle fünf Sekunden und + /// Gemessen an einer Quelle mit Keyframesn alle fünf Sekunden und /// einem Sprung auf Sekunde 12 war es genau so: Bild ab 0, Ton ab 1,98. - /// Deshalb rundet der Koordinator den Sprung auf das Schlüsselbild — + /// Deshalb rundet der Koordinator den Sprung auf das Keyframe — /// hier wird geprüft, dass die Zeile das auch verträgt. /// /// @@ -247,7 +247,7 @@ public class HlsLaufTests var quelle = Path.Combine(_ordner, "lang.mkv"); Lang(quelle); - // Auf dem Schlüsselbild, so wie der Koordinator es rundet. + // Auf dem Keyframe, so wie der Koordinator es rundet. var (bild, ton) = Anfang(Laufen(quelle, ab: 0)); Assert.That(Math.Abs(bild - ton), Is.LessThan(0.2), diff --git a/server/tests/Flaro.Media.Tests/SchluesselbilderTests.cs b/server/tests/Flaro.Media.Tests/KeyframesTests.cs similarity index 93% rename from server/tests/Flaro.Media.Tests/SchluesselbilderTests.cs rename to server/tests/Flaro.Media.Tests/KeyframesTests.cs index 6f24942..e140aee 100644 --- a/server/tests/Flaro.Media.Tests/SchluesselbilderTests.cs +++ b/server/tests/Flaro.Media.Tests/KeyframesTests.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging.Abstractions; namespace Flaro.Media.Tests; -/// Sagt die Schlüsselbildsuche dasselbe wie ffmpeg? +/// Sagt die Keyframesuche dasselbe wie ffmpeg? /// /// /// @@ -24,7 +24,7 @@ namespace Flaro.Media.Tests; /// bleibt. /// /// -public class SchluesselbilderTests +public class KeyframesTests { private string _ordner = ""; private string _datei = ""; @@ -83,8 +83,8 @@ public class SchluesselbilderTests [TestCase(100.2)] public async Task Die_Suche_trifft_den_Anfang_des_Stroms(double wunsch) { - var suche = new Schluesselbilder( - FfmpegOptions.FromEnvironment(), NullLogger.Instance); + var suche = new Keyframes( + FfmpegOptions.FromEnvironment(), NullLogger.Instance); var gemeldet = await suche.DavorAsync(_datei, wunsch); var echt = EchterAnfang(wunsch); @@ -102,8 +102,8 @@ public class SchluesselbilderTests [TestCase(73.3)] public async Task Nie_hinter_dem_Wunsch(double wunsch) { - var suche = new Schluesselbilder( - FfmpegOptions.FromEnvironment(), NullLogger.Instance); + var suche = new Keyframes( + FfmpegOptions.FromEnvironment(), NullLogger.Instance); Assert.That(await suche.DavorAsync(_datei, wunsch), Is.LessThanOrEqualTo(wunsch)); } diff --git a/web/app/src/api.ts b/web/app/src/api.ts index b70cc04..f282342 100644 --- a/web/app/src/api.ts +++ b/web/app/src/api.ts @@ -88,7 +88,7 @@ export type Film = { /** Das Bild, das jetzt im Aufmacher gilt — vom Server gewählt. */ heroImage?: string | null; /** Der Abspann, sofern er im Bild gefunden wurde. */ - markers?: Marke[] | null; + markers?: Segment[] | null; }; export type Serie = { id: string; title: string; year: number | null; overview: string | null; @@ -120,7 +120,7 @@ export type Serie = { heroImage?: string | null; }; /** Ein überspringbarer Abschnitt einer Folge. */ -export type Marke = { kind: 'intro' | 'credits'; startSeconds: number; endSeconds: number }; +export type Segment = { kind: 'intro' | 'credits'; startSeconds: number; endSeconds: number }; export type Folge = { id: string; season: number; episode: number; @@ -129,7 +129,7 @@ export type Folge = { /** Zu welcher Serie sie gehört — für die Sprachwahl der Serie. */ seriesId?: string | null; /** Vorspann und Abspann, sofern der Server sie gefunden hat. */ - markers?: Marke[] | null; + markers?: Segment[] | null; /** Wann sie freigegeben ist. Liegt das in der Zukunft, gibt es sie noch nicht. */ publishAt?: string | null; }; @@ -196,11 +196,11 @@ export type Scanstand = { missing: number | null; /** Woran der Lauf gerade ist, in einem Satz. */ step: string | null; - /** Woran die Markensuche gerade ist — `null`, wenn keine läuft. + /** Woran die Segmentesuche gerade ist — `null`, wenn keine läuft. * * **Ein eigenes Feld, nicht `step`.** Sie hängt sich hinter den Scan und * läuft weiter, wenn der längst „fertig" meldet. */ - marken?: string | null; + segmente?: string | null; problems: { path: string; reason: string }[] | null; error: string | null; metadata: { moviesMatched: number; seriesMatched: number; episodesMatched: number; skipped: boolean; error: string | null } | null; @@ -653,19 +653,19 @@ export const naechsteFolgen = (take = 20) => /** Eine Folge für sich — tolerant, wie die Nachbarn. * * Für den Spieler: er bekommt nur eine Kennung und möchte wissen, ob es - * dazu Marken gibt. Eine 404 heisst „kein Folgentitel", und mehr hängt + * dazu Segmente gibt. Eine 404 heisst „kein Folgentitel", und mehr hängt * nicht daran. */ /** Stösst die Suche nach Vorspann und Abspann einer Serie an. * * Antwortet mit 202: der Lauf dauert Minuten — er liest von jeder Folge * einer Staffel ein Stück Ton —, und darauf wartet niemand. */ -/** Wie weit ein Markenlauf greift. +/** Wie weit ein Segmentelauf greift. * * **„Schon durchsucht" und „hat eine Marke" sind zwei verschiedene * Dinge.** Ein Titel, bei dem die Suche einmal nichts fand, trägt einen * Vermerk und keine Marke — und war damit für jeden weiteren Lauf * erledigt, obwohl er der interessanteste Fall ist. */ -export type Markenumfang = +export type Segmentumfang = /** Nur, wo noch nie gesucht wurde. */ | 'Neue' /** Auch, wo gesucht und nichts gefunden wurde — nicht, wo schon eine Marke steht. */ @@ -673,21 +673,21 @@ export type Markenumfang = /** Alles, auch was längst eine Marke hat. */ | 'Alles'; -export const markenSuchen = (serieId: string, umfang: Markenumfang = 'Alles') => +export const segmenteSuchen = (serieId: string, umfang: Segmentumfang = 'Alles') => anfrage( `/series/${serieId}/marken?umfang=${umfang}`, { method: 'POST' }); -/** Die Marken eines Titels — Folge oder Film. +/** Die Segmente eines Titels — Folge oder Film. * * **Eine Kennung, zwei mögliche Wege.** Der Spieler bekommt nur eine * Kennung und weiss nicht, was dahintersteckt; erst die Antwort sagt es. - * Eine 404 auf beiden Wegen heisst: keine Marken, und mehr hängt nicht + * Eine 404 auf beiden Wegen heisst: keine Segmente, und mehr hängt nicht * daran. */ -export async function markenHolen(titelId: string): Promise { +export async function segmenteHolen(titelId: string): Promise { const folge = await folgeLesen(titelId); if (folge) return folge.markers ?? []; try { - const f = await anfrage<{ markers?: Marke[] | null }>(`/movies/${titelId}`); + const f = await anfrage<{ markers?: Segment[] | null }>(`/movies/${titelId}`); return f.markers ?? []; } catch { return []; @@ -698,7 +698,7 @@ export async function markenHolen(titelId: string): Promise { * * **Antwortet sofort, arbeitet weiter.** Der Lauf liest je Folge ein Stück * Ton; über fünfundneunzig Serien sind das Stunden. */ -export const mediathekMarken = (id: string, umfang: Markenumfang) => +export const mediathekSegmente = (id: string, umfang: Segmentumfang) => anfrage(`/libraries/${id}/marken?umfang=${umfang}`, { method: 'POST' }); /** Die Adresse der Messkurve eines Films — zum Herunterladen. @@ -706,15 +706,15 @@ export const mediathekMarken = (id: string, umfang: Markenumfang) => * **Keine Erkennung, sondern Zahlen zum Ansehen.** Welche Schwelle bei * einem Abspann über laufendem Bild trägt, lässt sich nur an der Kurve * entscheiden, nicht am Ergebnis der Erkennung. */ -export const markenkurveAdresse = (filmId: string) => +export const segmentkurveAdresse = (filmId: string) => `/api/v1/movies/${filmId}/markenkurve`; -/** Nimmt die Marken eines Films wieder weg — wenn die Erkennung danebenlag. */ -export const filmmarkenVerwerfen = (filmId: string) => +/** Nimmt die Segmente eines Films wieder weg — wenn die Erkennung danebenlag. */ +export const filmsegmenteVerwerfen = (filmId: string) => anfrage(`/movies/${filmId}/marken`, { method: 'DELETE' }); /** Stösst die Abspannsuche für einen Film an. */ -export const filmmarkenSuchen = (filmId: string, umfang: Markenumfang = 'Alles') => +export const filmsegmenteSuchen = (filmId: string, umfang: Segmentumfang = 'Alles') => anfrage(`/movies/${filmId}/marken?umfang=${umfang}`, { method: 'POST' }); export async function folgeLesen(id: string): Promise { @@ -819,7 +819,7 @@ export const stromStarten = (datei: string, wunsch: Stromwunsch) => method: 'POST', body: JSON.stringify(wunsch), }); -/** Beendet einen Strom und räumt seinen Kratzplatz weg. */ +/** Beendet einen Strom und räumt seinen Transcode-Cache weg. */ export const stromBeenden = (sitzung: string) => anfrage(`/stream/${sitzung}`, { method: 'DELETE' }); @@ -1190,7 +1190,7 @@ export const statistik = (wochen = 12) => export type Speicherort = { name: string; variable: string; pfad: string; belegt: number; frei: number | null; bilder: number | null; - /** Nur beim Kratzplatz: die Unterordner einzeln. */ + /** Nur beim Transcode-Cache: die Unterordner einzeln. */ teile: { name: string; frei: number; dateien: number }[] | null; }; @@ -1202,11 +1202,11 @@ export type Aufraeumbericht = { teile: { name: string; frei: number; dateien: number }[]; }; -/** Räumt den Kratzplatz auf — Verwaistes immer, der Rest nach der Obergrenze. +/** Räumt den Transcode-Cache auf — Verwaistes immer, der Rest nach der Obergrenze. * * POST und nicht DELETE wie beim Bildspeicher: dort wird ein Ordner geleert, * hier eine Regel angewandt. */ -export const kratzplatzAufraeumen = () => +export const transcodeAufraeumen = () => anfrage('/system/storage/transcode/cleanup', { method: 'POST' }); export type Wartungseinstellung = { diff --git a/web/app/src/spieler.ts b/web/app/src/spieler.ts index 2d3feb8..a8198c6 100644 --- a/web/app/src/spieler.ts +++ b/web/app/src/spieler.ts @@ -1,7 +1,7 @@ import type HlsTyp from 'hls.js'; import { - stromStarten, stromBeenden, untertitelHolen, fortschritt, nachbarfolge, markenHolen, - type Stromauskunft, type Spurauskunft, type Folge, type Marke, + stromStarten, stromBeenden, untertitelHolen, fortschritt, nachbarfolge, segmenteHolen, + type Stromauskunft, type Spurauskunft, type Folge, type Segment, } from './api'; import { dauer, knopf, meldung, hinweisZeigen, folgenname } from './util'; import { serienton, serientext, spurFuer } from './sprachwahl'; @@ -386,10 +386,10 @@ export function spieler(anfrage: Spieleranfrage): void { // **Zwei Knöpfe, nicht einer.** Im Abspann einer Serie gibt es zwei // vernünftige Antworten — weiterschauen oder den Abspann überspringen — // und welche gemeint ist, weiss nur der Zuschauer. - const markenreihe = document.createElement('div'); - markenreihe.className = 'pl-marken hidden'; + const segmentreihe = document.createElement('div'); + segmentreihe.className = 'pl-segmente hidden'; - schicht.append(video, dreher, mitte, kopf, bedienung, markenreihe, meldungsfeld, kleinleiste); + schicht.append(video, dreher, mitte, kopf, bedienung, segmentreihe, meldungsfeld, kleinleiste); document.body.appendChild(schicht); document.body.classList.add('pl-offen'); @@ -626,7 +626,7 @@ export function spieler(anfrage: Spieleranfrage): void { let vorige: Folge | null = null; let naechste: Folge | null = null; /** Vorspann und Abspann dieser Folge — leer heisst: keine erkannt. */ - let marken: Marke[] = []; + let segmente: Segment[] = []; // **Eine Folge weiter, eine zurück.** // @@ -670,11 +670,11 @@ export function spieler(anfrage: Spieleranfrage): void { const [davor, danach, gefunden] = await Promise.all([ nachbarfolge(anfrage.titelId, 'previous'), nachbarfolge(anfrage.titelId, 'next'), - markenHolen(anfrage.titelId), + segmenteHolen(anfrage.titelId), ]); vorige = davor; naechste = danach; - marken = gefunden; + segmente = gefunden; if (geschlossen) return; vorigeKnopf.classList.toggle('hidden', vorige?.files[0] == null); naechsteKnopf.classList.toggle('hidden', naechste?.files[0] == null); @@ -827,7 +827,7 @@ export function spieler(anfrage: Spieleranfrage): void { kleinStand.textContent = laenge > 0 ? `${dauer(jetzt)} / ${dauer(laenge)}` : dauer(jetzt); kleinGefuellt.style.width = `${Math.min(100, anteil * 100)}%`; - markenstand(jetzt); + segmentstand(jetzt); } /** Zeigt den Überspringen-Knopf, solange man in einer Marke steht. @@ -841,8 +841,8 @@ export function spieler(anfrage: Spieleranfrage): void { * mehrmals je Sekunde; die Knöpfe jedesmal neu zu bauen hiesse, einen * Klick mitten im Drücken zu verlieren. */ - let markenstandJetzt = ''; - /** Ob die Marken-Knöpfe inzwischen an der Bedienung hängen. + let segmentstandJetzt = ''; + /** Ob die Segmente-Knöpfe inzwischen an der Bedienung hängen. * * **Erst für sich, dann mit der Bedienung.** Eine neue Marke zeigt ihre * Knöpfe von selbst — sonst müsste man erst das Bild anfassen, um das @@ -850,27 +850,27 @@ export function spieler(anfrage: Spieleranfrage): void { * Sobald man sie aber einmal eingeblendet hat, gehören die Knöpfe dazu * und gehen mit ihr. Keine Frist, kein Zeitgeber: der Zuschauer sagt * selbst, wann er genug gesehen hat. */ - let markenGebunden = false; - let markeLaeuft = false; + let segmentGebunden = false; + let segmentLaeuft = false; - function markenstand(jetzt: number): void { - const drin = marken.find(m => jetzt >= m.startSeconds && jetzt < m.endSeconds); + function segmentstand(jetzt: number): void { + const drin = segmente.find(m => jetzt >= m.startSeconds && jetzt < m.endSeconds); const abspann = drin?.kind === 'credits'; const weiter = abspann && naechste?.files[0] != null; const kennung = `${drin?.startSeconds ?? ''}|${weiter}`; // Eine neue Marke ist ein neues Angebot: sie steht wieder für sich, // bis die Bedienung das nächste Mal auftaucht. - if (kennung !== markenstandJetzt) markenGebunden = false; + if (kennung !== segmentstandJetzt) segmentGebunden = false; - markeLaeuft = drin != null; - markenreihe.classList.toggle( + segmentLaeuft = drin != null; + segmentreihe.classList.toggle( 'hidden', - !drin || (markenGebunden && schicht.classList.contains('pl-still'))); + !drin || (segmentGebunden && schicht.classList.contains('pl-still'))); - if (!drin || kennung === markenstandJetzt) return; - markenstandJetzt = kennung; - markenreihe.replaceChildren(); + if (!drin || kennung === segmentstandJetzt) return; + segmentstandJetzt = kennung; + segmentreihe.replaceChildren(); // **Im Abspann einer Serie zuerst die nächste Folge.** Das ist, was man // dort will; „überspringen" steht daneben für den, der den Abspann @@ -878,7 +878,7 @@ export function spieler(anfrage: Spieleranfrage): void { if (weiter) { const w = knopf('Nächste Folge', 'btn btn-primary'); w.onclick = e => { e.stopPropagation(); folgeWechseln(naechste); }; - markenreihe.appendChild(w); + segmentreihe.appendChild(w); } const u = knopf( @@ -894,11 +894,11 @@ export function spieler(anfrage: Spieleranfrage): void { // heisst überspringen Schluss" — und damit bekam niemand die Szene // je zu sehen. Geschlossen wird nur, wenn nach dieser Marke nichts // mehr kommt. - const danach = marken.some(m => m.startSeconds >= drin.endSeconds - 1); + const danach = segmente.some(m => m.startSeconds >= drin.endSeconds - 1); if (abspann && !weiter && !danach) { void beenden(); return; } void springen(drin.endSeconds); }; - markenreihe.appendChild(u); + segmentreihe.appendChild(u); } /** Ans Ende springen und zumachen — für den Abspann ohne Nachfolger. @@ -1365,11 +1365,11 @@ export function spieler(anfrage: Spieleranfrage): void { // Mausbewegung, auch wenn die Bedienung längst da ist. Das jedes Mal // als „einmal gezeigt" zu werten hiesse, dem Intro seinen Knopf zu // nehmen, bevor es ihn je hatte. - if (schicht.classList.contains('pl-still')) markenGebunden = true; + if (schicht.classList.contains('pl-still')) segmentGebunden = true; schicht.classList.remove('pl-still'); - // Im Stehen rechnet `markenstand` nicht weiter: wer pausiert im Intro + // Im Stehen rechnet `segmentstand` nicht weiter: wer pausiert im Intro // die Maus bewegt, bekäme die Bedienung zurück und den Knopf nicht. - if (markeLaeuft) markenreihe.classList.remove('hidden'); + if (segmentLaeuft) segmentreihe.classList.remove('hidden'); verbergenPlanen(); } function verbergenPlanen(): void { diff --git a/web/app/src/style.css b/web/app/src/style.css index da40379..a14efec 100644 --- a/web/app/src/style.css +++ b/web/app/src/style.css @@ -659,7 +659,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; } * wiederholen hiesse, vier gleich aussehende Zahlen nebeneinanderzustellen, * von denen keine sagt, woher sie kommt. Das Kürzel sagt es in zwei Zeichen. * - * **Und kein Logo.** Die Marken von IMDb und Rotten Tomatoes mitzuliefern + * **Und kein Logo.** Die Segmente von IMDb und Rotten Tomatoes mitzuliefern * hiesse, fremde Zeichen auszuliefern — mit allem, was daran hängt. */ .bewertung.fremd { font-weight: 500; } .bewertungsquelle { @@ -685,7 +685,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; } /* **Der Primärknopf ist hell, nicht farbig.** * * Genau das macht Netflix: der Abspielknopf ist weiss mit schwarzer Schrift, - * die Markenfarbe erscheint nur im Schriftzug. Der Akzent sass hier auf neun + * die Segmentefarbe erscheint nur im Schriftzug. Der Akzent sass hier auf neun * Dingen gleichzeitig — Marke, Navigation, Knopf, Fortschritt, Plakette, * Stern, Fokus, Filter, Kontokreis. Wenn alles Akzent ist, markiert er * nichts. */ @@ -928,7 +928,7 @@ textarea.feld { * Leiste gar nicht. Hier liegt er über dem Bild, rechts über der Stelle, * an der die Leiste erscheinen würde, und bleibt stehen, solange die Marke * läuft. */ -.pl-marken { +.pl-segmente { position: absolute; right: var(--s-section); bottom: calc(var(--s-section) * 3.5); @@ -936,7 +936,7 @@ textarea.feld { display: flex; gap: var(--s-snug); } -.pl-marken .btn { box-shadow: 0 2px 12px rgba(0, 0, 0, .6); } +.pl-segmente .btn { box-shadow: 0 2px 12px rgba(0, 0, 0, .6); } .pl-unten { display: grid; grid-template-columns: 1fr auto 1fr; diff --git a/web/app/src/views/detail.ts b/web/app/src/views/detail.ts index fde35e0..4f94384 100644 --- a/web/app/src/views/detail.ts +++ b/web/app/src/views/detail.ts @@ -2,8 +2,8 @@ import { ApiFehler, film as filmLaden, serie as serieLaden, folgen, vorschlaege, zuordnen, bildAdresse, istAdmin, gesehen, favorit, weglegen, serienstimmen, type Film, type Serie, type Folge, type Datei, type Stand, - type Mitwirkender, type Spur, aehnliche, metadatenAuffrischen, scanstand, markenSuchen, - filmmarkenSuchen, filmmarkenVerwerfen, markenkurveAdresse, + type Mitwirkender, type Spur, aehnliche, metadatenAuffrischen, scanstand, segmenteSuchen, + filmsegmenteSuchen, filmsegmenteVerwerfen, segmentkurveAdresse, } from '../api'; import { esc, dauer, groesse, knopf, feld, fehlerzeile, leer, anteil, meldung, hinweisZeigen, @@ -33,7 +33,7 @@ type Art = 'filme' | 'serien'; * die Besetzung mit runden Porträts. */ /** Wie die fremden Quellen heissen — kurz an der Kachel, lang im Tooltip. * - * **Kein Logo.** Die Marken von IMDb und Rotten Tomatoes mitzuliefern + * **Kein Logo.** Die Segmente von IMDb und Rotten Tomatoes mitzuliefern * hiesse, fremde Zeichen auszuliefern; ein Kürzel sagt dasselbe und gehört * uns. * @@ -513,7 +513,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem if (melden && istAdmin() && art === 'filme') { reihe.appendChild(rundKnopf('uhr', false, 'Abspann neu suchen', async () => { - await filmmarkenSuchen(eintrag.id, 'Alles'); + await filmsegmenteSuchen(eintrag.id, 'Alles'); hinweisZeigen('Der Abspann wird gesucht. Der Server sieht sich dafür ' + 'die letzten zwanzig Minuten an — das dauert einen Augenblick.'); // Kurz warten und nachsehen: anders als bei einer Serie ist das @@ -526,7 +526,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem // begründen. Der Knopf lädt sie als Tabelle herunter. reihe.appendChild(rundKnopf('protokoll', false, 'Messwerte des Abspanns holen', async () => { - window.location.href = markenkurveAdresse(eintrag.id); + window.location.href = segmentkurveAdresse(eintrag.id); hinweisZeigen('Die Messung läuft — der Server sieht sich dafür die ' + 'letzten zwanzig Minuten dreimal an.'); })); @@ -534,9 +534,9 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem // **Nur, wenn es etwas zu verwerfen gibt.** Ein Knopf, der nichts // wegnehmen kann, ist eine Behauptung über einen Zustand. if ('markers' in eintrag && (eintrag.markers?.length ?? 0) > 0) { - reihe.appendChild(rundKnopf('schliessen', false, 'Abspannmarke verwerfen', + reihe.appendChild(rundKnopf('schliessen', false, 'Abspannsegment verwerfen', async () => { - await filmmarkenVerwerfen(eintrag.id); + await filmsegmenteVerwerfen(eintrag.id); hinweisZeigen('Marke entfernt. Der nächste Scan legt sie nicht wieder an.'); await laden(); })); @@ -546,7 +546,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem if (melden && istAdmin() && art === 'serien') { reihe.appendChild(rundKnopf('uhr', false, 'Vorspann und Abspann neu suchen', async () => { - await markenSuchen(eintrag.id, 'Alles'); + await segmenteSuchen(eintrag.id, 'Alles'); hinweisZeigen('Vorspann und Abspann werden gesucht. ' + 'Der Server liest dafür jede Folge einmal durch — das dauert ein paar Minuten.'); })); @@ -1112,14 +1112,14 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem * gar nicht gefunden wurde oder bloss nicht angezeigt wird, ist von * aussen nicht zu unterscheiden — und beides hat eine andere Ursache. * Einmal nachsehen zu können erspart eine Runde Raten. */ - function markenwort(marken: Folge['markers']): string { + function segmentwort(segmente: Folge['markers']): string { if (!istAdmin()) return ''; - if (!marken || marken.length === 0) return 'keine Marken'; + if (!segmente || segmente.length === 0) return 'keine Segmente'; // **Mit Zeiten, nicht bloss mit Namen.** Dass eine Marke da ist, sagt // noch nicht, dass sie an der richtigen Stelle steht — und genau das // ist der Unterschied zwischen „die Erkennung hat nichts gefunden" // und „der Knopf kommt zur falschen Minute". - return marken + return segmente .map(m => `${m.kind === 'intro' ? 'Vorspann' : 'Abspann'} ` + `${dauer(m.startSeconds)}–${dauer(m.endSeconds)}`) .join(' · '); @@ -1135,8 +1135,8 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem : folge.state && folge.state.positionSeconds > 0 ? `noch ${Math.round(((folge.state.runtimeSeconds ?? 0) - folge.state.positionSeconds) / 60)} Min` : ''; - const marken = markenwort(folge.markers); - const daneben = [nebenbei, marken].filter(Boolean).join(' · '); + const segmente = segmentwort(folge.markers); + const daneben = [nebenbei, segmente].filter(Boolean).join(' · '); zeile.innerHTML = `${String(folge.episode).padStart(2, '0')} · ${esc(folge.title ?? 'Ohne Titel')}` + (daneben ? `${esc(daneben)}` : ''); diff --git a/web/app/src/views/einstellungen/mediatheken.ts b/web/app/src/views/einstellungen/mediatheken.ts index d1298d9..323fc87 100644 --- a/web/app/src/views/einstellungen/mediatheken.ts +++ b/web/app/src/views/einstellungen/mediatheken.ts @@ -1,6 +1,6 @@ import { bibliotheken, bibliothekAnlegen, bibliothekUmbenennen, bibliothekLoeschen, scanStarten, - mediathekMarken, type Markenumfang, + mediathekSegmente, type Segmentumfang, scanstand, metadatenAuffrischen, vermisste, vermisstesEntfernen, type VermissteDatei, pruefungSetzen, mediathekZuruecksetzen, type Bibliothek, } from '../../api'; @@ -227,10 +227,10 @@ export function bibliotheksbereich( // Angebot.** Den gibt es auf der Serienseite; hier gilt er für alles, // was in dieser Mediathek liegt — Serien über den Tonabgleich, Filme // über das Bild. - /** Startet einen Markenlauf und hängt die Standzeile daran. */ - function markenlauf(umfang: Markenumfang): void { + /** Startet einen Segmentelauf und hängt die Standzeile daran. */ + function segmentelauf(umfang: Segmentumfang): void { laufendSetzen(true); - void mediathekMarken(aktuell.id, umfang).then(() => { + void mediathekSegmente(aktuell.id, umfang).then(() => { hinweisZeigen('Die Suche läuft. Der Stand steht hier und im Protokoll von heute.'); void verfolgen(false); }); @@ -243,29 +243,29 @@ export function bibliotheksbereich( // herauskam — und eben nicht die vierhundert, bei denen längst etwas // steht. „Alles neu" ist die seltene und teure; sie gehört nicht // hinter denselben Knopf. - const marken = knopf('Fehlende Marken suchen', 'btn'); - marken.title = 'Nimmt sich die Titel vor, bei denen keine Marke steht.'; - marken.onclick = () => nachfragen({ - titel: `Fehlende Marken in „${aktuell.name}" suchen?`, + const segmente = knopf('Fehlende Segmente suchen', 'btn'); + segmente.title = 'Nimmt sich die Titel vor, bei denen keine Marke steht.'; + segmente.onclick = () => nachfragen({ + titel: `Fehlende Segmente in „${aktuell.name}" suchen?`, text: 'Nur, wo noch kein Vorspann oder Abspann steht. Bei einer Serie ' + 'wird eine Staffel ganz gelesen, sobald einer ihrer Folgen etwas ' + 'fehlt — der Tonabgleich vergleicht die Folgen miteinander und ' + 'geht nicht einzeln. Es läuft im Hintergrund weiter.', bestaetigen: 'Suchen', - tun: async () => markenlauf('Luecken'), + tun: async () => segmentelauf('Luecken'), }); - const markenAlle = knopf('Alles neu durchsuchen', 'btn'); - markenAlle.title = 'Bestimmt auch vorhandene Marken noch einmal.'; - markenAlle.onclick = () => nachfragen({ - titel: `Alle Marken in „${aktuell.name}" neu bestimmen?`, + const segmenteAlle = knopf('Alles neu durchsuchen', 'btn'); + segmenteAlle.title = 'Bestimmt auch vorhandene Segmente noch einmal.'; + segmenteAlle.onclick = () => nachfragen({ + titel: `Alle Segmente in „${aktuell.name}" neu bestimmen?`, text: `Jeder Titel wird noch einmal durchsucht — auch die, bei denen ` + `längst eine Marke steht. Der Server liest dafür von jeder Folge die ` + `Tonspur und sieht sich bei jedem Film die letzten zwanzig Minuten an. ` + `Bei ${aktuell.itemCount ?? 'vielen'} Titeln dauert das Stunden. ` + `Für „was fehlt noch?" ist der Knopf daneben der richtige.`, bestaetigen: 'Alles neu', - tun: async () => markenlauf('Alles'), + tun: async () => segmentelauf('Alles'), }); const umbenennen = knopf('Umbenennen', 'btn'); @@ -335,13 +335,13 @@ export function bibliotheksbereich( laufendSetzen(false); stand.textContent = s.error ?? 'Gescheitert.'; return; - } else if (s.marken) { - // **Die Markensuche hängt sich hinten an und läuft weiter.** + } else if (s.segmente) { + // **Die Segmentesuche hängt sich hinten an und läuft weiter.** // Sie dauert Stunden, wo der Scan Minuten dauert; ohne diesen // Zweig stünde hier längst das Ergebnis des Scans, während der // Server noch jede Folge durchhört. laufendSetzen(true); - stand.textContent = s.marken; + stand.textContent = s.segmente; } // **Alle Zähler auf demselben Takt.** @@ -357,9 +357,9 @@ export function bibliotheksbereich( // Mediathek, ohne je Sekunde zwei Abrufe zu kosten. if (melden && i > 0 && i % 3 === 0) geaendert(await laden()); - // Ein fertiger Scan ist nicht das Ende, solange die Markensuche + // Ein fertiger Scan ist nicht das Ende, solange die Segmentesuche // dahinter noch läuft — sonst fiele die Zeile mitten im Lauf weg. - if (s.state === 'finished' && !s.marken) { + if (s.state === 'finished' && !s.segmente) { const teile = [`${s.filesFound} Dateien`, `${s.added} neu`, `${s.removed} entfernt`]; // „ohne Folgennummer" stimmte nicht mehr: gezählt wird jetzt auch, // was eine Nummer trägt, aber zu keiner Serie gehört. @@ -402,7 +402,7 @@ export function bibliotheksbereich( // und danach nachzusehen. Der Knopf heisst „Seitenleiste bearbeiten". const knoepfe = document.createElement('div'); knoepfe.className = 'knopfreihe'; - knoepfe.append(los, auffrischen, marken, markenAlle, pruefung, umbenennen, + knoepfe.append(los, auffrischen, segmente, segmenteAlle, pruefung, umbenennen, zuruecksetzen, entfernen); // **Nur wenn hier wirklich etwas liegt.** Ein Knopf, der nichts zu tun diff --git a/web/app/src/views/einstellungen/uebersicht.ts b/web/app/src/views/einstellungen/uebersicht.ts index 5da9d8f..b9e99cc 100644 --- a/web/app/src/views/einstellungen/uebersicht.ts +++ b/web/app/src/views/einstellungen/uebersicht.ts @@ -1,7 +1,7 @@ import { doppelteZusammenlegen, servernameSetzen, systemauskunft, laufendeWiedergaben, serverzustand, bildAdresse, speicherorte, - bildspeicherLeeren, kratzplatzAufraeumen, type Wiedergabe, + bildspeicherLeeren, transcodeAufraeumen, type Wiedergabe, } from '../../api'; import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../../util'; import { dialog } from '../../dialog'; @@ -246,7 +246,7 @@ export function speicherbereich(): HTMLElement { + `Wird gesetzt über ${esc(o.variable)}.`; oben.appendChild(text); - // **Beim Kratzplatz aufgeschlüsselt.** „39 GB belegt" sagt nicht, ob + // **Beim Transcode-Cache aufgeschlüsselt.** „39 GB belegt" sagt nicht, ob // das Segmente, umgepackte Filme oder Untertitel sind — und davon // hängt ab, ob es ein Problem ist. Die Dateizahl steht dabei, weil // bei den Untertiteln die Anzahl das Problem ist und nicht die Grösse. @@ -262,7 +262,7 @@ export function speicherbereich(): HTMLElement { raeumen.onclick = async () => { raeumen.disabled = true; try { - const bericht = await kratzplatzAufraeumen(); + const bericht = await transcodeAufraeumen(); await laden(); const gemeldet = document.createElement('p'); gemeldet.className = 'hinweis'; diff --git a/web/app/src/views/einstellungen/wartung.ts b/web/app/src/views/einstellungen/wartung.ts index a72d409..7948a01 100644 --- a/web/app/src/views/einstellungen/wartung.ts +++ b/web/app/src/views/einstellungen/wartung.ts @@ -8,7 +8,7 @@ import { abschnitt, zeile } from './teile'; /** Was der Server in Abständen von selbst tut. * - * **Warum das eine eigene Seite ist.** Die drei Zahlen zum Kratzplatz + * **Warum das eine eigene Seite ist.** Die drei Zahlen zum Transcode-Cache * standen zuerst unter „Wiedergabe" — dort ist ihr Anlass, aber nicht ihr * Thema. Sobald es eine Sicherung und ein Aufräumen der Datenbank gibt, * gehören sie zusammen: alles hier ist etwas, das der Server ohne Zutun @@ -61,9 +61,9 @@ export function wartungsbereich(): HTMLElement { liste.innerHTML = ''; stand.forEach(s => liste.appendChild(aufgabe(s, werte))); - /* ---------- Der Kratzplatz ---------- */ + /* ---------- Der Transcode-Cache ---------- */ - liste.appendChild(abschnitt('Kratzplatz')); + liste.appendChild(abschnitt('Transcode-Cache')); liste.appendChild(zeile( 'Obergrenze',