perf(metadata): avoid SAF copies during quality probes

This commit is contained in:
zarzet
2026-07-28 01:50:41 +07:00
parent 26570792a9
commit 80d9d87870
7 changed files with 186 additions and 3 deletions
+4 -1
View File
@@ -524,7 +524,10 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
lookupItems: _lookupItemsWithUpdates([updated]),
);
await _db.upsert(updated.toJson());
_bumpHistoryRevision();
// Swiping through older tracks can backfill several quality records in a
// short burst. Coalesce their DB-derived Library refreshes just like
// download completion writes instead of rebuilding every view per swipe.
_scheduleIndexBump();
}
Future<void> updateMetadataForItem({
@@ -392,7 +392,7 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier {
}
try {
final result = await PlatformBridge.readFileMetadata(filePath);
final result = await PlatformBridge.readDisplayAudioMetadata(filePath);
final error = result['error'];
if (error != null) {
if (_isPermanentProbeError(error.toString())) {
+3 -1
View File
@@ -297,7 +297,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
_hasLoadedResolvedAudioMetadata = true;
try {
final metadata = await PlatformBridge.readFileMetadata(sourcePath);
final metadata = await PlatformBridge.readDisplayAudioMetadata(
sourcePath,
);
if (!mounted ||
generation != _metadataLoadGeneration ||
sourcePath != cleanFilePath) {
+20
View File
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/services/download_request_payload.dart';
import 'package:spotiflac_android/utils/audio_format_utils.dart';
import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('PlatformBridge');
@@ -833,6 +834,25 @@ class PlatformBridge {
return _invokeMap('readFileMetadata', {'file_path': filePath});
}
/// Reads the tags and quality fields used for automatic Library display.
///
/// Android's readAudioMetadata implementation can inspect most SAF files
/// through /proc/self/fd, avoiding the full-file cache copy required by tag
/// editing. Keep the complete reader as a compatibility fallback for
/// malformed or incorrectly named files that the lightweight scanner could
/// only identify from their filename.
static Future<Map<String, dynamic>> readDisplayAudioMetadata(
String filePath,
) async {
final scanned = await readAudioMetadata(filePath);
if (scanned != null &&
scanned['error'] == null &&
scanned['metadataFromFilename'] != true) {
return normalizeScannedAudioMetadata(scanned);
}
return readFileMetadata(filePath);
}
static Future<Map<String, dynamic>> editFileMetadata(
String filePath,
Map<String, String> metadata,
+33
View File
@@ -3,6 +3,39 @@ import 'package:spotiflac_android/utils/string_utils.dart';
/// Audio format/quality helpers shared by the download queue and history
/// providers.
Map<String, dynamic> normalizeScannedAudioMetadata(
Map<String, dynamic> metadata,
) {
dynamic firstValue(String snakeCase, String camelCase) {
final snakeValue = metadata[snakeCase];
return snakeValue ?? metadata[camelCase];
}
final normalized = <String, dynamic>{...metadata};
final metadataFromFilename = metadata['metadataFromFilename'] == true;
// readAudioMetadata uses the LibraryScanResult contract. Automatic metadata
// probes use the same snake_case keys as readFileMetadata so callers can use
// the cheaper SAF file-descriptor path without knowing which backend result
// shape was returned.
if (!metadataFromFilename) {
normalized['title'] = firstValue('title', 'trackName');
normalized['artist'] = firstValue('artist', 'artistName');
normalized['album'] = firstValue('album', 'albumName');
}
normalized['album_artist'] = firstValue('album_artist', 'albumArtist');
normalized['date'] = firstValue('date', 'releaseDate');
normalized['track_number'] = firstValue('track_number', 'trackNumber');
normalized['total_tracks'] = firstValue('total_tracks', 'totalTracks');
normalized['disc_number'] = firstValue('disc_number', 'discNumber');
normalized['total_discs'] = firstValue('total_discs', 'totalDiscs');
normalized['bit_depth'] = firstValue('bit_depth', 'bitDepth');
normalized['sample_rate'] = firstValue('sample_rate', 'sampleRate');
normalized['audio_codec'] =
firstValue('audio_codec', 'audioCodec') ?? metadata['format'];
return normalized;
}
int? readPositiveBitrateKbps(dynamic value) {
final parsed = readPositiveInt(value);
if (parsed == null) return null;