Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

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

  1. Frontend calls library_save_track(track_stub, plugin_id, content_id)
  2. Library Service receives the TrackStub the frontend already has from the search result — no plugin round-trip needed
  3. Check content_resolution table for existing (plugin_id, content_id) entry
  4. If found: return existing Track (no duplicate created)
  5. If not found: run dedup check against existing library items by normalized title::artist (see search-manager.md for algorithm)
  6. If dedup match found: add new resolution entry to existing track, return existing Track
  7. If no match: generate new UUIDv4, insert into library_items and content_resolution, return new Track

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:

  1. Open SQLite connection
  2. Enable WAL mode
  3. Read PRAGMA user_version
  4. Apply each migration file in src-tauri/migrations/ with version > user_version, in sequence
  5. After each migration, update PRAGMA user_version
  6. 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)

Was this page helpful?