Einen einzelnen Titel neu holen, statt achthundert

Eine neue Quelle wirkt erst beim nächsten Holen der Metadaten. Das ging
bisher nur je Mediathek: achthundert Titel, eine Viertelstunde — nur um
an einem Film zu sehen, ob ein Schlüssel etwas bringt. Wer auf einer
Detailseite bemerkt, dass IMDb und Rotten Tomatoes fehlen, hatte von dort
keinen Weg dorthin.

Der Server kann eine Auswahl längst; es fehlte bloss die Kennung der
Mediathek in der Antwort, damit die Seite weiss, wohin ihr Titel gehört.
Der runde Knopf holt diesen einen Titel, wartet den Lauf ab und zeichnet
die Seite neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-09 16:02:39 +02:00
co-authored by Claude Opus 5
parent 18db89399d
commit e05bbfc2cd
5 changed files with 75 additions and 5 deletions
+22 -2
View File
@@ -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.
/// </remarks>
IReadOnlyList<Fremdwertungsantwort>? ExternalRatings = null);
IReadOnlyList<Fremdwertungsantwort>? ExternalRatings = null,
/// <summary>Zu welcher Mediathek der Titel gehört.</summary>
///
/// <remarks>
/// <b>Damit die Detailseite einen einzelnen Titel auffrischen kann.</b>
/// 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.
/// </remarks>
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.
/// </remarks>
IReadOnlyList<Fremdwertungsantwort>? ExternalRatings = null);
IReadOnlyList<Fremdwertungsantwort>? ExternalRatings = null,
/// <summary>Zu welcher Mediathek der Titel gehört.</summary>
///
/// <remarks>
/// <b>Damit die Detailseite einen einzelnen Titel auffrischen kann.</b>
/// 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.
/// </remarks>
Guid? LibraryId = null);
public sealed record EpisodeResponse(
Guid Id, int Season, int Episode, string? Title, string? Overview, string? StillImage,
@@ -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.**
+8
View File
@@ -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;
+2
View File
@@ -39,6 +39,8 @@ export const icons = {
abspielen: rahmen('<path d="M8 5.5v13l11-6.5z"/>', true),
haken: rahmen('<path d="m5 12.5 4.5 4.5L19 7"/>'),
hakenKreis: rahmen('<circle cx="12" cy="12" r="9"/><path d="m8 12.3 2.8 2.7L16 9.6"/>'),
// Kreisbogen mit Pfeilspitze — das gewohnte Zeichen fürs Neuholen.
auffrischen: rahmen('<path d="M20 12a8 8 0 1 1-2.4-5.7"/><path d="M20 3.5V9h-5.5"/>'),
stift: rahmen('<path d="M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z"/>'),
download: rahmen('<path d="M12 3v12"/><path d="m7.5 11 4.5 4.5L16.5 11"/><path d="M4 20h16"/>'),
links: rahmen('<path d="M15 5 8 12l7 7"/>'),
+39 -1
View File
@@ -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<void> {
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