Upload processed audio tracks to S3

This commit is contained in:
lucast committed 2026-09-29 17:03:19 +02:00
1 parent 0d09198e57
commit 492a54dd0d
6 files changed
+100 -170

No files matched your search

+45 -35
View File
@@ -1,47 +1,57 @@
import { unlink } from "node:fs/promises"
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import Ffmpeg from "fluent-ffmpeg"
import type { H3Event } from "h3"
import { resolveUploadFile } from "./upload-files"
export async function getAudioDuration(event: H3Event, relativePath: string): Promise<number> {
if (!relativePath) throw new Error("filePath is undefined")
const { path: sourcePath } = await resolveUploadFile(event, relativePath)
const jobs: Array<[string, string, string, string, string]> = [
["128k", "mp3", "libmp3lame", "low.mp3", "audio/mpeg"],
["256k", "mp3", "libmp3lame", "mid.mp3", "audio/mpeg"],
["96k", "mp4", "aac", "low.m4a", "audio/mp4"],
["192k", "mp4", "aac", "mid.m4a", "audio/mp4"],
["320k", "mp4", "aac", "high.m4a", "audio/mp4"],
]
function getDuration(sourcePath: string): Promise<number> {
return new Promise((resolve, reject) => {
Ffmpeg.ffprobe(sourcePath, (error, metadata) => {
if (error) return reject(0)
if (error) return reject(error)
if (metadata.format.duration) return resolve(metadata.format.duration)
reject(0)
reject(new Error("Unable to read audio duration"))
})
})
}
export async function processAudioFile(event: H3Event, relativePath: string) {
if (!relativePath) throw new Error("filePath is undefined")
const { path: sourcePath } = await resolveUploadFile(event, relativePath)
const directory = path.dirname(sourcePath)
const jobs: Array<[string, string, string]> = [
["128k", "mp3", "libmp3lame"],
["256k", "mp3", "libmp3lame"],
["96k", "mp4", "aac"],
["192k", "mp4", "aac"],
["320k", "mp4", "aac"],
]
const names = ["low.mp3", "mid.mp3", "low.m4a", "mid.m4a", "high.m4a"]
function transcode(sourcePath: string, outputPath: string, bitrate: string, format: string, codec: string) {
return new Promise<void>((resolve, reject) => {
Ffmpeg(sourcePath)
.noVideo()
.audioBitrate(bitrate)
.toFormat(format)
.audioCodec(codec)
.on("end", () => resolve())
.on("error", reject)
.save(outputPath)
})
}
await Promise.all(
jobs.map(([bitrate, format, codec], index) =>
new Promise<void>((resolve, reject) => {
Ffmpeg(sourcePath)
.noVideo()
.audioBitrate(bitrate)
.toFormat(format)
.audioCodec(codec)
.on("end", () => resolve())
.on("error", reject)
.save(path.join(directory, names[index]!))
}),
),
)
await unlink(sourcePath)
export async function prepareAudioUpload(data: Buffer, extension: string) {
const directory = await mkdtemp(path.join(os.tmpdir(), "music-v2-audio-"))
const sourcePath = path.join(directory, `original${extension ? `.${extension}` : ""}`)
try {
await writeFile(sourcePath, data)
const duration = await getDuration(sourcePath)
const completed = await Promise.allSettled(jobs.map(([bitrate, format, codec, name]) =>
transcode(sourcePath, path.join(directory, name), bitrate, format, codec),
))
const failedJob = completed.find((result) => result.status === "rejected")
if (failedJob?.status === "rejected") throw failedJob.reason
const renditions = await Promise.all(jobs.map(async ([, , , name, contentType]) => ({
name,
contentType,
body: await readFile(path.join(directory, name)),
})))
return { duration, original: data, renditions }
} finally {
await rm(directory, { recursive: true, force: true })
}
}