diff --git a/src/Ytdlp.NET.Console/Helpers/MenuRenderer.cs b/src/Ytdlp.NET.Console/Helpers/MenuRenderer.cs
index 508a20f..5f76e38 100644
--- a/src/Ytdlp.NET.Console/Helpers/MenuRenderer.cs
+++ b/src/Ytdlp.NET.Console/Helpers/MenuRenderer.cs
@@ -14,6 +14,7 @@ public static string ShowMainMenu()
WriteItem("8", "Benchmarks");
WriteItem("9", "Extractors List");
WriteItem("10", "Subtitle List");
+ WriteItem("11", "Live Stream Download");
WriteItem("0", "Exit");
Console.WriteLine();
diff --git a/src/Ytdlp.NET.Console/Pages/DownloadVideoPage.cs b/src/Ytdlp.NET.Console/Pages/DownloadVideoPage.cs
index 2d4ba97..e984203 100644
--- a/src/Ytdlp.NET.Console/Pages/DownloadVideoPage.cs
+++ b/src/Ytdlp.NET.Console/Pages/DownloadVideoPage.cs
@@ -55,4 +55,3 @@ public static async Task ShowAsync(Ytdlp baseYtdlp)
Console.ReadKey();
}
}
-
diff --git a/src/Ytdlp.NET.Console/Pages/LivetreamDownloadPage.cs b/src/Ytdlp.NET.Console/Pages/LivetreamDownloadPage.cs
new file mode 100644
index 0000000..4458eb6
--- /dev/null
+++ b/src/Ytdlp.NET.Console/Pages/LivetreamDownloadPage.cs
@@ -0,0 +1,50 @@
+using ManuHub.Ytdlp.NET;
+using YtdlpNetConsoleApp.Helpers;
+
+namespace YtdlpNetConsoleApp.Pages;
+
+internal static class LivetreamDownloadPage
+{
+ public static async Task ShowAsync(Ytdlp baseYtdlp)
+ {
+ Console.Clear();
+ ConsoleTheme.WriteSection("Livestream Downloader");
+
+ var url = ConsoleExtensions.ReadInput("Enter livestream URL");
+
+ if (string.IsNullOrWhiteSpace(url))
+ return;
+
+ var ytdlp = baseYtdlp
+ .WithFormat("bv+ba/b")
+ .WithLiveFromStart()
+ .WithFFmpegAsLiveDownloader()
+ .WithOutputTemplate("%(title)s.%(ext)s")
+ .WithOutputFolder("./livestreams")
+ .WithConcurrentFragments(8)
+ .WithEmbedMetadata()
+ .WithEmbedThumbnail();
+
+ var dashboard = new ProgressDashboard();
+ ytdlp.ProgressDownload += (_, p) =>
+ {
+ dashboard.Update(p.Percent, p.Speed, p.ETA, p.Size);
+ };
+ ytdlp.OutputMessage += (_, msg) =>
+ {
+ Console.WriteLine(msg);
+ };
+ ytdlp.CommandCompleted += (_, result) =>
+ {
+ Console.WriteLine();
+ ConsoleExtensions.WriteLineColor(result.Success ? "SUCCESS" : "FAILED", result.Success ? ConsoleColor.Green : ConsoleColor.Red
+ );
+ };
+ Console.WriteLine("\nStarting download...\n");
+ Console.WriteLine(ytdlp.Preview(url));
+ await ytdlp.DownloadAsync(url);
+ dashboard.Complete();
+ Console.WriteLine("\nPress any key...");
+ Console.ReadKey();
+ }
+}
\ No newline at end of file
diff --git a/src/Ytdlp.NET.Console/Program.cs b/src/Ytdlp.NET.Console/Program.cs
index c9b4b91..f29259e 100644
--- a/src/Ytdlp.NET.Console/Program.cs
+++ b/src/Ytdlp.NET.Console/Program.cs
@@ -72,6 +72,10 @@ static async Task Main(string[] args)
await SubtitlePage.ShowAsync(_ytdlp!);
break;
+ case "11":
+ await LivetreamDownloadPage.ShowAsync(_ytdlp!);
+ break;
+
case "0":
return;
}
diff --git a/src/Ytdlp.NET/Events/DownloadProgressEventArgs.cs b/src/Ytdlp.NET/Events/DownloadProgressEventArgs.cs
index 4a32526..e7f4f13 100644
--- a/src/Ytdlp.NET/Events/DownloadProgressEventArgs.cs
+++ b/src/Ytdlp.NET/Events/DownloadProgressEventArgs.cs
@@ -29,6 +29,12 @@ public class DownloadProgressEventArgs : EventArgs
///
public string ETA { get; set; } = default!;
+ ///
+ /// Gets or sets the elapsed time since the download started.
+ /// Especially useful for livestreams where ETA may not be applicable.
+ ///
+ public string Elapsed { get; set; } = default!;
+
///
/// Gets or sets the number of fragments downloaded (if applicable).
///
diff --git a/src/Ytdlp.NET/Parsing/ProgressParser.cs b/src/Ytdlp.NET/Parsing/ProgressParser.cs
index 3a5b429..8ab7d85 100644
--- a/src/Ytdlp.NET/Parsing/ProgressParser.cs
+++ b/src/Ytdlp.NET/Parsing/ProgressParser.cs
@@ -46,6 +46,13 @@ public ProgressParser(ILogger? logger = null)
{ new Regex(RegexPatterns.ResumeDownload, compiledIgnoreCase), HandleResumeDownload },
{ new Regex(RegexPatterns.DownloadAlreadyDownloaded, compiledIgnoreCase), HandleDownloadAlreadyCompleted },
+
+ // Live / no-total-size progress (livestreams)
+ // Matches both:
+ // [download] 8.36MiB at 169.71KiB/s (00:01:07) (frag 244)
+ // 2: [download] 8.36MiB at 169.71KiB/s (00:01:07) (frag 244)
+ { new Regex(RegexPatterns.DownloadProgressLive, compiledIgnoreCase), HandleDownloadProgressLive },
+
// Progress
{ new Regex(RegexPatterns.DownloadProgressComplete, compiledIgnoreCase), HandleDownloadProgressComplete },
{ new Regex(RegexPatterns.DownloadProgressWithFrag, compiledIgnoreCase), HandleDownloadProgressWithFrag },
@@ -68,6 +75,7 @@ public ProgressParser(ILogger? logger = null)
// Optional: add playlist awareness
{ new Regex(RegexPatterns.PlaylistItem, compiledIgnoreCase), m =>
LogAndNotify(LogType.Information, $"Playlist progress: item {m.Groups["item"].Value}/{m.Groups["total"].Value} - {m.Groups["playlist"].Value}") },
+
};
}
@@ -190,6 +198,33 @@ private void HandleDownloadProgressWithFrag(Match match)
}
}
+ private void HandleDownloadProgressLive(Match match)
+ {
+ string sizeString = match.Groups["size"].Value;
+ string speedString = match.Groups["speed"].Value;
+ string elapsedString = match.Groups["elapsed"].Value;
+ string fragString = match.Groups["frag"].Success ? match.Groups["frag"].Value : string.Empty;
+
+ var args = new DownloadProgressEventArgs
+ {
+ // No reliable percent for live streams
+ Percent = 0,
+ Size = sizeString, // bytes downloaded so far
+ Speed = speedString,
+ ETA = null, // or leave empty – we only have elapsed
+ Elapsed = elapsedString,
+ Fragments = fragString,
+ Message = string.IsNullOrEmpty(fragString)
+ ? $"Live: {sizeString} | {speedString} | elapsed {elapsedString}"
+ : $"Live: {sizeString} | {speedString} | elapsed {elapsedString} | frag {fragString}"
+ };
+
+ LogAndNotify(LogType.Information, args.Message);
+ ProgressDownload?.Invoke(this, args);
+
+ // Never mark download as completed from a live progress line
+ }
+
private static bool IsFinalFragment(string frag)
{
if (string.IsNullOrEmpty(frag) || !frag.Contains('/')) return true;
diff --git a/src/Ytdlp.NET/Parsing/RegexPatterns.cs b/src/Ytdlp.NET/Parsing/RegexPatterns.cs
index 72d4bf6..d6283ca 100644
--- a/src/Ytdlp.NET/Parsing/RegexPatterns.cs
+++ b/src/Ytdlp.NET/Parsing/RegexPatterns.cs
@@ -11,6 +11,11 @@ internal static class RegexPatterns
public const string DownloadProgress = @"\[download\]\s+(?:(?[\d\.]+)%(?:\s+of\s+\~?\s*(?[\d\.\w]+))?\s+at\s+(?:(?[\d\.\w]+\/s)|[\w\s]+)\s+ETA\s(?[\d\:]+))?";
//@"\[download\]\s*(?\d+\.\d+)%\s*of\s*(?[^\s]+)\s*at\s*(?[^\s]+)\s*ETA\s*(?[^\s]+)";
public const string DownloadProgressWithFrag = @"\[download\]\s*(?\d+\.\d+)%\s*of\s*(~?\s*(?[^\s]+))\s*at\s*(?[^\s]+)\s*ETA\s*(?[^\s]+)\s*\(frag\s*(?\d+/\d+)\)";
+
+ // Live progress – no percent / total size
+ // Groups: size, speed, elapsed, frag (optional)
+ public const string DownloadProgressLive =
+ @"^(?:\d+:\s*)?\[download\]\s+(?[\d.]+\s*[KMGT]?i?B)\s+at\s+(?[\d.]+\s*[KMGT]?i?B/s)\s+\((?[\d:]+)\)(?:\s+\(frag\s+(?\d+(?:/\d+)?)\))?";
public const string DownloadProgressComplete = @"\[download\]\s*(?100(?:\.0)?)%\s*of\s*(?[^\s]+)\s*at\s*(?[^\s]+|Unknown)\s*ETA\s*(?[^\s]+|Unknown)";
public const string UnknownError = @"\[download\]\s*Unknown error";
public const string MergingFormats = @"\[Merger\]\s*Merging formats into\s*""(?.+)""";
diff --git a/src/Ytdlp.NET/Ytdlp.NET.csproj b/src/Ytdlp.NET/Ytdlp.NET.csproj
index 0551efd..55a0f60 100644
--- a/src/Ytdlp.NET/Ytdlp.NET.csproj
+++ b/src/Ytdlp.NET/Ytdlp.NET.csproj
@@ -16,7 +16,7 @@
$([System.DateTime]::UtcNow.Year)
$([System.DateTime]::UtcNow.DayOfYear)
- 4.0.0
+ 4.3.0
$(Version)
$(Version)+build.$(BuildYear).$(BuildDay)