UNPKG

taglib-wasm

Version:

TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers

494 lines (493 loc) 14.3 kB
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 };