mirror of
https://github.com/leinelissen/jellyfin-audio-player.git
synced 2026-09-02 21:03:11 +03:00
feat: update react native
This commit is contained in:
@@ -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.
|
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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",
|
||||||
|
|||||||
Generated
+516
-414
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user