diff --git a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs index 00bbd7b..cddb4df 100644 --- a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs @@ -913,6 +913,21 @@ public static class LibraryContentEndpoints : Einwand.Belegt("Für diese Serie werden gerade Marken gesucht."); }).RequireAuthorization(FlaroPolicies.Administrator); + // **Eine ganze Mediathek auf einmal.** Je Serie einen Knopf zu + // drücken ist bei fünfundneunzig Serien kein Angebot — und nach + // jeder Änderung an der Erkennung will man ohnehin alles noch + // 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) => + { + if (!await db.Libraries.AsNoTracking().AnyAsync(x => x.Id == id)) + return Results.NotFound(); + + erkennung.MediathekStarten(id, erneut); + return Results.Accepted(); + }).RequireAuthorization(FlaroPolicies.Administrator); + app.MapGet("/api/v1/episodes/{id:guid}/next", (Guid id, ClaimsPrincipal wer, FlaroContext db) => Nachbarfolge(id, wer, db, vorwaerts: true)).RequireAuthorization(); diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs index da9c3fc..395f9fb 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Markenerkennung.cs @@ -124,7 +124,8 @@ public sealed class Markenerkennung( /// nichts mehr zu tun und ist in Sekunden vorbei. /// /// - public async Task NachScanAsync(Guid mediathek, CancellationToken ct = default) + public async Task NachScanAsync( + Guid mediathek, bool erneut = false, CancellationToken ct = default) { using var raum = scopes.CreateScope(); var db = raum.ServiceProvider.GetRequiredService(); @@ -145,7 +146,7 @@ public sealed class Markenerkennung( { if (ct.IsCancellationRequested) return; if (!_laufend.TryAdd(serie, true)) continue; - try { await LaufenAsync(serie, erneut: false, ct); } + try { await LaufenAsync(serie, erneut, ct); } catch (Exception fehler) { log.LogWarning(fehler, "Markenerkennung für {Serie} abgebrochen.", serie); @@ -153,11 +154,33 @@ public sealed class Markenerkennung( finally { _laufend.TryRemove(serie, out _); } } - await FilmeAsync(db, mediathek, ct); + await FilmeAsync(db, mediathek, erneut, ct); log.LogInformation("Marken für Mediathek {Kennung} durch.", mediathek); } + /// Startet den Lauf für eine ganze Mediathek — im Hintergrund. + /// + /// + /// Der Knopf darf nicht warten. Über fünfundneunzig Serien + /// 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. + /// + public void MediathekStarten(Guid mediathek, bool erneut) + { + _ = Task.Run(async () => + { + try { await NachScanAsync(mediathek, erneut, CancellationToken.None); } + catch (Exception fehler) + { + log.LogWarning(fehler, + "Markenlauf für Mediathek {Kennung} abgebrochen.", mediathek); + } + }); + } + /// Die Filme einer Mediathek, denen noch ein Abspann fehlt. /// /// @@ -165,11 +188,15 @@ public sealed class Markenerkennung( /// eine Mediathek immer nur eines von beidem enthält. Die Schleife /// läuft dann ins Leere, und das kostet eine Abfrage. /// - private async Task FilmeAsync(FlaroContext db, Guid mediathek, CancellationToken ct) + private async Task FilmeAsync( + FlaroContext db, Guid mediathek, bool erneut, CancellationToken ct) { var offen = await db.Movies.AsNoTracking() .Where(m => m.LibraryId == mediathek) - .Where(m => !db.Markensuchen.Any(x => x.MovieId == m.Id && x.Art == Markenart.Abspann)) + // 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)) .Select(m => m.Id) .ToListAsync(ct); if (offen.Count == 0) return; @@ -180,7 +207,7 @@ public sealed class Markenerkennung( foreach (var film in offen) { if (ct.IsCancellationRequested) return; - try { if (await FilmAsync(film, erneut: false, ct)) gefunden++; } + try { if (await FilmAsync(film, erneut, ct)) gefunden++; } catch (Exception fehler) { log.LogWarning(fehler, "Abspannsuche für Film {Film} abgebrochen.", film); diff --git a/web/app/src/api.ts b/web/app/src/api.ts index bc75e02..1a0490a 100644 --- a/web/app/src/api.ts +++ b/web/app/src/api.ts @@ -675,6 +675,14 @@ export async function markenHolen(titelId: string): Promise { } } +/** Sucht Vorspann und Abspann in einer ganzen Mediathek. + * + * **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' }); + /** Nimmt die Marken eines Films wieder weg — wenn die Erkennung danebenlag. */ export const filmmarkenVerwerfen = (filmId: string) => anfrage(`/movies/${filmId}/marken`, { method: 'DELETE' }); diff --git a/web/app/src/views/einstellungen/mediatheken.ts b/web/app/src/views/einstellungen/mediatheken.ts index 2ca899f..5101689 100644 --- a/web/app/src/views/einstellungen/mediatheken.ts +++ b/web/app/src/views/einstellungen/mediatheken.ts @@ -1,9 +1,10 @@ import { bibliotheken, bibliothekAnlegen, bibliothekUmbenennen, bibliothekLoeschen, scanStarten, + mediathekMarken, scanstand, metadatenAuffrischen, vermisste, vermisstesEntfernen, type VermissteDatei, pruefungSetzen, mediathekZuruecksetzen, type Bibliothek, } from '../../api'; -import { esc, knopf, fehlerzeile, leer, meldung } from '../../util'; +import { esc, knopf, fehlerzeile, leer, meldung, hinweisZeigen} from '../../util'; import { dialog, nachfragen } from '../../dialog'; import { abschnitt } from './teile'; @@ -222,6 +223,26 @@ export function bibliotheksbereich( }, }); + // **Je Serie einen Knopf zu drücken ist bei fünfundneunzig kein + // 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 () => { + await mediathekMarken(aktuell.id, true); + hinweisZeigen('Die Suche läuft. Das Protokoll von heute zeigt den Fortschritt.'); + }, + }); + const umbenennen = knopf('Umbenennen', 'btn'); umbenennen.onclick = () => dialog({ titel: 'Mediathek umbenennen', @@ -347,7 +368,7 @@ export function bibliotheksbereich( // und danach nachzusehen. Der Knopf heisst „Seitenleiste bearbeiten". const knoepfe = document.createElement('div'); knoepfe.className = 'knopfreihe'; - knoepfe.append(los, auffrischen, pruefung, umbenennen, zuruecksetzen, entfernen); + knoepfe.append(los, auffrischen, marken, 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