show file name in scan log, fix progress total by using Jellyfin page callback
All checks were successful
Build and Push Docker Image / build (push) Successful in 33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 33s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> {
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && <Badge variant="error">{errors} error(s)</Badge>}
|
||||
</div>
|
||||
|
||||
{(running || progressTotal > 0) && (
|
||||
{(running || progressScanned > 0) && (
|
||||
<>
|
||||
<div className="bg-gray-200 rounded-full h-1.5 overflow-hidden my-2">
|
||||
<div className="h-full bg-blue-600 rounded-full transition-all duration-300" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
{progressTotal > 0 && (
|
||||
<div className="bg-gray-200 rounded-full h-1.5 overflow-hidden my-2">
|
||||
<div className="h-full bg-blue-600 rounded-full transition-all duration-300" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-gray-500 text-xs">
|
||||
<span>{progressScanned} / {progressTotal}</span>
|
||||
<span>{progressScanned}{progressTotal > 0 ? ` / ${progressTotal}` : ''} scanned</span>
|
||||
{currentItem && <span className="truncate max-w-xs text-gray-400">{currentItem}</span>}
|
||||
</div>
|
||||
</>
|
||||
@@ -222,21 +224,24 @@ export function ScanPage() {
|
||||
<table className="w-full border-collapse text-[0.82rem]">
|
||||
<thead>
|
||||
<tr>
|
||||
{['Type', 'Name', 'Status'].map((h) => (
|
||||
{['Type', 'File', 'Status'].map((h) => (
|
||||
<th key={h} className="text-left text-[0.68rem] font-bold uppercase tracking-[0.06em] text-gray-500 py-1 px-2 border-b-2 border-gray-200 whitespace-nowrap">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{log.map((item, i) => (
|
||||
<tr key={i} className="hover:bg-gray-50">
|
||||
<td className="py-1.5 px-2 border-b border-gray-100">{item.type}</td>
|
||||
<td className="py-1.5 px-2 border-b border-gray-100">{item.name}</td>
|
||||
<td className="py-1.5 px-2 border-b border-gray-100">
|
||||
<Badge variant={item.status as 'error' | 'done' | 'pending'}>{item.status}</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{log.map((item, i) => {
|
||||
const fileName = item.file ? item.file.split('/').pop() ?? item.name : item.name;
|
||||
return (
|
||||
<tr key={i} className="hover:bg-gray-50">
|
||||
<td className="py-1.5 px-2 border-b border-gray-100">{item.type}</td>
|
||||
<td className="py-1.5 px-2 border-b border-gray-100" title={item.file ?? item.name}>{fileName}</td>
|
||||
<td className="py-1.5 px-2 border-b border-gray-100">
|
||||
<Badge variant={item.status as 'error' | 'done' | 'pending'}>{item.status}</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user