支持命令行选择视频、音频和字幕

This commit is contained in:
nilaoda 2022-08-27 20:52:03 +08:00
parent 983599b61f
commit 9bf57058cf
8 changed files with 292 additions and 45 deletions

View File

@ -28,6 +28,9 @@ namespace N_m3u8DL_RE.Common.Resource
public static string cmd_baseUrl { get => GetText("cmd_baseUrl"); } public static string cmd_baseUrl { get => GetText("cmd_baseUrl"); }
public static string cmd_header { get => GetText("cmd_header"); } public static string cmd_header { get => GetText("cmd_header"); }
public static string cmd_muxImport { get => GetText("cmd_muxImport"); } 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_Input { get => GetText("cmd_Input"); }
public static string cmd_keys { get => GetText("cmd_keys"); } public static string cmd_keys { get => GetText("cmd_keys"); }
public static string cmd_keyText { get => GetText("cmd_keyText"); } 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_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); }
public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); } public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); }
public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); } 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_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 cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); }
public static string fetch { get => GetText("fetch"); } public static string fetch { get => GetText("fetch"); }
public static string ffmpegMerge { get => GetText("ffmpegMerge"); } 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 startDownloading { get => GetText("startDownloading"); }
public static string streamsInfo { get => GetText("streamsInfo"); } public static string streamsInfo { get => GetText("streamsInfo"); }
public static string writeJson { get => GetText("writeJson"); } public static string writeJson { get => GetText("writeJson"); }
public static string noStreamsToDownload { get => GetText("noStreamsToDownload"); }
private static string GetText(string key) private static string GetText(string key)
{ {

View File

@ -238,18 +238,90 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "解密时使用shaka-packager替代mp4decrypt", zhTW: "解密时使用shaka-packager替代mp4decrypt",
enUS: "Use shaka-packager instead of mp4decrypt to decrypt" 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 ["cmd_concurrentDownload"] = new TextContainer
( (
zhCN: "并发下载已选择的音频、视频和字幕", zhCN: "并发下载已选择的音频、视频和字幕",
zhTW: "並發下載已選擇的音訊、影片和字幕", zhTW: "並發下載已選擇的音訊、影片和字幕",
enUS: "Concurrently download the selected audio, video and subtitles" 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 ["cmd_muxAfterDone"] = new TextContainer
( (
zhCN: "所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\r\n\r\n" + zhCN: "所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\r\n\r\n" +
@ -258,11 +330,11 @@ namespace N_m3u8DL_RE.Common.Resource
"* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" + "* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" +
"* keep=BOOL: 混流完成是否删除文件 true, false (默认: true)\r\n\r\n" + "* keep=BOOL: 混流完成是否删除文件 true, false (默认: true)\r\n\r\n" +
"例如: \r\n" + "例如: \r\n" +
"\r\n#混流为mp4容器\r\n" + "# 混流为mp4容器\r\n" +
"-M format=mp4\r\n" + "-M format=mp4\r\n" +
"\r\n#使用mkvmerge, 自动寻找程序\r\n" + "# 使用mkvmerge, 自动寻找程序\r\n" +
"-M format=mkv:muxer=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", "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n",
zhTW: "所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + zhTW: "所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\r\n\r\n" +
"* format=FORMAT: 指定混流容器 mkv, mp4\r\n" + "* format=FORMAT: 指定混流容器 mkv, mp4\r\n" +
@ -270,11 +342,11 @@ namespace N_m3u8DL_RE.Common.Resource
"* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" + "* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" +
"* keep=BOOL: 混流完成是否刪除文件 true, false (默認: true)\r\n\r\n" + "* keep=BOOL: 混流完成是否刪除文件 true, false (默認: true)\r\n\r\n" +
"例如: \r\n" + "例如: \r\n" +
"\r\n#混流為mp4容器\r\n" + "# 混流為mp4容器\r\n" +
"-M format=mp4\r\n" + "-M format=mp4\r\n" +
"\r\n#使用mkvmerge, 自動尋找程序\r\n" + "# 使用mkvmerge, 自動尋找程序\r\n" +
"-M format=mkv:muxer=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", "-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" + 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" + "* 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" + "* 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" + "* keep=BOOL: set whether or not delete files. true, false (Default: true)\r\n\r\n" +
"Examples: \r\n" + "Examples: \r\n" +
"\r\n#mux to mp4\r\n" + "# mux to mp4\r\n" +
"-M format=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" + "-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" "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n"
), ),
["cmd_muxImport"] = new TextContainer ["cmd_muxImport"] = new TextContainer
@ -296,35 +368,29 @@ namespace N_m3u8DL_RE.Common.Resource
"* lang=CODE: 指定媒体文件语言代码 (非必须)\r\n" + "* lang=CODE: 指定媒体文件语言代码 (非必须)\r\n" +
"* name=NAME: 指定媒体文件描述信息 (非必须)\r\n\r\n" + "* name=NAME: 指定媒体文件描述信息 (非必须)\r\n\r\n" +
"例如: \r\n" + "例如: \r\n" +
"\r\n#引入外部字幕\r\n" + "# 引入外部字幕\r\n" +
"--mux-import path=zh-Hans.srt:lang=chi:name=\"中文 (简体)\"\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)\"", "--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" + zhTW: "混流時引入外部媒體檔案. 你能夠以:分隔形式指定如下參數:\r\n\r\n" +
"* path=PATH: 指定媒體檔案路徑\r\n" + "* path=PATH: 指定媒體檔案路徑\r\n" +
"* lang=CODE: 指定媒體檔案語言代碼 (非必須)\r\n" + "* lang=CODE: 指定媒體檔案語言代碼 (非必須)\r\n" +
"* name=NAME: 指定媒體檔案描述訊息 (非必須)\r\n\r\n" + "* name=NAME: 指定媒體檔案描述訊息 (非必須)\r\n\r\n" +
"例如: \r\n" + "例如: \r\n" +
"\r\n#引入外部字幕\r\n" + "# 引入外部字幕\r\n" +
"--mux-import path=zh-Hant.srt:lang=chi:name=\"中文 (繁體)\"\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)\"", "--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" + 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" + "* path=PATH: set file path\r\n" +
"* lang=CODE: set media language code (not required)\r\n" + "* lang=CODE: set media language code (not required)\r\n" +
"* name=NAME: set description (not required)\r\n\r\n" + "* name=NAME: set description (not required)\r\n\r\n" +
"Examples: \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" + "--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)\"" "--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 ["cmd_writeMetaJson"] = new TextContainer
( (
zhCN: "解析后的信息是否输出json文件", zhCN: "解析后的信息是否输出json文件",
@ -481,6 +547,12 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "寫出meta json", zhTW: "寫出meta json",
enUS: "Writing meta json" enUS: "Writing meta json"
), ),
["noStreamsToDownload"] = new TextContainer
(
zhCN: "没有找到需要下载的流",
zhTW: "沒有找到需要下載的流",
enUS: "No stream found to download"
),
}; };
} }

View File

@ -7,12 +7,15 @@ using System.CommandLine;
using System.CommandLine.Binding; using System.CommandLine.Binding;
using System.CommandLine.Parsing; using System.CommandLine.Parsing;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.CommandLine 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<string> Input = new(name: "input", description: ResString.cmd_Input); private readonly static Argument<string> Input = new(name: "input", description: ResString.cmd_Input);
private readonly static Option<string?> TmpDir = new(new string[] { "--tmp-dir" }, description: ResString.cmd_tmpDir); private readonly static Option<string?> TmpDir = new(new string[] { "--tmp-dir" }, description: ResString.cmd_tmpDir);
private readonly static Option<string?> SaveDir = new(new string[] { "--save-dir" }, description: ResString.cmd_saveDir); private readonly static Option<string?> SaveDir = new(new string[] { "--save-dir" }, description: ResString.cmd_saveDir);
@ -27,8 +30,8 @@ namespace N_m3u8DL_RE.CommandLine
private readonly static Option<SubtitleFormat> SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.VTT); private readonly static Option<SubtitleFormat> SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.VTT);
private readonly static Option<bool> AutoSelect = new(new string[] { "--auto-select" }, description: ResString.cmd_autoSelect, getDefaultValue: () => false); private readonly static Option<bool> AutoSelect = new(new string[] { "--auto-select" }, description: ResString.cmd_autoSelect, getDefaultValue: () => false);
private readonly static Option<bool> SubOnly = new(new string[] { "--sub-only" }, description: ResString.cmd_subOnly, getDefaultValue: () => false); private readonly static Option<bool> SubOnly = new(new string[] { "--sub-only" }, description: ResString.cmd_subOnly, getDefaultValue: () => false);
private readonly static Option<int> ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => 8); private readonly static Option<int> ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => 8) { ArgumentHelpName = "number" };
private readonly static Option<int> DownloadRetryCount = new(new string[] { "--download-retry-count" }, description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3); private readonly static Option<int> DownloadRetryCount = new(new string[] { "--download-retry-count" }, description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3) { ArgumentHelpName = "number" };
private readonly static Option<bool> SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false); private readonly static Option<bool> SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false);
private readonly static Option<bool> SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false); private readonly static Option<bool> SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false);
private readonly static Option<bool> BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => false); private readonly static Option<bool> 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<bool> AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, getDefaultValue: () => false); private readonly static Option<bool> AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, getDefaultValue: () => false);
private readonly static Option<bool> MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); private readonly static Option<bool> MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false);
private readonly static Option<bool> UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); private readonly static Option<bool> UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false);
private readonly static Option<string?> DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath); private readonly static Option<string?> DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath) { ArgumentHelpName = "PATH" };
private readonly static Option<string?> FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath); private readonly static Option<string?> FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath) { ArgumentHelpName = "PATH" };
private readonly static Option<string?> BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl); private readonly static Option<string?> BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl);
private readonly static Option<bool> ConcurrentDownload = new(new string[] { "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false); private readonly static Option<bool> ConcurrentDownload = new(new string[] { "-mt", "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false);
//复杂命令行如下 //复杂命令行如下
private readonly static Option<MuxOptions?> MuxAfterDone = new(new string[] { "-M", "--mux-after-done" }, description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" }; private readonly static Option<MuxOptions?> MuxAfterDone = new(new string[] { "-M", "--mux-after-done" }, description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<List<OutputFile>> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" }; private readonly static Option<List<OutputFile>> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> VideoFilter = new(new string[] { "-sv", "--select-video" }, description: ResString.cmd_selectVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> AudioFilter = new(new string[] { "-sa", "--select-audio" }, description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> SubtitleFilter = new(new string[] { "-ss", "--select-subtitle" }, description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
/// <summary>
/// 流过滤器
/// </summary>
/// <param name="result"></param>
/// <returns></returns>
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;
}
/// <summary> /// <summary>
/// 分割Header /// 分割Header
@ -180,6 +253,9 @@ namespace N_m3u8DL_RE.CommandLine
BaseUrl = bindingContext.ParseResult.GetValueForOption(BaseUrl), BaseUrl = bindingContext.ParseResult.GetValueForOption(BaseUrl),
MuxImports = bindingContext.ParseResult.GetValueForOption(MuxImports), MuxImports = bindingContext.ParseResult.GetValueForOption(MuxImports),
ConcurrentDownload = bindingContext.ParseResult.GetValueForOption(ConcurrentDownload), 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); var parsedHeaders = bindingContext.ParseResult.GetValueForOption(Headers);
@ -215,13 +291,13 @@ namespace N_m3u8DL_RE.CommandLine
public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> action) public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> 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, Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,
BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix,
FFmpegBinaryPath, FFmpegBinaryPath,
LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption,
MuxAfterDone, MuxImports MuxAfterDone, MuxImports, VideoFilter, AudioFilter, SubtitleFilter
}; };
rootCommand.TreatUnmatchedTokensAsErrors = true; rootCommand.TreatUnmatchedTokensAsErrors = true;
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder());

View File

@ -21,7 +21,7 @@ namespace N_m3u8DL_RE.CommandLine
try try
{ {
var index = _arg.IndexOf(key + "="); 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 chars = _arg[(index + key.Length + 1)..].ToCharArray();
var result = new StringBuilder(); var result = new StringBuilder();

View File

@ -146,6 +146,18 @@ namespace N_m3u8DL_RE.CommandLine
/// See: <see cref="CommandInvoker.MuxImports"/>. /// See: <see cref="CommandInvoker.MuxImports"/>.
/// </summary> /// </summary>
public List<OutputFile>? MuxImports { get; set; } public List<OutputFile>? MuxImports { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.VideoFilter"/>.
/// </summary>
public StreamFilter? VideoFilter { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.AudioFilter"/>.
/// </summary>
public StreamFilter? AudioFilter { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.SubtitleFilter"/>.
/// </summary>
public StreamFilter? SubtitleFilter { get; set; }
public bool MuxKeepFiles { get; set; } public bool MuxKeepFiles { get; set; }
} }
} }

View File

@ -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";
}
}

View File

@ -44,6 +44,16 @@ namespace N_m3u8DL_RE
await CommandInvoker.InvokeArgs(args, DoWorkAsync); 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) static async Task DoWorkAsync(MyOption option)
{ {
Logger.LogLevel = option.LogLevel; 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); var basicStreams = lists.Where(x => x.MediaType == null || x.MediaType == MediaType.VIDEO);
//可选音频轨道 //可选音频轨道
@ -203,8 +213,6 @@ namespace N_m3u8DL_RE
Logger.InfoMarkUp(item.ToString()); Logger.InfoMarkUp(item.ToString());
} }
//展示交互式选择框
//var selectedStreams = PromptUtil.SelectStreams(lists);
var selectedStreams = new List<StreamSpec>(); var selectedStreams = new List<StreamSpec>();
if (option.AutoSelect) if (option.AutoSelect)
{ {
@ -213,7 +221,7 @@ namespace N_m3u8DL_RE
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language); var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
foreach (var lang in langs) 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); selectedStreams.AddRange(subs);
} }
@ -221,10 +229,22 @@ namespace N_m3u8DL_RE
{ {
selectedStreams.AddRange(subs); 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 else
{ {
selectedStreams = PromptUtil.SelectStreams(lists); //展示交互式选择框
selectedStreams = FilterUtil.SelectStreams(lists);
} }
if (!selectedStreams.Any())
throw new Exception(ResString.noStreamsToDownload);
//一个以上的话需要手动重新加载playlist //一个以上的话需要手动重新加载playlist
if (lists.Count() > 1) if (lists.Count() > 1)
await extractor.FetchPlayListAsync(selectedStreams); await extractor.FetchPlayListAsync(selectedStreams);

View File

@ -1,6 +1,7 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -10,8 +11,47 @@ using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util
{ {
public class PromptUtil public class FilterUtil
{ {
public static List<StreamSpec> DoFilter(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{
if (filter == null) return new List<StreamSpec>();
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<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists) public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
{ {
if (lists.Count() == 1) if (lists.Count() == 1)