taglib-wasm 2.2.1 → 2.2.3

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 (88) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +5 -3
  3. package/dist/disc-folder.d.ts +3 -1
  4. package/dist/disc-folder.d.ts.map +1 -1
  5. package/dist/disc-folder.js +187 -152
  6. package/dist/index.browser.js +17 -10
  7. package/dist/index.d.ts +1 -1
  8. package/dist/simple.browser.js +13 -8
  9. package/dist/src/constants/properties.d.ts +2 -2
  10. package/dist/src/constants/specialized-properties.d.ts +2 -2
  11. package/dist/src/constants/specialized-properties.js +2 -2
  12. package/dist/src/folder-api/album-corroborate.d.ts +13 -0
  13. package/dist/src/folder-api/album-corroborate.d.ts.map +1 -0
  14. package/dist/src/folder-api/album-corroborate.js +36 -0
  15. package/dist/src/folder-api/album-discs.d.ts +13 -0
  16. package/dist/src/folder-api/album-discs.d.ts.map +1 -0
  17. package/dist/src/folder-api/album-discs.js +122 -0
  18. package/dist/src/folder-api/album-grouping.d.ts +24 -0
  19. package/dist/src/folder-api/album-grouping.d.ts.map +1 -0
  20. package/dist/src/folder-api/album-grouping.js +118 -0
  21. package/dist/src/folder-api/album-identity.d.ts +27 -0
  22. package/dist/src/folder-api/album-identity.d.ts.map +1 -0
  23. package/dist/src/folder-api/album-identity.js +151 -0
  24. package/dist/src/folder-api/{group-albums.d.ts → album-types.d.ts} +55 -26
  25. package/dist/src/folder-api/album-types.d.ts.map +1 -0
  26. package/dist/src/folder-api/album-types.js +0 -0
  27. package/dist/src/folder-api/folder-disc.d.ts +24 -0
  28. package/dist/src/folder-api/folder-disc.d.ts.map +1 -0
  29. package/dist/src/folder-api/folder-disc.js +169 -0
  30. package/dist/src/folder-api/index.d.ts +3 -1
  31. package/dist/src/folder-api/index.d.ts.map +1 -1
  32. package/dist/src/folder-api/index.js +2 -4
  33. package/dist/src/folder-api/scan-for-albums.d.ts +1 -1
  34. package/dist/src/folder-api/scan-for-albums.d.ts.map +1 -1
  35. package/dist/src/folder-api/scan-for-albums.js +1 -3
  36. package/dist/src/runtime/detector.d.ts +16 -1
  37. package/dist/src/runtime/detector.d.ts.map +1 -1
  38. package/dist/src/runtime/detector.js +58 -52
  39. package/dist/src/runtime/platform-io.d.ts +8 -0
  40. package/dist/src/runtime/platform-io.d.ts.map +1 -1
  41. package/dist/src/runtime/platform-io.js +11 -11
  42. package/dist/src/runtime/unified-loader/module-selection.d.ts +2 -2
  43. package/dist/src/runtime/unified-loader/module-selection.d.ts.map +1 -1
  44. package/dist/src/runtime/unified-loader/module-selection.js +3 -5
  45. package/dist/src/runtime/wasi-adapter/audio-properties.d.ts +20 -0
  46. package/dist/src/runtime/wasi-adapter/audio-properties.d.ts.map +1 -0
  47. package/dist/src/runtime/wasi-adapter/audio-properties.js +85 -0
  48. package/dist/src/runtime/wasi-adapter/file-handle.d.ts +5 -19
  49. package/dist/src/runtime/wasi-adapter/file-handle.d.ts.map +1 -1
  50. package/dist/src/runtime/wasi-adapter/file-handle.js +67 -389
  51. package/dist/src/runtime/wasi-adapter/handle-state.d.ts +28 -0
  52. package/dist/src/runtime/wasi-adapter/handle-state.d.ts.map +1 -0
  53. package/dist/src/runtime/wasi-adapter/handle-state.js +58 -0
  54. package/dist/src/runtime/wasi-adapter/mp4-items.d.ts +34 -0
  55. package/dist/src/runtime/wasi-adapter/mp4-items.d.ts.map +1 -0
  56. package/dist/src/runtime/wasi-adapter/mp4-items.js +80 -0
  57. package/dist/src/runtime/wasi-adapter/property-surface.d.ts +25 -0
  58. package/dist/src/runtime/wasi-adapter/property-surface.d.ts.map +1 -0
  59. package/dist/src/runtime/wasi-adapter/property-surface.js +104 -0
  60. package/dist/src/runtime/wasi-adapter/structured-fields.d.ts +44 -0
  61. package/dist/src/runtime/wasi-adapter/structured-fields.d.ts.map +1 -0
  62. package/dist/src/runtime/wasi-adapter/structured-fields.js +139 -0
  63. package/dist/src/runtime/wasi-adapter/tag-keys.d.ts +28 -0
  64. package/dist/src/runtime/wasi-adapter/tag-keys.d.ts.map +1 -0
  65. package/dist/src/runtime/wasi-adapter/tag-keys.js +46 -0
  66. package/dist/src/taglib/audio-file-impl.d.ts.map +1 -1
  67. package/dist/src/taglib/audio-file-impl.js +2 -2
  68. package/dist/src/taglib/embind-adapter.d.ts.map +1 -1
  69. package/dist/src/taglib/embind-adapter.js +4 -1
  70. package/dist/src/taglib/id3v2-frames.d.ts.map +1 -1
  71. package/dist/src/taglib/id3v2-frames.js +2 -2
  72. package/dist/src/types/audio-formats.d.ts +5 -5
  73. package/dist/src/types/audio-formats.d.ts.map +1 -1
  74. package/dist/src/types/format-property-keys.d.ts +1 -1
  75. package/dist/src/types/format-property-keys.d.ts.map +1 -1
  76. package/dist/src/types/tags.d.ts +8 -1
  77. package/dist/src/types/tags.d.ts.map +1 -1
  78. package/dist/src/utils/tag-mapping.d.ts.map +1 -1
  79. package/dist/src/utils/tag-mapping.js +2 -0
  80. package/dist/src/version.d.ts +1 -1
  81. package/dist/src/version.js +1 -1
  82. package/dist/src/web-utils/data-url.d.ts.map +1 -1
  83. package/dist/src/web-utils/data-url.js +4 -2
  84. package/dist/taglib-wasi.wasm +0 -0
  85. package/dist/taglib-web.wasm +0 -0
  86. package/package.json +8 -13
  87. package/dist/src/folder-api/group-albums.d.ts.map +0 -1
  88. package/dist/src/folder-api/group-albums.js +0 -544
package/LICENSE CHANGED
@@ -42,7 +42,7 @@ The WebAssembly binaries are subject to LGPL requirements. This means:
42
42
  To rebuild the WebAssembly binaries with a modified TagLib:
43
43
  1. Modify the TagLib source in lib/taglib/
44
44
  2. Run: npm run build:wasm (builds both backends; requires the
45
- Emscripten SDK and WASI SDK 33 — see CONTRIBUTING.md)
45
+ Emscripten SDK and WASI SDK 34 — see CONTRIBUTING.md)
46
46
  3. The new binaries will be in build/taglib-web.wasm and
47
47
  build/taglib-wasi.wasm
48
48
 
package/README.md CHANGED
@@ -386,7 +386,7 @@ import { readProperties } from "taglib-wasm/simple";
386
386
  const props = await readProperties("song.m4a");
387
387
 
388
388
  console.log(props.containerFormat); // "MP4" (container format)
389
- console.log(props.codec); // "AAC" or "ALAC" (compressed media format)
389
+ console.log(props.codec); // "AAC", "ALAC", "AC-3", … (compressed media format)
390
390
  console.log(props.isLossless); // false for AAC, true for ALAC
391
391
  console.log(props.bitsPerSample); // 16 for most formats
392
392
  console.log(props.bitrate); // 256 (kbps)
@@ -407,7 +407,7 @@ Container format vs Codec:
407
407
 
408
408
  Supported formats:
409
409
 
410
- - **MP4 container** (.mp4, .m4a) – Can contain AAC (lossy) or ALAC (lossless)
410
+ - **MP4 container** (.mp4, .m4a) – Can contain AAC, ALAC, AC-3, E-AC-3, DTS, FLAC, or Opus
411
411
  - **OGG container** (.ogg) – Can contain Vorbis, Opus, FLAC, or Speex
412
412
  - **MP3** – Both container and codec (lossy)
413
413
  - **FLAC** – Both container and codec (lossless)
@@ -444,9 +444,11 @@ Supported formats:
444
444
  `taglib-wasm` is designed to support all formats supported by TagLib:
445
445
 
446
446
  - **.mp3** – ID3v2 and ID3v1 tags
447
+ - **.aac** – ADTS / raw AAC streams (ID3v2 metadata, read through the MPEG reader)
447
448
  - **.m4a/.mp4** – MPEG-4/AAC metadata for AAC and Apple Lossless audio
448
449
  - **.flac** – Vorbis comments and audio properties (plus BWF `bext`/iXML)
449
- - **.ogg** – Ogg Vorbis format with full metadata support
450
+ - **.ogg** – Ogg container: Vorbis with full metadata support, plus FLAC-in-Ogg
451
+ and Speex
450
452
  - **.wav** – INFO chunk metadata, plus BWF `bext` and iXML
451
453
  - **Additional formats** – Opus, APE, MPC, WavPack, TrueAudio, AIFF, WMA, and
452
454
  more
@@ -14,6 +14,8 @@
14
14
  * discFolderInfo("CD1"); // { kind: "exact", number: 1, ... }
15
15
  * ```
16
16
  */
17
- export * from "./src/folder-api/group-albums.js";
17
+ export { discFolderInfo } from "./src/folder-api/folder-disc.js";
18
+ export { groupAlbums } from "./src/folder-api/album-grouping.js";
19
+ export type { AlbumDisc, AlbumGroup, AlbumGroupingResult, AlbumGroupItem, AlbumGroupKey, DiscConfidence, DiscFolderInfo, GroupAlbumsOptions, } from "./src/folder-api/album-types.js";
18
20
  export type { FolderScanResult } from "./src/folder-api/types.js";
19
21
  //# sourceMappingURL=disc-folder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"disc-folder.d.ts","sourceRoot":"","sources":["../disc-folder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,kCAAkC,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"disc-folder.d.ts","sourceRoot":"","sources":["../disc-folder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AACjE,YAAY,EACV,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -1,23 +1,4 @@
1
- // src/utils/path.ts
2
- function basename(path) {
3
- const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
4
- return i === -1 ? path : path.slice(i + 1);
5
- }
6
-
7
- // src/folder-api/group-albums.ts
8
- function discFolderInfo(name) {
9
- const parse = parseDiscName(name);
10
- if (!parse) return void 0;
11
- return {
12
- kind: parse.kind,
13
- gated: parse.gated,
14
- number: parse.number,
15
- total: parse.total,
16
- title: parse.title,
17
- discTitle: parse.discTitle,
18
- confidence: parse.gated ? "low" : baseConfidence(parse)
19
- };
20
- }
1
+ // src/folder-api/folder-disc.ts
21
2
  function dirname(path) {
22
3
  const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
23
4
  return i === -1 ? path : path.slice(0, i);
@@ -168,6 +149,32 @@ function baseConfidence(parse) {
168
149
  return "low";
169
150
  }
170
151
  }
152
+ function discFolderInfo(name) {
153
+ const parse = parseDiscName(name);
154
+ if (!parse) return void 0;
155
+ return {
156
+ kind: parse.kind,
157
+ gated: parse.gated,
158
+ number: parse.number,
159
+ total: parse.total,
160
+ title: parse.title,
161
+ discTitle: parse.discTitle,
162
+ confidence: parse.gated ? "low" : baseConfidence(parse)
163
+ };
164
+ }
165
+
166
+ // src/utils/path.ts
167
+ function basename(path) {
168
+ const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
169
+ return i === -1 ? path : path.slice(i + 1);
170
+ }
171
+
172
+ // src/folder-api/album-corroborate.ts
173
+ var CONFIDENCE_RANK = {
174
+ low: 0,
175
+ medium: 1,
176
+ high: 2
177
+ };
171
178
  function corroborated(parse, siblings) {
172
179
  if (!parse.gated && parse.kind === "exact") return true;
173
180
  if (siblings.length === 0) return false;
@@ -193,13 +200,8 @@ function corroborated(parse, siblings) {
193
200
  }
194
201
  return false;
195
202
  }
196
- function flatPrefix(filename) {
197
- const separated = filename.match(/^(\d{1,2})[\s._-](\d{1,2})/);
198
- if (separated) return { disc: parseInt(separated[1], 10), form: "separated" };
199
- const compact = filename.match(/^(\d)(\d{2})(?:\D|$)/);
200
- if (compact) return { disc: parseInt(compact[1], 10), form: "compact" };
201
- return void 0;
202
- }
203
+
204
+ // src/folder-api/album-identity.ts
203
205
  function normalizeKey(s) {
204
206
  return s.trim().toLowerCase().replace(/\s+/g, " ");
205
207
  }
@@ -208,134 +210,63 @@ function isGenericAlbumArtist(albumArtist) {
208
210
  const n = normalizeKey(albumArtist);
209
211
  return n === "various artists" || n === "va";
210
212
  }
211
- var CONFIDENCE_RANK = {
212
- low: 0,
213
- medium: 1,
214
- high: 2
215
- };
216
- function groupAlbums(result, options = {}) {
217
- const minRank = CONFIDENCE_RANK[options.minFolderConfidence ?? "low"];
218
- const useFlatPrefixes = options.flatDiscPrefixes ?? true;
219
- const useFolderFallback = options.folderFallback ?? true;
220
- const okItems = [];
221
- const errors = [];
222
- for (const item of result.items) {
223
- if (item.status === "ok") {
224
- okItems.push({ path: item.path, tags: item.tags, metadata: item });
225
- } else {
226
- errors.push({ path: item.path, error: item.error });
227
- }
228
- }
229
- if (okItems.length === 0) {
230
- return { albums: [], singles: [], unmatched: [], errors };
213
+ function evidenceFor(item, confirmedDiscs, flatSubdivisions2) {
214
+ const dir = dirname(item.path);
215
+ const own = confirmedDiscs.get(dir);
216
+ if (own) {
217
+ return {
218
+ folderDiscNumber: own.parse.number,
219
+ total: own.parse.total,
220
+ folderDiscTitle: own.parse.discTitle,
221
+ confidence: own.confidence,
222
+ albumDir: dirname(dir)
223
+ };
231
224
  }
232
- const scanRoot = options.scanRoot;
233
- const tree = /* @__PURE__ */ new Map();
234
- for (const item of okItems) {
235
- const dir = dirname(item.path);
236
- let node = tree.get(dir);
237
- if (!node) {
238
- node = { files: [], parent: dirname(dir) };
239
- tree.set(dir, node);
225
+ let cursor = dirname(dir);
226
+ while (cursor !== void 0) {
227
+ const entry = confirmedDiscs.get(cursor);
228
+ if (entry) {
229
+ return {
230
+ folderDiscNumber: entry.parse.number,
231
+ total: entry.parse.total,
232
+ folderDiscTitle: entry.parse.discTitle,
233
+ confidence: entry.confidence,
234
+ albumDir: dirname(cursor)
235
+ };
240
236
  }
241
- node.files.push(item);
237
+ const parent = dirname(cursor);
238
+ if (parent === cursor) break;
239
+ cursor = parent;
242
240
  }
243
- const confirmedDiscs = /* @__PURE__ */ new Map();
244
- for (const [dir, node] of tree) {
245
- if (node.files.length === 0) continue;
246
- const parse = parseDiscName(basename(dir));
247
- if (!parse) continue;
248
- const siblings = [...tree.keys()].filter(
249
- (d) => d !== dir && dirname(d) === dirname(dir)
250
- );
251
- const corroboratedBySiblings = corroborated(parse, siblings);
252
- let confidence = parse.gated ? "low" : baseConfidence(parse);
253
- if (parse.gated) {
254
- if (!corroboratedBySiblings) continue;
255
- } else if (parse.kind === "bonus" || parse.kind === "bare") {
256
- if (!corroboratedBySiblings) continue;
257
- } else if (corroboratedBySiblings) {
258
- confidence = "high";
259
- }
260
- if (CONFIDENCE_RANK[confidence] < minRank) continue;
261
- confirmedDiscs.set(dir, { parse, confidence });
241
+ const flat = flatSubdivisions2.get(dir)?.get(item.path);
242
+ if (flat !== void 0) {
243
+ return {
244
+ folderDiscNumber: flat,
245
+ total: void 0,
246
+ folderDiscTitle: void 0,
247
+ confidence: "medium",
248
+ albumDir: dir
249
+ };
262
250
  }
263
- const flatSubdivisions = /* @__PURE__ */ new Map();
264
- if (useFlatPrefixes) {
265
- for (const [dir, node] of tree) {
266
- if (node.files.length === 0 || confirmedDiscs.has(dir)) continue;
267
- const byFile = /* @__PURE__ */ new Map();
268
- const forms = /* @__PURE__ */ new Set();
269
- for (const item of node.files) {
270
- const prefix = flatPrefix(basename(item.path));
271
- if (prefix) {
272
- byFile.set(item.path, prefix.disc);
273
- forms.add(prefix.form);
274
- }
275
- }
276
- if (byFile.size < 2) continue;
277
- const distinct = [...new Set(byFile.values())].sort((a, b) => a - b);
278
- const allCompact = forms.size === 1 && forms.has("compact");
279
- const plausible = allCompact ? distinct.every((d) => d >= 1 && d <= distinct.length) : true;
280
- if (plausible) flatSubdivisions.set(dir, byFile);
281
- }
251
+ return void 0;
252
+ }
253
+ function folderIdentity(dir, confirmedDiscs, scanRoot) {
254
+ const own = confirmedDiscs.get(dir);
255
+ if (!own) {
256
+ const title2 = basename(dir);
257
+ return title2 ? { title: title2, titleSource: dir } : void 0;
258
+ }
259
+ if (own.parse.title !== void 0) {
260
+ return { title: own.parse.title, titleSource: dirname(dir) };
282
261
  }
262
+ const source = dirname(dir);
263
+ if (source === scanRoot || source === dir) return void 0;
264
+ const title = basename(source);
265
+ return title ? { title, titleSource: source } : void 0;
266
+ }
267
+ function buildAlbumIdentity(okItems, confirmedDiscs, flatSubdivisions2, options) {
283
268
  const albums = /* @__PURE__ */ new Map();
284
269
  const unmatched = [];
285
- const evidenceFor = (item) => {
286
- const dir = dirname(item.path);
287
- const own = confirmedDiscs.get(dir);
288
- if (own) {
289
- return {
290
- folderDiscNumber: own.parse.number,
291
- total: own.parse.total,
292
- folderDiscTitle: own.parse.discTitle,
293
- confidence: own.confidence,
294
- albumDir: dirname(dir)
295
- };
296
- }
297
- let cursor = dirname(dir);
298
- while (cursor !== void 0) {
299
- const entry = confirmedDiscs.get(cursor);
300
- if (entry) {
301
- return {
302
- folderDiscNumber: entry.parse.number,
303
- total: entry.parse.total,
304
- folderDiscTitle: entry.parse.discTitle,
305
- confidence: entry.confidence,
306
- albumDir: dirname(cursor)
307
- };
308
- }
309
- const parent = dirname(cursor);
310
- if (parent === cursor) break;
311
- cursor = parent;
312
- }
313
- const flat = flatSubdivisions.get(dir)?.get(item.path);
314
- if (flat !== void 0) {
315
- return {
316
- folderDiscNumber: flat,
317
- total: void 0,
318
- folderDiscTitle: void 0,
319
- confidence: "medium",
320
- albumDir: dir
321
- };
322
- }
323
- return void 0;
324
- };
325
- const folderIdentity = (dir) => {
326
- const own = confirmedDiscs.get(dir);
327
- if (!own) {
328
- const title2 = basename(dir);
329
- return title2 ? { title: title2, titleSource: dir } : void 0;
330
- }
331
- if (own.parse.title !== void 0) {
332
- return { title: own.parse.title, titleSource: dirname(dir) };
333
- }
334
- const source = dirname(dir);
335
- if (source === scanRoot || source === dir) return void 0;
336
- const title = basename(source);
337
- return title ? { title, titleSource: source } : void 0;
338
- };
339
270
  for (const item of okItems) {
340
271
  const dir = dirname(item.path);
341
272
  const albumTag = item.tags.album?.[0]?.trim();
@@ -356,7 +287,7 @@ function groupAlbums(result, options = {}) {
356
287
  };
357
288
  albums.set(key2, acc2);
358
289
  }
359
- const evidence2 = evidenceFor(item);
290
+ const evidence2 = evidenceFor(item, confirmedDiscs, flatSubdivisions2);
360
291
  acc2.evidence.set(item.path, evidence2);
361
292
  acc2.items.push({
362
293
  ...item.metadata,
@@ -365,11 +296,11 @@ function groupAlbums(result, options = {}) {
365
296
  });
366
297
  continue;
367
298
  }
368
- if (!useFolderFallback) {
299
+ if (!options.useFolderFallback) {
369
300
  unmatched.push(item.metadata);
370
301
  continue;
371
302
  }
372
- const identity = folderIdentity(dir);
303
+ const identity = folderIdentity(dir, confirmedDiscs, options.scanRoot);
373
304
  if (!identity) {
374
305
  unmatched.push(item.metadata);
375
306
  continue;
@@ -388,7 +319,7 @@ function groupAlbums(result, options = {}) {
388
319
  };
389
320
  albums.set(key, acc);
390
321
  }
391
- const evidence = evidenceFor(item);
322
+ const evidence = evidenceFor(item, confirmedDiscs, flatSubdivisions2);
392
323
  acc.evidence.set(item.path, evidence);
393
324
  acc.items.push({
394
325
  ...item.metadata,
@@ -415,6 +346,11 @@ function groupAlbums(result, options = {}) {
415
346
  break;
416
347
  }
417
348
  }
349
+ return { albums, unmatched };
350
+ }
351
+
352
+ // src/folder-api/album-discs.ts
353
+ function assembleDiscs(albums) {
418
354
  const groups = [];
419
355
  for (const acc of albums.values()) {
420
356
  const dirTagCommon = /* @__PURE__ */ new Map();
@@ -530,6 +466,105 @@ function groupAlbums(result, options = {}) {
530
466
  items: sortedDiscs.flatMap((d) => d.items)
531
467
  });
532
468
  }
469
+ return groups;
470
+ }
471
+
472
+ // src/folder-api/album-grouping.ts
473
+ function flatPrefix(filename) {
474
+ const separated = filename.match(/^(\d{1,2})[\s._-](\d{1,2})/);
475
+ if (separated) return { disc: parseInt(separated[1], 10), form: "separated" };
476
+ const compact = filename.match(/^(\d)(\d{2})(?:\D|$)/);
477
+ if (compact) return { disc: parseInt(compact[1], 10), form: "compact" };
478
+ return void 0;
479
+ }
480
+ function normalizeOk(result) {
481
+ const okItems = [];
482
+ const errors = [];
483
+ for (const item of result.items) {
484
+ if (item.status === "ok") {
485
+ okItems.push({ path: item.path, tags: item.tags, metadata: item });
486
+ } else {
487
+ errors.push({ path: item.path, error: item.error });
488
+ }
489
+ }
490
+ return { okItems, errors };
491
+ }
492
+ function buildTree(okItems) {
493
+ const tree = /* @__PURE__ */ new Map();
494
+ for (const item of okItems) {
495
+ const dir = dirname(item.path);
496
+ let node = tree.get(dir);
497
+ if (!node) {
498
+ node = { files: [], parent: dirname(dir) };
499
+ tree.set(dir, node);
500
+ }
501
+ node.files.push(item);
502
+ }
503
+ return tree;
504
+ }
505
+ function confirmDiscs(tree, minRank) {
506
+ const confirmedDiscs = /* @__PURE__ */ new Map();
507
+ for (const [dir, node] of tree) {
508
+ if (node.files.length === 0) continue;
509
+ const parse = parseDiscName(basename(dir));
510
+ if (!parse) continue;
511
+ const siblings = [...tree.keys()].filter(
512
+ (d) => d !== dir && dirname(d) === dirname(dir)
513
+ );
514
+ const corroboratedBySiblings = corroborated(parse, siblings);
515
+ let confidence = parse.gated ? "low" : baseConfidence(parse);
516
+ if (parse.gated) {
517
+ if (!corroboratedBySiblings) continue;
518
+ } else if (parse.kind === "bonus" || parse.kind === "bare") {
519
+ if (!corroboratedBySiblings) continue;
520
+ } else if (corroboratedBySiblings) {
521
+ confidence = "high";
522
+ }
523
+ if (CONFIDENCE_RANK[confidence] < minRank) continue;
524
+ confirmedDiscs.set(dir, { parse, confidence });
525
+ }
526
+ return confirmedDiscs;
527
+ }
528
+ function flatSubdivisions(tree, confirmedDiscs, useFlatPrefixes) {
529
+ const flat = /* @__PURE__ */ new Map();
530
+ if (!useFlatPrefixes) return flat;
531
+ for (const [dir, node] of tree) {
532
+ if (node.files.length === 0 || confirmedDiscs.has(dir)) continue;
533
+ const byFile = /* @__PURE__ */ new Map();
534
+ const forms = /* @__PURE__ */ new Set();
535
+ for (const item of node.files) {
536
+ const prefix = flatPrefix(basename(item.path));
537
+ if (prefix) {
538
+ byFile.set(item.path, prefix.disc);
539
+ forms.add(prefix.form);
540
+ }
541
+ }
542
+ if (byFile.size < 2) continue;
543
+ const distinct = [...new Set(byFile.values())].sort((a, b) => a - b);
544
+ const allCompact = forms.size === 1 && forms.has("compact");
545
+ const plausible = allCompact ? distinct.every((d) => d >= 1 && d <= distinct.length) : true;
546
+ if (plausible) flat.set(dir, byFile);
547
+ }
548
+ return flat;
549
+ }
550
+ function groupAlbums(result, options = {}) {
551
+ const minRank = CONFIDENCE_RANK[options.minFolderConfidence ?? "low"];
552
+ const useFlatPrefixes = options.flatDiscPrefixes ?? true;
553
+ const useFolderFallback = options.folderFallback ?? true;
554
+ const { okItems, errors } = normalizeOk(result);
555
+ if (okItems.length === 0) {
556
+ return { albums: [], singles: [], unmatched: [], errors };
557
+ }
558
+ const tree = buildTree(okItems);
559
+ const confirmedDiscs = confirmDiscs(tree, minRank);
560
+ const flat = flatSubdivisions(tree, confirmedDiscs, useFlatPrefixes);
561
+ const { albums, unmatched } = buildAlbumIdentity(
562
+ okItems,
563
+ confirmedDiscs,
564
+ flat,
565
+ { scanRoot: options.scanRoot, useFolderFallback }
566
+ );
567
+ const groups = assembleDiscs(albums);
533
568
  const albumsOut = [];
534
569
  const singles = [];
535
570
  for (const group of groups) {
@@ -574,8 +574,8 @@ var init_audio_file_bwf = __esm({
574
574
 
575
575
  // src/taglib/id3v2-frames.ts
576
576
  function assertMp3(format) {
577
- if (format !== "MP3") {
578
- throw new UnsupportedFormatError(format, ["MP3"], {
577
+ if (format !== "MP3" && format !== "AAC") {
578
+ throw new UnsupportedFormatError(format, ["MP3", "AAC"], {
579
579
  operation: "id3v2Frames"
580
580
  });
581
581
  }
@@ -1371,7 +1371,7 @@ var init_specialized_properties = __esm({
1371
1371
  },
1372
1372
  releaseType: {
1373
1373
  key: "RELEASETYPE",
1374
- description: "Release type (album, single, EP, compilation, ...). Multi-value (e.g. 'album' + 'EP'). TagLib 2.3.1 translates the RELEASETYPE key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM TYPE' (TagLib's description casing), MP4 freeform atom, APEv2 MUSICBRAINZ_ALBUMTYPE, ASF 'MusicBrainz/Album Type', Vorbis/Matroska raw RELEASETYPE",
1374
+ description: "Release type (album, single, EP, compilation, ...). Multi-value (e.g. 'album' + 'EP'). TagLib 2.3.2 translates the RELEASETYPE key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM TYPE' (TagLib's description casing), MP4 freeform atom, APEv2 MUSICBRAINZ_ALBUMTYPE, ASF 'MusicBrainz/Album Type', Vorbis/Matroska raw RELEASETYPE",
1375
1375
  type: "string",
1376
1376
  supportedFormats: [
1377
1377
  "ID3v2",
@@ -1389,7 +1389,7 @@ var init_specialized_properties = __esm({
1389
1389
  },
1390
1390
  releaseCountry: {
1391
1391
  key: "RELEASECOUNTRY",
1392
- description: "Release country (ISO 3166-1 code, e.g. 'US'). TagLib 2.3.1 translates the RELEASECOUNTRY key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM RELEASE COUNTRY' (TagLib's description casing; an uppercase TXXX:RELEASECOUNTRY description also reads back), MP4 freeform atom 'MusicBrainz Album Release Country', APEv2 raw RELEASECOUNTRY, ASF 'MusicBrainz/Album Release Country', WAV ICNT, Vorbis/Matroska raw RELEASECOUNTRY",
1392
+ description: "Release country (ISO 3166-1 code, e.g. 'US'). TagLib 2.3.2 translates the RELEASECOUNTRY key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM RELEASE COUNTRY' (TagLib's description casing; an uppercase TXXX:RELEASECOUNTRY description also reads back), MP4 freeform atom 'MusicBrainz Album Release Country', APEv2 raw RELEASECOUNTRY, ASF 'MusicBrainz/Album Release Country', WAV ICNT, Vorbis/Matroska raw RELEASECOUNTRY",
1393
1393
  type: "string",
1394
1394
  supportedFormats: [
1395
1395
  "ID3v2",
@@ -2121,7 +2121,10 @@ function wrapEmbindHandle(raw) {
2121
2121
  ...containerFormat === "MP4" || containerFormat === "ASF" ? { isEncrypted: pw.isEncrypted() } : {},
2122
2122
  ...formatVersion > 0 ? { formatVersion } : {},
2123
2123
  ...isValidBitrateMode(bitrateMode) ? { bitrateMode } : {},
2124
- ...codec === "Opus" ? { outputGainDb: pw.outputGainDb() } : {}
2124
+ // OpusHead gain exists only in an Ogg Opus stream. Gating on codec
2125
+ // alone fabricated outputGainDb: 0 for MP4 tracks once the MP4 codec
2126
+ // enum was mapped (the WASI side never emitted it there) — taglib-kswf.
2127
+ ...containerFormat === "OGG" && codec === "Opus" ? { outputGainDb: pw.outputGainDb() } : {}
2125
2128
  };
2126
2129
  }
2127
2130
  };
@@ -2437,8 +2440,8 @@ var init_audio_file_impl = __esm({
2437
2440
  }
2438
2441
  setChapters(chapters, options) {
2439
2442
  const fmt = this.getFormat();
2440
- if (fmt !== "MP3" && fmt !== "MP4") {
2441
- throw new UnsupportedFormatError(fmt, ["MP3", "MP4"], {
2443
+ if (fmt !== "MP3" && fmt !== "AAC" && fmt !== "MP4") {
2444
+ throw new UnsupportedFormatError(fmt, ["MP3", "AAC", "MP4"], {
2442
2445
  operation: "setChapters"
2443
2446
  });
2444
2447
  }
@@ -2944,6 +2947,8 @@ function normalizeTagInput(input) {
2944
2947
  if (val === "explicit") props.itunesAdvisory = ["1"];
2945
2948
  else if (val === "clean") props.itunesAdvisory = ["2"];
2946
2949
  else if (val === "unspecified") props.itunesAdvisory = [];
2950
+ } else if (field === "bpm") {
2951
+ props[field] = [String(Math.trunc(val))];
2947
2952
  } else if (NUMERIC_FIELDS.has(field)) {
2948
2953
  props[field] = [String(val)];
2949
2954
  } else if (field === "r128TrackGain" || field === "r128AlbumGain") {
@@ -3004,7 +3009,7 @@ var VERSION;
3004
3009
  var init_version = __esm({
3005
3010
  "src/version.ts"() {
3006
3011
  "use strict";
3007
- VERSION = "2.2.1";
3012
+ VERSION = "2.2.3";
3008
3013
  }
3009
3014
  });
3010
3015
 
@@ -3701,9 +3706,11 @@ function propertyValue(props, key) {
3701
3706
  // src/web-utils/data-url.ts
3702
3707
  init_classes();
3703
3708
  function pictureToDataURL(picture) {
3709
+ const data = picture.data;
3710
+ const CHUNK = 32768;
3704
3711
  let binary = "";
3705
- for (const byte of picture.data) {
3706
- binary += String.fromCodePoint(byte);
3712
+ for (let i = 0; i < data.length; i += CHUNK) {
3713
+ binary += String.fromCharCode(...data.subarray(i, i + CHUNK));
3707
3714
  }
3708
3715
  const base64 = btoa(binary);
3709
3716
  return `data:${picture.mimeType};base64,${base64}`;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Main module exports for TagLib-Wasm
3
3
  *
4
- * TagLib v2.3.1 compiled to WebAssembly with TypeScript bindings
4
+ * TagLib v2.3.2 compiled to WebAssembly with TypeScript bindings
5
5
  * for universal audio metadata handling across all JavaScript runtimes.
6
6
  *
7
7
  * @module TagLib-Wasm
@@ -562,8 +562,8 @@ var init_audio_file_bwf = __esm({
562
562
 
563
563
  // src/taglib/id3v2-frames.ts
564
564
  function assertMp3(format) {
565
- if (format !== "MP3") {
566
- throw new UnsupportedFormatError(format, ["MP3"], {
565
+ if (format !== "MP3" && format !== "AAC") {
566
+ throw new UnsupportedFormatError(format, ["MP3", "AAC"], {
567
567
  operation: "id3v2Frames"
568
568
  });
569
569
  }
@@ -1303,7 +1303,7 @@ var init_specialized_properties = __esm({
1303
1303
  },
1304
1304
  releaseType: {
1305
1305
  key: "RELEASETYPE",
1306
- description: "Release type (album, single, EP, compilation, ...). Multi-value (e.g. 'album' + 'EP'). TagLib 2.3.1 translates the RELEASETYPE key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM TYPE' (TagLib's description casing), MP4 freeform atom, APEv2 MUSICBRAINZ_ALBUMTYPE, ASF 'MusicBrainz/Album Type', Vorbis/Matroska raw RELEASETYPE",
1306
+ description: "Release type (album, single, EP, compilation, ...). Multi-value (e.g. 'album' + 'EP'). TagLib 2.3.2 translates the RELEASETYPE key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM TYPE' (TagLib's description casing), MP4 freeform atom, APEv2 MUSICBRAINZ_ALBUMTYPE, ASF 'MusicBrainz/Album Type', Vorbis/Matroska raw RELEASETYPE",
1307
1307
  type: "string",
1308
1308
  supportedFormats: [
1309
1309
  "ID3v2",
@@ -1321,7 +1321,7 @@ var init_specialized_properties = __esm({
1321
1321
  },
1322
1322
  releaseCountry: {
1323
1323
  key: "RELEASECOUNTRY",
1324
- description: "Release country (ISO 3166-1 code, e.g. 'US'). TagLib 2.3.1 translates the RELEASECOUNTRY key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM RELEASE COUNTRY' (TagLib's description casing; an uppercase TXXX:RELEASECOUNTRY description also reads back), MP4 freeform atom 'MusicBrainz Album Release Country', APEv2 raw RELEASECOUNTRY, ASF 'MusicBrainz/Album Release Country', WAV ICNT, Vorbis/Matroska raw RELEASECOUNTRY",
1324
+ description: "Release country (ISO 3166-1 code, e.g. 'US'). TagLib 2.3.2 translates the RELEASECOUNTRY key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM RELEASE COUNTRY' (TagLib's description casing; an uppercase TXXX:RELEASECOUNTRY description also reads back), MP4 freeform atom 'MusicBrainz Album Release Country', APEv2 raw RELEASECOUNTRY, ASF 'MusicBrainz/Album Release Country', WAV ICNT, Vorbis/Matroska raw RELEASECOUNTRY",
1325
1325
  type: "string",
1326
1326
  supportedFormats: [
1327
1327
  "ID3v2",
@@ -2053,7 +2053,10 @@ function wrapEmbindHandle(raw) {
2053
2053
  ...containerFormat === "MP4" || containerFormat === "ASF" ? { isEncrypted: pw.isEncrypted() } : {},
2054
2054
  ...formatVersion > 0 ? { formatVersion } : {},
2055
2055
  ...isValidBitrateMode(bitrateMode) ? { bitrateMode } : {},
2056
- ...codec === "Opus" ? { outputGainDb: pw.outputGainDb() } : {}
2056
+ // OpusHead gain exists only in an Ogg Opus stream. Gating on codec
2057
+ // alone fabricated outputGainDb: 0 for MP4 tracks once the MP4 codec
2058
+ // enum was mapped (the WASI side never emitted it there) — taglib-kswf.
2059
+ ...containerFormat === "OGG" && codec === "Opus" ? { outputGainDb: pw.outputGainDb() } : {}
2057
2060
  };
2058
2061
  }
2059
2062
  };
@@ -2369,8 +2372,8 @@ var init_audio_file_impl = __esm({
2369
2372
  }
2370
2373
  setChapters(chapters, options) {
2371
2374
  const fmt = this.getFormat();
2372
- if (fmt !== "MP3" && fmt !== "MP4") {
2373
- throw new UnsupportedFormatError(fmt, ["MP3", "MP4"], {
2375
+ if (fmt !== "MP3" && fmt !== "AAC" && fmt !== "MP4") {
2376
+ throw new UnsupportedFormatError(fmt, ["MP3", "AAC", "MP4"], {
2374
2377
  operation: "setChapters"
2375
2378
  });
2376
2379
  }
@@ -2876,6 +2879,8 @@ function normalizeTagInput(input) {
2876
2879
  if (val === "explicit") props.itunesAdvisory = ["1"];
2877
2880
  else if (val === "clean") props.itunesAdvisory = ["2"];
2878
2881
  else if (val === "unspecified") props.itunesAdvisory = [];
2882
+ } else if (field === "bpm") {
2883
+ props[field] = [String(Math.trunc(val))];
2879
2884
  } else if (NUMERIC_FIELDS.has(field)) {
2880
2885
  props[field] = [String(val)];
2881
2886
  } else if (field === "r128TrackGain" || field === "r128AlbumGain") {
@@ -2936,7 +2941,7 @@ var VERSION;
2936
2941
  var init_version = __esm({
2937
2942
  "src/version.ts"() {
2938
2943
  "use strict";
2939
- VERSION = "2.2.1";
2944
+ VERSION = "2.2.3";
2940
2945
  }
2941
2946
  });
2942
2947
 
@@ -299,7 +299,7 @@ export declare const PROPERTIES: {
299
299
  };
300
300
  readonly releaseType: {
301
301
  readonly key: "RELEASETYPE";
302
- readonly description: "Release type (album, single, EP, compilation, ...). Multi-value (e.g. 'album' + 'EP'). TagLib 2.3.1 translates the RELEASETYPE key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM TYPE' (TagLib's description casing), MP4 freeform atom, APEv2 MUSICBRAINZ_ALBUMTYPE, ASF 'MusicBrainz/Album Type', Vorbis/Matroska raw RELEASETYPE";
302
+ readonly description: "Release type (album, single, EP, compilation, ...). Multi-value (e.g. 'album' + 'EP'). TagLib 2.3.2 translates the RELEASETYPE key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM TYPE' (TagLib's description casing), MP4 freeform atom, APEv2 MUSICBRAINZ_ALBUMTYPE, ASF 'MusicBrainz/Album Type', Vorbis/Matroska raw RELEASETYPE";
303
303
  readonly type: "string";
304
304
  readonly supportedFormats: readonly ["ID3v2", "MP4", "Vorbis", "APE", "ASF", "Matroska"];
305
305
  readonly mappings: {
@@ -313,7 +313,7 @@ export declare const PROPERTIES: {
313
313
  };
314
314
  readonly releaseCountry: {
315
315
  readonly key: "RELEASECOUNTRY";
316
- readonly description: "Release country (ISO 3166-1 code, e.g. 'US'). TagLib 2.3.1 translates the RELEASECOUNTRY key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM RELEASE COUNTRY' (TagLib's description casing; an uppercase TXXX:RELEASECOUNTRY description also reads back), MP4 freeform atom 'MusicBrainz Album Release Country', APEv2 raw RELEASECOUNTRY, ASF 'MusicBrainz/Album Release Country', WAV ICNT, Vorbis/Matroska raw RELEASECOUNTRY";
316
+ readonly description: "Release country (ISO 3166-1 code, e.g. 'US'). TagLib 2.3.2 translates the RELEASECOUNTRY key per format: ID3v2 TXXX 'MUSICBRAINZ ALBUM RELEASE COUNTRY' (TagLib's description casing; an uppercase TXXX:RELEASECOUNTRY description also reads back), MP4 freeform atom 'MusicBrainz Album Release Country', APEv2 raw RELEASECOUNTRY, ASF 'MusicBrainz/Album Release Country', WAV ICNT, Vorbis/Matroska raw RELEASECOUNTRY";
317
317
  readonly type: "string";
318
318
  readonly supportedFormats: readonly ["ID3v2", "MP4", "Vorbis", "APE", "ASF", "Matroska", "WAV"];
319
319
  readonly mappings: {