import 'dart:io'; import 'package:drift/drift.dart'; import 'package:on_audio_query/on_audio_query.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; import 'categories.dart'; import 'database.dart'; const _uuid = Uuid(); /// Zählt die Musikdateien, die der MediaStore kennt. Grundlage für die /// automatische Erkennung: weicht die Zahl von der Bibliothek ab, ist etwas /// dazugekommen oder verschwunden. Future countAndroidMediaStoreSongs() async { final all = await OnAudioQuery().querySongs(uriType: UriType.EXTERNAL); return all.where((s) => s.isMusic ?? true).length; } /// Android-Scan über MediaStore (der einzige zuverlässige Weg ab Android 11, /// da Scoped Storage direkten Dateizugriff blockiert). Findet automatisch alle /// Musik auf dem Gerät. Schreibt in dieselbe DB wie der Desktop-Scan, /// UUID-stabil über den Dateipfad. Future scanAndroidMediaStore( MeloDb db, { Directory? coverDir, void Function(int done, int total)? onProgress, }) async { final audioQuery = OnAudioQuery(); final existing = {for (final s in await db.allSongs()) s.path: s}; final covers = coverDir ?? Directory(p.join((await getApplicationSupportDirectory()).path, 'covers')); await covers.create(recursive: true); final all = await audioQuery.querySongs( sortType: SongSortType.TITLE, orderType: OrderType.ASC_OR_SMALLER, uriType: UriType.EXTERNAL, ); final songs = all.where((s) => s.isMusic ?? true).toList(); final now = DateTime.now().millisecondsSinceEpoch; final companions = []; // Siehe scan_service.dart: erst Songs, dann Kategorien. final categories = >{}; var done = 0; if (songs.isNotEmpty) onProgress?.call(0, songs.length); for (final s in songs) { final prev = existing[s.data]; final id = prev?.id ?? _uuid.v4(); var coverPath = prev?.coverPath; if (coverPath == null) { try { final art = await audioQuery.queryArtwork(s.id, ArtworkType.AUDIO, size: 512); if (art != null && art.isNotEmpty) { final f = File(p.join(covers.path, '$id.img')); await f.writeAsBytes(art); coverPath = f.path; } } catch (_) { // Kaputtes Cover verzögert nicht den Scan. } } // Siehe scan_service.dart: von Hand korrigierte Metadaten bleiben stehen. final behalten = prev?.metadataEdited == true; companions.add(SongsCompanion.insert( id: id, path: s.data, title: behalten ? prev!.title : s.title, artist: Value(behalten ? prev!.artist : (s.artist == '' ? null : s.artist)), album: Value(behalten ? prev!.album : s.album), metadataEdited: Value(behalten), durationMs: Value(s.duration), coverPath: Value(coverPath), dateAddedMs: prev?.dateAddedMs ?? (s.dateAdded != null ? s.dateAdded! * 1000 : now), updatedAtMs: now, deleted: const Value(false), )); if (prev?.categoriesEdited != true) { categories[id] = parseCategories(s.genre); } ++done; if (done % 50 == 0 || done == songs.length) { onProgress?.call(done, songs.length); } } await db.upsertSongs(companions); for (final entry in categories.entries) { await db.setCategories(entry.key, entry.value); } if (companions.isNotEmpty) { await db.markMissing(now); } return companions.length; }