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.
- package/README.md +13 -1
- package/dist/index.browser.js +148 -6
- package/dist/simple.browser.js +148 -6
- package/dist/src/folder-api/group-albums.d.ts +96 -0
- package/dist/src/folder-api/group-albums.d.ts.map +1 -0
- package/dist/src/folder-api/group-albums.js +505 -0
- package/dist/src/folder-api/index.d.ts +2 -0
- package/dist/src/folder-api/index.d.ts.map +1 -1
- package/dist/src/folder-api/index.js +6 -0
- package/dist/src/folder-api/scan-for-albums.d.ts +20 -0
- package/dist/src/folder-api/scan-for-albums.d.ts.map +1 -0
- package/dist/src/folder-api/scan-for-albums.js +22 -0
- package/dist/src/runtime/loader-types.d.ts +11 -0
- package/dist/src/runtime/loader-types.d.ts.map +1 -1
- package/dist/src/runtime/loader-types.js +10 -0
- package/dist/src/runtime/module-loader.d.ts.map +1 -1
- package/dist/src/runtime/module-loader.js +9 -0
- package/dist/src/runtime/unified-loader/loader.d.ts.map +1 -1
- package/dist/src/runtime/unified-loader/loader.js +5 -0
- package/dist/src/runtime/wasi-adapter/file-handle.d.ts.map +1 -1
- package/dist/src/runtime/wasi-adapter/file-handle.js +28 -3
- package/dist/src/taglib/load-audio-data.d.ts.map +1 -1
- package/dist/src/taglib/load-audio-data.js +19 -5
- package/dist/src/taglib/metadata-extent.d.ts +41 -0
- package/dist/src/taglib/metadata-extent.d.ts.map +1 -0
- package/dist/src/taglib/metadata-extent.js +128 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/taglib-wasi.wasm +0 -0
- package/dist/taglib-web.wasm +0 -0
- package/dist/taglib-wrapper.js +1 -1
- 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
|
package/dist/index.browser.js
CHANGED
|
@@ -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 =
|
|
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(
|
|
2373
|
-
combined.set(
|
|
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
|
-
|
|
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.
|
|
2733
|
+
VERSION = "1.7.0";
|
|
2592
2734
|
}
|
|
2593
2735
|
});
|
|
2594
2736
|
|
package/dist/simple.browser.js
CHANGED
|
@@ -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 =
|
|
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(
|
|
2305
|
-
combined.set(
|
|
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
|
-
|
|
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.
|
|
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"}
|