Add daemon restart, preview diagnostics, browse caching, and rate-limit pauses
This commit is contained in:
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user