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 _safeDeleteFile(String path) async { try { final file = File(path); if (await file.exists()) { await file.delete(); } } catch (_) {} } Future _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 applyFfmpegReEnrichResult({ required LocalLibraryItem item, required Map 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; try { 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?)?.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) return false; await writeReEnrichSafSidecarLrc(safUri: safUri, reEnrichResult: result); } if (ffmpegResult != null) { await writeReEnrichSidecarLrc( audioFilePath: item.filePath, reEnrichResult: result, ); } return ffmpegResult != null; } finally { if (_hasValue(downloadedCoverPath)) { await _safeDeleteFile(downloadedCoverPath!); } if (_hasValue(extractedCoverPath)) { await _cleanupTempFileAndParent(extractedCoverPath!); } if (_hasValue(tempPath)) { await _safeDeleteFile(tempPath!); } } }