taglib-wasm
Version:
TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers
337 lines (336 loc) • 10.6 kB
JavaScript
import {
cStringToJS,
jsToCString,
loadTagLibModuleForWorkers
} from "./wasm-workers.js";
import { EnvironmentError, InvalidFormatError, MemoryError } from "./errors.js";
class AudioFileWorkers {
constructor(module, fileId) {
this.module = module;
this.fileId = fileId;
this.tagPtr = module._taglib_file_tag?.(fileId) || 0;
this.propsPtr = module._taglib_file_audioproperties?.(fileId) || 0;
}
/**
* Check if the file is valid and was loaded successfully.
* @returns true if the file is valid and can be processed
*/
isValid() {
return this.module._taglib_file_is_valid?.(this.fileId) !== 0;
}
/**
* Get the file format.
* @returns Audio format (e.g., "MP3", "FLAC", "OGG")
*/
format() {
const formatPtr = this.module._taglib_file_format?.(this.fileId) || 0;
if (formatPtr === 0) return "MP3";
const formatStr = cStringToJS(this.module, formatPtr);
return formatStr;
}
/**
* Get basic tag information.
* @returns Object containing title, artist, album, etc.
*/
tag() {
if (this.tagPtr === 0) return {};
const title = this.module._taglib_tag_title?.(this.tagPtr) || 0;
const artist = this.module._taglib_tag_artist?.(this.tagPtr) || 0;
const album = this.module._taglib_tag_album?.(this.tagPtr) || 0;
const comment = this.module._taglib_tag_comment?.(this.tagPtr) || 0;
const genre = this.module._taglib_tag_genre?.(this.tagPtr) || 0;
const year = this.module._taglib_tag_year?.(this.tagPtr) || 0;
const track = this.module._taglib_tag_track?.(this.tagPtr) || 0;
return {
title: title ? cStringToJS(this.module, title) : void 0,
artist: artist ? cStringToJS(this.module, artist) : void 0,
album: album ? cStringToJS(this.module, album) : void 0,
comment: comment ? cStringToJS(this.module, comment) : void 0,
genre: genre ? cStringToJS(this.module, genre) : void 0,
year: year || void 0,
track: track || void 0
};
}
/**
* Get audio properties (duration, bitrate, etc.).
* @returns Audio properties or null if unavailable
*/
audioProperties() {
if (this.propsPtr === 0) return null;
const length = this.module._taglib_audioproperties_length?.(this.propsPtr) || 0;
const bitrate = this.module._taglib_audioproperties_bitrate?.(this.propsPtr) || 0;
const sampleRate = this.module._taglib_audioproperties_samplerate?.(
this.propsPtr
) || 0;
const channels = this.module._taglib_audioproperties_channels?.(
this.propsPtr
) || 0;
return {
length,
bitrate,
sampleRate,
channels,
bitsPerSample: 0,
// Not available in C API compatibility mode
codec: "Unknown",
// Not available in C API compatibility mode
containerFormat: "UNKNOWN",
// Not available in C API compatibility mode
isLossless: false
// Not available in C API compatibility mode
};
}
/**
* Set the title tag.
* @param title - New title value
*/
setTitle(title) {
if (this.tagPtr === 0) return;
const titlePtr = jsToCString(this.module, title);
this.module._taglib_tag_set_title?.(this.tagPtr, titlePtr);
this.module._free(titlePtr);
}
/**
* Set the artist tag.
* @param artist - New artist value
*/
setArtist(artist) {
if (this.tagPtr === 0) return;
const artistPtr = jsToCString(this.module, artist);
this.module._taglib_tag_set_artist?.(this.tagPtr, artistPtr);
this.module._free(artistPtr);
}
/**
* Set the album tag.
* @param album - New album value
*/
setAlbum(album) {
if (this.tagPtr === 0) return;
const albumPtr = jsToCString(this.module, album);
this.module._taglib_tag_set_album?.(this.tagPtr, albumPtr);
this.module._free(albumPtr);
}
/**
* Set the comment tag.
* @param comment - New comment value
*/
setComment(comment) {
if (this.tagPtr === 0) return;
const commentPtr = jsToCString(this.module, comment);
this.module._taglib_tag_set_comment?.(this.tagPtr, commentPtr);
this.module._free(commentPtr);
}
/**
* Set the genre tag.
* @param genre - New genre value
*/
setGenre(genre) {
if (this.tagPtr === 0) return;
const genrePtr = jsToCString(this.module, genre);
this.module._taglib_tag_set_genre?.(this.tagPtr, genrePtr);
this.module._free(genrePtr);
}
/**
* Set the year tag.
* @param year - Release year
*/
setYear(year) {
if (this.tagPtr === 0) return;
this.module._taglib_tag_set_year?.(this.tagPtr, year);
}
/**
* Set the track number tag.
* @param track - Track number
*/
setTrack(track) {
if (this.tagPtr === 0) return;
this.module._taglib_tag_set_track?.(this.tagPtr, track);
}
/**
* Save changes to the file.
* Note: In Workers context, this saves to the in-memory buffer only.
* @returns true if save was successful
*/
save() {
if (this.fileId !== 0) {
return this.module._taglib_file_save?.(this.fileId) !== 0;
}
return false;
}
/**
* Get the current file buffer after modifications.
* Note: This is not implemented in the Workers API.
* @returns Empty Uint8Array (not implemented)
* @throws {Error} Consider using the Full API for this functionality
*/
getFileBuffer() {
console.warn(
"getFileBuffer() is not implemented in Workers API. Use Full API for this functionality."
);
return new Uint8Array(0);
}
/**
* Get extended metadata with format-agnostic field names.
* Note: Currently returns only basic fields in Workers API.
* @returns Extended tag object with basic fields populated
*/
extendedTag() {
const basicTag = this.tag();
return {
...basicTag,
// Advanced fields placeholder - would be populated by PropertyMap reading
acoustidFingerprint: void 0,
acoustidId: void 0,
musicbrainzTrackId: void 0,
musicbrainzReleaseId: void 0,
musicbrainzArtistId: void 0,
musicbrainzReleaseGroupId: void 0,
albumArtist: void 0,
composer: void 0,
discNumber: void 0,
totalTracks: void 0,
totalDiscs: void 0,
bpm: void 0,
compilation: void 0,
titleSort: void 0,
artistSort: void 0,
albumSort: void 0,
replayGainTrackGain: void 0,
replayGainTrackPeak: void 0,
replayGainAlbumGain: void 0,
replayGainAlbumPeak: void 0,
appleSoundCheck: void 0
};
}
/**
* Set extended metadata using format-agnostic field names.
* Note: Currently only supports basic fields in Workers API.
* @param tag - Partial extended tag object with fields to update
*/
setExtendedTag(tag) {
if (tag.title !== void 0) this.setTitle(tag.title);
if (tag.artist !== void 0) this.setArtist(tag.artist);
if (tag.album !== void 0) this.setAlbum(tag.album);
if (tag.comment !== void 0) this.setComment(tag.comment);
if (tag.genre !== void 0) this.setGenre(tag.genre);
if (tag.year !== void 0) this.setYear(tag.year);
if (tag.track !== void 0) this.setTrack(tag.track);
}
/**
* Clean up resources.
* Always call this when done to prevent memory leaks.
*/
dispose() {
if (this.fileId !== 0) {
this.module._taglib_file_delete?.(this.fileId);
this.fileId = 0;
}
}
}
class TagLibWorkers {
constructor(module) {
this.module = module;
}
/**
* Initialize TagLib for Workers with Wasm binary
*
* @param wasmBinary - The WebAssembly binary as Uint8Array
* @param config - Optional configuration for the Wasm module
*
* @example
* ```typescript
* // In a Cloudflare Worker
* import wasmBinary from "../build/taglib.wasm.js";
*
* const taglib = await TagLibWorkers.initialize(wasmBinary);
* const file = taglib.open(audioBuffer);
* const metadata = file.tag();
* ```
*/
static async initialize(wasmBinary, config) {
const module = await loadTagLibModuleForWorkers(wasmBinary, config);
return new TagLibWorkers(module);
}
/**
* Open an audio file from a buffer.
*
* @param buffer - Audio file data as Uint8Array
* @returns AudioFileWorkers instance
* @throws {Error} If Wasm module is not initialized
* @throws {Error} If file format is invalid or unsupported
* @throws {Error} If Workers API C-style functions are not available
*
* @example
* ```typescript
* const audioData = new Uint8Array(await request.arrayBuffer());
* const file = taglib.open(audioData);
* ```
*/
open(buffer) {
if (!this.module.HEAPU8) {
throw new MemoryError(
"Wasm module not properly initialized: missing HEAPU8. The module may not have loaded correctly in the Workers environment."
);
}
let dataPtr;
if (this.module.allocate && this.module.ALLOC_NORMAL !== void 0) {
dataPtr = this.module.allocate(buffer, this.module.ALLOC_NORMAL);
} else {
dataPtr = this.module._malloc(buffer.length);
this.module.HEAPU8.set(buffer, dataPtr);
}
if (!this.module._taglib_file_new_from_buffer) {
throw new EnvironmentError(
"Workers",
"requires C-style functions which are not available. Use the Full API instead for this environment",
"C-style function exports"
);
}
const fileId = this.module._taglib_file_new_from_buffer(
dataPtr,
buffer.length
);
if (fileId === 0) {
this.module._free(dataPtr);
throw new InvalidFormatError(
"Failed to open audio file. File format may be invalid or not supported",
buffer.length
);
}
this.module._free(dataPtr);
return new AudioFileWorkers(this.module, fileId);
}
/**
* Open an audio file from a buffer (backward compatibility).
* Consider using `open()` for consistency with the Full API.
* @param buffer Audio file data as Uint8Array
* @returns Audio file instance
*/
openFile(buffer) {
return this.open(buffer);
}
/**
* Get the underlying Wasm module for advanced usage.
* @returns The initialized TagLib Wasm module
*/
getModule() {
return this.module;
}
}
async function processAudioMetadata(wasmBinary, audioData, config) {
const taglib = await TagLibWorkers.initialize(wasmBinary, config);
const file = taglib.open(audioData);
try {
const tag = file.tag();
const properties = file.audioProperties();
const format = file.format();
return { tag, properties, format };
} finally {
file.dispose();
}
}
export {
AudioFileWorkers,
TagLibWorkers,
processAudioMetadata
};