# How Folia Handles Local Music Library Indexing and Metadata: Complete Technical Guide

> Discover how Folia efficiently indexes local music libraries with the File System Access API and Web Workers for responsive metadata extraction. Learn the technical details.

- Repository: [冬霧/folia-major](https://github.com/chthollyphile/folia-major)
- Tags: deep-dive
- Published: 2026-07-06

---

**Folia indexes local music libraries by scanning root directories with the File System Access API, creating deterministic snapshots, diffing them against previous states to detect changes, and extracting embedded metadata via Web Workers to keep the UI responsive.**

Folia, an open-source music player from the `chthollyphile/folia-major` repository, implements a sophisticated three-phase pipeline for local music library indexing and metadata extraction. The system treats each root folder as an import target, leveraging modern browser APIs to handle large collections without blocking the main thread.

## The Three-Phase Import Pipeline

### Phase 1: Directory Selection and Snapshot Creation

When a user initiates an import via `importFolder` in [`src/services/localMusicService.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/services/localMusicService.ts), Folia first obtains a directory handle using the File System Access API. The system walks the entire directory tree, classifying each file through `getSnapshotFileKind` (lines 17-42) into categories: *audio*, *lyric*, *translation-lyric*, *cover*, or *other*.

For each file discovered, Folia constructs a deterministic hash tree via `hashSnapshotNode` that represents the complete folder state. This snapshot serves as the foundation for incremental updates, allowing Folia to detect exactly what changed between imports.

```typescript
// src/services/localMusicService.ts
function getSnapshotFileKind(fileName: string): LocalLibrarySnapshotFile['kind'] {
    if (TRANSLATION_LYRIC_EXTENSIONS.test(fileName)) return 'translationLyric';
    if (LYRIC_EXTENSIONS.test(fileName))          return 'lyric';
    if (getFolderCoverPriority(fileName) !== -1)  return 'cover';
    if (isAudioFileName(fileName))                return 'audio';
    return 'other';
}

```

### Phase 2: Diff Planning and Change Detection

With the new snapshot in hand, Folia executes `collectImportDiffPlan` (lines 88-115) to compare it against the previously saved state. This generates an `ImportDiffPlan` containing:

- `changedEntries` – Audio files requiring re-import
- `reusedSongs` – Existing tracks with valid file handles  
- `removedSongs` – Tracks no longer present in the filesystem
- `lrcMap`, `tlrcMap`, `coverMap` – Mappings for the best lyric and cover candidates per track

This diff-based approach ensures that only modified files undergo expensive metadata parsing, significantly speeding up subsequent imports.

### Phase 3: Metadata Extraction and Persistence

For each entry in `changedEntries`, Folia calls `buildImportedSong` (lines 57-84) to construct a `LocalSong` object. The process runs three concurrent streams:

1. **Embedded metadata extraction**: The audio file handle is passed to `parseEmbeddedMetadataAsync` in [`src/utils/localMetadataWorkerClient.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/localMetadataWorkerClient.ts), which spawns a Web Worker ([`src/workers/metadataParser.worker.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/workers/metadataParser.worker.ts)) to parse title, artist, album, bitrate, duration, and embedded covers without freezing the UI.

2. **Duration fallback**: If embedded tags lack duration data, Folia creates a temporary `HTMLAudioElement` via `getAudioDuration` to read the file's length.

3. **Sidecar file attachment**: Local lyric files (`.lrc`, `.vtt`, `.ttml`, `.qrc`, `.yrc`, `.krc`) and folder-level covers (`cover.png|jpg|jpeg`) are matched and attached to the song record using the maps generated during diff planning.

```typescript
// src/utils/localMetadataWorkerClient.ts
export const parseEmbeddedMetadataAsync = (file: File, includeCover = false) =>
    new Promise<EmbeddedMetadataResult | null>((resolve) => {
        const worker = initMetadataWorker();
        const requestId = `meta_req_${++workerRequestId}`;
        workerCallbacks.set(requestId, resolve);
        worker.postMessage({ type: 'parse-metadata', file, includeCover, requestId });
    });

```

```typescript
async function getAudioDuration(file: File): Promise<number> {
    return new Promise((resolve) => {
        const audio = new Audio();
        const url = URL.createObjectURL(file);
        audio.addEventListener('loadedmetadata', () => {
            resolve(audio.duration * 1000);
            URL.revokeObjectURL(url);
        });
        audio.addEventListener('error', () => {
            URL.revokeObjectURL(url);
            resolve(0);
        });
        audio.src = url;
    });
}

```

Finally, the completed `LocalSong` objects are persisted to IndexedDB via `saveLocalSongs` (defined in [`src/services/db.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/services/db.ts)), and the new snapshot is stored via `saveLocalLibrarySnapshot` for future comparisons.

## Background Hydration and Cover Optimization

After the foreground import completes, Folia continues processing via `hydrateImportedSongsInBackground`. This background phase re-reads each file to update omitted fields like replay-gain and ensures embedded covers are fully extracted.

Additionally, Folia populates representative covers by selecting one cover per folder or album (preferring embedded images) and propagating it to sibling tracks. This reduces redundant image decoding and improves memory efficiency across the library.

```typescript
// src/services/localMusicService.ts
async function hydrateImportedSongsInBackground(rootFolderName: string, songs: LocalSong[]) {
    // … concurrent workers read each file, update metadata, and batch‑save.
}

```

## Summary

- **Root-folder imports**: Folia uses the File System Access API to scan directories and persists directory handles for future rescans.
- **Deterministic snapshots**: The `hashSnapshotNode` system creates immutable representations of folder states, enabling incremental updates.
- **Diff-based processing**: `collectImportDiffPlan` identifies only `changedEntries`, avoiding unnecessary re-processing of existing tracks.
- **Worker-based extraction**: Metadata parsing runs in Web Workers ([`metadataParser.worker.ts`](https://github.com/chthollyphile/folia-major/blob/main/metadataParser.worker.ts)) via `parseEmbeddedMetadataAsync` to prevent UI blocking.
- **Sidecar support**: Automatic detection of lyric files (`.lrc`, `.vtt`, `.ttml`, `.qrc`, `.yrc`, `.krc`) and folder covers (`cover.png|jpg|jpeg`).
- **Persistent storage**: IndexedDB stores both the song library and library snapshots in [`src/services/db.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/services/db.ts).
- **Background enrichment**: `hydrateImportedSongsInBackground` continues metadata extraction after the initial import completes.

## Frequently Asked Questions

### How does Folia detect changes in my local music library between imports?

Folia compares deterministic snapshots of your directory structure. During the initial scan, `hashSnapshotNode` creates a hash tree representing the folder state in [`src/services/localMusicService.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/services/localMusicService.ts). On subsequent imports, `collectImportDiffPlan` compares the new snapshot against the stored one, identifying `changedEntries` (modified files), `reusedSongs` (unchanged tracks), and `removedSongs` (deleted files). Only changed entries undergo full metadata extraction, making rescans nearly instantaneous for large libraries.

### What audio metadata formats does Folia support?

Folia extracts standard embedded metadata including title, artist, album, bitrate, and duration across common audio formats via the Web Worker in [`src/workers/metadataParser.worker.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/workers/metadataParser.worker.ts). It also handles embedded lyrics and cover art. For sidecar files, it recognizes lyric formats such as `.lrc`, `.vtt`, `.ttml`, `.qrc`, `.yrc`, and `.krc` through the utilities in [`src/utils/lyrics/autoMatchBestLyric.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/lyrics/autoMatchBestLyric.ts). Folder-level cover images named `cover.png`, `cover.jpg`, or `cover.jpeg` are automatically detected and associated with tracks in that directory.

### Why does Folia use Web Workers for metadata extraction?

To maintain UI responsiveness while processing large libraries, Folia offloads metadata parsing to a dedicated Web Worker. The `parseEmbeddedMetadataAsync` function in [`src/utils/localMetadataWorkerClient.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/localMetadataWorkerClient.ts) handles the communication, allowing the browser to parse audio tags in a separate thread while the main interface remains fluid. This architecture prevents the "freezing" behavior common in browser-based music players when indexing thousands of files.

### What happens if an audio file has no embedded duration metadata?

When embedded tags lack duration information, Folia executes a fallback via `getAudioDuration`. This function creates a blob URL from the file, loads it into a temporary `HTMLAudioElement`, and reads the `duration` property from the browser's media engine. The value is converted to milliseconds and stored in the song record, ensuring accurate timeline displays even for files with incomplete tag data.