/** Download an existing Mureka song's stem archive; never generates a song. */ import { mkdir, readFile, writeFile, link, unlink, lstat } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { createHash, randomUUID } from 'node:crypto'; import { murekaAccount, murekaError, murekaRequest, type MurekaOptions } from './providers/mureka-useapi.js'; const sha = (bytes: Buffer): string => createHash('sha256').update(bytes).digest('hex'); const zip = (bytes: Buffer): boolean => bytes.length > 4 && bytes.readUInt32LE(0) === 0x04034b50; async function exists(path: string): Promise { try { await lstat(path); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; throw error; } } /** Publish a fully written file without replacing an existing destination. */ async function publish(path: string, bytes: Buffer | string): Promise { const temp = `${path}.${randomUUID()}.tmp`; await writeFile(temp, bytes, { flag: 'wx', mode: 0o600 }); try { await link(temp, path); } finally { await unlink(temp); } } export async function downloadMurekaStems(songId: string, outputPath: string, options: MurekaOptions = {}) { if (!songId.trim() || !outputPath.trim()) throw murekaError(options, 'Stem download requires a song ID and output ZIP path.'); const path = resolve(outputPath); const manifestPath = `${path}.mureka.json`; if (await exists(path) || await exists(manifestPath)) { try { const record = JSON.parse(await readFile(manifestPath, 'utf8')); const bytes = await readFile(path); if (record.schemaVersion === 1 && record.provider === 'mureka-useapi' && record.songId === songId && record.sha256 === sha(bytes) && zip(bytes)) { return { ...record, path, manifestPath, reused: true }; } } catch { /* Refuse unknown or incomplete files rather than overwrite them. */ } throw murekaError(options, 'Output already exists without matching verified stem provenance. Choose a new output path.'); } let result; try { result = await murekaRequest('music/download', options, { ...murekaAccount(options), song_id: songId, type: 'stem' }); } catch (error) { if ((error as { details?: { httpStatus?: number } }).details?.httpStatus === 403) { throw murekaError(options, 'Mureka refused stem access (HTTP 403). Check stem-download entitlement on the linked account; no archive was downloaded.', { httpStatus: 403 }); } throw error; } const url = result.oss_key; if (typeof url !== 'string' || !URL.canParse(url) || new URL(url).protocol !== 'https:') throw murekaError(options, 'Mureka did not return an HTTPS stem archive URL.'); let response: Response; try { response = await (options.fetcher ?? fetch)(url, { signal: AbortSignal.timeout(60_000) }); } catch { throw murekaError(options, 'Stem archive download failed.'); } if (!response.ok) throw murekaError(options, `Stem archive download returned HTTP ${response.status}.`); const bytes = Buffer.from(await response.arrayBuffer()); if (!zip(bytes)) throw murekaError(options, 'Mureka returned an invalid ZIP signature.'); const record = { schemaVersion: 1, provider: 'mureka-useapi', songId, sha256: sha(bytes), bytes: bytes.length, downloadedAt: new Date().toISOString() }; await mkdir(dirname(path), { recursive: true }); await publish(path, bytes); await publish(manifestPath, `${JSON.stringify(record, null, 2)}\n`); return { ...record, path, manifestPath, reused: false }; }