taglib-wasm 0.3.3 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CONTRIBUTING.md +293 -0
  2. package/NOTICE +34 -0
  3. package/README.md +122 -511
  4. package/dist/index.d.ts +132 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +137 -0
  7. package/dist/index.ts +220 -0
  8. package/dist/src/constants.d.ts +201 -0
  9. package/dist/src/constants.d.ts.map +1 -0
  10. package/dist/src/constants.ts +227 -0
  11. package/dist/src/errors.d.ts +89 -0
  12. package/dist/src/errors.d.ts.map +1 -0
  13. package/dist/src/errors.ts +237 -0
  14. package/dist/src/file-utils.d.ts +205 -0
  15. package/dist/src/file-utils.d.ts.map +1 -0
  16. package/dist/src/file-utils.ts +467 -0
  17. package/dist/src/file.js +47 -0
  18. package/dist/src/global.d.ts +10 -0
  19. package/dist/src/mod.d.ts +9 -0
  20. package/dist/src/mod.d.ts.map +1 -0
  21. package/dist/src/mod.ts +19 -0
  22. package/dist/src/simple.d.ts +347 -0
  23. package/dist/src/simple.d.ts.map +1 -0
  24. package/dist/src/simple.ts +659 -0
  25. package/dist/src/taglib.d.ts +502 -0
  26. package/dist/src/taglib.d.ts.map +1 -0
  27. package/dist/src/taglib.ts +959 -0
  28. package/dist/src/types.d.ts +323 -0
  29. package/dist/src/types.d.ts.map +1 -0
  30. package/dist/src/types.ts +538 -0
  31. package/dist/src/utils/file.d.ts +15 -0
  32. package/dist/src/utils/file.d.ts.map +1 -0
  33. package/dist/src/utils/file.ts +82 -0
  34. package/dist/src/utils/write.d.ts +15 -0
  35. package/dist/src/utils/write.d.ts.map +1 -0
  36. package/dist/src/utils/write.ts +61 -0
  37. package/dist/src/wasm-workers.d.ts +33 -0
  38. package/dist/src/wasm-workers.d.ts.map +1 -0
  39. package/dist/src/wasm-workers.ts +176 -0
  40. package/dist/src/wasm.d.ts +97 -0
  41. package/dist/src/wasm.d.ts.map +1 -0
  42. package/dist/src/wasm.ts +133 -0
  43. package/dist/src/web-utils.d.ts +180 -0
  44. package/dist/src/web-utils.d.ts.map +1 -0
  45. package/dist/src/web-utils.ts +347 -0
  46. package/dist/src/workers.d.ts +219 -0
  47. package/dist/src/workers.d.ts.map +1 -0
  48. package/dist/src/workers.ts +465 -0
  49. package/dist/src/write.js +33 -0
  50. package/dist/taglib-wrapper.d.ts +5 -0
  51. package/dist/taglib-wrapper.js +14 -0
  52. package/dist/taglib.wasm +0 -0
  53. package/index.ts +100 -7
  54. package/package.json +40 -16
  55. package/src/errors.ts +237 -0
  56. package/src/file-utils.ts +467 -0
  57. package/src/global.d.ts +10 -0
  58. package/src/simple.ts +399 -84
  59. package/src/taglib.ts +522 -28
  60. package/src/types.ts +1 -1
  61. package/src/utils/file.ts +82 -0
  62. package/src/utils/write.ts +61 -0
  63. package/src/wasm-workers.ts +13 -4
  64. package/src/wasm.ts +1 -1
  65. package/src/web-utils.ts +347 -0
  66. package/src/workers.ts +32 -13
  67. package/build/taglib.js +0 -2407
  68. package/build/taglib.wasm +0 -0
package/src/taglib.ts CHANGED
@@ -4,7 +4,16 @@ import type {
4
4
  FileType,
5
5
  PropertyMap,
6
6
  Tag as BasicTag,
7
+ Picture,
7
8
  } from "./types.ts";
9
+ import {
10
+ InvalidFormatError,
11
+ MetadataError,
12
+ TagLibInitializationError,
13
+ UnsupportedFormatError,
14
+ } from "./errors.ts";
15
+ import { readFileData } from "./utils/file.ts";
16
+ import { writeFileData } from "./utils/write.ts";
8
17
 
9
18
  /**
10
19
  * Extended Tag interface with read/write capabilities for audio metadata.
@@ -12,7 +21,7 @@ import type {
12
21
  *
13
22
  * @example
14
23
  * ```typescript
15
- * const file = await taglib.openFile(buffer);
24
+ * const file = await taglib.open("song.mp3");
16
25
  * const tag = file.tag();
17
26
  *
18
27
  * // Read metadata
@@ -48,7 +57,7 @@ export interface Tag extends BasicTag {
48
57
  *
49
58
  * @example
50
59
  * ```typescript
51
- * const file = await taglib.openFile(audioBuffer);
60
+ * const file = await taglib.open("song.mp3");
52
61
  *
53
62
  * // Check if valid
54
63
  * if (!file.isValid()) {
@@ -161,17 +170,170 @@ export interface AudioFile {
161
170
  */
162
171
  getFileBuffer(): Uint8Array;
163
172
 
173
+ /**
174
+ * Save all changes to a file on disk.
175
+ * This first saves changes to the in-memory buffer, then writes to the specified path.
176
+ * @param path - Optional file path. If not provided, saves to the original path (if opened from a file).
177
+ * @throws {Error} If no path is available or write fails
178
+ */
179
+ saveToFile(path?: string): Promise<void>;
180
+
164
181
  /**
165
182
  * Check if the file was loaded successfully and is valid.
166
183
  * @returns true if the file is valid and can be processed
167
184
  */
168
185
  isValid(): boolean;
169
186
 
187
+ /**
188
+ * Get all pictures/cover art from the audio file.
189
+ * @returns Array of Picture objects
190
+ */
191
+ getPictures(): Picture[];
192
+
193
+ /**
194
+ * Set pictures/cover art in the audio file (replaces all existing).
195
+ * @param pictures - Array of Picture objects to set
196
+ */
197
+ setPictures(pictures: Picture[]): void;
198
+
199
+ /**
200
+ * Add a single picture to the audio file.
201
+ * @param picture - Picture object to add
202
+ */
203
+ addPicture(picture: Picture): void;
204
+
205
+ /**
206
+ * Remove all pictures from the audio file.
207
+ */
208
+ removePictures(): void;
209
+
170
210
  /**
171
211
  * Release all resources associated with this file.
172
212
  * Always call this when done to prevent memory leaks.
173
213
  */
174
214
  dispose(): void;
215
+
216
+ // Extended metadata helper methods
217
+
218
+ /**
219
+ * Get MusicBrainz Track ID.
220
+ * @returns MusicBrainz Track ID or undefined if not set
221
+ */
222
+ getMusicBrainzTrackId(): string | undefined;
223
+
224
+ /**
225
+ * Set MusicBrainz Track ID.
226
+ * @param id - MusicBrainz Track ID (UUID format)
227
+ */
228
+ setMusicBrainzTrackId(id: string): void;
229
+
230
+ /**
231
+ * Get MusicBrainz Release ID.
232
+ * @returns MusicBrainz Release ID or undefined if not set
233
+ */
234
+ getMusicBrainzReleaseId(): string | undefined;
235
+
236
+ /**
237
+ * Set MusicBrainz Release ID.
238
+ * @param id - MusicBrainz Release ID (UUID format)
239
+ */
240
+ setMusicBrainzReleaseId(id: string): void;
241
+
242
+ /**
243
+ * Get MusicBrainz Artist ID.
244
+ * @returns MusicBrainz Artist ID or undefined if not set
245
+ */
246
+ getMusicBrainzArtistId(): string | undefined;
247
+
248
+ /**
249
+ * Set MusicBrainz Artist ID.
250
+ * @param id - MusicBrainz Artist ID (UUID format)
251
+ */
252
+ setMusicBrainzArtistId(id: string): void;
253
+
254
+ /**
255
+ * Get AcoustID fingerprint.
256
+ * @returns AcoustID fingerprint or undefined if not set
257
+ */
258
+ getAcoustIdFingerprint(): string | undefined;
259
+
260
+ /**
261
+ * Set AcoustID fingerprint.
262
+ * @param fingerprint - AcoustID fingerprint
263
+ */
264
+ setAcoustIdFingerprint(fingerprint: string): void;
265
+
266
+ /**
267
+ * Get AcoustID ID.
268
+ * @returns AcoustID ID or undefined if not set
269
+ */
270
+ getAcoustIdId(): string | undefined;
271
+
272
+ /**
273
+ * Set AcoustID ID.
274
+ * @param id - AcoustID ID
275
+ */
276
+ setAcoustIdId(id: string): void;
277
+
278
+ /**
279
+ * Get ReplayGain track gain.
280
+ * @returns ReplayGain track gain (e.g., "-6.54 dB") or undefined
281
+ */
282
+ getReplayGainTrackGain(): string | undefined;
283
+
284
+ /**
285
+ * Set ReplayGain track gain.
286
+ * @param gain - ReplayGain track gain (e.g., "-6.54 dB")
287
+ */
288
+ setReplayGainTrackGain(gain: string): void;
289
+
290
+ /**
291
+ * Get ReplayGain track peak.
292
+ * @returns ReplayGain track peak (0.0-1.0) or undefined
293
+ */
294
+ getReplayGainTrackPeak(): string | undefined;
295
+
296
+ /**
297
+ * Set ReplayGain track peak.
298
+ * @param peak - ReplayGain track peak (0.0-1.0)
299
+ */
300
+ setReplayGainTrackPeak(peak: string): void;
301
+
302
+ /**
303
+ * Get ReplayGain album gain.
304
+ * @returns ReplayGain album gain (e.g., "-7.89 dB") or undefined
305
+ */
306
+ getReplayGainAlbumGain(): string | undefined;
307
+
308
+ /**
309
+ * Set ReplayGain album gain.
310
+ * @param gain - ReplayGain album gain (e.g., "-7.89 dB")
311
+ */
312
+ setReplayGainAlbumGain(gain: string): void;
313
+
314
+ /**
315
+ * Get ReplayGain album peak.
316
+ * @returns ReplayGain album peak (0.0-1.0) or undefined
317
+ */
318
+ getReplayGainAlbumPeak(): string | undefined;
319
+
320
+ /**
321
+ * Set ReplayGain album peak.
322
+ * @param peak - ReplayGain album peak (0.0-1.0)
323
+ */
324
+ setReplayGainAlbumPeak(peak: string): void;
325
+
326
+ /**
327
+ * Get Apple Sound Check normalization data.
328
+ * @returns Apple Sound Check data (iTunNORM) or undefined
329
+ */
330
+ getAppleSoundCheck(): string | undefined;
331
+
332
+ /**
333
+ * Set Apple Sound Check normalization data.
334
+ * @param data - Apple Sound Check data (iTunNORM format)
335
+ */
336
+ setAppleSoundCheck(data: string): void;
175
337
  }
176
338
 
177
339
  /**
@@ -179,18 +341,21 @@ export interface AudioFile {
179
341
  * Wraps the native TagLib C++ FileHandle object.
180
342
  *
181
343
  * @internal This class is not meant to be instantiated directly.
182
- * Use TagLib.openFile() to create instances.
344
+ * Use TagLib.open() to create instances.
183
345
  */
184
346
  export class AudioFileImpl implements AudioFile {
185
347
  private fileHandle: any;
186
348
  private cachedTag: Tag | null = null;
187
349
  private cachedAudioProperties: AudioProperties | null = null;
350
+ private sourcePath?: string;
188
351
 
189
352
  constructor(
190
353
  private module: TagLibModule,
191
354
  fileHandle: any,
355
+ sourcePath?: string,
192
356
  ) {
193
357
  this.fileHandle = fileHandle;
358
+ this.sourcePath = sourcePath;
194
359
  }
195
360
 
196
361
  /** @inheritdoc */
@@ -202,7 +367,10 @@ export class AudioFileImpl implements AudioFile {
202
367
  tag(): Tag {
203
368
  const tagWrapper = this.fileHandle.getTag();
204
369
  if (!tagWrapper) {
205
- throw new Error("Failed to get tag from file");
370
+ throw new MetadataError(
371
+ "read",
372
+ "Tag may be corrupted or format not fully supported"
373
+ );
206
374
  }
207
375
 
208
376
  return {
@@ -281,7 +449,11 @@ export class AudioFileImpl implements AudioFile {
281
449
  /** @inheritdoc */
282
450
  getMP4Item(key: string): string | undefined {
283
451
  if (!this.isMP4()) {
284
- throw new Error("Not an MP4 file");
452
+ const format = this.getFormat();
453
+ throw new UnsupportedFormatError(
454
+ format,
455
+ ["MP4", "M4A"]
456
+ );
285
457
  }
286
458
  const value = this.fileHandle.getMP4Item(key);
287
459
  return value === "" ? undefined : value;
@@ -290,7 +462,11 @@ export class AudioFileImpl implements AudioFile {
290
462
  /** @inheritdoc */
291
463
  setMP4Item(key: string, value: string): void {
292
464
  if (!this.isMP4()) {
293
- throw new Error("Not an MP4 file");
465
+ const format = this.getFormat();
466
+ throw new UnsupportedFormatError(
467
+ format,
468
+ ["MP4", "M4A"]
469
+ );
294
470
  }
295
471
  this.fileHandle.setMP4Item(key, value);
296
472
  }
@@ -298,7 +474,11 @@ export class AudioFileImpl implements AudioFile {
298
474
  /** @inheritdoc */
299
475
  removeMP4Item(key: string): void {
300
476
  if (!this.isMP4()) {
301
- throw new Error("Not an MP4 file");
477
+ const format = this.getFormat();
478
+ throw new UnsupportedFormatError(
479
+ format,
480
+ ["MP4", "M4A"]
481
+ );
302
482
  }
303
483
  this.fileHandle.removeMP4Item(key);
304
484
  }
@@ -314,34 +494,225 @@ export class AudioFileImpl implements AudioFile {
314
494
 
315
495
  /** @inheritdoc */
316
496
  getFileBuffer(): Uint8Array {
317
- const bufferString = this.fileHandle.getBuffer();
318
- if (!bufferString) {
497
+ const buffer = this.fileHandle.getBuffer();
498
+ if (!buffer) {
319
499
  return new Uint8Array(0);
320
500
  }
321
501
 
322
- // Convert string to Uint8Array
323
- const buffer = new Uint8Array(bufferString.length);
324
- for (let i = 0; i < bufferString.length; i++) {
325
- buffer[i] = bufferString.charCodeAt(i);
326
- }
502
+ // The buffer is already a Uint8Array from the C++ side
327
503
  return buffer;
328
504
  }
329
505
 
506
+ /** @inheritdoc */
507
+ async saveToFile(path?: string): Promise<void> {
508
+ // Determine the target path
509
+ const targetPath = path || this.sourcePath;
510
+ if (!targetPath) {
511
+ throw new Error(
512
+ "No file path available. Either provide a path or open the file from a path."
513
+ );
514
+ }
515
+
516
+ // First save to in-memory buffer
517
+ if (!this.save()) {
518
+ throw new Error("Failed to save changes to in-memory buffer");
519
+ }
520
+
521
+ // Get the updated buffer and write to file
522
+ const buffer = this.getFileBuffer();
523
+ await writeFileData(targetPath, buffer);
524
+ }
525
+
330
526
  /** @inheritdoc */
331
527
  isValid(): boolean {
332
528
  return this.fileHandle.isValid();
333
529
  }
334
530
 
531
+ /** @inheritdoc */
532
+ getPictures(): Picture[] {
533
+ const picturesArray = this.fileHandle.getPictures();
534
+ const pictures: Picture[] = [];
535
+
536
+ // Convert from Emscripten array to TypeScript array
537
+ for (let i = 0; i < picturesArray.length; i++) {
538
+ const pic = picturesArray[i];
539
+ pictures.push({
540
+ mimeType: pic.mimeType,
541
+ data: pic.data,
542
+ type: pic.type,
543
+ description: pic.description
544
+ });
545
+ }
546
+
547
+ return pictures;
548
+ }
549
+
550
+ /** @inheritdoc */
551
+ setPictures(pictures: Picture[]): void {
552
+ // Convert TypeScript array to format expected by C++
553
+ const picturesArray = pictures.map(pic => ({
554
+ mimeType: pic.mimeType,
555
+ data: pic.data,
556
+ type: pic.type,
557
+ description: pic.description || ""
558
+ }));
559
+
560
+ this.fileHandle.setPictures(picturesArray);
561
+ }
562
+
563
+ /** @inheritdoc */
564
+ addPicture(picture: Picture): void {
565
+ const pic = {
566
+ mimeType: picture.mimeType,
567
+ data: picture.data,
568
+ type: picture.type,
569
+ description: picture.description || ""
570
+ };
571
+
572
+ this.fileHandle.addPicture(pic);
573
+ }
574
+
575
+ /** @inheritdoc */
576
+ removePictures(): void {
577
+ this.fileHandle.removePictures();
578
+ }
579
+
335
580
  /** @inheritdoc */
336
581
  dispose(): void {
337
582
  if (this.fileHandle) {
338
- // Embind will handle cleanup when the object goes out of scope
339
- // But we can help by clearing our references
583
+ // Explicitly destroy the C++ object to free memory immediately
584
+ if (typeof this.fileHandle.destroy === 'function') {
585
+ this.fileHandle.destroy();
586
+ }
587
+ // Clear all references
340
588
  this.fileHandle = null;
341
589
  this.cachedTag = null;
342
590
  this.cachedAudioProperties = null;
343
591
  }
344
592
  }
593
+
594
+ // Extended metadata implementations
595
+
596
+ /** @inheritdoc */
597
+ getMusicBrainzTrackId(): string | undefined {
598
+ const value = this.getProperty("MUSICBRAINZ_TRACKID");
599
+ return value || undefined;
600
+ }
601
+
602
+ /** @inheritdoc */
603
+ setMusicBrainzTrackId(id: string): void {
604
+ this.setProperty("MUSICBRAINZ_TRACKID", id);
605
+ }
606
+
607
+ /** @inheritdoc */
608
+ getMusicBrainzReleaseId(): string | undefined {
609
+ const value = this.getProperty("MUSICBRAINZ_ALBUMID");
610
+ return value || undefined;
611
+ }
612
+
613
+ /** @inheritdoc */
614
+ setMusicBrainzReleaseId(id: string): void {
615
+ this.setProperty("MUSICBRAINZ_ALBUMID", id);
616
+ }
617
+
618
+ /** @inheritdoc */
619
+ getMusicBrainzArtistId(): string | undefined {
620
+ const value = this.getProperty("MUSICBRAINZ_ARTISTID");
621
+ return value || undefined;
622
+ }
623
+
624
+ /** @inheritdoc */
625
+ setMusicBrainzArtistId(id: string): void {
626
+ this.setProperty("MUSICBRAINZ_ARTISTID", id);
627
+ }
628
+
629
+ /** @inheritdoc */
630
+ getAcoustIdFingerprint(): string | undefined {
631
+ const value = this.getProperty("ACOUSTID_FINGERPRINT");
632
+ return value || undefined;
633
+ }
634
+
635
+ /** @inheritdoc */
636
+ setAcoustIdFingerprint(fingerprint: string): void {
637
+ this.setProperty("ACOUSTID_FINGERPRINT", fingerprint);
638
+ }
639
+
640
+ /** @inheritdoc */
641
+ getAcoustIdId(): string | undefined {
642
+ const value = this.getProperty("ACOUSTID_ID");
643
+ return value || undefined;
644
+ }
645
+
646
+ /** @inheritdoc */
647
+ setAcoustIdId(id: string): void {
648
+ this.setProperty("ACOUSTID_ID", id);
649
+ }
650
+
651
+ /** @inheritdoc */
652
+ getReplayGainTrackGain(): string | undefined {
653
+ const value = this.getProperty("REPLAYGAIN_TRACK_GAIN");
654
+ return value || undefined;
655
+ }
656
+
657
+ /** @inheritdoc */
658
+ setReplayGainTrackGain(gain: string): void {
659
+ this.setProperty("REPLAYGAIN_TRACK_GAIN", gain);
660
+ }
661
+
662
+ /** @inheritdoc */
663
+ getReplayGainTrackPeak(): string | undefined {
664
+ const value = this.getProperty("REPLAYGAIN_TRACK_PEAK");
665
+ return value || undefined;
666
+ }
667
+
668
+ /** @inheritdoc */
669
+ setReplayGainTrackPeak(peak: string): void {
670
+ this.setProperty("REPLAYGAIN_TRACK_PEAK", peak);
671
+ }
672
+
673
+ /** @inheritdoc */
674
+ getReplayGainAlbumGain(): string | undefined {
675
+ const value = this.getProperty("REPLAYGAIN_ALBUM_GAIN");
676
+ return value || undefined;
677
+ }
678
+
679
+ /** @inheritdoc */
680
+ setReplayGainAlbumGain(gain: string): void {
681
+ this.setProperty("REPLAYGAIN_ALBUM_GAIN", gain);
682
+ }
683
+
684
+ /** @inheritdoc */
685
+ getReplayGainAlbumPeak(): string | undefined {
686
+ const value = this.getProperty("REPLAYGAIN_ALBUM_PEAK");
687
+ return value || undefined;
688
+ }
689
+
690
+ /** @inheritdoc */
691
+ setReplayGainAlbumPeak(peak: string): void {
692
+ this.setProperty("REPLAYGAIN_ALBUM_PEAK", peak);
693
+ }
694
+
695
+ /** @inheritdoc */
696
+ getAppleSoundCheck(): string | undefined {
697
+ // Apple Sound Check is stored differently in MP4 files
698
+ if (this.isMP4()) {
699
+ return this.getMP4Item("iTunNORM");
700
+ }
701
+ // For other formats, it might be in properties
702
+ const value = this.getProperty("ITUNESOUNDCHECK");
703
+ return value || undefined;
704
+ }
705
+
706
+ /** @inheritdoc */
707
+ setAppleSoundCheck(data: string): void {
708
+ // Apple Sound Check is stored differently in MP4 files
709
+ if (this.isMP4()) {
710
+ this.setMP4Item("iTunNORM", data);
711
+ } else {
712
+ // For other formats, store in properties
713
+ this.setProperty("ITUNESOUNDCHECK", data);
714
+ }
715
+ }
345
716
  }
346
717
 
347
718
  /**
@@ -359,7 +730,7 @@ export class AudioFileImpl implements AudioFile {
359
730
  * const taglib = new TagLib(module);
360
731
  *
361
732
  * // Open and process a file
362
- * const file = await taglib.openFile(audioBuffer);
733
+ * const file = await taglib.open("song.mp3");
363
734
  * const tag = file.tag();
364
735
  * console.log(`Title: ${tag.title}`);
365
736
  * file.dispose();
@@ -381,7 +752,7 @@ export class TagLib {
381
752
  * @example
382
753
  * ```typescript
383
754
  * const taglib = await TagLib.initialize();
384
- * const file = await taglib.openFile(buffer);
755
+ * const file = await taglib.open("song.mp3");
385
756
  * ```
386
757
  */
387
758
  static async initialize(): Promise<TagLib> {
@@ -392,35 +763,53 @@ export class TagLib {
392
763
  }
393
764
 
394
765
  /**
395
- * Open an audio file from a buffer.
766
+ * Open an audio file from various sources.
396
767
  * Automatically detects the file format based on content.
397
768
  *
398
- * @param buffer - Audio file data as ArrayBuffer or typed array
769
+ * @param input - File path (string), ArrayBuffer, Uint8Array, or File object
399
770
  * @returns Promise resolving to AudioFile instance
400
771
  * @throws {Error} If the file format is invalid or unsupported
401
772
  * @throws {Error} If the module is not properly initialized
402
773
  *
403
774
  * @example
404
775
  * ```typescript
776
+ * // From file path
777
+ * const file = await taglib.open("song.mp3");
778
+ *
405
779
  * // From ArrayBuffer
406
- * const file = await taglib.openFile(arrayBuffer);
780
+ * const file = await taglib.open(arrayBuffer);
407
781
  *
408
782
  * // From Uint8Array
409
- * const uint8Array = new Uint8Array(buffer);
410
- * const file = await taglib.openFile(uint8Array.buffer);
783
+ * const file = await taglib.open(uint8Array);
784
+ *
785
+ * // From File object (browser)
786
+ * const file = await taglib.open(fileObject);
411
787
  *
412
788
  * // Remember to dispose when done
413
789
  * file.dispose();
414
790
  * ```
415
791
  */
416
- async openFile(buffer: ArrayBuffer): Promise<AudioFile> {
792
+ async open(input: string | ArrayBuffer | Uint8Array | File): Promise<AudioFile> {
417
793
  // Check if Embind is available
418
794
  if (!this.module.createFileHandle) {
419
- throw new Error(
420
- "TagLib module not properly initialized - createFileHandle not found",
795
+ throw new TagLibInitializationError(
796
+ "TagLib module not properly initialized: createFileHandle not found. " +
797
+ "Make sure the module is fully loaded before calling open."
421
798
  );
422
799
  }
423
800
 
801
+ // Track the source path if input is a string
802
+ const sourcePath = typeof input === "string" ? input : undefined;
803
+
804
+ // Read file data if input is a path or File object
805
+ const audioData = await readFileData(input);
806
+
807
+ // Ensure we pass the correct slice of the buffer
808
+ const buffer = audioData.buffer.slice(
809
+ audioData.byteOffset,
810
+ audioData.byteOffset + audioData.byteLength
811
+ );
812
+
424
813
  // Convert ArrayBuffer to Uint8Array for Embind
425
814
  const uint8Array = new Uint8Array(buffer);
426
815
 
@@ -430,10 +819,92 @@ export class TagLib {
430
819
  // Load the buffer - Embind should handle Uint8Array conversion
431
820
  const success = fileHandle.loadFromBuffer(uint8Array);
432
821
  if (!success) {
433
- throw new Error("Failed to load file from buffer");
822
+ throw new InvalidFormatError(
823
+ "Failed to load audio file. File may be corrupted or in an unsupported format",
824
+ buffer.byteLength
825
+ );
826
+ }
827
+
828
+ return new AudioFileImpl(this.module, fileHandle, sourcePath);
829
+ }
830
+
831
+ /**
832
+ * Update tags in a file and save changes to disk in one operation.
833
+ * This is a convenience method that opens, modifies, saves, and closes the file.
834
+ *
835
+ * @param path - File path to update
836
+ * @param tags - Object containing tags to update
837
+ * @throws {Error} If file operations fail
838
+ *
839
+ * @example
840
+ * ```typescript
841
+ * await taglib.updateFile("song.mp3", {
842
+ * title: "New Title",
843
+ * artist: "New Artist"
844
+ * });
845
+ * ```
846
+ */
847
+ async updateFile(path: string, tags: Partial<BasicTag>): Promise<void> {
848
+ const file = await this.open(path);
849
+ try {
850
+ const tag = file.tag();
851
+
852
+ // Apply tag updates
853
+ if (tags.title !== undefined) tag.setTitle(tags.title);
854
+ if (tags.artist !== undefined) tag.setArtist(tags.artist);
855
+ if (tags.album !== undefined) tag.setAlbum(tags.album);
856
+ if (tags.year !== undefined) tag.setYear(tags.year);
857
+ if (tags.track !== undefined) tag.setTrack(tags.track);
858
+ if (tags.genre !== undefined) tag.setGenre(tags.genre);
859
+ if (tags.comment !== undefined) tag.setComment(tags.comment);
860
+
861
+ // Save to file
862
+ await file.saveToFile();
863
+ } finally {
864
+ file.dispose();
434
865
  }
866
+ }
435
867
 
436
- return new AudioFileImpl(this.module, fileHandle);
868
+ /**
869
+ * Copy a file with new tags.
870
+ * Opens the source file, applies new tags, and saves to a new location.
871
+ *
872
+ * @param sourcePath - Source file path
873
+ * @param destPath - Destination file path
874
+ * @param tags - Object containing tags to apply
875
+ * @throws {Error} If file operations fail
876
+ *
877
+ * @example
878
+ * ```typescript
879
+ * await taglib.copyWithTags("original.mp3", "copy.mp3", {
880
+ * title: "Copy of Song",
881
+ * comment: "This is a copy"
882
+ * });
883
+ * ```
884
+ */
885
+ async copyWithTags(
886
+ sourcePath: string,
887
+ destPath: string,
888
+ tags: Partial<BasicTag>
889
+ ): Promise<void> {
890
+ const file = await this.open(sourcePath);
891
+ try {
892
+ const tag = file.tag();
893
+
894
+ // Apply tag updates
895
+ if (tags.title !== undefined) tag.setTitle(tags.title);
896
+ if (tags.artist !== undefined) tag.setArtist(tags.artist);
897
+ if (tags.album !== undefined) tag.setAlbum(tags.album);
898
+ if (tags.year !== undefined) tag.setYear(tags.year);
899
+ if (tags.track !== undefined) tag.setTrack(tags.track);
900
+ if (tags.genre !== undefined) tag.setGenre(tags.genre);
901
+ if (tags.comment !== undefined) tag.setComment(tags.comment);
902
+
903
+ // Save to new location
904
+ await file.saveToFile(destPath);
905
+ } finally {
906
+ file.dispose();
907
+ }
437
908
  }
438
909
 
439
910
  /**
@@ -463,3 +934,26 @@ export class TagLib {
463
934
  export async function createTagLib(module: WasmModule): Promise<TagLib> {
464
935
  return new TagLib(module);
465
936
  }
937
+
938
+ /**
939
+ * Re-export error types for convenient error handling
940
+ */
941
+ export {
942
+ EnvironmentError,
943
+ FileOperationError,
944
+ InvalidFormatError,
945
+ isEnvironmentError,
946
+ isFileOperationError,
947
+ isInvalidFormatError,
948
+ isMemoryError,
949
+ isMetadataError,
950
+ isTagLibError,
951
+ isUnsupportedFormatError,
952
+ MemoryError,
953
+ MetadataError,
954
+ SUPPORTED_FORMATS,
955
+ TagLibError,
956
+ TagLibErrorCode,
957
+ TagLibInitializationError,
958
+ UnsupportedFormatError,
959
+ } from "./errors.ts";