diff --git a/lib/library/android_scan.dart b/lib/library/android_scan.dart index b40ffc1..39f4949 100644 --- a/lib/library/android_scan.dart +++ b/lib/library/android_scan.dart @@ -34,11 +34,11 @@ Future scanAndroidMediaStore( final now = DateTime.now().millisecondsSinceEpoch; final companions = []; - final livePaths = []; var done = 0; + if (songs.isNotEmpty) onProgress?.call(0, songs.length); + for (final s in songs) { - livePaths.add(s.data); final prev = existing[s.data]; final id = prev?.id ?? _uuid.v4(); @@ -76,7 +76,7 @@ Future scanAndroidMediaStore( } await db.upsertSongs(companions); - if (livePaths.isNotEmpty) { + if (companions.isNotEmpty) { await db.markMissing(now); } return companions.length; diff --git a/lib/library/library_screen.dart b/lib/library/library_screen.dart index 13ba10c..354e4cd 100644 --- a/lib/library/library_screen.dart +++ b/lib/library/library_screen.dart @@ -49,7 +49,7 @@ class LibraryScreen extends StatelessWidget { content: Text(lib.scanError!), actions: [ TextButton( - onPressed: () => lib.scanError = null, + onPressed: lib.dismissScanError, child: const Text('Verwerfen'), ), ], diff --git a/lib/library/library_service.dart b/lib/library/library_service.dart index 72da9eb..818f236 100644 --- a/lib/library/library_service.dart +++ b/lib/library/library_service.dart @@ -57,6 +57,11 @@ class LibraryService extends ChangeNotifier { return granted; } + void dismissScanError() { + scanError = null; + notifyListeners(); + } + Future _scan() async { if (scanning) return; scanning = true; diff --git a/lib/library/scan_service.dart b/lib/library/scan_service.dart index 354af35..d45fb66 100644 --- a/lib/library/scan_service.dart +++ b/lib/library/scan_service.dart @@ -45,11 +45,11 @@ Future scanFolders( final now = DateTime.now().millisecondsSinceEpoch; final companions = []; - final livePaths = []; var done = 0; + if (files.isNotEmpty) onProgress?.call(0, files.length); + for (final file in files) { - livePaths.add(file.path); final prev = existing[file.path]; final id = prev?.id ?? _uuid.v4(); @@ -96,7 +96,7 @@ Future scanFolders( } await db.upsertSongs(companions); - if (livePaths.isNotEmpty) { + if (companions.isNotEmpty) { await db.markMissing(now); } return companions.length; diff --git a/lib/library/song_list.dart b/lib/library/song_list.dart index 29bd169..488b45e 100644 --- a/lib/library/song_list.dart +++ b/lib/library/song_list.dart @@ -27,7 +27,17 @@ class SongList extends StatelessWidget { title: Text(s.title, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: Text(s.artist ?? 'Unbekannt', maxLines: 1, overflow: TextOverflow.ellipsis), - onTap: () => playSongs(handler, songs, i), + onTap: () async { + try { + await playSongs(handler, songs, i); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Wiedergabe fehlgeschlagen: $e')), + ); + } + } + }, ); }, ); diff --git a/lib/player/audio_handler.dart b/lib/player/audio_handler.dart index 918052a..8ca3506 100644 --- a/lib/player/audio_handler.dart +++ b/lib/player/audio_handler.dart @@ -34,12 +34,8 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final sources = items .map((item) => AudioSource.uri(Uri.parse(item.id), tag: item)) .toList(); - try { - await _player.setAudioSources(sources, initialIndex: startIndex); - await play(); - } catch (e) { - playbackState.addError(e); - } + await _player.setAudioSources(sources, initialIndex: startIndex); + await play(); } @override diff --git a/lib/player/now_playing_screen.dart b/lib/player/now_playing_screen.dart index b5e5e40..b89cde6 100644 --- a/lib/player/now_playing_screen.dart +++ b/lib/player/now_playing_screen.dart @@ -17,6 +17,7 @@ class NowPlayingScreen extends StatelessWidget { appBar: AppBar( backgroundColor: Colors.transparent, leading: IconButton( + tooltip: 'Schließen', icon: const Icon(Icons.keyboard_arrow_down), onPressed: () => Navigator.of(context).maybePop(), ), diff --git a/test/library_db_test.dart b/test/library_db_test.dart index 0f0bbbb..d7af5a3 100644 --- a/test/library_db_test.dart +++ b/test/library_db_test.dart @@ -59,41 +59,13 @@ void main() { expect(songs.map((s) => s.path), ['/a.mp3']); }); - test('leerer Scan loescht nichts', () async { - final now1 = 100; - final now2 = 200; - // Initialer Scan - await db.upsertSongs([ - SongsCompanion.insert(id: '1', path: '/a.mp3', title: 'A', dateAddedMs: now1, updatedAtMs: now1), - ]); - // Zweiter Scan: nichts gefunden - // Wenn livePaths.isEmpty, wird markMissing NICHT aufgerufen - // Daher bleibt der Song unverändert - final songs = await db.watchSongs().first; - expect(songs.length, 1); // Song bleibt, wurde nicht gelöscht + test('scan_test.dart: CRITICAL-1 — empty scan deletes nothing (in scan_test.dart)', () { + // Diese Regression ist in scan_test.dart mit echtem scanFolders() getestet. + // Hier nur ein Placeholder um die alte Test-Nummer zu behalten. }); - test('wiedergefundene Datei kommt zurueck', () async { - final now1 = 100; - final now2 = 200; - final now3 = 300; - // Initialer Scan - await db.upsertSongs([ - SongsCompanion.insert(id: '1', path: '/a.mp3', title: 'A', dateAddedMs: now1, updatedAtMs: now1), - ]); - // Zweiter Scan: /a.mp3 gefunden - await db.upsertSongs([ - SongsCompanion.insert(id: '1', path: '/a.mp3', title: 'A', dateAddedMs: now1, updatedAtMs: now2), - ]); - // Dritter Scan: nichts gefunden → markMissing wird NICHT aufgerufen - // Song bleibt mit now2, sichtbar - // Vierter Scan: /a.mp3 neu gefunden - await db.upsertSongs([ - SongsCompanion.insert(id: '1', path: '/a.mp3', title: 'A', dateAddedMs: now1, updatedAtMs: now3, deleted: const Value(false)), - ]); - // Nun bei Zeitstempel now3 markMissing aufrufen (Songs mit updatedAtMs < now3 sind die Zeichen eines vorherigen Scans) - await db.markMissing(now3); - final songs = await db.watchSongs().first; - expect(songs.length, 1); // Song ist sichtbar weil updatedAtMs=now3, nicht gelöscht + test('scan_test.dart: CRITICAL-2 — rescan restores deleted songs (in scan_test.dart)', () { + // Diese Regression ist in scan_test.dart mit echtem scanFolders() getestet. + // Hier nur ein Placeholder um die alte Test-Nummer zu behalten. }); } diff --git a/test/scan_test.dart b/test/scan_test.dart index 2c7f71a..7c4d50b 100644 --- a/test/scan_test.dart +++ b/test/scan_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:melo/library/database.dart'; @@ -56,4 +57,46 @@ void main() { expect(secondIds, firstIds); expect((await db.watchSongs().first).length, 3); // keine Duplikate }); + + test('CRITICAL-1: leerer Scan loescht nicht (nonexistent folder)', () async { + // Initialer Scan + await scanFolders(db, [fixtures], coverDir: coverDir); + final initialCount = (await db.watchSongs().first).length; + expect(initialCount, 3); + + // Scan mit nicht-existierendem Ordner (livePaths wird leer) + await scanFolders(db, ['/this/does/not/exist'], coverDir: coverDir); + + // Alle Songs sollten noch da sein + final afterEmpty = await db.watchSongs().first; + expect(afterEmpty.length, initialCount); + }); + + test('CRITICAL-2: Datei wird nach Tombstone wieder sichtbar', () async { + // Initialer Scan: alle 3 Dateien + await scanFolders(db, [fixtures], coverDir: coverDir); + var songs = await db.watchSongs().first; + expect(songs.length, 3); + final nachtpulsId = songs.firstWhere((s) => s.title == 'Nachtpuls').id; + + // Manuell tombstonen + await db.upsertSongs([ + SongsCompanion.insert( + id: nachtpulsId, + path: p.join(fixtures, '01 Nachtpuls.mp3'), + title: 'Nachtpuls', + dateAddedMs: 1, + updatedAtMs: 1, + deleted: const Value(true), + ), + ]); + songs = await db.watchSongs().first; + expect(songs.where((s) => s.title == 'Nachtpuls').length, 0); // nicht sichtbar + + // Re-Scan sollte die Datei wieder finden und deleted=false setzen + await scanFolders(db, [fixtures], coverDir: coverDir); + songs = await db.watchSongs().first; + expect(songs.where((s) => s.title == 'Nachtpuls').length, 1); // wieder da + expect(songs.length, 3); // alle 3 Songs + }); }