The library is a per-profile SQLite database (<root>/profiles/<id>/data.db) keyed by canonical file path. It survives moves and renames as much as possible β see the import matcher in playlists.
- Tag extraction β
lofty 0.24reads ID3v2 / Vorbis Comments / MP4 atoms and surfaces title, artist(s), album, album artist, year, track / disc number, genre, embedded artwork, POPM ratings, and the tagged musical key (TKEY/INITIALKEY). - Folder cover fallback β when a track carries no embedded picture, the scanner inspects its parent directory for a sidecar image with one of the canonical stems (
cover,folder,front,albumart,album,artwork) and an extension the thumbnail pipeline can decode (jpg/jpeg,png,webp,bmp,gif,tiff). The first match β by stem priority, not alphabetical β is hash-addressed into the sharedartwork/dir like an embedded picture. The provenance is taggedsource = 'folder'in theartworktable so a future cleanup job can distinguish it from'embedded','deezer', or'user'entries. Covers common CD-rip / lossless layouts where the artwork sits beside the audio files. - Audio quality β sample rate, bitrate, channel count, bit depth and codec are captured at scan time. Hi-Res badges (β₯ 24-bit, β₯ 44.1 kHz) light up automatically on covers and rows.
- Watch folders β
notify 8drives a per-folder filesystem watcher with debounced rescans so files dropped into a watched directory appear without a manual refresh. Deletions flag rowsis_available = 0rather than purging them, so play history, ratings and playlist memberships survive a reorganisation.
analysis.rs computes peak, integrated loudness (dB), ReplayGain (β18 LUFS reference) and BPM (autocorrelation). Runs on demand (per track) or as a background sweep (whole library), gated by a Settings toggle. Results land in track_analysis and feed:
- per-stream gain in the audio engine (
replaygain_enabled), - the BPM bucketing in smart playlists,
- the per-track audio specs strip under the player.
The scanner splits "Artist A, Artist B" (and ; / feat. / & variants) on insert. Each contributor lands in its own artist row, linked to the track via the track_artist many-to-many table with a position column for stable ordering. Queries rebuild the display string with GROUP_CONCAT(...) ORDER BY position. The ArtistLink React component receives parallel artist_name + artist_ids strings so every contributor is individually clickable, matching Spotify's behaviour.
- Library tabs β Morceaux, Albums, Artistes, Genres, Dossiers; each tab keeps its own scroll position and sort memory (per profile). Clicking a genre tile opens a Spotify-style genre detail page (
get_genre_detailinbrowse.rs) with every track tagged with that genre, sorted Artist β Album β Disc β Track. - A-Z navigator β letter rail on the artists tab, NFD-normalised so accents (Γ β E, Γ β N) bucket correctly.
- Multi-select β ctrl/shift across rows with a floating action bar (Play / Add to queue / Add to playlist / Remove) anchored to the bottom of the viewport.
- Track Properties dialog β foobar2000-style modal with the full tag set, audio specs, analysis results, file path and a Show in Explorer button.
- POPM ratings β 5-star with half-steps, round-tripped to the file's tag. Edit surfaces: inline
StarRatingin the library track list, integer-star submenu in the right-clickTrackContextMenu(any view), full half-star widget in theTrackPropertiesModal. The backend commandset_track_ratingwrites the POPM frame back to the file (binary<email>\0<rating><counter>for ID3v2, textRATING=0-100for Vorbis / MP4 / APE), updatestrack.ratingin the DB, then emitstrack:updatedso every open view re-fetches without polling. Containers lofty can't open (DSD) keep a DB-only rating; the next folder scan still preserves it because the fast-path skip on(mtime, size)never re-extracts unchanged files. Smart playlists expose this as therating_minrule β see smart-playlists.md. - Lightbox β double-click any cover or artist photo to view full-size with keyboard navigation.
FTS5 contentless index over title, artist, album with prefix matching. Auto-sync triggers (AFTER INSERT/UPDATE/DELETE on track) keep the index current using the 'delete' command on the contentless table. Queries are issued from the React top bar with a 150 ms debounce.
commands/library.rs exposes the watch-folder lifecycle: add_folder_to_library, set_folder_watched (toggle the in-memory notify watcher), and remove_folder_from_library. The remove path detaches the watcher, deletes every track that lived under the folder, then drops the library_folder row in a single transaction. The schema's track.folder_id ON DELETE SET NULL would otherwise leave orphan tracks with library_id still set β making the user "remove" a folder while its tracks stayed in the library, which never matches what they expect.
UI: per-folder trash button in the Library β Folders tab, two-step confirm-on-second-click that auto-clears after 3 s.
hooks/useDragDropImport.ts wires Tauri 2's window-level onDragDropEvent into the existing import flow via a single backend command: commands/library.rs::import_paths. The command accepts a mix of folders and audio files β files contribute their parent directory β dedupes the resolved folder set, then for each one tries an INSERT OR IGNORE INTO library_folder (the (library_id, path) UNIQUE constraint absorbs duplicates) and runs scan_folder_inner. Aggregated ScanSummary is returned to the frontend so the user sees one toast with the total counts.
Auto-creates a default library on the very first drop when the profile has none, mirroring the existing pickFolder import path.
UI: emerald drop overlay in AppLayout renders a fade-in border + drop hint while the user is dragging, then a spinner while the backend scan runs. pointer-events: none on the overlay so the drop still hits Tauri's native handler.
commands/duplicates.rs groups every available track by file_hash (BLAKE3 of the audio bytes, computed at scan time) so byte-identical copies in different folders fall into the same bucket regardless of metadata. Re-encodes of the same source β e.g. CBR vs VBR rips β won't match because the bytes differ; that's a fingerprinting problem and out of scope for the MVP.
find_duplicates returns one entry per group, ordered by added_at ASC so the oldest copy renders first (usually the one to keep). delete_tracks(track_ids) cascades through the schema's ON DELETE constraints to clean up track_artist, track_genre, playlist_track, play_event, etc. β but the audio files on disk are not touched: the user removes them via the OS so we don't accidentally wipe a backup.
UI: DuplicatesModal launched from Settings β Stockage β "Rechercher". Each group exposes a radio selector (defaults to oldest) and the footer's "Supprimer N doublons" wipes every other entry from the database.
commands/deezer.rs::set_album_artwork_from_deezer and set_album_artwork_from_file. The file picker validates magic bytes (JPEG / PNG / WebP) before accepting upload, and batch_fetch_missing_album_covers walks all albums without an artwork_id, querying Deezer in parallel with a small concurrency cap.
Scanner sidecar lookup, mirror of the folder-cover fallback but resolved against the track's ancestors instead of the immediate parent.
commands/scan.rs::extract_artist_image walks up to 3 parent directories from each track and accepts the first match where either:
- the filename stem is one of
ARTIST_IMAGE_STEMS = ["artist", "performer", "band"], or - the stem's
canonical_name(...)equals the artist's canonical name (coversDaft Punk.jpgat the root of aDaft Punk/folder).
Both common layouts from issue #31 work out of the box:
Music/<Artist>/<Album>/track.flacβ matchesartist.jpgtwo levels up.Music/<Album>/track.flacβ matches<Artist>.jpgsitting beside the album folder (strict name-match so an unrelatedcover.jpgis never mistaken for an artist photo).
Hash-addressed via BLAKE3 into the shared artwork/<hash>.{jpg,png,webp,β¦} cache and linked through the existing artist.artwork_id β artwork foreign key (no schema change). The UPDATE β¦ WHERE artwork_id IS NULL guard means scanner runs never overwrite a manually uploaded image or a previously cached Deezer picture.
Resolution priority in commands/browse.rs::get_artist_detail is now: local sidecar β Deezer cache β live Deezer fetch (last skipped when offline). ArtistDetailView prefers artwork_path over picture_path and refuses to clobber a local image with a late-arriving Deezer response.
The "Various Artists" sentinel is explicitly excluded so a compilation folder never inherits a stray album cover as an artist photo.
For libraries scanned before the feature shipped, commands/scan.rs::rescan_local_artist_images (exposed as Settings β Library β Local artist images) walks every artist WHERE artwork_id IS NULL and probes up to 16 tracks per artist with extract_artist_image, stopping at the first hit. Already-linked rows are filtered out at the SQL level, so the rescan is cheap to re-run.
The pencil overlay on the artist photo in ArtistDetailView opens ArtistImagePickerModal, which exposes three actions backed by commands/deezer.rs:
- Search Deezer β
search_artists_deezer+set_artist_artwork_from_deezer(downloads the chosen picture into the profile artwork cache, marks source"deezer"). - Pick a local file β
set_artist_artwork_from_file(same magic-byte validation as the album cover picker: jpg / png / webp). - Remove image β
clear_artist_artworksetsartist.artwork_id = NULLso the next render falls back through the resolution chain (Deezer cache β live fetch).
Both set_artist_artwork_from_* overwrite artwork_id unconditionally β an explicit user pick beats any automatic resolution.