perf(embed): native-first tag writes and overlapped fetches in finalization

This commit is contained in:
zarzet
2026-07-14 14:55:59 +07:00
parent 4605517d55
commit e37f13275f
2 changed files with 165 additions and 82 deletions
+56 -43
View File
@@ -2512,6 +2512,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
safBaseName = safFileName.replaceFirst(RegExp(r'\.[^.]+$'), '');
}
String? finalSafFileName = safFileName;
// Filled by the SAF embed op from the local temp so the final quality
// probe doesn't have to copy the published file back out of SAF.
Map<String, dynamic>? probedFinalMetadata;
String? genre;
String? label;
@@ -2585,18 +2588,16 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
);
}
final extendedMetadata = await _loadExtendedMetadataForDeezerId(
// Genre/label/copyright are only consumed at embed time (the payload
// copy is just echoed back in the response), so this lookup overlaps
// with the download instead of delaying its start; it is awaited right
// after the download returns.
final extendedMetadataFuture = _loadExtendedMetadataForDeezerId(
deezerTrackId,
);
if (extendedMetadata != null) {
genre = extendedMetadata.genre;
label = extendedMetadata.label;
copyright = extendedMetadata.copyright;
if (await shouldAbortWork('during extended metadata lookup')) {
return;
}
}
).catchError((Object e) {
_log.w('Extended metadata lookup failed: $e');
return null;
});
Map<String, dynamic> result;
@@ -2699,6 +2700,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
_log.d('Result: $result');
final extendedMetadata = await extendedMetadataFuture;
if (extendedMetadata != null) {
genre = extendedMetadata.genre;
label = extendedMetadata.label;
copyright = extendedMetadata.copyright;
}
final resultFilePath = result['file_path'] as String?;
final resultFileToCleanup =
(resultFilePath != null && result['success'] == true)
@@ -3436,39 +3444,41 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final backendLabel = result['label'] as String?;
final backendCopyright = result['copyright'] as String?;
if (isMp3File) {
await _embedMetadataToFile(
tempPath,
finalTrack,
format: 'mp3',
genre: backendGenre ?? genre,
label: backendLabel ?? label,
copyright: backendCopyright,
downloadService: item.service,
);
} else if (isOpusFile) {
await _embedMetadataToFile(
tempPath,
finalTrack,
format: 'opus',
genre: backendGenre ?? genre,
label: backendLabel ?? label,
copyright: backendCopyright,
downloadService: item.service,
);
} else {
await _embedMetadataToFile(
tempPath,
finalTrack,
format: 'flac',
genre: backendGenre ?? genre,
label: backendLabel ?? label,
copyright: backendCopyright,
downloadService: item.service,
writeExternalLrc: false,
);
// writeExternalLrc: false — tempPath lives in the cache dir,
// so a sidecar .lrc written next to it would be orphaned;
// the SAF .lrc is written by _saveExternalLrc after publish,
// reusing the LRC fetched here via result['lyrics_lrc'].
final embedFormat = isMp3File
? 'mp3'
: isOpusFile
? 'opus'
: 'flac';
final fetchedLrc = await _embedMetadataToFile(
tempPath,
finalTrack,
format: embedFormat,
genre: backendGenre ?? genre,
label: backendLabel ?? label,
copyright: backendCopyright,
downloadService: item.service,
writeExternalLrc: false,
);
final existingLrc = result['lyrics_lrc'] as String?;
if ((existingLrc == null || existingLrc.isEmpty) &&
fetchedLrc != null &&
fetchedLrc.isNotEmpty) {
result['lyrics_lrc'] = fetchedLrc;
}
// Probe quality from the local temp now: the same probe on
// the published content:// URI would copy the whole file
// out of SAF again just to read a few header bytes.
try {
probedFinalMetadata = await PlatformBridge.readFileMetadata(
tempPath,
);
} catch (_) {}
return (tempPath, newFileName);
},
);
@@ -3700,7 +3710,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (canProbeFinalMetadata) {
try {
final metadata = await PlatformBridge.readFileMetadata(filePath);
final probed = probedFinalMetadata;
final metadata = (probed != null && probed['error'] == null)
? probed
: await PlatformBridge.readFileMetadata(filePath);
if (metadata['error'] == null) {
final probedBitDepth = metadata['bit_depth'] is num
? (metadata['bit_depth'] as num).toInt()
@@ -586,7 +586,10 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
///
/// [format] must be one of `'flac'`, `'m4a'`, `'mp3'`, or `'opus'`.
/// [writeExternalLrc] only applies to FLAC and M4A (non-SAF paths handle LRC separately).
Future<void> _embedMetadataToFile(
/// Returns the fetched LRC content (when lyrics were fetched) so callers
/// can reuse it — e.g. the SAF external-.lrc save — without a second
/// network fetch.
Future<String?> _embedMetadataToFile(
String filePath,
Track track, {
required String format,
@@ -601,22 +604,26 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
_log.d(
'Metadata embedding disabled, skipping $format metadata/cover embed',
);
return;
return null;
}
final isFlac = format == 'flac';
final isM4a = format == 'm4a';
final isMp3 = format == 'mp3';
String? coverPath;
Future<String?>? coverFuture;
var coverUrl = normalizeRemoteHttpUrl(track.coverUrl);
if (coverUrl != null && coverUrl.isNotEmpty) {
if (settings.maxQualityCover) {
coverUrl = _upgradeToMaxQualityCover(coverUrl);
}
coverPath = await _sharedEmbedCover(coverUrl);
// Started here, awaited only after the lyrics fetch below so the two
// network round trips overlap. Errors are handled inside the fetch
// (it resolves to null), never as an unhandled rejection.
coverFuture = _sharedEmbedCover(coverUrl);
}
String? lrcContent;
try {
final metadata = <String, String>{
'TITLE': track.name,
@@ -676,7 +683,6 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
settings.embedLyrics &&
!skipLyrics &&
(lyricsMode == 'external' || lyricsMode == 'both');
String? lrcContent;
if (shouldEmbedLyrics || shouldSaveExternalLyrics) {
try {
@@ -742,6 +748,7 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
}
}
final coverPath = coverFuture == null ? null : await coverFuture;
final validCover = coverPath != null && await File(coverPath).exists()
? coverPath
: null;
@@ -781,46 +788,104 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
);
if (ac4Result['handled'] == true) {
_log.d('AC-4 metadata embedded natively for $format');
return;
return lrcContent;
}
} catch (e) {
_log.w('AC-4 metadata path failed, falling back to FFmpeg: $e');
}
}
String? ffmpegResult;
if (isFlac) {
ffmpegResult = await FFmpegService.embedMetadata(
flacPath: filePath,
coverPath: validCover,
metadata: metadata,
artistTagMode: settings.artistTagMode,
);
} else if (isM4a) {
ffmpegResult = await FFmpegService.embedMetadataToM4a(
m4aPath: filePath,
coverPath: validCover,
metadata: metadata,
);
} else if (isMp3) {
ffmpegResult = await FFmpegService.embedMetadataToMp3(
mp3Path: filePath,
coverPath: validCover,
metadata: metadata,
);
} else {
ffmpegResult = await FFmpegService.embedMetadataToOpus(
opusPath: filePath,
coverPath: validCover,
metadata: metadata,
artistTagMode: settings.artistTagMode,
);
// Native Go tag writers handle these formats in-process, streaming the
// audio through untouched — no FFmpeg spawn, no full container remux,
// no temp-promote copy. The Go side answers method=ffmpeg for files it
// can't handle natively, and any failure falls back to FFmpeg below.
// Scanned ReplayGain (opt-in, non-FLAC) keeps the FFmpeg path: its
// extra tags (e.g. Opus R128_TRACK_GAIN) ride the FFmpeg metadata map.
var embeddedNatively = false;
if (scannedReplayGain == null) {
try {
final nativeFields = <String, String>{
'title': track.name,
'artist': track.artistName,
'album': track.albumName,
'artist_tag_mode': settings.artistTagMode,
'album_artist': ?albumArtist,
if (track.releaseDate != null) 'date': track.releaseDate!,
if (track.isrc != null) 'isrc': track.isrc!,
if (track.composer != null && track.composer!.isNotEmpty)
'composer': track.composer!,
if (genre != null && genre.isNotEmpty) 'genre': genre,
if (label != null && label.isNotEmpty) 'label': label,
if (copyright != null && copyright.isNotEmpty)
'copyright': copyright,
if (track.trackNumber != null && track.trackNumber! > 0)
'track_number': track.trackNumber!.toString(),
if (track.totalTracks != null && track.totalTracks! > 0)
'track_total': track.totalTracks!.toString(),
if (track.discNumber != null && track.discNumber! > 0)
'disc_number': track.discNumber!.toString(),
if (track.totalDiscs != null && track.totalDiscs! > 0)
'disc_total': track.totalDiscs!.toString(),
'cover_path': ?validCover,
if (shouldEmbedLyrics && lrcContent != null) ...{
'lyrics': lrcContent,
'unsyncedlyrics': lrcContent,
} else if ((isFlac || isM4a) && !shouldEmbedLyrics) ...{
// Mirrors the FFmpeg map: embedding disabled strips lyrics a
// provider may have pre-embedded (set-or-clear semantics).
'lyrics': '',
if (isFlac) 'unsyncedlyrics': '',
},
};
final response = await PlatformBridge.editFileMetadata(
filePath,
nativeFields,
);
embeddedNatively = response['method'] != 'ffmpeg';
} catch (e) {
_log.w(
'Native $format tag embed failed, falling back to FFmpeg: $e',
);
}
}
if (ffmpegResult != null) {
_log.d('Metadata embedded to $format via FFmpeg');
if (embeddedNatively) {
_log.d('Metadata embedded to $format natively');
} else {
_log.w('FFmpeg $format metadata embed failed');
String? ffmpegResult;
if (isFlac) {
ffmpegResult = await FFmpegService.embedMetadata(
flacPath: filePath,
coverPath: validCover,
metadata: metadata,
artistTagMode: settings.artistTagMode,
);
} else if (isM4a) {
ffmpegResult = await FFmpegService.embedMetadataToM4a(
m4aPath: filePath,
coverPath: validCover,
metadata: metadata,
);
} else if (isMp3) {
ffmpegResult = await FFmpegService.embedMetadataToMp3(
mp3Path: filePath,
coverPath: validCover,
metadata: metadata,
);
} else {
ffmpegResult = await FFmpegService.embedMetadataToOpus(
opusPath: filePath,
coverPath: validCover,
metadata: metadata,
artistTagMode: settings.artistTagMode,
);
}
if (ffmpegResult != null) {
_log.d('Metadata embedded to $format via FFmpeg');
} else {
_log.w('FFmpeg $format metadata embed failed');
}
}
if (isM4a && settings.embedReplayGain && scannedReplayGain != null) {
@@ -838,7 +903,11 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
}
if (isFlac) {
if (settings.artistTagMode == artistTagModeSplitVorbis) {
// The native embed already wrote split tags via artist_tag_mode;
// this second full-file rewrite is only needed after FFmpeg, whose
// metadata map can't express repeated ARTIST tags.
if (!embeddedNatively &&
settings.artistTagMode == artistTagModeSplitVorbis) {
try {
await PlatformBridge.rewriteSplitArtistTags(
filePath,
@@ -872,8 +941,9 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
} catch (e) {
_log.e('Failed to embed metadata to $format: $e');
}
// coverPath is owned by _embedCoverCache: kept for the next track of the
// same release, deleted on LRU eviction or when the queue drains.
// The cover file is owned by _embedCoverCache: kept for the next track
// of the same release, deleted on LRU eviction or when the queue drains.
return lrcContent;
}
static const _embedCoverCacheMax = 8;