Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adaptive Retry Mechanism #7

Merged
merged 8 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 177 additions & 50 deletions CommandLineApp/Conversion/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
/// <summary>
/// Converts a video file to a specified format using FFmpeg.
/// </summary>
/// <param name="file">The path to the input video file.</param>
/// <param name="dateTime">The optional date and time to set for the output file.</param>
/// <param name="o">The options to use for the conversion.</param>
/// <param name="s">The options to use for the conversion.</param>
/// <returns>A task that represents the asynchronous conversion operation.</returns>
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<IVideoStream>().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.
*
Expand All @@ -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;
Expand All @@ -109,21 +123,134 @@ 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");
table.AddColumn(savedChange);
table.AddRow(originalSizeString, compressedSizeString, savedString);

AnsiConsole.Write(table);
return (originalSize, compressedSize);
}

private bool ShouldRetry(Settings s, string outP, List<IStream> 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<string>()
.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<IStream> enumerable, Settings s)
{
var resolutionChanged = false;
var changeResolution = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.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<IStream> enumerable, Settings s)
{
var width = enumerable.OfType<IVideoStream>().First().Width;
var height = enumerable.OfType<IVideoStream>().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<string>()
.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<string>()
.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<string>("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<string>()
.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");
}
}
35 changes: 23 additions & 12 deletions CommandLineApp/Conversion/ProcessHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ public void SetTimeStamps(string path, DateTime date)
File.SetLastAccessTime(path, date);
}

public IConversion? ConfigureConversion(Settings o, IEnumerable<IStream> streams, string outP)
public IConversion? ConfigureConversion(Settings o, IList<IStream> streams, string outP)
{
var listOfStreams = streams.ToList();
var videoStream = listOfStreams.OfType<IVideoStream>().FirstOrDefault();
var audioStream = listOfStreams.OfType<IAudioStream>().FirstOrDefault();
var videoStream = streams.OfType<IVideoStream>().FirstOrDefault();
var audioStream = streams.OfType<IAudioStream>().FirstOrDefault();

if (videoStream is null && audioStream is null)
{
Expand All @@ -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);

Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion CommandLineApp/Conversion/StreamConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ public void SetCpuForAv1(IConversion conversion, double framerate)

public void SetVp9Args(IConversion conversion)
=> conversion.AddParameter(string.Join(" ", _vp9Args));
}
}
29 changes: 2 additions & 27 deletions CommandLineApp/Downloaders/VideoDownloaderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ private bool ValidTimeRange(RunResult<VideoData> 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;

Expand All @@ -132,30 +133,4 @@ private bool ValidTimeRange(RunResult<VideoData> fetch)
_logger.Error(TrimTimeError);
return false;
}

/// <summary>
/// Extracts the start and end times from a given download section string.
/// </summary>
/// <param name="downloadSection">The string containing the download section details.</param>
/// <returns>A tuple containing the start and end times as floats.</returns>
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);
}
}
2 changes: 1 addition & 1 deletion CommandLineApp/Models/DownloadResult.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace dis.CommandLineApp.Models;

public sealed record DownloadResult(string? OutPath, DateTime? Date);
public sealed record DownloadResult(string? OutPath, DateTime? Date);
Loading