mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-11 08:57:52 +02:00
209 lines
6.6 KiB
Dart
209 lines
6.6 KiB
Dart
import 'dart:io';
|
|
import 'package:ffmpeg_kit_flutter_new_audio/ffmpeg_kit.dart';
|
|
import 'package:ffmpeg_kit_flutter_new_audio/return_code.dart';
|
|
import 'package:spotiflac_android/utils/logger.dart';
|
|
|
|
final _log = AppLogger('FFmpeg');
|
|
|
|
/// FFmpeg service for iOS using ffmpeg_kit_flutter plugin
|
|
class FFmpegServiceIOS {
|
|
/// Execute FFmpeg command and return result
|
|
static Future<FFmpegResultIOS> _execute(String command) async {
|
|
try {
|
|
final session = await FFmpegKit.execute(command);
|
|
final returnCode = await session.getReturnCode();
|
|
final output = await session.getOutput() ?? '';
|
|
return FFmpegResultIOS(
|
|
success: ReturnCode.isSuccess(returnCode),
|
|
returnCode: returnCode?.getValue() ?? -1,
|
|
output: output,
|
|
);
|
|
} catch (e) {
|
|
_log.e('FFmpeg execute error: $e');
|
|
return FFmpegResultIOS(success: false, returnCode: -1, output: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Convert M4A (DASH segments) to FLAC
|
|
static Future<String?> convertM4aToFlac(String inputPath) async {
|
|
final outputPath = inputPath.replaceAll('.m4a', '.flac');
|
|
final command = '-i "$inputPath" -c:a flac -compression_level 8 "$outputPath" -y';
|
|
final result = await _execute(command);
|
|
|
|
if (result.success) {
|
|
try {
|
|
await File(inputPath).delete();
|
|
} catch (_) {}
|
|
return outputPath;
|
|
}
|
|
|
|
_log.e('M4A to FLAC conversion failed: ${result.output}');
|
|
return null;
|
|
}
|
|
|
|
/// Convert FLAC to MP3
|
|
static Future<String?> 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';
|
|
|
|
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;
|
|
_log.e('FLAC to MP3 conversion failed: ${result.output}');
|
|
return null;
|
|
}
|
|
|
|
/// Convert FLAC to M4A
|
|
static Future<String?> convertFlacToM4a(String inputPath, {String codec = 'aac', String bitrate = '256k'}) async {
|
|
final dir = File(inputPath).parent.path;
|
|
final baseName = inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', '');
|
|
final outputDir = '$dir${Platform.pathSeparator}M4A';
|
|
await Directory(outputDir).create(recursive: true);
|
|
final outputPath = '$outputDir${Platform.pathSeparator}$baseName.m4a';
|
|
|
|
String command;
|
|
if (codec == 'alac') {
|
|
command = '-i "$inputPath" -codec:a alac -map 0:a -map_metadata 0 "$outputPath" -y';
|
|
} else {
|
|
command = '-i "$inputPath" -codec:a aac -b:a $bitrate -map 0:a -map_metadata 0 "$outputPath" -y';
|
|
}
|
|
|
|
final result = await _execute(command);
|
|
if (result.success) return outputPath;
|
|
_log.e('FLAC to M4A conversion failed: ${result.output}');
|
|
return null;
|
|
}
|
|
|
|
/// Embed cover art to FLAC file
|
|
static Future<String?> embedCover(String flacPath, String coverPath) async {
|
|
final tempOutput = '$flacPath.tmp';
|
|
final command = '-i "$flacPath" -i "$coverPath" -map 0:a -map 1:0 -c copy -metadata:s:v title="Album cover" -metadata:s:v comment="Cover (front)" -disposition:v attached_pic "$tempOutput" -y';
|
|
|
|
final result = await _execute(command);
|
|
|
|
if (result.success) {
|
|
try {
|
|
await File(flacPath).delete();
|
|
await File(tempOutput).rename(flacPath);
|
|
return flacPath;
|
|
} catch (e) {
|
|
_log.e('Failed to replace file after cover embed: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
try {
|
|
final tempFile = File(tempOutput);
|
|
if (await tempFile.exists()) await tempFile.delete();
|
|
} catch (_) {}
|
|
|
|
_log.e('Cover embed failed: ${result.output}');
|
|
return null;
|
|
}
|
|
|
|
/// Embed metadata and cover art to FLAC file
|
|
/// Returns the file path on success, null on failure
|
|
static Future<String?> embedMetadata({
|
|
required String flacPath,
|
|
String? coverPath,
|
|
Map<String, String>? metadata,
|
|
}) async {
|
|
final tempOutput = '$flacPath.tmp';
|
|
|
|
// Construct command
|
|
final StringBuffer cmdBuffer = StringBuffer();
|
|
cmdBuffer.write('-i "$flacPath" ');
|
|
|
|
// Add cover input if available
|
|
if (coverPath != null) {
|
|
cmdBuffer.write('-i "$coverPath" ');
|
|
}
|
|
|
|
// Map audio stream
|
|
cmdBuffer.write('-map 0:a ');
|
|
|
|
// Map cover stream if available
|
|
if (coverPath != null) {
|
|
cmdBuffer.write('-map 1:0 ');
|
|
cmdBuffer.write('-c:v copy ');
|
|
cmdBuffer.write('-disposition:v attached_pic ');
|
|
cmdBuffer.write('-metadata:s:v title="Album cover" ');
|
|
cmdBuffer.write('-metadata:s:v comment="Cover (front)" ');
|
|
}
|
|
|
|
// Copy audio codec (don't re-encode)
|
|
cmdBuffer.write('-c:a copy ');
|
|
|
|
// Add text metadata
|
|
if (metadata != null) {
|
|
metadata.forEach((key, value) {
|
|
// Sanitize value: escape double quotes
|
|
final sanitizedValue = value.replaceAll('"', '\\"');
|
|
cmdBuffer.write('-metadata $key="$sanitizedValue" ');
|
|
});
|
|
}
|
|
|
|
cmdBuffer.write('"$tempOutput" -y');
|
|
|
|
final command = cmdBuffer.toString();
|
|
_log.d('Executing FFmpeg command: $command');
|
|
|
|
final result = await _execute(command);
|
|
|
|
if (result.success) {
|
|
try {
|
|
await File(flacPath).delete();
|
|
await File(tempOutput).rename(flacPath);
|
|
return flacPath;
|
|
} catch (e) {
|
|
_log.e('Failed to replace file after metadata embed: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Clean up temp file if exists
|
|
try {
|
|
final tempFile = File(tempOutput);
|
|
if (await tempFile.exists()) {
|
|
await tempFile.delete();
|
|
}
|
|
} catch (_) {}
|
|
|
|
_log.e('Metadata/Cover embed failed: ${result.output}');
|
|
return null;
|
|
}
|
|
|
|
/// Check if FFmpeg is available
|
|
static Future<bool> isAvailable() async {
|
|
try {
|
|
final session = await FFmpegKit.execute('-version');
|
|
final returnCode = await session.getReturnCode();
|
|
return ReturnCode.isSuccess(returnCode);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Get FFmpeg version info
|
|
static Future<String?> getVersion() async {
|
|
try {
|
|
final session = await FFmpegKit.execute('-version');
|
|
return await session.getOutput();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class FFmpegResultIOS {
|
|
final bool success;
|
|
final int returnCode;
|
|
final String output;
|
|
|
|
FFmpegResultIOS({required this.success, required this.returnCode, required this.output});
|
|
}
|