diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs index c2f7f92..78487ac 100644 --- a/src/Ytdlp.NET.Console/Program.cs +++ b/src/Ytdlp.NET.Console/Program.cs @@ -180,19 +180,20 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase) Console.WriteLine(ytdlp.Preview(url)); - // await ytdlp.ExecuteAsync(url); + await ytdlp.DownloadAsync(url); } - private static async Task TestDownloadAudioAsync(Ytdlp ytdlp) + private static async Task TestDownloadAudioAsync(Ytdlp ytdlpBase) { Console.WriteLine("\nTest 7: Extracting audio..."); var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; - await ytdlp + var ytdlp = ytdlpBase .WithExtractAudio(AudioFormat.Mp3) .WithFormat("ba") - .WithOutputFolder("./downloads/audio") - .DownloadAsync(url); + .WithOutputFolder("./downloads/audio"); + + await ytdlp.DownloadAsync(url); } // Test 8: Batch download (concurrent) @@ -214,30 +215,32 @@ private static async Task TestBatchDownloadAsync(Ytdlp baseYtdlp) } // Test 9: SponsorBlock removal - private static async Task TestSponsorBlockAsync(Ytdlp ytdlp) + private static async Task TestSponsorBlockAsync(Ytdlp ytdlpBase) { Console.WriteLine("\nTest 9: Download with SponsorBlock removal..."); var url = "https://www.youtube.com/watch?v=oDSEGkT6J-0"; - await ytdlp + var ytdlp = ytdlpBase .WithFormat("best") .WithSponsorblockRemove("all") // Removes sponsor, intro, etc. - .WithOutputFolder("./downloads/sponsorblock") - .DownloadAsync(url); + .WithOutputFolder("./downloads/sponsorblock"); + + await ytdlp.DownloadAsync(url); } // Test 10: Concurrent fragments (faster download) - private static async Task TestConcurrentFragmentsAsync(Ytdlp ytdlp) + private static async Task TestConcurrentFragmentsAsync(Ytdlp ytdlpBase) { Console.WriteLine("\nTest 10: Download with concurrent fragments..."); var url = "https://www.youtube.com/watch?v=oDSEGkT6J-0"; - await ytdlp + var ytdlp = ytdlpBase .WithConcurrentFragments(8) // 8 parallel fragments .WithFormat("b") .WithOutputTemplate("%(title)s.%(ext)s") - .WithOutputFolder("./downloads/concurrent") - .DownloadAsync(url); + .WithOutputFolder("./downloads/concurrent"); + + await ytdlp.DownloadAsync(url); } // Test 11: Cancellation support diff --git a/src/Ytdlp.NET/Core/DownloadRunner.cs b/src/Ytdlp.NET/Core/DownloadRunner.cs index 4c29589..ddee76c 100644 --- a/src/Ytdlp.NET/Core/DownloadRunner.cs +++ b/src/Ytdlp.NET/Core/DownloadRunner.cs @@ -1,4 +1,6 @@ -namespace ManuHub.Ytdlp.NET.Core; +using System.Diagnostics; + +namespace ManuHub.Ytdlp.NET.Core; public sealed class DownloadRunner { @@ -17,82 +19,109 @@ public DownloadRunner(ProcessFactory factory, ProgressParser parser, ILogger log _logger = logger; } - public async Task RunAsync(string arguments, CancellationToken ct) + public async Task RunAsync(string arguments, CancellationToken ct, bool tuneProcess = true) { - var process = _factory.Create(arguments); + 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 { - if (!process.Start()) - throw new YtdlpException("Failed to start yt-dlp process."); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // ✅ Attach BEFORE Start (fix race condition) + process.Exited += (_, _) => tcs.TrySetResult(true); - // Improved cancellation: Try to close streams first, then kill - using var ctsRegistration = ct.Register(() => + process.OutputDataReceived += (s, e) => { + if (e.Data == null) return; + try { - if (!process.HasExited) - { - process.Kill(entireProcessTree: true); - _logger.Log(LogType.Info, "yt-dlp process killed due to cancellation"); - } + _progressParser.ParseProgress(e.Data); + OnProgress?.Invoke(this, e.Data); } - catch + catch (Exception ex) { - // silent - already dead or disposed + _logger.Log(LogType.Error, $"Parse error: {ex.Message}"); } - }); + }; - // Read output and error concurrently - var outputTask = Task.Run(async () => + process.ErrorDataReceived += (s, e) => { - string? line; - while ((line = await process.StandardOutput.ReadLineAsync()) != null) - { - ct.ThrowIfCancellationRequested(); - _progressParser.ParseProgress(line); - OnProgress?.Invoke(this, line); - } - }, ct); + if (e.Data == null) return; + + OnErrorMessage?.Invoke(this, e.Data); + _logger.Log(LogType.Error, e.Data); + }; - var errorTask = Task.Run(async () => + 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(() => { - string? line; - while ((line = await process.StandardError.ReadLineAsync()) != null) + if (!process.HasExited) { - ct.ThrowIfCancellationRequested(); - OnErrorMessage?.Invoke(this, line); - _logger.Log(LogType.Error, line); + _logger.Log(LogType.Info, "Cancellation requested → killing process tree"); + ProcessFactory.SafeKill(process, _logger); } - }, ct); + }); - await Task.WhenAll(outputTask, errorTask); + // Wait for exit OR cancellation + await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, ct)); - // Wait for exit (may throw OperationCanceledException) - await process.WaitForExitAsync(ct); + // Ensure process is dead + if (!process.HasExited) + { + ProcessFactory.SafeKill(process, _logger); + } - // Only throw on real failure (not cancellation) - if (process.ExitCode != 0 && !ct.IsCancellationRequested) + try { - throw new YtdlpException($"yt-dlp exited with code {process.ExitCode}"); + await process.WaitForExitAsync(ct); } + catch (OperationCanceledException) + { + ProcessFactory.SafeKill(process, _logger); + } + + var success = process.ExitCode == 0 && !ct.IsCancellationRequested; + + var message = success + ? "Completed successfully" + : ct.IsCancellationRequested + ? "Cancelled by user" + : $"Failed with exit code {process.ExitCode}"; - // Success or intentional cancel - var success = !ct.IsCancellationRequested; - var message = success ? "Completed successfully" : "Cancelled by user"; - OnCommandCompleted?.Invoke(this, new CommandCompletedEventArgs(success, message)); + Complete(success, message); } catch (OperationCanceledException) { - // Normal cancel path — no need to log again - OnCommandCompleted?.Invoke(this, new CommandCompletedEventArgs(false, "Cancelled by user")); - throw; // let caller handle if needed + Complete(false, "Cancelled by user"); + throw; } catch (Exception ex) { var msg = $"Error executing yt-dlp: {ex.Message}"; - OnErrorMessage?.Invoke(this, msg); _logger.Log(LogType.Error, msg); + OnErrorMessage?.Invoke(this, msg); + throw new YtdlpException(msg, ex); } } diff --git a/src/Ytdlp.NET/Core/ProbeRunner.cs b/src/Ytdlp.NET/Core/ProbeRunner.cs index 69b3338..50bbde4 100644 --- a/src/Ytdlp.NET/Core/ProbeRunner.cs +++ b/src/Ytdlp.NET/Core/ProbeRunner.cs @@ -7,62 +7,121 @@ public sealed class ProbeRunner private readonly ProcessFactory _factory; private readonly ILogger _logger; + public event EventHandler? OnOutput; // optional: for live output if needed + public event EventHandler? OnErrorMessage; + public event EventHandler? OnCommandCompleted; + public ProbeRunner(ProcessFactory factory, ILogger logger) { _factory = factory; _logger = logger; } - public async Task RunAsync(string args, CancellationToken ct = default, int bufferKb = 128) + public async Task RunAsync(string args, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { - var process = _factory.Create(args); + if (string.IsNullOrWhiteSpace(args)) + throw new ArgumentException("Arguments cannot be empty", nameof(args)); - // Validate buffer size: minimum 8 KB - if (bufferKb < 8) bufferKb = 8; + // Reasonable buffer: 256 KB default (good for large JSON), min 64 KB + if (bufferKb < 64) bufferKb = 64; int bufferSize = bufferKb * 1024; - try + using var process = _factory.Create(args); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + int completed = 0; + + void Complete(bool success, string message) { - process.Start(); - - // Use StreamReader with large buffer + explicit UTF-8 - string output; - using (var reader = new StreamReader(process.StandardOutput.BaseStream, - Encoding.UTF8, - detectEncodingFromByteOrderMarks: false, - bufferSize: bufferSize, // default 8kb for JSON - leaveOpen: true)) // don't close underlying stream + if (Interlocked.Exchange(ref completed, 1) == 0) { - output = await reader.ReadToEndAsync(); + OnCommandCompleted?.Invoke(this, new CommandCompletedEventArgs(success, message)); } + } - // Optional: drain stderr in background (prevents blocking if warnings are many) - _ = Task.Run(() => process.StandardError.ReadToEndAsync(), ct); + try + { + // Attach Exited handler BEFORE starting + process.Exited += (_, _) => tcs.TrySetResult(true); - using (ct.Register(() => - { - try { if (!process.HasExited) process.Kill(true); } catch { } - })) + // Handle stderr (warnings, errors, verbose info from yt-dlp) + process.ErrorDataReceived += (s, e) => { - await process.WaitForExitAsync(ct); - } + if (!string.IsNullOrEmpty(e.Data)) + { + OnErrorMessage?.Invoke(this, e.Data); + _logger.Log(LogType.Warning, e.Data); + } + }; + + if (!process.Start()) + throw new YtdlpException("Failed to start yt-dlp probe process."); + + if (tuneProcess) + ProcessFactory.Tune(process); - if (string.IsNullOrWhiteSpace(output)) + process.BeginErrorReadLine(); // Only stderr uses events + + // 🔥 Large-buffered reader for heavy JSON on stdout + using var reader = new StreamReader( + process.StandardOutput.BaseStream, + Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + bufferSize: bufferSize, + leaveOpen: true); + + // Start reading asynchronously + var readTask = reader.ReadToEndAsync(ct); // Pass ct for better cancellation support + + // Cancellation support + using var registration = ct.Register(() => { - _logger.Log(LogType.Warning, "Empty output."); - return null; - } + if (!process.HasExited) + { + _logger.Log(LogType.Info, "Probe cancellation requested → SafeKill"); + ProcessFactory.SafeKill(process, _logger); + } + }); + + // Wait for either process exit, read completion, or cancellation + await Task.WhenAny(tcs.Task, readTask, Task.Delay(Timeout.Infinite, ct)); + + // Ensure process is terminated if still running + if (!process.HasExited) + ProcessFactory.SafeKill(process, _logger); + + // Wait for clean exit + await process.WaitForExitAsync(ct); + + // Get the full output + string output = await readTask; + + // Determine success + bool success = process.ExitCode == 0 + && !ct.IsCancellationRequested + && !string.IsNullOrWhiteSpace(output); + + string message = success ? "Probe completed successfully" : + ct.IsCancellationRequested ? "Probe cancelled by user" : + $"Probe failed with exit code {process.ExitCode}"; + + Complete(success, message); - return output; + // Return trimmed output only on real success + return success ? output.Trim() : null; } catch (OperationCanceledException) { - _logger.Log(LogType.Warning, "Process cancelled."); + Complete(false, "Probe cancelled by user"); + _logger.Log(LogType.Warning, "Probe was cancelled."); return null; } catch (Exception ex) { - _logger.Log(LogType.Warning, $"Process failed: {ex.Message}"); + var msg = $"Error executing yt-dlp probe: {ex.Message}"; + _logger.Log(LogType.Warning, msg); + OnErrorMessage?.Invoke(this, msg); + Complete(false, msg); return null; } } diff --git a/src/Ytdlp.NET/Core/ProcessFactory.cs b/src/Ytdlp.NET/Core/ProcessFactory.cs index 0b1ca9e..b62e7af 100644 --- a/src/Ytdlp.NET/Core/ProcessFactory.cs +++ b/src/Ytdlp.NET/Core/ProcessFactory.cs @@ -6,31 +6,87 @@ namespace ManuHub.Ytdlp.NET.Core; public sealed class ProcessFactory { private readonly string _ytdlpPath; + private readonly string _workingDirectory; - public ProcessFactory(string ytdlpPath) + public ProcessFactory(string ytdlpPath, string? workingDirectory = null) { - _ytdlpPath = ytdlpPath; + _ytdlpPath = ytdlpPath ?? throw new ArgumentNullException(nameof(ytdlpPath)); + _workingDirectory = workingDirectory ?? Environment.CurrentDirectory; } public Process Create(string arguments) { + if (string.IsNullOrWhiteSpace(arguments)) + throw new ArgumentException("Arguments cannot be empty", nameof(arguments)); + var psi = new ProcessStartInfo { FileName = _ytdlpPath, Arguments = arguments, + + // Must for async/event-based reading RedirectStandardOutput = true, RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 + StandardErrorEncoding = Encoding.UTF8, + + WorkingDirectory = _workingDirectory }; + // Force consistent encoding (yt-dlp / python) psi.Environment["PYTHONIOENCODING"] = "utf-8"; psi.Environment["PYTHONUTF8"] = "1"; psi.Environment["LC_ALL"] = "en_US.UTF-8"; psi.Environment["LANG"] = "en_US.UTF-8"; - return new Process { StartInfo = psi, EnableRaisingEvents = true }; + var process = new Process + { + StartInfo = psi, + EnableRaisingEvents = true + }; + + return process; + } + + public static void Tune(Process process) + { + try + { + if (!process.HasExited) + { + process.PriorityClass = ProcessPriorityClass.BelowNormal; + } + } + catch + { + // Ignore platform-specific failures + } + } + + public static void SafeKill(Process process, ILogger? logger = null) + { + try + { + 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.Kill(entireProcessTree: true); + + logger?.Log(LogType.Info, "Process killed (entire tree)"); + } + catch (Exception ex) + { + logger?.Log(LogType.Error, $"Failed to kill process: {ex.Message}"); + } } } \ No newline at end of file diff --git a/src/Ytdlp.NET/Ytdlp.NET.csproj b/src/Ytdlp.NET/Ytdlp.NET.csproj index b4b161d..a1ce2ce 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.0 + 3.0.1 ManuHub Manojbabu A .NET wrapper for yt-dlp with advanced features like concurrent downloads, SponsorBlock, and improved format parsing © 2025-2026 ManuHub. Allrights researved @@ -20,36 +20,42 @@ icon.png icon.png - Ytdlp.NET v3.0 + Ytdlp.NET v3.0 ✨ Major Redesign with Fluent API - ✨ Major Updates: - - Complete v3.0 redesign with immutable, fluent API (`WithXxx()` methods). - - Added IAsyncDisposable support for proper async cleanup of child processes. - - Thread-safe usage: safe for parallel downloads using multiple instances. - - Events fully supported: OnProgressDownload, OnProgressMessage, OnCompleteDownload, OnPostProcessingComplete, OnErrorMessage, OnCommandCompleted. - - All old command methods removed; only `WithXxx()` methods remain. - - Improved cancellation handling for downloads and metadata fetching. - - Enhanced metadata probe methods: GetMetadataAsync, GetAvailableFormatsAsync, GetBestVideoFormatIdAsync, GetBestAudioFormatIdAsync. - - Flexible output templates and FFmpeg/Deno integration. - - Custom command injection (`AddCustomCommand`) validated safely. + - Complete rewrite using an immutable, fluent API (`WithXxx()` methods). + - Added `IAsyncDisposable` support for proper async cleanup of processes. + - Thread-safe design: safe to run multiple instances in parallel. + - Rich event system: OnProgressDownload, OnProgressMessage, OnCompleteDownload, + OnPostProcessingComplete, OnErrorMessage, OnCommandCompleted. + - Improved cancellation handling for both downloads and metadata probing. + - Enhanced probe methods: GetMetadataAsync, GetAvailableFormatsAsync, + GetBestVideoFormatIdAsync, GetBestAudioFormatIdAsync. + - Better output templates, FFmpeg/Deno integration, and safe custom command support. - 🚀 Features: - - Fluent, chainable API for downloads, audio extraction, subtitles, and post-processing. - - Supports batch downloads with sequential or parallel execution. - - Real-time progress tracking with events. - - Automatic cleanup of resources when disposed asynchronously. - - Cross-platform: Windows, macOS, Linux (yt-dlp supported). + 🛠 Improvements in this update + - Better internal DownloadRunner and ProbeRunner for heavy JSON output and cancellation safety. + - Safer process killing and stream handling to prevent potential hangs. - ⚠️ Breaking Changes: - - Old SetFormat/SetOutputFolder methods removed; replace with `WithFormat()`, `WithOutputFolder()`, etc. - - Deprecated events/methods removed. - - Always use a new instance per download for parallel execution. - - Use `await using` with Ytdlp for async disposal. + 🚀 New Features + - Fully chainable fluent API for downloads, audio extraction, subtitles, post-processing and more. + - Batch download support (sequential or parallel execution). + - Real-time progress tracking via events. + - Automatic resource cleanup with `await using`. + - Cross-platform (Windows, macOS, Linux). - 🛠 Notes: - - Namespace migrated to `ManuHub.Ytdlp.NET`. - - Recommended: Use companion NuGet packages for yt-dlp, FFmpeg, FFprobe, and Deno. - - All examples updated for fluent v3.0 API. + ⚠️ Breaking Changes + - All old command methods (SetFormat, SetOutputFolder, etc.) have been removed. + - Only the new fluent `WithXxx()` style remains. + - Namespace changed to `ManuHub.Ytdlp.NET`. + - Always use a fresh instance per download for parallel scenarios. + - Use `await using` for proper async disposal. + + 🛠 Notes + - Companion packages recommended: ManuHub.Ytdlp, ManuHub.FFmpeg, ManuHub.FFprobe, ManuHub.Deno. + - All examples and documentation updated for v3.0. + - This release greatly improves reliability, cancellation, and maintainability. + + See the repository for the full migration guide. false true diff --git a/src/Ytdlp.NET/Ytdlp.cs b/src/Ytdlp.NET/Ytdlp.cs index e194998..5554e3b 100644 --- a/src/Ytdlp.NET/Ytdlp.cs +++ b/src/Ytdlp.NET/Ytdlp.cs @@ -1301,15 +1301,16 @@ public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stab /// /// List all supported extractors and exit /// - /// + /// + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// Buffer size in KB. /// List of extractor names - public async Task> ExtractorsAsync(CancellationToken ct = default, int bufferKb = 128) + public async Task> ExtractorsAsync(CancellationToken ct = default, bool tuneProcess= true, int bufferKb = 256) { try { List list = new(); - var result = await Probe().RunAsync("--list-extractors", ct, bufferKb); + var result = await Probe().RunAsync("--list-extractors", ct, tuneProcess, bufferKb); if (string.IsNullOrWhiteSpace(result)) { @@ -1341,13 +1342,14 @@ public async Task> ExtractorsAsync(CancellationToken ct = default, /// /// The source URL(video or playlist) to probe. /// The to abort the process. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// Buffer size in KB. /// /// A object containing the parsed metadata output; /// returns if the process fails, returns empty, or is cancelled. /// /// - public async Task GetMetadataAsync(string url, CancellationToken ct = default, int bufferKb = 128) + public async Task GetMetadataAsync(string url, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("URL cannot be empty.", nameof(url)); @@ -1364,7 +1366,7 @@ public async Task> ExtractorsAsync(CancellationToken ct = default, $"--no-warnings " + $"{Quote(url)}"; - var json = await Probe().RunAsync(arguments, ct); + var json = await Probe().RunAsync(arguments, ct, tuneProcess, bufferKb); if (string.IsNullOrWhiteSpace(json)) { @@ -1398,13 +1400,14 @@ public async Task> ExtractorsAsync(CancellationToken ct = default, /// /// The source URL (video or playlist) to probe. /// The to abort the process. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// The buffer size for the process output stream (default 128KB). /// /// A raw JSON containing the parsed metadata output; /// returns if the process fails, returns empty, or is cancelled. /// /// - public async Task GetMetadataRawAsync(string url, CancellationToken ct = default, int bufferKb = 128) + public async Task GetMetadataRawAsync(string url, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("URL cannot be empty.", nameof(url)); @@ -1421,7 +1424,7 @@ public async Task> ExtractorsAsync(CancellationToken ct = default, $"--no-warnings " + $"{Quote(url)}"; - var json = await Probe().RunAsync(arguments, ct); + var json = await Probe().RunAsync(arguments, ct, tuneProcess, bufferKb); if (string.IsNullOrWhiteSpace(json)) { @@ -1448,18 +1451,19 @@ public async Task> ExtractorsAsync(CancellationToken ct = default, /// /// The video or playlist URL to probe. /// The to abort the process. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// The buffer size in kilobytes for the process output stream (default 128KB). /// /// A containing all available streams; /// returns an empty list or if the probe fails or is cancelled. /// /// - public async Task> GetFormatsAsync(string url, CancellationToken ct = default, int bufferKb = 128) + public async Task> GetFormatsAsync(string url, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("Video URL cannot be empty.", nameof(url)); - var output = await Probe().RunAsync($"-F {Quote(url)}", ct, bufferKb); + var output = await Probe().RunAsync($"-F {Quote(url)}", ct,tuneProcess, bufferKb); if (string.IsNullOrWhiteSpace(output)) { @@ -1475,13 +1479,14 @@ public async Task> GetFormatsAsync(string url, CancellationToken ct /// /// The video or playlist URL to probe. /// The to abort the process. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// The buffer size in kilobytes for the process output stream (default 128KB). /// /// A object if successful; /// returns if the process fails or is cancelled. /// /// - public async Task GetMetadataLiteAsync(string url, CancellationToken ct = default, int bufferKb = 128) + public async Task GetMetadataLiteAsync(string url, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("URL cannot be empty.", nameof(url)); @@ -1506,7 +1511,7 @@ public async Task> GetFormatsAsync(string url, CancellationToken ct var arguments = $"{printArg} --skip-download --no-playlist --quiet {Quote(url)}"; - var output = await Probe().RunAsync(arguments, ct, bufferKb); + var output = await Probe().RunAsync(arguments, ct, tuneProcess, bufferKb); if (string.IsNullOrWhiteSpace(output)) return null; @@ -1545,13 +1550,14 @@ public async Task> GetFormatsAsync(string url, CancellationToken ct /// The source URL to probe. /// A collection of field names to extract (e.g., "title", "uploader"). /// A to abort the yt-dlp process. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// The buffer size in kilobytes for the process output (default 128KB). /// /// A containing the requested fields and their values; /// returns if the process fails, returns no data, or is cancelled. /// /// - public async Task?> GetMetadataLiteAsync(string url, IEnumerable fields, CancellationToken ct = default, int bufferKb = 128) + public async Task?> GetMetadataLiteAsync(string url, IEnumerable fields, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("URL cannot be empty.", nameof(url)); @@ -1569,7 +1575,7 @@ public async Task> GetFormatsAsync(string url, CancellationToken ct var arguments = $"--print \"{printFormat}\" --skip-download --no-playlist --quiet {Quote(url)}"; - var rawOutput = await Probe().RunAsync(arguments, ct, bufferKb); + var rawOutput = await Probe().RunAsync(arguments, ct, tuneProcess, bufferKb); if (string.IsNullOrWhiteSpace(rawOutput)) return null; @@ -1607,15 +1613,16 @@ public async Task> GetFormatsAsync(string url, CancellationToken ct /// /// The video or playlist URL to probe. /// The to abort the process. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// The buffer size in kilobytes for the process output stream (default 128KB). /// /// A representing the best audio format ID (e.g., "140"); /// returns an empty string or throws if no suitable audio is found. /// /// - public async Task GetBestAudioFormatIdAsync(string url, CancellationToken ct = default, int bufferKb = 128) + public async Task GetBestAudioFormatIdAsync(string url, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { - var meta = await GetMetadataAsync(url, ct, bufferKb); + var meta = await GetMetadataAsync(url, ct,tuneProcess, bufferKb); var best = meta?.Formats? .Where(f => f.IsAudio && (f.Abr > 0 || f.Tbr > 0)) .OrderByDescending(f => f.Abr ?? f.Tbr ?? 0) @@ -1630,15 +1637,16 @@ public async Task GetBestAudioFormatIdAsync(string url, CancellationToke /// The source URL to probe for video formats. /// The maximum vertical resolution allowed (default 1080p). /// A to cancel the underlying yt-dlp process. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// The buffer size in kilobytes for the process output (default 128KB). /// /// A representing the best video format ID (e.g., "137" or "248"); /// returns an empty string or if no suitable format is found. /// /// - public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = 1080, CancellationToken ct = default, int bufferKb = 128) + public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = 1080, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256) { - var meta = await GetMetadataAsync(url, ct, bufferKb); + var meta = await GetMetadataAsync(url, ct,tuneProcess, bufferKb); var best = meta?.Formats? .Where(f => !f.IsAudio && f.Height.HasValue && f.Height <= maxHeight) .OrderByDescending(f => f.Height) @@ -1653,10 +1661,11 @@ public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = /// /// The source URL to download. /// A to stop the execution. + /// Whether to tune the process for better performance (true by default). If false, the process will use the default buffer size and may have slower output processing. /// /// /// - public async Task DownloadAsync(string url, CancellationToken ct = default) + public async Task DownloadAsync(string url, CancellationToken ct = default, bool tuneProcess = true) { ct.ThrowIfCancellationRequested(); @@ -1712,7 +1721,7 @@ void OnProgressMessageHandler(object? s, string msg) try { - await download.RunAsync(arguments, ct); + await download.RunAsync(arguments, ct, tuneProcess); } finally { @@ -1728,11 +1737,12 @@ void OnProgressMessageHandler(object? s, string msg) /// An enumerable collection of source URLs to process. /// The maximum number of simultaneous yt-dlp processes (default is 3). /// A to stop the batch execution. + /// Whether to tune the processes for better performance (true by default). If false, the processes will use the default buffer size and may have slower output processing. /// /// A representing the asynchronous execution of the process. /// /// - public async Task DownloadBatchAsync(IEnumerable urls, int maxConcurrency = 3, CancellationToken ct = default) + public async Task DownloadBatchAsync(IEnumerable urls, int maxConcurrency = 3, CancellationToken ct = default, bool tuneProcess = true) { if (urls == null || !urls.Any()) { @@ -1747,7 +1757,7 @@ public async Task DownloadBatchAsync(IEnumerable urls, int maxConcurrenc await throttler.WaitAsync(); try { - await DownloadAsync(url, ct); + await DownloadAsync(url, ct, tuneProcess); } catch (YtdlpException ex) {