diff --git a/server/src/Flaro.Api/Endpoints/Contracts.cs b/server/src/Flaro.Api/Endpoints/Contracts.cs index 44b0851..b045c10 100644 --- a/server/src/Flaro.Api/Endpoints/Contracts.cs +++ b/server/src/Flaro.Api/Endpoints/Contracts.cs @@ -445,7 +445,17 @@ public sealed record MovieResponse( /// Wertung von TMDB und bleibt, wo es ist; ein Client, der nur das /// kennt, zeigt weiter dasselbe wie bisher. /// - IReadOnlyList? ExternalRatings = null); + IReadOnlyList? ExternalRatings = null, + + /// Zu welcher Mediathek der Titel gehört. + /// + /// + /// Damit die Detailseite einen einzelnen Titel auffrischen kann. + /// Das Auffrischen hängt an der Mediathek, nicht am Titel; ohne diese + /// Kennung müsste der Client erst alle Mediatheken durchgehen, um zu + /// erfahren, wohin dieser eine Film gehört. + /// + Guid? LibraryId = null); public sealed record CreditResponse( Guid Id, string Name, string Role, string? Character, string? Image); @@ -516,7 +526,17 @@ public sealed record SeriesResponse( /// Wertung von TMDB und bleibt, wo es ist; ein Client, der nur das /// kennt, zeigt weiter dasselbe wie bisher. /// - IReadOnlyList? ExternalRatings = null); + IReadOnlyList? ExternalRatings = null, + + /// Zu welcher Mediathek der Titel gehört. + /// + /// + /// Damit die Detailseite einen einzelnen Titel auffrischen kann. + /// Das Auffrischen hängt an der Mediathek, nicht am Titel; ohne diese + /// Kennung müsste der Client erst alle Mediatheken durchgehen, um zu + /// erfahren, wohin dieser eine Film gehört. + /// + Guid? LibraryId = null); public sealed record EpisodeResponse( Guid Id, int Season, int Episode, string? Title, string? Overview, string? StillImage, diff --git a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs index b9bccd0..c359bbb 100644 --- a/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/LibraryContentEndpoints.cs @@ -655,7 +655,8 @@ public static class LibraryContentEndpoints SortTitle: film.SortTitleValue, SortTitlePinned: film.SortTitlePinned, PublishAt: film.PublishAt, - ExternalRatings: await FremdwertungenZu(db, film: id, serie: null))); + ExternalRatings: await FremdwertungenZu(db, film: id, serie: null), + LibraryId: film.LibraryId)); }).RequireAuthorization(); app.MapGet("/api/v1/series/{id:guid}", async ( @@ -691,7 +692,8 @@ public static class LibraryContentEndpoints SortTitle: serie.SortTitleValue, SortTitlePinned: serie.SortTitlePinned, PublishAt: folgen.Min(f => f.PublishAt), - ExternalRatings: await FremdwertungenZu(db, film: null, serie: id))); + ExternalRatings: await FremdwertungenZu(db, film: null, serie: id), + LibraryId: serie.LibraryId)); }).RequireAuthorization(); // **Eine Folge für sich.** diff --git a/web/app/src/api.ts b/web/app/src/api.ts index 38014dd..65559d6 100644 --- a/web/app/src/api.ts +++ b/web/app/src/api.ts @@ -81,6 +81,10 @@ export type Film = { sortTitle: string | null; /** Ob der Sortiertitel von Hand gesetzt wurde. */ sortTitlePinned: boolean; + /** Zu welcher Mediathek der Titel gehört — nur beim Einzelabruf. + * + * Das Auffrischen hängt an der Mediathek, nicht am Titel. */ + libraryId: string | null; }; export type Serie = { id: string; title: string; year: number | null; overview: string | null; @@ -104,6 +108,10 @@ export type Serie = { titlePinned: boolean; sortTitle: string | null; sortTitlePinned: boolean; + /** Zu welcher Mediathek der Titel gehört — nur beim Einzelabruf. + * + * Das Auffrischen hängt an der Mediathek, nicht am Titel. */ + libraryId: string | null; }; export type Folge = { id: string; season: number; episode: number; diff --git a/web/app/src/icons.ts b/web/app/src/icons.ts index 14f21f6..cbd3fef 100644 --- a/web/app/src/icons.ts +++ b/web/app/src/icons.ts @@ -39,6 +39,8 @@ export const icons = { abspielen: rahmen('', true), haken: rahmen(''), hakenKreis: rahmen(''), + // Kreisbogen mit Pfeilspitze — das gewohnte Zeichen fürs Neuholen. + auffrischen: rahmen(''), stift: rahmen(''), download: rahmen(''), links: rahmen(''), diff --git a/web/app/src/views/detail.ts b/web/app/src/views/detail.ts index c6a402f..5135620 100644 --- a/web/app/src/views/detail.ts +++ b/web/app/src/views/detail.ts @@ -2,7 +2,7 @@ import { ApiFehler, film as filmLaden, serie as serieLaden, folgen, vorschlaege, zuordnen, bildAdresse, istAdmin, gesehen, favorit, weglegen, type Film, type Serie, type Folge, type Datei, type Stand, - type Mitwirkender, type Spur, aehnliche, + type Mitwirkender, type Spur, aehnliche, metadatenAuffrischen, scanstand, } from '../api'; import { esc, dauer, groesse, knopf, feld, fehlerzeile, leer, anteil, meldung, hinweisZeigen, @@ -61,6 +61,26 @@ function rang(quelle: string): number { return stelle < 0 ? QUELLENFOLGE.length : stelle; } +/** Wartet, bis der angestossene Lauf durch ist. + * + * **Ein einzelner Titel dauert Sekunden.** Ein Fortschrittsbalken dafür + * wäre mehr Gerät als Nutzen; es reicht, die Seite danach neu zu zeichnen. + * + * **Nach einer Minute wird nicht weiter gewartet.** So lange kann es nur + * dauern, wenn etwas anderes läuft — ein ganzer Scan etwa. Dann ist weiter + * zu warten sinnloser, als zu zeigen, was gerade dasteht. */ +async function laufAbwarten(mediathek: string): Promise { + for (let i = 0; i < 60; i++) { + await new Promise(fertig => window.setTimeout(fertig, 1000)); + try { + if ((await scanstand(mediathek)).state !== 'running') return; + } catch { + // Antwortet der Stand nicht, ist Weiterwarten geraten statt gewusst. + return; + } + } +} + export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElement { const wurzel = document.createElement('div'); wurzel.className = 'detail'; @@ -417,6 +437,24 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem async () => { await weglegen(eintrag.id); await laden(); })); } + // **Diesen einen Titel neu holen, nicht den ganzen Bestand.** + // + // Eine neue Quelle — fanart.tv, OMDb — wirkt erst beim nächsten Holen. + // Bisher ging das nur je Mediathek: achthundert Titel, eine + // Viertelstunde, nur um an einem Film zu sehen, ob ein Schlüssel etwas + // bringt. Der Server kann längst eine Auswahl; es fehlte bloss der Weg + // dorthin von der Seite, auf der man das Fehlen bemerkt. + if (istAdmin() && eintrag.libraryId) { + const mediathek = eintrag.libraryId; + reihe.appendChild(rundKnopf('auffrischen', false, 'Metadaten neu holen', + async () => { + await metadatenAuffrischen(mediathek, [eintrag.id]); + hinweisZeigen('Metadaten werden neu geholt …'); + await laufAbwarten(mediathek); + await laden(); + })); + } + // **Nicht über `rundKnopf`.** Der schaltet sich beim Klick ab und erst im // Fehlerfall wieder an — richtig für etwas, das danach die Seite neu lädt. // Ein Dialog kehrt sofort zurück; der Knopf bliebe für den Rest der Seite