Skip to content

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:

typescript
import { findDuplicates, scanFolder } from "taglib-wasm";
import { writeTagsBatch } from "taglib-wasm/simple"; // batch writes

Runtime 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:

typescript
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

typescript
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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
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

typescript
// 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

typescript
// 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

typescript
// 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:

typescript
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

  1. Start with small directories when testing to understand performance characteristics
  2. Use progress callbacks for user feedback on long operations
  3. Handle errors gracefully - some files may be corrupted or inaccessible
  4. Consider memory usage when processing large collections
  5. Use appropriate concurrency based on your system resources
  6. Filter by extensions to avoid processing non-audio files
  7. 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.

typescript
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 (CD1 high, Album (Disc 1) medium unless corroborated by siblings, Volume 1 medium, Bonus Disc and bare numbers gated), with sibling corroboration; embedded discNumber wins 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) and item.discNumber — the single authority for UI cards and lint buckets. album.directory is 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 in unmatched; per-file scan errors land in errors (disjoint from everything else).
  • Compilation: album.compilation is true/false only when the embedded compilation flags agree (COMPILATION/TCMP/cpil), undefined otherwise.

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).

Released under the MIT License.