add paths page to check volume accessibility after scan
All checks were successful
Build and Push Docker Image / build (push) Successful in 38s
All checks were successful
Build and Push Docker Image / build (push) Successful in 38s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
35
server/api/paths.ts
Normal file
35
server/api/paths.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { Hono } from 'hono';
|
||||
import { getDb } from '../db/index';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
interface PathInfo {
|
||||
prefix: string;
|
||||
itemCount: number;
|
||||
accessible: boolean;
|
||||
}
|
||||
|
||||
app.get('/', (c) => {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.query<{ prefix: string; count: number }, []>(
|
||||
`SELECT substr(file_path, 1, instr(substr(file_path, 2), '/') + 1) AS prefix,
|
||||
COUNT(*) AS count
|
||||
FROM media_items
|
||||
WHERE file_path IS NOT NULL AND file_path != ''
|
||||
GROUP BY prefix
|
||||
ORDER BY prefix`,
|
||||
)
|
||||
.all();
|
||||
|
||||
const paths: PathInfo[] = rows.map((r: { prefix: string; count: number }) => ({
|
||||
prefix: r.prefix,
|
||||
itemCount: r.count,
|
||||
accessible: existsSync(r.prefix),
|
||||
}));
|
||||
|
||||
return c.json({ paths });
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -11,6 +11,7 @@ import executeRoutes from './api/execute';
|
||||
import nodesRoutes from './api/nodes';
|
||||
import subtitlesRoutes from './api/subtitles';
|
||||
import dashboardRoutes from './api/dashboard';
|
||||
import pathsRoutes from './api/paths';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -41,6 +42,7 @@ app.route('/api/review', reviewRoutes);
|
||||
app.route('/api/execute', executeRoutes);
|
||||
app.route('/api/subtitles', subtitlesRoutes);
|
||||
app.route('/api/nodes', nodesRoutes);
|
||||
app.route('/api/paths', pathsRoutes);
|
||||
|
||||
// ─── Static assets (production: serve Vite build) ────────────────────────────
|
||||
|
||||
|
||||
75
src/features/paths/PathsPage.tsx
Normal file
75
src/features/paths/PathsPage.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '~/shared/lib/api';
|
||||
import { Badge } from '~/shared/components/ui/badge';
|
||||
import { Button } from '~/shared/components/ui/button';
|
||||
|
||||
interface PathInfo {
|
||||
prefix: string;
|
||||
itemCount: number;
|
||||
accessible: boolean;
|
||||
}
|
||||
|
||||
export function PathsPage() {
|
||||
const [paths, setPaths] = useState<PathInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
api.get<{ paths: PathInfo[] }>('/api/paths')
|
||||
.then((d) => setPaths(d.paths))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<h1 className="text-lg font-bold">Paths</h1>
|
||||
<Button variant="secondary" size="sm" onClick={load} disabled={loading}>
|
||||
{loading ? 'Checking…' : 'Refresh'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{paths.length === 0 && !loading && (
|
||||
<p className="text-gray-500">No media items scanned yet. Run a scan first.</p>
|
||||
)}
|
||||
|
||||
{paths.length > 0 && (
|
||||
<>
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 text-xs text-gray-500 uppercase tracking-wide">
|
||||
<th className="py-2 pr-4">Path</th>
|
||||
<th className="py-2 pr-4 text-right">Items</th>
|
||||
<th className="py-2">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paths.map((p) => (
|
||||
<tr key={p.prefix} className="border-b border-gray-100">
|
||||
<td className="py-2 pr-4 font-mono text-sm">{p.prefix}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums">{p.itemCount}</td>
|
||||
<td className="py-2">
|
||||
{p.accessible ? (
|
||||
<Badge variant="keep">Accessible</Badge>
|
||||
) : (
|
||||
<Badge variant="error">Not mounted</Badge>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{paths.some((p) => !p.accessible) && (
|
||||
<p className="mt-4 text-xs text-gray-500">
|
||||
Paths marked "Not mounted" are not reachable from the container.
|
||||
Add a Docker volume mount for the missing path, or configure a path mapping in Settings to translate the Jellyfin path to a local path.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,6 +50,7 @@ function RootLayout() {
|
||||
<VersionBadge />
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
<NavLink to="/scan">Scan</NavLink>
|
||||
<NavLink to="/paths">Paths</NavLink>
|
||||
<NavLink to="/review/audio">Audio</NavLink>
|
||||
<NavLink to="/review/subtitles">Subs</NavLink>
|
||||
<NavLink to="/execute">Execute</NavLink>
|
||||
|
||||
6
src/routes/paths.tsx
Normal file
6
src/routes/paths.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { PathsPage } from '~/features/paths/PathsPage';
|
||||
|
||||
export const Route = createFileRoute('/paths')({
|
||||
component: PathsPage,
|
||||
});
|
||||
Reference in New Issue
Block a user