diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 9d42ef5..c5f57ff 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -13,11 +13,11 @@ Navidrome is downloaded from its official GitHub release during the desktop buil ## FFmpeg -Resonant desktop builds bundle a platform-specific FFmpeg executable for media transcoding. The binary is supplied by `ffmpeg-static` 5.3.0 and reports FFmpeg 6.0 on Apple Silicon. +Resonant desktop builds bundle a platform-specific FFmpeg executable for media transcoding. The binary and its matching build notices come from the pinned `ffmpeg-static` `b6.1.1` release. The exact FFmpeg build varies by platform and is recorded in the notice bundled with the app (the Apple Silicon binary reports FFmpeg 6.0). - Project: https://ffmpeg.org/ -- Source: https://github.com/FFmpeg/FFmpeg/tree/n6.0 -- Binary packaging: https://github.com/eugeneware/ffmpeg-static/tree/5.3.0 +- Source project: https://github.com/FFmpeg/FFmpeg +- Binary release: https://github.com/eugeneware/ffmpeg-static/releases/tag/b6.1.1 - License: GNU General Public License v3.0 or later -The exact upstream binary build information and license copied by `ffmpeg-static` are included in every packaged Resonant app. +The preparation script downloads and SHA-256 verifies the binary, license, and build information published with that release. The matching notices are included in every packaged Resonant app. diff --git a/package-lock.json b/package-lock.json index 2cb1599..a6d6e79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "@types/spark-md5": "^3.0.5", "@vitejs/plugin-react": "latest", "esbuild": "latest", - "ffmpeg-static": "5.3.0", "jsdom": "latest", "typescript": "latest", "vite": "^8.1.4", @@ -280,22 +279,6 @@ "node": ">=20.19.0" } }, - "node_modules/@derhuerst/http-basic": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", - "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "caseless": "^0.12.0", - "concat-stream": "^2.0.0", - "http-response-object": "^3.0.1", - "parse-cache-control": "^1.0.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -1969,19 +1952,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2037,20 +2007,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2061,22 +2017,6 @@ "node": ">=18" } }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2126,24 +2066,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -2192,16 +2114,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/es-module-lexer": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", @@ -2289,23 +2201,6 @@ } } }, - "node_modules/ffmpeg-static": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", - "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", - "dev": true, - "hasInstallScript": true, - "license": "GPL-3.0-or-later", - "dependencies": { - "@derhuerst/http-basic": "^8.2.0", - "env-paths": "^2.2.0", - "https-proxy-agent": "^5.0.0", - "progress": "^2.0.3" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2334,37 +2229,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/http-response-object": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", - "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^10.0.3" - } - }, - "node_modules/http-response-object/node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -2375,13 +2239,6 @@ "node": ">=8" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -2756,13 +2613,6 @@ "node": ">=4" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.16", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", @@ -2796,12 +2646,6 @@ "node": ">=12.20.0" } }, - "node_modules/parse-cache-control": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", - "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", - "dev": true - }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -2887,16 +2731,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2936,21 +2770,6 @@ "license": "MIT", "peer": true }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -3009,27 +2828,6 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -3086,16 +2884,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -3214,13 +3002,6 @@ "license": "0BSD", "optional": true }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true, - "license": "MIT" - }, "node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", @@ -3273,13 +3054,6 @@ "dev": true, "license": "MIT" }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/vite": { "version": "8.1.4", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", diff --git a/package.json b/package.json index 487414b..fc5c263 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@types/spark-md5": "^3.0.5", "@vitejs/plugin-react": "latest", "esbuild": "latest", - "ffmpeg-static": "5.3.0", "jsdom": "latest", "typescript": "latest", "vite": "^8.1.4", diff --git a/scripts/prepare-navidrome.mjs b/scripts/prepare-navidrome.mjs index b46daf0..85a6222 100644 --- a/scripts/prepare-navidrome.mjs +++ b/scripts/prepare-navidrome.mjs @@ -4,10 +4,11 @@ import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:f import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import ffmpegPath from "ffmpeg-static"; const VERSION = "0.61.2"; const RELEASE_API = `https://api.github.com/repos/navidrome/navidrome/releases/tags/v${VERSION}`; +const FFMPEG_RELEASE = "b6.1.1"; +const FFMPEG_RELEASE_API = `https://api.github.com/repos/eugeneware/ffmpeg-static/releases/tags/${FFMPEG_RELEASE}`; const targetTriple = execFileSync("rustc", ["--print", "host-tuple"], { encoding: "utf8" }).trim(); const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -19,9 +20,18 @@ const archives = { "x86_64-pc-windows-msvc": `navidrome_${VERSION}_windows_amd64.zip`, }; +const ffmpegPlatforms = { + "aarch64-apple-darwin": "darwin-arm64", + "x86_64-apple-darwin": "darwin-x64", + "aarch64-unknown-linux-gnu": "linux-arm64", + "x86_64-unknown-linux-gnu": "linux-x64", + "x86_64-pc-windows-msvc": "win32-x64", +}; + const archiveName = archives[targetTriple]; -if (!archiveName) { - throw new Error(`Navidrome ${VERSION} is not configured for Rust target ${targetTriple}.`); +const ffmpegPlatform = ffmpegPlatforms[targetTriple]; +if (!archiveName || !ffmpegPlatform) { + throw new Error(`Resonant's media sidecars are not configured for Rust target ${targetTriple}.`); } const extension = targetTriple.includes("windows") ? ".exe" : ""; @@ -38,6 +48,8 @@ await mkdir(resourceDirectory, { recursive: true }); let binaryReady = false; let licenseReady = false; let ffmpegReady = false; +let ffmpegLicenseReady = false; +let ffmpegReadmeReady = false; try { await readFile(outputPath); binaryReady = true; @@ -54,18 +66,52 @@ try { await readFile(ffmpegOutputPath); ffmpegReady = true; } catch { - // Copy the pinned npm-provided binary below when missing. + // Download the pinned release asset below when missing. } - -if (!ffmpegPath) throw new Error(`ffmpeg-static does not provide a binary for ${process.platform}-${process.arch}.`); -if (!ffmpegReady) { - await copyFile(ffmpegPath, ffmpegOutputPath); - if (!extension) await chmod(ffmpegOutputPath, 0o755); +try { + await readFile(ffmpegLicensePath); + ffmpegLicenseReady = true; +} catch { + // Download the matching binary license below when missing. +} +try { + await readFile(ffmpegReadmePath); + ffmpegReadmeReady = true; +} catch { + // Download the matching binary build information below when missing. } -await copyFile(join(dirname(ffmpegPath), "ffmpeg.LICENSE"), ffmpegLicensePath); -await copyFile(join(dirname(ffmpegPath), "ffmpeg.README"), ffmpegReadmePath); const headers = { Accept: "application/vnd.github+json", "User-Agent": "Resonant-sidecar-preparer" }; +const verifiedReleaseAsset = async (release, assetName) => { + const asset = release.assets.find((candidate) => candidate.name === assetName); + if (!asset) throw new Error(`The pinned release does not contain ${assetName}.`); + const response = await fetch(asset.browser_download_url, { headers }); + if (!response.ok) throw new Error(`Could not download ${assetName} (${response.status}).`); + const bytes = Buffer.from(await response.arrayBuffer()); + const actualDigest = createHash("sha256").update(bytes).digest("hex"); + const expectedDigest = asset.digest?.replace(/^sha256:/, ""); + if (!expectedDigest || actualDigest !== expectedDigest) { + throw new Error(`Checksum verification failed for ${assetName}.`); + } + return bytes; +}; + +if (!ffmpegReady || !ffmpegLicenseReady || !ffmpegReadmeReady) { + const releaseResponse = await fetch(FFMPEG_RELEASE_API, { headers }); + if (!releaseResponse.ok) throw new Error(`Could not read FFmpeg ${FFMPEG_RELEASE} release metadata (${releaseResponse.status}).`); + const release = await releaseResponse.json(); + if (!ffmpegReady) { + await writeFile(ffmpegOutputPath, await verifiedReleaseAsset(release, `ffmpeg-${ffmpegPlatform}`)); + if (!extension) await chmod(ffmpegOutputPath, 0o755); + } + if (!ffmpegLicenseReady) { + await writeFile(ffmpegLicensePath, await verifiedReleaseAsset(release, `${ffmpegPlatform}.LICENSE`)); + } + if (!ffmpegReadmeReady) { + await writeFile(ffmpegReadmePath, await verifiedReleaseAsset(release, `${ffmpegPlatform}.README`)); + } +} + if (!licenseReady) { const licenseResponse = await fetch(`https://raw.githubusercontent.com/navidrome/navidrome/v${VERSION}/LICENSE`, { headers }); if (!licenseResponse.ok) throw new Error(`Could not download the Navidrome license (${licenseResponse.status}).`); @@ -79,17 +125,7 @@ if (binaryReady) { const releaseResponse = await fetch(RELEASE_API, { headers }); if (!releaseResponse.ok) throw new Error(`Could not read the Navidrome release metadata (${releaseResponse.status}).`); const release = await releaseResponse.json(); -const asset = release.assets.find((candidate) => candidate.name === archiveName); -if (!asset) throw new Error(`The official Navidrome release does not contain ${archiveName}.`); - -const archiveResponse = await fetch(asset.browser_download_url, { headers }); -if (!archiveResponse.ok) throw new Error(`Could not download ${archiveName} (${archiveResponse.status}).`); -const archive = Buffer.from(await archiveResponse.arrayBuffer()); -const actualDigest = createHash("sha256").update(archive).digest("hex"); -const expectedDigest = asset.digest?.replace(/^sha256:/, ""); -if (!expectedDigest || actualDigest !== expectedDigest) { - throw new Error(`Checksum verification failed for ${archiveName}.`); -} +const archive = await verifiedReleaseAsset(release, archiveName); const workingDirectory = await mkdtemp(join(tmpdir(), "resonant-navidrome-")); try { @@ -103,4 +139,4 @@ try { await rm(workingDirectory, { recursive: true, force: true }); } -process.stdout.write(`Downloaded and verified Navidrome ${VERSION}; FFmpeg is ready for ${targetTriple}.\n`); +process.stdout.write(`Downloaded and verified Navidrome ${VERSION}; FFmpeg ${FFMPEG_RELEASE} is ready for ${targetTriple}.\n`);