Files
Firelink/scripts/check-updates.js
T
NimBold 036ff02dac chore(engines): refresh ffmpeg provider builds
Update Windows and Linux FFmpeg source locks to BtbN 8.1.2-22-g94138f6973 from autobuild-2026-07-06-14-19.

Replace the bundled macOS Apple Silicon FFmpeg binary with Martin Riedl snapshot N-125450-gfad2e0bc50 and record its verified checksum.

Teach the update checker to compare FFmpeg provider builds separately from upstream stable releases so future refreshes are not missed.

Keep yt-dlp pinned at the current official stable 2026.07.04 and update Vitest to 4.1.10 so the release update gate stays clean.
2026-07-06 20:43:21 +03:30

218 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
const userAgent = 'firelink-update-check';
function parseJsonFile(file) {
return JSON.parse(fs.readFileSync(path.join(repoRoot, file), 'utf8'));
}
function normalizeVersion(value) {
return String(value || '')
.replace(/^v/, '')
.replace(/^release-/, '');
}
function compareVersions(left, right) {
const a = normalizeVersion(left).split(/[.-]/).map(part => (/^\d+$/.test(part) ? Number(part) : part));
const b = normalizeVersion(right).split(/[.-]/).map(part => (/^\d+$/.test(part) ? Number(part) : part));
const length = Math.max(a.length, b.length);
for (let index = 0; index < length; index += 1) {
const av = a[index] ?? 0;
const bv = b[index] ?? 0;
if (av === bv) continue;
if (typeof av === 'number' && typeof bv === 'number') return av > bv ? 1 : -1;
return String(av).localeCompare(String(bv));
}
return 0;
}
function npmOutdated(cwd) {
try {
execFileSync('npm', ['outdated', '--json'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
return {};
} catch (error) {
const output = error.stdout?.toString() || '{}';
return JSON.parse(output || '{}');
}
}
async function fetchJson(url) {
const response = await fetch(url, { headers: { 'User-Agent': userAgent } });
if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${url}`);
return response.json();
}
async function fetchText(url) {
const response = await fetch(url, { headers: { 'User-Agent': userAgent } });
if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${url}`);
return response.text();
}
async function githubLatest(repo) {
return fetchJson(`https://api.github.com/repos/${repo}/releases/latest`);
}
async function latestFfmpegStable() {
const html = await fetchText('https://ffmpeg.org/releases/');
const versions = [...html.matchAll(/ffmpeg-(\d+\.\d+(?:\.\d+)?)\.tar\.xz/g)].map(match => match[1]);
return [...new Set(versions)].sort(compareVersions).at(-1);
}
async function latestMartinRiedlMacArm64Release() {
const html = await fetchText('https://ffmpeg.martin-riedl.de/');
const releaseSection = html.split('Download Release Build')[1] || '';
const match =
releaseSection.match(/macOS \(Apple Silicon\/arm64\)[\s\S]*?<b>Release:\s*<\/b>\s*([0-9.]+)/) ||
releaseSection.match(/macOS \(Apple Silicon\/arm64\)[\s\S]*?Release:\s*([0-9.]+)/);
return match?.[1];
}
async function latestMartinRiedlMacArm64Snapshot() {
const html = await fetchText('https://ffmpeg.martin-riedl.de/');
const snapshotSection = html.split('Download Snapshot Build')[1]?.split('Download Release Build')[0] || '';
const match =
snapshotSection.match(/macOS \(Apple Silicon\/arm64\)[\s\S]*?<b>Release:\s*<\/b>\s*([A-Za-z0-9.-]+)/) ||
snapshotSection.match(/macOS \(Apple Silicon\/arm64\)[\s\S]*?Release:\s*([A-Za-z0-9.-]+)/);
return match?.[1];
}
async function latestBtbnFfmpegN81Build() {
const releases = await fetchJson('https://api.github.com/repos/BtbN/FFmpeg-Builds/releases?per_page=10');
for (const release of releases) {
if (release.tag_name === 'latest') continue;
const versions = (release.assets || [])
.map(asset => asset.name.match(/^ffmpeg-n(8\.1\.\d+-\d+-g[0-9a-f]+)-(?:win64-gpl-8\.1\.zip|linux64-gpl-8\.1\.tar\.xz)$/)?.[1])
.filter(Boolean);
const unique = [...new Set(versions)];
if (unique.length === 1 && versions.length >= 2) {
return unique[0];
}
}
return undefined;
}
function printNpmReport(label, outdated) {
const entries = Object.entries(outdated);
if (!entries.length) {
console.log(`${label}: current`);
return 0;
}
console.log(`${label}: ${entries.length} outdated package(s)`);
for (const [name, info] of entries) {
console.log(` ${name}: ${info.current} -> ${info.latest} (wanted ${info.wanted})`);
}
return entries.length;
}
function sourceEngineVersions(sourceLock) {
const rows = [];
for (const [target, engines] of Object.entries(sourceLock.targets || {})) {
for (const [engine, meta] of Object.entries(engines)) {
rows.push({ target, engine, version: meta.version, url: meta.url });
}
}
return rows;
}
function packagedEngineVersions(engineLock) {
const rows = [];
for (const [target, targetLock] of Object.entries(engineLock.targets || {})) {
for (const [engine, meta] of Object.entries(targetLock.engines || {})) {
rows.push({ target, engine, version: meta.version });
}
}
return rows;
}
function checkRows(rows, latestByEngine, latestByTargetEngine = {}) {
let outdated = 0;
for (const row of rows) {
const latest = latestByTargetEngine[`${row.target}:${row.engine}`] || latestByEngine[row.engine];
if (!latest) continue;
const current = normalizeVersion(row.version);
const wanted = normalizeVersion(latest);
const status = compareVersions(current, wanted) < 0 ? 'outdated' : 'current';
if (status === 'outdated') outdated += 1;
console.log(` ${row.target} ${row.engine}: ${current} -> ${wanted} ${status}`);
}
return outdated;
}
async function main() {
let outdatedCount = 0;
outdatedCount += printNpmReport('root npm', npmOutdated(repoRoot));
outdatedCount += printNpmReport(
'Firefox extension npm',
npmOutdated(path.join(repoRoot, 'Extensions', 'Firefox'))
);
const [
ytDlp,
deno,
aria2,
ffmpeg,
martinRiedlMacArm64Ffmpeg,
martinRiedlMacArm64Snapshot,
btbnFfmpegN81Build,
] = await Promise.all([
githubLatest('yt-dlp/yt-dlp'),
githubLatest('denoland/deno'),
githubLatest('aria2/aria2'),
latestFfmpegStable(),
latestMartinRiedlMacArm64Release(),
latestMartinRiedlMacArm64Snapshot(),
latestBtbnFfmpegN81Build(),
]);
const latestByEngine = {
'yt-dlp': ytDlp.tag_name,
deno: deno.tag_name,
aria2c: aria2.tag_name,
ffmpeg,
};
const latestByTargetEngine = {
'x86_64-pc-windows-msvc:ffmpeg': btbnFfmpegN81Build || ffmpeg,
'x86_64-unknown-linux-gnu:ffmpeg': btbnFfmpegN81Build || ffmpeg,
'aarch64-apple-darwin:ffmpeg': martinRiedlMacArm64Snapshot || martinRiedlMacArm64Ffmpeg,
};
console.log('\nlatest engines:');
for (const [engine, version] of Object.entries(latestByEngine)) {
console.log(` ${engine}: ${normalizeVersion(version)}`);
}
console.log('\nlatest engine provider builds:');
console.log(` BtbN FFmpeg n8.1 Windows/Linux: ${normalizeVersion(btbnFfmpegN81Build || ffmpeg)}`);
console.log(` Martin Riedl FFmpeg macOS arm64 snapshot: ${normalizeVersion(martinRiedlMacArm64Snapshot || martinRiedlMacArm64Ffmpeg)}`);
console.log('\nengine source lock:');
outdatedCount += checkRows(
sourceEngineVersions(parseJsonFile('engine-sources.lock.json')),
latestByEngine,
latestByTargetEngine
);
console.log('\npackaged engine lock:');
outdatedCount += checkRows(
packagedEngineVersions(parseJsonFile('engines.lock.json')),
latestByEngine,
latestByTargetEngine
);
if (outdatedCount > 0) {
console.error(`\n${outdatedCount} outdated item(s) found.`);
process.exit(1);
}
console.log('\nAll checked packages and engines are current.');
}
main().catch(error => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});