mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Compare commits
2 Commits
3fa068e48b
...
52856d9dd9
| Author | SHA1 | Date | |
|---|---|---|---|
| 52856d9dd9 | |||
| 7dc1771326 |
@ -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)
|
||||
{
|
||||
@ -79,24 +80,33 @@ public sealed class CommandLine
|
||||
|
||||
if (args.Length < 1)
|
||||
{
|
||||
Console.WriteLine("Missing required parameters.");
|
||||
PrintHelp();
|
||||
Console.WriteLine("[ERROR]: Missing required parameters.");
|
||||
return;
|
||||
}
|
||||
|
||||
Master.InputFile = args[0];
|
||||
var hasOutputFolder = args.Length > 1 && !args[1].StartsWith("-");
|
||||
var inputFiles = new List<string>();
|
||||
|
||||
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<string, string>(Master.Parameters)
|
||||
}).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)
|
||||
@ -286,12 +327,18 @@ public sealed class CommandLine
|
||||
{
|
||||
Console.WriteLine(@"
|
||||
Usage:
|
||||
splitter <input.mp4> <output_folder> [options] [--] <ffmpeg passthrough>
|
||||
splitter [<input.mp4> ...] [options] [--] <ffmpeg passthrough>
|
||||
|
||||
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.
|
||||
Default: <OriginalName>_Seg%03d.mp4
|
||||
Supports %03d or %d for segment index.
|
||||
Default: [NAME]_seg[NN].[EXT]
|
||||
Supports [NAME], [N], [NN], [NNN], [NNNN], [EXT] placeholders.
|
||||
|
||||
--duration=<value> Override target segment duration.
|
||||
Accepted formats:
|
||||
@ -305,7 +352,7 @@ Options:
|
||||
--duration=45
|
||||
|
||||
Without --force:
|
||||
Segments are equalized so all have same length.
|
||||
Default: maximum of 58 seconds, but segments are equalized so all have same length.
|
||||
|
||||
--force Use fixed segment duration exactly as given.
|
||||
Last segment may be shorter.
|
||||
@ -350,12 +397,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
|
||||
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
|
||||
|
||||
@ -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);
|
||||
|
||||
38
splitter.cs
38
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user