UNPKG

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
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 };