diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs
index 78487ac..431edf3 100644
--- a/src/Ytdlp.NET.Console/Program.cs
+++ b/src/Ytdlp.NET.Console/Program.cs
@@ -19,20 +19,20 @@ private static async Task Main(string[] args)
.WithFFmpegLocation("tools");
// Run all demos/tests sequentially
- await TestGetVersionAsync(baseYtdlp);
- await TestUpdateAsync(baseYtdlp);
+ //await TestGetVersionAsync(baseYtdlp);
+ //await TestUpdateAsync(baseYtdlp);
- await TestGetFormatsAsync(baseYtdlp);
- await TestGetMetadataAsync(baseYtdlp);
- await TestGetLiteMetadataAsync(baseYtdlp);
- await TestGetTitleAsync(baseYtdlp);
+ //await TestGetFormatsAsync(baseYtdlp);
+ //await TestGetMetadataAsync(baseYtdlp);
+ //await TestGetLiteMetadataAsync(baseYtdlp);
+ //await TestGetTitleAsync(baseYtdlp);
await TestDownloadVideoAsync(baseYtdlp);
- await TestDownloadAudioAsync(baseYtdlp);
- await TestBatchDownloadAsync(baseYtdlp);
- await TestSponsorBlockAsync(baseYtdlp);
- await TestConcurrentFragmentsAsync(baseYtdlp);
- await TestCancellationAsync(baseYtdlp);
+ //await TestDownloadAudioAsync(baseYtdlp);
+ //await TestBatchDownloadAsync(baseYtdlp);
+ //await TestSponsorBlockAsync(baseYtdlp);
+ //await TestConcurrentFragmentsAsync(baseYtdlp);
+ //await TestCancellationAsync(baseYtdlp);
var lists = await baseYtdlp.ExtractorsAsync();
@@ -104,10 +104,12 @@ private static async Task TestGetMetadataAsync(Ytdlp ytdlp)
var stopwatch = Stopwatch.StartNew();
Console.WriteLine("\nTest 4: Fetching detailed metedata...");
-
+ var token = new CancellationTokenSource().Token; // In real use, you might want to cancel if it takes too long
+ using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(90));
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutCts.Token);
var url1 = "https://www.youtube.com/watch?v=983bBbJx0Mk&list=RD983bBbJx0Mk&start_radio=1&pp=ygUFc29uZ3OgBwE%3D"; //playlist
var url2 = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; // video
- var metadata = await ytdlp.GetMetadataAsync(url1);
+ var metadata = await ytdlp.GetMetadataAsync(url2, linkedCts.Token);
stopwatch.Stop(); // stop timer
Console.WriteLine($"Detailed metedata took {stopwatch.Elapsed.TotalSeconds:F3} seconds");
@@ -157,7 +159,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=3pecPwPIFIc&pp=ugUEEgJtbA%3D%3D";
+ var url = "https://www.youtube.com/watch?v=89-i4aPOMrc";
var ytdlp = ytdlpBase
.With720pOrBest()
@@ -170,7 +172,7 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
// Subscribe to events
ytdlp.OnProgressDownload += (sender, args) =>
- Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA}");
+ Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA} - Size {args.Size}");
ytdlp.OnCompleteDownload += (sender, message) =>
Console.WriteLine($"Download complete: {message}");
@@ -240,6 +242,9 @@ private static async Task TestConcurrentFragmentsAsync(Ytdlp ytdlpBase)
.WithOutputTemplate("%(title)s.%(ext)s")
.WithOutputFolder("./downloads/concurrent");
+ ytdlp.OnProgressDownload += (sender, args) =>
+ Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA} - FRA {args.Fragments}");
+
await ytdlp.DownloadAsync(url);
}
@@ -250,18 +255,31 @@ private static async Task TestCancellationAsync(Ytdlp ytdlp)
var url = "https://www.youtube.com/watch?v=zGlwuHqGVIA"; // A longer video
var cts = new CancellationTokenSource();
+
var downloadTask = ytdlp
.WithFormat("b")
.WithOutputTemplate("%(title)s.%(ext)s")
.WithOutputFolder("./downloads/cancel")
.DownloadAsync(url, cts.Token);
+ ytdlp.OnCommandCompleted += (sender, args) =>
+ {
+ if (args.Success)
+ Console.WriteLine("Download completed successfully.");
+ else if (args.Message.Contains("cancelled", StringComparison.OrdinalIgnoreCase))
+ Console.WriteLine("Download was cancelled.");
+ else
+ Console.WriteLine($"Download failed: {args.Message}");
+ };
+
// Simulate cancel after 20 seconds
await Task.Delay(20000);
cts.Cancel();
try
{
+
+
await downloadTask;
}
catch (OperationCanceledException)
diff --git a/src/Ytdlp.NET/Core/DownloadRunner.cs b/src/Ytdlp.NET/Core/DownloadRunner.cs
index ddee76c..32db8d1 100644
--- a/src/Ytdlp.NET/Core/DownloadRunner.cs
+++ b/src/Ytdlp.NET/Core/DownloadRunner.cs
@@ -1,6 +1,4 @@
-using System.Diagnostics;
-
-namespace ManuHub.Ytdlp.NET.Core;
+namespace ManuHub.Ytdlp.NET.Core;
public sealed class DownloadRunner
{
@@ -88,9 +86,7 @@ void Complete(bool success, string message)
// Ensure process is dead
if (!process.HasExited)
- {
- ProcessFactory.SafeKill(process, _logger);
- }
+ ProcessFactory.SafeKill(process);
try
{
@@ -98,7 +94,8 @@ void Complete(bool success, string message)
}
catch (OperationCanceledException)
{
- ProcessFactory.SafeKill(process, _logger);
+ if (!process.HasExited)
+ ProcessFactory.SafeKill(process);
}
var success = process.ExitCode == 0 && !ct.IsCancellationRequested;
diff --git a/src/Ytdlp.NET/Core/ProcessFactory.cs b/src/Ytdlp.NET/Core/ProcessFactory.cs
index b62e7af..74d39fc 100644
--- a/src/Ytdlp.NET/Core/ProcessFactory.cs
+++ b/src/Ytdlp.NET/Core/ProcessFactory.cs
@@ -1,4 +1,5 @@
-using System.Diagnostics;
+using ManuHub.Ytdlp.NET.Extensions;
+using System.Diagnostics;
using System.Text;
namespace ManuHub.Ytdlp.NET.Core;
@@ -24,7 +25,6 @@ public Process Create(string arguments)
FileName = _ytdlpPath,
Arguments = arguments,
- // Must for async/event-based reading
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
@@ -75,18 +75,15 @@ public static void SafeKill(Process process, ILogger? logger = null)
if (process.HasExited)
return;
- // Close streams first → prevents ReadLine hang
- try { process.StandardOutput?.Close(); } catch { }
- try { process.StandardError?.Close(); } catch { }
- try { process.StandardInput?.Close(); } catch { }
+ process.KillTree();
- process.Kill(entireProcessTree: true);
-
- logger?.Log(LogType.Info, "Process killed (entire tree)");
+ if(logger != null)
+ logger.Log(LogType.Info, "Process killed (entire tree)");
}
catch (Exception ex)
{
- logger?.Log(LogType.Error, $"Failed to kill process: {ex.Message}");
+ if(logger != null)
+ logger.Log(LogType.Error, $"Failed to kill process: {ex.Message}");
}
}
}
\ No newline at end of file
diff --git a/src/Ytdlp.NET/Extensions/ProcessExtensions.cs b/src/Ytdlp.NET/Extensions/ProcessExtensions.cs
new file mode 100644
index 0000000..5527dde
--- /dev/null
+++ b/src/Ytdlp.NET/Extensions/ProcessExtensions.cs
@@ -0,0 +1,109 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ManuHub.Ytdlp.NET.Extensions;
+
+///
+/// Process extensions for killing full process tree.
+///
+internal static class ProcessExtensions
+{
+ private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
+
+ public static void KillTree(this Process process)
+ {
+ process.KillTree(_defaultTimeout);
+ }
+
+ public static void KillTree(this Process process, TimeSpan timeout)
+ {
+ string stdout;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ RunProcessAndWaitForExit(
+ "taskkill",
+ $"/T /F /PID {process.Id}",
+ timeout,
+ out stdout);
+ }
+ else
+ {
+ var children = new HashSet();
+ GetAllChildIdsUnix(process.Id, children, timeout);
+ foreach (var childId in children)
+ {
+ KillProcessUnix(childId, timeout);
+ }
+ KillProcessUnix(process.Id, timeout);
+ }
+ }
+
+ private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout)
+ {
+ string stdout;
+ var exitCode = RunProcessAndWaitForExit(
+ "pgrep",
+ $"-P {parentId}",
+ timeout,
+ out stdout);
+
+ if (exitCode == 0 && !string.IsNullOrEmpty(stdout))
+ {
+ using (var reader = new StringReader(stdout))
+ {
+ while (true)
+ {
+ var text = reader.ReadLine();
+ if (text == null)
+ {
+ return;
+ }
+
+ int id;
+ if (int.TryParse(text, out id))
+ {
+ children.Add(id);
+ GetAllChildIdsUnix(id, children, timeout);
+ }
+ }
+ }
+ }
+ }
+
+ private static void KillProcessUnix(int processId, TimeSpan timeout)
+ {
+ string stdout;
+ RunProcessAndWaitForExit(
+ "kill",
+ $"-TERM {processId}",
+ timeout,
+ out stdout);
+ }
+
+ private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ var process = Process.Start(startInfo);
+
+ stdout = null;
+ if (process.WaitForExit((int)timeout.TotalMilliseconds))
+ {
+ stdout = process.StandardOutput.ReadToEnd();
+ }
+ else
+ {
+ process.Kill();
+ }
+
+ return process.ExitCode;
+ }
+}
+
diff --git a/src/Ytdlp.NET/Parsing/ProgressParser.cs b/src/Ytdlp.NET/Parsing/ProgressParser.cs
index 865b645..1dcf906 100644
--- a/src/Ytdlp.NET/Parsing/ProgressParser.cs
+++ b/src/Ytdlp.NET/Parsing/ProgressParser.cs
@@ -183,7 +183,7 @@ private void HandleDownloadProgress(Match match)
{
// Existing logic unchanged
string percentString = match.Groups["percent"].Value;
- string sizeString = match.Groups["size"].Value;
+ string sizeString = match.Groups["total"].Value;
string speedString = match.Groups["speed"].Value;
string etaString = match.Groups["eta"].Value;
diff --git a/src/Ytdlp.NET/Parsing/RegexPatterns.cs b/src/Ytdlp.NET/Parsing/RegexPatterns.cs
index 3ef0751..f1dca10 100644
--- a/src/Ytdlp.NET/Parsing/RegexPatterns.cs
+++ b/src/Ytdlp.NET/Parsing/RegexPatterns.cs
@@ -17,7 +17,8 @@ internal static class RegexPatterns
public const string DownloadDestination = @"\[download\]\s*Destination:\s*(?.+)";
public const string ResumeDownload = @"\[download\]\s*Resuming download at byte\s*(?\d+)";
public const string DownloadAlreadyDownloaded = @"\[download\]\s*(?[^\n]+?)\s*has already been downloaded";
- public const string DownloadProgress = @"\[download\]\s*(?\d+\.\d+)%\s*of\s*(?[^\s]+)\s*at\s*(?[^\s]+)\s*ETA\s*(?[^\s]+)";
+ public const string DownloadProgress = @"\[download\]\s+(?:(?[\d\.]+)%(?:\s+of\s+\~?\s*(?[\d\.\w]+))?\s+at\s+(?:(?[\d\.\w]+\/s)|[\w\s]+)\s+ETA\s(?[\d\:]+))?";
+ //@"\[download\]\s*(?\d+\.\d+)%\s*of\s*(?[^\s]+)\s*at\s*(?[^\s]+)\s*ETA\s*(?[^\s]+)";
public const string DownloadProgressWithFrag = @"\[download\]\s*(?\d+\.\d+)%\s*of\s*(~?\s*(?[^\s]+))\s*at\s*(?[^\s]+)\s*ETA\s*(?[^\s]+)\s*\(frag\s*(?\d+/\d+)\)";
public const string DownloadProgressComplete = @"\[download\]\s*(?100(?:\.0)?)%\s*of\s*(?[^\s]+)\s*at\s*(?[^\s]+|Unknown)\s*ETA\s*(?[^\s]+|Unknown)";
public const string UnknownError = @"\[download\]\s*Unknown error";
@@ -27,8 +28,6 @@ internal static class RegexPatterns
public const string SpecificError = @"\[(?[^\]]+)\]\s*(?[^\s:]+):\s*ERROR:\s*(?.+)";
public const string DownloadingSubtitles = @"\[info\]\s*Downloading subtitles:\s*(?[^\s]+)";
- // ───────────── New / Enhanced Patterns for v2.0 ─────────────
-
// More reliable merger success detection (variation of "successfully merged")
public const string MergerSuccess = @"(?:has been successfully merged|merged formats successfully)";
diff --git a/src/Ytdlp.NET/README.md b/src/Ytdlp.NET/README.md
index f5124e3..438a86b 100644
--- a/src/Ytdlp.NET/README.md
+++ b/src/Ytdlp.NET/README.md
@@ -239,7 +239,7 @@ await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);
* `.WithJsRuntime(Runtime runtime, string runtimePath)`
* `.WithNoJsRuntime()`
* `.WithFlatPlaylist()`
-* ` WithLiveFromStart()`
+* `.WithLiveFromStart()`
* `.WithWaitForVideo(TimeSpan? maxWait = null)`
* `.WithMarkWatched()`
@@ -436,9 +436,16 @@ await ytdlp.DownloadAsync(url);
| `SetFFMpegLocation()` | `WithFFmpegLocation()` |
| `ExtractAudio()` | `WithExtractAudio()` |
| `UseProxy()` | `WithProxy()` |
+| `AddCustomCommand()` | `AddFlag(string flag)` or `AddOption(string key, string value)` |
---
+## Custom commands
+```csharp
+AddFlag("--no-check-certificate");
+AddOption("--external-downloader", "aria2c");
+```
+
## Important behavior changes
### Instances are immutable
diff --git a/src/Ytdlp.NET/Ytdlp.NET.csproj b/src/Ytdlp.NET/Ytdlp.NET.csproj
index a1ce2ce..cb1874d 100644
--- a/src/Ytdlp.NET/Ytdlp.NET.csproj
+++ b/src/Ytdlp.NET/Ytdlp.NET.csproj
@@ -7,7 +7,7 @@
Ytdlp.NET
Ytdlp.NET
ManuHub.Ytdlp.NET
- 3.0.1
+ 3.0.2
ManuHub Manojbabu
A .NET wrapper for yt-dlp with advanced features like concurrent downloads, SponsorBlock, and improved format parsing
© 2025-2026 ManuHub. Allrights researved
diff --git a/src/Ytdlp.NET/Ytdlp.cs b/src/Ytdlp.NET/Ytdlp.cs
index 5554e3b..420979a 100644
--- a/src/Ytdlp.NET/Ytdlp.cs
+++ b/src/Ytdlp.NET/Ytdlp.cs
@@ -1,5 +1,6 @@
using ManuHub.Ytdlp.NET.Core;
using System.Collections.Immutable;
+using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -1366,6 +1367,9 @@ public async Task> ExtractorsAsync(CancellationToken ct = default,
$"--no-warnings " +
$"{Quote(url)}";
+ if(ct.IsCancellationRequested)
+ Debug.WriteLine("Cancellation requested before starting the process.");
+
var json = await Probe().RunAsync(arguments, ct, tuneProcess, bufferKb);
if (string.IsNullOrWhiteSpace(json))