import 'dart:io'; import 'package:on_audio_query/on_audio_query.dart'; import 'package:path/path.dart' as p; import 'package:yumi/shared/data_sources/models/sc_music_folder_mode.dart'; import 'package:yumi/shared/data_sources/models/sc_music_mode.dart'; import 'package:yumi/shared/tools/sc_permission_utils.dart'; class LocalMusicScanner { LocalMusicScanner({OnAudioQuery? audioQuery}) : _audioQuery = audioQuery ?? OnAudioQuery(); final OnAudioQuery _audioQuery; Future> scanMp3Folders({ List addedSongs = const [], }) async { final hasPermission = await SCPermissionUtils.checkAudioPermission(); if (!hasPermission) { return const []; } final List songs; try { songs = await _audioQuery.querySongs( sortType: SongSortType.TITLE, orderType: OrderType.ASC_OR_SMALLER, uriType: UriType.EXTERNAL, ignoreCase: true, ); } catch (_) { return const []; } final addedIds = addedSongs.map((item) => item.id).toSet(); final addedPaths = addedSongs.map((item) => item.localPath).toSet(); final byFolder = >{}; final seen = {}; for (final song in songs) { if (!_isMp3(song)) { continue; } final localPath = _songString(song, "_data").trim(); if (localPath.isEmpty || !_fileExists(localPath)) { continue; } final id = _songString(song, "_id"); final key = id.isNotEmpty ? id : localPath; if (!seen.add(key)) { continue; } final folderPath = p.dirname(localPath); final fileName = _safeFileName(song, localPath); final title = _safeTitle(song, fileName); final model = SCMusicMode( id: key, title: title, artist: _songString(song, "artist"), durationMs: _songInt(song, "duration"), localPath: localPath, contentUri: _songString(song, "_uri"), folderPath: folderPath, fileName: fileName, size: _songInt(song, "_size"), addedAt: DateTime.now().millisecondsSinceEpoch, ); byFolder.putIfAbsent(folderPath, () => []).add(model); } final folders = byFolder.entries.map((entry) { final path = entry.key; final folderSongs = entry.value..sort( (a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()), ); final addedCount = folderSongs.where((song) { return addedIds.contains(song.id) || addedPaths.contains(song.localPath); }).length; return SCMusicFolderMode( id: path, name: p.basename(path).isEmpty ? path : p.basename(path), path: path, songs: List.unmodifiable(folderSongs), addedCount: addedCount, ); }).toList() ..sort( (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), ); return List.unmodifiable(folders); } bool _isMp3(SongModel song) { final extension = _songString( song, "file_extension", ).toLowerCase().replaceFirst(".", ""); if (extension == "mp3") { return true; } return _songString(song, "_display_name").toLowerCase().endsWith(".mp3") || _songString(song, "_data").toLowerCase().endsWith(".mp3"); } bool _fileExists(String path) { try { return File(path).existsSync(); } catch (_) { return false; } } String _safeFileName(SongModel song, String localPath) { final displayName = _songString(song, "_display_name").trim(); if (displayName.isNotEmpty) { return displayName; } return p.basename(localPath); } String _safeTitle(SongModel song, String fileName) { final title = _songString(song, "title").trim(); if (title.isNotEmpty) { return title; } final displayNameWOExt = _songString(song, "_display_name_wo_ext").trim(); if (displayNameWOExt.isNotEmpty) { return displayNameWOExt; } return p.basenameWithoutExtension(fileName); } String _songString(SongModel song, String key) { final value = _songValue(song, key); if (value == null) { return ""; } return value.toString(); } int _songInt(SongModel song, String key) { final value = _songValue(song, key); if (value is int) { return value; } if (value is num) { return value.toInt(); } return int.tryParse(value?.toString() ?? "") ?? 0; } Object? _songValue(SongModel song, String key) { try { return song.getMap[key]; } catch (_) { return null; } } }