taglib-wasm 1.6.0 → 1.7.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 (32) hide show
  1. package/README.md +13 -1
  2. package/dist/index.browser.js +148 -6
  3. package/dist/simple.browser.js +148 -6
  4. package/dist/src/folder-api/group-albums.d.ts +96 -0
  5. package/dist/src/folder-api/group-albums.d.ts.map +1 -0
  6. package/dist/src/folder-api/group-albums.js +505 -0
  7. package/dist/src/folder-api/index.d.ts +2 -0
  8. package/dist/src/folder-api/index.d.ts.map +1 -1
  9. package/dist/src/folder-api/index.js +6 -0
  10. package/dist/src/folder-api/scan-for-albums.d.ts +20 -0
  11. package/dist/src/folder-api/scan-for-albums.d.ts.map +1 -0
  12. package/dist/src/folder-api/scan-for-albums.js +22 -0
  13. package/dist/src/runtime/loader-types.d.ts +11 -0
  14. package/dist/src/runtime/loader-types.d.ts.map +1 -1
  15. package/dist/src/runtime/loader-types.js +10 -0
  16. package/dist/src/runtime/module-loader.d.ts.map +1 -1
  17. package/dist/src/runtime/module-loader.js +9 -0
  18. package/dist/src/runtime/unified-loader/loader.d.ts.map +1 -1
  19. package/dist/src/runtime/unified-loader/loader.js +5 -0
  20. package/dist/src/runtime/wasi-adapter/file-handle.d.ts.map +1 -1
  21. package/dist/src/runtime/wasi-adapter/file-handle.js +28 -3
  22. package/dist/src/taglib/load-audio-data.d.ts.map +1 -1
  23. package/dist/src/taglib/load-audio-data.js +19 -5
  24. package/dist/src/taglib/metadata-extent.d.ts +41 -0
  25. package/dist/src/taglib/metadata-extent.d.ts.map +1 -0
  26. package/dist/src/taglib/metadata-extent.js +128 -0
  27. package/dist/src/version.d.ts +1 -1
  28. package/dist/src/version.js +1 -1
  29. package/dist/taglib-wasi.wasm +0 -0
  30. package/dist/taglib-web.wasm +0 -0
  31. package/dist/taglib-wrapper.js +1 -1
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -196,7 +196,7 @@ file.save();
196
196
  Process entire music collections efficiently:
197
197
 
198
198
  ```typescript
199
- import { findDuplicates, scanFolder } from "taglib-wasm";
199
+ import { findDuplicates, scanFolder, scanForAlbums } from "taglib-wasm";
200
200
 
201
201
  // Scan a music library
202
202
  const result = await scanFolder("/path/to/music", {
@@ -227,6 +227,18 @@ const duplicates = await findDuplicates("/path/to/music", {
227
227
  criteria: ["artist", "title"],
228
228
  });
229
229
  console.log(`Found ${duplicates.length} groups of duplicates`);
230
+
231
+ // Group into albums with disc subdivisions (tags are authority, folder
232
+ // names are evidence)
233
+ const { albums, singles, unmatched } = await scanForAlbums("/path/to/music");
234
+ for (const album of albums) {
235
+ console.log(
236
+ `${
237
+ album.albumArtist ?? ""
238
+ } - ${album.album}: ${album.discs.length} disc(s)`,
239
+ );
240
+ }
241
+ console.log(`${singles.length} singles, ${unmatched.length} unmatched`);
230
242
  ```
231
243
 
232
244
  ### Working with Cover Art
@@ -2357,6 +2357,137 @@ var init_audio_file_impl = __esm({
2357
2357
  }
2358
2358
  });
2359
2359
 
2360
+ // src/taglib/metadata-extent.ts
2361
+ function be32(b, offset) {
2362
+ return (b[offset] << 24 | b[offset + 1] << 16 | b[offset + 2] << 8 | b[offset + 3]) >>> 0;
2363
+ }
2364
+ function startsWith(bytes, magic, at = 0) {
2365
+ if (bytes.length < at + magic.length) return false;
2366
+ for (let i = 0; i < magic.length; i++) {
2367
+ if (bytes[at + i] !== magic.charCodeAt(i)) return false;
2368
+ }
2369
+ return true;
2370
+ }
2371
+ function id3v2End(bytes) {
2372
+ if (bytes.length < 10) return void 0;
2373
+ const size = bytes[6] << 21 | bytes[7] << 14 | bytes[8] << 7 | bytes[9];
2374
+ const hasFooter = (bytes[5] & 16) !== 0;
2375
+ return 10 + size + (hasFooter ? 10 : 0);
2376
+ }
2377
+ function flacEnd(bytes, limit, start) {
2378
+ let offset = start + 4;
2379
+ for (; ; ) {
2380
+ if (offset + 4 > bytes.length) return void 0;
2381
+ const isLast = (bytes[offset] & 128) !== 0;
2382
+ const length = bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3];
2383
+ offset += 4 + length;
2384
+ if (offset > limit) return offset;
2385
+ if (isLast) return offset;
2386
+ }
2387
+ }
2388
+ function mp4MoovEnd(bytes, limit, start) {
2389
+ let offset = start;
2390
+ const readable = Math.min(bytes.length, limit);
2391
+ while (offset + 8 <= readable) {
2392
+ const size = be32(bytes, offset);
2393
+ const type = String.fromCharCode(
2394
+ bytes[offset + 4],
2395
+ bytes[offset + 5],
2396
+ bytes[offset + 6],
2397
+ bytes[offset + 7]
2398
+ );
2399
+ if (size < 8) return void 0;
2400
+ if (type === "moov") return offset + size;
2401
+ offset += size;
2402
+ }
2403
+ return void 0;
2404
+ }
2405
+ function oggMetadataEnd(bytes, limit, start) {
2406
+ let offset = start;
2407
+ let packetsCompleted = 0;
2408
+ let first = true;
2409
+ for (; ; ) {
2410
+ if (offset + 27 > bytes.length || !startsWith(bytes, "OggS", offset)) {
2411
+ return void 0;
2412
+ }
2413
+ const segmentCount = bytes[offset + 26];
2414
+ const tableStart = offset + 27;
2415
+ if (tableStart + segmentCount > bytes.length) return void 0;
2416
+ let payload = 0;
2417
+ for (let i = 0; i < segmentCount; i++) {
2418
+ const segment = bytes[tableStart + i];
2419
+ payload += segment;
2420
+ if (segment < 255) packetsCompleted++;
2421
+ }
2422
+ if (first) {
2423
+ const payloadStart = tableStart + segmentCount;
2424
+ if (payloadStart + 5 > bytes.length) return void 0;
2425
+ if (bytes[payloadStart] === 127 && startsWith(bytes, "FLAC", payloadStart + 1)) {
2426
+ return void 0;
2427
+ }
2428
+ first = false;
2429
+ }
2430
+ offset = tableStart + segmentCount + payload;
2431
+ if (packetsCompleted >= 2 || offset > limit) return offset;
2432
+ }
2433
+ }
2434
+ function isMpegFrameSync(bytes, at = 0) {
2435
+ if (bytes.length < at + 4) return false;
2436
+ if (bytes[at] !== 255 || (bytes[at + 1] & 224) !== 224) return false;
2437
+ const version = bytes[at + 1] >> 3 & 3;
2438
+ const layer = bytes[at + 1] >> 1 & 3;
2439
+ if (version === 1 || layer === 0) return false;
2440
+ const bitrateIndex = bytes[at + 2] >> 4 & 15;
2441
+ const sampleRateIndex = bytes[at + 2] >> 2 & 3;
2442
+ return bitrateIndex !== 0 && bitrateIndex !== 15 && sampleRateIndex !== 3;
2443
+ }
2444
+ function containerEnd(bytes, at, limit) {
2445
+ if (at >= bytes.length) return bytes.length < limit ? null : void 0;
2446
+ if (at + 8 > bytes.length) return void 0;
2447
+ if (startsWith(bytes, "fLaC", at)) return flacEnd(bytes, limit, at);
2448
+ if (startsWith(bytes, "ftyp", at + 4)) return mp4MoovEnd(bytes, limit, at);
2449
+ if (startsWith(bytes, "OggS", at)) return oggMetadataEnd(bytes, limit, at);
2450
+ return null;
2451
+ }
2452
+ function metadataFitsInHeader(header, limit) {
2453
+ let end;
2454
+ if (startsWith(header, "ID3")) {
2455
+ const tagEnd = id3v2End(header);
2456
+ if (tagEnd === void 0) return false;
2457
+ end = tagEnd;
2458
+ if (tagEnd <= limit) {
2459
+ const behind = containerEnd(header, tagEnd, limit);
2460
+ if (behind === void 0) return false;
2461
+ if (behind !== null) end = behind;
2462
+ }
2463
+ } else {
2464
+ const container = containerEnd(header, 0, limit);
2465
+ if (container === null) {
2466
+ end = isMpegFrameSync(header) ? 0 : void 0;
2467
+ } else {
2468
+ end = container;
2469
+ }
2470
+ }
2471
+ if (end === void 0 || end > header.length) return false;
2472
+ return end <= limit;
2473
+ }
2474
+ function trailerFitsInFooter(tail, footerSize) {
2475
+ if (tail.length < Math.min(footerSize, 32)) return false;
2476
+ for (const offsetFromEnd of [32, 32 + 128]) {
2477
+ const at = tail.length - offsetFromEnd;
2478
+ if (at < 0 || !startsWith(tail, "APETAGEX", at)) continue;
2479
+ const size = (tail[at + 12] | tail[at + 13] << 8 | tail[at + 14] << 16 | tail[at + 15] << 24) >>> 0;
2480
+ const trailing = offsetFromEnd === 32 ? 0 : 128;
2481
+ if (size + 32 + trailing > footerSize) return false;
2482
+ }
2483
+ return true;
2484
+ }
2485
+ var init_metadata_extent = __esm({
2486
+ "src/taglib/metadata-extent.ts"() {
2487
+ "use strict";
2488
+ }
2489
+ });
2490
+
2360
2491
  // src/taglib/load-audio-data.ts
2361
2492
  async function loadAudioData(input, opts) {
2362
2493
  if (opts.partial && typeof File !== "undefined" && input instanceof File) {
@@ -2365,12 +2496,20 @@ async function loadAudioData(input, opts) {
2365
2496
  if (input.size <= headerSize + footerSize) {
2366
2497
  return { data: await readFileData(input), isPartiallyLoaded: false };
2367
2498
  }
2368
- const header = await input.slice(0, headerSize).arrayBuffer();
2499
+ const header = new Uint8Array(
2500
+ await input.slice(0, headerSize).arrayBuffer()
2501
+ );
2502
+ if (!metadataFitsInHeader(header, headerSize)) {
2503
+ return { data: await readFileData(input), isPartiallyLoaded: false };
2504
+ }
2369
2505
  const footerStart = Math.max(0, input.size - footerSize);
2370
- const footer = await input.slice(footerStart).arrayBuffer();
2506
+ const footer = new Uint8Array(await input.slice(footerStart).arrayBuffer());
2507
+ if (!trailerFitsInFooter(footer, footerSize)) {
2508
+ return { data: await readFileData(input), isPartiallyLoaded: false };
2509
+ }
2371
2510
  const combined = new Uint8Array(header.byteLength + footer.byteLength);
2372
- combined.set(new Uint8Array(header), 0);
2373
- combined.set(new Uint8Array(footer), header.byteLength);
2511
+ combined.set(header, 0);
2512
+ combined.set(footer, header.byteLength);
2374
2513
  return { data: combined, isPartiallyLoaded: true };
2375
2514
  }
2376
2515
  if (opts.partial && typeof input === "string") {
@@ -2381,7 +2520,9 @@ async function loadAudioData(input, opts) {
2381
2520
  opts.maxHeaderSize,
2382
2521
  opts.maxFooterSize
2383
2522
  );
2384
- return { data, isPartiallyLoaded: true };
2523
+ if (metadataFitsInHeader(data, opts.maxHeaderSize) && trailerFitsInFooter(data, opts.maxFooterSize)) {
2524
+ return { data, isPartiallyLoaded: true };
2525
+ }
2385
2526
  }
2386
2527
  return { data: await readFileData(input), isPartiallyLoaded: false };
2387
2528
  }
@@ -2391,6 +2532,7 @@ var init_load_audio_data = __esm({
2391
2532
  "src/taglib/load-audio-data.ts"() {
2392
2533
  "use strict";
2393
2534
  init_file();
2535
+ init_metadata_extent();
2394
2536
  }
2395
2537
  });
2396
2538
 
@@ -2588,7 +2730,7 @@ var VERSION;
2588
2730
  var init_version = __esm({
2589
2731
  "src/version.ts"() {
2590
2732
  "use strict";
2591
- VERSION = "1.6.0";
2733
+ VERSION = "1.7.0";
2592
2734
  }
2593
2735
  });
2594
2736
 
@@ -2289,6 +2289,137 @@ var init_audio_file_impl = __esm({
2289
2289
  }
2290
2290
  });
2291
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
+
2292
2423
  // src/taglib/load-audio-data.ts
2293
2424
  async function loadAudioData(input, opts) {
2294
2425
  if (opts.partial && typeof File !== "undefined" && input instanceof File) {
@@ -2297,12 +2428,20 @@ async function loadAudioData(input, opts) {
2297
2428
  if (input.size <= headerSize + footerSize) {
2298
2429
  return { data: await readFileData(input), isPartiallyLoaded: false };
2299
2430
  }
2300
- 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
+ }
2301
2437
  const footerStart = Math.max(0, input.size - footerSize);
2302
- 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
+ }
2303
2442
  const combined = new Uint8Array(header.byteLength + footer.byteLength);
2304
- combined.set(new Uint8Array(header), 0);
2305
- combined.set(new Uint8Array(footer), header.byteLength);
2443
+ combined.set(header, 0);
2444
+ combined.set(footer, header.byteLength);
2306
2445
  return { data: combined, isPartiallyLoaded: true };
2307
2446
  }
2308
2447
  if (opts.partial && typeof input === "string") {
@@ -2313,7 +2452,9 @@ async function loadAudioData(input, opts) {
2313
2452
  opts.maxHeaderSize,
2314
2453
  opts.maxFooterSize
2315
2454
  );
2316
- return { data, isPartiallyLoaded: true };
2455
+ if (metadataFitsInHeader(data, opts.maxHeaderSize) && trailerFitsInFooter(data, opts.maxFooterSize)) {
2456
+ return { data, isPartiallyLoaded: true };
2457
+ }
2317
2458
  }
2318
2459
  return { data: await readFileData(input), isPartiallyLoaded: false };
2319
2460
  }
@@ -2323,6 +2464,7 @@ var init_load_audio_data = __esm({
2323
2464
  "src/taglib/load-audio-data.ts"() {
2324
2465
  "use strict";
2325
2466
  init_file();
2467
+ init_metadata_extent();
2326
2468
  }
2327
2469
  });
2328
2470
 
@@ -2520,7 +2662,7 @@ var VERSION;
2520
2662
  var init_version = __esm({
2521
2663
  "src/version.ts"() {
2522
2664
  "use strict";
2523
- VERSION = "1.6.0";
2665
+ VERSION = "1.7.0";
2524
2666
  }
2525
2667
  });
2526
2668
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @fileoverview Pure album grouping over a FolderScanResult (2026-08-03 spec).
3
+ *
4
+ * Given a folder scan, produce albums with disc subdivisions, using embedded
5
+ * tags as authority and folder/filename structure as evidence. Synchronous,
6
+ * runtime-agnostic, no wasm, no I/O: everything the algorithm needs arrives
7
+ * in the scan result (tags, paths, statuses).
8
+ *
9
+ * Model contract (invariants, enforced by tests):
10
+ * 1. Every ok item appears exactly once across albums[*].items, singles,
11
+ * and unmatched; errors is disjoint.
12
+ * 2. discs >= 1 per album; items >= 1 per disc.
13
+ * 3. discNumber === tagDiscNumber ?? folderDiscNumber on every disc.
14
+ * 4. key is stable for identical input.
15
+ * 5. Every item's albumDir/discNumber match the disc it was assigned to.
16
+ * 6. compilation is true/false only under full tag agreement, else undefined.
17
+ * 7. singles holds exactly the ok items whose resolved album has one file.
18
+ */
19
+ import type { AudioFileMetadata, FolderScanResult } from "./types.js";
20
+ export type AlbumGroupKey = string & {
21
+ __brand: "AlbumGroupKey";
22
+ };
23
+ export type DiscConfidence = "high" | "medium" | "low";
24
+ /** A file inside an album, carrying its own resolution. */
25
+ export interface AlbumGroupItem extends AudioFileMetadata {
26
+ /** The album directory this file resolves to: its own directory, or one
27
+ * level up when that directory is a confirmed disc folder. */
28
+ albumDir: string;
29
+ /** This file's resolved disc number — the containing disc's resolved
30
+ * number; undefined for discs with no number. */
31
+ discNumber: number | undefined;
32
+ }
33
+ export interface AlbumDisc {
34
+ /** Resolved disc number: tag value when files agree, else folder-implied. */
35
+ discNumber: number | undefined;
36
+ /** Total discs: common tag totalDiscs, else "of N" from folder, else max sibling number. */
37
+ totalDiscs: number | undefined;
38
+ /** Disc number parsed from the folder name; absent when no folder evidence. */
39
+ folderDiscNumber: number | undefined;
40
+ /** Subtitle after the marker, e.g. "Bonus Tracks" from "Disc 2 (Bonus Tracks)". */
41
+ folderDiscTitle: string | undefined;
42
+ /** Disc number from embedded tags; present only when all tagged files in the disc agree. */
43
+ tagDiscNumber: number | undefined;
44
+ /** Confidence in the folder-name evidence; "high" for tag-derived discs. */
45
+ confidence: DiscConfidence;
46
+ /** Files sorted by (track, filename), each carrying its own resolution. */
47
+ items: AlbumGroupItem[];
48
+ }
49
+ export interface AlbumGroup {
50
+ /** Opaque, stable identity. See the key construction rule (identity step). */
51
+ key: AlbumGroupKey;
52
+ albumArtist: string | undefined;
53
+ album: string | undefined;
54
+ /** Where the album identity came from. */
55
+ source: "tags" | "folder";
56
+ /** Compilation evidence from embedded tags (COMPILATION/TCMP/cpil): true
57
+ * when the group's files agree and the flag is set, false when they agree
58
+ * and it is unset, undefined when tags are absent or disagree. */
59
+ compilation: boolean | undefined;
60
+ /** The album's directory: the common directory of all items when they
61
+ * share one (folder-derived albums always do), else undefined. */
62
+ directory: string | undefined;
63
+ /** One entry per resolved disc number; a single-disc album has one entry. */
64
+ discs: AlbumDisc[];
65
+ /** All files across discs, sorted by (discNumber, track, filename). */
66
+ items: AlbumGroupItem[];
67
+ }
68
+ export interface AlbumGroupingResult {
69
+ albums: AlbumGroup[];
70
+ /** Ok items that resolve to an album of exactly one file — a single, not
71
+ * an album. Cardinality is the only library rule. */
72
+ singles: AudioFileMetadata[];
73
+ /** Ok items not attributable to any album (untagged, no folder title evidence). */
74
+ unmatched: AudioFileMetadata[];
75
+ /** Per-file scan errors, surfaced exactly as the scan produced them. */
76
+ errors: Array<{
77
+ path: string;
78
+ error: Error;
79
+ }>;
80
+ }
81
+ export interface GroupAlbumsOptions {
82
+ /** Drop folder disc evidence below this tier. Default "low" (accept all). */
83
+ minFolderConfidence?: DiscConfidence;
84
+ /** Parse flat disc prefixes (1-01) from filenames. Default true. */
85
+ flatDiscPrefixes?: boolean;
86
+ /** Group untagged files by folder into albums. Default true. */
87
+ folderFallback?: boolean;
88
+ /** The directory the scan started at. When set, a bare disc folder
89
+ * (CD1/) directly under it is unmatched instead of an album named after
90
+ * the root. Default: no guard — the scan root cannot be inferred from a
91
+ * bare FolderScanResult (the common ancestor is the album folder, not the
92
+ * scanned root). scanForAlbums always passes the folder path it scanned. */
93
+ scanRoot?: string;
94
+ }
95
+ export declare function groupAlbums(result: FolderScanResult, options?: GroupAlbumsOptions): AlbumGroupingResult;
96
+ //# sourceMappingURL=group-albums.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-albums.d.ts","sourceRoot":"","sources":["../../../src/folder-api/group-albums.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAOtE,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAElE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEvD,2DAA2D;AAC3D,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD;kEAC8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB;qDACiD;IACjD,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4FAA4F;IAC5F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,+EAA+E;IAC/E,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,mFAAmF;IACnF,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,4FAA4F;IAC5F,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,4EAA4E;IAC5E,UAAU,EAAE,cAAc,CAAC;IAC3B,2EAA2E;IAC3E,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,8EAA8E;IAC9E,GAAG,EAAE,aAAa,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B;;sEAEkE;IAClE,WAAW,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC;sEACkE;IAClE,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,6EAA6E;IAC7E,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,uEAAuE;IACvE,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB;yDACqD;IACrD,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,mFAAmF;IACnF,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAC/B,wEAAwE;IACxE,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,cAAc,CAAC;IACrC,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gEAAgE;IAChE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;gFAI4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA4SD,wBAAgB,WAAW,CACzB,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,kBAAuB,GAC/B,mBAAmB,CAwcrB"}