Folder Operations Guide
The folder API scans directories, reads metadata in batches, and finds duplicate tracks — the pieces a music library manager, duplicate finder, or bulk metadata editor is built from.
Overview
Import these functions from taglib-wasm, or from the Node-only taglib-wasm/folder entry that exists so a browser bundle cannot silently resolve a filesystem graph (Import Paths compares every entry):
import { findDuplicates, scanFolder } from "taglib-wasm/folder";
import { writeTagsBatch } from "taglib-wasm/simple"; // batch writesRuntime Support
scanFolder(), findDuplicates(), and exportFolderMetadata() walk a filesystem, so they need a runtime that has one:
- ✅ Deno
- ✅ Node.js
- ✅ Bun
- ❌ Browsers (no filesystem access)
- ❌ Cloudflare Workers (no filesystem access)
groupAlbums() and discFolderInfo() are pure functions over a scan result and run anywhere, browsers included.
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
- Expect per-file failures — a corrupted or unreadable file is reported as an
erroritem in the results array instead of thrown, so checkresult.items.filter((i) => i.status === "error") - Consider memory usage when processing large collections
- Pick concurrency by storage: around 12 for an SSD, 6 for an HDD, 4 for a network drive (see Album Processing).
scanFolder()deliberately stays at 4 internally; reach forreadTagsBatch()orreadMetadataBatch()when you need to tune it - Filter by extensions to avoid processing non-audio files
- Export metadata regularly for backup and analysis
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).
Next Steps
- Album Processing — batch workflows that pair a folder scan with metadata updates
- Folder API Reference — every function signature for the scans, duplicate groups, and album grouping above