feat: update react native

This commit is contained in:
Lei Nelissen
2026-05-07 22:03:41 +02:00
parent 1865809e80
commit 8bc51cbda1
5 changed files with 890 additions and 1338 deletions
-303
View File
@@ -1,303 +0,0 @@
# SQLite Migration Implementation
This document describes the SQLite-based storage system implemented for Fintunes.
## Overview
The implementation provides a complete offline-first data storage solution using SQLite with:
- Schema-driven database design with proper indexing
- Live query support for reactive UI updates
- Unified driver interface for multiple source types (Jellyfin, Emby)
- Automated prefill system with bounded concurrency and cursor-based resume
- Proper error handling with automatic retries
## Architecture
### Database Layer (`src/store/db/`)
#### Schema (`schema.ts`)
Defines all database tables using Drizzle ORM:
- **sources**: Server connection information
- **app_settings**: Global application settings (single row)
- **sleep_timer**: Sleep timer state (single row)
- **artists, albums, tracks, playlists**: Media entities
- **downloads**: Download tracking
- **search_queries**: Search history
- **album_artists, track_artists, playlist_tracks**: Many-to-many relationships
- **album_similar**: Similar album recommendations
- **sync_cursors**: Prefill progress tracking
All updateable tables include `created_at` and `updated_at` timestamps. Most entity fields are stored in promoted columns for filtering/sorting, with additional metadata in `metadata_json`.
#### Client (`client.ts`)
Manages database connection and migrations:
- Singleton pattern for database instance
- Automatic table creation on first run
- Index creation for query performance
- Uses `@op-engineering/op-sqlite` as the SQLite driver
#### Live Queries (`live-queries.ts`)
Reactive query support based on table-level change notifications:
- `useLiveQuery()`: React hook for live data
- `useLiveQueryOne()`: Hook for single record
- `invalidateTable()`: Notify listeners of changes
- Manual invalidation after writes
**Caveats:**
- Table-level granularity (not row-level)
- Requires manual invalidation
- Not suitable for very large result sets
#### Helpers (`helpers.ts`)
Common database operations:
- `upsert()`: Insert or update with automatic timestamps
- `bulkUpsert()`: Efficient batch operations
- Entity-specific helpers (upsertArtist, upsertAlbum, etc.)
- Sync cursor management
### Source Drivers (`src/store/sources/`)
#### Types (`types.ts`)
Common interfaces and types:
- `SourceDriver`: Interface all drivers must implement
- `Source`: Source connection information
- `ListParams`: Paging parameters (offset, limit)
- Entity types: Artist, Album, Track, Playlist, etc.
#### Jellyfin Driver (`jellyfin/driver.ts`)
Complete Jellyfin API implementation:
- All list methods support paging (default 500 items per page)
- Proper authentication headers
- Error handling with typed errors
- Stream URL generation with platform-specific codecs
- Playback reporting
#### Emby Driver (`emby/driver.ts`)
Complete Emby API implementation:
- Same feature set as Jellyfin
- Different authentication header (`X-Emby-Authorization`)
- Compatible with Emby server API
### Prefill System (`src/store/prefill/`)
#### Orchestrator (`orchestrator.ts`)
Manages basic entity prefilling:
- Bounded concurrency (max 5 concurrent requests)
- Page size: 500 items
- Cursor-based resume support
- Automatic retry (up to 5 attempts with exponential backoff)
- Progress callbacks for UI updates
**Prefill order:**
1. Artists and Albums (parallel)
2. Playlists
#### Task Graph (`task-graph.ts`)
Handles dependent prefill tasks:
- Album tracks (requires albums)
- Playlist tracks (requires playlists)
- Similar albums (requires albums)
- Lyrics (requires tracks)
**Execution order:**
1. Album tracks and Playlist tracks (parallel)
2. Similar albums and Lyrics (parallel)
#### Main Coordinator (`index.ts`)
- `runPrefill()`: Execute complete prefill workflow
- Combines orchestrator and task graph
- Single function to prefill entire source
## Usage
### Initialize Database
```typescript
import { initializeDatabase } from '@/store/db';
// Initialize on app start
initializeDatabase();
```
### Create Source and Driver
```typescript
import { Source, SourceType, JellyfinDriver } from '@/store/sources';
const source: Source = {
id: 'my-server-id',
uri: 'https://jellyfin.example.com',
userId: 'user-id',
accessToken: 'access-token',
deviceId: 'device-id',
type: SourceType.JELLYFIN_V1,
};
const driver = new JellyfinDriver(source);
```
### Run Prefill
```typescript
import { runPrefill } from '@/store/prefill';
await runPrefill(source.id, driver, (progress) => {
console.log(`${progress.entityType}: ${progress.totalFetched} items`);
if (progress.completed) {
console.log(`Completed: ${progress.entityType}`);
}
});
```
### Query Data with Live Updates
```typescript
import { useLiveQuery } from '@/store/db';
function AlbumsList({ sourceId }: { sourceId: string }) {
const albums = useLiveQuery(
'SELECT * FROM albums WHERE source_id = ? ORDER BY name',
[sourceId],
['albums'] // Tables to watch
);
if (!albums) return <Loading />;
return (
<View>
{albums.map(album => (
<AlbumItem key={album.id} album={album} />
))}
</View>
);
}
```
### Insert/Update Data
```typescript
import { upsertAlbum, invalidateTable } from '@/store/db';
await upsertAlbum({
sourceId: 'my-server',
id: 'album-123',
name: 'New Album',
isFolder: false,
// ... other fields
});
// Manually invalidate to trigger live query updates
invalidateTable('albums');
```
## Testing
Basic smoke tests are provided in `src/store/db/__tests__/smoke.test.ts`:
```bash
npm test
```
Tests verify:
- Database initialization
- Entity upsert operations
- Sync cursor management
## Performance Considerations
### Indexes
All common query patterns are indexed:
- `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)`
- Relationship tables by source and foreign keys
### Paging
All list endpoints support paging to avoid memory issues:
- Default page size: 500 items
- Configurable via `ListParams.limit`
- Offset-based pagination
### Concurrency
Prefill system limits concurrent requests:
- Max 5 concurrent API requests
- Prevents overwhelming the server
- Bounded memory usage
## Migration from Redux
The core infrastructure is complete. To fully migrate from Redux:
1. Replace Redux selectors with SQLite queries
2. Use `useLiveQuery` instead of Redux hooks
3. Replace Redux actions with direct DB operations
4. Remove Redux slices one by one
5. Update tests to use SQLite
## Next Steps
### Phase 4: Redux Removal
- [ ] Replace music slice with SQLite queries
- [ ] Replace settings slice with app_settings table
- [ ] Replace downloads slice with downloads table
- [ ] Replace search slice with search_queries table
- [ ] Replace sleep timer slice with sleep_timer table
- [ ] Update all components to use live queries
### Phase 5: Onboarding UI
- [ ] Wire prefill progress to onboarding screen
- [ ] Show entity counts and progress
- [ ] Handle errors gracefully
- [ ] Allow cancellation and retry
### Future Enhancements
- [ ] Incremental sync (only fetch changes)
- [ ] Background sync service
- [ ] Conflict resolution for multiple sources
- [ ] Query optimization based on usage patterns
- [ ] Database vacuum and optimization
- [ ] Export/import functionality
## Files Changed
### New Files
- `src/store/db/schema.ts` - Database schema
- `src/store/db/client.ts` - Database connection
- `src/store/db/live-queries.ts` - Live query support
- `src/store/db/helpers.ts` - Database helpers
- `src/store/db/index.ts` - Module exports
- `src/store/sources/types.ts` - Driver interfaces
- `src/store/sources/jellyfin/driver.ts` - Jellyfin driver
- `src/store/sources/emby/driver.ts` - Emby driver
- `src/store/sources/jellyfin/index.ts` - Jellyfin exports
- `src/store/sources/emby/index.ts` - Emby exports
- `src/store/sources/index.ts` - Sources exports
- `src/store/prefill/orchestrator.ts` - Prefill orchestrator
- `src/store/prefill/task-graph.ts` - Task graph
- `src/store/prefill/index.ts` - Prefill exports
- `src/store/db/__tests__/smoke.test.ts` - Basic tests
### Modified Files
- `package.json` - Added drizzle-orm, @op-engineering/op-sqlite, drizzle-kit
- `PLAN.md` - Updated progress tracker
## Dependencies Added
```json
{
"dependencies": {
"drizzle-orm": "^0.45.1",
"@op-engineering/op-sqlite": "^15.2.5"
},
"devDependencies": {
"drizzle-kit": "^0.31.8"
}
}
```
## License
Same as parent project.
-303
View File
@@ -1,303 +0,0 @@
# Type Architecture and Infrastructure
## Overview
This document explains the reorganized type architecture and restored infrastructure for the Jellyfin audio player.
## Type Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ API Response (PascalCase) │
│ JellyfinAlbum { Id, Name, ArtistItems: [{ Id, Name }] } │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Driver Transformation │
│ Transform PascalCase → camelCase + add metadataJson │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Schema-Compatible Type (camelCase) │
│ Album { id, name, metadataJson, artistItems: Artist[] } │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Database Insert │
│ db.insert(albums).values({ ...album, sourceId, timestamps }) │
└─────────────────────────────────────────────────────────────────┘
```
## Directory Structure
```
src/store/
├── db/
│ ├── index.ts # Database client (singleton export)
│ ├── types.ts # Schema types (derived from Drizzle)
│ ├── live-queries.ts # React hooks for reactive queries
│ ├── schema/
│ │ ├── artists.ts # Artist table definition
│ │ ├── albums.ts # Album table definition
│ │ ├── tracks.ts # Track table definition
│ │ └── ... # Other tables
│ └── migrations/
│ └── migrations.js # Drizzle-generated migrations
├── sources/
│ ├── types.ts # Base types using schema types
│ ├── jellyfin/
│ │ ├── driver.ts # Jellyfin implementation
│ │ ├── types.ts # Re-exports + Jellyfin types
│ │ └── api-types.ts # Jellyfin API response types
│ └── emby/
│ ├── driver.ts # Emby implementation
│ ├── types.ts # Re-exports + Emby types
│ └── api-types.ts # Emby API response types
└── prefill/
├── orchestrator.ts # Main prefill orchestration
└── task-graph.ts # Dependent task execution
```
## Type Definitions
### 1. Schema Types (`db/types.ts`)
These are derived from the Drizzle schema using `InferSelectModel`:
```typescript
import type { InferSelectModel } from 'drizzle-orm';
import { albums } from './schema/albums';
export type Album = InferSelectModel<typeof albums>;
// Result: { id, sourceId, name, productionYear, isFolder, ..., createdAt, updatedAt }
export type InsertAlbum = typeof albums.$inferInsert;
// Used for inserts
```
### 2. API Response Types (`sources/*/api-types.ts`)
API types match the external API responses (PascalCase):
```typescript
// jellyfin/api-types.ts
export interface JellyfinAlbum {
Id: string;
Name: string;
ProductionYear?: number;
IsFolder: boolean;
AlbumArtist?: string;
DateCreated?: string;
ArtistItems?: JellyfinArtist[];
}
export interface JellyfinItemsResponse<T> {
Items: T[];
TotalRecordCount: number;
StartIndex: number;
}
```
### 3. Source Driver Types (`sources/types.ts`)
Driver return types are schema-compatible but exclude fields added at insert time:
```typescript
// Based on schema but without sourceId, timestamps
export type Album = Omit<SchemaAlbum, 'sourceId' | 'createdAt' | 'updatedAt' | 'lastRefreshed'> & {
artistItems?: Artist[]; // Temporary field for relationships
};
// Driver methods return these types:
abstract class SourceDriver {
abstract getAlbums(params?: ListParams): Promise<Album[]>;
}
```
## Driver Implementation Pattern
### Transformation Example
```typescript
async getAlbums(params?: ListParams): Promise<Album[]> {
// 1. Fetch from API using API types
const response = await this.fetch<JellyfinItemsResponse<JellyfinAlbum>>(url);
// 2. Transform to schema-compatible format
return response.Items.map(item => ({
// Map PascalCase → camelCase
id: item.Id,
name: item.Name,
productionYear: item.ProductionYear,
isFolder: item.IsFolder || false,
albumArtist: item.AlbumArtist,
dateCreated: item.DateCreated ? new Date(item.DateCreated).getTime() : undefined,
// Store full API response as JSON
metadataJson: JSON.stringify(item),
// Transform nested relationships
artistItems: item.ArtistItems?.map(artist => ({
id: artist.Id,
name: artist.Name,
isFolder: artist.IsFolder,
metadataJson: JSON.stringify(artist),
})) || [],
}));
}
```
### Database Insert Example
```typescript
const albums = await driver.getAlbums();
const now = Date.now();
await db.insert(albumsTable).values(
albums.map(album => ({
...album,
sourceId: 'source-123', // Add sourceId
createdAt: now, // Add timestamps
updatedAt: now,
}))
).onConflictDoUpdate({
target: [albumsTable.sourceId, albumsTable.id],
set: {
name: albums[0].name,
// ... other fields
updatedAt: now,
},
});
```
## Prefill Infrastructure
### Orchestrator (`prefill/orchestrator.ts`)
Manages basic entity prefill with:
- **p-queue** for bounded concurrency (max 5 concurrent requests)
- **Cursor-based resume** via `sync_cursors` table
- **Progress callbacks** for UI updates
```typescript
const orchestrator = new PrefillOrchestrator(sourceId, driver, {
concurrency: 5,
pageSize: 500,
onProgress: (progress) => {
console.log(`${progress.entityType}: ${progress.totalFetched} items`);
},
});
await orchestrator.runPrefill();
```
### Task Graph (`prefill/task-graph.ts`)
Handles dependent tasks that require parent entities:
- Album tracks (requires albums)
- Playlist tracks (requires playlists)
- Similar albums (optional)
- Lyrics (optional)
```typescript
const taskGraph = new PrefillTaskGraph(sourceId, driver, {
concurrency: 5,
onProgress: callback,
});
await taskGraph.runAllTasks();
```
## Live Queries (`db/live-queries.ts`)
React hooks for reactive database queries:
```typescript
// Hook that re-renders when albums table changes
const albums = useLiveQuery<Album>(
'SELECT * FROM albums WHERE source_id = ? ORDER BY name',
[sourceId],
['albums'] // Tables to watch
);
// Invalidate manually after inserts
await db.insert(albums).values(newAlbums);
invalidateTable('albums');
```
## Benefits
### Type Safety
- ✅ Compile-time errors if schema changes
- ✅ No confusion between API and internal types
- ✅ IntelliSense support throughout
### Maintainability
- ✅ API changes isolated to api-types.ts
- ✅ Schema changes propagate automatically
- ✅ Clear separation of concerns
### Flexibility
- ✅ Full API response preserved in `metadataJson`
- ✅ Can add computed fields at query time
- ✅ Easy to add new drivers
## Migration Path
When adding a new entity type:
1. **Define schema** in `db/schema/new-entity.ts`
2. **Add to db/types.ts** exports
3. **Update sources/types.ts** with driver return type
4. **Add API type** to `api-types.ts` (PascalCase)
5. **Implement driver method** with transformation
6. **Add to orchestrator** if needed for prefill
## Example: Adding a New Entity
```typescript
// 1. Schema (db/schema/genres.ts)
export const genres = sqliteTable('genres', {
sourceId: text('source_id').notNull(),
id: text('id').primaryKey(),
name: text('name').notNull(),
metadataJson: text('metadata_json'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull(),
});
// 2. Export type (db/types.ts)
export type Genre = InferSelectModel<typeof genres>;
// 3. Driver type (sources/types.ts)
export type Genre = Omit<SchemaGenre, 'sourceId' | 'createdAt' | 'updatedAt'>;
// 4. API type (jellyfin/api-types.ts)
export interface JellyfinGenre {
Id: string;
Name: string;
}
// 5. Driver method (jellyfin/driver.ts)
async getGenres(): Promise<Genre[]> {
const response = await this.fetch<JellyfinItemsResponse<JellyfinGenre>>('/Genres');
return response.Items.map(item => ({
id: item.Id,
name: item.Name,
metadataJson: JSON.stringify(item),
}));
}
```
## Notes
- `metadataJson` field stores the complete API response for future extensibility
- `artistItems` is a temporary field on Album/Track types for relationship data
- Actual artist relationships are stored in separate junction tables
- Schema types are the source of truth for database structure
- API types document the external contracts
+373 -317
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -37,7 +37,7 @@
"p-queue": "^9.1.0", "p-queue": "^9.1.0",
"react": "19.2.3", "react": "19.2.3",
"react-airplay": "1.2.0", "react-airplay": "1.2.0",
"react-native": "^0.83.1", "react-native": "^0.83.9",
"react-native-accessibility-settings": "0.1.2", "react-native-accessibility-settings": "0.1.2",
"react-native-collapsible": "1.6.2", "react-native-collapsible": "1.6.2",
"react-native-dotenv": "3.4.11", "react-native-dotenv": "3.4.11",
+516 -414
View File
File diff suppressed because it is too large Load Diff