diff --git a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs index e108efa..24d5258 100644 --- a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs +++ b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs @@ -29,13 +29,12 @@ namespace N_m3u8DL_RE.Common.Entity StartRange == segment.StartRange && StopRange == segment.StopRange && ExpectLength == segment.ExpectLength && - EqualityComparer.Default.Equals(EncryptInfo, segment.EncryptInfo) && Url == segment.Url; } public override int GetHashCode() { - return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, EncryptInfo, Url); + return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url); } } } diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs index b031a27..adb8c88 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs @@ -123,6 +123,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 Full path to the tool used for MP4 decryption, like C:\Tools\mp4decrypt.exe 的本地化字符串。 + /// + public static string cmd_decryptionBinaryPath { + get { + return ResourceManager.GetString("cmd_decryptionBinaryPath", resourceCulture); + } + } + /// /// 查找类似 Delete temporary files when done 的本地化字符串。 /// @@ -152,7 +161,7 @@ namespace N_m3u8DL_RE.Common.Resource { } /// - /// 查找类似 Pass decryption key(s) to mp4decrypt. format: + /// 查找类似 Pass decryption key(s) to mp4decrypt/shaka-packager. format: ///--key KID1:KEY1 --key KID2:KEY2 的本地化字符串。 /// public static string cmd_keys { @@ -287,6 +296,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 Use shaka-packager instead of mp4decrypt 的本地化字符串。 + /// + public static string cmd_useShakaPackager { + get { + return ResourceManager.GetString("cmd_useShakaPackager", resourceCulture); + } + } + /// /// 查找类似 Write meta json after parsed 的本地化字符串。 /// diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.resx index 9fd5184..23a385f 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.resx @@ -231,7 +231,7 @@ - Pass decryption key(s) to mp4decrypt. format: + Pass decryption key(s) to mp4decrypt/shaka-packager. format: --key KID1:KEY1 --key KID2:KEY2 @@ -240,4 +240,10 @@ Decrypt MP4 segments in real time + + Use shaka-packager instead of mp4decrypt + + + Full path to the tool used for MP4 decryption, like C:\Tools\mp4decrypt.exe + \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx index 2f4b653..3ccd4b4 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx @@ -251,7 +251,7 @@ 设置保存文件命名模板, 支持使用变量 - 设置解密密钥, 程序调用mp4decrpyt进行解密. 格式: + 设置解密密钥, 程序调用mp4decrpyt/shaka-packager进行解密. 格式: --key KID1:KEY1 --key KID2:KEY2 @@ -260,4 +260,10 @@ 实时解密MP4分片 + + 使用shaka-packager替代mp4decrypt + + + MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe + \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx index 1ab99c0..1c3f1c9 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx @@ -228,7 +228,7 @@ 將輸入Url的Params添加至分片, 對某些網站很有用, 例如 kakao.com - 設置解密密鑰, 程序調用mp4decrpyt進行解密. 格式: + 設置解密密鑰, 程序調用mp4decrpyt/shaka-packager進行解密. 格式: --key KID1:KEY1 --key KID2:KEY2 @@ -237,4 +237,10 @@ 實時解密MP4分片 + + 使用shaka-packager替代mp4decrypt + + + MP4解密所用工具的全路徑, 例如 C:\Tools\mp4decrypt.exe + \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs b/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs index 24b453b..ef31d36 100644 --- a/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs @@ -46,5 +46,19 @@ namespace N_m3u8DL_RE.Common.Util str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s"; return str; } + + /// + /// 寻找可执行程序 + /// + /// + /// + public static string? FindExecutable(string name) + { + var fileExt = OperatingSystem.IsWindows() ? ".exe" : ""; + var searchPath = new[] { Environment.CurrentDirectory, Environment.ProcessPath }; + var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? + Array.Empty(); + return searchPath.Concat(envPath).Select(p => Path.Combine(p, name + fileExt)).FirstOrDefault(File.Exists); + } } } diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index 07b1b75..106d5f1 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -33,6 +33,8 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option WriteMetaJson = new(new string[] { "--write-meta-json" }, description: ResString.cmd_writeMetaJson, getDefaultValue: () => true); 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); class MyOptionBinder : BinderBase { @@ -63,6 +65,8 @@ namespace N_m3u8DL_RE.CommandLine Keys = bindingContext.ParseResult.GetValueForOption(Keys), UrlProcessorArgs = bindingContext.ParseResult.GetValueForOption(UrlProcessorArgs), MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption), + UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager), + DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath), }; //在这里设置语言 @@ -87,11 +91,11 @@ namespace N_m3u8DL_RE.CommandLine public static async Task InvokeArgs(string[] args, Func action) { - var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220723") + var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220724") { Input, TmpDir, SaveDir, SaveName, ThreadCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, - BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Keys, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, - LogLevel, UILanguage, UrlProcessorArgs, MP4RealTimeDecryption + BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, + LogLevel, UILanguage, UrlProcessorArgs, Keys, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption }; rootCommand.TreatUnmatchedTokensAsErrors = true; rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 18da283..71103ef 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -74,6 +74,10 @@ namespace N_m3u8DL_RE.CommandLine /// public bool MP4RealTimeDecryption { get; set; } /// + /// See: . + /// + public bool UseShakaPackager { get; set; } + /// /// See: . /// public SubtitleFormat SubtitleFormat { get; set; } @@ -97,5 +101,9 @@ namespace N_m3u8DL_RE.CommandLine /// See: . /// public string? UILanguage { get; set; } + /// + /// See: . + /// + public string? DecryptionBinaryPath { get; set; } } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs index f820ad4..844397a 100644 --- a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs +++ b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs @@ -28,6 +28,8 @@ namespace N_m3u8DL_RE.Config SavePattern = option.SavePattern; Keys = option.Keys; MP4RealTimeDecryption = option.MP4RealTimeDecryption; + UseShakaPackager = option.UseShakaPackager; + DecryptionBinaryPath = option.DecryptionBinaryPath; } /// @@ -79,6 +81,14 @@ namespace N_m3u8DL_RE.Config /// public bool MP4RealTimeDecryption { get; set; } = true; /// + /// 使用shaka-packager替代mp4decrypt + /// + public bool UseShakaPackager { get; set; } + /// + /// MP4解密所用工具的全路径 + /// + public string? DecryptionBinaryPath { get; set; } + /// /// 字幕格式 /// public SubtitleFormat SubtitleFormat { get; set; } = SubtitleFormat.VTT; diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index 3e9cce5..b94188b 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -52,13 +52,10 @@ namespace N_m3u8DL_RE.DownloadManager var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}"); //mp4decrypt - var APP_DIR = Path.GetDirectoryName(Environment.ProcessPath)!; - var fileName = "mp4decrypt"; - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - fileName += ".exe"; - var mp4decrypt = Path.Combine(APP_DIR, fileName); - if (!File.Exists(mp4decrypt)) mp4decrypt = fileName; + var mp4decrypt = DownloaderConfig.DecryptionBinaryPath!; var mp4InitFile = ""; + var mp4InitFileDec = ""; + var currentKID = ""; Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}; output: {output}"); @@ -106,15 +103,18 @@ namespace N_m3u8DL_RE.DownloadManager 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}[/]"); + currentKID = info.KID; //实时解密 if (DownloaderConfig.MP4RealTimeDecryption && streamSpec.Playlist.MediaInit.EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID); if (dResult) { - result.ActualFilePath = dec; + //实时解密不需要init文件用于合并 + FileDic!.Remove(streamSpec.Playlist.MediaInit, out _); + mp4InitFileDec = dec; } } } @@ -138,7 +138,7 @@ namespace N_m3u8DL_RE.DownloadManager { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec, mp4InitFile); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID, mp4InitFile); if (dResult) { File.Delete(enc); @@ -147,9 +147,11 @@ namespace N_m3u8DL_RE.DownloadManager } }); - if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "") + if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "") { File.Delete(mp4InitFile); + if (mp4InitFileDec != "") + File.Delete(mp4InitFileDec); } //校验分片数量 @@ -345,7 +347,7 @@ namespace N_m3u8DL_RE.DownloadManager } //删除临时文件夹 - if (DownloaderConfig.DelAfterDone) + if (!DownloaderConfig.SkipMerge && DownloaderConfig.DelAfterDone) { var files = FileDic.Values.Select(v => v!.ActualFilePath); foreach (var file in files) @@ -359,14 +361,14 @@ namespace N_m3u8DL_RE.DownloadManager } //调用mp4decrypt解密 - if (!DownloaderConfig.MP4RealTimeDecryption && DownloaderConfig.Keys != null && DownloaderConfig.Keys.Length > 0) + if (File.Exists(output) && !DownloaderConfig.MP4RealTimeDecryption && DownloaderConfig.Keys != null && DownloaderConfig.Keys.Length > 0) { if (totalCount > 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) { var enc = output; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); Logger.InfoMarkUp($"[grey]Decrypting...[/]"); - var result = await MP4DecryptUtil.DecryptAsync(mp4decrypt, DownloaderConfig.Keys, enc, dec); + var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID); if (result) { File.Delete(enc); diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index 32bc4bd..ef20495 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -38,6 +38,33 @@ namespace N_m3u8DL_RE try { + //预先检查 + if (option.Keys != null && option.Keys.Length > 0) + { + if (string.IsNullOrEmpty(option.DecryptionBinaryPath)) + { + if (option.UseShakaPackager) + { + var file = GlobalUtil.FindExecutable("shaka-packager"); + var file2 = GlobalUtil.FindExecutable("packager-linux-x64"); + var file3 = GlobalUtil.FindExecutable("packager-osx-x64"); + var file4 = GlobalUtil.FindExecutable("packager-win-x64"); + if (file == null && file2 == null && file3 == null && file4 == null) throw new FileNotFoundException("shaka-packager not found!"); + option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4; + } + else + { + var file = GlobalUtil.FindExecutable("mp4decrypt"); + if (file == null) throw new FileNotFoundException("mp4decrypt not found!"); + option.DecryptionBinaryPath = file; + } + } + else if (!File.Exists(option.DecryptionBinaryPath)) + { + throw new FileNotFoundException(option.DecryptionBinaryPath); + } + } + var parserConfig = new ParserConfig() { AppendUrlParams = option.AppendUrlParams, diff --git a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs index 79729ad..6844860 100644 --- a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs +++ b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs @@ -1,41 +1,73 @@ -using N_m3u8DL_RE.Config; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics; namespace N_m3u8DL_RE.Util { internal class MP4DecryptUtil { - public static async Task DecryptAsync(string bin, string[]? keys, string source, string dest, string init = "") + public static async Task DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "") { if (keys == null || keys.Length == 0) return false; - var cmd = string.Join(" ", keys.Select(k => $"--key {k}")); - if (init != "") + var keyPair = keys.First(); + if (!string.IsNullOrEmpty(kid)) { - cmd += $" --fragments-info \"{init}\" "; + var test = keys.Where(k => k.StartsWith(kid)); + if (test.Any()) keyPair = test.First(); } - cmd += $" \"{source}\" \"{dest}\""; - await Process.Start(new ProcessStartInfo() + if (keyPair == null) return false; + + //shakaPackager 无法单独解密init文件 + if (source.EndsWith("_init.mp4") && shakaPackager) return true; + + var cmd = ""; + + var tmpFile = ""; + if (shakaPackager) { - FileName = bin, - Arguments = cmd, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - })!.WaitForExitAsync(); + var enc = source; + //shakaPackager 手动构造文件 + if (init != "") + { + tmpFile = Path.ChangeExtension(source, ".itmp"); + DownloadUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile); + enc = tmpFile; + } + + cmd = $"--enable_raw_key_decryption input=\"{enc}\",stream=0,output=\"{dest}\" " + + $"--keys key_id={keyPair.Split(':')[0]}:key={keyPair.Split(':')[1]}"; + } + else + { + cmd = string.Join(" ", keys.Select(k => $"--key {k}")); + if (init != "") + { + cmd += $" --fragments-info \"{init}\" "; + } + cmd += $" \"{source}\" \"{dest}\""; + } + + await RunCommandAsync(bin, cmd); if (File.Exists(dest) && new FileInfo(dest).Length > 0) { + if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile); return true; } return false; } + + private static async Task RunCommandAsync(string name, string arg) + { + await Process.Start(new ProcessStartInfo() + { + FileName = name, + Arguments = arg, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + })!.WaitForExitAsync(); + } } }