diff --git a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs index c1c2442..fd90b78 100644 --- a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs +++ b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs @@ -17,6 +17,9 @@ namespace N_m3u8DL_RE.Common.Entity public string? Name { get; set; } public Choise? Default { get; set; } + //由于用户选择 被跳过的分片总时长 + public double? SkippedDuration { get; set; } + //MSS信息 public MSSData? MSSData { get; set; } diff --git a/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs b/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs index 9a637c7..a0f4979 100644 --- a/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs +++ b/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs @@ -225,6 +225,22 @@ namespace N_m3u8DL_RE.Common.Entity return sb.ToString(); } + /// + /// 字幕向前平移指定时间 + /// + /// + public void LeftShiftTime(TimeSpan time) + { + foreach (var cue in this.Cues) + { + if (cue.StartTime.TotalSeconds - time.TotalSeconds > 0) cue.StartTime -= time; + else cue.StartTime = TimeSpan.FromSeconds(0); + + if (cue.EndTime.TotalSeconds - time.TotalSeconds > 0) cue.EndTime -= time; + else cue.EndTime = TimeSpan.FromSeconds(0); + } + } + public string ToVtt() { return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString(); diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index 9e2c6e5..f88cd00 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -10,6 +10,9 @@ namespace N_m3u8DL_RE.Common.Resource public class ResString { public readonly static string ReLiveTs = ""; + public static string customRangeWarn { get => GetText("customRangeWarn"); } + public static string customRangeFound { get => GetText("customRangeFound"); } + public static string customRangeInvalid { get => GetText("customRangeInvalid"); } public static string autoBinaryMerge { get => GetText("autoBinaryMerge"); } public static string autoBinaryMerge2 { get => GetText("autoBinaryMerge2"); } public static string autoBinaryMerge3 { get => GetText("autoBinaryMerge3"); } @@ -41,6 +44,7 @@ namespace N_m3u8DL_RE.Common.Resource public static string cmd_selectSubtitle { get => GetText("cmd_selectSubtitle"); } public static string cmd_dropSubtitle { get => GetText("cmd_dropSubtitle"); } public static string cmd_selectSubtitle_more { get => GetText("cmd_selectSubtitle_more"); } + public static string cmd_custom_range { get => GetText("cmd_custom_range"); } public static string cmd_customHLSMethod { get => GetText("cmd_customHLSMethod"); } public static string cmd_customHLSKey { get => GetText("cmd_customHLSKey"); } public static string cmd_customHLSIv { get => GetText("cmd_customHLSIv"); } @@ -69,6 +73,7 @@ namespace N_m3u8DL_RE.Common.Resource public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); } public static string cmd_useSystemProxy { get => GetText("cmd_useSystemProxy"); } public static string cmd_customProxy { get => GetText("cmd_customProxy"); } + public static string cmd_customRange { get => GetText("cmd_customRange"); } public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); } public static string cmd_livePipeMux { get => GetText("cmd_livePipeMux"); } public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); } diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index ff63767..8581ebf 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -10,6 +10,24 @@ namespace N_m3u8DL_RE.Common.Resource { public static Dictionary LANG_DIC = new() { + ["customRangeWarn"] = new TextContainer + ( + zhCN: "请注意,自定义下载范围有时会导致音画不同步", + zhTW: "請注意,自定義下載範圍有時會導致音畫不同步", + enUS: "Please note that custom range may sometimes result in audio and video being out of sync" + ), + ["customRangeInvalid"] = new TextContainer + ( + zhCN: "自定义下载范围无效", + zhTW: "自定義下載範圍無效", + enUS: "User customed range invalid" + ), + ["customRangeFound"] = new TextContainer + ( + zhCN: "用户自定义下载范围:", + zhTW: "用戶自定義下載範圍:", + enUS: "User customed range: " + ), ["processImageSub"] = new TextContainer ( zhCN: "正在处理图形字幕", @@ -304,6 +322,12 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "設置請求代理, 如 http://127.0.0.1:8888", enUS: "Set web request proxy, like http://127.0.0.1:8888" ), + ["cmd_customRange"] = new TextContainer + ( + zhCN: "仅下载部分分片. 输入 \"--morehelp custom-range\" 以查看详细信息", + zhTW: "僅下載部分分片. 輸入 \"--morehelp custom-range\" 以查看詳細訊息", + enUS: "Download only part of the segments. Use \"--morehelp custom-range\" for more details" + ), ["cmd_useSystemProxy"] = new TextContainer ( zhCN: "使用系统默认代理", @@ -478,6 +502,39 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "通過正則表達式去除符合要求的字幕流.", enUS: "Drop subtitle streams by regular expressions." ), + ["cmd_custom_range"] = new TextContainer + ( + zhCN: "下载点播内容时, 仅下载部分分片.\r\n\r\n" + + "例如: \r\n" + + "# 下载[0,10]共11个分片\r\n" + + "--custom-range 0-10\r\n" + + "# 下载从序号10开始的后续分片\r\n" + + "--custom-range 10-\r\n" + + "# 下载前100个分片\r\n" + + "--custom-range -99\r\n" + + "# 下载第5分钟到20分钟的内容\r\n" + + "--custom-range 05:00-20:00\r\n", + zhTW: "下載點播內容時, 僅下載部分分片.\r\n\r\n" + + "例如: \r\n" + + "# 下載[0,10]共11個分片\r\n" + + "--custom-range 0-10\r\n" + + "# 下載從序號10開始的後續分片\r\n" + + "--custom-range 10-\r\n" + + "# 下載前100個分片\r\n" + + "--custom-range -99\r\n" + + "# 下載第5分鐘到20分鐘的內容\r\n" + + "--custom-range 05:00-20:00\r\n", + enUS: "Download only part of the segments when downloading vod content.\r\n\r\n" + + "Examples: \r\n" + + "# Download [0,10], a total of 11 segments\r\n" + + "--custom-range 0-10\r\n" + + "# Download subsequent segments starting from index 10\r\n" + + "--custom-range 10-\r\n" + + "# Download the first 100 segments\r\n" + + "--custom-range -99\r\n" + + "# Download content from the 05:00 to 20:00\r\n" + + "--custom-range 05:00-20:00\r\n" + ), ["cmd_selectSubtitle_more"] = new TextContainer ( zhCN: "通过正则表达式选择符合要求的字幕流. 参考 --select-video\r\n\r\n" + @@ -505,6 +562,7 @@ namespace N_m3u8DL_RE.Common.Resource "* format=FORMAT: 指定混流容器 mkv, mp4\r\n" + "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\r\n" + "* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" + + "* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\r\n" + "* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)\r\n\r\n" + "例如: \r\n" + "# 混流为mp4容器\r\n" + @@ -517,6 +575,7 @@ namespace N_m3u8DL_RE.Common.Resource "* format=FORMAT: 指定混流容器 mkv, mp4\r\n" + "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默認: ffmpeg)\r\n" + "* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" + + "* skip_sub=BOOL: 是否忽略字幕文件 (默認: false)\r\n" + "* keep=BOOL: 混流完成是否保留文件 true, false (默認: false)\r\n\r\n" + "例如: \r\n" + "# 混流為mp4容器\r\n" + @@ -529,6 +588,7 @@ namespace N_m3u8DL_RE.Common.Resource "* format=FORMAT: set container. mkv, mp4\r\n" + "* muxer=MUXER: set muxer. ffmpeg, mkvmerge (Default: ffmpeg)\r\n" + "* bin_path=PATH: set binary file path. (Default: auto)\r\n" + + "* skip_sub=BOOL: set whether or not skip subtitle files (Default: false)\r\n" + "* keep=BOOL: set whether or not keep files. true, false (Default: false)\r\n\r\n" + "Examples: \r\n" + "# mux to mp4\r\n" + diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index 1d36b4b..ed0511e 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -18,10 +18,12 @@ namespace N_m3u8DL_RE.CommandLine { internal partial class CommandInvoker { - public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20230618"; + public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20230628"; [GeneratedRegex("((best|worst)\\d*|all)")] private static partial Regex ForStrRegex(); + [GeneratedRegex("(\\d*)-(\\d*)")] + private static partial Regex RangeRegex(); private readonly static Argument Input = new(name: "input", description: ResString.cmd_Input); private readonly static Option TmpDir = new(new string[] { "--tmp-dir" }, description: ResString.cmd_tmpDir); @@ -60,6 +62,9 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option UseSystemProxy = new(new string[] { "--use-system-proxy" }, description: ResString.cmd_useSystemProxy, getDefaultValue: () => true); private readonly static Option CustomProxy = new(new string[] { "--custom-proxy" }, description: ResString.cmd_customProxy, parseArgument: ParseProxy) { ArgumentHelpName = "URL" }; + //只下载部分分片 + private readonly static Option CustomRange = new(new string[] { "--custom-range" }, description: ResString.cmd_customRange, parseArgument: ParseCustomRange) { ArgumentHelpName = "RANGE" }; + //morehelp private readonly static Option MoreHelp = new(new string[] { "--morehelp" }, description: ResString.cmd_moreHelp) { ArgumentHelpName = "OPTION" }; @@ -94,6 +99,55 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option DropAudioFilter = new(new string[] { "-da", "--drop-audio" }, description: ResString.cmd_dropAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private readonly static Option DropSubtitleFilter = new(new string[] { "-ds", "--drop-subtitle" }, description: ResString.cmd_dropSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + /// + /// 解析用户定义的下载范围 + /// + /// + /// + /// + private static CustomRange? ParseCustomRange(ArgumentResult result) + { + var input = result.Tokens.First().Value; + //支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00; + try + { + if (string.IsNullOrEmpty(input)) + return null; + + var arr = input.Split('-'); + if (arr.Length != 2) + throw new ArgumentException("Bad format!"); + + if (input.Contains(":")) + { + return new CustomRange() + { + InputStr = input, + StartSec = arr[0] == "" ? 0 : OtherUtil.ParseDur(arr[0]).TotalSeconds, + EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds, + }; + } + else if (RangeRegex().IsMatch(input)) + { + var left = RangeRegex().Match(input).Groups[1].Value; + var right = RangeRegex().Match(input).Groups[2].Value; + return new CustomRange() + { + InputStr = input, + StartSegIndex = left == "" ? 0 : long.Parse(left), + EndSegIndex = right == "" ? long.MaxValue : long.Parse(right), + }; + } + + throw new ArgumentException("Bad format!"); + } + catch (Exception ex) + { + result.ErrorMessage = $"error in parse CustomRange: " + ex.Message; + return null; + } + } + /// /// 解析用户代理 /// @@ -365,6 +419,13 @@ namespace N_m3u8DL_RE.CommandLine result.ErrorMessage = $"keep={keep} not valid"; return null; } + //是否忽略字幕 + var skipSub = p.GetValue("skip_sub") ?? "false"; + if (skipSub != "true" && skipSub != "false") + { + result.ErrorMessage = $"skip_sub={keep} not valid"; + return null; + } //冲突检测 if (muxer == "mkvmerge" && format == "mp4") { @@ -376,6 +437,7 @@ namespace N_m3u8DL_RE.CommandLine UseMkvmerge = muxer == "mkvmerge", MuxToMp4 = format == "mp4", KeepFiles = keep == "true", + SkipSubtitle = skipSub == "true", BinPath = bin_path == "auto" ? null : bin_path }; } @@ -431,6 +493,7 @@ namespace N_m3u8DL_RE.CommandLine LiveFixVttByAudio = bindingContext.ParseResult.GetValueForOption(LiveFixVttByAudio), UseSystemProxy = bindingContext.ParseResult.GetValueForOption(UseSystemProxy), CustomProxy = bindingContext.ParseResult.GetValueForOption(CustomProxy), + CustomRange = bindingContext.ParseResult.GetValueForOption(CustomRange), LiveWaitTime = bindingContext.ParseResult.GetValueForOption(LiveWaitTime), NoDateInfo = bindingContext.ParseResult.GetValueForOption(NoDateInfo), NoLog = bindingContext.ParseResult.GetValueForOption(NoLog), @@ -458,10 +521,8 @@ namespace N_m3u8DL_RE.CommandLine if (muxAfterDoneValue != null) { option.MuxAfterDone = true; - option.MuxKeepFiles = muxAfterDoneValue.KeepFiles; - option.MuxToMp4 = muxAfterDoneValue.MuxToMp4; - option.UseMkvmerge = muxAfterDoneValue.UseMkvmerge; - if (option.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath; + option.MuxOptions = muxAfterDoneValue; + if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath; else option.FFmpegBinaryPath = muxAfterDoneValue.BinPath; } @@ -485,6 +546,7 @@ namespace N_m3u8DL_RE.CommandLine "select-video" => ResString.cmd_selectVideo_more, "select-audio" => ResString.cmd_selectAudio_more, "select-subtitle" => ResString.cmd_selectSubtitle_more, + "custom-range" => ResString.cmd_custom_range, _ => $"Option=\"{option}\" not found" }; Console.WriteLine($"More Help:\r\n\r\n --{option}\r\n\r\n" + msg); @@ -498,7 +560,7 @@ namespace N_m3u8DL_RE.CommandLine FFmpegBinaryPath, LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, MuxAfterDone, - CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, TaskStartAt, + CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, CustomRange, TaskStartAt, LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveFixVttByAudio, LiveRecordLimit, LiveWaitTime, MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, MoreHelp }; diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 7716010..ec2369a 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -113,14 +113,6 @@ namespace N_m3u8DL_RE.CommandLine /// public bool MuxAfterDone { get; set; } /// - /// See: . - /// - public bool MuxToMp4 { get; set; } - /// - /// See: . - /// - public bool UseMkvmerge { get; set; } - /// /// See: . /// public bool ConcurrentDownload { get; set; } @@ -221,10 +213,14 @@ namespace N_m3u8DL_RE.CommandLine /// public WebProxy? CustomProxy { get; set; } /// + /// See: . + /// + public CustomRange? CustomRange { get; set; } + /// /// See: . /// public int? LiveWaitTime { get; set; } - public bool MuxKeepFiles { get; set; } + public MuxOptions MuxOptions { get; set; } //public bool LiveWriteHLS { get; set; } = true; /// /// See: . diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index 760148a..c2225ee 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -126,6 +126,9 @@ namespace N_m3u8DL_RE.DownloadManager var currentKID = ""; var readInfo = false; //是否读取过 + //用户自定义范围导致被跳过的时长 计算字幕偏移使用 + var skippedDur = streamSpec.SkippedDuration ?? 0d; + Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); //创建文件夹 @@ -360,7 +363,7 @@ namespace N_m3u8DL_RE.DownloadManager //手动计算MPEGTS if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { - vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); + vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); } if (first) { finalVtt = vtt; first = false; } else finalVtt.AddCuesFromOne(vtt); @@ -371,6 +374,8 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + //设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) @@ -405,6 +410,8 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + //设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) @@ -435,7 +442,7 @@ namespace N_m3u8DL_RE.DownloadManager //手动计算MPEGTS if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { - vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); + vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); } if (first) { finalVtt = vtt; first = false; } else finalVtt.AddCuesFromOne(vtt); @@ -451,6 +458,8 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + //设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) @@ -485,7 +494,7 @@ namespace N_m3u8DL_RE.DownloadManager //手动计算MPEGTS if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { - vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); + vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); } if (first) { finalVtt = vtt; first = false; } else finalVtt.AddCuesFromOne(vtt); @@ -502,6 +511,8 @@ namespace N_m3u8DL_RE.DownloadManager FileDic.Clear(); var index = 0; var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + //设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); var subContentFixed = finalVtt.ToVtt(); //转换字幕格式 if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) @@ -595,6 +606,7 @@ namespace N_m3u8DL_RE.DownloadManager //记录所有文件信息 if (File.Exists(output)) + { OutputFiles.Add(new OutputFile() { Index = task.Id, @@ -604,6 +616,7 @@ namespace N_m3u8DL_RE.DownloadManager Mediainfos = mediaInfos, MediaType = streamSpec.MediaType, }); + } return true; } @@ -683,24 +696,29 @@ namespace N_m3u8DL_RE.DownloadManager if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) { OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); + //是否跳过字幕 + if (DownloaderConfig.MyOptions.MuxOptions.SkipSubtitle) + { + OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); + } if (DownloaderConfig.MyOptions.MuxImports != null) { OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports); } OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var ext = DownloaderConfig.MyOptions.MuxToMp4 ? ".mp4" : ".mkv"; + var ext = DownloaderConfig.MyOptions.MuxOptions.MuxToMp4 ? ".mp4" : ".mkv"; var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); var outName = $"{dirName}.MUX"; var outPath = Path.Combine(saveDir, outName); Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); var result = false; - if (DownloaderConfig.MyOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); - else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxToMp4, !DownloaderConfig.MyOptions.NoDateInfo); + if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); + else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxToMp4, !DownloaderConfig.MyOptions.NoDateInfo); //完成后删除各轨道文件 if (result) { - if (!DownloaderConfig.MyOptions.MuxKeepFiles) + if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) { Logger.WarnMarkUp("[grey]Cleaning files...[/]"); OutputFiles.ForEach(f => File.Delete(f.FilePath)); diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs index 229d3cf..0f531aa 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs @@ -892,24 +892,29 @@ namespace N_m3u8DL_RE.DownloadManager if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) { OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); + //是否跳过字幕 + if (DownloaderConfig.MyOptions.MuxOptions.SkipSubtitle) + { + OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); + } if (DownloaderConfig.MyOptions.MuxImports != null) { OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports); } OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var ext = DownloaderConfig.MyOptions.MuxToMp4 ? ".mp4" : ".mkv"; + var ext = DownloaderConfig.MyOptions.MuxOptions.MuxToMp4 ? ".mp4" : ".mkv"; var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); var outName = $"{dirName}.MUX"; var outPath = Path.Combine(saveDir, outName); Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); var result = false; - if (DownloaderConfig.MyOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); - else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxToMp4, !DownloaderConfig.MyOptions.NoDateInfo); + if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); + else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxToMp4, !DownloaderConfig.MyOptions.NoDateInfo); //完成后删除各轨道文件 if (result) { - if (!DownloaderConfig.MyOptions.MuxKeepFiles) + if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) { Logger.WarnMarkUp("[grey]Cleaning files...[/]"); OutputFiles.ForEach(f => File.Delete(f.FilePath)); diff --git a/src/N_m3u8DL-RE/Entity/CustomRange.cs b/src/N_m3u8DL-RE/Entity/CustomRange.cs new file mode 100644 index 0000000..e59b4c1 --- /dev/null +++ b/src/N_m3u8DL-RE/Entity/CustomRange.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Entity +{ + public class CustomRange + { + public required string InputStr { get; set; } + public double? StartSec { get; set; } + public double? EndSec { get; set; } + + public long? StartSegIndex { get; set; } + public long? EndSegIndex { get; set;} + + public override string? ToString() + { + return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}"; + } + } +} diff --git a/src/N_m3u8DL-RE/Entity/MuxOptions.cs b/src/N_m3u8DL-RE/Entity/MuxOptions.cs index 2761832..7d43757 100644 --- a/src/N_m3u8DL-RE/Entity/MuxOptions.cs +++ b/src/N_m3u8DL-RE/Entity/MuxOptions.cs @@ -11,6 +11,7 @@ namespace N_m3u8DL_RE.Entity public bool UseMkvmerge { get; set; } = false; public bool MuxToMp4 { get; set; } = false; public bool KeepFiles { get; set; } = false; + public bool SkipSubtitle { get; set; } = false; public string? BinPath { get; set; } } } diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index 4f42c8a..10f8ed6 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -96,6 +96,7 @@ namespace N_m3u8DL_RE throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!"); } + //LivePipeMux开启时 LiveRealTimeMerge必须开启 if (option.LivePipeMux && !option.LiveRealTimeMerge) { Logger.WarnMarkUp("LivePipeMux detected, forced enable LiveRealTimeMerge"); @@ -114,7 +115,7 @@ namespace N_m3u8DL_RE Logger.Extra($"ffmpeg => {option.FFmpegBinaryPath}"); //预先检查mkvmerge - if (option.UseMkvmerge && option.MuxAfterDone) + if (option.MuxOptions != null && option.MuxOptions.UseMkvmerge && option.MuxAfterDone) { if (option.MkvmergeBinaryPath == null) option.MkvmergeBinaryPath = GlobalUtil.FindExecutable("mkvmerge"); @@ -299,10 +300,13 @@ namespace N_m3u8DL_RE option.BinaryMerge = true; } + //应用用户自定义的分片范围 + if (!livingFlag) + FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange); + //记录文件 extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams); - Logger.Info(ResString.selectedStream); foreach (var item in selectedStreams) { @@ -322,7 +326,6 @@ namespace N_m3u8DL_RE Console.ReadKey(); #endif - Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]"); //开始MuxAfterDone后自动使用二进制版 diff --git a/src/N_m3u8DL-RE/Util/FilterUtil.cs b/src/N_m3u8DL-RE/Util/FilterUtil.cs index b9f3629..eb77a9a 100644 --- a/src/N_m3u8DL-RE/Util/FilterUtil.cs +++ b/src/N_m3u8DL-RE/Util/FilterUtil.cs @@ -1,5 +1,6 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Entity; using Spectre.Console; @@ -182,5 +183,49 @@ namespace N_m3u8DL_RE.Util } } } + + /// + /// 应用用户自定义的分片范围 + /// + /// + /// + public static void ApplyCustomRange(List selectedSteams, CustomRange? customRange) + { + var resultList = selectedSteams.Select(x => 0d).ToList(); + + if (customRange == null) return; + + Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]"); + Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]"); + + var filteByIndex = customRange.StartSegIndex != null && customRange.EndSegIndex != null; + var filteByTime = customRange.StartSec != null && customRange.EndSec != null; + + if (!filteByIndex && !filteByTime) + { + Logger.ErrorMarkUp(ResString.customRangeInvalid); + return; + } + + foreach (var stream in selectedSteams) + { + var skippedDur = 0d; + if (stream.Playlist == null) continue; + foreach (var part in stream.Playlist.MediaParts) + { + var newSegments = new List(); + if (filteByIndex) + newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList(); + else + newSegments = part.MediaSegments.Where(seg => stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) >= customRange.StartSec + && stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) <= customRange.EndSec).ToList(); + + if (newSegments.Count > 0) + skippedDur += part.MediaSegments.Where(seg => seg.Index < newSegments.First().Index).Sum(x => x.Duration); + part.MediaSegments = newSegments; + } + stream.SkippedDuration = skippedDur; + } + } } }