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

1,675 lines (1,644 loc) 100 kB
var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/types/audio-formats.ts function isNamedAudioInput(input) { return typeof input === "object" && input !== null && "name" in input && "data" in input && !(input instanceof File) && !(input instanceof Uint8Array) && !(input instanceof ArrayBuffer); } var init_audio_formats = __esm({ "src/types/audio-formats.ts"() { "use strict"; } }); // src/types/tags.ts var init_tags = __esm({ "src/types/tags.ts"() { "use strict"; } }); // src/types/metadata-mappings.ts var init_metadata_mappings = __esm({ "src/types/metadata-mappings.ts"() { "use strict"; } }); // src/types/pictures.ts var PICTURE_TYPE_VALUES, PICTURE_TYPE_NAMES; var init_pictures = __esm({ "src/types/pictures.ts"() { "use strict"; PICTURE_TYPE_VALUES = { Other: 0, FileIcon: 1, OtherFileIcon: 2, FrontCover: 3, BackCover: 4, LeafletPage: 5, Media: 6, LeadArtist: 7, Artist: 8, Conductor: 9, Band: 10, Composer: 11, Lyricist: 12, RecordingLocation: 13, DuringRecording: 14, DuringPerformance: 15, MovieScreenCapture: 16, ColouredFish: 17, Illustration: 18, BandLogo: 19, PublisherLogo: 20 }; PICTURE_TYPE_NAMES = { 0: "Other", 1: "FileIcon", 2: "OtherFileIcon", 3: "FrontCover", 4: "BackCover", 5: "LeafletPage", 6: "Media", 7: "LeadArtist", 8: "Artist", 9: "Conductor", 10: "Band", 11: "Composer", 12: "Lyricist", 13: "RecordingLocation", 14: "DuringRecording", 15: "DuringPerformance", 16: "MovieScreenCapture", 17: "ColouredFish", 18: "Illustration", 19: "BandLogo", 20: "PublisherLogo" }; } }); // src/types/chapters.ts var init_chapters = __esm({ "src/types/chapters.ts"() { "use strict"; } }); // src/types/bwf.ts var init_bwf = __esm({ "src/types/bwf.ts"() { "use strict"; } }); // src/types/config.ts var init_config = __esm({ "src/types/config.ts"() { "use strict"; } }); // src/types/format-property-keys.ts var init_format_property_keys = __esm({ "src/types/format-property-keys.ts"() { "use strict"; } }); // src/types/index.ts var init_types = __esm({ "src/types/index.ts"() { "use strict"; init_audio_formats(); init_tags(); init_metadata_mappings(); init_pictures(); init_chapters(); init_bwf(); init_config(); init_format_property_keys(); } }); // src/types.ts var init_types2 = __esm({ "src/types.ts"() { "use strict"; init_types(); } }); // src/bwf/bext.ts function readFixedString(bytes, offset, len) { let end = offset; const max = Math.min(offset + len, bytes.length); while (end < max && bytes[end] !== 0) end++; let s = ""; for (let i = offset; i < end; i++) s += String.fromCharCode(bytes[i]); return s; } function writeFixedString(bytes, offset, len, value) { for (let i = 0; i < len; i++) { bytes[offset + i] = i < value.length ? value.charCodeAt(i) & 255 : 0; } } function clampInt16(n) { return Math.max(-32768, Math.min(32767, Math.round(n))); } function loudnessRaw(v) { return v === void 0 ? LOUDNESS_UNSET : clampInt16(v * 100); } function loudnessFromRaw(raw) { return raw === LOUDNESS_UNSET ? void 0 : raw / 100; } function bytesToHex(bytes) { let s = ""; for (const b of bytes) s += b.toString(16).padStart(2, "0"); return s; } function hexToBytes(hex, outLen) { const clean = hex.replace(/[^0-9a-fA-F]/g, ""); const out = new Uint8Array(outLen); for (let i = 0; i + 1 < clean.length + 1 && i / 2 < outLen; i += 2) { const pair = clean.slice(i, i + 2); if (pair.length === 2) out[i / 2] = parseInt(pair, 16); } return out; } function decodeBext(bytes) { if (bytes.length < VERSION_OFFSET + 2) return void 0; const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const timeRefLow = dv.getUint32(338, true); const timeRefHigh = dv.getUint32(342, true); const version = dv.getUint16(VERSION_OFFSET, true); const result = { description: readFixedString(bytes, 0, 256), originator: readFixedString(bytes, 256, 32), originatorReference: readFixedString(bytes, 288, 32), originationDate: readFixedString(bytes, 320, 10), originationTime: readFixedString(bytes, 330, 8), timeReferenceSamples: BigInt(timeRefHigh) << 32n | BigInt(timeRefLow), version, codingHistory: bytes.length > FIXED_PREFIX_LEN ? readFixedString( bytes, FIXED_PREFIX_LEN, bytes.length - FIXED_PREFIX_LEN ) : "" }; if (version >= 1 && bytes.length >= UMID_OFFSET + UMID_LEN) { result.umid = bytesToHex( bytes.subarray(UMID_OFFSET, UMID_OFFSET + UMID_LEN) ); } if (version >= 2 && bytes.length >= LOUDNESS_OFFSET + 10) { const lv = loudnessFromRaw(dv.getInt16(LOUDNESS_OFFSET, true)); const lr = loudnessFromRaw(dv.getInt16(LOUDNESS_OFFSET + 2, true)); const mtp = loudnessFromRaw(dv.getInt16(LOUDNESS_OFFSET + 4, true)); const mml = loudnessFromRaw(dv.getInt16(LOUDNESS_OFFSET + 6, true)); const msl = loudnessFromRaw(dv.getInt16(LOUDNESS_OFFSET + 8, true)); if (lv !== void 0) result.loudnessValueDb = lv; if (lr !== void 0) result.loudnessRangeDb = lr; if (mtp !== void 0) result.maxTruePeakLevelDbtp = mtp; if (mml !== void 0) result.maxMomentaryLoudnessDb = mml; if (msl !== void 0) result.maxShortTermLoudnessDb = msl; } return result; } function encodeBext(b) { const codingHistory = b.codingHistory ?? ""; const bytes = new Uint8Array(FIXED_PREFIX_LEN + codingHistory.length); const dv = new DataView(bytes.buffer); const hasLoudness = b.loudnessValueDb !== void 0 || b.loudnessRangeDb !== void 0 || b.maxTruePeakLevelDbtp !== void 0 || b.maxMomentaryLoudnessDb !== void 0 || b.maxShortTermLoudnessDb !== void 0; const version = Number.isInteger(b.version) ? b.version : hasLoudness ? 2 : b.umid ? 1 : 0; writeFixedString(bytes, 0, 256, b.description ?? ""); writeFixedString(bytes, 256, 32, b.originator ?? ""); writeFixedString(bytes, 288, 32, b.originatorReference ?? ""); writeFixedString(bytes, 320, 10, b.originationDate ?? ""); writeFixedString(bytes, 330, 8, b.originationTime ?? ""); const tr = b.timeReferenceSamples ?? 0n; dv.setUint32(338, Number(tr & 0xffffffffn), true); dv.setUint32(342, Number(tr >> 32n & 0xffffffffn), true); dv.setUint16(VERSION_OFFSET, version, true); if (version >= 1 && b.umid) { bytes.set(hexToBytes(b.umid, UMID_LEN), UMID_OFFSET); } if (version >= 2) { dv.setInt16(LOUDNESS_OFFSET, loudnessRaw(b.loudnessValueDb), true); dv.setInt16(LOUDNESS_OFFSET + 2, loudnessRaw(b.loudnessRangeDb), true); dv.setInt16(LOUDNESS_OFFSET + 4, loudnessRaw(b.maxTruePeakLevelDbtp), true); dv.setInt16( LOUDNESS_OFFSET + 6, loudnessRaw(b.maxMomentaryLoudnessDb), true ); dv.setInt16( LOUDNESS_OFFSET + 8, loudnessRaw(b.maxShortTermLoudnessDb), true ); } writeFixedString( bytes, FIXED_PREFIX_LEN, codingHistory.length, codingHistory ); return bytes; } var FIXED_PREFIX_LEN, VERSION_OFFSET, UMID_OFFSET, UMID_LEN, LOUDNESS_OFFSET, LOUDNESS_UNSET; var init_bext = __esm({ "src/bwf/bext.ts"() { "use strict"; FIXED_PREFIX_LEN = 602; VERSION_OFFSET = 346; UMID_OFFSET = 348; UMID_LEN = 64; LOUDNESS_OFFSET = 412; LOUDNESS_UNSET = 32767; } }); // src/errors/base.ts var SUPPORTED_FORMATS, TagLibError; var init_base = __esm({ "src/errors/base.ts"() { "use strict"; SUPPORTED_FORMATS = [ "MP3", "MP4", "M4A", "FLAC", "OGG", "WAV", "MKA" ]; TagLibError = class _TagLibError extends Error { /** * Creates a new TagLibError * @param code - Error code for programmatic handling * @param message - Human-readable error message * @param details - Additional context about the error */ constructor(code, message, details) { super(message); __publicField(this, "code", code); __publicField(this, "details", details); this.name = "TagLibError"; Object.setPrototypeOf(this, _TagLibError.prototype); } }; } }); // src/errors/classes.ts function errorMessage(error) { return error instanceof Error ? error.message : String(error); } function createErrorMessage(prefix, details) { return `${prefix}: ${details}`; } function formatFileSize(bytes) { if (bytes < 1024) return `${bytes} bytes`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } var TagLibInitializationError, InvalidFormatError, UnsupportedFormatError, FileOperationError, MetadataError, MemoryError, EnvironmentError; var init_classes = __esm({ "src/errors/classes.ts"() { "use strict"; init_base(); TagLibInitializationError = class _TagLibInitializationError extends TagLibError { /** * Creates a new TagLibInitializationError * @param message - Description of the initialization failure * @param details - Additional context about the error */ constructor(message, details) { super( "INITIALIZATION", createErrorMessage("Failed to initialize TagLib Wasm module", message), details ); this.name = "TagLibInitializationError"; Object.setPrototypeOf(this, _TagLibInitializationError.prototype); } }; InvalidFormatError = class _InvalidFormatError extends TagLibError { /** * Creates a new InvalidFormatError * @param message - Description of the format error * @param bufferSize - Size of the audio buffer in bytes * @param details - Additional context about the error */ constructor(message, bufferSize, details) { const errorDetails = [`Invalid audio file format: ${message}`]; if (bufferSize !== void 0) { errorDetails.push(`Buffer size: ${formatFileSize(bufferSize)}`); if (bufferSize < 1024) { errorDetails.push( "Audio files must be at least 1KB to contain valid headers." ); } } super( "INVALID_FORMAT", errorDetails.join(". "), { ...details, bufferSize } ); __publicField(this, "bufferSize", bufferSize); this.name = "InvalidFormatError"; Object.setPrototypeOf(this, _InvalidFormatError.prototype); } }; UnsupportedFormatError = class _UnsupportedFormatError extends TagLibError { /** * Creates a new UnsupportedFormatError * @param format - The unsupported format that was encountered * @param supportedFormats - List of formats that are supported * @param details - Additional context about the error */ constructor(format, supportedFormats = SUPPORTED_FORMATS, details) { super( "UNSUPPORTED_FORMAT", `Unsupported audio format: ${format}. Supported formats: ${supportedFormats.join(", ")}`, { ...details, format, supportedFormats } ); __publicField(this, "format", format); __publicField(this, "supportedFormats", supportedFormats); this.name = "UnsupportedFormatError"; Object.setPrototypeOf(this, _UnsupportedFormatError.prototype); } }; FileOperationError = class _FileOperationError extends TagLibError { /** * Creates a new FileOperationError * @param operation - The file operation that failed * @param message - Description of the failure * @param path - File path involved in the operation * @param details - Additional context about the error */ constructor(operation, message, path, details) { const errorDetails = [`Failed to ${operation} file`]; if (path) { errorDetails.push(`Path: ${path}`); } errorDetails.push(message); super( "FILE_OPERATION", errorDetails.join(". "), { ...details, operation, path } ); __publicField(this, "operation", operation); __publicField(this, "path", path); this.name = "FileOperationError"; Object.setPrototypeOf(this, _FileOperationError.prototype); } }; MetadataError = class _MetadataError extends TagLibError { /** * Creates a new MetadataError * @param operation - The metadata operation that failed * @param message - Description of the failure * @param field - The metadata field involved * @param details - Additional context about the error */ constructor(operation, message, field, details) { const errorDetails = [`Failed to ${operation} metadata`]; if (field) { errorDetails.push(`Field: ${field}`); } errorDetails.push(message); super( "METADATA", errorDetails.join(". "), { ...details, operation, field } ); __publicField(this, "operation", operation); __publicField(this, "field", field); this.name = "MetadataError"; Object.setPrototypeOf(this, _MetadataError.prototype); } }; MemoryError = class _MemoryError extends TagLibError { /** * Creates a new MemoryError * @param message - Description of the memory failure * @param details - Additional context about the error */ constructor(message, details) { super( "MEMORY", createErrorMessage("Memory allocation failed", message), details ); this.name = "MemoryError"; Object.setPrototypeOf(this, _MemoryError.prototype); } }; EnvironmentError = class _EnvironmentError extends TagLibError { /** * Creates a new EnvironmentError * @param environment - The runtime environment name * @param reason - Why the environment is incompatible * @param requiredFeature - The feature that is missing */ constructor(environment, reason, requiredFeature) { const message = requiredFeature ? `Environment '${environment}' ${reason}. Required feature: ${requiredFeature}.` : `Environment '${environment}' ${reason}.`; super("ENVIRONMENT", message); __publicField(this, "environment", environment); __publicField(this, "reason", reason); __publicField(this, "requiredFeature", requiredFeature); this.name = "EnvironmentError"; Object.setPrototypeOf(this, _EnvironmentError.prototype); } }; } }); // src/errors/guards.ts function isTagLibError(error) { return error instanceof TagLibError; } function isInvalidFormatError(error) { return error instanceof InvalidFormatError; } function isUnsupportedFormatError(error) { return error instanceof UnsupportedFormatError; } function isFileOperationError(error) { return error instanceof FileOperationError; } function isMetadataError(error) { return error instanceof MetadataError; } function isMemoryError(error) { return error instanceof MemoryError; } function isEnvironmentError(error) { return error instanceof EnvironmentError; } var init_guards = __esm({ "src/errors/guards.ts"() { "use strict"; init_base(); init_classes(); } }); // src/errors/index.ts var init_errors = __esm({ "src/errors/index.ts"() { "use strict"; init_base(); init_classes(); init_guards(); } }); // src/errors.ts var init_errors2 = __esm({ "src/errors.ts"() { "use strict"; init_errors(); } }); // src/taglib/audio-file-bwf.ts function requireBwf(format) { if (!BWF_FORMATS.has(format)) { throw new UnsupportedFormatError(format, ["WAV", "FLAC"], { operation: "BWF metadata" }); } } function getBextData(handle) { const raw = handle.getBextData(); return raw && raw.length > 0 ? raw : void 0; } function setBextData(handle, format, data) { requireBwf(format); handle.setBextData(data); } function getBext(handle) { const raw = handle.getBextData(); return raw && raw.length > 0 ? decodeBext(raw) : void 0; } function setBext(handle, format, bext) { requireBwf(format); handle.setBextData(encodeBext(bext)); } function getIxml(handle) { return handle.getIxml(); } function setIxml(handle, format, data) { requireBwf(format); handle.setIxml(data); } var BWF_FORMATS; var init_audio_file_bwf = __esm({ "src/taglib/audio-file-bwf.ts"() { "use strict"; init_bext(); init_errors2(); BWF_FORMATS = /* @__PURE__ */ new Set(["WAV", "FLAC"]); } }); // src/taglib/id3v2-frames.ts function assertMp3(format) { if (format !== "MP3") { throw new UnsupportedFormatError(format, ["MP3"], { operation: "id3v2Frames" }); } } function assertFrameId(id, operation) { if (!FRAME_ID_PATTERN.test(id)) { throw new MetadataError( operation, `Invalid ID3v2 frame ID: "${id}". Frame IDs must match [A-Z0-9]{4}`, id ); } } function assertFrameBodies(id, data) { for (const body of data) { if (body.length === 0) { throw new MetadataError( "write", "Frame data must not be empty: zero-size ID3v2 frames are invalid", id ); } } } function toPublicFrame(frame) { return { id: frame.id, data: frame.data, ...frame.flags ? { flags: frame.flags } : {} }; } var FRAME_ID_PATTERN; var init_id3v2_frames = __esm({ "src/taglib/id3v2-frames.ts"() { "use strict"; init_classes(); FRAME_ID_PATTERN = /^[A-Z0-9]{4}$/; } }); // browser-stub:platform-io-browser-stub function getPlatformIO() { throw new Error("Filesystem operations are not available in the browser."); } var init_platform_io_browser_stub = __esm({ "browser-stub:platform-io-browser-stub"() { } }); // src/utils/write.ts async function writeFileData(path, data) { try { await getPlatformIO().writeFile(path, data); } catch (error) { if (error instanceof EnvironmentError) throw error; throw new FileOperationError( "write", error.message, path ); } } var init_write = __esm({ "src/utils/write.ts"() { "use strict"; init_errors2(); init_platform_io_browser_stub(); } }); // browser-stub:node-fs-browser-stub function getNodeFsSync() { return null; } var init_node_fs_browser_stub = __esm({ "browser-stub:node-fs-browser-stub"() { } }); // src/utils/rating.ts function normalized(value) { return value; } var init_rating = __esm({ "src/utils/rating.ts"() { "use strict"; } }); // src/runtime/detector.ts function isDeno() { return g.Deno !== void 0; } var g, EXNREF_PROBE; var init_detector = __esm({ "src/runtime/detector.ts"() { "use strict"; g = globalThis; EXNREF_PROBE = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, // Wasm header 6, 6, // Global section, 6 bytes 1, // 1 global 105, 0, // exnref, const 208, 105, 11 // ref.null exnref, end ]); } }); // src/constants/additional-properties.ts var ADDITIONAL_PROPERTIES; var init_additional_properties = __esm({ "src/constants/additional-properties.ts"() { "use strict"; ADDITIONAL_PROPERTIES = { albumArtistSort: { key: "ALBUMARTISTSORT", description: "Sort name for album artist (for alphabetization)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TSO2" }, vorbis: "ALBUMARTISTSORT", mp4: "soaa" } }, composerSort: { key: "COMPOSERSORT", description: "Sort name for composer (for alphabetization)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TSOC" }, vorbis: "COMPOSERSORT", mp4: "soco" } }, subtitle: { key: "SUBTITLE", description: "Subtitle or description refinement", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TIT3" }, vorbis: "SUBTITLE", mp4: "----:com.apple.iTunes:SUBTITLE" } }, label: { key: "LABEL", description: "Record label name", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TPUB" }, vorbis: "LABEL", mp4: "----:com.apple.iTunes:LABEL", wav: "IPUB" } }, producer: { key: "PRODUCER", description: "Producer of the recording", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "PRODUCER" }, vorbis: "PRODUCER", mp4: "----:com.apple.iTunes:PRODUCER" } }, radioStationOwner: { key: "RADIOSTATIONOWNER", description: "Owner of the radio station", type: "string", supportedFormats: ["ID3v2"], mappings: { id3v2: { frame: "TRSO" } } }, asin: { key: "ASIN", description: "Amazon Standard Identification Number", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "ASIN" }, vorbis: "ASIN", mp4: "----:com.apple.iTunes:ASIN" } }, musicbrainzReleaseArtistId: { key: "MUSICBRAINZ_ALBUMARTISTID", description: "MusicBrainz Release Artist ID (UUID)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "MusicBrainz Album Artist Id" }, vorbis: "MUSICBRAINZ_ALBUMARTISTID", mp4: "----:com.apple.iTunes:MusicBrainz Album Artist Id" } }, musicbrainzWorkId: { key: "MUSICBRAINZ_WORKID", description: "MusicBrainz Work ID (UUID)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "MusicBrainz Work Id" }, vorbis: "MUSICBRAINZ_WORKID", mp4: "----:com.apple.iTunes:MusicBrainz Work Id" } }, musicbrainzReleaseTrackId: { key: "MUSICBRAINZ_RELEASETRACKID", description: "MusicBrainz Release Track ID (UUID)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "MusicBrainz Release Track Id" }, vorbis: "MUSICBRAINZ_RELEASETRACKID", mp4: "----:com.apple.iTunes:MusicBrainz Release Track Id" } }, podcastId: { key: "PODCASTID", description: "Podcast episode identifier", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TGID" }, vorbis: "PODCASTID", mp4: "egid" } }, podcastUrl: { key: "PODCASTURL", description: "Podcast feed URL", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "WFED" }, vorbis: "PODCASTURL", mp4: "purl" } }, originalArtist: { key: "ORIGINALARTIST", description: "Original artist of a cover or remix", type: "string", supportedFormats: ["ID3v2", "Vorbis"], mappings: { id3v2: { frame: "TOPE" }, vorbis: "ORIGINALARTIST" } }, originalAlbum: { key: "ORIGINALALBUM", description: "Original album of a cover or remix", type: "string", supportedFormats: ["ID3v2", "Vorbis"], mappings: { id3v2: { frame: "TOAL" }, vorbis: "ORIGINALALBUM" } }, originalDate: { key: "ORIGINALDATE", description: "Original release date", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TDOR" }, vorbis: "ORIGINALDATE", mp4: "----:com.apple.iTunes:ORIGINALDATE" } }, script: { key: "SCRIPT", description: "Writing script used for text (e.g., Latn, Jpan)", type: "string", supportedFormats: ["MP4", "Vorbis"], mappings: { vorbis: "SCRIPT", mp4: "----:com.apple.iTunes:SCRIPT" } }, involvedPeople: { key: "INVOLVEDPEOPLE", description: "List of involved people and their roles", type: "string", supportedFormats: ["ID3v2"], mappings: { id3v2: { frame: "TIPL" } } }, encoding: { key: "ENCODING", description: "Encoding software or settings", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TSSE" }, vorbis: "ENCODING", mp4: "\xA9too", wav: "ISFT" } } }; } }); // src/constants/basic-properties.ts var BASIC_PROPERTIES; var init_basic_properties = __esm({ "src/constants/basic-properties.ts"() { "use strict"; BASIC_PROPERTIES = { title: { key: "TITLE", description: "The title of the track", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TIT2" }, vorbis: "TITLE", mp4: "\xA9nam", wav: "INAM" } }, artist: { key: "ARTIST", description: "The primary performer(s) of the track", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TPE1" }, vorbis: "ARTIST", mp4: "\xA9ART", wav: "IART" } }, album: { key: "ALBUM", description: "The album/collection name", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TALB" }, vorbis: "ALBUM", mp4: "\xA9alb", wav: "IPRD" } }, date: { key: "DATE", description: "The date of recording (typically year)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TDRC" }, vorbis: "DATE", mp4: "\xA9day", wav: "ICRD" } }, trackNumber: { key: "TRACKNUMBER", description: "The track number within the album", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TRCK" }, vorbis: "TRACKNUMBER", mp4: "trkn", wav: "ITRK" } }, genre: { key: "GENRE", description: "The musical genre", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "TCON" }, vorbis: "GENRE", mp4: "\xA9gen", wav: "IGNR" } }, comment: { key: "COMMENT", description: "Comments or notes about the track", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis", "WAV"], mappings: { id3v2: { frame: "COMM" }, vorbis: "COMMENT", mp4: "\xA9cmt", wav: "ICMT" } } }; } }); // src/constants/general-extended-properties.ts var GENERAL_EXTENDED_PROPERTIES; var init_general_extended_properties = __esm({ "src/constants/general-extended-properties.ts"() { "use strict"; GENERAL_EXTENDED_PROPERTIES = { albumArtist: { key: "ALBUMARTIST", description: "The album artist (band/orchestra/ensemble)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TPE2" }, vorbis: "ALBUMARTIST", mp4: "aART" } }, composer: { key: "COMPOSER", description: "The original composer(s) of the track", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TCOM" }, vorbis: "COMPOSER", mp4: "\xA9wrt" } }, copyright: { key: "COPYRIGHT", description: "Copyright information", type: "string", supportedFormats: ["ID3v2", "Vorbis"], mappings: { vorbis: "COPYRIGHT" } }, encodedBy: { key: "ENCODEDBY", description: "The encoding software or person", type: "string", supportedFormats: ["ID3v2", "Vorbis"], mappings: { vorbis: "ENCODEDBY" } }, discNumber: { key: "DISCNUMBER", description: "The disc number for multi-disc sets", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TPOS" }, vorbis: "DISCNUMBER", mp4: "disk" } }, bpm: { key: "BPM", description: "Beats per minute", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TBPM" }, vorbis: "BPM", mp4: "tmpo" } }, totalTracks: { key: "TRACKTOTAL", description: "Total number of tracks on the album", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TRCK" }, vorbis: "TRACKTOTAL", mp4: "trkn" } }, totalDiscs: { key: "DISCTOTAL", description: "Total number of discs in the set", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TPOS" }, vorbis: "DISCTOTAL", mp4: "disk" } }, compilation: { key: "COMPILATION", description: "Whether the album is a compilation (various artists)", type: "boolean", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TCMP" }, vorbis: "COMPILATION", mp4: "cpil" } }, // Sorting Properties titleSort: { key: "TITLESORT", description: "Sort name for title (for alphabetization)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TSOT" }, vorbis: "TITLESORT", mp4: "sonm" } }, artistSort: { key: "ARTISTSORT", description: "Sort name for artist (for alphabetization)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TSOP" }, vorbis: "ARTISTSORT", mp4: "soar" } }, albumSort: { key: "ALBUMSORT", description: "Sort name for album (for alphabetization)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TSOA" }, vorbis: "ALBUMSORT", mp4: "soal" } }, // Additional common properties lyricist: { key: "LYRICIST", description: "The lyrics/text writer(s)", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "LYRICIST" } }, conductor: { key: "CONDUCTOR", description: "The conductor", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "CONDUCTOR" } }, remixedBy: { key: "REMIXER", description: "Person who remixed the track", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TPE4" }, vorbis: "REMIXER", mp4: "----:com.apple.iTunes:REMIXER" } }, language: { key: "LANGUAGE", description: "Language of vocals/lyrics", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "LANGUAGE" } }, publisher: { key: "PUBLISHER", description: "The publisher", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "PUBLISHER" } }, mood: { key: "MOOD", description: "The mood/atmosphere of the track", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "MOOD" } }, media: { key: "MEDIA", description: "Media type (CD, vinyl, etc.)", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "MEDIA" } }, grouping: { key: "GROUPING", description: "Content group/work", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "GROUPING" } }, work: { key: "WORK", description: "Work name", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "WORK" } }, lyrics: { key: "LYRICS", description: "Lyrics content", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "LYRICS" } }, isrc: { key: "ISRC", description: "International Standard Recording Code", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "ISRC" } }, catalogNumber: { key: "CATALOGNUMBER", description: "Catalog number", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "CATALOGNUMBER" } }, barcode: { key: "BARCODE", description: "Barcode (EAN/UPC)", type: "string", supportedFormats: ["Vorbis"], mappings: { vorbis: "BARCODE" } } }; } }); // src/constants/specialized-properties.ts var SPECIALIZED_PROPERTIES; var init_specialized_properties = __esm({ "src/constants/specialized-properties.ts"() { "use strict"; SPECIALIZED_PROPERTIES = { // MusicBrainz Identifiers musicbrainzArtistId: { key: "MUSICBRAINZ_ARTISTID", description: "MusicBrainz Artist ID (UUID)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "MusicBrainz Artist Id" }, vorbis: "MUSICBRAINZ_ARTISTID", mp4: "----:com.apple.iTunes:MusicBrainz Artist Id" } }, musicbrainzReleaseId: { key: "MUSICBRAINZ_ALBUMID", description: "MusicBrainz Release ID (UUID)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "MusicBrainz Album Id" }, vorbis: "MUSICBRAINZ_ALBUMID", mp4: "----:com.apple.iTunes:MusicBrainz Album Id" } }, musicbrainzTrackId: { key: "MUSICBRAINZ_TRACKID", description: "MusicBrainz Recording ID (UUID)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "UFID", description: "http://musicbrainz.org" }, vorbis: "MUSICBRAINZ_TRACKID", mp4: "----:com.apple.iTunes:MusicBrainz Track Id" } }, musicbrainzReleaseGroupId: { key: "MUSICBRAINZ_RELEASEGROUPID", description: "MusicBrainz Release Group ID (UUID)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "MusicBrainz Release Group Id" }, vorbis: "MUSICBRAINZ_RELEASEGROUPID", mp4: "----:com.apple.iTunes:MusicBrainz Release Group Id" } }, // ReplayGain Properties replayGainTrackGain: { key: "REPLAYGAIN_TRACK_GAIN", description: "ReplayGain track gain in dB (e.g., '-6.54 dB')", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "ReplayGain_Track_Gain" }, vorbis: "REPLAYGAIN_TRACK_GAIN", mp4: "----:com.apple.iTunes:replaygain_track_gain" } }, replayGainTrackPeak: { key: "REPLAYGAIN_TRACK_PEAK", description: "ReplayGain track peak value (0.0-1.0)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "ReplayGain_Track_Peak" }, vorbis: "REPLAYGAIN_TRACK_PEAK", mp4: "----:com.apple.iTunes:replaygain_track_peak" } }, replayGainAlbumGain: { key: "REPLAYGAIN_ALBUM_GAIN", description: "ReplayGain album gain in dB", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "ReplayGain_Album_Gain" }, vorbis: "REPLAYGAIN_ALBUM_GAIN", mp4: "----:com.apple.iTunes:replaygain_album_gain" } }, replayGainAlbumPeak: { key: "REPLAYGAIN_ALBUM_PEAK", description: "ReplayGain album peak value (0.0-1.0)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "ReplayGain_Album_Peak" }, vorbis: "REPLAYGAIN_ALBUM_PEAK", mp4: "----:com.apple.iTunes:replaygain_album_peak" } }, // AcoustID Properties acoustidFingerprint: { key: "ACOUSTID_FINGERPRINT", description: "AcoustID fingerprint (Chromaprint)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "Acoustid Fingerprint" }, vorbis: "ACOUSTID_FINGERPRINT", mp4: "----:com.apple.iTunes:Acoustid Fingerprint" } }, acoustidId: { key: "ACOUSTID_ID", description: "AcoustID UUID", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "Acoustid Id" }, vorbis: "ACOUSTID_ID", mp4: "----:com.apple.iTunes:Acoustid Id" } }, // Apple Sound Check appleSoundCheck: { key: "ITUNNORM", description: "Apple Sound Check normalization data", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "iTunNORM" }, vorbis: "ITUNNORM", mp4: "----:com.apple.iTunes:iTunNORM" } }, /** * Apple gapless-playback data (encoder delay, padding, original sample count). * * Exists so the two Apple freeform atoms are presented consistently: without * it, `properties()` answered `appleSoundCheck` for iTunNORM but the raw * uppercased `ITUNSMPB` for its sibling, because only one had an alias. The * name mirrors `appleSoundCheck` — vendor-prefixed, feature-named rather than * atom-named (tuneup-ibo). */ appleGaplessInfo: { key: "ITUNSMPB", description: "Apple gapless playback data (encoder delay and padding)", type: "string", supportedFormats: ["ID3v2", "MP4", "Vorbis"], mappings: { id3v2: { frame: "TXXX", description: "iTunSMPB" }, vorbis: "ITUNSMPB", mp4: "----:com.apple.iTunes:iTunSMPB" } } }; } }); // src/constants/properties.ts function toTagLibKey(key) { return _toTagLib[key] ?? key; } function mp4AtomWireKey(atom) { return _mp4AtomToWireKey[atom]; } function mp4FreeformAtomNames(wireKeys) { const names = []; for (const key of wireKeys) { const atom = _mp4FreeformAtoms[key]; if (atom !== void 0) names.push(atom); } return names; } function fromTagLibKey(key) { return _fromTagLib[key] ?? key; } function remapKeysFromTagLib(obj) { const result = {}; for (const [key, value] of Object.entries(obj)) { result[fromTagLibKey(key)] = value; } return result; } var PROPERTIES, _toTagLib, _fromTagLib, _mp4FreeformAtoms, _mp4AtomToWireKey; var init_properties = __esm({ "src/constants/properties.ts"() { "use strict"; init_additional_properties(); init_basic_properties(); init_general_extended_properties(); init_specialized_properties(); PROPERTIES = { ...BASIC_PROPERTIES, ...GENERAL_EXTENDED_PROPERTIES, ...SPECIALIZED_PROPERTIES, ...ADDITIONAL_PROPERTIES }; _toTagLib = {}; _fromTagLib = {}; for (const [camelKey, meta] of Object.entries(PROPERTIES)) { const wireKey = meta.key; _toTagLib[camelKey] = wireKey; _fromTagLib[wireKey] = camelKey; } _toTagLib["year"] = "DATE"; _toTagLib["track"] = "TRACKNUMBER"; _fromTagLib["disc"] = "discNumber"; _mp4FreeformAtoms = {}; for (const meta of Object.values(PROPERTIES)) { const { key, mappings } = meta; const atom = mappings?.mp4; if (typeof atom !== "string" || !atom.startsWith("----:")) continue; const bare = atom.slice(atom.lastIndexOf(":") + 1); if (bare === bare.toUpperCase()) continue; _mp4FreeformAtoms[key] = atom; const readKey = bare.toUpperCase(); const camel = _fromTagLib[key]; if (camel !== void 0 && _fromTagLib[readKey] === void 0) { _fromTagLib[readKey] = camel; } } _mp4AtomToWireKey = {}; for (const meta of Object.values(PROPERTIES)) { const { key, mappings } = meta; const atom = mappings?.mp4; if (typeof atom !== "string" || atom.startsWith("----:")) continue; if (_mp4AtomToWireKey[atom] === void 0) _mp4AtomToWireKey[atom] = key; } } }); // src/taglib/mp4-item-names.ts var MP4_ITEM_NAMES_KEY; var init_mp4_item_names = __esm({ "src/taglib/mp4-item-names.ts"() { "use strict"; MP4_ITEM_NAMES_KEY = "_mp4ItemNames"; } }); // src/taglib/mutable-tag-impl.ts function buildMutableTag(handle) { let data = handle.getTagData(); const tag = { get title() { return data.title; }, get artist() { return data.artist; }, get album() { return data.album; }, get comment() { return data.comment; }, get genre() { return data.genre; }, get year() { return data.year; }, get track() { return data.track; }, get date() { return handle.getProperty("DATE") || void 0; }, setTitle: (value) => { handle.setTagData({ title: value }); data = handle.getTagData(); return tag; }, setArtist: (value) => { handle.setTagData({ artist: value }); data = handle.getTagData(); return tag; }, setAlbum: (value) => { handle.setTagData({ album: value }); data = handle.getTagData(); return tag; }, setComment: (value) => { handle.setTagData({ comment: value }); data = handle.getTagData(); return tag; }, setGenre: (value) => { handle.setTagData({ genre: value }); data = handle.getTagData(); return tag; }, setYear: (value) => { handle.setTagData({ year: value }); data = handle.getTagData(); return tag; }, setTrack: (value) => { const existing = value > 0 ? handle.getProperty(RAW_TRACK_WIRE_KEY) : ""; const slash = existing.indexOf("/"); if (slash !== -1) { handle.setProperty( RAW_TRACK_WIRE_KEY, `${value}${existing.slice(slash)}` ); } else { handle.setTagData({ track: value }); } data = handle.getTagData(); return tag; }, setDate: (value) => { if (value === "") { handle.setTagData({ year: 0 }); } else { handle.setProperty("DATE", value); } data = handle.getTagData(); return tag; } }; return tag; } var RAW_TRACK_WIRE_KEY; var init_mutable_tag_impl = __esm({ "src/taglib/mutable-tag-impl.ts"() { "use strict"; init_properties(); RAW_TRACK_WIRE_KEY = toTagLibKey("trackNumber"); } }); // src/taglib/audio-file-base.ts var LYRICS_PROPERTY_KEY, LYRICS_WIRE_KEY, EMPTY_KEY_SET, BaseAudioFileImpl; var init_audio_file_base = __esm({ "src/taglib/audio-file-base.ts"() { "use strict"; init_properties(); init_errors2(); init_mp4_item_names(); init_mutable_tag_impl(); LYRICS_PROPERTY_KEY = "lyrics"; LYRICS_WIRE_KEY = toTagLibKey(LYRICS_PROPERTY_KEY); EMPTY_KEY_SET = /* @__PURE__ */ new Set(); BaseAudioFileImpl = class { constructor(module, fileHandle, sourcePath, originalSource, isPartiallyLoaded = false, partialLoadOptions) { __publicField(this, "module", module); __publicField(this, "fileHandle"); __publicField(this, "cachedAudioProperties", null); __publicField(this, "sourcePath"); __publicField(this, "originalSource"); __publicField(this, "isPartiallyLoaded", false); __publicField(this, "partialLoadOptions"); /** Text-property wire keys in the partial header at load; undefined if full. */ __publicField(this, "partialKeysAtLoad"); this.fileHandle = fileHandle; this.sourcePath = sourcePath; this.originalSource = originalSource; this.isPartiallyLoaded = isPartiallyLoaded; this.partialLoadOptions = partialLoadOptions; this.partialKeysAtLoad = isPartiallyLoaded ? new Set(Object.keys(fileHandle.getProperties())) : void 0; } /** * Wire-key text properties present in the partial header at load but now * absent — the user deleted them. The partial-load reconstruct subtracts these * from its preserve-the-original merge so a deletion persists without wiping * frames beyond the loaded header (taglib-d14). Lyrics are excluded (they ride * their own reconstruct path); empty for a full load. */ partialDeletedPropertyKeys() { if (!this.partialKeysAtLoad) return EMPTY_KEY_SET; const current = new Set(Object.keys(this.handle.getProperties())); const deleted = /* @__PURE__ */ new Set(); for (const key of this.partialKeysAtLoad) { if (key !== LYRICS_WIRE_KEY && !current.has(key)) deleted.add(key); } return deleted; } get handle() { if (!this.fileHandle) { throw new MetadataError("read", "File handle has been disposed"); } return this.fileHandle; } getFormat() { return this.handle.getFormat(); } isFormat(format) { return this.getFormat() === format; } tag() { return buildMutableTag(this.handle); } audioProperties() { if (!this.cachedAudioProperties) { this.cachedAudioProperties = this.handle.getAudioProperties() ?? null; } return this.cachedAudioProperties ?? void 0; } properties() { const remapped = remapKeysFromTagLib(this.handle.getProperties()); delete remapped[LYRICS_PROPERTY_KEY]; return remapped; } setProperties(properties) { const translated = {}; for (const [key, values] of Object.entries(properties)) { if (values !== void 0) translated[toTagLibKey(key)] = values; } if (!(LYRICS_WIRE_KEY in translated)) { const existing = this.handle.getProperties()[LYRICS_WIRE_KEY]; if (existing !== void 0) translated[LYRICS_WIRE_KEY] = existing; } const atomNames = mp4FreeformAtomNames(Object.keys(translated)); if (atomNames.length > 0) translated[MP4_ITEM_NAMES_KEY] = atomNames; this.handle.setProperties(translated); } getProperty(key) { const value = this.handle.getProperty(toTagLibKey(key)); if (value !== "") return value; if (!this.isMP4()) return void 0; const remapped = this.properties()[key]?.[0]; return remapped === "" ? void 0 : remapped; } setProperty(key, value) { const wireKey = toTagLibKey(key); const atomNames = mp4FreeformAtomNames([wireKey]); if (atomNames.length > 0 && this.isMP4()) { this.handle.setProperties({ ...