Skip to content
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
12 changes: 7 additions & 5 deletions src/Ytdlp.NET.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ private static async Task TestGetLiteMetadataAsync(Ytdlp ytdlp)
private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
{
Console.WriteLine("\nTest 6: Downloading a video...");
var url = "https://www.youtube.com/watch?v=89-i4aPOMrc"; //"https://www.dailymotion.com/video/xa3ron2";
var url = "https://www.youtube.com/watch?v=2vTkipUlhik"; //"https://www.dailymotion.com/video/xa3ron2";

var ytdlp = ytdlpBase
.WithFormat("ba/b")
Expand All @@ -184,6 +184,12 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
Console.WriteLine($"Process failed: {args.Message}");
};

ytdlp.OnOutputMessage += (s, msg) =>
Console.WriteLine(msg);

ytdlp.OnErrorMessage += (sender, msg) =>
Console.WriteLine(msg);

ytdlp.OnProgressDownload += (sender, args) =>
Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA} - Size {args.Size}");

Expand All @@ -196,8 +202,6 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
ytdlp.OnPostProcessingComplete += (sender, message) =>
Console.WriteLine($"Post-processing done: {message}");

Console.WriteLine(ytdlp.Preview(url));

await ytdlp.DownloadAsync(url);
}

Expand Down Expand Up @@ -294,8 +298,6 @@ private static async Task TestCancellationAsync(Ytdlp ytdlp)

try
{


await downloadTask;
}
catch (OperationCanceledException)
Expand Down
230 changes: 187 additions & 43 deletions src/Ytdlp.NET/Core/DownloadRunner.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace ManuHub.Ytdlp.NET.Core;
using System.Text;

namespace ManuHub.Ytdlp.NET.Core;

public sealed class DownloadRunner
{
Expand All @@ -7,7 +9,8 @@ public sealed class DownloadRunner
private readonly ILogger _logger;

public event EventHandler<string>? OnProgress;
public event EventHandler<string>? OnErrorMessage;
public event EventHandler<string>? OnOutput;
public event EventHandler<string>? OnError;
public event EventHandler<CommandCompletedEventArgs>? OnCommandCompleted;

public DownloadRunner(ProcessFactory factory, ProgressParser parser, ILogger logger)
Expand All @@ -33,71 +36,105 @@ void Complete(bool success, string message)

try
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!process.Start())
throw new YtdlpException("Failed to start yt-dlp process.");

// ✅ Attach BEFORE Start (fix race condition)
process.Exited += (_, _) => tcs.TrySetResult(true);
if (tuneProcess)
ProcessFactory.Tune(process);

process.OutputDataReceived += (s, e) =>
// ---------------------------
// STDOUT reader
// ---------------------------
var stdoutTask = Task.Run(async () =>
{
if (e.Data == null) return;
using var reader = new StreamReader(
process.StandardOutput.BaseStream,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
bufferSize: 8192,
leaveOpen: true);

try
{
_progressParser.ParseProgress(e.Data);
OnProgress?.Invoke(this, e.Data);
}
catch (Exception ex)
while (!ct.IsCancellationRequested)
{
_logger.Log(LogType.Error, $"Parse error: {ex.Message}");
var readTask = reader.ReadLineAsync();

var completedTask = await Task.WhenAny(readTask, Task.Delay(Timeout.Infinite, ct));

if (completedTask != readTask)
break;

var line = await readTask;
if (line == null)
break;

try
{
_progressParser.ParseProgress(line);
OnProgress?.Invoke(this, line);
OnOutput?.Invoke(this, line);
}
catch (Exception ex)
{
_logger.Log(LogType.Error, $"Parse error: {ex.Message}");
}
}
};
}, ct);

process.ErrorDataReceived += (s, e) =>
// ---------------------------
// STDERR reader
// ---------------------------
var stderrTask = Task.Run(async () =>
{
if (e.Data == null) return;
using var reader = new StreamReader(
process.StandardError.BaseStream,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
bufferSize: 8192,
leaveOpen: true);

OnErrorMessage?.Invoke(this, e.Data);
_logger.Log(LogType.Error, e.Data);
};
while (!ct.IsCancellationRequested)
{
var readTask = reader.ReadLineAsync();

if (!process.Start())
throw new YtdlpException("Failed to start yt-dlp process.");
var completedTask = await Task.WhenAny(readTask, Task.Delay(Timeout.Infinite, ct));

if (tuneProcess)
ProcessFactory.Tune(process);
if (completedTask != readTask)
break;

var line = await readTask;
if (line == null)
break;

// ✅ Start reading AFTER handlers
process.BeginOutputReadLine();
process.BeginErrorReadLine();
OnError?.Invoke(this, line);
_logger.Log(LogType.Error, line);
}
}, ct);

// 🔥 Cancellation
// ---------------------------
// Cancellation handling
// ---------------------------
using var registration = ct.Register(() =>
{
if (!process.HasExited)
{
_logger.Log(LogType.Info, "Cancellation requested → killing process tree");
_logger.Log(LogType.Info,
"Cancellation requested → killing process tree");

ProcessFactory.SafeKill(process, _logger);
}
});

// Wait for exit OR cancellation
await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, ct));
// ---------------------------
// Wait for ALL
// ---------------------------
await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct));

// Ensure process is dead
// ---------------------------
// Final safety kill
// ---------------------------
if (!process.HasExited)
ProcessFactory.SafeKill(process);

try
{
await process.WaitForExitAsync(ct);
}
catch (OperationCanceledException)
{
if (!process.HasExited)
ProcessFactory.SafeKill(process);
}

var success = process.ExitCode == 0 && !ct.IsCancellationRequested;

var message = success
Expand All @@ -117,9 +154,116 @@ void Complete(bool success, string message)
{
var msg = $"Error executing yt-dlp: {ex.Message}";
_logger.Log(LogType.Error, msg);
OnErrorMessage?.Invoke(this, msg);
OnError?.Invoke(this, msg);

throw new YtdlpException(msg, ex);
}
}

// Working code - old method
//public async Task RunAsync(string arguments, CancellationToken ct, bool tuneProcess = true)
//{
// using var process = _factory.Create(arguments);

// int completed = 0;

// void Complete(bool success, string message)
// {
// if (Interlocked.Exchange(ref completed, 1) == 0)
// {
// OnCommandCompleted?.Invoke(this, new CommandCompletedEventArgs(success, message));
// }
// }

// try
// {
// var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

// // ✅ Attach BEFORE Start (fix race condition)
// process.Exited += (_, _) => tcs.TrySetResult(true);

// process.OutputDataReceived += (s, e) =>
// {
// if (e.Data == null) return;

// try
// {
// _progressParser.ParseProgress(e.Data);
// OnProgress?.Invoke(this, e.Data);
// }
// catch (Exception ex)
// {
// _logger.Log(LogType.Error, $"Parse error: {ex.Message}");
// }
// };

// process.ErrorDataReceived += (s, e) =>
// {
// if (e.Data == null) return;

// OnErrorMessage?.Invoke(this, e.Data);
// _logger.Log(LogType.Error, e.Data);
// };

// if (!process.Start())
// throw new YtdlpException("Failed to start yt-dlp process.");

// if (tuneProcess)
// ProcessFactory.Tune(process);

// // ✅ Start reading AFTER handlers
// process.BeginOutputReadLine();
// process.BeginErrorReadLine();

// // 🔥 Cancellation
// using var registration = ct.Register(() =>
// {
// if (!process.HasExited)
// {
// _logger.Log(LogType.Info, "Cancellation requested → killing process tree");
// ProcessFactory.SafeKill(process, _logger);
// }
// });

// // Wait for exit OR cancellation
// await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, ct));

// // Ensure process is dead
// if (!process.HasExited)
// ProcessFactory.SafeKill(process);

// try
// {
// await process.WaitForExitAsync(ct);
// }
// catch (OperationCanceledException)
// {
// if (!process.HasExited)
// ProcessFactory.SafeKill(process);
// }

// var success = process.ExitCode == 0 && !ct.IsCancellationRequested;

// var message = success
// ? "Completed successfully"
// : ct.IsCancellationRequested
// ? "Cancelled by user"
// : $"Failed with exit code {process.ExitCode}";

// Complete(success, message);
// }
// catch (OperationCanceledException)
// {
// Complete(false, "Cancelled by user");
// throw;
// }
// catch (Exception ex)
// {
// var msg = $"Error executing yt-dlp: {ex.Message}";
// _logger.Log(LogType.Error, msg);
// OnErrorMessage?.Invoke(this, msg);

// throw new YtdlpException(msg, ex);
// }
//}
}
11 changes: 3 additions & 8 deletions src/Ytdlp.NET/Parsing/ProgressParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
private bool _isDownloadCompleted;
private bool _postProcessingStarted;
private int _postProcessStepCount;
private int _deleteCount;

Check warning on line 17 in src/Ytdlp.NET/Parsing/ProgressParser.cs

View workflow job for this annotation

GitHub Actions / build

The field 'ProgressParser._deleteCount' is assigned but its value is never used

public ProgressParser(ILogger? logger = null)
{
Expand Down Expand Up @@ -57,7 +57,7 @@
if (string.IsNullOrWhiteSpace(output))
return;

OnOutputMessage?.Invoke(this, output.TrimEnd());
//OnOutputMessage?.Invoke(this, output.TrimEnd());

foreach (var (regex, handler) in _regexHandlers)
{
Expand Down Expand Up @@ -108,7 +108,7 @@
string etaString = match.Groups["eta"].Value;

if (!double.TryParse(percentString.Replace("%", ""), out double percent))
percent = 0;
percent = 0;

var args = new DownloadProgressEventArgs
{
Expand Down Expand Up @@ -276,10 +276,7 @@
private void LogAndNotify(LogType logType, string message)
{
_logger.Log(logType, message);
if (logType == LogType.Error)
OnErrorMessage?.Invoke(this, message);
else
OnProgressMessage?.Invoke(this, message);
OnProgressMessage?.Invoke(this, message);
}

private void LogAndNotifyComplete(string message)
Expand All @@ -291,9 +288,7 @@

// ───────────── Events (unchanged) ─────────────
#region Events
public event EventHandler<string>? OnOutputMessage;
public event EventHandler<string>? OnProgressMessage;
public event EventHandler<string>? OnErrorMessage;
public event EventHandler<DownloadProgressEventArgs>? OnProgressDownload;
public event EventHandler<string>? OnCompleteDownload;
public event EventHandler<string>? OnPostProcessingStart;
Expand Down
Loading
Loading