file browser: download videos via ReClip as a background job
/download-video now delegates to the ReClip service (its own yt-dlp) and runs as a background job: POST returns a jobId immediately and the client polls GET /download-video/:jobId, so a large download no longer holds one long request open (which was 504-ing behind the reverse proxy). the finished file is streamed into the user's folder with a title-based name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -516,16 +516,41 @@ export const useFileBrowserApp = (
|
||||
const handleVideoDownload = async () => {
|
||||
const url = videoUrl.trim();
|
||||
if (!url) return;
|
||||
const toastId = toast.loading(audioOnly ? 'Extracting audio...' : 'Downloading video...');
|
||||
const wasAudio = audioOnly;
|
||||
const dir = currentPath;
|
||||
// Close the dialog right away — the download runs as a background job and is tracked via a toast,
|
||||
// so a large video no longer holds the request open (which was 504-ing behind the reverse proxy).
|
||||
setShowVideoDownload(false);
|
||||
setVideoUrl('');
|
||||
setAudioOnly(false);
|
||||
|
||||
const toastId = toast.loading(wasAudio ? 'Extracting audio…' : 'Downloading video…');
|
||||
try {
|
||||
await files.downloadVideo(url, currentPath, audioOnly);
|
||||
toast.success('Download complete', { id: toastId });
|
||||
setShowVideoDownload(false);
|
||||
setVideoUrl('');
|
||||
setAudioOnly(false);
|
||||
await refresh();
|
||||
const { jobId } = await files.downloadVideo(url, dir, wasAudio);
|
||||
const deadline = Date.now() + 60 * 60 * 1000;
|
||||
for (;;) {
|
||||
if (Date.now() > deadline) {
|
||||
toast.error('Download timed out', { id: toastId });
|
||||
return;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
const st = await files.downloadVideoStatus(jobId).catch(() => null);
|
||||
if (!st) continue;
|
||||
if (st.status === 'error') {
|
||||
toast.error(st.error || 'Download failed', { id: toastId });
|
||||
return;
|
||||
}
|
||||
if (st.status === 'transferring') {
|
||||
toast.loading('Saving to folder…', { id: toastId });
|
||||
}
|
||||
if (st.status === 'done') {
|
||||
toast.success(st.filename ? `Downloaded ${st.filename}` : 'Download complete', { id: toastId });
|
||||
await refresh();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error('Download failed', { id: toastId });
|
||||
toast.error('Could not start the download', { id: toastId });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,10 @@ export const useFilesAPI = (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 }),
|
||||
client.post<{ jobId: string }>(withRoot('/file-browser/download-video'), { url, path, audioOnly }),
|
||||
|
||||
downloadVideoStatus: (jobId: string) =>
|
||||
client.get<DownloadVideoStatus>(withRoot(`/file-browser/download-video/${jobId}`)),
|
||||
|
||||
tts: (path: string, opts?: { saveNextTo?: boolean }) =>
|
||||
client.post<{ audioPath: string; audioRoot: string }>('/file-browser/tts', { path, root, ...opts }),
|
||||
@@ -139,6 +142,12 @@ export type UseFilesAPIType = ReturnType<typeof useFilesAPI>;
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export type DownloadVideoStatus = {
|
||||
status: 'downloading' | 'transferring' | 'done' | 'error';
|
||||
error?: string;
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
export type AudioTrack = { id: number; codec: string; channels: number; lang: string; title: string };
|
||||
export type SubtitleTrack = { id: number; codec: string; lang: string; title: string };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user