taglib-wasm
Version:
TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers
494 lines (493 loc) • 14.3 kB
JavaScript
import { PICTURE_TYPE_VALUES } from "./types.js";
import {
FileOperationError,
InvalidFormatError,
MetadataError
} from "./errors.js";
import { writeFileData } from "./utils/write.js";
import { getGlobalWorkerPool } from "./worker-pool.js";
let cachedTagLib = null;
let useWorkerPool = false;
let workerPoolInstance = null;
function setWorkerPoolMode(enabled, pool) {
useWorkerPool = enabled;
if (enabled && pool) {
workerPoolInstance = pool;
} else if (enabled && !workerPoolInstance) {
workerPoolInstance = getGlobalWorkerPool();
} else if (!enabled) {
workerPoolInstance = null;
}
}
async function getTagLib() {
if (!cachedTagLib) {
const { TagLib } = await import("./taglib.js");
cachedTagLib = await TagLib.initialize();
}
return cachedTagLib;
}
async function readTags(file) {
if (useWorkerPool && workerPoolInstance && (typeof file === "string" || file instanceof Uint8Array)) {
return workerPoolInstance.readTags(file);
}
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
return audioFile.tag();
} finally {
audioFile.dispose();
}
}
async function applyTags(file, tags, options) {
if (useWorkerPool && workerPoolInstance && (typeof file === "string" || file instanceof Uint8Array)) {
return workerPoolInstance.applyTags(file, tags);
}
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
const tag = audioFile.tag();
if (tags.title !== void 0) tag.setTitle(tags.title);
if (tags.artist !== void 0) tag.setArtist(tags.artist);
if (tags.album !== void 0) tag.setAlbum(tags.album);
if (tags.comment !== void 0) tag.setComment(tags.comment);
if (tags.genre !== void 0) tag.setGenre(tags.genre);
if (tags.year !== void 0) tag.setYear(tags.year);
if (tags.track !== void 0) tag.setTrack(tags.track);
if (!audioFile.save()) {
throw new FileOperationError(
"save",
"Failed to save metadata changes. The file may be read-only or corrupted."
);
}
return audioFile.getFileBuffer();
} finally {
audioFile.dispose();
}
}
async function updateTags(file, tags, options) {
if (typeof file !== "string") {
throw new Error("updateTags requires a file path string to save changes");
}
if (useWorkerPool && workerPoolInstance) {
return workerPoolInstance.updateTags(file, tags);
}
const modifiedBuffer = await applyTags(file, tags, options);
await writeFileData(file, modifiedBuffer);
}
async function readProperties(file) {
if (useWorkerPool && workerPoolInstance && (typeof file === "string" || file instanceof Uint8Array)) {
const props = await workerPoolInstance.readProperties(file);
if (!props) {
throw new MetadataError(
"read",
"File may not contain valid audio data",
"audioProperties"
);
}
return props;
}
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
const props = audioFile.audioProperties();
if (!props) {
throw new MetadataError(
"read",
"File may not contain valid audio data",
"audioProperties"
);
}
return props;
} finally {
audioFile.dispose();
}
}
const Title = "title";
const Artist = "artist";
const Album = "album";
const Comment = "comment";
const Genre = "genre";
const Year = "year";
const Track = "track";
const AlbumArtist = "albumartist";
const Composer = "composer";
const DiscNumber = "discnumber";
async function isValidAudioFile(file) {
try {
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
const valid = audioFile.isValid();
audioFile.dispose();
return valid;
} catch {
return false;
}
}
async function getFormat(file) {
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
return void 0;
}
return audioFile.getFormat();
} finally {
audioFile.dispose();
}
}
async function clearTags(file) {
return applyTags(file, {
title: "",
artist: "",
album: "",
comment: "",
genre: "",
year: 0,
track: 0
});
}
async function readPictures(file) {
if (useWorkerPool && workerPoolInstance && (typeof file === "string" || file instanceof Uint8Array)) {
return workerPoolInstance.readPictures(file);
}
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
return audioFile.getPictures();
} finally {
audioFile.dispose();
}
}
async function applyPictures(file, pictures) {
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
audioFile.setPictures(pictures);
if (!audioFile.save()) {
throw new FileOperationError(
"save",
"Failed to save picture changes. The file may be read-only or corrupted."
);
}
return audioFile.getFileBuffer();
} finally {
audioFile.dispose();
}
}
async function addPicture(file, picture) {
const taglib = await getTagLib();
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
audioFile.addPicture(picture);
if (!audioFile.save()) {
throw new FileOperationError(
"save",
"Failed to save picture changes. The file may be read-only or corrupted."
);
}
return audioFile.getFileBuffer();
} finally {
audioFile.dispose();
}
}
async function clearPictures(file) {
return applyPictures(file, []);
}
async function getCoverArt(file) {
const pictures = await readPictures(file);
if (pictures.length === 0) {
return null;
}
const frontCover = pictures.find(
(pic) => pic.type === PICTURE_TYPE_VALUES.FrontCover
);
if (frontCover) {
return frontCover.data;
}
return pictures[0].data;
}
async function setCoverArt(file, imageData, mimeType) {
if (useWorkerPool && workerPoolInstance && (typeof file === "string" || file instanceof Uint8Array)) {
return workerPoolInstance.setCoverArt(file, imageData, mimeType);
}
const picture = {
mimeType,
data: imageData,
type: PICTURE_TYPE_VALUES.FrontCover,
description: "Front Cover"
};
return applyPictures(file, [picture]);
}
function findPictureByType(pictures, type) {
const typeValue = typeof type === "string" ? PICTURE_TYPE_VALUES[type] : type;
return pictures.find((pic) => pic.type === typeValue) || null;
}
async function replacePictureByType(file, newPicture) {
const pictures = await readPictures(file);
const filteredPictures = pictures.filter(
(pic) => pic.type !== newPicture.type
);
filteredPictures.push(newPicture);
return applyPictures(file, filteredPictures);
}
async function getPictureMetadata(file) {
const pictures = await readPictures(file);
return pictures.map((pic) => ({
type: pic.type,
mimeType: pic.mimeType,
description: pic.description,
size: pic.data.length
}));
}
async function readTagsBatch(files, options = {}) {
const startTime = Date.now();
const {
concurrency = 4,
continueOnError = true,
onProgress
} = options;
const results = [];
const errors = [];
const taglib = await getTagLib();
let processed = 0;
const total = files.length;
for (let i = 0; i < files.length; i += concurrency) {
const chunk = files.slice(i, i + concurrency);
const chunkPromises = chunk.map(async (file, idx) => {
const fileIndex = i + idx;
const fileName = typeof file === "string" ? file : `file-${fileIndex}`;
try {
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
const tags = audioFile.tag();
results.push({ file: fileName, data: tags });
} finally {
audioFile.dispose();
}
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
errors.push({ file: fileName, error: err });
if (!continueOnError) {
throw err;
}
}
processed++;
onProgress?.(processed, total, fileName);
});
await Promise.all(chunkPromises);
}
return {
results,
errors,
duration: Date.now() - startTime
};
}
async function readPropertiesBatch(files, options = {}) {
const startTime = Date.now();
const {
concurrency = 4,
continueOnError = true,
onProgress
} = options;
const results = [];
const errors = [];
const taglib = await getTagLib();
let processed = 0;
const total = files.length;
for (let i = 0; i < files.length; i += concurrency) {
const chunk = files.slice(i, i + concurrency);
const chunkPromises = chunk.map(async (file, idx) => {
const fileIndex = i + idx;
const fileName = typeof file === "string" ? file : `file-${fileIndex}`;
try {
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
const properties = audioFile.audioProperties();
results.push({ file: fileName, data: properties });
} finally {
audioFile.dispose();
}
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
errors.push({ file: fileName, error: err });
if (!continueOnError) {
throw err;
}
}
processed++;
onProgress?.(processed, total, fileName);
});
await Promise.all(chunkPromises);
}
return {
results,
errors,
duration: Date.now() - startTime
};
}
async function readMetadataBatch(files, options = {}) {
const startTime = Date.now();
const {
concurrency = 4,
continueOnError = true,
onProgress
} = options;
const results = [];
const errors = [];
const taglib = await getTagLib();
let processed = 0;
const total = files.length;
for (let i = 0; i < files.length; i += concurrency) {
const chunk = files.slice(i, i + concurrency);
const chunkPromises = chunk.map(async (file, idx) => {
const fileIndex = i + idx;
const fileName = typeof file === "string" ? file : `file-${fileIndex}`;
try {
const audioFile = await taglib.open(file);
try {
if (!audioFile.isValid()) {
throw new InvalidFormatError(
"File may be corrupted or in an unsupported format"
);
}
const tags = audioFile.tag();
const properties = audioFile.audioProperties();
const pictures = audioFile.getPictures();
const hasCoverArt = pictures.length > 0;
const dynamics = {};
const replayGainTrackGain = audioFile.getProperty(
"REPLAYGAIN_TRACK_GAIN"
);
if (replayGainTrackGain) {
dynamics.replayGainTrackGain = replayGainTrackGain;
}
const replayGainTrackPeak = audioFile.getProperty(
"REPLAYGAIN_TRACK_PEAK"
);
if (replayGainTrackPeak) {
dynamics.replayGainTrackPeak = replayGainTrackPeak;
}
const replayGainAlbumGain = audioFile.getProperty(
"REPLAYGAIN_ALBUM_GAIN"
);
if (replayGainAlbumGain) {
dynamics.replayGainAlbumGain = replayGainAlbumGain;
}
const replayGainAlbumPeak = audioFile.getProperty(
"REPLAYGAIN_ALBUM_PEAK"
);
if (replayGainAlbumPeak) {
dynamics.replayGainAlbumPeak = replayGainAlbumPeak;
}
let appleSoundCheck = audioFile.getProperty("ITUNNORM");
if (!appleSoundCheck && audioFile.isMP4()) {
appleSoundCheck = audioFile.getMP4Item(
"----:com.apple.iTunes:iTunNORM"
);
}
if (appleSoundCheck) dynamics.appleSoundCheck = appleSoundCheck;
results.push({
file: fileName,
data: {
tags,
properties,
hasCoverArt,
dynamics: Object.keys(dynamics).length > 0 ? dynamics : void 0
}
});
} finally {
audioFile.dispose();
}
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
errors.push({ file: fileName, error: err });
if (!continueOnError) {
throw err;
}
}
processed++;
onProgress?.(processed, total, fileName);
});
await Promise.all(chunkPromises);
}
return {
results,
errors,
duration: Date.now() - startTime
};
}
import { PICTURE_TYPE_NAMES, PICTURE_TYPE_VALUES as PICTURE_TYPE_VALUES2 } from "./types.js";
export {
Album,
AlbumArtist,
Artist,
Comment,
Composer,
DiscNumber,
Genre,
PICTURE_TYPE_NAMES,
PICTURE_TYPE_VALUES2 as PICTURE_TYPE_VALUES,
Title,
Track,
Year,
addPicture,
applyPictures,
applyTags,
clearPictures,
clearTags,
findPictureByType,
getCoverArt,
getFormat,
getPictureMetadata,
isValidAudioFile,
readMetadataBatch,
readPictures,
readProperties,
readPropertiesBatch,
readTags,
readTagsBatch,
replacePictureByType,
setCoverArt,
setWorkerPoolMode,
updateTags
};