Ytdlp.NET is a fluent, strongly-typed, and immutable .NET wrapper around
yt-dlp. It provides a fully asynchronous, event-driven interface for downloading, metadata extraction, and media processing from YouTube and hundreds of other platforms.
ClipMate - MAUI.NET App - Download
Video Downloader - .NET App - Download
- Fluent API: Build yt-dlp commands with
WithXxx()methods. - Immutable & thread-safe: Each method returns a new instance, safe for parallel usage.
- 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.
- Lifecycle Refinement: No disposal required. The library no longer implements
IDisposableorIAsyncDisposable. Instances are plain configuration objects. - Advanced Execution: New
ExecuteRawAsync()for power users who need to execute custom commands that bypass the fluent builder. - Deep Metadata Support: Added
GetDeepMetadataAsync()andGetDeepMetadataRawAsync()for full hierarchical structure (playlists → seasons → episodes). - Traverse Helper: New
Traverse()method for easy iteration over nested playlist entries. - Improved Auth: Enhanced
WithAdobePassAuthentication()andWithAuthentication()handling. - Subtitle Extraction: New
GetSubtitlesAsync()for streamlined subtitle retrieval. - MSO Listing: New
GetAdobePassListAsync()for Adobe Pass mso listing. - Robust Core: Modernized
ProcessRunnerandProcessFactoryfor efficient, isolated execution
- Namespace:
ManuHub.Ytdlp.NET - External JS runtime: yt-dlp requires an external JS runtime like deno.exe (from denoland/deno) for YouTube downloads with JS challenges.
tools/
├─ yt-dlp.exe
├─ ffmpeg.exe
├─ ffprobe.exe
└─ deno.exe
- Recommended: Use companion NuGet packages:
| Package | Description |
|---|---|
| ManuHub.Ytdlp | Core download engine |
| ManuHub.Deno | JavaScript challenge resolution |
| ManuHub.FFmpeg | Post-processing, merging, and conversion |
| ManuHub.FFprobe | Format probing and metadata extraction |
Example path resolution in .NET:
var ytdlpPath = Path.Combine(AppContext.BaseDirectory, "tools", "yt-dlp.exe");
var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "tools");Ytdlp holds no unmanaged resources. Create instances, share them, and let the GC collect them. All internal runners are created per-call.
Every configuration method (e.g., WithOutputFolder, WithFormat) returns a new instance, ensuring the original is never modified. This makes branching configurations safe and clean.
A single Ytdlp instance can be shared across threads. Each execution creates isolated internal runners, allowing concurrent downloads without synchronization.
Implemented secure authentication handling for various scenarios, including standard username/password and Adobe Pass authentication.
- .WithAuthentication(string username, string password)
- .WithAdobePassAuthentication(string mso, string username, string password)
It securely handles credentials by passing them via standard input to the yt-dlp process, avoiding exposure in command-line arguments or logs. The library ensures that sensitive information is not stored in memory longer than necessary and is properly disposed of after use.
var ytdlp = new Ytdlp("yt-dlp.exe")
.WithOutputFolder("./downloads")
.WithBestVideoPlusBestAudio()
.WithEmbedMetadata();
// Subscribe to events
ytdlp.ProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%");
ytdlp.DownloadCompleted += (s, msg) => Console.WriteLine($"Finished: {msg}");
// Execute
await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=XXX");// Define a shared base configuration
var baseConfig = new Ytdlp("yt-dlp.exe").WithOutputFolder("./media");
// Create specialized versions
var audioOnly = baseConfig.WithBestAudioOnly();
var highRes = baseConfig.WithMaxHeightOrBest(1080);
// baseConfig, audioOnly, and highRes are independent, thread-safe instances
await Task.WhenAll(
audioOnly.DownloadAsync(url1),
highRes.DownloadAsync(url2)
);For power users who need to execute custom commands that bypass the fluent builder, use ExecuteRawAsync. This acts as an "escape hatch" for scenarios where specific, non-standard, or experimental yt-dlp flags are required.
This method automatically intelligently switches output handling based on how you use it:
- Streaming Mode: Provide an
Action<string>toonLineReceivedto stream output in real-time (ideal for progress monitoring or logs). - Capture Mode: Pass null to
onLineReceivedto capture the entire process output into result.FullOutput (ideal for JSON metadata probing or one-off commands).
var ytdlp = new Ytdlp("yt-dlp.exe");
// 1. Capture Mode (Probe/Manual)
var result = await ytdlp.ExecuteRawAsync("--version");
Console.WriteLine($"yt-dlp version: {result.FullOutput}");
// 2. Streaming Mode (Real-time tracking)
await ytdlp.ExecuteRawAsync("--help", onLineReceived: line => Console.WriteLine(line));Note: While
ExecuteRawAsynchandles security and formatting, logical validation (e.g., passing validyt-dlpflags) remains the responsibility of the developer. Always prefer the fluentWithXxx()methods for standard download tasks.
var ytdlp = new Ytdlp("tools\\yt-dlp.exe");
var metadata = await ytdlp.GetMetadataAsync("https://www.youtube.com/watch?v=abc123");
Console.WriteLine($"Title: {metadata?.Title}, Duration: {metadata?.Duration}");var metadata = await ytdlp.GetDeepMetadataAsync(url);
foreach (var root in metadata.Entries ?? [])
{
foreach (var item in root.Traverse())
{
Console.WriteLine(item.Title);
}
}var ytdlp = new Ytdlp("tools\\yt-dlp.exe")
.WithFormat("best")
.WithOutputFolder("./batch");
var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" };
// Safe: Concurrent usage of the same instance
await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);var ytdlp = new Ytdlp("tools\\yt-dlp.exe")
.WithExtractAudio(AudioFormat.Mp3, 5)
.WithOutputFolder("./audio")
.WithEmbedThumbnail()
.WithEmbedMetadata();
await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U");var ytdlp = new Ytdlp("tools\\yt-dlp.exe")
.WithFormat("best")
.WithOutputFolder("./playlists")
.WithPlaylistStart(1)
.WithPlaylistEnd(5)
.OutputTemplate("%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s");
await ytdlp.DownloadAsync("https://www.youtube.com/playlist?list=PL12345");var ytdlp = new Ytdlp("tools\\yt-dlp.exe");
var formats = await ytdlp.GetFormatsAsync("https://www.youtube.com/watch?v=abc123");
foreach(var format in formats)
Console.WriteLine($"Id: {metadata?.Id}, Extension: {metadata?.Extension}");var ytdlp = new Ytdlp("tools\\yt-dlp.exe");
string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url);
string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 720);
await ytdlp
.WithFormat($"{bestVideo}+{bestAudio}/best")
.WithOutputFolder("./downloads")
.DownloadAsync(url);var ytdlp = new Ytdlp("tools\\yt-dlp.exe");
var subtitles = await ytdlp.GetSubtitlesAsync("https://www.youtube.com/watch?v=abc123");
foreach (var sub in subtitles)
{
Console.WriteLine($"Language: {sub.Language}, Format: {sub.Format}, Url: {sub.Url}");
}var msoList = await ytdlp.GetAdobePassListAsync();| Event | Description |
|---|---|
ProgressDownload |
Download progress |
ProgressMessage |
Informational messages |
DownloadCompleted |
File finished |
PostProcessingStarted |
Post‑processing start |
PostProcessingCompleted |
Post‑processing finished |
OutputMessage |
Raw output line |
ErrorMessage |
Error message |
CommandCompleted |
Process finished |
// Progress events
ytdlp.ProgressDownload += (s, e) => Console.WriteLine($"{e.Percent:F1}% {e.Speed} ETA {e.ETA}");
ytdlp.ProgressMessage += (s, msg) => Console.WriteLine(msg);
// Output events
ytdlp.ErrorMessage += (s, err) => Console.WriteLine($"Error: {err}");
ytdlp.OutputMessage += (s, msg) => Console.WriteLine(msg);
// Lifecycle events
ytdlp.DownloadCompleted += (s, msg) => Console.WriteLine($"Finished: {msg}");
ytdlp.CommandCompleted += (s, e) => Console.WriteLine($"Command finished: {e.Command}");
// Post-Processing events
ytdlp.PostProcessingStarted += (s, msg) => Console.WriteLine($"Post-processing-start: {msg}");
ytdlp.PostProcessingCompleted += (s, msg) => Console.WriteLine($"Post-processing-complete: {msg}");VersionAsync()UpdateAsync(UpdateChannel channel, string specificVersion)GetExtractorsAsync()GetAdobePassListAsync()GetSubtitlesAsync(string url)GetMetadataAsync(string url)GetMetadataRawAsync(string url)GetDeepMetadataAsync(string url)GetDeepMetadataRawAsync(string url)GetFormatsAsync(string url)GetMetadataLiteAsync(string url)GetMetadataLiteAsync(string url, IEnumerable<string> fields)GetBestAudioFormatIdAsync(string url)GetBestVideoFormatIdAsync(string url, int maxHeight)
DownloadAsync(string url)DownloadBatchAsync(IEnumerable<string> urls, int maxConcurrency)
ExecuteRawAsync(string arguments, Action<string>? onLineReceived = null, CancellationToken ct = default, bool tuneProcess = true)
.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()
.WithProxy(string? proxy).WithSocketTimeout(TimeSpan timeout).WithForceIpv4().WithForceIpv6().WithEnableFileUrls()
.WithGeoVerificationProxy(string url).WithGeoBypassCountry(string countryCode)
.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()
.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)
.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()
.WithThumbnails(bool allSizes = false)
.WithQuiet().WithNoWarnings().WithSimulate().WithNoSimulate().WithSkipDownload().WithVerbose()
.WithAddHeader(string header, string value).WithSleepInterval(double seconds, double? maxSeconds = null).WithSleepSubtitles(double seconds)
.WithFormat(string format).WithMergeOutputFormat(string format)
.WithSubtitles(string languages = "all", bool auto = false)
.WithAuthentication(string username, string password).WithTwoFactor(string code).WithVideoPassword(string password).WithAdobePassAuthentication(string mso, string username, string password)
.WithExtractAudio(string format, int quality = 5).WithRemuxVideo(string format)usage 'mp4' or 'mp4>mkv'.WithRecodeVideo(string format, string? videoCodec = null, string? audioCodec = null).WithPostprocessorArgs(PostProcessors postprocessor, string args).WithKeepVideo().WithNoPostOverwrites().WithEmbedSubtitles().WithEmbedThumbnail().WithEmbedMetadata().WithEmbedChapters().WithEmbedInfoJson().WithNoEmbedInfoJson().WithReplaceInMetadata(string field, string regex, string replacement).WithConcatPlaylist(string policy = "always").WithFFmpegLocation(string? ffmpegPath).WithConvertSubtitles(string format = "none").WithConvertThumbnails(string format = "jpg").WithSplitChapters() => AddFlag("--split-chapters").WithRemoveChapters(string regex).WithForceKeyframesAtCuts().WithUsePostProcessor(PostProcessors postProcessor, string? postProcessorArgs = null)
.WithSponsorblockMark(string categories = "all").WithSponsorblockRemove(string categories = "all").WithNoSponsorblock()
.AddFlag(string flag).AddOption(string key, string value)
.WithExternalDownloader(string downloaderName, string? downloaderArgs = null).WithAria2(int connections = 16).WithHlsNative().WithFfmpegAsLiveDownloader(string? extraFfmpegArgs = null)
If you need specific arguments not covered by the fluent API:
ytdlp.AddFlag("--no-check-certificate")
.AddOption("--external-downloader", "aria2c")
.DownloadAsync(url);Version 4.0.0 is a major release that refines the API for better maintainability and removes the overhead of manual lifecycle management.
Note: The primary breaking change is the removal of
IDisposable/IAsyncDisposable. You no longer need to dispose of yourYtdlpinstances.
| Feature | v3.x | v4.x |
|---|---|---|
| Lifecycle | Required IAsyncDisposable |
No disposal required |
| Architecture | Immutable Fluent API | Immutable Fluent API (Refactored) |
| Core Process | ProcessFactory |
ProcessFactory (Refactored) |
| Core Runner | ProbeRunner DownloadRunner |
ProcessRunner |
// Previously required disposal
await using var ytdlp = new Ytdlp()
.WithFormat("best")
.WithOutputFolder("./downloads");
await ytdlp.DownloadAsync(url);// Cleaner: No disposal required
var ytdlp = new Ytdlp()
.WithFormat("best")
.WithOutputFolder("./downloads");
await ytdlp.DownloadAsync(url);We have streamlined the Ytdlp lifecycle. Because the instance does not hold unmanaged resources that require explicit cleanup, we have removed the IDisposable and IAsyncDisposable interfaces.
- Cleaner Code: Your codebase is now free of
await usingorusingstatements forYtdlpinstances. - Refactored Core: The internal
ProcessFactoryhas been updated and introduceProcessRunnerto handle process execution more efficiently without needing to manage the object lifecycle manually.
- Remove
await usingorusing: Simply delete the disposal keywords where you instantiateYtdlp. - Verify Events: Ensure event subscriptions are attached to the instance used for the specific execution.
- Dependencies: Ensure
yt-dlp(and optionallyFFmpeg/FFprobe) are available on your system path or point to their specific locations viaWithFfmpegLocation()(if configured). - Performance:
tuneProcess: true(default) is enabled for download methods to optimize output buffer management.
- ClipMate downloader
- Console examples
Contributions are welcome!
Open issues or PRs on GitHub.
MIT License — see LICENSE
Manoj Babu, ManuHub
Thanks to all contributors ❤️

