feat: self-managing yt-dlp + ffmpeg version display
yt-dlp is now a managed, auto-updating binary rather than a static bundled one. On launch AeroFetch seeds a writable copy under userData from the bundled seed, self-heals it if it goes missing, and — throttled to once a day — self-updates it to the configured channel (default: nightly). This keeps the binary current so YouTube changes don't silently cause 403s, and an app reinstall (or portable re-extraction) can no longer roll it back. Settings shows an auto-update toggle, the live version, last-checked time, and the channel selector. Also surface the bundled ffmpeg/ffprobe versions in Settings (display only: they have no self-update path and, unlike yt-dlp, don't break as sites change, so there is no auto-update for them). - binaries: split the read-only bundled seed from the managed userData copy - ytdlp: ensureManagedYtdlp (seed + self-heal), startup auto-update runner - ytdlpPolicy: pure once-a-day throttle decision (unit-tested) - ffmpeg: parse ffmpeg/ffprobe -version for the Settings panel - settings/ipc/preload/renderer: new settings, IPC channels, types, and UI Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+72
-3
@@ -1,13 +1,37 @@
|
||||
import { execFile } from 'child_process'
|
||||
import { existsSync } from 'fs'
|
||||
import { getYtdlpPath } from './binaries'
|
||||
import { existsSync, mkdirSync, copyFileSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
import { getYtdlpPath, getBundledYtdlpPath } from './binaries'
|
||||
import { getSettings, setSettings } from './settings'
|
||||
import { shouldAutoCheckYtdlp } from './ytdlpPolicy'
|
||||
import {
|
||||
isYtdlpUpdateChannel,
|
||||
type YtdlpVersionResult,
|
||||
type YtdlpUpdateChannel,
|
||||
type YtdlpUpdateResult
|
||||
type YtdlpUpdateResult,
|
||||
type YtdlpAutoUpdateStatus
|
||||
} from '@shared/ipc'
|
||||
|
||||
/**
|
||||
* Ensure the writable managed yt-dlp.exe exists, copying the bundled seed in if
|
||||
* it doesn't (first run, or after the user/AV deleted it). Best-effort and
|
||||
* idempotent — when the copy is already present this is just one existsSync, so
|
||||
* it's cheap to call before any spawn/update. Does nothing if the seed itself is
|
||||
* gone (a broken install); callers surface their own missing-binary error then.
|
||||
*/
|
||||
export function ensureManagedYtdlp(): void {
|
||||
const managed = getYtdlpPath()
|
||||
if (existsSync(managed)) return
|
||||
const seed = getBundledYtdlpPath()
|
||||
if (!existsSync(seed)) return
|
||||
try {
|
||||
mkdirSync(dirname(managed), { recursive: true })
|
||||
copyFileSync(seed, managed)
|
||||
} catch {
|
||||
/* best-effort; a failed seed just leaves the managed copy absent */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step-1 spike: spawn the bundled yt-dlp and read back `--version`.
|
||||
* Proves the main-process → bundled-binary → IPC path end to end.
|
||||
@@ -51,6 +75,9 @@ export function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpdateRes
|
||||
return Promise.resolve({ ok: false, error: 'Unsupported update channel.' })
|
||||
}
|
||||
|
||||
// Self-heal: restore the managed copy from the bundled seed before updating, so
|
||||
// a deleted yt-dlp.exe is re-seeded and then updated in one click.
|
||||
ensureManagedYtdlp()
|
||||
const ytdlpPath = getYtdlpPath()
|
||||
|
||||
if (!existsSync(ytdlpPath)) {
|
||||
@@ -78,3 +105,45 @@ export function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpdateRes
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Startup yt-dlp maintenance: always seed the managed copy, then — if auto-update
|
||||
* is on and the daily throttle has elapsed — self-update it to the configured
|
||||
* channel. Whether it changed is decided by comparing `--version` before/after
|
||||
* (robust against yt-dlp's localized "up to date" wording). Status is pushed to
|
||||
* the renderer so the Settings UI reflects a check it didn't trigger.
|
||||
*
|
||||
* Best-effort: every failure path still records the attempt time (so an
|
||||
* unreachable update server doesn't re-hit the network every launch) and never
|
||||
* throws — a stale binary must never block the app from starting.
|
||||
*/
|
||||
export async function runStartupYtdlpAutoUpdate(
|
||||
send: (status: YtdlpAutoUpdateStatus) => void
|
||||
): Promise<void> {
|
||||
ensureManagedYtdlp()
|
||||
const settings = getSettings()
|
||||
if (!shouldAutoCheckYtdlp(settings.autoUpdateYtdlp, settings.ytdlpLastUpdateCheck, Date.now())) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = settings.ytdlpChannel
|
||||
send({ phase: 'checking', channel })
|
||||
|
||||
const before = await getYtdlpVersion()
|
||||
const result = await updateYtdlp(channel)
|
||||
setSettings({ ytdlpLastUpdateCheck: Date.now() })
|
||||
|
||||
if (!result.ok) {
|
||||
send({ phase: 'error', channel, error: result.error, checkedAt: Date.now() })
|
||||
return
|
||||
}
|
||||
|
||||
const after = await getYtdlpVersion()
|
||||
const changed = before.ok && after.ok && before.version !== after.version
|
||||
send({
|
||||
phase: changed ? 'updated' : 'current',
|
||||
channel,
|
||||
version: after.ok ? after.version : undefined,
|
||||
checkedAt: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user