diff --git a/server/api/scan.ts b/server/api/scan.ts index b41eba9..8a47bf9 100644 --- a/server/api/scan.ts +++ b/server/api/scan.ts @@ -34,8 +34,8 @@ app.get('/', (c) => { const scanned = (db.prepare("SELECT COUNT(*) as n FROM media_items WHERE scan_status = 'scanned'").get() as { n: number }).n; const errors = (db.prepare("SELECT COUNT(*) as n FROM media_items WHERE scan_status = 'error'").get() as { n: number }).n; const recentItems = db.prepare( - 'SELECT name, type, scan_status FROM media_items ORDER BY last_scanned_at DESC LIMIT 50' - ).all() as { name: string; type: string; scan_status: string }[]; + 'SELECT name, type, scan_status, file_path FROM media_items ORDER BY last_scanned_at DESC LIMIT 50' + ).all() as { name: string; type: string; scan_status: string; file_path: string }[]; return c.json({ running, progress: { scanned, total, errors }, recentItems, scanLimit: currentScanLimit() }); }); @@ -134,21 +134,7 @@ async function runScan(limit: number | null = null): Promise { let processed = 0; let errors = 0; - let total = isDev ? 250 : 0; - - if (!isDev) { - try { - const countUrl = new URL(`${jellyfinCfg.url}/Users/${jellyfinCfg.userId}/Items`); - countUrl.searchParams.set('Recursive', 'true'); - countUrl.searchParams.set('IncludeItemTypes', 'Movie,Episode'); - countUrl.searchParams.set('Limit', '1'); - const countRes = await fetch(countUrl.toString(), { headers: { 'X-Emby-Token': jellyfinCfg.apiKey } }); - if (countRes.ok) { - const body = (await countRes.json()) as { TotalRecordCount: number }; - total = limit != null ? Math.min(limit, body.TotalRecordCount) : body.TotalRecordCount; - } - } catch { /* ignore */ } - } + let total = 0; const upsertItem = db.prepare(` INSERT INTO media_items ( @@ -191,7 +177,11 @@ async function runScan(limit: number | null = null): Promise { const getPlanByItemId = db.prepare('SELECT id FROM review_plans WHERE item_id = ?'); const getStreamsByItemId = db.prepare('SELECT * FROM media_streams WHERE item_id = ?'); - const itemSource = isDev ? getDevItems(jellyfinCfg) : getAllItems(jellyfinCfg); + const itemSource = isDev + ? getDevItems(jellyfinCfg) + : getAllItems(jellyfinCfg, (_fetched, jellyfinTotal) => { + total = limit != null ? Math.min(limit, jellyfinTotal) : jellyfinTotal; + }); for await (const jellyfinItem of itemSource) { if (signal.aborted) break; if (!isDev && limit != null && processed >= limit) break; @@ -248,12 +238,12 @@ async function runScan(limit: number | null = null): Promise { const planRow = getPlanByItemId.get(itemId) as { id: number }; for (const dec of analysis.decisions) upsertDecision.run(planRow.id, dec.stream_id, dec.action, dec.target_index); - emitSse('log', { name: jellyfinItem.Name, type: jellyfinItem.Type, status: 'scanned' }); + emitSse('log', { name: jellyfinItem.Name, type: jellyfinItem.Type, status: 'scanned', file: jellyfinItem.Path }); } catch (err) { errors++; logError(`Error scanning ${jellyfinItem.Name}:`, err); try { db.prepare("UPDATE media_items SET scan_status = 'error', scan_error = ? WHERE jellyfin_id = ?").run(String(err), jellyfinItem.Id); } catch { /* ignore */ } - emitSse('log', { name: jellyfinItem.Name, type: jellyfinItem.Type, status: 'error' }); + emitSse('log', { name: jellyfinItem.Name, type: jellyfinItem.Type, status: 'error', file: jellyfinItem.Path }); } } diff --git a/src/features/scan/ScanPage.tsx b/src/features/scan/ScanPage.tsx index 87a1f8a..ee7a777 100644 --- a/src/features/scan/ScanPage.tsx +++ b/src/features/scan/ScanPage.tsx @@ -3,8 +3,8 @@ import { api } from '~/shared/lib/api'; import { Button } from '~/shared/components/ui/button'; import { Badge } from '~/shared/components/ui/badge'; -interface ScanStatus { running: boolean; progress: { scanned: number; total: number; errors: number }; recentItems: { name: string; type: string; scan_status: string }[]; scanLimit: number | null; } -interface LogEntry { name: string; type: string; status: string; } +interface ScanStatus { running: boolean; progress: { scanned: number; total: number; errors: number }; recentItems: { name: string; type: string; scan_status: string; file_path: string }[]; scanLimit: number | null; } +interface LogEntry { name: string; type: string; status: string; file?: string; } // Mutable buffer for SSE data — flushed to React state on an interval interface SseBuf { @@ -93,7 +93,7 @@ export function ScanPage() { setErrors(s.progress.errors); setStatusLabel(s.running ? 'Scan in progress…' : 'Scan idle'); if (s.scanLimit != null) setLimit(String(s.scanLimit)); - setLog(s.recentItems.map((i) => ({ name: i.name, type: i.type, status: i.scan_status }))); + setLog(s.recentItems.map((i) => ({ name: i.name, type: i.type, status: i.scan_status, file: i.file_path }))); }; useEffect(() => { load(); }, []); @@ -204,13 +204,15 @@ export function ScanPage() { {errors > 0 && {errors} error(s)} - {(running || progressTotal > 0) && ( + {(running || progressScanned > 0) && ( <> -
-
-
+ {progressTotal > 0 && ( +
+
+
+ )}
- {progressScanned} / {progressTotal} + {progressScanned}{progressTotal > 0 ? ` / ${progressTotal}` : ''} scanned {currentItem && {currentItem}}
@@ -222,21 +224,24 @@ export function ScanPage() { - {['Type', 'Name', 'Status'].map((h) => ( + {['Type', 'File', 'Status'].map((h) => ( ))} - {log.map((item, i) => ( - - - - - - ))} + {log.map((item, i) => { + const fileName = item.file ? item.file.split('/').pop() ?? item.name : item.name; + return ( + + + + + + ); + })}
{h}
{item.type}{item.name} - {item.status} -
{item.type}{fileName} + {item.status} +