refactor(deps): remove ffprobe-static and fix playwright flakiness

Replace ffprobe-static with ffmpeg-static for metadata extraction in
migration scripts. This removes the requirement for ffprobe binaries
across Docker, CI, and Vercel environments, simplifying the dependency
graph.
This commit is contained in:
Richard R 2026-02-16 01:41:54 -07:00
parent da0fce708f
commit fa406a8cb2
8 changed files with 20 additions and 53 deletions

View file

@ -72,6 +72,5 @@ IMPORT_LIBRARY_DIRS=
# (Required without Docker) Path to your local whisper.cpp CLI binary for STT timestamp generation
WHISPER_CPP_BIN=/whisper.cpp/build/bin/whisper-cli
# (Optional) Override ffmpeg/ffprobe binary paths used for audiobook processing/probing
# (Optional) Override ffmpeg binary path used for audiobook processing
FFMPEG_BIN=
FFPROBE_BIN=

View file

@ -10,7 +10,6 @@ jobs:
runs-on: ubuntu-latest
env:
FFMPEG_BIN: /usr/bin/ffmpeg
FFPROBE_BIN: /usr/bin/ffprobe
USE_EMBEDDED_WEED_MINI: true
BASE_URL: http://127.0.0.1:3003
S3_ENDPOINT: http://127.0.0.1:8333

View file

@ -45,7 +45,7 @@ FROM node:lts-alpine AS runner
# Add runtime OS dependencies:
# - libreoffice-writer: required for DOCX → PDF conversion
# ffmpeg/ffprobe are provided by ffmpeg-static/ffprobe-static from node_modules.
# ffmpeg is provided by ffmpeg-static from node_modules.
RUN apk add --no-cache ca-certificates libreoffice-writer
# Install pnpm globally for running the app

View file

@ -7,7 +7,7 @@ This guide covers deploying OpenReader WebUI to Vercel with external Postgres an
## What works on Vercel
- Documents (PDF/EPUB/TXT/MD) work with `POSTGRES_URL` + external S3 storage.
- Audiobook routes work on Node.js serverless functions using `ffmpeg-static`/`ffprobe-static`.
- Audiobook routes work on Node.js serverless functions using `ffmpeg-static`.
:::warning DOCX Conversion Limitation
`docx` conversion requires `soffice` (LibreOffice), which is not available in a standard Vercel runtime.
@ -65,9 +65,9 @@ For internet-exposed Vercel deployments, set both `BASE_URL` and `AUTH_SECRET`.
For all variables and defaults, see [Environment Variables](../reference/environment-variables).
:::
## 2. FFmpeg/ffprobe packaging in Vercel functions
## 2. FFmpeg packaging in Vercel functions
`ffmpeg-static` and `ffprobe-static` binaries must be included in function traces. This repo already does that in `next.config.ts` via `outputFileTracingIncludes` for:
`ffmpeg-static` binaries must be included in function traces. This repo already does that in `next.config.ts` via `outputFileTracingIncludes` for:
- `/api/audiobook`
- `/api/audiobook/chapter`
@ -75,7 +75,7 @@ For all variables and defaults, see [Environment Variables](../reference/environ
- `/api/whisper`
:::info
`serverExternalPackages` should include `ffmpeg-static` and `ffprobe-static` so package paths resolve at runtime instead of being bundled into route output.
`serverExternalPackages` should include `ffmpeg-static` so package paths resolve at runtime instead of being bundled into route output.
:::
If you change route paths or split handlers, update `outputFileTracingIncludes` accordingly.

View file

@ -49,7 +49,6 @@ This is the single reference page for OpenReader WebUI environment variables.
| `IMPORT_LIBRARY_DIRS` | Library import | unset | Set multiple roots (comma/colon/semicolon separated) |
| `WHISPER_CPP_BIN` | Word timing | unset | Set to enable `whisper.cpp` timestamps |
| `FFMPEG_BIN` | Audio runtime | auto-detected (`ffmpeg-static`) | Override ffmpeg binary path |
| `FFPROBE_BIN` | Audio runtime | auto-detected (`ffprobe-static`) | Override ffprobe binary path |
## Client Runtime and Feature Flags
@ -369,10 +368,3 @@ Absolute path or executable name for the ffmpeg binary used by audiobook/process
- Resolution order: `FFMPEG_BIN` -> `ffmpeg-static`
- Example: `/var/task/node_modules/ffmpeg-static/ffmpeg`
### FFPROBE_BIN
Absolute path or executable name for the ffprobe binary used for audio probing.
- Resolution order: `FFPROBE_BIN` -> `ffprobe-static`
- Example: `/var/task/node_modules/ffprobe-static/bin/linux/x64/ffprobe`

View file

@ -40,7 +40,6 @@
"epubjs": "^0.3.93",
"fast-xml-parser": "^5.3.5",
"ffmpeg-static": "^5.3.0",
"ffprobe-static": "^3.1.0",
"howler": "^2.2.4",
"jszip": "^3.10.1",
"lru-cache": "^11.2.6",
@ -65,7 +64,6 @@
"@playwright/test": "^1.58.2",
"@tailwindcss/typography": "^0.5.19",
"@types/better-sqlite3": "^7.6.13",
"@types/ffprobe-static": "^2.0.3",
"@types/howler": "^2.2.12",
"@types/node": "^20.19.33",
"@types/pg": "^8.16.0",

View file

@ -67,9 +67,6 @@ importers:
ffmpeg-static:
specifier: ^5.3.0
version: 5.3.0
ffprobe-static:
specifier: ^3.1.0
version: 3.1.0
howler:
specifier: ^2.2.4
version: 2.2.4
@ -137,9 +134,6 @@ importers:
'@types/better-sqlite3':
specifier: ^7.6.13
version: 7.6.13
'@types/ffprobe-static':
specifier: ^2.0.3
version: 2.0.3
'@types/howler':
specifier: ^2.2.12
version: 2.2.12
@ -1395,9 +1389,6 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/ffprobe-static@2.0.3':
resolution: {integrity: sha512-86Q8dIzhjknxA6935ZcVLkVU3t6bP+F08mdriWZk2+3UqjxIbT3QEADjbO5Yq8826cVs/aZBREVZ7jUoSTgHiw==}
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
@ -2406,9 +2397,6 @@ packages:
resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==}
engines: {node: '>=16'}
ffprobe-static@3.1.0:
resolution: {integrity: sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -5306,8 +5294,6 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/ffprobe-static@2.0.3': {}
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
@ -6397,8 +6383,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
ffprobe-static@3.1.0: {}
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1

View file

@ -15,7 +15,7 @@ import {
const require = createRequire(import.meta.url);
const { Pool } = require('pg');
const BetterSqlite3 = require('better-sqlite3');
const ffprobeStatic = require('ffprobe-static');
const ffmpegStatic = require('ffmpeg-static');
const DOCSTORE_DIR = path.join(process.cwd(), 'docstore');
const DOCUMENTS_V1_DIR = path.join(DOCSTORE_DIR, 'documents_v1');
@ -321,34 +321,29 @@ function chooseBinary(preferred, bundled, envVarName, packageName) {
return bundledValue;
}
function getFFprobePath() {
return chooseBinary(process.env.FFPROBE_BIN || null, ffprobeStatic?.path || null, 'FFPROBE_BIN', 'ffprobe-static');
function getFFmpegPath() {
return chooseBinary(process.env.FFMPEG_BIN || null, ffmpegStatic || null, 'FFMPEG_BIN', 'ffmpeg-static');
}
async function ffprobeTitleTag(filePath) {
return new Promise((resolve) => {
const child = spawn(getFFprobePath(), [
'-v', 'quiet',
'-print_format', 'json',
'-show_entries', 'format_tags=title',
filePath,
const child = spawn(getFFmpegPath(), [
'-i', filePath,
'-f', 'ffmetadata',
'-',
]);
let output = '';
child.stdout.on('data', (data) => { output += data.toString(); });
let stdout = '';
child.stdout.on('data', (data) => { stdout += data.toString(); });
child.on('error', () => resolve(null));
child.on('close', (code) => {
if (code !== 0) {
child.on('close', () => {
const line = stdout.split(/\r?\n/).find((l) => l.startsWith('title='));
if (!line) {
resolve(null);
return;
}
try {
const parsed = JSON.parse(output);
const title = parsed?.format?.tags?.title;
resolve(typeof title === 'string' ? title : null);
} catch {
resolve(null);
}
const raw = line.slice('title='.length).trim();
resolve(raw.length > 0 ? raw : null);
});
});
}