Skip to content

Commit 3c08cf8

Browse files
authored
Refactoring the process runner and update events. (#28)
1 parent 17b6124 commit 3c08cf8

4 files changed

Lines changed: 207 additions & 66 deletions

File tree

src/Ytdlp.NET.Console/Program.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ private static async Task TestGetLiteMetadataAsync(Ytdlp ytdlp)
160160
private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
161161
{
162162
Console.WriteLine("\nTest 6: Downloading a video...");
163-
var url = "https://www.youtube.com/watch?v=89-i4aPOMrc"; //"https://www.dailymotion.com/video/xa3ron2";
163+
var url = "https://www.youtube.com/watch?v=2vTkipUlhik"; //"https://www.dailymotion.com/video/xa3ron2";
164164

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

187+
ytdlp.OnOutputMessage += (s, msg) =>
188+
Console.WriteLine(msg);
189+
190+
ytdlp.OnErrorMessage += (sender, msg) =>
191+
Console.WriteLine(msg);
192+
187193
ytdlp.OnProgressDownload += (sender, args) =>
188194
Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA} - Size {args.Size}");
189195

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

199-
Console.WriteLine(ytdlp.Preview(url));
200-
201205
await ytdlp.DownloadAsync(url);
202206
}
203207

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

295299
try
296300
{
297-
298-
299301
await downloadTask;
300302
}
301303
catch (OperationCanceledException)
Lines changed: 187 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace ManuHub.Ytdlp.NET.Core;
1+
using System.Text;
2+
3+
namespace ManuHub.Ytdlp.NET.Core;
24

35
public sealed class DownloadRunner
46
{
@@ -7,7 +9,8 @@ public sealed class DownloadRunner
79
private readonly ILogger _logger;
810

911
public event EventHandler<string>? OnProgress;
10-
public event EventHandler<string>? OnErrorMessage;
12+
public event EventHandler<string>? OnOutput;
13+
public event EventHandler<string>? OnError;
1114
public event EventHandler<CommandCompletedEventArgs>? OnCommandCompleted;
1215

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

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

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

41-
process.OutputDataReceived += (s, e) =>
45+
// ---------------------------
46+
// STDOUT reader
47+
// ---------------------------
48+
var stdoutTask = Task.Run(async () =>
4249
{
43-
if (e.Data == null) return;
50+
using var reader = new StreamReader(
51+
process.StandardOutput.BaseStream,
52+
Encoding.UTF8,
53+
detectEncodingFromByteOrderMarks: false,
54+
bufferSize: 8192,
55+
leaveOpen: true);
4456

45-
try
46-
{
47-
_progressParser.ParseProgress(e.Data);
48-
OnProgress?.Invoke(this, e.Data);
49-
}
50-
catch (Exception ex)
57+
while (!ct.IsCancellationRequested)
5158
{
52-
_logger.Log(LogType.Error, $"Parse error: {ex.Message}");
59+
var readTask = reader.ReadLineAsync();
60+
61+
var completedTask = await Task.WhenAny(readTask, Task.Delay(Timeout.Infinite, ct));
62+
63+
if (completedTask != readTask)
64+
break;
65+
66+
var line = await readTask;
67+
if (line == null)
68+
break;
69+
70+
try
71+
{
72+
_progressParser.ParseProgress(line);
73+
OnProgress?.Invoke(this, line);
74+
OnOutput?.Invoke(this, line);
75+
}
76+
catch (Exception ex)
77+
{
78+
_logger.Log(LogType.Error, $"Parse error: {ex.Message}");
79+
}
5380
}
54-
};
81+
}, ct);
5582

56-
process.ErrorDataReceived += (s, e) =>
83+
// ---------------------------
84+
// STDERR reader
85+
// ---------------------------
86+
var stderrTask = Task.Run(async () =>
5787
{
58-
if (e.Data == null) return;
88+
using var reader = new StreamReader(
89+
process.StandardError.BaseStream,
90+
Encoding.UTF8,
91+
detectEncodingFromByteOrderMarks: false,
92+
bufferSize: 8192,
93+
leaveOpen: true);
5994

60-
OnErrorMessage?.Invoke(this, e.Data);
61-
_logger.Log(LogType.Error, e.Data);
62-
};
95+
while (!ct.IsCancellationRequested)
96+
{
97+
var readTask = reader.ReadLineAsync();
6398

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

67-
if (tuneProcess)
68-
ProcessFactory.Tune(process);
101+
if (completedTask != readTask)
102+
break;
103+
104+
var line = await readTask;
105+
if (line == null)
106+
break;
69107

70-
// ✅ Start reading AFTER handlers
71-
process.BeginOutputReadLine();
72-
process.BeginErrorReadLine();
108+
OnError?.Invoke(this, line);
109+
_logger.Log(LogType.Error, line);
110+
}
111+
}, ct);
73112

74-
// 🔥 Cancellation
113+
// ---------------------------
114+
// Cancellation handling
115+
// ---------------------------
75116
using var registration = ct.Register(() =>
76117
{
77118
if (!process.HasExited)
78119
{
79-
_logger.Log(LogType.Info, "Cancellation requested → killing process tree");
120+
_logger.Log(LogType.Info,
121+
"Cancellation requested → killing process tree");
122+
80123
ProcessFactory.SafeKill(process, _logger);
81124
}
82125
});
83126

84-
// Wait for exit OR cancellation
85-
await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, ct));
127+
// ---------------------------
128+
// Wait for ALL
129+
// ---------------------------
130+
await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct));
86131

87-
// Ensure process is dead
132+
// ---------------------------
133+
// Final safety kill
134+
// ---------------------------
88135
if (!process.HasExited)
89136
ProcessFactory.SafeKill(process);
90137

91-
try
92-
{
93-
await process.WaitForExitAsync(ct);
94-
}
95-
catch (OperationCanceledException)
96-
{
97-
if (!process.HasExited)
98-
ProcessFactory.SafeKill(process);
99-
}
100-
101138
var success = process.ExitCode == 0 && !ct.IsCancellationRequested;
102139

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

122159
throw new YtdlpException(msg, ex);
123160
}
124161
}
162+
163+
// Working code - old method
164+
//public async Task RunAsync(string arguments, CancellationToken ct, bool tuneProcess = true)
165+
//{
166+
// using var process = _factory.Create(arguments);
167+
168+
// int completed = 0;
169+
170+
// void Complete(bool success, string message)
171+
// {
172+
// if (Interlocked.Exchange(ref completed, 1) == 0)
173+
// {
174+
// OnCommandCompleted?.Invoke(this, new CommandCompletedEventArgs(success, message));
175+
// }
176+
// }
177+
178+
// try
179+
// {
180+
// var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
181+
182+
// // ✅ Attach BEFORE Start (fix race condition)
183+
// process.Exited += (_, _) => tcs.TrySetResult(true);
184+
185+
// process.OutputDataReceived += (s, e) =>
186+
// {
187+
// if (e.Data == null) return;
188+
189+
// try
190+
// {
191+
// _progressParser.ParseProgress(e.Data);
192+
// OnProgress?.Invoke(this, e.Data);
193+
// }
194+
// catch (Exception ex)
195+
// {
196+
// _logger.Log(LogType.Error, $"Parse error: {ex.Message}");
197+
// }
198+
// };
199+
200+
// process.ErrorDataReceived += (s, e) =>
201+
// {
202+
// if (e.Data == null) return;
203+
204+
// OnErrorMessage?.Invoke(this, e.Data);
205+
// _logger.Log(LogType.Error, e.Data);
206+
// };
207+
208+
// if (!process.Start())
209+
// throw new YtdlpException("Failed to start yt-dlp process.");
210+
211+
// if (tuneProcess)
212+
// ProcessFactory.Tune(process);
213+
214+
// // ✅ Start reading AFTER handlers
215+
// process.BeginOutputReadLine();
216+
// process.BeginErrorReadLine();
217+
218+
// // 🔥 Cancellation
219+
// using var registration = ct.Register(() =>
220+
// {
221+
// if (!process.HasExited)
222+
// {
223+
// _logger.Log(LogType.Info, "Cancellation requested → killing process tree");
224+
// ProcessFactory.SafeKill(process, _logger);
225+
// }
226+
// });
227+
228+
// // Wait for exit OR cancellation
229+
// await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, ct));
230+
231+
// // Ensure process is dead
232+
// if (!process.HasExited)
233+
// ProcessFactory.SafeKill(process);
234+
235+
// try
236+
// {
237+
// await process.WaitForExitAsync(ct);
238+
// }
239+
// catch (OperationCanceledException)
240+
// {
241+
// if (!process.HasExited)
242+
// ProcessFactory.SafeKill(process);
243+
// }
244+
245+
// var success = process.ExitCode == 0 && !ct.IsCancellationRequested;
246+
247+
// var message = success
248+
// ? "Completed successfully"
249+
// : ct.IsCancellationRequested
250+
// ? "Cancelled by user"
251+
// : $"Failed with exit code {process.ExitCode}";
252+
253+
// Complete(success, message);
254+
// }
255+
// catch (OperationCanceledException)
256+
// {
257+
// Complete(false, "Cancelled by user");
258+
// throw;
259+
// }
260+
// catch (Exception ex)
261+
// {
262+
// var msg = $"Error executing yt-dlp: {ex.Message}";
263+
// _logger.Log(LogType.Error, msg);
264+
// OnErrorMessage?.Invoke(this, msg);
265+
266+
// throw new YtdlpException(msg, ex);
267+
// }
268+
//}
125269
}

src/Ytdlp.NET/Parsing/ProgressParser.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void ParseProgress(string? output)
5757
if (string.IsNullOrWhiteSpace(output))
5858
return;
5959

60-
OnOutputMessage?.Invoke(this, output.TrimEnd());
60+
//OnOutputMessage?.Invoke(this, output.TrimEnd());
6161

6262
foreach (var (regex, handler) in _regexHandlers)
6363
{
@@ -108,7 +108,7 @@ private void HandleDownloadProgress(Match match)
108108
string etaString = match.Groups["eta"].Value;
109109

110110
if (!double.TryParse(percentString.Replace("%", ""), out double percent))
111-
percent = 0;
111+
percent = 0;
112112

113113
var args = new DownloadProgressEventArgs
114114
{
@@ -276,10 +276,7 @@ private void HandleUnknownOutput(string output)
276276
private void LogAndNotify(LogType logType, string message)
277277
{
278278
_logger.Log(logType, message);
279-
if (logType == LogType.Error)
280-
OnErrorMessage?.Invoke(this, message);
281-
else
282-
OnProgressMessage?.Invoke(this, message);
279+
OnProgressMessage?.Invoke(this, message);
283280
}
284281

285282
private void LogAndNotifyComplete(string message)
@@ -291,9 +288,7 @@ private void LogAndNotifyComplete(string message)
291288

292289
// ───────────── Events (unchanged) ─────────────
293290
#region Events
294-
public event EventHandler<string>? OnOutputMessage;
295291
public event EventHandler<string>? OnProgressMessage;
296-
public event EventHandler<string>? OnErrorMessage;
297292
public event EventHandler<DownloadProgressEventArgs>? OnProgressDownload;
298293
public event EventHandler<string>? OnCompleteDownload;
299294
public event EventHandler<string>? OnPostProcessingStart;

0 commit comments

Comments
 (0)