taglib-wasm 2.2.1 → 2.2.2
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/dist/disc-folder.d.ts +3 -1
- package/dist/disc-folder.d.ts.map +1 -1
- package/dist/disc-folder.js +187 -152
- package/dist/index.browser.js +1 -1
- package/dist/simple.browser.js +1 -1
- package/dist/src/folder-api/album-corroborate.d.ts +13 -0
- package/dist/src/folder-api/album-corroborate.d.ts.map +1 -0
- package/dist/src/folder-api/album-corroborate.js +36 -0
- package/dist/src/folder-api/album-discs.d.ts +13 -0
- package/dist/src/folder-api/album-discs.d.ts.map +1 -0
- package/dist/src/folder-api/album-discs.js +122 -0
- package/dist/src/folder-api/album-grouping.d.ts +24 -0
- package/dist/src/folder-api/album-grouping.d.ts.map +1 -0
- package/dist/src/folder-api/album-grouping.js +118 -0
- package/dist/src/folder-api/album-identity.d.ts +27 -0
- package/dist/src/folder-api/album-identity.d.ts.map +1 -0
- package/dist/src/folder-api/album-identity.js +151 -0
- package/dist/src/folder-api/{group-albums.d.ts → album-types.d.ts} +55 -26
- package/dist/src/folder-api/album-types.d.ts.map +1 -0
- package/dist/src/folder-api/album-types.js +0 -0
- package/dist/src/folder-api/folder-disc.d.ts +24 -0
- package/dist/src/folder-api/folder-disc.d.ts.map +1 -0
- package/dist/src/folder-api/folder-disc.js +169 -0
- package/dist/src/folder-api/index.d.ts +3 -1
- package/dist/src/folder-api/index.d.ts.map +1 -1
- package/dist/src/folder-api/index.js +2 -4
- package/dist/src/folder-api/scan-for-albums.d.ts +1 -1
- package/dist/src/folder-api/scan-for-albums.d.ts.map +1 -1
- package/dist/src/folder-api/scan-for-albums.js +1 -3
- package/dist/src/runtime/wasi-adapter/audio-properties.d.ts +20 -0
- package/dist/src/runtime/wasi-adapter/audio-properties.d.ts.map +1 -0
- package/dist/src/runtime/wasi-adapter/audio-properties.js +84 -0
- package/dist/src/runtime/wasi-adapter/file-handle.d.ts +5 -19
- package/dist/src/runtime/wasi-adapter/file-handle.d.ts.map +1 -1
- package/dist/src/runtime/wasi-adapter/file-handle.js +67 -389
- package/dist/src/runtime/wasi-adapter/handle-state.d.ts +28 -0
- package/dist/src/runtime/wasi-adapter/handle-state.d.ts.map +1 -0
- package/dist/src/runtime/wasi-adapter/handle-state.js +58 -0
- package/dist/src/runtime/wasi-adapter/mp4-items.d.ts +34 -0
- package/dist/src/runtime/wasi-adapter/mp4-items.d.ts.map +1 -0
- package/dist/src/runtime/wasi-adapter/mp4-items.js +80 -0
- package/dist/src/runtime/wasi-adapter/property-surface.d.ts +25 -0
- package/dist/src/runtime/wasi-adapter/property-surface.d.ts.map +1 -0
- package/dist/src/runtime/wasi-adapter/property-surface.js +104 -0
- package/dist/src/runtime/wasi-adapter/structured-fields.d.ts +44 -0
- package/dist/src/runtime/wasi-adapter/structured-fields.d.ts.map +1 -0
- package/dist/src/runtime/wasi-adapter/structured-fields.js +139 -0
- package/dist/src/runtime/wasi-adapter/tag-keys.d.ts +28 -0
- package/dist/src/runtime/wasi-adapter/tag-keys.d.ts.map +1 -0
- package/dist/src/runtime/wasi-adapter/tag-keys.js +46 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -1
- package/dist/src/folder-api/group-albums.d.ts.map +0 -1
- package/dist/src/folder-api/group-albums.js +0 -544
package/dist/disc-folder.d.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* discFolderInfo("CD1"); // { kind: "exact", number: 1, ... }
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
export
|
|
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;
|
|
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"}
|
package/dist/disc-folder.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
// src/
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
237
|
+
const parent = dirname(cursor);
|
|
238
|
+
if (parent === cursor) break;
|
|
239
|
+
cursor = parent;
|
|
242
240
|
}
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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) {
|
package/dist/index.browser.js
CHANGED
package/dist/simple.browser.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sibling corroboration for gated disc-folder names (taglib-ys7m split).
|
|
3
|
+
*
|
|
4
|
+
* Gated names (title-word markers, single-letter tokens, bonus, bare) are
|
|
5
|
+
* discs only when a sibling under the same parent confirms them — an
|
|
6
|
+
* unconditional disc sibling, or sibling numbering of their own (a folder
|
|
7
|
+
* never confirms itself). Side-letter tokens corroborate each other
|
|
8
|
+
* ("CD D" + "CD E").
|
|
9
|
+
*/
|
|
10
|
+
import type { DiscConfidence, DiscParse } from "./album-types.js";
|
|
11
|
+
export declare const CONFIDENCE_RANK: Record<DiscConfidence, number>;
|
|
12
|
+
export declare function corroborated(parse: DiscParse, siblings: string[]): boolean;
|
|
13
|
+
//# sourceMappingURL=album-corroborate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"album-corroborate.d.ts","sourceRoot":"","sources":["../../../src/folder-api/album-corroborate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElE,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAI1D,CAAC;AAEF,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAoC1E"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { basename } from "../utils/path.js";
|
|
2
|
+
import { parseDiscName } from "./folder-disc.js";
|
|
3
|
+
const CONFIDENCE_RANK = {
|
|
4
|
+
low: 0,
|
|
5
|
+
medium: 1,
|
|
6
|
+
high: 2
|
|
7
|
+
};
|
|
8
|
+
function corroborated(parse, siblings) {
|
|
9
|
+
if (!parse.gated && parse.kind === "exact") return true;
|
|
10
|
+
if (siblings.length === 0) return false;
|
|
11
|
+
const siblingParses = siblings.map((s) => parseDiscName(basename(s))).filter((p) => p !== void 0);
|
|
12
|
+
const hasExactSibling = siblingParses.some(
|
|
13
|
+
(p) => !p.gated && p.kind === "exact"
|
|
14
|
+
);
|
|
15
|
+
const hasNumberedSibling = siblingParses.some(
|
|
16
|
+
(p) => p.number !== void 0
|
|
17
|
+
);
|
|
18
|
+
if (parse.number !== void 0 && hasNumberedSibling) return true;
|
|
19
|
+
if (!parse.gated && parse.kind === "embedded" && hasExactSibling) return true;
|
|
20
|
+
if (!parse.gated && parse.kind === "volume" && hasNumberedSibling) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (parse.sideLetter) {
|
|
24
|
+
return siblingParses.some((p) => p.sideLetter);
|
|
25
|
+
}
|
|
26
|
+
if (parse.kind === "bonus" || parse.kind === "bare") {
|
|
27
|
+
const numberedCount = siblingParses.filter((p) => p.number !== void 0).length;
|
|
28
|
+
if (numberedCount >= 2) return true;
|
|
29
|
+
if (parse.number !== void 0 && hasNumberedSibling) return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
CONFIDENCE_RANK,
|
|
35
|
+
corroborated
|
|
36
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disc assembly and finalization (spec steps 6-7, taglib-ys7m split).
|
|
3
|
+
*
|
|
4
|
+
* Each file resolves to a disc number: the common tag value among its
|
|
5
|
+
* directory's files (missing abstains, disagreement falls back to the
|
|
6
|
+
* folder), else the folder evidence (own disc parse, ancestor, or flat
|
|
7
|
+
* filename prefix). Files resolving to the SAME number merge into one
|
|
8
|
+
* AlbumDisc, with tagDiscNumber recomputed over the merged set.
|
|
9
|
+
*/
|
|
10
|
+
import type { AlbumAccumulator, AlbumGroup } from "./album-types.js";
|
|
11
|
+
/** Assemble discs per album, then sort discs and items (steps 6-7). */
|
|
12
|
+
export declare function assembleDiscs(albums: Map<AlbumAccumulator["key"], AlbumAccumulator>): AlbumGroup[];
|
|
13
|
+
//# sourceMappingURL=album-discs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"album-discs.d.ts","sourceRoot":"","sources":["../../../src/folder-api/album-discs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EACV,gBAAgB,EAEhB,UAAU,EAEX,MAAM,kBAAkB,CAAC;AAE1B,uEAAuE;AACvE,wBAAgB,aAAa,CAC3B,MAAM,EAAE,GAAG,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,GACrD,UAAU,EAAE,CAoKd"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { dirname } from "./folder-disc.js";
|
|
2
|
+
function assembleDiscs(albums) {
|
|
3
|
+
const groups = [];
|
|
4
|
+
for (const acc of albums.values()) {
|
|
5
|
+
const dirTagCommon = /* @__PURE__ */ new Map();
|
|
6
|
+
const resolvedByFile = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const item of acc.items) {
|
|
8
|
+
const dir = dirname(item.path);
|
|
9
|
+
if (!dirTagCommon.has(dir)) {
|
|
10
|
+
const tagged = acc.items.filter((i) => dirname(i.path) === dir).map((i) => i.tags.discNumber).filter((d) => d !== void 0 && d > 0);
|
|
11
|
+
dirTagCommon.set(
|
|
12
|
+
dir,
|
|
13
|
+
tagged.length > 0 && tagged.every((d) => d === tagged[0]) ? tagged[0] : void 0
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const evidence = acc.evidence.get(item.path);
|
|
17
|
+
const tagCommon = dirTagCommon.get(dir);
|
|
18
|
+
resolvedByFile.set(
|
|
19
|
+
item.path,
|
|
20
|
+
tagCommon ?? evidence?.folderDiscNumber
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const discs = /* @__PURE__ */ new Map();
|
|
24
|
+
const order = [];
|
|
25
|
+
const noEvidenceItems = [];
|
|
26
|
+
for (const item of acc.items) {
|
|
27
|
+
const evidence = acc.evidence.get(item.path);
|
|
28
|
+
if (evidence === void 0) {
|
|
29
|
+
noEvidenceItems.push(item);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const resolved = resolvedByFile.get(item.path);
|
|
33
|
+
const discKey = resolved === void 0 ? `dir:${dirname(item.path)}` : String(resolved);
|
|
34
|
+
let disc = discs.get(discKey);
|
|
35
|
+
if (!disc) {
|
|
36
|
+
disc = {
|
|
37
|
+
discNumber: resolved,
|
|
38
|
+
totalDiscs: evidence.total,
|
|
39
|
+
folderDiscNumber: evidence.folderDiscNumber,
|
|
40
|
+
folderDiscTitle: evidence.folderDiscTitle,
|
|
41
|
+
tagDiscNumber: void 0,
|
|
42
|
+
confidence: evidence.confidence,
|
|
43
|
+
items: []
|
|
44
|
+
};
|
|
45
|
+
discs.set(discKey, disc);
|
|
46
|
+
order.push(discKey);
|
|
47
|
+
}
|
|
48
|
+
disc.items.push(item);
|
|
49
|
+
}
|
|
50
|
+
if (noEvidenceItems.length > 0) {
|
|
51
|
+
const numbered = [...discs.values()].filter((d) => d.folderDiscNumber !== void 0).sort((a, b) => a.folderDiscNumber - b.folderDiscNumber);
|
|
52
|
+
if (numbered.length > 0) {
|
|
53
|
+
numbered[0].items.push(...noEvidenceItems);
|
|
54
|
+
} else {
|
|
55
|
+
const key = "dir:__none__";
|
|
56
|
+
let disc = discs.get(key);
|
|
57
|
+
if (!disc) {
|
|
58
|
+
disc = {
|
|
59
|
+
discNumber: void 0,
|
|
60
|
+
totalDiscs: void 0,
|
|
61
|
+
folderDiscNumber: void 0,
|
|
62
|
+
folderDiscTitle: void 0,
|
|
63
|
+
tagDiscNumber: void 0,
|
|
64
|
+
confidence: "high",
|
|
65
|
+
items: []
|
|
66
|
+
};
|
|
67
|
+
discs.set(key, disc);
|
|
68
|
+
order.push(key);
|
|
69
|
+
}
|
|
70
|
+
disc.items.push(...noEvidenceItems);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const disc of discs.values()) {
|
|
74
|
+
const tagged = disc.items.map((i) => i.tags.discNumber).filter((d) => d !== void 0 && d > 0);
|
|
75
|
+
const common = tagged.length > 0 && tagged.every((d) => d === tagged[0]) ? tagged[0] : void 0;
|
|
76
|
+
disc.tagDiscNumber = common;
|
|
77
|
+
disc.discNumber = common ?? disc.folderDiscNumber;
|
|
78
|
+
for (const item of disc.items) item.discNumber = disc.discNumber;
|
|
79
|
+
}
|
|
80
|
+
const albumTaggedTotals = acc.items.map((i) => i.tags.totalDiscs).filter((t) => t !== void 0 && t > 0);
|
|
81
|
+
const commonTotal = albumTaggedTotals.length > 0 && albumTaggedTotals.every((t) => t === albumTaggedTotals[0]) ? albumTaggedTotals[0] : void 0;
|
|
82
|
+
const ofN = [...discs.values()].map((d) => d.totalDiscs).find(
|
|
83
|
+
(t) => t !== void 0
|
|
84
|
+
);
|
|
85
|
+
const maxSibling = [...discs.values()].map((d) => d.discNumber).filter((n) => n !== void 0).reduce((a, b) => Math.max(a, b), 0) || void 0;
|
|
86
|
+
for (const disc of discs.values()) {
|
|
87
|
+
disc.totalDiscs = commonTotal ?? ofN ?? maxSibling;
|
|
88
|
+
}
|
|
89
|
+
const sortedDiscs = [...discs.values()].sort((a, b) => {
|
|
90
|
+
const an = a.discNumber === void 0 ? -1 : a.discNumber;
|
|
91
|
+
const bn = b.discNumber === void 0 ? -1 : b.discNumber;
|
|
92
|
+
if (an !== bn) return an - bn;
|
|
93
|
+
return (a.folderDiscTitle ?? "").localeCompare(b.folderDiscTitle ?? "");
|
|
94
|
+
});
|
|
95
|
+
for (const disc of sortedDiscs) {
|
|
96
|
+
disc.items.sort((a, b) => {
|
|
97
|
+
const at = a.tags.track ?? Infinity;
|
|
98
|
+
const bt = b.tags.track ?? Infinity;
|
|
99
|
+
if (at !== bt) return at - bt;
|
|
100
|
+
return a.path.localeCompare(b.path);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const flags = acc.items.map((i) => i.tags.compilation).filter((c) => c !== void 0);
|
|
104
|
+
const compilation = flags.length > 0 && flags.every((f) => f === flags[0]) ? flags[0] : void 0;
|
|
105
|
+
const albumDirs = new Set(acc.items.map((i) => i.albumDir));
|
|
106
|
+
const directory = albumDirs.size === 1 ? [...albumDirs][0] : acc.directory;
|
|
107
|
+
groups.push({
|
|
108
|
+
key: acc.key,
|
|
109
|
+
albumArtist: acc.albumArtist,
|
|
110
|
+
album: acc.album,
|
|
111
|
+
source: acc.source,
|
|
112
|
+
compilation,
|
|
113
|
+
directory,
|
|
114
|
+
discs: sortedDiscs,
|
|
115
|
+
items: sortedDiscs.flatMap((d) => d.items)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return groups;
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
assembleDiscs
|
|
122
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
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). taglib-ys7m split: disc
|
|
8
|
+
* recognition lives in folder-disc.ts, identity in album-identity.ts, disc
|
|
9
|
+
* assembly in album-discs.ts, types in album-types.ts.
|
|
10
|
+
*
|
|
11
|
+
* Model contract (invariants, enforced by tests):
|
|
12
|
+
* 1. Every ok item appears exactly once across albums[*].items, singles,
|
|
13
|
+
* and unmatched; errors is disjoint.
|
|
14
|
+
* 2. discs >= 1 per album; items >= 1 per disc.
|
|
15
|
+
* 3. discNumber === tagDiscNumber ?? folderDiscNumber on every disc.
|
|
16
|
+
* 4. key is stable for identical input.
|
|
17
|
+
* 5. Every item's albumDir/discNumber match the disc it was assigned to.
|
|
18
|
+
* 6. compilation is true/false only under full tag agreement, else undefined.
|
|
19
|
+
* 7. singles holds exactly the ok items whose resolved album has one file.
|
|
20
|
+
*/
|
|
21
|
+
import type { FolderScanResult } from "./types.js";
|
|
22
|
+
import type { AlbumGroupingResult, GroupAlbumsOptions } from "./album-types.js";
|
|
23
|
+
export declare function groupAlbums(result: FolderScanResult, options?: GroupAlbumsOptions): AlbumGroupingResult;
|
|
24
|
+
//# sourceMappingURL=album-grouping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"album-grouping.d.ts","sourceRoot":"","sources":["../../../src/folder-api/album-grouping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAEV,mBAAmB,EAInB,kBAAkB,EAEnB,MAAM,kBAAkB,CAAC;AA0I1B,wBAAgB,WAAW,CACzB,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,kBAAuB,GAC/B,mBAAmB,CAuCrB"}
|