import 'package:flutter/foundation.dart'; import 'package:spotiflac_android/services/ffmpeg_conversion.dart'; import 'package:spotiflac_android/services/ffmpeg_decryption.dart'; import 'package:spotiflac_android/services/ffmpeg_metadata.dart'; import 'package:spotiflac_android/services/ffmpeg_models.dart'; import 'package:spotiflac_android/services/ffmpeg_probe.dart'; import 'package:spotiflac_android/services/ffmpeg_replaygain.dart'; import 'package:spotiflac_android/utils/artist_utils.dart'; import 'package:spotiflac_android/utils/audio_conversion_utils.dart'; export 'package:spotiflac_android/services/ffmpeg_models.dart'; /// Compatibility entry point for callers; feature modules own the operations. class FFmpegService { @visibleForTesting static List buildCoverResizeArguments({ required String inputPath, required String outputPath, required int maxDimension, }) => FFmpegMetadata.buildCoverResizeArguments( inputPath: inputPath, outputPath: outputPath, maxDimension: maxDimension, ); /// Preserves aspect ratio, allowing upscale to the explicitly chosen size. static Future resizeCoverArt({ required String inputPath, required String outputPath, required int maxDimension, }) => FFmpegMetadata.resizeCoverArt( inputPath: inputPath, outputPath: outputPath, maxDimension: maxDimension, ); @visibleForTesting static ({int width, int height})? imageDimensionsFromProperties( Map properties, ) => FFmpegProbe.imageDimensionsFromProperties(properties); /// Reads image dimensions without decoding a bitmap into Dart memory. static Future<({int width, int height})?> probeImageDimensions( String filePath, ) => FFmpegProbe.imageDimensions(filePath); static Future probePrimaryAudioCodec(String filePath) => FFmpegProbe.audioCodec(filePath); static bool isLosslessAudioCodec(String? codec) => FFmpegProbe.isLosslessAudioCodec(codec); /// Checks the native FLAC magic, distinguishing FLAC-in-MP4 payloads. static Future isNativeFlacFile(String filePath) => FFmpegProbe.isNativeFlacFile(filePath); static Future convertM4aToFlac( String inputPath, { String? sourceCodec, @visibleForTesting Future Function(List)? execute, }) => FFmpegConversion.convertM4aToFlac( inputPath, sourceCodec: sourceCodec, execute: execute, ); /// Corrects the suffix without replacing an existing sibling file. static Future ensureNativeFlacExtension(String inputPath) => FFmpegConversion.ensureNativeFlacExtension(inputPath); static Future decryptWithDescriptor({ required String inputPath, required DownloadDecryptionDescriptor descriptor, bool deleteOriginal = true, Future Function(String)? prepareOutput, }) => FFmpegDecryption.decrypt( inputPath: inputPath, descriptor: descriptor, deleteOriginal: deleteOriginal, prepareOutput: prepareOutput, ); /// Measures EBU R128 loudness and true peak using a -18 LUFS reference. static Future scanReplayGain( String filePath, { void Function()? onUnsupportedDecoder, }) => FFmpegReplayGain.scan( filePath, onUnsupportedDecoder: onUnsupportedDecoder, ); @visibleForTesting static ReplayGainResult? parseReplayGainScan( FFmpegResult result, { void Function()? onUnsupportedDecoder, }) => FFmpegReplayGain.parseScan( result, onUnsupportedDecoder: onUnsupportedDecoder, ); /// With [returnTempPath], hands temp output to [onTempReady] for SAF write and /// cleanup by the caller. Otherwise publishes to the local file. static Future writeAlbumReplayGainTags( String filePath, String albumGain, String albumPeak, { bool returnTempPath = false, void Function(String tempPath)? onTempReady, }) => FFmpegReplayGain.writeAlbumTags( filePath, albumGain, albumPeak, returnTempPath: returnTempPath, onTempReady: onTempReady, ); static Future writeTrackReplayGainTags( String filePath, String trackGain, String trackPeak, ) => FFmpegReplayGain.writeTrackTags(filePath, trackGain, trackPeak); static Future embedMetadata({ required String flacPath, String? coverPath, Map? metadata, String artistTagMode = artistTagModeJoined, }) => FFmpegMetadata.embedMetadata( flacPath: flacPath, coverPath: coverPath, metadata: metadata, artistTagMode: artistTagMode, ); static Future embedMetadataToMp3({ required String mp3Path, String? coverPath, Map? metadata, String artistTagMode = artistTagModeJoined, bool preserveMetadata = false, }) => FFmpegMetadata.embedMetadataToMp3( mp3Path: mp3Path, coverPath: coverPath, metadata: metadata, artistTagMode: artistTagMode, preserveMetadata: preserveMetadata, ); static Future embedMetadataToOpus({ required String opusPath, String? coverPath, Map? metadata, String artistTagMode = artistTagModeJoined, bool preserveMetadata = false, @visibleForTesting Future Function(List)? execute, }) => FFmpegMetadata.embedMetadataToOpus( opusPath: opusPath, coverPath: coverPath, metadata: metadata, artistTagMode: artistTagMode, preserveMetadata: preserveMetadata, execute: execute, ); static Future embedMetadataToM4a({ required String m4aPath, String? coverPath, Map? metadata, String artistTagMode = artistTagModeJoined, bool preserveMetadata = true, }) => FFmpegMetadata.embedMetadataToM4a( m4aPath: m4aPath, coverPath: coverPath, metadata: metadata, artistTagMode: artistTagMode, preserveMetadata: preserveMetadata, ); /// Converts with metadata and cover preservation; lossless caps never upscale. static Future convertAudioFormat({ required String inputPath, required String targetFormat, required String bitrate, required Map metadata, String? coverPath, String artistTagMode = artistTagModeJoined, bool deleteOriginal = true, int? sourceBitDepth, LosslessConversionQuality losslessQuality = const LosslessConversionQuality(), LosslessConversionProcessing losslessProcessing = const LosslessConversionProcessing(), }) => FFmpegConversion.convertAudioFormat( inputPath: inputPath, targetFormat: targetFormat, bitrate: bitrate, metadata: metadata, coverPath: coverPath, artistTagMode: artistTagMode, deleteOriginal: deleteOriginal, sourceBitDepth: sourceBitDepth, losslessQuality: losslessQuality, losslessProcessing: losslessProcessing, ); static Future?> splitCueToTracks({ required String audioPath, required String outputDir, required List tracks, required Map albumMetadata, String? coverPath, void Function(int current, int total)? onProgress, }) => FFmpegConversion.splitCueToTracks( audioPath: audioPath, outputDir: outputDir, tracks: tracks, albumMetadata: albumMetadata, coverPath: coverPath, onProgress: onProgress, ); }