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/README.md b/README.md
index bb16a44..353f922 100644
--- a/README.md
+++ b/README.md
@@ -1,362 +1,379 @@
-  
+#   
+

# Ytdlp.NET
+

### ClipMate - MAUI.NET App - [Download](https://apps.microsoft.com/detail/9NTP1DH4CQ4X?hl=en&gl=IN&ocid=pdpshare)
-
+
+---
### Video Downloader - .NET App
+

-[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)
+---
-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.DownloadAsync("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.DownloadAsync(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")
- .ExecuteAsync(url);
+ .WithFormat($"{bestVideo}+{bestAudio}/best")
+ .DownloadAsync(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.DownloadAsync(url);
+});
-await ytdlp.ExecuteAsync(url);
+await Task.WhenAll(tasks);
```
-### Download best 1080p video + best audio (auto-selected)
+**OR**
```csharp
-var ytdlp = new Ytdlp();
+var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" };
-string url = "https://www.youtube.com/watch?v=Xt50Sodg7sA";
+ await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe")
+ .WithFormat("best")
+ .WithOutputFolder("./batch");
-// Auto-select best formats
-string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 1080);
-string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url);
+await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);
+```
-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)
+---
-```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);
+---
+
+# 🔧 Fluent API Methods
+
+### Output
-await ytdlp
- .SetFormat($"{bestVideo}+{bestAudio}/best")
- .SetOutputFolder("./downloads")
- .SetOutputTemplate("%(title)s [%(resolution)s - %(id)s].%(ext)s")
- .EmbedMetadata()
- .EmbedThumbnail()
- .ExecuteAsync(url);
+```
+WithOutputFolder()
+WithTempFolder()
+WithHomeFolder()
+WithOutputTemplate()
+WithFFmpegLocation()
```
-### Monitor download progress with a simple console bar
-```csharp
-ytdlp.OnProgressDownload += (sender, args) =>
-{
- ConsoleProgress.Update(args.Percent, $"{args.Speed} ETA {args.ETA}");
-};
+### Formats
+
+```
+WithFormat()
+With720pOrBest()
+WithExtractAudio()
+```
-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()
+```
+
+### Advanced
-await ytdlp
- .SetFormat(bestAudioId)
- .ExtractAudio("mp3")
- .SetOutputFolder("./audio")
- .SetOutputTemplate("%(title)s - %(uploader)s.%(ext)s")
- .ExecuteAsync(url);
```
+AddFlag()
+AddOption()
+AddCustomCommand()
+```
+
+---
+
+# 🔄 Upgrade Guide (v2 → v3)
+
+v3 introduces a **new immutable fluent API**.
+
+Old mutable commands were removed.
+
+---
+
+## ❌ Old API (v2)
-### Remove SponsorBlock segments (all categories)
```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.DownloadAsync(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..fac6d13
--- /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 93%
rename from src/VideoDownloader/VideoDownloader.csproj
rename to apps/ClipMate.Lite/ClipMate.Lite.csproj
index 61c590b..fd27ece 100644
--- a/src/VideoDownloader/VideoDownloader.csproj
+++ b/apps/ClipMate.Lite/ClipMate.Lite.csproj
@@ -25,10 +25,6 @@
-
-
-
-
True
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 59%
rename from src/YtDlpWrapper/YtDlpWrapper.csproj
rename to archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj
index 8a02d1e..f60eea8 100644
--- a/src/YtDlpWrapper/YtDlpWrapper.csproj
+++ b/archives/Ytdlp.Wrapper/Ytdlp.Wrapper.csproj
@@ -1,24 +1,24 @@
- net6.0;net8.0;net9.0
+ net8.0;net9.0;net10.0;
enable
enable
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/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.v3.Console/Program.cs b/feature/Ytdlp.NET.vNext.Console/Program.cs
similarity index 100%
rename from src/Ytdlp.NET.v3.Console/Program.cs
rename to feature/Ytdlp.NET.vNext.Console/Program.cs
diff --git a/src/Ytdlp.NET.v3.Console/Ytdlp.NET.v3.Console.csproj b/feature/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj
similarity index 84%
rename from src/Ytdlp.NET.v3.Console/Ytdlp.NET.v3.Console.csproj
rename to feature/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj
index eded871..56b2004 100644
--- a/src/Ytdlp.NET.v3.Console/Ytdlp.NET.v3.Console.csproj
+++ b/feature/Ytdlp.NET.vNext.Console/Ytdlp.NET.vNext.Console.csproj
@@ -14,8 +14,4 @@
-
-
-
-
diff --git a/src/Ytdlp.NET.v3/Core/ProgressParser.cs b/feature/Ytdlp.NET.vNext/Core/ProgressParser.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Core/ProgressParser.cs
rename to feature/Ytdlp.NET.vNext/Core/ProgressParser.cs
diff --git a/src/Ytdlp.NET.v3/Core/RegexPatterns.cs b/feature/Ytdlp.NET.vNext/Core/RegexPatterns.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Core/RegexPatterns.cs
rename to feature/Ytdlp.NET.vNext/Core/RegexPatterns.cs
diff --git a/src/Ytdlp.NET.v3/Core/UpdateChannel.cs b/feature/Ytdlp.NET.vNext/Core/UpdateChannel.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Core/UpdateChannel.cs
rename to feature/Ytdlp.NET.vNext/Core/UpdateChannel.cs
diff --git a/src/Ytdlp.NET.v3/DefaultLogger.cs b/feature/Ytdlp.NET.vNext/DefaultLogger.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/DefaultLogger.cs
rename to feature/Ytdlp.NET.vNext/DefaultLogger.cs
diff --git a/src/Ytdlp.NET.v3/DownloadProgressEventArgs.cs b/feature/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/DownloadProgressEventArgs.cs
rename to feature/Ytdlp.NET.vNext/DownloadProgressEventArgs.cs
diff --git a/src/Ytdlp.NET.v3/Helpers/FormatFilters.cs b/feature/Ytdlp.NET.vNext/Helpers/FormatFilters.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Helpers/FormatFilters.cs
rename to feature/Ytdlp.NET.vNext/Helpers/FormatFilters.cs
diff --git a/src/Ytdlp.NET.v3/ILogger.cs b/feature/Ytdlp.NET.vNext/ILogger.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/ILogger.cs
rename to feature/Ytdlp.NET.vNext/ILogger.cs
diff --git a/src/Ytdlp.NET.v3/LICENSE.txt b/feature/Ytdlp.NET.vNext/LICENSE.txt
similarity index 100%
rename from src/Ytdlp.NET.v3/LICENSE.txt
rename to feature/Ytdlp.NET.vNext/LICENSE.txt
diff --git a/src/Ytdlp.NET.v3/LogType.cs b/feature/Ytdlp.NET.vNext/LogType.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/LogType.cs
rename to feature/Ytdlp.NET.vNext/LogType.cs
diff --git a/src/Ytdlp.NET.v3/Models/Format.cs b/feature/Ytdlp.NET.vNext/Models/Format.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Models/Format.cs
rename to feature/Ytdlp.NET.vNext/Models/Format.cs
diff --git a/src/Ytdlp.NET.v3/Models/Metadata.cs b/feature/Ytdlp.NET.vNext/Models/Metadata.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Models/Metadata.cs
rename to feature/Ytdlp.NET.vNext/Models/Metadata.cs
diff --git a/src/Ytdlp.NET.v3/Models/MetadataLite.cs b/feature/Ytdlp.NET.vNext/Models/MetadataLite.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Models/MetadataLite.cs
rename to feature/Ytdlp.NET.vNext/Models/MetadataLite.cs
diff --git a/src/Ytdlp.NET.v3/README.md b/feature/Ytdlp.NET.vNext/README.md
similarity index 100%
rename from src/Ytdlp.NET.v3/README.md
rename to feature/Ytdlp.NET.vNext/README.md
diff --git a/src/Ytdlp.NET.v3/Ytdlp.NET.v3.csproj b/feature/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj
similarity index 89%
rename from src/Ytdlp.NET.v3/Ytdlp.NET.v3.csproj
rename to feature/Ytdlp.NET.vNext/Ytdlp.NET.vNext.csproj
index 46b37fb..50989d0 100644
--- a/src/Ytdlp.NET.v3/Ytdlp.NET.v3.csproj
+++ b/feature/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.v3/Ytdlp.cs b/feature/Ytdlp.NET.vNext/Ytdlp.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/Ytdlp.cs
rename to feature/Ytdlp.NET.vNext/Ytdlp.cs
diff --git a/src/Ytdlp.NET.v3/YtdlpBuilder.cs b/feature/Ytdlp.NET.vNext/YtdlpBuilder.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/YtdlpBuilder.cs
rename to feature/Ytdlp.NET.vNext/YtdlpBuilder.cs
diff --git a/src/Ytdlp.NET.v3/YtdlpCommand.cs b/feature/Ytdlp.NET.vNext/YtdlpCommand.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/YtdlpCommand.cs
rename to feature/Ytdlp.NET.vNext/YtdlpCommand.cs
diff --git a/src/Ytdlp.NET.v3/YtdlpException.cs b/feature/Ytdlp.NET.vNext/YtdlpException.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/YtdlpException.cs
rename to feature/Ytdlp.NET.vNext/YtdlpException.cs
diff --git a/src/Ytdlp.NET.v3/YtdlpGeneral.cs b/feature/Ytdlp.NET.vNext/YtdlpGeneral.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/YtdlpGeneral.cs
rename to feature/Ytdlp.NET.vNext/YtdlpGeneral.cs
diff --git a/src/Ytdlp.NET.v3/YtdlpProbe.cs b/feature/Ytdlp.NET.vNext/YtdlpProbe.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/YtdlpProbe.cs
rename to feature/Ytdlp.NET.vNext/YtdlpProbe.cs
diff --git a/src/Ytdlp.NET.v3/YtdlpRootBuilder.cs b/feature/Ytdlp.NET.vNext/YtdlpRootBuilder.cs
similarity index 100%
rename from src/Ytdlp.NET.v3/YtdlpRootBuilder.cs
rename to feature/Ytdlp.NET.vNext/YtdlpRootBuilder.cs
diff --git a/src/Ytdlp.NET.v3/icon.png b/feature/Ytdlp.NET.vNext/icon.png
similarity index 100%
rename from src/Ytdlp.NET.v3/icon.png
rename to feature/Ytdlp.NET.vNext/icon.png
diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs
index d54fbcc..c2f7f92 100644
--- a/src/Ytdlp.NET.Console/Program.cs
+++ b/src/Ytdlp.NET.Console/Program.cs
@@ -11,30 +11,30 @@ 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)
- 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");
// Run all demos/tests sequentially
- //await TestGetVersionAsync(ytdlp);
- //await TestUpdateVersionAsync(ytdlp);
+ await TestGetVersionAsync(baseYtdlp);
+ await TestUpdateAsync(baseYtdlp);
- //await TestGetFormatsAsync(ytdlp);
- //await TestGetFormatsDetailedAsync(ytdlp);
- //await TestGetMetadataAsync(ytdlp);
- //await TestGetSimpleMetadataAsync(ytdlp);
- //await TestGetTitleAsync(ytdlp);
+ await TestGetFormatsAsync(baseYtdlp);
+ await TestGetMetadataAsync(baseYtdlp);
+ await TestGetLiteMetadataAsync(baseYtdlp);
+ await TestGetTitleAsync(baseYtdlp);
- await TestDownloadVideoAsync(ytdlp);
- await TestDownloadAudioAsync(ytdlp);
- await TestBatchDownloadAsync(ytdlp);
- //await TestSponsorBlockAsync(ytdlp);
- await TestConcurrentFragmentsAsync(ytdlp);
- await TestCancellationAsync(ytdlp);
+ await TestDownloadVideoAsync(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();
@@ -57,30 +57,27 @@ 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...");
- var version = await ytdlp.GetVersionAsync();
+ var version = await ytdlp.VersionAsync();
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.GetAvailableFormatsAsync(url);
+ var formats = await ytdlp.GetFormatsAsync(url);
stopwatch.Stop(); // stop timer
Console.WriteLine($"Available formats took {stopwatch.Elapsed.TotalSeconds:F3} seconds");
@@ -102,36 +99,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.GetAvailableFormatsAsync(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 +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 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(url1);
stopwatch.Stop(); // stop timer
Console.WriteLine($"Detailed metedata took {stopwatch.Elapsed.TotalSeconds:F3} seconds");
@@ -167,11 +133,10 @@ private static async Task TestGetMetadataAsync(Ytdlp ytdlp)
}
}
- // Test 5: Get simple metedata
- private static async Task TestGetSimpleMetadataAsync(Ytdlp ytdlp)
+ 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";
@@ -186,24 +151,22 @@ 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 url = "https://www.youtube.com/watch?v=3pecPwPIFIc&pp=ugUEEgJtbA%3D%3D";
+
+ var ytdlp = ytdlpBase
+ .With720pOrBest()
+ .WithConcurrentFragments(8)
+ .WithHomeFolder("./downloads")
+ .WithTempFolder("./downloads/temp")
+ .WithOutputTemplate("%(title)s.%(ext)s")
+ .WithMtime()
+ .WithTrimFilenames(100);
// Subscribe to events
ytdlp.OnProgressDownload += (sender, args) =>
@@ -215,28 +178,25 @@ 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);
+ Console.WriteLine(ytdlp.Preview(url));
+
+ // await ytdlp.ExecuteAsync(url);
}
- // Test 7 Extract audio only
private static async Task TestDownloadAudioAsync(Ytdlp ytdlp)
{
Console.WriteLine("\nTest 7: Extracting audio...");
var url = "https://www.youtube.com/watch?v=ZGnQH0LN_98";
await ytdlp
- .ExtractAudio("mp3")
- .SetFormat("ba")
- .SetOutputFolder("./downloads/audio")
- .ExecuteAsync(url);
+ .WithExtractAudio(AudioFormat.Mp3)
+ .WithFormat("ba")
+ .WithOutputFolder("./downloads/audio")
+ .DownloadAsync(url);
}
// 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
@@ -246,10 +206,11 @@ private static async Task TestBatchDownloadAsync(Ytdlp ytdlp)
"https://www.youtube.com/watch?v=oDSEGkT6J-0"
};
- await ytdlp
- .SetFormat("best[height<=480]") // Lower quality for speed
- .SetOutputFolder("./downloads/batch")
- .ExecuteBatchAsync(urls, maxConcurrency: 3);
+ var ytdlp = baseYtdlp
+ .WithFormat("best[height<=480]") // Lower quality for speed
+ .WithOutputFolder("./downloads/batch");
+
+ await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);
}
// Test 9: SponsorBlock removal
@@ -259,10 +220,10 @@ 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")
- .ExecuteAsync(url);
+ .WithFormat("best")
+ .WithSponsorblockRemove("all") // Removes sponsor, intro, etc.
+ .WithOutputFolder("./downloads/sponsorblock")
+ .DownloadAsync(url);
}
// Test 10: Concurrent fragments (faster download)
@@ -273,10 +234,10 @@ private static async Task TestConcurrentFragmentsAsync(Ytdlp ytdlp)
await ytdlp
.WithConcurrentFragments(8) // 8 parallel fragments
- .SetFormat("b")
- .SetOutputTemplate("%(title)s.%(ext)s")
- .SetOutputFolder("./downloads/concurrent")
- .ExecuteAsync(url);
+ .WithFormat("b")
+ .WithOutputTemplate("%(title)s.%(ext)s")
+ .WithOutputFolder("./downloads/concurrent")
+ .DownloadAsync(url);
}
// Test 11: Cancellation support
@@ -287,10 +248,10 @@ private static async Task TestCancellationAsync(Ytdlp ytdlp)
var cts = new CancellationTokenSource();
var downloadTask = ytdlp
- .SetFormat("b")
- .SetOutputTemplate("%(title)s.%(ext)s")
- .SetOutputFolder("./downloads/cancel")
- .ExecuteAsync(url, cts.Token);
+ .WithFormat("b")
+ .WithOutputTemplate("%(title)s.%(ext)s")
+ .WithOutputFolder("./downloads/cancel")
+ .DownloadAsync(url, cts.Token);
// Simulate cancel after 20 seconds
await Task.Delay(20000);
@@ -314,13 +275,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")
- .ExecuteAsync(url);
+ var downloadTask = ytdlp
+ .WithSimulate()
+ .WithOutputTemplate("%(title)s.%(ext)s")
+ .WithOutputFolder("./downloads/cancel")
+ .AddFlag("--get-title")
+ .DownloadAsync(url);
await downloadTask;
}
diff --git a/src/Ytdlp.NET.Test/ProgressParserTests.cs b/src/Ytdlp.NET.Test/ProgressParserTests.cs
deleted file mode 100644
index b7bd093..0000000
--- a/src/Ytdlp.NET.Test/ProgressParserTests.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Ytdlp.NET.Test
-{
- internal class ProgressParserTests
- {
- }
-}
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/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/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/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/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/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 ead863b..f5124e3 100644
--- a/src/Ytdlp.NET/README.md
+++ b/src/Ytdlp.NET/README.md
@@ -1,27 +1,20 @@
  
# 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,457 @@ 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 v3.0
+
+* 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.
+
---
-## 🚀 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());
+## 🛠 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)`
+* `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)`
-string url = "https://www.youtube.com/watch?v=Xt50Sodg7sA";
-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.DownloadAsync("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.DownloadAsync(url);
+});
+await Task.WhenAll(tasks);
```
+### **Key points**:
+
+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**.
-## 📦 Prerequisites
+---
-**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:
+## 📦 Basic Usage
-- **.NET**: .NET 8.0 or higher
-- **yt-dlp**:
+### Download a Single Video
-### Recommended: Use companion NuGet packages (easiest & portable)
+```csharp
+await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger())
+ .WithFormat("best")
+ .WithOutputFolder("./downloads")
+ .WithEmbedMetadata()
+ .WithEmbedThumbnail();
-We provide official build packages that automatically download and manage the latest stable binaries:
+ytdlp.OnProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%");
+ytdlp.OnCompleteDownload += (s, msg) => Console.WriteLine($"Download complete: {msg}");
-```xml
-
-
-
-
-
-
+await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U");
```
+### Extract Audio
+
```csharp
-var ytdlp = new Ytdlp(ytDlpPath: @"\Tools\yt-dlp.exe");
+await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe")
+ .WithExtractAudio("mp3")
+ .WithOutputFolder("./audio")
+ .WithEmbedMetadata();
+
+await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U");
```
-## ✨ Basic Usage
+---
+
+### Fetch Metadata
+
+```csharp
+await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe");
+
+var metadata = await ytdlp.GetMetadataAsync("https://www.youtube.com/watch?v=abc123");
-### 🔽 Download a Single Video
+Console.WriteLine($"Title: {metadata?.Title}, Duration: {metadata?.Duration}");
+```
+
+---
-Download a video with the best quality to a specified folder:
+### Fetch Formats
```csharp
-var ytdlp = new Ytdlp("yt-dlp", new ConsoleLogger());
+await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe");
-await ytdlp
- .SetFormat("best")
- .SetOutputFolder("downloads")
- .DownloadThumbnails()
- .ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U");
+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}");
```
-### 🎵 Extract Audio + Embed Metadata
+---
+
+### Best Format Selection
```csharp
+await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe");
+
+string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url);
+string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 720);
+
await ytdlp
- .ExtractAudio("mp3")
- .EmbedMetadata()
- .SetOutputFolder("audio")
- .ExecuteAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U");
+ .WithFormat($"{bestVideo}+{bestAudio}/best")
+ .WithOutputFolder("./downloads")
+ .DownloadAsync(url);
```
-### 🧾 List Available Formats
+---
+
+### Batch Downloads
```csharp
-var formats = await ytdlp.GetAvailableFormatsAsync("https://youtube.com/watch?v=abc123");
-foreach (var f in formats)
+var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" };
+
+var tasks = urls.Select(async url =>
{
- Console.WriteLine($"ID: {f.ID}, Resolution: {f.Resolution}, VCodec: {f.VCodec}");
-}
-```
+ await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe")
+ .WithFormat("best")
+ .WithOutputFolder("./batch");
-### 🧪 Get Video Metadata Only
+ await ytdlp.DownloadAsync(url);
+});
-```csharp
-var metadata = await ytdlp.GetMetadataAsync("https://youtube.com/watch?v=abc123");
-Console.WriteLine($"Title: {metadata?.Title}, Duration: {metadata?.Duration}");
+await Task.WhenAll(tasks);
```
+**OR**
-### 📦 Batch Download
+```csharp
+var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" };
-Sequential (one after another)
+ await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe")
+ .WithFormat("best")
+ .WithOutputFolder("./batch");
-```csharp
-await ytdlp
- .SetFormat("best")
- .SetOutputFolder("batch")
- .ExecuteBatchAsync(new[] {
- "https://youtu.be/vid1", "https://youtu.be/vid2"
- });
+await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);
```
-Parallel (max 3 at a time)
-```csharp
-await ytdlp
- .SetFormat("best")
- .SetOutputFolder("batch")
- .ExecuteBatchAsync(new[] {
- "https://youtu.be/vid1", "https://youtu.be/vid2"
- }, maxConcurrency: 3);
-```
+---
-### ⚙️ 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
+## Fluent Methods (v3.0)
+
+### 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()`
+
+### Network Options
+* `.WithProxy(string? proxy)`
+* `.WithSocketTimeout(TimeSpan timeout)`
+* `.WithForceIpv4()`
+* `.WithForceIpv6()`
+* `.WithEnableFileUrls()`
+
+### Geo-restriction Options
+* `.WithGeoVerificationProxy(string url)`
+* `.WithGeoBypassCountry(string countryCode)`
+
+### 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)`
+* `.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()`
+
+### 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)`
+* `.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()`
+* `.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
+* `.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 ...
-```csharp
-ytdlp.AddCustomCommand("--sponsorblock-mark all");
-```
-Will be validated against internal whitelist. Invalid commands will trigger error logging via ILogger.
+---
-### 📡 Events
+### Events
```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.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.OnCompleteDownload += (sender, message) => Console.WriteLine($"Download complete: {message}");
-ytdlp.OnPostProcessingComplete += (s, msg) => Console.WriteLine($"Postprocessing: {msg}");
-ytdlp.OnCommandCompleted += (success, message) => Console.WriteLine($"Finished: {message}");
+ytdlp.OnCommandCompleted += (s, e) => Console.WriteLine($"Command finished: {e.Command}");
```
-### 📄 Output Template
+---
-You can customize file naming using yt-dlp placeholders:
-```csharp
-ytdlp.SetOutputTemplate("%(title)s-%(id)s.%(ext)s");
-```
+# 🔄 Upgrade Guide (v2 → v3)
-### 🧪 Validation & Safety
+v3 introduces a **new immutable fluent API**.
-All AddCustomCommand(...) calls are validated against a known safe set of yt-dlp options, minimizing the risk of malformed or unsupported commands.
+Old mutable commands were removed.
-### ❗ Error Handling
+---
-All exceptions are wrapped in YtdlpException:
+## ❌ Old API (v2)
```csharp
-try
-{
- await ytdlp.ExecuteAsync("https://invalid-url");
-}
-catch (YtdlpException ex)
-{
- Console.WriteLine($"Error: {ex.Message}");
-}
+var ytdlp = new Ytdlp();
+
+await ytdlp
+ .SetFormat("best")
+ .SetOutputFolder("./downloads")
+ .ExecuteAsync(url);
```
-### 🧪 Version Check
+---
+
+## ✅ New API (v3)
```csharp
-string version = await ytdlp.GetVersionAsync();
-Console.WriteLine($"yt-dlp version: {version}");
+await using var ytdlp = new Ytdlp()
+ .WithFormat("best")
+ .WithOutputFolder("./downloads");
+
+await ytdlp.DownloadAsync(url);
```
-### 🔄 Update Check
+---
+
+## 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
-UpdateChannel channel = UpdateChannel.Stable; // Master, Nightly.
-string version = await ytdlp.UpdateAsync(channel);
-Console.WriteLine($"yt-dlp version: {version}");
-```
+var baseYtdlp = new Ytdlp();
-### 💡 Tips
+var download = baseYtdlp
+ .WithFormat("best")
+ .WithOutputFolder("./downloads");
+```
-- For livestreams, use:
- ```csharp
- .DownloadLivestream(true)
- ```
-- To skip already-downloaded videos:
- ```csharp
- .SkipDownloaded()
- ```
+---
-### 🛠 Custom Logging
+### Event subscription
-Implement your own ILogger:
+Attach events **to the configured instance**.
```csharp
-public class ConsoleLogger : ILogger
-{
- public void Log(LogType type, string message)
- {
- Console.WriteLine($"[{type}] {message}");
- }
-}
+var download = baseYtdlp.WithFormat("best");
+
+download.OnProgressDownload += ...
```
-### ⚠️ 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`|
+### Proper disposal
-### 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
+Use **`await using`** for automatic cleanup.
-## 🤝 Contributing
+```csharp
+await using var ytdlp = new Ytdlp();
+```
+
+---
-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.
-## 📄 License
+### ✅ Notes
-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.
+* 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`, `GetFormatsAsync`, `GetBestVideoFormatIdAsync`, etc.).
---
-**Author:** Manojbabu (ManuHub)
-**Repository:** [Ytdlp.NET](https://github.com/manusoft/yt-dlp-wrapper)
+### License
+
+MIT License — see [LICENSE](https://github.com/manusoft/Ytdlp.NET/blob/master/LICENSE.md)
+
+**Author:** Manojbabu (ManuHub)
+**Repository:** [Ytdlp.NET](https://github.com/manusoft/Ytdlp.NET)
diff --git a/src/Ytdlp.NET/Ytdlp.NET.csproj b/src/Ytdlp.NET/Ytdlp.NET.csproj
index 9e6b107..b4b161d 100644
--- a/src/Ytdlp.NET/Ytdlp.NET.csproj
+++ b/src/Ytdlp.NET/Ytdlp.NET.csproj
@@ -7,31 +7,49 @@
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
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
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..e194998 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,794 +12,1249 @@ 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;
+ #region Frozen configuration
+ private readonly string _ytdlpPath;
private readonly ILogger _logger;
- private readonly ProbeRunner _probe;
- private readonly DownloadRunner _download;
+ 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;
+
+ private readonly ImmutableArray _flags;
+ private readonly ImmutableArray<(string Key, string Value)> _options;
+ #endregion
+
+ #region Events
+ public event EventHandler? OnProgressDownload;
+ public event EventHandler? OnProgressMessage;
+ public event EventHandler? OnOutputMessage;
+ public event EventHandler? OnCompleteDownload;
+ public event EventHandler? OnPostProcessingComplete;
+ public event EventHandler? OnCommandCompleted;
+ public event EventHandler? OnErrorMessage;
+ #endregion
+
+ #region Flag to prevent double disposal
+ private bool _disposed = false;
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ // Optionally, cancel running downloads (if you store CancellationTokens)
+ // e.g., _cts?.Cancel();
+
+ await Task.CompletedTask;
+ }
+ #endregion
- private string _format = "best";
- private string _outputFolder = ".";
- private string _outputTemplate = "%(title)s.%(ext)s";
+ #region Constructors
+
+ public Ytdlp(string ytdlpPath = "yt-dlp", ILogger? logger = null)
+ {
+ _ytdlpPath = ValidatePath(ytdlpPath);
+ _logger = logger ?? new DefaultLogger();
+
+ // defaults
+ _outputFolder = null;
+ _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? homeFolder = null,
+ string? tempFolder = 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,
+ 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);
+ }
- //
- /// Fired for general progress messages from yt-dlp output.
+ #endregion
+
+ // ==================================================================================================================
+ // Fluent configuration methods
+ // ==================================================================================================================
+
+ #region General Options
+
+ ///
+ /// Ignore download and postprocessing errors. The download will be considered successful even if the postprocessing fails
///
- //public event EventHandler? OnProgress;
+ ///
+ public Ytdlp WithIgnoreErrors() => AddFlag("--ignore-errors");
+ ///
+ /// IgAbort downloading of further videos if an error occurs
+ ///
+ ///
+ public Ytdlp WithAbortOnError() => AddFlag("--abort-on-error");
///
- /// Fired when the yt-dlp process completes (success or failure/cancel).
+ /// 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 event EventHandler? OnCommandCompleted;
+ ///
+ public Ytdlp WithIgnoreConfig() => AddFlag("--ignore-config");
///
- /// Fired for every output line from yt-dlp (stdout).
+ /// 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 event EventHandler? OnOutputMessage;
+ ///
+ ///
+ public Ytdlp WithConfigLocations(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Config folder path required");
+ return AddOption("--config-locations", Path.GetFullPath(path));
+ }
///
- /// Fired when download progress updates are parsed (percentage, speed, ETA).
+ /// Path to an additional directory to search for plugins. This option can be used multiple times to add multiple directories.
///
- public event EventHandler? OnProgressDownload;
+ ///
+ ///
+ public Ytdlp WithPluginDirs(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("plugin folder path required");
+ return AddOption("--plugin-dirs", path);
+ }
///
- /// Fired when a single download completes successfully.
+ /// Clear plugin directories to search, including defaults and those provided by previous
///
- public event EventHandler? OnCompleteDownload;
+ ///
+ ///
+ public Ytdlp WithNoPluginDirs(string path) => AddFlag("--no-plugin-dirs");
///
- /// Fired for informational progress messages (e.g. merging, extracting).
+ /// 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, needs to be passed before enabling other runtimes
///
- public event EventHandler? OnProgressMessage;
+ /// Supported runtimes are deno, node, quickjs, bun
+ ///
+ public Ytdlp WithJsRuntime(Runtime runtime, string path)
+ {
+ var builder = $"{runtime}:{path}";
+ return AddOption("--js-runtime", builder);
+ }
///
- /// Fired for error messages from yt-dlp.
+ /// Clear JavaScript runtimes to enable, including defaults and those provided by
///
- public event EventHandler? OnErrorMessage;
+ public Ytdlp WithNoJsRuntime() => AddFlag("--no-js-runtime");
///
- /// Fired when post-processing (e.g. merging, conversion) completes.
+ /// Do not extract a playlist's URL result entries; some entry metadata may be missing and downloading may be bypassed
///
- public event EventHandler? OnPostProcessingComplete;
+ public Ytdlp WithFlatPlaylist() => AddFlag("--flat-playlist");
- // Valid options set (used for custom command validation)
- private static readonly HashSet ValidOptions = new HashSet(StringComparer.Ordinal)
- {
- // ───────── 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
-
- ///
- /// 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);
- _logger = logger ?? new DefaultLogger();
+ ///
+ /// Download livestreams from the start. Currently experimental and only supported for YouTube, Twitch, and TVer.
+ ///
+ public Ytdlp WithLiveFromStart() => AddFlag("--live-from-start");
- var factory = new ProcessFactory(ytdlpPath);
+ ///
+ /// 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)>();
- _probe = new ProbeRunner(factory, _logger);
- _download = new DownloadRunner(factory, _progressParser, _logger);
+ opts.Add(("--wait-for-video", "any")); // "any" = wait indefinitely or until timeout
- // 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);
+ if (maxWait.HasValue && maxWait.Value.TotalSeconds > 0)
+ {
+ opts.Add(("--wait-for-video", maxWait.Value.TotalSeconds.ToString("F0")));
+ }
- // Subscribe to process complete events
- _download.OnCommandCompleted += (s, e) => OnCommandCompleted?.Invoke(this, e);
+ return new Ytdlp(this, extraOptions: opts!);
}
+ ///
+ /// Mark videos watched (even with Simulate())
+ ///
+ public Ytdlp WithMarkWatched() => AddFlag("--mark-watched");
+
#endregion
- #region Output & Path Configuration
+ #region Network Options
///
- /// Sets the output folder for downloaded files.
+ /// 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/.
///
- /// The target output directory.
- /// The current instance for chaining.
- /// Thrown if path is empty.
- public Ytdlp SetOutputFolder([Required] string outputFolderPath)
- {
- if (string.IsNullOrWhiteSpace(outputFolderPath))
- throw new ArgumentException("Output folder path cannot be empty.", nameof(outputFolderPath));
-
- _outputFolder = outputFolderPath;
- return this;
- }
+ /// Pass in an empty string for direct connection
+ public Ytdlp WithProxy(string? proxy) => string.IsNullOrWhiteSpace(proxy) ? this : new Ytdlp(this, proxy: proxy);
///
- /// Sets the temporary folder path used by yt-dlp.
+ /// Time to wait before giving up, in seconds
///
- /// Path to temporary folder.
- /// The current instance for chaining.
- /// Thrown if path is empty.
- public Ytdlp SetTempFolder([Required] string tempFolderPath)
+ ///
+ public Ytdlp WithSocketTimeout(TimeSpan timeout)
{
- if (string.IsNullOrWhiteSpace(tempFolderPath))
- throw new ArgumentException("Temporary folder path cannot be empty.", nameof(tempFolderPath));
-
- _commandBuilder.Append($"--paths temp:{SanitizeInput(tempFolderPath)} ");
- return this;
+ if (timeout <= TimeSpan.Zero) return this;
+ double seconds = timeout.TotalSeconds;
+ return AddOption("--socket-timeout", seconds.ToString("F0"));
}
///
- /// Sets the home folder path used by yt-dlp.
+ /// Make all connections via IPv4
///
- /// Path to home folder.
- /// The current instance for chaining.
- /// Thrown if path is empty.
- public Ytdlp SetHomeFolder([Required] string homeFolderPath)
- {
- if (string.IsNullOrWhiteSpace(homeFolderPath))
- throw new ArgumentException("Home folder path cannot be empty.", nameof(homeFolderPath));
+ public Ytdlp WithForceIpv4() => AddFlag("--force-ipv4");
- _commandBuilder.Append($"--paths home:{SanitizeInput(homeFolderPath)} ");
- return this;
- }
+ ///
+ /// Make all connections via IPv6
+ ///
+ public Ytdlp WithForceIpv6() => AddFlag("--force-ipv6");
///
- /// Specifies the location of FFmpeg executable.
+ /// Enable file:// URLs. This is disabled by default for security reasons.
///
- /// Path to ffmpeg executable or folder.
- /// The current instance for chaining.
- /// Thrown if path is empty.
- public Ytdlp SetFFmpegLocation([Required] string ffmpegFolder)
- {
- if (string.IsNullOrWhiteSpace(ffmpegFolder))
- throw new ArgumentException("FFmpeg folder cannot be empty.", nameof(ffmpegFolder));
+ public Ytdlp WithEnableFileUrls() => AddFlag("--enable-file-urls");
- _commandBuilder.Append($"--ffmpeg-location {SanitizeInput(ffmpegFolder)} ");
- return this;
- }
+ #endregion
+
+ #region Geo-restriction
///
- /// Sets the output filename template.
+ /// Use this proxy to verify the IP address for some geo-restricted sites.
+ /// The default proxy specified by (or none, if the option is not present) is used for the actual downloading
///
- /// Template string (e.g. "%(title)s.%(ext)s").
- /// The current instance for chaining.
- /// Thrown if template is empty.
- public Ytdlp SetOutputTemplate([Required] string template)
- {
- if (string.IsNullOrWhiteSpace(template))
- throw new ArgumentException("Output template cannot be empty.", nameof(template));
+ ///
+ ///
+ public Ytdlp WithGeoVerificationProxy(string url) => AddOption("--geo-verification-proxy", url);
- _outputTemplate = template.Replace("\\", "/").Trim();
- return this;
+ ///
+ /// 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 Format Selection & Extraction
+ #region Video Selection
///
- /// Sets the format selector string passed to -f/--format.
+ /// 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
///
- /// Format string (e.g. "best", "137+251", "bv*+ba").
- /// The current instance for chaining.
- public Ytdlp SetFormat([Required] string format)
+ ///
+ ///
+ ///
+ public Ytdlp WithPlaylistItems(string items)
{
- _format = format;
- return this;
+ if (string.IsNullOrWhiteSpace(items))
+ throw new ArgumentException("Playlist items string cannot be empty.", nameof(items));
+ return AddOption("--playlist-items", items.Trim());
}
///
- /// Configures audio-only extraction with the specified format.
+ /// Abort download if filesize is smaller than SIZE
///
- /// Audio format (e.g. "mp3", "m4a", "best").
- /// The current instance for chaining.
- /// Thrown if format is empty.
- public Ytdlp ExtractAudio(string audioFormat)
+ /// e.g. 50k or 44.6M
+ 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 AddOption("--min-filesize", size.Trim());
}
///
- /// Limits video resolution by height (uses bestvideo[height<=...]).
+ /// Abort download if filesize is larger than SIZE
///
- /// Max height (e.g. "1080", "720").
- /// The current instance for chaining.
- /// Thrown if resolution is empty.
- public Ytdlp SetResolution(string resolution)
+ /// e.g. 50k or 44.6M
+ 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 AddOption("--max-filesize", size.Trim());
}
- #endregion
-
- #region Metadata & Format Fetching
-
///
- /// Appends --version to the command (useful for preview or testing).
+ /// 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
///
- /// The current instance for chaining.
- public Ytdlp Version()
+ /// "today-2weeks" or "YYYYMMDD"
+ public Ytdlp WithDate(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 AddOption("--date", date.Trim());
}
///
- /// Appends --update to the command (useful for preview or testing).
+ /// Download only videos uploaded on or before this date. The date formats accepted are the same as
///
- /// The current instance for chaining.
- public Ytdlp Update()
+ /// "today-2weeks" or "YYYYMMDD"
+ public Ytdlp WithDateBefore(string date)
{
- _commandBuilder.Append("--update ");
- return this;
+ // 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());
}
///
- /// Appends --write-info-json to save metadata as JSON file.
+ /// Download only videos uploaded on or after this date. The date formats accepted are the same as
///
- /// The current instance for chaining.
- public Ytdlp WriteMetadataToJson()
+ /// "today-2weeks" or "YYYYMMDD"
+ public Ytdlp WithDateAfter(string date)
{
- _commandBuilder.Append("--write-info-json ");
- return this;
+ // 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());
}
///
- /// Appends --dump-json (simulate and output metadata only).
+ /// Generic video filter. Any "OUTPUT TEMPLATE" field can be compared with a number or a string using the operators defined in "Filtering Formats".
///
- /// The current instance for chaining.
- public Ytdlp ExtractMetadataOnly()
+ ///
+ ///
+ ///
+ public Ytdlp WithMatchFilter(string filterExpression)
{
- _commandBuilder.Append("--dump-json ");
- return this;
+ if (string.IsNullOrWhiteSpace(filterExpression))
+ throw new ArgumentException("Match filter expression cannot be empty", nameof(filterExpression));
+
+ return AddOption("--match-filter", filterExpression.Trim());
}
- #endregion
+ ///
+ /// Download only the video, if the URL refers to a video and a playlist
+ ///
+ ///
+ public Ytdlp WithNoPlaylist() => AddFlag("--no-playlist");
- #region Download & Post-Processing Options
+ ///
+ /// Download the playlist, if the URL refers to a video and a playlist
+ ///
+ ///
+ public Ytdlp WithYesPlaylist() => AddFlag("--yes-playlist");
///
- /// Embeds metadata into the output file.
+ /// Download only videos suitable for the given age
///
- /// The current instance for chaining.
- public Ytdlp EmbedMetadata()
+ ///
+ public Ytdlp WithAgeLimit(int years)
{
- _commandBuilder.Append("--embed-metadata ");
- return this;
+ if (years < 0) throw new ArgumentOutOfRangeException(nameof(years));
+ return AddOption("--age-limit", years.ToString());
}
///
- /// Embeds thumbnail into the output file.
+ /// Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it
///
- /// The current instance for chaining.
- public Ytdlp EmbedThumbnail()
+ ///
+ ///
+ ///
+ public Ytdlp WithDownloadArchive(string archivePath = "archive.txt")
{
- _commandBuilder.Append("--embed-thumbnail ");
- return this;
+ if (string.IsNullOrWhiteSpace(archivePath))
+ throw new ArgumentException("Archive path cannot be empty", nameof(archivePath));
+ return AddOption("--download-archive", Path.GetFullPath(archivePath));
}
///
- /// Downloads thumbnails as separate files.
+ /// Abort after downloading number files
///
- /// The current instance for chaining.
- public Ytdlp DownloadThumbnails()
+ ///
+ public Ytdlp WithMaxDownloads(int count)
{
- _commandBuilder.Append("--write-thumbnail ");
- return this;
+ if (count < 1) throw new ArgumentOutOfRangeException(nameof(count));
+ return AddOption("--max-downloads", count.ToString());
}
///
- /// Downloads subtitles in the specified languages.
+ /// Stop the download process when encountering a file that is in the archive supplied with the option
///
- /// Language codes (default: "all").
- /// The current instance for chaining.
- /// Thrown if languages is empty.
- public Ytdlp DownloadSubtitles(string languages = "all")
- {
- _commandBuilder.Append($"--write-sub --sub-langs {SanitizeInput(languages)} ");
- return this;
- }
+ ///
+ 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());
- public Ytdlp DownloadLivestream(bool fromStart = true)
+ ///
+ /// Number of retries for a fragment (default is 10), or -1 for "infinite" (DASH, hlsnative and ISM)
+ ///
+ ///
+ public Ytdlp WithFragmentRetries(int retries)
{
- _commandBuilder.Append(fromStart ? "--live-from-start " : "--no-live-from-start ");
- return this;
+ // -1 = infinite
+ string value = retries < 0 ? "infinite" : retries.ToString();
+ return AddOption("--fragment-retries", value);
}
- public Ytdlp DownloadLiveStreamRealTime()
+ ///
+ /// 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
+ ///
+ 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);
+
+ ///
+ /// Do not automatically adjust the buffer size
+ ///
+ ///
+ public Ytdlp WithNoResizeBuffer() => AddFlag("--no-resize-buffer");
+
+ ///
+ /// Download playlist videos in random order
+ ///
+ public Ytdlp WithPlaylistRandom() => AddFlag("--playlist-random");
+
+ ///
+ /// 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");
+
+ ///
+ /// 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.
+ /// Needs ffmpeg. This option can be used multiple times to download multiple sections
+ ///
+ /// e.g. "*10:15-inf", "intro"
+ ///
+ public Ytdlp WithDownloadSections(string regex)
{
- _commandBuilder.Append("--live-from-start --recode-video mp4 ");
- return this;
+ if (string.IsNullOrWhiteSpace(regex)) return this;
+ return AddOption("--download-sections", regex);
}
- public Ytdlp DownloadSections(string timeRanges)
- {
- if (string.IsNullOrWhiteSpace(timeRanges))
- throw new ArgumentException("Time ranges cannot be empty.", nameof(timeRanges));
- _commandBuilder.Append($"--download-sections {SanitizeInput(timeRanges)} ");
- return this;
- }
+ #endregion
+
+ #region Filesystem Options
- public Ytdlp DownloadAudioAndVideoSeparately()
+ ///
+ /// Sets the home folder for yt-dlp (used for config or as base directory).
+ /// Path is automatically normalized and quoted.
+ ///
+ ///
+ ///
+ public Ytdlp WithHomeFolder(string? path)
{
- _commandBuilder.Append("--write-video --write-audio ");
- return this;
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Home folder path required");
+ return new Ytdlp(this, homeFolder: Path.GetFullPath(path));
}
- public Ytdlp PostProcessFiles(string operation)
+ ///
+ /// Sets the temporary folder for yt-dlp intermediate files (fragments, etc.).
+ /// Path is automatically normalized and quoted.
+ ///
+ ///
+ ///
+ public Ytdlp WithTempFolder(string? path)
{
- if (string.IsNullOrWhiteSpace(operation))
- throw new ArgumentException("Operation cannot be empty.", nameof(operation));
-
- _commandBuilder.Append($"--postprocessor-args \"{SanitizeInput(operation)}\" ");
- return this;
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Temp folder path required");
+ return new Ytdlp(this, tempFolder: Path.GetFullPath(path));
}
- public Ytdlp MergePlaylistIntoSingleVideo(string format)
+ ///
+ /// Sets the output folder
+ ///
+ ///
+ ///
+ public Ytdlp WithOutputFolder(string path)
{
- if (string.IsNullOrWhiteSpace(format))
- throw new ArgumentException("Format cannot be empty.", nameof(format));
-
- _commandBuilder.Append($"--merge-output-format {SanitizeInput(format)} ");
- return this;
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Output folder path required");
+ return new Ytdlp(this, outputFolder: Path.GetFullPath(path));
}
- public Ytdlp ConcatenateVideos()
+ ///
+ /// Output filename template
+ ///
+ ///
+ public Ytdlp WithOutputTemplate(string template)
{
- _commandBuilder.Append("--concat-playlist always ");
- return this;
+ if (string.IsNullOrWhiteSpace(template)) throw new ArgumentException("Template required");
+ return new Ytdlp(this, outputTemplate: template.Trim());
}
- public Ytdlp ReplaceMetadata(string field, string regex, string replacement)
+ ///
+ /// 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 (string.IsNullOrWhiteSpace(field) || string.IsNullOrWhiteSpace(regex) || replacement == null)
- throw new ArgumentException("Metadata field, regex, and replacement cannot be empty.");
+ if (length < 10)
+ throw new ArgumentOutOfRangeException(nameof(length), "Length should be at least 10 characters");
- _commandBuilder.Append($"--replace-in-metadata {SanitizeInput(field)} {SanitizeInput(regex)} {SanitizeInput(replacement)} ");
- return this;
+ return AddOption("--trim-filenames", length.ToString());
}
///
- /// Keeps temporary/intermediate files after processing.
+ /// 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
///
- /// True to keep temp files.
- /// The current instance for chaining.
- public Ytdlp SetKeepTempFiles(bool keep)
+ 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 ,
+ ///
+ 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 path)
{
- if (keep) _commandBuilder.Append("-k");
- return this;
+ if (string.IsNullOrWhiteSpace(path))
+ throw new ArgumentException("Json file path cannot be empty.", nameof(path));
+ return AddOption("--load-info-json", path);
}
- public Ytdlp SetDownloadTimeout(string timeout)
+ ///
+ /// Netscape formatted file to read cookies from and dump cookie jar in
+ ///
+ ///
+ ///
+ public Ytdlp WithCookiesFile(string path)
{
- if (string.IsNullOrWhiteSpace(timeout))
- throw new ArgumentException("Timeout cannot be empty.", nameof(timeout));
-
- _commandBuilder.Append($"--download-timeout {SanitizeInput(timeout)} ");
- return this;
+ if (string.IsNullOrWhiteSpace(path))
+ throw new ArgumentException("Cookie file path cannot be empty.", nameof(path));
+ return new Ytdlp(this, cookiesFile: Path.GetFullPath(path));
}
- public Ytdlp SetTimeout(TimeSpan timeout)
+ ///
+ /// 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 (timeout.TotalSeconds <= 0)
- throw new ArgumentException("Timeout must be greater than zero.", nameof(timeout));
+ if (allSizes)
+ return AddFlag("--write-all-thumbnails");
- _commandBuilder.Append($"--timeout {timeout.TotalSeconds} ");
- return this;
+ return AddFlag("--write-thumbnail");
}
+
+ #endregion
+
+ #region Verbosity and Simulation Options
+
///
- /// Sets number of retries for failed downloads/fragments.
+ /// Activate quiet mode. If used with --verbose, print the log to stderr
///
- /// Retry count or "infinite".
- /// The current instance for chaining.
- public Ytdlp SetRetries(string retries)
+ 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)
{
- _commandBuilder.Append($"--retries {SanitizeInput(retries)} ");
- return this;
+ if (string.IsNullOrWhiteSpace(header) || string.IsNullOrWhiteSpace(value))
+ throw new ArgumentException("Header and value cannot be empty.");
+ return AddOption("--add-headers", $"{header}:{value}");
}
///
- /// Limits download speed (e.g. "500K", "1M").
+ /// 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
///
- /// Rate limit string.
- /// The current instance for chaining.
- public Ytdlp SetDownloadRate(string rate)
+ ///
+ ///
+ ///
+ ///
+ public Ytdlp WithSleepInterval(double seconds, double? maxSeconds = null)
{
- _commandBuilder.Append($"--limit-rate {SanitizeInput(rate)} ");
- 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!);
}
///
- /// Skips already downloaded files using an archive.
+ /// Number of seconds to sleep before each subtitle download
///
- /// The current instance for chaining.
- public Ytdlp SkipDownloaded()
+ ///
+ ///
+ ///
+ public Ytdlp WithSleepSubtitles(double seconds)
{
- _commandBuilder.Append("--download-archive downloaded.txt "); return this;
+ if (seconds <= 0) throw new ArgumentOutOfRangeException(nameof(seconds));
+ return AddOption("--sleep-subtitles", seconds.ToString("F2", CultureInfo.InvariantCulture));
}
#endregion
- #region Authentication & Security
+ #region Video Format Options
///
- /// Sets username and password for authentication.
+ /// Video format code
///
- /// Username or email.
- /// Password.
- /// The current instance for chaining.
- public Ytdlp SetAuthentication(string username, string password)
+ ///
+ 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)
{
- if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
- throw new ArgumentException("Username and password cannot be empty.");
+ // Common values: mp4, mkv, webm, mov, avi, flv
+ if (string.IsNullOrWhiteSpace(format))
+ throw new ArgumentException("Merge output format cannot be empty", nameof(format));
- _commandBuilder.Append($"--username {SanitizeInput(username)} --password {SanitizeInput(password)} ");
- return this;
+ return AddOption("--merge-output-format", format.Trim().ToLowerInvariant());
}
+ #endregion
+
+ #region Subtitle Options
+
///
- /// Loads cookies from a file.
+ /// Write subtitle file
///
- /// Path to cookies file (Netscape format).
- /// The current instance for chaining.
- public Ytdlp UseCookies(string cookieFile)
+ /// 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)
{
- if (string.IsNullOrWhiteSpace(cookieFile))
- throw new ArgumentException("Cookie file path cannot be empty.", nameof(cookieFile));
+ var flags = new List { "--write-subs" };
+ if (auto) flags.Add("--write-auto-subs");
- _commandBuilder.Append($"--cookies {SanitizeInput(cookieFile)} ");
- return this;
+ return new Ytdlp(this, extraFlags: flags, extraOptions: new[] { ("--sub-langs", languages) });
}
+ #endregion
+
+ #region Authentication Options
+
///
- /// Adds a custom HTTP header.
+ /// Login with this account ID and account password.
///
- /// Header name (e.g. "Referer").
- /// Header value.
- /// The current instance for chaining.
- public Ytdlp SetCustomHeader(string header, string value)
+ /// Account ID
+ /// Account password
+ ///
+ public Ytdlp WithAuthentication(string username, string password)
{
- if (string.IsNullOrWhiteSpace(header) || string.IsNullOrWhiteSpace(value))
- throw new ArgumentException("Header and value cannot be empty.");
-
- _commandBuilder.Append($"--add-header \"{SanitizeInput(header)}:{SanitizeInput(value)}\" ");
- return this;
+ 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 Network & Headers
+ #region Post-Processing Options
///
- /// Sets custom User-Agent header.
+ /// Convert video files to audio-only files (requires ffmpeg and ffprobe).
///
- /// User-Agent string.
- /// The current instance for chaining.
- public Ytdlp SetUserAgent(string userAgent)
+ /// 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)
{
- if (string.IsNullOrWhiteSpace(userAgent))
- throw new ArgumentException("User agent cannot be empty.", nameof(userAgent));
+ return this
+ .AddFlag("--extract-audio")
+ .AddOption("--audio-format", format.ToString().ToLowerInvariant())
+ .AddOption("--audio-quality", quality.ToString());
+ }
- _commandBuilder.Append($"--user-agent {SanitizeInput(userAgent)} ");
- return this;
+ ///
+ /// 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;
}
///
- /// Sets custom Referer header.
+ /// Give these arguments to the postprocessors. Specify the postprocessor/executable name and to give the argument to the specified
///
- /// Referer URL.
- /// The current instance for chaining.
- public Ytdlp SetReferer(string referer)
+ /// 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(referer))
- throw new ArgumentException("Referer URL cannot be empty.", nameof(referer));
+ if (string.IsNullOrWhiteSpace(args))
+ throw new ArgumentException("Both postprocessor name and arguments are required");
- _commandBuilder.Append($"--referer {SanitizeInput(referer)} ");
- return this;
+ string combined = $"{postprocessor.ToString().Trim()}:{args.Trim()}";
+ return AddOption("--postprocessor-args", combined);
}
///
- /// Uses a proxy server for all requests.
+ /// Keep the intermediate video file on disk after post-processing
///
- /// 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));
+ public Ytdlp WithKeepVideo() => AddFlag("-k");
+
+ ///
+ /// Do not overwrite post-processed files
+ ///
+ public Ytdlp WithNoPostOverwrites() => AddFlag("--no-post-overwrites");
- _commandBuilder.Append($"--proxy {SanitizeInput(proxy)} ");
- return this;
+ ///
+ /// 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;
}
///
- /// Disables advertisements where supported.
+ /// Embed thumbnail in the video as cover art
+ ///
+ public Ytdlp WithEmbedThumbnail() => AddFlag("--embed-thumbnail");
+
+ ///
+ /// Embed metadata to the video file
+ ///
+ public Ytdlp WithEmbedMetadata() => AddFlag("--embed-metadata");
+
+ ///
+ /// Add chapter markers to the video file
///
- /// The current instance for chaining.
- public Ytdlp DisableAds()
+ 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)
{
- _commandBuilder.Append("--no-ads ");
- return this;
+ 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}");
}
- #endregion
+ ///
+ /// 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);
- #region Playlist & Selection
+ ///
+ /// 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);
+ }
- public Ytdlp SelectPlaylistItems(string items)
+ ///
+ /// Convert the thumbnails to another format. You can specify multiple rules using similar WithRemuxVideo().
+ ///
+ /// (currently supported: jpg, png, webp)
+ ///
+ public Ytdlp WithConvertThumbnails(string format = "jpg")
{
- if (string.IsNullOrWhiteSpace(items))
- throw new ArgumentException("Playlist items cannot be empty.", nameof(items));
+ // Supported: jpg, png, webp
+ if (string.IsNullOrWhiteSpace(format))
+ throw new ArgumentException("Thumbnail format cannot be empty", nameof(format));
- _commandBuilder.Append($"--playlist-items {SanitizeInput(items)} ");
- return this;
+ return AddOption("--convert-thumbnails", format.Trim().ToLowerInvariant());
}
+ ///
+ /// 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");
+
#endregion
- #region Logging & Simulation
+ #region SponsorBlock Options
+
+ ///
+ /// 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);
+
+ ///
+ /// 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);
///
- /// Writes yt-dlp log output to a file.
+ /// Disable both WithSponsorblockMark() and WithSponsorblockRemove() options and do not use any sponsorblock features
///
- /// Path to log file.
- /// The current instance for chaining.
- public Ytdlp LogToFile(string logFile)
+ ///
+ public Ytdlp WithNoSponsorblock() => AddFlag("--no-sponsorblock");
+
+ #endregion
+
+ #region Core
+ public Ytdlp AddFlag(string flag) => new Ytdlp(this, extraFlags: new[] { flag.Trim() });
+
+ 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(logFile))
- throw new ArgumentException("Log file path cannot be empty.", nameof(logFile));
+ 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!);
+ }
- _commandBuilder.Append($"--write-log {SanitizeInput(logFile)} ");
- return this;
+ public Ytdlp WithAria2(int connections = 16)
+ {
+ return new Ytdlp(this, extraOptions: new[]
+ {
+ ("--downloader", "aria2c"),
+ ("--downloader-args", $"aria2c:-x{connections} -k1M")
+ });
}
+ public Ytdlp WithHlsNative() => AddOption("--downloader", "hlsnative");
+
+ public Ytdlp WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null) => WithExternalDownloader("ffmpeg", extraFfmpegArgs);
+
+ #endregion
+
+ #region Redundant options
+
///
- /// Simulates download without saving files.
+ /// Playlist start index
///
- /// The current instance for chaining.
- public Ytdlp Simulate()
+ ///
+ ///
+ ///
+ public Ytdlp WithPlaylistStart(int index)
{
- _commandBuilder.Append("--simulate ");
- return this;
+ if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1");
+ return AddOption("--playlist-start", index.ToString());
}
///
- /// Ignore warnings
+ /// Playlist end index
///
- /// The current instance for chaining.
- public Ytdlp NoWarning()
+ ///
+ ///
+ ///
+ public Ytdlp WithPlaylistEnd(int index)
{
- _commandBuilder.Append("--no-warnings ");
- return this;
+ if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), "Must be >= 1");
+ return AddOption("--playlist-end", index.ToString());
}
- #endregion
-
- #region Advanced & Specialized Options
+ public Ytdlp WithUserAgent(string userAgent)
+ {
+ if (string.IsNullOrWhiteSpace(userAgent))
+ throw new ArgumentException("User-Agent cannot be empty", nameof(userAgent));
+ return AddOption("--user-agent", userAgent.Trim());
+ }
- public Ytdlp WithConcurrentFragments(int count)
+ public Ytdlp WithReferer(string referer)
{
- if (count < 1) throw new ArgumentOutOfRangeException(nameof(count));
- _commandBuilder.Append($"--concurrent-fragments {count} ");
- return this;
+ if (string.IsNullOrWhiteSpace(referer))
+ throw new ArgumentException("Referer cannot be empty", nameof(referer));
+ return AddOption("--referer", referer.Trim());
}
- public Ytdlp RemoveSponsorBlock(params string[] categories)
+ public Ytdlp WithMatchTitle(string regex)
{
- var cats = categories.Length == 0 ? "all" : string.Join(",", categories);
- _commandBuilder.Append($"--sponsorblock-remove {SanitizeInput(cats)} ");
- return this;
+ if (string.IsNullOrWhiteSpace(regex))
+ throw new ArgumentException("Regex cannot be empty", nameof(regex));
+ return AddOption("--match-title", regex.Trim());
}
- public Ytdlp EmbedSubtitles(string languages = "all", string? convertTo = null)
+ public Ytdlp WithRejectTitle(string regex)
{
- _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;
+ if (string.IsNullOrWhiteSpace(regex))
+ throw new ArgumentException("Regex cannot be empty", nameof(regex));
+ return AddOption("--reject-title", regex.Trim());
}
- public Ytdlp CookiesFromBrowser(string browser, string? profile = null)
- {
- var arg = profile != null ? $"{browser}:{profile}" : browser;
- _commandBuilder.Append($"--cookies-from-browser {SanitizeInput(arg)} ");
- return this;
- }
+ public Ytdlp WithBreakOnReject() => AddFlag("--break-on-reject");
+ #endregion
+
+ #region Bonus
+
+ public Ytdlp WithBestUpTo1440p() => new Ytdlp(this, format: "bestvideo[height<=?1440]+bestaudio/best");
+ public Ytdlp With1080pOrBest() => new Ytdlp(this, format: "bestvideo[height<=?1080]+bestaudio/best");
+
+ 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");
+
+ public Ytdlp WithMp4PostProcessingPreset()
+ => this
+ .WithRemuxVideo(MediaFormat.Mp4)
+ .WithEmbedMetadata()
+ .WithEmbedChapters()
+ .WithEmbedThumbnail();
- public Ytdlp GeoBypassCountry(string countryCode)
+ public Ytdlp WithMkvOutput()
+ => this
+ .WithRemuxVideo(MediaFormat.Mkv)
+ .WithMergeOutputFormat("mkv");
+
+ public Ytdlp WithMaxHeight(int height)
{
- if (countryCode.Length != 2) throw new ArgumentException("Country code must be 2 letters.");
- _commandBuilder.Append($"--geo-bypass-country {SanitizeInput(countryCode.ToUpperInvariant())} ");
- return this;
+ if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive");
+
+ string formatSelector = $"bestvideo[height<={height}]+bestaudio/best";
+ return new Ytdlp(this, format: formatSelector);
}
- public Ytdlp AddCustomCommand(string customCommand)
+ public Ytdlp WithMaxHeightOrBest(int height)
{
- if (string.IsNullOrWhiteSpace(customCommand))
- throw new ArgumentException("Custom command cannot be empty.", nameof(customCommand));
+ if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive");
- var parts = customCommand
- .Split(' ', StringSplitOptions.RemoveEmptyEntries)
- .Select(SanitizeInput)
- .ToArray();
+ string formatSelector = $"bestvideo[height<={height}]+bestaudio/best[height<={height}]/best";
+ return new Ytdlp(this, format: formatSelector);
+ }
- // Validate only option tokens (flags)
- foreach (var part in parts)
- {
- 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;
- }
- }
+ public Ytdlp WithBestVideoPlusBestAudio() => new Ytdlp(this, format: "bestvideo+bestaudio/best");
- _commandBuilder.Append(' ').Append(string.Join(' ', parts));
+ public Ytdlp WithBestAudioOnly() => new Ytdlp(this, format: "bestaudio");
- return this;
- }
+ public Ytdlp WithNo4k() => new Ytdlp(this, format: "bestvideo[height<=?2160]+bestaudio/best");
+ public Ytdlp WithBestM4aAudio() => new Ytdlp(this, format: "bestaudio[ext=m4a]/bestaudio/best");
#endregion
+ // ==================================================================================================================
+ // Probe and Download Functions
+ // ==================================================================================================================
+
#region Execution & Utility Methods
///
- /// Preview Commands
+ /// Command preview ofr debug operatons
///
///
///
- ///
- public string PreviewCommand(string url)
+ public string Preview(string url)
{
- if (string.IsNullOrWhiteSpace(url))
- throw new ArgumentException("URL cannot be empty.", nameof(url));
-
- string template = Path.Combine(_outputFolder, _outputTemplate.Replace("\\", "/"));
-
- string arguments = $"{_commandBuilder} -f \"{_format}\" -o \"{template}\" {SanitizeInput(url)}";
-
- return $"{_ytDlpPath} {arguments}";
+ var argsList = BuildArguments(url);
+ return string.Join(" ", argsList.Select(Quote));
}
-
///
/// Retrieves the current version string of the underlying yt-dlp executable.
///
@@ -809,9 +1263,9 @@ public string PreviewCommand(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);
+ 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;
@@ -821,14 +1275,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;
@@ -841,7 +1295,45 @@ public async Task UpdateAsync(UpdateChannel channel = UpdateChannel.Stab
return "yt-dlp update check completed (no changes detected).";
-
+
+ }
+
+ ///
+ /// 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();
+ }
}
///
@@ -870,9 +1362,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 +1419,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))
{
@@ -962,12 +1454,12 @@ 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));
- 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 +1504,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 +1567,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;
@@ -1156,52 +1648,78 @@ public async Task GetBestVideoFormatIdAsync(string url, int maxHeight =
return best?.FormatId ?? "bestvideo";
}
-
///
- /// Executes a download process for the specified URL using an optional output template.
+ /// Executes download processing for a URL.
///
- /// The source URL to process.
- /// A to terminate the process execution.
- ///
- /// A representing the asynchronous execution of the process.
- ///
+ /// The source URL to download.
+ /// A to stop the execution.
+ ///
///
///
- public async Task ExecuteAsync(string url, CancellationToken ct = default)
+ public async Task DownloadAsync(string url, CancellationToken ct = default)
{
- if (string.IsNullOrWhiteSpace(url))
- throw new ArgumentException("URL cannot be empty.", nameof(url));
+ ct.ThrowIfCancellationRequested();
- if (string.IsNullOrWhiteSpace(_format))
- _format = "best";
+ 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, $"Output folder: {Path.GetFullPath(_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 {_outputFolder}: {ex.Message}");
- throw new YtdlpException($"Failed to create output folder {_outputFolder}", ex);
+ _logger.Log(LogType.Error, $"Failed to create necessary folders: {ex.Message}");
+ throw new YtdlpException("Failed to create required folders", ex);
}
- // Reset ProgressParser for this download
- _progressParser.Reset();
- _logger.Log(LogType.Info, $"Starting download for URL: {url}");
+ 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);
- // Use provided template or default
- string template = Path.Combine(_outputFolder, _outputTemplate.Replace("\\", "/"));
+ // Forward progress events locally inside this method
+ void OnProgressDownloadHandler(object? s, DownloadProgressEventArgs e)
+ => OnProgressDownload?.Invoke(this, e);
- // Build command with format and output template
- string arguments = $"{_commandBuilder} -f \"{_format}\" -o \"{template}\" {SanitizeInput(url)}";
+ void OnProgressMessageHandler(object? s, string msg)
+ => OnProgressMessage?.Invoke(this, msg);
- _logger.Log(LogType.Info, arguments);
+ // Attach progress handlers
+ progressParser.OnProgressDownload += OnProgressDownloadHandler;
+ progressParser.OnProgressMessage += OnProgressMessageHandler;
- _commandBuilder.Clear(); // Clear after building arguments
+ // 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);
- await _download.RunAsync(arguments, ct);
+ 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;
+ }
}
///
@@ -1214,7 +1732,7 @@ public async Task ExecuteAsync(string url, CancellationToken ct = default)
/// 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())
{
@@ -1222,17 +1740,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 =>
@@ -1240,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)
{
@@ -1257,291 +1764,157 @@ 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;
- }
+ #region Helpers
- private static string SanitizeInput(string input)
+ // Get probe runner
+ private ProbeRunner Probe()
{
- if (string.IsNullOrEmpty(input))
- return input;
-
- // escape internal quotes
- input = input.Replace("\"", "\\\"");
-
- // wrap with quotes (CRITICAL for paths with spaces)
- return $"\"{input}\"";
+ // Create isolated execution components
+ var factory = new ProcessFactory(_ytdlpPath);
+ return new ProbeRunner(factory, _logger);
}
- private List ParseFormats(string result)
+ private List BuildArguments(string url)
{
- var formats = new List();
- if (string.IsNullOrWhiteSpace(result)) return formats;
+ var args = new List();
- var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- bool inFormatSection = false;
+ bool usingAbsoluteOutput = !string.IsNullOrWhiteSpace(_outputFolder);
- foreach (var line in lines)
+ if (usingAbsoluteOutput && !string.IsNullOrWhiteSpace(_tempFolder))
{
- if (line.Contains("[info] Available formats")) { inFormatSection = true; continue; }
- if (!inFormatSection || line.Contains("RESOLUTION") || line.StartsWith("---")) continue;
- if (string.IsNullOrWhiteSpace(line) || !Regex.IsMatch(line, @"^\S+\s+\S+")) break;
-
- try
- {
- var format = Format.FromParsedLine(line);
- if (!string.IsNullOrEmpty(format.Id) && !formats.Exists(f => f.Id == format.Id))
- formats.Add(format);
- }
- catch (Exception ex)
- {
- _logger.Log(LogType.Warning, $"Failed parsing format line: {line} → {ex.Message}");
- }
+ _logger.Log(LogType.Debug, "Temp folder ignored because absolute output template is used.");
}
- _logger.Log(LogType.Info, $"Parsed {formats.Count} formats");
- 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
+ // temp folder
+ if (!usingAbsoluteOutput && !string.IsNullOrWhiteSpace(_tempFolder))
{
- 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.");
- }
+ args.Add("--paths");
+ args.Add($"temp:{_tempFolder.Replace("\\", "/")}");
+ }
- // JSON options
- var jsonOptions = new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true,
- NumberHandling = JsonNumberHandling.AllowReadingFromString,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
- };
+ // home folder only if NOT using absolute output
+ if (!usingAbsoluteOutput && !string.IsNullOrWhiteSpace(_homeFolder))
+ {
+ args.Add("--paths");
+ args.Add($"home:{_homeFolder.Replace("\\", "/")}");
+ }
- // Deserialize
- var videoInfo = JsonSerializer.Deserialize(json, jsonOptions);
+ // Output template
+ if (!string.IsNullOrWhiteSpace(_outputTemplate))
+ {
+ args.Add("-o");
- if (videoInfo?.Formats == null || !videoInfo.Formats.Any())
+ if (usingAbsoluteOutput)
{
- _logger.Log(LogType.Warning, "No formats array in JSON or empty → falling back to -F");
- return await GetAvailableFormatsAsync(url, ct);
- }
+ var full = Path.Combine(_outputFolder!, _outputTemplate)
+ .Replace("\\", "/");
- // 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);
+ args.Add(full);
}
-
- if (detailedFormats.Count > 0)
+ else
{
- _logger.Log(LogType.Info, $"Successfully parsed {detailedFormats.Count} detailed formats from JSON");
- return detailedFormats;
+ args.Add(_outputTemplate);
}
-
- _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)
+
+ // Format
+ if (!string.IsNullOrWhiteSpace(_format))
{
- _logger.Log(LogType.Warning, "Format fetch cancelled");
- throw;
+ args.Add("-f");
+ args.Add(_format);
}
- catch (Exception ex)
+
+ // Concurrent fragments
+ if (_concurrentFragments > 1)
{
- _logger.Log(LogType.Error, $"Unexpected error in GetFormatsDetailedAsync: {ex.Message} → fallback");
+ args.Add("--concurrent-fragments");
+ args.Add(_concurrentFragments.Value.ToString());
}
- // 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));
+ // Flags
+ if (_flags.Length > 0)
+ args.AddRange(_flags);
- try
+ // Options
+ if (_options.Length > 0)
{
- // Use a rare separator that is unlikely to appear in title/description
- const string separator = "|||YTDLP.NET|||";
-
- var fields = new[]
+ foreach (var kv in _options)
{
- "%(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
- };
+ args.Add(kv.Key);
+ if (kv.Value != null)
+ args.Add(kv.Value);
+ }
+ }
- var printArg = $"--print \"{string.Join(separator, fields)}\"";
+ // 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); }
- var arguments = $"{printArg} --skip-download --no-playlist --quiet {SanitizeInput(url)}";
+ // URL last
+ args.Add(url);
- var output = await _probe.RunAsync(arguments, ct, bufferKb);
+ return args;
+ }
- if (string.IsNullOrWhiteSpace(output))
- return null;
+ private static string ValidatePath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ throw new ArgumentException("yt-dlp path cannot be empty");
- var parts = output.Trim().Split(separator);
+ if (!File.Exists(path) && !IsExecutableInPath(path))
+ throw new FileNotFoundException($"yt-dlp executable not found: {path}");
- if (parts.Length < 6) // at least id, title, duration, thumbnail, views, size
- return null;
+ return path;
+ }
- 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;
- }
+ 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}\"";
}
- [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)
+ private List ParseFormats(string result)
{
- if (string.IsNullOrWhiteSpace(url))
- throw new ArgumentException("URL cannot be empty.", nameof(url));
+ var formats = new List();
+ if (string.IsNullOrWhiteSpace(result)) return formats;
- if (fields == null || !fields.Any())
- throw new ArgumentException("At least one field must be requested.", nameof(fields));
+ var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ bool inFormatSection = false;
- try
+ foreach (var line in lines)
{
- 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);
+ if (line.Contains("[info] Available formats")) { inFormatSection = true; continue; }
+ if (!inFormatSection || line.Contains("RESOLUTION") || line.StartsWith("---")) continue;
+ if (string.IsNullOrWhiteSpace(line) || !Regex.IsMatch(line, @"^\S+\s+\S+")) break;
- int index = 0;
- foreach (var field in fields)
+ try
{
- var value = parts[index++].Trim();
- result[field] = value;
+ var format = Format.FromParsedLine(line);
+ if (!string.IsNullOrEmpty(format.Id) && !formats.Exists(f => f.Id == format.Id))
+ formats.Add(format);
+ }
+ catch (Exception ex)
+ {
+ _logger.Log(LogType.Warning, $"Failed parsing format line: {line} → {ex.Message}");
}
-
- 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;
}
+
+ _logger.Log(LogType.Info, $"Parsed {formats.Count} formats");
+ return formats;
}
+
#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 81%
rename from src/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs
rename to tests/Ytdlp.NET.Test/Obsolete/ParseFormatTest.cs
index 505964d..8ff402f 100644
--- a/src/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/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/Obsolete/ProgressParserTests.cs b/tests/Ytdlp.NET.Test/ProgressParserTests.cs
similarity index 98%
rename from src/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs
rename to tests/Ytdlp.NET.Test/ProgressParserTests.cs
index e070387..5ce32ed 100644
--- a/src/Ytdlp.NET.Test/Obsolete/ProgressParserTests.cs
+++ b/tests/Ytdlp.NET.Test/ProgressParserTests.cs
@@ -1,5 +1,5 @@
-using System.Text.RegularExpressions;
-using YtdlpDotNet;
+using ManuHub.Ytdlp.NET;
+using System.Text.RegularExpressions;
namespace Ytdlp.NET.Test.Obsolete;
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