mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 06:52:18 +02:00
refactor(metadata): reuse the platform tag mapper
This commit is contained in:
1 parent
d2d5593b9c
commit
a396bcf08b
3 files changed
+156
-53
No files matched your search
@@ -104,58 +104,6 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState {
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, String> _mapMetadataForTagEmbed(Map<String, dynamic> source) {
|
||||
final mapped = <String, String>{};
|
||||
|
||||
void put(String key, dynamic value) {
|
||||
final normalized = value?.toString().trim();
|
||||
if (normalized == null || normalized.isEmpty) return;
|
||||
mapped[key] = normalized;
|
||||
}
|
||||
|
||||
put('TITLE', source['title']);
|
||||
put('ARTIST', source['artist']);
|
||||
put('ALBUM', source['album']);
|
||||
put('ALBUMARTIST', source['album_artist']);
|
||||
put('DATE', source['date']);
|
||||
put('ISRC', source['isrc']);
|
||||
put('GENRE', source['genre']);
|
||||
put('ORGANIZATION', source['label']);
|
||||
put('COPYRIGHT', source['copyright']);
|
||||
put('COMPOSER', source['composer']);
|
||||
put('COMMENT', source['comment']);
|
||||
put('LYRICS', source['lyrics']);
|
||||
put('UNSYNCEDLYRICS', source['lyrics']);
|
||||
if (parseExplicitFlag(source['explicit']) == true) {
|
||||
mapped['ITUNESADVISORY'] = '1';
|
||||
}
|
||||
|
||||
final trackNumber = source['track_number'];
|
||||
final totalTracks = source['total_tracks'];
|
||||
if (trackNumber != null && trackNumber.toString() != '0') {
|
||||
final trackTag =
|
||||
totalTracks != null &&
|
||||
totalTracks.toString().isNotEmpty &&
|
||||
totalTracks.toString() != '0'
|
||||
? '${trackNumber.toString()}/${totalTracks.toString()}'
|
||||
: trackNumber;
|
||||
put('TRACKNUMBER', trackTag);
|
||||
}
|
||||
final discNumber = source['disc_number'];
|
||||
final totalDiscs = source['total_discs'];
|
||||
if (discNumber != null && discNumber.toString() != '0') {
|
||||
final discTag =
|
||||
totalDiscs != null &&
|
||||
totalDiscs.toString().isNotEmpty &&
|
||||
totalDiscs.toString() != '0'
|
||||
? '${discNumber.toString()}/${totalDiscs.toString()}'
|
||||
: discNumber;
|
||||
put('DISCNUMBER', discTag);
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
String? _extractLossyBitrateLabel(String? quality) {
|
||||
if (quality == null || quality.isEmpty) return null;
|
||||
final match = RegExp(
|
||||
|
||||
@@ -528,7 +528,9 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
try {
|
||||
final result = await PlatformBridge.readFileMetadata(workingPath);
|
||||
if (result['error'] == null) {
|
||||
final mapped = _mapMetadataForTagEmbed(result);
|
||||
// Map separately so a non-explicit file keeps the fallback advisory.
|
||||
final mapped = <String, String>{};
|
||||
mergePlatformMetadataForTagEmbed(target: mapped, source: result);
|
||||
metadata.addAll(mapped);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -15,6 +16,158 @@ void main() {
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
|
||||
for (final fallbackExplicit in [false, true]) {
|
||||
for (final fileExplicit in <bool?>[true, false, null]) {
|
||||
testWidgets(
|
||||
'lyrics embed preserves tags with fallback explicit=$fallbackExplicit '
|
||||
'and file explicit=$fileExplicit',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
tester.view.physicalSize = const Size(1000, 2400);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
const path = 'content://library/document/song.opus';
|
||||
final workingPath = '${Directory.systemTemp.path}/lyrics-test.opus';
|
||||
const lyrics = '[00:01.00]Fresh online lyrics';
|
||||
List<String>? embedArguments;
|
||||
const ffmpeg = MethodChannel('flutter.arthenica.com/ffmpeg_kit');
|
||||
const events = MethodChannel(
|
||||
'flutter.arthenica.com/ffmpeg_kit_event',
|
||||
);
|
||||
const paths = MethodChannel('plugins.flutter.io/path_provider');
|
||||
messenger.setMockMethodCallHandler(paths, (_) async {
|
||||
return Directory.systemTemp.path;
|
||||
});
|
||||
messenger.setMockMethodCallHandler(events, (_) async => null);
|
||||
messenger.setMockMethodCallHandler(ffmpeg, (call) async {
|
||||
if (call.method == 'ffmpegSession') {
|
||||
embedArguments = List<String>.from(
|
||||
(call.arguments as Map)['arguments'] as List,
|
||||
);
|
||||
// Inspect the real embed command without writing an audio file.
|
||||
throw PlatformException(code: 'captured_embed');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'safStat':
|
||||
return jsonEncode({'exists': true, 'size': 100});
|
||||
case 'readAudioMetadata':
|
||||
case 'getSafFileModTimes':
|
||||
return '{}';
|
||||
case 'safCopyToTemp':
|
||||
return workingPath;
|
||||
case 'extractCoverToFile':
|
||||
return jsonEncode({'error': 'No cover'});
|
||||
case 'getLyricsLRCWithSource':
|
||||
final online = (call.arguments as Map)['file_path'] == '';
|
||||
return jsonEncode({
|
||||
'lyrics': online ? lyrics : '',
|
||||
'source': online ? 'Example Lyrics' : '',
|
||||
});
|
||||
case 'readFileMetadata':
|
||||
if ((call.arguments as Map)['file_path'] != workingPath) {
|
||||
return '{}';
|
||||
}
|
||||
return jsonEncode({
|
||||
'title': ' File title ',
|
||||
'artist': ' First Artist; Second Artist ',
|
||||
'album': ' ',
|
||||
'album_artist': ' Album Artist ',
|
||||
'date': ' 2026-01-02 ',
|
||||
'isrc': ' EXAMPLE12345 ',
|
||||
'genre': ' Genre ',
|
||||
'label': ' Label ',
|
||||
'copyright': ' Copyright ',
|
||||
'composer': ' Composer ',
|
||||
'comment': ' Comment ',
|
||||
'lyrics': 'Old embedded lyrics',
|
||||
'track_number': 2,
|
||||
'total_tracks': 12,
|
||||
'disc_number': 1,
|
||||
'total_discs': 3,
|
||||
'explicit': fileExplicit,
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
addTearDown(() {
|
||||
for (final mocked in [channel, ffmpeg, events, paths]) {
|
||||
messenger.setMockMethodCallHandler(mocked, null);
|
||||
}
|
||||
});
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: TrackMetadataScreen(
|
||||
item: DownloadHistoryItem(
|
||||
id: 'lyrics-song',
|
||||
trackName: 'Fallback title',
|
||||
artistName: 'Fallback artist',
|
||||
albumName: 'Fallback album',
|
||||
filePath: path,
|
||||
service: 'example-provider',
|
||||
downloadedAt: DateTime(2026),
|
||||
format: 'opus',
|
||||
explicit: fallbackExplicit,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Fetch from Online'));
|
||||
await tester.tap(find.text('Fetch from Online'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Fresh online lyrics'), findsOneWidget);
|
||||
await tester.ensureVisible(find.text('Embed Lyrics'));
|
||||
await tester.tap(find.text('Embed Lyrics'));
|
||||
// Allow real filesystem cleanup after the mocked native boundary.
|
||||
await tester.runAsync(() async {
|
||||
for (var i = 0; i < 100 && embedArguments == null; i++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
expect(embedArguments, isNotNull);
|
||||
final tags = <String, String>{};
|
||||
for (var i = 0; i < embedArguments!.length - 1; i++) {
|
||||
if (embedArguments![i] != '-metadata') continue;
|
||||
final tag = embedArguments![i + 1];
|
||||
final separator = tag.indexOf('=');
|
||||
tags[tag.substring(0, separator)] = tag.substring(separator + 1);
|
||||
}
|
||||
expect(tags, {
|
||||
'TITLE': 'File title',
|
||||
'ARTIST': 'First Artist; Second Artist',
|
||||
'ALBUM': 'Fallback album',
|
||||
'ALBUMARTIST': 'Album Artist',
|
||||
'DATE': '2026-01-02',
|
||||
'YEAR': '2026',
|
||||
'ISRC': 'EXAMPLE12345',
|
||||
'GENRE': 'Genre',
|
||||
'ORGANIZATION': 'Label',
|
||||
'COPYRIGHT': 'Copyright',
|
||||
'COMPOSER': 'Composer',
|
||||
'COMMENT': 'Comment',
|
||||
'TRACKNUMBER': '2/12',
|
||||
'DISCNUMBER': '1/3',
|
||||
'LYRICS': lyrics,
|
||||
'UNSYNCEDLYRICS': lyrics,
|
||||
if (fallbackExplicit || fileExplicit == true) 'ITUNESADVISORY': '1',
|
||||
});
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final scenario in [
|
||||
'fallback',
|
||||
'local',
|
||||
|
||||
Reference in new issue
Block a user