From 3d78e952707205e72684e96e8fefd7298cb8bf73 Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Mon, 5 Feb 2024 21:51:18 +0200
Subject: [PATCH 1/8] chore(Settings): expanded description
- Added video codec information
- Added resolution information
---
CommandLineApp/Models/Settings.cs | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/CommandLineApp/Models/Settings.cs b/CommandLineApp/Models/Settings.cs
index f0e4453..1ca2a20 100644
--- a/CommandLineApp/Models/Settings.cs
+++ b/CommandLineApp/Models/Settings.cs
@@ -23,10 +23,15 @@ You should use a sane value between 22 and 38
""")]
[CommandOption("-c|--crf")]
[DefaultValue(25)]
- public int Crf { get; init; }
+ public int Crf { get; set; }
+ [Description("""
+ Resolution to be used for when compressing the video.
+ Available resolutions:
+ 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p
+ """)]
[CommandOption("-r|--resolution")]
- public string? Resolution { get; init; }
+ public string? Resolution { get; set; }
[Description("""
Trim input
@@ -36,7 +41,12 @@ Trim input
[CommandOption("-t|--trim")]
public string? Trim { get; init; }
- [Description("Video codec")]
+ [Description("""
+ Avaliable codecs:
+ h264, libx264 - default
+ vp9, libvpx-vp9 - VP9 (Webm) is recommended for 1080p
+ av1, libaom-av1 - AV1 is recommended for 4K
+ """)]
[CommandOption("--video-codec")]
public string? VideoCodec { get; init; }
From e2571a3324b198ba53bb7b2439ddfb9e32ace35a Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Mon, 5 Feb 2024 21:52:17 +0200
Subject: [PATCH 2/8] feat(Globals): add ValidResolutions array
---
Globals.cs | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/Globals.cs b/Globals.cs
index cd477f0..7701de4 100644
--- a/Globals.cs
+++ b/Globals.cs
@@ -13,6 +13,20 @@ public sealed class Globals
OverwriteFiles = false
};
+ ///
+ /// Lists of valid video resolutions.
+ ///
+ public readonly IEnumerable ValidResolutions = [
+ "144p",
+ "240p",
+ "360p",
+ "480p",
+ "720p",
+ "1080p",
+ "1440p",
+ "2160p"
+ ];
+
///
/// This list maintains the paths to videos that have been temporarily downloaded.
/// These paths are stored for future deletion of the corresponding files.
@@ -27,4 +41,4 @@ public sealed class Globals
{ ["vp9", "libvpx-vp9"], VideoCodec.vp9 },
{ ["av1", "libaom-av1"], VideoCodec.av1 }
};
-}
\ No newline at end of file
+}
From b095f7cb88a43c68441875f3fa3b3521518fa0ef Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Mon, 5 Feb 2024 21:56:53 +0200
Subject: [PATCH 3/8] refactor(RootCommand): use globals
- Use ValidResolutions to check resoluions
- Use VideoCodecs to check video codecs
---
CommandLineApp/RootCommand.cs | 29 +++++++----------------------
1 file changed, 7 insertions(+), 22 deletions(-)
diff --git a/CommandLineApp/RootCommand.cs b/CommandLineApp/RootCommand.cs
index 153213f..69c3a41 100644
--- a/CommandLineApp/RootCommand.cs
+++ b/CommandLineApp/RootCommand.cs
@@ -142,20 +142,9 @@ private void ValidateTrim(string? trim)
private void ValidateResolution(string? resolution)
{
var hasResolution = resolution is not null;
- if (!hasResolution) return;
+ if (hasResolution is false) return;
- var validResolution = resolution switch
- {
- "144p" => true,
- "240p" => true,
- "360p" => true,
- "480p" => true,
- "720p" => true,
- "1080p" => true,
- "1440p" => true,
- "2160p" => true,
- _ => false
- };
+ var validResolution = globals.ValidResolutions.Contains(resolution);
if (validResolution is false)
ValidationResult.Error("Invalid resolution");
}
@@ -165,14 +154,10 @@ private void ValidateVideoCodec(string? videoCodec)
var hasVideoCodec = videoCodec is not null;
if (!hasVideoCodec) return;
- var validVideoCodec = videoCodec switch
- {
- "h264" => true,
- "h265" => true,
- "vp8" => true,
- "vp9" => true,
- _ => false
- };
+ var validVideoCodec = globals.VideoCodecs.Values
+ .Any(codec => codec.ToString()
+ .Equals(videoCodec,
+ StringComparison.InvariantCultureIgnoreCase));
if (validVideoCodec is false)
ValidationResult.Error("Invalid video codec");
}
@@ -218,7 +203,7 @@ public override async Task ExecuteAsync(CommandContext context, Settings se
private async Task Download(IEnumerable links, Dictionary videos, Settings options)
{
var list = links.ToList();
- if (list.Count == 0)
+ if (list.Count is 0)
return;
foreach (var downloadOptions in list.Select(link => new DownloadOptions(link, options)))
From e152e4864694e2209f3ba3ea8d59d5e20532071b Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Mon, 5 Feb 2024 21:58:10 +0200
Subject: [PATCH 4/8] chore: add EOF
---
CommandLineApp/Conversion/StreamConfigurator.cs | 2 +-
CommandLineApp/Models/DownloadResult.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/CommandLineApp/Conversion/StreamConfigurator.cs b/CommandLineApp/Conversion/StreamConfigurator.cs
index f0992ba..3172e5a 100644
--- a/CommandLineApp/Conversion/StreamConfigurator.cs
+++ b/CommandLineApp/Conversion/StreamConfigurator.cs
@@ -61,4 +61,4 @@ public void SetCpuForAv1(IConversion conversion, double framerate)
public void SetVp9Args(IConversion conversion)
=> conversion.AddParameter(string.Join(" ", _vp9Args));
-}
\ No newline at end of file
+}
diff --git a/CommandLineApp/Models/DownloadResult.cs b/CommandLineApp/Models/DownloadResult.cs
index c66355c..0454ba9 100644
--- a/CommandLineApp/Models/DownloadResult.cs
+++ b/CommandLineApp/Models/DownloadResult.cs
@@ -1,3 +1,3 @@
namespace dis.CommandLineApp.Models;
-public sealed record DownloadResult(string? OutPath, DateTime? Date);
\ No newline at end of file
+public sealed record DownloadResult(string? OutPath, DateTime? Date);
From 5fe6c2bda3dd0ed390b31c5ece9058ff081f68bf Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Mon, 5 Feb 2024 22:00:04 +0200
Subject: [PATCH 5/8] refactor: simplify start and end time parsing
---
.../Downloaders/VideoDownloaderBase.cs | 29 ++-----------------
1 file changed, 2 insertions(+), 27 deletions(-)
diff --git a/CommandLineApp/Downloaders/VideoDownloaderBase.cs b/CommandLineApp/Downloaders/VideoDownloaderBase.cs
index ad63c97..b847b50 100644
--- a/CommandLineApp/Downloaders/VideoDownloaderBase.cs
+++ b/CommandLineApp/Downloaders/VideoDownloaderBase.cs
@@ -111,7 +111,8 @@ private bool ValidTimeRange(RunResult fetch)
{
// check if the time range is beyond the video length
var split = Query.OptionSet.DownloadSections!.Values[0];
- var (start, end) = ParseStartAndEndTime(split);
+ var start = float.Parse(split.Split('-')[0].TrimStart('*'));
+ var end = float.Parse(split.Split('-')[1]);
var duration = fetch.Data.Duration;
@@ -132,30 +133,4 @@ private bool ValidTimeRange(RunResult fetch)
_logger.Error(TrimTimeError);
return false;
}
-
- ///
- /// Extracts the start and end times from a given download section string.
- ///
- /// The string containing the download section details.
- /// A tuple containing the start and end times as floats.
- private static (float, float) ParseStartAndEndTime(string downloadSection)
- {
- /*
- * The download section string is split into two parts using '-' as a separator.
- * The '*' character, which is used as a regex symbol for "yt-dlp",
- * it is not relevant for our parsing and is therefore removed.
- * The start time is always on the left side of the split, and the end time is always on the right side.
- */
-
- var span = downloadSection.AsSpan();
- var separatorIndex = span.IndexOf('-');
-
- var startSpan = span[..separatorIndex].Trim('*');
- var endSpan = span[(separatorIndex + 1)..];
-
- var start = float.Parse(startSpan);
- var end = float.Parse(endSpan);
-
- return (start, end);
- }
}
From 224af7b7324506038e4ba0ab02b913fa8b4831fe Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Mon, 5 Feb 2024 22:09:13 +0200
Subject: [PATCH 6/8] refactor: improve conversion process configuration and
handling
- Now we take `IList` instead of `IEnumerable`, eliminating the need to convert to a list within the method
- Fix inappopriate usage of output file extension for VP9 & AV1
- AV1 using `yuv420p10le` instead of `yuv420`
---
CommandLineApp/Conversion/ProcessHandler.cs | 35 ++++++++++++++-------
1 file changed, 23 insertions(+), 12 deletions(-)
diff --git a/CommandLineApp/Conversion/ProcessHandler.cs b/CommandLineApp/Conversion/ProcessHandler.cs
index c8924e6..0862278 100644
--- a/CommandLineApp/Conversion/ProcessHandler.cs
+++ b/CommandLineApp/Conversion/ProcessHandler.cs
@@ -15,11 +15,10 @@ public void SetTimeStamps(string path, DateTime date)
File.SetLastAccessTime(path, date);
}
- public IConversion? ConfigureConversion(Settings o, IEnumerable streams, string outP)
+ public IConversion? ConfigureConversion(Settings o, IList streams, string outP)
{
- var listOfStreams = streams.ToList();
- var videoStream = listOfStreams.OfType().FirstOrDefault();
- var audioStream = listOfStreams.OfType().FirstOrDefault();
+ var videoStream = streams.OfType().FirstOrDefault();
+ var audioStream = streams.OfType().FirstOrDefault();
if (videoStream is null && audioStream is null)
{
@@ -29,11 +28,9 @@ public void SetTimeStamps(string path, DateTime date)
var parameters = $"-crf {o.Crf}";
var conversion = FFmpeg.Conversions.New()
- .SetPreset(ConversionPreset.VerySlow)
+ .AddParameter(parameters)
.SetPixelFormat(PixelFormat.yuv420p)
- .SetOutput(outP)
- .UseMultiThread(o.MultiThread)
- .AddParameter(parameters);
+ .SetPreset(ConversionPreset.VerySlow);
var videoCodec = codecParser.GetCodec(o.VideoCodec);
@@ -44,17 +41,31 @@ public void SetTimeStamps(string path, DateTime date)
switch (videoCodec)
{
case VideoCodec.vp9:
- configurator.SetVp9Args(conversion);
- break;
+ {
+ configurator.SetVp9Args(conversion);
+ outP = Path.ChangeExtension(outP, "webm");
+ break;
+ }
case VideoCodec.av1:
- configurator.SetCpuForAv1(conversion, videoStream.Framerate);
- break;
+ {
+ configurator.SetCpuForAv1(conversion, videoStream.Framerate);
+ outP = Path.ChangeExtension(outP, "webm");
+ conversion.SetPixelFormat(PixelFormat.yuv420p10le);
+ break;
+ }
+ default:
+ {
+ conversion.UseMultiThread(o.MultiThread);
+ break;
+ }
}
if (string.IsNullOrEmpty(o.Resolution) is false)
configurator.SetResolution(videoStream, o.Resolution);
}
+ conversion.SetOutput(outP);
+
if (audioStream is null)
return conversion;
From 3216880a2e7df96b738936dba3eb72b541d4dfb6 Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Tue, 6 Feb 2024 17:55:16 +0200
Subject: [PATCH 7/8] feat: introduce adaptive retry mechanism for compression
- Implement an interactive retry mechanism that activates when output video is larger than the original
- Make resolution and Constant Rate Factor (CRF) adjustable within the retry workflow
- Include user prompts to enter custom resolution and CRF values during retries
---
CommandLineApp/Conversion/Converter.cs | 227 +++++++++++++++++++------
1 file changed, 177 insertions(+), 50 deletions(-)
diff --git a/CommandLineApp/Conversion/Converter.cs b/CommandLineApp/Conversion/Converter.cs
index 75505a4..09d1b1e 100644
--- a/CommandLineApp/Conversion/Converter.cs
+++ b/CommandLineApp/Conversion/Converter.cs
@@ -6,61 +6,76 @@
namespace dis.CommandLineApp.Conversion;
-public sealed class Converter(PathHandler pathHandler, ProcessHandler processHandler, ILogger logger)
+public sealed class Converter(PathHandler pathHandler, Globals globals, ProcessHandler processHandler, ILogger logger)
{
///
/// Converts a video file to a specified format using FFmpeg.
///
/// The path to the input video file.
/// The optional date and time to set for the output file.
- /// The options to use for the conversion.
+ /// The options to use for the conversion.
/// A task that represents the asynchronous conversion operation.
- public async Task ConvertVideo(string file, DateTime? dateTime, Settings o)
+ public async Task ConvertVideo(string file, DateTime? dateTime, Settings s)
{
- Console.CancelKeyPress += HandleCancellation;
+ while (true)
+ {
+ Console.CancelKeyPress += HandleCancellation;
- var cmpPath = pathHandler.GetCompressPath(file, o.VideoCodec);
- var outP = pathHandler.ConstructFilePath(o, cmpPath);
+ var cmpPath = pathHandler.GetCompressPath(file, s.VideoCodec);
+ var outP = pathHandler.ConstructFilePath(s, cmpPath);
- var mediaInfo = await FFmpeg.GetMediaInfo(file);
- var streams = mediaInfo.Streams;
+ var mediaInfo = await FFmpeg.GetMediaInfo(file);
+ var streams = mediaInfo.Streams.ToList();
- var conversion = processHandler.ConfigureConversion(o, streams, outP);
- if (conversion is null)
- {
- logger.Error("Could not configure conversion");
- return;
- }
+ var conversion = processHandler.ConfigureConversion(s, streams, outP);
+ if (conversion is null)
+ {
+ logger.Error("Could not configure conversion");
+ return;
+ }
- try
- {
- await AnsiConsole.Status().StartAsync("Starting conversion...", async ctx =>
+ try
{
- ctx.Spinner(Spinner.Known.Arrow);
-
- conversion.OnProgress += (_, args) =>
- {
- var percent = (int)Math.Round(args.Duration.TotalSeconds / args.TotalLength.TotalSeconds * 100);
- if (percent is 0)
- return;
-
- ctx.Status($"[green]Conversion progress: {percent}%[/]");
- ctx.Refresh();
- };
- await conversion.Start();
- });
- if (dateTime.HasValue)
- processHandler.SetTimeStamps(outP, dateTime.Value);
- }
- catch (Exception)
- {
- logger.Error("Conversion failed");
- logger.Error("FFmpeg args: {Conversion}", $"ffmpeg {conversion.Build().Trim()}");
- return;
- }
+ await AnsiConsole.Status()
+ .StartAsync("Starting conversion...", async ctx =>
+ {
+ ctx.Spinner(Spinner.Known.Arrow);
+
+ conversion.OnProgress += (_, args) =>
+ {
+ var percent = (int)Math.Round(args.Duration.TotalSeconds / args.TotalLength.TotalSeconds * 100);
+ if (percent is 0) return;
+
+ ctx.Status($"[green]Conversion progress: {percent}%[/]");
+ ctx.Refresh();
+ };
+ await conversion.Start();
+ });
+ if (dateTime.HasValue) processHandler.SetTimeStamps(outP, dateTime.Value);
+ }
+ catch (Exception)
+ {
+ logger.Error("Conversion failed");
+ logger.Error("FFmpeg args: {Conversion}", $"ffmpeg {conversion.Build().Trim()}");
+ return;
+ }
- AnsiConsole.MarkupLine($"Converted video saved at: [green]{outP}[/]");
+ AnsiConsole.MarkupLine($"Converted video saved at: [green]{outP}[/]");
+ var (originalSize, compressedSize) = ResultsTable(file, outP);
+ if (compressedSize > originalSize)
+ {
+ var videoStream = streams.OfType().FirstOrDefault();
+ if (videoStream is not null)
+ if (ShouldRetry(s, outP, streams)) continue;
+ }
+
+ break;
+ }
+ }
+
+ private static (double, double) ResultsTable(string file, string outP)
+ {
/*
* File sizes are read and compared to calculate the difference and percentage saved.
*
@@ -71,19 +86,18 @@ await AnsiConsole.Status().StartAsync("Starting conversion...", async ctx =>
* The results are presented in a table with columns "Original", "Compressed", and "Saved".
*
* For example:
- * ┌─────────────────────────┬─────────────────────────────┬────────────────────────────┐
- * │ Original │ Compressed │ Saved │
- * ├─────────────────────────┼─────────────────────────────┼────────────────────────────┤
- * │ Original size: 1.10 MiB │ Compressed size: 417.28 KiB │ Saved: 709.82 KiB (62.98%) │
- * └─────────────────────────┴─────────────────────────────┴────────────────────────────┘
+ * ┌───────────────────────────┬─────────────────────────────┬─────────────────────────────┐
+ * │ Original │ Compressed │ Saved │
+ * ├───────────────────────────┼─────────────────────────────┼─────────────────────────────┤
+ * │ Original size: 710.86 KiB │ Compressed size: 467.02 KiB │ Saved: -243.84 KiB (-34.3%) │
+ * └───────────────────────────┴─────────────────────────────┴─────────────────────────────┘
*/
-
var originalSize = (double)new FileInfo(file).Length;
var compressedSize = (double)new FileInfo(outP).Length;
var saved = originalSize - compressedSize;
var savedPercent = saved / originalSize * 100;
- var savedPercentRounded = Math.Round(savedPercent, 2)
+ var savedPercentRounded = (savedPercent > 0 ? "-" : "+") + Math.Round(Math.Abs(savedPercent), 2)
.ToString(CultureInfo.InvariantCulture);
var originalMiB = originalSize / 1024.0 / 1024.0;
@@ -109,7 +123,6 @@ await AnsiConsole.Status().StartAsync("Starting conversion...", async ctx =>
: $"{savedSymbol}{Math.Round(savedMiB, 2):F2} MiB";
var savedString = $"{savedChange}: [{savedColor}]{savedSizeString} ({savedPercentRounded}%)[/]";
-
var table = new Table();
table.AddColumn("Original");
table.AddColumn("Compressed");
@@ -117,13 +130,127 @@ await AnsiConsole.Status().StartAsync("Starting conversion...", async ctx =>
table.AddRow(originalSizeString, compressedSizeString, savedString);
AnsiConsole.Write(table);
+ return (originalSize, compressedSize);
+ }
+
+ private bool ShouldRetry(Settings s, string outP, List enumerable)
+ {
+ AnsiConsole.MarkupLine("[yellow]The resulting file is larger than the original.[/]");
+
+ var deleteAndRetry = AskForRetry();
+ if (deleteAndRetry is false) return false;
+
+ var resolutionChanged = AskForResolutionChange(enumerable, s);
+ var crfChanged = AskForCrfChange(s);
+
+ if (resolutionChanged || crfChanged)
+ return true;
+
+ return DeleteConvertedVideo(outP);
+ }
+
+ private static bool AskForRetry()
+ {
+ var deleteAndRetry = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Do you want to delete the converted video and try again with a better setting?")
+ .AddChoices(["Yes", "No"]));
+
+ return deleteAndRetry is "Yes";
+ }
+
+ private bool AskForResolutionChange(List enumerable, Settings s)
+ {
+ var resolutionChanged = false;
+ var changeResolution = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Would you like to change the resolution?")
+ .AddChoices(["Yes", "No"]));
+
+ if (changeResolution is "Yes") resolutionChanged = ChangeResolution(enumerable, s);
+ return resolutionChanged;
}
- private void HandleCancellation(object? sender, ConsoleCancelEventArgs e)
+ private bool ChangeResolution(List enumerable, Settings s)
+ {
+ var width = enumerable.OfType().First().Width;
+ var height = enumerable.OfType().First().Height;
+
+ var maxDimension = Math.Max(width, height);
+ var currentResolution = $"{maxDimension}p";
+
+ var resolutionList = globals.ValidResolutions.ToList();
+ var currentResolutionIndex = resolutionList.FindIndex(res => res == currentResolution);
+
+ if (currentResolutionIndex <= 0) return false;
+
+ var lowerResolutions = resolutionList.GetRange(0, currentResolutionIndex);
+ var chosenResolution = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Please select a lower resolution for the conversion.")
+ .AddChoices(lowerResolutions));
+
+ s.Resolution = chosenResolution;
+ return true;
+ }
+
+ private static bool AskForCrfChange(Settings s)
+ {
+ var crfChanged = false;
+ var crfAnswer = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Would you like to enter a new crf value?")
+ .AddChoices(["Yes", "No"]));
+
+ if (crfAnswer is "Yes") crfChanged = ChangeCrfValue(s);
+
+ return crfChanged;
+ }
+
+ private static bool ChangeCrfValue(Settings s)
+ {
+ bool crfChanged;
+
+ while (true)
+ {
+ var crfAnswer = AnsiConsole.Ask("Please enter new value");
+ var crfValue = int.Parse(new string(crfAnswer.Where(char.IsDigit).ToArray()));
+
+ if (crfValue <= s.Crf)
+ {
+ AnsiConsole.WriteLine("Please enter a value higher than the current CRF.");
+ continue;
+ }
+
+ s.Crf = crfValue;
+ crfChanged = true;
+ break;
+ }
+
+ return crfChanged;
+ }
+
+ private static bool DeleteConvertedVideo(string path)
+ {
+ var deleteAnswer = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Do you want to delete the converted video?")
+ .AddChoices(["Yes", "No"]));
+ if (deleteAnswer is "No")
+ return false;
+
+ File.Delete(path);
+ AnsiConsole.MarkupLine("[green]Deleted the converted video.[/]");
+
+ return false;
+ }
+
+
+ private static void HandleCancellation(object? sender, ConsoleCancelEventArgs e)
{
if (e.SpecialKey is not ConsoleSpecialKey.ControlC)
return;
- AnsiConsole.WriteLine();
+
AnsiConsole.WriteLine("Canceled");
}
}
From 07ba4b277e7107ab0928397eee4cd0b0b730c468 Mon Sep 17 00:00:00 2001
From: DontEatOreo <57304299+DontEatOreo@users.noreply.github.com>
Date: Wed, 7 Feb 2024 13:49:21 +0200
Subject: [PATCH 8/8] chore: version bump
---
default.nix | 2 +-
dis.csproj | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/default.nix b/default.nix
index 82cf33c..dd0ede1 100644
--- a/default.nix
+++ b/default.nix
@@ -9,7 +9,7 @@
}:
buildDotnetModule {
pname = "dis";
- version = "9.0.1";
+ version = "9.1.0";
src = ./.;
diff --git a/dis.csproj b/dis.csproj
index d958a7c..c28315d 100644
--- a/dis.csproj
+++ b/dis.csproj
@@ -5,7 +5,7 @@
net8.0
enable
enable
- 9.0.1
+ 9.1.0