From 8a637c7788081df53fdbd4c9cf2718fed9dc127a Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Fri, 20 Feb 2026 05:28:26 +0000 Subject: [PATCH] yt-dlp downloads --- .../Screens/Dashboard/Files/Screen/index.tsx | 83 +++++++++++++++++++ src/servers/api/file-browser/router.ts | 24 ++++++ src/workspaces/apps/FileBrowser/useFiles.ts | 3 + 3 files changed, 110 insertions(+) diff --git a/src/apps/officer-web/Screens/Dashboard/Files/Screen/index.tsx b/src/apps/officer-web/Screens/Dashboard/Files/Screen/index.tsx index d28c536e..ed3f3011 100644 --- a/src/apps/officer-web/Screens/Dashboard/Files/Screen/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Files/Screen/index.tsx @@ -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 | 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) => { Create Workspace here + setShowVideoDownload(true)} className="cursor-pointer"> + + Download video + )} @@ -929,6 +955,63 @@ export const Files = ({ basePath = '/' }: FilesProps) => { cwd={{ root: homeRoot, path: currentPath.replace(/^\//, '') }} /> )} + + { + if (!open) { + setShowVideoDownload(false); + setVideoUrl(''); + setAudioOnly(false); + } + }} + > + + + Download video + Download a video from a URL using yt-dlp + +
{ + ev.preventDefault(); + handleVideoDownload(); + }} + className="flex flex-col gap-4" + > + 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" + /> + +
+ + +
+
+
+
); }; diff --git a/src/servers/api/file-browser/router.ts b/src/servers/api/file-browser/router.ts index be4dacf6..9e96c167 100644 --- a/src/servers/api/file-browser/router.ts +++ b/src/servers/api/file-browser/router.ts @@ -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'); diff --git a/src/workspaces/apps/FileBrowser/useFiles.ts b/src/workspaces/apps/FileBrowser/useFiles.ts index e933d6ad..cc0ecafe 100644 --- a/src/workspaces/apps/FileBrowser/useFiles.ts +++ b/src/workspaces/apps/FileBrowser/useFiles.ts @@ -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]!)}`));