diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs index 64017c1..839f803 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.Designer.cs @@ -69,6 +69,24 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 Dolby Vision content is detected, binary merging is automatically enabled 的本地化字符串。 + /// + public static string autoBinaryMerge2 { + get { + return ResourceManager.GetString("autoBinaryMerge2", resourceCulture); + } + } + + /// + /// 查找类似 An unrecognized encryption method is detected, binary merging is automatically enabled 的本地化字符串。 + /// + public static string autoBinaryMerge3 { + get { + return ResourceManager.GetString("autoBinaryMerge3", resourceCulture); + } + } + /// /// 查找类似 Bad m3u8 的本地化字符串。 /// @@ -368,6 +386,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 ffmpeg not found, please download at: https://ffmpeg.org/download.html 的本地化字符串。 + /// + public static string ffmpegNotFound { + get { + return ResourceManager.GetString("ffmpegNotFound", resourceCulture); + } + } + /// /// 查找类似 Extracting TTML(raw) subtitle... 的本地化字符串。 /// @@ -503,6 +530,15 @@ namespace N_m3u8DL_RE.Common.Resource { } } + /// + /// 查找类似 Reading media info... 的本地化字符串。 + /// + public static string readingInfo { + get { + return ResourceManager.GetString("readingInfo", resourceCulture); + } + } + /// /// 查找类似 Trying to search for KEY from text file... 的本地化字符串。 /// diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.resx b/src/N_m3u8DL-RE.Common/Resource/ResString.resx index 59b6000..17f494e 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.resx @@ -264,4 +264,16 @@ Trying to search for KEY from text file... + + ffmpeg not found, please download at: https://ffmpeg.org/download.html + + + Reading media info... + + + Dolby Vision content is detected, binary merging is automatically enabled + + + An unrecognized encryption method is detected, binary merging is automatically enabled + \ 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 aad8e88..8bbc2ca 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hans.resx @@ -284,4 +284,16 @@ 正在尝试从文本文件搜索KEY... + + 找不到ffmpeg,请自行下载:https://ffmpeg.org/download.html + + + 读取媒体信息... + + + 检测到杜比视界内容,自动开启二进制合并 + + + 检测到无法识别的加密方式,自动开启二进制合并 + \ 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 57d407f..147d752 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.zh-Hant.resx @@ -261,4 +261,16 @@ 正在嘗試從文本文件搜尋KEY... + + 找不到ffmpeg,請自行下載:https://ffmpeg.org/download.html + + + 讀取媒體訊息... + + + 檢測到杜比視界內容,自動開啟二進位制合併 + + + 檢測到無法識別的加密方式,自動開啟二進位制合併 + \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs index 35bfe84..989fed7 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs @@ -57,7 +57,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS } catch (Exception ex) { - Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.ToString()); + Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message); encryptInfo.Method = EncryptMethod.UNKNOWN; } diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index a1797d9..dc77935 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -26,17 +26,37 @@ namespace N_m3u8DL_RE.DownloadManager NowDateTime = DateTime.Now; } - private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task) + private string? ReadInit(byte[] data) { - 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; + } + + private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos) + { + if (!DownloaderConfig.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) { - 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; + DownloaderConfig.BinaryMerge = true; + Logger.WarnMarkUp(ResString.autoBinaryMerge2); } + if (mediainfos.All(m => m.Type == "Audio")) + { + streamSpec.MediaType = MediaType.AUDIO; + } + else if (mediainfos.All(m => m.Type == "Subtitle")) + { + streamSpec.MediaType = MediaType.SUBTITLES; + if (streamSpec.Extension == null || streamSpec.Extension == "ts") + streamSpec.Extension = "vtt"; + } + } + + private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task) + { ConcurrentDictionary FileDic = new(); var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments); @@ -50,19 +70,14 @@ namespace N_m3u8DL_RE.DownloadManager var saveDir = DownloaderConfig.SaveDir ?? Environment.CurrentDirectory; var saveName = DownloaderConfig.SaveName != null ? $"{DownloaderConfig.SaveName}.{type}.{streamSpec.Language}".TrimEnd('.') : dirName; var headers = DownloaderConfig.Headers; - var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}"); - //检测目标文件是否存在 - while (File.Exists(output)) - { - Logger.WarnMarkUp($"{output} => {output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output))}"); - } //mp4decrypt var mp4decrypt = DownloaderConfig.DecryptionBinaryPath!; var mp4InitFile = ""; var currentKID = ""; + var readInfo = false; //是否读取过 - Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}; output: {output}"); + Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); //创建文件夹 if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); @@ -83,7 +98,13 @@ namespace N_m3u8DL_RE.DownloadManager //下载init if (streamSpec.Playlist?.MediaInit != null) { - totalCount++; + //对于fMP4,自动开启二进制合并 + if (!DownloaderConfig.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) + { + DownloaderConfig.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]"); + } + var path = Path.Combine(tmpDir, "_init.mp4.tmp"); var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, headers); FileDic[streamSpec.Playlist.MediaInit] = result; @@ -94,12 +115,6 @@ namespace N_m3u8DL_RE.DownloadManager mp4InitFile = result.ActualFilePath; task.Increment(1); - //修改输出后缀 - if (streamSpec.MediaType == Common.Enum.MediaType.AUDIO) - output = Path.ChangeExtension(output, ".m4a"); - else - output = Path.ChangeExtension(output, ".mp4"); - //读取mp4信息 if (result != null && result.Success) { @@ -125,11 +140,53 @@ namespace N_m3u8DL_RE.DownloadManager FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; } } + //ffmpeg读取信息 + if (!readInfo) + { + Logger.WarnMarkUp(ResString.readingInfo); + var mediainfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.FFmpegBinaryPath!, result.ActualFilePath); + mediainfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + ChangeSpecInfo(streamSpec, mediainfos); + readInfo = true; + } } } - //开始下载 + //计算填零个数 var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); + + //下载第一个分片 + if (!readInfo) + { + var seg = segments.First(); + segments = segments.Skip(1); + + var index = seg.Index; + var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); + var result = await Downloader.DownloadSegmentAsync(seg, path, headers); + FileDic[seg] = result; + task.Increment(1); + //实时解密 + if (DownloaderConfig.MP4RealTimeDecryption && seg.EncryptInfo.Method != Common.Enum.EncryptMethod.NONE && result != null) + { + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.UseShakaPackager, mp4decrypt, DownloaderConfig.Keys, enc, dec, currentKID, mp4InitFile); + if (dResult) + { + File.Delete(enc); + result.ActualFilePath = dec; + } + } + //ffmpeg读取信息 + Logger.WarnMarkUp(ResString.readingInfo); + var mediainfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.FFmpegBinaryPath!, result!.ActualFilePath); + mediainfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + ChangeSpecInfo(streamSpec, mediainfos); + readInfo = true; + } + + //开始下载 var options = new ParallelOptions() { MaxDegreeOfParallelism = DownloaderConfig.ThreadCount @@ -155,6 +212,19 @@ namespace N_m3u8DL_RE.DownloadManager } }); + var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}"); + //检测目标文件是否存在 + while (File.Exists(output)) + { + Logger.WarnMarkUp($"{output} => {output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output))}"); + } + + //修改输出后缀 + if (streamSpec.MediaType == Common.Enum.MediaType.AUDIO) + output = Path.ChangeExtension(output, ".m4a"); + else if (streamSpec.MediaType != Common.Enum.MediaType.SUBTITLES) + output = Path.ChangeExtension(output, ".mp4"); + if (DownloaderConfig.MP4RealTimeDecryption && mp4InitFile != "") { File.Delete(mp4InitFile); @@ -346,13 +416,6 @@ namespace N_m3u8DL_RE.DownloadManager //合并 if (!DownloaderConfig.SkipMerge) { - //对于fMP4,自动开启二进制合并 - if (!DownloaderConfig.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES && mp4InitFile != "") - { - DownloaderConfig.BinaryMerge = true; - Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.autoBinaryMerge}[/]"); - } - //字幕也使用二进制合并 if (DownloaderConfig.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES) { diff --git a/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs b/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs index 165bd2e..c83bd65 100644 --- a/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs +++ b/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs @@ -47,7 +47,7 @@ namespace N_m3u8DL_RE.Downloader } else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR) { - throw new NotSupportedException("SAMPLE-AES-CTR"); + //throw new NotSupportedException("SAMPLE-AES-CTR"); } } return dResult; diff --git a/src/N_m3u8DL-RE/Entity/Mediainfo.cs b/src/N_m3u8DL-RE/Entity/Mediainfo.cs new file mode 100644 index 0000000..6cf34c9 --- /dev/null +++ b/src/N_m3u8DL-RE/Entity/Mediainfo.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spectre.Console; + +namespace N_m3u8DL_RE.Entity +{ + internal class Mediainfo + { + public string Id { get; set; } + public string Text { get; set; } + public string BaseInfo { get; set; } + public string Bitrate { get; set; } + public string Resolution { get; set; } + public string Fps { get; set; } + public string Type { get; set; } + public bool DolbyVison { get; set; } + + public override string? ToString() + { + return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i))); + } + + public string ToStringMarkUp() + { + return "[steelblue]" + ToString().EscapeMarkup() + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]"; + } + } +} diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index ebb2060..d575d9a 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -39,13 +39,12 @@ namespace N_m3u8DL_RE try { //预先检查ffmpeg - if (!option.BinaryMerge) - { + if (option.FFmpegBinaryPath == null) option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg"); - if (string.IsNullOrEmpty(option.FFmpegBinaryPath)) - { - throw new FileNotFoundException("ffmpeg not found!"); - } + + if (string.IsNullOrEmpty(option.FFmpegBinaryPath)) + { + throw new FileNotFoundException(ResString.ffmpegNotFound); } //预先检查 @@ -192,6 +191,12 @@ namespace N_m3u8DL_RE if (lists.Count() > 1) await extractor.FetchPlayListAsync(selectedStreams); + //无法识别的加密方式,自动开启二进制合并 + if (selectedStreams.Any(s => s.Playlist.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN)))) + { + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]"); + } + if (option.WriteMetaJson) { Logger.Warn(ResString.writeJson); @@ -228,7 +233,11 @@ namespace N_m3u8DL_RE } catch (Exception ex) { - Logger.Error(ex.ToString()); + string msg = ex.Message; +#if DEBUG + msg = ex.ToString(); +#endif + Logger.Error(msg); await Task.Delay(3000); } } diff --git a/src/N_m3u8DL-RE/Util/MediainfoUtil.cs b/src/N_m3u8DL-RE/Util/MediainfoUtil.cs new file mode 100644 index 0000000..62c27fc --- /dev/null +++ b/src/N_m3u8DL-RE/Util/MediainfoUtil.cs @@ -0,0 +1,86 @@ +using N_m3u8DL_RE.Entity; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace N_m3u8DL_RE.Util +{ + internal partial class MediainfoUtil + { + [RegexGenerator(" Stream #.*")] + private static partial Regex TextRegex(); + [RegexGenerator("#0:\\d(\\[0x\\w+?\\])")] + private static partial Regex IdRegex(); + [RegexGenerator(": (\\w+): (.*)")] + private static partial Regex TypeRegex(); + [RegexGenerator("(.*?)(,|$)")] + private static partial Regex BaseInfoRegex(); + [RegexGenerator(" \\/ 0x\\w+")] + private static partial Regex ReplaceRegex(); + [RegexGenerator("\\d{2,}x\\d+")] + private static partial Regex ResRegex(); + [RegexGenerator("\\d+ kb\\/s")] + private static partial Regex BitrateRegex(); + [RegexGenerator("\\d+ fps")] + private static partial Regex FpsRegex(); + + public static async Task> ReadInfoAsync(string binary, string file) + { + var result = new List(); + + if (string.IsNullOrEmpty(file) || !File.Exists(file)) return result; + + string cmd = "-hide_banner -i \"" + file + "\""; + var p = Process.Start(new ProcessStartInfo() + { + FileName = binary, + Arguments = cmd, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + })!; + var output = p.StandardError.ReadToEnd(); + await p.WaitForExitAsync(); + + foreach (Match stream in TextRegex().Matches(output)) + { + var info = new Mediainfo() + { + Text = TypeRegex().Match(stream.Value).Groups[2].Value, + Id = IdRegex().Match(stream.Value).Groups[1].Value, + Type = TypeRegex().Match(stream.Value).Groups[1].Value, + }; + + info.Resolution = ResRegex().Match(info.Text).Value; + info.Bitrate = BitrateRegex().Match(info.Text).Value; + info.Fps = FpsRegex().Match(info.Text).Value; + info.BaseInfo = BaseInfoRegex().Match(info.Text).Groups[1].Value; + info.BaseInfo = ReplaceRegex().Replace(info.BaseInfo, ""); + + if (info.BaseInfo.Contains("dvhe") + || info.BaseInfo.Contains("dvh1") + || info.BaseInfo.Contains("DOVI") + || info.Type.Contains("dvvideo") + ) + info.DolbyVison = true; + + result.Add(info); + } + + if (result.Count == 0) + { + result.Add(new Mediainfo() + { + Type = "Unknown" + }); + } + + return result; + } + } +} diff --git a/src/N_m3u8DL-RE/Util/MergeUtil.cs b/src/N_m3u8DL-RE/Util/MergeUtil.cs index 558520b..15517b2 100644 --- a/src/N_m3u8DL-RE/Util/MergeUtil.cs +++ b/src/N_m3u8DL-RE/Util/MergeUtil.cs @@ -51,6 +51,13 @@ namespace N_m3u8DL_RE.Util { string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime; + //同名文件已存在的共存策略 + if (File.Exists($"{outputPath}.{muxFormat.ToLower()}")) + { + outputPath = Path.Combine(Path.GetDirectoryName(outputPath)!, + Path.GetFileName(outputPath) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")); + } + bool useAACFilter = true; StringBuilder command = new StringBuilder("-loglevel warning -i concat:\""); string ddpAudio = string.Empty;