From a3e133892900641c931ac3eb75bed429b0d43163 Mon Sep 17 00:00:00 2001 From: Scrub <72096833+ScrubN@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:14:07 -0400 Subject: [PATCH] Replace CLI seconds with FFmpeg-style time duration parsing (#1032) * Create Time struct for parsing various time strings This enables the use of "12", "12s", "12000ms", "0:00:12", "0.2m" to get a value of 12 seconds * Create tests * Implement Time in CLI * I hate working with CommandLineParser * Update documentation * Use decimal instead of double to avoid rounding errors * Rename Time -> TimeDuration * Add more tests --- TwitchDownloaderCLI.Tests/GlobalUsings.cs | 1 + .../ModelTests/TimeDurationTests.cs | 42 +++++++++ .../TwitchDownloaderCLI.Tests.csproj | 27 ++++++ TwitchDownloaderCLI/Models/TimeDuration.cs | 91 +++++++++++++++++++ .../Modes/Arguments/ChatDownloadArgs.cs | 9 +- .../Modes/Arguments/ChatRenderArgs.cs | 9 +- .../Modes/Arguments/ChatUpdateArgs.cs | 9 +- .../Modes/Arguments/VideoDownloadArgs.cs | 9 +- TwitchDownloaderCLI/Modes/DownloadChat.cs | 8 +- TwitchDownloaderCLI/Modes/DownloadVideo.cs | 8 +- TwitchDownloaderCLI/Modes/RenderChat.cs | 4 +- TwitchDownloaderCLI/Modes/UpdateChat.cs | 8 +- TwitchDownloaderCLI/README.md | 47 +++++++--- TwitchDownloaderWPF.sln | 10 ++ 14 files changed, 241 insertions(+), 41 deletions(-) create mode 100644 TwitchDownloaderCLI.Tests/GlobalUsings.cs create mode 100644 TwitchDownloaderCLI.Tests/ModelTests/TimeDurationTests.cs create mode 100644 TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj create mode 100644 TwitchDownloaderCLI/Models/TimeDuration.cs diff --git a/TwitchDownloaderCLI.Tests/GlobalUsings.cs b/TwitchDownloaderCLI.Tests/GlobalUsings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/TwitchDownloaderCLI.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/TwitchDownloaderCLI.Tests/ModelTests/TimeDurationTests.cs b/TwitchDownloaderCLI.Tests/ModelTests/TimeDurationTests.cs new file mode 100644 index 00000000..6a320f7f --- /dev/null +++ b/TwitchDownloaderCLI.Tests/ModelTests/TimeDurationTests.cs @@ -0,0 +1,42 @@ +using TwitchDownloaderCLI.Models; +using static System.TimeSpan; + +namespace TwitchDownloaderCLI.Tests.ModelTests +{ + public class TimeDurationTests + { + [Theory] + [InlineData("200ms", 200 * TicksPerMillisecond)] + [InlineData("55", 55 * TicksPerSecond)] + [InlineData("0.2", 2 * TicksPerSecond / 10)] + [InlineData("23.189", 23189 * TicksPerSecond / 1000)] + [InlineData("55s", 55 * TicksPerSecond)] + [InlineData("17m", 17 * TicksPerMinute)] + [InlineData("31h", 31 * TicksPerHour)] + [InlineData("0:09:27", 9 * TicksPerMinute + 27 * TicksPerSecond)] + [InlineData("11:30", 11 * TicksPerHour + 30 * TicksPerMinute)] + [InlineData("12:03:45", 12 * TicksPerHour + 3 * TicksPerMinute + 45 * TicksPerSecond)] + public void CorrectlyParsesTimeStrings(string input, long expectedTicks) + { + var expected = new TimeDuration(expectedTicks); + + var actual = new TimeDuration(input); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("100 s")] + [InlineData("123d")] + [InlineData("0:12345")] + public void ThrowsOnBadFormat(string input) + { + Assert.ThrowsAny(() => + { + _ = TimeDuration.Parse(input); + }); + } + } +} \ No newline at end of file diff --git a/TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj b/TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj new file mode 100644 index 00000000..89b7f8fd --- /dev/null +++ b/TwitchDownloaderCLI.Tests/TwitchDownloaderCLI.Tests.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/TwitchDownloaderCLI/Models/TimeDuration.cs b/TwitchDownloaderCLI/Models/TimeDuration.cs new file mode 100644 index 00000000..abf7489f --- /dev/null +++ b/TwitchDownloaderCLI/Models/TimeDuration.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace TwitchDownloaderCLI.Models +{ + [DebuggerDisplay("{_timeSpan}")] + public readonly record struct TimeDuration + { + private readonly TimeSpan _timeSpan; + + /// + /// Constructor used by CommandLineParser + /// + public TimeDuration(string str) + { + this = Parse(str); + } + + public TimeDuration(TimeSpan timeSpan) + { + _timeSpan = timeSpan; + } + + public TimeDuration(long ticks) + { + _timeSpan = TimeSpan.FromTicks(ticks); + } + + public static TimeDuration Parse(string str) + { + if (string.IsNullOrWhiteSpace(str)) + { + throw new FormatException(); + } + + if (str.Contains(':')) + { + var timeSpan = TimeSpan.Parse(str); + return new TimeDuration(timeSpan); + } + + var multiplier = GetMultiplier(str, out var span); + if (decimal.TryParse(span, NumberStyles.Number, null, out var result)) + { + var ticks = (long)(result * multiplier); + return new TimeDuration(ticks); + } + + throw new FormatException(); + } + + private static long GetMultiplier(string input, out ReadOnlySpan trimmedInput) + { + if (char.IsDigit(input[^1])) + { + trimmedInput = input.AsSpan(); + return TimeSpan.TicksPerSecond; + } + + if (Regex.IsMatch(input, @"\dms$", RegexOptions.RightToLeft)) + { + trimmedInput = input.AsSpan()[..^2]; + return TimeSpan.TicksPerMillisecond; + } + + if (Regex.IsMatch(input, @"\ds$", RegexOptions.RightToLeft)) + { + trimmedInput = input.AsSpan()[..^1]; + return TimeSpan.TicksPerSecond; + } + + if (Regex.IsMatch(input, @"\dm$", RegexOptions.RightToLeft)) + { + trimmedInput = input.AsSpan()[..^1]; + return TimeSpan.TicksPerMinute; + } + + if (Regex.IsMatch(input, @"\dh$", RegexOptions.RightToLeft)) + { + trimmedInput = input.AsSpan()[..^1]; + return TimeSpan.TicksPerHour; + } + + throw new FormatException(); + } + + public static implicit operator TimeSpan(TimeDuration timeDuration) => timeDuration._timeSpan; + } +} \ No newline at end of file diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs index 6ac13d2c..15a3ee30 100644 --- a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs +++ b/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs @@ -1,4 +1,5 @@ using CommandLine; +using TwitchDownloaderCLI.Models; using TwitchDownloaderCore.Tools; namespace TwitchDownloaderCLI.Modes.Arguments @@ -15,11 +16,11 @@ internal sealed class ChatDownloadArgs : TwitchDownloaderArgs [Option("compression", Default = ChatCompression.None, HelpText = "Compresses an output json chat file using a specified compression, usually resulting in 40-90% size reductions. Valid values are: None, Gzip.")] public ChatCompression Compression { get; set; } - [Option('b', "beginning", HelpText = "Time in seconds to crop beginning.")] - public double CropBeginningTime { get; set; } + [Option('b', "beginning", HelpText = "Time to crop beginning. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##).")] + public TimeDuration CropBeginningTime { get; set; } - [Option('e', "ending", HelpText = "Time in seconds to crop ending.")] - public double CropEndingTime { get; set; } + [Option('e', "ending", HelpText = "Time to crop ending. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##).")] + public TimeDuration CropEndingTime { get; set; } [Option('E', "embed-images", Default = false, HelpText = "Embed first party emotes, badges, and cheermotes into the chat download for offline rendering.")] public bool EmbedData { get; set; } diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs index 15704583..12cc5422 100644 --- a/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs +++ b/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs @@ -1,4 +1,5 @@ using CommandLine; +using TwitchDownloaderCLI.Models; namespace TwitchDownloaderCLI.Modes.Arguments { @@ -26,11 +27,11 @@ internal sealed class ChatRenderArgs : TwitchDownloaderArgs [Option('h', "chat-height", Default = 600, HelpText = "Height of chat render.")] public int ChatHeight { get; set; } - [Option('b', "beginning", Default = -1, HelpText = "Time in seconds to crop beginning of the render.")] - public int CropBeginningTime { get; set; } + [Option('b', "beginning", HelpText = "Time to crop beginning of the render. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##).")] + public TimeDuration CropBeginningTime { get; set; } = new(-1); - [Option('e', "ending", Default = -1, HelpText = "Time in seconds to crop ending of the render.")] - public int CropEndingTime { get; set; } + [Option('e', "ending", HelpText = "Time to crop ending of the render. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##).")] + public TimeDuration CropEndingTime { get; set; } = new(-1); [Option("bttv", Default = true, HelpText = "Enable BTTV emotes.")] public bool? BttvEmotes { get; set; } diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs index f26ea60b..134aa5e5 100644 --- a/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs +++ b/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs @@ -1,4 +1,5 @@ using CommandLine; +using TwitchDownloaderCLI.Models; using TwitchDownloaderCore.Tools; namespace TwitchDownloaderCLI.Modes.Arguments @@ -21,11 +22,11 @@ internal sealed class ChatUpdateArgs : TwitchDownloaderArgs [Option('R', "replace-embeds", Default = false, HelpText = "Replace all embedded emotes, badges, and cheermotes in the file. All embedded images will be overwritten!")] public bool ReplaceEmbeds { get; set; } - [Option('b', "beginning", Default = -1, HelpText = "New time in seconds for chat beginning. Comments may be added but not removed. -1 = No crop.")] - public int CropBeginningTime { get; set; } + [Option('b', "beginning", HelpText = "New time for chat beginning. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##). Comments may be added but not removed. -1 = No crop.")] + public TimeDuration CropBeginningTime { get; set; } = new(-1); - [Option('e', "ending", Default = -1, HelpText = "New time in seconds for chat ending. Comments may be added but not removed. -1 = No crop.")] - public int CropEndingTime { get; set; } + [Option('e', "ending", HelpText = "New time for chat ending. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##). Comments may be added but not removed. -1 = No crop.")] + public TimeDuration CropEndingTime { get; set; } = new(-1); [Option("bttv", Default = true, HelpText = "Enable BTTV embedding in chat download.")] public bool? BttvEmotes { get; set; } diff --git a/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs index b30d6c99..e82bda93 100644 --- a/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs +++ b/TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs @@ -1,4 +1,5 @@ using CommandLine; +using TwitchDownloaderCLI.Models; namespace TwitchDownloaderCLI.Modes.Arguments { @@ -14,11 +15,11 @@ internal sealed class VideoDownloadArgs : TwitchDownloaderArgs [Option('q', "quality", HelpText = "The quality the program will attempt to download.")] public string Quality { get; set; } - [Option('b', "beginning", HelpText = "Time in seconds to crop beginning.")] - public int CropBeginningTime { get; set; } + [Option('b', "beginning", HelpText = "Time to crop beginning. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##).")] + public TimeDuration CropBeginningTime { get; set; } - [Option('e', "ending", HelpText = "Time in seconds to crop ending.")] - public int CropEndingTime { get; set; } + [Option('e', "ending", HelpText = "Time to crop ending. Can be milliseconds (#ms), seconds (#s), minutes (#m), hours (#h), or time (##:##:##).")] + public TimeDuration CropEndingTime { get; set; } [Option('t', "threads", Default = 4, HelpText = "Number of download threads.")] public int DownloadThreads { get; set; } diff --git a/TwitchDownloaderCLI/Modes/DownloadChat.cs b/TwitchDownloaderCLI/Modes/DownloadChat.cs index 95c312ef..043c8201 100644 --- a/TwitchDownloaderCLI/Modes/DownloadChat.cs +++ b/TwitchDownloaderCLI/Modes/DownloadChat.cs @@ -49,10 +49,10 @@ private static ChatDownloadOptions GetDownloadOptions(ChatDownloadArgs inputOpti _ => throw new NotSupportedException($"{fileExtension} is not a valid chat file extension.") }, Id = vodClipIdMatch.Value, - CropBeginning = inputOptions.CropBeginningTime > 0.0, - CropBeginningTime = inputOptions.CropBeginningTime, - CropEnding = inputOptions.CropEndingTime > 0.0, - CropEndingTime = inputOptions.CropEndingTime, + CropBeginning = inputOptions.CropBeginningTime > TimeSpan.Zero, + CropBeginningTime = ((TimeSpan)inputOptions.CropBeginningTime).TotalSeconds, + CropEnding = inputOptions.CropEndingTime > TimeSpan.Zero, + CropEndingTime = ((TimeSpan)inputOptions.CropEndingTime).TotalSeconds, EmbedData = inputOptions.EmbedData, Filename = inputOptions.Compression is ChatCompression.Gzip ? inputOptions.OutputFile + ".gz" diff --git a/TwitchDownloaderCLI/Modes/DownloadVideo.cs b/TwitchDownloaderCLI/Modes/DownloadVideo.cs index 32dd77cb..8278499d 100644 --- a/TwitchDownloaderCLI/Modes/DownloadVideo.cs +++ b/TwitchDownloaderCLI/Modes/DownloadVideo.cs @@ -62,10 +62,10 @@ private static VideoDownloadOptions GetDownloadOptions(VideoDownloadArgs inputOp ".m4a" => "Audio", _ => throw new ArgumentException("Only MP4 and M4A audio files are supported.") }, - CropBeginning = inputOptions.CropBeginningTime > 0.0, - CropBeginningTime = TimeSpan.FromSeconds(inputOptions.CropBeginningTime), - CropEnding = inputOptions.CropEndingTime > 0.0, - CropEndingTime = TimeSpan.FromSeconds(inputOptions.CropEndingTime), + CropBeginning = inputOptions.CropBeginningTime > TimeSpan.Zero, + CropBeginningTime = inputOptions.CropBeginningTime, + CropEnding = inputOptions.CropEndingTime > TimeSpan.Zero, + CropEndingTime = inputOptions.CropEndingTime, FfmpegPath = string.IsNullOrWhiteSpace(inputOptions.FfmpegPath) ? FfmpegHandler.FfmpegExecutableName : Path.GetFullPath(inputOptions.FfmpegPath), TempFolder = inputOptions.TempFolder, CacheCleanerCallback = directoryInfos => diff --git a/TwitchDownloaderCLI/Modes/RenderChat.cs b/TwitchDownloaderCLI/Modes/RenderChat.cs index 87b89486..f4f78c27 100644 --- a/TwitchDownloaderCLI/Modes/RenderChat.cs +++ b/TwitchDownloaderCLI/Modes/RenderChat.cs @@ -36,8 +36,8 @@ private static ChatRenderOptions GetRenderOptions(ChatRenderArgs inputOptions, I MessageColor = SKColor.Parse(inputOptions.MessageColor), ChatHeight = inputOptions.ChatHeight, ChatWidth = inputOptions.ChatWidth, - StartOverride = inputOptions.CropBeginningTime, - EndOverride = inputOptions.CropEndingTime, + StartOverride = (int)((TimeSpan)inputOptions.CropBeginningTime).TotalSeconds, + EndOverride = (int)((TimeSpan)inputOptions.CropEndingTime).TotalSeconds, BttvEmotes = (bool)inputOptions.BttvEmotes!, FfzEmotes = (bool)inputOptions.FfzEmotes!, StvEmotes = (bool)inputOptions.StvEmotes!, diff --git a/TwitchDownloaderCLI/Modes/UpdateChat.cs b/TwitchDownloaderCLI/Modes/UpdateChat.cs index a06cca71..1c950a25 100644 --- a/TwitchDownloaderCLI/Modes/UpdateChat.cs +++ b/TwitchDownloaderCLI/Modes/UpdateChat.cs @@ -68,10 +68,10 @@ private static ChatUpdateOptions GetUpdateOptions(ChatUpdateArgs inputOptions, I OutputFormat = outFormat, EmbedMissing = inputOptions.EmbedMissing, ReplaceEmbeds = inputOptions.ReplaceEmbeds, - CropBeginning = !double.IsNegative(inputOptions.CropBeginningTime), - CropBeginningTime = inputOptions.CropBeginningTime, - CropEnding = !double.IsNegative(inputOptions.CropEndingTime), - CropEndingTime = inputOptions.CropEndingTime, + CropBeginning = inputOptions.CropBeginningTime >= TimeSpan.Zero, + CropBeginningTime = ((TimeSpan)inputOptions.CropBeginningTime).TotalSeconds, + CropEnding = inputOptions.CropEndingTime >= TimeSpan.Zero, + CropEndingTime = ((TimeSpan)inputOptions.CropEndingTime).TotalSeconds, BttvEmotes = (bool)inputOptions.BttvEmotes!, FfzEmotes = (bool)inputOptions.FfzEmotes!, StvEmotes = (bool)inputOptions.StvEmotes!, diff --git a/TwitchDownloaderCLI/README.md b/TwitchDownloaderCLI/README.md index 1729c744..ec12f73a 100644 --- a/TwitchDownloaderCLI/README.md +++ b/TwitchDownloaderCLI/README.md @@ -14,6 +14,13 @@ Also can concatenate/combine/merge Transport Stream files, either those parts do - [Arguments for mode tsmerge](#arguments-for-mode-tsmerge) - [Example Commands](#example-commands) - [Additional Notes](#additional-notes) + - [ID parsing](#id-parsing) + - [String arguments](#string-arguments) + - [Boolean flags](#boolean-flags) + - [Enum flag arguments](#enum-flag-arguments) + - [Time durations](#time-durations) + - [Rendering prerequisites](#rendering-prerequisites) + - [TSMerge notes](#tsmerge-notes) --- @@ -39,12 +46,10 @@ File the program will output to. File extension will be used to determine downlo The quality that the program will attempt to download, for example "1080p60". If not found, the highest quality stream will be downloaded. **-b / --beginning** -Time in seconds to trim beginning. For example if I had a 10 second stream but only wanted the last 7 seconds of it I would use `-b 3` to skip the first 3 seconds. +Time to trim beginning. See [Time durations](#time-durations) for a more detailed explanation. **-e / --ending** -Time in seconds to trim ending. For example if I had a 10 second stream but only wanted the first 4 seconds of it I would use `-e 4` to end on the 4th second. - -Extra example, if I wanted only seconds 3-6 in a 10 second stream I would do `-b 3 -e 6` +Time to trim ending. See [Time durations](#time-durations) for a more detailed explanation. **-t / --threads** (Default: `4`) Number of download threads. @@ -98,10 +103,10 @@ File the program will output to. File extension will be used to determine downlo (Default: `None`) Compresses an output json chat file using a specified compression, usually resulting in 40-90% size reductions. Valid values are: `None`, `Gzip`. More formats will be supported in the future. **-b / --beginning** -Time in seconds to trim beginning. For example if I had a 10 second stream but only wanted the last 7 seconds of it I would use `-b 3` to skip the first 3 seconds. +Time to trim beginning. See [Time durations](#time-durations) for a more detailed explanation. **-e / --ending** -Time in seconds to trim ending. For example if I had a 10 second stream but only wanted the first 4 seconds of it I would use `-e 4` to end on the 4th second. +Time to trim ending. See [Time durations](#time-durations) for a more detailed explanation. **-E / --embed-images** (Default: `false`) Embed first party emotes, badges, and cheermotes into the download file for offline rendering. Useful for archival purposes, file size will be larger. @@ -143,10 +148,10 @@ Path to output file. File extension will be used to determine new chat type. Val (Default: `false`) Replace all embedded emotes, badges, and cheermotes in the file. All embedded data will be overwritten! **b / --beginning** -(Default: `-1`) New time in seconds for chat beginning. Comments may be added but not removed. -1 = No crop. +(Default: `-1`) New time for chat beginning (`-1` = keep current crop). See [Time durations](#time-durations) for a more detailed explanation. Comments may be added but not removed. **-e / --ending** -(Default: `-1`) New time in seconds for chat beginning. Comments may be added but not removed. -1 = No crop. +(Default: `-1`) New time for chat ending (`-1` = keep current crop). See [Time durations](#time-durations) for a more detailed explanation. Comments may be added but not removed. **--bttv** (Default: `true`) Enable embedding BTTV emotes. @@ -188,10 +193,10 @@ File the program will output to. (Default: `600`) Height of chat render. **-b / --beginning** -(Default: `-1`) Time in seconds to crop the beginning of the render. +(Default: `-1`) Time to crop the beginning of the render (`-1` = keep current crop). See [Time durations](#time-durations) for a more detailed explanation. **-e / --ending** -(Default: `-1`) Time in seconds to crop the ending of the render. +(Default: `-1`) Time to crop the ending of the render (`-1` = keep current crop). See [Time durations](#time-durations) for a more detailed explanation. **--bttv** (Default: `true`) Enable BTTV emotes. @@ -347,6 +352,10 @@ Download a VOD with defaults ./TwitchDownloaderCLI videodownload --id 612942303 -o video.mp4 +Download a small portion of a VOD + + ./TwitchDownloaderCLI videodownload --id 612942303 -b 0:01:40 -e 0:03:20 -o video.mp4 + Download a Clip with defaults ./TwitchDownloaderCLI clipdownload --id NurturingCalmHamburgerVoHiYo -o clip.mp4 @@ -403,17 +412,33 @@ Print the available options for the VOD downloader ## Additional Notes +### ID parsing All `--id` inputs will accept either video/clip IDs or full video/clip URLs. i.e. `--id 612942303` or `--id https://twitch.tv/videos/612942303`. +### String arguments String arguments that contain spaces should be wrapped in either single quotes ' or double quotes " depending on your shell. i.e. `--output 'my output file.mp4'` or `--output "my output file.mp4"` +### Boolean flags Default true boolean flags must be assigned: `--default-true-flag=false`. Default false boolean flags should still be raised normally: `--default-false-flag`. -Enum flag arguments may be assigned as `--flag Value1,Value2,Value3` or `--flag "Value1, Value2, Value3"`. +### Enum flag arguments +Enum flag arguments may be assigned without spaces `--flag Value1,Value2,Value3` or with spaces when wrapped in quotes `--flag "Value1, Value2, Value3"` (see [String arguments](#string-arguments)). + +### Time durations +Time duration arguments may be formatted in milliseconds `###ms`, seconds `###s`, minutes `###m`, hours `###h`, or [time](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.parse?view=net-6.0) (i.e. `hh:mm:ss`, `hh:mm`, `dd.hh:mm:ss.ms`). +If the time duration is given as a number without a unit, seconds will be assumed. Decimals are supported. + +"Beginning" arguments set when trimming begins. For example, `--beginning 17s` will make the output start 17 seconds after the source begins. + +"Ending" arguments set when trimming ends. For example, `--ending 17s` will make the output end at 17 seconds after the source begins. + +If `--beginning 17s` and `--ending 27s` are used together, the resulting output will be of seconds `17-27` of the source and will have a duration of 10 seconds. +### Rendering prerequisites For Linux users, ensure both `fontconfig` and `libfontconfig1` are installed. `apt-get install fontconfig libfontconfig1` on Ubuntu. Some distros, like Linux Alpine, lack fonts for some languages (Arabic, Persian, Thai, etc.) If this is the case for you, install additional fonts families such as [Noto](https://fonts.google.com/noto/specimen/Noto+Sans) or check your distro's wiki page on fonts as it may have an install command for this specific scenario, such as the [Linux Alpine](https://wiki.alpinelinux.org/wiki/Fonts) font page. +### TSMerge notes The list file for `tsmerge` may contain relative or absolute paths, with one path per line. Alternatively, the list file may also be an M3U8 playlist file. \ No newline at end of file diff --git a/TwitchDownloaderWPF.sln b/TwitchDownloaderWPF.sln index 6e0ed46f..5a88d046 100644 --- a/TwitchDownloaderWPF.sln +++ b/TwitchDownloaderWPF.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchDownloaderCLI", "Twit EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchDownloaderCore.Tests", "TwitchDownloaderCore.Tests\TwitchDownloaderCore.Tests.csproj", "{FE8F0DC2-6750-4956-9208-9CEE9B524184}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchDownloaderCLI.Tests", "TwitchDownloaderCLI.Tests\TwitchDownloaderCLI.Tests.csproj", "{2AC32E05-56EE-41E2-BAFD-94C3FE900057}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,14 @@ Global {FE8F0DC2-6750-4956-9208-9CEE9B524184}.Release|Any CPU.Build.0 = Release|Any CPU {FE8F0DC2-6750-4956-9208-9CEE9B524184}.Release|x64.ActiveCfg = Release|Any CPU {FE8F0DC2-6750-4956-9208-9CEE9B524184}.Release|x64.Build.0 = Release|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Debug|x64.ActiveCfg = Debug|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Debug|x64.Build.0 = Debug|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Release|Any CPU.Build.0 = Release|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Release|x64.ActiveCfg = Release|Any CPU + {2AC32E05-56EE-41E2-BAFD-94C3FE900057}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE