diff --git a/README.md b/README.md index 99f9078..8e08f89 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ The library exposes **event‑driven progress reporting**, **metadata probing**, * Cross‑platform support * Strongly typed event system * Async execution -* `IAsyncDisposable` support --- @@ -59,24 +58,27 @@ The library exposes **event‑driven progress reporting**, **metadata probing**, --- -### Highlights +## ✨ Features -* Improved UpdateAsync with specific version support -* Immutable **fluent builder API** -* `IAsyncDisposable` implemented -* Thread‑safe usage -* Simplified event handling -* Improved metadata probing -* Better cancellation support -* Safer command building +* **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 (where yt-dlp is supported). --- # 🔧 Required Tools -`yt-dlp` relies on external tools. +## ⚠️ Important Notes + +* **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**: -Recommended folder structure: ``` tools/ @@ -86,91 +88,243 @@ tools/ └─ deno.exe ``` -Example usage: +- **Recommended:** Use companion NuGet packages: + +| Package | Description | +|---------|-------------| +| **ManuHub.Ytdlp** | Core yt-dlp wrapper with fluent API and event handling. | +| **ManuHub.Deno** | Provides the required Deno runtime for yt-dlp for JavaScript challenges. | +| **ManuHub.FFmpeg** | Provides the required FFmpeg executable for post-processing. | +| **ManuHub.FFprobe** | Provides the required FFprobe executable for format probing. | + +Example path resolution in .NET: ```csharp -var ytdlpPath = Path.Combine("tools", "yt-dlp.exe"); +var ytdlpPath = Path.Combine(AppContext.BaseDirectory, "tools", "yt-dlp.exe"); +var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "tools"); ``` --- -# 🔧 Basic Usage +## 🚨 No Disposal Required: + +> **Ytdlp** holds no unmanaged resources and does not implement **IDisposable** or **IAsyncDisposable**. Instances are plain configuration objects — create them, share them freely, and let the GC collect them when they go out of scope. All internal runners and parsers are created per-call and cleaned up automatically after each execution. + +## 🔐 Improved Secure Authentication Support +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. + +## 🌲 Deep Metadata Support -### Download a video +Ytdlp.NET now supports **deep playlist extraction** with full hierarchical structure support (seasons → episodes → nested playlists). + +### 🔹 Flat Mode (default - no change) ```csharp -await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") +var metadata = await ytdlp.GetMetadataAsync(url); +``` + +* Fast +* Returns only top-level items +* Fully backward compatible + +--- + +### 🔹 Deep Mode (NEW) + +```csharp +var metadata = await ytdlp.GetDeepMetadataAsync(url); +``` + +* Returns full hierarchy +* Supports playlists → seasons → episodes +* Slightly slower but complete data + +--- + +## 🔁 Traverse Nested Entries + +Use this helper to read all items in deep mode: + +```csharp +foreach (var root in metadata.Entries ?? []) +{ + foreach (var item in root.Traverse()) + { + Console.WriteLine(item.Title); + } +} +``` + +--- + +## 🔧 Thread Safety + +* **Immutable & thread-safe**: Each `WithXxx()` call returns a new instance. + +### **Sequential download example**: + +```csharp +var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) + .WithFormat("best") + .WithOutputFolder("./downloads"); + +ytdlp.ProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); +ytdlp.DownloadCompleted += (s, msg) => Console.WriteLine($"Download complete: {msg}"); + +await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); +``` + +### **Parallel download example**: + +```csharp +var urls = new[] { "https://youtu.be/video1", "https://youtu.be/video2" }; + +var tasks = urls.Select(async url => +{ + var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) + .WithFormat("best") + .WithOutputFolder("./batch"); + + ytdlp.ProgressDownload += (s, e) => Console.WriteLine($"[{url}] {e.Percent:F2}%"); + ytdlp.DownloadCompleted += (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. No shared state between instances, so no need to worry about thread safety. +3. Attach events **after the `WithXxx()` call**. + +--- + + +## 📦 Basic Usage + +### Download a Single Video + +```csharp +var ytdlp = new Ytdlp("tools\\yt-dlp.exe", new ConsoleLogger()) .WithFormat("best") .WithOutputFolder("./downloads") - .WithOutputTemplate("%(title)s.%(ext)s"); + .WithEmbedMetadata() + .WithEmbedThumbnail(); -ytdlp.OnProgressDownload += (s, e) => - Console.WriteLine($"{e.Percent:F1}% {e.Speed} ETA {e.ETA}"); +ytdlp.rogressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); +ytdlp.DownloadCompleted += (s, msg) => Console.WriteLine($"Download complete: {msg}"); -await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=VIDEO_ID"); +await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); +``` + +### Extract Audio + +```csharp +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"); ``` --- -# 🎧 Extract audio +## Download a Playlist ```csharp -await using var ytdlp = new Ytdlp() - .WithExtractAudio("mp3") - .WithOutputFolder("./audio"); +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(url); +await ytdlp.DownloadAsync("https://www.youtube.com/playlist?list=PL12345"); ``` --- -# 📊 Monitor Progress +# 📊 Monitor Progress & Events ```csharp -ytdlp.OnProgressDownload += (s, e) => -{ +ytdlp.ProgressDownload += (s, e) => Console.WriteLine($"{e.Percent:F1}% {e.Speed} ETA {e.ETA}"); -}; -ytdlp.OnCompleteDownload += (s, msg) => -{ +ytdlp.DownloadCompleted += (s, msg) => Console.WriteLine($"Finished: {msg}"); -}; + +ytdlp.ProgressMessage += (s, msg) => Console.WriteLine(msg); + +ytdlp.PostProcessingStarted += (s, msg) => + Console.WriteLine($"Post-processing-start: {msg}") + +ytdlp.PostProcessingCompleted += (s, msg) => + Console.WriteLine($"Post-processing-complete: {msg}"); + +ytdlp.ErrorMessage += (s, err) => Console.WriteLine($"Error: {err}"); + +ytdlp.OutputMessage += (s, msg) => Console.WriteLine(msg); + +ytdlp.CommandCompleted += (s, e) => + Console.WriteLine($"Command finished: {e.Command}"); ``` --- -# 📦 Fetch Metadata +### Fetch Metadata ```csharp -await using var ytdlp = new Ytdlp(); +var ytdlp = new Ytdlp("tools\\yt-dlp.exe"); -var metadata = await ytdlp.GetMetadataAsync(url); +var metadata = await ytdlp.GetMetadataAsync("https://www.youtube.com/watch?v=abc123"); + +Console.WriteLine($"Title: {metadata?.Title}, Duration: {metadata?.Duration}"); +``` + +--- + +### Fetch Formats -Console.WriteLine(metadata?.Title); -Console.WriteLine(metadata?.Duration); +```csharp +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}"); ``` --- -# 🎬 Auto‑Select Best Formats +### Best Format Selection ```csharp -await using var ytdlp = new Ytdlp(); +var ytdlp = new Ytdlp("tools\\yt-dlp.exe"); -string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, 1080); string bestAudio = await ytdlp.GetBestAudioFormatIdAsync(url); +string bestVideo = await ytdlp.GetBestVideoFormatIdAsync(url, maxHeight: 720); await ytdlp .WithFormat($"{bestVideo}+{bestAudio}/best") + .WithOutputFolder("./downloads") .DownloadAsync(url); ``` --- -# 📄 Get Subtitles +## Get Subtitles ```csharp -await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe"); +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) { @@ -180,18 +334,21 @@ foreach (var sub in subtitles) --- -# ⚡ Parallel Downloads +## Get Adobe Pass MSO List +```csharp +var msoList = await ytdlp.GetAdobePassListAsync(); +``` + +--- + +### Batch Downloads ```csharp -var urls = new[] -{ - "https://youtu.be/video1", - "https://youtu.be/video2" -}; +var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" }; var tasks = urls.Select(async url => { - await using var ytdlp = new Ytdlp() + var ytdlp = new Ytdlp("tools\\yt-dlp.exe") .WithFormat("best") .WithOutputFolder("./batch"); @@ -200,57 +357,55 @@ var tasks = urls.Select(async url => await Task.WhenAll(tasks); ``` - **OR** ```csharp var urls = new[] { "https://youtu.be/vid1", "https://youtu.be/vid2" }; - await using var ytdlp = new Ytdlp("tools\\yt-dlp.exe") +var ytdlp = new Ytdlp("tools\\yt-dlp.exe") .WithFormat("best") .WithOutputFolder("./batch"); await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3); ``` - --- # 📡 Events -| Event | Description | -| -------------------------- | ------------------------ | -| `OnProgressDownload` | Download progress | -| `OnProgressMessage` | Informational messages | -| `OnCompleteDownload` | File finished | -| `OnPostProcessingStart` | Post‑processing start | -| `OnPostProcessingComplete` | Post‑processing finished | -| `OnOutputMessage` | Raw output line | -| `OnErrorMessage` | Error message | -| `OnCommandCompleted` | Process finished | +| 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 | --- - ## 🛠 Methods -* `VersionAsync(CancellationToken ct)` -* `UpdateAsync(UpdateChannel channel, string specificVersion, CancellationToken ct)` -* `ExtractorsAsync(CancellationToken ct, int bufferKb)` -* `GetMetadataAsync(string url, CancellationToken ct, int bufferKb)` -* `GetMetadataRawAsync(string url, CancellationToken ct, int bufferKb)` -* `GetDeepMetadataAsync(string url, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256)` -* `GetDeepMetadataRawAsync(string url, CancellationToken ct = default, bool tuneProcess = true, int bufferKb = 256)` -* `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)` +* `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 fields)` +* `GetBestAudioFormatIdAsync(string url)` +* `GetBestVideoFormatIdAsync(string url, int maxHeight)` +* `ExecuteAsync(string url)` +* `ExecuteBatchAsync(IEnumerable urls, int maxConcurrency)` --- - -# 🔧 Fluent API Methods +## Fluent Methods ### General Options * `.WithIgnoreErrors()` @@ -361,6 +516,8 @@ await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3); ### Authentication Options * `.WithAuthentication(string username, string password)` * `.WithTwoFactor(string code)` +* `.WithVideoPassword(string password)` +* `.WithAdobePassAuthentication(string mso, string username, string password)` ### Post-Processing Options * `.WithExtractAudio(string format, int quality = 5)` @@ -404,21 +561,20 @@ AND MORE ... --- +# 🔄 Upgrade Guide (v3 → v4) -# 🔄 Upgrade Guide (v2 → v3) - -v3 introduces a **new immutable fluent API**. +v4 introduces a **new immutable fluent API**. Old mutable commands were removed. --- -## ❌ Old API (v2) +## ❌ Old API (v3) ```csharp var ytdlp = new Ytdlp(); -await ytdlp +await using var ytdlp .SetFormat("best") .SetOutputFolder("./downloads") .ExecuteAsync(url); @@ -426,10 +582,10 @@ await ytdlp --- -## ✅ New API (v3) +## ✅ New API (v4) ```csharp -await using var ytdlp = new Ytdlp() +var ytdlp = new Ytdlp() .WithFormat("best") .WithOutputFolder("./downloads"); @@ -438,21 +594,6 @@ await ytdlp.DownloadAsync(url); --- -## Method changes - -| v2 | v3 | -| --------------------- | ---------------------- | -| `SetFormat()` | `WithFormat()` | -| `SetOutputFolder()` | `WithOutputFolder()` | -| `SetTempFolder()` | `WithTempFolder()` | -| `SetOutputTemplate()` | `WithOutputTemplate()` | -| `SetFFMpegLocation()` | `WithFFmpegLocation()` | -| `ExtractAudio()` | `WithExtractAudio()` | -| `UseProxy()` | `WithProxy()` | -| `AddCustomCommand()` | `AddFlag(string flag)` or `AddOption(string key, string value)` | - ---- - ## Custom commands ```csharp AddFlag("--no-check-certificate"); @@ -482,29 +623,34 @@ Attach events **to the configured instance**. ```csharp var download = baseYtdlp.WithFormat("best"); -download.OnProgressDownload += ... +download.ProgressDownload += ... ``` --- -### Proper disposal +### No disposal required -Use **`await using`** for automatic cleanup. - -```csharp -await using var ytdlp = new Ytdlp(); -``` +**Ytdlp** holds no unmanaged resources and does not implement **IDisposable** or **IAsyncDisposable**. --- # 🧪 Example Apps -* ClipMate MAUI downloader -* Windows GUI downloader +* ClipMate downloader * Console examples --- +### ✅ Notes + +* All commands now start with `WithXxx()`. +* Immutable: no shared state; safe for parallel usage. +* No need to dispose intermediate instances. +* Deprecated old methods removed. +* Probe methods remain the same (`GetMetadataAsync`, `GetFormatsAsync`, `GetBestVideoFormatIdAsync`, etc.). + +--- + # 🤝 Contributing Contributions are welcome! diff --git a/src/Ytdlp.NET/Core/ProcessFactory.cs b/src/Ytdlp.NET/Core/ProcessFactory.cs index 19b872f..e347a3d 100644 --- a/src/Ytdlp.NET/Core/ProcessFactory.cs +++ b/src/Ytdlp.NET/Core/ProcessFactory.cs @@ -25,50 +25,23 @@ public sealed class ProcessFactory /// public ProcessFactory(string ytdlpPath, string? workingDirectory = null) { - Console.WriteLine($"[PF] Input path: {ytdlpPath}"); - Console.WriteLine($"[PF] Current directory: {Environment.CurrentDirectory}"); - - if (File.Exists(ytdlpPath)) - { - var full = Path.GetFullPath(ytdlpPath); - var fi = new FileInfo(full); - - Console.WriteLine($"[PF] Full path: {full}"); - Console.WriteLine($"[PF] Size: {fi.Length}"); - } - else - { - Console.WriteLine($"[PF] File.Exists = false"); - } - if (string.IsNullOrWhiteSpace(ytdlpPath)) throw new ArgumentException("yt-dlp path cannot be empty.", nameof(ytdlpPath)); - if (!File.Exists(ytdlpPath) && !IsOnSystemPath(ytdlpPath)) - throw new FileNotFoundException($"yt-dlp executable not found: {ytdlpPath}", ytdlpPath); + // 1. Resolve to absolute path OR PATH lookup + _ytdlpPath = ResolveYtDlp(ytdlpPath); + // 2. Validate working directory _workingDirectory = workingDirectory ?? Environment.CurrentDirectory; if (!Directory.Exists(_workingDirectory)) throw new DirectoryNotFoundException($"Working directory not found: {_workingDirectory}"); - ToolPermissionManager.EnsureExecutableIfFile(ytdlpPath); - - _ytdlpPath = ytdlpPath; - //if (string.IsNullOrWhiteSpace(ytdlpPath)) - // throw new ArgumentException("yt-dlp path cannot be empty.", nameof(ytdlpPath)); + // 3. Sanity check (protect against fake/corrupt binaries) + ValidateBinary(_ytdlpPath); - //if (!File.Exists(ytdlpPath) && !IsOnSystemPath(ytdlpPath)) - // throw new FileNotFoundException($"yt-dlp executable not found: {ytdlpPath}", ytdlpPath); - - //_workingDirectory = workingDirectory ?? Environment.CurrentDirectory; - - //if (!Directory.Exists(_workingDirectory)) - // throw new DirectoryNotFoundException($"Working directory not found: {_workingDirectory}"); - - //ToolPermissionManager.EnsureExecutableIfFile(ytdlpPath); - - //_ytdlpPath = ytdlpPath; + // 4. Platform permission fix (safe no-op on Windows) + ToolPermissionManager.EnsureExecutableIfFile(_ytdlpPath); } /// @@ -85,10 +58,6 @@ public ProcessFactory(string ytdlpPath, string? workingDirectory = null) /// public Process Create(string arguments) { - Console.WriteLine($"[PF] Launching: {_ytdlpPath}"); - Console.WriteLine($"[PF] WorkingDir: {_workingDirectory}"); - - if (string.IsNullOrWhiteSpace(arguments)) throw new ArgumentException("Arguments cannot be empty.", nameof(arguments)); @@ -168,8 +137,50 @@ public static void SafeKill(Process process, ILogger? logger = null) } } - private static bool IsOnSystemPath(string name) => - Environment.GetEnvironmentVariable("PATH")? - .Split(Path.PathSeparator) - .Any(p => File.Exists(Path.Combine(p, name))) ?? false; + private static string ResolveYtDlp(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new FileNotFoundException("yt-dlp path is empty"); + + // 1. Absolute or local file + if (File.Exists(path)) + return Path.GetFullPath(path); + + // 2. Try PATH resolution (IMPORTANT for CI) + var fromPath = FindInPath(path); + if (fromPath != null) + return fromPath; + + throw new FileNotFoundException($"yt-dlp not found: {path}"); + } + + private static string? FindInPath(string exe) + { + var paths = Environment.GetEnvironmentVariable("PATH")? + .Split(Path.PathSeparator); + + if (paths == null) + return null; + + foreach (var p in paths) + { + var full = Path.Combine(p, exe); + if (File.Exists(full)) + return full; + } + + return null; + } + + private static void ValidateBinary(string path) + { + if (!File.Exists(path)) + throw new FileNotFoundException($"yt-dlp not found: {path}"); + + var fileInfo = new FileInfo(path); + + // yt-dlp is NEVER tiny + if (fileInfo.Length < 1024) + throw new InvalidOperationException($"Invalid yt-dlp binary detected: {fileInfo.FullName} ({fileInfo.Length} bytes)."); + } } \ No newline at end of file diff --git a/src/Ytdlp.NET/README.md b/src/Ytdlp.NET/README.md index ff61e99..2d49230 100644 --- a/src/Ytdlp.NET/README.md +++ b/src/Ytdlp.NET/README.md @@ -3,9 +3,44 @@ # Ytdlp.NET > **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. + +--- + +-- + +## ✨ Features + +* **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 (where yt-dlp is supported). --- +## 🚀 New in this release + +* Add more WithXxx() methods for advanced options. +* New **GetAdobePassListAsync()** for Adobe Pass mso listing. +* New **GetSubtitlesAsync()** for subtitle extraction. +* New **Traverse()** method for easy iteration over nested playlist entries. +* New **GetDeepMetadataAsync()** method for comprehensive metadata extraction. +* New **GetDeepMetadataRawAsync()** for raw JSON metadata. +* Improved **Metadata** model with more fields and better parsing. +* Improved **UpdateAsync** with specific version support. +* 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. + +--- + +# 🔧 Required Tools ## ⚠️ Important Notes * **Namespace migrated**: `ManuHub.Ytdlp.NET` — update your `using` directives. @@ -20,7 +55,7 @@ Tools/ └─ ffprobe.exe ``` -> Recommended: Use companion NuGet packages: +- **Recommended:** Use companion NuGet packages: | Package | Description | |---------|-------------| @@ -94,62 +129,9 @@ foreach (var root in metadata.Entries ?? []) --- -## 🚀 New in this release - -* Add more WithXxx() methods for advanced options. -* New **GetAdobePassListAsync()** for Adobe Pass mso listing. -* New **GetSubtitlesAsync()** for subtitle extraction. -* New **Traverse()** method for easy iteration over nested playlist entries. -* New **GetDeepMetadataAsync()** method for comprehensive metadata extraction. -* New **GetDeepMetadataRawAsync()** for raw JSON metadata. -* Improved **Metadata** model with more fields and better parsing. -* Improved **UpdateAsync** with specific version support. -* 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. - ---- - -## ✨ Features - -* **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). - ---- - -## 🛠 Methods -* `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 fields)` -* `GetBestAudioFormatIdAsync(string url)` -* `GetBestVideoFormatIdAsync(string url, int maxHeight)` -* `ExecuteAsync(string url)` -* `ExecuteBatchAsync(IEnumerable urls, int maxConcurrency)` - - -## 🔧 Thread Safety & Disposal +## 🔧 Thread Safety * **Immutable & thread-safe**: Each `WithXxx()` call returns a new instance. -* **Async disposal**: `Ytdlp` implements `IAsyncDisposable`. ### **Sequential download example**: @@ -224,6 +206,7 @@ await ytdlp.DownloadAsync("https://www.youtube.com/watch?v=RGg-Qx1rL9U"); --- ## Download a Playlist + ```csharp var ytdlp = new Ytdlp("tools\\yt-dlp.exe") .WithFormat("best") @@ -235,6 +218,35 @@ var ytdlp = new Ytdlp("tools\\yt-dlp.exe") await ytdlp.DownloadAsync("https://www.youtube.com/playlist?list=PL12345"); ``` +--- + +# 📊 Monitor Progress & Events + +```csharp +ytdlp.ProgressDownload += (s, e) => + Console.WriteLine($"{e.Percent:F1}% {e.Speed} ETA {e.ETA}"); + +ytdlp.DownloadCompleted += (s, msg) => + Console.WriteLine($"Finished: {msg}"); + +ytdlp.ProgressMessage += (s, msg) => Console.WriteLine(msg); + +ytdlp.PostProcessingStarted += (s, msg) => + Console.WriteLine($"Post-processing-start: {msg}") + +ytdlp.PostProcessingCompleted += (s, msg) => + Console.WriteLine($"Post-processing-complete: {msg}"); + +ytdlp.ErrorMessage += (s, err) => Console.WriteLine($"Error: {err}"); + +ytdlp.OutputMessage += (s, msg) => Console.WriteLine(msg); + +ytdlp.CommandCompleted += (s, e) => + Console.WriteLine($"Command finished: {e.Command}"); +``` + +--- + ### Fetch Metadata ```csharp @@ -324,6 +336,42 @@ await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3); ``` --- +# 📡 Events + +| 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 | + +--- + + +## 🛠 Methods +* `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 fields)` +* `GetBestAudioFormatIdAsync(string url)` +* `GetBestVideoFormatIdAsync(string url, int maxHeight)` +* `ExecuteAsync(string url)` +* `ExecuteBatchAsync(IEnumerable urls, int maxConcurrency)` + +--- + ## Fluent Methods ### General Options @@ -480,21 +528,6 @@ AND MORE ... --- -### Events - -```csharp -ytdlp.ProgressDownload += (s, e) => Console.WriteLine($"Progress: {e.Percent:F2}%"); -ytdlp.ProgressMessage += (s, msg) => Console.WriteLine(msg); -ytdlp.DownloadCompleted += (s, msg) => Console.WriteLine($"Done: {msg}"); -ytdlp.PostProcessingStarted += (s, msg) => Console.WriteLine($"Post-processing-start: {msg}") -ytdlp.PostProcessingCompleted += (s, msg) => Console.WriteLine($"Post-processing-complete: {msg}"); -ytdlp.ErrorMessage += (s, err) => Console.WriteLine($"Error: {err}"); -ytdlp.OutputMessage += (s, msg) => Console.WriteLine(msg); -ytdlp.CommandCompleted += (s, e) => Console.WriteLine($"Command finished: {e.Command}"); -``` - ---- - # 🔄 Upgrade Guide (v3 → v4) v4 introduces a **new immutable fluent API**. @@ -562,10 +595,9 @@ download.ProgressDownload += ... --- -### Removed disposal of old instances +### No disposal required -No need to dispose intermediate instances since they are immutable and stateless. -So you can create a base instance and reuse it without worrying about disposal: +**Ytdlp** holds no unmanaged resources and does not implement **IDisposable** or **IAsyncDisposable**. ```csharp var ytdlp = new Ytdlp(); @@ -573,7 +605,6 @@ var ytdlp = new Ytdlp(); --- - ### ✅ Notes * All commands now start with `WithXxx()`. diff --git a/tests/Ytdlp.NET.Test/ArgumentTests.cs b/tests/Ytdlp.NET.Test/ArgumentTests.cs index 49ae86b..0025e6c 100644 --- a/tests/Ytdlp.NET.Test/ArgumentTests.cs +++ b/tests/Ytdlp.NET.Test/ArgumentTests.cs @@ -5,10 +5,16 @@ namespace ManuHub.Ytdlp.NET.Test; public class ArgumentTests { private readonly string _fullFakePath; + private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; public ArgumentTests() { - _fullFakePath = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; + + _fullFakePath = RunIntegration + ? "yt-dlp.exe" + : Path.Combine(Path.GetTempPath(), $"yt-dlp-fake-{Guid.NewGuid():N}.exe"); + + if (RunIntegration) return; if (!File.Exists(_fullFakePath)) { diff --git a/tests/Ytdlp.NET.Test/AudioFormatTests.cs b/tests/Ytdlp.NET.Test/AudioFormatTests.cs index 96d3154..10e7e46 100644 --- a/tests/Ytdlp.NET.Test/AudioFormatTests.cs +++ b/tests/Ytdlp.NET.Test/AudioFormatTests.cs @@ -10,10 +10,15 @@ namespace ManuHub.Ytdlp.NET.Test; public class AudioFormatTests { private readonly string _fullFakePath; + private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; public AudioFormatTests() { - _fullFakePath = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; + _fullFakePath = RunIntegration + ? "yt-dlp.exe" + : Path.Combine(Path.GetTempPath(), $"yt-dlp-fake-{Guid.NewGuid():N}.exe"); + + if (RunIntegration) return; if (!File.Exists(_fullFakePath)) { diff --git a/tests/Ytdlp.NET.Test/YtdlpAdobeAuthTests.cs b/tests/Ytdlp.NET.Test/YtdlpAdobeAuthTests.cs index 859e50b..cc5f215 100644 --- a/tests/Ytdlp.NET.Test/YtdlpAdobeAuthTests.cs +++ b/tests/Ytdlp.NET.Test/YtdlpAdobeAuthTests.cs @@ -3,10 +3,15 @@ public class YtdlpAdobeAuthTests { private readonly string _fullFakePath; + private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; public YtdlpAdobeAuthTests() { - _fullFakePath = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; + _fullFakePath = RunIntegration + ? "yt-dlp.exe" + : Path.Combine(Path.GetTempPath(), $"yt-dlp-fake-{Guid.NewGuid():N}.exe"); + + if (RunIntegration) return; if (!File.Exists(_fullFakePath)) { diff --git a/tests/Ytdlp.NET.Test/YtdlpAuthTests.cs b/tests/Ytdlp.NET.Test/YtdlpAuthTests.cs index 8aa78cb..e83f7e5 100644 --- a/tests/Ytdlp.NET.Test/YtdlpAuthTests.cs +++ b/tests/Ytdlp.NET.Test/YtdlpAuthTests.cs @@ -3,10 +3,15 @@ public class YtdlpAuthTests { private readonly string _fullFakePath; + private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; public YtdlpAuthTests() { - _fullFakePath = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; + _fullFakePath = RunIntegration + ? "yt-dlp.exe" + : Path.Combine(Path.GetTempPath(), $"yt-dlp-fake-{Guid.NewGuid():N}.exe"); + + if (RunIntegration) return; if (!File.Exists(_fullFakePath)) { diff --git a/tests/Ytdlp.NET.Test/YtdlpBuilderTests.cs b/tests/Ytdlp.NET.Test/YtdlpBuilderTests.cs index 430080e..d75c3f5 100644 --- a/tests/Ytdlp.NET.Test/YtdlpBuilderTests.cs +++ b/tests/Ytdlp.NET.Test/YtdlpBuilderTests.cs @@ -11,11 +11,20 @@ namespace ManuHub.Ytdlp.NET.Test; public class YtdlpBuilderTests { private readonly string _fullFakePath; - public static readonly string _fakeFfmpegPath = OperatingSystem.IsWindows() ? "ffmpeg.exe" : "ffmpeg"; + public readonly string _fakeFfmpegPath; + private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; public YtdlpBuilderTests() { - _fullFakePath = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; + _fullFakePath = RunIntegration + ? "yt-dlp.exe" + : Path.Combine(Path.GetTempPath(), $"yt-dlp-fake-{Guid.NewGuid():N}.exe"); + + _fakeFfmpegPath = RunIntegration + ? "ffmpeg.exe" + : Path.Combine(Path.GetTempPath(), $"ffmpeg-fake-{Guid.NewGuid():N}.exe"); + + if (RunIntegration) return; if (!File.Exists(_fullFakePath)) { @@ -29,6 +38,17 @@ public YtdlpBuilderTests() // ignore the error because the file is being taken care of. } } + + if (!File.Exists(_fakeFfmpegPath)) + { + try + { + File.WriteAllText(_fakeFfmpegPath,""); + } + catch (Exception) + { + } + } } // ── Construction ────────────────────────────────────────────────────── diff --git a/tests/Ytdlp.NET.Test/YtdlpCancellationTests.cs b/tests/Ytdlp.NET.Test/YtdlpCancellationTests.cs index ddc3f0b..bab8fae 100644 --- a/tests/Ytdlp.NET.Test/YtdlpCancellationTests.cs +++ b/tests/Ytdlp.NET.Test/YtdlpCancellationTests.cs @@ -11,12 +11,17 @@ namespace ManuHub.Ytdlp.NET.Test; public class YtdlpCancellationTests { private readonly string _fullFakePath; + private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; private readonly string SampleUrl = "https://www.youtube.com/watch?v=RGg-Qx1rL9U"; private readonly string SampleUrl2 = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; public YtdlpCancellationTests() { - _fullFakePath = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; + _fullFakePath = RunIntegration + ? "yt-dlp.exe" + : Path.Combine(Path.GetTempPath(), $"yt-dlp-fake-{Guid.NewGuid():N}.exe"); + + if (RunIntegration) return; if (!File.Exists(_fullFakePath)) { diff --git a/tests/Ytdlp.NET.Test/YtdlpEventTests.cs b/tests/Ytdlp.NET.Test/YtdlpEventTests.cs index 4f969d9..9525b51 100644 --- a/tests/Ytdlp.NET.Test/YtdlpEventTests.cs +++ b/tests/Ytdlp.NET.Test/YtdlpEventTests.cs @@ -9,12 +9,16 @@ namespace ManuHub.Ytdlp.NET.Test; /// public class YtdlpEventTests { - private readonly string _fullFakePath; + private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; public YtdlpEventTests() { - _fullFakePath = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; + _fullFakePath = RunIntegration + ? "yt-dlp.exe" + : Path.Combine(Path.GetTempPath(), $"yt-dlp-fake-{Guid.NewGuid():N}.exe"); + + if (RunIntegration) return; if (!File.Exists(_fullFakePath)) { diff --git a/tests/Ytdlp.NET.Test/YtdlpIntegrationTests.cs b/tests/Ytdlp.NET.Test/YtdlpIntegrationTests.cs index 9b11aae..0e34583 100644 --- a/tests/Ytdlp.NET.Test/YtdlpIntegrationTests.cs +++ b/tests/Ytdlp.NET.Test/YtdlpIntegrationTests.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using System.Runtime.InteropServices; namespace ManuHub.Ytdlp.NET.Test; @@ -12,22 +11,14 @@ namespace ManuHub.Ytdlp.NET.Test; [Collection("Integration")] public class YtdlpIntegrationTests { - // Cross-platform binary name selection - private readonly string binaryName = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp"; - private static readonly bool RunIntegration = Environment.GetEnvironmentVariable("YTDLP_INTEGRATION_TESTS") == "1"; + // Cross-platform binary name selection + private readonly string binaryName = "yt-dlp.exe"; + // A short, stable, public-domain video suitable for testing private const string TestVideoUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; - public YtdlpIntegrationTests() - { - Console.WriteLine($"OS: {RuntimeInformation.OSDescription}"); - Console.WriteLine($"ProcessArch: {RuntimeInformation.ProcessArchitecture}"); - Console.WriteLine($"yt-dlp exists: {File.Exists("yt-dlp.exe")}"); - Console.WriteLine($"yt-dlp size: {new FileInfo("yt-dlp.exe").Length}"); - } - /// /// Helper to instantiate Ytdlp with anti-bot arguments for reliable CI/CD runs. ///