Kratzplatz, Markenerkennung, Tonabdruck — erfundene Wörter für Fachbegriffe
„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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
32f592b83e
commit
ed13d5fa43
@@ -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()));
|
||||
}
|
||||
|
||||
/// <summary>Die Marken zu einer Reihe von Folgen.</summary>
|
||||
/// <summary>Die Segmente zu einer Reihe von Folgen.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Als Nachschlagewerk und nicht je Folge einzeln: eine Staffel mit
|
||||
/// zwanzig Folgen wären sonst zwanzig Abfragen für zwanzig kurze Zeilen.
|
||||
/// </remarks>
|
||||
private static async Task<ILookup<Guid, MarkerResponse>> MarkenZu(
|
||||
private static async Task<ILookup<Guid, MarkerResponse>> SegmenteZu(
|
||||
FlaroContext db, IReadOnlyCollection<Guid> 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));
|
||||
}
|
||||
|
||||
/// <summary>Dasselbe für Filme.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
private static async Task<ILookup<Guid, MarkerResponse>> FilmmarkenZu(
|
||||
private static async Task<ILookup<Guid, MarkerResponse>> FilmsegmenteZu(
|
||||
FlaroContext db, IReadOnlyCollection<Guid> 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<ILookup<Guid, MediaStreamResponse>> SpurenZu(FlaroContext db, List<Guid> 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<MovieResponse>(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<EpisodeResponse>(items, items.Count));
|
||||
}).RequireAuthorization();
|
||||
|
||||
Reference in New Issue
Block a user