Add daemon restart, preview diagnostics, browse caching, and rate-limit pauses

This commit is contained in:
Fisto
2026-09-17 07:33:35 +02:00
parent cba7ae8143
commit dea1724eb3
13 changed files with 367 additions and 56 deletions
+51
View File
@@ -123,6 +123,8 @@ export interface MediaItemResult {
remotePreviewUrl: string | null;
/** Why no preview exists, when a fetch failed (for example "http 404"). */
previewError: string | null;
/** Local animated GIF for hover playback, when the original was cached. */
animUrl: string | null;
localUrl: string | null;
thumbUrl: string | null;
provider: string | null;
@@ -175,6 +177,7 @@ export class MediaService {
log: line => this.deps.log?.(line),
onError: (scrapeId, message) => this.recordError("scrape", `${scrapeId}: ${message}`),
walkCyclePages: number(process.env.VESKSLOP_MEDIA_WALK_CYCLE_PAGES, 5),
rateLimitPauseMs: number(process.env.VESKSLOP_MEDIA_RATELIMIT_PAUSE_MS, 60_000),
download: {
maxFileBytes: number(process.env.VESKSLOP_MEDIA_MAX_FILE_BYTES, 50 * 1024 * 1024),
previewMaxBytes: number(process.env.VESKSLOP_MEDIA_PREVIEW_MAX_BYTES, 5 * 1024 * 1024),
@@ -559,6 +562,17 @@ export class MediaService {
};
}
/** Absolute path for a cached animated GIF, or null. */
getMediaAnim(scrapeId: string, itemId: number): { path: string; contentType: string } | null {
const item = this.getMediaItem(scrapeId, itemId);
if (!item?.anim_hash) return null;
return {
path: join(this.rootDir, scrapeId, "thumbs", `${item.anim_hash}.gif`),
contentType: "image/gif"
};
}
/** Absolute path for a cached preview, or null. */
getMediaThumb(scrapeId: string, itemId: number): { path: string; contentType: string } | null {
const item = this.getMediaItem(scrapeId, itemId);
@@ -653,6 +667,42 @@ export class MediaService {
return totals;
}
/** Preview failures across scrapes with their reasons, newest scrape first. */
previewFailures(
limit = 50
): Array<{
scrapeId: string;
itemId: number;
kind: MediaKind;
provider: string | null;
url: string;
error: string;
}> {
const results: Array<{
scrapeId: string;
itemId: number;
kind: MediaKind;
provider: string | null;
url: string;
error: string;
}> = [];
for (const meta of this.engine.list()) {
if (results.length >= limit) break;
const store = new ScrapeStore(scrapeDir(this.rootDir, meta.id));
try {
for (const row of store.previewFailures(limit - results.length)) {
results.push({ scrapeId: meta.id, itemId: row.id, ...row });
}
} finally {
store.close();
}
}
return results;
}
/** Distinct posters across every scrape, biggest first. */
listAuthors(): Array<{ id: string | null; name: string | null; count: number }> {
const totals = new Map<string, { id: string | null; name: string | null; count: number }>();
@@ -801,6 +851,7 @@ export class MediaService {
playUrl: row.play_url,
remotePreviewUrl: row.preview_url,
previewError: row.preview_error,
animUrl: row.anim_hash ? `/api/media/${meta.id}/${row.id}/anim` : null,
localUrl:
row.file_path && !meta.downloadDir
? `/api/media/${meta.id}/${row.id}/file`