Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/TheOutdoorProgrammer/crate/llms.txt

Use this file to discover all available pages before exploring further.

Crate stores all library data in a SQLite database (crate.db) opened in WAL mode. The schema is intentionally simple: five tables in the main database plus a separate activity log. This page documents every column, index, and constraint — useful if you are writing a custom importer, a migration script, or running analytical queries against your collection.
The built-in importer (Settings → Library → Import Existing Library) handles the common case: MP3 and FLAC files with embedded tags, with dry-run preview and idempotent re-runs. Use direct database writes only when the built-in importer does not fit — unsupported formats, exotic tag layouts, or bulk migrations from another tool.

Main Database (crate.db)

artists

CREATE TABLE artists (
    id                       INTEGER PRIMARY KEY AUTOINCREMENT,
    name                     TEXT    NOT NULL,
    provider                 TEXT    NOT NULL,
    provider_id              TEXT    NOT NULL,
    image_url                TEXT,
    status                   TEXT    NOT NULL DEFAULT 'watched'
                                     CHECK (status IN ('watched', 'partial', 'owned')),
    watch_new_releases        INTEGER NOT NULL DEFAULT 0,
    watch_new_releases_since  TEXT,
    created_at               TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at               TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
ColumnTypeNotes
idINTEGER PKAuto-increment
nameTEXT NOT NULLArtist display name
providerTEXT NOT NULLProvider name, e.g. musicbrainz, deezer, local
provider_idTEXT NOT NULLProvider-specific ID (MBID, Deezer int as string, loc-… hash)
image_urlTEXTArtist image URL; nullable
statusTEXTwatched — full discography tracked; partial — some albums present; owned — files exist but not fully tracked
watch_new_releasesINTEGER0 or 1; opt-in per-artist new-release detection
watch_new_releases_sinceTEXTRFC3339 timestamp; only releases after this date are auto-added
created_atTEXTRFC3339
updated_atTEXTRFC3339

albums

CREATE TABLE albums (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    artist_id   INTEGER NOT NULL REFERENCES artists(id) ON DELETE CASCADE,
    title       TEXT    NOT NULL,
    year        INTEGER,
    provider    TEXT    NOT NULL,
    provider_id TEXT    NOT NULL,
    cover_url   TEXT,
    record_type TEXT    NOT NULL DEFAULT 'album',
    status      TEXT    NOT NULL DEFAULT 'watched'
                        CHECK (status IN ('watched', 'owned', 'ignored')),
    created_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
ColumnTypeNotes
idINTEGER PKAuto-increment
artist_idINTEGER FKReferences artists(id); cascades on delete
titleTEXT NOT NULLAlbum title
yearINTEGERRelease year; nullable
providerTEXT NOT NULLSame convention as artists.provider
provider_idTEXT NOT NULLProvider-specific album/release-group ID
cover_urlTEXTAlbum cover URL; nullable
record_typeTEXTalbum, single, ep, or compilation
statusTEXTwatched — all tracks wanted; owned — files present; ignored — tracks suppressed
created_atTEXTRFC3339
updated_atTEXTRFC3339

tracks

CREATE TABLE tracks (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    album_id            INTEGER NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
    title               TEXT    NOT NULL,
    track_number        INTEGER NOT NULL DEFAULT 1,
    disc_number         INTEGER NOT NULL DEFAULT 1,
    duration_ms         INTEGER NOT NULL DEFAULT 0,
    provider            TEXT    NOT NULL,
    provider_id         TEXT    NOT NULL,
    status              TEXT    NOT NULL DEFAULT 'wanted'
                                CHECK (status IN ('wanted', 'downloading', 'owned', 'ignored')),
    file_path           TEXT,
    created_at          TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at          TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    -- Added by later migrations:
    downloaded_from     TEXT,
    download_format     TEXT,
    download_bitrate    INTEGER,
    downloaded_filename TEXT,
    mb_recording_id     TEXT
);
ColumnTypeNotes
idINTEGER PKAuto-increment
album_idINTEGER FKReferences albums(id); cascades on delete
titleTEXT NOT NULLTrack title
track_numberINTEGERTrack number on disc; defaults to 1
disc_numberINTEGERDisc number; defaults to 1
duration_msINTEGERDuration in milliseconds
providerTEXT NOT NULLSame convention as parent album
provider_idTEXT NOT NULLProvider-specific track ID
statusTEXTwanted — queued for download; downloading — in progress; owned — file exists; ignored — suppressed
file_pathTEXTPath to file on disk when owned. Paths relative to the library dir and absolute paths both work. Crate never deletes files outside the library dir.
downloaded_fromTEXTslskd username the file was downloaded from
download_formatTEXTFile format detected at search time: flac, mp3, wav, etc.
download_bitrateINTEGERBitrate in kbps (0 for lossless formats like FLAC)
downloaded_filenameTEXTOriginal filename as downloaded from slskd, before organizing
mb_recording_idTEXTMusicBrainz recording ID (AcoustID/Picard); used as a high-confidence match signal during import
created_atTEXTRFC3339
updated_atTEXTRFC3339

download_queue

Do not insert rows into download_queue directly. The table is managed entirely by the download service. To make Crate download a track, set tracks.status = 'wanted' and let the scheduler pick it up — or trigger an immediate search from the UI.
CREATE TABLE download_queue (
    id                   INTEGER PRIMARY KEY AUTOINCREMENT,
    track_id             INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
    slskd_search_id      TEXT,
    status               TEXT    NOT NULL DEFAULT 'pending'
                                 CHECK (status IN ('pending', 'searching', 'downloading',
                                                   'organizing', 'complete', 'failed')),
    attempts             INTEGER NOT NULL DEFAULT 0,
    last_attempt         TEXT,
    error                TEXT,
    created_at           TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    -- Added by later migrations:
    next_retry_at        TEXT,
    source               TEXT    NOT NULL DEFAULT 'user',
    last_progress_bytes  INTEGER NOT NULL DEFAULT 0
);
ColumnTypeNotes
idINTEGER PKAuto-increment
track_idINTEGER FKReferences tracks(id); cascades on delete
slskd_search_idTEXTslskd search UUID, or username|transferID during an active download
statusTEXTpendingsearchingdownloadingorganizingcomplete / failed
attemptsINTEGERNumber of attempts made
last_attemptTEXTRFC3339 timestamp of most recent attempt
errorTEXTError message from last failure; nullable
next_retry_atTEXTRFC3339 timestamp for next automatic retry; nullable
sourceTEXTuser for manually triggered downloads; other values reserved for internal use
last_progress_bytesINTEGERByte offset of the most recent transfer progress update
created_atTEXTRFC3339

settings

A flat key/value store for all runtime configuration. Values from this table override environment-variable defaults and are set through the Settings UI.
ColumnTypeNotes
keyTEXT PKSetting identifier
valueTEXT NOT NULLSetting value
Known keys:
KeyDescription
provider_primaryActive provider for search (e.g. musicbrainz)
slskd_urlslskd API base URL
slskd_api_keyslskd API key
library_pathAbsolute path to the organized library directory
download_format_preferencePreferred format string
min_bitrateMinimum acceptable bitrate in kbps
scan_intervalHow often to auto-queue wanted tracks (e.g. 6h)
cache_ttl_hoursProvider response cache TTL in hours (default 24)
quality_tiersJSON array of quality tier objects
upgrade_last_artist_idInternal cursor for the daily upgrade scanner
navidrome_urlNavidrome base URL for post-download library scans
navidrome_userNavidrome username
navidrome_passwordNavidrome password
activity_retention_daysHow many days of activity log to keep
naming_templateLibrary folder/file layout template (empty = default {artist}/{album} ({year})/{track:2} - {title})

slskd_blacklist

Tracks files and users that should never be retried during automatic or manual searches.
CREATE TABLE slskd_blacklist (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    username   TEXT NOT NULL,
    filename   TEXT NOT NULL,
    reason     TEXT NOT NULL DEFAULT '',
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
CREATE UNIQUE INDEX idx_blacklist_user_file ON slskd_blacklist(username, filename);
ColumnTypeNotes
idINTEGER PKAuto-increment
usernameTEXT NOT NULLslskd username
filenameTEXT NOT NULLFull file path on the remote user’s machine
reasonTEXT NOT NULLWhy it was blacklisted (e.g. "transfer Errored"); empty string when unspecified
created_atTEXTRFC3339
A unique index on (username, filename) prevents duplicates. Blacklisted entries are skipped by the file-picker during both auto-downloads and manual searches, but remain visible (dimmed) in the manual search results.

user_cooldowns

Temporarily blocks users who triggered a shadow ban (e.g. went offline mid-transfer or whose queued downloads stalled). Unlike the blacklist, entries expire automatically.
CREATE TABLE user_cooldowns (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    username   TEXT NOT NULL,
    reason     TEXT NOT NULL DEFAULT '',
    expires_at TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
CREATE INDEX idx_cooldowns_username ON user_cooldowns(username);
CREATE INDEX idx_cooldowns_expires  ON user_cooldowns(expires_at);
ColumnTypeNotes
idINTEGER PKAuto-increment
usernameTEXT NOT NULLslskd username under cooldown
reasonTEXT NOT NULLWhy the cooldown was applied; empty string when unspecified
expires_atTEXT NOT NULLRFC3339 timestamp; downloader skips this user until this time passes
created_atTEXTRFC3339

Indexes

CREATE INDEX idx_albums_artist_id        ON albums(artist_id);
CREATE INDEX idx_tracks_album_id         ON tracks(album_id);
CREATE INDEX idx_tracks_status           ON tracks(status);
CREATE INDEX idx_download_queue_status   ON download_queue(status);
CREATE INDEX idx_download_queue_track_id ON download_queue(track_id);

-- Prevents two active download attempts for the same track
CREATE UNIQUE INDEX idx_download_queue_track_active
    ON download_queue(track_id)
    WHERE status IN ('pending', 'searching', 'downloading', 'organizing');

-- Deduplication constraints
CREATE UNIQUE INDEX idx_artists_provider ON artists(provider, provider_id);
CREATE UNIQUE INDEX idx_albums_provider  ON albums(provider, provider_id);
CREATE UNIQUE INDEX idx_tracks_provider  ON tracks(provider, provider_id);
The unique partial index idx_download_queue_track_active is the duplicate-download guard: SQLite enforces that at most one row per track_id can exist in any active state at the same time, making it impossible to accidentally queue the same track twice.

Writing a Custom Importer

If the built-in importer doesn’t fit your workflow, insert directly into artists, albums, and tracks. A few conventions keep your data compatible with the rest of Crate:
Use local as the provider value for any entity that does not have a real provider ID. local is a reserved name — it is always treated as healthy (no orphaned indicator, no gRPC calls), and entities using it can be relinked to a real provider at any time from the UI or via the Relink API.If you do have real provider IDs (e.g. you are importing from a MusicBrainz export), use musicbrainz or deezer as the provider value and supply the actual IDs. Those entities will behave identically to ones discovered through search.For the local provider, use any unique string as the provider_id. A hash of artist + album + title (lowercase, trimmed) works well — it survives file moves and is stable across re-runs.

SQL Example

The following inserts a fully owned album with one track:
-- Insert an artist
INSERT INTO artists (name, provider, provider_id, status, created_at, updated_at)
VALUES ('Radiohead', 'musicbrainz', 'a74b1b7f-71a5-4011-9441-d0b5e4122711', 'partial',
        strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));

-- Insert an album (use last_insert_rowid() for artist_id)
INSERT INTO albums (artist_id, title, year, provider, provider_id, record_type, status,
                    created_at, updated_at)
VALUES (last_insert_rowid(), 'OK Computer', 1997, 'musicbrainz',
        'b1392450-e666-3926-a536-22c65f834433', 'album', 'owned',
        strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));

-- Insert an owned track
INSERT INTO tracks (album_id, title, track_number, disc_number, duration_ms,
                    provider, provider_id, status, file_path, created_at, updated_at)
VALUES (last_insert_rowid(), 'Airbag', 1, 1, 282000,
        'musicbrainz', 'some-track-mbid', 'owned',
        '/music/Radiohead/OK Computer (1997)/01 - Airbag.flac',
        strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
Custom importer checklist:
  • Set status = 'owned' on tracks and provide file_path — otherwise the scheduler will queue them for download.
  • Set status = 'wanted' on tracks you want Crate to find and download for you.
  • Use local as provider (with a hash-derived ID) when you have no real provider IDs.
  • Do not insert into download_queue; let Crate manage that table.
  • Foreign keys cascade on delete — removing an artist row removes all of its albums and tracks.

Activity Log (activity.db)

The activity log lives in a separate SQLite file (activity.db by default; configurable via CRATE_ACTIVITY_PATH). It contains a single table:
CREATE TABLE activity_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    action      TEXT,
    entity_type TEXT,
    entity_id   INTEGER,
    details     TEXT,
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
ColumnDescription
actionEvent type, e.g. download_complete, library_import, track_bad
entity_typeThe kind of entity involved: track, album, artist, library
entity_idThe primary key of the entity in crate.db
detailsHuman-readable summary string
created_atRFC3339 timestamp
The activity log is completely independent of crate.db. You can delete it, truncate activity_log, or point CRATE_ACTIVITY_PATH at a new file at any time — the main database is unaffected. Retention is configured via the activity_retention_days settings key.

Build docs developers (and LLMs) love