UNPKG

taglib-wasm

Version:

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

720 lines (719 loc) 21.6 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { InvalidFormatError, MetadataError, TagLibInitializationError, UnsupportedFormatError } from "./errors.js"; import { getFileSize, readFileData, readPartialFileData } from "./utils/file.js"; import { writeFileData } from "./utils/write.js"; import { getGlobalWorkerPool } from "./worker-pool.js"; class AudioFileImpl { constructor(module, fileHandle, sourcePath, originalSource, isPartiallyLoaded = false, partialLoadOptions) { this.module = module; __publicField(this, "fileHandle"); __publicField(this, "cachedTag", null); __publicField(this, "cachedAudioProperties", null); __publicField(this, "sourcePath"); __publicField(this, "originalSource"); __publicField(this, "isPartiallyLoaded", false); __publicField(this, "partialLoadOptions"); this.fileHandle = fileHandle; this.sourcePath = sourcePath; this.originalSource = originalSource; this.isPartiallyLoaded = isPartiallyLoaded; this.partialLoadOptions = partialLoadOptions; } /** @inheritdoc */ getFormat() { return this.fileHandle.getFormat(); } /** @inheritdoc */ tag() { const tagWrapper = this.fileHandle.getTag(); if (!tagWrapper) { throw new MetadataError( "read", "Tag may be corrupted or format not fully supported" ); } return { title: tagWrapper.title(), artist: tagWrapper.artist(), album: tagWrapper.album(), comment: tagWrapper.comment(), genre: tagWrapper.genre(), year: tagWrapper.year(), track: tagWrapper.track(), setTitle: (value) => tagWrapper.setTitle(value), setArtist: (value) => tagWrapper.setArtist(value), setAlbum: (value) => tagWrapper.setAlbum(value), setComment: (value) => tagWrapper.setComment(value), setGenre: (value) => tagWrapper.setGenre(value), setYear: (value) => tagWrapper.setYear(value), setTrack: (value) => tagWrapper.setTrack(value) }; } /** @inheritdoc */ audioProperties() { if (!this.cachedAudioProperties) { const propsWrapper = this.fileHandle.getAudioProperties(); if (!propsWrapper) { return null; } this.cachedAudioProperties = { length: propsWrapper.lengthInSeconds(), bitrate: propsWrapper.bitrate(), sampleRate: propsWrapper.sampleRate(), channels: propsWrapper.channels(), bitsPerSample: propsWrapper.bitsPerSample(), codec: propsWrapper.codec(), containerFormat: propsWrapper.containerFormat(), isLossless: propsWrapper.isLossless() }; } return this.cachedAudioProperties; } /** @inheritdoc */ properties() { const jsObj = this.fileHandle.getProperties(); const result = {}; const keys = Object.keys(jsObj); for (const key of keys) { result[key] = jsObj[key]; } return result; } /** @inheritdoc */ setProperties(properties) { this.fileHandle.setProperties(properties); } /** @inheritdoc */ getProperty(key) { const value = this.fileHandle.getProperty(key); return value === "" ? void 0 : value; } /** @inheritdoc */ setProperty(key, value) { this.fileHandle.setProperty(key, value); } /** @inheritdoc */ isMP4() { return this.fileHandle.isMP4(); } /** @inheritdoc */ getMP4Item(key) { if (!this.isMP4()) { const format = this.getFormat(); throw new UnsupportedFormatError( format, ["MP4", "M4A"] ); } const value = this.fileHandle.getMP4Item(key); return value === "" ? void 0 : value; } /** @inheritdoc */ setMP4Item(key, value) { if (!this.isMP4()) { const format = this.getFormat(); throw new UnsupportedFormatError( format, ["MP4", "M4A"] ); } this.fileHandle.setMP4Item(key, value); } /** @inheritdoc */ removeMP4Item(key) { if (!this.isMP4()) { const format = this.getFormat(); throw new UnsupportedFormatError( format, ["MP4", "M4A"] ); } this.fileHandle.removeMP4Item(key); } /** @inheritdoc */ save() { if (this.isPartiallyLoaded && this.originalSource) { throw new Error( "Cannot save partially loaded file directly. Use saveToFile() instead, which will automatically load the full file." ); } this.cachedTag = null; this.cachedAudioProperties = null; return this.fileHandle.save(); } /** @inheritdoc */ getFileBuffer() { const buffer = this.fileHandle.getBuffer(); if (!buffer) { return new Uint8Array(0); } return buffer; } /** @inheritdoc */ async saveToFile(path) { const targetPath = path ?? this.sourcePath; if (!targetPath) { throw new Error( "No file path available. Either provide a path or open the file from a path." ); } if (this.isPartiallyLoaded && this.originalSource) { const fullData = await readFileData(this.originalSource); const fullFileHandle = this.module.createFileHandle(); const success = fullFileHandle.loadFromBuffer(fullData); if (!success) { throw new InvalidFormatError( "Failed to load full audio file for saving", fullData.byteLength ); } const partialTag = this.fileHandle.getTag(); const fullTag = fullFileHandle.getTag(); if (partialTag && fullTag) { fullTag.setTitle(partialTag.title()); fullTag.setArtist(partialTag.artist()); fullTag.setAlbum(partialTag.album()); fullTag.setComment(partialTag.comment()); fullTag.setGenre(partialTag.genre()); fullTag.setYear(partialTag.year()); fullTag.setTrack(partialTag.track()); } const properties = this.fileHandle.getProperties(); fullFileHandle.setProperties(properties); const pictures = this.fileHandle.getPictures(); fullFileHandle.setPictures(pictures); if (!fullFileHandle.save()) { fullFileHandle.destroy(); throw new Error("Failed to save changes to full file"); } const buffer = fullFileHandle.getBuffer(); fullFileHandle.destroy(); await writeFileData(targetPath, buffer); this.isPartiallyLoaded = false; this.originalSource = void 0; } else { if (!this.save()) { throw new Error("Failed to save changes to in-memory buffer"); } const buffer = this.getFileBuffer(); await writeFileData(targetPath, buffer); } } /** @inheritdoc */ isValid() { return this.fileHandle.isValid(); } /** @inheritdoc */ getPictures() { const picturesArray = this.fileHandle.getPictures(); const pictures = []; for (let i = 0; i < picturesArray.length; i++) { const pic = picturesArray[i]; pictures.push({ mimeType: pic.mimeType, data: pic.data, type: pic.type, description: pic.description }); } return pictures; } /** @inheritdoc */ setPictures(pictures) { const picturesArray = pictures.map((pic) => ({ mimeType: pic.mimeType, data: pic.data, type: pic.type, description: pic.description ?? "" })); this.fileHandle.setPictures(picturesArray); } /** @inheritdoc */ addPicture(picture) { const pic = { mimeType: picture.mimeType, data: picture.data, type: picture.type, description: picture.description ?? "" }; this.fileHandle.addPicture(pic); } /** @inheritdoc */ removePictures() { this.fileHandle.removePictures(); } /** @inheritdoc */ getRatings() { const ratingsArray = this.fileHandle.getRatings(); return ratingsArray.map( (r) => ({ rating: r.rating, email: r.email || void 0, counter: r.counter || void 0 }) ); } /** @inheritdoc */ setRatings(ratings) { const ratingsArray = ratings.map((r) => ({ rating: r.rating, email: r.email ?? "", counter: r.counter ?? 0 })); this.fileHandle.setRatings(ratingsArray); } /** @inheritdoc */ getRating() { const ratings = this.getRatings(); return ratings.length > 0 ? ratings[0].rating : void 0; } /** @inheritdoc */ setRating(rating, email) { this.setRatings([{ rating, email, counter: 0 }]); } /** @inheritdoc */ dispose() { if (this.fileHandle) { if (typeof this.fileHandle.destroy === "function") { this.fileHandle.destroy(); } this.fileHandle = null; this.cachedTag = null; this.cachedAudioProperties = null; } } // Extended metadata implementations /** @inheritdoc */ getMusicBrainzTrackId() { const value = this.getProperty("MUSICBRAINZ_TRACKID"); return value ?? void 0; } /** @inheritdoc */ setMusicBrainzTrackId(id) { this.setProperty("MUSICBRAINZ_TRACKID", id); } /** @inheritdoc */ getMusicBrainzReleaseId() { const value = this.getProperty("MUSICBRAINZ_ALBUMID"); return value ?? void 0; } /** @inheritdoc */ setMusicBrainzReleaseId(id) { this.setProperty("MUSICBRAINZ_ALBUMID", id); } /** @inheritdoc */ getMusicBrainzArtistId() { const value = this.getProperty("MUSICBRAINZ_ARTISTID"); return value ?? void 0; } /** @inheritdoc */ setMusicBrainzArtistId(id) { this.setProperty("MUSICBRAINZ_ARTISTID", id); } /** @inheritdoc */ getAcoustIdFingerprint() { const value = this.getProperty("ACOUSTID_FINGERPRINT"); return value ?? void 0; } /** @inheritdoc */ setAcoustIdFingerprint(fingerprint) { this.setProperty("ACOUSTID_FINGERPRINT", fingerprint); } /** @inheritdoc */ getAcoustIdId() { const value = this.getProperty("ACOUSTID_ID"); return value ?? void 0; } /** @inheritdoc */ setAcoustIdId(id) { this.setProperty("ACOUSTID_ID", id); } /** @inheritdoc */ getReplayGainTrackGain() { const value = this.getProperty("REPLAYGAIN_TRACK_GAIN"); return value ?? void 0; } /** @inheritdoc */ setReplayGainTrackGain(gain) { this.setProperty("REPLAYGAIN_TRACK_GAIN", gain); } /** @inheritdoc */ getReplayGainTrackPeak() { const value = this.getProperty("REPLAYGAIN_TRACK_PEAK"); return value ?? void 0; } /** @inheritdoc */ setReplayGainTrackPeak(peak) { this.setProperty("REPLAYGAIN_TRACK_PEAK", peak); } /** @inheritdoc */ getReplayGainAlbumGain() { const value = this.getProperty("REPLAYGAIN_ALBUM_GAIN"); return value ?? void 0; } /** @inheritdoc */ setReplayGainAlbumGain(gain) { this.setProperty("REPLAYGAIN_ALBUM_GAIN", gain); } /** @inheritdoc */ getReplayGainAlbumPeak() { const value = this.getProperty("REPLAYGAIN_ALBUM_PEAK"); return value ?? void 0; } /** @inheritdoc */ setReplayGainAlbumPeak(peak) { this.setProperty("REPLAYGAIN_ALBUM_PEAK", peak); } /** @inheritdoc */ getAppleSoundCheck() { if (this.isMP4()) { return this.getMP4Item("iTunNORM"); } const value = this.getProperty("ITUNESOUNDCHECK"); return value ?? void 0; } /** @inheritdoc */ setAppleSoundCheck(data) { if (this.isMP4()) { this.setMP4Item("iTunNORM", data); } else { this.setProperty("ITUNESOUNDCHECK", data); } } } class TagLib { /** * Create a new TagLib instance with a pre-loaded WASM module. * @param module - The loaded WebAssembly module * @internal Use {@link TagLib.initialize} instead for automatic module loading */ constructor(module) { __publicField(this, "module"); __publicField(this, "workerPool"); this.module = module; } /** * Initialize TagLib with optional configuration. * This is the recommended way to create a TagLib instance. * * @param options - Optional configuration for loading the WASM module * @returns Promise resolving to initialized TagLib instance * * @example * ```typescript * // Basic usage * const taglib = await TagLib.initialize(); * * // With pre-loaded WASM binary (for offline usage) * const wasmBinary = await fetch("taglib.wasm").then(r => r.arrayBuffer()); * const taglib = await TagLib.initialize({ wasmBinary }); * * // With custom WASM URL * const taglib = await TagLib.initialize({ wasmUrl: "/assets/taglib.wasm" }); * * // With worker pool enabled * const taglib = await TagLib.initialize({ useWorkerPool: true }); * ``` */ static async initialize(options) { const { loadTagLibModule } = await import("../index.js"); const module = await loadTagLibModule(options); const taglib = new TagLib(module); if (options?.useWorkerPool) { taglib.workerPool = getGlobalWorkerPool(options.workerPoolOptions); } return taglib; } /** * Enable or disable worker pool for this TagLib instance */ setWorkerPool(pool) { this.workerPool = pool ?? void 0; } /** * Get the current worker pool instance */ getWorkerPool() { return this.workerPool; } /** * Open an audio file from various sources. * Automatically detects the file format based on content. * * @param input - File path (string), ArrayBuffer, Uint8Array, or File object * @returns Promise resolving to AudioFile instance * @throws {Error} If the file format is invalid or unsupported * @throws {Error} If the module is not properly initialized * * @example * ```typescript * // From file path * const file = await taglib.open("song.mp3"); * * // From ArrayBuffer * const file = await taglib.open(arrayBuffer); * * // From Uint8Array * const file = await taglib.open(uint8Array); * * // From File object (browser) * const file = await taglib.open(fileObject); * * // Remember to dispose when done * file.dispose(); * ``` */ async open(input, options) { if (!this.module.createFileHandle) { throw new TagLibInitializationError( "TagLib module not properly initialized: createFileHandle not found. Make sure the module is fully loaded before calling open." ); } const sourcePath = typeof input === "string" ? input : void 0; const opts = { partial: false, maxHeaderSize: 1024 * 1024, // 1MB maxFooterSize: 128 * 1024, // 128KB ...options }; let audioData; let isPartiallyLoaded = false; if (opts.partial && typeof File !== "undefined" && input instanceof File) { const headerSize = Math.min(opts.maxHeaderSize, input.size); const footerSize = Math.min(opts.maxFooterSize, input.size); if (input.size <= headerSize + footerSize) { audioData = await readFileData(input); } else { const header = await input.slice(0, headerSize).arrayBuffer(); const footerStart = Math.max(0, input.size - footerSize); const footer = await input.slice(footerStart).arrayBuffer(); const combined = new Uint8Array(header.byteLength + footer.byteLength); combined.set(new Uint8Array(header), 0); combined.set(new Uint8Array(footer), header.byteLength); audioData = combined; isPartiallyLoaded = true; } } else if (opts.partial && typeof input === "string") { const fileSize = await getFileSize(input); if (fileSize > opts.maxHeaderSize + opts.maxFooterSize) { audioData = await readPartialFileData( input, opts.maxHeaderSize, opts.maxFooterSize ); isPartiallyLoaded = true; } else { audioData = await readFileData(input); } } else { audioData = await readFileData(input); } const buffer = audioData.buffer.slice( audioData.byteOffset, audioData.byteOffset + audioData.byteLength ); const uint8Array = new Uint8Array(buffer); const fileHandle = this.module.createFileHandle(); const success = fileHandle.loadFromBuffer(uint8Array); if (!success) { throw new InvalidFormatError( "Failed to load audio file. File may be corrupted or in an unsupported format", buffer.byteLength ); } return new AudioFileImpl( this.module, fileHandle, sourcePath, input, // Store original source for lazy loading isPartiallyLoaded, opts ); } /** * Update tags in a file and save changes to disk in one operation. * This is a convenience method that opens, modifies, saves, and closes the file. * * @param path - File path to update * @param tags - Object containing tags to update * @throws {Error} If file operations fail * * @example * ```typescript * await taglib.updateFile("song.mp3", { * title: "New Title", * artist: "New Artist" * }); * ``` */ async updateFile(path, tags) { const file = await this.open(path); try { const tag = file.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.year !== void 0) tag.setYear(tags.year); if (tags.track !== void 0) tag.setTrack(tags.track); if (tags.genre !== void 0) tag.setGenre(tags.genre); if (tags.comment !== void 0) tag.setComment(tags.comment); await file.saveToFile(); } finally { file.dispose(); } } /** * Copy a file with new tags. * Opens the source file, applies new tags, and saves to a new location. * * @param sourcePath - Source file path * @param destPath - Destination file path * @param tags - Object containing tags to apply * @throws {Error} If file operations fail * * @example * ```typescript * await taglib.copyWithTags("original.mp3", "copy.mp3", { * title: "Copy of Song", * comment: "This is a copy" * }); * ``` */ async copyWithTags(sourcePath, destPath, tags) { const file = await this.open(sourcePath); try { const tag = file.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.year !== void 0) tag.setYear(tags.year); if (tags.track !== void 0) tag.setTrack(tags.track); if (tags.genre !== void 0) tag.setGenre(tags.genre); if (tags.comment !== void 0) tag.setComment(tags.comment); await file.saveToFile(destPath); } finally { file.dispose(); } } /** * Execute batch operations on a file using the worker pool. * This method provides efficient batch processing using Web Workers. * * @param file - File path or Uint8Array * @param operations - Array of operations to execute * @returns Result of the batch operations * * @example * ```typescript * const result = await taglib.batchOperations("song.mp3", [ * { method: "setTitle", args: ["New Title"] }, * { method: "setArtist", args: ["New Artist"] }, * { method: "save" } * ]); * ``` */ async batchOperations(file, operations) { if (!this.workerPool) { throw new Error( "Worker pool not initialized. Enable it with TagLib.initialize({ useWorkerPool: true })" ); } return this.workerPool.batchOperations(file, operations); } /** * Process multiple files in parallel using the worker pool. * * @param files - Array of file paths * @param operation - Operation to perform on each file * @returns Array of results * * @example * ```typescript * const tags = await taglib.processFiles( * ["song1.mp3", "song2.mp3", "song3.mp3"], * "readTags" * ); * ``` */ async processFiles(files, operation) { if (!this.workerPool) { throw new Error( "Worker pool not initialized. Enable it with TagLib.initialize({ useWorkerPool: true })" ); } return Promise.all( files.map((file) => { if (operation === "readTags") { return this.workerPool.readTags(file); } else { return this.workerPool.readProperties(file); } }) ); } /** * Get the TagLib version. * @returns Version string (e.g., "2.1.0") */ version() { return "2.1.0"; } } async function createTagLib(module) { return new TagLib(module); } import { EnvironmentError, FileOperationError, InvalidFormatError as InvalidFormatError2, isEnvironmentError, isFileOperationError, isInvalidFormatError, isMemoryError, isMetadataError, isTagLibError, isUnsupportedFormatError, MemoryError, MetadataError as MetadataError2, SUPPORTED_FORMATS, TagLibError, TagLibInitializationError as TagLibInitializationError2, UnsupportedFormatError as UnsupportedFormatError2 } from "./errors.js"; export { AudioFileImpl, EnvironmentError, FileOperationError, InvalidFormatError2 as InvalidFormatError, MemoryError, MetadataError2 as MetadataError, SUPPORTED_FORMATS, TagLib, TagLibError, TagLibInitializationError2 as TagLibInitializationError, UnsupportedFormatError2 as UnsupportedFormatError, createTagLib, isEnvironmentError, isFileOperationError, isInvalidFormatError, isMemoryError, isMetadataError, isTagLibError, isUnsupportedFormatError };