yt-dlp downloads
This commit is contained in:
@@ -18,9 +18,12 @@ import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
Upload,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import { getIcon } from 'material-file-icons';
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useFiles, type DirEntry, useTasks, type TaskSummary, Breadcrumb, Toolbar } from 'apps/FileBrowser';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useUserState } from '@/state/useUserState';
|
||||
@@ -61,6 +64,9 @@ export const Files = ({ basePath = '/' }: FilesProps) => {
|
||||
const [cloning, setCloning] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [runningTask, setRunningTask] = useState<{ task: TaskSummary; entry: DirEntry } | null>(null);
|
||||
const [showVideoDownload, setShowVideoDownload] = useState(false);
|
||||
const [videoUrl, setVideoUrl] = useState('');
|
||||
const [audioOnly, setAudioOnly] = useState(false);
|
||||
const dragCounter = useRef(0);
|
||||
const { getMatchingTasks } = useTasks();
|
||||
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -366,6 +372,22 @@ export const Files = ({ basePath = '/' }: FilesProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoDownload = async () => {
|
||||
const url = videoUrl.trim();
|
||||
if (!url) return;
|
||||
const toastId = toast.loading(audioOnly ? 'Extracting audio...' : 'Downloading video...');
|
||||
try {
|
||||
await files.downloadVideo(url, currentPath, audioOnly);
|
||||
toast.success('Download complete', { id: toastId });
|
||||
setShowVideoDownload(false);
|
||||
setVideoUrl('');
|
||||
setAudioOnly(false);
|
||||
await refresh();
|
||||
} catch {
|
||||
toast.error('Download failed', { id: toastId });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCut = () => {
|
||||
const paths = selected.size > 0 ? selectedPaths() : [];
|
||||
if (paths.length === 0) return;
|
||||
@@ -908,6 +930,10 @@ export const Files = ({ basePath = '/' }: FilesProps) => {
|
||||
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||
Create Workspace here
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => setShowVideoDownload(true)} className="cursor-pointer">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download video
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)}
|
||||
@@ -929,6 +955,63 @@ export const Files = ({ basePath = '/' }: FilesProps) => {
|
||||
cwd={{ root: homeRoot, path: currentPath.replace(/^\//, '') }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
open={showVideoDownload}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowVideoDownload(false);
|
||||
setVideoUrl('');
|
||||
setAudioOnly(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Download video</DialogTitle>
|
||||
<DialogDescription>Download a video from a URL using yt-dlp</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
ev.preventDefault();
|
||||
handleVideoDownload();
|
||||
}}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
value={videoUrl}
|
||||
onChange={(ev) => setVideoUrl(ev.target.value)}
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
className="h-10 w-full text-sm rounded-md border border-duck-dark/20 bg-background/60 text-duck-dark placeholder:text-duck-dark/40 outline-none focus:border-duck-teal/50 px-3"
|
||||
/>
|
||||
<label className="flex items-center gap-2 cursor-pointer text-sm text-duck-dark/70">
|
||||
<Checkbox checked={audioOnly} onCheckedChange={(v) => setAudioOnly(v === true)} />
|
||||
Extract audio only (mp3)
|
||||
</label>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowVideoDownload(false);
|
||||
setVideoUrl('');
|
||||
setAudioOnly(false);
|
||||
}}
|
||||
className="px-4 py-2 text-sm rounded-md text-duck-dark/70 hover:bg-duck-dark/5 cursor-pointer transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!videoUrl.trim()}
|
||||
className="px-4 py-2 text-sm rounded-md bg-duck-teal text-duck-yellow hover:bg-duck-teal/90 cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -690,6 +690,30 @@ router.post('/move', async (ctx) => {
|
||||
return ctx.json({ ok: true, results });
|
||||
});
|
||||
|
||||
// Download video via yt-dlp
|
||||
router.post('/download-video', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined);
|
||||
const { url, path, audioOnly } = ctx.get('body') as { url: string; path: string; audioOnly?: boolean };
|
||||
if (!url) throw errors.BAD_REQUEST('url is required');
|
||||
if (!path) throw errors.BAD_REQUEST('path is required');
|
||||
|
||||
const absPath = resolveUserPath(rootDir, path);
|
||||
const args = ['yt-dlp', '-o', '%(title)s.%(ext)s'];
|
||||
if (audioOnly) args.push('-x', '--audio-format', 'mp3');
|
||||
args.push(url);
|
||||
|
||||
const proc = Bun.spawn(args, { cwd: absPath, stdout: 'pipe', stderr: 'pipe' });
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
throw errors.BAD_REQUEST(stderr.trim() || 'yt-dlp download failed');
|
||||
}
|
||||
|
||||
return ctx.json({ ok: true });
|
||||
});
|
||||
|
||||
// Git clone a repository into a directory
|
||||
router.post('/git-clone', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
|
||||
@@ -68,6 +68,9 @@ export const useFiles = (root: string = 'home') => {
|
||||
|
||||
gitClone: (url: string, path: string) => client.post(withRoot('/file-browser/git-clone'), { url, path }),
|
||||
|
||||
downloadVideo: (url: string, path: string, audioOnly: boolean) =>
|
||||
client.post(withRoot('/file-browser/download-video'), { url, path, audioOnly }),
|
||||
|
||||
download: async (paths: string[]) => {
|
||||
if (paths.length === 1) {
|
||||
const blob = await client.getBlob(withRoot(`/file-browser/download?path=${encodeURIComponent(paths[0]!)}`));
|
||||
|
||||
Reference in New Issue
Block a user