Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 257 additions & 111 deletions README.md

Large diffs are not rendered by default.

95 changes: 53 additions & 42 deletions src/Ytdlp.NET/Core/ProcessFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,50 +25,23 @@ public sealed class ProcessFactory
/// </summary>
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);
}

/// <summary>
Expand All @@ -85,10 +58,6 @@ public ProcessFactory(string ytdlpPath, string? workingDirectory = null)
/// </remarks>
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));

Expand Down Expand Up @@ -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).");
}
}
179 changes: 105 additions & 74 deletions src/Ytdlp.NET/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,7 +55,7 @@ Tools/
└─ ffprobe.exe
```

> Recommended: Use companion NuGet packages:
- **Recommended:** Use companion NuGet packages:

| Package | Description |
|---------|-------------|
Expand Down Expand Up @@ -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<string> fields)`
* `GetBestAudioFormatIdAsync(string url)`
* `GetBestVideoFormatIdAsync(string url, int maxHeight)`
* `ExecuteAsync(string url)`
* `ExecuteBatchAsync(IEnumerable<string> 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**:

Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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<string> fields)`
* `GetBestAudioFormatIdAsync(string url)`
* `GetBestVideoFormatIdAsync(string url, int maxHeight)`
* `ExecuteAsync(string url)`
* `ExecuteBatchAsync(IEnumerable<string> urls, int maxConcurrency)`

---

## Fluent Methods

### General Options
Expand Down Expand Up @@ -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**.
Expand Down Expand Up @@ -562,18 +595,16 @@ 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();
```

---


### ✅ Notes

* All commands now start with `WithXxx()`.
Expand Down
8 changes: 7 additions & 1 deletion tests/Ytdlp.NET.Test/ArgumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down
Loading
Loading