Files
SpotiFLAC-Mobile/test/replaygain_service_test.dart
T
zarzet bfffb8da11 fix(replaygain): write and verify native Opus gain tags
Preserve audio and artwork while replacing conflicting Opus gain tags with R128 comments. Verify manual, download, and album writes, handle SAF failures, and refresh playback normalization after saving.
2026-09-06 21:03:57 +07:00

198 lines
5.7 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/replaygain_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('com.zarz.spotiflac/backend');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
late Directory directory;
late String tempPath;
late List<String> calls;
late Map<String, dynamic> metadata;
late Map<String, String> editedFields;
var method = 'native_ogg';
var applyEdits = true;
var saveSaf = true;
setUp(() async {
directory = await Directory.systemTemp.createTemp('replaygain-test-');
tempPath = '${directory.path}/saf_song.opus';
calls = [];
editedFields = {};
metadata = {
'audio_codec': 'opus',
'replaygain_track_gain': '-4.00 dB',
'replaygain_album_gain': '-3.00 dB',
};
method = 'native_ogg';
applyEdits = true;
saveSaf = true;
messenger.setMockMethodCallHandler(channel, (call) async {
calls.add(call.method);
final args = Map<String, dynamic>.from(call.arguments as Map);
switch (call.method) {
case 'safCopyToTemp':
await File(tempPath).writeAsString('original audio');
return tempPath;
case 'editFileMetadata':
editedFields = Map<String, String>.from(
jsonDecode(args['metadata_json'] as String) as Map,
);
if (applyEdits) {
metadata.addAll(editedFields);
if (metadata['audio_codec'] == 'opus') {
metadata.removeWhere((key, _) => key.endsWith('_peak'));
}
}
return jsonEncode({'success': true, 'method': method});
case 'readFileMetadata':
return jsonEncode(metadata);
case 'writeTempToSaf':
expect(args['temp_path'], tempPath);
expect(await File(tempPath).exists(), isTrue);
return jsonEncode({'success': saveSaf});
default:
fail('Unexpected method: ${call.method}');
}
});
});
tearDown(() async {
messenger.setMockMethodCallHandler(channel, null);
await directory.delete(recursive: true);
});
test(
'manual SAF scan writes and verifies Opus before copying back',
() async {
final result = await ReplayGainService.applyToFile(
'content://music/document/42',
scan: (path) async {
expect(path, tempPath);
expect(await File(path).exists(), isTrue);
calls.add('scan');
return ReplayGainResult(
trackGain: '-12.20 dB',
trackPeak: '1.258925',
integratedLufs: -5.8,
truePeakLinear: 1.258925,
);
},
);
expect(result, isTrue);
expect(calls, [
'safCopyToTemp',
'scan',
'editFileMetadata',
'readFileMetadata',
'writeTempToSaf',
]);
expect(editedFields['replaygain_track_gain'], '-12.20 dB');
expect(await File(tempPath).exists(), isFalse);
},
);
test(
'album gain on an extensionless SAF URI uses the native writer',
() async {
expect(
await ReplayGainService.writeAlbumTags(
'content://music/document/42',
'-10.00 dB',
'1.300000',
),
isTrue,
);
expect(editedFields, {
'replaygain_album_gain': '-10.00 dB',
'replaygain_album_peak': '1.300000',
});
expect(metadata['replaygain_track_gain'], '-4.00 dB');
expect(calls.last, 'writeTempToSaf');
expect(await File(tempPath).exists(), isFalse);
},
);
test('native fallback instruction is not a successful Opus write', () async {
method = 'ffmpeg';
applyEdits = false;
expect(
await ReplayGainService.writeTrackTags(
'content://music/document/42',
'-12.20 dB',
'1.258925',
),
isFalse,
);
expect(calls, ['safCopyToTemp', 'editFileMetadata']);
expect(await File(tempPath).exists(), isFalse);
});
test('unchanged gain fails verification and never overwrites SAF', () async {
applyEdits = false;
expect(
await ReplayGainService.writeTrackTags(
'content://music/document/42',
'-12.20 dB',
'1.258925',
),
isFalse,
);
expect(calls, ['safCopyToTemp', 'editFileMetadata', 'readFileMetadata']);
expect(await File(tempPath).exists(), isFalse);
});
test(
'SAF save failure is reported and the temporary copy is removed',
() async {
saveSaf = false;
expect(
await ReplayGainService.writeTrackTags(
'content://music/document/42',
'-12.20 dB',
'1.258925',
),
isFalse,
);
expect(calls.last, 'writeTempToSaf');
expect(await File(tempPath).exists(), isFalse);
},
);
test(
'scan failure leaves SAF untouched and removes its temporary copy',
() async {
expect(
await ReplayGainService.applyToFile(
'content://music/document/42',
scan: (_) async => null,
),
isFalse,
);
expect(calls, ['safCopyToTemp']);
expect(await File(tempPath).exists(), isFalse);
},
);
test('FLAC native method and peak verification remain supported', () async {
method = 'native';
metadata['audio_codec'] = 'flac';
expect(
await ReplayGainService.writeTrackTags(
'${directory.path}/song.flac',
'-12.20 dB',
'1.258925',
),
isTrue,
);
expect(metadata['replaygain_track_peak'], '1.258925');
expect(calls, ['editFileMetadata', 'readFileMetadata']);
});
}