diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index ae1a685..468194d 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -28,6 +28,9 @@ namespace N_m3u8DL_RE.Common.Resource public static string cmd_baseUrl { get => GetText("cmd_baseUrl"); } public static string cmd_header { get => GetText("cmd_header"); } public static string cmd_muxImport { get => GetText("cmd_muxImport"); } + public static string cmd_selectVideo { get => GetText("cmd_selectVideo"); } + public static string cmd_selectAudio { get => GetText("cmd_selectAudio"); } + public static string cmd_selectSubtitle { get => GetText("cmd_selectSubtitle"); } public static string cmd_Input { get => GetText("cmd_Input"); } public static string cmd_keys { get => GetText("cmd_keys"); } public static string cmd_keyText { get => GetText("cmd_keyText"); } @@ -49,9 +52,7 @@ 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_useMkvmerge { get => GetText("cmd_useMkvmerge"); } public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); } - public static string cmd_muxToMp4 { get => GetText("cmd_muxToMp4"); } public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); } public static string fetch { get => GetText("fetch"); } public static string ffmpegMerge { get => GetText("ffmpegMerge"); } @@ -78,6 +79,7 @@ namespace N_m3u8DL_RE.Common.Resource public static string startDownloading { get => GetText("startDownloading"); } public static string streamsInfo { get => GetText("streamsInfo"); } public static string writeJson { get => GetText("writeJson"); } + public static string noStreamsToDownload { get => GetText("noStreamsToDownload"); } private static string GetText(string key) { diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index 6cd0eb6..e88c4a4 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -238,18 +238,90 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "解密时使用shaka-packager替代mp4decrypt", enUS: "Use shaka-packager instead of mp4decrypt to decrypt" ), - ["cmd_useMkvmerge"] = new TextContainer - ( - zhCN: "混流时使用mkvmerge替代ffmpeg", - zhTW: "混流时使用mkvmerge替代ffmpeg", - enUS: "Use mkvmerge instead of ffmpeg to mux" - ), ["cmd_concurrentDownload"] = new TextContainer ( zhCN: "并发下载已选择的音频、视频和字幕", zhTW: "並發下載已選擇的音訊、影片和字幕", enUS: "Concurrently download the selected audio, video and subtitles" ), + ["cmd_selectVideo"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数:\r\n\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codec=REGEX:res=REGEX\r\n" + + "frame=REGEX:ch=REGEX:range=REGEX:url=REGEX:for=FOR\r\n\r\n" + + "* for=FOR: 选择方式. best[number], worst[number], all (默认: best)\r\n\r\n" + + "例如: \r\n" + + "# 选择最佳视频\r\n" + + "-sv best\r\n" + + "# 选择4K+HEVC视频\r\n" + + "-sv res=\"3840*\":codec=hvc1:for=best\r\n", + zhTW: "通過正則表達式選擇符合要求的影片軌. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codec=REGEX:res=REGEX\r\n" + + "frame=REGEX:ch=REGEX:range=REGEX:url=REGEX:for=FOR\r\n\r\n" + + "* for=FOR: 選擇方式. best[number], worst[number], all (默認: best)\r\n\r\n" + + "例如: \r\n" + + "# 選擇最佳影片\r\n" + + "-sv best\r\n" + + "# 選擇4K+HEVC影片\r\n" + + "-sv res=\"3840*\":codec=hvc1:for=best\r\n", + enUS: "Select video streams by regular expressions. OPTIONS is a colon separated list of:\r\n\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codec=REGEX:res=REGEX\r\n" + + "frame=REGEX:ch=REGEX:range=REGEX:url=REGEX:for=FOR\r\n\r\n" + + "* for=FOR: Select type. best[number], worst[number], all (Default: best)\r\n\r\n" + + "Examples: \r\n" + + "# select best video\r\n" + + "-sv best\r\n" + + "# select 4K+HEVC video\r\n" + + "-sv res=\"3840*\":codec=hvc1:for=best\r\n" + ), + ["cmd_selectAudio"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的音频流. 参考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 选择所有音频\r\n" + + "-sa all\r\n" + + "# 选择最佳英语音轨\r\n" + + "-sa lang=en:for=best\r\n" + + "# 选择最佳的2条英语(或日语)音轨\r\n" + + "-sa lang=\"ja|en\":for=best2\r\n", + zhTW: "通過正則表達式選擇符合要求的音軌. 參考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 選擇所有音訊\r\n" + + "-sa all\r\n" + + "# 選擇最佳英語音軌\r\n" + + "-sa lang=en:for=best\r\n" + + "# 選擇最佳的2條英語(或日語)音軌\r\n" + + "-sa lang=\"ja|en\":for=best2\r\n", + enUS: "Select audio streams by regular expressions. ref --select-video\r\n\r\n" + + "Examples: \r\n" + + "# select all\r\n" + + "-sa all\r\n" + + "# select best eng audio\r\n" + + "-sa lang=en:for=best\r\n" + + "# select best 2, and language is ja or en\r\n" + + "-sa lang=\"ja|en\":for=best2\r\n" + ), + ["cmd_selectSubtitle"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的字幕流. 参考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 选择所有字幕\r\n" + + "-ss all\r\n" + + "# 选择所有带有\"中文\"的字幕\r\n" + + "-ss name=\"中文\":for=all\r\n", + zhTW: "通過正則表達式選擇符合要求的字幕流. 參考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 選擇所有字幕\r\n" + + "-ss all\r\n" + + "# 選擇所有帶有\"中文\"的字幕\r\n" + + "-ss name=\"中文\":for=all\r\n", + enUS: "Select subtitle streams by regular expressions. ref --select-video\r\n\r\n" + + "Examples: \r\n" + + "# select all subs\r\n" + + "-ss all\r\n" + + "# select all subs containing \"English\"\r\n" + + "-ss name=\"English\":for=all\r\n" + ), ["cmd_muxAfterDone"] = new TextContainer ( zhCN: "所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\r\n\r\n" + @@ -258,11 +330,11 @@ namespace N_m3u8DL_RE.Common.Resource "* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" + "* keep=BOOL: 混流完成是否删除文件 true, false (默认: true)\r\n\r\n" + "例如: \r\n" + - "\r\n#混流为mp4容器\r\n" + + "# 混流为mp4容器\r\n" + "-M format=mp4\r\n" + - "\r\n#使用mkvmerge, 自动寻找程序\r\n" + + "# 使用mkvmerge, 自动寻找程序\r\n" + "-M format=mkv:muxer=mkvmerge\r\n" + - "\r\n#使用mkvmerge, 自定义程序路径\r\n" + + "# 使用mkvmerge, 自定义程序路径\r\n" + "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", zhTW: "所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + "* format=FORMAT: 指定混流容器 mkv, mp4\r\n" + @@ -270,11 +342,11 @@ namespace N_m3u8DL_RE.Common.Resource "* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" + "* keep=BOOL: 混流完成是否刪除文件 true, false (默認: true)\r\n\r\n" + "例如: \r\n" + - "\r\n#混流為mp4容器\r\n" + + "# 混流為mp4容器\r\n" + "-M format=mp4\r\n" + - "\r\n#使用mkvmerge, 自動尋找程序\r\n" + + "# 使用mkvmerge, 自動尋找程序\r\n" + "-M format=mkv:muxer=mkvmerge\r\n" + - "\r\n#使用mkvmerge, 自訂程序路徑\r\n" + + "# 使用mkvmerge, 自訂程序路徑\r\n" + "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", enUS: "When all works is done, try to mux the downloaded streams. OPTIONS is a colon separated list of:\r\n\r\n" + "* format=FORMAT: set container. mkv, mp4\r\n" + @@ -282,11 +354,11 @@ namespace N_m3u8DL_RE.Common.Resource "* bin_path=PATH: set binary file path. (Default: auto)\r\n" + "* keep=BOOL: set whether or not delete files. true, false (Default: true)\r\n\r\n" + "Examples: \r\n" + - "\r\n#mux to mp4\r\n" + + "# mux to mp4\r\n" + "-M format=mp4\r\n" + - "\r\n#use mkvmerge, auto detect bin path\r\n" + + "# use mkvmerge, auto detect bin path\r\n" + "-M format=mkv:muxer=mkvmerge\r\n" + - "\r\n#use mkvmerge, set bin path\r\n" + + "# use mkvmerge, set bin path\r\n" + "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n" ), ["cmd_muxImport"] = new TextContainer @@ -296,35 +368,29 @@ namespace N_m3u8DL_RE.Common.Resource "* lang=CODE: 指定媒体文件语言代码 (非必须)\r\n" + "* name=NAME: 指定媒体文件描述信息 (非必须)\r\n\r\n" + "例如: \r\n" + - "\r\n#引入外部字幕\r\n" + + "# 引入外部字幕\r\n" + "--mux-import path=zh-Hans.srt:lang=chi:name=\"中文 (简体)\"\r\n" + - "\r\n#引入外部音轨+字幕\r\n" + + "# 引入外部音轨+字幕\r\n" + "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"", zhTW: "混流時引入外部媒體檔案. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + "* path=PATH: 指定媒體檔案路徑\r\n" + "* lang=CODE: 指定媒體檔案語言代碼 (非必須)\r\n" + "* name=NAME: 指定媒體檔案描述訊息 (非必須)\r\n\r\n" + "例如: \r\n" + - "\r\n#引入外部字幕\r\n" + + "# 引入外部字幕\r\n" + "--mux-import path=zh-Hant.srt:lang=chi:name=\"中文 (繁體)\"\r\n" + - "\r\n#引入外部音軌+字幕\r\n" + + "# 引入外部音軌+字幕\r\n" + "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"", enUS: "When MuxAfterDone enabled, allow to import local media files. OPTIONS is a colon separated list of:\r\n\r\n" + "* path=PATH: set file path\r\n" + "* lang=CODE: set media language code (not required)\r\n" + "* name=NAME: set description (not required)\r\n\r\n" + "Examples: \r\n" + - "\r\n#import subtitle\r\n" + + "# import subtitle\r\n" + "--mux-import path=en-US.srt:lang=eng:name=\"English (Original)\"\r\n" + - "\r\n#import audio and subtitle\r\n" + + "# import audio and subtitle\r\n" + "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"" ), - ["cmd_muxToMp4"] = new TextContainer - ( - zhCN: "混流时使用mp4容器而非mkv", - zhTW: "混流時使用mp4容器而非mkv", - enUS: "Use mp4 container instead of mkv when muxing" - ), ["cmd_writeMetaJson"] = new TextContainer ( zhCN: "解析后的信息是否输出json文件", @@ -481,6 +547,12 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "寫出meta json", enUS: "Writing meta json" ), + ["noStreamsToDownload"] = new TextContainer + ( + zhCN: "没有找到需要下载的流", + zhTW: "沒有找到需要下載的流", + enUS: "No stream found to download" + ), }; } diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index e879a27..83ab725 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -7,12 +7,15 @@ using System.CommandLine; using System.CommandLine.Binding; using System.CommandLine.Parsing; using System.Globalization; -using System.Linq; +using System.Text.RegularExpressions; namespace N_m3u8DL_RE.CommandLine { - internal class CommandInvoker + internal partial class CommandInvoker { + [RegexGenerator("((best|worst)\\d*|all)")] + private static partial Regex ForStrRegex(); + 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); private readonly static Option SaveDir = new(new string[] { "--save-dir" }, description: ResString.cmd_saveDir); @@ -27,8 +30,8 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.VTT); private readonly static Option AutoSelect = new(new string[] { "--auto-select" }, description: ResString.cmd_autoSelect, getDefaultValue: () => false); private readonly static Option SubOnly = new(new string[] { "--sub-only" }, description: ResString.cmd_subOnly, getDefaultValue: () => false); - private readonly static Option ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => 8); - private readonly static Option DownloadRetryCount = new(new string[] { "--download-retry-count" }, description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3); + private readonly static Option ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => 8) { ArgumentHelpName = "number" }; + private readonly static Option DownloadRetryCount = new(new string[] { "--download-retry-count" }, description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3) { ArgumentHelpName = "number" }; private readonly static Option SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false); private readonly static Option SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false); private readonly static Option BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => false); @@ -39,15 +42,85 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, getDefaultValue: () => false); private readonly static Option MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); private readonly static Option UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); - private readonly static Option DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath); - private readonly static Option FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath); + private readonly static Option DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath) { ArgumentHelpName = "PATH" }; + private readonly static Option FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath) { ArgumentHelpName = "PATH" }; private readonly static Option BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl); - private readonly static Option ConcurrentDownload = new(new string[] { "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false); + private readonly static Option ConcurrentDownload = new(new string[] { "-mt", "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false); //复杂命令行如下 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" }; + private readonly static Option VideoFilter = new(new string[] { "-sv", "--select-video" }, description: ResString.cmd_selectVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + 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 StreamFilter? ParseStreamFilter(ArgumentResult result) + { + var streamFilter = new StreamFilter(); + var input = result.Tokens.First().Value; + var p = new ComplexParamParser(input); + + + //目标范围 + var forStr = ""; + if (input == ForStrRegex().Match(input).Value) + { + forStr = input; + } + else + { + forStr = p.GetValue("for") ?? "best"; + if (forStr != ForStrRegex().Match(input).Value) + { + result.ErrorMessage = $"for={forStr} not valid"; + return null; + } + } + streamFilter.For = forStr; + + var id = p.GetValue("id"); + if (!string.IsNullOrEmpty(id)) + streamFilter.GroupIdReg = new Regex(id); + + var lang = p.GetValue("lang"); + if (!string.IsNullOrEmpty(lang)) + streamFilter.LanguageReg = new Regex(lang); + + var name = p.GetValue("name"); + if (!string.IsNullOrEmpty(name)) + streamFilter.NameReg = new Regex(name); + + var codecs = p.GetValue("codecs"); + if (!string.IsNullOrEmpty(codecs)) + streamFilter.CodecsReg = new Regex(codecs); + + var res = p.GetValue("res"); + if (!string.IsNullOrEmpty(res)) + streamFilter.ResolutionReg = new Regex(res); + + var frame = p.GetValue("frame"); + if (!string.IsNullOrEmpty(frame)) + streamFilter.FrameRateReg = new Regex(frame); + + var channel = p.GetValue("channel"); + if (!string.IsNullOrEmpty(channel)) + streamFilter.ChannelsReg = new Regex(channel); + + var range = p.GetValue("range"); + if (!string.IsNullOrEmpty(range)) + streamFilter.VideoRangeReg = new Regex(range); + + var url = p.GetValue("url"); + if (!string.IsNullOrEmpty(url)) + streamFilter.UrlReg = new Regex(url); + + return streamFilter; + } /// /// 分割Header @@ -180,6 +253,9 @@ namespace N_m3u8DL_RE.CommandLine BaseUrl = bindingContext.ParseResult.GetValueForOption(BaseUrl), MuxImports = bindingContext.ParseResult.GetValueForOption(MuxImports), ConcurrentDownload = bindingContext.ParseResult.GetValueForOption(ConcurrentDownload), + VideoFilter = bindingContext.ParseResult.GetValueForOption(VideoFilter), + AudioFilter = bindingContext.ParseResult.GetValueForOption(AudioFilter), + SubtitleFilter = bindingContext.ParseResult.GetValueForOption(SubtitleFilter), }; var parsedHeaders = bindingContext.ParseResult.GetValueForOption(Headers); @@ -215,13 +291,13 @@ namespace N_m3u8DL_RE.CommandLine public static async Task InvokeArgs(string[] args, Func action) { - var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220826") + var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220827") { 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 + MuxAfterDone, MuxImports, VideoFilter, AudioFilter, SubtitleFilter }; rootCommand.TreatUnmatchedTokensAsErrors = true; rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); diff --git a/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs b/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs index 4fa3b82..8674dd4 100644 --- a/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs +++ b/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs @@ -21,7 +21,7 @@ namespace N_m3u8DL_RE.CommandLine try { var index = _arg.IndexOf(key + "="); - if (index == -1) return _arg.IndexOf(key) != -1 ? "true" : null; + if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null; var chars = _arg[(index + key.Length + 1)..].ToCharArray(); var result = new StringBuilder(); diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 1672f23..f727724 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -146,6 +146,18 @@ namespace N_m3u8DL_RE.CommandLine /// See: . /// public List? MuxImports { get; set; } + /// + /// See: . + /// + public StreamFilter? VideoFilter { get; set; } + /// + /// See: . + /// + public StreamFilter? AudioFilter { get; set; } + /// + /// See: . + /// + public StreamFilter? SubtitleFilter { get; set; } public bool MuxKeepFiles { get; set; } } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/StreamFilter.cs b/src/N_m3u8DL-RE/Entity/StreamFilter.cs new file mode 100644 index 0000000..6536613 --- /dev/null +++ b/src/N_m3u8DL-RE/Entity/StreamFilter.cs @@ -0,0 +1,25 @@ +using N_m3u8DL_RE.Common.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Entity +{ + public class StreamFilter + { + public Regex? GroupIdReg { get; set; } + public Regex? LanguageReg { get; set; } + public Regex? NameReg { get; set; } + public Regex? CodecsReg { get; set; } + public Regex? ResolutionReg { get; set; } + public Regex? FrameRateReg { get; set; } + public Regex? ChannelsReg { get; set; } + public Regex? VideoRangeReg { get; set; } + public Regex? UrlReg { get; set; } + + public string For { get; set; } = "best"; + } +} diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index ea19ce4..3acd6d3 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -44,6 +44,16 @@ namespace N_m3u8DL_RE await CommandInvoker.InvokeArgs(args, DoWorkAsync); } + static int GetOrder(StreamSpec streamSpec) + { + if (streamSpec.Channels == null) return 0; + else + { + var str = streamSpec.Channels.Split('/')[0]; + return int.TryParse(str, out var order) ? order : 0; + } + } + static async Task DoWorkAsync(MyOption option) { Logger.LogLevel = option.LogLevel; @@ -182,7 +192,7 @@ namespace N_m3u8DL_RE } //全部媒体 - var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth); + var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder); //基本流 var basicStreams = lists.Where(x => x.MediaType == null || x.MediaType == MediaType.VIDEO); //可选音频轨道 @@ -203,8 +213,6 @@ namespace N_m3u8DL_RE Logger.InfoMarkUp(item.ToString()); } - //展示交互式选择框 - //var selectedStreams = PromptUtil.SelectStreams(lists); var selectedStreams = new List(); if (option.AutoSelect) { @@ -213,7 +221,7 @@ namespace N_m3u8DL_RE var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language); foreach (var lang in langs) { - selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).First()); + selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).ThenByDescending(GetOrder).First()); } selectedStreams.AddRange(subs); } @@ -221,10 +229,22 @@ namespace N_m3u8DL_RE { selectedStreams.AddRange(subs); } + else if (option.VideoFilter != null || option.AudioFilter != null || option.SubtitleFilter != null) + { + basicStreams = FilterUtil.DoFilter(basicStreams, option.VideoFilter); + audios = FilterUtil.DoFilter(audios, option.AudioFilter); + subs = FilterUtil.DoFilter(subs, option.SubtitleFilter); + selectedStreams = basicStreams.Concat(audios).Concat(subs).ToList(); + } else { - selectedStreams = PromptUtil.SelectStreams(lists); + //展示交互式选择框 + selectedStreams = FilterUtil.SelectStreams(lists); } + + if (!selectedStreams.Any()) + throw new Exception(ResString.noStreamsToDownload); + //一个以上的话,需要手动重新加载playlist if (lists.Count() > 1) await extractor.FetchPlayListAsync(selectedStreams); diff --git a/src/N_m3u8DL-RE/Util/PromptUtil.cs b/src/N_m3u8DL-RE/Util/FilterUtil.cs similarity index 54% rename from src/N_m3u8DL-RE/Util/PromptUtil.cs rename to src/N_m3u8DL-RE/Util/FilterUtil.cs index de45bf5..3634f03 100644 --- a/src/N_m3u8DL-RE/Util/PromptUtil.cs +++ b/src/N_m3u8DL-RE/Util/FilterUtil.cs @@ -1,6 +1,7 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Resource; +using N_m3u8DL_RE.Entity; using Spectre.Console; using System; using System.Collections.Generic; @@ -10,8 +11,47 @@ using System.Threading.Tasks; namespace N_m3u8DL_RE.Util { - public class PromptUtil + public class FilterUtil { + public static List DoFilter(IEnumerable lists, StreamFilter? filter) + { + if (filter == null) return new List(); + + var inputs = lists.Where(_ => true); + if (filter.GroupIdReg != null) + inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId)); + if (filter.LanguageReg != null) + inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language)); + if (filter.NameReg != null) + inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name)); + if (filter.CodecsReg != null) + inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs)); + if (filter.ResolutionReg != null) + inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution)); + if (filter.FrameRateReg != null) + inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($"{i.FrameRate}")); + if (filter.ChannelsReg != null) + inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels)); + if (filter.VideoRangeReg != null) + inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange)); + if (filter.UrlReg != null) + inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url)); + + var bestNumberStr = filter.For.Replace("best", ""); + var worstNumberStr = filter.For.Replace("worst", ""); + + if (filter.For == "best" && inputs.Count() > 0) + inputs = inputs.Take(1).ToList(); + else if (filter.For == "worst" && inputs.Count() > 0) + inputs = inputs.TakeLast(1).ToList(); + else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0) + inputs = inputs.Take(bestNumber).ToList(); + else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0) + inputs = inputs.TakeLast(worstNumber).ToList(); + + return inputs.ToList(); + } + public static List SelectStreams(IEnumerable lists) { if (lists.Count() == 1)