User-curated playlists live in the per-profile playlist table alongside the auto-generated smart playlists. The is_smart flag is the only thing distinguishing them in the schema β the UI filters on it to render the two groups in their own sections.
commands/playlist.rs exposes:
list_playlists/get_playlistβ both computetrack_countandtotal_duration_msin the SELECT (denormalisation lives in the query, not the schema) and resolvecover_hashβcover_pathfrom the sharedmetadata_artworkcache.create_playlist,update_playlist,delete_playlistβ all bumpupdated_at.add_track_to_playlist/add_tracks_to_playlist/remove_track_from_playlist/reorder_playlist_track.add_source_to_playlistβ bulk-add by(source_type, source_id): every track of an album / artist / library / liked / recent in one round-trip.
Drag-and-drop is implemented with @dnd-kit over a virtualised list (@tanstack/react-virtual). The reorder_playlist_track command renumbers the affected position slice in a single transaction; rows outside the moved range stay untouched.
export_playlist_m3u writes a UTF-8 .m3u8 with #EXTINF:<duration>,<artist> - <title> headers and absolute file paths. The importer accepts both .m3u and .m3u8 (foobar2000, VLC, Rekordbox, hand-written sets) and matches against the active library:
- Canonical-path match β strip Windows
\\?\prefix, lowercase the drive letter, normalise separators. - Basename fallback β if path-match fails, try the basename. This recovers playlists exported before a library reorganisation.
Unmatched entries are surfaced (capped at 20 to keep the toast readable) so the user can investigate without losing the import.
liked_track is a one-column table keyed by track_id with a liked_at timestamp. The dedicated "Liked" view sorts by liked_at DESC; the heart icon in track rows is wired to toggle_like_track.
User playlists support custom covers, managed alongside the existing cover_hash column added for smart playlists. Two modes, one column flag:
cover_is_auto |
Behaviour |
|---|---|
1 (default) |
Auto-cover. After every mutation (add_track, add_tracks, remove_track, reorder_track, add_source, import_m3u), the backend re-runs the compositor on the first 4 album artworks (Spotify-style 2Γ2 grid). |
0 |
Manual upload. Mutations leave the cover untouched. |
commands/playlist_cover.rs exposes three Tauri commands:
set_playlist_cover_from_file(playlist_id, file_path)β magic-byte validates jpg/png/webp (8 MB cap), normalises through the same compositor used for auto-covers (re-encodes to a 640Γ640 JPEG so everycover_hashresolves to one extension), flipscover_is_auto = 0.regenerate_playlist_auto_cover(playlist_id)β explicit "refresh now" escape hatch.clear_playlist_cover(playlist_id)β drops the manual cover, switches back tocover_is_auto = 1, and immediately re-runs the auto-pipeline so the visual feedback is instant rather than blank-until-next-mutation.
Smart playlists (is_smart = 1) are excluded from every code path here β they're owned by the smart-playlist regen flow. The post-mutation hook (playlist_cover::maybe_regen_auto_cover) checks both flags before running and logs a warning on failure (never blocks the mutation it was triggered by).
The frontend exposes the controls in the edit modal (CreatePlaylistModal.tsx) Spotify-style: large preview tile on the left, hover overlay with a pencil icon ("Choose photo"), and a ... menu top-right with "Change photo" / "Remove photo". The remove option is conditional on cover_hash != null.
browse.rs::list_recent_plays projects the last 50 distinct tracks from play_event, deduplicated by track id (you only see a given track once even if you played it three times in a row). Drives both the "RΓ©cents" sidebar entry and the home carousel.