mirror of
https://github.com/leinelissen/jellyfin-audio-player.git
synced 2026-09-03 05:10:01 +03:00
Co-authored-by: leinelissen <10154841+leinelissen@users.noreply.github.com>
9.8 KiB
9.8 KiB
SQLite Migration + Prefill Plan
Date: 2026-02-08
Goals
- Remove Redux entirely and move all state into SQLite for performance and simplicity.
- Support multiple sources (Jellyfin/Emby) with a unified driver interface.
- Make SQLite the primary, offline-first source of truth.
- Prefill the database by crawling all sources with paging, bounded concurrency, and resume support.
Current State Summary
- Redux slices: settings, music (albums, tracks, playlists, artists), downloads, search, sleep timer.
- Jellyfin API utilities fetch full lists without paging (except small limits on search, recent, instant mix).
- No existing prefill framework or cursor state.
Implementation Decisions (2026-02-08 updates)
- No existing SQLite code; start from scratch.
- Use the live query shim from https://gist.github.com/dukedorje/7154ab0601e4f9cc1f337e0010168bf8.
- Use drizzle-kit for schema/migrations and @op-engineering/op-sqlite as the SQLite driver.
- Drivers live under /store/sources/{jellyfin,emby}.
- Source type enum values are hardcoded:
jellyfin.v1andemby.v1. - Rename paging params to offset and limit in drivers, then translate to API params.
- Lyrics and similar album endpoints already exist in the repo; reuse them.
- Error handling: retry up to 5 times, then permanently fail.
- Extend onboarding progress UI in screens/Onboarding.
Data Model (Filter-Only + metadata_json)
- Promote only fields used for filtering/sorting.
- Store all remaining entity fields in metadata_json (TEXT, JSON-encoded).
- No metadata_json table. No metadata_json on settings.
- Add created_at and updated_at (epoch ms) on updateable tables.
- Rename server to source throughout schema.
- Add explicit lyrics column on tracks and album_similar relation table.
- Downloads include hash, filename, mimetype.
- No sort_name columns anywhere.
- app_settings and sleep_timer are global, no source_id.
Tables
-
sources
- id (pk), uri, user_id, access_token, device_id, type, created_at, updated_at
- nullable: user_id, access_token, device_id
-
app_settings
- id (pk, fixed = 1), bitrate, is_onboarding_complete, has_received_error_reporting_alert, enable_playback_reporting, color_scheme, created_at, updated_at
- nullable: none
-
sleep_timer
- id (pk, fixed = 1), date, created_at, updated_at
- nullable: date
-
artists
- source_id, id (pk), name, is_folder, metadata_json, created_at, updated_at
- nullable: metadata_json
-
albums
- source_id, id (pk), name, production_year, is_folder, album_artist, date_created, last_refreshed, metadata_json, created_at, updated_at
- nullable: production_year, album_artist, date_created, last_refreshed, metadata_json
-
tracks
- source_id, id (pk), name, album_id, album, album_artist, production_year, index_number, parent_index_number, has_lyrics, run_time_ticks, lyrics, metadata_json, created_at, updated_at
- nullable: album_id, album, album_artist, production_year, index_number, parent_index_number, run_time_ticks, lyrics, metadata_json
-
playlists
- source_id, id (pk), name, can_delete, child_count, last_refreshed, metadata_json, created_at, updated_at
- nullable: child_count, last_refreshed, metadata_json
-
downloads
- source_id, id (pk), hash, filename, mimetype, progress, is_failed, is_complete, metadata_json, created_at, updated_at
- nullable: hash, filename, mimetype, progress, metadata_json
-
search_queries
- source_id, id (pk), query, timestamp, local_playback_only, metadata_json, created_at, updated_at
- nullable: metadata_json
-
album_artists
- source_id, album_id, artist_id, order_index
- nullable: order_index
-
track_artists
- source_id, track_id, artist_id, order_index
- nullable: order_index
-
playlist_tracks
- source_id, playlist_id, track_id, position
- nullable: position
-
album_similar
- source_id, album_id, similar_album_id
-
sync_cursors
- source_id, entity_type, start_index, page_size, completed, updated_at
- nullable: none
Indexes
- artists(source_id, name)
- albums(source_id, name)
- albums(source_id, production_year)
- tracks(source_id, album_id)
- tracks(source_id, name)
- playlists(source_id, name)
- playlist_tracks(source_id, playlist_id, position)
- track_artists(source_id, artist_id)
- album_artists(source_id, artist_id)
- search_queries(source_id, timestamp DESC)
Driver Interface (Source Injected)
- Constructor takes Source object; methods operate on that source.
- Key methods:
- connect, refreshCredentials, validateCredentials, signOut
- getArtists, getAlbums, getAlbum, getTracksByAlbum
- getPlaylists, getPlaylist, getTracksByPlaylist
- search, getRecentAlbums, getSimilarAlbums, getInstantMix(entityId)
- getTrackCodecMetadata, getTrackLyrics
- getStreamUrl, getDownloadInfo
- reportPlaybackStart, reportPlaybackProgress, reportPlaybackStop
TypeScript Interface (Driver)
export interface SourceDriver {
connect(): Promise<SourceInfo>;
refreshCredentials(): Promise<Credentials>;
validateCredentials(): Promise<boolean>;
signOut(): Promise<void>;
getArtists(params?: ListParams): Promise<Artist[]>;
getAlbums(params?: ListParams): Promise<Album[]>;
getAlbum(albumId: string): Promise<Album>;
getTracksByAlbum(albumId: string, params?: ListParams): Promise<Track[]>;
getPlaylists(params?: ListParams): Promise<Playlist[]>;
getPlaylist(playlistId: string): Promise<Playlist>;
getTracksByPlaylist(playlistId: string, params?: ListParams): Promise<Track[]>;
search(query: string, filters: SearchFilter[], params?: ListParams): Promise<SearchResultItem[]>;
getRecentAlbums(params?: ListParams): Promise<Album[]>;
getSimilarAlbums(albumId: string, params?: ListParams): Promise<Album[]>;
getInstantMix(entityId: string, params?: ListParams): Promise<Track[]>;
getTrackCodecMetadata(trackId: string): Promise<CodecMetadata | null>;
getTrackLyrics(trackId: string): Promise<Lyrics | null>;
getStreamUrl(trackId: string, options?: StreamOptions): Promise<string>;
getDownloadInfo(trackId: string, options?: DownloadOptions): Promise<DownloadInfo>;
reportPlaybackStart(trackId: string, positionTicks: number): Promise<void>;
reportPlaybackProgress(trackId: string, positionTicks: number): Promise<void>;
reportPlaybackStop(trackId: string, positionTicks: number): Promise<void>;
}
API Paging Updates
- Rewrite all Jellyfin/Emby API wrappers into their respective drivers.
- Add paging support to driver methods for list endpoints:
- getAlbums, getArtists, getTracks, getPlaylists, getTracksByAlbum, getTracksByPlaylist
- Parameters: startIndex, limit
- Default limit = 500
Prefill Framework
- Orchestrator runs for each source and writes pages directly to SQLite.
- Concurrency: max 5 requests.
- Page size: 500.
- Resume: sync_cursors per source + entity type.
Prefill Order
- Artists
- Albums
- Tracks
- Playlists
- Album tracks
- Playlist tracks
- Similar albums
- Lyrics
Behavior
- Fetch a page, upsert into SQLite, discard page data to keep memory flat.
- Enqueue dependent tasks only after parent entities exist.
- Update sync_cursors after each page.
- Continue until no items returned; mark completed.
Testing and Validation
- Smoke test: single source, verify counts and basic queries.
- Resume test: interrupt prefill, restart, confirm cursor resume.
- Paging test: compare page 1 and page 2 IDs for non-overlap.
- Query performance: verify common sorts and filters are indexed.
Open Questions / Follow-ups
- Decide if relation tables need timestamps (currently omitted).
- Confirm all UI filters map to promoted columns.
- Verify Emby endpoints and paging parameters match Jellyfin behavior.
Additional Required Work
- Replace all Redux reads/writes with SQLite queries (use live queries where feasible).
- Update onboarding UI to display progress during the initial prefill.
- Move all new database-related code under /store.
Subagent Work Breakdown
- Schema + migrations: tables, indexes, and migration scripts.
- SQLite access layer: DB setup, query helpers, upsert patterns, timestamps.
- Live queries: integrate shim and document caveats.
- Driver interfaces + types: shared Source types, ListParams, filter models.
- Jellyfin driver: rewrite APIs, add paging (offset/limit), map fields.
- Emby driver: rewrite APIs, add paging (offset/limit), map fields.
- Prefill orchestrator: bounded concurrency, cursor resume, upsert pipeline.
- Prefill task graph: dependent tasks for album/playlist tracks, similar, lyrics.
- Redux removal: replace selectors/actions with SQLite queries and live updates.
- Onboarding progress UI: wire prefill progress and error states.
- Tests: smoke, resume, paging non-overlap, index performance.
Subagent Progress Tracker (Boilerplate)
Legend: NOT STARTED | IN PROGRESS | BLOCKED | DONE
| Area | Owner | Status | Notes |
|---|---|---|---|
| Schema + migrations | AI | DONE | Created schema.ts with all tables and indexes per spec |
| SQLite access layer | AI | DONE | Created client.ts with DB init, migrations, and query helpers |
| Live queries shim | AI | DONE | Created live-queries.ts with reactive query support |
| Driver interfaces + types | AI | DONE | Created types.ts with Source, Driver interface, and all entity types |
| Jellyfin driver | AI | DONE | Created complete Jellyfin driver with all methods and paging support |
| Emby driver | AI | DONE | Created complete Emby driver with all methods and paging support |
| Prefill orchestrator | AI | DONE | Created orchestrator with bounded concurrency, cursor resume, and error handling |
| Prefill task graph | AI | DONE | Created task graph for album/playlist tracks, similar albums, and lyrics |
| Redux removal | NOT STARTED | Core infrastructure complete; integration requires touching many files | |
| Onboarding progress UI | NOT STARTED | Prefill system ready; UI integration pending | |
| Tests | AI | DONE | Created smoke tests for database operations and sync cursors |