Audio file discovery and metadata reading for Sono.
Step 1. Add this to your pubspec.yaml in the dependencies section:
sono_query:
git:
url: https://github.com/appsono/sono_query.git
ref: <current_tag>Step 2. Add the import and start querying:
import 'package:sono_query/sono_query.dart';
// Get all songs at once
final songs = await SonoQuery.getSongs();
// Or stream in batches, useful for large libraries (recommended)
await for (final song in SonoQuery.getSongsStream()) {
print(song);
}Pass a ScanConfig to control filtering and artist parsing:
final config = ScanConfig(
// Skip these directories entirely
excludedPaths: [
'/storage/emulated/0/Ringtones',
'/storage/emulated/0/Music/trashSongs',
],
// Scan additional directories (desktop only, ~/Music is always included)
additionalPaths: [
'/home/user/Downloads/Music',
];
// Skip songs shorter than 10 seconds
minDuration: const Duration(seconds: 10),
// Enable multi-artist parsing (see below)
artistParser: const ArtistParserConfig(),
);Songs whose file paths start with any excluded prefix are filtered out.
On Android this is pushed into the MediaStore WHERE clause (DATA NOT LIKE ?).
On desktop/iOS it's applied during the directory walk.
By default only ~/Music (Linux) or %USERPROFILE%\Music (Windows) is scanned.
Additional paths are scanned alongside it.
Songs shorter than minDuration are skipped.
On Android this is a MediaStore WHERE clause (DURATION > ?).
On desktop/iOS it's applied after metadata reading.
Songs downloaded with yt-dlp or other tools often store multiple artists in a single tag separated by /, ;, ,, etc. ArtistParserConfig splits these automatically.
final config = ScanConfig(
artistParser: ArtistParserConfig(
// These are the defaults; override to customize
delimiters: [' / ', '; ', ';', ' + ', ', ', '/'],
// Artist names that should never be split,
// even if they contain a delimiter
excludedArtists: ['Tyler, The Creator', 'mathis + the world'],
),
);
final songs = await SonoQuery.getSongs(config: config);
for (final song in songs) {
print(song.artist); // raw tag: "Jay-Z / Kanye West"
print(song.artists); // parsed: ["Jay-Z", "Kanye West"]
}Use backslash (\) before a character to prevent splitting:
| Raw tag | Parsed result |
|---|---|
Artist / Artist2 |
["Artist1", "Artist2"] |
A + B + C |
["A", "B", "C"] |
AC\/DC |
["AC/DC"] |
AC\/DC / Someone |
["AC/DC", "Someone"] |
ArtistParser can also be used independently from scanning:
final artists = ArtistParser.parse(
'Tyler, The Creator, Someone',
ArtistParserConfig(excludedArtists: ['Tyler, The Creator']),
)
// -> ["Tyler, The Creator", "Someone"]Both getSongs and getSongsStream accept an onProgress callback for live progress reporting:
final songs = await SonoQuery.getSongs(
onProgress: (progress) {
print('${progress.phase.name}: '
'${progress.completed}/${progress.total} '
'(${(progress.progress * 100).toStringAsFixed(0)}%)');
if (progress.currentPath != null) {
print(' → ${progress.currentPath}');
}
},
);ScanProgress fields:
| Field | Type | Description |
|---|---|---|
total |
int |
Total files discovered |
completed |
int |
Files processed so far |
progress |
double |
0.0 - 1.0 fraction |
currentPath |
String? |
File currently being processed |
phase |
ScanPhase |
discovering > reading > done |
Both getSongs and getSongsStream accept an onError callback for files that fail metadata reading. Failed files are skipped, scanning continues:
final songs = await SonoQuery.getSongs(
onError: (path, error) => print('Failed to read $path: $error'),
);final cover = await SonoQuery.getCover('/path/to/song.mp3');
// Returns Uint8List? (JPEG, PNG, or WebP bytes)Of course all of this won't work without permission
Add to your AndroidManifest.xml:
<!-- Android 13+ (API 33) -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- Android 12 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />Must be requested at runtime before calling SonoQuery.getSongs().
Without it, the query WILL return an empty list.
Add to your Info.plist if scanning outside the app sandbox:
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>It scans the app's Documents and Music directories by default. Files must be added to these directories through the Files app or iTunes file sharing.
No special permissions needed. Scans ~/Music (Linux) or %USERPROFILE%\Music (Windows).
Add more directories via ScanConfig.additionalPaths.
| Platform | Method | Metadata source |
|---|---|---|
| Android | MediaStore | MediaStore |
| iOS | FileManager | audio_metadata_reader |
| Linux | dart:io | audio_metadata_reader |
| Windows | dart:io | audio_metadata_reader |
MP3, M4A, FLAC, OGG, Opus, WAV
Metadata reading by audio_metadata_reader
MIT