file browser: video download — metadata prefetch + playlist handling
Reworks the download-video dialog into a prefetch-then-download flow, mirroring
ReClip's own web UI. Still a pure proxy to ReClip (its yt-dlp); no downloader
logic moves to the platform.
Server (thin ReClip proxies alongside /download-video):
- POST /file-browser/video-info { url } → ReClip /api/info → { title, thumbnail, duration, uploader }
- POST /file-browser/video-playlist { url } → ReClip /api/playlist → { urls }
Both return { error } inline (200) so the client can render failures per-card.
UI (VideoDownloadDialog, now self-contained; useFileBrowserApp exposes `files`
and drops the old single-shot state/handler):
- Paste a URL → Fetch. A playlist URL (list=) expands via /video-playlist, then
each entry's /video-info is prefetched sequentially (ReClip does yt-dlp per
video), rendering a card (thumbnail, title, uploader, duration) that fills in
progressively.
- Per-entry Download, plus Download All when there's more than one; per-card
status (downloading → saving → saved / retry-on-error) via the existing
background job + poll.
- Playlists get an optional "subfolder you name" field (ReClip's /api/playlist
carries no playlist title); blank = current folder.
- Quality is always best (matches the mobile Share flow — no picker); the
audio-only toggle applies to the whole batch.
Verified ReClip's contract live: /api/info returns the metadata fields, and
/api/playlist returns { urls } (17 entries in ~1.2s).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1328,6 +1328,39 @@ router.get('/download-video/:jobId', (ctx) => {
|
||||
return ctx.json({ status: job.status, error: job.error, filename: job.filename });
|
||||
});
|
||||
|
||||
// Prefetch a single video's metadata (title / thumbnail / duration / uploader) — proxied straight to
|
||||
// ReClip's /api/info so the download dialog can show a preview card before committing. Errors (private
|
||||
// video, timeout, …) come back as { error } with a 200 so the client can render them inline.
|
||||
router.post('/video-info', async (ctx) => {
|
||||
const { url } = ctx.get('body') as { url?: string };
|
||||
if (!url) throw errors.BAD_REQUEST('url is required');
|
||||
const res = await fetch(`${RECLIP_BASE}/api/info`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url }),
|
||||
signal: AbortSignal.timeout(90_000),
|
||||
}).catch(() => null);
|
||||
if (!res) return ctx.json({ error: `Could not reach ReClip at ${RECLIP_BASE}` });
|
||||
const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
||||
return ctx.json(data);
|
||||
});
|
||||
|
||||
// Expand a playlist URL into its individual video URLs (ReClip's /api/playlist → { urls }). The client
|
||||
// then prefetches /video-info per url to build the per-entry cards.
|
||||
router.post('/video-playlist', async (ctx) => {
|
||||
const { url } = ctx.get('body') as { url?: string };
|
||||
if (!url) throw errors.BAD_REQUEST('url is required');
|
||||
const res = await fetch(`${RECLIP_BASE}/api/playlist`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url }),
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
}).catch(() => null);
|
||||
if (!res) return ctx.json({ error: `Could not reach ReClip at ${RECLIP_BASE}` });
|
||||
const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
||||
return ctx.json(data);
|
||||
});
|
||||
|
||||
// Git clone a repository into a directory
|
||||
router.post('/git-clone', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
|
||||
Reference in New Issue
Block a user