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
207 changes: 154 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);
| `OnProgressDownload` | Download progress |
| `OnProgressMessage` | Informational messages |
| `OnCompleteDownload` | File finished |
| `OnPostProcessingStart` | Post‑processing start |
| `OnPostProcessingComplete` | Post‑processing finished |
| `OnOutputMessage` | Raw output line |
| `OnErrorMessage` | Error message |
Expand All @@ -206,62 +207,155 @@ await ytdlp.DownloadBatchAsync(urls, maxConcurrency: 3);

# 🔧 Fluent API Methods

### Output

```
WithOutputFolder()
WithTempFolder()
WithHomeFolder()
WithOutputTemplate()
WithFFmpegLocation()
```

### Formats

```
WithFormat()
With720pOrBest()
WithExtractAudio()
```

### Metadata

```
GetMetadataAsync()
GetBestAudioFormatIdAsync()
GetBestVideoFormatIdAsync()
GetAvailableFormatsAsync()
```

### Features

```
WithEmbedMetadata()
WithEmbedThumbnail()
WithEmbedSubtitles()
WithSubtitles()
WithConcurrentFragments()
WithSponsorblockRemove()
```

### Network

```
WithProxy()
WithCookiesFile()
WithCookiesFromBrowser()
```

### Advanced

```
AddFlag()
AddOption()
AddCustomCommand()
```
### 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, 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(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 ...

---


# 🔄 Upgrade Guide (v2 → v3)

v3 introduces a **new immutable fluent API**.
Expand Down Expand Up @@ -306,9 +400,16 @@ await ytdlp.DownloadAsync(url);
| `SetFFMpegLocation()` | `WithFFmpegLocation()` |
| `ExtractAudio()` | `WithExtractAudio()` |
| `UseProxy()` | `WithProxy()` |
| `AddCustomCommand()` | `AddFlag(string flag)` or `AddOption(string key, string value)` |

---

## Custom commands
```csharp
AddFlag("--no-check-certificate");
AddOption("--external-downloader", "aria2c");
```

## Important behavior changes

### Instances are immutable
Expand Down
10 changes: 6 additions & 4 deletions src/Ytdlp.NET.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,12 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)
var url = "https://www.youtube.com/watch?v=89-i4aPOMrc"; //"https://www.dailymotion.com/video/xa3ron2";

var ytdlp = ytdlpBase
.WithFormat("95+ba/b")
.WithFormat("ba/b")
.WithExtractAudio(AudioFormat.Mp3)
.WithConcurrentFragments(8)
.WithHomeFolder("./downloads")
.WithTempFolder("./downloads/temp")
.WithOutputFolder("./downloads")
//.WithHomeFolder("./downloads")
//.WithTempFolder("./downloads/temp")
.WithOutputTemplate("%(title)s.%(ext)s");
//.WithEmbedMetadata()
//.WithEmbedThumbnail()
Expand Down Expand Up @@ -215,7 +217,7 @@ private static async Task TestDownloadVideoAsync(Ytdlp ytdlpBase)

Console.WriteLine(ytdlp.Preview(url));

await ytdlp.DownloadAsync(url);
await ytdlp.DownloadAsync(url);
}

private static async Task TestDownloadAudioAsync(Ytdlp ytdlpBase)
Expand Down
29 changes: 9 additions & 20 deletions src/Ytdlp.NET/Parsing/ProgressParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
private bool _isDownloadCompleted;
private bool _postProcessingStarted;
private int _postProcessStepCount;
private int _deleteCount;

Check warning on line 17 in src/Ytdlp.NET/Parsing/ProgressParser.cs

View workflow job for this annotation

GitHub Actions / build

The field 'ProgressParser._deleteCount' is assigned but its value is never used

public ProgressParser(ILogger? logger = null)
{
Expand Down Expand Up @@ -102,19 +102,13 @@
{
if (_isDownloadCompleted) return;

// Existing logic unchanged
string percentString = match.Groups["percent"].Value;
string sizeString = match.Groups["total"].Value;
string speedString = match.Groups["speed"].Value;
string etaString = match.Groups["eta"].Value;

if (!double.TryParse(percentString.Replace("%", ""), out double percent))
percent = 0;

if (percent >= 99.0 && !_isDownloadCompleted)
{
HandleDownloadProgressComplete(match);
}
percent = 0;

var args = new DownloadProgressEventArgs
{
Expand All @@ -127,13 +121,17 @@

LogAndNotify(LogType.Info, args.Message);
OnProgressDownload?.Invoke(this, args);

if (percent >= 99.0 && !_isDownloadCompleted)
{
HandleDownloadProgressComplete(match);
}
}

private void HandleDownloadProgressWithFrag(Match match)
{
if (_isDownloadCompleted) return;

// Existing + prevent premature complete if fragments remain
string percentString = match.Groups["percent"].Value;
string sizeString = match.Groups["size"].Value;
string speedString = match.Groups["speed"].Value;
Expand All @@ -156,8 +154,6 @@
LogAndNotify(LogType.Info, args.Message);
OnProgressDownload?.Invoke(this, args);

Debug.WriteLine($"DEBUG: Progress with frag → percent: {percent}, size: {sizeString}, speed: {speedString}, eta: {etaString}, frag: {fragString}");

// Only trigger complete if really done (avoid false 100% on fragment level)
if (percent >= 99.0 && IsFinalFragment(fragString) && !_isDownloadCompleted)
{
Expand Down Expand Up @@ -229,7 +225,7 @@

_postProcessStepCount++;

// Better group extraction
// Extract processor and action safely
string processor = match.Groups["processor"].Success
? match.Groups["processor"].Value.Trim()
: "PostProcessor";
Expand All @@ -241,11 +237,11 @@
var message = $"[{processor}] {action}";
LogAndNotify(LogType.Info, $"Post-processing [{_postProcessStepCount}]: {message}");

// === KEY CHANGE: Only complete on MoveFiles or after many steps ===
// Trigger completion when we hit the real last step (MoveFiles is usually the final one)
bool isFinalStep =
processor.Equals("MoveFiles", StringComparison.OrdinalIgnoreCase) ||
action.Contains("Moving file", StringComparison.OrdinalIgnoreCase) ||
_postProcessStepCount >= 8; // safety net
_postProcessStepCount >= 10; // safety net for unusual cases

if (isFinalStep)
{
Expand All @@ -261,13 +257,6 @@
}
}

private void HandleSponsorBlock(Match match)
{
var action = match.Groups["action"].Value.Trim();
var details = match.Groups["details"].Success ? match.Groups["details"].Value.Trim() : "";
LogAndNotify(LogType.Info, $"SponsorBlock {action}{(string.IsNullOrEmpty(details) ? "" : $": {details}")}");
}

private void HandleUnknownOutput(string output)
{
var lower = output.ToLowerInvariant().Trim();
Expand Down
5 changes: 3 additions & 2 deletions src/Ytdlp.NET/Parsing/RegexPatterns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal static class RegexPatterns
public const string ResumeDownload = @"\[download\]\s*Resuming download at byte\s*(?<byte>\d+)";
public const string DownloadAlreadyDownloaded = @"\[download\]\s*(?<path>[^\n]+?)\s*has already been downloaded";
public const string DownloadProgress = @"\[download\]\s+(?:(?<percent>[\d\.]+)%(?:\s+of\s+\~?\s*(?<total>[\d\.\w]+))?\s+at\s+(?:(?<speed>[\d\.\w]+\/s)|[\w\s]+)\s+ETA\s(?<eta>[\d\:]+))?";
//@"\[download\]\s*(?<percent>\d+\.\d+)%\s*of\s*(?<size>[^\s]+)\s*at\s*(?<speed>[^\s]+)\s*ETA\s*(?<eta>[^\s]+)";
//@"\[download\]\s*(?<percent>\d+\.\d+)%\s*of\s*(?<size>[^\s]+)\s*at\s*(?<speed>[^\s]+)\s*ETA\s*(?<eta>[^\s]+)";
public const string DownloadProgressWithFrag = @"\[download\]\s*(?<percent>\d+\.\d+)%\s*of\s*(~?\s*(?<size>[^\s]+))\s*at\s*(?<speed>[^\s]+)\s*ETA\s*(?<eta>[^\s]+)\s*\(frag\s*(?<frag>\d+/\d+)\)";
public const string DownloadProgressComplete = @"\[download\]\s*(?<percent>100(?:\.0)?)%\s*of\s*(?<size>[^\s]+)\s*at\s*(?<speed>[^\s]+|Unknown)\s*ETA\s*(?<eta>[^\s]+|Unknown)";
public const string UnknownError = @"\[download\]\s*Unknown error";
Expand All @@ -32,5 +32,6 @@ internal static class RegexPatterns
public const string MoveFiles = @"\[MoveFiles\]\s*(?<action>.+)";

// Generic fallback for any unknown post-processor
public const string PostProcessorGeneric = @"\[(?<processor>FixupM3u8|VideoRemuxer|Metadata|ThumbnailsConvertor|EmbedThumbnail|MoveFiles|Merger|ffmpeg|ConvertSubs|SponsorBlock)\]\s*(?<action>.+)";
public const string PostProcessorGeneric =
@"\[(?<processor>Merger|ModifyChapters|SplitChapters|ExtractAudio|VideoRemuxer|VideoConvertor|Metadata|EmbedSubtitle|EmbedThumbnail|SubtitlesConvertor|ThumbnailsConvertor|FixupStretched|FixupM4a|FixupM3u8|FixupTimestamp|FixupDuration|MoveFiles|ffmpeg|ConvertSubs|SponsorBlock)\]\s*(?<action>.+)";
}
Loading
Loading