teidesu-scripts/utils/media-metadata.ts
2025-09-14 21:52:13 +00:00

102 lines
2.6 KiB
TypeScript

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<Record<
| 'TITLE'
| 'ARTIST'
| 'COMPOSER'
| 'ALBUM'
| 'DATE'
| 'DISCNUMBER'
| 'TRACKNUMBER'
| 'COMMENT'
| 'PRODUCER'
| 'COPYRIGHT'
| 'ISRC'
| 'LYRICS'
| 'MAIN_ARTIST',
string | number | string[] | null
>
>
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<Record<string, string | string[]>>) {
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<void>((resolve, reject) => {
pipe.on('error', reject)
pipe.on('finish', resolve)
})
}