Skip to content

Commit c4f4e6c

Browse files
authored
Improve process termination, progress parsing, and docs (#24)
- Add ProcessExtensions.KillTree for reliable cross-platform process tree termination; update ProcessFactory to use it - Enhance download progress regex and parsing for better accuracy - Improve cancellation handling and add pre-checks in Ytdlp - Update demo/test code: comment out most tests, improve progress/cancellation output, change test URLs - Document AddFlag/AddOption in README, fix minor typos, add table entry - Bump version to 3.0.2
1 parent d20ebbe commit c4f4e6c

9 files changed

Lines changed: 169 additions & 38 deletions

File tree

src/Ytdlp.NET.Console/Program.cs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,20 @@ private static async Task Main(string[] args)
1919
.WithFFmpegLocation("tools");
2020

2121
// Run all demos/tests sequentially
22-
await TestGetVersionAsync(baseYtdlp);
23-
await TestUpdateAsync(baseYtdlp);
22+
//await TestGetVersionAsync(baseYtdlp);
23+
//await TestUpdateAsync(baseYtdlp);
2424

25-
await TestGetFormatsAsync(baseYtdlp);
26-
await TestGetMetadataAsync(baseYtdlp);
27-
await TestGetLiteMetadataAsync(baseYtdlp);
28-
await TestGetTitleAsync(baseYtdlp);
25+
//await TestGetFormatsAsync(baseYtdlp);
26+
//await TestGetMetadataAsync(baseYtdlp);
27+
//await TestGetLiteMetadataAsync(baseYtdlp);
28+
//await TestGetTitleAsync(baseYtdlp);
2929

3030
await TestDownloadVideoAsync(baseYtdlp);
31-
await TestDownloadAudioAsync(baseYtdlp);
32-
await TestBatchDownloadAsync(baseYtdlp);
33-
await TestSponsorBlockAsync(baseYtdlp);
34-
await TestConcurrentFragmentsAsync(baseYtdlp);
35-
await TestCancellationAsync(baseYtdlp);
31+
//await TestDownloadAudioAsync(baseYtdlp);
32+
//await TestBatchDownloadAsync(baseYtdlp);
33+
//await TestSponsorBlockAsync(baseYtdlp);
34+
//await TestConcurrentFragmentsAsync(baseYtdlp);
35+
//await TestCancellationAsync(baseYtdlp);
3636

3737
var lists = await baseYtdlp.ExtractorsAsync();
3838

@@ -104,10 +104,12 @@ private static async Task TestGetMetadataAsync(Ytdlp ytdlp)
104104
var stopwatch = Stopwatch.StartNew();
105105

106106
Console.WriteLine("\nTest 4: Fetching detailed metedata...");
107-
107+
var token = new CancellationTokenSource().Token; // In real use, you might want to cancel if it takes too long
108+
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(90));
109+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutCts.Token);
108110
var url1 = "https://www.youtube.com/watch?v=983bBbJx0Mk&list=RD983bBbJx0Mk&start_radio=1&pp=ygUFc29uZ3OgBwE%3D"; //playlist
109111
var url2 = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; // video
110-
var metadata = await ytdlp.GetMetadataAsync(url1);
112+
var metadata = await ytdlp.GetMetadataAsync(url2, linkedCts.Token);
111113
stopwatch.Stop(); // stop timer
112114

113115
Console.WriteLine($"Detailed metedata took {stopwatch.Elapsed.TotalSeconds:F3} seconds");
@@ -157,7 +159,7 @@ private static async Task TestGetLiteMetadataAsync(Ytdlp ytdlp)
157159
private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
158160
{
159161
Console.WriteLine("\nTest 6: Downloading a video...");
160-
var url = "https://www.youtube.com/watch?v=3pecPwPIFIc&pp=ugUEEgJtbA%3D%3D";
162+
var url = "https://www.youtube.com/watch?v=89-i4aPOMrc";
161163

162164
var ytdlp = ytdlpBase
163165
.With720pOrBest()
@@ -170,7 +172,7 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
170172

171173
// Subscribe to events
172174
ytdlp.OnProgressDownload += (sender, args) =>
173-
Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA}");
175+
Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA} - Size {args.Size}");
174176

175177
ytdlp.OnCompleteDownload += (sender, message) =>
176178
Console.WriteLine($"Download complete: {message}");
@@ -240,6 +242,9 @@ private static async Task TestConcurrentFragmentsAsync(Ytdlp ytdlpBase)
240242
.WithOutputTemplate("%(title)s.%(ext)s")
241243
.WithOutputFolder("./downloads/concurrent");
242244

245+
ytdlp.OnProgressDownload += (sender, args) =>
246+
Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA} - FRA {args.Fragments}");
247+
243248
await ytdlp.DownloadAsync(url);
244249
}
245250

@@ -250,18 +255,31 @@ private static async Task TestCancellationAsync(Ytdlp ytdlp)
250255
var url = "https://www.youtube.com/watch?v=zGlwuHqGVIA"; // A longer video
251256

252257
var cts = new CancellationTokenSource();
258+
253259
var downloadTask = ytdlp
254260
.WithFormat("b")
255261
.WithOutputTemplate("%(title)s.%(ext)s")
256262
.WithOutputFolder("./downloads/cancel")
257263
.DownloadAsync(url, cts.Token);
258264

265+
ytdlp.OnCommandCompleted += (sender, args) =>
266+
{
267+
if (args.Success)
268+
Console.WriteLine("Download completed successfully.");
269+
else if (args.Message.Contains("cancelled", StringComparison.OrdinalIgnoreCase))
270+
Console.WriteLine("Download was cancelled.");
271+
else
272+
Console.WriteLine($"Download failed: {args.Message}");
273+
};
274+
259275
// Simulate cancel after 20 seconds
260276
await Task.Delay(20000);
261277
cts.Cancel();
262278

263279
try
264280
{
281+
282+
265283
await downloadTask;
266284
}
267285
catch (OperationCanceledException)

src/Ytdlp.NET/Core/DownloadRunner.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Diagnostics;
2-
3-
namespace ManuHub.Ytdlp.NET.Core;
1+
namespace ManuHub.Ytdlp.NET.Core;
42

53
public sealed class DownloadRunner
64
{
@@ -88,17 +86,16 @@ void Complete(bool success, string message)
8886

8987
// Ensure process is dead
9088
if (!process.HasExited)
91-
{
92-
ProcessFactory.SafeKill(process, _logger);
93-
}
89+
ProcessFactory.SafeKill(process);
9490

9591
try
9692
{
9793
await process.WaitForExitAsync(ct);
9894
}
9995
catch (OperationCanceledException)
10096
{
101-
ProcessFactory.SafeKill(process, _logger);
97+
if (!process.HasExited)
98+
ProcessFactory.SafeKill(process);
10299
}
103100

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

src/Ytdlp.NET/Core/ProcessFactory.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Diagnostics;
1+
using ManuHub.Ytdlp.NET.Extensions;
2+
using System.Diagnostics;
23
using System.Text;
34

45
namespace ManuHub.Ytdlp.NET.Core;
@@ -24,7 +25,6 @@ public Process Create(string arguments)
2425
FileName = _ytdlpPath,
2526
Arguments = arguments,
2627

27-
// Must for async/event-based reading
2828
RedirectStandardOutput = true,
2929
RedirectStandardError = true,
3030
RedirectStandardInput = true,
@@ -75,18 +75,15 @@ public static void SafeKill(Process process, ILogger? logger = null)
7575
if (process.HasExited)
7676
return;
7777

78-
// Close streams first → prevents ReadLine hang
79-
try { process.StandardOutput?.Close(); } catch { }
80-
try { process.StandardError?.Close(); } catch { }
81-
try { process.StandardInput?.Close(); } catch { }
78+
process.KillTree();
8279

83-
process.Kill(entireProcessTree: true);
84-
85-
logger?.Log(LogType.Info, "Process killed (entire tree)");
80+
if(logger != null)
81+
logger.Log(LogType.Info, "Process killed (entire tree)");
8682
}
8783
catch (Exception ex)
8884
{
89-
logger?.Log(LogType.Error, $"Failed to kill process: {ex.Message}");
85+
if(logger != null)
86+
logger.Log(LogType.Error, $"Failed to kill process: {ex.Message}");
9087
}
9188
}
9289
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Diagnostics;
2+
using System.Runtime.InteropServices;
3+
4+
namespace ManuHub.Ytdlp.NET.Extensions;
5+
6+
/// <summary>
7+
/// Process extensions for killing full process tree.
8+
/// </summary>
9+
internal static class ProcessExtensions
10+
{
11+
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
12+
13+
public static void KillTree(this Process process)
14+
{
15+
process.KillTree(_defaultTimeout);
16+
}
17+
18+
public static void KillTree(this Process process, TimeSpan timeout)
19+
{
20+
string stdout;
21+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
22+
{
23+
RunProcessAndWaitForExit(
24+
"taskkill",
25+
$"/T /F /PID {process.Id}",
26+
timeout,
27+
out stdout);
28+
}
29+
else
30+
{
31+
var children = new HashSet<int>();
32+
GetAllChildIdsUnix(process.Id, children, timeout);
33+
foreach (var childId in children)
34+
{
35+
KillProcessUnix(childId, timeout);
36+
}
37+
KillProcessUnix(process.Id, timeout);
38+
}
39+
}
40+
41+
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
42+
{
43+
string stdout;
44+
var exitCode = RunProcessAndWaitForExit(
45+
"pgrep",
46+
$"-P {parentId}",
47+
timeout,
48+
out stdout);
49+
50+
if (exitCode == 0 && !string.IsNullOrEmpty(stdout))
51+
{
52+
using (var reader = new StringReader(stdout))
53+
{
54+
while (true)
55+
{
56+
var text = reader.ReadLine();
57+
if (text == null)
58+
{
59+
return;
60+
}
61+
62+
int id;
63+
if (int.TryParse(text, out id))
64+
{
65+
children.Add(id);
66+
GetAllChildIdsUnix(id, children, timeout);
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
private static void KillProcessUnix(int processId, TimeSpan timeout)
74+
{
75+
string stdout;
76+
RunProcessAndWaitForExit(
77+
"kill",
78+
$"-TERM {processId}",
79+
timeout,
80+
out stdout);
81+
}
82+
83+
private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)
84+
{
85+
var startInfo = new ProcessStartInfo
86+
{
87+
FileName = fileName,
88+
Arguments = arguments,
89+
RedirectStandardOutput = true,
90+
UseShellExecute = false,
91+
CreateNoWindow = true
92+
};
93+
94+
var process = Process.Start(startInfo);
95+
96+
stdout = null;
97+
if (process.WaitForExit((int)timeout.TotalMilliseconds))
98+
{
99+
stdout = process.StandardOutput.ReadToEnd();
100+
}
101+
else
102+
{
103+
process.Kill();
104+
}
105+
106+
return process.ExitCode;
107+
}
108+
}
109+

src/Ytdlp.NET/Parsing/ProgressParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ private void HandleDownloadProgress(Match match)
183183
{
184184
// Existing logic unchanged
185185
string percentString = match.Groups["percent"].Value;
186-
string sizeString = match.Groups["size"].Value;
186+
string sizeString = match.Groups["total"].Value;
187187
string speedString = match.Groups["speed"].Value;
188188
string etaString = match.Groups["eta"].Value;
189189

src/Ytdlp.NET/Parsing/RegexPatterns.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ internal static class RegexPatterns
1717
public const string DownloadDestination = @"\[download\]\s*Destination:\s*(?<path>.+)";
1818
public const string ResumeDownload = @"\[download\]\s*Resuming download at byte\s*(?<byte>\d+)";
1919
public const string DownloadAlreadyDownloaded = @"\[download\]\s*(?<path>[^\n]+?)\s*has already been downloaded";
20-
public const string DownloadProgress = @"\[download\]\s*(?<percent>\d+\.\d+)%\s*of\s*(?<size>[^\s]+)\s*at\s*(?<speed>[^\s]+)\s*ETA\s*(?<eta>[^\s]+)";
20+
public const string DownloadProgress = @"\[download\]\s+(?:(?<percent>[\d\.]+)%(?:\s+of\s+\~?\s*(?<total>[\d\.\w]+))?\s+at\s+(?:(?<speed>[\d\.\w]+\/s)|[\w\s]+)\s+ETA\s(?<eta>[\d\:]+))?";
21+
//@"\[download\]\s*(?<percent>\d+\.\d+)%\s*of\s*(?<size>[^\s]+)\s*at\s*(?<speed>[^\s]+)\s*ETA\s*(?<eta>[^\s]+)";
2122
public const string DownloadProgressWithFrag = @"\[download\]\s*(?<percent>\d+\.\d+)%\s*of\s*(~?\s*(?<size>[^\s]+))\s*at\s*(?<speed>[^\s]+)\s*ETA\s*(?<eta>[^\s]+)\s*\(frag\s*(?<frag>\d+/\d+)\)";
2223
public const string DownloadProgressComplete = @"\[download\]\s*(?<percent>100(?:\.0)?)%\s*of\s*(?<size>[^\s]+)\s*at\s*(?<speed>[^\s]+|Unknown)\s*ETA\s*(?<eta>[^\s]+|Unknown)";
2324
public const string UnknownError = @"\[download\]\s*Unknown error";
@@ -27,8 +28,6 @@ internal static class RegexPatterns
2728
public const string SpecificError = @"\[(?<source>[^\]]+)\]\s*(?<id>[^\s:]+):\s*ERROR:\s*(?<error>.+)";
2829
public const string DownloadingSubtitles = @"\[info\]\s*Downloading subtitles:\s*(?<language>[^\s]+)";
2930

30-
// ───────────── New / Enhanced Patterns for v2.0 ─────────────
31-
3231
// More reliable merger success detection (variation of "successfully merged")
3332
public const string MergerSuccess = @"(?:has been successfully merged|merged formats successfully)";
3433

src/Ytdlp.NET/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);
239239
* `.WithJsRuntime(Runtime runtime, string runtimePath)`
240240
* `.WithNoJsRuntime()`
241241
* `.WithFlatPlaylist()`
242-
* ` WithLiveFromStart()`
242+
* `.WithLiveFromStart()`
243243
* `.WithWaitForVideo(TimeSpan? maxWait = null)`
244244
* `.WithMarkWatched()`
245245

@@ -436,9 +436,16 @@ await ytdlp.DownloadAsync(url);
436436
| `SetFFMpegLocation()` | `WithFFmpegLocation()` |
437437
| `ExtractAudio()` | `WithExtractAudio()` |
438438
| `UseProxy()` | `WithProxy()` |
439+
| `AddCustomCommand()` | `AddFlag(string flag)` or `AddOption(string key, string value)` |
439440

440441
---
441442

443+
## Custom commands
444+
```csharp
445+
AddFlag("--no-check-certificate");
446+
AddOption("--external-downloader", "aria2c");
447+
```
448+
442449
## Important behavior changes
443450

444451
### Instances are immutable

src/Ytdlp.NET/Ytdlp.NET.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<PackageId>Ytdlp.NET</PackageId>
88
<AssemblyName>Ytdlp.NET</AssemblyName>
99
<RootNamespace>ManuHub.Ytdlp.NET</RootNamespace>
10-
<Version>3.0.1</Version>
10+
<Version>3.0.2</Version>
1111
<Authors>ManuHub Manojbabu</Authors>
1212
<PackageDescription>A .NET wrapper for yt-dlp with advanced features like concurrent downloads, SponsorBlock, and improved format parsing</PackageDescription>
1313
<Copyright>© 2025-2026 ManuHub. Allrights researved</Copyright>

src/Ytdlp.NET/Ytdlp.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using ManuHub.Ytdlp.NET.Core;
22
using System.Collections.Immutable;
3+
using System.Diagnostics;
34
using System.Globalization;
45
using System.Text.Json;
56
using System.Text.Json.Serialization;
@@ -1366,6 +1367,9 @@ public async Task<List<string>> ExtractorsAsync(CancellationToken ct = default,
13661367
$"--no-warnings " +
13671368
$"{Quote(url)}";
13681369

1370+
if(ct.IsCancellationRequested)
1371+
Debug.WriteLine("Cancellation requested before starting the process.");
1372+
13691373
var json = await Probe().RunAsync(arguments, ct, tuneProcess, bufferKb);
13701374

13711375
if (string.IsNullOrWhiteSpace(json))

0 commit comments

Comments
 (0)