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:
@@ -41,6 +41,7 @@ import {
|
||||
type YtdlpVersionResult,
|
||||
type YtdlpUpdateChannel,
|
||||
type YtdlpUpdateResult,
|
||||
type FfmpegVersionResult,
|
||||
type AppUpdateInfo,
|
||||
type MediaKind,
|
||||
type CookieSource,
|
||||
@@ -209,6 +210,9 @@ export function SettingsView(): React.JSX.Element {
|
||||
const customCommandEnabled = useSettings((s) => s.customCommandEnabled)
|
||||
const defaultTemplateId = useSettings((s) => s.defaultTemplateId)
|
||||
const notifyOnComplete = useSettings((s) => s.notifyOnComplete)
|
||||
const autoUpdateYtdlp = useSettings((s) => s.autoUpdateYtdlp)
|
||||
const ytdlpChannel = useSettings((s) => s.ytdlpChannel)
|
||||
const ytdlpLastUpdateCheck = useSettings((s) => s.ytdlpLastUpdateCheck)
|
||||
const update = useSettings((s) => s.update)
|
||||
const templates = useTemplates((s) => s.templates)
|
||||
const errorEntries = useErrorLog((s) => s.entries)
|
||||
@@ -219,8 +223,8 @@ export function SettingsView(): React.JSX.Element {
|
||||
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [version, setVersion] = useState<YtdlpVersionResult | null>(null)
|
||||
const [ffmpeg, setFfmpeg] = useState<FfmpegVersionResult | null>(null)
|
||||
|
||||
const [updateChannel, setUpdateChannel] = useState<YtdlpUpdateChannel>('stable')
|
||||
const [updating, setUpdating] = useState(false)
|
||||
const [updateResult, setUpdateResult] = useState<YtdlpUpdateResult | null>(null)
|
||||
|
||||
@@ -251,6 +255,28 @@ export function SettingsView(): React.JSX.Element {
|
||||
return window.api.onAppUpdateProgress((p) => setAppFraction(p.fraction))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Show the current yt-dlp version without a manual click, and reflect a
|
||||
// background auto-update (which may run on launch) live in this panel.
|
||||
window.api.getYtdlpVersion().then(setVersion).catch(() => {})
|
||||
// ffmpeg/ffprobe are display-only (bundled, not auto-updated); load them once.
|
||||
window.api.getFfmpegVersions().then(setFfmpeg).catch(() => {})
|
||||
return window.api.onYtdlpAutoUpdateStatus((s) => {
|
||||
if (s.phase === 'checking') {
|
||||
setChecking(true)
|
||||
return
|
||||
}
|
||||
setChecking(false)
|
||||
if (s.version) setVersion({ ok: true, version: s.version })
|
||||
if (s.checkedAt) useSettings.setState({ ytdlpLastUpdateCheck: s.checkedAt })
|
||||
if (s.phase === 'updated') {
|
||||
setUpdateResult({ ok: true, output: `Updated to yt-dlp ${s.version ?? ''}`.trim() })
|
||||
} else if (s.phase === 'error' && s.error) {
|
||||
setUpdateResult({ ok: false, error: s.error })
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
async function checkAppUpdate(): Promise<void> {
|
||||
setAppChecking(true)
|
||||
setAppUpd(null)
|
||||
@@ -378,7 +404,7 @@ export function SettingsView(): React.JSX.Element {
|
||||
setUpdating(true)
|
||||
setUpdateResult(null)
|
||||
try {
|
||||
const result = await window.api.updateYtdlp(updateChannel)
|
||||
const result = await window.api.updateYtdlp(ytdlpChannel)
|
||||
setUpdateResult(result)
|
||||
if (result.ok) setVersion(null) // stale — prompt a re-check rather than show a wrong version
|
||||
} catch (e) {
|
||||
@@ -900,14 +926,21 @@ export function SettingsView(): React.JSX.Element {
|
||||
<Subtitle2>About</Subtitle2>
|
||||
</div>
|
||||
<Caption1 className={styles.hint}>
|
||||
AeroFetch is a generic frontend for yt-dlp. It bundles yt-dlp and ffmpeg.
|
||||
AeroFetch is a generic frontend for yt-dlp. It bundles ffmpeg and manages its
|
||||
own copy of yt-dlp, keeping it up to date automatically.
|
||||
</Caption1>
|
||||
<div className={styles.folderRow}>
|
||||
<Button onClick={checkVersion} disabled={checking}>
|
||||
Check yt-dlp version
|
||||
</Button>
|
||||
{checking && <Spinner size="tiny" />}
|
||||
</div>
|
||||
|
||||
<Field
|
||||
label="Keep yt-dlp updated automatically"
|
||||
hint="Checks once a day on launch and updates yt-dlp in the background, so YouTube changes don't start breaking downloads with 403 errors."
|
||||
>
|
||||
<Switch
|
||||
checked={autoUpdateYtdlp}
|
||||
onChange={(_, d) => update({ autoUpdateYtdlp: d.checked })}
|
||||
label={autoUpdateYtdlp ? 'On' : 'Off'}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{version?.ok && (
|
||||
<Text style={{ fontFamily: tokens.fontFamilyMonospace }}>yt-dlp {version.version}</Text>
|
||||
)}
|
||||
@@ -916,13 +949,37 @@ export function SettingsView(): React.JSX.Element {
|
||||
{version.error}
|
||||
</Caption1>
|
||||
)}
|
||||
{ffmpeg && (
|
||||
<>
|
||||
<Text style={{ fontFamily: tokens.fontFamilyMonospace }}>
|
||||
ffmpeg {ffmpeg.ffmpeg ?? 'not found'}
|
||||
</Text>
|
||||
<Text style={{ fontFamily: tokens.fontFamilyMonospace }}>
|
||||
ffprobe {ffmpeg.ffprobe ?? 'not found'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{ytdlpLastUpdateCheck > 0 && (
|
||||
<Caption1 className={styles.hint}>
|
||||
Last checked for updates {new Date(ytdlpLastUpdateCheck).toLocaleString()}.
|
||||
</Caption1>
|
||||
)}
|
||||
<div className={styles.folderRow}>
|
||||
<Button onClick={checkVersion} disabled={checking}>
|
||||
Check yt-dlp version
|
||||
</Button>
|
||||
{checking && <Spinner size="tiny" />}
|
||||
</div>
|
||||
|
||||
<Field label="Update channel" hint="Nightly tracks yt-dlp's daily build; stable is the tagged release.">
|
||||
<Field
|
||||
label="Update channel"
|
||||
hint="Nightly tracks yt-dlp's daily build; stable is the tagged release. Nightly follows YouTube changes fastest."
|
||||
>
|
||||
<Select
|
||||
aria-label="Update channel"
|
||||
value={updateChannel}
|
||||
value={ytdlpChannel}
|
||||
options={UPDATE_CHANNEL_OPTIONS}
|
||||
onChange={(v) => setUpdateChannel(v as YtdlpUpdateChannel)}
|
||||
onChange={(v) => update({ ytdlpChannel: v as YtdlpUpdateChannel })}
|
||||
/>
|
||||
</Field>
|
||||
<div className={styles.folderRow}>
|
||||
|
||||
@@ -29,6 +29,9 @@ if (import.meta.env.DEV && !window.api) {
|
||||
cookiesBrowser: 'chrome',
|
||||
restrictFilenames: false,
|
||||
downloadArchive: false,
|
||||
autoUpdateYtdlp: true,
|
||||
ytdlpChannel: 'nightly',
|
||||
ytdlpLastUpdateCheck: Date.now() - 3_600_000,
|
||||
customCommandEnabled: false,
|
||||
defaultTemplateId: null,
|
||||
notifyOnComplete: true,
|
||||
@@ -67,6 +70,7 @@ if (import.meta.env.DEV && !window.api) {
|
||||
runAppUpdate: async () => ({ ok: true }),
|
||||
onAppUpdateProgress: () => () => {},
|
||||
getYtdlpVersion: async () => ({ ok: true, version: '2025.06.01 (UI preview mock)' }),
|
||||
getFfmpegVersions: async () => ({ ffmpeg: '7.1-full_build (UI preview mock)', ffprobe: '7.1-full_build (UI preview mock)' }),
|
||||
probe: async (url: string) => {
|
||||
await new Promise((r) => setTimeout(r, 700)) // simulate network latency
|
||||
// A 'list'/'playlist' URL exercises the playlist selection UI in preview.
|
||||
@@ -154,6 +158,8 @@ if (import.meta.env.DEV && !window.api) {
|
||||
await new Promise((r) => setTimeout(r, 600)) // simulate the self-update run
|
||||
return { ok: true, output: `yt-dlp is already up to date (channel: ${channel}, UI preview mock)` }
|
||||
},
|
||||
// No background updater in the browser preview — hand back an inert unsubscribe.
|
||||
onYtdlpAutoUpdateStatus: () => () => {},
|
||||
listErrorLog: async () => [],
|
||||
clearErrorLog: async () => [],
|
||||
exportBackup: async () => ({ ok: true, path: 'C:\\Users\\you\\Downloads\\aerofetch-backup.json' }),
|
||||
|
||||
@@ -23,6 +23,9 @@ const FALLBACK: Settings = {
|
||||
cookiesBrowser: 'chrome',
|
||||
restrictFilenames: false,
|
||||
downloadArchive: false,
|
||||
autoUpdateYtdlp: true,
|
||||
ytdlpChannel: 'nightly',
|
||||
ytdlpLastUpdateCheck: 0,
|
||||
customCommandEnabled: false,
|
||||
defaultTemplateId: null,
|
||||
notifyOnComplete: true,
|
||||
|
||||
Reference in New Issue
Block a user