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;