diff --git a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs index 24d5258..6623891 100644 --- a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs +++ b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs @@ -11,6 +11,7 @@ namespace N_m3u8DL_RE.Common.Entity public long Index { get; set; } public double Duration { get; set; } public string? Title { get; set; } + public DateTime? DateTime { get; set; } public long? StartRange { get; set; } public long? StopRange { get => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null; } diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index 39694da..9078216 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -64,6 +64,7 @@ namespace N_m3u8DL_RE.Common.Resource public static string cmd_customProxy { get => GetText("cmd_customProxy"); } public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); } public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); } + public static string cmd_liveWaitTime { get => GetText("cmd_liveWaitTime"); } 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"); } diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index 9b63a9d..36b59f8 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -262,6 +262,12 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "以點播方式下載直播流", enUS: "Download live streams as vod" ), + ["cmd_liveWaitTime"] = new TextContainer + ( + zhCN: "手动设置直播列表刷新间隔", + zhTW: "手動設置直播列表刷新間隔", + enUS: "Manually set the live playlist refresh interval" + ), ["cmd_customHLSMethod"] = new TextContainer ( zhCN: "指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)", diff --git a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs index 870d4cb..66cae0d 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs @@ -295,7 +295,7 @@ namespace N_m3u8DL_RE.Parser.Extractor //program date time else if (line.StartsWith(HLSTags.ext_x_program_date_time)) { - // + segment.DateTime = DateTime.Parse(ParserUtil.GetAttribute(line)); } //解析不连续标记,需要单独合并(timestamp不同) else if (line.StartsWith(HLSTags.ext_x_discontinuity)) diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index a8e9300..69cfea5 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -70,6 +70,7 @@ namespace N_m3u8DL_RE.CommandLine 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 LiveWaitTime = new(new string[] { "--live-wait-time" }, description: ResString.cmd_liveRecordLimit) { ArgumentHelpName = "SEC" }; //复杂命令行如下 @@ -374,6 +375,7 @@ namespace N_m3u8DL_RE.CommandLine LivePerformAsVod = bindingContext.ParseResult.GetValueForOption(LivePerformAsVod), UseSystemProxy = bindingContext.ParseResult.GetValueForOption(UseSystemProxy), CustomProxy = bindingContext.ParseResult.GetValueForOption(CustomProxy), + LiveWaitTime = bindingContext.ParseResult.GetValueForOption(LiveWaitTime), }; if (bindingContext.ParseResult.HasOption(CustomHLSMethod)) option.CustomHLSMethod = bindingContext.ParseResult.GetValueForOption(CustomHLSMethod); @@ -439,7 +441,7 @@ namespace N_m3u8DL_RE.CommandLine LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, MuxAfterDone, CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, - LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LiveRecordLimit, + LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LiveRecordLimit, LiveWaitTime, MuxImports, VideoFilter, AudioFilter, SubtitleFilter, MoreHelp }; rootCommand.TreatUnmatchedTokensAsErrors = true; diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index a4ea8de..24cc8b5 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -196,6 +196,10 @@ namespace N_m3u8DL_RE.CommandLine /// See: . /// public WebProxy? CustomProxy { get; set; } + /// + /// See: . + /// + public int? LiveWaitTime { get; set; } public bool MuxKeepFiles { get; set; } } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs index 8f576d9..eab733c 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs @@ -40,6 +40,7 @@ namespace N_m3u8DL_RE.DownloadManager int WAIT_SEC = 0; //刷新间隔 ConcurrentDictionary RecordingDurDic = new(); //已录制时长 ConcurrentDictionary LastUrlDic = new(); //上次下载的url + ConcurrentDictionary DateTimeDic = new(); //上次下载的dateTime CancellationTokenSource CancellationTokenSource = new(); //取消Wait public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) @@ -84,6 +85,39 @@ namespace N_m3u8DL_RE.DownloadManager } } + /// + /// 获取时间戳 + /// + /// + /// + private long GetUnixTimestamp(DateTime dateTime) + { + return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds(); + } + + /// + /// 获取分段文件夹 + /// + /// + /// + /// + private string GetSegmentName(MediaSegment segment, bool allHasDatetime) + { + bool hls = StreamExtractor.ExtractorType == ExtractorType.HLS; + + string name = OtherUtil.GetFileNameFromInput(segment.Url, false); + if (hls && allHasDatetime) + { + name = GetUnixTimestamp(segment.DateTime!.Value).ToString(); + } + else if (hls && segment.Index > 10) + { + name = segment.Index.ToString(); + } + + return name; + } + private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos, ref bool useAACFilter) { if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) @@ -125,7 +159,6 @@ namespace N_m3u8DL_RE.DownloadManager var readInfo = false; //是否读取过 bool useAACFilter = false; //ffmpeg合并flag bool initDownloaded = false; //是否下载过init文件 - bool hls = StreamExtractor.ExtractorType == ExtractorType.HLS; ConcurrentDictionary FileDic = new(); List mediaInfos = new(); FileStream? fileOutputStream = null; @@ -205,8 +238,7 @@ namespace N_m3u8DL_RE.DownloadManager } } - //计算填零个数 - var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); + var allHasDatetime = segments.All(s => s.DateTime != null); //下载第一个分片 if (!readInfo) @@ -214,7 +246,7 @@ namespace N_m3u8DL_RE.DownloadManager var seg = segments.First(); segments = segments.Skip(1); //获取文件名 - var filename = hls && seg.Index > 10 ? seg.Index.ToString(pad) : OtherUtil.GetFileNameFromInput(seg.Url, false); + var filename = GetSegmentName(seg, allHasDatetime); var index = seg.Index; var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); @@ -262,7 +294,7 @@ namespace N_m3u8DL_RE.DownloadManager await Parallel.ForEachAsync(segments, options, async (seg, _) => { //获取文件名 - var filename = hls && seg.Index > 10 ? seg.Index.ToString(pad) : OtherUtil.GetFileNameFromInput(seg.Url, false); + var filename = GetSegmentName(seg, allHasDatetime); var index = seg.Index; var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); @@ -505,7 +537,7 @@ namespace N_m3u8DL_RE.DownloadManager if (WAIT_SEC != 0) { //过滤不需要下载的片段 - FilterMediaSegments(streamSpec, LastUrlDic[streamSpec.ToShortString()]); + FilterMediaSegments(streamSpec, LastUrlDic[streamSpec.ToShortString()], DateTimeDic[streamSpec.ToShortString()]); var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments; if (newList.Count > 0) { @@ -513,6 +545,9 @@ namespace N_m3u8DL_RE.DownloadManager await target.SendAsync(newList); //更新最新链接 LastUrlDic[streamSpec.ToShortString()] = GetPath(newList.Last().Url); + //尝试更新时间戳 + var dt = newList.Last().DateTime; + DateTimeDic[streamSpec.ToShortString()] = dt != null ? GetUnixTimestamp(dt.Value) : 0L; task.MaxValue += newList.Count; } try @@ -532,11 +567,22 @@ namespace N_m3u8DL_RE.DownloadManager target.Complete(); } - private void FilterMediaSegments(StreamSpec streamSpec, string lastUrl) + private void FilterMediaSegments(StreamSpec streamSpec, string lastUrl, long dateTime) { - if (string.IsNullOrEmpty(lastUrl)) return; + if (string.IsNullOrEmpty(lastUrl) && dateTime == 0) return; + + var index = -1; + + //优先使用dateTime判断 + if (dateTime != 0 && streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null)) + { + index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetUnixTimestamp(s.DateTime!.Value) == dateTime); + } + else + { + index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetPath(s.Url) == lastUrl); + } - var index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetPath(s.Url) == lastUrl); if (index > -1) { streamSpec.Playlist!.MediaParts[0].MediaSegments = streamSpec.Playlist!.MediaParts[0].MediaSegments.Skip(index + 1).ToList(); @@ -566,11 +612,16 @@ namespace N_m3u8DL_RE.DownloadManager foreach (var item in SelectedSteams) { LastUrlDic[item.ToShortString()] = ""; + DateTimeDic[item.ToShortString()] = 0L; } //设置等待时间 if (WAIT_SEC == 0) { WAIT_SEC = (int)(SelectedSteams.Min(s => s.Playlist!.MediaParts[0].MediaSegments.Sum(s => s.Duration)) / 2); + WAIT_SEC -= 2; //再提前两秒吧 留出冗余 + if (DownloaderConfig.MyOptions.LiveWaitTime != null) + WAIT_SEC = DownloaderConfig.MyOptions.LiveWaitTime.Value; + if (WAIT_SEC <= 0) WAIT_SEC = 1; Logger.WarnMarkUp($"set refresh interval to {WAIT_SEC} seconds"); }