feat(flutter/cache): drift schema + new deps + CI codegen step (#357 plan B)

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>
This commit is contained in:
2026-05-09 23:07:34 -04:00
parent 8d5c90e0ed
commit 326b7008a9
4 changed files with 146 additions and 0 deletions
+6
View File
@@ -56,6 +56,12 @@ jobs:
- name: Pub get - name: Pub get
run: flutter pub get run: flutter pub get
- name: Codegen (drift, etc.)
# build_runner generates *.g.dart files (drift schemas, etc.)
# which are gitignored. Must run before analyze + test so the
# generated symbols exist.
run: dart run build_runner build --delete-conflicting-outputs
- name: Analyze - name: Analyze
run: flutter analyze --fatal-infos run: flutter analyze --fatal-infos
+4
View File
@@ -43,3 +43,7 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# drift codegen output (regenerated by build_runner; CI runs build_runner)
*.g.dart
+130
View File
@@ -0,0 +1,130 @@
// 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;
}
+6
View File
@@ -24,12 +24,18 @@ dependencies:
pub_semver: ^2.1.4 pub_semver: ^2.1.4
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
path_provider: ^2.1.5 path_provider: ^2.1.5
drift: ^2.18.0
drift_flutter: ^0.2.0
sqlite3_flutter_libs: ^0.5.24
connectivity_plus: ^6.0.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
mocktail: ^1.0.4 mocktail: ^1.0.4
drift_dev: ^2.18.0
build_runner: ^2.4.13
flutter: flutter:
uses-material-design: true uses-material-design: true