taglib-wasm 0.4.1 → 0.4.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/README.md CHANGED
@@ -43,6 +43,8 @@ TagLib itself is legendary, and a core dependency of many music apps.
43
43
  reliability
44
44
  - **✅ Two API styles** – Use the “Simple” API (3 functions), or the full “Core”
45
45
  API for more advanced applications
46
+ - **✅ Batch folder operations** – Scan directories, process multiple files,
47
+ find duplicates, and export metadata catalogs
46
48
 
47
49
  ## 📦 Installation
48
50
 
@@ -167,6 +169,36 @@ file.save();
167
169
  file.dispose();
168
170
  ```
169
171
 
172
+ ### Batch Folder Operations
173
+
174
+ Process entire music collections efficiently:
175
+
176
+ ```typescript
177
+ import { findDuplicates, scanFolder } from "taglib-wasm/folder";
178
+
179
+ // Scan a music library
180
+ const result = await scanFolder("/path/to/music", {
181
+ recursive: true,
182
+ concurrency: 4,
183
+ onProgress: (processed, total, file) => {
184
+ console.log(`Processing ${processed}/${total}: ${file}`);
185
+ },
186
+ });
187
+
188
+ console.log(`Found ${result.totalFound} audio files`);
189
+ console.log(`Successfully processed ${result.totalProcessed} files`);
190
+
191
+ // Process results
192
+ for (const file of result.files) {
193
+ console.log(`${file.path}: ${file.tags.artist} - ${file.tags.title}`);
194
+ console.log(`Duration: ${file.properties?.duration}s`);
195
+ }
196
+
197
+ // Find duplicates
198
+ const duplicates = await findDuplicates("/path/to/music", ["artist", "title"]);
199
+ console.log(`Found ${duplicates.size} groups of duplicates`);
200
+ ```
201
+
170
202
  ### Working with Cover Art
171
203
 
172
204
  ```typescript
package/dist/index.d.ts CHANGED
@@ -79,6 +79,14 @@ export { FormatMappings, getAllTagNames, isValidTagName, Tags, } from "./src/con
79
79
  * @see {@link copyCoverArt} - Copy cover art between files
80
80
  */
81
81
  export { copyCoverArt, exportAllPictures, exportCoverArt, exportPictureByType, findCoverArtFiles, importCoverArt, importPictureWithType, loadPictureFromFile, savePictureToFile, } from "./src/file-utils.ts";
82
+ /**
83
+ * Folder/batch operations for processing multiple audio files.
84
+ * @see {@link scanFolder} - Scan folder for audio files and read metadata
85
+ * @see {@link updateFolderTags} - Update tags for multiple files
86
+ * @see {@link findDuplicates} - Find duplicate files based on metadata
87
+ * @see {@link exportFolderMetadata} - Export folder metadata to JSON
88
+ */
89
+ export { type AudioFileMetadata, exportFolderMetadata, findDuplicates, type FolderScanOptions, type FolderScanResult, scanFolder, updateFolderTags, } from "./src/folder-api.ts";
82
90
  /**
83
91
  * Web browser utilities for cover art operations.
84
92
  * @see {@link pictureToDataURL} - Convert picture to data URL
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;GAKG;AACH,OAAO,EACL,aAAa,IAAI,SAAS,EAC1B,YAAY,EACZ,MAAM,GACP,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;GAUG;AACH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,aAAa,EACb,wBAAwB,EACxB,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;GAWG;AACH,OAAO,EACL,UAAU,EACV,aAAa,EACb,SAAS,EACT,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,oBAAoB,EACpB,WAAW,EACX,UAAU,GACX,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EACd,IAAI,GACL,MAAM,oBAAoB,CAAC;AAC5B;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAE7B;;;;;GAKG;AACH,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;GAQG;AACH,YAAY,EACV,WAAW,EACX,eAAe,EACf,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,WAAW,EACX,GAAG,EACH,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB;;GAEG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C;;;;GAIG;AACH,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,UAAU,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IAEtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA0BvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;GAKG;AACH,OAAO,EACL,aAAa,IAAI,SAAS,EAC1B,YAAY,EACZ,MAAM,GACP,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;GAUG;AACH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,aAAa,EACb,wBAAwB,EACxB,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;GAWG;AACH,OAAO,EACL,UAAU,EACV,aAAa,EACb,SAAS,EACT,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,oBAAoB,EACpB,WAAW,EACX,UAAU,GACX,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EACd,IAAI,GACL,MAAM,oBAAoB,CAAC;AAC5B;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;GAMG;AACH,OAAO,EACL,KAAK,iBAAiB,EACtB,oBAAoB,EACpB,cAAc,EACd,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,UAAU,EACV,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B;;;;;GAKG;AACH,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;GAQG;AACH,YAAY,EACV,WAAW,EACX,eAAe,EACf,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,WAAW,EACX,GAAG,EACH,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB;;GAEG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C;;;;GAIG;AACH,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,UAAU,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IAEtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA0BvB"}
package/dist/index.js CHANGED
@@ -57,6 +57,12 @@ import {
57
57
  loadPictureFromFile,
58
58
  savePictureToFile
59
59
  } from "./src/file-utils.js";
60
+ import {
61
+ exportFolderMetadata,
62
+ findDuplicates,
63
+ scanFolder,
64
+ updateFolderTags
65
+ } from "./src/folder-api.js";
60
66
  import {
61
67
  canvasToPicture,
62
68
  createPictureDownloadURL,
@@ -114,8 +120,10 @@ export {
114
120
  displayPicture,
115
121
  exportAllPictures,
116
122
  exportCoverArt,
123
+ exportFolderMetadata,
117
124
  exportPictureByType,
118
125
  findCoverArtFiles,
126
+ findDuplicates,
119
127
  findPictureByType,
120
128
  getAllTagNames,
121
129
  getCoverArt,
@@ -141,7 +149,9 @@ export {
141
149
  readTags,
142
150
  replacePictureByType,
143
151
  savePictureToFile,
152
+ scanFolder,
144
153
  setCoverArt,
145
154
  setCoverArtFromCanvas,
155
+ updateFolderTags,
146
156
  updateTags
147
157
  };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Batch folder operations for taglib-wasm
3
+ * Provides APIs for scanning directories and processing multiple audio files
4
+ */
5
+ import { type Tag } from "./simple.ts";
6
+ import type { AudioProperties } from "./types.ts";
7
+ /**
8
+ * Metadata for a single audio file including path information
9
+ */
10
+ export interface AudioFileMetadata {
11
+ /** Absolute or relative path to the audio file */
12
+ path: string;
13
+ /** Basic tag information (title, artist, album, etc.) */
14
+ tags: Tag;
15
+ /** Audio properties (duration, bitrate, sample rate, etc.) */
16
+ properties?: AudioProperties;
17
+ /** Any errors encountered while reading this file */
18
+ error?: Error;
19
+ }
20
+ /**
21
+ * Options for scanning folders
22
+ */
23
+ export interface FolderScanOptions {
24
+ /** Whether to scan subdirectories recursively (default: true) */
25
+ recursive?: boolean;
26
+ /** File extensions to include (default: common audio formats) */
27
+ extensions?: string[];
28
+ /** Maximum number of files to process (default: unlimited) */
29
+ maxFiles?: number;
30
+ /** Progress callback called after each file is processed */
31
+ onProgress?: (processed: number, total: number, currentFile: string) => void;
32
+ /** Whether to include audio properties (default: true) */
33
+ includeProperties?: boolean;
34
+ /** Whether to continue on errors (default: true) */
35
+ continueOnError?: boolean;
36
+ /** Number of files to process in parallel (default: 4) */
37
+ concurrency?: number;
38
+ }
39
+ /**
40
+ * Result of a folder scan operation
41
+ */
42
+ export interface FolderScanResult {
43
+ /** Successfully processed files with metadata */
44
+ files: AudioFileMetadata[];
45
+ /** Files that failed to process */
46
+ errors: Array<{
47
+ path: string;
48
+ error: Error;
49
+ }>;
50
+ /** Total number of audio files found */
51
+ totalFound: number;
52
+ /** Total number of files successfully processed */
53
+ totalProcessed: number;
54
+ /** Time taken in milliseconds */
55
+ duration: number;
56
+ }
57
+ /**
58
+ * Scan a folder and read metadata from all audio files
59
+ *
60
+ * @param folderPath - Path to the folder to scan
61
+ * @param options - Scanning options
62
+ * @returns Metadata for all audio files found
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * // Scan a folder recursively
67
+ * const result = await scanFolder("/path/to/music");
68
+ * console.log(`Found ${result.totalFound} audio files`);
69
+ * console.log(`Successfully processed ${result.totalProcessed} files`);
70
+ *
71
+ * // Process results
72
+ * for (const file of result.files) {
73
+ * console.log(`${file.path}: ${file.tags.artist} - ${file.tags.title}`);
74
+ * }
75
+ *
76
+ * // Scan with options
77
+ * const result2 = await scanFolder("/path/to/music", {
78
+ * recursive: false,
79
+ * extensions: [".mp3", ".flac"],
80
+ * onProgress: (processed, total, file) => {
81
+ * console.log(`Processing ${processed}/${total}: ${file}`);
82
+ * }
83
+ * });
84
+ * ```
85
+ */
86
+ export declare function scanFolder(folderPath: string, options?: FolderScanOptions): Promise<FolderScanResult>;
87
+ /**
88
+ * Update metadata for multiple files in a folder
89
+ *
90
+ * @param updates - Array of objects containing path and tags to update
91
+ * @param options - Update options
92
+ * @returns Results of the update operation
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // Update multiple files
97
+ * const updates = [
98
+ * { path: "/music/song1.mp3", tags: { artist: "New Artist" } },
99
+ * { path: "/music/song2.mp3", tags: { album: "New Album" } }
100
+ * ];
101
+ *
102
+ * const result = await updateFolderTags(updates);
103
+ * console.log(`Updated ${result.successful} files`);
104
+ * ```
105
+ */
106
+ export declare function updateFolderTags(updates: Array<{
107
+ path: string;
108
+ tags: Partial<Tag>;
109
+ }>, options?: {
110
+ continueOnError?: boolean;
111
+ concurrency?: number;
112
+ }): Promise<{
113
+ successful: number;
114
+ failed: Array<{
115
+ path: string;
116
+ error: Error;
117
+ }>;
118
+ duration: number;
119
+ }>;
120
+ /**
121
+ * Find duplicate audio files based on metadata
122
+ *
123
+ * @param folderPath - Path to scan for duplicates
124
+ * @param criteria - Which fields to compare (default: artist and title)
125
+ * @returns Groups of potential duplicate files
126
+ */
127
+ export declare function findDuplicates(folderPath: string, criteria?: Array<keyof Tag>): Promise<Map<string, AudioFileMetadata[]>>;
128
+ /**
129
+ * Export metadata from a folder to JSON
130
+ *
131
+ * @param folderPath - Path to scan
132
+ * @param outputPath - Where to save the JSON file
133
+ * @param options - Scan options
134
+ */
135
+ export declare function exportFolderMetadata(folderPath: string, outputPath: string, options?: FolderScanOptions): Promise<void>;
136
+ //# sourceMappingURL=folder-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"folder-api.d.ts","sourceRoot":"","sources":["../../src/folder-api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAuB,KAAK,GAAG,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAgBlD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,IAAI,EAAE,GAAG,CAAC;IACV,8DAA8D;IAC9D,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,qDAAqD;IACrD,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7E,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oDAAoD;IACpD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,mCAAmC;IACnC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAC9C,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAgHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAsF3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAA;CAAE,CAAC,EACpD,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAChE,OAAO,CAAC;IACT,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CA0CD;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,KAAK,CAAC,MAAM,GAAG,CAAuB,GAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC,CA4B3C;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAuBf"}
@@ -0,0 +1,242 @@
1
+ import { TagLib } from "./taglib.js";
2
+ import { applyTags, readTags } from "./simple.js";
3
+ function join(...paths) {
4
+ return paths.filter((p) => p).join("/").replace(/\/+/g, "/");
5
+ }
6
+ function extname(path) {
7
+ const lastDot = path.lastIndexOf(".");
8
+ if (lastDot === -1 || lastDot === path.length - 1) return "";
9
+ return path.slice(lastDot);
10
+ }
11
+ const DEFAULT_AUDIO_EXTENSIONS = [
12
+ ".mp3",
13
+ ".m4a",
14
+ ".mp4",
15
+ ".flac",
16
+ ".ogg",
17
+ ".oga",
18
+ ".opus",
19
+ ".wav",
20
+ ".wv",
21
+ ".ape",
22
+ ".mpc",
23
+ ".tta",
24
+ ".wma"
25
+ ];
26
+ async function* walkDirectory(path, options = {}) {
27
+ const { recursive = true, extensions = DEFAULT_AUDIO_EXTENSIONS } = options;
28
+ if (typeof Deno !== "undefined") {
29
+ for await (const entry of Deno.readDir(path)) {
30
+ const fullPath = join(path, entry.name);
31
+ if (entry.isDirectory && recursive) {
32
+ yield* walkDirectory(fullPath, options);
33
+ } else if (entry.isFile) {
34
+ const ext = extname(entry.name).toLowerCase();
35
+ if (extensions.includes(ext)) {
36
+ yield fullPath;
37
+ }
38
+ }
39
+ }
40
+ } else if (typeof globalThis.process !== "undefined" && globalThis.process.versions?.node) {
41
+ const fs = await import("fs/promises");
42
+ const entries = await fs.readdir(path, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ const fullPath = join(path, entry.name);
45
+ if (entry.isDirectory() && recursive) {
46
+ yield* walkDirectory(fullPath, options);
47
+ } else if (entry.isFile()) {
48
+ const ext = extname(entry.name).toLowerCase();
49
+ if (extensions.includes(ext)) {
50
+ yield fullPath;
51
+ }
52
+ }
53
+ }
54
+ } else if (typeof globalThis.process !== "undefined" && globalThis.process.versions?.bun) {
55
+ const fs = await import("fs/promises");
56
+ const entries = await fs.readdir(path, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ const fullPath = join(path, entry.name);
59
+ if (entry.isDirectory() && recursive) {
60
+ yield* walkDirectory(fullPath, options);
61
+ } else if (entry.isFile()) {
62
+ const ext = extname(entry.name).toLowerCase();
63
+ if (extensions.includes(ext)) {
64
+ yield fullPath;
65
+ }
66
+ }
67
+ }
68
+ } else {
69
+ throw new Error("Directory scanning not supported in this runtime");
70
+ }
71
+ }
72
+ async function processBatch(files, processor, concurrency) {
73
+ const results = [];
74
+ const executing = [];
75
+ for (const file of files) {
76
+ const promise = processor(file).then((result) => {
77
+ results.push(result);
78
+ });
79
+ executing.push(promise);
80
+ if (executing.length >= concurrency) {
81
+ await Promise.race(executing);
82
+ executing.splice(executing.findIndex((p) => p === promise), 1);
83
+ }
84
+ }
85
+ await Promise.all(executing);
86
+ return results;
87
+ }
88
+ async function scanFolder(folderPath, options = {}) {
89
+ const startTime = Date.now();
90
+ const {
91
+ maxFiles = Infinity,
92
+ includeProperties = true,
93
+ continueOnError = true,
94
+ concurrency = 4,
95
+ onProgress
96
+ } = options;
97
+ const files = [];
98
+ const errors = [];
99
+ const filePaths = [];
100
+ let fileCount = 0;
101
+ for await (const filePath of walkDirectory(folderPath, options)) {
102
+ filePaths.push(filePath);
103
+ fileCount++;
104
+ if (fileCount >= maxFiles) break;
105
+ }
106
+ const totalFound = filePaths.length;
107
+ let processed = 0;
108
+ const taglib = await TagLib.initialize();
109
+ try {
110
+ const processor = async (filePath) => {
111
+ try {
112
+ const tags = await readTags(filePath);
113
+ let properties;
114
+ if (includeProperties) {
115
+ const audioFile = await taglib.open(filePath);
116
+ try {
117
+ const props = audioFile.audioProperties();
118
+ if (props) {
119
+ properties = props;
120
+ }
121
+ } finally {
122
+ audioFile.dispose();
123
+ }
124
+ }
125
+ processed++;
126
+ onProgress?.(processed, totalFound, filePath);
127
+ return { path: filePath, tags, properties };
128
+ } catch (error) {
129
+ const err = error instanceof Error ? error : new Error(String(error));
130
+ if (continueOnError) {
131
+ errors.push({ path: filePath, error: err });
132
+ processed++;
133
+ onProgress?.(processed, totalFound, filePath);
134
+ return { path: filePath, tags: {}, error: err };
135
+ } else {
136
+ throw err;
137
+ }
138
+ }
139
+ };
140
+ const batchSize = concurrency * 10;
141
+ for (let i = 0; i < filePaths.length; i += batchSize) {
142
+ const batch = filePaths.slice(
143
+ i,
144
+ Math.min(i + batchSize, filePaths.length)
145
+ );
146
+ const batchResults = await processBatch(batch, processor, concurrency);
147
+ files.push(...batchResults.filter((r) => !r.error));
148
+ }
149
+ } finally {
150
+ }
151
+ return {
152
+ files,
153
+ errors,
154
+ totalFound,
155
+ totalProcessed: processed,
156
+ duration: Date.now() - startTime
157
+ };
158
+ }
159
+ async function updateFolderTags(updates, options = {}) {
160
+ const startTime = Date.now();
161
+ const { continueOnError = true, concurrency = 4 } = options;
162
+ let successful = 0;
163
+ const failed = [];
164
+ const processor = async (update) => {
165
+ try {
166
+ await applyTags(update.path, update.tags);
167
+ successful++;
168
+ } catch (error) {
169
+ const err = error instanceof Error ? error : new Error(String(error));
170
+ if (continueOnError) {
171
+ failed.push({ path: update.path, error: err });
172
+ } else {
173
+ throw err;
174
+ }
175
+ }
176
+ };
177
+ const batchSize = concurrency * 10;
178
+ for (let i = 0; i < updates.length; i += batchSize) {
179
+ const batch = updates.slice(i, Math.min(i + batchSize, updates.length));
180
+ await processBatch(
181
+ batch.map((u) => u.path),
182
+ async (path) => {
183
+ const update = batch.find((u) => u.path === path);
184
+ await processor(update);
185
+ return { path, tags: {} };
186
+ },
187
+ concurrency
188
+ );
189
+ }
190
+ return {
191
+ successful,
192
+ failed,
193
+ duration: Date.now() - startTime
194
+ };
195
+ }
196
+ async function findDuplicates(folderPath, criteria = ["artist", "title"]) {
197
+ const result = await scanFolder(folderPath);
198
+ const duplicates = /* @__PURE__ */ new Map();
199
+ for (const file of result.files) {
200
+ const key = criteria.map((field) => file.tags[field] || "").filter((v) => v !== "").join("|");
201
+ if (key) {
202
+ const group = duplicates.get(key) || [];
203
+ group.push(file);
204
+ if (group.length > 1) {
205
+ duplicates.set(key, group);
206
+ }
207
+ }
208
+ }
209
+ for (const [key, files] of duplicates.entries()) {
210
+ if (files.length < 2) {
211
+ duplicates.delete(key);
212
+ }
213
+ }
214
+ return duplicates;
215
+ }
216
+ async function exportFolderMetadata(folderPath, outputPath, options) {
217
+ const result = await scanFolder(folderPath, options);
218
+ const data = {
219
+ folder: folderPath,
220
+ scanDate: (/* @__PURE__ */ new Date()).toISOString(),
221
+ summary: {
222
+ totalFiles: result.totalFound,
223
+ processedFiles: result.totalProcessed,
224
+ errors: result.errors.length,
225
+ duration: result.duration
226
+ },
227
+ files: result.files,
228
+ errors: result.errors
229
+ };
230
+ if (typeof Deno !== "undefined") {
231
+ await Deno.writeTextFile(outputPath, JSON.stringify(data, null, 2));
232
+ } else if (typeof globalThis.process !== "undefined") {
233
+ const fs = await import("fs/promises");
234
+ await fs.writeFile(outputPath, JSON.stringify(data, null, 2));
235
+ }
236
+ }
237
+ export {
238
+ exportFolderMetadata,
239
+ findDuplicates,
240
+ scanFolder,
241
+ updateFolderTags
242
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taglib-wasm",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,10 @@
16
16
  "./simple": {
17
17
  "types": "./dist/src/simple.d.ts",
18
18
  "default": "./dist/src/simple.js"
19
+ },
20
+ "./folder": {
21
+ "types": "./dist/src/folder-api.d.ts",
22
+ "default": "./dist/src/folder-api.js"
19
23
  }
20
24
  },
21
25
  "files": [