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)