1
mirror of https://github.com/nilaoda/N_m3u8DL-CLI synced 2025-09-12 19:20:49 +02:00

Compare commits

..

3 Commits
2.7.1 ... 2.7.2

Author SHA1 Message Date
nilaoda
01e7735018 v2.7.2 2020-08-09 20:59:38 +08:00
nilaoda
a0c41d6116 v2.7.2 2020-08-09 20:34:32 +08:00
nilaoda
c3c25774de v2.7.2 2020-08-09 20:33:15 +08:00
10 changed files with 139 additions and 20 deletions

View File

@@ -2,6 +2,9 @@
namespace N_m3u8DL_CLI
{
/**
* https://www.cnblogs.com/linxuanchen/p/c-sharp-command-line-argument-parser.html
*/
public class CommandLineArgument
{
List<CommandLineArgument> _arguments;

View File

@@ -192,7 +192,7 @@ namespace N_m3u8DL_CLI
if (sd.SegDur < 0) sd.SegDur = 0; //防止负数
sd.FileUrl = firstSeg["segUri"].Value<string>();
//VTT字幕
if (isVTT == false && sd.FileUrl.Trim('\"').EndsWith(".vtt"))
if (isVTT == false && (sd.FileUrl.Trim('\"').EndsWith(".vtt") || sd.FileUrl.Trim('\"').EndsWith(".webvtt")))
isVTT = true;
sd.Method = firstSeg["method"].Value<string>();
if (sd.Method != "NONE")
@@ -281,8 +281,8 @@ namespace N_m3u8DL_CLI
sd.SegDur = info["duration"].Value<double>();
if (sd.SegDur < 0) sd.SegDur = 0; //防止负数
sd.FileUrl = info["segUri"].Value<string>();
//VTT字幕
if (isVTT == false && sd.FileUrl.Trim('\"').EndsWith(".vtt"))
//VTT字幕
if (isVTT == false && (sd.FileUrl.Trim('\"').EndsWith(".vtt") || sd.FileUrl.Trim('\"').EndsWith(".webvtt")))
isVTT = true;
sd.Method = info["method"].Value<string>();
if (sd.Method != "NONE")
@@ -370,7 +370,7 @@ namespace N_m3u8DL_CLI
}
else //开始合并
{
LOGGER.PrintLine(strings.downloadComplete + (DisableIntegrityCheck ? "(" + strings.disableIntegrityCheck + ")" : ""));
LOGGER.PrintLine(strings.downloadComplete + (DisableIntegrityCheck ? "("+strings.disableIntegrityCheck+")" : ""));
Console.WriteLine();
if (NoMerge == false)
{
@@ -381,7 +381,10 @@ namespace N_m3u8DL_CLI
LOGGER.PrintLine(strings.startMerging, LOGGER.Warning);
//VTT字幕
if (isVTT == true)
{
MuxFormat = "vtt";
Global.ReAdjustVtt(Global.GetFiles(DownDir + "\\Part_0", ".ts"));
}
//只有一个Part直接用ffmpeg合并
if (PartsCount == 1)
{
@@ -497,6 +500,9 @@ namespace N_m3u8DL_CLI
parser.Parse(); //开始解析
Thread.Sleep(1000);
LOGGER.CursorIndex = 5;
Global.HadReadInfo = false;
Global.VIDEO_TYPE = "";
Global.AUDIO_TYPE = "";
DoDownload();
}
if (externalSub) //下载独立字幕
@@ -509,16 +515,19 @@ namespace N_m3u8DL_CLI
parser.Headers = Headers; //继承Header
parser.BaseUrl = "";
parser.M3u8Url = externalSubUrl;
parser.DownName = DownName + "(Subtitle)";
parser.DownName = DownName.Replace("(Audio)", "") + "(Subtitle)";
parser.DownDir = Path.Combine(Path.GetDirectoryName(DownDir), parser.DownName);
LOGGER.WriteLine(strings.startParsing + externalSubUrl);
LOGGER.WriteLine(strings.downloadingExternalSubtitleTrack);
DownName = DownName + "(Subtitle)";
DownName = parser.DownName;
fflogName = "_ffreport(Subtitle).log";
DownDir = parser.DownDir;
parser.Parse(); //开始解析
Thread.Sleep(1000);
LOGGER.CursorIndex = 5;
Global.HadReadInfo = false;
Global.VIDEO_TYPE = "";
Global.AUDIO_TYPE = "";
DoDownload();
}
LOGGER.PrintLine(strings.taskDone, LOGGER.Warning);
@@ -530,7 +539,7 @@ namespace N_m3u8DL_CLI
FFmpeg.OutPutPath = Path.Combine(Directory.GetParent(DownDir).FullName, DownName);
FFmpeg.ReportFile = driverName + "\\:" + exePath.Remove(0, exePath.IndexOf(':') + 1).Replace("\\", "/") + "/Logs/" + Path.GetFileNameWithoutExtension(LOGGER.LOGFILE) + fflogName;
//合并分段
LOGGER.PrintLine(strings.startMerging);
for (int i = 0; i < PartsCount; i++)
@@ -637,6 +646,9 @@ namespace N_m3u8DL_CLI
parser.Parse(); //开始解析
Thread.Sleep(1000);
LOGGER.CursorIndex = 5;
Global.HadReadInfo = false;
Global.VIDEO_TYPE = "";
Global.AUDIO_TYPE = "";
DoDownload();
}
if (externalSub) //下载独立字幕
@@ -649,16 +661,19 @@ namespace N_m3u8DL_CLI
parser.Headers = Headers; //继承Header
parser.BaseUrl = "";
parser.M3u8Url = externalSubUrl;
parser.DownName = DownName + "(Subtitle)";
parser.DownName = DownName.Replace("(Audio)", "") + "(Subtitle)";
parser.DownDir = Path.Combine(Path.GetDirectoryName(DownDir), parser.DownName);
LOGGER.WriteLine(strings.startParsing + externalSubUrl);
LOGGER.WriteLine(strings.downloadingExternalSubtitleTrack);
DownName = DownName + "(Subtitle)";
DownName = parser.DownName;
fflogName = "_ffreport(Subtitle).log";
DownDir = parser.DownDir;
parser.Parse(); //开始解析
Thread.Sleep(1000);
LOGGER.CursorIndex = 5;
Global.HadReadInfo = false;
Global.VIDEO_TYPE = "";
Global.AUDIO_TYPE = "";
DoDownload();
}
LOGGER.PrintLine(strings.taskDone, LOGGER.Warning);

View File

@@ -30,8 +30,8 @@ namespace N_m3u8DL_CLI
/*===============================================================================*/
static string nowVer = "2.7.1";
static string nowDate = "20200719";
static string nowVer = "2.5.7";
static string nowDate = "20200809";
public static void WriteInit()
{
Console.Clear();
@@ -1015,5 +1015,56 @@ namespace N_m3u8DL_CLI
return wr;
}
}
/**
* 通过X-TIMESTAMP-MAP 调整VTT字幕的时间轴
*/
public static void ReAdjustVtt(string[] vtts)
{
string MsToTime(int ms)
{
TimeSpan ts = new TimeSpan(0, 0, 0, 0, ms);
string str = "";
str = (ts.Hours.ToString("00") + ":") + ts.Minutes.ToString("00") + ":" + ts.Seconds.ToString("00") + "." + ts.Milliseconds.ToString("000");
return str;
}
int TimeToMs(string line)
{
int hh = Convert.ToInt32(line.Split(':')[0]);
int mm = Convert.ToInt32(line.Split(':')[1]);
int ss = Convert.ToInt32(line.Split(':')[2].Split('.')[0]);
int ms = Convert.ToInt32(line.Split(':')[2].Split('.')[1]);
return hh * 60 * 60 * 1000 + mm * 60 * 1000 + ss * 1000 + ms;
}
int addTime = 0;
int baseTime = 0;
for (int i = 0; i < vtts.Length; i++)
{
string tmp = File.ReadAllText(vtts[i], Encoding.UTF8);
if (!Regex.IsMatch(tmp, "X-TIMESTAMP-MAP.*MPEGTS:(\\d+)"))
break;
if (i > 0)
{
int newTime = Convert.ToInt32(Regex.Match(tmp, "X-TIMESTAMP-MAP.*MPEGTS:(\\d+)").Groups[1].Value);
//计算偏移量
//LOGGER.PrintLine((newTime - baseTime).ToString());
addTime = addTime + ((newTime - baseTime) / 100);
if ((newTime - baseTime) == 6300000)
addTime -= 3000;
//将新的作为基准时间
baseTime = newTime;
foreach (Match m in Regex.Matches(tmp, @"(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})"))
{
string start = m.Groups[1].Value;
string end = m.Groups[2].Value;
tmp = tmp.Replace(m.Value, MsToTime(TimeToMs(start) + addTime) + " --> " + MsToTime(TimeToMs(end) + addTime));
}
}
File.WriteAllText(vtts[i], Regex.Replace(tmp, "X-TIMESTAMP-MAP=.*", ""), Encoding.UTF8);
}
//Console.ReadLine();
}
}
}

View File

@@ -24,6 +24,7 @@ namespace N_m3u8DL_CLI
private string downName = string.Empty;
private string keyFile = string.Empty;
private string keyBase64 = string.Empty;
private string keyIV = string.Empty;
private bool liveStream = false;
private long bestBandwidth = 0;
private string bestUrl = string.Empty;
@@ -60,6 +61,7 @@ namespace N_m3u8DL_CLI
public string KeyFile { get => keyFile; set => keyFile = value; }
public string KeyBase64 { get => keyBase64; set => keyBase64 = value; }
public bool LiveStream { get => liveStream; set => liveStream = value; }
public string KeyIV { get => keyIV; set => keyIV = value; }
public void Parse()
{
@@ -125,7 +127,7 @@ namespace N_m3u8DL_CLI
File.WriteAllText(m3u8SavePath, m3u8Content);
//针对优酷#EXT-X-VERSION:7杜比视界片源修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
{
Regex ykmap = new Regex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"");
foreach (Match m in ykmap.Matches(m3u8Content))
@@ -151,13 +153,22 @@ namespace N_m3u8DL_CLI
if (!string.IsNullOrEmpty(keyBase64))
{
string line = $"#EXT-X-KEY:METHOD=AES-128,URI=\"base64:{keyBase64}\"";
string line = "";
if (string.IsNullOrEmpty(keyIV))
line = $"#EXT-X-KEY:METHOD=AES-128,URI=\"base64:{keyBase64}\"";
else
line = $"#EXT-X-KEY:METHOD=AES-128,URI=\"base64:{keyBase64}\",IV=0x{keyIV.Replace("0x", "")}";
m3u8CurrentKey = ParseKey(line);
}
if (!string.IsNullOrEmpty(keyFile))
{
string line = "";
Uri u = new Uri(keyFile);
string line = $"#EXT-X-KEY:METHOD=AES-128,URI=\"{u.ToString()}\"";
if (string.IsNullOrEmpty(keyIV))
line = $"#EXT-X-KEY:METHOD=AES-128,URI=\"{u.ToString()}\"";
else
line = $"#EXT-X-KEY:METHOD=AES-128,URI=\"{u.ToString()}\",IV=0x{keyIV.Replace("0x", "")}";
m3u8CurrentKey = ParseKey(line);
}
@@ -267,7 +278,7 @@ namespace N_m3u8DL_CLI
segInfo.Add("key", m3u8CurrentKey[1]);
//没有读取到IV自己生成
if (m3u8CurrentKey[2] == "")
segInfo.Add("iv", "0x" + Convert.ToString(segIndex, 2).PadLeft(32, '0'));
segInfo.Add("iv", "0x" + Convert.ToString(segIndex, 16).PadLeft(32, '0'));
else
segInfo.Add("iv", m3u8CurrentKey[2]);
}
@@ -580,8 +591,14 @@ namespace N_m3u8DL_CLI
MasterListCheck();
}
bool downloadingM3u8KeyTip = false;
public string[] ParseKey(string line)
{
if (!downloadingM3u8KeyTip)
{
LOGGER.PrintLine(strings.downloadingM3u8Key, LOGGER.Warning);
downloadingM3u8KeyTip = true;
}
string[] tmp = line.Replace(HLSTags.ext_x_key + ":", "").Split(',');
string[] key = new string[] { "NONE", "", "" };
string u_l = Global.GetTagAttribute(lastKeyLine.Replace(HLSTags.ext_x_key + ":", ""), "URI");
@@ -608,7 +625,6 @@ namespace N_m3u8DL_CLI
}
else
{
LOGGER.PrintLine(strings.downloadingM3u8Key, LOGGER.Warning);
LOGGER.WriteLine(strings.downloadingM3u8Key + " " + key[1]);
if (key[1].StartsWith("http"))
{
@@ -632,11 +648,11 @@ namespace N_m3u8DL_CLI
{
indexs = "0-1-2-3-4-5-6-7-18-16-15-13-12-11-10-8".Split('-');
}
else if (algorithmNum == 0)
else if (algorithmNum == 0)
{
indexs = "0-1-2-3-4-5-6-7-8-10-11-12-14-15-16-18".Split('-');
}
else if(algorithmNum == 2)
else if (algorithmNum == 2)
{
var a_CODE = (int)Encoding.ASCII.GetBytes("a")[0];
@@ -681,7 +697,7 @@ namespace N_m3u8DL_CLI
return key;
}
}
else if(encKey.Length == 17)
else if (encKey.Length == 17)
{
indexs = "1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16".Split('-');
}

View File

@@ -235,11 +235,14 @@ namespace N_m3u8DL_CLI.NetCore
/// 2020年2月23日
/// - 不支持的加密方式将标记为NOTSUPPORTED并强制启用二进制合并
/// - 启用二进制合并的情况下如果m3u8文件中存在map文件则合并为mp4格式
/// - 支持优酷视频解密
/// 2020年2月24日
/// - 直播流录制优化逻辑,避免忙等待
/// - 直播Waiting时不再输出Parser内容
/// - 直播录制的日志记录
/// - 增加新的选项--liveRecDur限制直播录制时长
/// 2020年2月25日
/// - 修复优酷解密过程错误写入冗余数据的bug
/// 2020年2月27日
/// - 细节bug修复
/// 2020年2月28日
@@ -263,6 +266,7 @@ namespace N_m3u8DL_CLI.NetCore
/// - 增加同名文件合并时共存策略
/// 2020年4月17日
/// - 优化异常捕获
/// - 控制台输出设置为UTF-8
/// - 细节优化
/// 2020年4月22日
/// - 51cto getsign
@@ -273,6 +277,16 @@ namespace N_m3u8DL_CLI.NetCore
/// 2020年7月18日
/// - 从当前路径和exe路径同时寻找ffmpeg
/// - 支持多语言本地化(简繁英)
/// 2020年8月4日
/// - 修复外挂字幕命名问题
/// - 修复外挂字幕识别问题
/// - 修复外挂轨道的一些逻辑问题
/// - 优化多语言识别逻辑
/// 2020年8月5日
/// - 支持相对时间的vtt合并(还存在问题)
/// 2020年8月9日
/// - 修复IV错误导致的AES-128解密异常问题
/// - 支持自定义IV(--useKeyIV)
/// </summary>
///
@@ -312,13 +326,18 @@ namespace N_m3u8DL_CLI.NetCore
{
SetConsoleCtrlHandler(cancelHandler, true);
ServicePointManager.ServerCertificateValidationCallback = ValidateServerCertificate;
string loc = "zh-CN";
string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
if (currLoc == "zh-TW" || currLoc == "zh-HK" || currLoc == "zh-MO")
{
loc = "zh-TW";
}
else if (loc != "zh-CN" && loc != "zh-SG")
else if (loc == "zh-CN" || loc == "zh-SG")
{
loc = "zh-CN";
}
else
{
loc = "en-US";
}
@@ -384,6 +403,7 @@ namespace N_m3u8DL_CLI.NetCore
string reqHeaders = "";
string keyFile = "";
string keyBase64 = "";
string keyIV = "";
string muxSetJson = "MUXSETS.json";
string workDir = CURRENT_PATH + "\\Downloads";
bool muxFastStart = false;
@@ -442,6 +462,10 @@ namespace N_m3u8DL_CLI.NetCore
{
muxFastStart = true;
}
if (arguments.Has("--enableYouKuAes"))
{
Downloader.YouKuAES = true;
}
if (arguments.Has("--disableIntegrityCheck"))
{
DownloadManager.DisableIntegrityCheck = true;
@@ -472,6 +496,10 @@ namespace N_m3u8DL_CLI.NetCore
{
keyBase64 = arguments.Get("--useKeyBase64").Next;
}
if (arguments.Has("--useKeyIV"))
{
keyIV = arguments.Get("--useKeyIV").Next;
}
if (arguments.Has("--stopSpeed"))
{
Global.STOP_SPEED = Convert.ToInt64(arguments.Get("--stopSpeed").Next);
@@ -631,6 +659,7 @@ namespace N_m3u8DL_CLI.NetCore
parser.DownDir = Path.Combine(workDir, parser.DownName);
parser.M3u8Url = testurl;
parser.KeyBase64 = keyBase64;
parser.KeyIV = keyIV;
parser.KeyFile = keyFile;
if (baseUrl != "")
parser.BaseUrl = baseUrl;

View File

@@ -197,6 +197,7 @@
--muxSetJson File Set a json file for mux
--useKeyFile File Use 16 bytes file as KEY for AES-128 decryption
--useKeyBase64 Base64String Use Base64 String as KEY for AES-128 decryption
--useKeyIV HEXString Use HEX String as IV for AES-128 decryption
--downloadRange Range Set range for a video
--stopSpeed Number Speed below this, retry(KB/s)
--maxSpeed Number Set max download speed(KB/s)

View File

@@ -197,6 +197,7 @@
--muxSetJson File 使用外部json文件定义混流选项
--useKeyFile File 使用外部16字节文件定义AES-128解密KEY
--useKeyBase64 Base64String 使用Base64字符串定义AES-128解密KEY
--useKeyIV HEXString 使用HEX字符串定义AES-128解密IV
--downloadRange Range 仅下载视频的一部分分片或长度
--liveRecDur HH:MM:SS 直播录制时,达到此长度自动退出软件
--stopSpeed Number 当速度低于此值时,重试(单位为KB/s)

View File

@@ -197,6 +197,7 @@
--muxSetJson File 使用外部json文件定義混流選項
--useKeyFile File 使用外部16字節文件定義AES-128解密KEY
--useKeyBase64 Base64String 使用Base64字符串定義AES-128解密KEY
--useKeyIV HEXString 使用HEX字符串定義AES-128解密IV
--downloadRange Range 僅下載視頻的壹部分分片或長度
--liveRecDur HH:MM:SS 直播錄制時,達到此長度自動退出軟件
--stopSpeed Number 當速度低於此值時,重試(單位為KB/s)

View File

@@ -61,6 +61,7 @@ N_m3u8DL-CLI.exe <URL|JSON|FILE> [OPTIONS]
--muxSetJson File 使用外部json文件定义混流选项
--useKeyFile File 使用外部16字节文件定义AES-128解密KEY
--useKeyBase64 Base64String 使用Base64字符串定义AES-128解密KEY
--useKeyIV HEXString 使用HEX字符串定义AES-128解密IV
--downloadRange Range 仅下载视频的一部分分片或长度
--liveRecDur HH:MM:SS 直播录制时,达到此长度自动退出软件
--stopSpeed Number 当速度低于此值时,重试(单位为KB/s)

View File

@@ -43,6 +43,7 @@ N_m3u8DL-CLI.exe <URL|JSON|FILE> [OPTIONS]
--muxSetJson File Set a json file for mux
--useKeyFile File Use 16 bytes file as KEY for AES-128 decryption
--useKeyBase64 Base64String Use Base64 String as KEY for AES-128 decryption
--useKeyIV HEXString Use HEX String as IV for AES-128 decryption
--downloadRange Range Set range for a video
--stopSpeed Number Speed below this, retry(KB/s)
--maxSpeed Number Set max download speed(KB/s)