import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; 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 targetTriple = execFileSync("rustc", ["--print", "host-tuple"], { encoding: "utf8" }).trim(); const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const archives = { "aarch64-apple-darwin": `navidrome_${VERSION}_darwin_arm64.tar.gz`, "x86_64-apple-darwin": `navidrome_${VERSION}_darwin_amd64.tar.gz`, "aarch64-unknown-linux-gnu": `navidrome_${VERSION}_linux_arm64.tar.gz`, "x86_64-unknown-linux-gnu": `navidrome_${VERSION}_linux_amd64.tar.gz`, "x86_64-pc-windows-msvc": `navidrome_${VERSION}_windows_amd64.zip`, }; const archiveName = archives[targetTriple]; if (!archiveName) { throw new Error(`Navidrome ${VERSION} is not configured for Rust target ${targetTriple}.`); } const extension = targetTriple.includes("windows") ? ".exe" : ""; const outputDirectory = join(projectRoot, "src-tauri/binaries"); const outputPath = join(outputDirectory, `navidrome-${targetTriple}${extension}`); const ffmpegOutputPath = join(outputDirectory, `ffmpeg-${targetTriple}${extension}`); const resourceDirectory = join(projectRoot, "src-tauri/resources"); const licensePath = join(resourceDirectory, "navidrome-LICENSE"); const ffmpegLicensePath = join(resourceDirectory, "ffmpeg-LICENSE"); const ffmpegReadmePath = join(resourceDirectory, "ffmpeg-README"); await mkdir(outputDirectory, { recursive: true }); await mkdir(resourceDirectory, { recursive: true }); let binaryReady = false; let licenseReady = false; let ffmpegReady = false; try { await readFile(outputPath); binaryReady = true; } catch { // Download the pinned official release below when missing. } try { await readFile(licensePath); licenseReady = true; } catch { // Include Navidrome's GPL license in the packaged app when missing. } try { await readFile(ffmpegOutputPath); ffmpegReady = true; } catch { // Copy the pinned npm-provided binary 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); } 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" }; 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}).`); await writeFile(licensePath, await licenseResponse.text()); } if (binaryReady) { process.stdout.write(`Navidrome ${VERSION} and FFmpeg sidecars are ready for ${targetTriple}.\n`); process.exit(0); } 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 workingDirectory = await mkdtemp(join(tmpdir(), "resonant-navidrome-")); try { const archivePath = join(workingDirectory, archiveName); await writeFile(archivePath, archive); execFileSync("tar", ["-xf", archivePath], { cwd: workingDirectory, stdio: "inherit" }); const extractedBinary = join(workingDirectory, `navidrome${extension}`); await copyFile(extractedBinary, outputPath); if (!extension) await chmod(outputPath, 0o755); } finally { await rm(workingDirectory, { recursive: true, force: true }); } process.stdout.write(`Downloaded and verified Navidrome ${VERSION}; FFmpeg is ready for ${targetTriple}.\n`);