drop the built-in Extract Audio entry, and its cache

Extract Audio now exists as a task, with recursion, multi-select scoping, a
format choice and multi-track handling — none of which the one-shot menu entry
had, since it always produced a single mp3.

/extract-audio stays because the file viewer's button plays its output rather
than saving it beside the video, but it no longer returns a cached file: like
transcription and OCR, it always redoes the work. The path is now named outRel,
since it is an output location rather than a cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
brunorezio
2026-07-26 03:50:47 +01:00
co-authored by Claude Opus 5
parent 9d01000578
commit 0c7c015fc3
4 changed files with 9 additions and 45 deletions
+7 -7
View File
@@ -849,14 +849,14 @@ router.post('/extract-audio', async (ctx) => {
const s = await stat(absPath); const s = await stat(absPath);
if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot extract audio from a directory'); if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot extract audio from a directory');
// No caching: the extraction is always redone and overwrites the previous result. The output still
// lives under the user's data dir because this endpoint backs the file viewer's Extract Audio
// button, which plays it rather than saving it beside the video — the Extract Audio task is what
// writes into the user's folders.
const userDataDir = getUserDataDir(user.email); const userDataDir = getUserDataDir(user.email);
const { dir, name } = parsePath(filePath.replace(/^\/+/, '')); const { dir, name } = parsePath(filePath.replace(/^\/+/, ''));
const cacheRel = dir ? `cache/audio/${dir}/${name}.mp3` : `cache/audio/${name}.mp3`; const outRel = dir ? `cache/audio/${dir}/${name}.mp3` : `cache/audio/${name}.mp3`;
const cacheAbs = resolve(userDataDir, cacheRel); const cacheAbs = resolve(userDataDir, outRel);
if (existsSync(cacheAbs)) {
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data', cached: true });
}
await mkdir(dirname(cacheAbs), { recursive: true }); await mkdir(dirname(cacheAbs), { recursive: true });
@@ -871,7 +871,7 @@ router.post('/extract-audio', async (ctx) => {
throw errors.BAD_REQUEST(stderr.trim() || 'Audio extraction failed'); throw errors.BAD_REQUEST(stderr.trim() || 'Audio extraction failed');
} }
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data', cached: false }); return ctx.json({ audioPath: outRel, audioRoot: 'user-data', cached: false });
}); });
// Extract archive (zip, tar, 7z, rar) into a sibling folder // Extract archive (zip, tar, 7z, rar) into a sibling folder
@@ -62,7 +62,6 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
setRenamingName, setRenamingName,
handleReadAloud, handleReadAloud,
handleOcr, handleOcr,
handleExtractAudio,
handleExtract, handleExtract,
handlePlay, handlePlay,
getMatchingTaskGroups, getMatchingTaskGroups,
@@ -180,7 +179,6 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
onRenamingChange={setRenamingName} onRenamingChange={setRenamingName}
onReadAloud={handleReadAloud} onReadAloud={handleReadAloud}
onOcr={handleOcr} onOcr={handleOcr}
onExtractAudio={handleExtractAudio}
onExtract={handleExtract} onExtract={handleExtract}
onPlay={handlePlay} onPlay={handlePlay}
taskGroups={getMatchingTaskGroups(entry.name, entry.type)} taskGroups={getMatchingTaskGroups(entry.name, entry.type)}
@@ -13,7 +13,6 @@ import {
LayoutGrid, LayoutGrid,
Volume2, Volume2,
ScanText, ScanText,
AudioLines,
FolderArchive, FolderArchive,
ClipboardCopy, ClipboardCopy,
Music, Music,
@@ -64,7 +63,6 @@ export type FileItemProps = {
onRenamingChange: (name: string | null) => void; onRenamingChange: (name: string | null) => void;
onReadAloud: (entry: DirEntry) => void; onReadAloud: (entry: DirEntry) => void;
onOcr: (entry: DirEntry) => void; onOcr: (entry: DirEntry) => void;
onExtractAudio: (entry: DirEntry) => void;
onExtract: (entry: DirEntry) => void; onExtract: (entry: DirEntry) => void;
onPlay: (entry: DirEntry) => void; onPlay: (entry: DirEntry) => void;
taskGroups: TaskGroup[]; taskGroups: TaskGroup[];
@@ -96,7 +94,6 @@ type MenuItemsProps = {
onDownload: (e: DirEntry) => void; onDownload: (e: DirEntry) => void;
onReadAloud: (e: DirEntry) => void; onReadAloud: (e: DirEntry) => void;
onOcr: (e: DirEntry) => void; onOcr: (e: DirEntry) => void;
onExtractAudio: (e: DirEntry) => void;
onExtract: (e: DirEntry) => void; onExtract: (e: DirEntry) => void;
onPlay: (e: DirEntry) => void; onPlay: (e: DirEntry) => void;
onCut: () => void; onCut: () => void;
@@ -116,7 +113,6 @@ const DropdownMenuItems = ({
onDownload, onDownload,
onReadAloud, onReadAloud,
onOcr, onOcr,
onExtractAudio,
onExtract, onExtract,
onPlay, onPlay,
onCut, onCut,
@@ -128,12 +124,11 @@ const DropdownMenuItems = ({
const fileType = entry.type === 'file' ? getFileType(entry.name) : null; const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text'; const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
const showOcr = fileType === 'image'; const showOcr = fileType === 'image';
const showExtractAudio = fileType === 'video';
const showExtract = fileType === 'archive'; const showExtract = fileType === 'archive';
const showPlay = fileType === 'audio' || entry.type === 'directory'; const showPlay = fileType === 'audio' || entry.type === 'directory';
const hasTasks = taskGroups.length > 0; const hasTasks = taskGroups.length > 0;
const hasActions = showPlay || showReadAloud || showOcr || showExtractAudio || showExtract || hasTasks; const hasActions = showPlay || showReadAloud || showOcr || showExtract || hasTasks;
// Only nest when more than one category matched — a file usually matches a single category, and // Only nest when more than one category matched — a file usually matches a single category, and
// Run Task > Video > Convert would just add a hop. // Run Task > Video > Convert would just add a hop.
const nestTasks = taskGroups.length > 1; const nestTasks = taskGroups.length > 1;
@@ -159,12 +154,6 @@ const DropdownMenuItems = ({
Extract Text (OCR) Extract Text (OCR)
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{showExtractAudio && (
<DropdownMenuItem onClick={() => onExtractAudio(entry)} className="cursor-pointer">
<AudioLines className="mr-2 h-4 w-4" />
Extract Audio
</DropdownMenuItem>
)}
{showExtract && ( {showExtract && (
<DropdownMenuItem onClick={() => onExtract(entry)} className="cursor-pointer"> <DropdownMenuItem onClick={() => onExtract(entry)} className="cursor-pointer">
<FolderArchive className="mr-2 h-4 w-4" /> <FolderArchive className="mr-2 h-4 w-4" />
@@ -260,7 +249,6 @@ const ContextMenuItems = ({
onDownload, onDownload,
onReadAloud, onReadAloud,
onOcr, onOcr,
onExtractAudio,
onExtract, onExtract,
onPlay, onPlay,
onCut, onCut,
@@ -272,12 +260,11 @@ const ContextMenuItems = ({
const fileType = entry.type === 'file' ? getFileType(entry.name) : null; const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text'; const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
const showOcr = fileType === 'image'; const showOcr = fileType === 'image';
const showExtractAudio = fileType === 'video';
const showExtract = fileType === 'archive'; const showExtract = fileType === 'archive';
const showPlay = fileType === 'audio' || entry.type === 'directory'; const showPlay = fileType === 'audio' || entry.type === 'directory';
const hasTasks = taskGroups.length > 0; const hasTasks = taskGroups.length > 0;
const hasActions = showPlay || showReadAloud || showOcr || showExtractAudio || showExtract || hasTasks; const hasActions = showPlay || showReadAloud || showOcr || showExtract || hasTasks;
// Only nest when more than one category matched — a file usually matches a single category, and // Only nest when more than one category matched — a file usually matches a single category, and
// Run Task > Video > Convert would just add a hop. // Run Task > Video > Convert would just add a hop.
const nestTasks = taskGroups.length > 1; const nestTasks = taskGroups.length > 1;
@@ -303,12 +290,6 @@ const ContextMenuItems = ({
Extract Text (OCR) Extract Text (OCR)
</ContextMenuItem> </ContextMenuItem>
)} )}
{showExtractAudio && (
<ContextMenuItem onClick={() => onExtractAudio(entry)} className="cursor-pointer">
<AudioLines className="mr-2 h-4 w-4" />
Extract Audio
</ContextMenuItem>
)}
{showExtract && ( {showExtract && (
<ContextMenuItem onClick={() => onExtract(entry)} className="cursor-pointer"> <ContextMenuItem onClick={() => onExtract(entry)} className="cursor-pointer">
<FolderArchive className="mr-2 h-4 w-4" /> <FolderArchive className="mr-2 h-4 w-4" />
@@ -513,7 +494,6 @@ export const FileItem = ({
onRenamingChange, onRenamingChange,
onReadAloud, onReadAloud,
onOcr, onOcr,
onExtractAudio,
onExtract, onExtract,
onPlay, onPlay,
taskGroups, taskGroups,
@@ -595,7 +575,6 @@ export const FileItem = ({
onDownload, onDownload,
onReadAloud, onReadAloud,
onOcr, onOcr,
onExtractAudio,
onExtract, onExtract,
onPlay, onPlay,
onCut, onCut,
@@ -457,18 +457,6 @@ export const useFileBrowserApp = (
} }
}; };
const handleExtractAudio = async (entry: DirEntry) => {
const filePath = entryPath(entry.name);
const toastId = toast.loading('Extracting audio from video...');
try {
const { audioPath, audioRoot } = await files.extractAudio(filePath);
toast.dismiss(toastId);
setSearchParams({ view: filePath, ephemeral: audioPath, ephemeralRoot: audioRoot });
} catch {
toast.error('Failed to extract audio from video', { id: toastId });
}
};
const handlePlay = (entry: DirEntry) => { const handlePlay = (entry: DirEntry) => {
const filePath = entryPath(entry.name); const filePath = entryPath(entry.name);
setSearchParams({ play: filePath }); setSearchParams({ play: filePath });
@@ -765,7 +753,6 @@ export const useFileBrowserApp = (
handleCreateDashboardHere, handleCreateDashboardHere,
handleReadAloud, handleReadAloud,
handleOcr, handleOcr,
handleExtractAudio,
handleExtract, handleExtract,
handlePlay, handlePlay,
handleGitClone, handleGitClone,