import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; part 'database.g.dart'; /// Songs. Sync-fähig ab Tag 1: [id] ist eine stabile UUID, [updatedAtMs] und /// [deleted] (Tombstone) ermöglichen späteren Cloud-Sync ohne Schema-Umbau. class Songs extends Table { TextColumn get id => text()(); // uuid TextColumn get path => text().unique()(); TextColumn get title => text()(); TextColumn get artist => text().nullable()(); TextColumn get album => text().nullable()(); IntColumn get durationMs => integer().nullable()(); TextColumn get coverPath => text().nullable()(); IntColumn get dateAddedMs => integer()(); IntColumn get updatedAtMs => integer()(); BoolColumn get deleted => boolean().withDefault(const Constant(false))(); @override Set get primaryKey => {id}; } /// Vom Nutzer gewählte Musikordner, die gescannt werden. class Folders extends Table { TextColumn get id => text()(); // uuid TextColumn get path => text().unique()(); IntColumn get updatedAtMs => integer()(); BoolColumn get deleted => boolean().withDefault(const Constant(false))(); @override Set get primaryKey => {id}; } @DriftDatabase(tables: [Songs, Folders]) class MeloDb extends _$MeloDb { MeloDb([QueryExecutor? executor]) : super(executor ?? _open()); @override int get schemaVersion => 1; Stream> watchSongs() { return (select(songs) ..where((s) => s.deleted.equals(false)) ..orderBy([(s) => OrderingTerm(expression: s.title)])) .watch(); } Stream> watchRecent({int limit = 50}) { return (select(songs) ..where((s) => s.deleted.equals(false)) ..orderBy([ (s) => OrderingTerm(expression: s.dateAddedMs, mode: OrderingMode.desc) ]) ..limit(limit)) .watch(); } Stream> searchSongs(String query) { final like = '%${query.toLowerCase()}%'; return (select(songs) ..where((s) => s.deleted.equals(false) & (s.title.lower().like(like) | s.artist.lower().like(like) | s.album.lower().like(like))) ..orderBy([(s) => OrderingTerm(expression: s.title)])) .watch(); } Future> allSongs() => select(songs).get(); Future upsertSongs(List items) async { await batch((b) => b.insertAllOnConflictUpdate(songs, items)); } /// Tombstone für Songs, deren Datei beim Scan nicht mehr gefunden wurde. Future markMissing(List livePaths, int now) async { final q = update(songs)..where((s) => s.deleted.equals(false)); if (livePaths.isNotEmpty) { q.where((s) => s.path.isNotIn(livePaths)); } await q.write( SongsCompanion(deleted: const Value(true), updatedAtMs: Value(now)), ); } Stream> watchFolders() => (select(folders)..where((f) => f.deleted.equals(false))).watch(); Future> activeFolders() => (select(folders)..where((f) => f.deleted.equals(false))).get(); Future addFolder(FoldersCompanion folder) => into(folders).insert(folder, onConflict: DoUpdate((_) => folder, target: [folders.path])); } LazyDatabase _open() { return LazyDatabase(() async { final dir = await getApplicationSupportDirectory(); final file = File(p.join(dir.path, 'melo.sqlite')); return NativeDatabase.createInBackground(file); }); }