From 61720f3f2a95a490d2388d2cb26ff1e0596dca02 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 19 Jan 2026 02:55:39 +0700 Subject: [PATCH] chore(ios): sync FFmpeg service and add palette_generator dependency --- build_assets/ffmpeg_service_ios.dart | 141 +++++++++++++++++++++++++-- pubspec_ios.yaml | 1 + 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/build_assets/ffmpeg_service_ios.dart b/build_assets/ffmpeg_service_ios.dart index b386490a..da0543ce 100644 --- a/build_assets/ffmpeg_service_ios.dart +++ b/build_assets/ffmpeg_service_ios.dart @@ -42,17 +42,27 @@ class FFmpegServiceIOS { } /// Convert FLAC to MP3 - static Future convertFlacToMp3(String inputPath, {String bitrate = '320k'}) async { - final dir = File(inputPath).parent.path; - final baseName = inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', ''); - final outputDir = '$dir${Platform.pathSeparator}MP3'; - await Directory(outputDir).create(recursive: true); - final outputPath = '$outputDir${Platform.pathSeparator}$baseName.mp3'; + /// If deleteOriginal is true, deletes the FLAC file after conversion + static Future convertFlacToMp3( + String inputPath, { + String bitrate = '320k', + bool deleteOriginal = true, + }) async { + // Convert in same folder, just change extension + final outputPath = inputPath.replaceAll('.flac', '.mp3'); final command = '-i "$inputPath" -codec:a libmp3lame -b:a $bitrate -map 0:a -map_metadata 0 -id3v2_version 3 "$outputPath" -y'; final result = await _execute(command); - if (result.success) return outputPath; + if (result.success) { + // Delete original FLAC if requested + if (deleteOriginal) { + try { + await File(inputPath).delete(); + } catch (_) {} + } + return outputPath; + } _log.e('FLAC to MP3 conversion failed: ${result.output}'); return null; } @@ -177,6 +187,123 @@ class FFmpegServiceIOS { return null; } + /// Embed metadata and cover art to MP3 file using ID3v2 tags + /// Returns the file path on success, null on failure + static Future embedMetadataToMp3({ + required String mp3Path, + String? coverPath, + Map? metadata, + }) async { + final tempOutput = '$mp3Path.tmp'; + + final StringBuffer cmdBuffer = StringBuffer(); + cmdBuffer.write('-i "$mp3Path" '); + + if (coverPath != null) { + cmdBuffer.write('-i "$coverPath" '); + } + + cmdBuffer.write('-map 0:a '); + + if (coverPath != null) { + cmdBuffer.write('-map 1:0 '); + cmdBuffer.write('-c:v:0 copy '); + cmdBuffer.write('-id3v2_version 3 '); + cmdBuffer.write('-metadata:s:v title="Album cover" '); + cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); + } + + cmdBuffer.write('-c:a copy '); + + if (metadata != null) { + // Convert FLAC/Vorbis tags to ID3v2 tags for MP3 + final id3Metadata = _convertToId3Tags(metadata); + id3Metadata.forEach((key, value) { + final sanitizedValue = value.replaceAll('"', '\\"'); + cmdBuffer.write('-metadata $key="$sanitizedValue" '); + }); + } + + cmdBuffer.write('-id3v2_version 3 "$tempOutput" -y'); + + final command = cmdBuffer.toString(); + _log.d('Executing FFmpeg MP3 embed command: $command'); + + final result = await _execute(command); + + if (result.success) { + try { + await File(mp3Path).delete(); + await File(tempOutput).rename(mp3Path); + _log.d('MP3 metadata embedded successfully'); + return mp3Path; + } catch (e) { + _log.e('Failed to replace MP3 file after metadata embed: $e'); + return null; + } + } + + try { + final tempFile = File(tempOutput); + if (await tempFile.exists()) { + await tempFile.delete(); + } + } catch (_) {} + + _log.e('MP3 Metadata/Cover embed failed: ${result.output}'); + return null; + } + + /// Convert FLAC/Vorbis comment tags to ID3v2 compatible tags + static Map _convertToId3Tags(Map vorbisMetadata) { + final id3Map = {}; + + for (final entry in vorbisMetadata.entries) { + final key = entry.key.toUpperCase(); + final value = entry.value; + + // Map Vorbis comments to ID3v2 frame names + switch (key) { + case 'TITLE': + id3Map['title'] = value; + break; + case 'ARTIST': + id3Map['artist'] = value; + break; + case 'ALBUM': + id3Map['album'] = value; + break; + case 'ALBUMARTIST': + id3Map['album_artist'] = value; + break; + case 'TRACKNUMBER': + case 'TRACK': + id3Map['track'] = value; + break; + case 'DISCNUMBER': + case 'DISC': + id3Map['disc'] = value; + break; + case 'DATE': + case 'YEAR': + id3Map['date'] = value; + break; + case 'ISRC': + id3Map['TSRC'] = value; // ID3v2 ISRC frame + break; + case 'LYRICS': + case 'UNSYNCEDLYRICS': + id3Map['lyrics'] = value; + break; + default: + // Pass through other tags as-is + id3Map[key.toLowerCase()] = value; + } + } + + return id3Map; + } + /// Check if FFmpeg is available static Future isAvailable() async { try { diff --git a/pubspec_ios.yaml b/pubspec_ios.yaml index dc2fb835..da99f5d8 100644 --- a/pubspec_ios.yaml +++ b/pubspec_ios.yaml @@ -38,6 +38,7 @@ dependencies: # Material Expressive 3 / Dynamic Color dynamic_color: ^1.7.0 material_color_utilities: ^0.11.1 + palette_generator: ^0.3.3+4 # Permissions permission_handler: ^12.0.1