mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 21:30:23 +02:00
fix(metadata): preserve explicit advisory across conversions
This commit is contained in:
@@ -93,6 +93,10 @@ class AudioMetadataMapper {
|
||||
case 'COMMENT':
|
||||
case 'DESCRIPTION':
|
||||
fields['comment'] = value;
|
||||
case 'ITUNESADVISORY':
|
||||
case 'EXPLICIT':
|
||||
case 'ISEXPLICIT':
|
||||
fields['explicit'] = _normalizeAdvisoryValue(value);
|
||||
case 'LYRICS':
|
||||
case 'UNSYNCEDLYRICS':
|
||||
fields['lyrics'] = value;
|
||||
@@ -163,6 +167,10 @@ class AudioMetadataMapper {
|
||||
vorbis['COMPOSER'] = value;
|
||||
case 'COMMENT':
|
||||
vorbis['COMMENT'] = value;
|
||||
case 'ITUNESADVISORY':
|
||||
case 'EXPLICIT':
|
||||
case 'ISEXPLICIT':
|
||||
vorbis['ITUNESADVISORY'] = _normalizeAdvisoryValue(value);
|
||||
case 'LYRICS':
|
||||
case 'UNSYNCEDLYRICS':
|
||||
vorbis['LYRICS'] = value;
|
||||
@@ -294,6 +302,32 @@ class AudioMetadataMapper {
|
||||
return m4a;
|
||||
}
|
||||
|
||||
/// Maps content-advisory and release identity tags to fields consumed by
|
||||
/// the native M4A editor. Content advisory is written as the integer `rtng`
|
||||
/// atom by that editor instead of an arbitrary MP4 text tag.
|
||||
static Map<String, String> m4aReleaseIdentityFields(
|
||||
Map<String, String> metadata,
|
||||
) {
|
||||
final fields = <String, String>{};
|
||||
for (final entry in metadata.entries) {
|
||||
final key = _normalizeKey(entry.key);
|
||||
switch (key) {
|
||||
case 'ITUNESADVISORY':
|
||||
case 'EXPLICIT':
|
||||
case 'ISEXPLICIT':
|
||||
fields['explicit'] = _normalizeAdvisoryValue(entry.value);
|
||||
case 'RELEASETYPE':
|
||||
fields['album_type'] = entry.value;
|
||||
case 'BARCODE':
|
||||
case 'UPC':
|
||||
fields['upc'] = entry.value;
|
||||
case 'COMPILATION':
|
||||
fields['compilation'] = entry.value;
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// Maps generic metadata keys to ID3 names understood by FFmpeg.
|
||||
static Map<String, String> convertToId3Tags(Map<String, String> metadata) {
|
||||
final id3 = <String, String>{};
|
||||
@@ -335,6 +369,12 @@ class AudioMetadataMapper {
|
||||
id3['composer'] = value;
|
||||
case 'COMMENT':
|
||||
id3['comment'] = value;
|
||||
case 'ITUNESADVISORY':
|
||||
case 'EXPLICIT':
|
||||
case 'ISEXPLICIT':
|
||||
// ID3 has no dedicated advisory frame. FFmpeg stores this as the
|
||||
// conventional TXXX:ITUNESADVISORY user-text frame.
|
||||
id3['ITUNESADVISORY'] = _normalizeAdvisoryValue(value);
|
||||
case 'REPLAYGAINTRACKGAIN':
|
||||
id3['REPLAYGAIN_TRACK_GAIN'] = value;
|
||||
case 'REPLAYGAINTRACKPEAK':
|
||||
@@ -377,4 +417,12 @@ class AudioMetadataMapper {
|
||||
|
||||
static String _normalizeKey(String key) =>
|
||||
key.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), '');
|
||||
|
||||
static String _normalizeAdvisoryValue(String value) {
|
||||
return switch (value.trim().toLowerCase()) {
|
||||
'true' || 'yes' || 'explicit' => '1',
|
||||
'false' || 'no' => '0',
|
||||
final value => value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,9 @@ Future<void> _performBatchConversion(
|
||||
'TITLE': item.trackName,
|
||||
'ARTIST': item.artistName,
|
||||
'ALBUM': item.albumName,
|
||||
if (item.historyItem?.explicit == true ||
|
||||
item.localItem?.explicit == true)
|
||||
'ITUNESADVISORY': '1',
|
||||
};
|
||||
try {
|
||||
final result = await PlatformBridge.readFileMetadata(item.filePath);
|
||||
|
||||
@@ -2401,6 +2401,19 @@ class FFmpegService {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAlac) {
|
||||
await _writeM4AFreeformTags(outputPath, metadata);
|
||||
final identityWritten = await _writeM4AReleaseIdentityTags(
|
||||
outputPath,
|
||||
metadata,
|
||||
);
|
||||
if (!identityWritten) {
|
||||
_log.e('ALAC release identity metadata write failed');
|
||||
await _cleanupConversionOutput(outputPlan);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return _finalizeConversionOutput(
|
||||
plan: outputPlan,
|
||||
inputPath: inputPath,
|
||||
@@ -2565,26 +2578,12 @@ class FFmpegService {
|
||||
/// Restores the iTunes atoms that FFmpeg does not reliably map from generic
|
||||
/// metadata keys. This runs after a successful remux, when the container is
|
||||
/// canonical enough for the native editor even if the original was not.
|
||||
static Future<void> _writeM4AReleaseIdentityTags(
|
||||
static Future<bool> _writeM4AReleaseIdentityTags(
|
||||
String m4aPath,
|
||||
Map<String, String> metadata,
|
||||
) async {
|
||||
final fields = <String, String>{};
|
||||
for (final entry in metadata.entries) {
|
||||
final key = entry.key.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), '');
|
||||
switch (key) {
|
||||
case 'ITUNESADVISORY':
|
||||
fields['explicit'] = entry.value;
|
||||
case 'RELEASETYPE':
|
||||
fields['album_type'] = entry.value;
|
||||
case 'BARCODE':
|
||||
case 'UPC':
|
||||
fields['upc'] = entry.value;
|
||||
case 'COMPILATION':
|
||||
fields['compilation'] = entry.value;
|
||||
}
|
||||
}
|
||||
if (fields.isEmpty) return;
|
||||
final fields = AudioMetadataMapper.m4aReleaseIdentityFields(metadata);
|
||||
if (fields.isEmpty) return true;
|
||||
|
||||
try {
|
||||
final result = await PlatformBridge.editFileMetadata(m4aPath, fields);
|
||||
@@ -2592,9 +2591,12 @@ class FFmpegService {
|
||||
_log.w(
|
||||
'Native M4A release identity write was not completed for $m4aPath',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
_log.w('M4A release identity write failed for $m4aPath: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user