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 +32 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/src/folder-api.d.ts +136 -0
- package/dist/src/folder-api.d.ts.map +1 -0
- package/dist/src/folder-api.js +242 -0
- package/package.json +5 -1
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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.
|
|
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": [
|