// 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}; } enum CacheSource { manual, autoLiked, autoPlaylist, autoPrefetch, incidental } @DriftDatabase(tables: [ CachedArtists, CachedAlbums, CachedTracks, CachedLikes, CachedPlaylists, CachedPlaylistTracks, AudioCacheIndex, SyncMetadata, ]) class AppDb extends _$AppDb { AppDb([QueryExecutor? e]) : super(e ?? driftDatabase(name: 'minstrel_cache')); @override int get schemaVersion => 2; @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'); } }, ); }