Skip to content

Developer Guide Library

Owen Williams edited this page Feb 22, 2026 · 1 revision

Introduction to Mixxx's Library Backend

The library backend is responsible for storing, retrieving, and synchronizing all track metadata in Mixxx. It manages an internal SQLite database, an in-memory track cache, and a plug-in interface for external track collections (Rekordbox, Serato, etc.).

The entry point for the rest of the application is the Library class (src/library/library.h), which owns the TrackCollectionManager and wires up all the library features and sidebar models. Most code outside the library should interact with TrackCollectionManager rather than the lower-level components directly.

The Database

Mixxx stores all library state in a single SQLite file (mixxxdb.sqlite in the user's Mixxx configuration directory). The connection is pooled via mixxx::DbConnectionPool, which lets background threads (the scanner, analysis workers) open their own connections without blocking the main thread.

The two core tables are:

  • library -- one row per track. Contains all metadata columns (artist, title, bpm, key, rating, color, etc.) plus internal housekeeping columns such as mixxx_deleted (soft-delete flag) and header_parsed.
  • track_locations -- one row per distinct file path. Stores the absolute location, filename, directory, filesize, fs_deleted (whether the file was absent on the last scan), and needs_verification. The library table references track_locations via a foreign key.

Separating the two tables allows Mixxx to detect moved files: the same track_locations row can be re-pointed to a new path without losing any metadata stored in library.

Column name constants are defined in src/library/dao/trackschema.h.

TrackCollectionManager

TrackCollectionManager is the authoritative gateway for all mutating library operations. Every add, hide, purge, relocate, or metadata-save operation must go through this class so that both the internal collection and any connected external collections stay in sync.

class TrackCollectionManager : public QObject,
    public virtual GlobalTrackCacheSaver {
  public:
    TrackCollection* internalCollection() const;
    const QList<ExternalTrackCollection*>& externalCollections() const;

    TrackPointer getTrackById(TrackId trackId) const;
    TrackPointer getTrackByRef(const TrackRef& trackRef) const;

    bool hideTracks(const QList<TrackId>& trackIds) const;
    bool unhideTracks(const QList<TrackId>& trackIds) const;
    void purgeTracks(const QList<TrackRef>& trackRefs) const;

    DirectoryDAO::AddResult addDirectory(const mixxx::FileInfo& newDir) const;
    DirectoryDAO::RemoveResult removeDirectory(const mixxx::FileInfo& oldDir) const;
    DirectoryDAO::RelocateResult relocateDirectory(
            const QString& oldDir, const QString& newDir) const;

    TrackPointer getOrAddTrack(
            const TrackRef& trackRef,
            bool* pAlreadyInLibrary = nullptr) const;

    SaveTrackResult saveTrack(const TrackPointer& pTrack) const;
    void startLibraryScan();
};

TrackCollectionManager also implements GlobalTrackCacheSaver, which means it is the object responsible for flushing Track objects back to the database when they are evicted from the GlobalTrackCache.

Thread affinity

TrackCollectionManager and TrackCollection (and all DAOs inside it) enforce main-thread-only access via DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY. Background threads (scanner, analysis) use their own database connections obtained from the shared DbConnectionPool and communicate results back via Qt signals.

TrackCollection

TrackCollection owns the main-thread QSqlDatabase connection and all of the Data Access Objects (DAOs). It also owns CrateStorage and a QSharedPointer<BaseTrackCache>.

class TrackCollection : public QObject,
    public virtual SqlStorage {
  public:
    TrackDAO&       getTrackDAO();
    PlaylistDAO&    getPlaylistDAO();
    const DirectoryDAO& getDirectoryDAO() const;
    AnalysisDao&    getAnalysisDAO();

    const CrateStorage& crates() const;

    // Crate write helpers (delegate to CrateStorage inside a transaction
    // and emit change signals):
    bool insertCrate(const Crate& crate, CrateId* pCrateId = nullptr);
    bool updateCrate(const Crate& crate);
    bool deleteCrate(CrateId crateId);
    bool addCrateTracks(CrateId crateId, const QList<TrackId>& trackIds);
    bool removeCrateTracks(CrateId crateId, const QList<TrackId>& trackIds);

  signals:
    void tracksAdded(const QSet<TrackId>& trackIds);
    void tracksChanged(const QSet<TrackId>& trackIds);
    void tracksRemoved(const QSet<TrackId>& trackIds);
    void crateInserted(CrateId id);
    void crateUpdated(CrateId id);
    void crateDeleted(CrateId id);
    void crateTracksChanged(CrateId, const QList<TrackId>& added,
                            const QList<TrackId>& removed);
};

The private interface (only accessible to TrackCollectionManager) is where the actual mutations live: addTrack, hideTracks, purgeTracks, relocateDirectory, etc. This ensures that all mutations flow through TrackCollectionManager.

Data Access Objects (DAOs)

All DAOs live under src/library/dao/ and inherit from the DAO marker interface (dao.h). They each receive a QSqlDatabase reference via initialize() and operate directly against the SQLite database.

TrackDAO

The most important DAO. Key responsibilities:

  • Resolving tracks -- resolveTrackIds(fileInfos, flags) maps a list of FileInfo objects to TrackIds. The flags parameter controls whether hidden tracks are un-hidden (UnhideHidden) and whether new tracks are added to the database (AddMissing).
  • Loading tracks -- getTrackByRef(TrackRef) fetches a track from the database (or the GlobalTrackCache) and returns a TrackPointer. The TrackRef can be constructed from a TrackId or a file location.
  • Saving tracks -- saveTrack(Track*) writes dirty track metadata back to the database. Normally called by TrackCollectionManager when the GlobalTrackCache evicts a track.
  • Detecting moved files -- detectMovedTracks() cross-references a list of newly-seen paths against the set of paths that were missing on the last scan, and rebuilds the track_locations rows accordingly.
  • Play counter -- updatePlayCounterFromPlayedHistory() updates timesplayed and last_played_at from the history table.

TrackDAO emits tracksAdded, tracksChanged, and tracksRemoved signals when the database is modified.

PlaylistDAO

Manages the Playlists and PlaylistTracks tables.

Playlists have a HiddenType column that repurposes the playlist mechanism for internal features:

HiddenType Meaning
PLHT_NOT_HIDDEN (0) User-visible playlist
PLHT_AUTO_DJ (1) Auto DJ queue
PLHT_SET_LOG (2) History (set log)

Key API:

int  createPlaylist(const QString& name, HiddenType type);
void deletePlaylist(int playlistId);
void renamePlaylist(int playlistId, const QString& newName);
bool setPlaylistLocked(int playlistId, bool locked);

bool appendTracksToPlaylist(const QList<TrackId>& trackIds, int playlistId);
bool insertTrackIntoPlaylist(TrackId trackId, int playlistId, int position);
void removeTrackFromPlaylist(int playlistId, int position);
void moveTrack(int playlistId, int oldPosition, int newPosition);

QList<TrackId> getTrackIdsInPlaylistOrder(int playlistId) const;
QList<QPair<int,QString>> getPlaylists(HiddenType hidden) const;

Track positions within a playlist are stored as an integer position column in PlaylistTracks and are contiguous but not necessarily sequential after insertions and deletions — use orderTracksByCurrPos() to compact them when needed.

CrateStorage

Unlike the DAOs, CrateStorage is accessed through TrackCollection which wraps the write operations in transactions and emits the appropriate signals. Do not call CrateStorage write methods directly — use TrackCollection::insertCrate(), addCrateTracks(), etc. instead.

CrateStorage exposes read-only query results as forward-only iterator objects rather than materializing full lists:

// Read a single crate by id or name:
bool readCrateById(CrateId id, Crate* pCrate) const;
bool readCrateByName(const QString& name, Crate* pCrate) const;

// Iterate all crates (ordered by name, locale-aware):
CrateSelectResult selectCrates() const;
// Usage:
CrateSelectResult result = crates.selectCrates();
Crate crate;
while (result.populateNext(&crate)) { /* ... */ }

// Iterate crate contents:
CrateTrackSelectResult selectCrateTracksSorted(CrateId crateId) const;

// Summary view (includes track count and total duration):
CrateSummarySelectResult selectCrateSummaries() const;

// Which crates contain a given track:
CrateTrackSelectResult selectTrackCratesSorted(TrackId trackId) const;

// Auto DJ sources:
CrateSelectResult selectAutoDjCrates(bool autoDjSource = true) const;

The Crate value type (crateid.h, crate.h) carries: CrateId id, QString name, bool locked, bool autoDjSource.

Other DAOs

DAO Table(s) Purpose
DirectoryDAO directories Watched root directories; used by scanner
AnalysisDao track_analysis Waveform summary data and BPM/key analysis blobs
CueDAO cues Hot cues, loop cues, intro/outro markers per track
LibraryHashDAO LibraryHashes Directory content hashes for fast change detection

GlobalTrackCache

The GlobalTrackCache is a process-wide singleton that ensures at most one Track object exists in memory for any given track at a time. All code that needs a Track goes through the cache so that metadata edits made in one place are immediately visible everywhere else.

Tracks are indexed by both TrackId and canonical file path. A lookup that hits the cache increments the shared_ptr reference count. When the count drops to zero (all callers have released their TrackPointer), the cache calls GlobalTrackCacheSaver::saveEvictedTrack() (implemented by TrackCollectionManager) to flush the Track to the database.

The cache is protected by an internal mutex, making it safe to access from multiple threads — though the actual database write-back occurs on the main thread.

TrackRef and TrackPointer

TrackRef (src/track/trackref.h) is a lightweight identifier that can hold either a TrackId or a canonical file location (or both). It is used to look up or create tracks without requiring a full Track object to be loaded.

TrackPointer is simply QSharedPointer<Track>. Code that holds a TrackPointer keeps the track alive in the cache. Releasing all TrackPointers to a track triggers save-and-eviction.

Library Scanner

LibraryScanner runs in a dedicated QThread and uses a QThreadPool of ScannerTask workers to parallelise directory traversal.

The scan pipeline:

  1. LibraryScanner::scan() signals startScan, launching the scan in the scanner thread's event loop.
  2. RecursiveScanDirectoryTask walks each watched root directory, hashing directory contents with LibraryHashDAO to skip unchanged directories on incremental scans.
  3. ImportFilesTask processes changed directories: new files are passed to TrackDAO with AddMissing | UnhideHidden flags; modified files have their metadata refreshed.
  4. After traversal, TrackDAO::detectMovedTracks() cross-references newly-seen paths with missing paths to resolve file moves.
  5. scanFinished() and scanSummary() signals are emitted. The summary reports counts of added, modified, relocated, and removed tracks.

The scanner uses its own QSqlDatabase connection from the shared DbConnectionPool, so it never blocks the main thread's database operations.

Track Models

The library uses the Qt model/view pattern. The abstract TrackModel interface (src/library/trackmodel.h) extends QAbstractTableModel with library-specific concepts:

  • Capability flags control what context-menu operations are available for a given model: Reorder, ReceiveDrops, AddToTrackSet, AddToAutoDJ, Locked, EditMetadata, LoadToDeck, LoadToSampler, Hide, Purge, etc.
  • SortColumnId is a stable enum (values must never change, as controller scripts reference them numerically) that identifies sortable columns independently of their display order.
  • getTrack(QModelIndex) and getTrackId(QModelIndex) map a view row back to a TrackPointer or TrackId.

BaseTrackCache

BaseTrackCache is a shared, in-memory column cache that sits between table models and the database. Because the base SQL view for the main track list involves a join between library and track_locations, and because multiple models would otherwise each maintain their own copy of the same data, BaseTrackCache caches all column values keyed by TrackId and exposes filterAndSort() for efficient search and multi-column sort without touching the database.

A single BaseTrackCache instance is created by TrackCollection and shared (as a QSharedPointer) among all table models that display the main library.

Concrete table models

Class What it shows
LibraryTableModel All non-deleted tracks in the main library
PlaylistTableModel Tracks in a specific playlist
CrateTableModel Tracks in a specific crate
BaseSqlTableModel Base class for SQL-backed models
BaseExternalTrackModel Tracks from an external collection
BaseExternalPlaylistModel Playlists from an external collection

External Libraries

ExternalTrackCollection is a pure-virtual interface for synchronising an external DJ application's library with Mixxx's internal one. Implementations exist for Rekordbox (src/library/rekordbox/), Serato (src/library/serato/), Traktor (src/library/traktor/), iTunes (src/library/itunes/), Rhythmbox (src/library/rhythmbox/), and Banshee (src/library/banshee/).

The contract for implementors:

class ExternalTrackCollection : public QObject {
  public:
    // Identifying information:
    virtual QString name() const = 0;
    virtual QString description() const = 0;

    // Lifecycle:
    virtual void establishConnection() = 0;   // async
    virtual void finishPendingTasksAndDisconnect() = 0; // sync/blocking

    enum class ConnectionState {
        Connecting, Connected, Disconnecting, Disconnected,
    };
    virtual ConnectionState connectionState() const = 0;

    // Change notifications (called by TrackCollectionManager after
    // the corresponding operation succeeds on the internal collection):
    virtual void relocateDirectory(const QString& oldRoot,
                                   const QString& newRoot) = 0;
    virtual void updateTracks(const QList<TrackRef>& updatedTracks) = 0;
    virtual void purgeTracks(const QList<TrackRef>& purgedTracks) = 0;
    virtual void purgeAllTracks(const QDir& rootDir) = 0;
};

All notification methods must be non-blocking — implementations should queue work to a background thread. Mixxx calls them after the internal database operation has already committed, so they are always best-effort notifications rather than part of the same transaction.

TrackCollectionManager holds a QList<ExternalTrackCollection*> and iterates it inside every mutating method to keep all collections in sync.

Read-only external collections

Several external libraries (Traktor, Rhythmbox, Banshee, iTunes) are read-only: Mixxx can import and browse their tracks but does not write back to their databases. These are implemented as LibraryFeature subclasses that parse the external library's database or XML export into a local view. They do not implement ExternalTrackCollection and are not registered with TrackCollectionManager.

Key Signals

The library backend communicates changes upward via Qt signals on TrackCollection:

Signal When emitted
tracksAdded(QSet<TrackId>) New tracks inserted into the database
tracksChanged(QSet<TrackId>) Metadata updated for existing tracks
tracksRemoved(QSet<TrackId>) Tracks hidden or purged
crateInserted(CrateId) A new crate was created
crateUpdated(CrateId) Crate name or properties changed
crateDeleted(CrateId) A crate was deleted
crateTracksChanged(CrateId, added, removed) Tracks added to or removed from a crate
crateSummaryChanged(QSet<CrateId>) Track count / duration summary needs refresh

Table models connect to these signals (via BaseTrackCache for the track-level ones) to invalidate and refresh their views without re-running a full SQL query.

Clone this wiki locally