diff --git a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs
index 21c71f0..6047724 100644
--- a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs
+++ b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs
@@ -893,11 +893,12 @@ 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, bool erneut = false) =>
+ Guid id, FlaroContext db, Markenerkennung erkennung,
+ Markenumfang umfang = Markenumfang.Alles) =>
{
if (!await db.Movies.AsNoTracking().AnyAsync(x => x.Id == id)) return Results.NotFound();
- _ = erkennung.FilmAsync(id, erneut);
+ _ = erkennung.FilmAsync(id, umfang);
return Results.Accepted();
}).RequireAuthorization(FlaroPolicies.Administrator);
@@ -914,12 +915,17 @@ public static class LibraryContentEndpoints
return weg > 0 ? Results.NoContent() : Results.NotFound();
}).RequireAuthorization(FlaroPolicies.Administrator);
+ // **Bei einem einzelnen Titel ist „alles" die Vorgabe.** Wer den
+ // Knopf bei genau dieser Serie drückt, ist mit genau dieser Serie
+ // 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, bool erneut = false) =>
+ Guid id, FlaroContext db, Markenerkennung erkennung,
+ Markenumfang umfang = Markenumfang.Alles) =>
{
if (!await db.Series.AsNoTracking().AnyAsync(x => x.Id == id)) return Results.NotFound();
- return erkennung.Starten(id, erneut)
+ return erkennung.Starten(id, umfang)
? Results.Accepted()
: Einwand.Belegt("Für diese Serie werden gerade Marken gesucht.");
}).RequireAuthorization(FlaroPolicies.Administrator);
@@ -930,12 +936,13 @@ 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, bool erneut = false) =>
+ Guid id, FlaroContext db, Markenerkennung erkennung,
+ Markenumfang umfang = Markenumfang.Neue) =>
{
if (!await db.Libraries.AsNoTracking().AnyAsync(x => x.Id == id))
return Results.NotFound();
- erkennung.MediathekStarten(id, erneut);
+ erkennung.MediathekStarten(id, umfang);
return Results.Accepted();
}).RequireAuthorization(FlaroPolicies.Administrator);
diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs
index 998ebff..a32266e 100644
--- a/server/src/Flaro.Api/Markenerkennung.cs
+++ b/server/src/Flaro.Api/Markenerkennung.cs
@@ -7,6 +7,37 @@ using Microsoft.EntityFrameworkCore;
namespace Flaro.Api;
+/// Wie weit ein Markenlauf greift.
+///
+///
+///
+/// „Schon durchsucht" und „hat eine Marke" 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
+/// interessanteste Fall ist.
+///
+///
+/// Nach jeder Verbesserung an der Erkennung will man genau diese Titel
+/// noch einmal — und eben nicht die vierhundert, bei denen längst etwas
+/// steht.
+///
+///
+public enum Markenumfang
+{
+ /// 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.
+ ///
+ Luecken,
+
+ /// Alles, auch was längst eine Marke hat.
+ Alles,
+}
+
/// Sucht Vorspann und Abspann einer Staffel.
///
///
@@ -105,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, bool erneut = false)
+ public bool Starten(Guid serieId, Markenumfang umfang = Markenumfang.Neue)
{
if (!_laufend.TryAdd(serieId, true)) return false;
@@ -113,7 +144,7 @@ public sealed class Markenerkennung(
{
try
{
- await LaufenAsync(serieId, erneut, CancellationToken.None);
+ await LaufenAsync(serieId, umfang, CancellationToken.None);
}
catch (Exception fehler)
{
@@ -146,7 +177,8 @@ public sealed class Markenerkennung(
///
///
public async Task NachScanAsync(
- Guid mediathek, bool erneut = false, CancellationToken ct = default)
+ Guid mediathek, Markenumfang umfang = Markenumfang.Neue,
+ CancellationToken ct = default)
{
using var raum = scopes.CreateScope();
var db = raum.ServiceProvider.GetRequiredService();
@@ -171,7 +203,7 @@ public sealed class Markenerkennung(
if (serien.Count > 0 && await leser.VorhandenAsync(ct))
{
await SerienAsync(db, mediathekname, serien
- .Select(s => (s.Id, s.Title)).ToList(), mediathek, erneut, ct);
+ .Select(s => (s.Id, s.Title)).ToList(), mediathek, umfang, ct);
}
else if (serien.Count > 0)
{
@@ -184,7 +216,7 @@ public sealed class Markenerkennung(
// Stand hier früher `if (serien.Count == 0) return;`, und damit
// kam eine reine Filmmediathek nie bis zu dieser Zeile: der
// Abspann eines Films wurde nach einem Scan nie gesucht.
- await FilmeAsync(db, mediathekname, mediathek, erneut, ct,
+ await FilmeAsync(db, mediathekname, mediathek, umfang, ct,
schritt => _stand[mediathek] = schritt);
}
finally
@@ -196,7 +228,7 @@ 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, bool erneut, CancellationToken ct)
+ Guid mediathek, Markenumfang umfang, CancellationToken ct)
{
var folgen = await db.Episodes.AsNoTracking()
.CountAsync(f => db.Series.Any(s => s.Id == f.SeriesId && s.LibraryId == mediathek), ct);
@@ -226,7 +258,7 @@ public sealed class Markenerkennung(
if (!_laufend.TryAdd(id, true)) continue;
try
{
- var stand = await LaufenAsync(id, erneut, ct, Melden);
+ var stand = await LaufenAsync(id, umfang, ct, Melden);
marken += stand.Marken;
if (!stand.Angefasst) continue;
angefasst++;
@@ -259,11 +291,11 @@ public sealed class Markenerkennung(
/// Tagesprotokoll — und an den Folgen selbst, bei denen danach eine
/// Marke steht.
///
- public void MediathekStarten(Guid mediathek, bool erneut)
+ public void MediathekStarten(Guid mediathek, Markenumfang umfang)
{
_ = Task.Run(async () =>
{
- try { await NachScanAsync(mediathek, erneut, CancellationToken.None); }
+ try { await NachScanAsync(mediathek, umfang, CancellationToken.None); }
catch (Exception fehler)
{
log.LogWarning(fehler, "Markenlauf für Mediathek {Kennung} abgebrochen: {Grund}",
@@ -280,15 +312,25 @@ public sealed class Markenerkennung(
/// läuft dann ins Leere, und das kostet eine Abfrage.
///
private async Task FilmeAsync(
- FlaroContext db, string mediathekname, Guid mediathek, bool erneut,
+ FlaroContext db, string mediathekname, Guid mediathek, Markenumfang umfang,
CancellationToken ct, Action melden)
{
- var offen = await db.Movies.AsNoTracking()
- .Where(m => m.LibraryId == mediathek)
- // Bei „erneut" auch die, bei denen schon gesucht wurde: genau
- // dafür drückt jemand den Knopf.
- .Where(m => erneut
- || !db.Markensuchen.Any(x => x.MovieId == m.Id && x.Art == Markenart.Abspann))
+ // **In C# verzweigt, nicht im Ausdruck.** Drei Bedingungen in einem
+ // `Where` mit einem Aufzählungswert übersetzt EF Core zwar meist,
+ // aber „meist" ist hier kein Argument: so ist sicher, was in SQL
+ // ankommt.
+ var abfrage = db.Movies.AsNoTracking().Where(m => m.LibraryId == mediathek);
+ abfrage = umfang switch
+ {
+ Markenumfang.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)),
+ _ => abfrage.Where(m =>
+ !db.Markensuchen.Any(x => x.MovieId == m.Id && x.Art == Markenart.Abspann)),
+ };
+
+ var offen = await abfrage
.Select(m => new { m.Id, m.Title, m.Year })
.OrderBy(m => m.Title)
.ToListAsync(ct);
@@ -306,7 +348,7 @@ public sealed class Markenerkennung(
var titel = Filmname(film.Title, film.Year);
melden(new Scanschritt(Scanphase.Marken, titel, i + 1, offen.Count));
- try { if (await FilmAsync(film.Id, erneut, ct)) gefunden++; }
+ try { if (await FilmAsync(film.Id, umfang, ct)) gefunden++; }
catch (Exception fehler)
{
log.LogWarning(fehler, "Abspannsuche für „{Film}“ abgebrochen: {Grund}",
@@ -342,7 +384,8 @@ public sealed class Markenerkennung(
/// Sekunden ungenau. Die Kante holt danach eine Handvoll kurzer Sonden.
///
///
- public async Task FilmAsync(Guid filmId, bool erneut, CancellationToken ct = default)
+ public async Task FilmAsync(
+ Guid filmId, Markenumfang umfang, CancellationToken ct = default)
{
using var raum = scopes.CreateScope();
var db = raum.ServiceProvider.GetRequiredService();
@@ -372,7 +415,14 @@ public sealed class Markenerkennung(
return false;
}
- if (!erneut && await db.Markensuchen.AsNoTracking()
+ if (umfang == Markenumfang.Luecken && await db.Marken.AsNoTracking()
+ .AnyAsync(m => m.MovieId == filmId && m.Art == Markenart.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))
{
log.LogInformation("„{Film}“: schon durchsucht — übersprungen.", titel);
@@ -468,7 +518,8 @@ public sealed class Markenerkennung(
private readonly record struct Serienstand(int Marken, bool Angefasst);
private async Task LaufenAsync(
- Guid serieId, bool erneut, CancellationToken ct, Action? melden = null)
+ Guid serieId, Markenumfang umfang, CancellationToken ct,
+ Action? melden = null)
{
using var raum = scopes.CreateScope();
var db = raum.ServiceProvider.GetRequiredService();
@@ -505,6 +556,15 @@ public sealed class Markenerkennung(
.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()
+ .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 })
+ .ToListAsync(ct);
+
var staffeln = folgen.GroupBy(f => f.SeasonNumber).OrderBy(g => g.Key).ToList();
log.LogInformation("„{Serie}“: {Staffeln} Staffel(n), {Folgen} Folge(n).",
@@ -528,10 +588,18 @@ public sealed class Markenerkennung(
// Folge, und Vorspann wie Abspann sind danach zwei Ausschnitte
// aus demselben Abdruck. 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,
+ // nicht der einzelnen Folge — für eine Lücke läuft sie ganz.
+ bool Luecke(Markenart art) => geordnet.Exists(
+ f => !vorhanden.Exists(v => v.Folge == f.Id && v.Art == art));
+
var fehlt = new[] { Markenart.Intro, Markenart.Abspann }
- .Where(art => erneut || gesucht.FirstOrDefault(
- x => x.Staffel == staffel.Key && x.Art == art) is not { } lauf
- || lauf.Folgen != geordnet.Count)
+ .Where(art => umfang == Markenumfang.Alles
+ || gesucht.FirstOrDefault(
+ x => x.Staffel == staffel.Key && x.Art == art) is not { } lauf
+ || lauf.Folgen != geordnet.Count
+ || (umfang == Markenumfang.Luecken && Luecke(art)))
.ToList();
if (fehlt.Count == 0) continue;
angefasst = true;
diff --git a/server/tests/Flaro.Api.Tests/MarkenTests.cs b/server/tests/Flaro.Api.Tests/MarkenTests.cs
index 18a488d..10fdca5 100644
--- a/server/tests/Flaro.Api.Tests/MarkenTests.cs
+++ b/server/tests/Flaro.Api.Tests/MarkenTests.cs
@@ -296,4 +296,109 @@ public class MarkenTests
"der Vermerk bleibt, sonst legte der nächste Scan dieselbe Marke wieder an");
});
}
+
+ /// Wie weit ein Lauf greift — drei Umfänge, drei Antworten.
+ ///
+ ///
+ ///
+ /// „Schon durchsucht" und „hat eine Marke" 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
+ /// Lauf erledigt, obwohl er der interessanteste Fall ist.
+ ///
+ ///
+ /// Geprüft wird am Vermerk: fasst der Lauf den Film an, schreibt er
+ /// GelaufenAm neu. Das ist die einzige Spur, die auch ohne
+ /// ffmpeg und ohne echten Abspann entsteht.
+ ///
+ ///
+ [TestCase("Neue", false, TestName = "Neue überspringt, was schon durchsucht ist")]
+ [TestCase("Luecken", true, TestName = "Luecken nimmt sich den Film ohne Marke vor")]
+ [TestCase("Alles", true, TestName = "Alles nimmt sich alles vor")]
+ public async Task Der_Umfang_entscheidet_bei_einem_Film_ohne_Marke(
+ string umfang, bool erwartetAngefasst)
+ {
+ Datei("Matrix (1999)/Matrix (1999).mkv");
+
+ using var server = new FlaroServer();
+ var client = await Eingelesen(server, "Movies");
+ var filme = await client.GetFromJsonAsync>("/api/v1/movies");
+ var film = filme!.Items[0].Id;
+
+ // Gesucht wurde schon, gefunden nichts: Vermerk ja, Marke nein.
+ var vorher = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
+ await server.InDerDatenbank(async db =>
+ {
+ // **Ohne Laufzeit kommt der Lauf nicht bis zum Vermerk.** Die
+ // Prüfdatei ist leer; die Laufzeit steht sonst aus ffprobe.
+ var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film);
+ datei.Duration = TimeSpan.FromMinutes(120);
+
+ db.Markensuchen.Add(new Markensuche
+ {
+ MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher,
+ });
+ await db.SaveChangesAsync();
+ });
+
+ (await client.PostAsync($"/api/v1/movies/{film}/marken?umfang={umfang}", null))
+ .EnsureSuccessStatusCode();
+
+ var gelaufen = vorher;
+ for (var versuch = 0; versuch < 40; versuch++)
+ {
+ await server.InDerDatenbank(async db =>
+ gelaufen = await db.Markensuchen.AsNoTracking()
+ .Where(x => x.MovieId == film).Select(x => x.GelaufenAm).FirstAsync());
+ if (gelaufen != vorher) break;
+ await Task.Delay(50);
+ }
+
+ Assert.That(gelaufen != vorher, Is.EqualTo(erwartetAngefasst),
+ erwartetAngefasst
+ ? "dieser Umfang muss den Film noch einmal anfassen"
+ : "dieser Umfang muss ihn überspringen");
+ }
+
+ /// Ein Film mit Marke bleibt bei „nur Lücken" unangetastet.
+ [Test]
+ public async Task Luecken_laesst_einen_Film_mit_Marke_in_Ruhe()
+ {
+ Datei("Matrix (1999)/Matrix (1999).mkv");
+
+ using var server = new FlaroServer();
+ var client = await Eingelesen(server, "Movies");
+ var filme = await client.GetFromJsonAsync>("/api/v1/movies");
+ var film = filme!.Items[0].Id;
+
+ var vorher = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
+ await server.InDerDatenbank(async db =>
+ {
+ var datei = await db.MediaFiles.FirstAsync(d => d.MovieId == film);
+ datei.Duration = TimeSpan.FromMinutes(120);
+
+ db.Marken.Add(new Marke
+ {
+ MovieId = film, Art = Markenart.Abspann,
+ VonSekunden = 6900, BisSekunden = 7200, ErkanntAm = vorher,
+ });
+ db.Markensuchen.Add(new Markensuche
+ {
+ MovieId = film, Art = Markenart.Abspann, Folgen = 1, GelaufenAm = vorher,
+ });
+ await db.SaveChangesAsync();
+ });
+
+ (await client.PostAsync($"/api/v1/movies/{film}/marken?umfang=Luecken", null))
+ .EnsureSuccessStatusCode();
+ await Task.Delay(400);
+
+ var gelaufen = DateTimeOffset.MinValue;
+ await server.InDerDatenbank(async db =>
+ gelaufen = await db.Markensuchen.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");
+ }
}
diff --git a/web/app/src/api.ts b/web/app/src/api.ts
index 8e9ad68..17efbaf 100644
--- a/web/app/src/api.ts
+++ b/web/app/src/api.ts
@@ -659,9 +659,23 @@ export const naechsteFolgen = (take = 20) =>
*
* Antwortet mit 202: der Lauf dauert Minuten — er liest von jeder Folge
* einer Staffel ein Stück Ton —, und darauf wartet niemand. */
-export const markenSuchen = (serieId: string, erneut = false) =>
+/** Wie weit ein Markenlauf 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 =
+ /** Nur, wo noch nie gesucht wurde. */
+ | 'Neue'
+ /** Auch, wo gesucht und nichts gefunden wurde — nicht, wo schon eine Marke steht. */
+ | 'Luecken'
+ /** Alles, auch was längst eine Marke hat. */
+ | 'Alles';
+
+export const markenSuchen = (serieId: string, umfang: Markenumfang = 'Alles') =>
anfrage(
- `/series/${serieId}/marken${erneut ? '?erneut=true' : ''}`, { method: 'POST' });
+ `/series/${serieId}/marken?umfang=${umfang}`, { method: 'POST' });
/** Die Marken eines Titels — Folge oder Film.
*
@@ -684,17 +698,16 @@ 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, erneut = false) =>
- anfrage(
- `/libraries/${id}/marken${erneut ? '?erneut=true' : ''}`, { method: 'POST' });
+export const mediathekMarken = (id: string, umfang: Markenumfang) =>
+ anfrage(`/libraries/${id}/marken?umfang=${umfang}`, { method: 'POST' });
/** Nimmt die Marken eines Films wieder weg — wenn die Erkennung danebenlag. */
export const filmmarkenVerwerfen = (filmId: string) =>
anfrage(`/movies/${filmId}/marken`, { method: 'DELETE' });
/** Stösst die Abspannsuche für einen Film an. */
-export const filmmarkenSuchen = (filmId: string, erneut = false) =>
- anfrage(`/movies/${filmId}/marken${erneut ? '?erneut=true' : ''}`, { method: 'POST' });
+export const filmmarkenSuchen = (filmId: string, umfang: Markenumfang = 'Alles') =>
+ anfrage(`/movies/${filmId}/marken?umfang=${umfang}`, { method: 'POST' });
export async function folgeLesen(id: string): Promise {
try {
diff --git a/web/app/src/views/detail.ts b/web/app/src/views/detail.ts
index b4d5ad4..e601860 100644
--- a/web/app/src/views/detail.ts
+++ b/web/app/src/views/detail.ts
@@ -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, true);
+ await filmmarkenSuchen(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
@@ -536,7 +536,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, true);
+ await markenSuchen(eintrag.id, 'Alles');
hinweisZeigen('Vorspann und Abspann werden gesucht. '
+ 'Der Server liest dafür jede Folge einmal durch — das dauert ein paar Minuten.');
}));
diff --git a/web/app/src/views/einstellungen/mediatheken.ts b/web/app/src/views/einstellungen/mediatheken.ts
index 598da84..d1298d9 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,
+ mediathekMarken, type Markenumfang,
scanstand, metadatenAuffrischen, vermisste, vermisstesEntfernen, type VermissteDatei,
pruefungSetzen, mediathekZuruecksetzen, type Bibliothek,
} from '../../api';
@@ -227,22 +227,45 @@ 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.
- const marken = knopf('Vorspann und Abspann suchen', 'btn');
- marken.title = 'Sucht überspringbare Abschnitte in allen Titeln dieser Mediathek.';
- marken.onclick = () => nachfragen({
- titel: `Vorspann und Abspann in „${aktuell.name}" suchen?`,
- text: 'Der Server liest dafür von jeder Folge einmal die Tonspur und sieht '
- + 'sich bei Filmen die letzten zwanzig Minuten an. Über einen grossen '
- + 'Bestand dauert das Stunden — es läuft im Hintergrund weiter, und im '
- + 'Protokoll von heute steht, wie weit es ist. Schon durchsuchte Titel '
- + 'werden noch einmal geprüft.',
- bestaetigen: 'Suchen',
- tun: async () => {
- laufendSetzen(true);
- await mediathekMarken(aktuell.id, true);
+ /** Startet einen Markenlauf und hängt die Standzeile daran. */
+ function markenlauf(umfang: Markenumfang): void {
+ laufendSetzen(true);
+ void mediathekMarken(aktuell.id, umfang).then(() => {
hinweisZeigen('Die Suche läuft. Der Stand steht hier und im Protokoll von heute.');
void verfolgen(false);
- },
+ });
+ }
+
+ // **Zwei Knöpfe, weil es zwei Fragen sind.**
+ //
+ // „Was fehlt noch?" ist die alltägliche: nach jeder Verbesserung an
+ // der Erkennung will man die Titel noch einmal, bei denen nichts
+ // 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?`,
+ 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'),
+ });
+
+ 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?`,
+ 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'),
});
const umbenennen = knopf('Umbenennen', 'btn');
@@ -379,7 +402,8 @@ export function bibliotheksbereich(
// und danach nachzusehen. Der Knopf heisst „Seitenleiste bearbeiten".
const knoepfe = document.createElement('div');
knoepfe.className = 'knopfreihe';
- knoepfe.append(los, auffrischen, marken, pruefung, umbenennen, zuruecksetzen, entfernen);
+ knoepfe.append(los, auffrischen, marken, markenAlle, pruefung, umbenennen,
+ zuruecksetzen, entfernen);
// **Nur wenn hier wirklich etwas liegt.** Ein Knopf, der nichts zu tun
// hat, ist schlechter als keiner — und die Zahl daneben sagt, worum es