diff --git a/FFMP.cs b/FFMP.cs index df7202e..251528d 100644 --- a/FFMP.cs +++ b/FFMP.cs @@ -12,74 +12,73 @@ class FFMP { static void Main(string[] args) -{ - try { - Console.WriteLine("Starting application..."); + try + { + Console.WriteLine("Starting application..."); - // Split args into application and FFmpeg arguments - var appArgs = args.TakeWhile(arg => arg != "--").ToArray(); - var ffmpegArgs = args.SkipWhile(arg => arg != "--").Skip(1).ToArray(); + // Split args into application and FFmpeg arguments + var appArgs = args.TakeWhile(arg => arg != "--").ToArray(); + var ffmpegArgs = args.SkipWhile(arg => arg != "--").Skip(1).ToArray(); - if (!appArgs.Any()) - { - Console.WriteLine("Error: No arguments provided. Please specify required arguments."); - Environment.Exit(1); - } + if (!appArgs.Any()) + { + Console.WriteLine("Error: No arguments provided. Please specify required arguments."); + Environment.Exit(1); + } - Console.CancelKeyPress += (sender, e) => - { - Console.WriteLine("Terminating processes..."); - foreach (var process in TrackedProcesses.ToList()) + Console.CancelKeyPress += (sender, e) => { - try + Console.WriteLine("Terminating processes..."); + foreach (var process in TrackedProcesses.ToList()) { - if (process != null && !process.HasExited) + try { - process.Kill(); - Console.WriteLine($"Terminated FFmpeg process with ID {process.Id}"); + if (process != null && !process.HasExited) + { + process.Kill(); + Console.WriteLine($"Terminated FFmpeg process with ID {process.Id}"); + } + } + catch (InvalidOperationException ex) + { + Console.WriteLine($"Process already terminated or invalid: {ex.Message}"); } } - catch (InvalidOperationException ex) - { - Console.WriteLine($"Process already terminated or invalid: {ex.Message}"); - } - } - e.Cancel = true; - Environment.Exit(0); - }; + e.Cancel = true; + Environment.Exit(0); + }; - Parser.Default.ParseArguments(appArgs) - .WithParsed(options => - { - if (ValidateOptions(options)) - { - Run(options, ffmpegArgs).Wait(); - } - else + Parser.Default.ParseArguments(appArgs) + .WithParsed(options => { - Environment.Exit(1); - } - }) - .WithNotParsed(errors => - { - Console.WriteLine("Failed to parse arguments."); - foreach (var error in errors) + if (ValidateOptions(options)) + { + Run(options, ffmpegArgs).Wait(); + } + else + { + Environment.Exit(1); + } + }) + .WithNotParsed(errors => { - Console.WriteLine(error.ToString()); - } + Console.WriteLine("Failed to parse arguments."); + foreach (var error in errors) + { + Console.WriteLine(error.ToString()); + } - Environment.Exit(1); - }); - } - catch (Exception ex) - { - Console.WriteLine($"Fatal error: {ex.Message}"); - Console.WriteLine(ex.StackTrace); + Environment.Exit(1); + }); + } + catch (Exception ex) + { + Console.WriteLine($"Fatal error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } } -} - private static readonly ConcurrentBag TrackedProcesses = new ConcurrentBag(); @@ -113,6 +112,8 @@ static async Task Run(Options options, string[] ffmpegArgs) try { Console.WriteLine($"Processing file: {inputFile}"); + + // Call the ProcessFile method await ProcessFile(inputFile, options, ffmpegArgs, progress, inputFiles.Count); } catch (Exception ex) @@ -136,7 +137,6 @@ static async Task Run(Options options, string[] ffmpegArgs) } } - static IEnumerable GetInputFiles(Options options) { try @@ -191,113 +191,124 @@ static IEnumerable GetInputFiles(Options options) return Enumerable.Empty(); } - static async Task ProcessFile(string inputFile, Options options, string[] ffmpegArgs, ProgressBar progress, int totalFiles) -{ - var outputFile = GenerateOutputFilePath(inputFile, options.OutputPattern); - - if (string.IsNullOrEmpty(Path.GetDirectoryName(outputFile))) + static async Task ProcessFile(string inputFile, Options options, string[] ffmpegArgs, ProgressBar progress, + int totalFiles) { - outputFile = Path.Combine(Path.GetDirectoryName(inputFile)!, outputFile); - } - - Directory.CreateDirectory(Path.GetDirectoryName(outputFile)!); + string outputFile; - if (File.Exists(outputFile) && !options.Overwrite) - { - Console.WriteLine($"Skipping {inputFile}, output already exists."); - return; - } + // Determine output file for conversion + if (options.Convert) + { + var targetExtension = options.OutputFormat ?? ".mkv"; // Default to MKV if not provided + var baseFileName = Path.Combine(Path.GetDirectoryName(inputFile)!, + Path.GetFileNameWithoutExtension(inputFile)); + outputFile = $"{baseFileName}{targetExtension}"; + } + else + { + outputFile = GenerateOutputFilePath(inputFile, options.OutputPattern); + } - // Build FFmpeg arguments - var arguments = $"-i \"{Path.GetFullPath(inputFile)}\" -c:v {options.Codec}"; - if (!string.IsNullOrEmpty(options.Preset)) - { - arguments += $" -preset {options.Preset}"; - } + // Ensure output directory exists + Directory.CreateDirectory(Path.GetDirectoryName(outputFile)!); - // Append FFmpeg-specific arguments - if (ffmpegArgs.Any()) - { - arguments += $" {string.Join(" ", ffmpegArgs)}"; - } + if (File.Exists(outputFile) && !options.Overwrite) + { + Console.WriteLine($"Skipping {inputFile}, output already exists."); + return; + } - arguments += $" \"{Path.GetFullPath(outputFile)}\""; + // Build FFmpeg arguments + var arguments = $"-i \"{Path.GetFullPath(inputFile)}\""; + if (options.Convert) + { + arguments += $" \"{outputFile}\""; + } + else + { + arguments += $" -c:v {options.Codec}"; + if (!string.IsNullOrEmpty(options.Preset)) + { + arguments += $" -preset {options.Preset}"; + } - if (!options.Verbose) - { - arguments = $"-loglevel error {arguments}"; - } + if (ffmpegArgs.Any()) + { + arguments += $" {string.Join(" ", ffmpegArgs)}"; + } - //Console.WriteLine($"Executing FFmpeg command: ffmpeg {arguments}"); + arguments += $" \"{Path.GetFullPath(outputFile)}\""; + } - var process = new Process - { - StartInfo = new ProcessStartInfo + // Set log level based on verbose mode + if (options.Verbose) { - FileName = "ffmpeg", - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - UseShellExecute = false, - CreateNoWindow = true + arguments = $"-loglevel verbose {arguments}"; } - }; - - try - { - process.Start(); - TrackedProcesses.Add(process); - - if (options.Overwrite) + else { - await process.StandardInput.WriteLineAsync("Y"); - await process.StandardInput.FlushAsync(); + arguments = $"-loglevel error {arguments}"; } - if (options.Verbose) + var process = new Process { - var outputTask = Task.Run(() => + StartInfo = new ProcessStartInfo { - while (!process.StandardOutput.EndOfStream) - { - Console.WriteLine(process.StandardOutput.ReadLine()); - } - }); + FileName = "ffmpeg", + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + try + { + process.Start(); - var errorTask = Task.Run(() => + // Capture and display output in verbose mode + if (options.Verbose) { - while (!process.StandardError.EndOfStream) + var outputTask = Task.Run(() => { - Console.WriteLine(process.StandardError.ReadLine()); - } - }); + while (!process.StandardOutput.EndOfStream) + { + Console.WriteLine(process.StandardOutput.ReadLine()); + } + }); - await Task.WhenAll(outputTask, errorTask); - } + var errorTask = Task.Run(() => + { + while (!process.StandardError.EndOfStream) + { + Console.WriteLine(process.StandardError.ReadLine()); + } + }); - await process.WaitForExitAsync(); + await Task.WhenAll(outputTask, errorTask); + } + else + { + // Wait silently for process to finish + await process.WaitForExitAsync(); + } - if (process.ExitCode == 0) - { - Console.WriteLine($"FFmpeg process completed successfully for file: {inputFile}"); - progress.Report(1.0 / totalFiles); + if (process.ExitCode == 0) + { + Console.WriteLine($"FFmpeg process completed successfully for file: {inputFile}"); + progress.Report(1.0 / totalFiles); + } + else + { + Console.WriteLine($"FFmpeg process exited with code {process.ExitCode} for file: {inputFile}"); + } } - else + catch (Exception ex) { - Console.WriteLine($"FFmpeg process exited with code {process.ExitCode}"); + Console.WriteLine($"Error executing FFmpeg for file {inputFile}: {ex.Message}"); } } - catch (Exception ex) - { - Console.WriteLine($"Error executing FFmpeg: {ex.Message}"); - } - finally - { - TrackedProcesses.TryTake(out process); - } -} - static string GenerateOutputFilePath(string inputFile, string pattern) { @@ -322,18 +333,32 @@ private static bool ValidateOptions(Options options) { bool isValid = true; - if (string.IsNullOrWhiteSpace(options.Codec)) + if (options.Convert) { - Console.WriteLine("Error: The 'codec' argument is required."); - isValid = false; + // Ensure required fields for conversion + if (string.IsNullOrWhiteSpace(options.OutputFormat)) + { + Console.WriteLine("Error: The 'output-format' argument is required when using '--convert'."); + isValid = false; + } } - - if (string.IsNullOrWhiteSpace(options.OutputPattern)) + else { - Console.WriteLine("Error: The 'output-pattern' argument is required."); - isValid = false; + // Ensure required fields for transcoding + if (string.IsNullOrWhiteSpace(options.Codec)) + { + Console.WriteLine("Error: The 'codec' argument is required."); + isValid = false; + } + + if (string.IsNullOrWhiteSpace(options.OutputPattern)) + { + Console.WriteLine("Error: The 'output-pattern' argument is required."); + isValid = false; + } } + // Common validation for both modes if (string.IsNullOrWhiteSpace(options.InputDirectory) && string.IsNullOrWhiteSpace(options.InputFile)) { Console.WriteLine("Error: Either 'directory' or 'file' argument must be provided for input."); diff --git a/FFMP.csproj b/FFMP.csproj index 0588314..c65a333 100644 --- a/FFMP.csproj +++ b/FFMP.csproj @@ -5,9 +5,9 @@ net8.0 enable enable - 1.2.0 - 1.2 - 1.2 + 1.3.0 + 1.3 + 1.3 diff --git a/Options.cs b/Options.cs index b05bf9c..d9dcf46 100644 --- a/Options.cs +++ b/Options.cs @@ -7,12 +7,11 @@ public class Options [Option('t', "threads", Default = 2, HelpText = "Number of threads to use. Default is 2.")] public int ThreadCount { get; set; } - [Option("codec", Required = true, HelpText = "Codec to use for video processing, e.g., 'libx265'.")] + [Option("codec", HelpText = "Codec to use for video processing, e.g., 'libx265'.")] public string Codec { get; set; } = string.Empty; - [Option("preset", Required = false, HelpText = "Preset to use for FFmpeg encoding, e.g., 'fast'.")] + [Option("preset", HelpText = "Preset to use for FFmpeg encoding, e.g., 'fast'.")] public string Preset { get; set; } = string.Empty; - public string FFmpegOptions { get; set; } = string.Empty; [Option('d', "directory", HelpText = "Directory containing files to process.")] public string? InputDirectory { get; set; } @@ -29,10 +28,16 @@ public class Options [Option("delete", Default = false, HelpText = "Delete the source file after processing.")] public bool DeleteSource { get; set; } - [Option("output-pattern", Required = true, HelpText = "Pattern for output file paths. Use {{name}}, {{ext}}, and {{dir}} placeholders.")] + [Option("output-pattern", + HelpText = "Pattern for output file paths. Use {{name}}, {{ext}}, and {{dir}} placeholders.")] public string OutputPattern { get; set; } = string.Empty; - - [Value(0, Required = false, HelpText = "Arguments to pass directly to FFmpeg after '--'.")] - public IEnumerable FFmpegArguments { get; set; } = Enumerable.Empty(); + [Option("convert", Default = false, HelpText = "Enable conversion mode.")] + public bool Convert { get; set; } + + [Option("output-format", HelpText = "Target output format (e.g., '.mkv').")] + public string? OutputFormat { get; set; } + + [Value(0, HelpText = "Arguments to pass directly to FFmpeg after '--'.")] + public IEnumerable FFmpegArguments { get; set; } = Enumerable.Empty(); } \ No newline at end of file diff --git a/README.md b/README.md index 496c761..762cda8 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ A multithreaded C# CLI for digital media processing using FFMPEG. Transcode as m ## 💻 Usage +### General Usage + FFMP's usage is almost identical to FFMPEG, consider this simple example: ```bash @@ -48,11 +50,29 @@ You can adjust your codec by using any codec that is installed on your system be If you need to pass any other arguments to FFMPEG (like audio codecs, video bitrate, subtitle processing, etc.) you can do it like this: +### Advanced Arguments for FFMPEG + ```bash dotnet path/to/FFMP.dll --codec libx265 --preset fast -t "/path/to/videos.txt" --output-pattern "/path/to/output/files/{{name}}_compressed{{ext}}" --threads 2 -- -crf 22 -pix_fmt yuv420p10le -c:a libopus -b:a 320k -c:s copy ``` Everything behind the `--` indicator is passed directly to FFMPEG. +### Mass-Converting Files + +Introduced in Version 1.3.0, FFMP now features "Mass-Converting" Files. This takes advantage of everything FFMP already offers and enables Mass-Converting files from one format to another. +Not only can you provide directories or txt-files as sources, multiple videos are converted in parallel. + +**Converting using a directory** + +```bash +dotnet path/to/FFMP.dll --convert -d "/path/to/videos/directory" --output-format .mkv +``` + +**Converting using a txt-file** +```bash +dotnet path/to/FFMP.dll --convert -d "/path/to/videos.txt" --output-format .mkv +``` + If you want to see all of FFMPEGs output, just use the `--verbose` flag. For more ffmpeg options, visit [ffmpeg's documentation](https://ffmpeg.org/ffmpeg.html).