From 35bb0ecf99cd225f21697fd2dcbb147a58bbacc7 Mon Sep 17 00:00:00 2001 From: manusoft Date: Fri, 20 Mar 2026 05:28:51 +0400 Subject: [PATCH 1/8] Update all solution files and projects. --- README.md | 495 ++--- Ytdlp.NET.slnx | 21 + .../ClipMate.Lite}/App/Program.cs | 0 .../ClipMate.Lite/ClipMate.Lite.csproj | 2 +- .../ClipMate.Lite}/Core/AppLogger.cs | 0 .../ClipMate.Lite}/Core/DownloadSession.cs | 0 .../ClipMate.Lite}/Core/MediaFormat.cs | 0 .../ClipMate.Lite}/Core/YtdlpService.cs | 0 .../Properties/Resources.Designer.cs | 0 .../ClipMate.Lite}/Properties/Resources.resx | 0 .../ClipMate.Lite}/Resources/folder_24.png | Bin .../ClipMate.Lite}/Resources/icon.PNG | Bin .../ClipMate.Lite}/Resources/icon.ico | Bin .../ClipMate.Lite}/Resources/iconvd.PNG | Bin .../ClipMate.Lite}/Resources/search_24.png | Bin .../ClipMate.Lite}/UI/UIThread.cs | 0 .../ClipMate.Lite}/UI/frmMain.Designer.cs | 0 .../ClipMate.Lite}/UI/frmMain.cs | 0 .../ClipMate.Lite}/UI/frmMain.resx | 0 .../Ytdlp.Wrapper.Console}/Program.cs | 0 .../Ytdlp.Wrapper.Console.csproj | 8 +- .../Ytdlp.Wrapper}/AudioQuality.cs | 0 .../DownloadProgressEventArgs.cs | 0 .../Ytdlp.Wrapper}/LICENSE.txt | 0 .../Ytdlp.Wrapper}/LogType.cs | 0 .../Ytdlp.Wrapper}/Logger.cs | 0 .../Ytdlp.Wrapper}/NotUsingFunctions.cs | 0 .../Ytdlp.Wrapper}/ProgressParser.cs | 0 .../Ytdlp.Wrapper}/README.md | 0 .../Ytdlp.Wrapper}/RegexPatterns.cs | 0 .../Ytdlp.Wrapper}/StringExtensions.cs | 0 .../Ytdlp.Wrapper}/VideoFormat.cs | 0 .../Ytdlp.Wrapper}/VideoInfo.cs | 0 .../Ytdlp.Wrapper}/VideoQuality.cs | 0 .../Ytdlp.Wrapper}/YtDlpEngine.cs | 0 .../Ytdlp.Wrapper/Ytdlp.Wrapper.csproj | 2 +- .../Ytdlp.Wrapper}/Ytdlp.cs | 0 .../Ytdlp.Wrapper}/icon.PNG | Bin examples/01_DownloadVideo.cs | 23 + examples/02_ExtractAudio.cs | 21 + examples/03_BatchDownload.cs | 25 + examples/04_MetadataProbe.cs | 24 + examples/05_BestFormats.cs | 24 + examples/06_SponsorBlockRemove.cs | 18 + examples/07_ConcurrentFragments.cs | 18 + examples/08_CancelDownload.cs | 26 + src/Ytdlp.NET.Console/Program.cs | 103 +- .../Program.cs | 0 .../Ytdlp.NET.vNext.Console.csproj} | 2 +- .../Core/ProgressParser.cs | 0 .../Core/RegexPatterns.cs | 0 .../Core/UpdateChannel.cs | 0 .../DefaultLogger.cs | 0 .../DownloadProgressEventArgs.cs | 0 .../Helpers/FormatFilters.cs | 0 .../ILogger.cs | 0 .../LICENSE.txt | 0 .../LogType.cs | 0 .../Models/Format.cs | 0 .../Models/Metadata.cs | 0 .../Models/MetadataLite.cs | 0 .../README.md | 0 .../Ytdlp.NET.vNext.csproj} | 0 .../Ytdlp.cs | 0 .../YtdlpBuilder.cs | 0 .../YtdlpCommand.cs | 0 .../YtdlpException.cs | 0 .../YtdlpGeneral.cs | 0 .../YtdlpProbe.cs | 0 .../YtdlpRootBuilder.cs | 0 .../icon.png | Bin src/Ytdlp.NET/Models/SimpleMetadata.cs | 51 - src/Ytdlp.NET/Models/SingleVideoJson.cs | 41 - src/Ytdlp.NET/README.md | 481 ++--- src/Ytdlp.NET/Ytdlp.NET.csproj | 40 +- src/Ytdlp.NET/Ytdlp.cs | 1666 ++++++++--------- {src => tests}/Ytdlp.NET.ParseTest/Program.cs | 0 .../Ytdlp.NET.ParseTest/TestStrings.cs | 0 .../Ytdlp.NET.ParseTest.csproj | 0 .../Ytdlp.NET.ParseTest/YtdlpFormat.cs | 0 .../Ytdlp.NET.ParseTest/YtdlpFormatParser.cs | 0 {src => tests}/Ytdlp.NET.Test/BuilderTests.cs | 0 {src => tests}/Ytdlp.NET.Test/CommandTests.cs | 0 .../Obsolete/ParseFormatTest.cs | 0 .../Obsolete/ProgressParserTests.cs | 0 .../Ytdlp.NET.Test/Obsolete/TestConstants.cs | 0 .../Ytdlp.NET.Test/ProgressParserTests.cs | 0 .../Ytdlp.NET.Test/Ytdlp.NET.Test.csproj | 4 +- yt-dlp-wrapper.sln | 92 - 89 files changed, 1435 insertions(+), 1752 deletions(-) create mode 100644 Ytdlp.NET.slnx rename {src/VideoDownloader => apps/ClipMate.Lite}/App/Program.cs (100%) rename src/VideoDownloader/VideoDownloader.csproj => apps/ClipMate.Lite/ClipMate.Lite.csproj (94%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Core/AppLogger.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Core/DownloadSession.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Core/MediaFormat.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Core/YtdlpService.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Properties/Resources.Designer.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Properties/Resources.resx (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Resources/folder_24.png (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Resources/icon.PNG (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Resources/icon.ico (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Resources/iconvd.PNG (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/Resources/search_24.png (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/UI/UIThread.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/UI/frmMain.Designer.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/UI/frmMain.cs (100%) rename {src/VideoDownloader => apps/ClipMate.Lite}/UI/frmMain.resx (100%) rename {src/ConsoleApp.Test => archives/Ytdlp.Wrapper.Console}/Program.cs (100%) rename src/ConsoleApp.Test/ConsoleApp.Test.csproj => archives/Ytdlp.Wrapper.Console/Ytdlp.Wrapper.Console.csproj (50%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/AudioQuality.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/DownloadProgressEventArgs.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/LICENSE.txt (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/LogType.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/Logger.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/NotUsingFunctions.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/ProgressParser.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/README.md (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/RegexPatterns.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/StringExtensions.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/VideoFormat.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/VideoInfo.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/VideoQuality.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/YtDlpEngine.cs (100%) rename src/YtDlpWrapper/YtDlpWrapper.csproj => archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj (95%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/Ytdlp.cs (100%) rename {src/YtDlpWrapper => archives/Ytdlp.Wrapper}/icon.PNG (100%) create mode 100644 examples/01_DownloadVideo.cs create mode 100644 examples/02_ExtractAudio.cs create mode 100644 examples/03_BatchDownload.cs create mode 100644 examples/04_MetadataProbe.cs create mode 100644 examples/05_BestFormats.cs create mode 100644 examples/06_SponsorBlockRemove.cs create mode 100644 examples/07_ConcurrentFragments.cs create mode 100644 examples/08_CancelDownload.cs rename src/{Ytdlp.NET.v3.Console => Ytdlp.NET.vNext.Console}/Program.cs (100%) rename src/{Ytdlp.NET.v3.Console/Ytdlp.NET.v3.Console.csproj => Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj} (88%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Core/ProgressParser.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Core/RegexPatterns.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Core/UpdateChannel.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/DefaultLogger.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/DownloadProgressEventArgs.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Helpers/FormatFilters.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/ILogger.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/LICENSE.txt (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/LogType.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Models/Format.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Models/Metadata.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Models/MetadataLite.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/README.md (100%) rename src/{Ytdlp.NET.v3/Ytdlp.NET.v3.csproj => Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj} (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/Ytdlp.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/YtdlpBuilder.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/YtdlpCommand.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/YtdlpException.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/YtdlpGeneral.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/YtdlpProbe.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/YtdlpRootBuilder.cs (100%) rename src/{Ytdlp.NET.v3 => Ytdlp.NET.vNext}/icon.png (100%) delete mode 100644 src/Ytdlp.NET/Models/SimpleMetadata.cs delete mode 100644 src/Ytdlp.NET/Models/SingleVideoJson.cs rename {src => tests}/Ytdlp.NET.ParseTest/Program.cs (100%) rename {src => tests}/Ytdlp.NET.ParseTest/TestStrings.cs (100%) rename {src => tests}/Ytdlp.NET.ParseTest/Ytdlp.NET.ParseTest.csproj (100%) rename {src => tests}/Ytdlp.NET.ParseTest/YtdlpFormat.cs (100%) rename {src => tests}/Ytdlp.NET.ParseTest/YtdlpFormatParser.cs (100%) rename {src => tests}/Ytdlp.NET.Test/BuilderTests.cs (100%) rename {src => tests}/Ytdlp.NET.Test/CommandTests.cs (100%) rename {src => tests}/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs (100%) rename {src => tests}/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs (100%) rename {src => tests}/Ytdlp.NET.Test/Obsolete/TestConstants.cs (100%) rename {src => tests}/Ytdlp.NET.Test/ProgressParserTests.cs (100%) rename {src => tests}/Ytdlp.NET.Test/Ytdlp.NET.Test.csproj (83%) delete mode 100644 yt-dlp-wrapper.sln diff --git a/README.md b/README.md index bb16a44..dec51b9 100644 --- a/README.md +++ b/README.md @@ -1,362 +1,367 @@ -![Static Badge](https://img.shields.io/badge/ytdlp.NET-red) ![NuGet Version](https://img.shields.io/nuget/v/ytdlp.net) ![NuGet Downloads](https://img.shields.io/nuget/dt/ytdlp.net) +# ![Static Badge](https://img.shields.io/badge/ytdlp.NET-red) ![NuGet Version](https://img.shields.io/nuget/v/ytdlp.net) ![NuGet Downloads](https://img.shields.io/nuget/dt/ytdlp.net) + ![Visitors](https://visitor-badge.laobi.icu/badge?page_id=manusoft/yt-dlp-wrapper) # Ytdlp.NET + ![icon](https://github.com/user-attachments/assets/2147c398-4e0f-43e2-99cb-32b34be7dc2f) ### ClipMate - MAUI.NET App - [Download](https://apps.microsoft.com/detail/9NTP1DH4CQ4X?hl=en&gl=IN&ocid=pdpshare) -image +image +--- ### Video Downloader - .NET App + ![Screenshot 2025-01-23 153252](https://github.com/user-attachments/assets/1b977927-ea26-4220-bd41-9f64d6716058) [Download the latest App](https://github.com/manusoft/yt-dlp-wrapper/releases/download/v1.0.0/gui-app.zip) +--- -A .NET wrapper for the `yt-dlp` command-line tool, providing a fluent interface to build and execute commands for downloading videos, audio, subtitles, thumbnails, and more from YouTube and other supported platforms. `Ytdlp.NET` simplifies interaction with `yt-dlp` by offering a strongly-typed API, progress parsing, and event-based feedback for real-time monitoring. +# Ytdlp.NET -## Features +**Ytdlp.NET** is a modern **.NET wrapper for `yt-dlp`** that provides a fluent, strongly‑typed API for downloading videos, extracting audio, fetching metadata, and monitoring progress. -- **Fluent Interface**: Build `yt-dlp` commands with a chainable, intuitive API. -- **Progress Tracking**: Parse and monitor download progress, errors, and completion events. -- **Batch Downloading**: Download multiple videos or playlists in a single operation. -- **Format Selection**: Easily select video/audio formats, resolutions, and other options. -- **Event-Driven**: Subscribe to events for progress, errors, and command completion. -- **Customizable**: Support for custom `yt-dlp` options and advanced configurations. -- **Cross-Platform**: Compatible with Windows, macOS, and Linux (requires `yt-dlp` installed). +The library exposes **event‑driven progress reporting**, **metadata probing**, and **safe command construction** while staying very close to the native `yt-dlp` functionality. -# Ytdlp.NET API Documentation +--- -**Namespace**: `YtdlpNET` -**Main Class**: `Ytdlp` (fluent wrapper around yt-dlp) +# ✨ Features -The `Ytdlp` class provides a fluent, chainable API to build yt-dlp commands, fetch metadata, list formats, and execute downloads with rich event support and progress tracking. +* Fluent API (`WithXxx()` methods) +* Immutable design (thread‑safe instances) +* Real‑time progress events +* Metadata & format probing +* Batch downloads +* Cancellation support +* Cross‑platform support +* Strongly typed event system +* Async execution +* `IAsyncDisposable` support -## Constructor +--- -```csharp -public Ytdlp(string ytDlpPath = "yt-dlp", ILogger? logger = null) -``` +# 🚀 New in v3.0 -- **ytDlpPath**: Path to yt-dlp executable (default: searches PATH for "yt-dlp"). -- **logger**: Optional logger (falls back to `DefaultLogger`). +Major redesign for reliability and modern .NET usage. -**Throws**: `YtdlpException` if executable is not found. +### Highlights -## Events +* Immutable **fluent builder API** +* `IAsyncDisposable` implemented +* Thread‑safe usage +* Simplified event handling +* Improved metadata probing +* Better cancellation support +* Safer command building -All events are invoked on the UI/main thread if possible (when used in UI apps). +--- -| Event | Type | Description | -|------------------------------|---------------------------------------|-------------| -| `OnProgress` | `EventHandler` | General progress line from stdout | -| `OnError` | `EventHandler` | Error line from stderr | -| `OnCommandCompleted` | `EventHandler` | Process finished (success/failure/cancel) | -| `OnOutputMessage` | `EventHandler` | Every stdout line | -| `OnProgressDownload` | `EventHandler` | Parsed download progress (% / speed / ETA) | -| `OnCompleteDownload` | `EventHandler` | Single file download completed | -| `OnProgressMessage` | `EventHandler` | Info messages (merging, extracting, etc.) | -| `OnErrorMessage` | `EventHandler` | Error/info messages from parser | -| `OnPostProcessingComplete` | `EventHandler` | Post-processing (merge, convert) finished | +# 🔧 Required Tools -## Core Methods +`yt-dlp` relies on external tools. -### Output & Path Configuration +Recommended folder structure: -```csharp -Ytdlp SetOutputFolder(string outputFolderPath) -Ytdlp SetTempFolder(string tempFolderPath) -Ytdlp SetHomeFolder(string homeFolderPath) -Ytdlp SetOutputTemplate(string template) -Ytdlp SetFFMpegLocation(string ffmpegFolder) +``` +tools/ +├─ yt-dlp.exe +├─ ffmpeg.exe +├─ ffprobe.exe +└─ deno.exe ``` -### Format Selection +Example usage: ```csharp -Ytdlp SetFormat(string format) -Ytdlp ExtractAudio(string audioFormat) -Ytdlp SetResolution(string resolution) +var ytdlpPath = Path.Combine("tools", "yt-dlp.exe"); ``` -### Metadata & Format Fetching +--- -```csharp -Task GetVersionAsync(CancellationToken ct = default) -Task GetVideoMetadataJsonAsync(string url, CancellationToken ct = default) -Task> GetFormatsDetailedAsync(string url, CancellationToken ct = default) -Task GetBestAudioFormatIdAsync(string url, CancellationToken ct = default) -Task GetBestVideoFormatIdAsync(string url, int maxHeight = 1080, CancellationToken ct = default) -``` +# 🔧 Basic Usage -### Download Execution +### Download a video ```csharp -string PreviewCommand() -Task ExecuteAsync(string url, CancellationToken ct = default, string? outputTemplate = null) -Task ExecuteBatchAsync(IEnumerable urls, CancellationToken ct = default) -Task ExecuteBatchAsync(IEnumerable urls, int maxConcurrency = 3, CancellationToken ct = default) -``` +await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") + .WithFormat("best") + .WithOutputFolder("./downloads") + .WithOutputTemplate("%(title)s.%(ext)s"); -### Common Options (chainable) +ytdlp.OnProgressDownload += (s, e) => + Console.WriteLine($"{e.Percent:F1}% {e.Speed} ETA {e.ETA}"); -```csharp -Ytdlp EmbedMetadata() -Ytdlp EmbedThumbnail() -Ytdlp DownloadThumbnails() -Ytdlp DownloadSubtitles(string languages = "all") -Ytdlp SetRetries(string retries) -Ytdlp SetDownloadRate(string rate) -Ytdlp UseProxy(string proxy) -Ytdlp Simulate() -Ytdlp SkipDownloaded() -Ytdlp SetKeepTempFiles(bool keep) +await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=VIDEO_ID"); ``` -### Advanced / Specialized Options +--- + +# 🎧 Extract audio ```csharp -Ytdlp WithConcurrentFragments(int count) -Ytdlp RemoveSponsorBlock(params string[] categories) -Ytdlp EmbedSubtitles(string languages = "all", string? convertTo = null) -Ytdlp CookiesFromBrowser(string browser, string? profile = null) -Ytdlp GeoBypassCountry(string countryCode) -Ytdlp AddCustomCommand(string customCommand) -Ytdlp SetUserAgent(string userAgent) -Ytdlp SetReferer(string referer) -Ytdlp UseCookies(string cookieFile) -Ytdlp SetCustomHeader(string header, string value) -Ytdlp SetAuthentication(string username, string password) -Ytdlp DownloadLivestream(bool fromStart = true) -Ytdlp DownloadSections(string timeRanges) -Ytdlp DownloadLiveStreamRealTime() -Ytdlp MergePlaylistIntoSingleVideo(string format) -Ytdlp SelectPlaylistItems(string items) -Ytdlp ConcatenateVideos() -Ytdlp ReplaceMetadata(string field, string regex, string replacement) -Ytdlp LogToFile(string logFile) -Ytdlp DisableAds() -Ytdlp SetTimeout(TimeSpan timeout) -Ytdlp SetDownloadTimeout(string timeout) +await using var ytdlp = new Ytdlp() + .WithExtractAudio("mp3") + .WithOutputFolder("./audio"); + +await ytdlp.ExecuteAsync(url); ``` -### Utility / Info +--- + +# 📊 Monitor Progress ```csharp -Ytdlp Version() -Ytdlp Update() -Ytdlp WriteMetadataToJson() -Ytdlp ExtractMetadataOnly() +ytdlp.OnProgressDownload += (s, e) => +{ + Console.WriteLine($"{e.Percent:F1}% {e.Speed} ETA {e.ETA}"); +}; + +ytdlp.OnCompleteDownload += (s, msg) => +{ + Console.WriteLine($"Finished: {msg}"); +}; ``` -## Usage Examples +--- -### Basic download (720p video + best audio) +# 📦 Fetch Metadata ```csharp -var ytdlp = new Ytdlp(); +await using var ytdlp = new Ytdlp(); -await ytdlp - .SetFormat("bestvideo[height<=720]+bestaudio/best") - .SetOutputFolder("./downloads") - .SetOutputTemplate("%(title)s [%(resolution)s].%(ext)s") - .EmbedMetadata() - .EmbedThumbnail() - .ExecuteAsync("https://www.youtube.com/watch?v=VIDEO_ID"); +var metadata = await ytdlp.GetMetadataAsync(url); + +Console.WriteLine(metadata?.Title); +Console.WriteLine(metadata?.Duration); ``` -### Auto-select best formats +--- + +# 🎬 Auto‑Select Best Formats ```csharp -var bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); -var bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 1080); +await using var ytdlp = new Ytdlp(); + +string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, 1080); +string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); await ytdlp - .SetFormat($"{bestVideo}+{bestAudio}/best") + .WithFormat($"{bestVideo}+{bestAudio}/best") .ExecuteAsync(url); ``` -### Fetch metadata only +--- + +# ⚡ Parallel Downloads ```csharp -var meta = await ytdlp.GetVideoMetadataJsonAsync(url); -Console.WriteLine($"Title: {meta?.Title}"); -Console.WriteLine($"Duration: {meta?.Duration} s"); -Console.WriteLine($"Best thumbnail: {meta?.BestThumbnailUrl}"); -``` +var urls = new[] +{ + "https://youtu.be/video1", + "https://youtu.be/video2" +}; -### Monitor progress +var tasks = urls.Select(async url => +{ + await using var ytdlp = new Ytdlp() + .WithFormat("best") + .WithOutputFolder("./batch"); -```csharp -ytdlp.OnProgressDownload += (s, e) => - Console.Write($"\r[{new string('=', (int)e.Percent / 3)}] {e.Percent:F1}% {e.Speed} ETA {e.ETA}"); + await ytdlp.ExecuteAsync(url); +}); -await ytdlp.ExecuteAsync(url); +await Task.WhenAll(tasks); ``` -### Download best 1080p video + best audio (auto-selected) +--- -```csharp -var ytdlp = new Ytdlp(); +# 📡 Events -string url = "https://www.youtube.com/watch?v=Xt50Sodg7sA"; +| Event | Description | +| -------------------------- | ------------------------ | +| `OnProgressDownload` | Download progress | +| `OnProgressMessage` | Informational messages | +| `OnCompleteDownload` | File finished | +| `OnPostProcessingComplete` | Post‑processing finished | +| `OnOutputMessage` | Raw output line | +| `OnErrorMessage` | Error message | +| `OnCommandCompleted` | Process finished | -// Auto-select best formats -string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 1080); -string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); +--- -await ytdlp - .SetFormat($"{bestVideo}+{bestAudio}/best") - .SetOutputFolder("./downloads") - .SetOutputTemplate("%(title)s [%(resolution)s - %(id)s].%(ext)s") - .EmbedMetadata() - .EmbedThumbnail() - .ExecuteAsync(url);### 1. Download best 1080p video + best audio (auto-selected) +# 🔧 Fluent API Methods -```csharp -var ytdlp = new Ytdlp(); +### Output -string url = "https://www.youtube.com/watch?v=Xt50Sodg7sA"; +``` +WithOutputFolder() +WithTempFolder() +WithHomeFolder() +WithOutputTemplate() +WithFFmpegLocation() +``` -// Auto-select best formats -string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 1080); -string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); +### Formats -await ytdlp - .SetFormat($"{bestVideo}+{bestAudio}/best") - .SetOutputFolder("./downloads") - .SetOutputTemplate("%(title)s [%(resolution)s - %(id)s].%(ext)s") - .EmbedMetadata() - .EmbedThumbnail() - .ExecuteAsync(url); +``` +WithFormat() +With720pOrBest() +WithExtractAudio() ``` -### Monitor download progress with a simple console bar -```csharp -ytdlp.OnProgressDownload += (sender, args) => -{ - ConsoleProgress.Update(args.Percent, $"{args.Speed} ETA {args.ETA}"); -}; - -ytdlp.OnCompleteDownload += (sender, msg) => ConsoleProgress.Complete($"Finished: {msg}"); +### Metadata -await ytdlp - .SetFormat("best[height<=720]") - .ExecuteAsync(url); +``` +GetMetadataAsync() +GetBestAudioFormatIdAsync() +GetBestVideoFormatIdAsync() +GetAvailableFormatsAsync() ``` -### Fetch metadata and display video info +### Features -```csharp -var metadata = await ytdlp.GetVideoMetadataJsonAsync(url); +``` +WithEmbedMetadata() +WithEmbedThumbnail() +WithEmbedSubtitles() +WithSubtitles() +WithConcurrentFragments() +WithSponsorblockRemove() +``` + +### Network -if (metadata != null) -{ - Console.WriteLine($"Title: {metadata.Title}"); - Console.WriteLine($"Channel: {metadata.Channel} ({metadata.ChannelFollowerCount:N0} followers)"); - Console.WriteLine($"Duration: {metadata.DurationTimeSpan?.ToString(@"mm\:ss") ?? "N/A"}"); - Console.WriteLine($"Views: {metadata.ViewCount:N0}"); - Console.WriteLine($"Likes: {metadata.LikeCount:N0}"); - Console.WriteLine($"Best thumb: {metadata.BestThumbnailUrl}"); - - // List categories and tags - if (metadata.Categories?.Any() == true) - Console.WriteLine($"Categories: {string.Join(", ", metadata.Categories)}"); - - if (metadata.Tags?.Any() == true) - Console.WriteLine($"Tags: {string.Join(", ", metadata.Tags.Take(10))}..."); -} ``` -### Download only the best audio as MP3 -```csharp -string bestAudioId = await ytdlp.GetBestAudioFormatIdAsync(url); +WithProxy() +WithCookiesFile() +WithCookiesFromBrowser() +``` -await ytdlp - .SetFormat(bestAudioId) - .ExtractAudio("mp3") - .SetOutputFolder("./audio") - .SetOutputTemplate("%(title)s - %(uploader)s.%(ext)s") - .ExecuteAsync(url); +### Advanced + +``` +AddFlag() +AddOption() +AddCustomCommand() ``` -### Remove SponsorBlock segments (all categories) +--- + +# 🔄 Upgrade Guide (v2 → v3) + +v3 introduces a **new immutable fluent API**. + +Old mutable commands were removed. + +--- + +## ❌ Old API (v2) + ```csharp +var ytdlp = new Ytdlp(); + await ytdlp - .RemoveSponsorBlock("all") // or specific: "sponsor", "intro", "outro", etc. .SetFormat("best") - .SetOutputFolder("./sponsor-free") + .SetOutputFolder("./downloads") .ExecuteAsync(url); ``` -### Download with concurrent fragments (faster on good connections) +--- + +## ✅ New API (v3) + ```csharp -await ytdlp - .WithConcurrentFragments(8) // 8 parallel chunks - .SetFormat("best") - .ExecuteAsync(url); +await using var ytdlp = new Ytdlp() + .WithFormat("best") + .WithOutputFolder("./downloads"); + +await ytdlp.ExecuteAsync(url); ``` -### Batch download (concurrent, 4 at a time) + +--- + +## Method changes + +| v2 | v3 | +| --------------------- | ---------------------- | +| `SetFormat()` | `WithFormat()` | +| `SetOutputFolder()` | `WithOutputFolder()` | +| `SetTempFolder()` | `WithTempFolder()` | +| `SetOutputTemplate()` | `WithOutputTemplate()` | +| `SetFFMpegLocation()` | `WithFFmpegLocation()` | +| `ExtractAudio()` | `WithExtractAudio()` | +| `UseProxy()` | `WithProxy()` | + +--- + +## Important behavior changes + +### Instances are immutable + +Every `WithXxx()` call returns a **new instance**. + ```csharp -var urls = new[] -{ - "https://www.youtube.com/watch?v=VIDEO1", - "https://www.youtube.com/watch?v=VIDEO2", - "https://www.youtube.com/watch?v=VIDEO3", - "https://www.youtube.com/watch?v=VIDEO4" -}; +var baseYtdlp = new Ytdlp(); -await ytdlp - .SetFormat("best[height<=480]") // lower quality for faster batch - .SetOutputFolder("./batch") - .ExecuteBatchAsync(urls, maxConcurrency: 4); +var download = baseYtdlp + .WithFormat("best") + .WithOutputFolder("./downloads"); ``` -### Cancel a long download after 15 seconds +--- + +### Event subscription + +Attach events **to the configured instance**. + ```csharp -var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); +var download = baseYtdlp.WithFormat("best"); -try -{ - await ytdlp - .SetFormat("best") - .ExecuteAsync(url, cts.Token); -} -catch (OperationCanceledException) -{ - Console.WriteLine("Download cancelled as expected."); -} +download.OnProgressDownload += ... ``` -### Simulate & preview command first -```csharp -ytdlp - .SetFormat("137+251") - .EmbedMetadata() - .SetOutputTemplate("%(title)s.%(ext)s"); +--- -Console.WriteLine("Preview command:"); -Console.WriteLine(ytdlp.PreviewCommand()); +### Proper disposal -// → Outputs: --embed-metadata -f "137+251" -o "%(title)s.%(ext)s" -``` +Use **`await using`** for automatic cleanup. -### Advanced: Use cookies from browser + proxy + custom header ```csharp -await ytdlp - .CookiesFromBrowser("chrome") - .UseProxy("http://proxy.example.com:8080") - .SetCustomHeader("Referer", "https://example.com") - .SetFormat("best") - .ExecuteAsync("https://private-video-url"); +await using var ytdlp = new Ytdlp(); ``` + +--- + +# 🧪 Example Apps + +* ClipMate MAUI downloader +* Windows GUI downloader +* Console examples + --- -## Contributing +# 🤝 Contributing -Contributions are welcome! Please submit issues or pull requests to the [GitHub repository](https://github.com/manusoft/yt-dlp-wrapper). Ensure code follows the project’s style guidelines and includes unit tests. +Contributions are welcome! + +Open issues or PRs on GitHub. + +--- + +# 📜 License + +MIT License + +See: + +[https://github.com/manusoft/yt-dlp-wrapper/blob/master/LICENSE.txt](https://github.com/manusoft/yt-dlp-wrapper/blob/master/LICENSE.txt) + +--- -## License +# 👨‍💻 Author -This project is licensed under the MIT License. See the [LICENSE](https://github.com/manusoft/yt-dlp-wrapper/blob/master/LICENSE.txt) file for details. +**Manoj Babu** +ManuHub \ No newline at end of file diff --git a/Ytdlp.NET.slnx b/Ytdlp.NET.slnx new file mode 100644 index 0000000..44e3e25 --- /dev/null +++ b/Ytdlp.NET.slnx @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/VideoDownloader/App/Program.cs b/apps/ClipMate.Lite/App/Program.cs similarity index 100% rename from src/VideoDownloader/App/Program.cs rename to apps/ClipMate.Lite/App/Program.cs diff --git a/src/VideoDownloader/VideoDownloader.csproj b/apps/ClipMate.Lite/ClipMate.Lite.csproj similarity index 94% rename from src/VideoDownloader/VideoDownloader.csproj rename to apps/ClipMate.Lite/ClipMate.Lite.csproj index 61c590b..44491cc 100644 --- a/src/VideoDownloader/VideoDownloader.csproj +++ b/apps/ClipMate.Lite/ClipMate.Lite.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/VideoDownloader/Core/AppLogger.cs b/apps/ClipMate.Lite/Core/AppLogger.cs similarity index 100% rename from src/VideoDownloader/Core/AppLogger.cs rename to apps/ClipMate.Lite/Core/AppLogger.cs diff --git a/src/VideoDownloader/Core/DownloadSession.cs b/apps/ClipMate.Lite/Core/DownloadSession.cs similarity index 100% rename from src/VideoDownloader/Core/DownloadSession.cs rename to apps/ClipMate.Lite/Core/DownloadSession.cs diff --git a/src/VideoDownloader/Core/MediaFormat.cs b/apps/ClipMate.Lite/Core/MediaFormat.cs similarity index 100% rename from src/VideoDownloader/Core/MediaFormat.cs rename to apps/ClipMate.Lite/Core/MediaFormat.cs diff --git a/src/VideoDownloader/Core/YtdlpService.cs b/apps/ClipMate.Lite/Core/YtdlpService.cs similarity index 100% rename from src/VideoDownloader/Core/YtdlpService.cs rename to apps/ClipMate.Lite/Core/YtdlpService.cs diff --git a/src/VideoDownloader/Properties/Resources.Designer.cs b/apps/ClipMate.Lite/Properties/Resources.Designer.cs similarity index 100% rename from src/VideoDownloader/Properties/Resources.Designer.cs rename to apps/ClipMate.Lite/Properties/Resources.Designer.cs diff --git a/src/VideoDownloader/Properties/Resources.resx b/apps/ClipMate.Lite/Properties/Resources.resx similarity index 100% rename from src/VideoDownloader/Properties/Resources.resx rename to apps/ClipMate.Lite/Properties/Resources.resx diff --git a/src/VideoDownloader/Resources/folder_24.png b/apps/ClipMate.Lite/Resources/folder_24.png similarity index 100% rename from src/VideoDownloader/Resources/folder_24.png rename to apps/ClipMate.Lite/Resources/folder_24.png diff --git a/src/VideoDownloader/Resources/icon.PNG b/apps/ClipMate.Lite/Resources/icon.PNG similarity index 100% rename from src/VideoDownloader/Resources/icon.PNG rename to apps/ClipMate.Lite/Resources/icon.PNG diff --git a/src/VideoDownloader/Resources/icon.ico b/apps/ClipMate.Lite/Resources/icon.ico similarity index 100% rename from src/VideoDownloader/Resources/icon.ico rename to apps/ClipMate.Lite/Resources/icon.ico diff --git a/src/VideoDownloader/Resources/iconvd.PNG b/apps/ClipMate.Lite/Resources/iconvd.PNG similarity index 100% rename from src/VideoDownloader/Resources/iconvd.PNG rename to apps/ClipMate.Lite/Resources/iconvd.PNG diff --git a/src/VideoDownloader/Resources/search_24.png b/apps/ClipMate.Lite/Resources/search_24.png similarity index 100% rename from src/VideoDownloader/Resources/search_24.png rename to apps/ClipMate.Lite/Resources/search_24.png diff --git a/src/VideoDownloader/UI/UIThread.cs b/apps/ClipMate.Lite/UI/UIThread.cs similarity index 100% rename from src/VideoDownloader/UI/UIThread.cs rename to apps/ClipMate.Lite/UI/UIThread.cs diff --git a/src/VideoDownloader/UI/frmMain.Designer.cs b/apps/ClipMate.Lite/UI/frmMain.Designer.cs similarity index 100% rename from src/VideoDownloader/UI/frmMain.Designer.cs rename to apps/ClipMate.Lite/UI/frmMain.Designer.cs diff --git a/src/VideoDownloader/UI/frmMain.cs b/apps/ClipMate.Lite/UI/frmMain.cs similarity index 100% rename from src/VideoDownloader/UI/frmMain.cs rename to apps/ClipMate.Lite/UI/frmMain.cs diff --git a/src/VideoDownloader/UI/frmMain.resx b/apps/ClipMate.Lite/UI/frmMain.resx similarity index 100% rename from src/VideoDownloader/UI/frmMain.resx rename to apps/ClipMate.Lite/UI/frmMain.resx diff --git a/src/ConsoleApp.Test/Program.cs b/archives/Ytdlp.Wrapper.Console/Program.cs similarity index 100% rename from src/ConsoleApp.Test/Program.cs rename to archives/Ytdlp.Wrapper.Console/Program.cs diff --git a/src/ConsoleApp.Test/ConsoleApp.Test.csproj b/archives/Ytdlp.Wrapper.Console/Ytdlp.Wrapper.Console.csproj similarity index 50% rename from src/ConsoleApp.Test/ConsoleApp.Test.csproj rename to archives/Ytdlp.Wrapper.Console/Ytdlp.Wrapper.Console.csproj index 80eed9a..c7ec028 100644 --- a/src/ConsoleApp.Test/ConsoleApp.Test.csproj +++ b/archives/Ytdlp.Wrapper.Console/Ytdlp.Wrapper.Console.csproj @@ -2,17 +2,13 @@ Exe - net9.0 + net10.0 enable enable - - - - - + diff --git a/src/YtDlpWrapper/AudioQuality.cs b/archives/Ytdlp.Wrapper/AudioQuality.cs similarity index 100% rename from src/YtDlpWrapper/AudioQuality.cs rename to archives/Ytdlp.Wrapper/AudioQuality.cs diff --git a/src/YtDlpWrapper/DownloadProgressEventArgs.cs b/archives/Ytdlp.Wrapper/DownloadProgressEventArgs.cs similarity index 100% rename from src/YtDlpWrapper/DownloadProgressEventArgs.cs rename to archives/Ytdlp.Wrapper/DownloadProgressEventArgs.cs diff --git a/src/YtDlpWrapper/LICENSE.txt b/archives/Ytdlp.Wrapper/LICENSE.txt similarity index 100% rename from src/YtDlpWrapper/LICENSE.txt rename to archives/Ytdlp.Wrapper/LICENSE.txt diff --git a/src/YtDlpWrapper/LogType.cs b/archives/Ytdlp.Wrapper/LogType.cs similarity index 100% rename from src/YtDlpWrapper/LogType.cs rename to archives/Ytdlp.Wrapper/LogType.cs diff --git a/src/YtDlpWrapper/Logger.cs b/archives/Ytdlp.Wrapper/Logger.cs similarity index 100% rename from src/YtDlpWrapper/Logger.cs rename to archives/Ytdlp.Wrapper/Logger.cs diff --git a/src/YtDlpWrapper/NotUsingFunctions.cs b/archives/Ytdlp.Wrapper/NotUsingFunctions.cs similarity index 100% rename from src/YtDlpWrapper/NotUsingFunctions.cs rename to archives/Ytdlp.Wrapper/NotUsingFunctions.cs diff --git a/src/YtDlpWrapper/ProgressParser.cs b/archives/Ytdlp.Wrapper/ProgressParser.cs similarity index 100% rename from src/YtDlpWrapper/ProgressParser.cs rename to archives/Ytdlp.Wrapper/ProgressParser.cs diff --git a/src/YtDlpWrapper/README.md b/archives/Ytdlp.Wrapper/README.md similarity index 100% rename from src/YtDlpWrapper/README.md rename to archives/Ytdlp.Wrapper/README.md diff --git a/src/YtDlpWrapper/RegexPatterns.cs b/archives/Ytdlp.Wrapper/RegexPatterns.cs similarity index 100% rename from src/YtDlpWrapper/RegexPatterns.cs rename to archives/Ytdlp.Wrapper/RegexPatterns.cs diff --git a/src/YtDlpWrapper/StringExtensions.cs b/archives/Ytdlp.Wrapper/StringExtensions.cs similarity index 100% rename from src/YtDlpWrapper/StringExtensions.cs rename to archives/Ytdlp.Wrapper/StringExtensions.cs diff --git a/src/YtDlpWrapper/VideoFormat.cs b/archives/Ytdlp.Wrapper/VideoFormat.cs similarity index 100% rename from src/YtDlpWrapper/VideoFormat.cs rename to archives/Ytdlp.Wrapper/VideoFormat.cs diff --git a/src/YtDlpWrapper/VideoInfo.cs b/archives/Ytdlp.Wrapper/VideoInfo.cs similarity index 100% rename from src/YtDlpWrapper/VideoInfo.cs rename to archives/Ytdlp.Wrapper/VideoInfo.cs diff --git a/src/YtDlpWrapper/VideoQuality.cs b/archives/Ytdlp.Wrapper/VideoQuality.cs similarity index 100% rename from src/YtDlpWrapper/VideoQuality.cs rename to archives/Ytdlp.Wrapper/VideoQuality.cs diff --git a/src/YtDlpWrapper/YtDlpEngine.cs b/archives/Ytdlp.Wrapper/YtDlpEngine.cs similarity index 100% rename from src/YtDlpWrapper/YtDlpEngine.cs rename to archives/Ytdlp.Wrapper/YtDlpEngine.cs diff --git a/src/YtDlpWrapper/YtDlpWrapper.csproj b/archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj similarity index 95% rename from src/YtDlpWrapper/YtDlpWrapper.csproj rename to archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj index 8a02d1e..91f01cd 100644 --- a/src/YtDlpWrapper/YtDlpWrapper.csproj +++ b/archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj @@ -1,7 +1,7 @@  - net6.0;net8.0;net9.0 + net8.0;net9.0;net10.0; enable enable CS0649,CS8600,CS8601,CS8604,CS8618,CS8625 diff --git a/src/YtDlpWrapper/Ytdlp.cs b/archives/Ytdlp.Wrapper/Ytdlp.cs similarity index 100% rename from src/YtDlpWrapper/Ytdlp.cs rename to archives/Ytdlp.Wrapper/Ytdlp.cs diff --git a/src/YtDlpWrapper/icon.PNG b/archives/Ytdlp.Wrapper/icon.PNG similarity index 100% rename from src/YtDlpWrapper/icon.PNG rename to archives/Ytdlp.Wrapper/icon.PNG diff --git a/examples/01_DownloadVideo.cs b/examples/01_DownloadVideo.cs new file mode 100644 index 0000000..1d4a72c --- /dev/null +++ b/examples/01_DownloadVideo.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + await using var ytdlp = new Ytdlp() + .WithFormat("bestvideo[height<=720]+bestaudio/best") + .WithOutputFolder("./downloads") + .WithOutputTemplate("%(title)s [%(resolution)s].%(ext)s") + .EmbedMetadata() + .EmbedThumbnail(); + + ytdlp.OnProgressDownload += (s, e) => + Console.Write($"\rProgress: {e.Percent:F1}% ETA: {e.ETA} Speed: {e.Speed}"); + ytdlp.OnCompleteDownload += (s, msg) => + Console.WriteLine($"\nDownload completed: {msg}"); + + await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=VIDEO_ID"); + } +} \ No newline at end of file diff --git a/examples/02_ExtractAudio.cs b/examples/02_ExtractAudio.cs new file mode 100644 index 0000000..c042551 --- /dev/null +++ b/examples/02_ExtractAudio.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + await using var ytdlp = new Ytdlp() + .WithFormat("bestaudio") + .ExtractAudio("mp3") + .WithOutputFolder("./audio") + .WithOutputTemplate("%(title)s - %(uploader)s.%(ext)s") + .EmbedMetadata(); + + ytdlp.OnProgressDownload += (s, e) => + Console.Write($"\rProgress: {e.Percent:F1}% ETA: {e.ETA}"); + + await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=VIDEO_ID"); + } +} \ No newline at end of file diff --git a/examples/03_BatchDownload.cs b/examples/03_BatchDownload.cs new file mode 100644 index 0000000..8a5cbdb --- /dev/null +++ b/examples/03_BatchDownload.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + var urls = new[] + { + "https://www.youtube.com/watch?v=VID1", + "https://www.youtube.com/watch?v=VID2", + "https://www.youtube.com/watch?v=VID3" + }; + + await using var ytdlp = new Ytdlp() + .WithFormat("best[height<=480]") + .WithOutputFolder("./batch"); + + ytdlp.OnProgressDownload += (s, e) => + Console.WriteLine($"{e.Percent:F1}% - {e.Speed} - ETA {e.ETA}"); + + await ytdlp.ExecuteBatchAsync(urls, maxConcurrency: 3); + } +} \ No newline at end of file diff --git a/examples/04_MetadataProbe.cs b/examples/04_MetadataProbe.cs new file mode 100644 index 0000000..87f7edb --- /dev/null +++ b/examples/04_MetadataProbe.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + await using var ytdlp = new Ytdlp(); + + var url = "https://www.youtube.com/watch?v=VIDEO_ID"; + var metadata = await ytdlp.GetMetadataAsync(url); + + if (metadata != null) + { + Console.WriteLine($"Type: {metadata.Type}"); // Playlist / Video + Console.WriteLine($"Title: {metadata.Title}"); + Console.WriteLine($"Duration: {metadata.DurationTimeSpan}"); + Console.WriteLine($"Uploader: {metadata.Uploader}"); + Console.WriteLine($"Views: {metadata.ViewCount:N0}"); + Console.WriteLine($"Categories: {string.Join(", ", metadata.Categories ?? Array.Empty())}"); + } + } +} \ No newline at end of file diff --git a/examples/05_BestFormats.cs b/examples/05_BestFormats.cs new file mode 100644 index 0000000..1db6fea --- /dev/null +++ b/examples/05_BestFormats.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + var url = "https://www.youtube.com/watch?v=VIDEO_ID"; + + await using var ytdlp = new Ytdlp(); + + string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 1080); + string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); + + await ytdlp + .WithFormat($"{bestVideo}+{bestAudio}/best") + .WithOutputFolder("./downloads") + .WithOutputTemplate("%(title)s [%(resolution)s - %(id)s].%(ext)s") + .EmbedMetadata() + .EmbedThumbnail() + .ExecuteAsync(url); + } +} \ No newline at end of file diff --git a/examples/06_SponsorBlockRemove.cs b/examples/06_SponsorBlockRemove.cs new file mode 100644 index 0000000..bb6b1d7 --- /dev/null +++ b/examples/06_SponsorBlockRemove.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + var url = "https://www.youtube.com/watch?v=VIDEO_ID"; + + await using var ytdlp = new Ytdlp() + .RemoveSponsorBlock("all") + .WithFormat("best") + .WithOutputFolder("./sponsor-free"); + + await ytdlp.ExecuteAsync(url); + } +} \ No newline at end of file diff --git a/examples/07_ConcurrentFragments.cs b/examples/07_ConcurrentFragments.cs new file mode 100644 index 0000000..ef080e6 --- /dev/null +++ b/examples/07_ConcurrentFragments.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + var url = "https://www.youtube.com/watch?v=VIDEO_ID"; + + await using var ytdlp = new Ytdlp() + .WithConcurrentFragments(8) + .WithFormat("best") + .WithOutputFolder("./downloads"); + + await ytdlp.ExecuteAsync(url); + } +} \ No newline at end of file diff --git a/examples/08_CancelDownload.cs b/examples/08_CancelDownload.cs new file mode 100644 index 0000000..f6b8db7 --- /dev/null +++ b/examples/08_CancelDownload.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; + +class Program +{ + static async Task Main() + { + var url = "https://www.youtube.com/watch?v=VIDEO_ID"; + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); + + await using var ytdlp = new Ytdlp() + .WithFormat("best") + .WithOutputFolder("./downloads"); + + try + { + await ytdlp.ExecuteAsync(url, cts.Token); + } + catch (OperationCanceledException) + { + Console.WriteLine("Download cancelled after 15 seconds."); + } + } +} \ No newline at end of file diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs index d54fbcc..117a16c 100644 --- a/src/Ytdlp.NET.Console/Program.cs +++ b/src/Ytdlp.NET.Console/Program.cs @@ -15,25 +15,28 @@ private static async Task Main(string[] args) Console.WriteLine("----------------------------------------"); // Initialize the wrapper (assuming yt-dlp is in PATH or specify path) - var ytdlp = new Ytdlp(ytdlpPath: $"tools\\yt-dlp.exe", logger: new ConsoleLogger()); - ytdlp.SetFFmpegLocation($"tools"); + await using var baseYtdlp = new Ytdlp(ytdlpPath: $"tools\\yt-dlp.exe", logger: new ConsoleLogger()) + .WithFFmpegLocation("tools") + .WithTempFolder(@"c:\Downloads\YouTube\temp") + .WithOutputTemplate("%(upload_date>%Y-%m-%d)s - %(title)s.%(ext)s") + .WithAria2(16); // Run all demos/tests sequentially - //await TestGetVersionAsync(ytdlp); - //await TestUpdateVersionAsync(ytdlp); - - //await TestGetFormatsAsync(ytdlp); - //await TestGetFormatsDetailedAsync(ytdlp); - //await TestGetMetadataAsync(ytdlp); - //await TestGetSimpleMetadataAsync(ytdlp); - //await TestGetTitleAsync(ytdlp); - - await TestDownloadVideoAsync(ytdlp); - await TestDownloadAudioAsync(ytdlp); - await TestBatchDownloadAsync(ytdlp); + //await TestGetVersionAsync(baseYtdlp); + //await TestUpdateVersionAsync(baseYtdlp); + + //await TestGetFormatsAsync(baseYtdlp); + //await TestGetFormatsDetailedAsync(baseYtdlp); + //await TestGetMetadataAsync(baseYtdlp); + //await TestGetLiteMetadataAsync(baseYtdlp); + //await TestGetTitleAsync(baseYtdlp); + + await TestDownloadVideoAsync(baseYtdlp); + //await TestDownloadAudioAsync(ytdlp); + //await TestBatchDownloadAsync(ytdlp); //await TestSponsorBlockAsync(ytdlp); - await TestConcurrentFragmentsAsync(ytdlp); - await TestCancellationAsync(ytdlp); + //await TestConcurrentFragmentsAsync(ytdlp); + //await TestCancellationAsync(ytdlp); Console.WriteLine("\nAll tests completed. Press any key to exit..."); @@ -167,8 +170,8 @@ private static async Task TestGetMetadataAsync(Ytdlp ytdlp) } } - // Test 5: Get simple metedata - private static async Task TestGetSimpleMetadataAsync(Ytdlp ytdlp) + // Test 5: Get lite metedata + private static async Task TestGetLiteMetadataAsync(Ytdlp ytdlp) { var stopwatch = Stopwatch.StartNew(); Console.WriteLine("\nTest 5: Fetching simple metedata..."); @@ -186,25 +189,19 @@ private static async Task TestGetSimpleMetadataAsync(Ytdlp ytdlp) Console.WriteLine($"Id: {data["id"]}"); Console.WriteLine($"Thumbnail: {data["thumbnail"]}"); } - - - - // Basic info - //Console.WriteLine($"ID : {metadata.Id}"); - //Console.WriteLine($"Title : {metadata.Title}"); - //Console.WriteLine($"Duration : {metadata.Duration}"); - //Console.WriteLine($"Thumbnail : {metadata.Thumbnail}"); - //Console.WriteLine($"View Count : {metadata.ViewCount}"); - //Console.WriteLine($"FileSize : {metadata.FileSize.ToString() ?? "NA"}"); - //Console.WriteLine($"Description : {(metadata.Description?.Length > 120 ? metadata.Description.Substring(0, 120) + "..." : metadata.Description)}"); } // Test 6: Download a video with progress events - private static async Task TestDownloadVideoAsync(Ytdlp ytdlp) + private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase) { Console.WriteLine("\nTest 6: Downloading a video..."); var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; + var ytdlp = ytdlpBase + .With720pOrBest() + .WithOutputFolder("./downloads") + .WithOutputTemplate("%(title)s.%(ext)s"); + // Subscribe to events ytdlp.OnProgressDownload += (sender, args) => Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA}"); @@ -215,11 +212,7 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlp) ytdlp.OnPostProcessingComplete += (sender, message) => Console.WriteLine($"Post-processing done: {message}"); - await ytdlp - .SetFormat("bv[height<=720]+ba/b") // 720p max - .SetOutputFolder("./downloads") - .SetOutputTemplate("%(title)s.%(ext)s") - .ExecuteAsync(url); + await ytdlp.ExecuteAsync(url); } // Test 7 Extract audio only @@ -229,9 +222,9 @@ private static async Task TestDownloadAudioAsync(Ytdlp ytdlp) var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; await ytdlp - .ExtractAudio("mp3") - .SetFormat("ba") - .SetOutputFolder("./downloads/audio") + .WithExtractAudio("mp3") + .WithFormat("ba") + .WithOutputFolder("./downloads/audio") .ExecuteAsync(url); } @@ -247,8 +240,8 @@ private static async Task TestBatchDownloadAsync(Ytdlp ytdlp) }; await ytdlp - .SetFormat("best[height<=480]") // Lower quality for speed - .SetOutputFolder("./downloads/batch") + .WithFormat("best[height<=480]") // Lower quality for speed + .WithOutputFolder("./downloads/batch") .ExecuteBatchAsync(urls, maxConcurrency: 3); } @@ -259,9 +252,9 @@ private static async Task TestSponsorBlockAsync(Ytdlp ytdlp) var url = "https://www.youtube.com/watch?v=oDSEGkT6J-0"; await ytdlp - .SetFormat("best") - .RemoveSponsorBlock("all") // Removes sponsor, intro, etc. - .SetOutputFolder("./downloads/sponsorblock") + .WithFormat("best") + //.WithRemoveSponsorBlock("all") // Removes sponsor, intro, etc. + .WithOutputFolder("./downloads/sponsorblock") .ExecuteAsync(url); } @@ -273,9 +266,9 @@ private static async Task TestConcurrentFragmentsAsync(Ytdlp ytdlp) await ytdlp .WithConcurrentFragments(8) // 8 parallel fragments - .SetFormat("b") - .SetOutputTemplate("%(title)s.%(ext)s") - .SetOutputFolder("./downloads/concurrent") + .WithFormat("b") + .WithOutputTemplate("%(title)s.%(ext)s") + .WithOutputFolder("./downloads/concurrent") .ExecuteAsync(url); } @@ -287,9 +280,9 @@ private static async Task TestCancellationAsync(Ytdlp ytdlp) var cts = new CancellationTokenSource(); var downloadTask = ytdlp - .SetFormat("b") - .SetOutputTemplate("%(title)s.%(ext)s") - .SetOutputFolder("./downloads/cancel") + .WithFormat("b") + .WithOutputTemplate("%(title)s.%(ext)s") + .WithOutputFolder("./downloads/cancel") .ExecuteAsync(url, cts.Token); // Simulate cancel after 20 seconds @@ -314,12 +307,12 @@ private static async Task TestGetTitleAsync(Ytdlp ytdlp) try { - var downloadTask = ytdlp - .Simulate() - .NoWarning() - .SetOutputTemplate("%(title)s.%(ext)s") - .SetOutputFolder("./downloads/cancel") - .AddCustomCommand("--get-title") + var downloadTask = ytdlp + //.Simulate() + //.NoWarning() + .WithOutputTemplate("%(title)s.%(ext)s") + .WithOutputFolder("./downloads/cancel") + .AddFlag("--get-title") .ExecuteAsync(url); await downloadTask; diff --git a/src/Ytdlp.NET.v3.Console/Program.cs b/src/Ytdlp.NET.vNext.Console/Program.cs similarity index 100% rename from src/Ytdlp.NET.v3.Console/Program.cs rename to src/Ytdlp.NET.vNext.Console/Program.cs diff --git a/src/Ytdlp.NET.v3.Console/Ytdlp.NET.v3.Console.csproj b/src/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj similarity index 88% rename from src/Ytdlp.NET.v3.Console/Ytdlp.NET.v3.Console.csproj rename to src/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj index eded871..3f67c73 100644 --- a/src/Ytdlp.NET.v3.Console/Ytdlp.NET.v3.Console.csproj +++ b/src/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Ytdlp.NET.v3/Core/ProgressParser.cs b/src/Ytdlp.NET.vNext/Core/ProgressParser.cs similarity index 100% rename from src/Ytdlp.NET.v3/Core/ProgressParser.cs rename to src/Ytdlp.NET.vNext/Core/ProgressParser.cs diff --git a/src/Ytdlp.NET.v3/Core/RegexPatterns.cs b/src/Ytdlp.NET.vNext/Core/RegexPatterns.cs similarity index 100% rename from src/Ytdlp.NET.v3/Core/RegexPatterns.cs rename to src/Ytdlp.NET.vNext/Core/RegexPatterns.cs diff --git a/src/Ytdlp.NET.v3/Core/UpdateChannel.cs b/src/Ytdlp.NET.vNext/Core/UpdateChannel.cs similarity index 100% rename from src/Ytdlp.NET.v3/Core/UpdateChannel.cs rename to src/Ytdlp.NET.vNext/Core/UpdateChannel.cs diff --git a/src/Ytdlp.NET.v3/DefaultLogger.cs b/src/Ytdlp.NET.vNext/DefaultLogger.cs similarity index 100% rename from src/Ytdlp.NET.v3/DefaultLogger.cs rename to src/Ytdlp.NET.vNext/DefaultLogger.cs diff --git a/src/Ytdlp.NET.v3/DownloadProgressEventArgs.cs b/src/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs similarity index 100% rename from src/Ytdlp.NET.v3/DownloadProgressEventArgs.cs rename to src/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs diff --git a/src/Ytdlp.NET.v3/Helpers/FormatFilters.cs b/src/Ytdlp.NET.vNext/Helpers/FormatFilters.cs similarity index 100% rename from src/Ytdlp.NET.v3/Helpers/FormatFilters.cs rename to src/Ytdlp.NET.vNext/Helpers/FormatFilters.cs diff --git a/src/Ytdlp.NET.v3/ILogger.cs b/src/Ytdlp.NET.vNext/ILogger.cs similarity index 100% rename from src/Ytdlp.NET.v3/ILogger.cs rename to src/Ytdlp.NET.vNext/ILogger.cs diff --git a/src/Ytdlp.NET.v3/LICENSE.txt b/src/Ytdlp.NET.vNext/LICENSE.txt similarity index 100% rename from src/Ytdlp.NET.v3/LICENSE.txt rename to src/Ytdlp.NET.vNext/LICENSE.txt diff --git a/src/Ytdlp.NET.v3/LogType.cs b/src/Ytdlp.NET.vNext/LogType.cs similarity index 100% rename from src/Ytdlp.NET.v3/LogType.cs rename to src/Ytdlp.NET.vNext/LogType.cs diff --git a/src/Ytdlp.NET.v3/Models/Format.cs b/src/Ytdlp.NET.vNext/Models/Format.cs similarity index 100% rename from src/Ytdlp.NET.v3/Models/Format.cs rename to src/Ytdlp.NET.vNext/Models/Format.cs diff --git a/src/Ytdlp.NET.v3/Models/Metadata.cs b/src/Ytdlp.NET.vNext/Models/Metadata.cs similarity index 100% rename from src/Ytdlp.NET.v3/Models/Metadata.cs rename to src/Ytdlp.NET.vNext/Models/Metadata.cs diff --git a/src/Ytdlp.NET.v3/Models/MetadataLite.cs b/src/Ytdlp.NET.vNext/Models/MetadataLite.cs similarity index 100% rename from src/Ytdlp.NET.v3/Models/MetadataLite.cs rename to src/Ytdlp.NET.vNext/Models/MetadataLite.cs diff --git a/src/Ytdlp.NET.v3/README.md b/src/Ytdlp.NET.vNext/README.md similarity index 100% rename from src/Ytdlp.NET.v3/README.md rename to src/Ytdlp.NET.vNext/README.md diff --git a/src/Ytdlp.NET.v3/Ytdlp.NET.v3.csproj b/src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj similarity index 100% rename from src/Ytdlp.NET.v3/Ytdlp.NET.v3.csproj rename to src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj diff --git a/src/Ytdlp.NET.v3/Ytdlp.cs b/src/Ytdlp.NET.vNext/Ytdlp.cs similarity index 100% rename from src/Ytdlp.NET.v3/Ytdlp.cs rename to src/Ytdlp.NET.vNext/Ytdlp.cs diff --git a/src/Ytdlp.NET.v3/YtdlpBuilder.cs b/src/Ytdlp.NET.vNext/YtdlpBuilder.cs similarity index 100% rename from src/Ytdlp.NET.v3/YtdlpBuilder.cs rename to src/Ytdlp.NET.vNext/YtdlpBuilder.cs diff --git a/src/Ytdlp.NET.v3/YtdlpCommand.cs b/src/Ytdlp.NET.vNext/YtdlpCommand.cs similarity index 100% rename from src/Ytdlp.NET.v3/YtdlpCommand.cs rename to src/Ytdlp.NET.vNext/YtdlpCommand.cs diff --git a/src/Ytdlp.NET.v3/YtdlpException.cs b/src/Ytdlp.NET.vNext/YtdlpException.cs similarity index 100% rename from src/Ytdlp.NET.v3/YtdlpException.cs rename to src/Ytdlp.NET.vNext/YtdlpException.cs diff --git a/src/Ytdlp.NET.v3/YtdlpGeneral.cs b/src/Ytdlp.NET.vNext/YtdlpGeneral.cs similarity index 100% rename from src/Ytdlp.NET.v3/YtdlpGeneral.cs rename to src/Ytdlp.NET.vNext/YtdlpGeneral.cs diff --git a/src/Ytdlp.NET.v3/YtdlpProbe.cs b/src/Ytdlp.NET.vNext/YtdlpProbe.cs similarity index 100% rename from src/Ytdlp.NET.v3/YtdlpProbe.cs rename to src/Ytdlp.NET.vNext/YtdlpProbe.cs diff --git a/src/Ytdlp.NET.v3/YtdlpRootBuilder.cs b/src/Ytdlp.NET.vNext/YtdlpRootBuilder.cs similarity index 100% rename from src/Ytdlp.NET.v3/YtdlpRootBuilder.cs rename to src/Ytdlp.NET.vNext/YtdlpRootBuilder.cs diff --git a/src/Ytdlp.NET.v3/icon.png b/src/Ytdlp.NET.vNext/icon.png similarity index 100% rename from src/Ytdlp.NET.v3/icon.png rename to src/Ytdlp.NET.vNext/icon.png diff --git a/src/Ytdlp.NET/Models/SimpleMetadata.cs b/src/Ytdlp.NET/Models/SimpleMetadata.cs deleted file mode 100644 index 7c66feb..0000000 --- a/src/Ytdlp.NET/Models/SimpleMetadata.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace ManuHub.Ytdlp.NET; - -/// -/// Lightweight metadata model returned by GetSimpleMetadataAsync. -/// Contains only the most commonly used fields, fetched in a single fast yt-dlp call. -/// -[Obsolete("This class will be removed in the next version. Use MetadataLight instead.", true)] -public class SimpleMetadata -{ - /// - /// Video ID (e.g. "Xt50Sodg7sA") - /// - public string? Id { get; init; } - - /// - /// Video title (supports Unicode / emoji / special characters) - /// - public string? Title { get; init; } - - /// - /// Video duration in seconds (null if not available) - /// - public double? Duration { get; init; } - - /// - /// Primary thumbnail URL - /// - public string? Thumbnail { get; init; } - - /// - /// View count (null if not available) - /// - public long? ViewCount { get; init; } - - /// - /// Approximate file size of best format (bytes, null if not available) - /// - public long? FileSize { get; init; } - - /// - /// Video description (first ~500 characters, supports Unicode) - /// - public string? Description { get; init; } - - /// - /// Convenience: Duration as TimeSpan - /// - public TimeSpan? DurationTimeSpan => Duration.HasValue - ? TimeSpan.FromSeconds(Duration.Value) - : null; -} diff --git a/src/Ytdlp.NET/Models/SingleVideoJson.cs b/src/Ytdlp.NET/Models/SingleVideoJson.cs deleted file mode 100644 index 1f67a95..0000000 --- a/src/Ytdlp.NET/Models/SingleVideoJson.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ManuHub.Ytdlp.NET; - -// Supporing class for single - -[Obsolete("This class will be removed in the next version. Use Metadta / MetadataLight instead.", true)] -public class SingleVideoJson -{ - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("title")] - public string? Title { get; set; } - - [JsonPropertyName("formats")] - public List? Formats { get; set; } -} - -[Obsolete("This class will be removed in the next version. Use Metadta / MetadataLight instead.", true)] -public class FormatJson -{ - [JsonPropertyName("format_id")] public string? FormatId { get; set; } - [JsonPropertyName("ext")] public string? Ext { get; set; } - [JsonPropertyName("height")] public int? Height { get; set; } - [JsonPropertyName("width")] public int? Width { get; set; } - [JsonPropertyName("resolution")] public string? Resolution { get; set; } - [JsonPropertyName("fps")] public double? Fps { get; set; } - [JsonPropertyName("audio_channels")] public int? AudioChannels { get; set; } - [JsonPropertyName("asr")] public double? Asr { get; set; } - [JsonPropertyName("tbr")] public double? Tbr { get; set; } - [JsonPropertyName("vbr")] public double? Vbr { get; set; } - [JsonPropertyName("abr")] public double? Abr { get; set; } - [JsonPropertyName("vcodec")] public string? Vcodec { get; set; } - [JsonPropertyName("acodec")] public string? Acodec { get; set; } - [JsonPropertyName("protocol")] public string? Protocol { get; set; } - [JsonPropertyName("language")] public string? Language { get; set; } - [JsonPropertyName("filesize")] public long? Filesize { get; set; } - [JsonPropertyName("filesize_approx")] public long? FilesizeApprox { get; set; } - [JsonPropertyName("format_note")] public string? FormatNote { get; set; } -} \ No newline at end of file diff --git a/src/Ytdlp.NET/README.md b/src/Ytdlp.NET/README.md index ead863b..812c95f 100644 --- a/src/Ytdlp.NET/README.md +++ b/src/Ytdlp.NET/README.md @@ -1,27 +1,20 @@ ![Static Badge](https://img.shields.io/badge/Ytdlp.NET-red) ![NuGet Version](https://img.shields.io/nuget/v/Ytdlp.NET) ![NuGet Downloads](https://img.shields.io/nuget/dt/Ytdlp.NET) # Ytdlp.NET -> **v2.2** -**Ytdlp.NET** is a fluent, strongly-typed .NET wrapper around the powerful [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) command-line tool. It provides an intuitive and customizable interface to download videos, extract audio, retrieve metadata, and process media from YouTube and hundreds of other supported platforms. +> **v3.0** ---- +**Ytdlp.NET** is a **fluent, strongly-typed .NET wrapper** around [`yt-dlp`](https://github.com/yt-dlp/yt-dlp). It provides a fully **async, event-driven interface** for downloading videos, extracting audio, retrieving metadata, and post-processing media from YouTube and hundreds of other platforms. -## Importanant Note +--- -> ### Namespace migrated to ``ManuHub.Ytdlp.NET`` (Update your using directives). +## ⚠️ Important Notes -### External JS Scripts Setup Guide - - To download from YouTube, yt-dlp needs to solve JavaScript challenges presented by YouTube using an external JavaScript runtime. - - **deno.exe** binary from denoland/deno, required as an external JavaScript runtime for yt-dlp since late 2025. - -### Recommended: Use companion NuGet packages -> **Manuhub.Ytdlp** -> **Manuhub.Deno** -> **Manuhub.FFmpeg** -> **Manuhub.FFprobe** +* **Namespace migrated**: `ManuHub.Ytdlp.NET` — update your `using` directives. +* **External JS runtime**: yt-dlp requires an external JS runtime like **deno.exe** (from [denoland/deno](https://deno.land)) for YouTube downloads with JS challenges. +* **Required tools**: -```text +``` Tools/ ├─ yt-dlp.exe ├─ deno.exe @@ -29,9 +22,15 @@ Tools/ └─ ffprobe.exe ``` -In .NET projects, you can reference the tools directory at runtime or copy the executable to your output folder as part of your build process. +> Recommended: Use companion NuGet packages: +> +> * `ManuHub.Ytdlp` +> * `ManuHub.Deno` +> * `ManuHub.FFmpeg` +> * `ManuHub.FFprobe` + +Example path resolution in .NET: -Example path resolution: ```csharp var ytdlpPath = Path.Combine(AppContext.BaseDirectory, "tools", "yt-dlp.exe"); var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "tools"); @@ -41,396 +40,222 @@ var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "tools"); ## ✨ Features -- **Fluent API**: Easily construct `yt-dlp` commands with chainable methods. -- **Progress & Events**: Real-time progress tracking, completion, and error callbacks. -- **Format Listing**: Retrieve and parse all available formats for any video. -- **Batch Downloads**: Download multiple videos with sequential or parallel execution. -- **Custom Command Injection**: Use `AddCustomCommand` to include advanced or new options. -- **Validated Options**: Rejects invalid yt-dlp commands with a built-in option whitelist. -- **Cross-Platform**: Works on Windows, macOS, and Linux (where `yt-dlp` is supported). -- **Output Templates**: Customize naming patterns with standard `yt-dlp` placeholders. -- **Update**: Implements update method to update latest yt-dlp version. ---- +* **Fluent API**: Build yt-dlp commands with `WithXxx()` methods. +* **Immutable & thread-safe**: Each method returns a new instance, safe for parallel usage. +* **Async & IAsyncDisposable**: Automatic cleanup of child processes. +* **Progress & Events**: Real-time progress tracking and post-processing notifications. +* **Format Listing**: Retrieve and parse available formats. +* **Batch Downloads**: Sequential or parallel execution. +* **Output Templates**: Flexible naming with yt-dlp placeholders. +* **Custom Command Injection**: Add extra yt-dlp options safely. +* **Cross-platform**: Windows, macOS, Linux (where yt-dlp is supported). --- -## 🚀 New in v2.2 - -- Major fixes in various probe sections and models. -- Added high-performance probe methods for metadata extraction -- Rich Metadata model parsing via `GetMetadataAsync()` -- JSON raw metadata parsing via `GetMetadataRawAsync()` -- Available formats parsing via `GetAvailableFormatsAsync()` -- Lite metadata pasing via `GetMetadataLiteAsync()` -- To get best video format `GetBestVideoFormatIdAsync(URL, maxHeight: 720)` -- To get best audio format `GetBestAudioFormatIdAsync(URL)` -- Convenience methods for best format auto-selection -- Implemented custom buffer size support (bufferKb) across all probe methods for optimized memory usage. -- Improved cancellation handling -- Better progress parsing and event system - -### Thread Safety & Disposal - -- **Ytdlp is not thread-safe** - Do **not** use the same instance from multiple threads or concurrent tasks. - Always create a fresh instance per download operation when running in parallel. - - **Safe example (concurrent batch)**: - ```csharp - var tasks = urls.Select(async url => - { - var y = new Ytdlp(); // new instance per task - await y.SetFormat("best").ExecuteAsync(url); - }); - await Task.WhenAll(tasks); - ``` - - **Unsafe (will cause race conditions)**: - ```csharp - var y = new Ytdlp(); // shared instance - var tasks = urls.Select(u => y.SetFormat("best").ExecuteAsync(u)); - await Task.WhenAll(tasks); - ``` - -- **Ytdlp is not thread-safe** - In v2.0 the class does not implement IDisposable. - Internal resources (e.g. child processes) are cleaned up automatically when the instance is garbage-collected. - Proper Dispose support and an immutable builder pattern (for safe reuse) are planned for later. - -### Fetching Video/Playlist Metadata -```csharp -var ytdlp = new Ytdlp(ytdlpPath: $"tools\\yt-dlp.exe", logger: new ConsoleLogger()); +## 🚀 New in v3.0 -string url = "https://www.youtube.com/watch?v=Xt50Sodg7sA"; +* Full support for `IAsyncDisposable` with `await using`. +* Immutable builder (`WithXxx`) for safe instance reuse. +* Updated examples for event-driven downloads. +* Simplified metadata fetching & format selection. +* High-performance probe methods with optional buffer size. +* Improved cancellation & error handling. -var metadata = await ytdlp.GetMetadataAsync(url); +--- -if (metadata == null) -{ - Console.WriteLine("No metadata returned."); - return; -} +## 🔧 Thread Safety & Disposal -// Basic info -Console.WriteLine($"Type : {metadata.Type}"); -Console.WriteLine($"ID : {metadata.Id}"); -Console.WriteLine($"Title : {metadata.Title}"); -Console.WriteLine($"Description : {(metadata.Description?.Length > 120 ? metadata.Description.Substring(0, 120) + "..." : metadata.Description)}"); -Console.WriteLine($"Thumbnail : {metadata.Thumbnail}"); +* **Immutable & thread-safe**: Each `WithXxx()` call returns a new instance. +* **Async disposal**: `Ytdlp` implements `IAsyncDisposable`. -``` +**Sequential download example**: -### Auto-Selecting Best Formats ```csharp -// Get best audio-only format ID -string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); -// → e.g. "251" (highest bitrate opus/webm) +await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) + .WithFormat("best") + .WithOutputFolder("./downloads"); -// Get best video ≤ 720p -string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 720); -// → e.g. "136" (720p mp4/avc1) +ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); +ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"Download complete: {msg}"); -// Download best combination -await ytdlp - .SetFormat($"{bestVideo}+{bestAudio}/best") - .SetOutputFolder("./downloads") - .ExecuteAsync(url); +await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); ``` -### Full Metadata + Format Selection Example -``` -var metadata = await ytdlp.GetMetadataAsync(url); +**Parallel download example**: -var best1080p = metadata.Formats? - .Where(f => f.Height == 1080 && f.Vcodec != "none") - .OrderByDescending(f => f.Fps ?? 0) - .FirstOrDefault(); +```csharp +var urls = new[] { "https://youtu.be/video1", "https://youtu.be/video2" }; -if (best1080p != null) +var tasks = urls.Select(async url => { - Console.WriteLine($"Best 1080p format: {best1080p.FormatId} – {best1080p.Resolution} @ {best1080p.Fps} fps"); -} -``` + await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) + .WithFormat("best") + .WithOutputFolder("./batch"); -### Full Raw JSON Metadata Example -``` -var jsonObjectMetadata = await ytdlp.GetMetadataRawAsync(url); + ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"[{url}] {e.Percent:F2}%"); + ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"[{url}] Download complete: {msg}"); -``` + await ytdlp.ExecuteAsync(url); +}); +await Task.WhenAll(tasks); +``` -## 📦 Prerequisites +**Key points**: -**Ytdlp.NET** is a lightweight wrapper around yt-dlp — it does **not** include yt-dlp, FFmpeg, FFprobe or Deno itself. -You have two main ways to set up the required dependencies: +1. Always create a **new instance per download** for parallel operations. +2. Always use `await using` for proper resource cleanup. +3. Attach events **after the `WithXxx()` call**. -- **.NET**: .NET 8.0 or higher -- **yt-dlp**: +--- -### Recommended: Use companion NuGet packages (easiest & portable) +## 📦 Basic Usage -We provide official build packages that automatically download and manage the latest stable binaries: - -```xml - - - - - - -``` +### Download a Single Video ```csharp -var ytdlp = new Ytdlp(ytDlpPath: @"\Tools\yt-dlp.exe"); -``` +await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) + .WithFormat("best") + .WithOutputFolder("./downloads") + .WithEmbedMetadata() + .WithEmbedThumbnail(); -## ✨ Basic Usage +ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); +ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"Download complete: {msg}"); -### 🔽 Download a Single Video +await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); +``` -Download a video with the best quality to a specified folder: +### Extract Audio ```csharp -var ytdlp = new Ytdlp("yt-dlp", new ConsoleLogger()); - -await ytdlp - .SetFormat("best") - .SetOutputFolder("downloads") - .DownloadThumbnails() - .ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); +await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") + .WithExtractAudio("mp3") + .WithOutputFolder("./audio") + .WithEmbedMetadata(); +await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); ``` -### 🎵 Extract Audio + Embed Metadata - -```csharp -await ytdlp - .ExtractAudio("mp3") - .EmbedMetadata() - .SetOutputFolder("audio") - .ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); -``` +--- -### 🧾 List Available Formats +### Fetch Metadata ```csharp -var formats = await ytdlp.GetAvailableFormatsAsync("https://youtube.com/watch?v=abc123"); -foreach (var f in formats) -{ - Console.WriteLine($"ID: {f.ID}, Resolution: {f.Resolution}, VCodec: {f.VCodec}"); -} -``` +await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe"); -### 🧪 Get Video Metadata Only +var metadata = await ytdlp.GetMetadataAsync("https://www.youtube.com/watch?v=abc123"); -```csharp -var metadata = await ytdlp.GetMetadataAsync("https://youtube.com/watch?v=abc123"); Console.WriteLine($"Title: {metadata?.Title}, Duration: {metadata?.Duration}"); ``` -### 📦 Batch Download +--- -Sequential (one after another) +### Best Format Selection ```csharp -await ytdlp - .SetFormat("best") - .SetOutputFolder("batch") - .ExecuteBatchAsync(new[] { - "https://youtu.be/vid1", "https://youtu.be/vid2" - }); -``` +await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe"); -Parallel (max 3 at a time) +string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); +string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 720); -```csharp await ytdlp - .SetFormat("best") - .SetOutputFolder("batch") - .ExecuteBatchAsync(new[] { - "https://youtu.be/vid1", "https://youtu.be/vid2" - }, maxConcurrency: 3); + .WithFormat($"{bestVideo}+{bestAudio}/best") + .WithOutputFolder("./downloads") + .ExecuteAsync(url); ``` -### ⚙️ Configuration & Options - -#### ✅ Common Fluent Methods - -#### 1. Output & Path Configuration -- ``.SetOutputFolder([Required] string outputFolderPath)`` -- ``.SetTempFolder([Required] string tempFolderPath)`` -- ``.SetHomeFolder([Required] string homeFolderPath)`` -- ``.SetFFmpegLocation([Required] string ffmpegFolder)`` -- ``.SetOutputTemplate([Required] string template)`` - -#### 2. Format Selection & Extraction -- ``.SetFormat([Required] string format)`` -- ``.ExtractAudio(string audioFormat)`` -- ``.SetResolution(string resolution)`` - -#### 3. Metadata & Format Fetching -- ``.WriteMetadataToJson()`` -- ``.ExtractMetadataOnly()`` - -#### 4. Download & Post-Processing Options -- ``.EmbedMetadata()`` -- ``.EmbedThumbnail()`` -- ``.DownloadThumbnails()`` -- ``.DownloadSubtitles(string languages = "all")`` -- ``.DownloadLivestream(bool fromStart = true)`` -- ``.DownloadLiveStreamRealTime()`` -- ``.DownloadSections(string timeRanges)`` -- ``.DownloadAudioAndVideoSeparately()`` -- ``.PostProcessFiles("--audio-quality 0")`` -- ``.MergePlaylistIntoSingleVideo(string format)`` -- ``.ConcatenateVideos()`` -- ``.ReplaceMetadata(string field, string regex, string replacement)`` -- ``.SetKeepTempFiles(bool keep)`` -- ``.SetDownloadTimeout(string timeout)`` -- ``.SetTimeout(TimeSpan timeout)`` -- ``.SetRetries(string retries)`` -- ``.SetDownloadRate(string rate)`` -- ``.SkipDownloaded()`` - -#### 5. Authentication & Security -- ``.SetAuthentication(string username, string password)`` -- ``.UseCookies("cookies.txt")`` -- ``.SetCustomHeader(string header, string value)`` - -#### 6. Network & Headers -- ``.SetUserAgent("MyApp/1.0")`` -- ``.SetReferer(string referer)`` -- ``.UseProxy(string proxy)`` -- ``.DisableAds()`` - -#### 7. Playlist & Selection -- ``.SelectPlaylistItems(string items)`` - -#### 8. Logging & Simulation -- ``.LogToFile(string logFile)`` -- ``.Simulate()`` -- ``.NoWarnings()`` - -#### 10. Advanced & Specialized Options -- ``.WithConcurrentFragments(int count)`` -- ``.RemoveSponsorBlock(params string[] categories)`` -- ``.EmbedSubtitles(string languages = "all", string? convertTo = null)`` -- ``.CookiesFromBrowser(string browser, string? profile = null)`` -- ``.GeoBypassCountry(string countryCode)`` -- ``.AddCustomCommand(string command)`` - -#### 🧩 Add Custom yt-dlp Option +--- + +### Batch Downloads ```csharp -ytdlp.AddCustomCommand("--sponsorblock-mark all"); -``` -Will be validated against internal whitelist. Invalid commands will trigger error logging via ILogger. +var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" }; -### 📡 Events +var tasks = urls.Select(async url => +{ + await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") + .WithFormat("best") + .WithOutputFolder("./batch"); -```csharp -ytdlp.OnProgressDownload += (sender, args) => Console.WriteLine($"Progress: {args.Percent:F2}% - {args.Speed} - ETA {args.ETA}"); -ytdlp.OnProgressMessage += (s, msg) => Console.WriteLine($"Progress: {msg}"); -ytdlp.OnErrorMessage += (s, err) => Console.WriteLine($"Error: {err}"); -ytdlp.OnOutputMessage += (s, msg) => Console.WriteLine(msg); -ytdlp.OnCompleteDownload += (sender, message) => Console.WriteLine($"Download complete: {message}"); -ytdlp.OnPostProcessingComplete += (s, msg) => Console.WriteLine($"Postprocessing: {msg}"); -ytdlp.OnCommandCompleted += (success, message) => Console.WriteLine($"Finished: {message}"); + await ytdlp.ExecuteAsync(url); +}); + +await Task.WhenAll(tasks); ``` -### 📄 Output Template +--- -You can customize file naming using yt-dlp placeholders: -```csharp -ytdlp.SetOutputTemplate("%(title)s-%(id)s.%(ext)s"); -``` +### Fluent Methods (v3.0) -### 🧪 Validation & Safety +#### Output & Paths -All AddCustomCommand(...) calls are validated against a known safe set of yt-dlp options, minimizing the risk of malformed or unsupported commands. +* `.WithOutputFolder(string path)` +* `.WithTempFolder(string path)` +* `.WithHomeFolder(string path)` +* `.WithFFmpegLocation(string path)` +* `.WithOutputTemplate(string template)` -### ❗ Error Handling +#### Format & Extraction -All exceptions are wrapped in YtdlpException: +* `.WithFormat(string format)` +* `.WithExtractAudio(string format = "mp3", int quality = 5)` +* `.With720pOrBest()` +* `.WithEmbedMetadata()` +* `.WithEmbedThumbnail()` +* `.WithEmbedChapters()` -```csharp -try -{ - await ytdlp.ExecuteAsync("https://invalid-url"); -} -catch (YtdlpException ex) -{ - Console.WriteLine($"Error: {ex.Message}"); -} -``` +#### Subtitles & Thumbnails -### 🧪 Version Check +* `.WithSubtitles(string langs = "all", bool auto = false)` +* `.WithEmbedSubtitles(string langs = "all", string? convertTo = null)` +* `.WithThumbnails(bool all = false)` -```csharp -string version = await ytdlp.GetVersionAsync(); -Console.WriteLine($"yt-dlp version: {version}"); -``` +#### Network & Auth -### 🔄 Update Check +* `.WithProxy(string proxy)` +* `.WithCookiesFile(string path)` +* `.WithCookiesFromBrowser(string browser)` -```csharp -UpdateChannel channel = UpdateChannel.Stable; // Master, Nightly. -string version = await ytdlp.UpdateAsync(channel); -Console.WriteLine($"yt-dlp version: {version}"); -``` +#### Download Control -### 💡 Tips +* `.WithConcurrentFragments(int count)` +* `.WithSponsorblockRemove(string categories = "all")` -- For livestreams, use: - ```csharp - .DownloadLivestream(true) - ``` -- To skip already-downloaded videos: - ```csharp - .SkipDownloaded() - ``` +#### Advanced -### 🛠 Custom Logging +* `.AddFlag(string flag)` +* `.AddOption(string key, string? value = null)` + +--- -Implement your own ILogger: +### Events ```csharp -public class ConsoleLogger : ILogger -{ - public void Log(LogType type, string message) - { - Console.WriteLine($"[{type}] {message}"); - } -} +ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); +ytdlp.OnProgressMessage += (s, msg) => Console.WriteLine(msg); +ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"Done: {msg}"); +ytdlp.OnPostProcessingComplete += (s, msg) => Console.WriteLine($"Post-processing: {msg}"); +ytdlp.OnErrorMessage += (s, err) => Console.WriteLine($"Error: {err}"); +ytdlp.OnOutputMessage += (s, msg) => Console.WriteLine(msg); +ytdlp.OnCommandCompleted += (s, e) => Console.WriteLine($"Command finished: {e.Command}"); ``` -### ⚠️ Deprecated -| Deprecated method | New method | -|-------------------|------------| -| `GetFormatsDetailedAsync(string url)` | `GetMetadataAsync(string url)` or `GetAvailableFormats(string url)` | -| `GetSimpleMetadataAsync(string url)` | `GetMetadataLiteAsync(string url)` | -| `GetSimpleMetadataAsync(string url, IEnumerable fields)` | `GetMetadataLiteAsync(string url, IEnumerable fields)`| - -| Deprecated model | New model | -|------------------|----------------| -| `SimpleMetadata` | `MetedataLight`| - -### Future versions -- `IDisposable` with process cleanup -- `YtdlpBuilder` for immutable instances -- `Ytdlp.Create()` will create a `YtdlpRootBuilder()` with `General()`, `Probe()` and `Download()` for easy use -- Persistent process pool for speed -- IAsyncDisposable for async cleanup +--- -## 🤝 Contributing +### ✅ Notes -Contributions are welcome! Please submit issues or pull requests to the [GitHub repository](https://github.com/manusoft/yt-dlp-wrapper). Ensure code follows the project’s style guidelines and includes unit tests. +* All commands now start with `WithXxx()`. +* Immutable: no shared state; safe for parallel usage. +* Always `await using` for proper disposal. +* Deprecated old methods removed. +* Probe methods remain the same (`GetMetadataAsync`, `GetAvailableFormatsAsync`, `GetBestVideoFormatIdAsync`, etc.). -## 📄 License +--- -This project is licensed under the MIT License. See the [LICENSE](https://github.com/manusoft/yt-dlp-wrapper/blob/master/LICENSE.txt) file for details. +### License ---- +MIT License — see [LICENSE](https://github.com/manusoft/yt-dlp-wrapper/blob/master/LICENSE.md) -**Author:** Manojbabu (ManuHub) +**Author:** Manojbabu (ManuHub) **Repository:** [Ytdlp.NET](https://github.com/manusoft/yt-dlp-wrapper) diff --git a/src/Ytdlp.NET/Ytdlp.NET.csproj b/src/Ytdlp.NET/Ytdlp.NET.csproj index 9e6b107..50b273d 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 - 2.2.0 + 3.0.0 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,18 +20,36 @@ icon.png icon.png - ⚠️ BREAKING CHANGE: - - 🏗️ Namespace migrated to 'ManuHub.Ytdlp.NET' (Update your using directives). - - 🛑 Several legacy probe methods are now [Obsolete] and scheduled for removal in the next major version. + Ytdlp.NET v3.0 - ✨ NEW FEATURES: - - 🔍 Added high-performance probe methods for metadata extraction. - - ⚡ Implemented custom buffer size support (bufferKb) across all probe methods for optimized memory usage. + ✨ 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. - ⚙️ IMPROVEMENTS: - - 🛠️ Internal refactoring of the probe library to ensure significantly faster fetching speeds. - - 🛡️ Enhanced input sanitization for yt-dlp process execution. - - 🛑 Metadata refactored, Major fixes inbug . + 🚀 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). + + ⚠️ 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. + + 🛠 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. false true diff --git a/src/Ytdlp.NET/Ytdlp.cs b/src/Ytdlp.NET/Ytdlp.cs index 0cea57b..2eeffa6 100644 --- a/src/Ytdlp.NET/Ytdlp.cs +++ b/src/Ytdlp.NET/Ytdlp.cs @@ -1,7 +1,6 @@ using ManuHub.Ytdlp.NET.Core; -using System.ComponentModel.DataAnnotations; +using System.Collections.Immutable; using System.Globalization; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -13,793 +12,917 @@ namespace ManuHub.Ytdlp.NET; /// and execute downloads with progress tracking and event support. /// /// -/// NOT THREAD-SAFE — do not share the same instance across threads or concurrent operations. -/// For parallel/batch downloads, create a new instance for each task. +/// THREAD-SAFE: Multiple threads can safely use the same instance concurrently. +/// Each call to creates isolated runners and parsers, preventing race conditions +/// and shared state issues. /// /// Example of safe concurrent usage: /// -/// var tasks = urls.Select(u => new Ytdlp().SetFormat("best").ExecuteAsync(u)); +/// var ytdlp = new Ytdlp() +/// .WithOutputFolder(@"D:\Downloads\YouTube") +/// .WithFormat("best"); +/// +/// var tasks = urls.Select(url => ytdlp.ExecuteAsync(url)); /// await Task.WhenAll(tasks); /// /// -/// Disposal: This class does not currently implement . -/// Resource cleanup (e.g. child processes) is handled internally. Proper disposal support -/// and an immutable builder pattern are planned for a future version. +/// Event forwarding: +/// All progress and output events are forwarded from the internal runners and parsers. +/// Subscriptions are safe per execution and cleaned up automatically to prevent memory leaks. +/// +/// Fluent builder: All configuration methods (e.g., , +/// , ) return a new instance. This preserves +/// immutability and thread-safety. +/// +/// Resource cleanup: Internal runners and parsers are disposed automatically after each +/// call. For advanced scenarios, future versions may implement +/// for global disposal of resources and cancellation support. /// -public sealed class Ytdlp +public sealed class Ytdlp : IAsyncDisposable { - private readonly string _ytDlpPath; - private readonly StringBuilder _commandBuilder = new(); - private readonly ProgressParser _progressParser; + // ────────────────────────────────────────────── Frozen configuration + private readonly string _ytdlpPath; private readonly ILogger _logger; - private readonly ProbeRunner _probe; - private readonly DownloadRunner _download; - - private string _format = "best"; - private string _outputFolder = "."; - private string _outputTemplate = "%(title)s.%(ext)s"; + private readonly string? _outputFolder; + private readonly string? _homeFolder; + private readonly string? _tempFolder; + private readonly string _outputTemplate; + private readonly string _format; + private readonly string? _cookiesFile; + private readonly string? _cookiesFromBrowser; + private readonly string? _proxy; + private readonly string? _ffmpegLocation; + private readonly string? _sponsorblockRemove; + private readonly int? _concurrentFragments; - // - /// Fired for general progress messages from yt-dlp output. - /// - //public event EventHandler? OnProgress; - - - /// - /// Fired when the yt-dlp process completes (success or failure/cancel). - /// - public event EventHandler? OnCommandCompleted; + private readonly ImmutableArray _flags; + private readonly ImmutableArray<(string Key, string? Value)> _options; - /// - /// Fired for every output line from yt-dlp (stdout). - /// - public event EventHandler? OnOutputMessage; - /// - /// Fired when download progress updates are parsed (percentage, speed, ETA). - /// + // Events public event EventHandler? OnProgressDownload; - - /// - /// Fired when a single download completes successfully. - /// - public event EventHandler? OnCompleteDownload; - - /// - /// Fired for informational progress messages (e.g. merging, extracting). - /// public event EventHandler? OnProgressMessage; - - /// - /// Fired for error messages from yt-dlp. - /// + public event EventHandler? OnOutputMessage; + public event EventHandler? OnCompleteDownload; + public event EventHandler? OnPostProcessingComplete; + public event EventHandler? OnCommandCompleted; public event EventHandler? OnErrorMessage; - /// - /// Fired when post-processing (e.g. merging, conversion) completes. - /// - public event EventHandler? OnPostProcessingComplete; + // Flag to prevent double disposal + private bool _disposed = false; - // Valid options set (used for custom command validation) - private static readonly HashSet ValidOptions = new HashSet(StringComparer.Ordinal) + public async ValueTask DisposeAsync() { - // ───────── Core ───────── - "--help","--version","--update","--update-to","--no-update", - "--config-location","--ignore-config", - - // ───────── Output / Files ───────── - "--output","-o","--paths","--output-na-placeholder", - "--restrict-filenames","--windows-filenames", - "--trim-filenames","--no-overwrites","--force-overwrites", - "--continue","--no-continue","--part","--no-part", - "--mtime","--no-mtime", - - // ───────── Format selection ───────── - "--format","-f","--format-sort","-S", - "--format-sort-force","--S-force", - "--format-sort-reset","--no-format-sort-force", - "--merge-output-format", - "--prefer-free-formats","--no-prefer-free-formats", - "--check-formats","--check-all-formats","--no-check-formats", - "--list-formats","-F", - "--video-multistreams","--no-video-multistreams", - "--audio-multistreams","--no-audio-multistreams", - - // ───────── Playlist ───────── - "--playlist-items","--playlist-start","--playlist-end", - "--playlist-random","--no-playlist","--yes-playlist", - "--flat-playlist","--no-flat-playlist","--concat-playlist", - "--playlist-reverse", - - // ───────── Network / Geo ───────── - "--proxy","--source-address","--force-ipv4","--force-ipv6", - "--geo-bypass","--no-geo-bypass", - "--geo-bypass-country","--geo-bypass-ip-block", - "--timeout","--socket-timeout", - "--retries","--fragment-retries", - "--retry-sleep","--file-access-retries", - "--http-chunk-size","--limit-rate","--throttled-rate", - - // ───────── Auth / Cookies ───────── - "--username","--password","--twofactor", - "--video-password","--netrc","--netrc-location", - "--cookies","--cookies-from-browser", - "--add-header","--user-agent","--referer", - "--age-limit", - - // ───────── Filters ───────── - "--match-title","--reject-title","--match-filter", - "--min-filesize","--max-filesize", - "--date","--datebefore","--dateafter", - "--download-archive","--force-write-archive", - "--break-on-existing","--break-per-input", - "--max-downloads", - - // ───────── Subtitles / Thumbnails ───────── - "--write-sub","--write-auto-sub", - "--sub-lang","--sub-langs","--sub-format", - "--convert-subs","--embed-subs", - "--write-thumbnail","--write-all-thumbnails", - "--embed-thumbnail", - - // ───────── Metadata ───────── - "--write-description","--write-info-json", - "--write-annotations","--write-chapters", - "--embed-metadata","--embed-info-json", - "--embed-chapters","--replace-in-metadata", - - // ───────── Post-processing ───────── - "--extract-audio","-x", - "--audio-format","--audio-quality", - "--recode-video","--remux-video", - "--postprocessor-args","--ffmpeg-location", - "--force-keyframes-at-cuts", - - // ───────── Live / Streaming ───────── - "--live-from-start","--no-live-from-start", - "--wait-for-video","--wait-for-video-to-end", - "--hls-use-mpegts","--no-hls-use-mpegts", - "--downloader","--downloader-args", - - // ───────── SponsorBlock ───────── - "--sponsorblock-mark","--sponsorblock-remove", - "--sponsorblock-chapter-title","--sponsorblock-api", - - // ───────── JS / Extractor ───────── - "--js-runtimes","--remote-components", - "--extractor-args","--force-generic-extractor", - - // ───────── Debug / Simulation ───────── - "--simulate","--skip-download", - "--dump-json","-j", - "--dump-single-json","-J", - "--print","--print-to-file", - "--quiet","--no-warnings","--verbose", - "--newline","--progress","--no-progress", - "--console-title","--write-log", - - // ───────── Misc ───────── - "--call-home","--write-pages","--write-link", - "--sleep-interval","--min-sleep-interval", - "--max-sleep-interval","--sleep-subtitles", - "--no-color", "--abort-on-error", "--concurrent-fragments", - }; - - #region Constructor & Initialization + if (_disposed) return; + _disposed = true; + + // Optionally, cancel running downloads (if you store CancellationTokens) + // e.g., _cts?.Cancel(); + + await Task.CompletedTask; + } + + // ────────────────────────────────────────────── Constructors - /// - /// Initializes a new instance of the class. - /// - /// Path to the yt-dlp executable (default: "yt-dlp"). - /// Optional logger instance (defaults to ). - /// Thrown if yt-dlp executable is not found. public Ytdlp(string ytdlpPath = "yt-dlp", ILogger? logger = null) { - _ytDlpPath = ValidatePath(ytdlpPath); - if (!File.Exists(_ytDlpPath) && !IsInPath(_ytDlpPath)) - throw new YtdlpException($"yt-dlp executable not found at {_ytDlpPath}. Install yt-dlp or specify a valid path."); - _commandBuilder = new StringBuilder(); - _progressParser = new ProgressParser(logger); + _ytdlpPath = ValidatePath(ytdlpPath); _logger = logger ?? new DefaultLogger(); - var factory = new ProcessFactory(ytdlpPath); - - _probe = new ProbeRunner(factory, _logger); - _download = new DownloadRunner(factory, _progressParser, _logger); + // defaults + _outputFolder = Directory.GetCurrentDirectory(); + _tempFolder = null; + _homeFolder = null; + _outputTemplate = "%(title)s [%(id)s].%(ext)s"; + _format = "b"; + _concurrentFragments = null; + _flags = ImmutableArray.Empty; + _options = ImmutableArray<(string, string?)>.Empty; + _cookiesFile = null; + _cookiesFromBrowser = null; + _proxy = null; + _ffmpegLocation = null; + _sponsorblockRemove = null; + } + + // Private copy constructor – every WithXxx() uses this + private Ytdlp(Ytdlp other, + string? outputFolder = null, + string? outputTemplate = null, + string? format = null, + int? concurrentFragments = null, + string? cookiesFile = null, + string? cookiesFromBrowser = null, + string? proxy = null, + string? ffmpegLocation = null, + string? sponsorblockRemove = null, + string? homeFolder = null, + string? tempFolder = null, + IEnumerable? extraFlags = null, + IEnumerable<(string, string?)>? extraOptions = null) + { + _ytdlpPath = other._ytdlpPath; + _logger = other._logger; + _outputFolder = outputFolder ?? other._outputFolder; + _homeFolder = homeFolder ?? other._homeFolder; + _tempFolder = tempFolder ?? other._tempFolder; + _outputTemplate = outputTemplate ?? other._outputTemplate; + _format = format ?? other._format; + _concurrentFragments = concurrentFragments ?? other._concurrentFragments; + _cookiesFile = cookiesFile ?? other._cookiesFile; + _cookiesFromBrowser = cookiesFromBrowser ?? other._cookiesFromBrowser; + _proxy = proxy ?? other._proxy; + _ffmpegLocation = ffmpegLocation ?? other._ffmpegLocation; + _sponsorblockRemove = sponsorblockRemove ?? other._sponsorblockRemove; + + + _flags = extraFlags is null ? other._flags : other._flags.AddRange(extraFlags); + _options = extraOptions is null ? other._options : other._options.AddRange(extraOptions); + } + + // ────────────────────────────────────────────── Fluent configuration methods + + public Ytdlp WithOutputFolder(string folder) + { + if (string.IsNullOrWhiteSpace(folder)) throw new ArgumentException("Output folder required"); + return new Ytdlp(this, outputFolder: Path.GetFullPath(folder)); + } + + public Ytdlp WithHomeFolder(string? path) + => string.IsNullOrWhiteSpace(path) + ? this + : new Ytdlp(this, homeFolder: Path.GetFullPath(path)); + + public Ytdlp WithTempFolder(string? path) + => string.IsNullOrWhiteSpace(path) + ? this + : new Ytdlp(this, tempFolder: Path.GetFullPath(path)); + + public Ytdlp WithFFmpegLocation(string? path) + => string.IsNullOrWhiteSpace(path) + ? this + : new Ytdlp(this, ffmpegLocation: path); + + public Ytdlp WithOutputTemplate(string template) + { + if (string.IsNullOrWhiteSpace(template)) throw new ArgumentException("Template required"); + return new Ytdlp(this, outputTemplate: template.Trim()); + } + + public Ytdlp WithFormat(string format) + => new Ytdlp(this, format: format.Trim()); + + public Ytdlp WithConcurrentFragments(int count = 8) + => count > 0 + ? new Ytdlp(this, concurrentFragments: count) + : this; + + + + public Ytdlp WithProxy(string? proxy) + => string.IsNullOrWhiteSpace(proxy) + ? this + : new Ytdlp(this, proxy: proxy); + + public Ytdlp WithCookiesFile(string? path) + => string.IsNullOrWhiteSpace(path) + ? this + : new Ytdlp(this, cookiesFile: Path.GetFullPath(path)); + + public Ytdlp WithCookiesFromBrowser(string browser) + => new Ytdlp(this, cookiesFromBrowser: browser); + + public Ytdlp WithSponsorblockRemove(string? categories = "all") + => string.IsNullOrWhiteSpace(categories) + ? this + : new Ytdlp(this, sponsorblockRemove: categories); + + public Ytdlp WithExtractAudio(string format = "mp3", int quality = 5) + => new Ytdlp(this, + extraFlags: new[] { "--extract-audio" }, + extraOptions: new[] + { + ("--audio-format", format), + ("--audio-quality", quality.ToString(CultureInfo.InvariantCulture)) + }); - // Subscribe to progress parser events - _progressParser.OnOutputMessage += (s, e) => OnOutputMessage?.Invoke(this, e); - _progressParser.OnProgressDownload += (s, e) => OnProgressDownload?.Invoke(this, e); - _progressParser.OnCompleteDownload += (s, e) => OnCompleteDownload?.Invoke(this, e); - _progressParser.OnProgressMessage += (s, e) => OnProgressMessage?.Invoke(this, e); - _progressParser.OnErrorMessage += (s, e) => OnErrorMessage?.Invoke(this, e); - _progressParser.OnPostProcessingComplete += (s, e) => OnPostProcessingComplete?.Invoke(this, e); + public Ytdlp WithSubtitles(string langs = "all", bool auto = false) + { + var flags = new List { "--write-subs" }; + if (auto) flags.Add("--write-auto-subs"); - // Subscribe to process complete events - _download.OnCommandCompleted += (s, e) => OnCommandCompleted?.Invoke(this, e); + return new Ytdlp(this, + extraFlags: flags, + extraOptions: new[] { ("--sub-langs", langs) }); } - #endregion - - #region Output & Path Configuration - - /// - /// Sets the output folder for downloaded files. - /// - /// The target output directory. - /// The current instance for chaining. - /// Thrown if path is empty. - public Ytdlp SetOutputFolder([Required] string outputFolderPath) + public Ytdlp WithEmbedSubtitles(string langs = "all", string? convertTo = null) { - if (string.IsNullOrWhiteSpace(outputFolderPath)) - throw new ArgumentException("Output folder path cannot be empty.", nameof(outputFolderPath)); + var flags = new List { "--embed-subs", "--write-subs" }; + var options = new List<(string, string?)> { ("--sub-langs", langs) }; - _outputFolder = outputFolderPath; - return this; + if (!string.IsNullOrWhiteSpace(convertTo)) + options.Add(("--convert-subs", convertTo)); + + return new Ytdlp(this, extraFlags: flags, extraOptions: options); } - /// - /// Sets the temporary folder path used by yt-dlp. - /// - /// Path to temporary folder. - /// The current instance for chaining. - /// Thrown if path is empty. - public Ytdlp SetTempFolder([Required] string tempFolderPath) - { - if (string.IsNullOrWhiteSpace(tempFolderPath)) - throw new ArgumentException("Temporary folder path cannot be empty.", nameof(tempFolderPath)); + public Ytdlp WithThumbnails(bool all = false) + => new Ytdlp(this, extraFlags: new[] { all ? "--write-all-thumbnails" : "--write-thumbnail" }); - _commandBuilder.Append($"--paths temp:{SanitizeInput(tempFolderPath)} "); - return this; - } + public Ytdlp WithEmbedThumbnail() => new Ytdlp(this, extraFlags: new[] { "--embed-thumbnail" }); + public Ytdlp WithEmbedMetadata() => new Ytdlp(this, extraFlags: new[] { "--embed-metadata" }); + public Ytdlp WithEmbedChapters() => new Ytdlp(this, extraFlags: new[] { "--embed-chapters" }); - /// - /// Sets the home folder path used by yt-dlp. - /// - /// Path to home folder. - /// The current instance for chaining. - /// Thrown if path is empty. - public Ytdlp SetHomeFolder([Required] string homeFolderPath) + public Ytdlp WithAria2(int connections = 16) { - if (string.IsNullOrWhiteSpace(homeFolderPath)) - throw new ArgumentException("Home folder path cannot be empty.", nameof(homeFolderPath)); - - _commandBuilder.Append($"--paths home:{SanitizeInput(homeFolderPath)} "); - return this; + return new Ytdlp(this, + extraOptions: new[] + { + ("--downloader", "aria2c"), + ("--downloader-args", $"aria2c:-x{connections} -k1M") + }); } - /// - /// Specifies the location of FFmpeg executable. - /// - /// Path to ffmpeg executable or folder. - /// The current instance for chaining. - /// Thrown if path is empty. - public Ytdlp SetFFmpegLocation([Required] string ffmpegFolder) + // 1. Playlist selection (items to download) + public Ytdlp WithPlaylistItems(string items) { - if (string.IsNullOrWhiteSpace(ffmpegFolder)) - throw new ArgumentException("FFmpeg folder cannot be empty.", nameof(ffmpegFolder)); - - _commandBuilder.Append($"--ffmpeg-location {SanitizeInput(ffmpegFolder)} "); - return this; + if (string.IsNullOrWhiteSpace(items)) + throw new ArgumentException("Playlist items string cannot be empty", nameof(items)); + return new Ytdlp(this, extraOptions: new[] { ("--playlist-items", items.Trim()) }); } - /// - /// Sets the output filename template. - /// - /// Template string (e.g. "%(title)s.%(ext)s"). - /// The current instance for chaining. - /// Thrown if template is empty. - public Ytdlp SetOutputTemplate([Required] string template) + // 2. Playlist start index + public Ytdlp WithPlaylistStart(int index) { - if (string.IsNullOrWhiteSpace(template)) - throw new ArgumentException("Output template cannot be empty.", nameof(template)); - - _outputTemplate = template.Replace("\\", "/").Trim(); - return this; + if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1"); + return new Ytdlp(this, extraOptions: new[] { ("--playlist-start", index.ToString()) }); } - #endregion - - #region Format Selection & Extraction - - /// - /// Sets the format selector string passed to -f/--format. - /// - /// Format string (e.g. "best", "137+251", "bv*+ba"). - /// The current instance for chaining. - public Ytdlp SetFormat([Required] string format) + // 3. Playlist end index + public Ytdlp WithPlaylistEnd(int index) { - _format = format; - return this; + if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1"); + return new Ytdlp(this, extraOptions: new[] { ("--playlist-end", index.ToString()) }); } - /// - /// Configures audio-only extraction with the specified format. - /// - /// Audio format (e.g. "mp3", "m4a", "best"). - /// The current instance for chaining. - /// Thrown if format is empty. - public Ytdlp ExtractAudio(string audioFormat) + // 4. Minimum filesize + public Ytdlp WithMinFileSize(string size) { - if (string.IsNullOrWhiteSpace(audioFormat)) - throw new ArgumentException("Audio format cannot be empty.", nameof(audioFormat)); - - _commandBuilder.Append($"--extract-audio --audio-format {SanitizeInput(audioFormat)} "); - return this; + // size examples: 50k, 4.2M, 1G + if (string.IsNullOrWhiteSpace(size)) + throw new ArgumentException("Size cannot be empty", nameof(size)); + return new Ytdlp(this, extraOptions: new[] { ("--min-filesize", size.Trim()) }); } - /// - /// Limits video resolution by height (uses bestvideo[height<=...]). - /// - /// Max height (e.g. "1080", "720"). - /// The current instance for chaining. - /// Thrown if resolution is empty. - public Ytdlp SetResolution(string resolution) + // 5. Maximum filesize + public Ytdlp WithMaxFileSize(string size) { - if (string.IsNullOrWhiteSpace(resolution)) - throw new ArgumentException("Resolution cannot be empty.", nameof(resolution)); - - _commandBuilder.Append($"--format \"bestvideo[height<={SanitizeInput(resolution)}]\" "); - return this; + if (string.IsNullOrWhiteSpace(size)) + throw new ArgumentException("Size cannot be empty", nameof(size)); + return new Ytdlp(this, extraOptions: new[] { ("--max-filesize", size.Trim()) }); } - #endregion - - #region Metadata & Format Fetching - - /// - /// Appends --version to the command (useful for preview or testing). - /// - /// The current instance for chaining. - public Ytdlp Version() + // 6. Date filter (upload date) + public Ytdlp WithUploadDate(string date) { - _commandBuilder.Append("--version "); - return this; + // formats: YYYYMMDD, today, yesterday, now-2weeks, etc. + if (string.IsNullOrWhiteSpace(date)) + throw new ArgumentException("Date cannot be empty", nameof(date)); + return new Ytdlp(this, extraOptions: new[] { ("--date", date.Trim()) }); } - /// - /// Appends --update to the command (useful for preview or testing). - /// - /// The current instance for chaining. - public Ytdlp Update() + // 7. Age limit / restriction + public Ytdlp WithAgeLimit(int years) { - _commandBuilder.Append("--update "); - return this; + if (years < 0) throw new ArgumentOutOfRangeException(nameof(years)); + return new Ytdlp(this, extraOptions: new[] { ("--age-limit", years.ToString()) }); } - /// - /// Appends --write-info-json to save metadata as JSON file. - /// - /// The current instance for chaining. - public Ytdlp WriteMetadataToJson() + // 8. User-Agent override + public Ytdlp WithUserAgent(string userAgent) { - _commandBuilder.Append("--write-info-json "); - return this; + if (string.IsNullOrWhiteSpace(userAgent)) + throw new ArgumentException("User-Agent cannot be empty", nameof(userAgent)); + return new Ytdlp(this, extraOptions: new[] { ("--user-agent", userAgent.Trim()) }); } - /// - /// Appends --dump-json (simulate and output metadata only). - /// - /// The current instance for chaining. - public Ytdlp ExtractMetadataOnly() + // 9. Referer override + public Ytdlp WithReferer(string referer) { - _commandBuilder.Append("--dump-json "); - return this; + if (string.IsNullOrWhiteSpace(referer)) + throw new ArgumentException("Referer cannot be empty", nameof(referer)); + return new Ytdlp(this, extraOptions: new[] { ("--referer", referer.Trim()) }); } - #endregion - - #region Download & Post-Processing Options - - /// - /// Embeds metadata into the output file. - /// - /// The current instance for chaining. - public Ytdlp EmbedMetadata() + // 10. Sleep interval between requests (anti-rate-limit) + public Ytdlp WithSleepInterval(double seconds, double? maxSeconds = null) { - _commandBuilder.Append("--embed-metadata "); - return this; + if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds)); + var opts = new List<(string, string?)> { ("--sleep-requests", seconds.ToString("F2", CultureInfo.InvariantCulture)) }; + if (maxSeconds.HasValue && maxSeconds > seconds) + { + opts.Add(("--max-sleep-requests", maxSeconds.Value.ToString("F2", CultureInfo.InvariantCulture))); + } + return new Ytdlp(this, extraOptions: opts); } - /// - /// Embeds thumbnail into the output file. - /// - /// The current instance for chaining. - public Ytdlp EmbedThumbnail() + // 11. Sleep between subtitle downloads + public Ytdlp WithSleepSubtitles(double seconds) { - _commandBuilder.Append("--embed-thumbnail "); - return this; + if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds)); + return new Ytdlp(this, extraOptions: new[] { ("--sleep-subtitles", seconds.ToString("F2", CultureInfo.InvariantCulture)) }); } - /// - /// Downloads thumbnails as separate files. - /// - /// The current instance for chaining. - public Ytdlp DownloadThumbnails() + // 12. Download archive file (skip already downloaded) + public Ytdlp WithDownloadArchive(string archivePath = "archive.txt") { - _commandBuilder.Append("--write-thumbnail "); - return this; + if (string.IsNullOrWhiteSpace(archivePath)) + throw new ArgumentException("Archive path cannot be empty", nameof(archivePath)); + return new Ytdlp(this, extraOptions: new[] { ("--download-archive", Path.GetFullPath(archivePath)) }); } - /// - /// Downloads subtitles in the specified languages. - /// - /// Language codes (default: "all"). - /// The current instance for chaining. - /// Thrown if languages is empty. - public Ytdlp DownloadSubtitles(string languages = "all") + // 13. Match title (regex include) + public Ytdlp WithMatchTitle(string regex) { - _commandBuilder.Append($"--write-sub --sub-langs {SanitizeInput(languages)} "); - return this; + if (string.IsNullOrWhiteSpace(regex)) + throw new ArgumentException("Regex cannot be empty", nameof(regex)); + return new Ytdlp(this, extraOptions: new[] { ("--match-title", regex.Trim()) }); } - - public Ytdlp DownloadLivestream(bool fromStart = true) + // 14. Reject title (regex exclude) + public Ytdlp WithRejectTitle(string regex) { - _commandBuilder.Append(fromStart ? "--live-from-start " : "--no-live-from-start "); - return this; + if (string.IsNullOrWhiteSpace(regex)) + throw new ArgumentException("Regex cannot be empty", nameof(regex)); + return new Ytdlp(this, extraOptions: new[] { ("--reject-title", regex.Trim()) }); } - public Ytdlp DownloadLiveStreamRealTime() + // 15. Max downloads (stop after N videos) + public Ytdlp WithMaxDownloads(int count) { - _commandBuilder.Append("--live-from-start --recode-video mp4 "); - return this; + if (count < 1) throw new ArgumentOutOfRangeException(nameof(count)); + return new Ytdlp(this, extraOptions: new[] { ("--max-downloads", count.ToString()) }); } - public Ytdlp DownloadSections(string timeRanges) + // Nice-to-have #16 + public Ytdlp WithNoMtime() => new Ytdlp(this, extraFlags: new[] { "--no-mtime" }); + + // Nice-to-have #17 + public Ytdlp WithNoCacheDir() => new Ytdlp(this, extraFlags: new[] { "--no-cache-dir" }); + + // Nice-to-have #18 – very popular for high-quality + fallback + public Ytdlp With1080pOrBest() + => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); + + // Nice-to-have #19 + public Ytdlp WithNoPlaylist() => new Ytdlp(this, extraFlags: new[] { "--no-playlist" }); + + // Nice-to-have #20 + public Ytdlp WithYesPlaylist() => new Ytdlp(this, extraFlags: new[] { "--yes-playlist" }); + + + // 21. Geo-bypass country (two-letter ISO code) + public Ytdlp WithGeoBypassCountry(string countryCode) { - if (string.IsNullOrWhiteSpace(timeRanges)) - throw new ArgumentException("Time ranges cannot be empty.", nameof(timeRanges)); + if (string.IsNullOrWhiteSpace(countryCode) || countryCode.Length != 2) + throw new ArgumentException("Geo-bypass country must be a 2-letter ISO code", nameof(countryCode)); - _commandBuilder.Append($"--download-sections {SanitizeInput(timeRanges)} "); - return this; + return new Ytdlp(this, + extraOptions: new[] { ("--geo-bypass-country", countryCode.Trim().ToUpperInvariant()) }); } - public Ytdlp DownloadAudioAndVideoSeparately() + // 22. No geo-bypass (disable automatic country bypass) + public Ytdlp WithNoGeoBypass() + => new Ytdlp(this, extraFlags: new[] { "--no-geo-bypass" }); + + // 23. Match-filter (advanced filter expression) + public Ytdlp WithMatchFilter(string filterExpression) { - _commandBuilder.Append("--write-video --write-audio "); - return this; + if (string.IsNullOrWhiteSpace(filterExpression)) + throw new ArgumentException("Match filter expression cannot be empty", nameof(filterExpression)); + + return new Ytdlp(this, + extraOptions: new[] { ("--match-filter", filterExpression.Trim()) }); } - public Ytdlp PostProcessFiles(string operation) + // 24. Break on existing (stop when file already in archive) + public Ytdlp WithBreakOnExisting() + => new Ytdlp(this, extraFlags: new[] { "--break-on-existing" }); + + // 25. Break on reject (stop when a video is filtered out by --match-filter) + public Ytdlp WithBreakOnReject() + => new Ytdlp(this, extraFlags: new[] { "--break-on-reject" }); + + // 26. Postprocessor args (ppa) - most common use-cases + public Ytdlp WithPostprocessorArgs(string postprocessorName, string arguments) { - if (string.IsNullOrWhiteSpace(operation)) - throw new ArgumentException("Operation cannot be empty.", nameof(operation)); + if (string.IsNullOrWhiteSpace(postprocessorName) || string.IsNullOrWhiteSpace(arguments)) + throw new ArgumentException("Both postprocessor name and arguments are required"); - _commandBuilder.Append($"--postprocessor-args \"{SanitizeInput(operation)}\" "); - return this; + string combined = $"{postprocessorName.Trim()}:{arguments.Trim()}"; + return new Ytdlp(this, + extraOptions: new[] { ("--postprocessor-args", combined) }); } - public Ytdlp MergePlaylistIntoSingleVideo(string format) + // 27. Force key frames at cuts (useful when cutting with --download-sections) + public Ytdlp WithForceKeyframesAtCuts() + => new Ytdlp(this, extraFlags: new[] { "--force-keyframes-at-cuts" }); + + // 28. Prefer free formats (when multiple formats have similar quality) + public Ytdlp WithPreferFreeFormats() + => new Ytdlp(this, extraFlags: new[] { "--prefer-free-formats" }); + + // 29. No prefer free formats (default behavior - explicit) + public Ytdlp WithNoPreferFreeFormats() + => new Ytdlp(this, extraFlags: new[] { "--no-prefer-free-formats" }); + + // 30. Merge output format (force container after download & post-processing) + public Ytdlp WithMergeOutputFormat(string format) { + // Common values: mp4, mkv, webm, mov, avi, flv if (string.IsNullOrWhiteSpace(format)) - throw new ArgumentException("Format cannot be empty.", nameof(format)); + throw new ArgumentException("Merge output format cannot be empty", nameof(format)); - _commandBuilder.Append($"--merge-output-format {SanitizeInput(format)} "); - return this; + return new Ytdlp(this, + extraOptions: new[] { ("--merge-output-format", format.Trim().ToLowerInvariant()) }); } - public Ytdlp ConcatenateVideos() - { - _commandBuilder.Append("--concat-playlist always "); - return this; - } + // Bonus 31 – very popular shortcut + public Ytdlp WithBestUpTo1080p() + => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); - public Ytdlp ReplaceMetadata(string field, string regex, string replacement) - { - if (string.IsNullOrWhiteSpace(field) || string.IsNullOrWhiteSpace(regex) || replacement == null) - throw new ArgumentException("Metadata field, regex, and replacement cannot be empty."); + // Bonus 32 + public Ytdlp WithKeepFragments() + => new Ytdlp(this, extraFlags: new[] { "--keep-fragments" }); - _commandBuilder.Append($"--replace-in-metadata {SanitizeInput(field)} {SanitizeInput(regex)} {SanitizeInput(replacement)} "); - return this; - } + // Bonus 33 – useful for debugging + public Ytdlp WithVerbose() + => new Ytdlp(this, extraFlags: new[] { "--verbose" }); - /// - /// Keeps temporary/intermediate files after processing. - /// - /// True to keep temp files. - /// The current instance for chaining. - public Ytdlp SetKeepTempFiles(bool keep) + + // 31. Reverse playlist order + public Ytdlp WithPlaylistReverse() + => new Ytdlp(this, extraFlags: new[] { "--playlist-reverse" }); + + // 32. Random playlist order + public Ytdlp WithPlaylistRandom() + => new Ytdlp(this, extraFlags: new[] { "--playlist-random" }); + + // 33. Lazy playlist (process entries as received – good for very large playlists) + public Ytdlp WithLazyPlaylist() + => new Ytdlp(this, extraFlags: new[] { "--lazy-playlist" }); + + // 34. Flat playlist (do not extract individual video URLs – faster for listing) + public Ytdlp WithFlatPlaylist() + => new Ytdlp(this, extraFlags: new[] { "--flat-playlist" }); + + // 35. Write info.json metadata file + public Ytdlp WithWriteInfoJson() + => new Ytdlp(this, extraFlags: new[] { "--write-info-json" }); + + // 36. Clean info.json (remove private/empty fields) + public Ytdlp WithCleanInfoJson() + => new Ytdlp(this, extraFlags: new[] { "--clean-info-json" }); + + // 37. No clean info.json (keep all fields) + public Ytdlp WithNoCleanInfoJson() + => new Ytdlp(this, extraFlags: new[] { "--no-clean-info-json" }); + + // 38. Simulate only (do not download anything – useful for testing/format listing) + public Ytdlp WithSimulate() + => new Ytdlp(this, extraFlags: new[] { "--simulate" }); + + // 39. Skip actual download (but do post-processing if applicable) + public Ytdlp WithSkipDownload() + => new Ytdlp(this, extraFlags: new[] { "--skip-download" }); + + // 40. Write description to .description file + public Ytdlp WithWriteDescription() + => new Ytdlp(this, extraFlags: new[] { "--write-description" }); + + // 41. Keep intermediate video file after post-processing + public Ytdlp WithKeepVideo() + => new Ytdlp(this, extraFlags: new[] { "-k", "--keep-video" }); + + // 42. Do not overwrite post-processed files + public Ytdlp WithNoPostOverwrites() + => new Ytdlp(this, extraFlags: new[] { "--no-post-overwrites" }); + + // 43. Force keyframes at cuts (important when using --download-sections) + + + // 44. Remux video into specified container format + public Ytdlp WithRemuxVideo(string format = "mp4") { - if (keep) _commandBuilder.Append("-k"); - return this; + // Supported: mp4, mkv, avi, webm, flv, mov, ... + if (string.IsNullOrWhiteSpace(format)) + throw new ArgumentException("Remux format cannot be empty", nameof(format)); + + return new Ytdlp(this, + extraOptions: new[] { ("--remux-video", format.Trim().ToLowerInvariant()) }); } - public Ytdlp SetDownloadTimeout(string timeout) + // 45. Recode / re-encode video into specified format + public Ytdlp WithRecodeVideo(string format = "mp4") { - if (string.IsNullOrWhiteSpace(timeout)) - throw new ArgumentException("Timeout cannot be empty.", nameof(timeout)); + // Supported: mp4, mkv, avi, webm, flv, mov, ... + if (string.IsNullOrWhiteSpace(format)) + throw new ArgumentException("Recode format cannot be empty", nameof(format)); - _commandBuilder.Append($"--download-timeout {SanitizeInput(timeout)} "); - return this; + return new Ytdlp(this, + extraOptions: new[] { ("--recode-video", format.Trim().ToLowerInvariant()) }); } - public Ytdlp SetTimeout(TimeSpan timeout) + // 46. Convert thumbnails to specified format + public Ytdlp WithConvertThumbnails(string format = "jpg") { - if (timeout.TotalSeconds <= 0) - throw new ArgumentException("Timeout must be greater than zero.", nameof(timeout)); + // Supported: jpg, png, webp + if (string.IsNullOrWhiteSpace(format)) + throw new ArgumentException("Thumbnail format cannot be empty", nameof(format)); - _commandBuilder.Append($"--timeout {timeout.TotalSeconds} "); - return this; + return new Ytdlp(this, + extraOptions: new[] { ("--convert-thumbnails", format.Trim().ToLowerInvariant()) }); } - /// - /// Sets number of retries for failed downloads/fragments. - /// - /// Retry count or "infinite". - /// The current instance for chaining. - public Ytdlp SetRetries(string retries) + // 48. Postprocessor arguments for Merger (most common use-case) + public Ytdlp WithMergerArgs(string args) + => WithPostprocessorArgs("Merger", args); + + // 49. Postprocessor arguments for ModifyChapters + public Ytdlp WithModifyChaptersArgs(string args) + => WithPostprocessorArgs("ModifyChapters", args); + + // 50. Postprocessor arguments for ExtractAudio + public Ytdlp WithExtractAudioArgs(string args) + => WithPostprocessorArgs("ExtractAudio", args); + + // Bonus – common combo: remux to mp4 + embed metadata + chapters + thumbnail + public Ytdlp WithMp4PostProcessingPreset() + => this + .WithRemuxVideo("mp4") + .WithEmbedMetadata() + .WithEmbedChapters() + .WithEmbedThumbnail(); + + // Bonus – force mkv container (popular for archiving) + public Ytdlp WithMkvOutput() + => new Ytdlp(this, + extraOptions: new[] + { + ("--remux-video", "mkv"), + ("--merge-output-format", "mkv") + }); + + // 51. Download livestream from the start (when possible) + public Ytdlp WithLiveFromStart() + => new Ytdlp(this, extraFlags: new[] { "--live-from-start" }); + + // 52. Explicitly disable downloading from the beginning of a live stream + public Ytdlp WithNoLiveFromStart() + => new Ytdlp(this, extraFlags: new[] { "--no-live-from-start" }); + + // 53. Wait for a scheduled live stream to start + public Ytdlp WithWaitForVideo(TimeSpan? maxWait = null) { - _commandBuilder.Append($"--retries {SanitizeInput(retries)} "); - return this; + var opts = new List<(string Key, string? Value)>(); + + opts.Add(("--wait-for-video", "any")); // "any" = wait indefinitely or until timeout + + if (maxWait.HasValue && maxWait.Value.TotalSeconds > 0) + { + opts.Add(("--wait-for-video", maxWait.Value.TotalSeconds.ToString("F0"))); + } + + return new Ytdlp(this, extraOptions: opts); } - /// - /// Limits download speed (e.g. "500K", "1M"). - /// - /// Rate limit string. - /// The current instance for chaining. - public Ytdlp SetDownloadRate(string rate) + // 54. Wait until the live stream actually ends before finishing + public Ytdlp WithWaitUntilLiveEnds() + => new Ytdlp(this, extraFlags: new[] { "--wait-for-video-to-end" }); + + // 55. Use mpegts container/format for HLS live streams (better compatibility in some players) + public Ytdlp WithHlsUseMpegts() + => new Ytdlp(this, extraFlags: new[] { "--hls-use-mpegts" }); + + // 56. Do not use mpegts for HLS (use default fragmented mp4) + public Ytdlp WithNoHlsUseMpegts() + => new Ytdlp(this, extraFlags: new[] { "--no-hls-use-mpegts" }); + + // 57. External downloader for live streams (e.g. ffmpeg, aria2c, ...) + public Ytdlp WithExternalDownloader(string downloaderName, string? downloaderArgs = null) { - _commandBuilder.Append($"--limit-rate {SanitizeInput(rate)} "); - return this; + if (string.IsNullOrWhiteSpace(downloaderName)) + throw new ArgumentException("Downloader name cannot be empty", nameof(downloaderName)); + + var opts = new List<(string, string?)> { ("--downloader", downloaderName.Trim()) }; + + if (!string.IsNullOrWhiteSpace(downloaderArgs)) + { + opts.Add(("--downloader-args", downloaderArgs.Trim())); + } + + return new Ytdlp(this, extraOptions: opts); } - /// - /// Skips already downloaded files using an archive. - /// - /// The current instance for chaining. - public Ytdlp SkipDownloaded() + // 58. Use ffmpeg as external downloader for live streams (most common choice) + public Ytdlp WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null) + => WithExternalDownloader("ffmpeg", extraFfmpegArgs); + + // 59. Set fragment retries specifically useful for unstable live streams + public Ytdlp WithFragmentRetries(int retries) { - _commandBuilder.Append("--download-archive downloaded.txt "); return this; + // -1 = infinite + string value = retries < 0 ? "infinite" : retries.ToString(); + return new Ytdlp(this, + extraOptions: new[] { ("--fragment-retries", value) }); } - #endregion + // 60. Prefer native HLS downloader (instead of ffmpeg) – sometimes more stable + public Ytdlp WithHlsNative() + => new Ytdlp(this, extraOptions: new[] { ("--downloader", "hlsnative") }); - #region Authentication & Security - /// - /// Sets username and password for authentication. - /// - /// Username or email. - /// Password. - /// The current instance for chaining. - public Ytdlp SetAuthentication(string username, string password) + // 63. Maximum video height / resolution limit + public Ytdlp WithMaxHeight(int height) { - if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) - throw new ArgumentException("Username and password cannot be empty."); + if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive"); - _commandBuilder.Append($"--username {SanitizeInput(username)} --password {SanitizeInput(password)} "); - return this; + string formatSelector = $"bestvideo[height<={height}]+bestaudio/best"; + return new Ytdlp(this, format: formatSelector); } - /// - /// Loads cookies from a file. - /// - /// Path to cookies file (Netscape format). - /// The current instance for chaining. - public Ytdlp UseCookies(string cookieFile) + // 64. Maximum video height with fallback to best available + public Ytdlp WithMaxHeightOrBest(int height) { - if (string.IsNullOrWhiteSpace(cookieFile)) - throw new ArgumentException("Cookie file path cannot be empty.", nameof(cookieFile)); + if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive"); - _commandBuilder.Append($"--cookies {SanitizeInput(cookieFile)} "); - return this; + string formatSelector = $"bestvideo[height<={height}]+bestaudio/best[height<={height}]/best"; + return new Ytdlp(this, format: formatSelector); } - /// - /// Adds a custom HTTP header. - /// - /// Header name (e.g. "Referer"). - /// Header value. - /// The current instance for chaining. - public Ytdlp SetCustomHeader(string header, string value) - { - if (string.IsNullOrWhiteSpace(header) || string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Header and value cannot be empty."); + // 65. Best video + best audio (classic high-quality merge) + public Ytdlp WithBestVideoPlusBestAudio() + => new Ytdlp(this, format: "bestvideo+bestaudio/best"); - _commandBuilder.Append($"--add-header \"{SanitizeInput(header)}:{SanitizeInput(value)}\" "); - return this; - } + // 67. Best video up to 720p + best audio + public Ytdlp With720pOrBest() + => new Ytdlp(this, format: "bv*[height<=?720]+ba/best/best"); + - #endregion + // 68. Audio-only – best quality audio + public Ytdlp WithBestAudioOnly() + => new Ytdlp(this, format: "bestaudio"); - #region Network & Headers + // 69. Prefer video formats with higher bitrate (when resolution is similar) + public Ytdlp WithFormatSortBitrate() + => new Ytdlp(this, extraOptions: new[] { ("-S", "br") }); - /// - /// Sets custom User-Agent header. - /// - /// User-Agent string. - /// The current instance for chaining. - public Ytdlp SetUserAgent(string userAgent) - { - if (string.IsNullOrWhiteSpace(userAgent)) - throw new ArgumentException("User agent cannot be empty.", nameof(userAgent)); + // 70. Prefer formats with higher resolution first, then bitrate + public Ytdlp WithFormatSortResolutionThenBitrate() + => new Ytdlp(this, extraOptions: new[] { ("-S", "res,br") }); - _commandBuilder.Append($"--user-agent {SanitizeInput(userAgent)} "); - return this; - } - /// - /// Sets custom Referer header. - /// - /// Referer URL. - /// The current instance for chaining. - public Ytdlp SetReferer(string referer) - { - if (string.IsNullOrWhiteSpace(referer)) - throw new ArgumentException("Referer URL cannot be empty.", nameof(referer)); + // Bonus A – very popular preset + public Ytdlp WithBestUpTo1440p() + => new Ytdlp(this, format: "bestvideo[height<=?1440]+bestaudio/best"); - _commandBuilder.Append($"--referer {SanitizeInput(referer)} "); - return this; - } + // Bonus B – avoid very high resolutions (4K+) + public Ytdlp WithNo4k() + => new Ytdlp(this, format: "bestvideo[height<=?2160]+bestaudio/best"); - /// - /// Uses a proxy server for all requests. - /// - /// Proxy URL (e.g. "http://host:port"). - /// The current instance for chaining. - public Ytdlp UseProxy(string proxy) - { - if (string.IsNullOrWhiteSpace(proxy)) - throw new ArgumentException("Proxy URL cannot be empty.", nameof(proxy)); + // Bonus C – audio-only with specific codec preference + public Ytdlp WithBestM4aAudio() + => new Ytdlp(this, format: "bestaudio[ext=m4a]/bestaudio/best"); - _commandBuilder.Append($"--proxy {SanitizeInput(proxy)} "); - return this; - } + // 71. Restrict filenames to ASCII-only + avoid problematic characters + public Ytdlp WithRestrictFilenames() + => new Ytdlp(this, extraFlags: new[] { "--restrict-filenames" }); - /// - /// Disables advertisements where supported. - /// - /// The current instance for chaining. - public Ytdlp DisableAds() + // 72. Force Windows-compatible filenames (avoid reserved names, invalid chars) + public Ytdlp WithWindowsFilenames() + => new Ytdlp(this, extraFlags: new[] { "--windows-filenames" }); + + // 73. Limit filename length (excluding extension) + public Ytdlp WithTrimFilenames(int maxLength) { - _commandBuilder.Append("--no-ads "); - return this; + if (maxLength < 10) + throw new ArgumentOutOfRangeException(nameof(maxLength), "Length should be at least 10 characters"); + + return new Ytdlp(this, + extraOptions: new[] { ("--trim-filenames", maxLength.ToString()) }); } - #endregion + // 74. No overwrite existing files + public Ytdlp WithNoOverwrites() + => new Ytdlp(this, extraFlags: new[] { "--no-overwrites" }); - #region Playlist & Selection + // 75. Force overwrite existing files + public Ytdlp WithForceOverwrites() + => new Ytdlp(this, extraFlags: new[] { "--force-overwrites" }); - public Ytdlp SelectPlaylistItems(string items) - { - if (string.IsNullOrWhiteSpace(items)) - throw new ArgumentException("Playlist items cannot be empty.", nameof(items)); + // 76. Continue partially downloaded files + public Ytdlp WithContinue() + => new Ytdlp(this, extraFlags: new[] { "--continue" }); - _commandBuilder.Append($"--playlist-items {SanitizeInput(items)} "); - return this; - } + // 77. Do not continue partially downloaded files (start from beginning) + public Ytdlp WithNoContinue() + => new Ytdlp(this, extraFlags: new[] { "--no-continue" }); - #endregion + // 78. Use .part files during download + public Ytdlp WithPartFiles() + => new Ytdlp(this, extraFlags: new[] { "--part" }); - #region Logging & Simulation + // 79. Do not use .part files (write directly to final filename) + public Ytdlp WithNoPartFiles() + => new Ytdlp(this, extraFlags: new[] { "--no-part" }); - /// - /// Writes yt-dlp log output to a file. - /// - /// Path to log file. - /// The current instance for chaining. - public Ytdlp LogToFile(string logFile) - { - if (string.IsNullOrWhiteSpace(logFile)) - throw new ArgumentException("Log file path cannot be empty.", nameof(logFile)); + // 80. Use server mtime (Last-Modified header) for file timestamp + public Ytdlp WithMtime() + => new Ytdlp(this, extraFlags: new[] { "--mtime" }); - _commandBuilder.Append($"--write-log {SanitizeInput(logFile)} "); - return this; - } - /// - /// Simulates download without saving files. - /// - /// The current instance for chaining. - public Ytdlp Simulate() - { - _commandBuilder.Append("--simulate "); - return this; - } + public Ytdlp AddFlag(string flag) + => new Ytdlp(this, extraFlags: new[] { flag.Trim() }); - /// - /// Ignore warnings - /// - /// The current instance for chaining. - public Ytdlp NoWarning() + public Ytdlp AddOption(string key, string? value = null) + => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); + + // ────────────────────────────────────────────── Command building (called only at execution time) + + private List BuildArguments(string url) { - _commandBuilder.Append("--no-warnings "); - return this; - } + var args = new List(); - #endregion + // Paths — use home, temp, and outputFolder separately + if (!string.IsNullOrWhiteSpace(_homeFolder)) + { + args.Add("--paths"); + args.Add($"home:{_homeFolder}"); + } - #region Advanced & Specialized Options + if (!string.IsNullOrWhiteSpace(_tempFolder)) + { + args.Add("--paths"); + args.Add($"temp:{_tempFolder}"); + } - public Ytdlp WithConcurrentFragments(int count) - { - if (count < 1) throw new ArgumentOutOfRangeException(nameof(count)); - _commandBuilder.Append($"--concurrent-fragments {count} "); - return this; - } + // Output folder is only for -o template + if (!string.IsNullOrWhiteSpace(_outputFolder) && !string.IsNullOrWhiteSpace(_outputTemplate)) + { + // Combine folder + template + string fullOutputPath = Path.Combine(_outputFolder, _outputTemplate) + .Replace('\\', '/'); // yt-dlp prefers forward slashes + args.Add("-o"); + args.Add(fullOutputPath); + } + else if (!string.IsNullOrWhiteSpace(_outputTemplate)) + { + args.Add("-o"); + args.Add(_outputTemplate); + } - public Ytdlp RemoveSponsorBlock(params string[] categories) - { - var cats = categories.Length == 0 ? "all" : string.Join(",", categories); - _commandBuilder.Append($"--sponsorblock-remove {SanitizeInput(cats)} "); - return this; - } + // Format + if (!string.IsNullOrWhiteSpace(_format)) + { + args.Add("-f"); + args.Add(_format); + } - public Ytdlp EmbedSubtitles(string languages = "all", string? convertTo = null) - { - _commandBuilder.Append($"--write-subs --sub-langs {SanitizeInput(languages)} "); - if (!string.IsNullOrEmpty(convertTo)) _commandBuilder.Append($"--convert-subs {SanitizeInput(convertTo)} "); - if (convertTo?.Equals("embed", StringComparison.OrdinalIgnoreCase) == true) - _commandBuilder.Append("--embed-subs "); - return this; - } + // Concurrent fragments + if (_concurrentFragments > 1) + { + args.Add("--concurrent-fragments"); + args.Add(_concurrentFragments.Value.ToString()); + } - public Ytdlp CookiesFromBrowser(string browser, string? profile = null) - { - var arg = profile != null ? $"{browser}:{profile}" : browser; - _commandBuilder.Append($"--cookies-from-browser {SanitizeInput(arg)} "); - return this; + // Flags + if (_flags.Length > 0) + args.AddRange(_flags); + + // Options + if (_options.Length > 0) + { + foreach (var kv in _options) + { + args.Add(kv.Key); + if (kv.Value != null) + args.Add(kv.Value); + } + } + + // Special single-value options + if (_cookiesFile is not null) { args.Add("--cookies"); args.Add(_cookiesFile); } + if (_cookiesFromBrowser is not null) { args.Add("--cookies-from-browser"); args.Add(Quote(_cookiesFromBrowser)); } + if (_proxy is not null) { args.Add("--proxy"); args.Add(_proxy); } + if (_ffmpegLocation is not null) { args.Add("--ffmpeg-location"); args.Add(_ffmpegLocation); } + if (_sponsorblockRemove is not null) { args.Add("--sponsorblock-remove"); args.Add(_sponsorblockRemove); } + + // URL last + args.Add(url); + + return args; } - public Ytdlp GeoBypassCountry(string countryCode) + public string Preview(string url) { - if (countryCode.Length != 2) throw new ArgumentException("Country code must be 2 letters."); - _commandBuilder.Append($"--geo-bypass-country {SanitizeInput(countryCode.ToUpperInvariant())} "); - return this; + var argsList = BuildArguments(url); + return string.Join(" ", argsList.Select(Quote)); } - public Ytdlp AddCustomCommand(string customCommand) + // ────────────────────────────────────────────── Execution + + public async Task ExecuteAsync(string url, CancellationToken ct = default) { - if (string.IsNullOrWhiteSpace(customCommand)) - throw new ArgumentException("Custom command cannot be empty.", nameof(customCommand)); + ct.ThrowIfCancellationRequested(); - var parts = customCommand - .Split(' ', StringSplitOptions.RemoveEmptyEntries) - .Select(SanitizeInput) - .ToArray(); + if (string.IsNullOrWhiteSpace(url)) + throw new ArgumentException("URL required", nameof(url)); - // Validate only option tokens (flags) - foreach (var part in parts) + // Ensure output folder exists + try { - if (part.StartsWith("-", StringComparison.Ordinal) && !IsAllowedOption(part)) - { - var errorMessage = $"Invalid yt-dlp option: {part}"; - OnErrorMessage?.Invoke(this, errorMessage); - _logger.Log(LogType.Error, errorMessage); - return this; - } + Directory.CreateDirectory(_outputFolder); + _logger.Log(LogType.Info, $"Ensured output folder exists: {_outputFolder}"); + } + catch (Exception ex) + { + _logger.Log(LogType.Error, $"Failed to create output folder: {ex.Message}"); + throw new YtdlpException("Failed to create output folder", ex); } - _commandBuilder.Append(' ').Append(string.Join(' ', parts)); + var argsList = BuildArguments(url); + var arguments = string.Join(" ", argsList.Select(Quote)); - return this; - } + _logger.Log(LogType.Info, $"Executing: {_ytdlpPath} {arguments}"); - #endregion + // Create isolated execution components + var factory = new ProcessFactory(_ytdlpPath); + var progressParser = new ProgressParser(_logger); + var download = new DownloadRunner(factory, progressParser, _logger); - #region Execution & Utility Methods + // Forward progress events locally inside this method + void OnProgressDownloadHandler(object? s, DownloadProgressEventArgs e) + => OnProgressDownload?.Invoke(this, e); - /// - /// Preview Commands - /// - /// - /// - /// - public string PreviewCommand(string url) + void OnProgressMessageHandler(object? s, string msg) + => OnProgressMessage?.Invoke(this, msg); + + // Attach progress handlers + progressParser.OnProgressDownload += OnProgressDownloadHandler; + progressParser.OnProgressMessage += OnProgressMessageHandler; + + // Forward other events + progressParser.OnOutputMessage += (_, e) => OnOutputMessage?.Invoke(this, e); + progressParser.OnCompleteDownload += (_, e) => OnCompleteDownload?.Invoke(this, e); + progressParser.OnErrorMessage += (_, e) => OnErrorMessage?.Invoke(this, e); + progressParser.OnPostProcessingComplete += (_, e) => OnPostProcessingComplete?.Invoke(this, e); + + download.OnCommandCompleted += (_, e) => OnCommandCompleted?.Invoke(this, e); + + try + { + await download.RunAsync(arguments, ct); + } + finally + { + // Unsubscribe immediately after execution to prevent memory leaks + progressParser.OnProgressDownload -= OnProgressDownloadHandler; + progressParser.OnProgressMessage -= OnProgressMessageHandler; + } + } + + // ────────────────────────────────────────────── Helpers + + private static string ValidatePath(string path) { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentException("URL cannot be empty.", nameof(url)); + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("yt-dlp path cannot be empty"); - string template = Path.Combine(_outputFolder, _outputTemplate.Replace("\\", "/")); + if (!File.Exists(path) && !IsExecutableInPath(path)) + throw new FileNotFoundException($"yt-dlp executable not found: {path}"); - string arguments = $"{_commandBuilder} -f \"{_format}\" -o \"{template}\" {SanitizeInput(url)}"; + return path; + } - return $"{_ytDlpPath} {arguments}"; + private static bool IsExecutableInPath(string name) + { + return Environment.GetEnvironmentVariable("PATH")? + .Split(Path.PathSeparator) + .Any(p => File.Exists(Path.Combine(p, name))) ?? false; } + private static string Quote(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return "\"\""; + // Escape " and \ + string escaped = value.Replace("\\", "\\\\").Replace("\"", "\\\""); + return $"\"{escaped}\""; + } + + #region Execution & Utility Methods + /// /// Retrieves the current version string of the underlying yt-dlp executable. @@ -811,7 +934,7 @@ public string PreviewCommand(string url) /// public async Task GetVersionAsync(CancellationToken ct = default) { - var output = await _probe.RunAsync("--version", ct); + var output = await Probe().RunAsync("--version", ct); string version = output is null ? string.Empty : output.Trim(); _logger.Log(LogType.Info, $"yt-dlp version: {version}"); return version; @@ -828,7 +951,7 @@ public async Task GetVersionAsync(CancellationToken ct = default) /// public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stable, CancellationToken cancellationToken = default) { - var output = await _probe.RunAsync($"--update-to {channel.ToString().ToLowerInvariant()}", cancellationToken); + var output = await Probe().RunAsync($"--update-to {channel.ToString().ToLowerInvariant()}", cancellationToken); if (output is null) return string.Empty; @@ -841,7 +964,7 @@ public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stab return "yt-dlp update check completed (no changes detected)."; - + } /// @@ -870,9 +993,9 @@ public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stab $"--lazy-playlist " + $"--quiet " + $"--no-warnings " + - $"{SanitizeInput(url)}"; + $"{Quote(url)}"; - var json = await _probe.RunAsync(arguments, ct); + var json = await Probe().RunAsync(arguments, ct); if (string.IsNullOrWhiteSpace(json)) { @@ -927,9 +1050,9 @@ public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stab $"--lazy-playlist " + $"--quiet " + $"--no-warnings " + - $"{SanitizeInput(url)}"; + $"{Quote(url)}"; - var json = await _probe.RunAsync(arguments, ct); + var json = await Probe().RunAsync(arguments, ct); if (string.IsNullOrWhiteSpace(json)) { @@ -967,7 +1090,7 @@ public async Task> GetAvailableFormatsAsync(string url, Cancellatio if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("Video URL cannot be empty.", nameof(url)); - var output = await _probe.RunAsync($"-F {SanitizeInput(url)}", ct, bufferKb); + var output = await Probe().RunAsync($"-F {Quote(url)}", ct, bufferKb); if (string.IsNullOrWhiteSpace(output)) { @@ -1012,9 +1135,9 @@ public async Task> GetAvailableFormatsAsync(string url, Cancellatio var printArg = $"--print \"{string.Join(separator, fields)}\""; - var arguments = $"{printArg} --skip-download --no-playlist --quiet {SanitizeInput(url)}"; + 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, bufferKb); if (string.IsNullOrWhiteSpace(output)) return null; @@ -1075,9 +1198,9 @@ public async Task> GetAvailableFormatsAsync(string url, Cancellatio var printParts = fields.Select(f => $"%({f})s"); var printFormat = string.Join(separator, printParts); - var arguments = $"--print \"{printFormat}\" --skip-download --no-playlist --quiet {SanitizeInput(url)}"; + 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, bufferKb); if (string.IsNullOrWhiteSpace(rawOutput)) return null; @@ -1157,53 +1280,14 @@ public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = } - /// - /// Executes a download process for the specified URL using an optional output template. - /// - /// The source URL to process. - /// A to terminate the process execution. - /// - /// A representing the asynchronous execution of the process. - /// - /// - /// - public async Task ExecuteAsync(string url, CancellationToken ct = default) + private ProbeRunner Probe() { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentException("URL cannot be empty.", nameof(url)); - - if (string.IsNullOrWhiteSpace(_format)) - _format = "best"; - - // Ensure output folder exists - try - { - Directory.CreateDirectory(_outputFolder); - _logger.Log(LogType.Info, $"Output folder: {Path.GetFullPath(_outputFolder)}"); - } - catch (Exception ex) - { - _logger.Log(LogType.Error, $"Failed to create output folder {_outputFolder}: {ex.Message}"); - throw new YtdlpException($"Failed to create output folder {_outputFolder}", ex); - } - - // Reset ProgressParser for this download - _progressParser.Reset(); - _logger.Log(LogType.Info, $"Starting download for URL: {url}"); - - // Use provided template or default - string template = Path.Combine(_outputFolder, _outputTemplate.Replace("\\", "/")); - - // Build command with format and output template - string arguments = $"{_commandBuilder} -f \"{_format}\" -o \"{template}\" {SanitizeInput(url)}"; - - _logger.Log(LogType.Info, arguments); - - _commandBuilder.Clear(); // Clear after building arguments - - await _download.RunAsync(arguments, ct); + // Create isolated execution components + var factory = new ProcessFactory(_ytdlpPath); + return new ProbeRunner(factory, _logger); } + /// /// Executes batch download processing for a collection of URLs with a specified concurrency limit. /// @@ -1258,25 +1342,6 @@ public async Task ExecuteBatchAsync(IEnumerable urls, int maxConcurrency #endregion #region Private Helpers - private static string ValidatePath(string path) - { - if (string.IsNullOrWhiteSpace(path)) - throw new ArgumentException("yt-dlp path cannot be empty.", nameof(path)); - return path; - } - - private static string SanitizeInput(string input) - { - if (string.IsNullOrEmpty(input)) - return input; - - // escape internal quotes - input = input.Replace("\"", "\\\""); - - // wrap with quotes (CRITICAL for paths with spaces) - return $"\"{input}\""; - } - private List ParseFormats(string result) { var formats = new List(); @@ -1307,241 +1372,6 @@ private List ParseFormats(string result) return formats; } - private bool IsInPath(string executable) - { - var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty(); - return paths.Any(path => File.Exists(Path.Combine(path, executable))); - } - - private static bool IsAllowedOption(string arg) - { - if (string.IsNullOrWhiteSpace(arg)) return false; - if (ValidOptions.Contains(arg)) return true; - if (arg.StartsWith("--") || arg.StartsWith("-")) return true; - return false; - } - - #endregion - - - #region Obsolete Methods - - [Obsolete("This method will be removed in the next version. Use GetAvailableFormatsAsync() or GetMetadataAsync() instead.", true)] - public async Task> GetFormatsDetailedAsync(string url, CancellationToken ct = default, int bufferKb = 128) - { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentException("URL cannot be empty.", nameof(url)); - - try - { - var arguments = - $"--dump-single-json " + - $"--simulate " + - $"--skip-download " + - $"--no-playlist " + - $"--quiet " + - $"--no-warnings " + - $"{SanitizeInput(url)}"; - - var json = await _probe.RunAsync(arguments, ct, bufferKb); - - if (string.IsNullOrWhiteSpace(json)) - { - _logger.Log(LogType.Warning, "Empty JSON output from --dump-single-json"); - throw new YtdlpException("No data returned from format query."); - } - - // JSON options - var jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - // Deserialize - var videoInfo = JsonSerializer.Deserialize(json, jsonOptions); - - if (videoInfo?.Formats == null || !videoInfo.Formats.Any()) - { - _logger.Log(LogType.Warning, "No formats array in JSON or empty → falling back to -F"); - return await GetAvailableFormatsAsync(url, ct); - } - - // Map in one clean pass — no modification during enumeration - var detailedFormats = new List(videoInfo.Formats.Count); - - foreach (var f in videoInfo.Formats) - { - if (string.IsNullOrEmpty(f.FormatId)) - continue; - - var fmt = new Format - { - Id = f.FormatId!, - Extension = f.Ext ?? string.Empty, - Height = f.Height, - Width = f.Width, - // Build resolution fallback - Resolution = !string.IsNullOrEmpty(f.Resolution) - ? f.Resolution - : (f.Height.HasValue ? $"{f.Height}p" : "audio only"), - Fps = f.Fps, - Channels = f.AudioChannels?.ToString(), - AudioSampleRate = f.Asr, - TotalBitrate = f.Tbr?.ToString(CultureInfo.InvariantCulture), - VideoBitrate = f.Vbr?.ToString(CultureInfo.InvariantCulture), - AudioBitrate = f.Abr?.ToString(CultureInfo.InvariantCulture), - VideoCodec = f.Vcodec == "none" ? null : f.Vcodec, - AudioCodec = f.Acodec == "none" ? null : f.Acodec, - Protocol = f.Protocol, - Language = f.Language, - FileSizeApprox = f.FilesizeApprox?.ToString("N0") ?? f.Filesize?.ToString("N0"), - ApproxFileSizeBytes = f.FilesizeApprox ?? f.Filesize, - Note = f.FormatNote, - MoreInfo = f.FormatNote, - }; - - detailedFormats.Add(fmt); - } - - if (detailedFormats.Count > 0) - { - _logger.Log(LogType.Info, $"Successfully parsed {detailedFormats.Count} detailed formats from JSON"); - return detailedFormats; - } - - _logger.Log(LogType.Warning, "JSON parsed but no valid formats after filtering → fallback"); - } - catch (JsonException jex) - { - _logger.Log(LogType.Warning, $"JSON deserialization failed: {jex.Message} → falling back"); - } - catch (OperationCanceledException) - { - _logger.Log(LogType.Warning, "Format fetch cancelled"); - throw; - } - catch (Exception ex) - { - _logger.Log(LogType.Error, $"Unexpected error in GetFormatsDetailedAsync: {ex.Message} → fallback"); - } - - // Ultimate fallback - return await GetAvailableFormatsAsync(url, ct); - } - - [Obsolete("This method will be removed in the next version. Use GetMetadataLiteAsync() instead.", true)] - public async Task GetSimpleMetadataAsync(string url, CancellationToken ct = default, int bufferKb = 128) - { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentException("URL cannot be empty.", nameof(url)); - - try - { - // Use a rare separator that is unlikely to appear in title/description - const string separator = "|||YTDLP.NET|||"; - - var fields = new[] - { - "%(id)s", - "%(title)s", - "%(duration)s", - "%(thumbnail)s", - "%(view_count)s", - "%(filesize,filesize_approx)s", - "%(description).500s" // limit to first 500 chars to avoid huge output - }; - - var printArg = $"--print \"{string.Join(separator, fields)}\""; - - var arguments = $"{printArg} --skip-download --no-playlist --quiet {SanitizeInput(url)}"; - - var output = await _probe.RunAsync(arguments, ct, bufferKb); - - if (string.IsNullOrWhiteSpace(output)) - return null; - - var parts = output.Trim().Split(separator); - - if (parts.Length < 6) // at least id, title, duration, thumbnail, views, size - return null; - - return new SimpleMetadata - { - Id = parts[0].Trim(), - Title = parts[1].Trim(), - Duration = double.TryParse(parts[2], NumberStyles.Any, CultureInfo.InvariantCulture, out var dur) ? dur : null, - Thumbnail = parts[3].Trim(), - ViewCount = long.TryParse(parts[4], NumberStyles.Any, CultureInfo.InvariantCulture, out var views) ? views : null, - FileSize = long.TryParse(parts[5], NumberStyles.Any, CultureInfo.InvariantCulture, out var size) ? size : null, - Description = parts.Length > 6 ? parts[6].Trim() : null - }; - } - catch (OperationCanceledException) - { - _logger.Log(LogType.Warning, "Simple metadata fetch cancelled."); - return null; - } - catch (Exception ex) - { - _logger.Log(LogType.Warning, $"Failed to fetch simple metadata: {ex.Message}"); - return null; - } - - } - - [Obsolete("This method will be removed in the next version. Use GetMetadataLiteAsync() instead.", true)] - public async Task?> GetSimpleMetadataAsync(string url, IEnumerable fields, CancellationToken ct = default, int bufferKb = 128) - { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentException("URL cannot be empty.", nameof(url)); - - if (fields == null || !fields.Any()) - throw new ArgumentException("At least one field must be requested.", nameof(fields)); - - try - { - const string separator = "|||YTDLP.NET|||"; - - // Build print format: %(id)s|||YTDLP.NET|||%(title)s|||YTDLP.NET|||... - var printParts = fields.Select(f => $"%({f})s"); - var printFormat = string.Join(separator, printParts); - - var arguments = $"--print \"{printFormat}\" --skip-download --no-playlist --quiet {SanitizeInput(url)}"; - - var rawOutput = await _probe.RunAsync(arguments, ct, bufferKb); - if (string.IsNullOrWhiteSpace(rawOutput)) - return null; - - var parts = rawOutput.Trim().Split(separator); - - // Should have exactly as many parts as requested fields - if (parts.Length != fields.Count()) - return null; - - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - - int index = 0; - foreach (var field in fields) - { - var value = parts[index++].Trim(); - result[field] = value; - } - - return result; - } - catch (OperationCanceledException) - { - _logger.Log(LogType.Warning, "Simple metadata fetch cancelled."); - return null; - } - catch (Exception ex) - { - _logger.Log(LogType.Warning, $"Simple metadata failed: {ex.Message}"); - return null; - } - } #endregion } diff --git a/src/Ytdlp.NET.ParseTest/Program.cs b/tests/Ytdlp.NET.ParseTest/Program.cs similarity index 100% rename from src/Ytdlp.NET.ParseTest/Program.cs rename to tests/Ytdlp.NET.ParseTest/Program.cs diff --git a/src/Ytdlp.NET.ParseTest/TestStrings.cs b/tests/Ytdlp.NET.ParseTest/TestStrings.cs similarity index 100% rename from src/Ytdlp.NET.ParseTest/TestStrings.cs rename to tests/Ytdlp.NET.ParseTest/TestStrings.cs diff --git a/src/Ytdlp.NET.ParseTest/Ytdlp.NET.ParseTest.csproj b/tests/Ytdlp.NET.ParseTest/Ytdlp.NET.ParseTest.csproj similarity index 100% rename from src/Ytdlp.NET.ParseTest/Ytdlp.NET.ParseTest.csproj rename to tests/Ytdlp.NET.ParseTest/Ytdlp.NET.ParseTest.csproj diff --git a/src/Ytdlp.NET.ParseTest/YtdlpFormat.cs b/tests/Ytdlp.NET.ParseTest/YtdlpFormat.cs similarity index 100% rename from src/Ytdlp.NET.ParseTest/YtdlpFormat.cs rename to tests/Ytdlp.NET.ParseTest/YtdlpFormat.cs diff --git a/src/Ytdlp.NET.ParseTest/YtdlpFormatParser.cs b/tests/Ytdlp.NET.ParseTest/YtdlpFormatParser.cs similarity index 100% rename from src/Ytdlp.NET.ParseTest/YtdlpFormatParser.cs rename to tests/Ytdlp.NET.ParseTest/YtdlpFormatParser.cs diff --git a/src/Ytdlp.NET.Test/BuilderTests.cs b/tests/Ytdlp.NET.Test/BuilderTests.cs similarity index 100% rename from src/Ytdlp.NET.Test/BuilderTests.cs rename to tests/Ytdlp.NET.Test/BuilderTests.cs diff --git a/src/Ytdlp.NET.Test/CommandTests.cs b/tests/Ytdlp.NET.Test/CommandTests.cs similarity index 100% rename from src/Ytdlp.NET.Test/CommandTests.cs rename to tests/Ytdlp.NET.Test/CommandTests.cs diff --git a/src/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs b/tests/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs similarity index 100% rename from src/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs rename to tests/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs diff --git a/src/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs b/tests/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs similarity index 100% rename from src/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs rename to tests/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs diff --git a/src/Ytdlp.NET.Test/Obsolete/TestConstants.cs b/tests/Ytdlp.NET.Test/Obsolete/TestConstants.cs similarity index 100% rename from src/Ytdlp.NET.Test/Obsolete/TestConstants.cs rename to tests/Ytdlp.NET.Test/Obsolete/TestConstants.cs diff --git a/src/Ytdlp.NET.Test/ProgressParserTests.cs b/tests/Ytdlp.NET.Test/ProgressParserTests.cs similarity index 100% rename from src/Ytdlp.NET.Test/ProgressParserTests.cs rename to tests/Ytdlp.NET.Test/ProgressParserTests.cs diff --git a/src/Ytdlp.NET.Test/Ytdlp.NET.Test.csproj b/tests/Ytdlp.NET.Test/Ytdlp.NET.Test.csproj similarity index 83% rename from src/Ytdlp.NET.Test/Ytdlp.NET.Test.csproj rename to tests/Ytdlp.NET.Test/Ytdlp.NET.Test.csproj index e48ef51..1c7f2c2 100644 --- a/src/Ytdlp.NET.Test/Ytdlp.NET.Test.csproj +++ b/tests/Ytdlp.NET.Test/Ytdlp.NET.Test.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false @@ -15,7 +15,7 @@ - + diff --git a/yt-dlp-wrapper.sln b/yt-dlp-wrapper.sln deleted file mode 100644 index 4094573..0000000 --- a/yt-dlp-wrapper.sln +++ /dev/null @@ -1,92 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.2.11408.102 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YtDlpWrapper", "src\YtDlpWrapper\YtDlpWrapper.csproj", "{97F0262B-8CE0-4BD9-A137-C586E2B0FC7B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp.Test", "src\ConsoleApp.Test\ConsoleApp.Test.csproj", "{75579413-4D4F-4CC4-8603-D64D13D85312}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoDownloader", "src\VideoDownloader\VideoDownloader.csproj", "{E71041F2-AB5C-4BE2-B701-BEA07932CD1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ytdlp.NET", "src\Ytdlp.NET\Ytdlp.NET.csproj", "{2671D067-5408-4CAD-9CEE-6AC95722367C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Archived", "Archived", "{83E393FE-5BD2-48EE-A50F-A2B0F23F9F98}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ytdlp.NET.Test", "src\Ytdlp.NET.Test\Ytdlp.NET.Test.csproj", "{FF120C1F-85CC-4233-9F2F-E6234B249469}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v2", "v2", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{80E51B9C-E5E3-4E8E-A83B-2E22F7D264B1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ytdlp.NET.Console", "src\Ytdlp.NET.Console\Ytdlp.NET.Console.csproj", "{8F98E4C7-7EE4-B89B-8699-A39D68834D5B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v3", "v3", "{1BFCED08-39EA-49D3-8B18-29C6C592B575}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ytdlp.NET.v3", "src\Ytdlp.NET.v3\Ytdlp.NET.v3.csproj", "{00D622D2-891D-A2CA-4236-2D201C531E00}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ytdlp.NET.ParseTest", "src\Ytdlp.NET.ParseTest\Ytdlp.NET.ParseTest.csproj", "{87F20DC1-EB96-4B57-AE7B-49FFA568C727}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ytdlp.NET.v3.Console", "src\Ytdlp.NET.v3.Console\Ytdlp.NET.v3.Console.csproj", "{8AFF2D5A-049B-49CC-AC93-FBF273F06ABB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {97F0262B-8CE0-4BD9-A137-C586E2B0FC7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97F0262B-8CE0-4BD9-A137-C586E2B0FC7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97F0262B-8CE0-4BD9-A137-C586E2B0FC7B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97F0262B-8CE0-4BD9-A137-C586E2B0FC7B}.Release|Any CPU.Build.0 = Release|Any CPU - {75579413-4D4F-4CC4-8603-D64D13D85312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {75579413-4D4F-4CC4-8603-D64D13D85312}.Debug|Any CPU.Build.0 = Debug|Any CPU - {75579413-4D4F-4CC4-8603-D64D13D85312}.Release|Any CPU.ActiveCfg = Release|Any CPU - {75579413-4D4F-4CC4-8603-D64D13D85312}.Release|Any CPU.Build.0 = Release|Any CPU - {E71041F2-AB5C-4BE2-B701-BEA07932CD1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E71041F2-AB5C-4BE2-B701-BEA07932CD1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E71041F2-AB5C-4BE2-B701-BEA07932CD1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E71041F2-AB5C-4BE2-B701-BEA07932CD1D}.Release|Any CPU.Build.0 = Release|Any CPU - {2671D067-5408-4CAD-9CEE-6AC95722367C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2671D067-5408-4CAD-9CEE-6AC95722367C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2671D067-5408-4CAD-9CEE-6AC95722367C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2671D067-5408-4CAD-9CEE-6AC95722367C}.Release|Any CPU.Build.0 = Release|Any CPU - {FF120C1F-85CC-4233-9F2F-E6234B249469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF120C1F-85CC-4233-9F2F-E6234B249469}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF120C1F-85CC-4233-9F2F-E6234B249469}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF120C1F-85CC-4233-9F2F-E6234B249469}.Release|Any CPU.Build.0 = Release|Any CPU - {8F98E4C7-7EE4-B89B-8699-A39D68834D5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F98E4C7-7EE4-B89B-8699-A39D68834D5B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F98E4C7-7EE4-B89B-8699-A39D68834D5B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F98E4C7-7EE4-B89B-8699-A39D68834D5B}.Release|Any CPU.Build.0 = Release|Any CPU - {00D622D2-891D-A2CA-4236-2D201C531E00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {00D622D2-891D-A2CA-4236-2D201C531E00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {00D622D2-891D-A2CA-4236-2D201C531E00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {00D622D2-891D-A2CA-4236-2D201C531E00}.Release|Any CPU.Build.0 = Release|Any CPU - {87F20DC1-EB96-4B57-AE7B-49FFA568C727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {87F20DC1-EB96-4B57-AE7B-49FFA568C727}.Debug|Any CPU.Build.0 = Debug|Any CPU - {87F20DC1-EB96-4B57-AE7B-49FFA568C727}.Release|Any CPU.ActiveCfg = Release|Any CPU - {87F20DC1-EB96-4B57-AE7B-49FFA568C727}.Release|Any CPU.Build.0 = Release|Any CPU - {8AFF2D5A-049B-49CC-AC93-FBF273F06ABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8AFF2D5A-049B-49CC-AC93-FBF273F06ABB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8AFF2D5A-049B-49CC-AC93-FBF273F06ABB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8AFF2D5A-049B-49CC-AC93-FBF273F06ABB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {97F0262B-8CE0-4BD9-A137-C586E2B0FC7B} = {83E393FE-5BD2-48EE-A50F-A2B0F23F9F98} - {75579413-4D4F-4CC4-8603-D64D13D85312} = {83E393FE-5BD2-48EE-A50F-A2B0F23F9F98} - {E71041F2-AB5C-4BE2-B701-BEA07932CD1D} = {80E51B9C-E5E3-4E8E-A83B-2E22F7D264B1} - {2671D067-5408-4CAD-9CEE-6AC95722367C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {FF120C1F-85CC-4233-9F2F-E6234B249469} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {8F98E4C7-7EE4-B89B-8699-A39D68834D5B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {00D622D2-891D-A2CA-4236-2D201C531E00} = {1BFCED08-39EA-49D3-8B18-29C6C592B575} - {87F20DC1-EB96-4B57-AE7B-49FFA568C727} = {1BFCED08-39EA-49D3-8B18-29C6C592B575} - {8AFF2D5A-049B-49CC-AC93-FBF273F06ABB} = {1BFCED08-39EA-49D3-8B18-29C6C592B575} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C26E5332-B173-4BCD-9C19-AF8F117473F9} - EndGlobalSection -EndGlobal From 4e82574df03bd1ca2e1ea6e2ab02de301a8aaf9e Mon Sep 17 00:00:00 2001 From: manusoft Date: Fri, 20 Mar 2026 07:44:58 +0400 Subject: [PATCH 2/8] Update process factory, core Ytdlp. --- archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj | 12 +- src/Ytdlp.NET.Console/Program.cs | 34 +- src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj | 6 +- src/Ytdlp.NET/Core/ProcessFactory.cs | 8 +- src/Ytdlp.NET/Ytdlp.NET.csproj | 6 +- src/Ytdlp.NET/Ytdlp.cs | 597 +++++++++----------- 6 files changed, 299 insertions(+), 364 deletions(-) diff --git a/archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj b/archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj index 91f01cd..f60eea8 100644 --- a/archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj +++ b/archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj @@ -7,18 +7,18 @@ CS0649,CS8600,CS8601,CS8604,CS8618,CS8625 YtDlpWrapper YtDlpWrapper - YTDLP-Wrapper - YtDlpWrapper is a C# wrapper for yt-dlp, an audio and video downloader with support for thousands of sites. Easily download videos, audio, subtitles, and thumbnails, or fetch video/playlist information with a simple API. - Copyright © 2025 ManuHub + Ytdlp-Wrapper + Ytdlp Wrapper is a .NET wrapper for yt-dlp, an audio and video downloader with support for thousands of sites. Easily download videos, audio, subtitles, and thumbnails, or fetch video/playlist information with a simple API. + Copyright © 2026 ManuHub Manojbabu youtube;download;yt-dlp;fast;audio;video - https://github.com/manusoft/yt-dlp-wrapper - https://github.com/manusoft/yt-dlp-wrapper + https://github.com/manusoft/Ytdlp.NET + https://github.com/manusoft/Ytdlp.NET LICENSE.txt icon.png icon.png README.md - 1.1.0 + 1.1.1 true true diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs index 117a16c..989661f 100644 --- a/src/Ytdlp.NET.Console/Program.cs +++ b/src/Ytdlp.NET.Console/Program.cs @@ -16,10 +16,7 @@ private static async Task Main(string[] args) // Initialize the wrapper (assuming yt-dlp is in PATH or specify path) await using var baseYtdlp = new Ytdlp(ytdlpPath: $"tools\\yt-dlp.exe", logger: new ConsoleLogger()) - .WithFFmpegLocation("tools") - .WithTempFolder(@"c:\Downloads\YouTube\temp") - .WithOutputTemplate("%(upload_date>%Y-%m-%d)s - %(title)s.%(ext)s") - .WithAria2(16); + .WithFFmpegLocation("tools"); // Run all demos/tests sequentially //await TestGetVersionAsync(baseYtdlp); @@ -31,9 +28,9 @@ private static async Task Main(string[] args) //await TestGetLiteMetadataAsync(baseYtdlp); //await TestGetTitleAsync(baseYtdlp); - await TestDownloadVideoAsync(baseYtdlp); + //await TestDownloadVideoAsync(baseYtdlp); //await TestDownloadAudioAsync(ytdlp); - //await TestBatchDownloadAsync(ytdlp); + await TestBatchDownloadAsync(baseYtdlp); //await TestSponsorBlockAsync(ytdlp); //await TestConcurrentFragmentsAsync(ytdlp); //await TestCancellationAsync(ytdlp); @@ -195,12 +192,16 @@ 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=ZGnQH0LN_98"; + var url = "https://www.youtube.com/watch?v=3pecPwPIFIc&pp=ugUEEgJtbA%3D%3D"; var ytdlp = ytdlpBase .With720pOrBest() - .WithOutputFolder("./downloads") - .WithOutputTemplate("%(title)s.%(ext)s"); + .WithConcurrentFragments(8) + .WithHomeFolder("./downloads") + .WithTempFolder("./downloads/temp") + .WithOutputTemplate("%(title)s.%(ext)s") + .WithMtime() + .WithTrimFilenames(100); // Subscribe to events ytdlp.OnProgressDownload += (sender, args) => @@ -212,7 +213,9 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase) ytdlp.OnPostProcessingComplete += (sender, message) => Console.WriteLine($"Post-processing done: {message}"); - await ytdlp.ExecuteAsync(url); + Console.WriteLine(ytdlp.Preview(url)); + + // await ytdlp.ExecuteAsync(url); } // Test 7 Extract audio only @@ -229,7 +232,7 @@ await ytdlp } // Test 8: Batch download (concurrent) - private static async Task TestBatchDownloadAsync(Ytdlp ytdlp) + private static async Task TestBatchDownloadAsync(Ytdlp baseYtdlp) { Console.WriteLine("\nTest 8: Batch download (3 concurrent)..."); var urls = new List @@ -239,10 +242,11 @@ private static async Task TestBatchDownloadAsync(Ytdlp ytdlp) "https://www.youtube.com/watch?v=oDSEGkT6J-0" }; - await ytdlp - .WithFormat("best[height<=480]") // Lower quality for speed - .WithOutputFolder("./downloads/batch") - .ExecuteBatchAsync(urls, maxConcurrency: 3); + var ytdlp = baseYtdlp + .WithFormat("best[height<=480]") // Lower quality for speed + .WithOutputFolder("./downloads/batch"); + + await ytdlp.ExecuteBatchAsync(urls, maxConcurrency: 3); } // Test 9: SponsorBlock removal diff --git a/src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj b/src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj index 46b37fb..50989d0 100644 --- a/src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj +++ b/src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj @@ -7,13 +7,13 @@ Ytdlp.NET ManuHub.Ytdlp ManuHub.Ytdlp - 3.0.0-preview-1 + 4.0.0-preview-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 yt-dlp youtube downloader video audio subtitles thumbnails fluent-api progress-tracking csharp dotnet sponsorblock concurrent - https://github.com/manusoft/yt-dlp-wrapper - https://github.com/yt-dlp/yt-dlp + https://github.com/manusoft/Ytdlp.NET + https://github.com/manusoft/Ytdlp.NET git README.md LICENSE.txt diff --git a/src/Ytdlp.NET/Core/ProcessFactory.cs b/src/Ytdlp.NET/Core/ProcessFactory.cs index ad1bb05..0b1ca9e 100644 --- a/src/Ytdlp.NET/Core/ProcessFactory.cs +++ b/src/Ytdlp.NET/Core/ProcessFactory.cs @@ -5,18 +5,18 @@ namespace ManuHub.Ytdlp.NET.Core; public sealed class ProcessFactory { - private readonly string _ytDlpPath; + private readonly string _ytdlpPath; - public ProcessFactory(string ytDlpPath) + public ProcessFactory(string ytdlpPath) { - _ytDlpPath = ytDlpPath; + _ytdlpPath = ytdlpPath; } public Process Create(string arguments) { var psi = new ProcessStartInfo { - FileName = _ytDlpPath, + FileName = _ytdlpPath, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, diff --git a/src/Ytdlp.NET/Ytdlp.NET.csproj b/src/Ytdlp.NET/Ytdlp.NET.csproj index 50b273d..581d349 100644 --- a/src/Ytdlp.NET/Ytdlp.NET.csproj +++ b/src/Ytdlp.NET/Ytdlp.NET.csproj @@ -7,13 +7,13 @@ Ytdlp.NET Ytdlp.NET ManuHub.Ytdlp.NET - 3.0.0 + 3.0.0-preview-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 yt-dlp youtube downloader video audio subtitles thumbnails fluent-api progress-tracking csharp dotnet sponsorblock concurrent - https://github.com/manusoft/yt-dlp-wrapper - https://github.com/yt-dlp/yt-dlp + https://github.com/manusoft/Ytdlp.NET + https://github.com/manusoft/Ytdlp.NET git README.md LICENSE.txt diff --git a/src/Ytdlp.NET/Ytdlp.cs b/src/Ytdlp.NET/Ytdlp.cs index 2eeffa6..9dfc24b 100644 --- a/src/Ytdlp.NET/Ytdlp.cs +++ b/src/Ytdlp.NET/Ytdlp.cs @@ -40,7 +40,7 @@ namespace ManuHub.Ytdlp.NET; /// public sealed class Ytdlp : IAsyncDisposable { - // ────────────────────────────────────────────── Frozen configuration + #region Frozen configuration private readonly string _ytdlpPath; private readonly ILogger _logger; @@ -58,9 +58,9 @@ public sealed class Ytdlp : IAsyncDisposable private readonly ImmutableArray _flags; private readonly ImmutableArray<(string Key, string? Value)> _options; + #endregion - - // Events + #region Events public event EventHandler? OnProgressDownload; public event EventHandler? OnProgressMessage; public event EventHandler? OnOutputMessage; @@ -68,8 +68,9 @@ public sealed class Ytdlp : IAsyncDisposable public event EventHandler? OnPostProcessingComplete; public event EventHandler? OnCommandCompleted; public event EventHandler? OnErrorMessage; + #endregion - // Flag to prevent double disposal + #region Flag to prevent double disposal private bool _disposed = false; public async ValueTask DisposeAsync() @@ -82,8 +83,9 @@ public async ValueTask DisposeAsync() await Task.CompletedTask; } + #endregion - // ────────────────────────────────────────────── Constructors + #region Constructors public Ytdlp(string ytdlpPath = "yt-dlp", ILogger? logger = null) { @@ -91,7 +93,7 @@ public Ytdlp(string ytdlpPath = "yt-dlp", ILogger? logger = null) _logger = logger ?? new DefaultLogger(); // defaults - _outputFolder = Directory.GetCurrentDirectory(); + _outputFolder = null; _tempFolder = null; _homeFolder = null; _outputTemplate = "%(title)s [%(id)s].%(ext)s"; @@ -103,12 +105,14 @@ public Ytdlp(string ytdlpPath = "yt-dlp", ILogger? logger = null) _cookiesFromBrowser = null; _proxy = null; _ffmpegLocation = null; - _sponsorblockRemove = null; + _sponsorblockRemove = null; } // Private copy constructor – every WithXxx() uses this private Ytdlp(Ytdlp other, string? outputFolder = null, + string? homeFolder = null, + string? tempFolder = null, string? outputTemplate = null, string? format = null, int? concurrentFragments = null, @@ -117,17 +121,17 @@ private Ytdlp(Ytdlp other, string? proxy = null, string? ffmpegLocation = null, string? sponsorblockRemove = null, - string? homeFolder = null, - string? tempFolder = null, IEnumerable? extraFlags = null, IEnumerable<(string, string?)>? extraOptions = null) { _ytdlpPath = other._ytdlpPath; _logger = other._logger; + _outputFolder = outputFolder ?? other._outputFolder; _homeFolder = homeFolder ?? other._homeFolder; _tempFolder = tempFolder ?? other._tempFolder; _outputTemplate = outputTemplate ?? other._outputTemplate; + _format = format ?? other._format; _concurrentFragments = concurrentFragments ?? other._concurrentFragments; _cookiesFile = cookiesFile ?? other._cookiesFile; @@ -135,13 +139,14 @@ private Ytdlp(Ytdlp other, _proxy = proxy ?? other._proxy; _ffmpegLocation = ffmpegLocation ?? other._ffmpegLocation; _sponsorblockRemove = sponsorblockRemove ?? other._sponsorblockRemove; - _flags = extraFlags is null ? other._flags : other._flags.AddRange(extraFlags); _options = extraOptions is null ? other._options : other._options.AddRange(extraOptions); } - // ────────────────────────────────────────────── Fluent configuration methods + #endregion + + #region Fluent configuration methods public Ytdlp WithOutputFolder(string folder) { @@ -149,20 +154,11 @@ public Ytdlp WithOutputFolder(string folder) return new Ytdlp(this, outputFolder: Path.GetFullPath(folder)); } - public Ytdlp WithHomeFolder(string? path) - => string.IsNullOrWhiteSpace(path) - ? this - : new Ytdlp(this, homeFolder: Path.GetFullPath(path)); + public Ytdlp WithHomeFolder(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, homeFolder: Path.GetFullPath(path)); - public Ytdlp WithTempFolder(string? path) - => string.IsNullOrWhiteSpace(path) - ? this - : new Ytdlp(this, tempFolder: Path.GetFullPath(path)); + public Ytdlp WithTempFolder(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, tempFolder: Path.GetFullPath(path)); - public Ytdlp WithFFmpegLocation(string? path) - => string.IsNullOrWhiteSpace(path) - ? this - : new Ytdlp(this, ffmpegLocation: path); + public Ytdlp WithFFmpegLocation(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, ffmpegLocation: path); public Ytdlp WithOutputTemplate(string template) { @@ -170,37 +166,20 @@ public Ytdlp WithOutputTemplate(string template) return new Ytdlp(this, outputTemplate: template.Trim()); } - public Ytdlp WithFormat(string format) - => new Ytdlp(this, format: format.Trim()); + public Ytdlp WithFormat(string format) => new Ytdlp(this, format: format.Trim()); - public Ytdlp WithConcurrentFragments(int count = 8) - => count > 0 - ? new Ytdlp(this, concurrentFragments: count) - : this; + public Ytdlp WithConcurrentFragments(int count = 8) => count > 0 ? new Ytdlp(this, concurrentFragments: count) : this; + public Ytdlp WithProxy(string? proxy) => string.IsNullOrWhiteSpace(proxy) ? this : new Ytdlp(this, proxy: proxy); + public Ytdlp WithCookiesFile(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, cookiesFile: Path.GetFullPath(path)); - public Ytdlp WithProxy(string? proxy) - => string.IsNullOrWhiteSpace(proxy) - ? this - : new Ytdlp(this, proxy: proxy); + public Ytdlp WithCookiesFromBrowser(string browser) => new Ytdlp(this, cookiesFromBrowser: browser); - public Ytdlp WithCookiesFile(string? path) - => string.IsNullOrWhiteSpace(path) - ? this - : new Ytdlp(this, cookiesFile: Path.GetFullPath(path)); - - public Ytdlp WithCookiesFromBrowser(string browser) - => new Ytdlp(this, cookiesFromBrowser: browser); - - public Ytdlp WithSponsorblockRemove(string? categories = "all") - => string.IsNullOrWhiteSpace(categories) - ? this - : new Ytdlp(this, sponsorblockRemove: categories); + public Ytdlp WithSponsorblockRemove(string? categories = "all") => string.IsNullOrWhiteSpace(categories) ? this : new Ytdlp(this, sponsorblockRemove: categories); public Ytdlp WithExtractAudio(string format = "mp3", int quality = 5) - => new Ytdlp(this, - extraFlags: new[] { "--extract-audio" }, + => new Ytdlp(this, extraFlags: new[] { "--extract-audio" }, extraOptions: new[] { ("--audio-format", format), @@ -212,9 +191,7 @@ public Ytdlp WithSubtitles(string langs = "all", bool auto = false) var flags = new List { "--write-subs" }; if (auto) flags.Add("--write-auto-subs"); - return new Ytdlp(this, - extraFlags: flags, - extraOptions: new[] { ("--sub-langs", langs) }); + return new Ytdlp(this, extraFlags: flags, extraOptions: new[] { ("--sub-langs", langs) }); } public Ytdlp WithEmbedSubtitles(string langs = "all", string? convertTo = null) @@ -228,17 +205,15 @@ public Ytdlp WithEmbedSubtitles(string langs = "all", string? convertTo = null) return new Ytdlp(this, extraFlags: flags, extraOptions: options); } - public Ytdlp WithThumbnails(bool all = false) - => new Ytdlp(this, extraFlags: new[] { all ? "--write-all-thumbnails" : "--write-thumbnail" }); + public Ytdlp WithThumbnails(bool all = false) => new Ytdlp(this, extraFlags: new[] { all ? "--write-all-thumbnails" : "--write-thumbnail" }); - public Ytdlp WithEmbedThumbnail() => new Ytdlp(this, extraFlags: new[] { "--embed-thumbnail" }); - public Ytdlp WithEmbedMetadata() => new Ytdlp(this, extraFlags: new[] { "--embed-metadata" }); - public Ytdlp WithEmbedChapters() => new Ytdlp(this, extraFlags: new[] { "--embed-chapters" }); + public Ytdlp WithEmbedThumbnail() => AddFlag("--embed-thumbnail"); + public Ytdlp WithEmbedMetadata() => AddFlag("--embed-metadata"); + public Ytdlp WithEmbedChapters() => AddFlag("--embed-chapters"); public Ytdlp WithAria2(int connections = 16) { - return new Ytdlp(this, - extraOptions: new[] + return new Ytdlp(this, extraOptions: new[] { ("--downloader", "aria2c"), ("--downloader-args", $"aria2c:-x{connections} -k1M") @@ -250,21 +225,21 @@ public Ytdlp WithPlaylistItems(string items) { if (string.IsNullOrWhiteSpace(items)) throw new ArgumentException("Playlist items string cannot be empty", nameof(items)); - return new Ytdlp(this, extraOptions: new[] { ("--playlist-items", items.Trim()) }); + return AddOption("--playlist-items", items.Trim()); } // 2. Playlist start index public Ytdlp WithPlaylistStart(int index) { if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1"); - return new Ytdlp(this, extraOptions: new[] { ("--playlist-start", index.ToString()) }); + return AddOption("--playlist-start", index.ToString()); } // 3. Playlist end index public Ytdlp WithPlaylistEnd(int index) { if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1"); - return new Ytdlp(this, extraOptions: new[] { ("--playlist-end", index.ToString()) }); + return AddOption("--playlist-end", index.ToString()); } // 4. Minimum filesize @@ -273,7 +248,7 @@ public Ytdlp WithMinFileSize(string size) // size examples: 50k, 4.2M, 1G if (string.IsNullOrWhiteSpace(size)) throw new ArgumentException("Size cannot be empty", nameof(size)); - return new Ytdlp(this, extraOptions: new[] { ("--min-filesize", size.Trim()) }); + return AddOption("--min-filesize", size.Trim()); } // 5. Maximum filesize @@ -281,7 +256,7 @@ public Ytdlp WithMaxFileSize(string size) { if (string.IsNullOrWhiteSpace(size)) throw new ArgumentException("Size cannot be empty", nameof(size)); - return new Ytdlp(this, extraOptions: new[] { ("--max-filesize", size.Trim()) }); + return AddOption("--max-filesize", size.Trim()); } // 6. Date filter (upload date) @@ -290,14 +265,14 @@ public Ytdlp WithUploadDate(string date) // formats: YYYYMMDD, today, yesterday, now-2weeks, etc. if (string.IsNullOrWhiteSpace(date)) throw new ArgumentException("Date cannot be empty", nameof(date)); - return new Ytdlp(this, extraOptions: new[] { ("--date", date.Trim()) }); + return AddOption("--date", date.Trim()); } // 7. Age limit / restriction public Ytdlp WithAgeLimit(int years) { if (years < 0) throw new ArgumentOutOfRangeException(nameof(years)); - return new Ytdlp(this, extraOptions: new[] { ("--age-limit", years.ToString()) }); + return AddOption("--age-limit", years.ToString()); } // 8. User-Agent override @@ -305,7 +280,7 @@ public Ytdlp WithUserAgent(string userAgent) { if (string.IsNullOrWhiteSpace(userAgent)) throw new ArgumentException("User-Agent cannot be empty", nameof(userAgent)); - return new Ytdlp(this, extraOptions: new[] { ("--user-agent", userAgent.Trim()) }); + return AddOption("--user-agent", userAgent.Trim()); } // 9. Referer override @@ -313,7 +288,7 @@ public Ytdlp WithReferer(string referer) { if (string.IsNullOrWhiteSpace(referer)) throw new ArgumentException("Referer cannot be empty", nameof(referer)); - return new Ytdlp(this, extraOptions: new[] { ("--referer", referer.Trim()) }); + return AddOption("--referer", referer.Trim()); } // 10. Sleep interval between requests (anti-rate-limit) @@ -332,7 +307,7 @@ public Ytdlp WithSleepInterval(double seconds, double? maxSeconds = null) public Ytdlp WithSleepSubtitles(double seconds) { if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds)); - return new Ytdlp(this, extraOptions: new[] { ("--sleep-subtitles", seconds.ToString("F2", CultureInfo.InvariantCulture)) }); + return AddOption("--sleep-subtitles", seconds.ToString("F2", CultureInfo.InvariantCulture)); } // 12. Download archive file (skip already downloaded) @@ -340,7 +315,7 @@ public Ytdlp WithDownloadArchive(string archivePath = "archive.txt") { if (string.IsNullOrWhiteSpace(archivePath)) throw new ArgumentException("Archive path cannot be empty", nameof(archivePath)); - return new Ytdlp(this, extraOptions: new[] { ("--download-archive", Path.GetFullPath(archivePath)) }); + return AddOption("--download-archive", Path.GetFullPath(archivePath)); } // 13. Match title (regex include) @@ -348,7 +323,7 @@ public Ytdlp WithMatchTitle(string regex) { if (string.IsNullOrWhiteSpace(regex)) throw new ArgumentException("Regex cannot be empty", nameof(regex)); - return new Ytdlp(this, extraOptions: new[] { ("--match-title", regex.Trim()) }); + return AddOption("--match-title", regex.Trim()); } // 14. Reject title (regex exclude) @@ -356,31 +331,30 @@ public Ytdlp WithRejectTitle(string regex) { if (string.IsNullOrWhiteSpace(regex)) throw new ArgumentException("Regex cannot be empty", nameof(regex)); - return new Ytdlp(this, extraOptions: new[] { ("--reject-title", regex.Trim()) }); + return AddOption("--reject-title", regex.Trim()); } // 15. Max downloads (stop after N videos) public Ytdlp WithMaxDownloads(int count) { if (count < 1) throw new ArgumentOutOfRangeException(nameof(count)); - return new Ytdlp(this, extraOptions: new[] { ("--max-downloads", count.ToString()) }); + return AddOption("--max-downloads", count.ToString()); } // Nice-to-have #16 - public Ytdlp WithNoMtime() => new Ytdlp(this, extraFlags: new[] { "--no-mtime" }); + public Ytdlp WithNoMtime() => AddFlag("--no-mtime"); // Nice-to-have #17 - public Ytdlp WithNoCacheDir() => new Ytdlp(this, extraFlags: new[] { "--no-cache-dir" }); + public Ytdlp WithNoCacheDir() => AddFlag("--no-cache-dir"); // Nice-to-have #18 – very popular for high-quality + fallback - public Ytdlp With1080pOrBest() - => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); + public Ytdlp With1080pOrBest() => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); // Nice-to-have #19 - public Ytdlp WithNoPlaylist() => new Ytdlp(this, extraFlags: new[] { "--no-playlist" }); + public Ytdlp WithNoPlaylist() => AddFlag("--no-playlist"); // Nice-to-have #20 - public Ytdlp WithYesPlaylist() => new Ytdlp(this, extraFlags: new[] { "--yes-playlist" }); + public Ytdlp WithYesPlaylist() => AddFlag("--yes-playlist"); // 21. Geo-bypass country (two-letter ISO code) @@ -389,13 +363,11 @@ public Ytdlp WithGeoBypassCountry(string countryCode) if (string.IsNullOrWhiteSpace(countryCode) || countryCode.Length != 2) throw new ArgumentException("Geo-bypass country must be a 2-letter ISO code", nameof(countryCode)); - return new Ytdlp(this, - extraOptions: new[] { ("--geo-bypass-country", countryCode.Trim().ToUpperInvariant()) }); + return AddOption("--geo-bypass-country", countryCode.Trim().ToUpperInvariant()); } // 22. No geo-bypass (disable automatic country bypass) - public Ytdlp WithNoGeoBypass() - => new Ytdlp(this, extraFlags: new[] { "--no-geo-bypass" }); + public Ytdlp WithNoGeoBypass() => AddFlag("--no-geo-bypass"); // 23. Match-filter (advanced filter expression) public Ytdlp WithMatchFilter(string filterExpression) @@ -403,17 +375,14 @@ public Ytdlp WithMatchFilter(string filterExpression) if (string.IsNullOrWhiteSpace(filterExpression)) throw new ArgumentException("Match filter expression cannot be empty", nameof(filterExpression)); - return new Ytdlp(this, - extraOptions: new[] { ("--match-filter", filterExpression.Trim()) }); + return AddOption("--match-filter", filterExpression.Trim()); } // 24. Break on existing (stop when file already in archive) - public Ytdlp WithBreakOnExisting() - => new Ytdlp(this, extraFlags: new[] { "--break-on-existing" }); + public Ytdlp WithBreakOnExisting() => AddFlag("--break-on-existing"); // 25. Break on reject (stop when a video is filtered out by --match-filter) - public Ytdlp WithBreakOnReject() - => new Ytdlp(this, extraFlags: new[] { "--break-on-reject" }); + public Ytdlp WithBreakOnReject() => AddFlag("--break-on-reject"); // 26. Postprocessor args (ppa) - most common use-cases public Ytdlp WithPostprocessorArgs(string postprocessorName, string arguments) @@ -422,21 +391,17 @@ public Ytdlp WithPostprocessorArgs(string postprocessorName, string arguments) throw new ArgumentException("Both postprocessor name and arguments are required"); string combined = $"{postprocessorName.Trim()}:{arguments.Trim()}"; - return new Ytdlp(this, - extraOptions: new[] { ("--postprocessor-args", combined) }); + return AddOption("--postprocessor-args", combined); } // 27. Force key frames at cuts (useful when cutting with --download-sections) - public Ytdlp WithForceKeyframesAtCuts() - => new Ytdlp(this, extraFlags: new[] { "--force-keyframes-at-cuts" }); + public Ytdlp WithForceKeyframesAtCuts() => AddFlag("--force-keyframes-at-cuts"); // 28. Prefer free formats (when multiple formats have similar quality) - public Ytdlp WithPreferFreeFormats() - => new Ytdlp(this, extraFlags: new[] { "--prefer-free-formats" }); + public Ytdlp WithPreferFreeFormats() => AddFlag("--prefer-free-formats"); // 29. No prefer free formats (default behavior - explicit) - public Ytdlp WithNoPreferFreeFormats() - => new Ytdlp(this, extraFlags: new[] { "--no-prefer-free-formats" }); + public Ytdlp WithNoPreferFreeFormats() => AddFlag("--no-prefer-free-formats"); // 30. Merge output format (force container after download & post-processing) public Ytdlp WithMergeOutputFormat(string format) @@ -445,70 +410,54 @@ public Ytdlp WithMergeOutputFormat(string format) if (string.IsNullOrWhiteSpace(format)) throw new ArgumentException("Merge output format cannot be empty", nameof(format)); - return new Ytdlp(this, - extraOptions: new[] { ("--merge-output-format", format.Trim().ToLowerInvariant()) }); + return AddOption("--merge-output-format", format.Trim().ToLowerInvariant()); } // Bonus 31 – very popular shortcut - public Ytdlp WithBestUpTo1080p() - => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); + public Ytdlp WithBestUpTo1080p() => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); // Bonus 32 - public Ytdlp WithKeepFragments() - => new Ytdlp(this, extraFlags: new[] { "--keep-fragments" }); + public Ytdlp WithKeepFragments() => AddFlag("--keep-fragments"); // Bonus 33 – useful for debugging - public Ytdlp WithVerbose() - => new Ytdlp(this, extraFlags: new[] { "--verbose" }); + public Ytdlp WithVerbose() => AddFlag("--verbose"); // 31. Reverse playlist order - public Ytdlp WithPlaylistReverse() - => new Ytdlp(this, extraFlags: new[] { "--playlist-reverse" }); + public Ytdlp WithPlaylistReverse() => AddFlag("--playlist-reverse"); // 32. Random playlist order - public Ytdlp WithPlaylistRandom() - => new Ytdlp(this, extraFlags: new[] { "--playlist-random" }); + public Ytdlp WithPlaylistRandom() => AddFlag("--playlist-random"); // 33. Lazy playlist (process entries as received – good for very large playlists) - public Ytdlp WithLazyPlaylist() - => new Ytdlp(this, extraFlags: new[] { "--lazy-playlist" }); + public Ytdlp WithLazyPlaylist() => AddFlag("--lazy-playlist"); // 34. Flat playlist (do not extract individual video URLs – faster for listing) - public Ytdlp WithFlatPlaylist() - => new Ytdlp(this, extraFlags: new[] { "--flat-playlist" }); + public Ytdlp WithFlatPlaylist() => AddFlag("--flat-playlist"); // 35. Write info.json metadata file - public Ytdlp WithWriteInfoJson() - => new Ytdlp(this, extraFlags: new[] { "--write-info-json" }); + public Ytdlp WithWriteInfoJson() => AddFlag("--write-info-json"); // 36. Clean info.json (remove private/empty fields) - public Ytdlp WithCleanInfoJson() - => new Ytdlp(this, extraFlags: new[] { "--clean-info-json" }); + public Ytdlp WithCleanInfoJson() => AddFlag("--clean-info-json"); // 37. No clean info.json (keep all fields) - public Ytdlp WithNoCleanInfoJson() - => new Ytdlp(this, extraFlags: new[] { "--no-clean-info-json" }); + public Ytdlp WithNoCleanInfoJson() => AddFlag("--no-clean-info-json"); // 38. Simulate only (do not download anything – useful for testing/format listing) - public Ytdlp WithSimulate() - => new Ytdlp(this, extraFlags: new[] { "--simulate" }); + public Ytdlp WithSimulate() => AddFlag("--simulate"); // 39. Skip actual download (but do post-processing if applicable) - public Ytdlp WithSkipDownload() - => new Ytdlp(this, extraFlags: new[] { "--skip-download" }); + public Ytdlp WithSkipDownload() => AddFlag("--skip-download"); // 40. Write description to .description file - public Ytdlp WithWriteDescription() - => new Ytdlp(this, extraFlags: new[] { "--write-description" }); + public Ytdlp WithWriteDescription() => AddFlag("--write-description"); // 41. Keep intermediate video file after post-processing - public Ytdlp WithKeepVideo() - => new Ytdlp(this, extraFlags: new[] { "-k", "--keep-video" }); + public Ytdlp WithKeepVideo() => new Ytdlp(this, extraFlags: new[] { "-k", "--keep-video" }); // 42. Do not overwrite post-processed files - public Ytdlp WithNoPostOverwrites() - => new Ytdlp(this, extraFlags: new[] { "--no-post-overwrites" }); + public Ytdlp WithNoPostOverwrites() => AddFlag("--no-post-overwrites"); // 43. Force keyframes at cuts (important when using --download-sections) @@ -520,8 +469,7 @@ public Ytdlp WithRemuxVideo(string format = "mp4") if (string.IsNullOrWhiteSpace(format)) throw new ArgumentException("Remux format cannot be empty", nameof(format)); - return new Ytdlp(this, - extraOptions: new[] { ("--remux-video", format.Trim().ToLowerInvariant()) }); + return AddOption("--remux-video", format.Trim().ToLowerInvariant()); } // 45. Recode / re-encode video into specified format @@ -531,8 +479,7 @@ public Ytdlp WithRecodeVideo(string format = "mp4") if (string.IsNullOrWhiteSpace(format)) throw new ArgumentException("Recode format cannot be empty", nameof(format)); - return new Ytdlp(this, - extraOptions: new[] { ("--recode-video", format.Trim().ToLowerInvariant()) }); + return AddOption("--recode-video", format.Trim().ToLowerInvariant()); } // 46. Convert thumbnails to specified format @@ -542,21 +489,17 @@ public Ytdlp WithConvertThumbnails(string format = "jpg") if (string.IsNullOrWhiteSpace(format)) throw new ArgumentException("Thumbnail format cannot be empty", nameof(format)); - return new Ytdlp(this, - extraOptions: new[] { ("--convert-thumbnails", format.Trim().ToLowerInvariant()) }); + return AddOption("--convert-thumbnails", format.Trim().ToLowerInvariant()); } // 48. Postprocessor arguments for Merger (most common use-case) - public Ytdlp WithMergerArgs(string args) - => WithPostprocessorArgs("Merger", args); + public Ytdlp WithMergerArgs(string args) => WithPostprocessorArgs("Merger", args); // 49. Postprocessor arguments for ModifyChapters - public Ytdlp WithModifyChaptersArgs(string args) - => WithPostprocessorArgs("ModifyChapters", args); + public Ytdlp WithModifyChaptersArgs(string args) => WithPostprocessorArgs("ModifyChapters", args); // 50. Postprocessor arguments for ExtractAudio - public Ytdlp WithExtractAudioArgs(string args) - => WithPostprocessorArgs("ExtractAudio", args); + public Ytdlp WithExtractAudioArgs(string args) => WithPostprocessorArgs("ExtractAudio", args); // Bonus – common combo: remux to mp4 + embed metadata + chapters + thumbnail public Ytdlp WithMp4PostProcessingPreset() @@ -576,12 +519,10 @@ public Ytdlp WithMkvOutput() }); // 51. Download livestream from the start (when possible) - public Ytdlp WithLiveFromStart() - => new Ytdlp(this, extraFlags: new[] { "--live-from-start" }); + public Ytdlp WithLiveFromStart() => AddFlag("--live-from-start"); // 52. Explicitly disable downloading from the beginning of a live stream - public Ytdlp WithNoLiveFromStart() - => new Ytdlp(this, extraFlags: new[] { "--no-live-from-start" }); + public Ytdlp WithNoLiveFromStart() => AddFlag("--no-live-from-start"); // 53. Wait for a scheduled live stream to start public Ytdlp WithWaitForVideo(TimeSpan? maxWait = null) @@ -599,16 +540,13 @@ public Ytdlp WithWaitForVideo(TimeSpan? maxWait = null) } // 54. Wait until the live stream actually ends before finishing - public Ytdlp WithWaitUntilLiveEnds() - => new Ytdlp(this, extraFlags: new[] { "--wait-for-video-to-end" }); + public Ytdlp WithWaitUntilLiveEnds() => AddFlag("--wait-for-video-to-end"); // 55. Use mpegts container/format for HLS live streams (better compatibility in some players) - public Ytdlp WithHlsUseMpegts() - => new Ytdlp(this, extraFlags: new[] { "--hls-use-mpegts" }); + public Ytdlp WithHlsUseMpegts() => AddFlag("--hls-use-mpegts"); // 56. Do not use mpegts for HLS (use default fragmented mp4) - public Ytdlp WithNoHlsUseMpegts() - => new Ytdlp(this, extraFlags: new[] { "--no-hls-use-mpegts" }); + public Ytdlp WithNoHlsUseMpegts() => AddFlag("--no-hls-use-mpegts"); // 57. External downloader for live streams (e.g. ffmpeg, aria2c, ...) public Ytdlp WithExternalDownloader(string downloaderName, string? downloaderArgs = null) @@ -627,21 +565,18 @@ public Ytdlp WithExternalDownloader(string downloaderName, string? downloaderArg } // 58. Use ffmpeg as external downloader for live streams (most common choice) - public Ytdlp WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null) - => WithExternalDownloader("ffmpeg", extraFfmpegArgs); + public Ytdlp WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null) => WithExternalDownloader("ffmpeg", extraFfmpegArgs); // 59. Set fragment retries specifically useful for unstable live streams public Ytdlp WithFragmentRetries(int retries) { // -1 = infinite string value = retries < 0 ? "infinite" : retries.ToString(); - return new Ytdlp(this, - extraOptions: new[] { ("--fragment-retries", value) }); + return AddOption("--fragment-retries", value); } // 60. Prefer native HLS downloader (instead of ffmpeg) – sometimes more stable - public Ytdlp WithHlsNative() - => new Ytdlp(this, extraOptions: new[] { ("--downloader", "hlsnative") }); + public Ytdlp WithHlsNative() => AddOption("--downloader", "hlsnative"); // 63. Maximum video height / resolution limit @@ -663,46 +598,36 @@ public Ytdlp WithMaxHeightOrBest(int height) } // 65. Best video + best audio (classic high-quality merge) - public Ytdlp WithBestVideoPlusBestAudio() - => new Ytdlp(this, format: "bestvideo+bestaudio/best"); + public Ytdlp WithBestVideoPlusBestAudio() => new Ytdlp(this, format: "bestvideo+bestaudio/best"); // 67. Best video up to 720p + best audio - public Ytdlp With720pOrBest() - => new Ytdlp(this, format: "bv*[height<=?720]+ba/best/best"); - + public Ytdlp With720pOrBest() => new Ytdlp(this, format: "bv*[height<=?720]+ba/best/best"); + // 68. Audio-only – best quality audio - public Ytdlp WithBestAudioOnly() - => new Ytdlp(this, format: "bestaudio"); + public Ytdlp WithBestAudioOnly() => new Ytdlp(this, format: "bestaudio"); // 69. Prefer video formats with higher bitrate (when resolution is similar) - public Ytdlp WithFormatSortBitrate() - => new Ytdlp(this, extraOptions: new[] { ("-S", "br") }); + public Ytdlp WithFormatSortBitrate() => AddOption("-S", "br"); // 70. Prefer formats with higher resolution first, then bitrate - public Ytdlp WithFormatSortResolutionThenBitrate() - => new Ytdlp(this, extraOptions: new[] { ("-S", "res,br") }); + public Ytdlp WithFormatSortResolutionThenBitrate() => AddOption("-S", "res,br"); // Bonus A – very popular preset - public Ytdlp WithBestUpTo1440p() - => new Ytdlp(this, format: "bestvideo[height<=?1440]+bestaudio/best"); + public Ytdlp WithBestUpTo1440p() => new Ytdlp(this, format: "bestvideo[height<=?1440]+bestaudio/best"); // Bonus B – avoid very high resolutions (4K+) - public Ytdlp WithNo4k() - => new Ytdlp(this, format: "bestvideo[height<=?2160]+bestaudio/best"); + public Ytdlp WithNo4k() => new Ytdlp(this, format: "bestvideo[height<=?2160]+bestaudio/best"); // Bonus C – audio-only with specific codec preference - public Ytdlp WithBestM4aAudio() - => new Ytdlp(this, format: "bestaudio[ext=m4a]/bestaudio/best"); + public Ytdlp WithBestM4aAudio() => new Ytdlp(this, format: "bestaudio[ext=m4a]/bestaudio/best"); // 71. Restrict filenames to ASCII-only + avoid problematic characters - public Ytdlp WithRestrictFilenames() - => new Ytdlp(this, extraFlags: new[] { "--restrict-filenames" }); + public Ytdlp WithRestrictFilenames() => AddFlag("--restrict-filenames"); // 72. Force Windows-compatible filenames (avoid reserved names, invalid chars) - public Ytdlp WithWindowsFilenames() - => new Ytdlp(this, extraFlags: new[] { "--windows-filenames" }); + public Ytdlp WithWindowsFilenames() => AddFlag("--windows-filenames"); // 73. Limit filename length (excluding extension) public Ytdlp WithTrimFilenames(int maxLength) @@ -710,129 +635,47 @@ public Ytdlp WithTrimFilenames(int maxLength) if (maxLength < 10) throw new ArgumentOutOfRangeException(nameof(maxLength), "Length should be at least 10 characters"); - return new Ytdlp(this, - extraOptions: new[] { ("--trim-filenames", maxLength.ToString()) }); + return AddOption("--trim-filenames", maxLength.ToString()); } // 74. No overwrite existing files - public Ytdlp WithNoOverwrites() - => new Ytdlp(this, extraFlags: new[] { "--no-overwrites" }); + public Ytdlp WithNoOverwrites() => AddFlag("--no-overwrites"); // 75. Force overwrite existing files - public Ytdlp WithForceOverwrites() - => new Ytdlp(this, extraFlags: new[] { "--force-overwrites" }); + public Ytdlp WithForceOverwrites() => AddFlag("--force-overwrites"); // 76. Continue partially downloaded files - public Ytdlp WithContinue() - => new Ytdlp(this, extraFlags: new[] { "--continue" }); + public Ytdlp WithContinue() => AddFlag("--continue"); // 77. Do not continue partially downloaded files (start from beginning) - public Ytdlp WithNoContinue() - => new Ytdlp(this, extraFlags: new[] { "--no-continue" }); + public Ytdlp WithNoContinue() => AddFlag("--no-continue"); // 78. Use .part files during download - public Ytdlp WithPartFiles() - => new Ytdlp(this, extraFlags: new[] { "--part" }); + public Ytdlp WithPartFiles() => AddFlag("--part"); // 79. Do not use .part files (write directly to final filename) - public Ytdlp WithNoPartFiles() - => new Ytdlp(this, extraFlags: new[] { "--no-part" }); + public Ytdlp WithNoPartFiles() => AddFlag("--no-part"); // 80. Use server mtime (Last-Modified header) for file timestamp - public Ytdlp WithMtime() - => new Ytdlp(this, extraFlags: new[] { "--mtime" }); - - - public Ytdlp AddFlag(string flag) - => new Ytdlp(this, extraFlags: new[] { flag.Trim() }); - - public Ytdlp AddOption(string key, string? value = null) - => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); - - // ────────────────────────────────────────────── Command building (called only at execution time) - - private List BuildArguments(string url) - { - var args = new List(); - - // Paths — use home, temp, and outputFolder separately - if (!string.IsNullOrWhiteSpace(_homeFolder)) - { - args.Add("--paths"); - args.Add($"home:{_homeFolder}"); - } - - if (!string.IsNullOrWhiteSpace(_tempFolder)) - { - args.Add("--paths"); - args.Add($"temp:{_tempFolder}"); - } - - // Output folder is only for -o template - if (!string.IsNullOrWhiteSpace(_outputFolder) && !string.IsNullOrWhiteSpace(_outputTemplate)) - { - // Combine folder + template - string fullOutputPath = Path.Combine(_outputFolder, _outputTemplate) - .Replace('\\', '/'); // yt-dlp prefers forward slashes - args.Add("-o"); - args.Add(fullOutputPath); - } - else if (!string.IsNullOrWhiteSpace(_outputTemplate)) - { - args.Add("-o"); - args.Add(_outputTemplate); - } - - // Format - if (!string.IsNullOrWhiteSpace(_format)) - { - args.Add("-f"); - args.Add(_format); - } - - // Concurrent fragments - if (_concurrentFragments > 1) - { - args.Add("--concurrent-fragments"); - args.Add(_concurrentFragments.Value.ToString()); - } - - // Flags - if (_flags.Length > 0) - args.AddRange(_flags); - - // Options - if (_options.Length > 0) - { - foreach (var kv in _options) - { - args.Add(kv.Key); - if (kv.Value != null) - args.Add(kv.Value); - } - } + public Ytdlp WithMtime() => AddFlag("--mtime"); - // Special single-value options - if (_cookiesFile is not null) { args.Add("--cookies"); args.Add(_cookiesFile); } - if (_cookiesFromBrowser is not null) { args.Add("--cookies-from-browser"); args.Add(Quote(_cookiesFromBrowser)); } - if (_proxy is not null) { args.Add("--proxy"); args.Add(_proxy); } - if (_ffmpegLocation is not null) { args.Add("--ffmpeg-location"); args.Add(_ffmpegLocation); } - if (_sponsorblockRemove is not null) { args.Add("--sponsorblock-remove"); args.Add(_sponsorblockRemove); } - // URL last - args.Add(url); + public Ytdlp AddFlag(string flag) => new Ytdlp(this, extraFlags: new[] { flag.Trim() }); - return args; - } + public Ytdlp AddOption(string key, string? value = null) => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); - public string Preview(string url) - { - var argsList = BuildArguments(url); - return string.Join(" ", argsList.Select(Quote)); - } - - // ────────────────────────────────────────────── Execution + #endregion + + #region Execution & Utility Methods + /// + /// + /// + /// + /// + /// + /// + /// public async Task ExecuteAsync(string url, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -840,16 +683,22 @@ public async Task ExecuteAsync(string url, CancellationToken ct = default) if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("URL required", nameof(url)); - // Ensure output folder exists + // Ensure directories exist if needed try { - Directory.CreateDirectory(_outputFolder); - _logger.Log(LogType.Info, $"Ensured output folder exists: {_outputFolder}"); + if (!string.IsNullOrWhiteSpace(_outputFolder)) + Directory.CreateDirectory(_outputFolder); + + if (!string.IsNullOrWhiteSpace(_homeFolder)) + Directory.CreateDirectory(_homeFolder); + + if (!string.IsNullOrWhiteSpace(_tempFolder)) + Directory.CreateDirectory(_tempFolder); } catch (Exception ex) { - _logger.Log(LogType.Error, $"Failed to create output folder: {ex.Message}"); - throw new YtdlpException("Failed to create output folder", ex); + _logger.Log(LogType.Error, $"Failed to create necessary folders: {ex.Message}"); + throw new YtdlpException("Failed to create required folders", ex); } var argsList = BuildArguments(url); @@ -893,37 +742,17 @@ void OnProgressMessageHandler(object? s, string msg) } } - // ────────────────────────────────────────────── Helpers - - private static string ValidatePath(string path) - { - if (string.IsNullOrWhiteSpace(path)) - throw new ArgumentException("yt-dlp path cannot be empty"); - - if (!File.Exists(path) && !IsExecutableInPath(path)) - throw new FileNotFoundException($"yt-dlp executable not found: {path}"); - - return path; - } - - private static bool IsExecutableInPath(string name) - { - return Environment.GetEnvironmentVariable("PATH")? - .Split(Path.PathSeparator) - .Any(p => File.Exists(Path.Combine(p, name))) ?? false; - } - - private static string Quote(string? value) + /// + /// + /// + /// + /// + public string Preview(string url) { - if (string.IsNullOrWhiteSpace(value)) return "\"\""; - // Escape " and \ - string escaped = value.Replace("\\", "\\\\").Replace("\"", "\\\""); - return $"\"{escaped}\""; + var argsList = BuildArguments(url); + return string.Join(" ", argsList.Select(Quote)); } - #region Execution & Utility Methods - - /// /// Retrieves the current version string of the underlying yt-dlp executable. /// @@ -1278,15 +1107,7 @@ public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = return best?.FormatId ?? "bestvideo"; } - - - private ProbeRunner Probe() - { - // Create isolated execution components - var factory = new ProcessFactory(_ytdlpPath); - return new ProbeRunner(factory, _logger); - } - + /// /// Executes batch download processing for a collection of URLs with a specified concurrency limit. @@ -1306,17 +1127,6 @@ public async Task ExecuteBatchAsync(IEnumerable urls, int maxConcurrency throw new YtdlpException("No URLs provided for batch download"); } - try - { - Directory.CreateDirectory(_outputFolder); - _logger.Log(LogType.Info, $"Output folder for batch: {Path.GetFullPath(_outputFolder)}"); - } - catch (Exception ex) - { - _logger.Log(LogType.Error, $"Failed to create output folder {_outputFolder}: {ex.Message}"); - throw new YtdlpException($"Failed to create output folder {_outputFolder}", ex); - } - using SemaphoreSlim throttler = new(maxConcurrency); var tasks = urls.Select(async url => @@ -1341,7 +1151,128 @@ public async Task ExecuteBatchAsync(IEnumerable urls, int maxConcurrency #endregion - #region Private Helpers + #region Helpers + + // Get probe runner + private ProbeRunner Probe() + { + // Create isolated execution components + var factory = new ProcessFactory(_ytdlpPath); + return new ProbeRunner(factory, _logger); + } + + // Command building (called only at execution time) + private List BuildArguments(string url) + { + var args = new List(); + + bool usingAbsoluteOutput = !string.IsNullOrWhiteSpace(_outputFolder); + + if (usingAbsoluteOutput && !string.IsNullOrWhiteSpace(_tempFolder)) + { + _logger.Log(LogType.Debug, "Temp folder ignored because absolute output template is used."); + } + + // temp folder + if (!usingAbsoluteOutput && !string.IsNullOrWhiteSpace(_tempFolder)) + { + args.Add("--paths"); + args.Add($"temp:{_tempFolder.Replace("\\", "/")}"); + } + + // home folder only if NOT using absolute output + if (!usingAbsoluteOutput && !string.IsNullOrWhiteSpace(_homeFolder)) + { + args.Add("--paths"); + args.Add($"home:{_homeFolder.Replace("\\", "/")}"); + } + + // Output template + if (!string.IsNullOrWhiteSpace(_outputTemplate)) + { + args.Add("-o"); + + if (usingAbsoluteOutput) + { + var full = Path.Combine(_outputFolder!, _outputTemplate) + .Replace("\\", "/"); + + args.Add(full); + } + else + { + args.Add(_outputTemplate); + } + } + + // Format + if (!string.IsNullOrWhiteSpace(_format)) + { + args.Add("-f"); + args.Add(_format); + } + + // Concurrent fragments + if (_concurrentFragments > 1) + { + args.Add("--concurrent-fragments"); + args.Add(_concurrentFragments.Value.ToString()); + } + + // Flags + if (_flags.Length > 0) + args.AddRange(_flags); + + // Options + if (_options.Length > 0) + { + foreach (var kv in _options) + { + args.Add(kv.Key); + if (kv.Value != null) + args.Add(kv.Value); + } + } + + // Special single-value options + if (_cookiesFile is not null) { args.Add("--cookies"); args.Add(_cookiesFile); } + if (_cookiesFromBrowser is not null) { args.Add("--cookies-from-browser"); args.Add(Quote(_cookiesFromBrowser)); } + if (_proxy is not null) { args.Add("--proxy"); args.Add(_proxy); } + if (_ffmpegLocation is not null) { args.Add("--ffmpeg-location"); args.Add(_ffmpegLocation); } + if (_sponsorblockRemove is not null) { args.Add("--sponsorblock-remove"); args.Add(_sponsorblockRemove); } + + // URL last + args.Add(url); + + return args; + } + + private static string ValidatePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("yt-dlp path cannot be empty"); + + if (!File.Exists(path) && !IsExecutableInPath(path)) + throw new FileNotFoundException($"yt-dlp executable not found: {path}"); + + return path; + } + + private static bool IsExecutableInPath(string name) + { + return Environment.GetEnvironmentVariable("PATH")? + .Split(Path.PathSeparator) + .Any(p => File.Exists(Path.Combine(p, name))) ?? false; + } + + private static string Quote(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return "\"\""; + // Escape " and \ + string escaped = value.Replace("\\", "\\\\").Replace("\"", "\\\""); + return $"\"{escaped}\""; + } + private List ParseFormats(string result) { var formats = new List(); From cb2d598ec4f986f9299b143cf0aa5cc469f53496 Mon Sep 17 00:00:00 2001 From: manusoft Date: Fri, 20 Mar 2026 07:50:39 +0400 Subject: [PATCH 3/8] Update test program. --- src/Ytdlp.NET.Console/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs index 989661f..6599280 100644 --- a/src/Ytdlp.NET.Console/Program.cs +++ b/src/Ytdlp.NET.Console/Program.cs @@ -28,9 +28,9 @@ private static async Task Main(string[] args) //await TestGetLiteMetadataAsync(baseYtdlp); //await TestGetTitleAsync(baseYtdlp); - //await TestDownloadVideoAsync(baseYtdlp); + await TestDownloadVideoAsync(baseYtdlp); //await TestDownloadAudioAsync(ytdlp); - await TestBatchDownloadAsync(baseYtdlp); + //await TestBatchDownloadAsync(baseYtdlp); //await TestSponsorBlockAsync(ytdlp); //await TestConcurrentFragmentsAsync(ytdlp); //await TestCancellationAsync(ytdlp); From 59f123932084354b8845563d622e2dab6c24bfb9 Mon Sep 17 00:00:00 2001 From: manusoft Date: Fri, 20 Mar 2026 15:36:25 +0400 Subject: [PATCH 4/8] Add new audio and media format enums and other enums. --- src/Ytdlp.NET/Enums/AudioFormat.cs | 14 + src/Ytdlp.NET/{Models => Enums}/LogType.cs | 0 src/Ytdlp.NET/Enums/MediaFormat.cs | 23 + src/Ytdlp.NET/Enums/PostProcessors.cs | 21 + src/Ytdlp.NET/Enums/Runtime.cs | 9 + .../{Models => Enums}/UpdateChannel.cs | 2 +- src/Ytdlp.NET/Ytdlp.cs | 1191 ++++++++++++----- 7 files changed, 926 insertions(+), 334 deletions(-) create mode 100644 src/Ytdlp.NET/Enums/AudioFormat.cs rename src/Ytdlp.NET/{Models => Enums}/LogType.cs (100%) create mode 100644 src/Ytdlp.NET/Enums/MediaFormat.cs create mode 100644 src/Ytdlp.NET/Enums/PostProcessors.cs create mode 100644 src/Ytdlp.NET/Enums/Runtime.cs rename src/Ytdlp.NET/{Models => Enums}/UpdateChannel.cs (97%) diff --git a/src/Ytdlp.NET/Enums/AudioFormat.cs b/src/Ytdlp.NET/Enums/AudioFormat.cs new file mode 100644 index 0000000..3da0f6a --- /dev/null +++ b/src/Ytdlp.NET/Enums/AudioFormat.cs @@ -0,0 +1,14 @@ +namespace ManuHub.Ytdlp.NET; + +public enum AudioFormat +{ + Best, + Aac, + Alac, + Flac, + M4a, + Mp3, + Opus, + Vorbis, + Wav +} \ No newline at end of file diff --git a/src/Ytdlp.NET/Models/LogType.cs b/src/Ytdlp.NET/Enums/LogType.cs similarity index 100% rename from src/Ytdlp.NET/Models/LogType.cs rename to src/Ytdlp.NET/Enums/LogType.cs diff --git a/src/Ytdlp.NET/Enums/MediaFormat.cs b/src/Ytdlp.NET/Enums/MediaFormat.cs new file mode 100644 index 0000000..2abfe71 --- /dev/null +++ b/src/Ytdlp.NET/Enums/MediaFormat.cs @@ -0,0 +1,23 @@ +namespace ManuHub.Ytdlp.NET; + +public enum MediaFormat +{ + Avi, + Flv, + Gif, + Mkv, + Mov, + Mp4, + Webm, + Aac, + Aiff, + Alac, + Flac, + M4a, + Mka, + Mp3, + Ogg, + Opus, + Vorbis, + Wav, +} diff --git a/src/Ytdlp.NET/Enums/PostProcessors.cs b/src/Ytdlp.NET/Enums/PostProcessors.cs new file mode 100644 index 0000000..90feba6 --- /dev/null +++ b/src/Ytdlp.NET/Enums/PostProcessors.cs @@ -0,0 +1,21 @@ +namespace ManuHub.Ytdlp.NET; + +public enum PostProcessors +{ + Merger, + ModifyChapters, + SplitChapters, + ExtractAudio, + VideoRemuxer, + VideoConvertor, + Metadata, + EmbedSubtitle, + EmbedThumbnail, + SubtitlesConvertor, + ThumbnailsConvertor, + FixupStretched, + FixupM4a, + FixupM3u8, + FixupTimestamp, + FixupDuration +} \ No newline at end of file diff --git a/src/Ytdlp.NET/Enums/Runtime.cs b/src/Ytdlp.NET/Enums/Runtime.cs new file mode 100644 index 0000000..972427c --- /dev/null +++ b/src/Ytdlp.NET/Enums/Runtime.cs @@ -0,0 +1,9 @@ +namespace ManuHub.Ytdlp.NET; + +public enum Runtime +{ + Deno, + Node, + QuickJs, + Bun, +} diff --git a/src/Ytdlp.NET/Models/UpdateChannel.cs b/src/Ytdlp.NET/Enums/UpdateChannel.cs similarity index 97% rename from src/Ytdlp.NET/Models/UpdateChannel.cs rename to src/Ytdlp.NET/Enums/UpdateChannel.cs index 44734ba..ce1b9c2 100644 --- a/src/Ytdlp.NET/Models/UpdateChannel.cs +++ b/src/Ytdlp.NET/Enums/UpdateChannel.cs @@ -5,4 +5,4 @@ public enum UpdateChannel Stable, Master, Nightly -} \ No newline at end of file +} diff --git a/src/Ytdlp.NET/Ytdlp.cs b/src/Ytdlp.NET/Ytdlp.cs index 9dfc24b..bf68536 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.ComponentModel.DataAnnotations; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -146,71 +147,864 @@ private Ytdlp(Ytdlp other, #endregion - #region Fluent configuration methods + // ================================================================================================================== + // Fluent configuration methods + // ================================================================================================================== + #region General Options + + /// + /// Additional JavaScript runtime to enable, with an optional location for the runtime (either the path to the binary or its containing directory). + /// This option can be used multiple times to enable multiple runtimes. Supported runtimes are (in order of priority, from highest to lowest): deno, node, quickjs, bun. + /// Only "deno" is enabled by default. The highest priority runtime that is both enabled and available will be used. + /// In order to use a lower priority runtime when "deno" is available, NoJsRuntime() needs to be passed before enabling other runtimes + /// + /// Supported runtimes are deno, node, quickjs, bun + /// + public Ytdlp WithJsRuntime(Runtime runtime, string runtimePath) + { + var builder = $"{runtime}:{runtimePath}"; + return AddOption("--js-runtime", builder); + } + + /// + /// Clear JavaScript runtimes to enable, including defaults and those provided by WithJsRuntime() + /// + public Ytdlp WithNoJsRuntime() => AddFlag("--no-js-runtime"); + + /// + /// Do not extract a playlist's URL result entries; some entry metadata may be missing and downloading may be bypassed + /// + public Ytdlp WithFlatPlaylist() => AddFlag("--flat-playlist"); + + /// + /// Download livestreams from the start. Currently experimental and only supported for YouTube, Twitch, and TVer. + /// + public Ytdlp WithLiveFromStart() => AddFlag("--live-from-start"); + + /// + /// Wait for scheduled streams to become available.Pass the minimum number of seconds(or range) to wait between retries + /// + /// + /// + public Ytdlp WithWaitForVideo(TimeSpan? maxWait = null) + { + var opts = new List<(string Key, string? Value)>(); + + opts.Add(("--wait-for-video", "any")); // "any" = wait indefinitely or until timeout + + if (maxWait.HasValue && maxWait.Value.TotalSeconds > 0) + { + opts.Add(("--wait-for-video", maxWait.Value.TotalSeconds.ToString("F0"))); + } + + return new Ytdlp(this, extraOptions: opts); + } + + /// + /// Mark videos watched (even with Simulate()) + /// + public Ytdlp WithMarkWatched() => AddFlag("--mark-watched"); + + #endregion + + #region Network Options + + /// + /// Use the specified HTTP/HTTPS/SOCKS proxy. To enable SOCKS proxy, specify a proper scheme, e.g. socks5://user:pass@127.0.0.1:1080/. + /// + /// Pass in an empty string for direct connection + public Ytdlp WithProxy(string? proxy) => string.IsNullOrWhiteSpace(proxy) ? this : new Ytdlp(this, proxy: proxy); + + /// + /// Time to wait before giving up, in seconds + /// + /// + public Ytdlp WithSocketTimeout(TimeSpan timeout) + { + if (timeout <= TimeSpan.Zero) return this; + double seconds = timeout.TotalSeconds; + return AddOption("--socket-timeout", seconds.ToString("F0")); + } + + /// + /// Make all connections via IPv4 + /// + public Ytdlp WithForceIpv4() => AddFlag("--force-ipv4"); + + /// + /// Make all connections via IPv6 + /// + public Ytdlp WithForceIpv6() => AddFlag("--force-ipv6"); + + /// + /// Enable file:// URLs. This is disabled by default for security reasons. + /// + public Ytdlp WithEnableFileUrl() => AddFlag("--enable-file-url"); + + #endregion + + #region Geo-restriction + + /// + /// Use this proxy to verify the IP address for some geo-restricted sites. + /// The default proxy specified by WithProxy() (or none, if the option is not present) is used for the actual downloading + /// + /// + /// + public Ytdlp WithGeoVerificationProxy(string url) => AddOption("--geo-verification-proxy", url); + + /// + /// How to fake X-Forwarded-For HTTP header to try bypassing geographic restriction. One of "default" (only when known to be useful), + /// "never", an IP block in CIDR notation, or a two-letter ISO 3166-2 country code + /// + /// + /// + /// + public Ytdlp WithGeoBypassCountry(string countryCode) + { + if (string.IsNullOrWhiteSpace(countryCode) || countryCode.Length != 2) throw new ArgumentException("Country code must be 2 letters."); + return AddOption("--xff", countryCode.ToUpper()); + } + + #endregion + + #region Video Selection + + /// + /// Comma-separated playlist_index of the items to download. You can specify a range using "[START]:[STOP][:STEP]". + /// For backward compatibility, START-STOP is also supported. Use negative indices to count from the right and negative STEP to download in reverse order. + /// E.g. "1:3,7,-5::2" used on a playlist of size 15 will download the items at index 1,2,3,7,11,13,15 + /// + /// + /// + /// + public Ytdlp WithPlaylistItems(string items) + { + if (string.IsNullOrWhiteSpace(items)) + throw new ArgumentException("Playlist items string cannot be empty.", nameof(items)); + return AddOption("--playlist-items", items.Trim()); + } + + /// + /// Abort download if filesize is smaller than SIZE + /// + /// e.g. 50k or 44.6M + public Ytdlp WithMinFileSize(string size) + { + // size examples: 50k, 4.2M, 1G + if (string.IsNullOrWhiteSpace(size)) + throw new ArgumentException("Size cannot be empty", nameof(size)); + return AddOption("--min-filesize", size.Trim()); + } + + /// + /// Abort download if filesize is larger than SIZE + /// + /// e.g. 50k or 44.6M + public Ytdlp WithMaxFileSize(string size) + { + if (string.IsNullOrWhiteSpace(size)) + throw new ArgumentException("Size cannot be empty", nameof(size)); + return AddOption("--max-filesize", size.Trim()); + } + + /// + /// Download only videos uploaded on this date. + /// The date can be "YYYYMMDD" or in the format [now|today|yesterday][-N[day|week|month|year]]. + /// E.g. "--date today-2weeks" downloads only videos uploaded on the same day two weeks ago + /// + /// "today-2weeks" or "YYYYMMDD" + public Ytdlp WithDate(string date) + { + // formats: YYYYMMDD, today, yesterday, now-2weeks, etc. + if (string.IsNullOrWhiteSpace(date)) + throw new ArgumentException("Date cannot be empty", nameof(date)); + return AddOption("--date", date.Trim()); + } + + /// + /// Generic video filter. Any "OUTPUT TEMPLATE" field can be compared with a number or a string using the operators defined in "Filtering Formats". + /// + /// + /// + /// + public Ytdlp WithMatchFilter(string filterExpression) + { + if (string.IsNullOrWhiteSpace(filterExpression)) + throw new ArgumentException("Match filter expression cannot be empty", nameof(filterExpression)); + + return AddOption("--match-filter", filterExpression.Trim()); + } + + /// + /// Download only the video, if the URL refers to a video and a playlist + /// + /// + public Ytdlp WithNoPlaylist() => AddFlag("--no-playlist"); + + /// + /// Download the playlist, if the URL refers to a video and a playlist + /// + /// + public Ytdlp WithYesPlaylist() => AddFlag("--yes-playlist"); + + /// + /// Download only videos suitable for the given age + /// + /// + public Ytdlp WithAgeLimit(int years) + { + if (years < 0) throw new ArgumentOutOfRangeException(nameof(years)); + return AddOption("--age-limit", years.ToString()); + } + + /// + /// Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it + /// + /// + /// + /// + public Ytdlp WithDownloadArchive(string archivePath = "archive.txt") + { + if (string.IsNullOrWhiteSpace(archivePath)) + throw new ArgumentException("Archive path cannot be empty", nameof(archivePath)); + return AddOption("--download-archive", Path.GetFullPath(archivePath)); + } + + /// + /// Abort after downloading number files + /// + /// + public Ytdlp WithMaxDownloads(int count) + { + if (count < 1) throw new ArgumentOutOfRangeException(nameof(count)); + return AddOption("--max-downloads", count.ToString()); + } + + /// + /// Stop the download process when encountering a file that is in the archive supplied with the option + /// + /// + public Ytdlp WithBreakOnExisting() => AddFlag("--break-on-existing"); + + #endregion + + #region Download Options + + /// + /// Number of fragments of a dash/hlsnative video that should be downloaded concurrently (default is 1) + /// + /// + public Ytdlp WithConcurrentFragments(int count = 8) => count > 0 ? new Ytdlp(this, concurrentFragments: count) : this; + + /// + /// Maximum download rate in bytes per second + /// + /// e.g. 50K or 4.2M + public Ytdlp WithLimitRate(string rate) => AddOption("--limit-rate", rate); + + /// + /// Minimum download rate in bytes per second below which throttling is assumed and the video data is re-extracted + /// + /// e.g. 100K + public Ytdlp WithThrottledRate(string rate) => AddOption("--throttled-rate", rate); + + /// + /// Number of retries (default is 10), or -1 for "infinite" + /// + /// + public Ytdlp WithRetries(int maxRetries) => AddOption("--retries", maxRetries < 0 ? "infinite" : maxRetries.ToString()); + + /// + /// Number of times to retry on file access error (default is 3), or -1 for "infinite" + /// + /// + public Ytdlp WithFileAccessRetries(int maxRetries) => AddOption("--file-access-retries", maxRetries < 0 ? "infinite" : maxRetries.ToString()); + + /// + /// Number of retries for a fragment (default is 10), or -1 for "infinite" (DASH, hlsnative and ISM) + /// + /// + public Ytdlp WithFragmentRetries(int retries) + { + // -1 = infinite + string value = retries < 0 ? "infinite" : retries.ToString(); + return AddOption("--fragment-retries", value); + } + + /// + /// Keep downloaded fragments on disk after downloading is finished + /// + public Ytdlp WithKeepFragments() => AddFlag("--keep-fragments"); + + /// + /// Size of download buffer, (default is 1024) + /// + /// e.g. 1024 or 16K + public Ytdlp WithBufferSize(string size) => AddOption("--buffer-size", size); + + /// + /// Download playlist videos in random order + /// + public Ytdlp WithPlaylistRandom() => AddFlag("--playlist-random"); + + /// + /// Process entries in the playlist as they are received. This disables n_entries, PlaylistRandom() and --playlist-reverse + /// + public Ytdlp WithLazyPlaylist() => AddFlag("--lazy-playlist"); + + /// + /// Use the mpegts container for HLS videos; allowing some players to play the video while downloading, + /// and reducing the chance of file corruption if download is interrupted. This is enabled by default for live streams + /// + /// + public Ytdlp WithHlsUseMpegts() => AddFlag("--hls-use-mpegts"); + + /// + /// Download only chapters that match the regular expression. A "*" prefix denotes time-range instead of chapter. + /// Negative timestamps are calculated from the end. "*from-url" can be used to download between the "start_time" and "end_time" extracted from the URL. + /// Needs ffmpeg. This option can be used multiple times to download multiple sections + /// + /// e.g. "*10:15-inf", "intro" + /// + public Ytdlp WithDownloadSections(string regex) + { + if (string.IsNullOrWhiteSpace(regex)) return this; + return AddOption("--download-sections", regex); + } + + + #endregion + + #region Filesystem Options + + /// + /// Sets the home folder for yt-dlp (used for config or as base directory). + /// Path is automatically normalized and quoted. + /// + /// + public Ytdlp WithHomeFolder(string? homeFolder) + { + if (string.IsNullOrWhiteSpace(homeFolder)) throw new ArgumentException("Home folder path cannot be empty"); + return new Ytdlp(this, homeFolder: Path.GetFullPath(homeFolder)); + } + + /// + /// Sets the temporary folder for yt-dlp intermediate files (fragments, etc.). + /// Path is automatically normalized and quoted. + /// + /// + public Ytdlp WithTempFolder(string? tempFolder) + { + if (string.IsNullOrWhiteSpace(tempFolder)) throw new ArgumentException("Temp folder path cannot be empty"); + return new Ytdlp(this, tempFolder: Path.GetFullPath(tempFolder)); + } + + /// + /// Sets the output folder + /// + /// + /// public Ytdlp WithOutputFolder(string folder) { if (string.IsNullOrWhiteSpace(folder)) throw new ArgumentException("Output folder required"); return new Ytdlp(this, outputFolder: Path.GetFullPath(folder)); } - public Ytdlp WithHomeFolder(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, homeFolder: Path.GetFullPath(path)); + /// + /// Output filename template + /// + /// + public Ytdlp WithOutputTemplate(string template) + { + if (string.IsNullOrWhiteSpace(template)) throw new ArgumentException("Template required"); + return new Ytdlp(this, outputTemplate: template.Trim()); + } + + /// + /// Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames + /// + public Ytdlp WithRestrictFilenames() => AddFlag("--restrict-filenames"); + + /// + /// Force filenames to be Windows-compatible + /// + public Ytdlp WithWindowsFilenames() => AddFlag("--windows-filenames"); + + /// + /// Limit the filename length (excluding extension) to the specified number of characters + /// + /// + public Ytdlp WithTrimFilenames(int length) + { + if (length < 10) + throw new ArgumentOutOfRangeException(nameof(length), "Length should be at least 10 characters"); + + return AddOption("--trim-filenames", length.ToString()); + } + + /// + /// Do not overwrite any files + /// + public Ytdlp WithNoOverwrites() => AddFlag("--no-overwrites"); + + /// + /// Overwrite all video and metadata files. This option includes + /// + /// + public Ytdlp WithForceOverwrites() => AddFlag("--force-overwrites"); + + /// + /// Do not resume partially downloaded fragments. If the file is not fragmented, restart download of the entire file + /// + /// + public Ytdlp WithNoContinue() => AddFlag("--no-continue"); + + /// + /// Do not use .part files - write directly into output file + /// + public Ytdlp WithNoPart() => AddFlag("--no-part"); + + /// + /// Use the Last-modified header to set the file modification time + /// + public Ytdlp WithMtime() => AddFlag("--mtime"); + + /// + /// Write video description to a .description file + /// + public Ytdlp WithWriteDescription() => AddFlag("--write-description"); + + /// + /// Write video metadata to a .info.json file (this may contain personal information) + /// + public Ytdlp WithWriteInfoJson() => AddFlag("--write-info-json"); + + /// + /// Do not write playlist metadata when using WriteVideoMetadata(), WriteVideoDescription() + /// + public Ytdlp WithNoWritePlaylistMetafiles() => AddFlag("--no-write-playlist-metafiles"); + + /// + /// Write all fields to the infojson + /// + public Ytdlp WithNoCleanInfoJson() => AddFlag("--no-clean-info-json"); + + /// + /// Retrieve video comments to be placed in the infojson. The comments are fetched even without this option if the extraction is known to be quick + /// + public Ytdlp WriteComments() => AddFlag("--write-comments"); + + /// + /// Do not retrieve video comments unless the extraction is known to be quick + /// + public Ytdlp WithNoWriteComments() => AddFlag("--no-write-comments"); + + /// + /// JSON file containing the video information (created with the WriteVideoMetadata() option) + /// + /// *.json + public Ytdlp WithLoadInfoJson(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException("Json file path cannot be empty.", nameof(filePath)); + return AddOption("--load-info-json", filePath); + } + + /// + /// Netscape formatted file to read cookies from and dump cookie jar in + /// + /// + /// + public Ytdlp WithCookiesFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("Cookie file path cannot be empty.", nameof(path)); + return new Ytdlp(this, cookiesFile: Path.GetFullPath(path)); + } + + /// + /// The name of the browser to load cookies from. Currently supported browsers are: brave, chrome, chromium, edge, firefox, opera, safari, vivaldi, whale. + /// Optionally, the KEYRING used for decrypting Chromium cookies on Linux, the name/path of the PROFILE to load cookies from, and the CONTAINER name (if Firefox) + /// ("none" for no container) can be given with their respective separators. By default, all containers of the most recently accessed profile are used. + /// keyrings are: basictext, gnomekeyring, kwallet, kwallet5, kwallet6 + /// + /// + public Ytdlp WithCookiesFromBrowser(string browser) => new Ytdlp(this, cookiesFromBrowser: browser); + + /// + /// Disable filesystem caching + /// + public Ytdlp WithNoCacheDir() => AddFlag("--no-cache-dir"); + + /// + /// Delete all filesystem cache files + /// + /// + public Ytdlp WithRemoveCacheDir() => AddFlag("--rm-cache-dir"); + + #endregion + + #region Thumbnail Options + + /// + /// Write thumbnail image to disk / Write all thumbnail image formats to disk + /// + /// + /// + public Ytdlp WithThumbnails(bool allSizes = false) + { + if (allSizes) + return AddFlag("--write-all-thumbnails"); + + return AddFlag("--write-thumbnail"); + } + + + #endregion + + #region Verbosity and Simulation Options + + /// + /// Activate quiet mode. If used with --verbose, print the log to stderr + /// + public Ytdlp WithQuiet() => AddFlag("--quiet"); + + /// + /// Ignore warnings + /// + public Ytdlp WithNoWarnings() => AddFlag("--no-warnings"); + + /// + /// Do not download the video and do not write anything to disk + /// + public Ytdlp WithSimulate() => AddFlag("--simulate"); + + /// + /// Download the video even if printing/listing options are used + /// + public Ytdlp WithNoSimulate() => AddFlag("--no-simulate"); + + /// + /// Do not download the video but write all related files (Alias: --no-download) + /// + /// + public Ytdlp WithSkipDownload() => AddFlag("--skip-download"); + + /// + /// Print various debugging information + /// + /// + public Ytdlp WithVerbose() => AddFlag("--verbose"); + + #endregion + + #region Workgrounds + + /// + /// Specify a custom HTTP header and its value. You can use this option multiple times + /// + /// "Referer" "User-Agent" + /// "URL", "UA" + /// + public Ytdlp WithAddHeader(string header, string value) + { + if (string.IsNullOrWhiteSpace(header) || string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Header and value cannot be empty."); + return AddOption("--add-headers", $"{header}:{value}"); + } + + /// + /// Number of seconds to sleep between requests during data extraction, Maximum number of seconds to sleep. + /// Can only be used along with --min-sleep-interval + /// + /// + /// + /// + /// + public Ytdlp WithSleepInterval(double seconds, double? maxSeconds = null) + { + if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds)); + var opts = new List<(string, string?)> { ("--sleep-requests", seconds.ToString("F2", CultureInfo.InvariantCulture)) }; + if (maxSeconds.HasValue && maxSeconds > seconds) + { + opts.Add(("--max-sleep-requests", maxSeconds.Value.ToString("F2", CultureInfo.InvariantCulture))); + } + return new Ytdlp(this, extraOptions: opts); + } + + /// + /// Number of seconds to sleep before each subtitle download + /// + /// + /// + /// + public Ytdlp WithSleepSubtitles(double seconds) + { + if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds)); + return AddOption("--sleep-subtitles", seconds.ToString("F2", CultureInfo.InvariantCulture)); + } + + #endregion + + #region Video Format Options + + /// + /// Video format code + /// + /// + public Ytdlp WithFormat(string format) => new Ytdlp(this, format: format.Trim()); + + #endregion + + #region Subtitle Options + + /// + /// Write subtitle file + /// + /// Languages of the subtitles to download (can be regex) or "all" separated by commas, e.g."en.*,ja" + /// (where "en.*" is a regex pattern that matches "en" followed by 0 or more of any character). + /// + /// Write automatically generated subtitle file + public Ytdlp WithSubtitles(string languages = "all", bool auto = false) + { + var flags = new List { "--write-subs" }; + if (auto) flags.Add("--write-auto-subs"); + + return new Ytdlp(this, extraFlags: flags, extraOptions: new[] { ("--sub-langs", languages) }); + } + + #endregion + + #region Authentication Options + + /// + /// Login with this account ID and account password. + /// + /// Account ID + /// Account password + /// + public Ytdlp WithAuthentication(string username, string password) + { + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + throw new ArgumentException("Username and password cannot be empty."); + return this + .AddOption("--username", username) + .AddOption("--password", password); + } + + /// + /// Two-factor authentication code + /// + /// Two-factor Code + /// + public Ytdlp WithTwoFactor(string code) => AddOption("--twofactor", code); + + #endregion + + #region Post-Processing Options + + /// + /// Convert video files to audio-only files (requires ffmpeg and ffprobe). + /// + /// Formats currently supported: best (default),aac, alac, flac, m4a, mp3, opus, vorbis, wav). + /// Audio quality (0–10, lower = better). Default: 5 (medium) + public Ytdlp WithExtractAudio(AudioFormat format = AudioFormat.Best, int quality = 5) + { + return this + .AddFlag("--extract-audio") + .AddOption("--audio-format", format.ToString().ToLowerInvariant()) + .AddOption("--audio-quality", quality.ToString()); + } + + /// + /// Remux the video into another container if necessary (requires ffmpeg and ffprobe) + /// If the target container does not support the video/audio codec, remuxing will fail. You can specify multiple rules; + /// e.g. "aac>m4a/mov>mp4/mkv" will remux aac to m4a, mov to mp4 and anything else to mkv + /// + /// (currently supported: avi, flv, gif, mkv, mov, mp4, webm, aac, aiff, alac, flac, m4a, mka, mp3, ogg, opus, vorbis, wav). + public Ytdlp WithRemuxVideo(MediaFormat format = MediaFormat.Mp4) => AddOption("--remux-video", format.ToString().ToLowerInvariant()); + + + /// + /// Re-encode the video into another format if necessary. The syntax and supported formats are the same as WithRemuxVideo() + /// + /// (currently supported: avi, flv, gif, mkv, mov, mp4, webm, aac, aiff, alac, flac, m4a, mka, mp3, ogg, opus, vorbis, wav). + /// + /// + public Ytdlp WithRecodeVideo(MediaFormat format = MediaFormat.Mp4, string? videoCodec = null, string? audioCodec = null) + { + var builder = AddOption("--recode-video", format.ToString().ToLowerInvariant()); + if (!string.IsNullOrWhiteSpace(videoCodec)) + builder = builder.AddOption("--video-codec", videoCodec); + if (!string.IsNullOrWhiteSpace(audioCodec)) + builder = builder.AddOption("--audio-codec", audioCodec); + return builder; + } + + /// + /// Give these arguments to the postprocessors. Specify the postprocessor/executable name and to give the argument to the specified + /// + /// Supported PP are: Merger, ModifyChapters, SplitChapters, ExtractAudio, + /// VideoRemuxer, VideoConvertor, Metadata, EmbedSubtitle, EmbedThumbnail, SubtitlesConvertor, ThumbnailsConvertor, + /// FixupStretched, FixupM4a, FixupM3u8, FixupTimestamp and FixupDuration. + /// + public Ytdlp WithPostprocessorArgs(PostProcessors postprocessor, string args) + { + if (string.IsNullOrWhiteSpace(args)) + throw new ArgumentException("Both postprocessor name and arguments are required"); + + string combined = $"{postprocessor.ToString().Trim()}:{args.Trim()}"; + return AddOption("--postprocessor-args", combined); + } + + /// + /// Keep the intermediate video file on disk after post-processing + /// + public Ytdlp WithKeepVideo() => AddFlag("-k"); + + /// + /// Do not overwrite post-processed files + /// + public Ytdlp WithNoPostOverwrites() => AddFlag("--no-post-overwrites"); + + /// + /// Embed subtitles in the video (only for mp4, webm and mkv videos) + /// + /// + /// + public Ytdlp WithEmbedSubtitles(string languages = "all", string? convertTo = null) + { + var builder = AddFlag("--sub-langs") + .AddOption("--write-sub", languages); + if (!string.IsNullOrWhiteSpace(convertTo)) + builder = builder.AddOption("--convert-subs", convertTo); + if (convertTo?.Equals("embed", StringComparison.OrdinalIgnoreCase) == true) + builder = builder.AddFlag("--embed-subs"); + return builder; + } + + /// + /// Embed thumbnail in the video as cover art + /// + public Ytdlp WithEmbedThumbnail() => AddFlag("--embed-thumbnail"); - public Ytdlp WithTempFolder(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, tempFolder: Path.GetFullPath(path)); + /// + /// Embed metadata to the video file + /// + public Ytdlp WithEmbedMetadata() => AddFlag("--embed-metadata"); + + /// + /// Add chapter markers to the video file + /// + public Ytdlp WithEmbedChapters() => AddFlag("--embed-chapters"); + + /// + /// Embed the infojson as an attachment to mkv/mka video files + /// + public Ytdlp WithEmbedInfoJson() => AddFlag("--embed-info-json"); + + /// + /// Do not embed the infojson as an attachment to the video file + /// + public Ytdlp WithNoEmbedInfoJson() => AddFlag("--no-embed-info-json"); + + /// + /// Replace text in a metadata field using the given regex. This option can be used multiple times. + /// + /// + /// + /// + /// + public Ytdlp WithReplaceInMetadata(string field, string regex, string replacement) + { + if (string.IsNullOrWhiteSpace(field) || string.IsNullOrWhiteSpace(regex) || replacement == null) + throw new ArgumentException("Metadata field, regex, and replacement cannot be empty."); + return AddFlag($"--replace-in-metadata {field} {regex} {replacement}"); + } + + /// + /// Concatenate videos in a playlist. All the video files must have the same codecs and number of streams to be concatenable + /// + /// never, always, multi_video (default; only when the videos form a single show) + public Ytdlp WithConcatPlaylist(string policy = "always") => AddOption("--concat-playlist", policy); + + /// + /// Location of the ffmpeg binary + /// + /// Either the path to the binary or its containing directory + public Ytdlp WithFFmpegLocation(string? ffmpegPath) + { + if (string.IsNullOrWhiteSpace(ffmpegPath)) return this; + return new Ytdlp(this, ffmpegLocation: ffmpegPath); + } + + /// + /// Convert the thumbnails to another format. You can specify multiple rules using similar WithRemuxVideo(). + /// + /// (currently supported: jpg, png, webp) + /// + public Ytdlp WithConvertthumbnails(string format = "none") => AddOption("--convert-thumbnails", format); + + /// + /// Force keyframes at cuts when downloading/splitting/removing sections. + /// This is slow due to needing a re-encode, but the resulting video may have fewer artifacts around the cuts + /// + /// + public Ytdlp WithForceKeyframesAtCuts() => AddFlag("--force-keyframes-at-cuts"); - public Ytdlp WithFFmpegLocation(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, ffmpegLocation: path); + #endregion - public Ytdlp WithOutputTemplate(string template) - { - if (string.IsNullOrWhiteSpace(template)) throw new ArgumentException("Template required"); - return new Ytdlp(this, outputTemplate: template.Trim()); - } + #region SponsorBlock Options - public Ytdlp WithFormat(string format) => new Ytdlp(this, format: format.Trim()); + /// + /// SponsorBlock categories to create chapters for, separated by commas. + /// Available categories are sponsor, intro, outro, selfpromo, preview, filler, interaction, music_offtopic, hook, poi_highlight, chapter, all and default (=all). + /// You can prefix the category with a "-" to exclude it. E.g. SponsorBlockMark("all,-preview) + /// + /// + /// + public Ytdlp WithSponsorblockMark(string categories = "all") => AddOption("--sponsorblock-mark", categories); - public Ytdlp WithConcurrentFragments(int count = 8) => count > 0 ? new Ytdlp(this, concurrentFragments: count) : this; + /// + /// SponsorBlock categories to be removed from the video file, separated by commas. + /// If a category is present in both mark and remove, remove takes precedence. Working and available categories are the same as for WithSponsorblockMark() + /// + /// + /// + public Ytdlp WithSponsorblockRemove(string categories = "all") => new Ytdlp(this, sponsorblockRemove: categories); - public Ytdlp WithProxy(string? proxy) => string.IsNullOrWhiteSpace(proxy) ? this : new Ytdlp(this, proxy: proxy); + /// + /// Disable both WithSponsorblockMark() and WithSponsorblockRemove() options and do not use any sponsorblock features + /// + /// + public Ytdlp WithNoSponsorblock() => AddFlag("--no-sponsorblock"); - public Ytdlp WithCookiesFile(string? path) => string.IsNullOrWhiteSpace(path) ? this : new Ytdlp(this, cookiesFile: Path.GetFullPath(path)); + #endregion - public Ytdlp WithCookiesFromBrowser(string browser) => new Ytdlp(this, cookiesFromBrowser: browser); + #region Core + public Ytdlp AddFlag(string flag) => new Ytdlp(this, extraFlags: new[] { flag.Trim() }); - public Ytdlp WithSponsorblockRemove(string? categories = "all") => string.IsNullOrWhiteSpace(categories) ? this : new Ytdlp(this, sponsorblockRemove: categories); + public Ytdlp AddOption(string key, string? value = null) => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); + #endregion - public Ytdlp WithExtractAudio(string format = "mp3", int quality = 5) - => new Ytdlp(this, extraFlags: new[] { "--extract-audio" }, - extraOptions: new[] - { - ("--audio-format", format), - ("--audio-quality", quality.ToString(CultureInfo.InvariantCulture)) - }); - public Ytdlp WithSubtitles(string langs = "all", bool auto = false) + public Ytdlp WithExternalDownloader(string downloaderName, string? downloaderArgs = null) { - var flags = new List { "--write-subs" }; - if (auto) flags.Add("--write-auto-subs"); - - return new Ytdlp(this, extraFlags: flags, extraOptions: new[] { ("--sub-langs", langs) }); - } + if (string.IsNullOrWhiteSpace(downloaderName)) + throw new ArgumentException("Downloader name cannot be empty", nameof(downloaderName)); - public Ytdlp WithEmbedSubtitles(string langs = "all", string? convertTo = null) - { - var flags = new List { "--embed-subs", "--write-subs" }; - var options = new List<(string, string?)> { ("--sub-langs", langs) }; + var opts = new List<(string, string?)> { ("--downloader", downloaderName.Trim()) }; - if (!string.IsNullOrWhiteSpace(convertTo)) - options.Add(("--convert-subs", convertTo)); + if (!string.IsNullOrWhiteSpace(downloaderArgs)) + { + opts.Add(("--downloader-args", downloaderArgs.Trim())); + } - return new Ytdlp(this, extraFlags: flags, extraOptions: options); + return new Ytdlp(this, extraOptions: opts); } - public Ytdlp WithThumbnails(bool all = false) => new Ytdlp(this, extraFlags: new[] { all ? "--write-all-thumbnails" : "--write-thumbnail" }); - - public Ytdlp WithEmbedThumbnail() => AddFlag("--embed-thumbnail"); - public Ytdlp WithEmbedMetadata() => AddFlag("--embed-metadata"); - public Ytdlp WithEmbedChapters() => AddFlag("--embed-chapters"); - public Ytdlp WithAria2(int connections = 16) { return new Ytdlp(this, extraOptions: new[] @@ -220,62 +1014,36 @@ public Ytdlp WithAria2(int connections = 16) }); } - // 1. Playlist selection (items to download) - public Ytdlp WithPlaylistItems(string items) - { - if (string.IsNullOrWhiteSpace(items)) - throw new ArgumentException("Playlist items string cannot be empty", nameof(items)); - return AddOption("--playlist-items", items.Trim()); - } + public Ytdlp WithHlsNative() => AddOption("--downloader", "hlsnative"); + + public Ytdlp WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null) => WithExternalDownloader("ffmpeg", extraFfmpegArgs); + + #region Redundant options - // 2. Playlist start index + /// + /// Playlist start index + /// + /// + /// + /// public Ytdlp WithPlaylistStart(int index) { if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1"); return AddOption("--playlist-start", index.ToString()); } - // 3. Playlist end index + /// + /// Playlist end index + /// + /// + /// + /// public Ytdlp WithPlaylistEnd(int index) { if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1"); return AddOption("--playlist-end", index.ToString()); } - // 4. Minimum filesize - public Ytdlp WithMinFileSize(string size) - { - // size examples: 50k, 4.2M, 1G - if (string.IsNullOrWhiteSpace(size)) - throw new ArgumentException("Size cannot be empty", nameof(size)); - return AddOption("--min-filesize", size.Trim()); - } - - // 5. Maximum filesize - public Ytdlp WithMaxFileSize(string size) - { - if (string.IsNullOrWhiteSpace(size)) - throw new ArgumentException("Size cannot be empty", nameof(size)); - return AddOption("--max-filesize", size.Trim()); - } - - // 6. Date filter (upload date) - public Ytdlp WithUploadDate(string date) - { - // formats: YYYYMMDD, today, yesterday, now-2weeks, etc. - if (string.IsNullOrWhiteSpace(date)) - throw new ArgumentException("Date cannot be empty", nameof(date)); - return AddOption("--date", date.Trim()); - } - - // 7. Age limit / restriction - public Ytdlp WithAgeLimit(int years) - { - if (years < 0) throw new ArgumentOutOfRangeException(nameof(years)); - return AddOption("--age-limit", years.ToString()); - } - - // 8. User-Agent override public Ytdlp WithUserAgent(string userAgent) { if (string.IsNullOrWhiteSpace(userAgent)) @@ -283,7 +1051,6 @@ public Ytdlp WithUserAgent(string userAgent) return AddOption("--user-agent", userAgent.Trim()); } - // 9. Referer override public Ytdlp WithReferer(string referer) { if (string.IsNullOrWhiteSpace(referer)) @@ -291,34 +1058,6 @@ public Ytdlp WithReferer(string referer) return AddOption("--referer", referer.Trim()); } - // 10. Sleep interval between requests (anti-rate-limit) - public Ytdlp WithSleepInterval(double seconds, double? maxSeconds = null) - { - if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds)); - var opts = new List<(string, string?)> { ("--sleep-requests", seconds.ToString("F2", CultureInfo.InvariantCulture)) }; - if (maxSeconds.HasValue && maxSeconds > seconds) - { - opts.Add(("--max-sleep-requests", maxSeconds.Value.ToString("F2", CultureInfo.InvariantCulture))); - } - return new Ytdlp(this, extraOptions: opts); - } - - // 11. Sleep between subtitle downloads - public Ytdlp WithSleepSubtitles(double seconds) - { - if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds)); - return AddOption("--sleep-subtitles", seconds.ToString("F2", CultureInfo.InvariantCulture)); - } - - // 12. Download archive file (skip already downloaded) - public Ytdlp WithDownloadArchive(string archivePath = "archive.txt") - { - if (string.IsNullOrWhiteSpace(archivePath)) - throw new ArgumentException("Archive path cannot be empty", nameof(archivePath)); - return AddOption("--download-archive", Path.GetFullPath(archivePath)); - } - - // 13. Match title (regex include) public Ytdlp WithMatchTitle(string regex) { if (string.IsNullOrWhiteSpace(regex)) @@ -326,7 +1065,6 @@ public Ytdlp WithMatchTitle(string regex) return AddOption("--match-title", regex.Trim()); } - // 14. Reject title (regex exclude) public Ytdlp WithRejectTitle(string regex) { if (string.IsNullOrWhiteSpace(regex)) @@ -334,71 +1072,18 @@ public Ytdlp WithRejectTitle(string regex) return AddOption("--reject-title", regex.Trim()); } - // 15. Max downloads (stop after N videos) - public Ytdlp WithMaxDownloads(int count) - { - if (count < 1) throw new ArgumentOutOfRangeException(nameof(count)); - return AddOption("--max-downloads", count.ToString()); - } + public Ytdlp WithBreakOnReject() => AddFlag("--break-on-reject"); + #endregion + - // Nice-to-have #16 - public Ytdlp WithNoMtime() => AddFlag("--no-mtime"); - // Nice-to-have #17 - public Ytdlp WithNoCacheDir() => AddFlag("--no-cache-dir"); // Nice-to-have #18 – very popular for high-quality + fallback public Ytdlp With1080pOrBest() => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); - // Nice-to-have #19 - public Ytdlp WithNoPlaylist() => AddFlag("--no-playlist"); - - // Nice-to-have #20 - public Ytdlp WithYesPlaylist() => AddFlag("--yes-playlist"); - - - // 21. Geo-bypass country (two-letter ISO code) - public Ytdlp WithGeoBypassCountry(string countryCode) - { - if (string.IsNullOrWhiteSpace(countryCode) || countryCode.Length != 2) - throw new ArgumentException("Geo-bypass country must be a 2-letter ISO code", nameof(countryCode)); - - return AddOption("--geo-bypass-country", countryCode.Trim().ToUpperInvariant()); - } - - // 22. No geo-bypass (disable automatic country bypass) - public Ytdlp WithNoGeoBypass() => AddFlag("--no-geo-bypass"); - - // 23. Match-filter (advanced filter expression) - public Ytdlp WithMatchFilter(string filterExpression) - { - if (string.IsNullOrWhiteSpace(filterExpression)) - throw new ArgumentException("Match filter expression cannot be empty", nameof(filterExpression)); - - return AddOption("--match-filter", filterExpression.Trim()); - } - - // 24. Break on existing (stop when file already in archive) - public Ytdlp WithBreakOnExisting() => AddFlag("--break-on-existing"); - - // 25. Break on reject (stop when a video is filtered out by --match-filter) - public Ytdlp WithBreakOnReject() => AddFlag("--break-on-reject"); - - // 26. Postprocessor args (ppa) - most common use-cases - public Ytdlp WithPostprocessorArgs(string postprocessorName, string arguments) - { - if (string.IsNullOrWhiteSpace(postprocessorName) || string.IsNullOrWhiteSpace(arguments)) - throw new ArgumentException("Both postprocessor name and arguments are required"); - string combined = $"{postprocessorName.Trim()}:{arguments.Trim()}"; - return AddOption("--postprocessor-args", combined); - } - // 27. Force key frames at cuts (useful when cutting with --download-sections) - public Ytdlp WithForceKeyframesAtCuts() => AddFlag("--force-keyframes-at-cuts"); - // 28. Prefer free formats (when multiple formats have similar quality) - public Ytdlp WithPreferFreeFormats() => AddFlag("--prefer-free-formats"); // 29. No prefer free formats (default behavior - explicit) public Ytdlp WithNoPreferFreeFormats() => AddFlag("--no-prefer-free-formats"); @@ -416,61 +1101,6 @@ public Ytdlp WithMergeOutputFormat(string format) // Bonus 31 – very popular shortcut public Ytdlp WithBestUpTo1080p() => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); - // Bonus 32 - public Ytdlp WithKeepFragments() => AddFlag("--keep-fragments"); - - // Bonus 33 – useful for debugging - public Ytdlp WithVerbose() => AddFlag("--verbose"); - - - // 31. Reverse playlist order - public Ytdlp WithPlaylistReverse() => AddFlag("--playlist-reverse"); - - // 32. Random playlist order - public Ytdlp WithPlaylistRandom() => AddFlag("--playlist-random"); - - // 33. Lazy playlist (process entries as received – good for very large playlists) - public Ytdlp WithLazyPlaylist() => AddFlag("--lazy-playlist"); - - // 34. Flat playlist (do not extract individual video URLs – faster for listing) - public Ytdlp WithFlatPlaylist() => AddFlag("--flat-playlist"); - - // 35. Write info.json metadata file - public Ytdlp WithWriteInfoJson() => AddFlag("--write-info-json"); - - // 36. Clean info.json (remove private/empty fields) - public Ytdlp WithCleanInfoJson() => AddFlag("--clean-info-json"); - - // 37. No clean info.json (keep all fields) - public Ytdlp WithNoCleanInfoJson() => AddFlag("--no-clean-info-json"); - - // 38. Simulate only (do not download anything – useful for testing/format listing) - public Ytdlp WithSimulate() => AddFlag("--simulate"); - - // 39. Skip actual download (but do post-processing if applicable) - public Ytdlp WithSkipDownload() => AddFlag("--skip-download"); - - // 40. Write description to .description file - public Ytdlp WithWriteDescription() => AddFlag("--write-description"); - - // 41. Keep intermediate video file after post-processing - public Ytdlp WithKeepVideo() => new Ytdlp(this, extraFlags: new[] { "-k", "--keep-video" }); - - // 42. Do not overwrite post-processed files - public Ytdlp WithNoPostOverwrites() => AddFlag("--no-post-overwrites"); - - // 43. Force keyframes at cuts (important when using --download-sections) - - - // 44. Remux video into specified container format - public Ytdlp WithRemuxVideo(string format = "mp4") - { - // Supported: mp4, mkv, avi, webm, flv, mov, ... - if (string.IsNullOrWhiteSpace(format)) - throw new ArgumentException("Remux format cannot be empty", nameof(format)); - - return AddOption("--remux-video", format.Trim().ToLowerInvariant()); - } // 45. Recode / re-encode video into specified format public Ytdlp WithRecodeVideo(string format = "mp4") @@ -492,19 +1122,10 @@ public Ytdlp WithConvertThumbnails(string format = "jpg") return AddOption("--convert-thumbnails", format.Trim().ToLowerInvariant()); } - // 48. Postprocessor arguments for Merger (most common use-case) - public Ytdlp WithMergerArgs(string args) => WithPostprocessorArgs("Merger", args); - - // 49. Postprocessor arguments for ModifyChapters - public Ytdlp WithModifyChaptersArgs(string args) => WithPostprocessorArgs("ModifyChapters", args); - - // 50. Postprocessor arguments for ExtractAudio - public Ytdlp WithExtractAudioArgs(string args) => WithPostprocessorArgs("ExtractAudio", args); - // Bonus – common combo: remux to mp4 + embed metadata + chapters + thumbnail public Ytdlp WithMp4PostProcessingPreset() => this - .WithRemuxVideo("mp4") + .WithRemuxVideo(MediaFormat.Mp4) .WithEmbedMetadata() .WithEmbedChapters() .WithEmbedThumbnail(); @@ -518,65 +1139,18 @@ public Ytdlp WithMkvOutput() ("--merge-output-format", "mkv") }); - // 51. Download livestream from the start (when possible) - public Ytdlp WithLiveFromStart() => AddFlag("--live-from-start"); - - // 52. Explicitly disable downloading from the beginning of a live stream - public Ytdlp WithNoLiveFromStart() => AddFlag("--no-live-from-start"); - - // 53. Wait for a scheduled live stream to start - public Ytdlp WithWaitForVideo(TimeSpan? maxWait = null) - { - var opts = new List<(string Key, string? Value)>(); - - opts.Add(("--wait-for-video", "any")); // "any" = wait indefinitely or until timeout - - if (maxWait.HasValue && maxWait.Value.TotalSeconds > 0) - { - opts.Add(("--wait-for-video", maxWait.Value.TotalSeconds.ToString("F0"))); - } - return new Ytdlp(this, extraOptions: opts); - } - // 54. Wait until the live stream actually ends before finishing - public Ytdlp WithWaitUntilLiveEnds() => AddFlag("--wait-for-video-to-end"); - // 55. Use mpegts container/format for HLS live streams (better compatibility in some players) - public Ytdlp WithHlsUseMpegts() => AddFlag("--hls-use-mpegts"); // 56. Do not use mpegts for HLS (use default fragmented mp4) public Ytdlp WithNoHlsUseMpegts() => AddFlag("--no-hls-use-mpegts"); // 57. External downloader for live streams (e.g. ffmpeg, aria2c, ...) - public Ytdlp WithExternalDownloader(string downloaderName, string? downloaderArgs = null) - { - if (string.IsNullOrWhiteSpace(downloaderName)) - throw new ArgumentException("Downloader name cannot be empty", nameof(downloaderName)); - - var opts = new List<(string, string?)> { ("--downloader", downloaderName.Trim()) }; - - if (!string.IsNullOrWhiteSpace(downloaderArgs)) - { - opts.Add(("--downloader-args", downloaderArgs.Trim())); - } - return new Ytdlp(this, extraOptions: opts); - } // 58. Use ffmpeg as external downloader for live streams (most common choice) - public Ytdlp WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null) => WithExternalDownloader("ffmpeg", extraFfmpegArgs); - - // 59. Set fragment retries specifically useful for unstable live streams - public Ytdlp WithFragmentRetries(int retries) - { - // -1 = infinite - string value = retries < 0 ? "infinite" : retries.ToString(); - return AddOption("--fragment-retries", value); - } - // 60. Prefer native HLS downloader (instead of ffmpeg) – sometimes more stable - public Ytdlp WithHlsNative() => AddOption("--downloader", "hlsnative"); // 63. Maximum video height / resolution limit @@ -603,17 +1177,9 @@ public Ytdlp WithMaxHeightOrBest(int height) // 67. Best video up to 720p + best audio public Ytdlp With720pOrBest() => new Ytdlp(this, format: "bv*[height<=?720]+ba/best/best"); - // 68. Audio-only – best quality audio public Ytdlp WithBestAudioOnly() => new Ytdlp(this, format: "bestaudio"); - // 69. Prefer video formats with higher bitrate (when resolution is similar) - public Ytdlp WithFormatSortBitrate() => AddOption("-S", "br"); - - // 70. Prefer formats with higher resolution first, then bitrate - public Ytdlp WithFormatSortResolutionThenBitrate() => AddOption("-S", "res,br"); - - // Bonus A – very popular preset public Ytdlp WithBestUpTo1440p() => new Ytdlp(this, format: "bestvideo[height<=?1440]+bestaudio/best"); @@ -623,49 +1189,8 @@ public Ytdlp WithMaxHeightOrBest(int height) // Bonus C – audio-only with specific codec preference public Ytdlp WithBestM4aAudio() => new Ytdlp(this, format: "bestaudio[ext=m4a]/bestaudio/best"); - // 71. Restrict filenames to ASCII-only + avoid problematic characters - public Ytdlp WithRestrictFilenames() => AddFlag("--restrict-filenames"); - - // 72. Force Windows-compatible filenames (avoid reserved names, invalid chars) - public Ytdlp WithWindowsFilenames() => AddFlag("--windows-filenames"); - - // 73. Limit filename length (excluding extension) - public Ytdlp WithTrimFilenames(int maxLength) - { - if (maxLength < 10) - throw new ArgumentOutOfRangeException(nameof(maxLength), "Length should be at least 10 characters"); - - return AddOption("--trim-filenames", maxLength.ToString()); - } - - // 74. No overwrite existing files - public Ytdlp WithNoOverwrites() => AddFlag("--no-overwrites"); - - // 75. Force overwrite existing files - public Ytdlp WithForceOverwrites() => AddFlag("--force-overwrites"); - - // 76. Continue partially downloaded files - public Ytdlp WithContinue() => AddFlag("--continue"); - - // 77. Do not continue partially downloaded files (start from beginning) - public Ytdlp WithNoContinue() => AddFlag("--no-continue"); - - // 78. Use .part files during download - public Ytdlp WithPartFiles() => AddFlag("--part"); - - // 79. Do not use .part files (write directly to final filename) - public Ytdlp WithNoPartFiles() => AddFlag("--no-part"); - - // 80. Use server mtime (Last-Modified header) for file timestamp - public Ytdlp WithMtime() => AddFlag("--mtime"); - - - public Ytdlp AddFlag(string flag) => new Ytdlp(this, extraFlags: new[] { flag.Trim() }); - public Ytdlp AddOption(string key, string? value = null) => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); - #endregion - #region Execution & Utility Methods /// @@ -1107,7 +1632,7 @@ public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = return best?.FormatId ?? "bestvideo"; } - + /// /// Executes batch download processing for a collection of URLs with a specified concurrency limit. @@ -1152,7 +1677,7 @@ public async Task ExecuteBatchAsync(IEnumerable urls, int maxConcurrency #endregion #region Helpers - + // Get probe runner private ProbeRunner Probe() { From 83cb5f257c86a8f152c81e386e0754695ca2d1d3 Mon Sep 17 00:00:00 2001 From: manusoft Date: Sat, 21 Mar 2026 02:51:01 +0400 Subject: [PATCH 5/8] Refactoring file struction add yml for build test. --- ...ytdlp-net-v2.yml => ytdlp-net-package.yml} | 3 +- .github/workflows/ytdlp-net.yml | 23 ++ src/Ytdlp.NET.Console/Program.cs | 8 +- .../CommandCompletedEventArgs.cs | 0 .../DownloadProgressEventArgs.cs | 0 .../{Helpers => Parsing}/ProgressParser.cs | 0 .../{Helpers => Parsing}/RegexPatterns.cs | 0 src/Ytdlp.NET/README.md | 93 ++++++-- src/Ytdlp.NET/Ytdlp.cs | 206 +++++++++--------- .../Obsolete/ParseFormatTest.cs | 40 ++-- .../Obsolete/ProgressParserTests.cs | 155 ------------- tests/Ytdlp.NET.Test/ProgressParserTests.cs | 157 ++++++++++++- 12 files changed, 375 insertions(+), 310 deletions(-) rename .github/workflows/{ytdlp-net-v2.yml => ytdlp-net-package.yml} (93%) create mode 100644 .github/workflows/ytdlp-net.yml rename src/Ytdlp.NET/{Helpers => Events}/CommandCompletedEventArgs.cs (100%) rename src/Ytdlp.NET/{Helpers => Events}/DownloadProgressEventArgs.cs (100%) rename src/Ytdlp.NET/{Helpers => Parsing}/ProgressParser.cs (100%) rename src/Ytdlp.NET/{Helpers => Parsing}/RegexPatterns.cs (100%) delete mode 100644 tests/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs diff --git a/.github/workflows/ytdlp-net-v2.yml b/.github/workflows/ytdlp-net-package.yml similarity index 93% rename from .github/workflows/ytdlp-net-v2.yml rename to .github/workflows/ytdlp-net-package.yml index 18cd7fb..e285678 100644 --- a/.github/workflows/ytdlp-net-v2.yml +++ b/.github/workflows/ytdlp-net-package.yml @@ -1,4 +1,4 @@ -name: Publish Ytdlp.NET v2.x.x Package +name: Publish Ytdlp.NET Package on: push: @@ -45,7 +45,6 @@ jobs: --api-key "${{ secrets.NUGET_KEY }}" ` --skip-duplicate # ── Publish to GitHub Packages ───────────────────────────────── - # GITHUB_TOKEN is automatically available + has package:write permission dotnet nuget push "$nupkg" ` --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" ` --api-key "${{ secrets.GITHUB_TOKEN }}" ` diff --git a/.github/workflows/ytdlp-net.yml b/.github/workflows/ytdlp-net.yml new file mode 100644 index 0000000..b4b5a71 --- /dev/null +++ b/.github/workflows/ytdlp-net.yml @@ -0,0 +1,23 @@ +name: Ytdlp.NET + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Restore dependencies + run: dotnet restore src/Ytdlp.NET/Ytdlp.NET.csproj + - name: Build + run: dotnet build src/Ytdlp.NET/Ytdlp.NET.csproj -c Release \ No newline at end of file diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs index 6599280..5807214 100644 --- a/src/Ytdlp.NET.Console/Program.cs +++ b/src/Ytdlp.NET.Console/Program.cs @@ -62,7 +62,7 @@ public void Log(LogType type, string message) private static async Task TestGetVersionAsync(Ytdlp ytdlp) { Console.WriteLine("\nTest 1: Getting yt-dlp version..."); - var version = await ytdlp.GetVersionAsync(); + var version = await ytdlp.VersionAsync(); Console.WriteLine($"Version: {version}"); } @@ -80,7 +80,7 @@ private static async Task TestGetFormatsAsync(Ytdlp ytdlp) Console.WriteLine("\nTest 2: Fetching available formats..."); var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; - var formats = await ytdlp.GetAvailableFormatsAsync(url); + var formats = await ytdlp.GetFormatsAsync(url); stopwatch.Stop(); // stop timer Console.WriteLine($"Available formats took {stopwatch.Elapsed.TotalSeconds:F3} seconds"); @@ -109,7 +109,7 @@ private static async Task TestGetFormatsDetailedAsync(Ytdlp ytdlp) Console.WriteLine("\nTest 3: Fetching detailed formats..."); var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; - var formats = await ytdlp.GetAvailableFormatsAsync(url); + var formats = await ytdlp.GetFormatsAsync(url); stopwatch.Stop(); // stop timer Console.WriteLine($"Detailed formats took {stopwatch.Elapsed.TotalSeconds:F3} seconds"); @@ -225,7 +225,7 @@ private static async Task TestDownloadAudioAsync(Ytdlp ytdlp) var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; await ytdlp - .WithExtractAudio("mp3") + .WithExtractAudio(AudioFormat.Mp3) .WithFormat("ba") .WithOutputFolder("./downloads/audio") .ExecuteAsync(url); diff --git a/src/Ytdlp.NET/Helpers/CommandCompletedEventArgs.cs b/src/Ytdlp.NET/Events/CommandCompletedEventArgs.cs similarity index 100% rename from src/Ytdlp.NET/Helpers/CommandCompletedEventArgs.cs rename to src/Ytdlp.NET/Events/CommandCompletedEventArgs.cs diff --git a/src/Ytdlp.NET/Helpers/DownloadProgressEventArgs.cs b/src/Ytdlp.NET/Events/DownloadProgressEventArgs.cs similarity index 100% rename from src/Ytdlp.NET/Helpers/DownloadProgressEventArgs.cs rename to src/Ytdlp.NET/Events/DownloadProgressEventArgs.cs diff --git a/src/Ytdlp.NET/Helpers/ProgressParser.cs b/src/Ytdlp.NET/Parsing/ProgressParser.cs similarity index 100% rename from src/Ytdlp.NET/Helpers/ProgressParser.cs rename to src/Ytdlp.NET/Parsing/ProgressParser.cs diff --git a/src/Ytdlp.NET/Helpers/RegexPatterns.cs b/src/Ytdlp.NET/Parsing/RegexPatterns.cs similarity index 100% rename from src/Ytdlp.NET/Helpers/RegexPatterns.cs rename to src/Ytdlp.NET/Parsing/RegexPatterns.cs diff --git a/src/Ytdlp.NET/README.md b/src/Ytdlp.NET/README.md index 812c95f..3db8e7b 100644 --- a/src/Ytdlp.NET/README.md +++ b/src/Ytdlp.NET/README.md @@ -63,12 +63,27 @@ var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "tools"); --- +## 🛠 Methods +* `VersionAsync(CancellationToken ct)` +* `UpdateAsync(UpdateChannel channel, CancellationToken ct)` +* `GetMetadataAsync(string url, CancellationToken ct, int bufferKb)` +* `GetMetadataRawAsync(string url, CancellationToken ct, int bufferKb)` +* `GetFormatsAsync(string url, CancellationToken ct, int bufferKb)` +* `GetMetadataLiteAsync(string url, CancellationToken ct, int bufferKb)` +* `GetMetadataLiteAsync(string url, IEnumerable fields, CancellationToken ct, int bufferKb)` +* `GetBestAudioFormatIdAsync(string url, CancellationToken ct, int bufferKb)` +* `GetBestVideoFormatIdAsync(string url, int maxHeight, CancellationToken ct, int bufferKb)` +* `ExecuteAsync(string url, CancellationToken ct)` +* `ExecuteBatchAsync(IEnumerable urls, int maxConcurrency, CancellationToken ct)` + + + ## 🔧 Thread Safety & Disposal * **Immutable & thread-safe**: Each `WithXxx()` call returns a new instance. * **Async disposal**: `Ytdlp` implements `IAsyncDisposable`. -**Sequential download example**: +### **Sequential download example**: ```csharp await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) @@ -81,7 +96,7 @@ ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"Download complete: { await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); ``` -**Parallel download example**: +### **Parallel download example**: ```csharp var urls = new[] { "https://youtu.be/video1", "https://youtu.be/video2" }; @@ -101,7 +116,7 @@ var tasks = urls.Select(async url => await Task.WhenAll(tasks); ``` -**Key points**: +### **Key points**: 1. Always create a **new instance per download** for parallel operations. 2. Always use `await using` for proper resource cleanup. @@ -151,6 +166,19 @@ Console.WriteLine($"Title: {metadata?.Title}, Duration: {metadata?.Duration}"); --- +### Fetch Formats + +```csharp +await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe"); + +var formats = await ytdlp.GetFormatsAsync("https://www.youtube.com/watch?v=abc123"); + +foreach(var format in formats) + Console.WriteLine($"Id: {metadata?.Id}, Extension: {metadata?.Extension}"); +``` + +--- + ### Best Format Selection ```csharp @@ -188,16 +216,52 @@ await Task.WhenAll(tasks); ### Fluent Methods (v3.0) -#### Output & Paths - -* `.WithOutputFolder(string path)` -* `.WithTempFolder(string path)` +#### General Options +* `.WithJsRuntime(Runtime runtime, string runtimePath)` +* `.WithNoJsRuntime()` +* `.WithFlatPlaylist()` +* ` WithLiveFromStart()` +* `.WithWaitForVideo(TimeSpan? maxWait = null)` +* `.WithMarkWatched()` + +#### Network Options +* `.WithProxy(string? proxy)` +* `.WithSocketTimeout(TimeSpan timeout)` +* `.WithForceIpv4()` +* `.WithForceIpv6()` +* `.WithEnableFileUrl()` + +#### Geo-restriction Options +* `.WithGeoVerificationProxy(string url)` +* `.WithGeoBypassCountry(string countryCode)` + +#### Filesystem Options * `.WithHomeFolder(string path)` +* `.WithTempFolder(string path)` +* `.WithOutputFolder(string path)` * `.WithFFmpegLocation(string path)` * `.WithOutputTemplate(string template)` +* `.WithRestrictFilenames()` +* `.WithWindowsFilenames()` +* `.WithTrimFilenames(int length)` +* `.WithNoOverwrites()` +* `.WithForceOverwrites()` +* `.WithNoContinue()` +* `.WithNoPart()` +* `.WithMtime()` +* `.WithWriteDescription()` +* `.WithWriteInfoJson()` +* `.WithNoWritePlaylistMetafiles()` +* `.WithNoCleanInfoJson()` +* `.WriteComments()` +* `.WithNoWriteComments()` +* `.WithLoadInfoJson(string path)` +* `.WithCookiesFile(string path)` +* `.WithCookiesFromBrowser(string browser)` +* `.WithNoCacheDir()` +* `.WithRemoveCacheDir()` #### Format & Extraction - * `.WithFormat(string format)` * `.WithExtractAudio(string format = "mp3", int quality = 5)` * `.With720pOrBest()` @@ -206,26 +270,17 @@ await Task.WhenAll(tasks); * `.WithEmbedChapters()` #### Subtitles & Thumbnails - * `.WithSubtitles(string langs = "all", bool auto = false)` * `.WithEmbedSubtitles(string langs = "all", string? convertTo = null)` * `.WithThumbnails(bool all = false)` -#### Network & Auth - -* `.WithProxy(string proxy)` -* `.WithCookiesFile(string path)` -* `.WithCookiesFromBrowser(string browser)` - #### Download Control - * `.WithConcurrentFragments(int count)` * `.WithSponsorblockRemove(string categories = "all")` -#### Advanced - +#### Advanced Options * `.AddFlag(string flag)` -* `.AddOption(string key, string? value = null)` +* `.AddOption(string key, string value)` --- diff --git a/src/Ytdlp.NET/Ytdlp.cs b/src/Ytdlp.NET/Ytdlp.cs index bf68536..69454c1 100644 --- a/src/Ytdlp.NET/Ytdlp.cs +++ b/src/Ytdlp.NET/Ytdlp.cs @@ -1,6 +1,5 @@ using ManuHub.Ytdlp.NET.Core; using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -58,7 +57,7 @@ public sealed class Ytdlp : IAsyncDisposable private readonly int? _concurrentFragments; private readonly ImmutableArray _flags; - private readonly ImmutableArray<(string Key, string? Value)> _options; + private readonly ImmutableArray<(string Key, string Value)> _options; #endregion #region Events @@ -101,7 +100,7 @@ public Ytdlp(string ytdlpPath = "yt-dlp", ILogger? logger = null) _format = "b"; _concurrentFragments = null; _flags = ImmutableArray.Empty; - _options = ImmutableArray<(string, string?)>.Empty; + _options = ImmutableArray<(string, string)>.Empty; _cookiesFile = null; _cookiesFromBrowser = null; _proxy = null; @@ -123,7 +122,7 @@ private Ytdlp(Ytdlp other, string? ffmpegLocation = null, string? sponsorblockRemove = null, IEnumerable? extraFlags = null, - IEnumerable<(string, string?)>? extraOptions = null) + IEnumerable<(string, string)>? extraOptions = null) { _ytdlpPath = other._ytdlpPath; _logger = other._logger; @@ -198,7 +197,7 @@ public Ytdlp WithWaitForVideo(TimeSpan? maxWait = null) opts.Add(("--wait-for-video", maxWait.Value.TotalSeconds.ToString("F0"))); } - return new Ytdlp(this, extraOptions: opts); + return new Ytdlp(this, extraOptions: opts!); } /// @@ -483,33 +482,35 @@ public Ytdlp WithDownloadSections(string regex) /// Sets the home folder for yt-dlp (used for config or as base directory). /// Path is automatically normalized and quoted. /// + /// /// - public Ytdlp WithHomeFolder(string? homeFolder) + public Ytdlp WithHomeFolder(string? path) { - if (string.IsNullOrWhiteSpace(homeFolder)) throw new ArgumentException("Home folder path cannot be empty"); - return new Ytdlp(this, homeFolder: Path.GetFullPath(homeFolder)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Home folder path required"); + return new Ytdlp(this, homeFolder: Path.GetFullPath(path)); } /// /// Sets the temporary folder for yt-dlp intermediate files (fragments, etc.). /// Path is automatically normalized and quoted. /// + /// /// - public Ytdlp WithTempFolder(string? tempFolder) + public Ytdlp WithTempFolder(string? path) { - if (string.IsNullOrWhiteSpace(tempFolder)) throw new ArgumentException("Temp folder path cannot be empty"); - return new Ytdlp(this, tempFolder: Path.GetFullPath(tempFolder)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Temp folder path required"); + return new Ytdlp(this, tempFolder: Path.GetFullPath(path)); } /// /// Sets the output folder /// - /// + /// /// - public Ytdlp WithOutputFolder(string folder) + public Ytdlp WithOutputFolder(string path) { - if (string.IsNullOrWhiteSpace(folder)) throw new ArgumentException("Output folder required"); - return new Ytdlp(this, outputFolder: Path.GetFullPath(folder)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Output folder path required"); + return new Ytdlp(this, outputFolder: Path.GetFullPath(path)); } /// @@ -604,12 +605,12 @@ public Ytdlp WithTrimFilenames(int length) /// /// JSON file containing the video information (created with the WriteVideoMetadata() option) /// - /// *.json - public Ytdlp WithLoadInfoJson(string filePath) + /// *.json + public Ytdlp WithLoadInfoJson(string path) { - if (string.IsNullOrWhiteSpace(filePath)) - throw new ArgumentException("Json file path cannot be empty.", nameof(filePath)); - return AddOption("--load-info-json", filePath); + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("Json file path cannot be empty.", nameof(path)); + return AddOption("--load-info-json", path); } /// @@ -731,7 +732,7 @@ public Ytdlp WithSleepInterval(double seconds, double? maxSeconds = null) { opts.Add(("--max-sleep-requests", maxSeconds.Value.ToString("F2", CultureInfo.InvariantCulture))); } - return new Ytdlp(this, extraOptions: opts); + return new Ytdlp(this, extraOptions: opts!); } /// @@ -986,7 +987,7 @@ public Ytdlp WithFFmpegLocation(string? ffmpegPath) #region Core public Ytdlp AddFlag(string flag) => new Ytdlp(this, extraFlags: new[] { flag.Trim() }); - public Ytdlp AddOption(string key, string? value = null) => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); + public Ytdlp AddOption(string key, string value) => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); #endregion @@ -1002,7 +1003,7 @@ public Ytdlp WithExternalDownloader(string downloaderName, string? downloaderArg opts.Add(("--downloader-args", downloaderArgs.Trim())); } - return new Ytdlp(this, extraOptions: opts); + return new Ytdlp(this, extraOptions: opts!); } public Ytdlp WithAria2(int connections = 16) @@ -1194,81 +1195,7 @@ public Ytdlp WithMaxHeightOrBest(int height) #region Execution & Utility Methods /// - /// - /// - /// - /// - /// - /// - /// - public async Task ExecuteAsync(string url, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentException("URL required", nameof(url)); - - // Ensure directories exist if needed - try - { - if (!string.IsNullOrWhiteSpace(_outputFolder)) - Directory.CreateDirectory(_outputFolder); - - if (!string.IsNullOrWhiteSpace(_homeFolder)) - Directory.CreateDirectory(_homeFolder); - - if (!string.IsNullOrWhiteSpace(_tempFolder)) - Directory.CreateDirectory(_tempFolder); - } - catch (Exception ex) - { - _logger.Log(LogType.Error, $"Failed to create necessary folders: {ex.Message}"); - throw new YtdlpException("Failed to create required folders", ex); - } - - var argsList = BuildArguments(url); - var arguments = string.Join(" ", argsList.Select(Quote)); - - _logger.Log(LogType.Info, $"Executing: {_ytdlpPath} {arguments}"); - - // Create isolated execution components - var factory = new ProcessFactory(_ytdlpPath); - var progressParser = new ProgressParser(_logger); - var download = new DownloadRunner(factory, progressParser, _logger); - - // Forward progress events locally inside this method - void OnProgressDownloadHandler(object? s, DownloadProgressEventArgs e) - => OnProgressDownload?.Invoke(this, e); - - void OnProgressMessageHandler(object? s, string msg) - => OnProgressMessage?.Invoke(this, msg); - - // Attach progress handlers - progressParser.OnProgressDownload += OnProgressDownloadHandler; - progressParser.OnProgressMessage += OnProgressMessageHandler; - - // Forward other events - progressParser.OnOutputMessage += (_, e) => OnOutputMessage?.Invoke(this, e); - progressParser.OnCompleteDownload += (_, e) => OnCompleteDownload?.Invoke(this, e); - progressParser.OnErrorMessage += (_, e) => OnErrorMessage?.Invoke(this, e); - progressParser.OnPostProcessingComplete += (_, e) => OnPostProcessingComplete?.Invoke(this, e); - - download.OnCommandCompleted += (_, e) => OnCommandCompleted?.Invoke(this, e); - - try - { - await download.RunAsync(arguments, ct); - } - finally - { - // Unsubscribe immediately after execution to prevent memory leaks - progressParser.OnProgressDownload -= OnProgressDownloadHandler; - progressParser.OnProgressMessage -= OnProgressMessageHandler; - } - } - - /// - /// + /// Command preview ofr debug operatons /// /// /// @@ -1286,7 +1213,7 @@ public string Preview(string url) /// A representing the yt-dlp version (e.g., "2023.03.04"); /// returns an empty string or throws if the binary cannot be found. /// - public async Task GetVersionAsync(CancellationToken ct = default) + public async Task VersionAsync(CancellationToken ct = default) { var output = await Probe().RunAsync("--version", ct); string version = output is null ? string.Empty : output.Trim(); @@ -1298,14 +1225,14 @@ public async Task GetVersionAsync(CancellationToken ct = default) /// Updates the underlying yt-dlp binary to the latest version on the specified release channel. /// /// The release channel to pull updates from (Master, Nightly, Stable.). - /// A to abort the download and installation process. + /// A to abort the download and installation process. /// /// A containing the update log or the new version number; /// returns an empty string or throws if the update process fails. /// - public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stable, CancellationToken cancellationToken = default) + public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stable, CancellationToken ct = default) { - var output = await Probe().RunAsync($"--update-to {channel.ToString().ToLowerInvariant()}", cancellationToken); + var output = await Probe().RunAsync($"--update-to {channel.ToString().ToLowerInvariant()}", ct); if (output is null) return string.Empty; @@ -1439,7 +1366,7 @@ public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stab /// returns an empty list or if the probe fails or is cancelled. /// /// - public async Task> GetAvailableFormatsAsync(string url, CancellationToken ct = default, int bufferKb = 128) + public async Task> GetFormatsAsync(string url, CancellationToken ct = default, int bufferKb = 128) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("Video URL cannot be empty.", nameof(url)); @@ -1633,6 +1560,79 @@ public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = return best?.FormatId ?? "bestvideo"; } + /// + /// Executes download processing for a URL. + /// + /// The source URL to download. + /// A to stop the execution. + /// + /// + /// + public async Task ExecuteAsync(string url, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(url)) + throw new ArgumentException("URL required", nameof(url)); + + // Ensure directories exist if needed + try + { + if (!string.IsNullOrWhiteSpace(_outputFolder)) + Directory.CreateDirectory(_outputFolder); + + if (!string.IsNullOrWhiteSpace(_homeFolder)) + Directory.CreateDirectory(_homeFolder); + + if (!string.IsNullOrWhiteSpace(_tempFolder)) + Directory.CreateDirectory(_tempFolder); + } + catch (Exception ex) + { + _logger.Log(LogType.Error, $"Failed to create necessary folders: {ex.Message}"); + throw new YtdlpException("Failed to create required folders", ex); + } + + var argsList = BuildArguments(url); + var arguments = string.Join(" ", argsList.Select(Quote)); + + _logger.Log(LogType.Info, $"Executing: {_ytdlpPath} {arguments}"); + + // Create isolated execution components + var factory = new ProcessFactory(_ytdlpPath); + var progressParser = new ProgressParser(_logger); + var download = new DownloadRunner(factory, progressParser, _logger); + + // Forward progress events locally inside this method + void OnProgressDownloadHandler(object? s, DownloadProgressEventArgs e) + => OnProgressDownload?.Invoke(this, e); + + void OnProgressMessageHandler(object? s, string msg) + => OnProgressMessage?.Invoke(this, msg); + + // Attach progress handlers + progressParser.OnProgressDownload += OnProgressDownloadHandler; + progressParser.OnProgressMessage += OnProgressMessageHandler; + + // Forward other events + progressParser.OnOutputMessage += (_, e) => OnOutputMessage?.Invoke(this, e); + progressParser.OnCompleteDownload += (_, e) => OnCompleteDownload?.Invoke(this, e); + progressParser.OnErrorMessage += (_, e) => OnErrorMessage?.Invoke(this, e); + progressParser.OnPostProcessingComplete += (_, e) => OnPostProcessingComplete?.Invoke(this, e); + + download.OnCommandCompleted += (_, e) => OnCommandCompleted?.Invoke(this, e); + + try + { + await download.RunAsync(arguments, ct); + } + finally + { + // Unsubscribe immediately after execution to prevent memory leaks + progressParser.OnProgressDownload -= OnProgressDownloadHandler; + progressParser.OnProgressMessage -= OnProgressMessageHandler; + } + } /// /// Executes batch download processing for a collection of URLs with a specified concurrency limit. diff --git a/tests/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs b/tests/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs index 505964d..8ff402f 100644 --- a/tests/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs +++ b/tests/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs @@ -1,5 +1,5 @@ -using System.Text.RegularExpressions; -using YtdlpDotNet; +using ManuHub.Ytdlp.NET; +using System.Text.RegularExpressions; namespace Ytdlp.NET.Test.Obsolete; @@ -26,10 +26,10 @@ public void ParseFormats_ParsesSampleOutput() var output = TestConstants.GetAvailableFormats(); var formats = ParseFormats(output); Assert.Equal(40, formats.Count); - Assert.Contains(formats, f => f.ID == "233" && f.VCodec == "audio only" && f.ACodec == "unknown"); - Assert.Contains(formats, f => f.ID == "sb3" && f.VCodec == "images" && f.ACodec == null && f.MoreInfo == "storyboard"); - Assert.Contains(formats, f => f.ID == "249-drc" && f.Resolution == "audio only" && f.Channels == "2" && f.FileSize == "1.64MiB" && f.ACodec == "opus"); - Assert.Contains(formats, f => f.ID == "18" && f.Resolution == "640x360" && f.Channels == "2" && f.VCodec == "avc1.42001E" && f.ACodec == "mp4a.40.2"); + Assert.Contains(formats, f => f.Id == "233" && f.VideoCodec == "audio only" && f.AudioCodec == "unknown"); + Assert.Contains(formats, f => f.Id == "sb3" && f.VideoCodec == "images" && f.AudioCodec == null && f.MoreInfo == "storyboard"); + Assert.Contains(formats, f => f.Id == "249-drc" && f.Resolution == "audio only" && f.Channels == "2" && f.FileSizeApprox == "1.64MiB" && f.AudioCodec == "opus"); + Assert.Contains(formats, f => f.Id == "18" && f.Resolution == "640x360" && f.Channels == "2" && f.VideoCodec == "avc1.42001E" && f.AudioCodec == "mp4a.40.2"); } public List ParseFormats(string result) @@ -77,10 +77,10 @@ public List ParseFormats(string result) try { // Parse ID - format.ID = parts[index++]; + format.Id = parts[index++]; // Check for duplicate ID - if (formats.Any(f => f.ID == format.ID)) + if (formats.Any(f => f.Id == format.Id)) { continue; } @@ -106,7 +106,7 @@ public List ParseFormats(string result) // Parse FPS (empty for audio-only formats) if (format.Resolution != "audio only" && index < parts.Length && Regex.IsMatch(parts[index], @"^\d+$")) { - format.FPS = parts[index++]; + //format.Fps = parts[index++]; } // Parse Channels (marked by '|' or number) @@ -125,14 +125,14 @@ public List ParseFormats(string result) // Parse FileSize if (index < parts.Length && (Regex.IsMatch(parts[index], @"^~?\d+\.\d+MiB$") || parts[index] == "")) { - format.FileSize = parts[index] == "" ? null : parts[index]; + format.FileSizeApprox = parts[index] == "" ? null : parts[index]; index++; } // Parse TBR if (index < parts.Length && Regex.IsMatch(parts[index], @"^\d+k$")) { - format.TBR = parts[index]; + format.TotalBitrate = parts[index]; index++; } @@ -154,17 +154,17 @@ public List ParseFormats(string result) { if (parts[index] == "audio" && index + 1 < parts.Length && parts[index + 1] == "only") { - format.VCodec = "audio only"; + format.VideoCodec = "audio only"; index += 2; } else if (parts[index] == "images") { - format.VCodec = "images"; + format.VideoCodec = "images"; index++; } else if (Regex.IsMatch(parts[index], @"^[a-zA-Z0-9\.]+$")) { - format.VCodec = parts[index]; + format.VideoCodec = parts[index]; index++; } } @@ -172,28 +172,28 @@ public List ParseFormats(string result) // Parse VBR if (index < parts.Length && Regex.IsMatch(parts[index], @"^\d+k$")) { - format.VBR = parts[index]; + format.VideoBitrate = parts[index]; index++; } // Parse ACodec if (index < parts.Length && (Regex.IsMatch(parts[index], @"^[a-zA-Z0-9\.]+$") || parts[index] == "unknown")) { - format.ACodec = parts[index]; + format.AudioCodec = parts[index]; index++; } // Parse ABR if (index < parts.Length && Regex.IsMatch(parts[index], @"^\d+k$")) { - format.ABR = parts[index]; + format.AudioBitrate = parts[index]; index++; } // Parse ASR if (index < parts.Length && Regex.IsMatch(parts[index], @"^\d+k$")) { - format.ASR = parts[index]; + //format.AudioSampleRate = parts[index]; index++; } @@ -207,9 +207,9 @@ public List ParseFormats(string result) format.MoreInfo = format.MoreInfo.Substring(1).Trim(); } // For storyboards, ensure MoreInfo is 'storyboard' and ACodec is null - if (format.VCodec == "images" && format.MoreInfo != "storyboard") + if (format.VideoCodec == "images" && format.MoreInfo != "storyboard") { - format.ACodec = null; + format.AudioCodec = null; format.MoreInfo = "storyboard"; } } diff --git a/tests/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs b/tests/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs deleted file mode 100644 index e070387..0000000 --- a/tests/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System.Text.RegularExpressions; -using YtdlpDotNet; - -namespace Ytdlp.NET.Test.Obsolete; - -public class ProgressParserTests -{ - [Fact] - public void ExtractingUrl_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[youtube] Extracting URL: https://www.youtube.com/watch?v=Gk0WHyRUcgM"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadingWebpage_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[youtube] Gk0WHyRUcgM: Downloading webpage"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadingJson_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[youtube] Gk0WHyRUcgM: Downloading ios player API JSON"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadingTvClientConfig_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[youtube] Gk0WHyRUcgM: Downloading tv client config"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadingM3u8_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[youtube] Gk0WHyRUcgM: Downloading m3u8 information"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadingManifest_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[youtube] Gk0WHyRUcgM: Downloading ios player API JSON"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadDestination_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[download] Destination: downloads\\"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadingFormat_LogsCorrectly() - { - var parser = new ProgressParser(); - var output = "[info] Gk0WHyRUcgM: Downloading 1 format(s): 248+251"; - parser.ParseProgress(output); - // Assert that OnProgressMessage was invoked with expected message - } - - [Fact] - public void DownloadingThumbnail_LogsCorrectly() - { - var logger = new TestLogger(); - var parser = new ProgressParser(logger); - string output = "[info] Downloading video thumbnail 41 ..."; - parser.ParseProgress(output); - Assert.Contains(logger.Logs, l => l.Message == "Downloading video thumbnail 41"); - } - - [Fact] - public void WritingThumbnail_LogsCorrectly() - { - var logger = new TestLogger(); - var parser = new ProgressParser(logger); - string output = "[info] Writing video thumbnail 41 to: downloads\\Kunukkitta Kozhi Jagadish.webp"; - parser.ParseProgress(output); - Assert.Contains(logger.Logs, l => l.Message == "Writing video thumbnail 41 to: downloads\\Kunukkitta Kozhi Jagadish.webp"); - } - - [Fact] - public void DeletingOriginalFile_LogsCorrectly() - { - // Arrange - var logger = new TestLogger(); - var parser = new ProgressParser(logger); - bool triggered = false; - parser.OnPostProcessingComplete += (s, e) => triggered = true; - - // Act - parser.ParseProgress("[Merger] Merging formats into \"C:\\Users\\manua\\Videos\\test.webm\""); - parser.ParseProgress("Deleting original file C:\\Users\\manua\\Videos\\test.f251.webm (pass -k to keep)"); - parser.ParseProgress("Deleting original file C:\\Users\\manua\\Videos\\test.f401.mp4 (pass -k to keep)"); - - // Assert - Assert.True(triggered, "OnPostProcessingComplete was not triggered after processing both deletion lines."); - Assert.Contains("OnPostProcessingComplete event triggered.", logger.GetMessages()); - } - - [Fact] - public void DeletingOriginalFile_Matches() { - var regex = new Regex(@"Deleting original file\s+(?.+?)\s+\(pass -k to keep\)", RegexOptions.IgnoreCase); - var match = regex.Match("Deleting original file C:\\Users\\manua\\Videos\\test.f251.webm (pass -k to keep)"); - Assert.True(match.Success); // Should pass - } - - [Fact] - public void Reset_ClearsDownloadCompletedFlag() - { - var logger = new TestLogger(); - var parser = new ProgressParser(logger); - parser.ParseProgress("[download] 100% of 29.53MiB at Unknown ETA Unknown"); - Assert.Contains(logger.Logs, l => l.Message.Contains("Download complete")); - parser.Reset(); - parser.ParseProgress("[download] 100% of 10MiB at Unknown ETA Unknown"); - Assert.Contains(logger.Logs, l => l.Message.Contains("Download complete: 100% of 10MiB")); - } - - private class TestLogger : ILogger - { - public List<(LogType Type, string Message)> Logs { get; } = new(); - //public void Log(LogType type, string message) => Logs.Add((type, message)); - - public void Log(LogType type, string message) - { - Logs.Add((type, message)); - } - - - // Helper to get messages as a list of strings for assertions - public IEnumerable GetMessages() - { - return Logs.Select(log => log.Message); - } - } -} diff --git a/tests/Ytdlp.NET.Test/ProgressParserTests.cs b/tests/Ytdlp.NET.Test/ProgressParserTests.cs index b7bd093..5ce32ed 100644 --- a/tests/Ytdlp.NET.Test/ProgressParserTests.cs +++ b/tests/Ytdlp.NET.Test/ProgressParserTests.cs @@ -1,12 +1,155 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using ManuHub.Ytdlp.NET; +using System.Text.RegularExpressions; -namespace Ytdlp.NET.Test +namespace Ytdlp.NET.Test.Obsolete; + +public class ProgressParserTests { - internal class ProgressParserTests + [Fact] + public void ExtractingUrl_LogsCorrectly() + { + var parser = new ProgressParser(); + var output = "[youtube] Extracting URL: https://www.youtube.com/watch?v=Gk0WHyRUcgM"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadingWebpage_LogsCorrectly() + { + var parser = new ProgressParser(); + var output = "[youtube] Gk0WHyRUcgM: Downloading webpage"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadingJson_LogsCorrectly() + { + var parser = new ProgressParser(); + var output = "[youtube] Gk0WHyRUcgM: Downloading ios player API JSON"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadingTvClientConfig_LogsCorrectly() + { + var parser = new ProgressParser(); + var output = "[youtube] Gk0WHyRUcgM: Downloading tv client config"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadingM3u8_LogsCorrectly() + { + var parser = new ProgressParser(); + var output = "[youtube] Gk0WHyRUcgM: Downloading m3u8 information"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadingManifest_LogsCorrectly() + { + var parser = new ProgressParser(); + var output = "[youtube] Gk0WHyRUcgM: Downloading ios player API JSON"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadDestination_LogsCorrectly() + { + var parser = new ProgressParser(); + var output = "[download] Destination: downloads\\"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadingFormat_LogsCorrectly() { + var parser = new ProgressParser(); + var output = "[info] Gk0WHyRUcgM: Downloading 1 format(s): 248+251"; + parser.ParseProgress(output); + // Assert that OnProgressMessage was invoked with expected message + } + + [Fact] + public void DownloadingThumbnail_LogsCorrectly() + { + var logger = new TestLogger(); + var parser = new ProgressParser(logger); + string output = "[info] Downloading video thumbnail 41 ..."; + parser.ParseProgress(output); + Assert.Contains(logger.Logs, l => l.Message == "Downloading video thumbnail 41"); + } + + [Fact] + public void WritingThumbnail_LogsCorrectly() + { + var logger = new TestLogger(); + var parser = new ProgressParser(logger); + string output = "[info] Writing video thumbnail 41 to: downloads\\Kunukkitta Kozhi Jagadish.webp"; + parser.ParseProgress(output); + Assert.Contains(logger.Logs, l => l.Message == "Writing video thumbnail 41 to: downloads\\Kunukkitta Kozhi Jagadish.webp"); + } + + [Fact] + public void DeletingOriginalFile_LogsCorrectly() + { + // Arrange + var logger = new TestLogger(); + var parser = new ProgressParser(logger); + bool triggered = false; + parser.OnPostProcessingComplete += (s, e) => triggered = true; + + // Act + parser.ParseProgress("[Merger] Merging formats into \"C:\\Users\\manua\\Videos\\test.webm\""); + parser.ParseProgress("Deleting original file C:\\Users\\manua\\Videos\\test.f251.webm (pass -k to keep)"); + parser.ParseProgress("Deleting original file C:\\Users\\manua\\Videos\\test.f401.mp4 (pass -k to keep)"); + + // Assert + Assert.True(triggered, "OnPostProcessingComplete was not triggered after processing both deletion lines."); + Assert.Contains("OnPostProcessingComplete event triggered.", logger.GetMessages()); + } + + [Fact] + public void DeletingOriginalFile_Matches() { + var regex = new Regex(@"Deleting original file\s+(?.+?)\s+\(pass -k to keep\)", RegexOptions.IgnoreCase); + var match = regex.Match("Deleting original file C:\\Users\\manua\\Videos\\test.f251.webm (pass -k to keep)"); + Assert.True(match.Success); // Should pass + } + + [Fact] + public void Reset_ClearsDownloadCompletedFlag() + { + var logger = new TestLogger(); + var parser = new ProgressParser(logger); + parser.ParseProgress("[download] 100% of 29.53MiB at Unknown ETA Unknown"); + Assert.Contains(logger.Logs, l => l.Message.Contains("Download complete")); + parser.Reset(); + parser.ParseProgress("[download] 100% of 10MiB at Unknown ETA Unknown"); + Assert.Contains(logger.Logs, l => l.Message.Contains("Download complete: 100% of 10MiB")); + } + + private class TestLogger : ILogger + { + public List<(LogType Type, string Message)> Logs { get; } = new(); + //public void Log(LogType type, string message) => Logs.Add((type, message)); + + public void Log(LogType type, string message) + { + Logs.Add((type, message)); + } + + + // Helper to get messages as a list of strings for assertions + public IEnumerable GetMessages() + { + return Logs.Select(log => log.Message); + } } } From 93c7eb307dcb0c6268b079fc64ced2876a2ed96a Mon Sep 17 00:00:00 2001 From: manusoft Date: Sat, 21 Mar 2026 03:09:58 +0400 Subject: [PATCH 6/8] Update documentations. --- src/Ytdlp.NET.Console/Program.cs | 68 +++++++------------------------- src/Ytdlp.NET/README.md | 8 ++-- 2 files changed, 19 insertions(+), 57 deletions(-) diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs index 5807214..3ab243b 100644 --- a/src/Ytdlp.NET.Console/Program.cs +++ b/src/Ytdlp.NET.Console/Program.cs @@ -11,7 +11,7 @@ private static async Task Main(string[] args) Console.InputEncoding = Encoding.UTF8; Console.Clear(); - Console.WriteLine("yt-dlp .NET Wrapper v2.0 Demo Console App"); + Console.WriteLine("Ytdlp.NET Wrapper v3.0 Demo Console App"); Console.WriteLine("----------------------------------------"); // Initialize the wrapper (assuming yt-dlp is in PATH or specify path) @@ -20,20 +20,19 @@ private static async Task Main(string[] args) // Run all demos/tests sequentially //await TestGetVersionAsync(baseYtdlp); - //await TestUpdateVersionAsync(baseYtdlp); + //await TestUpdateAsync(baseYtdlp); //await TestGetFormatsAsync(baseYtdlp); - //await TestGetFormatsDetailedAsync(baseYtdlp); - //await TestGetMetadataAsync(baseYtdlp); + await TestGetMetadataAsync(baseYtdlp); //await TestGetLiteMetadataAsync(baseYtdlp); //await TestGetTitleAsync(baseYtdlp); await TestDownloadVideoAsync(baseYtdlp); - //await TestDownloadAudioAsync(ytdlp); + //await TestDownloadAudioAsync(baseYtdlp); //await TestBatchDownloadAsync(baseYtdlp); - //await TestSponsorBlockAsync(ytdlp); - //await TestConcurrentFragmentsAsync(ytdlp); - //await TestCancellationAsync(ytdlp); + //await TestSponsorBlockAsync(baseYtdlp); + //await TestConcurrentFragmentsAsync(baseYtdlp); + //await TestCancellationAsync(baseYtdlp); Console.WriteLine("\nAll tests completed. Press any key to exit..."); @@ -57,8 +56,6 @@ public void Log(LogType type, string message) } } - - // Test 1: Get yt-dlp version private static async Task TestGetVersionAsync(Ytdlp ytdlp) { Console.WriteLine("\nTest 1: Getting yt-dlp version..."); @@ -66,19 +63,18 @@ private static async Task TestGetVersionAsync(Ytdlp ytdlp) Console.WriteLine($"Version: {version}"); } - private static async Task TestUpdateVersionAsync(Ytdlp ytdlp) + private static async Task TestUpdateAsync(Ytdlp ytdlp) { - Console.WriteLine("\nTest 1: Getting yt-dlp version..."); + Console.WriteLine("\nTest 2: Checking yt-dlp update..."); var version = await ytdlp.UpdateAsync(UpdateChannel.Stable); - Console.WriteLine($"Version: {version}"); + Console.WriteLine($"Status: {version}"); } - // Test 2: Get detailed formats private static async Task TestGetFormatsAsync(Ytdlp ytdlp) { var stopwatch = Stopwatch.StartNew(); - Console.WriteLine("\nTest 2: Fetching available formats..."); + Console.WriteLine("\nTest 3: Fetching available formats..."); var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; var formats = await ytdlp.GetFormatsAsync(url); @@ -102,36 +98,6 @@ private static async Task TestGetFormatsAsync(Ytdlp ytdlp) Console.WriteLine($"\nBest 1080p: ID {best1080p.Id}, {best1080p.VideoCodec}, ~{best1080p.ApproxFileSizeBytes / 1024 / 1024} MiB"); } - // Test 3: Get detailed formats - private static async Task TestGetFormatsDetailedAsync(Ytdlp ytdlp) - { - var stopwatch = Stopwatch.StartNew(); - - Console.WriteLine("\nTest 3: Fetching detailed formats..."); - var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; - var formats = await ytdlp.GetFormatsAsync(url); - - stopwatch.Stop(); // stop timer - Console.WriteLine($"Detailed formats took {stopwatch.Elapsed.TotalSeconds:F3} seconds"); - - Console.WriteLine($"Found {formats.Count} formats:"); - - foreach (var f in formats) - { - Console.WriteLine(f.ToString()); // Uses Format's ToString override - } - - // Example: Find best 1080p - var best1080p = formats - .Where(f => f.IsVideo && (f.Height ?? 0) == 1080) - .OrderByDescending(f => f.Fps ?? 0) - .FirstOrDefault(); - - if (best1080p != null) - Console.WriteLine($"\nBest 1080p: ID {best1080p.Id}, {best1080p.VideoCodec}, ~{best1080p.ApproxFileSizeBytes / 1024 / 1024} MiB"); - } - - // Test 4: Get metedata private static async Task TestGetMetadataAsync(Ytdlp ytdlp) { var stopwatch = Stopwatch.StartNew(); @@ -140,8 +106,7 @@ private static async Task TestGetMetadataAsync(Ytdlp ytdlp) 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 url3 = "https://www.youtube.com/watch?v=983bBbJx0Mk&list=RD983bBbJx0Mk&start_radio=1&pp=ygUFc29uZ3OgBwE%3D"; - var metadata = await ytdlp.GetMetadataAsync(url3); + var metadata = await ytdlp.GetMetadataAsync(url2); stopwatch.Stop(); // stop timer Console.WriteLine($"Detailed metedata took {stopwatch.Elapsed.TotalSeconds:F3} seconds"); @@ -167,11 +132,10 @@ private static async Task TestGetMetadataAsync(Ytdlp ytdlp) } } - // Test 5: Get lite metedata private static async Task TestGetLiteMetadataAsync(Ytdlp ytdlp) { var stopwatch = Stopwatch.StartNew(); - Console.WriteLine("\nTest 5: Fetching simple metedata..."); + Console.WriteLine("\nTest 5: Fetching lite metedata..."); var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98"; @@ -218,7 +182,6 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase) // await ytdlp.ExecuteAsync(url); } - // Test 7 Extract audio only private static async Task TestDownloadAudioAsync(Ytdlp ytdlp) { Console.WriteLine("\nTest 7: Extracting audio..."); @@ -257,7 +220,7 @@ private static async Task TestSponsorBlockAsync(Ytdlp ytdlp) await ytdlp .WithFormat("best") - //.WithRemoveSponsorBlock("all") // Removes sponsor, intro, etc. + .WithSponsorblockRemove("all") // Removes sponsor, intro, etc. .WithOutputFolder("./downloads/sponsorblock") .ExecuteAsync(url); } @@ -312,8 +275,7 @@ private static async Task TestGetTitleAsync(Ytdlp ytdlp) try { var downloadTask = ytdlp - //.Simulate() - //.NoWarning() + .WithSimulate() .WithOutputTemplate("%(title)s.%(ext)s") .WithOutputFolder("./downloads/cancel") .AddFlag("--get-title") diff --git a/src/Ytdlp.NET/README.md b/src/Ytdlp.NET/README.md index 3db8e7b..c0de207 100644 --- a/src/Ytdlp.NET/README.md +++ b/src/Ytdlp.NET/README.md @@ -304,13 +304,13 @@ ytdlp.OnCommandCompleted += (s, e) => Console.WriteLine($"Command finished: {e.C * Immutable: no shared state; safe for parallel usage. * Always `await using` for proper disposal. * Deprecated old methods removed. -* Probe methods remain the same (`GetMetadataAsync`, `GetAvailableFormatsAsync`, `GetBestVideoFormatIdAsync`, etc.). +* Probe methods remain the same (`GetMetadataAsync`, `GetFormatsAsync`, `GetBestVideoFormatIdAsync`, etc.). --- ### License -MIT License — see [LICENSE](https://github.com/manusoft/yt-dlp-wrapper/blob/master/LICENSE.md) +MIT License — see [LICENSE](https://github.com/manusoft/Ytdlp.NET/blob/master/LICENSE.md) -**Author:** Manojbabu (ManuHub) -**Repository:** [Ytdlp.NET](https://github.com/manusoft/yt-dlp-wrapper) +**Author:** Manojbabu (ManuHub) +**Repository:** [Ytdlp.NET](https://github.com/manusoft/Ytdlp.NET) From e521dcf2313e2a6299204ecd276891a89abe5613 Mon Sep 17 00:00:00 2001 From: manusoft Date: Sat, 21 Mar 2026 03:14:26 +0400 Subject: [PATCH 7/8] Update project structures. --- Ytdlp.NET.slnx | 6 +++--- apps/ClipMate.Lite/ClipMate.Lite.csproj | 4 ---- {src => feature}/Ytdlp.NET.vNext.Console/Program.cs | 0 .../Ytdlp.NET.vNext.Console.csproj | 4 ---- .../Ytdlp.NET.vNext/Core/ProgressParser.cs | 0 .../Ytdlp.NET.vNext/Core/RegexPatterns.cs | 0 .../Ytdlp.NET.vNext/Core/UpdateChannel.cs | 0 {src => feature}/Ytdlp.NET.vNext/DefaultLogger.cs | 0 .../Ytdlp.NET.vNext/DownloadProgressEventArgs.cs | 0 .../Ytdlp.NET.vNext/Helpers/FormatFilters.cs | 0 {src => feature}/Ytdlp.NET.vNext/ILogger.cs | 0 {src => feature}/Ytdlp.NET.vNext/LICENSE.txt | 0 {src => feature}/Ytdlp.NET.vNext/LogType.cs | 0 {src => feature}/Ytdlp.NET.vNext/Models/Format.cs | 0 {src => feature}/Ytdlp.NET.vNext/Models/Metadata.cs | 0 .../Ytdlp.NET.vNext/Models/MetadataLite.cs | 0 {src => feature}/Ytdlp.NET.vNext/README.md | 0 .../Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj | 0 {src => feature}/Ytdlp.NET.vNext/Ytdlp.cs | 0 {src => feature}/Ytdlp.NET.vNext/YtdlpBuilder.cs | 0 {src => feature}/Ytdlp.NET.vNext/YtdlpCommand.cs | 0 {src => feature}/Ytdlp.NET.vNext/YtdlpException.cs | 0 {src => feature}/Ytdlp.NET.vNext/YtdlpGeneral.cs | 0 {src => feature}/Ytdlp.NET.vNext/YtdlpProbe.cs | 0 .../Ytdlp.NET.vNext/YtdlpRootBuilder.cs | 0 {src => feature}/Ytdlp.NET.vNext/icon.png | Bin 26 files changed, 3 insertions(+), 11 deletions(-) rename {src => feature}/Ytdlp.NET.vNext.Console/Program.cs (100%) rename {src => feature}/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj (83%) rename {src => feature}/Ytdlp.NET.vNext/Core/ProgressParser.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/Core/RegexPatterns.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/Core/UpdateChannel.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/DefaultLogger.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/Helpers/FormatFilters.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/ILogger.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/LICENSE.txt (100%) rename {src => feature}/Ytdlp.NET.vNext/LogType.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/Models/Format.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/Models/Metadata.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/Models/MetadataLite.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/README.md (100%) rename {src => feature}/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj (100%) rename {src => feature}/Ytdlp.NET.vNext/Ytdlp.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/YtdlpBuilder.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/YtdlpCommand.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/YtdlpException.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/YtdlpGeneral.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/YtdlpProbe.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/YtdlpRootBuilder.cs (100%) rename {src => feature}/Ytdlp.NET.vNext/icon.png (100%) diff --git a/Ytdlp.NET.slnx b/Ytdlp.NET.slnx index 44e3e25..fac6d13 100644 --- a/Ytdlp.NET.slnx +++ b/Ytdlp.NET.slnx @@ -6,9 +6,9 @@ - - - + + + diff --git a/apps/ClipMate.Lite/ClipMate.Lite.csproj b/apps/ClipMate.Lite/ClipMate.Lite.csproj index 44491cc..fd27ece 100644 --- a/apps/ClipMate.Lite/ClipMate.Lite.csproj +++ b/apps/ClipMate.Lite/ClipMate.Lite.csproj @@ -25,10 +25,6 @@ - - - - True diff --git a/src/Ytdlp.NET.vNext.Console/Program.cs b/feature/Ytdlp.NET.vNext.Console/Program.cs similarity index 100% rename from src/Ytdlp.NET.vNext.Console/Program.cs rename to feature/Ytdlp.NET.vNext.Console/Program.cs diff --git a/src/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj b/feature/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj similarity index 83% rename from src/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj rename to feature/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj index 3f67c73..56b2004 100644 --- a/src/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj +++ b/feature/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj @@ -14,8 +14,4 @@ - - - - diff --git a/src/Ytdlp.NET.vNext/Core/ProgressParser.cs b/feature/Ytdlp.NET.vNext/Core/ProgressParser.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Core/ProgressParser.cs rename to feature/Ytdlp.NET.vNext/Core/ProgressParser.cs diff --git a/src/Ytdlp.NET.vNext/Core/RegexPatterns.cs b/feature/Ytdlp.NET.vNext/Core/RegexPatterns.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Core/RegexPatterns.cs rename to feature/Ytdlp.NET.vNext/Core/RegexPatterns.cs diff --git a/src/Ytdlp.NET.vNext/Core/UpdateChannel.cs b/feature/Ytdlp.NET.vNext/Core/UpdateChannel.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Core/UpdateChannel.cs rename to feature/Ytdlp.NET.vNext/Core/UpdateChannel.cs diff --git a/src/Ytdlp.NET.vNext/DefaultLogger.cs b/feature/Ytdlp.NET.vNext/DefaultLogger.cs similarity index 100% rename from src/Ytdlp.NET.vNext/DefaultLogger.cs rename to feature/Ytdlp.NET.vNext/DefaultLogger.cs diff --git a/src/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs b/feature/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs similarity index 100% rename from src/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs rename to feature/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs diff --git a/src/Ytdlp.NET.vNext/Helpers/FormatFilters.cs b/feature/Ytdlp.NET.vNext/Helpers/FormatFilters.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Helpers/FormatFilters.cs rename to feature/Ytdlp.NET.vNext/Helpers/FormatFilters.cs diff --git a/src/Ytdlp.NET.vNext/ILogger.cs b/feature/Ytdlp.NET.vNext/ILogger.cs similarity index 100% rename from src/Ytdlp.NET.vNext/ILogger.cs rename to feature/Ytdlp.NET.vNext/ILogger.cs diff --git a/src/Ytdlp.NET.vNext/LICENSE.txt b/feature/Ytdlp.NET.vNext/LICENSE.txt similarity index 100% rename from src/Ytdlp.NET.vNext/LICENSE.txt rename to feature/Ytdlp.NET.vNext/LICENSE.txt diff --git a/src/Ytdlp.NET.vNext/LogType.cs b/feature/Ytdlp.NET.vNext/LogType.cs similarity index 100% rename from src/Ytdlp.NET.vNext/LogType.cs rename to feature/Ytdlp.NET.vNext/LogType.cs diff --git a/src/Ytdlp.NET.vNext/Models/Format.cs b/feature/Ytdlp.NET.vNext/Models/Format.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Models/Format.cs rename to feature/Ytdlp.NET.vNext/Models/Format.cs diff --git a/src/Ytdlp.NET.vNext/Models/Metadata.cs b/feature/Ytdlp.NET.vNext/Models/Metadata.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Models/Metadata.cs rename to feature/Ytdlp.NET.vNext/Models/Metadata.cs diff --git a/src/Ytdlp.NET.vNext/Models/MetadataLite.cs b/feature/Ytdlp.NET.vNext/Models/MetadataLite.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Models/MetadataLite.cs rename to feature/Ytdlp.NET.vNext/Models/MetadataLite.cs diff --git a/src/Ytdlp.NET.vNext/README.md b/feature/Ytdlp.NET.vNext/README.md similarity index 100% rename from src/Ytdlp.NET.vNext/README.md rename to feature/Ytdlp.NET.vNext/README.md diff --git a/src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj b/feature/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj similarity index 100% rename from src/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj rename to feature/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj diff --git a/src/Ytdlp.NET.vNext/Ytdlp.cs b/feature/Ytdlp.NET.vNext/Ytdlp.cs similarity index 100% rename from src/Ytdlp.NET.vNext/Ytdlp.cs rename to feature/Ytdlp.NET.vNext/Ytdlp.cs diff --git a/src/Ytdlp.NET.vNext/YtdlpBuilder.cs b/feature/Ytdlp.NET.vNext/YtdlpBuilder.cs similarity index 100% rename from src/Ytdlp.NET.vNext/YtdlpBuilder.cs rename to feature/Ytdlp.NET.vNext/YtdlpBuilder.cs diff --git a/src/Ytdlp.NET.vNext/YtdlpCommand.cs b/feature/Ytdlp.NET.vNext/YtdlpCommand.cs similarity index 100% rename from src/Ytdlp.NET.vNext/YtdlpCommand.cs rename to feature/Ytdlp.NET.vNext/YtdlpCommand.cs diff --git a/src/Ytdlp.NET.vNext/YtdlpException.cs b/feature/Ytdlp.NET.vNext/YtdlpException.cs similarity index 100% rename from src/Ytdlp.NET.vNext/YtdlpException.cs rename to feature/Ytdlp.NET.vNext/YtdlpException.cs diff --git a/src/Ytdlp.NET.vNext/YtdlpGeneral.cs b/feature/Ytdlp.NET.vNext/YtdlpGeneral.cs similarity index 100% rename from src/Ytdlp.NET.vNext/YtdlpGeneral.cs rename to feature/Ytdlp.NET.vNext/YtdlpGeneral.cs diff --git a/src/Ytdlp.NET.vNext/YtdlpProbe.cs b/feature/Ytdlp.NET.vNext/YtdlpProbe.cs similarity index 100% rename from src/Ytdlp.NET.vNext/YtdlpProbe.cs rename to feature/Ytdlp.NET.vNext/YtdlpProbe.cs diff --git a/src/Ytdlp.NET.vNext/YtdlpRootBuilder.cs b/feature/Ytdlp.NET.vNext/YtdlpRootBuilder.cs similarity index 100% rename from src/Ytdlp.NET.vNext/YtdlpRootBuilder.cs rename to feature/Ytdlp.NET.vNext/YtdlpRootBuilder.cs diff --git a/src/Ytdlp.NET.vNext/icon.png b/feature/Ytdlp.NET.vNext/icon.png similarity index 100% rename from src/Ytdlp.NET.vNext/icon.png rename to feature/Ytdlp.NET.vNext/icon.png From db97533ca2350591e23ca9ee681defc5ebd8fe5b Mon Sep 17 00:00:00 2001 From: manusoft Date: Sun, 22 Mar 2026 15:14:06 +0400 Subject: [PATCH 8/8] Update fluent methods, and new functions added. --- README.md | 24 ++- src/Ytdlp.NET.Console/Program.cs | 35 ++-- src/Ytdlp.NET/Core/ProbeRunner.cs | 1 + src/Ytdlp.NET/README.md | 230 +++++++++++++++++++++--- src/Ytdlp.NET/Ytdlp.NET.csproj | 2 +- src/Ytdlp.NET/Ytdlp.cs | 279 ++++++++++++++++++++---------- 6 files changed, 426 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index dec51b9..353f922 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ![Screenshot 2025-01-23 153252](https://github.com/user-attachments/assets/1b977927-ea26-4220-bd41-9f64d6716058) -[Download the latest App](https://github.com/manusoft/yt-dlp-wrapper/releases/download/v1.0.0/gui-app.zip) +[Download the latest App](https://github.com/manusoft/Ytdlp.NET/releases/download/v1.0.0/gui-app.zip) --- @@ -94,7 +94,7 @@ await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"{e.Percent:F1}% {e.Speed} ETA {e.ETA}"); -await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=VIDEO_ID"); +await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=VIDEO_ID"); ``` --- @@ -106,7 +106,7 @@ await using var ytdlp = new Ytdlp() .WithExtractAudio("mp3") .WithOutputFolder("./audio"); -await ytdlp.ExecuteAsync(url); +await ytdlp.DownloadAsync(url); ``` --- @@ -150,7 +150,7 @@ string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); await ytdlp .WithFormat($"{bestVideo}+{bestAudio}/best") - .ExecuteAsync(url); + .DownloadAsync(url); ``` --- @@ -170,12 +170,24 @@ var tasks = urls.Select(async url => .WithFormat("best") .WithOutputFolder("./batch"); - await ytdlp.ExecuteAsync(url); + await ytdlp.DownloadAsync(url); }); await Task.WhenAll(tasks); ``` +**OR** + +```csharp +var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" }; + + await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") + .WithFormat("best") + .WithOutputFolder("./batch"); + +await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3); +``` + --- # 📡 Events @@ -278,7 +290,7 @@ await using var ytdlp = new Ytdlp() .WithFormat("best") .WithOutputFolder("./downloads"); -await ytdlp.ExecuteAsync(url); +await ytdlp.DownloadAsync(url); ``` --- diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs index 3ab243b..c2f7f92 100644 --- a/src/Ytdlp.NET.Console/Program.cs +++ b/src/Ytdlp.NET.Console/Program.cs @@ -19,21 +19,22 @@ 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 TestGetFormatsAsync(baseYtdlp); await TestGetMetadataAsync(baseYtdlp); - //await TestGetLiteMetadataAsync(baseYtdlp); - //await TestGetTitleAsync(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(); Console.WriteLine("\nAll tests completed. Press any key to exit..."); Console.ReadKey(); @@ -106,7 +107,7 @@ private static async Task TestGetMetadataAsync(Ytdlp ytdlp) 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(url2); + var metadata = await ytdlp.GetMetadataAsync(url1); stopwatch.Stop(); // stop timer Console.WriteLine($"Detailed metedata took {stopwatch.Elapsed.TotalSeconds:F3} seconds"); @@ -191,7 +192,7 @@ await ytdlp .WithExtractAudio(AudioFormat.Mp3) .WithFormat("ba") .WithOutputFolder("./downloads/audio") - .ExecuteAsync(url); + .DownloadAsync(url); } // Test 8: Batch download (concurrent) @@ -209,7 +210,7 @@ private static async Task TestBatchDownloadAsync(Ytdlp baseYtdlp) .WithFormat("best[height<=480]") // Lower quality for speed .WithOutputFolder("./downloads/batch"); - await ytdlp.ExecuteBatchAsync(urls, maxConcurrency: 3); + await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3); } // Test 9: SponsorBlock removal @@ -222,7 +223,7 @@ await ytdlp .WithFormat("best") .WithSponsorblockRemove("all") // Removes sponsor, intro, etc. .WithOutputFolder("./downloads/sponsorblock") - .ExecuteAsync(url); + .DownloadAsync(url); } // Test 10: Concurrent fragments (faster download) @@ -236,7 +237,7 @@ await ytdlp .WithFormat("b") .WithOutputTemplate("%(title)s.%(ext)s") .WithOutputFolder("./downloads/concurrent") - .ExecuteAsync(url); + .DownloadAsync(url); } // Test 11: Cancellation support @@ -250,7 +251,7 @@ private static async Task TestCancellationAsync(Ytdlp ytdlp) .WithFormat("b") .WithOutputTemplate("%(title)s.%(ext)s") .WithOutputFolder("./downloads/cancel") - .ExecuteAsync(url, cts.Token); + .DownloadAsync(url, cts.Token); // Simulate cancel after 20 seconds await Task.Delay(20000); @@ -279,7 +280,7 @@ private static async Task TestGetTitleAsync(Ytdlp ytdlp) .WithOutputTemplate("%(title)s.%(ext)s") .WithOutputFolder("./downloads/cancel") .AddFlag("--get-title") - .ExecuteAsync(url); + .DownloadAsync(url); await downloadTask; } diff --git a/src/Ytdlp.NET/Core/ProbeRunner.cs b/src/Ytdlp.NET/Core/ProbeRunner.cs index 38a3366..69b3338 100644 --- a/src/Ytdlp.NET/Core/ProbeRunner.cs +++ b/src/Ytdlp.NET/Core/ProbeRunner.cs @@ -67,3 +67,4 @@ public ProbeRunner(ProcessFactory factory, ILogger logger) } } } + diff --git a/src/Ytdlp.NET/README.md b/src/Ytdlp.NET/README.md index c0de207..f5124e3 100644 --- a/src/Ytdlp.NET/README.md +++ b/src/Ytdlp.NET/README.md @@ -66,6 +66,7 @@ var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "tools"); ## 🛠 Methods * `VersionAsync(CancellationToken ct)` * `UpdateAsync(UpdateChannel channel, CancellationToken ct)` +* `ExtractorsAsync(CancellationToken ct, int bufferKb)` * `GetMetadataAsync(string url, CancellationToken ct, int bufferKb)` * `GetMetadataRawAsync(string url, CancellationToken ct, int bufferKb)` * `GetFormatsAsync(string url, CancellationToken ct, int bufferKb)` @@ -93,7 +94,7 @@ await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"Download complete: {msg}"); -await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); +await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); ``` ### **Parallel download example**: @@ -110,7 +111,7 @@ var tasks = urls.Select(async url => ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"[{url}] {e.Percent:F2}%"); ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"[{url}] Download complete: {msg}"); - await ytdlp.ExecuteAsync(url); + await ytdlp.DownloadAsync(url); }); await Task.WhenAll(tasks); @@ -138,7 +139,7 @@ await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"Download complete: {msg}"); -await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); +await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); ``` ### Extract Audio @@ -149,7 +150,7 @@ await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") .WithOutputFolder("./audio") .WithEmbedMetadata(); -await ytdlp.ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); +await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); ``` --- @@ -190,7 +191,7 @@ string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 720); await ytdlp .WithFormat($"{bestVideo}+{bestAudio}/best") .WithOutputFolder("./downloads") - .ExecuteAsync(url); + .DownloadAsync(url); ``` --- @@ -206,36 +207,86 @@ var tasks = urls.Select(async url => .WithFormat("best") .WithOutputFolder("./batch"); - await ytdlp.ExecuteAsync(url); + await ytdlp.DownloadAsync(url); }); await Task.WhenAll(tasks); ``` +**OR** + +```csharp +var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" }; + + await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") + .WithFormat("best") + .WithOutputFolder("./batch"); + +await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3); +``` + --- -### Fluent Methods (v3.0) +## Fluent Methods (v3.0) -#### General Options +### General Options +* `.WithIgnoreErrors()` +* `.WithAbortOnError()` +* `.WithIgnoreConfig()` +* `.WithConfigLocations(string path)` +* `.WithPluginDirs(string path)` +* `.WithNoPluginDirs(string path)` * `.WithJsRuntime(Runtime runtime, string runtimePath)` * `.WithNoJsRuntime()` * `.WithFlatPlaylist()` * ` WithLiveFromStart()` * `.WithWaitForVideo(TimeSpan? maxWait = null)` -* `.WithMarkWatched()` +* `.WithMarkWatched()` -#### Network Options +### Network Options * `.WithProxy(string? proxy)` * `.WithSocketTimeout(TimeSpan timeout)` * `.WithForceIpv4()` * `.WithForceIpv6()` -* `.WithEnableFileUrl()` +* `.WithEnableFileUrls()` -#### Geo-restriction Options +### Geo-restriction Options * `.WithGeoVerificationProxy(string url)` * `.WithGeoBypassCountry(string countryCode)` -#### Filesystem Options +### Video Selection +* `.WithPlaylistItems(string items)` +* `.WithMinFileSize(string size)` +* `.WithMaxFileSize(string size)` +* `.WithDate(string date)` +* `.WithDateBefore(string date)` +* `.WithDateAfter(string date)` +* `.WithMatchFilter(string filterExpression)` +* `.WithNoPlaylist()` +* `.WithYesPlaylist()` +* `.WithAgeLimit(int years)` +* `.WithDownloadArchive(string archivePath = "archive.txt")` +* `.WithMaxDownloads(int count)` +* `.WithBreakOnExisting()` + +### Download Options +* `.WithConcurrentFragments(int count = 8)` +* `.WithLimitRate(string rate)` +* `.WithThrottledRate(string rate)` +* `.WithRetries(int maxRetries)` +* `.WithFileAccessRetries(int maxRetries)` +* `.WithFragmentRetries(int retries)` +* `.WithSkipUnavailableFragments()` +* `.WithAbortOnUnavailableFragments()` +* `.WithKeepFragments()` +* `.WithBufferSize(string size)` +* `.WithNoResizeBuffer()` +* `.WithPlaylistRandom()` +* `.WithHlsUseMpegts()` +* `.WithNoHlsUseMpegts()` +* `.WithDownloadSections(string regex)` + +### Filesystem Options * `.WithHomeFolder(string path)` * `.WithTempFolder(string path)` * `.WithOutputFolder(string path)` @@ -261,27 +312,70 @@ await Task.WhenAll(tasks); * `.WithNoCacheDir()` * `.WithRemoveCacheDir()` -#### Format & Extraction +### Thumbnail Options +* `.WithThumbnails(bool allSizes = false)` + +### Verbosity and Simulation Options +* `.WithQuiet()` +* `.WithNoWarnings()` +* `.WithSimulate()` +* `.WithNoSimulate()` +* `.WithSkipDownload()` +* `.WithVerbose()` + +### Workgrounds +* `.WithAddHeader(string header, string value)` +* `.WithSleepInterval(double seconds, double? maxSeconds = null)` +* `.WithSleepSubtitles(double seconds)` + +### Video Format Options * `.WithFormat(string format)` +* `.WithMergeOutputFormat(string format)` + +### Subtitle Options +* `.WithSubtitles(string languages = "all", bool auto = false)` + +### Authentication Options +* `.WithAuthentication(string username, string password)` +* `.WithTwoFactor(string code)` + +### Post-Processing Options * `.WithExtractAudio(string format = "mp3", int quality = 5)` -* `.With720pOrBest()` -* `.WithEmbedMetadata()` +* `.WithRemuxVideo(MediaFormat format = MediaFormat.Mp4)` +* `.WithRecodeVideo(MediaFormat format = MediaFormat.Mp4, string? videoCodec = null, string? audioCodec = null)` +* `.WithPostprocessorArgs(PostProcessors postprocessor, string args)` +* `.WithKeepVideo()` +* `.WithNoPostOverwrites()` +* `.WithEmbedSubtitles(string languages = "all", string? convertTo = null)` * `.WithEmbedThumbnail()` +* `.WithEmbedMetadata()` * `.WithEmbedChapters()` - -#### Subtitles & Thumbnails -* `.WithSubtitles(string langs = "all", bool auto = false)` -* `.WithEmbedSubtitles(string langs = "all", string? convertTo = null)` -* `.WithThumbnails(bool all = false)` - -#### Download Control -* `.WithConcurrentFragments(int count)` +* `.WithEmbedInfoJson()` +* `.WithNoEmbedInfoJson()` +* `.WithReplaceInMetadata(string field, string regex, string replacement)` +* `.WithConcatPlaylist(string policy = "always")` +* `.WithFFmpegLocation(string? ffmpegPath)` +* `.WithConvertThumbnails(string format = "jpg")` +* `.WithForceKeyframesAtCuts()` + +### SponsorBlock Options +* `.WithSponsorblockMark(string categories = "all")` * `.WithSponsorblockRemove(string categories = "all")` +* `.WithNoSponsorblock()` -#### Advanced Options +### Advanced Options * `.AddFlag(string flag)` * `.AddOption(string key, string value)` + +### Downloaders +* `.WithExternalDownloader(string downloaderName, string? downloaderArgs = null)` +* `.WithAria2(int connections = 16)` +* `.WithHlsNative()` +* `.WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null)` + +AND MORE ... + --- ### Events @@ -298,6 +392,92 @@ ytdlp.OnCommandCompleted += (s, e) => Console.WriteLine($"Command finished: {e.C --- +# 🔄 Upgrade Guide (v2 → v3) + +v3 introduces a **new immutable fluent API**. + +Old mutable commands were removed. + +--- + +## ❌ Old API (v2) + +```csharp +var ytdlp = new Ytdlp(); + +await ytdlp + .SetFormat("best") + .SetOutputFolder("./downloads") + .ExecuteAsync(url); +``` + +--- + +## ✅ New API (v3) + +```csharp +await using var ytdlp = new Ytdlp() + .WithFormat("best") + .WithOutputFolder("./downloads"); + +await ytdlp.DownloadAsync(url); +``` + +--- + +## Method changes + +| v2 | v3 | +| --------------------- | ---------------------- | +| `SetFormat()` | `WithFormat()` | +| `SetOutputFolder()` | `WithOutputFolder()` | +| `SetTempFolder()` | `WithTempFolder()` | +| `SetOutputTemplate()` | `WithOutputTemplate()` | +| `SetFFMpegLocation()` | `WithFFmpegLocation()` | +| `ExtractAudio()` | `WithExtractAudio()` | +| `UseProxy()` | `WithProxy()` | + +--- + +## Important behavior changes + +### Instances are immutable + +Every `WithXxx()` call returns a **new instance**. + +```csharp +var baseYtdlp = new Ytdlp(); + +var download = baseYtdlp + .WithFormat("best") + .WithOutputFolder("./downloads"); +``` + +--- + +### Event subscription + +Attach events **to the configured instance**. + +```csharp +var download = baseYtdlp.WithFormat("best"); + +download.OnProgressDownload += ... +``` + +--- + +### Proper disposal + +Use **`await using`** for automatic cleanup. + +```csharp +await using var ytdlp = new Ytdlp(); +``` + +--- + + ### ✅ Notes * All commands now start with `WithXxx()`. diff --git a/src/Ytdlp.NET/Ytdlp.NET.csproj b/src/Ytdlp.NET/Ytdlp.NET.csproj index 581d349..b4b161d 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-preview-1 + 3.0.0 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 69454c1..e194998 100644 --- a/src/Ytdlp.NET/Ytdlp.cs +++ b/src/Ytdlp.NET/Ytdlp.cs @@ -13,7 +13,7 @@ namespace ManuHub.Ytdlp.NET; /// /// /// THREAD-SAFE: Multiple threads can safely use the same instance concurrently. -/// Each call to creates isolated runners and parsers, preventing race conditions +/// Each call to creates isolated runners and parsers, preventing race conditions /// and shared state issues. /// /// Example of safe concurrent usage: @@ -35,7 +35,7 @@ namespace ManuHub.Ytdlp.NET; /// immutability and thread-safety. /// /// Resource cleanup: Internal runners and parsers are disposed automatically after each -/// call. For advanced scenarios, future versions may implement +/// call. For advanced scenarios, future versions may implement /// for global disposal of resources and cancellation support. /// public sealed class Ytdlp : IAsyncDisposable @@ -152,22 +152,71 @@ private Ytdlp(Ytdlp other, #region General Options + /// + /// Ignore download and postprocessing errors. The download will be considered successful even if the postprocessing fails + /// + /// + public Ytdlp WithIgnoreErrors() => AddFlag("--ignore-errors"); + + /// + /// IgAbort downloading of further videos if an error occurs + /// + /// + public Ytdlp WithAbortOnError() => AddFlag("--abort-on-error"); + + /// + /// Don't load any more configuration files except those given to . + /// For backward compatibility, if this option is found inside the system configuration file, the user configuration is not loaded. + /// + /// + public Ytdlp WithIgnoreConfig() => AddFlag("--ignore-config"); + + /// + /// Location of the main configuration file;either the path to the config or its containing directory ("-" for stdin). + /// Can be used multiple times and inside other configuration files. + /// + /// + /// + public Ytdlp WithConfigLocations(string path) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Config folder path required"); + return AddOption("--config-locations", Path.GetFullPath(path)); + } + + /// + /// Path to an additional directory to search for plugins. This option can be used multiple times to add multiple directories. + /// + /// + /// + public Ytdlp WithPluginDirs(string path) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("plugin folder path required"); + return AddOption("--plugin-dirs", path); + } + + /// + /// Clear plugin directories to search, including defaults and those provided by previous + /// + /// + /// + public Ytdlp WithNoPluginDirs(string path) => AddFlag("--no-plugin-dirs"); + /// /// Additional JavaScript runtime to enable, with an optional location for the runtime (either the path to the binary or its containing directory). /// This option can be used multiple times to enable multiple runtimes. Supported runtimes are (in order of priority, from highest to lowest): deno, node, quickjs, bun. /// Only "deno" is enabled by default. The highest priority runtime that is both enabled and available will be used. - /// In order to use a lower priority runtime when "deno" is available, NoJsRuntime() needs to be passed before enabling other runtimes + /// In order to use a lower priority runtime when "deno" is available, needs to be passed before enabling other runtimes /// /// Supported runtimes are deno, node, quickjs, bun /// - public Ytdlp WithJsRuntime(Runtime runtime, string runtimePath) + public Ytdlp WithJsRuntime(Runtime runtime, string path) { - var builder = $"{runtime}:{runtimePath}"; + var builder = $"{runtime}:{path}"; return AddOption("--js-runtime", builder); } /// - /// Clear JavaScript runtimes to enable, including defaults and those provided by WithJsRuntime() + /// Clear JavaScript runtimes to enable, including defaults and those provided by /// public Ytdlp WithNoJsRuntime() => AddFlag("--no-js-runtime"); @@ -239,7 +288,7 @@ public Ytdlp WithSocketTimeout(TimeSpan timeout) /// /// Enable file:// URLs. This is disabled by default for security reasons. /// - public Ytdlp WithEnableFileUrl() => AddFlag("--enable-file-url"); + public Ytdlp WithEnableFileUrls() => AddFlag("--enable-file-urls"); #endregion @@ -247,7 +296,7 @@ public Ytdlp WithSocketTimeout(TimeSpan timeout) /// /// Use this proxy to verify the IP address for some geo-restricted sites. - /// The default proxy specified by WithProxy() (or none, if the option is not present) is used for the actual downloading + /// The default proxy specified by (or none, if the option is not present) is used for the actual downloading /// /// /// @@ -322,6 +371,30 @@ public Ytdlp WithDate(string date) return AddOption("--date", date.Trim()); } + /// + /// Download only videos uploaded on or before this date. The date formats accepted are the same as + /// + /// "today-2weeks" or "YYYYMMDD" + public Ytdlp WithDateBefore(string date) + { + // formats: YYYYMMDD, today, yesterday, now-2weeks, etc. + if (string.IsNullOrWhiteSpace(date)) + throw new ArgumentException("Date cannot be empty", nameof(date)); + return AddOption("--datebefore", date.Trim()); + } + + /// + /// Download only videos uploaded on or after this date. The date formats accepted are the same as + /// + /// "today-2weeks" or "YYYYMMDD" + public Ytdlp WithDateAfter(string date) + { + // formats: YYYYMMDD, today, yesterday, now-2weeks, etc. + if (string.IsNullOrWhiteSpace(date)) + throw new ArgumentException("Date cannot be empty", nameof(date)); + return AddOption("--dateafter", date.Trim()); + } + /// /// Generic video filter. Any "OUTPUT TEMPLATE" field can be compared with a number or a string using the operators defined in "Filtering Formats". /// @@ -382,7 +455,7 @@ public Ytdlp WithMaxDownloads(int count) } /// - /// Stop the download process when encountering a file that is in the archive supplied with the option + /// Stop the download process when encountering a file that is in the archive supplied with the option /// /// public Ytdlp WithBreakOnExisting() => AddFlag("--break-on-existing"); @@ -432,6 +505,18 @@ public Ytdlp WithFragmentRetries(int retries) return AddOption("--fragment-retries", value); } + /// + /// Skip unavailable fragments for DASH, hlsnative and ISM downloads (default) + /// + /// + public Ytdlp WithSkipUnavailableFragments() => AddFlag("--skip-unavailable-fragments"); + + /// + /// Abort download if a fragment is unavailable + /// + /// + public Ytdlp WithAbortOnUnavailableFragments() => AddFlag("--abort-on-unavailable-fragments"); + /// /// Keep downloaded fragments on disk after downloading is finished /// @@ -444,14 +529,15 @@ public Ytdlp WithFragmentRetries(int retries) public Ytdlp WithBufferSize(string size) => AddOption("--buffer-size", size); /// - /// Download playlist videos in random order + /// Do not automatically adjust the buffer size /// - public Ytdlp WithPlaylistRandom() => AddFlag("--playlist-random"); + /// + public Ytdlp WithNoResizeBuffer() => AddFlag("--no-resize-buffer"); /// - /// Process entries in the playlist as they are received. This disables n_entries, PlaylistRandom() and --playlist-reverse + /// Download playlist videos in random order /// - public Ytdlp WithLazyPlaylist() => AddFlag("--lazy-playlist"); + public Ytdlp WithPlaylistRandom() => AddFlag("--playlist-random"); /// /// Use the mpegts container for HLS videos; allowing some players to play the video while downloading, @@ -460,6 +546,13 @@ public Ytdlp WithFragmentRetries(int retries) /// public Ytdlp WithHlsUseMpegts() => AddFlag("--hls-use-mpegts"); + /// + /// Do not use the mpegts container for HLS videos. This is default when not downloading live streams + /// + /// + public Ytdlp WithNoHlsUseMpegts() => AddFlag("--no-hls-use-mpegts"); + + /// /// Download only chapters that match the regular expression. A "*" prefix denotes time-range instead of chapter. /// Negative timestamps are calculated from the end. "*from-url" can be used to download between the "start_time" and "end_time" extracted from the URL. @@ -583,7 +676,7 @@ public Ytdlp WithTrimFilenames(int length) public Ytdlp WithWriteInfoJson() => AddFlag("--write-info-json"); /// - /// Do not write playlist metadata when using WriteVideoMetadata(), WriteVideoDescription() + /// Do not write playlist metadata when using , /// public Ytdlp WithNoWritePlaylistMetafiles() => AddFlag("--no-write-playlist-metafiles"); @@ -757,6 +850,20 @@ public Ytdlp WithSleepSubtitles(double seconds) /// public Ytdlp WithFormat(string format) => new Ytdlp(this, format: format.Trim()); + /// + /// Containers that may be used when merging formats, separated by "/", e.g. "mp4/mkv" Ignored if no merge is required. + /// + /// (currently supported: avi, flv, mkv, mov, mp4, webm) + /// + public Ytdlp WithMergeOutputFormat(string format) + { + // Common values: mp4, mkv, webm, mov, avi, flv + if (string.IsNullOrWhiteSpace(format)) + throw new ArgumentException("Merge output format cannot be empty", nameof(format)); + + return AddOption("--merge-output-format", format.Trim().ToLowerInvariant()); + } + #endregion #region Subtitle Options @@ -946,7 +1053,14 @@ public Ytdlp WithFFmpegLocation(string? ffmpegPath) /// /// (currently supported: jpg, png, webp) /// - public Ytdlp WithConvertthumbnails(string format = "none") => AddOption("--convert-thumbnails", format); + public Ytdlp WithConvertThumbnails(string format = "jpg") + { + // Supported: jpg, png, webp + if (string.IsNullOrWhiteSpace(format)) + throw new ArgumentException("Thumbnail format cannot be empty", nameof(format)); + + return AddOption("--convert-thumbnails", format.Trim().ToLowerInvariant()); + } /// /// Force keyframes at cuts when downloading/splitting/removing sections. @@ -990,7 +1104,7 @@ public Ytdlp WithFFmpegLocation(string? ffmpegPath) public Ytdlp AddOption(string key, string value) => new Ytdlp(this, extraOptions: new[] { (key.Trim(), value) }); #endregion - + #region Downloaders public Ytdlp WithExternalDownloader(string downloaderName, string? downloaderArgs = null) { if (string.IsNullOrWhiteSpace(downloaderName)) @@ -1019,6 +1133,8 @@ public Ytdlp WithAria2(int connections = 16) public Ytdlp WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null) => WithExternalDownloader("ffmpeg", extraFfmpegArgs); + #endregion + #region Redundant options /// @@ -1076,54 +1192,15 @@ public Ytdlp WithRejectTitle(string regex) public Ytdlp WithBreakOnReject() => AddFlag("--break-on-reject"); #endregion + #region Bonus - - - // Nice-to-have #18 – very popular for high-quality + fallback + public Ytdlp WithBestUpTo1440p() => new Ytdlp(this, format: "bestvideo[height<=?1440]+bestaudio/best"); public Ytdlp With1080pOrBest() => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); - - - - - // 29. No prefer free formats (default behavior - explicit) - public Ytdlp WithNoPreferFreeFormats() => AddFlag("--no-prefer-free-formats"); - - // 30. Merge output format (force container after download & post-processing) - public Ytdlp WithMergeOutputFormat(string format) - { - // Common values: mp4, mkv, webm, mov, avi, flv - if (string.IsNullOrWhiteSpace(format)) - throw new ArgumentException("Merge output format cannot be empty", nameof(format)); - - return AddOption("--merge-output-format", format.Trim().ToLowerInvariant()); - } - - // Bonus 31 – very popular shortcut public Ytdlp WithBestUpTo1080p() => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best"); + public Ytdlp With720pOrBest() => new Ytdlp(this, format: "bv*[height<=?720]+ba/best/best"); - // 45. Recode / re-encode video into specified format - public Ytdlp WithRecodeVideo(string format = "mp4") - { - // Supported: mp4, mkv, avi, webm, flv, mov, ... - if (string.IsNullOrWhiteSpace(format)) - throw new ArgumentException("Recode format cannot be empty", nameof(format)); - - return AddOption("--recode-video", format.Trim().ToLowerInvariant()); - } - - // 46. Convert thumbnails to specified format - public Ytdlp WithConvertThumbnails(string format = "jpg") - { - // Supported: jpg, png, webp - if (string.IsNullOrWhiteSpace(format)) - throw new ArgumentException("Thumbnail format cannot be empty", nameof(format)); - - return AddOption("--convert-thumbnails", format.Trim().ToLowerInvariant()); - } - - // Bonus – common combo: remux to mp4 + embed metadata + chapters + thumbnail public Ytdlp WithMp4PostProcessingPreset() => this .WithRemuxVideo(MediaFormat.Mp4) @@ -1131,30 +1208,11 @@ public Ytdlp WithMp4PostProcessingPreset() .WithEmbedChapters() .WithEmbedThumbnail(); - // Bonus – force mkv container (popular for archiving) public Ytdlp WithMkvOutput() - => new Ytdlp(this, - extraOptions: new[] - { - ("--remux-video", "mkv"), - ("--merge-output-format", "mkv") - }); - - - - - - // 56. Do not use mpegts for HLS (use default fragmented mp4) - public Ytdlp WithNoHlsUseMpegts() => AddFlag("--no-hls-use-mpegts"); - - // 57. External downloader for live streams (e.g. ffmpeg, aria2c, ...) - - - // 58. Use ffmpeg as external downloader for live streams (most common choice) - - + => this + .WithRemuxVideo(MediaFormat.Mkv) + .WithMergeOutputFormat("mkv"); - // 63. Maximum video height / resolution limit public Ytdlp WithMaxHeight(int height) { if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive"); @@ -1163,7 +1221,6 @@ public Ytdlp WithMaxHeight(int height) return new Ytdlp(this, format: formatSelector); } - // 64. Maximum video height with fallback to best available public Ytdlp WithMaxHeightOrBest(int height) { if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive"); @@ -1172,25 +1229,18 @@ public Ytdlp WithMaxHeightOrBest(int height) return new Ytdlp(this, format: formatSelector); } - // 65. Best video + best audio (classic high-quality merge) public Ytdlp WithBestVideoPlusBestAudio() => new Ytdlp(this, format: "bestvideo+bestaudio/best"); - // 67. Best video up to 720p + best audio - public Ytdlp With720pOrBest() => new Ytdlp(this, format: "bv*[height<=?720]+ba/best/best"); - - // 68. Audio-only – best quality audio public Ytdlp WithBestAudioOnly() => new Ytdlp(this, format: "bestaudio"); - // Bonus A – very popular preset - public Ytdlp WithBestUpTo1440p() => new Ytdlp(this, format: "bestvideo[height<=?1440]+bestaudio/best"); - - // Bonus B – avoid very high resolutions (4K+) public Ytdlp WithNo4k() => new Ytdlp(this, format: "bestvideo[height<=?2160]+bestaudio/best"); - // Bonus C – audio-only with specific codec preference public Ytdlp WithBestM4aAudio() => new Ytdlp(this, format: "bestaudio[ext=m4a]/bestaudio/best"); + #endregion - + // ================================================================================================================== + // Probe and Download Functions + // ================================================================================================================== #region Execution & Utility Methods @@ -1248,6 +1298,44 @@ public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stab } + /// + /// List all supported extractors and exit + /// + /// + /// Buffer size in KB. + /// List of extractor names + public async Task> ExtractorsAsync(CancellationToken ct = default, int bufferKb = 128) + { + try + { + List list = new(); + var result = await Probe().RunAsync("--list-extractors", ct, bufferKb); + + if (string.IsNullOrWhiteSpace(result)) + { + _logger.Log(LogType.Warning, "Empty extractor list."); + return list; + } + + var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var line in lines) + list.Add(line); + + return list; + } + catch (OperationCanceledException) + { + _logger.Log(LogType.Warning, "Extractors fetch cancelled."); + return new List(); + } + catch (Exception ex) + { + _logger.Log(LogType.Warning, $"Extrators fetch failed: {ex.Message}"); + return new List(); + } + } + /// /// Fetches video metadata from the specified URL. /// @@ -1568,7 +1656,7 @@ public async Task GetBestVideoFormatIdAsync(string url, int maxHeight = /// /// /// - public async Task ExecuteAsync(string url, CancellationToken ct = default) + public async Task DownloadAsync(string url, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -1644,7 +1732,7 @@ void OnProgressMessageHandler(object? s, string msg) /// A representing the asynchronous execution of the process. /// /// - public async Task ExecuteBatchAsync(IEnumerable urls, int maxConcurrency = 3, CancellationToken ct = default) + public async Task DownloadBatchAsync(IEnumerable urls, int maxConcurrency = 3, CancellationToken ct = default) { if (urls == null || !urls.Any()) { @@ -1659,7 +1747,7 @@ public async Task ExecuteBatchAsync(IEnumerable urls, int maxConcurrency await throttler.WaitAsync(); try { - await ExecuteAsync(url, ct); + await DownloadAsync(url, ct); } catch (YtdlpException ex) { @@ -1686,7 +1774,6 @@ private ProbeRunner Probe() return new ProbeRunner(factory, _logger); } - // Command building (called only at execution time) private List BuildArguments(string url) { var args = new List();