library: rename Scan nav/page to Library, show audio codecs per row
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m4s

Per-row audio codec summary (distinct lowercased codecs across an
item's audio streams) via scalar subquery on media_streams, rendered
as "ac3 · aac" in a new monospace Audio column.

v2026.04.15.9

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 19:10:00 +02:00
parent a2bdecd298
commit 7d30e6c1a6
4 changed files with 34 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "netfelix-audio-fix", "name": "netfelix-audio-fix",
"version": "2026.04.15.8", "version": "2026.04.15.9",
"scripts": { "scripts": {
"dev:server": "NODE_ENV=development bun --hot server/index.tsx", "dev:server": "NODE_ENV=development bun --hot server/index.tsx",
"dev:client": "vite", "dev:client": "vite",

View File

@@ -163,7 +163,11 @@ app.get("/items", (c) => {
` `
SELECT id, jellyfin_id, name, type, series_name, season_number, episode_number, SELECT id, jellyfin_id, name, type, series_name, season_number, episode_number,
scan_status, original_language, orig_lang_source, container, file_size, file_path, scan_status, original_language, orig_lang_source, container, file_size, file_path,
last_scanned_at, ingest_source last_scanned_at, ingest_source,
(SELECT GROUP_CONCAT(DISTINCT LOWER(codec))
FROM media_streams
WHERE item_id = media_items.id AND type = 'Audio' AND codec IS NOT NULL
) AS audio_codecs
FROM media_items FROM media_items
${where.sql} ${where.sql}
ORDER BY COALESCE(last_scanned_at, created_at) DESC, id DESC ORDER BY COALESCE(last_scanned_at, created_at) DESC, id DESC
@@ -186,6 +190,7 @@ app.get("/items", (c) => {
file_path: string; file_path: string;
last_scanned_at: string | null; last_scanned_at: string | null;
ingest_source: string | null; ingest_source: string | null;
audio_codecs: string | null;
}>; }>;
const total = (db.prepare(`SELECT COUNT(*) as n FROM media_items ${where.sql}`).get(...where.args) as { n: number }).n; const total = (db.prepare(`SELECT COUNT(*) as n FROM media_items ${where.sql}`).get(...where.args) as { n: number }).n;
return c.json({ rows, total, hasMore: query.offset + rows.length < total, query }); return c.json({ rows, total, hasMore: query.offset + rows.length < total, query });

View File

@@ -37,6 +37,7 @@ interface ScanItemsRow {
file_path: string; file_path: string;
last_scanned_at: string | null; last_scanned_at: string | null;
ingest_source: "scan" | "webhook" | null; ingest_source: "scan" | "webhook" | null;
audio_codecs: string | null;
} }
interface ScanItemsResponse { interface ScanItemsResponse {
@@ -397,7 +398,7 @@ export function ScanPage() {
return ( return (
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-bold m-0">Scan</h1> <h1 className="text-xl font-bold m-0">Library</h1>
<MqttBadge /> <MqttBadge />
</div> </div>
@@ -581,22 +582,32 @@ export function ScanPage() {
<table className="w-full border-collapse text-[0.8rem]"> <table className="w-full border-collapse text-[0.8rem]">
<thead> <thead>
<tr> <tr>
{["Scanned", "Name", "Type", "Series / Ep", "Language", "Container", "Size", "Source", "Status", "Path"].map( {[
(h) => ( "Scanned",
"Name",
"Type",
"Series / Ep",
"Language",
"Audio",
"Container",
"Size",
"Source",
"Status",
"Path",
].map((h) => (
<th <th
key={h} key={h}
className="text-left text-[0.66rem] font-bold uppercase tracking-[0.05em] text-gray-500 py-1 px-2 border-b border-gray-200 whitespace-nowrap" className="text-left text-[0.66rem] font-bold uppercase tracking-[0.05em] text-gray-500 py-1 px-2 border-b border-gray-200 whitespace-nowrap"
> >
{h} {h}
</th> </th>
), ))}
)}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{itemsRows.length === 0 && !itemsLoading && ( {itemsRows.length === 0 && !itemsLoading && (
<tr> <tr>
<td colSpan={10} className="py-3 px-2 text-gray-400"> <td colSpan={11} className="py-3 px-2 text-gray-400">
No items match the current filters. No items match the current filters.
</td> </td>
</tr> </tr>
@@ -616,6 +627,9 @@ export function ScanPage() {
<div>{row.original_language ?? "—"}</div> <div>{row.original_language ?? "—"}</div>
<div className="text-[0.68rem] text-gray-500">{row.orig_lang_source ?? "—"}</div> <div className="text-[0.68rem] text-gray-500">{row.orig_lang_source ?? "—"}</div>
</td> </td>
<td className="py-1.5 px-2 border-b border-gray-100 font-mono text-[0.72rem]">
{row.audio_codecs ? row.audio_codecs.split(",").join(" · ") : "—"}
</td>
<td className="py-1.5 px-2 border-b border-gray-100">{row.container ?? "—"}</td> <td className="py-1.5 px-2 border-b border-gray-100">{row.container ?? "—"}</td>
<td className="py-1.5 px-2 border-b border-gray-100 whitespace-nowrap">{formatFileSize(row.file_size)}</td> <td className="py-1.5 px-2 border-b border-gray-100 whitespace-nowrap">{formatFileSize(row.file_size)}</td>
<td className="py-1.5 px-2 border-b border-gray-100"> <td className="py-1.5 px-2 border-b border-gray-100">

View File

@@ -65,7 +65,7 @@ function RootLayout() {
<VersionBadge /> <VersionBadge />
<div className="flex flex-wrap items-center gap-0.5"> <div className="flex flex-wrap items-center gap-0.5">
<NavLink to="/" exact> <NavLink to="/" exact>
Scan Library
</NavLink> </NavLink>
<NavLink to="/pipeline">Pipeline</NavLink> <NavLink to="/pipeline">Pipeline</NavLink>
<NavLink to="/review/subtitles">Subtitles</NavLink> <NavLink to="/review/subtitles">Subtitles</NavLink>