Folder Operations Guide
The folder API provides efficient batch operations for processing multiple audio files, scanning directories, and managing music collections. This API is perfect for building music library managers, duplicate finders, and batch metadata editors.
Overview
The folder API is available through a dedicated import path:
import { findDuplicates, scanFolder } from "taglib-wasm";
import { writeTagsBatch } from "taglib-wasm/simple"; // batch writesRuntime Support Folder operations require filesystem access and are
available in:
- ✅ Deno
- ✅ Node.js
- ✅ Bun
- ❌ Browsers (no filesystem access)
- ❌ Cloudflare Workers (no filesystem access) :::
Scanning Folders
The scanFolder() function recursively scans directories for audio files and reads their metadata:
const result = await scanFolder("/path/to/music", {
recursive: true, // Scan subdirectories (default: true)
extensions: [".mp3", ".flac"], // File types to include (default: common audio formats)
maxFiles: 1000, // Limit number of files (default: unlimited)
includeProperties: true, // Include audio properties (default: true)
continueOnError: true, // Continue if files fail (default: true)
onProgress: (processed, total, file) => {
console.log(`Processing ${processed}/${total}: ${file}`);
},
});
// Access results
const okItems = result.items.filter((i) => i.status === "ok");
const errorItems = result.items.filter((i) => i.status === "error");
console.log(`Found ${result.items.length} audio files`);
console.log(`Processed ${okItems.length} successfully`);
console.log(`Errors: ${errorItems.length}`);
console.log(`Time taken: ${result.duration}ms`);
// Process each file
for (const file of result.items) {
console.log(`Path: ${file.path}`);
console.log(`Title: ${file.tags.title?.[0]}`);
console.log(`Artist: ${file.tags.artist?.[0]}`);
console.log(`Duration: ${file.properties?.duration}s`);
console.log(`Bitrate: ${file.properties?.bitrate} kbps`);
}Result Structure
type FolderScanItem =
| ({ status: "ok" } & AudioFileMetadata)
| { status: "error"; path: string; error: Error };
interface FolderScanResult {
items: FolderScanItem[]; // All processed files (ok or error)
duration: number; // Time taken in milliseconds
}
interface AudioFileMetadata {
path: string; // File path
tags: ExtendedTag; // Metadata tags (including extended fields)
properties?: AudioProperties; // Audio properties (if requested)
hasCoverArt?: boolean; // Whether file has embedded cover art
dynamics?: AudioDynamics; // ReplayGain and Sound Check data
}Batch Tag Updates
Update metadata for multiple files efficiently:
const updates = [
{
path: "/music/song1.mp3",
tags: { artist: "New Artist", album: "New Album" },
},
{
path: "/music/song2.mp3",
tags: { genre: "Electronic", year: 2024 },
},
];
const result = await writeTagsBatch(updates, {
continueOnError: true, // Continue if some files fail
concurrency: 4, // Process 4 files in parallel
onProgress: (processed, total, file) =>
console.log(`${processed}/${total}: ${file}`),
});
const updated = result.items.filter((i) => i.status === "ok").length;
const failures = result.items.filter((i) => i.status === "error");
console.log(`Updated ${updated} files`);
console.log(`Failed: ${failures.length}`);
console.log(`Time: ${result.duration}ms`);
// Check failures
for (const failure of failures) {
console.error(`Failed to update ${failure.path}: ${failure.error.message}`);
}Finding Duplicates
Find duplicate audio files based on metadata criteria:
// Find duplicates by artist and title (default criteria)
const duplicates = await findDuplicates("/path/to/music");
console.log(`Found ${duplicates.length} groups of duplicates`);
// Process each duplicate group
for (const group of duplicates) {
console.log(`\nDuplicate group: ${JSON.stringify(group.criteria)}`);
for (const file of group.files) {
console.log(` - ${file.path}`);
console.log(
` Size: ${file.properties?.duration}s @ ${file.properties?.bitrate}kbps`,
);
}
}
// Find duplicates by different criteria
const albumDuplicates = await findDuplicates("/music", {
criteria: ["album", "artist"],
});
const exactDuplicates = await findDuplicates("/music", {
criteria: ["artist", "album", "title", "track"],
});Exporting Metadata
Export your music library metadata to JSON for cataloging or analysis:
await exportFolderMetadata("/path/to/music", "./music-catalog.json", {
recursive: true,
includeProperties: true
});
// The exported JSON contains:
{
"folder": "/path/to/music",
"scanDate": "2024-01-20T10:30:00.000Z",
"summary": {
"totalFiles": 1234,
"processedFiles": 1230,
"errors": 4,
"duration": 5678
},
"files": [
{
"path": "/path/to/music/song.mp3",
"tags": {
"title": "Song Title",
"artist": "Artist Name",
// ... all tags
},
"properties": {
"duration": 180,
"bitrate": 320,
// ... all properties
}
}
// ... more files
],
"errors": [
{
"path": "/path/to/music/corrupt.mp3",
"error": "Invalid audio file format"
}
]
}Performance Optimization
Concurrency
scanFolder uses a fixed concurrency of 4 internally. For custom concurrency control, use batch APIs like readTagsBatch or readMetadataBatch.
Memory Management
When processing large collections:
// Process in smaller batches
const result = await scanFolder("/huge-library", {
maxFiles: 100, // Process only 100 files at a time
includeProperties: false, // Skip audio properties to save memory
});Progress Monitoring
For long-running operations:
let lastUpdate = Date.now();
const result = await scanFolder("/music", {
onProgress: (processed, total, file) => {
const now = Date.now();
if (now - lastUpdate > 1000) { // Update every second
const percent = ((processed / total) * 100).toFixed(1);
console.log(`Progress: ${percent}% (${processed}/${total})`);
console.log(`Current: ${file}`);
const elapsed = now - startTime;
const rate = processed / (elapsed / 1000);
const remaining = (total - processed) / rate;
console.log(`ETA: ${Math.round(remaining)}s`);
lastUpdate = now;
}
},
});Common Use Cases
Music Library Organization
// Organize music by artist/album structure
const result = await scanFolder("/unsorted-music");
for (const file of result.items) {
const artist = file.tags.artist?.[0] || "Unknown Artist";
const album = file.tags.album?.[0] || "Unknown Album";
const title = file.tags.title?.[0] || path.basename(file.path);
// Create organized structure
const newPath = path.join("/organized-music", artist, album, `${title}.mp3`);
// Move file to new location (using your preferred file operation method)
}Metadata Cleanup
// Find and fix missing metadata
const result = await scanFolder("/music");
const needsFixing = result.items.filter((file) =>
!file.tags.artist?.[0] ||
!file.tags.title?.[0] ||
!file.tags.album?.[0]
);
console.log(`Found ${needsFixing.length} files with missing metadata`);
// Batch update missing fields
const updates = needsFixing.map((file) => ({
path: file.path,
tags: {
artist: file.tags.artist?.[0] || "Unknown Artist",
album: file.tags.album?.[0] || "Unknown Album",
title: file.tags.title?.[0] ||
path.basename(file.path, path.extname(file.path)),
},
}));
await writeTagsBatch(updates);Duplicate Cleanup
// Find and handle duplicates
const duplicates = await findDuplicates("/music");
for (const group of duplicates) {
// Sort by quality (highest bitrate first)
const sorted = group.files.sort((a, b) =>
(b.properties?.bitrate || 0) - (a.properties?.bitrate || 0)
);
const [keep, ...remove] = sorted;
console.log(`Keeping: ${keep.path} (${keep.properties?.bitrate}kbps)`);
for (const file of remove) {
console.log(
`Consider removing: ${file.path} (${file.properties?.bitrate}kbps)`,
);
}
}Error Handling
The folder API provides detailed error information:
const result = await scanFolder("/music", {
continueOnError: true, // Don't stop on errors
});
// Check for errors
const errors = result.items.filter((i) => i.status === "error");
if (errors.length > 0) {
console.error(`Failed to process ${errors.length} files:`);
for (const { path, error } of errors) {
if (error.message.includes("Invalid audio file format")) {
console.error(`Corrupted file: ${path}`);
} else if (error.message.includes("Permission denied")) {
console.error(`No access: ${path}`);
} else {
console.error(`Unknown error in ${path}: ${error.message}`);
}
}
}Best Practices
- Start with small directories when testing to understand performance characteristics
- Use progress callbacks for user feedback on long operations
- Handle errors gracefully - some files may be corrupted or inaccessible
- Consider memory usage when processing large collections
- Use appropriate concurrency based on your system resources
- Filter by extensions to avoid processing non-audio files
- Export metadata regularly for backup and analysis
API Reference
For detailed API documentation, see the Folder API Reference.
Album Grouping
scanForAlbums() scans a folder and groups the result into albums with disc subdivisions. groupAlbums() is the pure, synchronous core over an existing scanFolder() result — runtime-agnostic, browsers included.
import { scanForAlbums } from "taglib-wasm";
const { albums, singles, unmatched, errors } = await scanForAlbums("/music", {
recursive: true,
});
for (const album of albums) {
console.log(
`${
album.albumArtist ?? ""
} — ${album.album} (${album.discs.length} disc(s))`,
);
for (const disc of album.discs) {
console.log(
` Disc ${disc.discNumber ?? "?"}: ${disc.items.length} tracks`,
);
}
}Semantics:
- Tags are authority, folder names are evidence. Disc folders are recognized by name with confidence tiers (
CD1high,Album (Disc 1)medium unless corroborated by siblings,Volume 1medium,Bonus Discand bare numbers gated), with sibling corroboration; embeddeddiscNumberwins over the folder. Flat filename prefixes (1-01,101) subdivide a folder. - Every file carries its own resolution:
item.albumDir(the album folder the file was attributed to) anditem.discNumber— the single authority for UI cards and lint buckets.album.directoryis the album folder for cover lookup. - Singles and unmatched: an album of exactly one file is a
single; ok items with no album tag and no folder title evidence land inunmatched; per-file scan errors land inerrors(disjoint from everything else). - Compilation:
album.compilationistrue/falseonly when the embedded compilation flags agree (COMPILATION/TCMP/cpil),undefinedotherwise.
Options (scanForAlbums takes FolderScanOptions plus GroupAlbumsOptions/ScanForAlbumsOptions): minFolderConfidence drops weak folder disc evidence, flatDiscPrefixes toggles filename-prefix parsing, folderFallback toggles folder-based grouping of untagged files, scanRoot pins the scanned directory (a bare CD1/ directly under it is unmatched).