taglib-wasm
Version:
TagLib-Wasm is the universal tagging library for TypeScript/JavaScript platforms: Browsers, Node.js, Deno, Bun, Cloudflare Workers, and Electron apps
123 lines (122 loc) • 3.94 kB
JavaScript
import { isNamedAudioInput } from "../types/audio-formats.js";
import { InvalidFormatError } from "../errors.js";
import { readExtendedTag } from "../utils/tag-mapping.js";
import { getTagLib } from "./config.js";
async function executeBatch(files, options, processor) {
if (files.length === 0) return { items: [], duration: 0 };
const startTime = Date.now();
const { concurrency = 4, continueOnError = true, onProgress, signal } = options;
const items = new Array(files.length);
const taglib = await getTagLib();
let processed = 0;
const total = files.length;
for (let i = 0; i < files.length; i += concurrency) {
signal?.throwIfAborted();
const chunk = files.slice(i, i + concurrency);
const chunkPromises = chunk.map(async (file, idx) => {
const index = i + idx;
const fileName = typeof file === "string" ? file : file instanceof File ? file.name : isNamedAudioInput(file) ? file.name : `buffer-${index}`;
try {
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
items[index] = {
status: "ok",
path: fileName,
data: processor(audioFile)
};
} finally {
audioFile.dispose();
}
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
items[index] = { status: "error", path: fileName, error: err };
if (!continueOnError) throw err;
}
processed++;
onProgress?.(processed, total, fileName);
});
await Promise.all(chunkPromises);
}
return { items, duration: Date.now() - startTime };
}
async function readTagsBatch(files, options = {}) {
return executeBatch(
files,
options,
(audioFile) => readExtendedTag(audioFile)
);
}
async function readPropertiesBatch(files, options = {}) {
return executeBatch(
files,
options,
(audioFile) => audioFile.audioProperties()
);
}
function extractDynamics(audioFile) {
const dynamics = {};
const fields = [
"replayGainTrackGain",
"replayGainTrackPeak",
"replayGainAlbumGain",
"replayGainAlbumPeak"
];
for (const field of fields) {
const val = audioFile.getProperty(field);
if (val) dynamics[field] = val;
}
let appleSoundCheck = audioFile.getProperty("appleSoundCheck");
if (!appleSoundCheck && audioFile.isMP4()) {
appleSoundCheck = audioFile.getMP4Item("----:com.apple.iTunes:iTunNORM");
}
if (appleSoundCheck) dynamics.appleSoundCheck = appleSoundCheck;
return Object.keys(dynamics).length > 0 ? dynamics : void 0;
}
async function readMetadata(file) {
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
let name;
if (typeof file === "string") {
name = file;
} else if (file instanceof File) {
name = file.name;
} else if (isNamedAudioInput(file)) {
name = file.name;
} else {
name = `buffer (${file.byteLength} bytes)`;
}
throw new InvalidFormatError(
`File may be corrupted or in an unsupported format. File: ${name}`
);
}
return {
tags: readExtendedTag(audioFile),
properties: audioFile.audioProperties(),
hasCoverArt: audioFile.getPictures().length > 0,
dynamics: extractDynamics(audioFile)
};
} finally {
audioFile.dispose();
}
}
async function readMetadataBatch(files, options = {}) {
return executeBatch(files, options, (audioFile) => ({
tags: readExtendedTag(audioFile),
properties: audioFile.audioProperties(),
hasCoverArt: audioFile.getPictures().length > 0,
dynamics: extractDynamics(audioFile)
}));
}
export {
readMetadata,
readMetadataBatch,
readPropertiesBatch,
readTagsBatch
};