Library Service specification
Library Service specification
Responsibility
CRUD for saved tracks, favorites, playlists, and playback history. Content resolution (core UUID ↔ plugin + foreign_id). Deduplication on first save. Schema migration on startup.
Public API
impl LibraryService {
// Tracks
pub fn save_track(&self, plugin_id: &str, content_id: &str, meta: TrackStub) -> Result<Track>;
pub fn remove_track(&self, id: Uuid) -> Result<()>;
pub fn get_track(&self, id: Uuid) -> Result<Option<Track>>;
pub fn find_by_foreign(&self, plugin_id: &str, content_id: &str) -> Result<Option<Track>>;
pub fn list_tracks(&self, offset: usize, limit: usize) -> Result<Vec<Track>>;
pub fn search_tracks(&self, query: &str) -> Result<Vec<Track>>;
// Favorites
pub fn favorite(&self, id: Uuid) -> Result<()>;
pub fn unfavorite(&self, id: Uuid) -> Result<()>;
pub fn list_favorites(&self) -> Result<Vec<Track>>;
// Playlists
pub fn create_playlist(&self, name: &str, description: Option<&str>) -> Result<Playlist>;
pub fn delete_playlist(&self, id: Uuid) -> Result<()>;
pub fn add_to_playlist(&self, playlist_id: Uuid, track_id: Uuid) -> Result<()>;
pub fn remove_from_playlist(&self, playlist_id: Uuid, track_id: Uuid) -> Result<()>;
pub fn reorder_playlist(&self, playlist_id: Uuid, track_ids: Vec<Uuid>) -> Result<()>;
pub fn get_playlist(&self, id: Uuid) -> Result<Option<Playlist>>;
pub fn list_playlists(&self) -> Result<Vec<Playlist>>;
// History
/// Record a play. Accepts either a saved track UUID or inline metadata
/// for unsaved tracks. Routes to playback_history or played_tracks accordingly.
pub fn record_play(&self, play: PlayRecord) -> Result<()>;
pub fn get_history(&self, limit: usize) -> Result<Vec<Track>>;
pub fn clear_history(&self) -> Result<()>;
}
Save flow
- Frontend calls
library_save_track(track_stub, plugin_id, content_id) - Library Service receives the
TrackStubthe frontend already has from the search result — no plugin round-trip needed - Check
content_resolutiontable for existing(plugin_id, content_id)entry - If found: return existing
Track(no duplicate created) - If not found: run dedup check against existing library items by normalized
title::artist(see search-manager.md for algorithm) - If dedup match found: add new resolution entry to existing track, return existing
Track - If no match: generate new UUIDv4, insert into
library_itemsandcontent_resolution, return newTrack
PlayRecord
/// Routes to playback_history when track_id is Some (track is saved),
/// or played_tracks when metadata is provided inline (track is unsaved).
pub struct PlayRecord {
pub track_id: Option<Uuid>, // saved track key
pub plugin_id: Option<String>, // for unsaved tracks
pub content_id: Option<String>, // for unsaved tracks
pub title: Option<String>, // for unsaved tracks
pub artist_name: Option<String>, // for unsaved tracks
pub album_title: Option<String>, // for unsaved tracks
pub position_seconds: Option<i64>,
pub completed: bool,
}
Migration runner
Runs on LibraryService::new(). Steps:
- Open SQLite connection
- Enable WAL mode
- Read
PRAGMA user_version - Apply each migration file in
src-tauri/migrations/with version >user_version, in sequence - After each migration, update
PRAGMA user_version - If any migration fails, log error and propagate as fatal (app cannot start without database)
Migration file naming: src-tauri/migrations/001_description.sql
References
- ADR-002 (SQLite for library storage)
- ADR-003 (UUIDv4 content IDs)
- FR-CORE-3 (unified library)
- FR-ID-1 (content resolution)