Files
SpotiFLAC-Mobile/lib/utils/ffmpeg_reenrich.dart
T
zarzet 11567fee7d refactor(reenrich): share the FFmpeg re-enrich apply flow
local_album and the queue batch action carried byte-identical ~110-line copies
of the FFmpeg re-enrich apply+SAF-writeback+temp-cleanup flow (differing only in
helper names). Extract one applyFfmpegReEnrichResult() so the SAF temp-file
cleanup — the class of bug fixed in fb7d101 — has a single source. The
track-detail copy stays: it drives snackbars and does not preserve metadata.
2026-07-14 18:33:18 +07:00

148 lines
4.6 KiB
Dart

import 'dart:io';
import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
bool _hasValue(String? value) => value != null && value.trim().isNotEmpty;
Future<void> _safeDeleteFile(String path) async {
try {
final file = File(path);
if (await file.exists()) {
await file.delete();
}
} catch (_) {}
}
Future<void> _cleanupTempFileAndParent(String path) async {
await _safeDeleteFile(path);
try {
final parent = File(path).parent;
if (await parent.exists()) {
await parent.delete();
}
} catch (_) {}
}
/// Embeds [result]'s metadata/cover into [item] via FFmpeg while preserving
/// existing tags, writes the output back through SAF when the source was staged
/// to a temp file, drops the filesystem .lrc sidecar, and cleans up every temp
/// artifact (including on the SAF-write failure path). Returns true when the
/// FFmpeg step produced an output.
///
/// Shared by the local-album and queue batch re-enrich flows. The track-detail
/// screen keeps its own copy because it drives snackbars and does not preserve
/// existing metadata.
Future<bool> applyFfmpegReEnrichResult({
required LocalLibraryItem item,
required Map<String, dynamic> result,
required String artistTagMode,
}) async {
final tempPath = result['temp_path'] as String?;
final safUri = result['saf_uri'] as String?;
final ffmpegTarget = _hasValue(tempPath) ? tempPath! : item.filePath;
final downloadedCoverPath = result['cover_path'] as String?;
String? effectiveCoverPath = downloadedCoverPath;
String? extractedCoverPath;
if (!_hasValue(effectiveCoverPath)) {
try {
final tempDir = await Directory.systemTemp.createTemp('reenrich_cover_');
final coverOutput = '${tempDir.path}${Platform.pathSeparator}cover.jpg';
final extracted = await PlatformBridge.extractCoverToFile(
ffmpegTarget,
coverOutput,
);
if (extracted['error'] == null) {
effectiveCoverPath = coverOutput;
extractedCoverPath = coverOutput;
} else {
try {
await tempDir.delete(recursive: true);
} catch (_) {}
}
} catch (_) {}
}
final metadata = (result['metadata'] as Map<String, dynamic>?)?.map(
(k, v) => MapEntry(k, v.toString()),
);
final format = item.format?.toLowerCase();
final lowerPath = item.filePath.toLowerCase();
final isMp3 = format == 'mp3' || lowerPath.endsWith('.mp3');
final isM4A =
format == 'm4a' ||
format == 'aac' ||
lowerPath.endsWith('.m4a') ||
lowerPath.endsWith('.aac');
final isOpus =
format == 'opus' ||
format == 'ogg' ||
lowerPath.endsWith('.opus') ||
lowerPath.endsWith('.ogg');
String? ffmpegResult;
if (isMp3) {
ffmpegResult = await FFmpegService.embedMetadataToMp3(
mp3Path: ffmpegTarget,
coverPath: effectiveCoverPath,
metadata: metadata,
preserveMetadata: true,
);
} else if (isM4A) {
ffmpegResult = await FFmpegService.embedMetadataToM4a(
m4aPath: ffmpegTarget,
coverPath: effectiveCoverPath,
metadata: metadata,
preserveMetadata: true,
);
} else if (isOpus) {
ffmpegResult = await FFmpegService.embedMetadataToOpus(
opusPath: ffmpegTarget,
coverPath: effectiveCoverPath,
metadata: metadata,
artistTagMode: artistTagMode,
preserveMetadata: true,
);
}
if (ffmpegResult != null && _hasValue(tempPath) && _hasValue(safUri)) {
final ok = await PlatformBridge.writeTempToSaf(ffmpegResult, safUri!);
if (!ok) {
if (_hasValue(downloadedCoverPath)) {
await _safeDeleteFile(downloadedCoverPath!);
}
if (_hasValue(extractedCoverPath)) {
await _cleanupTempFileAndParent(extractedCoverPath!);
}
await _safeDeleteFile(tempPath!);
return false;
}
await writeReEnrichSafSidecarLrc(safUri: safUri, reEnrichResult: result);
}
if (_hasValue(downloadedCoverPath)) {
await _safeDeleteFile(downloadedCoverPath!);
}
if (_hasValue(extractedCoverPath)) {
await _cleanupTempFileAndParent(extractedCoverPath!);
}
if (_hasValue(tempPath)) {
await _safeDeleteFile(tempPath!);
}
if (ffmpegResult != null) {
// Filesystem .lrc sidecar. SAF sidecar is written only after
// writeTempToSaf succeeds.
await writeReEnrichSidecarLrc(
audioFilePath: item.filePath,
reEnrichResult: result,
);
}
return ffmpegResult != null;
}