Compare commits

..

No commits in common. "52856d9dd93b13f40c8578533da70a3ac558b845" and "3fa068e48beea13257b27e6bfbc9b714c21f1a93" have entirely different histories.

3 changed files with 47 additions and 83 deletions

View File

@ -2,7 +2,6 @@
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;
@ -53,7 +52,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.OutputFolder) && Jobs.Length > 0; public bool IsValid => !string.IsNullOrEmpty(Master.InputFile) && !string.IsNullOrEmpty(Master.OutputFolder) && Jobs.Length > 0;
public CommandLine(string[] args) public CommandLine(string[] args)
{ {
@ -80,33 +79,24 @@ public sealed class CommandLine
if (args.Length < 1) if (args.Length < 1)
{ {
Console.WriteLine("Missing required parameters.");
PrintHelp(); PrintHelp();
Console.WriteLine("[ERROR]: Missing required parameters.");
return; return;
} }
var inputFiles = new List<string>(); Master.InputFile = args[0];
var hasOutputFolder = args.Length > 1 && !args[1].StartsWith("-");
foreach (var arg in args) 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))
{ {
if (!arg.StartsWith("-")) if (arg.StartsWith("--mask="))
{
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();
@ -179,8 +169,7 @@ public sealed class CommandLine
} }
} }
var files = inputFiles.SelectMany(x => FileMaskExpander.Expand(x)); var files = FileMaskExpander.Expand(Master.InputFile);
Jobs = files.Select(x => new SingleJob Jobs = files.Select(x => new SingleJob
{ {
InputFile = x, InputFile = x,
@ -199,36 +188,6 @@ 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)
{
PrintHelp();
Console.WriteLine("[ERROR]:No valid input files found.");
return;
}
if (string.IsNullOrWhiteSpace(Master.OutputFolder))
{
var firstInput = Jobs[0].InputFile;
Master.OutputFolder = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(firstInput))!, "Splitter");
Console.WriteLine($"Using default output folder: {Master.OutputFolder}");
}
}
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)
@ -327,18 +286,12 @@ public sealed class CommandLine
{ {
Console.WriteLine(@" Console.WriteLine(@"
Usage: Usage:
splitter [<input.mp4> ...] [options] [--] <ffmpeg passthrough> splitter <input.mp4> <output_folder> [options] [--] <ffmpeg passthrough>
Options: Options:
--out=<folder> Output folder for segments.
Default: same folder as input video + ""Splitter"".
--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: [NAME]_seg[NN].[EXT] Default: <OriginalName>_Seg%03d.mp4
Supports [NAME], [N], [NN], [NNN], [NNNN], [EXT] placeholders. Supports %03d or %d for segment index.
--duration=<value> Override target segment duration. --duration=<value> Override target segment duration.
Accepted formats: Accepted formats:
@ -352,7 +305,7 @@ Options:
--duration=45 --duration=45
Without --force: Without --force:
Default: maximum of 58 seconds, but segments are equalized so all have same length. Segments are equalized so all have same length.
--force Use fixed segment duration exactly as given. --force Use fixed segment duration exactly as given.
Last segment may be shorter. Last segment may be shorter.
@ -397,13 +350,12 @@ 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 splitter vertical-video.mp4 out/
splitter vertical-video.mp4 --duration=90s splitter vertical-video.mp4 out/ --duration=90s
splitter vertical-video.mp4 --duration=2m30s --mask=""[NAME]_[NNNN].mp4"" splitter vertical-video.mp4 out/ --duration=2m30s --mask=""Part%03d.mp4""
splitter vertical-video.mp4 --estimate splitter vertical-video.mp4 out/ --estimate
splitter vertical-video.mp4 --force --duration=45 -- -an -sn splitter vertical-video.mp4 out/ --force --duration=45 -- -an -sn
splitter horizontal-video.mp4 --out=Cropped/ --crop splitter horizontal-video.mp4 out/ --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 [Path.GetFullPath(input)]; return new[] { 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,6 +78,8 @@ 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)
{ {
@ -103,8 +105,7 @@ 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," + 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" : "")}" );
$" 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 [];
@ -133,7 +134,7 @@ static partial class Program
( (
Job : job, Job : job,
Info: info, Info: info,
OutputFileName : BuildOutputFileName(job, i), OutputFileName : BuildOutputFileName(job.OutputFolder, job.Mask, i),
SegmentIndex : i, SegmentIndex : i,
TotalSegments : segments, TotalSegments : segments,
SegmentStart : i * segmentLength, SegmentStart : i * segmentLength,
@ -260,20 +261,31 @@ static partial class Program
} }
} }
static string BuildOutputFileName(SingleJob job, int index) static string BuildOutputFileName(string folder, string mask, int index)
{ {
string fileName; string fileName;
fileName = Path.GetFileName(job.Mask ?? "[NAME]_seg[NN].[EXT]") if (mask.Contains("%03d"))
.Replace("[NAME]", Path.GetFileNameWithoutExtension(job.InputFile)) {
.Replace("[N]" , index.ToString()) fileName = string.Format(mask.Replace("%03d", "{0:000}"), index);
.Replace("[NN]" , index.ToString("00")) }
.Replace("[NNN]" , index.ToString("000")) else if (mask.Contains("%02d"))
.Replace("[NNNN]", index.ToString("0000")) {
.Replace("[EXT]" , Path.GetExtension(job.InputFile).TrimStart('.')) 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}";
}
return Path.Combine(job.OutputFolder, fileName); return Path.Combine(folder, fileName);
} }