import type { ProcessPromise } from 'zx' import { Readable } from 'node:stream' import { Bytes, write } from '@fuman/io' import { $ } from 'zx' export async function generateOpusImageBlob(image: Uint8Array) { // todo we should probably not use ffprobe here but whatever lol const proc = $`ffprobe -of json -v error -show_entries stream=codec_name,width,height pipe:0` proc.stdin.write(image) proc.stdin.end() const json = await proc.json() const img = json.streams[0] // https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8 const mime = img.codec_name === 'mjpeg' ? 'image/jpeg' : 'image/png' const description = 'Cover Artwork' const res = Bytes.alloc(image.length + 128) write.uint32be(res, 3) // picture type = album cover write.uint32be(res, mime.length) write.rawString(res, mime) write.uint32be(res, description.length) write.rawString(res, description) write.uint32be(res, img.width) write.uint32be(res, img.height) write.uint32be(res, 0) // color depth write.uint32be(res, 0) // color index (unused, for gifs) write.uint32be(res, image.length) write.bytes(res, image) return res.result() } export async function runMetaflac(options: { path: string tags: Partial > coverPath?: string }) { const params: string[] = [ '--remove-all-tags', ] for (const [key, value] of Object.entries(options.tags)) { if (value == null) continue if (Array.isArray(value)) { for (const v of value) { params.push(`--set-tag=${key}=${v}`) } } else { params.push(`--set-tag=${key}=${value}`) } } if (options.coverPath) { params.push(`--import-picture-from=${options.coverPath}`) } params.push(options.path) await $`metaflac ${params}` } export function generateFfmpegMetadataFlags(metadata: Partial>) { const res: string[] = [] for (const [key, value] of Object.entries(metadata)) { if (value == null) continue if (Array.isArray(value)) { for (const v of value) { res.push('-metadata', `${key}=${v}`) } } else { res.push('-metadata', `${key}=${value}`) } } return res } export async function pipeIntoProc(proc: ProcessPromise, stream: ReadableStream) { const pipe = Readable.fromWeb(stream as any).pipe(proc.stdin) await new Promise((resolve, reject) => { pipe.on('error', reject) pipe.on('finish', resolve) }) }