remove ffmpeg video transcode, back to native-only playback

Drop the /transcode (and dead /transcode-audio) routes and the frontend
mkv/avi transcode wiring. Only browser-native formats (mp4, webm, mov,
m4v, ogv) are classified as video now; other containers fall through to
the generic file view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:37:14 +00:00
co-authored by Claude Opus 4.8
parent 510254cbd4
commit 82d0e9afd6
4 changed files with 33 additions and 192 deletions
+2 -110
View File
@@ -1,6 +1,6 @@
import { createRouter } from '@@/create-router';
import { resolve, dirname, join, parse as parsePath } from 'node:path';
import { readdir, stat, mkdir, rm, rename, readFile, cp, unlink } from 'node:fs/promises';
import { readdir, stat, mkdir, rm, rename, readFile, cp } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { getHomeDir, DATA_PATH } from '@@/data-path';
import * as errors from '@@/custom-errors';
@@ -261,116 +261,8 @@ router.get('/raw', async (ctx) => {
});
});
// Transcode video via ffmpeg with caching — outputs a seekable MP4 file
router.get('/transcode', async (ctx) => {
const user = ctx.get('user');
const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined);
const relPath = (ctx.req.query('path') || '').replace(/^\/+/, '');
if (!relPath) throw errors.BAD_REQUEST('path is required');
const absPath = resolveUserPath(rootDir, relPath);
const s = await stat(absPath);
if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot transcode a directory');
const userDataDir = getUserDataDir(user.email);
const { dir, name } = parsePath(relPath);
const cacheRel = dir ? `cache/video/${dir}/${name}.mp4` : `cache/video/${name}.mp4`;
const cacheAbs = resolve(userDataDir, cacheRel);
if (!existsSync(cacheAbs)) {
await mkdir(dirname(cacheAbs), { recursive: true });
const tmpPath = cacheAbs + '.tmp';
const proc = Bun.spawn(
[
'ffmpeg',
'-i',
absPath,
'-c:v',
'libx264',
'-preset',
'ultrafast',
'-crf',
'23',
'-c:a',
'aac',
'-b:a',
'128k',
'-movflags',
'+faststart',
'-y',
tmpPath,
],
{ stdout: 'ignore', stderr: 'pipe' },
);
const exitCode = await proc.exited;
if (exitCode !== 0) {
const stderr = await new Response(proc.stderr).text();
await unlink(tmpPath).catch(() => {});
throw errors.BAD_REQUEST(stderr.trim() || 'Video transcoding failed');
}
await rename(tmpPath, cacheAbs);
}
const file = Bun.file(cacheAbs);
const total = file.size;
const rangeHeader = ctx.req.header('range');
if (rangeHeader) {
const match = rangeHeader.match(/bytes=(\d*)-(\d*)/);
if (match) {
const start = match[1] ? parseInt(match[1], 10) : 0;
const end = match[2] ? parseInt(match[2], 10) : total - 1;
const chunkSize = end - start + 1;
return new Response(file.slice(start, end + 1), {
status: 206,
headers: {
'Content-Type': 'video/mp4',
'Content-Range': `bytes ${start}-${end}/${total}`,
'Content-Length': String(chunkSize),
'Accept-Ranges': 'bytes',
},
});
}
}
return new Response(file, {
headers: {
'Content-Type': 'video/mp4',
'Content-Length': String(total),
'Accept-Ranges': 'bytes',
},
});
});
// Transcode audio via ffmpeg for universal playback (outputs MP3)
router.get('/transcode-audio', async (ctx) => {
const user = ctx.get('user');
const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined);
const relPath = (ctx.req.query('path') || '').replace(/^\/+/, '');
if (!relPath) throw errors.BAD_REQUEST('path is required');
const absPath = resolveUserPath(rootDir, relPath);
const s = await stat(absPath);
if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot transcode a directory');
const proc = Bun.spawn(['ffmpeg', '-i', absPath, '-c:a', 'libmp3lame', '-q:a', '2', '-f', 'mp3', 'pipe:1'], {
stdout: 'pipe',
stderr: 'ignore',
});
return new Response(proc.stdout as ReadableStream, {
headers: {
'Content-Type': 'audio/mpeg',
'Transfer-Encoding': 'chunked',
},
});
});
// Save a cached result (ocr/tts/transcriptions/audio) next to the original file
const CACHE_PREFIXES = ['cache/ocr/', 'cache/tts/', 'cache/transcriptions/', 'cache/audio/', 'cache/video/'];
const CACHE_PREFIXES = ['cache/ocr/', 'cache/tts/', 'cache/transcriptions/', 'cache/audio/'];
router.post('/save-result', async (ctx) => {
const user = ctx.get('user');