yumi-flutter/lib/services/music/local_music_scanner.dart
2026-04-29 14:52:09 +08:00

166 lines
4.8 KiB
Dart

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<List<SCMusicFolderMode>> scanMp3Folders({
List<SCMusicMode> addedSongs = const [],
}) async {
final hasPermission = await SCPermissionUtils.checkAudioPermission();
if (!hasPermission) {
return const <SCMusicFolderMode>[];
}
final List<SongModel> songs;
try {
songs = await _audioQuery.querySongs(
sortType: SongSortType.TITLE,
orderType: OrderType.ASC_OR_SMALLER,
uriType: UriType.EXTERNAL,
ignoreCase: true,
);
} catch (_) {
return const <SCMusicFolderMode>[];
}
final addedIds = addedSongs.map((item) => item.id).toSet();
final addedPaths = addedSongs.map((item) => item.localPath).toSet();
final byFolder = <String, List<SCMusicMode>>{};
final seen = <String>{};
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, () => <SCMusicMode>[]).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<SCMusicMode>.unmodifiable(folderSongs),
addedCount: addedCount,
);
}).toList()
..sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return List<SCMusicFolderMode>.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;
}
}
}