task modal: audio info panel for Get Lyrics (title/artist/length/lyrics)
Display-only panel at the top of the Get Lyrics run-task form (single file only):
- Backend: GET /file-browser/audio-meta?path= — ffprobe format tags + duration,
plus a second probe for embedded lyrics (USLT/SYLT/lyrics* keys, case-insensitive).
Returns { title, artist, duration, hasLyrics }; tolerant of missing tags/probe
failures.
- Client: files.audioMeta(path) + AudioMeta type in useFilesAPI.
- TaskRunnerModal: prefetch audioMeta for get-lyrics single-file runs (bypasses
the hasTrackPickers early-return, error-tolerant), and render AudioMetaPanel
above TaskInputForm — title/artist + a muted length + Lyrics: Yes/No chip,
filename fallback. Directories + other tasks unaffected (no panel, no probing).
Verified ffprobe logic on a real embedded-lyrics file. tsgo clean; formatted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -454,6 +454,74 @@ router.get('/audio-tracks', async (ctx) => {
|
||||
return ctx.json(tracks);
|
||||
});
|
||||
|
||||
// Read-only audio metadata for the Get Lyrics run-task panel: title/artist/duration + whether the file
|
||||
// already has embedded lyrics. Tolerant of missing tags / probe failures (defaults to empty/0/false).
|
||||
router.get('/audio-meta', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined);
|
||||
const relPath = (ctx.req.query('path') || '').replace(/^\/+/, '');
|
||||
if (!relPath) throw errors.BAD_REQUEST('path is required');
|
||||
const absPath = resolveUserPath(rootDir, relPath);
|
||||
|
||||
// Format tags + duration.
|
||||
const fmtProc = Bun.spawn(
|
||||
[
|
||||
'ffprobe',
|
||||
'-v',
|
||||
'error',
|
||||
'-show_entries',
|
||||
'format=duration:format_tags=title,artist,album,TITLE,ARTIST',
|
||||
'-of',
|
||||
'json',
|
||||
absPath,
|
||||
],
|
||||
{ stdout: 'pipe', stderr: 'ignore' },
|
||||
);
|
||||
const fmtOut = await new Response(fmtProc.stdout).text();
|
||||
await fmtProc.exited;
|
||||
|
||||
let format: { duration?: string; tags?: Record<string, string> } = {};
|
||||
try {
|
||||
format = (JSON.parse(fmtOut) as { format?: typeof format }).format ?? {};
|
||||
} catch {
|
||||
format = {};
|
||||
}
|
||||
const tags: Record<string, string> = {};
|
||||
for (const [k, val] of Object.entries(format.tags ?? {})) tags[k.toLowerCase()] = val; // ID3 case varies
|
||||
const durNum = format.duration ? parseFloat(format.duration) : 0;
|
||||
|
||||
// ID3 lyrics frames (USLT/SYLT) don't reliably surface in format_tags — probe stream+format tags and
|
||||
// flag lyrics if any key matches USLT/SYLT/lyrics (case-insensitive; some muxers emit `lyrics-XXX`).
|
||||
const lyrProc = Bun.spawn(
|
||||
['ffprobe', '-v', 'error', '-show_entries', 'stream_tags:format_tags', '-of', 'json', absPath],
|
||||
{ stdout: 'pipe', stderr: 'ignore' },
|
||||
);
|
||||
const lyrOut = await new Response(lyrProc.stdout).text();
|
||||
await lyrProc.exited;
|
||||
|
||||
let hasLyrics = false;
|
||||
try {
|
||||
const data = JSON.parse(lyrOut) as {
|
||||
format?: { tags?: Record<string, string> };
|
||||
streams?: Array<{ tags?: Record<string, string> }>;
|
||||
};
|
||||
const keys = [
|
||||
...Object.keys(data.format?.tags ?? {}),
|
||||
...(data.streams ?? []).flatMap((s) => Object.keys(s.tags ?? {})),
|
||||
];
|
||||
hasLyrics = keys.some((k) => /uslt|sylt|lyrics/i.test(k));
|
||||
} catch {
|
||||
hasLyrics = false;
|
||||
}
|
||||
|
||||
return ctx.json({
|
||||
title: tags.title ?? '',
|
||||
artist: tags.artist ?? '',
|
||||
duration: Number.isFinite(durNum) ? durNum : 0,
|
||||
hasLyrics,
|
||||
});
|
||||
});
|
||||
|
||||
// Recursively probe a folder's videos and group them by track layout, so the task runner can offer
|
||||
// one set of audio/subtitle pickers for a whole season when every episode matches — and flag the
|
||||
// odd files out when they don't. Two files "match" when their audio (language + channel count) and
|
||||
|
||||
Reference in New Issue
Block a user