From a2961d1494ae77df890d5abd94f38b81ffea537a Mon Sep 17 00:00:00 2001 From: AdiEcho <648299819@qq.com> Date: Tue, 16 Jul 2024 20:26:51 +0800 Subject: [PATCH 1/5] [Fix] fix reading the kid in a multi-DRM file --- src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs index 0482a0f7..26b4f96e 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs @@ -36,7 +36,12 @@ public static ParsedMP4Info ReadInit(byte[] data) if (SYSTEM_ID_WIDEVINE.SequenceEqual(systemId)) { var dataSize = box.Reader.ReadUInt32(); - info.PSSH = Convert.ToBase64String(box.Reader.ReadBytes((int)dataSize)); + var psshData = box.Reader.ReadBytes((int)dataSize); + info.PSSH = Convert.ToBase64String(psshData); + if (info.KID == "00000000000000000000000000000000") + { + info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower(); + } } }) .FullBox("encv", MP4Parser.AllData(data => ReadBox(data, info))) From 49cc091e69433f8ce431a1fa80b96acffb8149a4 Mon Sep 17 00:00:00 2001 From: AdiEcho <648299819@qq.com> Date: Wed, 24 Jul 2024 13:48:17 +0800 Subject: [PATCH 2/5] [Fix] fix decrypting multi-drm files --- src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs | 3 ++- .../DownloadManager/SimpleDownloadManager.cs | 17 +++++++----- src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs | 27 ++++++++++++++++++- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs index 26b4f96e..3e5ea622 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs @@ -8,6 +8,7 @@ public class ParsedMP4Info public string? PSSH; public string? KID; public string? Scheme; + public bool isMultiDRM; } public class MP4InitUtil @@ -19,7 +20,6 @@ public static ParsedMP4Info ReadInit(byte[] data) { var info = new ParsedMP4Info(); - //parse init new MP4Parser() .Box("moov", MP4Parser.Children) @@ -41,6 +41,7 @@ public static ParsedMP4Info ReadInit(byte[] data) if (info.KID == "00000000000000000000000000000000") { info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower(); + info.isMultiDRM = true; } } }) diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index 8ad42f4d..ea33bf99 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -76,7 +76,6 @@ private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos, r } } - private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer) { speedContainer.ResetVars(); @@ -167,7 +166,8 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask //读取mp4信息 if (result != null && result.Success) { - currentKID = MP4DecryptUtil.ReadInit(result.ActualFilePath); + var info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath); + currentKID = info.KID; // try shaka packager, which can handle WebM if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); @@ -179,7 +179,7 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: info.isMultiDRM); if (dResult) { FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; @@ -251,7 +251,8 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); + var info = MP4DecryptUtil.GetMP4Info(enc); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: info.isMultiDRM); if (dResult) { File.Delete(enc); @@ -288,7 +289,8 @@ await Parallel.ForEachAsync(segments, options, async (seg, _) => { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); + var info = MP4DecryptUtil.GetMP4Info(enc); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: info.isMultiDRM); if (dResult) { File.Delete(enc); @@ -594,12 +596,13 @@ await Parallel.ForEachAsync(segments, options, async (seg, _) => } //调用mp4decrypt解密 - if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && !DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) + if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && !DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) { var enc = output; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var info = MP4DecryptUtil.GetMP4Info(enc); Logger.InfoMarkUp($"[grey]Decrypting...[/]"); - var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); + var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: info.isMultiDRM); if (result) { File.Delete(enc); diff --git a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs index 41f9be63..2926b7bd 100644 --- a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs +++ b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs @@ -10,12 +10,18 @@ namespace N_m3u8DL_RE.Util internal class MP4DecryptUtil { private static string ZeroKid = "00000000000000000000000000000000"; - public static async Task DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "") + public static async Task DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false) { if (keys == null || keys.Length == 0) return false; string? keyPair = null; string? trackId = null; + + if (isMultiDRM) + { + trackId = "1"; + } + if (!string.IsNullOrEmpty(kid)) { var test = keys.Where(k => k.StartsWith(kid)); @@ -146,6 +152,25 @@ await Process.Start(new ProcessStartInfo() } } + public static ParsedMP4Info GetMP4Info(byte[] data) + { + var info = MP4InitUtil.ReadInit(data); + if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]"); + if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]"); + if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]"); + return info; + } + + public static ParsedMP4Info GetMP4Info(string output) + { + using (var fs = File.OpenRead(output)) + { + var header = new byte[1 * 1024 * 1024]; //1MB + fs.Read(header); + return GetMP4Info(header); + } + } + public static string? ReadInitShaka(string output, string bin) { Regex ShakaKeyIDRegex = new Regex("Key for key_id=([0-9a-f]+) was not found"); From 216273788f1405964e38690388ff461245716385 Mon Sep 17 00:00:00 2001 From: AdiEcho <648299819@qq.com> Date: Wed, 24 Jul 2024 17:50:28 +0800 Subject: [PATCH 3/5] [Fix] fix glibc error in building --- .github/workflows/build_latest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_latest.yml b/.github/workflows/build_latest.yml index b5e49103..d67398c2 100644 --- a/.github/workflows/build_latest.yml +++ b/.github/workflows/build_latest.yml @@ -20,6 +20,7 @@ on: env: DOTNET_SDK_VERSION: "8.0.*" + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: build-win-x64-arm64: From 6210ec9e4ae8f5edbe81de1120b042d016afd194 Mon Sep 17 00:00:00 2001 From: AdiEcho <648299819@qq.com> Date: Thu, 25 Jul 2024 00:36:22 +0800 Subject: [PATCH 4/5] =?UTF-8?q?[Feat]=20=E5=88=A0=E9=99=A4ReadInit?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DownloadManager/SimpleDownloadManager.cs | 23 ++++++++++--------- .../SimpleLiveRecordManager2.cs | 4 ++-- src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs | 19 --------------- 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index ea33bf99..5893cc5b 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -114,6 +114,7 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask var mp4InitFile = ""; var currentKID = ""; var readInfo = false; //是否读取过 + var mp4Info = new ParsedMP4Info(); //用户自定义范围导致被跳过的时长 计算字幕偏移使用 var skippedDur = streamSpec.SkippedDuration ?? 0d; @@ -166,8 +167,8 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask //读取mp4信息 if (result != null && result.Success) { - var info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath); - currentKID = info.KID; + mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath); + currentKID = mp4Info.KID; // try shaka packager, which can handle WebM if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); @@ -179,7 +180,7 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: info.isMultiDRM); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); if (dResult) { FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; @@ -238,7 +239,7 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask //读取init信息 if (string.IsNullOrEmpty(currentKID)) { - currentKID = MP4DecryptUtil.ReadInit(result.ActualFilePath); + currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; } // try shaka packager, which can handle WebM if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { @@ -251,8 +252,8 @@ private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var info = MP4DecryptUtil.GetMP4Info(enc); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: info.isMultiDRM); + mp4Info = MP4DecryptUtil.GetMP4Info(enc); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM); if (dResult) { File.Delete(enc); @@ -289,8 +290,8 @@ await Parallel.ForEachAsync(segments, options, async (seg, _) => { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var info = MP4DecryptUtil.GetMP4Info(enc); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: info.isMultiDRM); + mp4Info = MP4DecryptUtil.GetMP4Info(enc); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM); if (dResult) { File.Delete(enc); @@ -586,7 +587,7 @@ await Parallel.ForEachAsync(segments, options, async (seg, _) => //重新读取init信息 if (mergeSuccess && totalCount >= 1 && string.IsNullOrEmpty(currentKID) && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) { - currentKID = MP4DecryptUtil.ReadInit(output); + currentKID = MP4DecryptUtil.GetMP4Info(output).KID; // try shaka packager, which can handle WebM if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { currentKID = MP4DecryptUtil.ReadInitShaka(output, mp4decrypt); @@ -600,9 +601,9 @@ await Parallel.ForEachAsync(segments, options, async (seg, _) => { var enc = output; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var info = MP4DecryptUtil.GetMP4Info(enc); + mp4Info = MP4DecryptUtil.GetMP4Info(enc); Logger.InfoMarkUp($"[grey]Decrypting...[/]"); - var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: info.isMultiDRM); + var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); if (result) { File.Delete(enc); diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs index 4ab0b663..92674dc7 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs @@ -212,7 +212,7 @@ private async Task RecordStreamAsync(StreamSpec streamSpec, ProgressTask t //读取mp4信息 if (result != null && result.Success) { - currentKID = MP4DecryptUtil.ReadInit(result.ActualFilePath); + currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; //从文件读取KEY await SearchKeyAsync(currentKID); //实时解密 @@ -290,7 +290,7 @@ private async Task RecordStreamAsync(StreamSpec streamSpec, ProgressTask t //读取init信息 if (string.IsNullOrEmpty(currentKID)) { - currentKID = MP4DecryptUtil.ReadInit(result.ActualFilePath); + currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; } //从文件读取KEY await SearchKeyAsync(currentKID); diff --git a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs index 2926b7bd..a04291e2 100644 --- a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs +++ b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs @@ -133,25 +133,6 @@ await Process.Start(new ProcessStartInfo() return null; } - public static string? ReadInit(byte[] data) - { - var info = MP4InitUtil.ReadInit(data); - if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]"); - if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]"); - if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]"); - return info.KID; - } - - public static string? ReadInit(string output) - { - using (var fs = File.OpenRead(output)) - { - var header = new byte[1 * 1024 * 1024]; //1MB - fs.Read(header); - return ReadInit(header); - } - } - public static ParsedMP4Info GetMP4Info(byte[] data) { var info = MP4InitUtil.ReadInit(data); From 0c66acf612ee20231ac8d9ccde062f6f657c201b Mon Sep 17 00:00:00 2001 From: AdiEcho <648299819@qq.com> Date: Thu, 25 Jul 2024 00:38:54 +0800 Subject: [PATCH 5/5] =?UTF-8?q?[Fix]=20=E4=BF=AE=E5=A4=8DReadMe=E5=92=8Cmo?= =?UTF-8?q?re-help=E4=B8=AD=E6=98=BE=E7=A4=BA=E5=8F=82=E6=95=B0=E4=B8=8D?= =?UTF-8?q?=E5=87=86=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/N_m3u8DL-RE.Common/Resource/StaticText.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8261eddc..dddc509f 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ More Help: 通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数: -id=REGEX:lang=REGEX:name=REGEX:codec=REGEX:res=REGEX:frame=REGEX +id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX plistDurMin=hms:plistDurMax=hms:for=FOR @@ -151,7 +151,7 @@ plistDurMin=hms:plistDurMax=hms:for=FOR # 选择最佳视频 -sv best # 选择4K+HEVC视频 --sv res="3840*":codec=hvc1:for=best +-sv res="3840*":codecs=hvc1:for=best # 选择长度大于1小时20分钟30秒的视频 -sv plistDurMin="1h20m30s":for=best ``` diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index 5a8e3a8c..aaa46159 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -475,7 +475,7 @@ internal class StaticText ["cmd_selectVideo_more"] = new TextContainer ( zhCN: "通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数:\r\n\r\n" + - "id=REGEX:lang=REGEX:name=REGEX:codec=REGEX:res=REGEX:frame=REGEX\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + "plistDurMin=hms:plistDurMax=hms:role=string:for=FOR\r\n\r\n" + "* for=FOR: 选择方式. best[number], worst[number], all (默认: best)\r\n\r\n" + @@ -483,12 +483,12 @@ internal class StaticText "# 选择最佳视频\r\n" + "-sv best\r\n" + "# 选择4K+HEVC视频\r\n" + - "-sv res=\"3840*\":codec=hvc1:for=best\r\n" + + "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + "# 选择长度大于1小时20分钟30秒的视频\r\n" + "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + "-sv role=\"main\":for:best\r\n", zhTW: "通過正則表達式選擇符合要求的影片軌. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + - "id=REGEX:lang=REGEX:name=REGEX:codec=REGEX:res=REGEX:frame=REGEX\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + "plistDurMin=hms:plistDurMax=hms:role=string:for=FOR\r\n\r\n" + "* for=FOR: 選擇方式. best[number], worst[number], all (默認: best)\r\n\r\n" + @@ -496,12 +496,12 @@ internal class StaticText "# 選擇最佳影片\r\n" + "-sv best\r\n" + "# 選擇4K+HEVC影片\r\n" + - "-sv res=\"3840*\":codec=hvc1:for=best\r\n" + + "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + "# 選擇長度大於1小時20分鐘30秒的影片\r\n" + "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + "-sv role=\"main\":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:frame=REGEX\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + "plistDurMin=hms:plistDurMax=hms:role=string:for=FOR\r\n\r\n" + "* for=FOR: Select type. best[number], worst[number], all (Default: best)\r\n\r\n" + @@ -509,7 +509,7 @@ internal class StaticText "# select best video\r\n" + "-sv best\r\n" + "# select 4K+HEVC video\r\n" + - "-sv res=\"3840*\":codec=hvc1:for=best\r\n" + + "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + "# Select best video with duration longer than 1 hour 20 minutes 30 seconds\r\n" + "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + "-sv role=\"main\":for:best\r\n"