From ed13d5fa433b15512f37f67fdc5527ef90dcbcd8 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Fri, 16 Oct 2026 16:26:57 +0200 Subject: [PATCH] =?UTF-8?q?Kratzplatz,=20Markenerkennung,=20Tonabdruck=20?= =?UTF-8?q?=E2=80=94=20erfundene=20W=C3=B6rter=20f=C3=BCr=20Fachbegriffe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit „Scratch disk" wörtlich zu übersetzen ist derselbe Fehler wie „Wachhund" für Watchdog und „Keks" für Cookie. Und „Marke" ist kein Fachbegriff für das, was gemeint ist: Jellyfin nennt diese Abschnitte Media Segments, und das ist auch das Wort, in dem sich darüber reden lässt. Kratzplatz* → Transcode* (was es ist, steht in FLARO_TRANSCODE) Marke, Marken → Segment, Segmente Markenerkennung → Segmenterkennung Markenart/-suche → Segmentart/-suche Tonabdruck → Fingerprint (Chromaprint nennt es so) Tonabgleich → Fingerprintabgleich Schlüsselbild → Keyframe Bildabtaster → Frameabtaster Gewöhnliches Deutsch bleibt gewöhnliches Deutsch: Wartung, Freigabe, Meldung, Abspann, Vorspann. Das war nie das Problem. **Was sich nicht ändert, und zwar mit Absicht.** Die drei Einstellungsschlüssel `wartung.kratzplatz.paket.*` bleiben, wie sie sind — sie stehen in der Datenbank, und ein neuer Name hiesse, dass die Werte des Nutzers stillschweigend auf die Vorgabe zurückfallen. Dasselbe für die Endpunktpfade `/marken`: sie sind nicht sichtbar, und ein Umbenennen wäre Diff ohne Gewinn. **Die Migration ist von Hand geschrieben.** `dotnet ef` hat ein Gerüst erzeugt, das beide Tabellen löscht und neu anlegt — es sieht eine verschwundene und eine neue Tabelle, nicht eine umbenannte. Damit wäre jedes erkannte Segment und jeder Vermerk „hier wurde schon gesucht" weg gewesen. `RenameTable` und `RenameIndex` tun dasselbe, ohne eine Zeile anzufassen; nachgemessen an einer echten SQLite-Datei: Zeilen vorher 1 und 1, nachher 1 und 1, Inhalt unverändert. Nachgezogen: in `bilder.ts` und `main.ts` heisst „marke" ein Häkchen und das Logo — die blieben unberührt. Und „Segment" ist sächlich, was eine Blindersetzung nicht weiss: „eine Segment" stand danach an vierzehn Stellen. Co-Authored-By: Claude Opus 5 --- scratchpad/neu/data/flaro.db | Bin 389120 -> 450560 bytes scratchpad/neu/data/logs/flaro-2026-10-07.log | 3 + scratchpad/neu/data/logs/flaro-2026-10-08.log | 17 + scratchpad/neu/data/logs/flaro-2026-10-11.log | 1 + scratchpad/neu/data/logs/flaro-2026-10-14.log | 1 + scratchpad/neu/data/logs/flaro-2026-10-15.log | 2 + .../data/sicherungen/flaro-2026-10-08-1910.db | Bin 0 -> 393216 bytes .../data/sicherungen/flaro-2026-10-11-1917.db | Bin 0 -> 450560 bytes .../data/sicherungen/flaro-2026-10-14-1926.db | Bin 0 -> 450560 bytes .../src/Flaro.Api/Auth/TokenAuthentication.cs | 6 +- .../src/Flaro.Api/Endpoints/AuthEndpoints.cs | 4 +- server/src/Flaro.Api/Endpoints/Contracts.cs | 10 +- .../Flaro.Api/Endpoints/EinladungEndpoints.cs | 2 +- .../Endpoints/LibraryContentEndpoints.cs | 72 +- .../Flaro.Api/Endpoints/PlaybackEndpoints.cs | 8 +- .../Flaro.Api/Endpoints/SettingsEndpoints.cs | 14 +- .../Flaro.Api/Endpoints/StreamEndpoints.cs | 4 +- .../Flaro.Api/Endpoints/SystemEndpoints.cs | 10 +- server/src/Flaro.Api/PackageCoordinator.cs | 6 +- server/src/Flaro.Api/Program.cs | 18 +- server/src/Flaro.Api/ScanCoordinator.cs | 10 +- ...Markenerkennung.cs => Segmenterkennung.cs} | 238 +-- server/src/Flaro.Api/Streamkoordinator.cs | 22 +- ...tellungen.cs => Transcodeeinstellungen.cs} | 12 +- ...Kratzplatzpflege.cs => Transcodepflege.cs} | 12 +- server/src/Flaro.Core/Auth/AuthCode.cs | 2 +- server/src/Flaro.Core/Auth/Einladung.cs | 2 +- server/src/Flaro.Core/Auth/Secrets.cs | 4 +- server/src/Flaro.Core/Delivery/Paketname.cs | 2 +- ...Kratzplatzregeln.cs => Transcoderegeln.cs} | 12 +- .../src/Flaro.Core/Library/CatalogEntities.cs | 14 +- server/src/Flaro.Core/Library/Scanschritt.cs | 4 +- server/src/Flaro.Core/Library/SidecarNames.cs | 2 +- server/src/Flaro.Core/Media/Abspannkante.cs | 24 +- ...{Tonabgleich.cs => Fingerprintabgleich.cs} | 34 +- .../src/Flaro.Core/Playback/ProgressRule.cs | 4 +- server/src/Flaro.Data/FlaroContext.cs | 10 +- .../20261016141454_Segmente.Designer.cs | 1357 +++++++++++++++++ .../Migrations/20261016141454_Segmente.cs | 62 + .../Migrations/FlaroContextModelSnapshot.cs | 192 +-- ...Tonabdruckleser.cs => Fingerprintleser.cs} | 22 +- .../{Bildabtaster.cs => Frameabtaster.cs} | 40 +- server/src/Flaro.Media/HlsArgumente.cs | 16 +- server/src/Flaro.Media/Kapitelleser.cs | 10 +- .../{Schluesselbilder.cs => Keyframes.cs} | 22 +- .../tests/Flaro.Api.Tests/EinladungTests.cs | 2 +- server/tests/Flaro.Api.Tests/FlaroServer.cs | 4 +- .../{MarkenTests.cs => SegmentTests.cs} | 88 +- server/tests/Flaro.Api.Tests/StreamTests.cs | 12 +- .../{KratzplatzTests.cs => TranscodeTests.cs} | 20 +- .../Flaro.Api.Tests/WartungsplanTests.cs | 2 +- .../Flaro.Core.Tests/AbspannkanteTests.cs | 52 +- .../tests/Flaro.Core.Tests/AuthCodeTests.cs | 6 +- ...eckeTests.cs => EchteFingerprintsTests.cs} | 8 +- ...chTests.cs => FingerprintabgleichTests.cs} | 20 +- .../{Abdruecke => Fingerprints}/folgeA.txt | 0 .../{Abdruecke => Fingerprints}/folgeB.txt | 0 .../Flaro.Core.Tests/Flaro.Core.Tests.csproj | 2 +- .../Flaro.Core.Tests/ProgressRuleTests.cs | 8 +- .../Flaro.Core.Tests/ScanschrittTests.cs | 8 +- ...tzregelTests.cs => TranscoderegelTests.cs} | 22 +- ...leserTests.cs => FingerprintleserTests.cs} | 32 +- ...abtasterTests.cs => FrameabtasterTests.cs} | 24 +- .../Flaro.Media.Tests/HlsArgumenteTests.cs | 2 +- .../tests/Flaro.Media.Tests/HlsLaufTests.cs | 14 +- ...uesselbilderTests.cs => KeyframesTests.cs} | 12 +- web/app/src/api.ts | 44 +- web/app/src/spieler.ts | 56 +- web/app/src/style.css | 8 +- web/app/src/views/detail.ts | 26 +- .../src/views/einstellungen/mediatheken.ts | 40 +- web/app/src/views/einstellungen/uebersicht.ts | 6 +- web/app/src/views/einstellungen/wartung.ts | 6 +- 73 files changed, 2137 insertions(+), 694 deletions(-) create mode 100644 scratchpad/neu/data/logs/flaro-2026-10-08.log create mode 100644 scratchpad/neu/data/logs/flaro-2026-10-11.log create mode 100644 scratchpad/neu/data/logs/flaro-2026-10-14.log create mode 100644 scratchpad/neu/data/logs/flaro-2026-10-15.log create mode 100644 scratchpad/neu/data/sicherungen/flaro-2026-10-08-1910.db create mode 100644 scratchpad/neu/data/sicherungen/flaro-2026-10-11-1917.db create mode 100644 scratchpad/neu/data/sicherungen/flaro-2026-10-14-1926.db rename server/src/Flaro.Api/{Markenerkennung.cs => Segmenterkennung.cs} (81%) rename server/src/Flaro.Api/{Kratzplatzeinstellungen.cs => Transcodeeinstellungen.cs} (80%) rename server/src/Flaro.Api/{Kratzplatzpflege.cs => Transcodepflege.cs} (96%) rename server/src/Flaro.Core/Delivery/{Kratzplatzregeln.cs => Transcoderegeln.cs} (93%) rename server/src/Flaro.Core/Media/{Tonabgleich.cs => Fingerprintabgleich.cs} (88%) create mode 100644 server/src/Flaro.Data/Migrations/20261016141454_Segmente.Designer.cs create mode 100644 server/src/Flaro.Data/Migrations/20261016141454_Segmente.cs rename server/src/Flaro.Media/{Tonabdruckleser.cs => Fingerprintleser.cs} (89%) rename server/src/Flaro.Media/{Bildabtaster.cs => Frameabtaster.cs} (86%) rename server/src/Flaro.Media/{Schluesselbilder.cs => Keyframes.cs} (84%) rename server/tests/Flaro.Api.Tests/{MarkenTests.cs => SegmentTests.cs} (83%) rename server/tests/Flaro.Api.Tests/{KratzplatzTests.cs => TranscodeTests.cs} (94%) rename server/tests/Flaro.Core.Tests/{EchteAbdrueckeTests.cs => EchteFingerprintsTests.cs} (90%) rename server/tests/Flaro.Core.Tests/{TonabgleichTests.cs => FingerprintabgleichTests.cs} (89%) rename server/tests/Flaro.Core.Tests/{Abdruecke => Fingerprints}/folgeA.txt (100%) rename server/tests/Flaro.Core.Tests/{Abdruecke => Fingerprints}/folgeB.txt (100%) rename server/tests/Flaro.Core.Tests/{KratzplatzregelTests.cs => TranscoderegelTests.cs} (92%) rename server/tests/Flaro.Media.Tests/{TonabdruckleserTests.cs => FingerprintleserTests.cs} (61%) rename server/tests/Flaro.Media.Tests/{BildabtasterTests.cs => FrameabtasterTests.cs} (93%) rename server/tests/Flaro.Media.Tests/{SchluesselbilderTests.cs => KeyframesTests.cs} (93%) diff --git a/scratchpad/neu/data/flaro.db b/scratchpad/neu/data/flaro.db index 719a7a9665be0a0c3d1a11e340402a255ad48f96..7ad99424aad1234b0e55a88d858ad60d32dd1f51 100644 GIT binary patch delta 5171 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6be24d0eeced711eeee1c455cb413f3f716bf374 GIT binary patch literal 450560 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 /// /// 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',