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.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)
|
||||||
{
|
{
|
||||||
@ -79,24 +80,33 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
@ -286,12 +327,18 @@ 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:
|
||||||
|
--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: <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:
|
||||||
@ -305,7 +352,7 @@ Options:
|
|||||||
--duration=45
|
--duration=45
|
||||||
|
|
||||||
Without --force:
|
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.
|
--force Use fixed segment duration exactly as given.
|
||||||
Last segment may be shorter.
|
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.
|
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
|
||||||
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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
38
splitter.cs
38
splitter.cs
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user