feat(metadata): remove ReplayGain from individual and selected tracks

This commit is contained in:
zarzet committed 2026-09-25 17:12:21 +07:00
1 parent c24132cb9b
commit 9d530cea9e
19 files changed
+835 -171

No files matched your search

+27 -10
View File
@@ -536,7 +536,7 @@ Future<void> _performBatchConversion(
}
}
/// Batch-scans loudness and writes ReplayGain tags to [selectedItems].
/// Adds or removes ReplayGain tags for [selectedItems].
///
/// [onConfirmOpen] / [onConfirmClosed] let the caller hide and restore any
/// selection UI around the confirmation dialog; [onConfirmClosed] receives
@@ -545,6 +545,7 @@ Future<void> runBatchReplayGain(
BuildContext context,
List<UnifiedLibraryItem> selectedItems, {
required VoidCallback onExitSelectionMode,
bool remove = false,
VoidCallback? onConfirmOpen,
Future<void> Function(bool confirmed)? onConfirmClosed,
}) async {
@@ -555,9 +556,15 @@ Future<void> runBatchReplayGain(
final confirmed = await showAppDialog<bool>(
context: context,
builder: (ctx) => AppAlertDialog(
title: Text(ctx.l10n.replayGainBatchConfirmTitle),
title: Text(
remove
? ctx.l10n.trackRemoveReplayGain
: ctx.l10n.replayGainBatchConfirmTitle,
),
content: Text(
ctx.l10n.replayGainBatchConfirmMessage(selectedItems.length),
remove
? ctx.l10n.replayGainRemoveConfirmMessage(selectedItems.length)
: ctx.l10n.replayGainBatchConfirmMessage(selectedItems.length),
),
actions: [
AppDialogAction(
@@ -568,7 +575,11 @@ Future<void> runBatchReplayGain(
filled: true,
isDefault: true,
onPressed: () => Navigator.pop(ctx, true),
child: Text(ctx.l10n.replayGainBatchConfirmTitle),
child: Text(
remove
? ctx.l10n.trackRemoveReplayGain
: ctx.l10n.replayGainBatchConfirmTitle,
),
),
],
),
@@ -585,7 +596,9 @@ Future<void> runBatchReplayGain(
BatchProgressDialog.show(
context: context,
title: context.l10n.replayGainBatchAnalyzing,
title: remove
? context.l10n.replayGainRemoving
: context.l10n.replayGainBatchAnalyzing,
total: total,
icon: Icons.graphic_eq,
onCancel: () {
@@ -599,10 +612,12 @@ Future<void> runBatchReplayGain(
final item = selectedItems[i];
BatchProgressDialog.update(current: i + 1, detail: item.trackName);
try {
final ok = await ReplayGainService.applyToFile(
item.filePath,
onUnsupportedDecoder: () => unsupportedDecoder = true,
);
final ok = remove
? await ReplayGainService.removeFromFile(item.filePath)
: await ReplayGainService.applyToFile(
item.filePath,
onUnsupportedDecoder: () => unsupportedDecoder = true,
);
if (ok) successCount++;
} catch (_) {}
}
@@ -618,7 +633,9 @@ Future<void> runBatchReplayGain(
SnackBar(
content: Text(
[
context.l10n.replayGainBatchSuccess(successCount, total),
remove
? context.l10n.replayGainRemoveBatchSuccess(successCount, total)
: context.l10n.replayGainBatchSuccess(successCount, total),
if (unsupportedDecoder) context.l10n.replayGainUnsupportedDecoder,
].join('\n'),
),
+31 -4
View File
@@ -7,7 +7,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/logger.dart';
/// Standalone ReplayGain (re)scanning for existing audio files.
/// ReplayGain scanning and tag removal for existing audio files.
///
/// Computes EBU R128 loudness via FFmpeg and writes gain tags using the native
/// metadata editors where supported. Opus uses R128_* rather than legacy tags.
@@ -104,6 +104,33 @@ class ReplayGainService {
(path) => _writeLocalTags(path, gain, peak, album: true),
);
/// Removes track/album tags, including Opus R128 and M4A Sound Check tags.
/// Native editors copy the audio payload unchanged and publish atomically.
static Future<bool> removeFromFile(String filePath) =>
_updateFile(filePath, (path) async {
if (!_isNativeWritableFormat(path)) return false;
const fields = {
'replaygain_track_gain': '',
'replaygain_track_peak': '',
'replaygain_album_gain': '',
'replaygain_album_peak': '',
};
final result = await PlatformBridge.editFileMetadata(path, fields);
final method = result['method'];
if (result['success'] != true ||
result['error'] != null ||
method is! String ||
!(method == 'native' || method.startsWith('native_'))) {
return false;
}
final metadata = await PlatformBridge.readFileMetadata(path);
return metadata['error'] == null &&
metadata['audio_codec'] != null &&
fields.keys.every(
(key) => (metadata[key]?.toString() ?? '').trim().isEmpty,
);
});
static Future<bool> _writeLocalTags(
String path,
String gain,
@@ -182,7 +209,7 @@ class ReplayGainService {
if (isSaf) {
safTempPath = await PlatformBridge.copyContentUriToTemp(filePath);
if (safTempPath == null || safTempPath.isEmpty) {
_log.w('Failed to copy SAF file to temp for ReplayGain scan');
_log.w('Failed to copy SAF file to temp for ReplayGain update');
return false;
}
workingPath = safTempPath;
@@ -199,10 +226,10 @@ class ReplayGainService {
}
refreshPlaybackNormalization(filePath);
_log.i('ReplayGain tags written and verified: $filePath');
_log.i('ReplayGain tags updated and verified: $filePath');
return true;
} catch (e) {
_log.e('Failed to apply ReplayGain', e);
_log.e('Failed to update ReplayGain', e);
return false;
} finally {
if (safTempPath != null) {