navidrome/scanner
Deluan Quintão 5b758fc20c
fix(artwork): re-resolve artwork when image files change on disk (#5965)
* fix(artwork): re-resolve artwork when image files change on disk

An image-only folder change (replaced, added, or deleted cover/artist
images, with no audio files touched) was detected by the scanner but never
reached the artwork queue, so clients kept seeing the old coverArt hash
until something else forced a re-resolution.

Phase 1 now diffs each changed folder's image list and imagesUpdatedAt
against the previously persisted folder row, and at the end of the phase
bulk-enqueues re-resolution for the affected entities: albums with tracks
in the folder or its direct children (covering disc subfolder layouts),
and, when an artist-pattern image is involved, artists with albums under
the folder's subtree, mirroring the artist resolver's upward search. The
artist mapping mirrors the resolver's sole-album-artist album selection.

New repository helpers keep the mapping set-based and light: folder
GetAllIDs, media_file GetAlbumIDsByFolder (distinct, indexed by
folder_id), and album GetSoleAlbumArtistIDs.

* refactor: simplify the image-change artwork enqueue after review

Load the previous folder image state through the existing GetFolderUpdateInfo
bulk pre-pass instead of a per-folder SELECT inside the persist transaction,
and skip the diff for new folders, whose artwork the scanner already enqueues
inline. Move the artist-image classification into core/artwork
(IsArtistImageFile) so the scanner shares the resolver's ArtistArtPriority
token grammar instead of re-parsing it (the copy mistreated image-folder as a
filename glob). Move the folder-subtree query into the folder repository
(GetSubtreeIDs) with LIKE escaping and expression-tree batching, share the
sole-album-artist predicate between the resolver and the album repository
(model.SoleAlbumArtistFilter), extract a chunked single-column query helper,
and deduplicate the ArtworkQueueItem literals behind scanArtworkItem.

* fix(persistence): keep slash-form paths in GetSubtreeIDs subtree predicates

The scanner hands GetSubtreeIDs io/fs slash-form paths, but filepath.Clean
rewrites them with backslashes on Windows while folder.path is stored with
forward slashes, so the descendant predicates matched nothing and nested
artist folders were never re-enqueued there. Normalize with path.Clean, like
HasAudioOutsideFolders does, and cover a nested path in the repo test.

* refactor(persistence): move the sole-album-artist rule into the album repository

SQLizer filters belong in the persistence package, not model. The rule
becomes an unexported filter shared by GetSoleAlbumArtistIDs and a new
GetBySoleAlbumArtist repository method, which the artist artwork resolver now
calls instead of building the squirrel filter itself.

* perf(scanner): resolve image-change artists in one query over album.folder_ids

The artist half of the image-change enqueue walked folder subtree IDs, then
media_file rows, then album rows, marshalling thousands of bound IDs through
the driver on each hop. Matching albums by their own folder_ids instead is one
statement, and folder_ids is the same source the artist resolver uses to
compute an artist's folders.

Benchmarked against a copy of the production DB (97k tracks, 10k folders,
7k albums): 87ms +/-196% -> 17.4ms +/-8%, 7.1MB -> 172KB, 103k -> 1.5k allocs.
The subtree predicate becomes a shared folderSubtreeFilter, so Folder
GetSubtreeIDs and Album GetSoleAlbumArtistIDs are no longer needed.

* fix(scanner): persist ancestor folders discovered by a quick scan

A quick scan skipped any new folder with no files of its own, so an artist
folder holding only album subfolders never got a row. Adding artist.jpg to it
later then produced no artwork enqueue: the entry was new, so the image diff
was skipped, and it has no tracks, so nothing was enqueued inline either.

Skip only genuinely empty new folders, matching what a full scan already
persists. This also fixes artist artwork resolving as absent for artists first
imported by a quick scan, since the resolver's folder climb needs that row.

Also normalizes the selective-scan preload paths with path.Clean, so its
descendant predicates match the stored slash-form paths on Windows.

* fix(persistence): chunk subtree paths and match artist globs by basename

Two regressions from earlier commits on this branch.

Collapsing the subtree query into a single statement dropped the chunking the
old GetSubtreeIDs had: each path expands into 3 OR terms and SQLite rejects an
expression tree deeper than 1000, measured at 166 paths. A library with more
artist-image folders than that (the prod copy has 158) would fail the whole
collect, dropping the album items with it, so the scanner now keeps them when
the artist query fails.

The artist-image classifier compared whole tokens after stripping album/, so a
directory-bearing glob like images/artist.* never matched the basenames the
scanner has. Match on path.Base, which is what album/artist.* already reduced
to; the resolver climbs parent folders, so an exact prefix is not knowable
here and a conservative match is the right failure direction.

* refactor(persistence): halve the repository surface this PR adds

Research on the four new repository methods found two were avoidable.

GetAlbumIDsByFolder now expands the changed folders to their direct children
in its own subquery, so Folder.GetAllIDs has no callers and is deleted, one
round trip per scan disappears, and the previously unchunked id/parent_id IN
lists are covered by the existing chunking.

GetBySoleAlbumArtist becomes an exported SoleAlbumArtistFilter, matching the
ParticipantIDFilter precedent for sharing a Sqlizer with core/, so the rule
still lives in persistence but AlbumRepository gains nothing and the mock shim
that ignored the artist filter is gone.

Also drops queryAllSliceChunked, now callerless, in favour of the file-local
slices.Chunk convention used by the sibling folder queries.

Rejected on measurement: matching the album path by album.folder_ids is exactly
equivalent (13975 pairs, zero difference) but has no index, so it scans every
album and runs 5-200x slower than the media_file route.

* refactor(persistence): stop reading the deprecated album_artist_id column

Both artist lookups this PR touches now go through participation, matching
the precedent in core/archiver.go and share_repository.go.

SoleAlbumArtistFilter uses ParticipantIDFilter, which is also faster: the
album_artists unique constraint is a covering index for it, while the old
column needed album_artist_album_id plus a row fetch.

GetSoleAlbumArtistIDsInSubtrees reads the sole artist out of the participants
JSON it already parses for the sole-artist check, rather than joining back to
album_artists, which measured ~1.6x slower on a prod-sized copy.

Verified equivalent on that copy: 6828 sole-artist albums and 1088 subtree
artists resolve identically via the column, the join and the JSON. The tests
now set a deliberately wrong album_artist_id so they fail if either query
starts reading it again.

* docs: trim comments that carry rationale belonging in commit messages

Five comments had grown past the budget with benchmark numbers, rejected
alternatives, and a duplicate of the constant's own explanation.

* refactor(scanner): move the image-change enqueue into phase_1_folders

The three functions were methods on phaseFolders, so they belong with the
type; phase_1_image_changes.go also read like a fifth phase, which it wasn't.

* refactor(scanner): extract the image-change collector into its own type

phaseFolders no longer owns the per-library map and the mapping methods; it
records into a collector and asks it to enqueue once. The collector keeps the
library alongside the folders, so enqueue needs only ctx and the datastore.

* refactor(scanner): simplify enqueue method by removing redundant datastore parameter

Signed-off-by: Deluan <deluan@navidrome.org>

* docs(scanner): drop the stale zero-value claim on imageChangeCollector

The collector now takes its datastore at construction, so the zero value is
no longer usable.

* fix(scanner): pin the persist stage to concurrency 1 and guard the collector

The stage relied on go-pipeline defaulting to one worker; stating it at the
stage makes the constraint visible where someone would change it. The
collector takes a mutex too, so the type is safe on its own terms rather than
by configuration.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-08-16 13:24:07 -04:00
..
metadata_old fix(scanner): stop logging expected lyrics sniff misses as warnings (#5702) 2026-07-02 09:46:57 -04:00
controller.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00
controller_test.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00
external.go refactor: multiple syntax updates for Go 1.26 2026-05-19 18:02:36 -03:00
external_test.go feat(scanner): implement file-based target passing for large target lists 2025-12-16 16:08:32 -05:00
folder_entry.go fix(artwork): re-resolve artwork when image files change on disk (#5965) 2026-08-16 13:24:07 -04:00
folder_entry_test.go fix(artwork): re-resolve artwork when image files change on disk (#5965) 2026-08-16 13:24:07 -04:00
ignore_checker.go refactor: run Go modernize (#5002) 2026-02-08 09:57:30 -05:00
ignore_checker_test.go feat(scanner): implement selective folder scanning and file system watcher improvements (#4674) 2025-11-14 22:15:43 -05:00
image_changes.go fix(artwork): re-resolve artwork when image files change on disk (#5965) 2026-08-16 13:24:07 -04:00
phase_1_folders.go fix(artwork): re-resolve artwork when image files change on disk (#5965) 2026-08-16 13:24:07 -04:00
phase_2_missing_tracks.go fix(scanner): prevent duplicate tracks when multiple missing files match same target (#5183) 2026-03-14 00:07:21 -04:00
phase_2_missing_tracks_test.go fix(scanner): prevent duplicate tracks when multiple missing files match same target (#5183) 2026-03-14 00:07:21 -04:00
phase_3_refresh_albums.go feat(scanner): implement selective folder scanning and file system watcher improvements (#4674) 2025-11-14 22:15:43 -05:00
phase_3_refresh_albums_test.go feat(scanner): implement selective folder scanning and file system watcher improvements (#4674) 2025-11-14 22:15:43 -05:00
phase_4_playlists.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00
phase_4_playlists_test.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00
README.md docs(scanner): add overview README document 2025-04-25 12:54:29 -04:00
scanner.go fix(cli): accept absolute paths in selective scan --target (#5947) 2026-08-12 14:55:04 -04:00
scanner_benchmark_test.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00
scanner_internal_test.go fix(cli): accept absolute paths in selective scan --target (#5947) 2026-08-12 14:55:04 -04:00
scanner_multilibrary_test.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00
scanner_selective_test.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00
scanner_suite_test.go fix(scanner): resolve file symlinks with the production local storage FS (#5755) 2026-07-10 10:52:05 -04:00
scanner_test.go fix(artwork): re-resolve artwork when image files change on disk (#5965) 2026-08-16 13:24:07 -04:00
walk_dir_tree.go fix(scanner): resolve file symlinks with the production local storage FS (#5755) 2026-07-10 10:52:05 -04:00
walk_dir_tree_test.go fix(scanner): resolve file symlinks with the production local storage FS (#5755) 2026-07-10 10:52:05 -04:00
watcher.go feat(scanner): add Scanner.IgnoreDotFolders to allow indexing dot-prefixed folders (#5568) 2026-06-19 19:13:16 -04:00
watcher_test.go fix(cache): write the completion marker before closing the cache writer (#5927) 2026-08-10 14:10:05 -04:00

Navidrome Scanner: Technical Overview

This document provides a comprehensive technical explanation of Navidrome's music library scanner system.

Architecture Overview

The Navidrome scanner is built on a multi-phase pipeline architecture designed for efficient processing of music files. It systematically traverses file system directories, processes metadata, and maintains a database representation of the music library. A key performance feature is that some phases run sequentially while others execute in parallel.

flowchart TD
    subgraph "Scanner Execution Flow"
        Controller[Scanner Controller] --> Scanner[Scanner Implementation]
        
        Scanner --> Phase1[Phase 1: Folders Scan]
        Phase1 --> Phase2[Phase 2: Missing Tracks]
        
        Phase2 --> ParallelPhases
        
        subgraph ParallelPhases["Parallel Execution"]
            Phase3[Phase 3: Refresh Albums]
            Phase4[Phase 4: Playlist Import]
        end
        
        ParallelPhases --> FinalSteps[Final Steps: GC + Stats]
    end
    
    %% Triggers that can initiate a scan
    FileChanges[File System Changes] -->|Detected by| Watcher[Filesystem Watcher]
    Watcher -->|Triggers| Controller
    
    ScheduledJob[Scheduled Job] -->|Based on Scanner.Schedule| Controller
    ServerStartup[Server Startup] -->|If Scanner.ScanOnStartup=true| Controller
    ManualTrigger[Manual Scan via UI/API] -->|Admin user action| Controller
    CLICommand[Command Line: navidrome scan] -->|Direct invocation| Controller
    PIDChange[PID Configuration Change] -->|Forces full scan| Controller
    DBMigration[Database Migration] -->|May require full scan| Controller
    
    Scanner -.->|Alternative| External[External Scanner Process]

The execution flow shows that Phases 1 and 2 run sequentially, while Phases 3 and 4 execute in parallel to maximize performance before the final processing steps.

Core Components

Scanner Controller (controller.go)

This is the entry point for all scanning operations. It provides:

  • Public API for initiating scans and checking scan status
  • Event broadcasting to notify clients about scan progress
  • Serialization of scan operations (prevents concurrent scans)
  • Progress tracking and monitoring
  • Error collection and reporting
type Scanner interface {
    // ScanAll starts a full scan of the music library. This is a blocking operation.
    ScanAll(ctx context.Context, fullScan bool) (warnings []string, err error)
    Status(context.Context) (*StatusInfo, error)
}

Scanner Implementation (scanner.go)

The primary implementation that orchestrates the four-phase scanning pipeline. Each phase follows the Phase interface pattern:

type phase[T any] interface {
    producer() ppl.Producer[T]
    stages() []ppl.Stage[T]
    finalize(error) error
    description() string
}

This design enables:

  • Type-safe pipeline construction with generics
  • Modular phase implementation
  • Separation of concerns
  • Easy measurement of performance

External Scanner (external.go)

The External Scanner is a specialized implementation that offloads the scanning process to a separate subprocess. This is specifically designed to address memory management challenges in long-running Navidrome instances.

// scannerExternal is a scanner that runs an external process to do the scanning. It is used to avoid
// memory leaks or retention in the main process, as the scanner can consume a lot of memory. The
// external process will be spawned with the same executable as the current process, and will run
// the "scan" command with the "--subprocess" flag.
//
// The external process will send progress updates to the main process through its STDOUT, and the main
// process will forward them to the caller.
sequenceDiagram
    participant MP as Main Process
    participant ES as External Scanner
    participant SP as Subprocess (navidrome scan --subprocess)
    participant FS as File System
    participant DB as Database
    
    Note over MP: DevExternalScanner=true
    MP->>ES: ScanAll(ctx, fullScan)
    activate ES
    
    ES->>ES: Locate executable path
    ES->>SP: Start subprocess with args:<br>scan --subprocess --configfile ... etc.
    activate SP
    
    Note over ES,SP: Create pipe for communication
    
    par Subprocess executes scan
        SP->>FS: Read files & metadata
        SP->>DB: Update database
    and Main process monitors progress
        loop For each progress update
            SP->>ES: Send encoded progress info via stdout pipe
            ES->>MP: Forward progress info
        end
    end
    
    SP-->>ES: Subprocess completes (success/error)
    deactivate SP
    ES-->>MP: Return aggregated warnings/errors
    deactivate ES

Technical details:

  1. Process Isolation

    • Spawns a separate process using the same executable
    • Uses the --subprocess flag to indicate it's running as a child process
    • Preserves configuration by passing required flags (--configfile, --datafolder, etc.)
  2. Inter-Process Communication

    • Uses a pipe for bidirectional communication
    • Encodes/decodes progress updates using Go's gob encoding for efficient binary transfer
    • Properly handles process termination and error propagation
  3. Memory Management Benefits

    • Scanning operations can be memory-intensive, especially with large music libraries
    • Memory leaks or excessive allocations are automatically cleaned up when the process terminates
    • Main Navidrome process remains stable even if scanner encounters memory-related issues
  4. Error Handling

    • Detects non-zero exit codes from the subprocess
    • Propagates error messages back to the main process
    • Ensures resources are properly cleaned up, even in error conditions

Scanning Process Flow

Phase 1: Folder Scan (phase_1_folders.go)

This phase handles the initial traversal and media file processing.

flowchart TD
    A[Start Phase 1] --> B{Full Scan?}
    B -- Yes --> C[Scan All Folders]
    B -- No --> D[Scan Modified Folders]
    C --> E[Read File Metadata]
    D --> E
    E --> F[Create Artists]
    E --> G[Create Albums]
    F --> H[Save to Database]
    G --> H
    H --> I[Mark Missing Folders]
    I --> J[End Phase 1]

Technical implementation details:

  1. Folder Traversal

    • Uses walkDirTree to traverse the directory structure
    • Handles symbolic links and hidden files
    • Processes .ndignore files for exclusions
    • Maps files to appropriate types (audio, image, playlist)
  2. Metadata Extraction

    • Processes files in batches (defined by filesBatchSize = 200)
    • Extracts metadata using the configured storage backend
    • Converts raw metadata to MediaFile objects
    • Collects and normalizes tag information
  3. Album and Artist Creation

    • Groups tracks by album ID
    • Creates album records from track metadata
    • Handles album ID changes by tracking previous IDs
    • Creates artist records from track participants
  4. Database Persistence

    • Uses transactions for atomic updates
    • Preserves album annotations across ID changes
    • Updates library-artist mappings
    • Marks missing tracks for later processing
    • Pre-caches artwork for performance

Phase 2: Missing Tracks Processing (phase_2_missing_tracks.go)

This phase identifies tracks that have moved or been deleted.

flowchart TD
    A[Start Phase 2] --> B[Load Libraries]
    B --> C[Get Missing and Matching Tracks]
    C --> D[Group by PID]
    D --> E{Match Type?}
    E -- Exact --> F[Update Path]
    E -- Same PID --> G[Update If Only One]
    E -- Equivalent --> H[Update If No Better Match]
    F --> I[End Phase 2]
    G --> I
    H --> I

Technical implementation details:

  1. Track Identification Strategy

    • Uses persistent identifiers (PIDs) to track tracks across scans
    • Loads missing tracks and potential matches from the database
    • Groups tracks by PID to limit comparison scope
  2. Match Analysis

    • Applies three levels of matching criteria:
      • Exact match (full metadata equivalence)
      • Single match for a PID
      • Equivalent match (same base path or similar metadata)
    • Prioritizes matches in order of confidence
  3. Database Update Strategy

    • Preserves the original track ID
    • Updates the path to the new location
    • Deletes the duplicate entry
    • Uses transactions to ensure atomicity

Phase 3: Album Refresh (phase_3_refresh_albums.go)

This phase updates album information based on the latest track metadata.

flowchart TD
    A[Start Phase 3] --> B[Load Touched Albums]
    B --> C[Filter Unmodified]
    C --> D{Changes Detected?}
    D -- Yes --> E[Refresh Album Data]
    D -- No --> F[Skip]
    E --> G[Update Database]
    F --> H[End Phase 3]
    G --> H
    H --> I[Refresh Statistics]

Technical implementation details:

  1. Album Selection Logic

    • Loads albums that have been "touched" in previous phases
    • Uses a producer-consumer pattern for efficient processing
    • Retrieves all media files for each album for completeness
  2. Change Detection

    • Rebuilds album metadata from associated tracks
    • Compares album attributes for changes
    • Skips albums with no media files
    • Avoids unnecessary database updates
  3. Statistics Refreshing

    • Updates album play counts
    • Updates artist play counts
    • Maintains consistency between related entities

Phase 4: Playlist Import (phase_4_playlists.go)

This phase imports and updates playlists from the file system.

flowchart TD
    A[Start Phase 4] --> B{AutoImportPlaylists?}
    B -- No --> C[Skip]
    B -- Yes --> D{Admin User Exists?}
    D -- No --> E[Log Warning & Skip]
    D -- Yes --> F[Load Folders with Playlists]
    F --> G{For Each Folder}
    G --> H[Read Directory]
    H --> I{For Each Playlist}
    I --> J[Import Playlist]
    J --> K[Pre-cache Artwork]
    K --> L[End Phase 4]
    C --> L
    E --> L

Technical implementation details:

  1. Playlist Discovery

    • Loads folders known to contain playlists
    • Focuses on folders that have been touched in previous phases
    • Handles both playlist formats (M3U, NSP)
  2. Import Process

    • Uses the core.Playlists service for import
    • Handles both regular and smart playlists
    • Updates existing playlists when changed
    • Pre-caches playlist cover art
  3. Configuration Awareness

    • Respects the AutoImportPlaylists setting
    • Requires an admin user for playlist import
    • Logs appropriate messages for configuration issues

Final Processing Steps

After the four main phases, several finalization steps occur:

  1. Garbage Collection

    • Removes dangling tracks with no files
    • Cleans up empty albums
    • Removes orphaned artists
    • Deletes orphaned annotations
  2. Statistics Refresh

    • Updates artist song and album counts
    • Refreshes tag usage statistics
    • Updates aggregate metrics
  3. Library Status Update

    • Marks scan as completed
    • Updates last scan timestamp
    • Stores persistent ID configuration
  4. Database Optimization

    • Performs database maintenance
    • Optimizes tables and indexes
    • Reclaims space from deleted records

File System Watching

The watcher system (watcher.go) provides real-time monitoring of file system changes:

flowchart TD
    A[Start Watcher] --> B[For Each Library]
    B --> C[Start Library Watcher]
    C --> D[Monitor File Events]
    D --> E{Change Detected?}
    E -- Yes --> F[Wait for More Changes]
    F --> G{Time Elapsed?}
    G -- Yes --> H[Trigger Scan]
    G -- No --> F
    H --> I[Wait for Scan Completion]
    I --> D

Technical implementation details:

  1. Event Throttling

    • Uses a timer to batch changes
    • Prevents excessive rescanning
    • Configurable wait period
  2. Library-specific Watching

    • Each library has its own watcher goroutine
    • Translates paths to library-relative paths
    • Filters irrelevant changes
  3. Platform Adaptability

    • Uses storage-provided watcher implementation
    • Supports different notification mechanisms per platform
    • Graceful fallback when watching is not supported

Edge Cases and Optimizations

Handling Album ID Changes

The scanner carefully manages album identity across scans:

  • Tracks previous album IDs to handle ID generation changes
  • Preserves annotations when IDs change
  • Maintains creation timestamps for consistent sorting

Detecting Moved Files

A sophisticated algorithm identifies moved files:

  1. Groups missing and new files by their Persistent ID
  2. Applies multiple matching strategies in priority order
  3. Updates paths rather than creating duplicate entries

Resuming Interrupted Scans

If a scan is interrupted:

  • The next scan detects this condition
  • Forces a full scan if the previous one was a full scan
  • Continues from where it left off for incremental scans

Memory Efficiency

Several strategies minimize memory usage:

  • Batched file processing (200 files at a time)
  • External scanner process option
  • Database-side filtering where possible
  • Stream processing with pipelines

Concurrency Control

The scanner implements a sophisticated concurrency model to optimize performance:

  1. Phase-Level Parallelism:

    • Phases 1 and 2 run sequentially due to their dependencies
    • Phases 3 and 4 run in parallel using the chain.RunParallel() function
    • Final steps run sequentially to ensure data consistency
  2. Within-Phase Concurrency:

    • Each phase has configurable concurrency for its stages
    • For example, phase_1_folders.go processes folders concurrently: ppl.NewStage(p.processFolder, ppl.Name("process folder"), ppl.Concurrency(conf.Server.DevScannerThreads))
    • Multiple stages can exist within a phase, each with its own concurrency level
  3. Pipeline Architecture Benefits:

    • Producer-consumer pattern minimizes memory usage
    • Work is streamed through stages rather than accumulated
    • Back-pressure is automatically managed
  4. Thread Safety Mechanisms:

    • Atomic counters for statistics gathering
    • Mutex protection for shared resources
    • Transactional database operations

Configuration Options

The scanner's behavior can be customized through several configuration settings that directly affect its operation:

Core Scanner Options

Setting Description Default
Scanner.Enabled Whether the automatic scanner is enabled true
Scanner.Schedule Cron expression or duration for scheduled scans (e.g., "@daily") "0" (disabled)
Scanner.ScanOnStartup Whether to scan when the server starts true
Scanner.WatcherWait Delay before triggering scan after file changes detected 5s
Scanner.ArtistJoiner String used to join multiple artists in track metadata " • "

Playlist Processing

Setting Description Default
PlaylistsPath Path(s) to search for playlists (supports glob patterns) ""
AutoImportPlaylists Whether to import playlists during scanning true

Performance Options

Setting Description Default
DevExternalScanner Use external process for scanning (reduces memory issues) true
DevScannerThreads Number of concurrent processing threads during scanning 5

Persistent ID Options

Setting Description Default
PID.Track Format for track persistent IDs (critical for tracking moved files) "musicbrainz_trackid|albumid,discnumber,tracknumber,title"
PID.Album Format for album persistent IDs (affects album grouping) "musicbrainz_albumid|albumartistid,album,albumversion,releasedate"

These options can be set in the Navidrome configuration file (e.g., navidrome.toml) or via environment variables with the ND_ prefix (e.g., ND_SCANNER_ENABLED=false). For environment variables, dots in option names are replaced with underscores.

Conclusion

The Navidrome scanner represents a sophisticated system for efficiently managing music libraries. Its phase-based pipeline architecture, careful handling of edge cases, and performance optimizations allow it to handle libraries of significant size while maintaining data integrity and providing a responsive user experience.