diff --git a/CommandLine.cs b/CommandLine.cs index 90a01a6..133e0b6 100644 --- a/CommandLine.cs +++ b/CommandLine.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Text; +using OpenCvSharp; namespace splitter; @@ -52,7 +53,7 @@ public sealed class CommandLine public SingleJob Master { get; } = new SingleJob(); public SingleJob[] Jobs { get; } - public bool IsValid => !string.IsNullOrEmpty(Master.InputFile) && !string.IsNullOrEmpty(Master.OutputFolder) && Jobs.Length > 0; + public bool IsValid => !string.IsNullOrEmpty(Master.OutputFolder) && Jobs.Length > 0; public CommandLine(string[] args) { @@ -84,19 +85,28 @@ public sealed class CommandLine return; } - Master.InputFile = args[0]; - var hasOutputFolder = args.Length > 1 && !args[1].StartsWith("-"); + var inputFiles = new List(); - if (hasOutputFolder) - Master.OutputFolder = args[1]; - else - Master.OutputFolder = Path.Combine(Path.GetDirectoryName(Master.InputFile) ?? Directory.GetCurrentDirectory(), "Splitter"); - foreach (var arg in args.Skip(hasOutputFolder ? 2 : 1)) + foreach (var arg in args) { - if (arg.StartsWith("--mask=")) + if (!arg.StartsWith("-")) + { + inputFiles.Add(arg); + } + else if ( arg.StartsWith("--file=")) + { + var fileName = arg.Substring("--file=".Length); + if (File.Exists(arg)) + LoadFile(fileName, inputFiles); + } + else if (arg.StartsWith("--mask=")) { Master.Mask = arg.Substring("--mask=".Length); } + else if (arg.StartsWith("--out=")) + { + Master.OutputFolder = arg.Substring("--out=".Length); + } else if (arg.StartsWith("--detect=")) { Master.Detect = arg.Substring("--detect=".Length).ToLowerInvariant(); @@ -169,7 +179,8 @@ public sealed class CommandLine } } - var files = FileMaskExpander.Expand(Master.InputFile); + var files = inputFiles.SelectMany(x => FileMaskExpander.Expand(x)); + Jobs = files.Select(x => new SingleJob { InputFile = x, @@ -188,6 +199,36 @@ public sealed class CommandLine Rotate = Master.Rotate, Parameters = new Dictionary(Master.Parameters) }).ToArray(); + + if ( Jobs.Length == 0) + { + Console.WriteLine("No valid input files found."); + PrintHelp(); + return; + } + + if (string.IsNullOrWhiteSpace(Master.OutputFolder)) + { + Console.WriteLine("No output folder specified."); + PrintHelp(); + return; + } + } + + private void LoadFile(string fileName, List inputFiles) + { + if (!File.Exists(fileName)) + { + Console.WriteLine($"File not found: {fileName}"); + return; + } + var lines = File.ReadAllLines(fileName); + foreach (var line in lines) + { + var trimmed = line.Trim(); + if (trimmed.Length > 0 && !trimmed.StartsWith("#")) + inputFiles.AddRange(FileMaskExpander.Expand(trimmed)); + } } private static bool TryParseParameter(string spec, out string key, out string value) @@ -286,12 +327,15 @@ public sealed class CommandLine { Console.WriteLine(@" Usage: - splitter [options] [--] + splitter [ ...] [options] [--] Options: + --file= Input names or file masks (e.g. ""videos/*.mp4""). + If not specified, the first non-option argument is used as input. + --mask= Output filename pattern. - Default: _Seg%03d.mp4 - Supports %03d or %d for segment index. + Default: [NAME]_seg[NN].[EXT] + Supports [NAME], [N], [NN], [NNN], [NNNN], [EXT] placeholders. --duration= Override target segment duration. Accepted formats: @@ -350,12 +394,13 @@ Passthrough: input.mp4 can be a file mask, e.g. ""videos/*.mp4"". Output files will be named based on the input filename and the --mask pattern if provided. Examples: - splitter vertical-video.mp4 out/ - splitter vertical-video.mp4 out/ --duration=90s - splitter vertical-video.mp4 out/ --duration=2m30s --mask=""Part%03d.mp4"" - splitter vertical-video.mp4 out/ --estimate - splitter vertical-video.mp4 out/ --force --duration=45 -- -an -sn - splitter horizontal-video.mp4 out/ --crop + splitter vertical-video.mp4 --out=out/ + splitter vertical-video.mp4 --duration=90s + splitter vertical-video.mp4 --duration=2m30s --mask=""[NAME]_[NNNN].mp4"" + splitter vertical-video.mp4 --estimate + splitter vertical-video.mp4 --force --duration=45 -- -an -sn + splitter horizontal-video.mp4 --out=Cropped/ --crop + splitter --file=file_names.txt --out=Cropped/ --crop --detect=body Description: Splits a video into equal or fixed-length segments using multi-threaded diff --git a/FileMaskExpander.cs b/FileMaskExpander.cs index 5886652..31dec18 100644 --- a/FileMaskExpander.cs +++ b/FileMaskExpander.cs @@ -6,7 +6,7 @@ public static class FileMaskExpander { // If no mask, return the single full path if (!HasMask(input)) - return new[] { Path.GetFullPath(input) }; + return [Path.GetFullPath(input)]; string directory = Path.GetDirectoryName(input) ?? Directory.GetCurrentDirectory(); string pattern = Path.GetFileName(input); diff --git a/splitter.cs b/splitter.cs index bbccb12..ae12bf2 100644 --- a/splitter.cs +++ b/splitter.cs @@ -78,8 +78,6 @@ static partial class Program if (!Directory.Exists(job.OutputFolder)) Directory.CreateDirectory(job.OutputFolder); - job.Mask ??= $"{baseName}_seg%03d.mp4"; - var info = ProbeVideo.Probe(job.InputFile); if (info.Duration <= 0) { @@ -105,7 +103,8 @@ static partial class Program segmentLength = info.Duration / segments; } - LogInfo($"{baseName}: Duration {info.Duration:F2}s, {info.Width}x{info.Height} @ {info.Fps:F3}fps {info.Bitrate/1024:F0}kbps, Target duration: {target:F2}s Segments: {segments} segment length: {segmentLength:F2}s {(job.ForceFixed ? " fixed" : "")}" ); + LogInfo($"{baseName}: Duration {info.Duration:F2}s, {info.Width}x{info.Height} @ {info.Fps:F3}fps {info.Bitrate/1024:F0}kbps," + + $" Target duration: {target:F2}s Segments: {segments} segment length: {segmentLength:F2}s {(job.ForceFixed ? " fixed" : "")}" ); if (cmd.Master.EstimateOnly) return []; @@ -134,7 +133,7 @@ static partial class Program ( Job : job, Info: info, - OutputFileName : BuildOutputFileName(job.OutputFolder, job.Mask, i), + OutputFileName : BuildOutputFileName(job, i), SegmentIndex : i, TotalSegments : segments, SegmentStart : i * segmentLength, @@ -261,31 +260,20 @@ static partial class Program } } - static string BuildOutputFileName(string folder, string mask, int index) + static string BuildOutputFileName(SingleJob job, int index) { string fileName; - if (mask.Contains("%03d")) - { - fileName = string.Format(mask.Replace("%03d", "{0:000}"), index); - } - else if (mask.Contains("%02d")) - { - fileName = string.Format(mask.Replace("%02d", "{0:00}"), index); - } - else if (mask.Contains("%d")) - { - fileName = string.Format(mask.Replace("%d", "{0}"), index); - } - else - { - // If no placeholder, append index - var name = Path.GetFileNameWithoutExtension(mask); - var ext = Path.GetExtension(mask); - fileName = $"{name}_{index:000}{ext}"; - } + fileName = Path.GetFileName(job.Mask ?? "[NAME]_seg[NN].[EXT]") + .Replace("[NAME]", Path.GetFileNameWithoutExtension(job.InputFile)) + .Replace("[N]" , index.ToString()) + .Replace("[NN]" , index.ToString("00")) + .Replace("[NNN]" , index.ToString("000")) + .Replace("[NNNN]", index.ToString("0000")) + .Replace("[EXT]" , Path.GetExtension(job.InputFile).TrimStart('.')) + ; - return Path.Combine(folder, fileName); + return Path.Combine(job.OutputFolder, fileName); }