Phase 1 of audiobooks integration - establishes local storage foundation: - Add Drift (SQLite) database with tables for profiles, recently played, library cache, and sync metadata - Add DatabaseService singleton for database access - Add ProfileService for automatic user profile management - Profiles auto-created from MA authentication (username) or manual entry - Migration support for existing ownerName to profile - Hook profile creation into MA login flow and login screen - Update CI workflow to generate Drift code with build_runner - Add comprehensive implementation plan document This enables: - Per-user recently played isolation (no cross-contamination) - Fast app startup with cached data - Foundation for audiobook progress tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.0 KiB
8.0 KiB
Audiobooks Integration - Implementation Plan
Overview
Add audiobook support to Ensemble with automatic user profile separation based on Music Assistant authentication. Includes local database for performance caching and per-user data isolation.
Phase 1: Database Foundation
Goal: Add persistent local storage layer using Drift (SQLite)
Tasks
- Add Drift package dependencies (
drift,drift_flutter,path_provider) - Create database class with schema version management
- Create
profilestable:class Profiles extends Table { TextColumn get username => text()(); // MA username (primary key) TextColumn get displayName => text().nullable()(); TextColumn get source => text()(); // 'ma_auth' or 'manual' DateTimeColumn get createdAt => dateTime()(); BoolColumn get isActive => boolean().withDefault(const Constant(false))(); } - Create
recently_playedtable:class RecentlyPlayed extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get profileUsername => text().references(Profiles, #username)(); TextColumn get mediaId => text()(); TextColumn get mediaType => text()(); // 'track', 'album', 'artist', 'audiobook' TextColumn get name => text()(); TextColumn get artist => text().nullable()(); TextColumn get imageUrl => text().nullable()(); DateTimeColumn get playedAt => dateTime()(); } - Create
library_cachetable for albums/artists/audiobooks - Generate Drift code (
dart run build_runner build) - Create
DatabaseServicesingleton with initialization
Acceptance Criteria
- Database initializes on app startup
- Schema migrations work correctly
- CRUD operations functional for all tables
Phase 2: Profile System
Goal: Auto-create and manage user profiles based on MA authentication
Tasks
- Create
ProfileServiceclass - On MA login success: check if profile exists for username
- If not, create new profile with
displayNamefrom MA user info - Set as active profile
- If not, create new profile with
- On manual name entry (no-auth): create/select profile by entered name
- Add
getCurrentProfile()method - Migrate existing
ownerNameto profile on first launch post-update - Add profile indicator to settings screen (show current profile name)
Acceptance Criteria
- Profile auto-created on first login
- Correct profile selected on subsequent logins
- Existing users migrated seamlessly
Phase 3: Recently Played Migration
Goal: Move recently played from memory cache to database, scoped by profile
Tasks
- Update
CacheServiceto use database for recently played - Scope all recently played queries by
profileUsername - Update home screen to load recently played from database
- Add
addRecentlyPlayed()method that records profile + media info - Limit recently played to last 50 items per profile
- Remove old memory-based recently played logic
Acceptance Criteria
- Recently played persists across app restarts
- Each profile sees only their own recently played
- No cross-contamination between profiles
Phase 4: Library Caching
Goal: Cache library data locally for instant UI and reduced API calls
Tasks
- Create
SyncServicefor background library synchronization - On app launch: load from cache immediately, sync in background
- Store albums, artists in
library_cachetable - Add
lastSyncedtimestamp to track freshness - Implement delta sync (only fetch changed items if MA supports it)
- Add pull-to-refresh that forces full sync
- Show subtle sync indicator when background sync in progress
Acceptance Criteria
- Home screen loads instantly from cache
- Library tabs load instantly
- Background sync updates data without blocking UI
- Stale data refreshed within reasonable timeframe
Phase 5: Scrollable Library Tabs
Goal: Make library tab row scrollable to accommodate more tabs
Tasks
- Convert
TabBarto scrollable variant (isScrollable: true) - Adjust tab styling for scrollable layout
- Ensure indicator line spans correctly
- Test with varying number of tabs
- Add "Books" tab placeholder (disabled until Phase 6)
Acceptance Criteria
- Tabs scroll horizontally when they exceed screen width
- Current tab indicator works correctly
- Smooth scrolling experience
Phase 6: Audiobooks Tab - Browse by Author
Goal: Add Books tab with author → books hierarchy
Tasks
- Add MA API method to fetch audiobooks:
getAudiobooks() - Add MA API method to fetch audiobook authors
- Create
AudiobookAuthormodel - Create
Audiobookmodel with fields:id,name,authors,narrators,descriptionduration,imageUrl,chapters,resumePosition,fullyPlayed
- Create
BooksScreenwith author list view - Tapping author expands to show their books
- Add to library tab bar
- Cache audiobooks in local database
Acceptance Criteria
- Books tab shows list of authors
- Authors expandable to show their audiobooks
- Audiobook covers and titles display correctly
- Data cached locally for fast subsequent loads
Phase 7: Audiobook Detail Screen
Goal: Full audiobook detail view with metadata and chapters
Tasks
- Create
AudiobookDetailScreen - Display: cover art, title, author(s), narrator(s)
- Display: description (expandable if long)
- Display: total duration, progress indicator
- Chapter list with:
- Chapter title
- Duration
- Played indicator (based on resume position)
- Tapping chapter starts playback from that chapter
- "Resume" button that continues from
resume_position_ms - "Start Over" option
Acceptance Criteria
- All metadata displays correctly
- Chapters show with accurate durations
- Resume position reflected in UI
- Can start playback from any chapter
Phase 8: Audiobook Playback Integration
Goal: Proper audiobook playback with seek and resume
Tasks
- Implement
playAudiobook(bookId, startPosition?)method - Use
queueCommandSeek()to resume at correct position - Update now playing UI to show:
- Book title instead of track
- Current chapter name
- Chapter progress (not just total progress)
- Add skip forward/back buttons (30 sec for audiobooks?)
- Add chapter skip buttons (next/previous chapter)
- Record to recently played when audiobook starts (with 'audiobook' type)
Acceptance Criteria
- Audiobooks resume from correct position
- Chapter information shows in now playing
- Skip controls work correctly
- Recently played records audiobook plays
Phase 9: Polish & Edge Cases
Goal: Handle edge cases and improve UX
Tasks
- Handle audiobooks with no chapters gracefully
- Handle very long audiobooks (20+ hours)
- Add "Mark as Finished" option
- Add "Mark as Unplayed" option
- Ensure offline browsing works for cached audiobooks
- Test with various audiobook lengths and chapter counts
- Performance testing with large audiobook libraries
Acceptance Criteria
- No crashes on edge cases
- Smooth performance
- All user actions have appropriate feedback
Future Enhancements (Out of Scope)
- Sleep timer
- Playback speed control
- Series support (requires MA provider update)
- Bookmarks
- Listening statistics
- Download for offline playback
Technical Notes
Database Choice: Drift
- SQLite wrapper with type-safe queries
- Good for relational data (profiles → recently_played)
- Mature, well-documented
- Supports migrations
MA API Endpoints Used
music/audiobooks- List audiobooksmusic/audiobook/{id}- Get audiobook detailsplayer_queues/play_media- Start playbackplayer_queues/seek- Seek to positionmusic/in_progress_items- Get items with resume positions
Profile Data Flow
MA Login → Get user_info → Check/Create Profile → Set Active → Load Profile Data
Recently Played Flow
Play Media → Check Active Profile → Insert RecentlyPlayed Row → Update UI