taglib-wasm 1.5.3 → 1.6.1

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 (52) hide show
  1. package/LICENSE +12 -9
  2. package/README.md +6 -3
  3. package/dist/index.browser.js +402 -90
  4. package/dist/simple.browser.js +400 -89
  5. package/dist/src/constants/complex-properties.d.ts +18 -12
  6. package/dist/src/constants/complex-properties.d.ts.map +1 -1
  7. package/dist/src/constants/properties.d.ts +24 -0
  8. package/dist/src/constants/properties.d.ts.map +1 -1
  9. package/dist/src/constants/properties.js +34 -0
  10. package/dist/src/constants/specialized-properties.d.ts +23 -0
  11. package/dist/src/constants/specialized-properties.d.ts.map +1 -1
  12. package/dist/src/constants/specialized-properties.js +20 -0
  13. package/dist/src/constants/tags.d.ts.map +1 -1
  14. package/dist/src/constants/tags.js +2 -1
  15. package/dist/src/msgpack/encoder.d.ts.map +1 -1
  16. package/dist/src/msgpack/encoder.js +6 -4
  17. package/dist/src/runtime/detector.d.ts +0 -6
  18. package/dist/src/runtime/detector.d.ts.map +1 -1
  19. package/dist/src/runtime/detector.js +1 -14
  20. package/dist/src/runtime/wasi-adapter/file-handle.d.ts +2 -0
  21. package/dist/src/runtime/wasi-adapter/file-handle.d.ts.map +1 -1
  22. package/dist/src/runtime/wasi-adapter/file-handle.js +73 -52
  23. package/dist/src/taglib/audio-file-base.d.ts.map +1 -1
  24. package/dist/src/taglib/audio-file-base.js +33 -77
  25. package/dist/src/taglib/load-audio-data.d.ts.map +1 -1
  26. package/dist/src/taglib/load-audio-data.js +19 -5
  27. package/dist/src/taglib/metadata-extent.d.ts +41 -0
  28. package/dist/src/taglib/metadata-extent.d.ts.map +1 -0
  29. package/dist/src/taglib/metadata-extent.js +128 -0
  30. package/dist/src/taglib/mp4-item-names.d.ts +12 -0
  31. package/dist/src/taglib/mp4-item-names.d.ts.map +1 -0
  32. package/dist/src/taglib/mp4-item-names.js +4 -0
  33. package/dist/src/taglib/mutable-tag-impl.d.ts +14 -0
  34. package/dist/src/taglib/mutable-tag-impl.d.ts.map +1 -0
  35. package/dist/src/taglib/mutable-tag-impl.js +88 -0
  36. package/dist/src/taglib/save-reconstruct.d.ts.map +1 -1
  37. package/dist/src/taglib/save-reconstruct.js +4 -0
  38. package/dist/src/types/metadata-mappings.d.ts.map +1 -1
  39. package/dist/src/types/metadata-mappings.js +14 -0
  40. package/dist/src/types/tags.d.ts +16 -0
  41. package/dist/src/types/tags.d.ts.map +1 -1
  42. package/dist/src/utils/mirror-fields.d.ts +79 -0
  43. package/dist/src/utils/mirror-fields.d.ts.map +1 -0
  44. package/dist/src/utils/mirror-fields.js +67 -0
  45. package/dist/src/utils/tag-mapping.d.ts.map +1 -1
  46. package/dist/src/utils/tag-mapping.js +47 -5
  47. package/dist/src/version.d.ts +1 -1
  48. package/dist/src/version.js +1 -1
  49. package/dist/taglib-wasi.wasm +0 -0
  50. package/dist/taglib-web.wasm +0 -0
  51. package/dist/taglib-wrapper.js +1 -1
  52. package/package.json +3 -3
@@ -1375,6 +1375,26 @@ var init_specialized_properties = __esm({
1375
1375
  vorbis: "ITUNNORM",
1376
1376
  mp4: "----:com.apple.iTunes:iTunNORM"
1377
1377
  }
1378
+ },
1379
+ /**
1380
+ * Apple gapless-playback data (encoder delay, padding, original sample count).
1381
+ *
1382
+ * Exists so the two Apple freeform atoms are presented consistently: without
1383
+ * it, `properties()` answered `appleSoundCheck` for iTunNORM but the raw
1384
+ * uppercased `ITUNSMPB` for its sibling, because only one had an alias. The
1385
+ * name mirrors `appleSoundCheck` — vendor-prefixed, feature-named rather than
1386
+ * atom-named (tuneup-ibo).
1387
+ */
1388
+ appleGaplessInfo: {
1389
+ key: "ITUNSMPB",
1390
+ description: "Apple gapless playback data (encoder delay and padding)",
1391
+ type: "string",
1392
+ supportedFormats: ["ID3v2", "MP4", "Vorbis"],
1393
+ mappings: {
1394
+ id3v2: { frame: "TXXX", description: "iTunSMPB" },
1395
+ vorbis: "ITUNSMPB",
1396
+ mp4: "----:com.apple.iTunes:iTunSMPB"
1397
+ }
1378
1398
  }
1379
1399
  };
1380
1400
  }
@@ -1384,6 +1404,17 @@ var init_specialized_properties = __esm({
1384
1404
  function toTagLibKey(key) {
1385
1405
  return _toTagLib[key] ?? key;
1386
1406
  }
1407
+ function mp4AtomWireKey(atom) {
1408
+ return _mp4AtomToWireKey[atom];
1409
+ }
1410
+ function mp4FreeformAtomNames(wireKeys) {
1411
+ const names = [];
1412
+ for (const key of wireKeys) {
1413
+ const atom = _mp4FreeformAtoms[key];
1414
+ if (atom !== void 0) names.push(atom);
1415
+ }
1416
+ return names;
1417
+ }
1387
1418
  function fromTagLibKey(key) {
1388
1419
  return _fromTagLib[key] ?? key;
1389
1420
  }
@@ -1394,7 +1425,7 @@ function remapKeysFromTagLib(obj) {
1394
1425
  }
1395
1426
  return result;
1396
1427
  }
1397
- var PROPERTIES, _toTagLib, _fromTagLib;
1428
+ var PROPERTIES, _toTagLib, _fromTagLib, _mp4FreeformAtoms, _mp4AtomToWireKey;
1398
1429
  var init_properties = __esm({
1399
1430
  "src/constants/properties.ts"() {
1400
1431
  "use strict";
@@ -1418,6 +1449,129 @@ var init_properties = __esm({
1418
1449
  _toTagLib["year"] = "DATE";
1419
1450
  _toTagLib["track"] = "TRACKNUMBER";
1420
1451
  _fromTagLib["disc"] = "discNumber";
1452
+ _mp4FreeformAtoms = {};
1453
+ for (const meta of Object.values(PROPERTIES)) {
1454
+ const { key, mappings } = meta;
1455
+ const atom = mappings?.mp4;
1456
+ if (typeof atom !== "string" || !atom.startsWith("----:")) continue;
1457
+ const bare = atom.slice(atom.lastIndexOf(":") + 1);
1458
+ if (bare === bare.toUpperCase()) continue;
1459
+ _mp4FreeformAtoms[key] = atom;
1460
+ const readKey = bare.toUpperCase();
1461
+ const camel = _fromTagLib[key];
1462
+ if (camel !== void 0 && _fromTagLib[readKey] === void 0) {
1463
+ _fromTagLib[readKey] = camel;
1464
+ }
1465
+ }
1466
+ _mp4AtomToWireKey = {};
1467
+ for (const meta of Object.values(PROPERTIES)) {
1468
+ const { key, mappings } = meta;
1469
+ const atom = mappings?.mp4;
1470
+ if (typeof atom !== "string" || atom.startsWith("----:")) continue;
1471
+ if (_mp4AtomToWireKey[atom] === void 0) _mp4AtomToWireKey[atom] = key;
1472
+ }
1473
+ }
1474
+ });
1475
+
1476
+ // src/taglib/mp4-item-names.ts
1477
+ var MP4_ITEM_NAMES_KEY;
1478
+ var init_mp4_item_names = __esm({
1479
+ "src/taglib/mp4-item-names.ts"() {
1480
+ "use strict";
1481
+ MP4_ITEM_NAMES_KEY = "_mp4ItemNames";
1482
+ }
1483
+ });
1484
+
1485
+ // src/taglib/mutable-tag-impl.ts
1486
+ function buildMutableTag(handle) {
1487
+ let data = handle.getTagData();
1488
+ const tag = {
1489
+ get title() {
1490
+ return data.title;
1491
+ },
1492
+ get artist() {
1493
+ return data.artist;
1494
+ },
1495
+ get album() {
1496
+ return data.album;
1497
+ },
1498
+ get comment() {
1499
+ return data.comment;
1500
+ },
1501
+ get genre() {
1502
+ return data.genre;
1503
+ },
1504
+ get year() {
1505
+ return data.year;
1506
+ },
1507
+ get track() {
1508
+ return data.track;
1509
+ },
1510
+ get date() {
1511
+ return handle.getProperty("DATE") || void 0;
1512
+ },
1513
+ setTitle: (value) => {
1514
+ handle.setTagData({ title: value });
1515
+ data = handle.getTagData();
1516
+ return tag;
1517
+ },
1518
+ setArtist: (value) => {
1519
+ handle.setTagData({ artist: value });
1520
+ data = handle.getTagData();
1521
+ return tag;
1522
+ },
1523
+ setAlbum: (value) => {
1524
+ handle.setTagData({ album: value });
1525
+ data = handle.getTagData();
1526
+ return tag;
1527
+ },
1528
+ setComment: (value) => {
1529
+ handle.setTagData({ comment: value });
1530
+ data = handle.getTagData();
1531
+ return tag;
1532
+ },
1533
+ setGenre: (value) => {
1534
+ handle.setTagData({ genre: value });
1535
+ data = handle.getTagData();
1536
+ return tag;
1537
+ },
1538
+ setYear: (value) => {
1539
+ handle.setTagData({ year: value });
1540
+ data = handle.getTagData();
1541
+ return tag;
1542
+ },
1543
+ setTrack: (value) => {
1544
+ const existing = value > 0 ? handle.getProperty(RAW_TRACK_WIRE_KEY) : "";
1545
+ const slash = existing.indexOf("/");
1546
+ if (slash !== -1) {
1547
+ handle.setProperty(
1548
+ RAW_TRACK_WIRE_KEY,
1549
+ `${value}${existing.slice(slash)}`
1550
+ );
1551
+ } else {
1552
+ handle.setTagData({ track: value });
1553
+ }
1554
+ data = handle.getTagData();
1555
+ return tag;
1556
+ },
1557
+ setDate: (value) => {
1558
+ if (value === "") {
1559
+ handle.setTagData({ year: 0 });
1560
+ } else {
1561
+ handle.setProperty("DATE", value);
1562
+ }
1563
+ data = handle.getTagData();
1564
+ return tag;
1565
+ }
1566
+ };
1567
+ return tag;
1568
+ }
1569
+ var RAW_TRACK_WIRE_KEY;
1570
+ var init_mutable_tag_impl = __esm({
1571
+ "src/taglib/mutable-tag-impl.ts"() {
1572
+ "use strict";
1573
+ init_properties();
1574
+ RAW_TRACK_WIRE_KEY = toTagLibKey("trackNumber");
1421
1575
  }
1422
1576
  });
1423
1577
 
@@ -1428,6 +1582,8 @@ var init_audio_file_base = __esm({
1428
1582
  "use strict";
1429
1583
  init_properties();
1430
1584
  init_errors2();
1585
+ init_mp4_item_names();
1586
+ init_mutable_tag_impl();
1431
1587
  LYRICS_PROPERTY_KEY = "lyrics";
1432
1588
  LYRICS_WIRE_KEY = toTagLibKey(LYRICS_PROPERTY_KEY);
1433
1589
  EMPTY_KEY_SET = /* @__PURE__ */ new Set();
@@ -1478,79 +1634,7 @@ var init_audio_file_base = __esm({
1478
1634
  return this.getFormat() === format;
1479
1635
  }
1480
1636
  tag() {
1481
- const handle = this.handle;
1482
- let data = handle.getTagData();
1483
- const tag = {
1484
- get title() {
1485
- return data.title;
1486
- },
1487
- get artist() {
1488
- return data.artist;
1489
- },
1490
- get album() {
1491
- return data.album;
1492
- },
1493
- get comment() {
1494
- return data.comment;
1495
- },
1496
- get genre() {
1497
- return data.genre;
1498
- },
1499
- get year() {
1500
- return data.year;
1501
- },
1502
- get track() {
1503
- return data.track;
1504
- },
1505
- get date() {
1506
- return handle.getProperty("DATE") || void 0;
1507
- },
1508
- setTitle: (value) => {
1509
- handle.setTagData({ title: value });
1510
- data = handle.getTagData();
1511
- return tag;
1512
- },
1513
- setArtist: (value) => {
1514
- handle.setTagData({ artist: value });
1515
- data = handle.getTagData();
1516
- return tag;
1517
- },
1518
- setAlbum: (value) => {
1519
- handle.setTagData({ album: value });
1520
- data = handle.getTagData();
1521
- return tag;
1522
- },
1523
- setComment: (value) => {
1524
- handle.setTagData({ comment: value });
1525
- data = handle.getTagData();
1526
- return tag;
1527
- },
1528
- setGenre: (value) => {
1529
- handle.setTagData({ genre: value });
1530
- data = handle.getTagData();
1531
- return tag;
1532
- },
1533
- setYear: (value) => {
1534
- handle.setTagData({ year: value });
1535
- data = handle.getTagData();
1536
- return tag;
1537
- },
1538
- setTrack: (value) => {
1539
- handle.setTagData({ track: value });
1540
- data = handle.getTagData();
1541
- return tag;
1542
- },
1543
- setDate: (value) => {
1544
- if (value === "") {
1545
- handle.setTagData({ year: 0 });
1546
- } else {
1547
- handle.setProperty("DATE", value);
1548
- }
1549
- data = handle.getTagData();
1550
- return tag;
1551
- }
1552
- };
1553
- return tag;
1637
+ return buildMutableTag(this.handle);
1554
1638
  }
1555
1639
  audioProperties() {
1556
1640
  if (!this.cachedAudioProperties) {
@@ -1572,14 +1656,29 @@ var init_audio_file_base = __esm({
1572
1656
  const existing = this.handle.getProperties()[LYRICS_WIRE_KEY];
1573
1657
  if (existing !== void 0) translated[LYRICS_WIRE_KEY] = existing;
1574
1658
  }
1659
+ const atomNames = mp4FreeformAtomNames(Object.keys(translated));
1660
+ if (atomNames.length > 0) translated[MP4_ITEM_NAMES_KEY] = atomNames;
1575
1661
  this.handle.setProperties(translated);
1576
1662
  }
1577
1663
  getProperty(key) {
1578
1664
  const value = this.handle.getProperty(toTagLibKey(key));
1579
- return value === "" ? void 0 : value;
1665
+ if (value !== "") return value;
1666
+ if (!this.isMP4()) return void 0;
1667
+ const remapped = this.properties()[key]?.[0];
1668
+ return remapped === "" ? void 0 : remapped;
1580
1669
  }
1581
1670
  setProperty(key, value) {
1582
- this.handle.setProperty(toTagLibKey(key), value);
1671
+ const wireKey = toTagLibKey(key);
1672
+ const atomNames = mp4FreeformAtomNames([wireKey]);
1673
+ if (atomNames.length > 0 && this.isMP4()) {
1674
+ this.handle.setProperties({
1675
+ ...this.handle.getProperties(),
1676
+ [wireKey]: [value],
1677
+ [MP4_ITEM_NAMES_KEY]: atomNames
1678
+ });
1679
+ return;
1680
+ }
1681
+ this.handle.setProperty(wireKey, value);
1583
1682
  }
1584
1683
  isMP4() {
1585
1684
  return this.handle.isMP4();
@@ -1588,13 +1687,19 @@ var init_audio_file_base = __esm({
1588
1687
  if (!this.isMP4()) {
1589
1688
  throw new UnsupportedFormatError(this.getFormat(), ["MP4", "M4A"]);
1590
1689
  }
1591
- const value = this.handle.getMP4Item(key);
1690
+ const wireKey = mp4AtomWireKey(key);
1691
+ const value = wireKey !== void 0 ? this.handle.getProperty(wireKey) : this.handle.getMP4Item(key);
1592
1692
  return value === "" ? void 0 : value;
1593
1693
  }
1594
1694
  setMP4Item(key, value) {
1595
1695
  if (!this.isMP4()) {
1596
1696
  throw new UnsupportedFormatError(this.getFormat(), ["MP4", "M4A"]);
1597
1697
  }
1698
+ const wireKey = mp4AtomWireKey(key);
1699
+ if (wireKey !== void 0) {
1700
+ this.handle.setProperty(wireKey, value);
1701
+ return;
1702
+ }
1598
1703
  this.handle.setMP4Item(key, value);
1599
1704
  }
1600
1705
  removeMP4Item(key) {
@@ -1877,6 +1982,8 @@ function copyEditedState(target, source, sourceComplete, deletedKeys) {
1877
1982
  target.setTagData(source.getTagData());
1878
1983
  const merged = sourceComplete ? source.getProperties() : { ...target.getProperties(), ...source.getProperties() };
1879
1984
  for (const key of deletedKeys) delete merged[key];
1985
+ const atomNames = mp4FreeformAtomNames(Object.keys(merged));
1986
+ if (atomNames.length > 0) merged[MP4_ITEM_NAMES_KEY] = atomNames;
1880
1987
  target.setProperties(merged);
1881
1988
  copyExtraState(target, source, sourceComplete);
1882
1989
  }
@@ -1912,6 +2019,8 @@ var init_save_reconstruct = __esm({
1912
2019
  init_write();
1913
2020
  init_embind_adapter();
1914
2021
  init_extra_state_registry();
2022
+ init_properties();
2023
+ init_mp4_item_names();
1915
2024
  }
1916
2025
  });
1917
2026
 
@@ -2180,6 +2289,137 @@ var init_audio_file_impl = __esm({
2180
2289
  }
2181
2290
  });
2182
2291
 
2292
+ // src/taglib/metadata-extent.ts
2293
+ function be32(b, offset) {
2294
+ return (b[offset] << 24 | b[offset + 1] << 16 | b[offset + 2] << 8 | b[offset + 3]) >>> 0;
2295
+ }
2296
+ function startsWith(bytes, magic, at = 0) {
2297
+ if (bytes.length < at + magic.length) return false;
2298
+ for (let i = 0; i < magic.length; i++) {
2299
+ if (bytes[at + i] !== magic.charCodeAt(i)) return false;
2300
+ }
2301
+ return true;
2302
+ }
2303
+ function id3v2End(bytes) {
2304
+ if (bytes.length < 10) return void 0;
2305
+ const size = bytes[6] << 21 | bytes[7] << 14 | bytes[8] << 7 | bytes[9];
2306
+ const hasFooter = (bytes[5] & 16) !== 0;
2307
+ return 10 + size + (hasFooter ? 10 : 0);
2308
+ }
2309
+ function flacEnd(bytes, limit, start) {
2310
+ let offset = start + 4;
2311
+ for (; ; ) {
2312
+ if (offset + 4 > bytes.length) return void 0;
2313
+ const isLast = (bytes[offset] & 128) !== 0;
2314
+ const length = bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3];
2315
+ offset += 4 + length;
2316
+ if (offset > limit) return offset;
2317
+ if (isLast) return offset;
2318
+ }
2319
+ }
2320
+ function mp4MoovEnd(bytes, limit, start) {
2321
+ let offset = start;
2322
+ const readable = Math.min(bytes.length, limit);
2323
+ while (offset + 8 <= readable) {
2324
+ const size = be32(bytes, offset);
2325
+ const type = String.fromCharCode(
2326
+ bytes[offset + 4],
2327
+ bytes[offset + 5],
2328
+ bytes[offset + 6],
2329
+ bytes[offset + 7]
2330
+ );
2331
+ if (size < 8) return void 0;
2332
+ if (type === "moov") return offset + size;
2333
+ offset += size;
2334
+ }
2335
+ return void 0;
2336
+ }
2337
+ function oggMetadataEnd(bytes, limit, start) {
2338
+ let offset = start;
2339
+ let packetsCompleted = 0;
2340
+ let first = true;
2341
+ for (; ; ) {
2342
+ if (offset + 27 > bytes.length || !startsWith(bytes, "OggS", offset)) {
2343
+ return void 0;
2344
+ }
2345
+ const segmentCount = bytes[offset + 26];
2346
+ const tableStart = offset + 27;
2347
+ if (tableStart + segmentCount > bytes.length) return void 0;
2348
+ let payload = 0;
2349
+ for (let i = 0; i < segmentCount; i++) {
2350
+ const segment = bytes[tableStart + i];
2351
+ payload += segment;
2352
+ if (segment < 255) packetsCompleted++;
2353
+ }
2354
+ if (first) {
2355
+ const payloadStart = tableStart + segmentCount;
2356
+ if (payloadStart + 5 > bytes.length) return void 0;
2357
+ if (bytes[payloadStart] === 127 && startsWith(bytes, "FLAC", payloadStart + 1)) {
2358
+ return void 0;
2359
+ }
2360
+ first = false;
2361
+ }
2362
+ offset = tableStart + segmentCount + payload;
2363
+ if (packetsCompleted >= 2 || offset > limit) return offset;
2364
+ }
2365
+ }
2366
+ function isMpegFrameSync(bytes, at = 0) {
2367
+ if (bytes.length < at + 4) return false;
2368
+ if (bytes[at] !== 255 || (bytes[at + 1] & 224) !== 224) return false;
2369
+ const version = bytes[at + 1] >> 3 & 3;
2370
+ const layer = bytes[at + 1] >> 1 & 3;
2371
+ if (version === 1 || layer === 0) return false;
2372
+ const bitrateIndex = bytes[at + 2] >> 4 & 15;
2373
+ const sampleRateIndex = bytes[at + 2] >> 2 & 3;
2374
+ return bitrateIndex !== 0 && bitrateIndex !== 15 && sampleRateIndex !== 3;
2375
+ }
2376
+ function containerEnd(bytes, at, limit) {
2377
+ if (at >= bytes.length) return bytes.length < limit ? null : void 0;
2378
+ if (at + 8 > bytes.length) return void 0;
2379
+ if (startsWith(bytes, "fLaC", at)) return flacEnd(bytes, limit, at);
2380
+ if (startsWith(bytes, "ftyp", at + 4)) return mp4MoovEnd(bytes, limit, at);
2381
+ if (startsWith(bytes, "OggS", at)) return oggMetadataEnd(bytes, limit, at);
2382
+ return null;
2383
+ }
2384
+ function metadataFitsInHeader(header, limit) {
2385
+ let end;
2386
+ if (startsWith(header, "ID3")) {
2387
+ const tagEnd = id3v2End(header);
2388
+ if (tagEnd === void 0) return false;
2389
+ end = tagEnd;
2390
+ if (tagEnd <= limit) {
2391
+ const behind = containerEnd(header, tagEnd, limit);
2392
+ if (behind === void 0) return false;
2393
+ if (behind !== null) end = behind;
2394
+ }
2395
+ } else {
2396
+ const container = containerEnd(header, 0, limit);
2397
+ if (container === null) {
2398
+ end = isMpegFrameSync(header) ? 0 : void 0;
2399
+ } else {
2400
+ end = container;
2401
+ }
2402
+ }
2403
+ if (end === void 0 || end > header.length) return false;
2404
+ return end <= limit;
2405
+ }
2406
+ function trailerFitsInFooter(tail, footerSize) {
2407
+ if (tail.length < Math.min(footerSize, 32)) return false;
2408
+ for (const offsetFromEnd of [32, 32 + 128]) {
2409
+ const at = tail.length - offsetFromEnd;
2410
+ if (at < 0 || !startsWith(tail, "APETAGEX", at)) continue;
2411
+ const size = (tail[at + 12] | tail[at + 13] << 8 | tail[at + 14] << 16 | tail[at + 15] << 24) >>> 0;
2412
+ const trailing = offsetFromEnd === 32 ? 0 : 128;
2413
+ if (size + 32 + trailing > footerSize) return false;
2414
+ }
2415
+ return true;
2416
+ }
2417
+ var init_metadata_extent = __esm({
2418
+ "src/taglib/metadata-extent.ts"() {
2419
+ "use strict";
2420
+ }
2421
+ });
2422
+
2183
2423
  // src/taglib/load-audio-data.ts
2184
2424
  async function loadAudioData(input, opts) {
2185
2425
  if (opts.partial && typeof File !== "undefined" && input instanceof File) {
@@ -2188,12 +2428,20 @@ async function loadAudioData(input, opts) {
2188
2428
  if (input.size <= headerSize + footerSize) {
2189
2429
  return { data: await readFileData(input), isPartiallyLoaded: false };
2190
2430
  }
2191
- const header = await input.slice(0, headerSize).arrayBuffer();
2431
+ const header = new Uint8Array(
2432
+ await input.slice(0, headerSize).arrayBuffer()
2433
+ );
2434
+ if (!metadataFitsInHeader(header, headerSize)) {
2435
+ return { data: await readFileData(input), isPartiallyLoaded: false };
2436
+ }
2192
2437
  const footerStart = Math.max(0, input.size - footerSize);
2193
- const footer = await input.slice(footerStart).arrayBuffer();
2438
+ const footer = new Uint8Array(await input.slice(footerStart).arrayBuffer());
2439
+ if (!trailerFitsInFooter(footer, footerSize)) {
2440
+ return { data: await readFileData(input), isPartiallyLoaded: false };
2441
+ }
2194
2442
  const combined = new Uint8Array(header.byteLength + footer.byteLength);
2195
- combined.set(new Uint8Array(header), 0);
2196
- combined.set(new Uint8Array(footer), header.byteLength);
2443
+ combined.set(header, 0);
2444
+ combined.set(footer, header.byteLength);
2197
2445
  return { data: combined, isPartiallyLoaded: true };
2198
2446
  }
2199
2447
  if (opts.partial && typeof input === "string") {
@@ -2204,7 +2452,9 @@ async function loadAudioData(input, opts) {
2204
2452
  opts.maxHeaderSize,
2205
2453
  opts.maxFooterSize
2206
2454
  );
2207
- return { data, isPartiallyLoaded: true };
2455
+ if (metadataFitsInHeader(data, opts.maxHeaderSize) && trailerFitsInFooter(data, opts.maxFooterSize)) {
2456
+ return { data, isPartiallyLoaded: true };
2457
+ }
2208
2458
  }
2209
2459
  return { data: await readFileData(input), isPartiallyLoaded: false };
2210
2460
  }
@@ -2214,14 +2464,29 @@ var init_load_audio_data = __esm({
2214
2464
  "src/taglib/load-audio-data.ts"() {
2215
2465
  "use strict";
2216
2466
  init_file();
2467
+ init_metadata_extent();
2217
2468
  }
2218
2469
  });
2219
2470
 
2220
- // src/utils/tag-mapping.ts
2221
- function parseNumeric(value) {
2222
- const parsed = Number.parseInt(value, 10);
2471
+ // src/utils/mirror-fields.ts
2472
+ function parseLeadingInt(raw) {
2473
+ const parsed = Number.parseInt(raw, 10);
2223
2474
  return Number.isNaN(parsed) ? void 0 : parsed;
2224
2475
  }
2476
+ var MIRROR_FIELDS, DATE_MIRROR, TRACK_MIRROR;
2477
+ var init_mirror_fields = __esm({
2478
+ "src/utils/mirror-fields.ts"() {
2479
+ "use strict";
2480
+ MIRROR_FIELDS = [
2481
+ { raw: "date", numeric: "year" },
2482
+ { raw: "trackNumber", numeric: "track" }
2483
+ ];
2484
+ DATE_MIRROR = MIRROR_FIELDS[0];
2485
+ TRACK_MIRROR = MIRROR_FIELDS[1];
2486
+ }
2487
+ });
2488
+
2489
+ // src/utils/tag-mapping.ts
2225
2490
  function readExtendedTag(audioFile) {
2226
2491
  const tag = mapPropertiesToExtendedTag(audioFile.properties());
2227
2492
  const pictures = audioFile.getPictures();
@@ -2246,21 +2511,35 @@ function mapPropertiesToExtendedTag(props) {
2246
2511
  const values = props[propKey];
2247
2512
  if (!values || values.length === 0) continue;
2248
2513
  if (tagField === "year" || tagField === "track") {
2249
- const num = parseNumeric(values[0]);
2514
+ const num = parseLeadingInt(values[0]);
2250
2515
  if (num !== void 0) tag[tagField] = num;
2251
2516
  if (propKey === "date") {
2252
2517
  tag.date = values.length === 1 ? values[0] : values;
2253
2518
  }
2519
+ if (propKey === "trackNumber") {
2520
+ tag.trackNumber = values.length === 1 ? values[0] : values;
2521
+ }
2254
2522
  } else {
2255
2523
  tag[tagField] = values;
2256
2524
  }
2257
2525
  }
2526
+ for (const [propKey, totalField] of [
2527
+ ["trackNumber", "totalTracks"],
2528
+ ["discNumber", "totalDiscs"]
2529
+ ]) {
2530
+ if (tag[totalField] !== void 0) continue;
2531
+ const raw = props[propKey]?.[0];
2532
+ const slash = raw?.indexOf("/") ?? -1;
2533
+ if (raw === void 0 || slash === -1) continue;
2534
+ const total = parseLeadingInt(raw.slice(slash + 1));
2535
+ if (total !== void 0) tag[totalField] = total;
2536
+ }
2258
2537
  for (const [key, values] of Object.entries(props)) {
2259
2538
  if (BASIC_PROPERTY_KEYS[key]) continue;
2260
2539
  if (!values || values.length === 0) continue;
2261
2540
  const camelKey = fromTagLibKey(key);
2262
2541
  if (NUMERIC_FIELDS.has(camelKey)) {
2263
- const num = parseNumeric(values[0]);
2542
+ const num = parseLeadingInt(values[0]);
2264
2543
  if (num !== void 0) tag[camelKey] = num;
2265
2544
  } else if (camelKey === "compilation") {
2266
2545
  tag[camelKey] = values[0] === "1";
@@ -2273,8 +2552,29 @@ function mapPropertiesToExtendedTag(props) {
2273
2552
  function mergeTagUpdates(file, tags) {
2274
2553
  const currentProps = file.properties();
2275
2554
  const newProps = normalizeTagInput(tags);
2555
+ reconcilePairFields(currentProps, newProps, tags);
2276
2556
  file.setProperties({ ...currentProps, ...newProps });
2277
2557
  }
2558
+ function reconcilePairFields(currentProps, newProps, input) {
2559
+ const pairs = [
2560
+ ["trackNumber", "totalTracks", "track"],
2561
+ ["discNumber", "totalDiscs", "discNumber"]
2562
+ ];
2563
+ for (const [rawKey, totalKey, numericKey] of pairs) {
2564
+ const incoming = newProps[rawKey]?.[0];
2565
+ if (incoming?.includes("/")) {
2566
+ newProps[totalKey] = [];
2567
+ continue;
2568
+ }
2569
+ const numericOnly = input[numericKey] !== void 0 && input[rawKey] === void 0;
2570
+ if (!numericOnly || incoming === void 0) continue;
2571
+ const existing = currentProps[rawKey]?.[0];
2572
+ const slash = existing?.indexOf("/") ?? -1;
2573
+ if (existing !== void 0 && slash !== -1) {
2574
+ newProps[rawKey] = [`${incoming}${existing.slice(slash)}`];
2575
+ }
2576
+ }
2577
+ }
2278
2578
  function normalizeTagInput(input) {
2279
2579
  const props = {};
2280
2580
  for (const field of [
@@ -2297,6 +2597,9 @@ function normalizeTagInput(input) {
2297
2597
  if (input.track !== void 0) {
2298
2598
  props.trackNumber = [String(input.track)];
2299
2599
  }
2600
+ if (input.trackNumber !== void 0) {
2601
+ props.trackNumber = Array.isArray(input.trackNumber) ? input.trackNumber : [input.trackNumber];
2602
+ }
2300
2603
  for (const [field, val] of Object.entries(input)) {
2301
2604
  if (BASIC_FIELDS.has(field) || val === void 0) continue;
2302
2605
  if (field === "compilation") {
@@ -2309,6 +2612,12 @@ function normalizeTagInput(input) {
2309
2612
  props[field] = val;
2310
2613
  }
2311
2614
  }
2615
+ for (const [rawField, totalField] of [
2616
+ ["trackNumber", "totalTracks"],
2617
+ ["discNumber", "totalDiscs"]
2618
+ ]) {
2619
+ if (props[rawField]?.[0]?.includes("/")) delete props[totalField];
2620
+ }
2312
2621
  return props;
2313
2622
  }
2314
2623
  var BASIC_PROPERTY_KEYS, BASIC_FIELDS, NUMERIC_FIELDS;
@@ -2316,6 +2625,7 @@ var init_tag_mapping = __esm({
2316
2625
  "src/utils/tag-mapping.ts"() {
2317
2626
  "use strict";
2318
2627
  init_properties();
2628
+ init_mirror_fields();
2319
2629
  BASIC_PROPERTY_KEYS = {
2320
2630
  title: "title",
2321
2631
  artist: "artist",
@@ -2333,7 +2643,8 @@ var init_tag_mapping = __esm({
2333
2643
  "genre",
2334
2644
  "year",
2335
2645
  "date",
2336
- "track"
2646
+ "track",
2647
+ "trackNumber"
2337
2648
  ]);
2338
2649
  NUMERIC_FIELDS = /* @__PURE__ */ new Set([
2339
2650
  "year",
@@ -2351,7 +2662,7 @@ var VERSION;
2351
2662
  var init_version = __esm({
2352
2663
  "src/version.ts"() {
2353
2664
  "use strict";
2354
- VERSION = "1.5.3";
2665
+ VERSION = "1.6.1";
2355
2666
  }
2356
2667
  });
2357
2668