fix(metadata): preserve explicit advisory across conversions

This commit is contained in:
zarzet
2026-08-22 23:24:56 +07:00
parent 19591bdf19
commit 7557ffbf85
7 changed files with 118 additions and 17 deletions
+48
View File
@@ -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,
};
}
}
+3
View File
@@ -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);
+19 -17
View File
@@ -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;
}
}