diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index 468194d..8d523c3 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -52,8 +52,15 @@ namespace N_m3u8DL_RE.Common.Resource public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); } public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); } public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); } + public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); } + public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); } + public static string cmd_liveRealTimeMerge { get => GetText("cmd_liveRealTimeMerge"); } + public static string cmd_livePerformAsVod { get => GetText("cmd_livePerformAsVod"); } public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); } public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); } + public static string liveLimit { get => GetText("liveLimit"); } + public static string liveLimitReached { get => GetText("liveLimitReached"); } + public static string saveName { get => GetText("saveName"); } public static string fetch { get => GetText("fetch"); } public static string ffmpegMerge { get => GetText("ffmpegMerge"); } public static string ffmpegNotFound { get => GetText("ffmpegNotFound"); } diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index e88c4a4..e8c8ff9 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -232,6 +232,30 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "此字符串將直接傳遞給URL Processor", enUS: "Give these arguments to the URL Processors." ), + ["cmd_liveRealTimeMerge"] = new TextContainer + ( + zhCN: "录制直播时实时合并", + zhTW: "錄製直播時即時合併", + enUS: "Real-time merge into file when recording live" + ), + ["cmd_livePerformAsVod"] = new TextContainer + ( + zhCN: "以点播方式下载直播流", + zhTW: "以點播方式下載直播流", + enUS: "Download live streams as vod" + ), + ["cmd_liveKeepSegments"] = new TextContainer + ( + zhCN: "录制直播并开启实时合并时依然保留分片", + zhTW: "錄製直播並開啟即時合併時依然保留分片", + enUS: "Keep segments when recording a live broadcast and enable liveRealTimeMerge" + ), + ["cmd_liveRecordLimit"] = new TextContainer + ( + zhCN: "录制直播时的录制时长限制", + zhTW: "錄製直播時的錄製時長限制", + enUS: "Recording time limit when recording live" + ), ["cmd_useShakaPackager"] = new TextContainer ( zhCN: "解密时使用shaka-packager替代mp4decrypt", @@ -397,6 +421,24 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "解析後的訊息是否輸出json文件", enUS: "Write meta json after parsed" ), + ["liveLimit"] = new TextContainer + ( + zhCN: "本次直播录制时长上限: ", + zhTW: "本次直播錄製時長上限: ", + enUS: "Live recording duration limit: " + ), + ["liveLimitReached"] = new TextContainer + ( + zhCN: "到达直播录制上限,即将停止录制", + zhTW: "到達直播錄製上限,即將停止錄製", + enUS: "Live recording limit reached, will stop recording soon" + ), + ["saveName"] = new TextContainer + ( + zhCN: "保存文件名: ", + zhTW: "保存檔案名: ", + enUS: "Save Name: " + ), ["fetch"] = new TextContainer ( zhCN: "获取: ", diff --git a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs index 3ce9210..d7fb5cd 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs @@ -1,5 +1,6 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Util; @@ -471,10 +472,23 @@ namespace N_m3u8DL_RE.Parser.Extractor return streamList; } - - public async Task FetchPlayListAsync(List streamSpecs) + public async Task RefreshPlayListAsync(List streamSpecs) { + if (streamSpecs.Count == 0) return; + var rawText = await HTTPUtil.GetWebSourceAsync(ParserConfig.Url); + var newStreams = await ExtractStreamsAsync(rawText); + foreach (var streamSpec in streamSpecs) + { + var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString()); + if (match.Any()) + streamSpec.Playlist = match.First().Playlist; + } //这里才调用URL预处理器,节省开销 + await ProcessUrlAsync(streamSpecs); + } + + private async Task ProcessUrlAsync(List streamSpecs) + { for (int i = 0; i < streamSpecs.Count; i++) { var playlist = streamSpecs[i].Playlist; @@ -496,6 +510,12 @@ namespace N_m3u8DL_RE.Parser.Extractor } } + public async Task FetchPlayListAsync(List streamSpecs) + { + //这里才调用URL预处理器,节省开销 + await ProcessUrlAsync(streamSpecs); + } + public string PreProcessUrl(string url) { foreach (var p in ParserConfig.UrlProcessors) diff --git a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs index 6803a6e..2073674 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs @@ -536,5 +536,10 @@ namespace N_m3u8DL_RE.Parser.Extractor } } } + + public async Task RefreshPlayListAsync(List streamSpecs) + { + await FetchPlayListAsync(streamSpecs); + } } } diff --git a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs index e44bbf7..ad6fb73 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs @@ -18,6 +18,7 @@ namespace N_m3u8DL_RE.Parser.Extractor Task> ExtractStreamsAsync(string rawText); Task FetchPlayListAsync(List streamSpecs); + Task RefreshPlayListAsync(List streamSpecs); string PreProcessUrl(string url); diff --git a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs index f8f4c76..de376de 100644 --- a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs @@ -5,12 +5,14 @@ using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Extractor; using N_m3u8DL_RE.Common.Util; +using N_m3u8DL_RE.Common.Enum; namespace N_m3u8DL_RE.Parser { public class StreamExtractor { - public IExtractor Extractor { get; private set; } + public ExtractorType ExtractorType { get => extractor.ExtractorType; } + private IExtractor extractor; private ParserConfig parserConfig = new ParserConfig(); private string rawText; private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); @@ -56,13 +58,13 @@ namespace N_m3u8DL_RE.Parser if (rawText.StartsWith(HLSTags.ext_m3u)) { Logger.InfoMarkUp(ResString.matchHLS); - Extractor = new HLSExtractor(parserConfig); + extractor = new HLSExtractor(parserConfig); } else if (rawText.Contains("") && rawText.Contains(" streamSpecs) + { + try + { + await semaphore.WaitAsync(); + await extractor.RefreshPlayListAsync(streamSpecs); } finally { diff --git a/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs b/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs new file mode 100644 index 0000000..e5af045 --- /dev/null +++ b/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs @@ -0,0 +1,27 @@ +using N_m3u8DL_RE.Common.Util; +using Spectre.Console; +using Spectre.Console.Rendering; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Column +{ + internal class RecordingDurationColumn : ProgressColumn + { + protected override bool NoWrap => true; + private ConcurrentDictionary _recodingDurDic; + public Style MyStyle { get; set; } = new Style(foreground: Color.Grey); + public RecordingDurationColumn(ConcurrentDictionary recodingDurDic) + { + _recodingDurDic = recodingDurDic; + } + public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + { + return new Text(GlobalUtil.FormatTime(_recodingDurDic[task.Id]), MyStyle).LeftAligned(); + } + } +} diff --git a/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs b/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs new file mode 100644 index 0000000..b1cf60f --- /dev/null +++ b/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs @@ -0,0 +1,23 @@ +using Spectre.Console; +using Spectre.Console.Rendering; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Column +{ + internal class RecordingStatusColumn : ProgressColumn + { + protected override bool NoWrap => true; + public Style MyStyle { get; set; } = new Style(foreground: Color.Default); + public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow); + public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) + { + if (task.IsFinished) + return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftAligned(); + return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftAligned(); + } + } +} diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index ab719ca..5508687 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -3,6 +3,7 @@ using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Enum; using N_m3u8DL_RE.Util; +using NiL.JS.Expressions; using System.CommandLine; using System.CommandLine.Binding; using System.CommandLine.Parsing; @@ -47,6 +48,13 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl); private readonly static Option ConcurrentDownload = new(new string[] { "-mt", "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false); + //直播相关 + private readonly static Option LivePerformAsVod = new(new string[] { "--live-perform-as-vod" }, description: ResString.cmd_livePerformAsVod, getDefaultValue: () => false); + private readonly static Option LiveRealTimeMerge = new(new string[] { "--live-real-time-merge" }, description: ResString.cmd_liveRealTimeMerge, getDefaultValue: () => false); + private readonly static Option LiveKeepSegments = new(new string[] { "--live-keep-segments" }, description: ResString.cmd_liveKeepSegments, getDefaultValue: () => true); + private readonly static Option LiveRecordLimit = new(new string[] { "--live-record-limit" }, description: ResString.cmd_liveRecordLimit, parseArgument: ParseLiveLimit) { ArgumentHelpName = "HH:mm:ss" }; + + //复杂命令行如下 private readonly static Option MuxAfterDone = new(new string[] { "-M", "--mux-after-done" }, description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" }; private readonly static Option> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" }; @@ -54,11 +62,29 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option AudioFilter = new(new string[] { "-sa", "--select-audio" }, description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private readonly static Option SubtitleFilter = new(new string[] { "-ss", "--select-subtitle" }, description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + /// + /// 解析录制直播时长限制 + /// + /// + /// + private static TimeSpan? ParseLiveLimit(ArgumentResult result) + { + var input = result.Tokens.First().Value; + try + { + return OtherUtil.ParseDur(input); + } + catch (Exception) + { + result.ErrorMessage = "error in parse LiveRecordLimit: " + input; + return null; + } + } private static string? ParseSaveName(ArgumentResult result) { var input = result.Tokens.First().Value; - var newName = ConvertUtil.GetValidFileName(input); + var newName = OtherUtil.GetValidFileName(input); if (string.IsNullOrEmpty(newName)) { result.ErrorMessage = "Invalid save name!"; @@ -143,7 +169,7 @@ namespace N_m3u8DL_RE.CommandLine private static Dictionary ParseHeaders(ArgumentResult result) { var array = result.Tokens.Select(t => t.Value).ToArray(); - return ConvertUtil.SplitHeaderArrayToDic(array); + return OtherUtil.SplitHeaderArrayToDic(array); } /// @@ -269,6 +295,10 @@ namespace N_m3u8DL_RE.CommandLine VideoFilter = bindingContext.ParseResult.GetValueForOption(VideoFilter), AudioFilter = bindingContext.ParseResult.GetValueForOption(AudioFilter), SubtitleFilter = bindingContext.ParseResult.GetValueForOption(SubtitleFilter), + LiveRealTimeMerge = bindingContext.ParseResult.GetValueForOption(LiveRealTimeMerge), + LiveKeepSegments = bindingContext.ParseResult.GetValueForOption(LiveKeepSegments), + LiveRecordLimit = bindingContext.ParseResult.GetValueForOption(LiveRecordLimit), + LivePerformAsVod = bindingContext.ParseResult.GetValueForOption(LivePerformAsVod), }; var parsedHeaders = bindingContext.ParseResult.GetValueForOption(Headers); @@ -304,13 +334,15 @@ namespace N_m3u8DL_RE.CommandLine public static async Task InvokeArgs(string[] args, Func action) { - var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220914") + var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220917") { Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, FFmpegBinaryPath, LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, - MuxAfterDone, MuxImports, VideoFilter, AudioFilter, SubtitleFilter + MuxAfterDone, + LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LiveRecordLimit, + MuxImports, VideoFilter, AudioFilter, SubtitleFilter }; rootCommand.TreatUnmatchedTokensAsErrors = true; rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index f727724..dc05bea 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -51,6 +51,10 @@ namespace N_m3u8DL_RE.CommandLine /// public int DownloadRetryCount { get; set; } /// + /// See: . + /// + public TimeSpan? LiveRecordLimit { get; set; } + /// /// See: . /// public bool SkipMerge { get; set; } @@ -107,6 +111,18 @@ namespace N_m3u8DL_RE.CommandLine /// public bool ConcurrentDownload { get; set; } /// + /// See: . + /// + public bool LiveRealTimeMerge { get; set; } + /// + /// See: . + /// + public bool LiveKeepSegments { get; set; } + /// + /// See: . + /// + public bool LivePerformAsVod { get; set; } + /// /// See: . /// public SubtitleFormat SubtitleFormat { get; set; } diff --git a/src/N_m3u8DL-RE/Directory.Build.props b/src/N_m3u8DL-RE/Directory.Build.props index c4ac7ef..7f25d8a 100644 --- a/src/N_m3u8DL-RE/Directory.Build.props +++ b/src/N_m3u8DL-RE/Directory.Build.props @@ -9,7 +9,6 @@ true true zh-CN;zh-TW;en-US - 7.0.0-* true diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index 4dcaa4c..e09f479 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -367,7 +367,7 @@ namespace N_m3u8DL_RE.DownloadManager if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); output = Path.ChangeExtension(output, ".srt"); } await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); @@ -402,7 +402,7 @@ namespace N_m3u8DL_RE.DownloadManager if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); output = Path.ChangeExtension(output, ".srt"); } await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); @@ -435,7 +435,7 @@ namespace N_m3u8DL_RE.DownloadManager if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); output = Path.ChangeExtension(output, ".srt"); } await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); @@ -472,7 +472,7 @@ namespace N_m3u8DL_RE.DownloadManager if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) { path = Path.ChangeExtension(path, ".srt"); - subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); + subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat); output = Path.ChangeExtension(output, ".srt"); } await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false)); diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index c1db9c0..3692f4b 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -186,7 +186,7 @@ namespace N_m3u8DL_RE var streams = await extractor.ExtractStreamsAsync(); //直播检测 - var livingFlag = streams.Any(s => s.Playlist?.IsLive == true); + var livingFlag = streams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod; if (livingFlag) { Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]"); @@ -248,7 +248,7 @@ namespace N_m3u8DL_RE //HLS: 选中流中若有没加载出playlist的,加载playlist //DASH: 加载playlist (调用url预处理器) - if (selectedStreams.Any(s => s.Playlist == null) || extractor.Extractor.ExtractorType == ExtractorType.MPEG_DASH) + if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH) await extractor.FetchPlayListAsync(selectedStreams); //无法识别的加密方式,自动开启二进制合并 @@ -281,19 +281,11 @@ namespace N_m3u8DL_RE //尝试从URL或文件读取文件名 if (string.IsNullOrEmpty(option.SaveName)) { - if (File.Exists(option.Input)) - { - option.SaveName = Path.GetFileNameWithoutExtension(option.Input) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - } - else - { - var uri = new Uri(option.Input); - var name = uri.GetLeftPart(UriPartial.Path).Split('/').Last(); - name = string.Join(".", name.Split('.').SkipLast(1)).Trim('.'); - option.SaveName = ConvertUtil.GetValidFileName(name) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - } + option.SaveName = OtherUtil.GetFileNameFromInput(option.Input); } + Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]"); + //下载配置 var downloadConfig = new DownloaderConfig() { @@ -313,7 +305,12 @@ namespace N_m3u8DL_RE } else { - throw new NotSupportedException("Live not supported yet."); + var sldm = new SimpleLiveRecordManager(downloadConfig, selectedStreams, extractor); + var result = await sldm.StartRecordAsync(); + if (result) + Logger.InfoMarkUp("[white on green]Done[/]"); + else + Logger.ErrorMarkUp("[white on red]Faild[/]"); } } catch (Exception ex) diff --git a/src/N_m3u8DL-RE/Util/MergeUtil.cs b/src/N_m3u8DL-RE/Util/MergeUtil.cs index b57ed78..849465d 100644 --- a/src/N_m3u8DL-RE/Util/MergeUtil.cs +++ b/src/N_m3u8DL-RE/Util/MergeUtil.cs @@ -193,7 +193,7 @@ namespace N_m3u8DL_RE.Util command.Append($" -metadata date=\"{dateString}\" -ignore_unknown -copy_unknown "); command.Append($" \"{outputPath}.{ext}\""); - + InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory); if (File.Exists($"{outputPath}.{ext}") && new FileInfo($"{outputPath}.{ext}").Length > 1024) diff --git a/src/N_m3u8DL-RE/Util/ConvertUtil.cs b/src/N_m3u8DL-RE/Util/OtherUtil.cs similarity index 54% rename from src/N_m3u8DL-RE/Util/ConvertUtil.cs rename to src/N_m3u8DL-RE/Util/OtherUtil.cs index b42fd36..f7091a8 100644 --- a/src/N_m3u8DL-RE/Util/ConvertUtil.cs +++ b/src/N_m3u8DL-RE/Util/OtherUtil.cs @@ -1,11 +1,12 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Enum; +using System.CommandLine; using System.Text; namespace N_m3u8DL_RE.Util { - internal class ConvertUtil + internal class OtherUtil { public static Dictionary SplitHeaderArrayToDic(string[]? headers) { @@ -66,5 +67,55 @@ namespace N_m3u8DL_RE.Util } return title.Trim('.'); } + + /// + /// 从输入自动获取文件名 + /// + /// + /// + public static string GetFileNameFromInput(string input, bool addSuffix = true) + { + var saveName = addSuffix ? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") : string.Empty; + if (File.Exists(input)) + { + saveName = Path.GetFileNameWithoutExtension(input) + "_" + saveName; + } + else + { + var uri = new Uri(input); + var name = uri.GetLeftPart(UriPartial.Path).Split('/').Last(); + name = string.Join(".", name.Split('.').SkipLast(1)).Trim('.'); + saveName = GetValidFileName(name) + "_" + saveName; + } + return saveName; + } + + /// + /// 从 hh:mm:ss 解析TimeSpan + /// + /// + /// + public static TimeSpan ParseDur(string timeStr) + { + var arr = timeStr.Replace(":", ":").Split(':'); + var days = -1; + var hours = -1; + var mins = -1; + var secs = -1; + arr.Reverse().Select(i => Convert.ToInt32(i)).ToList().ForEach(item => + { + if (secs == -1) secs = item; + else if (mins == -1) mins = item; + else if (hours == -1) hours = item; + else if (days == -1) days = item; + }); + + if (days == -1) days = 0; + if (hours == -1) hours = 0; + if (mins == -1) mins = 0; + if (secs == -1) secs = 0; + + return new TimeSpan(days, hours, mins, secs); + } } }