feat: add opt-in save-before-playing setting backed by server-compiled single-file downloads
This commit is contained in:
@@ -24,7 +24,9 @@ import { serveStatic } from 'hono/bun';
|
||||
import { logger } from 'hono/logger';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createServer } from 'node:http';
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { readFileSync, readdirSync, statSync, openSync, unlinkSync, createReadStream } from 'node:fs';
|
||||
import { Readable } from 'node:stream';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { initDb, upsertUser, recordVideoAccess, getUserData } from './db.js';
|
||||
|
||||
@@ -305,6 +307,53 @@ app.get('/api/download/:videoId', async (c) => {
|
||||
|
||||
try {
|
||||
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
||||
|
||||
// ?mux=1 — "Save before playing" path: let yt-dlp download bestvideo up
|
||||
// to 720p PLUS bestaudio and compile them into one mp4 with ffmpeg on
|
||||
// the server, then stream the finished file. Default saves (no mux)
|
||||
// keep the progressive single-stream behavior below, unchanged.
|
||||
if (c.req.query('mux') === '1') {
|
||||
const tmp = `${tmpdir()}/ytp-mux-${videoId}-${Date.now()}.mp4`;
|
||||
try {
|
||||
await runYtdlp([
|
||||
url,
|
||||
'--no-warnings', '--no-playlist',
|
||||
'-f', 'bv*[height<=720][ext=mp4]+ba[ext=m4a]/bv*[height<=720]+ba/b[ext=mp4]/b',
|
||||
'--merge-output-format', 'mp4',
|
||||
'-N', '4',
|
||||
'-o', tmp,
|
||||
]);
|
||||
const size = statSync(tmp).size;
|
||||
// Open the fd first, then unlink: on Linux the data stays readable
|
||||
// until the fd closes, so the temp file cleans itself up even if the
|
||||
// client disconnects mid-transfer.
|
||||
const fd = openSync(tmp, 'r');
|
||||
unlinkSync(tmp);
|
||||
const stream = createReadStream('', { fd });
|
||||
|
||||
const fp = c.req.query('fp');
|
||||
if (fp) recordVideoAccess(fp, { id: videoId }).catch(() => {});
|
||||
|
||||
return new Response(Readable.toWeb(stream), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Length': String(size),
|
||||
'Content-Disposition': `attachment; filename="${videoId}.mp4"`,
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// ffmpeg missing or extraction failed — clean up any partial output
|
||||
// and fall through to the progressive proxy below.
|
||||
for (const leftover of [tmp, tmp + '.part']) {
|
||||
try { unlinkSync(leftover); } catch { /* not created */ }
|
||||
}
|
||||
console.warn(`[ytplayer] mux download failed for ${videoId}, falling back to progressive:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// --get-url with bestvideo+bestaudio/best format is not what we want
|
||||
// here — we need a SINGLE FILE so no ffmpeg muxing is required in the
|
||||
// browser. Use -f "bestvideo[ext=mp4][acodec!=none]/best[ext=mp4]/best"
|
||||
|
||||
Reference in New Issue
Block a user