326b7008a9
Adds the offline-mode foundation: - pubspec deps: drift, drift_flutter, sqlite3_flutter_libs, connectivity_plus + drift_dev/build_runner (dev) - lib/cache/db.dart with 8 tables: CachedArtists/Albums/Tracks/Likes/ Playlists/PlaylistTracks + AudioCacheIndex + SyncMetadata - *.g.dart added to .gitignore (build_runner regenerates per build) - CI workflow gains a 'Codegen (drift)' step between pub get and analyze so the generated symbols exist when analyze runs CacheSource enum drives tiered LRU eviction (manual/autoLiked/ autoPlaylist/autoPrefetch/incidental). Subsequent commits add the sync controller, audio cache manager, prefetcher, settings UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
131 lines
4.6 KiB
Dart
131 lines
4.6 KiB
Dart
// 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<Column> 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<Column> 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<Column> 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<Column> 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))();
|
|
DateTimeColumn get fetchedAt => dateTime().withDefault(currentDateAndTime)();
|
|
@override
|
|
Set<Column> 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<Column> 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<CacheSource>()();
|
|
@override
|
|
Set<Column> 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<Column> 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 => 1;
|
|
}
|