taglib-wasm 1.5.3 → 1.6.0

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 (47) hide show
  1. package/LICENSE +12 -9
  2. package/README.md +6 -3
  3. package/dist/index.browser.js +255 -85
  4. package/dist/simple.browser.js +253 -84
  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 +61 -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/mp4-item-names.d.ts +12 -0
  26. package/dist/src/taglib/mp4-item-names.d.ts.map +1 -0
  27. package/dist/src/taglib/mp4-item-names.js +4 -0
  28. package/dist/src/taglib/mutable-tag-impl.d.ts +14 -0
  29. package/dist/src/taglib/mutable-tag-impl.d.ts.map +1 -0
  30. package/dist/src/taglib/mutable-tag-impl.js +88 -0
  31. package/dist/src/taglib/save-reconstruct.d.ts.map +1 -1
  32. package/dist/src/taglib/save-reconstruct.js +4 -0
  33. package/dist/src/types/metadata-mappings.d.ts.map +1 -1
  34. package/dist/src/types/metadata-mappings.js +14 -0
  35. package/dist/src/types/tags.d.ts +16 -0
  36. package/dist/src/types/tags.d.ts.map +1 -1
  37. package/dist/src/utils/mirror-fields.d.ts +79 -0
  38. package/dist/src/utils/mirror-fields.d.ts.map +1 -0
  39. package/dist/src/utils/mirror-fields.js +67 -0
  40. package/dist/src/utils/tag-mapping.d.ts.map +1 -1
  41. package/dist/src/utils/tag-mapping.js +47 -5
  42. package/dist/src/version.d.ts +1 -1
  43. package/dist/src/version.js +1 -1
  44. package/dist/taglib-wasi.wasm +0 -0
  45. package/dist/taglib-web.wasm +0 -0
  46. package/dist/taglib-wrapper.js +1 -1
  47. package/package.json +3 -3
package/LICENSE CHANGED
@@ -27,21 +27,24 @@ ADDITIONAL LICENSING INFORMATION:
27
27
  This product includes software developed by Scott Wheeler and the TagLib
28
28
  contributors (https://taglib.org/).
29
29
 
30
- The WebAssembly binary (build/taglib.wasm) contains compiled TagLib code
31
- which is licensed under LGPL-2.1-or-later. The original TagLib source code
32
- is located in lib/taglib/. See lib/taglib/COPYING.LGPL for the full LGPL
33
- license terms.
30
+ The WebAssembly binaries (build/taglib-web.wasm for the Emscripten backend
31
+ and build/taglib-wasi.wasm for the WASI backend, shipped as dist/*.wasm)
32
+ contain compiled TagLib code which is licensed under LGPL-2.1-or-later.
33
+ The original TagLib source code is located in lib/taglib/. See
34
+ lib/taglib/COPYING.LGPL for the full LGPL license terms.
34
35
 
35
36
  LGPL COMPLIANCE:
36
- The WebAssembly binary is subject to LGPL requirements. This means:
37
- - You may use it in commercial applications
37
+ The WebAssembly binaries are subject to LGPL requirements. This means:
38
+ - You may use them in commercial applications
38
39
  - Modifications to TagLib must be shared under LGPL
39
40
  - You must enable users to relink with modified TagLib versions
40
41
 
41
- To rebuild the WebAssembly binary with a modified TagLib:
42
+ To rebuild the WebAssembly binaries with a modified TagLib:
42
43
  1. Modify the TagLib source in lib/taglib/
43
- 2. Run: npm run build:wasm
44
- 3. The new binary will be in build/taglib.wasm
44
+ 2. Run: npm run build:wasm (builds both backends; requires the
45
+ Emscripten SDK and WASI SDK 33 see CONTRIBUTING.md)
46
+ 3. The new binaries will be in build/taglib-web.wasm and
47
+ build/taglib-wasi.wasm
45
48
 
46
49
  For more information about LGPL compliance and dual licensing, see:
47
50
  https://github.com/CharlesWiltgen/TagLib-Wasm#licensing
package/README.md CHANGED
@@ -33,8 +33,11 @@ TagLib-Wasm is the **universal tagging library for TypeScript/JavaScript**
33
33
  possible
34
34
  - **Rich metadata** – Cover art, ratings, chapters (MP3 `CHAP`, MP4
35
35
  QuickTime/Nero), and broadcast metadata (BWF `bext`/iXML for WAV/FLAC)
36
+ - **Value fidelity** – Tag values round-trip verbatim: a `"3/12"` track or a
37
+ zero-padded `"03"` is preserved, and MP4 freeform atoms keep their exact
38
+ casing and vendor `mean`, so other tools still recognise them
36
39
  - **Zero dependencies** – Self-contained Wasm bundle
37
- - **Tested** – 293 tests across all formats
40
+ - **Tested** – 448 tests across all formats, with cross-backend parity coverage
38
41
  - **Two API styles** – Use the "Simple" API (3 functions), or the full "Core"
39
42
  API for more advanced applications
40
43
  - **Batch folder operations** – Scan directories, process multiple files, find
@@ -48,8 +51,8 @@ TagLib-Wasm is the **universal tagging library for TypeScript/JavaScript**
48
51
  npm install taglib-wasm
49
52
  ```
50
53
 
51
- > **Note:** Requires Node.js v22.6.0 or higher for WASI and WebAssembly
52
- > exception handling support. To consume the package as TypeScript source
54
+ > **Note:** Requires Node.js v24 or higher (the Active LTS line) for WASI and
55
+ > WebAssembly exception handling support. To consume the package as TypeScript source
53
56
  > (e.g., via `tsx`), see the
54
57
  > [installation guide](https://charleswiltgen.github.io/TagLib-Wasm/guide/installation.html).
55
58
 
@@ -1443,6 +1443,26 @@ var init_specialized_properties = __esm({
1443
1443
  vorbis: "ITUNNORM",
1444
1444
  mp4: "----:com.apple.iTunes:iTunNORM"
1445
1445
  }
1446
+ },
1447
+ /**
1448
+ * Apple gapless-playback data (encoder delay, padding, original sample count).
1449
+ *
1450
+ * Exists so the two Apple freeform atoms are presented consistently: without
1451
+ * it, `properties()` answered `appleSoundCheck` for iTunNORM but the raw
1452
+ * uppercased `ITUNSMPB` for its sibling, because only one had an alias. The
1453
+ * name mirrors `appleSoundCheck` — vendor-prefixed, feature-named rather than
1454
+ * atom-named (tuneup-ibo).
1455
+ */
1456
+ appleGaplessInfo: {
1457
+ key: "ITUNSMPB",
1458
+ description: "Apple gapless playback data (encoder delay and padding)",
1459
+ type: "string",
1460
+ supportedFormats: ["ID3v2", "MP4", "Vorbis"],
1461
+ mappings: {
1462
+ id3v2: { frame: "TXXX", description: "iTunSMPB" },
1463
+ vorbis: "ITUNSMPB",
1464
+ mp4: "----:com.apple.iTunes:iTunSMPB"
1465
+ }
1446
1466
  }
1447
1467
  };
1448
1468
  }
@@ -1452,6 +1472,17 @@ var init_specialized_properties = __esm({
1452
1472
  function toTagLibKey(key) {
1453
1473
  return _toTagLib[key] ?? key;
1454
1474
  }
1475
+ function mp4AtomWireKey(atom) {
1476
+ return _mp4AtomToWireKey[atom];
1477
+ }
1478
+ function mp4FreeformAtomNames(wireKeys) {
1479
+ const names = [];
1480
+ for (const key of wireKeys) {
1481
+ const atom = _mp4FreeformAtoms[key];
1482
+ if (atom !== void 0) names.push(atom);
1483
+ }
1484
+ return names;
1485
+ }
1455
1486
  function fromTagLibKey(key) {
1456
1487
  return _fromTagLib[key] ?? key;
1457
1488
  }
@@ -1462,7 +1493,7 @@ function remapKeysFromTagLib(obj) {
1462
1493
  }
1463
1494
  return result;
1464
1495
  }
1465
- var PROPERTIES, _toTagLib, _fromTagLib;
1496
+ var PROPERTIES, _toTagLib, _fromTagLib, _mp4FreeformAtoms, _mp4AtomToWireKey;
1466
1497
  var init_properties = __esm({
1467
1498
  "src/constants/properties.ts"() {
1468
1499
  "use strict";
@@ -1486,6 +1517,129 @@ var init_properties = __esm({
1486
1517
  _toTagLib["year"] = "DATE";
1487
1518
  _toTagLib["track"] = "TRACKNUMBER";
1488
1519
  _fromTagLib["disc"] = "discNumber";
1520
+ _mp4FreeformAtoms = {};
1521
+ for (const meta of Object.values(PROPERTIES)) {
1522
+ const { key, mappings } = meta;
1523
+ const atom = mappings?.mp4;
1524
+ if (typeof atom !== "string" || !atom.startsWith("----:")) continue;
1525
+ const bare = atom.slice(atom.lastIndexOf(":") + 1);
1526
+ if (bare === bare.toUpperCase()) continue;
1527
+ _mp4FreeformAtoms[key] = atom;
1528
+ const readKey = bare.toUpperCase();
1529
+ const camel = _fromTagLib[key];
1530
+ if (camel !== void 0 && _fromTagLib[readKey] === void 0) {
1531
+ _fromTagLib[readKey] = camel;
1532
+ }
1533
+ }
1534
+ _mp4AtomToWireKey = {};
1535
+ for (const meta of Object.values(PROPERTIES)) {
1536
+ const { key, mappings } = meta;
1537
+ const atom = mappings?.mp4;
1538
+ if (typeof atom !== "string" || atom.startsWith("----:")) continue;
1539
+ if (_mp4AtomToWireKey[atom] === void 0) _mp4AtomToWireKey[atom] = key;
1540
+ }
1541
+ }
1542
+ });
1543
+
1544
+ // src/taglib/mp4-item-names.ts
1545
+ var MP4_ITEM_NAMES_KEY;
1546
+ var init_mp4_item_names = __esm({
1547
+ "src/taglib/mp4-item-names.ts"() {
1548
+ "use strict";
1549
+ MP4_ITEM_NAMES_KEY = "_mp4ItemNames";
1550
+ }
1551
+ });
1552
+
1553
+ // src/taglib/mutable-tag-impl.ts
1554
+ function buildMutableTag(handle) {
1555
+ let data = handle.getTagData();
1556
+ const tag = {
1557
+ get title() {
1558
+ return data.title;
1559
+ },
1560
+ get artist() {
1561
+ return data.artist;
1562
+ },
1563
+ get album() {
1564
+ return data.album;
1565
+ },
1566
+ get comment() {
1567
+ return data.comment;
1568
+ },
1569
+ get genre() {
1570
+ return data.genre;
1571
+ },
1572
+ get year() {
1573
+ return data.year;
1574
+ },
1575
+ get track() {
1576
+ return data.track;
1577
+ },
1578
+ get date() {
1579
+ return handle.getProperty("DATE") || void 0;
1580
+ },
1581
+ setTitle: (value) => {
1582
+ handle.setTagData({ title: value });
1583
+ data = handle.getTagData();
1584
+ return tag;
1585
+ },
1586
+ setArtist: (value) => {
1587
+ handle.setTagData({ artist: value });
1588
+ data = handle.getTagData();
1589
+ return tag;
1590
+ },
1591
+ setAlbum: (value) => {
1592
+ handle.setTagData({ album: value });
1593
+ data = handle.getTagData();
1594
+ return tag;
1595
+ },
1596
+ setComment: (value) => {
1597
+ handle.setTagData({ comment: value });
1598
+ data = handle.getTagData();
1599
+ return tag;
1600
+ },
1601
+ setGenre: (value) => {
1602
+ handle.setTagData({ genre: value });
1603
+ data = handle.getTagData();
1604
+ return tag;
1605
+ },
1606
+ setYear: (value) => {
1607
+ handle.setTagData({ year: value });
1608
+ data = handle.getTagData();
1609
+ return tag;
1610
+ },
1611
+ setTrack: (value) => {
1612
+ const existing = value > 0 ? handle.getProperty(RAW_TRACK_WIRE_KEY) : "";
1613
+ const slash = existing.indexOf("/");
1614
+ if (slash !== -1) {
1615
+ handle.setProperty(
1616
+ RAW_TRACK_WIRE_KEY,
1617
+ `${value}${existing.slice(slash)}`
1618
+ );
1619
+ } else {
1620
+ handle.setTagData({ track: value });
1621
+ }
1622
+ data = handle.getTagData();
1623
+ return tag;
1624
+ },
1625
+ setDate: (value) => {
1626
+ if (value === "") {
1627
+ handle.setTagData({ year: 0 });
1628
+ } else {
1629
+ handle.setProperty("DATE", value);
1630
+ }
1631
+ data = handle.getTagData();
1632
+ return tag;
1633
+ }
1634
+ };
1635
+ return tag;
1636
+ }
1637
+ var RAW_TRACK_WIRE_KEY;
1638
+ var init_mutable_tag_impl = __esm({
1639
+ "src/taglib/mutable-tag-impl.ts"() {
1640
+ "use strict";
1641
+ init_properties();
1642
+ RAW_TRACK_WIRE_KEY = toTagLibKey("trackNumber");
1489
1643
  }
1490
1644
  });
1491
1645
 
@@ -1496,6 +1650,8 @@ var init_audio_file_base = __esm({
1496
1650
  "use strict";
1497
1651
  init_properties();
1498
1652
  init_errors2();
1653
+ init_mp4_item_names();
1654
+ init_mutable_tag_impl();
1499
1655
  LYRICS_PROPERTY_KEY = "lyrics";
1500
1656
  LYRICS_WIRE_KEY = toTagLibKey(LYRICS_PROPERTY_KEY);
1501
1657
  EMPTY_KEY_SET = /* @__PURE__ */ new Set();
@@ -1546,79 +1702,7 @@ var init_audio_file_base = __esm({
1546
1702
  return this.getFormat() === format;
1547
1703
  }
1548
1704
  tag() {
1549
- const handle = this.handle;
1550
- let data = handle.getTagData();
1551
- const tag = {
1552
- get title() {
1553
- return data.title;
1554
- },
1555
- get artist() {
1556
- return data.artist;
1557
- },
1558
- get album() {
1559
- return data.album;
1560
- },
1561
- get comment() {
1562
- return data.comment;
1563
- },
1564
- get genre() {
1565
- return data.genre;
1566
- },
1567
- get year() {
1568
- return data.year;
1569
- },
1570
- get track() {
1571
- return data.track;
1572
- },
1573
- get date() {
1574
- return handle.getProperty("DATE") || void 0;
1575
- },
1576
- setTitle: (value) => {
1577
- handle.setTagData({ title: value });
1578
- data = handle.getTagData();
1579
- return tag;
1580
- },
1581
- setArtist: (value) => {
1582
- handle.setTagData({ artist: value });
1583
- data = handle.getTagData();
1584
- return tag;
1585
- },
1586
- setAlbum: (value) => {
1587
- handle.setTagData({ album: value });
1588
- data = handle.getTagData();
1589
- return tag;
1590
- },
1591
- setComment: (value) => {
1592
- handle.setTagData({ comment: value });
1593
- data = handle.getTagData();
1594
- return tag;
1595
- },
1596
- setGenre: (value) => {
1597
- handle.setTagData({ genre: value });
1598
- data = handle.getTagData();
1599
- return tag;
1600
- },
1601
- setYear: (value) => {
1602
- handle.setTagData({ year: value });
1603
- data = handle.getTagData();
1604
- return tag;
1605
- },
1606
- setTrack: (value) => {
1607
- handle.setTagData({ track: value });
1608
- data = handle.getTagData();
1609
- return tag;
1610
- },
1611
- setDate: (value) => {
1612
- if (value === "") {
1613
- handle.setTagData({ year: 0 });
1614
- } else {
1615
- handle.setProperty("DATE", value);
1616
- }
1617
- data = handle.getTagData();
1618
- return tag;
1619
- }
1620
- };
1621
- return tag;
1705
+ return buildMutableTag(this.handle);
1622
1706
  }
1623
1707
  audioProperties() {
1624
1708
  if (!this.cachedAudioProperties) {
@@ -1640,14 +1724,29 @@ var init_audio_file_base = __esm({
1640
1724
  const existing = this.handle.getProperties()[LYRICS_WIRE_KEY];
1641
1725
  if (existing !== void 0) translated[LYRICS_WIRE_KEY] = existing;
1642
1726
  }
1727
+ const atomNames = mp4FreeformAtomNames(Object.keys(translated));
1728
+ if (atomNames.length > 0) translated[MP4_ITEM_NAMES_KEY] = atomNames;
1643
1729
  this.handle.setProperties(translated);
1644
1730
  }
1645
1731
  getProperty(key) {
1646
1732
  const value = this.handle.getProperty(toTagLibKey(key));
1647
- return value === "" ? void 0 : value;
1733
+ if (value !== "") return value;
1734
+ if (!this.isMP4()) return void 0;
1735
+ const remapped = this.properties()[key]?.[0];
1736
+ return remapped === "" ? void 0 : remapped;
1648
1737
  }
1649
1738
  setProperty(key, value) {
1650
- this.handle.setProperty(toTagLibKey(key), value);
1739
+ const wireKey = toTagLibKey(key);
1740
+ const atomNames = mp4FreeformAtomNames([wireKey]);
1741
+ if (atomNames.length > 0 && this.isMP4()) {
1742
+ this.handle.setProperties({
1743
+ ...this.handle.getProperties(),
1744
+ [wireKey]: [value],
1745
+ [MP4_ITEM_NAMES_KEY]: atomNames
1746
+ });
1747
+ return;
1748
+ }
1749
+ this.handle.setProperty(wireKey, value);
1651
1750
  }
1652
1751
  isMP4() {
1653
1752
  return this.handle.isMP4();
@@ -1656,13 +1755,19 @@ var init_audio_file_base = __esm({
1656
1755
  if (!this.isMP4()) {
1657
1756
  throw new UnsupportedFormatError(this.getFormat(), ["MP4", "M4A"]);
1658
1757
  }
1659
- const value = this.handle.getMP4Item(key);
1758
+ const wireKey = mp4AtomWireKey(key);
1759
+ const value = wireKey !== void 0 ? this.handle.getProperty(wireKey) : this.handle.getMP4Item(key);
1660
1760
  return value === "" ? void 0 : value;
1661
1761
  }
1662
1762
  setMP4Item(key, value) {
1663
1763
  if (!this.isMP4()) {
1664
1764
  throw new UnsupportedFormatError(this.getFormat(), ["MP4", "M4A"]);
1665
1765
  }
1766
+ const wireKey = mp4AtomWireKey(key);
1767
+ if (wireKey !== void 0) {
1768
+ this.handle.setProperty(wireKey, value);
1769
+ return;
1770
+ }
1666
1771
  this.handle.setMP4Item(key, value);
1667
1772
  }
1668
1773
  removeMP4Item(key) {
@@ -1945,6 +2050,8 @@ function copyEditedState(target, source, sourceComplete, deletedKeys) {
1945
2050
  target.setTagData(source.getTagData());
1946
2051
  const merged = sourceComplete ? source.getProperties() : { ...target.getProperties(), ...source.getProperties() };
1947
2052
  for (const key of deletedKeys) delete merged[key];
2053
+ const atomNames = mp4FreeformAtomNames(Object.keys(merged));
2054
+ if (atomNames.length > 0) merged[MP4_ITEM_NAMES_KEY] = atomNames;
1948
2055
  target.setProperties(merged);
1949
2056
  copyExtraState(target, source, sourceComplete);
1950
2057
  }
@@ -1980,6 +2087,8 @@ var init_save_reconstruct = __esm({
1980
2087
  init_write();
1981
2088
  init_embind_adapter();
1982
2089
  init_extra_state_registry();
2090
+ init_properties();
2091
+ init_mp4_item_names();
1983
2092
  }
1984
2093
  });
1985
2094
 
@@ -2285,11 +2394,25 @@ var init_load_audio_data = __esm({
2285
2394
  }
2286
2395
  });
2287
2396
 
2288
- // src/utils/tag-mapping.ts
2289
- function parseNumeric(value) {
2290
- const parsed = Number.parseInt(value, 10);
2397
+ // src/utils/mirror-fields.ts
2398
+ function parseLeadingInt(raw) {
2399
+ const parsed = Number.parseInt(raw, 10);
2291
2400
  return Number.isNaN(parsed) ? void 0 : parsed;
2292
2401
  }
2402
+ var MIRROR_FIELDS, DATE_MIRROR, TRACK_MIRROR;
2403
+ var init_mirror_fields = __esm({
2404
+ "src/utils/mirror-fields.ts"() {
2405
+ "use strict";
2406
+ MIRROR_FIELDS = [
2407
+ { raw: "date", numeric: "year" },
2408
+ { raw: "trackNumber", numeric: "track" }
2409
+ ];
2410
+ DATE_MIRROR = MIRROR_FIELDS[0];
2411
+ TRACK_MIRROR = MIRROR_FIELDS[1];
2412
+ }
2413
+ });
2414
+
2415
+ // src/utils/tag-mapping.ts
2293
2416
  function readExtendedTag(audioFile) {
2294
2417
  const tag = mapPropertiesToExtendedTag(audioFile.properties());
2295
2418
  const pictures = audioFile.getPictures();
@@ -2314,21 +2437,35 @@ function mapPropertiesToExtendedTag(props) {
2314
2437
  const values = props[propKey];
2315
2438
  if (!values || values.length === 0) continue;
2316
2439
  if (tagField === "year" || tagField === "track") {
2317
- const num = parseNumeric(values[0]);
2440
+ const num = parseLeadingInt(values[0]);
2318
2441
  if (num !== void 0) tag[tagField] = num;
2319
2442
  if (propKey === "date") {
2320
2443
  tag.date = values.length === 1 ? values[0] : values;
2321
2444
  }
2445
+ if (propKey === "trackNumber") {
2446
+ tag.trackNumber = values.length === 1 ? values[0] : values;
2447
+ }
2322
2448
  } else {
2323
2449
  tag[tagField] = values;
2324
2450
  }
2325
2451
  }
2452
+ for (const [propKey, totalField] of [
2453
+ ["trackNumber", "totalTracks"],
2454
+ ["discNumber", "totalDiscs"]
2455
+ ]) {
2456
+ if (tag[totalField] !== void 0) continue;
2457
+ const raw = props[propKey]?.[0];
2458
+ const slash = raw?.indexOf("/") ?? -1;
2459
+ if (raw === void 0 || slash === -1) continue;
2460
+ const total = parseLeadingInt(raw.slice(slash + 1));
2461
+ if (total !== void 0) tag[totalField] = total;
2462
+ }
2326
2463
  for (const [key, values] of Object.entries(props)) {
2327
2464
  if (BASIC_PROPERTY_KEYS[key]) continue;
2328
2465
  if (!values || values.length === 0) continue;
2329
2466
  const camelKey = fromTagLibKey(key);
2330
2467
  if (NUMERIC_FIELDS.has(camelKey)) {
2331
- const num = parseNumeric(values[0]);
2468
+ const num = parseLeadingInt(values[0]);
2332
2469
  if (num !== void 0) tag[camelKey] = num;
2333
2470
  } else if (camelKey === "compilation") {
2334
2471
  tag[camelKey] = values[0] === "1";
@@ -2341,8 +2478,29 @@ function mapPropertiesToExtendedTag(props) {
2341
2478
  function mergeTagUpdates(file, tags) {
2342
2479
  const currentProps = file.properties();
2343
2480
  const newProps = normalizeTagInput(tags);
2481
+ reconcilePairFields(currentProps, newProps, tags);
2344
2482
  file.setProperties({ ...currentProps, ...newProps });
2345
2483
  }
2484
+ function reconcilePairFields(currentProps, newProps, input) {
2485
+ const pairs = [
2486
+ ["trackNumber", "totalTracks", "track"],
2487
+ ["discNumber", "totalDiscs", "discNumber"]
2488
+ ];
2489
+ for (const [rawKey, totalKey, numericKey] of pairs) {
2490
+ const incoming = newProps[rawKey]?.[0];
2491
+ if (incoming?.includes("/")) {
2492
+ newProps[totalKey] = [];
2493
+ continue;
2494
+ }
2495
+ const numericOnly = input[numericKey] !== void 0 && input[rawKey] === void 0;
2496
+ if (!numericOnly || incoming === void 0) continue;
2497
+ const existing = currentProps[rawKey]?.[0];
2498
+ const slash = existing?.indexOf("/") ?? -1;
2499
+ if (existing !== void 0 && slash !== -1) {
2500
+ newProps[rawKey] = [`${incoming}${existing.slice(slash)}`];
2501
+ }
2502
+ }
2503
+ }
2346
2504
  function normalizeTagInput(input) {
2347
2505
  const props = {};
2348
2506
  for (const field of [
@@ -2365,6 +2523,9 @@ function normalizeTagInput(input) {
2365
2523
  if (input.track !== void 0) {
2366
2524
  props.trackNumber = [String(input.track)];
2367
2525
  }
2526
+ if (input.trackNumber !== void 0) {
2527
+ props.trackNumber = Array.isArray(input.trackNumber) ? input.trackNumber : [input.trackNumber];
2528
+ }
2368
2529
  for (const [field, val] of Object.entries(input)) {
2369
2530
  if (BASIC_FIELDS.has(field) || val === void 0) continue;
2370
2531
  if (field === "compilation") {
@@ -2377,6 +2538,12 @@ function normalizeTagInput(input) {
2377
2538
  props[field] = val;
2378
2539
  }
2379
2540
  }
2541
+ for (const [rawField, totalField] of [
2542
+ ["trackNumber", "totalTracks"],
2543
+ ["discNumber", "totalDiscs"]
2544
+ ]) {
2545
+ if (props[rawField]?.[0]?.includes("/")) delete props[totalField];
2546
+ }
2380
2547
  return props;
2381
2548
  }
2382
2549
  var BASIC_PROPERTY_KEYS, BASIC_FIELDS, NUMERIC_FIELDS;
@@ -2384,6 +2551,7 @@ var init_tag_mapping = __esm({
2384
2551
  "src/utils/tag-mapping.ts"() {
2385
2552
  "use strict";
2386
2553
  init_properties();
2554
+ init_mirror_fields();
2387
2555
  BASIC_PROPERTY_KEYS = {
2388
2556
  title: "title",
2389
2557
  artist: "artist",
@@ -2401,7 +2569,8 @@ var init_tag_mapping = __esm({
2401
2569
  "genre",
2402
2570
  "year",
2403
2571
  "date",
2404
- "track"
2572
+ "track",
2573
+ "trackNumber"
2405
2574
  ]);
2406
2575
  NUMERIC_FIELDS = /* @__PURE__ */ new Set([
2407
2576
  "year",
@@ -2419,7 +2588,7 @@ var VERSION;
2419
2588
  var init_version = __esm({
2420
2589
  "src/version.ts"() {
2421
2590
  "use strict";
2422
- VERSION = "1.5.3";
2591
+ VERSION = "1.6.0";
2423
2592
  }
2424
2593
  });
2425
2594
 
@@ -3101,8 +3270,9 @@ var Tags = {
3101
3270
  InvolvedPeople: "involvedPeople",
3102
3271
  Encoding: "encoding"
3103
3272
  };
3273
+ var VALID_TAG_NAMES = new Set(Object.values(Tags));
3104
3274
  function isValidTagName(name) {
3105
- return Object.values(Tags).includes(name);
3275
+ return VALID_TAG_NAMES.has(name);
3106
3276
  }
3107
3277
  function getAllTagNames() {
3108
3278
  return Object.values(Tags);