taglib-wasm 1.6.1 → 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 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
@@ -2730,7 +2730,7 @@ var VERSION;
2730
2730
  var init_version = __esm({
2731
2731
  "src/version.ts"() {
2732
2732
  "use strict";
2733
- VERSION = "1.6.1";
2733
+ VERSION = "1.7.0";
2734
2734
  }
2735
2735
  });
2736
2736
 
@@ -2662,7 +2662,7 @@ var VERSION;
2662
2662
  var init_version = __esm({
2663
2663
  "src/version.ts"() {
2664
2664
  "use strict";
2665
- VERSION = "1.6.1";
2665
+ VERSION = "1.7.0";
2666
2666
  }
2667
2667
  });
2668
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"}
@@ -0,0 +1,505 @@
1
+ import { basename } from "../utils/path.js";
2
+ function dirname(path) {
3
+ const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
4
+ return i === -1 ? path : path.slice(0, i);
5
+ }
6
+ const MARKERS = [
7
+ "cd",
8
+ "disc",
9
+ "disk",
10
+ "dvd",
11
+ "sacd",
12
+ "blu-ray",
13
+ "bd",
14
+ "vinyl",
15
+ "lp",
16
+ "record",
17
+ "cassette",
18
+ "tape",
19
+ "digital media"
20
+ ];
21
+ const WORD_NUMBERS = {
22
+ one: 1,
23
+ two: 2,
24
+ three: 3,
25
+ four: 4,
26
+ five: 5,
27
+ six: 6,
28
+ seven: 7,
29
+ eight: 8,
30
+ nine: 9,
31
+ ten: 10,
32
+ eleven: 11,
33
+ twelve: 12
34
+ };
35
+ function romanToInt(s) {
36
+ const values = {
37
+ i: 1,
38
+ v: 5,
39
+ x: 10,
40
+ l: 50,
41
+ c: 100,
42
+ d: 500,
43
+ m: 1e3
44
+ };
45
+ let total = 0;
46
+ let prev = 0;
47
+ for (let i = s.length - 1; i >= 0; i--) {
48
+ const v = values[s[i]];
49
+ if (v === void 0) return NaN;
50
+ total += v < prev ? -v : v;
51
+ prev = v;
52
+ }
53
+ return total;
54
+ }
55
+ function parseNumberToken(token) {
56
+ if (/^\d+$/.test(token)) return parseInt(token, 10);
57
+ const word = WORD_NUMBERS[token.toLowerCase()];
58
+ if (word) return word;
59
+ const roman = romanToInt(token.toLowerCase());
60
+ return Number.isFinite(roman) ? roman : void 0;
61
+ }
62
+ const SEP = String.raw`[\s._#-]*`;
63
+ const NUM = String.raw`(?:\d+|[ivxlcdm]+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)`;
64
+ function parseDiscName(name) {
65
+ const trimmed = name.trim();
66
+ if (/^(?:extras?|bonus)$/i.test(trimmed)) return void 0;
67
+ for (const marker of MARKERS) {
68
+ const left = String.raw`(?:^|[^a-z0-9])`;
69
+ const esc = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70
+ const re = new RegExp(left + String.raw`(${esc})${SEP}(${NUM})`, "i");
71
+ const m = trimmed.match(re);
72
+ if (!m) continue;
73
+ const number = parseNumberToken(m[2]);
74
+ if (number === void 0) continue;
75
+ const markerStart = m.index + m[1].length - marker.length;
76
+ const rawPrefix = trimmed.slice(0, markerStart).trim();
77
+ const title = rawPrefix.replace(/[()[\]]/g, "").replace(/[\s._#-]+$/, "") || void 0;
78
+ const isExact = title === void 0;
79
+ const after = trimmed.slice(m.index + m[0].length).trim();
80
+ const discTitle = after.replace(/^[()[\],:;\-–—\s]+/, "").replace(/[)\]\[,:;\-–—\s]+$/, "").trim() || void 0;
81
+ const ofMatch = after.trim().match(/^of\s+(\d+)\b/i);
82
+ const total = ofMatch ? parseInt(ofMatch[1], 10) : void 0;
83
+ return {
84
+ kind: isExact ? "exact" : "embedded",
85
+ number,
86
+ total,
87
+ title,
88
+ discTitle
89
+ };
90
+ }
91
+ const volume = trimmed.match(
92
+ /^(?:volume|vol\.?|part|pt\.?)\s*(\d+)(?:\s*of\s*(\d+))?$/i
93
+ );
94
+ if (volume) {
95
+ return {
96
+ kind: "volume",
97
+ number: parseInt(volume[1], 10),
98
+ total: volume[2] ? parseInt(volume[2], 10) : void 0,
99
+ title: void 0,
100
+ discTitle: void 0
101
+ };
102
+ }
103
+ if (/^bonus\s+disc$/i.test(trimmed)) {
104
+ return {
105
+ kind: "bonus",
106
+ number: void 0,
107
+ total: void 0,
108
+ title: void 0,
109
+ discTitle: void 0
110
+ };
111
+ }
112
+ if (/^(?:\d+|v\d+|p\d+|[ab])$/i.test(trimmed)) {
113
+ return {
114
+ kind: "bare",
115
+ number: /^\d+$/i.test(trimmed) ? parseInt(trimmed, 10) : void 0,
116
+ total: void 0,
117
+ title: void 0,
118
+ discTitle: void 0
119
+ };
120
+ }
121
+ return void 0;
122
+ }
123
+ function baseConfidence(parse) {
124
+ switch (parse.kind) {
125
+ case "exact":
126
+ return typeof parse.number === "number" && /^\d+$/.test(String(parse.number)) ? "high" : "low";
127
+ case "embedded":
128
+ return "medium";
129
+ case "volume":
130
+ return "medium";
131
+ case "bonus":
132
+ case "bare":
133
+ return "low";
134
+ }
135
+ }
136
+ function corroborated(parse, siblings) {
137
+ if (parse.kind === "exact") return true;
138
+ if (siblings.length === 0) return false;
139
+ const siblingParses = siblings.map((s) => parseDiscName(basename(s))).filter((p) => p !== void 0);
140
+ const hasExactSibling = siblingParses.some((p) => p.kind === "exact");
141
+ const hasNumberedSibling = siblingParses.some(
142
+ (p) => p.number !== void 0
143
+ );
144
+ if (parse.number !== void 0 && hasNumberedSibling) return true;
145
+ if (parse.kind === "embedded" && hasExactSibling) return true;
146
+ if (parse.kind === "volume" && hasNumberedSibling) return true;
147
+ if (parse.kind === "bonus" || parse.kind === "bare") {
148
+ const numberedCount = siblingParses.filter((p) => p.number !== void 0).length;
149
+ if (numberedCount >= 2) return true;
150
+ if (parse.number !== void 0 && hasNumberedSibling) return true;
151
+ }
152
+ return false;
153
+ }
154
+ function flatPrefix(filename) {
155
+ const separated = filename.match(/^(\d{1,2})[\s._-](\d{1,2})/);
156
+ if (separated) return { disc: parseInt(separated[1], 10), form: "separated" };
157
+ const compact = filename.match(/^(\d)(\d{2})(?:\D|$)/);
158
+ if (compact) return { disc: parseInt(compact[1], 10), form: "compact" };
159
+ return void 0;
160
+ }
161
+ function normalizeKey(s) {
162
+ return s.trim().toLowerCase().replace(/\s+/g, " ");
163
+ }
164
+ function isGenericAlbumArtist(albumArtist) {
165
+ if (!albumArtist || albumArtist.trim() === "") return true;
166
+ const n = normalizeKey(albumArtist);
167
+ return n === "various artists" || n === "va";
168
+ }
169
+ const CONFIDENCE_RANK = {
170
+ low: 0,
171
+ medium: 1,
172
+ high: 2
173
+ };
174
+ function groupAlbums(result, options = {}) {
175
+ const minRank = CONFIDENCE_RANK[options.minFolderConfidence ?? "low"];
176
+ const useFlatPrefixes = options.flatDiscPrefixes ?? true;
177
+ const useFolderFallback = options.folderFallback ?? true;
178
+ const okItems = [];
179
+ const errors = [];
180
+ for (const item of result.items) {
181
+ if (item.status === "ok") {
182
+ okItems.push({ path: item.path, tags: item.tags, metadata: item });
183
+ } else {
184
+ errors.push({ path: item.path, error: item.error });
185
+ }
186
+ }
187
+ if (okItems.length === 0) {
188
+ return { albums: [], singles: [], unmatched: [], errors };
189
+ }
190
+ const scanRoot = options.scanRoot;
191
+ const tree = /* @__PURE__ */ new Map();
192
+ for (const item of okItems) {
193
+ const dir = dirname(item.path);
194
+ let node = tree.get(dir);
195
+ if (!node) {
196
+ node = { files: [], parent: dirname(dir) };
197
+ tree.set(dir, node);
198
+ }
199
+ node.files.push(item);
200
+ }
201
+ const confirmedDiscs = /* @__PURE__ */ new Map();
202
+ for (const [dir, node] of tree) {
203
+ if (node.files.length === 0) continue;
204
+ const parse = parseDiscName(basename(dir));
205
+ if (!parse) continue;
206
+ const siblings = [...tree.keys()].filter(
207
+ (d) => d !== dir && dirname(d) === dirname(dir)
208
+ );
209
+ const corroboratedBySiblings = corroborated(parse, siblings);
210
+ let confidence = baseConfidence(parse);
211
+ if (parse.kind === "bonus" || parse.kind === "bare") {
212
+ if (!corroboratedBySiblings) continue;
213
+ } else if (corroboratedBySiblings) {
214
+ confidence = "high";
215
+ }
216
+ if (CONFIDENCE_RANK[confidence] < minRank) continue;
217
+ confirmedDiscs.set(dir, { parse, confidence });
218
+ }
219
+ const flatSubdivisions = /* @__PURE__ */ new Map();
220
+ if (useFlatPrefixes) {
221
+ for (const [dir, node] of tree) {
222
+ if (node.files.length === 0 || confirmedDiscs.has(dir)) continue;
223
+ const byFile = /* @__PURE__ */ new Map();
224
+ const forms = /* @__PURE__ */ new Set();
225
+ for (const item of node.files) {
226
+ const prefix = flatPrefix(basename(item.path));
227
+ if (prefix) {
228
+ byFile.set(item.path, prefix.disc);
229
+ forms.add(prefix.form);
230
+ }
231
+ }
232
+ if (byFile.size < 2) continue;
233
+ const distinct = [...new Set(byFile.values())].sort((a, b) => a - b);
234
+ const allCompact = forms.size === 1 && forms.has("compact");
235
+ const plausible = allCompact ? distinct.every((d) => d >= 1 && d <= distinct.length) : true;
236
+ if (plausible) flatSubdivisions.set(dir, byFile);
237
+ }
238
+ }
239
+ const albums = /* @__PURE__ */ new Map();
240
+ const unmatched = [];
241
+ const evidenceFor = (item) => {
242
+ const dir = dirname(item.path);
243
+ const own = confirmedDiscs.get(dir);
244
+ if (own) {
245
+ return {
246
+ folderDiscNumber: own.parse.number,
247
+ total: own.parse.total,
248
+ folderDiscTitle: own.parse.discTitle,
249
+ confidence: own.confidence,
250
+ albumDir: dirname(dir)
251
+ };
252
+ }
253
+ let cursor = dirname(dir);
254
+ while (cursor !== void 0) {
255
+ const entry = confirmedDiscs.get(cursor);
256
+ if (entry) {
257
+ return {
258
+ folderDiscNumber: entry.parse.number,
259
+ total: entry.parse.total,
260
+ folderDiscTitle: entry.parse.discTitle,
261
+ confidence: entry.confidence,
262
+ albumDir: dirname(cursor)
263
+ };
264
+ }
265
+ const parent = dirname(cursor);
266
+ if (parent === cursor) break;
267
+ cursor = parent;
268
+ }
269
+ const flat = flatSubdivisions.get(dir)?.get(item.path);
270
+ if (flat !== void 0) {
271
+ return {
272
+ folderDiscNumber: flat,
273
+ total: void 0,
274
+ folderDiscTitle: void 0,
275
+ confidence: "medium",
276
+ albumDir: dir
277
+ };
278
+ }
279
+ return void 0;
280
+ };
281
+ const folderIdentity = (dir) => {
282
+ const own = confirmedDiscs.get(dir);
283
+ if (!own) {
284
+ const title2 = basename(dir);
285
+ return title2 ? { title: title2, titleSource: dir } : void 0;
286
+ }
287
+ if (own.parse.title !== void 0) {
288
+ return { title: own.parse.title, titleSource: dirname(dir) };
289
+ }
290
+ const source = dirname(dir);
291
+ if (source === scanRoot || source === dir) return void 0;
292
+ const title = basename(source);
293
+ return title ? { title, titleSource: source } : void 0;
294
+ };
295
+ for (const item of okItems) {
296
+ const dir = dirname(item.path);
297
+ const albumTag = item.tags.album?.[0]?.trim();
298
+ const albumArtistTag = item.tags.albumArtist?.[0]?.trim();
299
+ if (albumTag) {
300
+ const artistPart = isGenericAlbumArtist(albumArtistTag) ? "" : normalizeKey(albumArtistTag ?? "");
301
+ const key2 = `${artistPart}::${normalizeKey(albumTag)}`;
302
+ let acc2 = albums.get(key2);
303
+ if (!acc2) {
304
+ acc2 = {
305
+ key: key2,
306
+ album: albumTag,
307
+ albumArtist: isGenericAlbumArtist(albumArtistTag) ? void 0 : albumArtistTag,
308
+ source: "tags",
309
+ directory: void 0,
310
+ items: [],
311
+ evidence: /* @__PURE__ */ new Map()
312
+ };
313
+ albums.set(key2, acc2);
314
+ }
315
+ const evidence2 = evidenceFor(item);
316
+ acc2.evidence.set(item.path, evidence2);
317
+ acc2.items.push({
318
+ ...item.metadata,
319
+ albumDir: evidence2?.albumDir ?? dir,
320
+ discNumber: evidence2?.folderDiscNumber
321
+ });
322
+ continue;
323
+ }
324
+ if (!useFolderFallback) {
325
+ unmatched.push(item.metadata);
326
+ continue;
327
+ }
328
+ const identity = folderIdentity(dir);
329
+ if (!identity) {
330
+ unmatched.push(item.metadata);
331
+ continue;
332
+ }
333
+ const key = `${identity.titleSource}::${normalizeKey(identity.title)}`;
334
+ let acc = albums.get(key);
335
+ if (!acc) {
336
+ acc = {
337
+ key,
338
+ album: identity.title,
339
+ albumArtist: void 0,
340
+ source: "folder",
341
+ directory: identity.titleSource,
342
+ items: [],
343
+ evidence: /* @__PURE__ */ new Map()
344
+ };
345
+ albums.set(key, acc);
346
+ }
347
+ const evidence = evidenceFor(item);
348
+ acc.evidence.set(item.path, evidence);
349
+ acc.items.push({
350
+ ...item.metadata,
351
+ albumDir: evidence?.albumDir ?? dir,
352
+ discNumber: evidence?.folderDiscNumber
353
+ });
354
+ }
355
+ for (const [key, acc] of [...albums]) {
356
+ if (acc.source === "tags") continue;
357
+ for (const other of albums.values()) {
358
+ if (other === acc || other.source !== "tags") continue;
359
+ if (normalizeKey(other.album) !== normalizeKey(acc.album)) continue;
360
+ const albumDir = acc.directory;
361
+ const sharesChain = albumDir !== void 0 && other.items.some((o) => {
362
+ const od = dirname(o.path);
363
+ return od === albumDir || od.startsWith(albumDir + "/");
364
+ });
365
+ if (!sharesChain) continue;
366
+ for (const item of acc.items) {
367
+ other.items.push(item);
368
+ other.evidence.set(item.path, acc.evidence.get(item.path));
369
+ }
370
+ albums.delete(key);
371
+ break;
372
+ }
373
+ }
374
+ const groups = [];
375
+ for (const acc of albums.values()) {
376
+ const dirTagCommon = /* @__PURE__ */ new Map();
377
+ const resolvedByFile = /* @__PURE__ */ new Map();
378
+ for (const item of acc.items) {
379
+ const dir = dirname(item.path);
380
+ if (!dirTagCommon.has(dir)) {
381
+ const tagged = acc.items.filter((i) => dirname(i.path) === dir).map((i) => i.tags.discNumber).filter((d) => d !== void 0 && d > 0);
382
+ dirTagCommon.set(
383
+ dir,
384
+ tagged.length > 0 && tagged.every((d) => d === tagged[0]) ? tagged[0] : void 0
385
+ );
386
+ }
387
+ const evidence = acc.evidence.get(item.path);
388
+ const tagCommon = dirTagCommon.get(dir);
389
+ resolvedByFile.set(
390
+ item.path,
391
+ tagCommon ?? evidence?.folderDiscNumber
392
+ );
393
+ }
394
+ const discs = /* @__PURE__ */ new Map();
395
+ const order = [];
396
+ const noEvidenceItems = [];
397
+ for (const item of acc.items) {
398
+ const evidence = acc.evidence.get(item.path);
399
+ if (evidence === void 0) {
400
+ noEvidenceItems.push(item);
401
+ continue;
402
+ }
403
+ const resolved = resolvedByFile.get(item.path);
404
+ const discKey = resolved === void 0 ? `dir:${dirname(item.path)}` : String(resolved);
405
+ let disc = discs.get(discKey);
406
+ if (!disc) {
407
+ disc = {
408
+ discNumber: resolved,
409
+ totalDiscs: evidence.total,
410
+ folderDiscNumber: evidence.folderDiscNumber,
411
+ folderDiscTitle: evidence.folderDiscTitle,
412
+ tagDiscNumber: void 0,
413
+ confidence: evidence.confidence,
414
+ items: []
415
+ };
416
+ discs.set(discKey, disc);
417
+ order.push(discKey);
418
+ }
419
+ disc.items.push(item);
420
+ }
421
+ if (noEvidenceItems.length > 0) {
422
+ const numbered = [...discs.values()].filter((d) => d.folderDiscNumber !== void 0).sort((a, b) => a.folderDiscNumber - b.folderDiscNumber);
423
+ if (numbered.length > 0) {
424
+ numbered[0].items.push(...noEvidenceItems);
425
+ } else {
426
+ const key = "dir:__none__";
427
+ let disc = discs.get(key);
428
+ if (!disc) {
429
+ disc = {
430
+ discNumber: void 0,
431
+ totalDiscs: void 0,
432
+ folderDiscNumber: void 0,
433
+ folderDiscTitle: void 0,
434
+ tagDiscNumber: void 0,
435
+ confidence: "high",
436
+ items: []
437
+ };
438
+ discs.set(key, disc);
439
+ order.push(key);
440
+ }
441
+ disc.items.push(...noEvidenceItems);
442
+ }
443
+ }
444
+ for (const disc of discs.values()) {
445
+ const tagged = disc.items.map((i) => i.tags.discNumber).filter((d) => d !== void 0 && d > 0);
446
+ const common = tagged.length > 0 && tagged.every((d) => d === tagged[0]) ? tagged[0] : void 0;
447
+ disc.tagDiscNumber = common;
448
+ disc.discNumber = common ?? disc.folderDiscNumber;
449
+ for (const item of disc.items) item.discNumber = disc.discNumber;
450
+ }
451
+ const albumTaggedTotals = acc.items.map((i) => i.tags.totalDiscs).filter((t) => t !== void 0 && t > 0);
452
+ const commonTotal = albumTaggedTotals.length > 0 && albumTaggedTotals.every((t) => t === albumTaggedTotals[0]) ? albumTaggedTotals[0] : void 0;
453
+ const ofN = [...discs.values()].map((d) => d.totalDiscs).find(
454
+ (t) => t !== void 0
455
+ );
456
+ const maxSibling = [...discs.values()].map((d) => d.discNumber).filter((n) => n !== void 0).reduce((a, b) => Math.max(a, b), 0) || void 0;
457
+ for (const disc of discs.values()) {
458
+ disc.totalDiscs = commonTotal ?? ofN ?? maxSibling;
459
+ }
460
+ const sortedDiscs = [...discs.values()].sort((a, b) => {
461
+ const an = a.discNumber === void 0 ? -1 : a.discNumber;
462
+ const bn = b.discNumber === void 0 ? -1 : b.discNumber;
463
+ if (an !== bn) return an - bn;
464
+ return (a.folderDiscTitle ?? "").localeCompare(b.folderDiscTitle ?? "");
465
+ });
466
+ for (const disc of sortedDiscs) {
467
+ disc.items.sort((a, b) => {
468
+ const at = a.tags.track ?? Infinity;
469
+ const bt = b.tags.track ?? Infinity;
470
+ if (at !== bt) return at - bt;
471
+ return a.path.localeCompare(b.path);
472
+ });
473
+ }
474
+ const flags = acc.items.map((i) => i.tags.compilation).filter((c) => c !== void 0);
475
+ const compilation = flags.length > 0 && flags.every((f) => f === flags[0]) ? flags[0] : void 0;
476
+ const albumDirs = new Set(acc.items.map((i) => i.albumDir));
477
+ const directory = albumDirs.size === 1 ? [...albumDirs][0] : acc.directory;
478
+ groups.push({
479
+ key: acc.key,
480
+ albumArtist: acc.albumArtist,
481
+ album: acc.album,
482
+ source: acc.source,
483
+ compilation,
484
+ directory,
485
+ discs: sortedDiscs,
486
+ items: sortedDiscs.flatMap((d) => d.items)
487
+ });
488
+ }
489
+ const albumsOut = [];
490
+ const singles = [];
491
+ for (const group of groups) {
492
+ if (group.items.length === 1) {
493
+ singles.push(group.items[0]);
494
+ } else {
495
+ albumsOut.push(group);
496
+ }
497
+ }
498
+ albumsOut.sort(
499
+ (a, b) => (a.albumArtist ?? "").localeCompare(b.albumArtist ?? "") || (a.album ?? "").localeCompare(b.album ?? "")
500
+ );
501
+ return { albums: albumsOut, singles, unmatched, errors };
502
+ }
503
+ export {
504
+ groupAlbums
505
+ };
@@ -5,4 +5,6 @@
5
5
  export type { AudioDynamics, AudioFileMetadata, DuplicateGroup, FolderScanItem, FolderScanOptions, FolderScanResult, FolderUpdateItem, FolderUpdateResult, } from "./types.js";
6
6
  export { scanFolder } from "./scan-operations.js";
7
7
  export { exportFolderMetadata, findDuplicates, updateFolderTags, } from "./folder-operations.js";
8
+ export { type AlbumDisc, type AlbumGroup, type AlbumGroupingResult, type AlbumGroupItem, type AlbumGroupKey, type DiscConfidence, groupAlbums, type GroupAlbumsOptions, } from "./group-albums.js";
9
+ export { scanForAlbums, type ScanForAlbumsOptions } from "./scan-for-albums.js";
8
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/folder-api/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,gBAAgB,GACjB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/folder-api/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,WAAW,EACX,KAAK,kBAAkB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -4,9 +4,15 @@ import {
4
4
  findDuplicates,
5
5
  updateFolderTags
6
6
  } from "./folder-operations.js";
7
+ import {
8
+ groupAlbums
9
+ } from "./group-albums.js";
10
+ import { scanForAlbums } from "./scan-for-albums.js";
7
11
  export {
8
12
  exportFolderMetadata,
9
13
  findDuplicates,
14
+ groupAlbums,
10
15
  scanFolder,
16
+ scanForAlbums,
11
17
  updateFolderTags
12
18
  };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @fileoverview Thin async wrapper: scan a folder, then group the scan into
3
+ * albums. Deno/Node/Bun only, like scanFolder — groupAlbums itself is pure
4
+ * and runtime-agnostic.
5
+ */
6
+ import type { FolderScanOptions } from "./types.js";
7
+ import { type AlbumGroupingResult, type GroupAlbumsOptions } from "./group-albums.js";
8
+ export type ScanForAlbumsOptions = FolderScanOptions & GroupAlbumsOptions;
9
+ /**
10
+ * Scan a folder and group the result into albums with disc subdivisions.
11
+ *
12
+ * Forwards every {@link FolderScanOptions} (recursive, extensions, maxFiles,
13
+ * onProgress, includeProperties, continueOnError, criteria, signal) to the
14
+ * scan; grouping options (minFolderConfidence, flatDiscPrefixes,
15
+ * folderFallback) apply to {@link groupAlbums}.
16
+ *
17
+ * @throws the same errors scanFolder throws (permission, missing path, abort).
18
+ */
19
+ export declare function scanForAlbums(folderPath: string, options?: ScanForAlbumsOptions): Promise<AlbumGroupingResult>;
20
+ //# sourceMappingURL=scan-for-albums.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-for-albums.d.ts","sourceRoot":"","sources":["../../../src/folder-api/scan-for-albums.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EACL,KAAK,mBAAmB,EAExB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,oBAAoB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAE1E;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAc9B"}
@@ -0,0 +1,22 @@
1
+ import { scanFolder } from "./scan-operations.js";
2
+ import {
3
+ groupAlbums
4
+ } from "./group-albums.js";
5
+ async function scanForAlbums(folderPath, options = {}) {
6
+ const {
7
+ minFolderConfidence,
8
+ flatDiscPrefixes,
9
+ folderFallback,
10
+ ...scanOptions
11
+ } = options;
12
+ const scan = await scanFolder(folderPath, scanOptions);
13
+ return groupAlbums(scan, {
14
+ minFolderConfidence,
15
+ flatDiscPrefixes,
16
+ folderFallback,
17
+ scanRoot: folderPath
18
+ });
19
+ }
20
+ export {
21
+ scanForAlbums
22
+ };
@@ -32,4 +32,15 @@ export interface LoadTagLibOptions {
32
32
  */
33
33
  disableOptimizations?: boolean;
34
34
  }
35
+ /**
36
+ * Describe why `wasmBinary` is not a WebAssembly module, or `null` if it is.
37
+ *
38
+ * Both loader entry points validate this before handing the buffer to
39
+ * Emscripten. Left to the glue, a bad buffer surfaces as an asynchronous
40
+ * `Aborted(CompileError: ...)` written straight to the console AFTER the
41
+ * returned promise settles — so a caller can neither attach it to their own
42
+ * failure nor suppress it. The two entry points raise different error types,
43
+ * hence a shared predicate rather than a shared throw.
44
+ */
45
+ export declare function describeNonWasmBinary(binary: ArrayBuffer | Uint8Array): string | null;
35
46
  //# sourceMappingURL=loader-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader-types.d.ts","sourceRoot":"","sources":["../../../src/runtime/loader-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IAEtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAEtC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC"}
1
+ {"version":3,"file":"loader-types.d.ts","sourceRoot":"","sources":["../../../src/runtime/loader-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IAEtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAEtC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,GAAG,UAAU,GAC/B,MAAM,GAAG,IAAI,CASf"}
@@ -0,0 +1,10 @@
1
+ function describeNonWasmBinary(binary) {
2
+ const bytes = binary instanceof Uint8Array ? binary : new Uint8Array(binary);
3
+ const MAGIC = [0, 97, 115, 109];
4
+ if (bytes.length >= 4 && MAGIC.every((b, i) => bytes[i] === b)) return null;
5
+ const found = Array.from(bytes.slice(0, 4)).map((b) => b.toString(16).padStart(2, "0")).join(" ");
6
+ return `wasmBinary is not a WebAssembly module: expected magic bytes 00 61 73 6d, found ${found || "(empty)"}. Size: ${bytes.length} bytes`;
7
+ }
8
+ export {
9
+ describeNonWasmBinary
10
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"module-loader.d.ts","sourceRoot":"","sources":["../../../src/runtime/module-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAS/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAqEvB"}
1
+ {"version":3,"file":"module-loader.d.ts","sourceRoot":"","sources":["../../../src/runtime/module-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAS/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA8EvB"}
@@ -1,3 +1,4 @@
1
+ import { describeNonWasmBinary } from "./loader-types.js";
1
2
  import {
2
3
  EnvironmentError,
3
4
  errorMessage,
@@ -18,6 +19,14 @@ async function loadTagLibModule(options) {
18
19
  { forceWasmType: "wasi" }
19
20
  );
20
21
  }
22
+ if (options?.wasmBinary) {
23
+ const problem = describeNonWasmBinary(options.wasmBinary);
24
+ if (problem) {
25
+ throw new TagLibInitializationError(problem, {
26
+ byteLength: options.wasmBinary.byteLength
27
+ });
28
+ }
29
+ }
21
30
  if (!options?.wasmBinary && !options?.wasmUrl && !options?.forceWasmType && isDenoCompiled()) {
22
31
  const wasmBinary = await tryLoadEmbeddedWasm();
23
32
  if (!wasmBinary) {
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../src/runtime/unified-loader/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAK5E,wBAAsB,uBAAuB,CAC3C,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA4C9B"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../src/runtime/unified-loader/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAM5E,wBAAsB,uBAAuB,CAC3C,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAiD9B"}
@@ -1,5 +1,6 @@
1
1
  import { detectRuntime } from "../detector.js";
2
2
  import { ModuleLoadError } from "./types.js";
3
+ import { describeNonWasmBinary } from "../loader-types.js";
3
4
  import { selectWasmType } from "./module-selection.js";
4
5
  import { loadModule } from "./module-loading.js";
5
6
  async function loadUnifiedTagLibModule(options = {}) {
@@ -9,6 +10,10 @@ async function loadUnifiedTagLibModule(options = {}) {
9
10
  "wasi"
10
11
  );
11
12
  }
13
+ if (options.wasmBinary) {
14
+ const problem = describeNonWasmBinary(options.wasmBinary);
15
+ if (problem) throw new ModuleLoadError(problem, "emscripten");
16
+ }
12
17
  const startTime = performance.now();
13
18
  const runtime = detectRuntime();
14
19
  if (options.debug) {
@@ -1 +1 @@
1
- {"version":3,"file":"file-handle.d.ts","sourceRoot":"","sources":["../../../../src/runtime/wasi-adapter/file-handle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file-handle.d.ts","sourceRoot":"","sources":["../../../../src/runtime/wasi-adapter/file-handle.ts"],"names":[],"mappings":"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"}
@@ -54,6 +54,15 @@ const INTERNAL_KEYS = /* @__PURE__ */ new Set([
54
54
  // Exact MP4 atom names for the write path, not a readable property.
55
55
  "_mp4ItemNames"
56
56
  ]);
57
+ function preserveEmptyValues(data) {
58
+ for (const [key, value] of Object.entries(data)) {
59
+ if (value !== "") continue;
60
+ if (AUDIO_KEYS.has(key) || INTERNAL_KEYS.has(key)) continue;
61
+ if (key.startsWith("----:")) continue;
62
+ data[key] = [""];
63
+ }
64
+ return data;
65
+ }
57
66
  const CONTAINER_TO_FORMAT = {
58
67
  MP3: "MP3",
59
68
  MP4: "MP4",
@@ -92,14 +101,18 @@ class WasiFileHandle {
92
101
  this.checkNotDestroyed();
93
102
  this.fileData = buffer;
94
103
  const msgpackData = readTagsFromWasm(this.wasi, buffer);
95
- this.tagData = decodeTagData(msgpackData);
104
+ this.tagData = preserveEmptyValues(
105
+ decodeTagData(msgpackData)
106
+ );
96
107
  return true;
97
108
  }
98
109
  loadFromPath(path) {
99
110
  this.checkNotDestroyed();
100
111
  this.filePath = path;
101
112
  const msgpackData = readTagsFromWasmPath(this.wasi, path);
102
- this.tagData = decodeTagData(msgpackData);
113
+ this.tagData = preserveEmptyValues(
114
+ decodeTagData(msgpackData)
115
+ );
103
116
  return true;
104
117
  }
105
118
  isValid() {
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "1.6.1";
1
+ export declare const VERSION = "1.7.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,4 +1,4 @@
1
- const VERSION = "1.6.1";
1
+ const VERSION = "1.7.0";
2
2
  export {
3
3
  VERSION
4
4
  };
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taglib-wasm",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "TagLib-Wasm is the universal tagging library for TypeScript/JavaScript platforms: Browsers, Node.js, Deno, Bun, Cloudflare Workers, and Electron apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",