// Drift database for Minstrel's offline cache (#357 plan B). // // Two cache layers in one database: // - Metadata cache (CachedArtists, CachedAlbums, CachedTracks, // CachedLikes, CachedPlaylists, CachedPlaylistTracks) — populated // by SyncController via /api/library/sync // - Audio cache index (AudioCacheIndex) — owned by AudioCacheManager // // Plus SyncMetadata holding the latest sync cursor. // // All entity ids are TEXT (server-side UUIDs serialized as strings). // build_runner generates db.g.dart from this file; it's gitignored and // regenerated in CI. import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; part 'db.g.dart'; class CachedArtists extends Table { TextColumn get id => text()(); TextColumn get name => text()(); TextColumn get sortName => text()(); TextColumn get mbid => text().nullable()(); TextColumn get artistThumbPath => text().nullable()(); TextColumn get artistFanartPath => text().nullable()(); DateTimeColumn get fetchedAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {id}; } class CachedAlbums extends Table { TextColumn get id => text()(); TextColumn get artistId => text()(); TextColumn get title => text()(); TextColumn get sortTitle => text()(); TextColumn get releaseDate => text().nullable()(); TextColumn get coverPath => text().nullable()(); TextColumn get mbid => text().nullable()(); DateTimeColumn get fetchedAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {id}; } class CachedTracks extends Table { TextColumn get id => text()(); TextColumn get albumId => text()(); TextColumn get artistId => text()(); TextColumn get title => text()(); IntColumn get durationMs => integer().withDefault(const Constant(0))(); IntColumn get trackNumber => integer().nullable()(); IntColumn get discNumber => integer().nullable()(); TextColumn get filePath => text().nullable()(); TextColumn get fileFormat => text().nullable()(); TextColumn get genre => text().nullable()(); DateTimeColumn get fetchedAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {id}; } class CachedLikes extends Table { TextColumn get userId => text()(); TextColumn get entityType => text()(); // 'track' | 'album' | 'artist' TextColumn get entityId => text()(); DateTimeColumn get likedAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {userId, entityType, entityId}; } class CachedPlaylists extends Table { TextColumn get id => text()(); TextColumn get userId => text()(); TextColumn get name => text()(); TextColumn get description => text().withDefault(const Constant(''))(); BoolColumn get isPublic => boolean().withDefault(const Constant(false))(); TextColumn get coverPath => text().nullable()(); IntColumn get trackCount => integer().withDefault(const Constant(0))(); IntColumn get durationSec => integer().withDefault(const Constant(0))(); /// Server's system_variant: null for user playlists, "for_you" / /// "songs_like_artist" / "discover" for system-generated mixes. /// Added in schemaVersion 2 to let the add-to-playlist sheet filter /// out system playlists locally without a REST round-trip. TextColumn get systemVariant => text().nullable()(); DateTimeColumn get fetchedAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {id}; } class CachedPlaylistTracks extends Table { TextColumn get playlistId => text()(); TextColumn get trackId => text()(); IntColumn get position => integer().withDefault(const Constant(0))(); @override Set get primaryKey => {playlistId, trackId}; } /// One row per fully-downloaded audio file. `source` drives tiered LRU /// eviction; `incidental` evicts first, `manual` last. class AudioCacheIndex extends Table { TextColumn get trackId => text()(); TextColumn get path => text()(); IntColumn get sizeBytes => integer()(); DateTimeColumn get cachedAt => dateTime().withDefault(currentDateAndTime)(); TextColumn get source => textEnum()(); @override Set get primaryKey => {trackId}; } /// Single-row table holding the latest sync cursor. class SyncMetadata extends Table { IntColumn get id => integer().withDefault(const Constant(1))(); IntColumn get cursor => integer().withDefault(const Constant(0))(); DateTimeColumn get lastSyncAt => dateTime().nullable()(); @override Set get primaryKey => {id}; } /// Single-row cache of the /api/home response (#357 follow-up). The /// home screen is the first thing the user sees on app open; storing /// the last successful HomeData as JSON lets us yield it immediately /// before the REST round-trip resolves, eliminating the cold-start /// blank on slow / remote connections. Schema 3+. class CachedHomeSnapshot extends Table { IntColumn get id => integer().withDefault(const Constant(1))(); TextColumn get json => text()(); DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); @override Set get primaryKey => {id}; } enum CacheSource { manual, autoLiked, autoPlaylist, autoPrefetch, incidental } @DriftDatabase(tables: [ CachedArtists, CachedAlbums, CachedTracks, CachedLikes, CachedPlaylists, CachedPlaylistTracks, AudioCacheIndex, SyncMetadata, CachedHomeSnapshot, ]) class AppDb extends _$AppDb { AppDb([QueryExecutor? e]) : super(e ?? driftDatabase(name: 'minstrel_cache')); @override int get schemaVersion => 3; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) => m.createAll(), onUpgrade: (m, from, to) async { if (from < 2) { // Schema 2: add CachedPlaylists.systemVariant. Existing // rows get null and the next sync rebuilds them with the // server's system_variant value. await m.addColumn(cachedPlaylists, cachedPlaylists.systemVariant); // Reset cursor so the next /api/library/sync re-emits all // playlists; otherwise the new column would stay null on // pre-existing rows until they happen to change server-side. await customStatement('UPDATE sync_metadata SET cursor = 0'); } if (from < 3) { // Schema 3: cached_home_snapshot for #357 drift-first // home page. No existing rows to migrate — table starts // empty; the first /api/home fetch populates it. await m.createTable(cachedHomeSnapshot); } }, ); }