Loading file list (or file masks) from file added.

This commit is contained in:
Alexander Shabarshov 2026-05-14 22:36:05 +01:00
parent 3fa068e48b
commit 7dc1771326
3 changed files with 78 additions and 45 deletions

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using OpenCvSharp;
namespace splitter; namespace splitter;
@ -52,7 +53,7 @@ public sealed class CommandLine
public SingleJob Master { get; } = new SingleJob(); public SingleJob Master { get; } = new SingleJob();
public SingleJob[] Jobs { get; } 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) public CommandLine(string[] args)
{ {
@ -84,19 +85,28 @@ public sealed class CommandLine
return; return;
} }
Master.InputFile = args[0]; var inputFiles = new List<string>();
var hasOutputFolder = args.Length > 1 && !args[1].StartsWith("-");
if (hasOutputFolder) foreach (var arg in args)
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))
{ {
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); Master.Mask = arg.Substring("--mask=".Length);
} }
else if (arg.StartsWith("--out="))
{
Master.OutputFolder = arg.Substring("--out=".Length);
}
else if (arg.StartsWith("--detect=")) else if (arg.StartsWith("--detect="))
{ {
Master.Detect = arg.Substring("--detect=".Length).ToLowerInvariant(); 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 Jobs = files.Select(x => new SingleJob
{ {
InputFile = x, InputFile = x,
@ -188,6 +199,36 @@ public sealed class CommandLine
Rotate = Master.Rotate, Rotate = Master.Rotate,
Parameters = new Dictionary<string, string>(Master.Parameters) Parameters = new Dictionary<string, string>(Master.Parameters)
}).ToArray(); }).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<string> 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) private static bool TryParseParameter(string spec, out string key, out string value)
@ -286,12 +327,15 @@ public sealed class CommandLine
{ {
Console.WriteLine(@" Console.WriteLine(@"
Usage: Usage:
splitter <input.mp4> <output_folder> [options] [--] <ffmpeg passthrough> splitter [<input.mp4> ...] [options] [--] <ffmpeg passthrough>
Options: Options:
--file=<path> Input names or file masks (e.g. ""videos/*.mp4"").
If not specified, the first non-option argument is used as input.
--mask=<pattern> Output filename pattern. --mask=<pattern> Output filename pattern.
Default: <OriginalName>_Seg%03d.mp4 Default: [NAME]_seg[NN].[EXT]
Supports %03d or %d for segment index. Supports [NAME], [N], [NN], [NNN], [NNNN], [EXT] placeholders.
--duration=<value> Override target segment duration. --duration=<value> Override target segment duration.
Accepted formats: 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. 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: Examples:
splitter vertical-video.mp4 out/ splitter vertical-video.mp4 --out=out/
splitter vertical-video.mp4 out/ --duration=90s splitter vertical-video.mp4 --duration=90s
splitter vertical-video.mp4 out/ --duration=2m30s --mask=""Part%03d.mp4"" splitter vertical-video.mp4 --duration=2m30s --mask=""[NAME]_[NNNN].mp4""
splitter vertical-video.mp4 out/ --estimate splitter vertical-video.mp4 --estimate
splitter vertical-video.mp4 out/ --force --duration=45 -- -an -sn splitter vertical-video.mp4 --force --duration=45 -- -an -sn
splitter horizontal-video.mp4 out/ --crop splitter horizontal-video.mp4 --out=Cropped/ --crop
splitter --file=file_names.txt --out=Cropped/ --crop --detect=body
Description: Description:
Splits a video into equal or fixed-length segments using multi-threaded Splits a video into equal or fixed-length segments using multi-threaded

View File

@ -6,7 +6,7 @@ public static class FileMaskExpander
{ {
// If no mask, return the single full path // If no mask, return the single full path
if (!HasMask(input)) if (!HasMask(input))
return new[] { Path.GetFullPath(input) }; return [Path.GetFullPath(input)];
string directory = Path.GetDirectoryName(input) ?? Directory.GetCurrentDirectory(); string directory = Path.GetDirectoryName(input) ?? Directory.GetCurrentDirectory();
string pattern = Path.GetFileName(input); string pattern = Path.GetFileName(input);

View File

@ -78,8 +78,6 @@ static partial class Program
if (!Directory.Exists(job.OutputFolder)) if (!Directory.Exists(job.OutputFolder))
Directory.CreateDirectory(job.OutputFolder); Directory.CreateDirectory(job.OutputFolder);
job.Mask ??= $"{baseName}_seg%03d.mp4";
var info = ProbeVideo.Probe(job.InputFile); var info = ProbeVideo.Probe(job.InputFile);
if (info.Duration <= 0) if (info.Duration <= 0)
{ {
@ -105,7 +103,8 @@ static partial class Program
segmentLength = info.Duration / segments; 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) if (cmd.Master.EstimateOnly)
return []; return [];
@ -134,7 +133,7 @@ static partial class Program
( (
Job : job, Job : job,
Info: info, Info: info,
OutputFileName : BuildOutputFileName(job.OutputFolder, job.Mask, i), OutputFileName : BuildOutputFileName(job, i),
SegmentIndex : i, SegmentIndex : i,
TotalSegments : segments, TotalSegments : segments,
SegmentStart : i * segmentLength, 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; string fileName;
if (mask.Contains("%03d")) fileName = Path.GetFileName(job.Mask ?? "[NAME]_seg[NN].[EXT]")
{ .Replace("[NAME]", Path.GetFileNameWithoutExtension(job.InputFile))
fileName = string.Format(mask.Replace("%03d", "{0:000}"), index); .Replace("[N]" , index.ToString())
} .Replace("[NN]" , index.ToString("00"))
else if (mask.Contains("%02d")) .Replace("[NNN]" , index.ToString("000"))
{ .Replace("[NNNN]", index.ToString("0000"))
fileName = string.Format(mask.Replace("%02d", "{0:00}"), index); .Replace("[EXT]" , Path.GetExtension(job.InputFile).TrimStart('.'))
} ;
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}";
}
return Path.Combine(folder, fileName); return Path.Combine(job.OutputFolder, fileName);
} }