优化部分代码
This commit is contained in:
parent
cacf9b0ff0
commit
adbe376ae0
|
@ -1,9 +1,4 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Entity;
|
||||
|
||||
|
@ -34,9 +29,6 @@ public class EncryptInfo
|
|||
{
|
||||
return m;
|
||||
}
|
||||
else
|
||||
{
|
||||
return EncryptMethod.UNKNOWN;
|
||||
}
|
||||
return EncryptMethod.UNKNOWN;
|
||||
}
|
||||
}
|
|
@ -3,5 +3,5 @@
|
|||
// 主要处理 EXT-X-DISCONTINUITY
|
||||
public class MediaPart
|
||||
{
|
||||
public List<MediaSegment> MediaSegments { get; set; } = new List<MediaSegment>();
|
||||
public List<MediaSegment> MediaSegments { get; set; } = [];
|
||||
}
|
|
@ -17,7 +17,7 @@ public class MediaSegment
|
|||
|
||||
public bool IsEncrypted => EncryptInfo.Method != EncryptMethod.NONE;
|
||||
|
||||
public string Url { get; set; }
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public string? NameFromVar { get; set; } // MPD分段文件名
|
||||
|
||||
|
|
|
@ -3,17 +3,18 @@
|
|||
public class Playlist
|
||||
{
|
||||
// 对应Url信息
|
||||
public string Url { get; set; }
|
||||
public string Url { get; set; } = string.Empty;
|
||||
// 是否直播
|
||||
public bool IsLive { get; set; } = false;
|
||||
// 直播刷新间隔毫秒(默认15秒)
|
||||
public double RefreshIntervalMs { get; set; } = 15000;
|
||||
// 所有分片时长总和
|
||||
public double TotalDuration { get => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); }
|
||||
public double TotalDuration => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration));
|
||||
|
||||
// 所有分片中最长时长
|
||||
public double? TargetDuration { get; set; }
|
||||
// INIT信息
|
||||
public MediaSegment? MediaInit { get; set; }
|
||||
// 分片信息
|
||||
public List<MediaPart> MediaParts { get; set; } = new List<MediaPart>();
|
||||
public List<MediaPart> MediaParts { get; set; } = [];
|
||||
}
|
|
@ -46,12 +46,12 @@ public class StreamSpec
|
|||
/// <summary>
|
||||
/// URL
|
||||
/// </summary>
|
||||
public string Url { get; set; }
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 原始URL
|
||||
/// </summary>
|
||||
public string OriginalUrl { get; set; }
|
||||
public string OriginalUrl { get; set; } = string.Empty;
|
||||
|
||||
public Playlist? Playlist { get; set; }
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@ public partial class WebVttSub
|
|||
private static partial Regex TSValueRegex();
|
||||
[GeneratedRegex("\\s")]
|
||||
private static partial Regex SplitRegex();
|
||||
[GeneratedRegex("<c\\..*?>([\\s\\S]*?)<\\/c>")]
|
||||
[GeneratedRegex(@"<c\..*?>([\s\S]*?)<\/c>")]
|
||||
private static partial Regex VttClassRegex();
|
||||
|
||||
public List<SubCue> Cues { get; set; } = new List<SubCue>();
|
||||
public List<SubCue> Cues { get; set; } = [];
|
||||
public long MpegtsTimestamp { get; set; } = 0L;
|
||||
|
||||
/// <summary>
|
||||
|
@ -71,47 +71,45 @@ public partial class WebVttSub
|
|||
continue;
|
||||
}
|
||||
|
||||
if (needPayload)
|
||||
if (!needPayload) continue;
|
||||
|
||||
if (string.IsNullOrEmpty(line.Trim()))
|
||||
{
|
||||
if (string.IsNullOrEmpty(line.Trim()))
|
||||
{
|
||||
var payload = string.Join(Environment.NewLine, payloads);
|
||||
if (string.IsNullOrEmpty(payload.Trim())) continue; // 没获取到payload 跳过添加
|
||||
var payload = string.Join(Environment.NewLine, payloads);
|
||||
if (string.IsNullOrEmpty(payload.Trim())) continue; // 没获取到payload 跳过添加
|
||||
|
||||
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||
var startTime = ConvertToTS(arr[0]);
|
||||
var endTime = ConvertToTS(arr[1]);
|
||||
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
|
||||
webSub.Cues.Add(new SubCue()
|
||||
{
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), // Remove Zero Width Space!
|
||||
Settings = style
|
||||
});
|
||||
payloads.Clear();
|
||||
needPayload = false;
|
||||
}
|
||||
else
|
||||
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||
var startTime = ConvertToTS(arr[0]);
|
||||
var endTime = ConvertToTS(arr[1]);
|
||||
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
|
||||
webSub.Cues.Add(new SubCue()
|
||||
{
|
||||
payloads.Add(line.Trim());
|
||||
}
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), // Remove Zero Width Space!
|
||||
Settings = style
|
||||
});
|
||||
payloads.Clear();
|
||||
needPayload = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
payloads.Add(line.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (BaseTimestamp != 0)
|
||||
if (BaseTimestamp == 0) return webSub;
|
||||
|
||||
foreach (var item in webSub.Cues)
|
||||
{
|
||||
foreach (var item in webSub.Cues)
|
||||
if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)
|
||||
{
|
||||
if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)
|
||||
{
|
||||
item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp);
|
||||
item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp);
|
||||
item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +125,7 @@ public partial class WebVttSub
|
|||
return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " "));
|
||||
})).TrimEnd();
|
||||
}
|
||||
else return text;
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -140,18 +138,17 @@ public partial class WebVttSub
|
|||
FixTimestamp(webSub, this.MpegtsTimestamp);
|
||||
foreach (var item in webSub.Cues)
|
||||
{
|
||||
if (!this.Cues.Contains(item))
|
||||
if (this.Cues.Contains(item)) continue;
|
||||
|
||||
// 如果相差只有1ms,且payload相同,则拼接
|
||||
var last = this.Cues.LastOrDefault();
|
||||
if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload)
|
||||
{
|
||||
// 如果相差只有1ms,且payload相同,则拼接
|
||||
var last = this.Cues.LastOrDefault();
|
||||
if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload)
|
||||
{
|
||||
last.EndTime = item.EndTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Cues.Add(item);
|
||||
}
|
||||
last.EndTime = item.EndTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Cues.Add(item);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
|
@ -173,10 +170,10 @@ public partial class WebVttSub
|
|||
// 当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒,而字幕起始却是2秒),才修复
|
||||
if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset)
|
||||
{
|
||||
for (int i = 0; i < sub.Cues.Count; i++)
|
||||
foreach (var subCue in sub.Cues)
|
||||
{
|
||||
sub.Cues[i].StartTime += offset;
|
||||
sub.Cues[i].EndTime += offset;
|
||||
subCue.StartTime += offset;
|
||||
subCue.EndTime += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +202,7 @@ public partial class WebVttSub
|
|||
str = parts.First();
|
||||
}
|
||||
var t = str.Split(':').Reverse().ToList();
|
||||
for (int i = 0; i < t.Count(); i++)
|
||||
for (int i = 0; i < t.Count; i++)
|
||||
{
|
||||
time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
|
||||
}
|
||||
|
@ -214,7 +211,7 @@ public partial class WebVttSub
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in GetCues()) // 输出时去除空串
|
||||
{
|
||||
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.JsonConverter;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ using Spectre.Console;
|
|||
|
||||
namespace N_m3u8DL_RE.Common.Log;
|
||||
|
||||
public class NonAnsiWriter : TextWriter
|
||||
public partial class NonAnsiWriter : TextWriter
|
||||
{
|
||||
public override Encoding Encoding => Console.OutputEncoding;
|
||||
|
||||
|
@ -28,15 +28,22 @@ public class NonAnsiWriter : TextWriter
|
|||
private void RemoveAnsiEscapeSequences(string? input)
|
||||
{
|
||||
// Use regular expression to remove ANSI escape sequences
|
||||
string output = Regex.Replace(input ?? "", @"\x1B\[(\d+;?)+m", "");
|
||||
output = Regex.Replace(output, @"\[\??\d+[AKlh]", "");
|
||||
output = Regex.Replace(output,"[\r\n] +","");
|
||||
var output = MyRegex().Replace(input ?? "", "");
|
||||
output = MyRegex1().Replace(output, "");
|
||||
output = MyRegex2().Replace(output, "");
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Console.Write(output);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\x1B\[(\d+;?)+m")]
|
||||
private static partial Regex MyRegex();
|
||||
[GeneratedRegex(@"\[\??\d+[AKlh]")]
|
||||
private static partial Regex MyRegex1();
|
||||
[GeneratedRegex("[\r\n] +")]
|
||||
private static partial Regex MyRegex2();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Log;
|
||||
namespace N_m3u8DL_RE.Common.Log;
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
|
|||
|
||||
namespace N_m3u8DL_RE.Common.Log;
|
||||
|
||||
public partial class Logger
|
||||
public static partial class Logger
|
||||
{
|
||||
[GeneratedRegex("{}")]
|
||||
private static partial Regex VarsRepRegex();
|
||||
|
@ -80,24 +80,23 @@ public partial class Logger
|
|||
Console.WriteLine(subWrite);
|
||||
}
|
||||
|
||||
if (IsWriteFile && File.Exists(LogFilePath))
|
||||
if (!IsWriteFile || !File.Exists(LogFilePath)) return;
|
||||
|
||||
var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();
|
||||
try
|
||||
{
|
||||
var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();
|
||||
try
|
||||
// 进入写入
|
||||
LogWriteLock.EnterWriteLock();
|
||||
using (StreamWriter sw = File.AppendText(LogFilePath))
|
||||
{
|
||||
// 进入写入
|
||||
LogWriteLock.EnterWriteLock();
|
||||
using (StreamWriter sw = File.AppendText(LogFilePath))
|
||||
{
|
||||
sw.WriteLine(plain);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放占用
|
||||
LogWriteLock.ExitWriteLock();
|
||||
sw.WriteLine(plain);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放占用
|
||||
LogWriteLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -117,82 +116,74 @@ public partial class Logger
|
|||
|
||||
public static void Info(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.INFO)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
if (LogLevel < LogLevel.INFO) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
|
||||
public static void InfoMarkUp(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.INFO)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
if (LogLevel < LogLevel.INFO) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
|
||||
public static void Debug(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.DEBUG)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
if (LogLevel < LogLevel.DEBUG) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
|
||||
public static void DebugMarkUp(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.DEBUG)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
if (LogLevel < LogLevel.DEBUG) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
|
||||
public static void Warn(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.WARN)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
if (LogLevel < LogLevel.WARN) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
|
||||
public static void WarnMarkUp(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.WARN)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
if (LogLevel < LogLevel.WARN) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
|
||||
public static void Error(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.ERROR)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
if (LogLevel < LogLevel.ERROR) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
|
||||
HandleLog(write, data);
|
||||
}
|
||||
|
||||
public static void ErrorMarkUp(string data, params object[] ps)
|
||||
{
|
||||
if (LogLevel >= LogLevel.ERROR)
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
if (LogLevel < LogLevel.ERROR) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
|
||||
HandleLog(write);
|
||||
}
|
||||
|
||||
public static void ErrorMarkUp(Exception exception)
|
||||
|
@ -213,24 +204,23 @@ public partial class Logger
|
|||
/// <param name="ps"></param>
|
||||
public static void Extra(string data, params object[] ps)
|
||||
{
|
||||
if (IsWriteFile && File.Exists(LogFilePath))
|
||||
if (!IsWriteFile || !File.Exists(LogFilePath)) return;
|
||||
|
||||
data = ReplaceVars(data, ps);
|
||||
var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup();
|
||||
try
|
||||
{
|
||||
data = ReplaceVars(data, ps);
|
||||
var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup();
|
||||
try
|
||||
// 进入写入
|
||||
LogWriteLock.EnterWriteLock();
|
||||
using (StreamWriter sw = File.AppendText(LogFilePath))
|
||||
{
|
||||
// 进入写入
|
||||
LogWriteLock.EnterWriteLock();
|
||||
using (StreamWriter sw = File.AppendText(LogFilePath))
|
||||
{
|
||||
sw.WriteLine(plain, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放占用
|
||||
LogWriteLock.ExitWriteLock();
|
||||
sw.WriteLine(plain, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放占用
|
||||
LogWriteLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
public static class ResString
|
||||
{
|
||||
public static string CurrentLoc = "en-US";
|
||||
public static string CurrentLoc { get; set; } = "en-US";
|
||||
|
||||
public static readonly string ReLiveTs = "<RE_LIVE_TS>";
|
||||
public static string singleFileRealtimeDecryptWarn => GetText("singleFileRealtimeDecryptWarn");
|
||||
|
@ -140,13 +140,11 @@ public static class ResString
|
|||
|
||||
private static string GetText(string key)
|
||||
{
|
||||
if (!StaticText.LANG_DIC.ContainsKey(key))
|
||||
if (!StaticText.LANG_DIC.TryGetValue(key, out var textObj))
|
||||
return "<...LANG TEXT MISSING...>";
|
||||
|
||||
if (CurrentLoc == "zh-CN" || CurrentLoc == "zh-SG" || CurrentLoc == "zh-Hans")
|
||||
return StaticText.LANG_DIC[key].ZH_CN;
|
||||
if (CurrentLoc.StartsWith("zh-"))
|
||||
return StaticText.LANG_DIC[key].ZH_TW;
|
||||
return StaticText.LANG_DIC[key].EN_US;
|
||||
if (CurrentLoc is "zh-CN" or "zh-SG" or "zh-Hans")
|
||||
return textObj.ZH_CN;
|
||||
return CurrentLoc.StartsWith("zh-") ? textObj.ZH_TW : textObj.EN_US;
|
||||
}
|
||||
}
|
|
@ -1,14 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
namespace N_m3u8DL_RE.Common.Resource;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Resource;
|
||||
|
||||
internal class StaticText
|
||||
internal static class StaticText
|
||||
{
|
||||
public static Dictionary<string, TextContainer> LANG_DIC = new()
|
||||
public static readonly Dictionary<string, TextContainer> LANG_DIC = new()
|
||||
{
|
||||
["singleFileSplitWarn"] = new TextContainer
|
||||
(
|
||||
|
@ -228,9 +222,9 @@ internal class StaticText
|
|||
),
|
||||
["cmd_decryptionBinaryPath"] = new TextContainer
|
||||
(
|
||||
zhCN: "MP4解密所用工具的全路径, 例如 C:\\Tools\\mp4decrypt.exe",
|
||||
zhTW: "MP4解密所用工具的全路徑, 例如 C:\\Tools\\mp4decrypt.exe",
|
||||
enUS: "Full path to the tool used for MP4 decryption, like C:\\Tools\\mp4decrypt.exe"
|
||||
zhCN: @"MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe",
|
||||
zhTW: @"MP4解密所用工具的全路徑, 例如 C:\Tools\mp4decrypt.exe",
|
||||
enUS: @"Full path to the tool used for MP4 decryption, like C:\Tools\mp4decrypt.exe"
|
||||
),
|
||||
["cmd_delAfterDone"] = new TextContainer
|
||||
(
|
||||
|
@ -240,15 +234,15 @@ internal class StaticText
|
|||
),
|
||||
["cmd_ffmpegBinaryPath"] = new TextContainer
|
||||
(
|
||||
zhCN: "ffmpeg可执行程序全路径, 例如 C:\\Tools\\ffmpeg.exe",
|
||||
zhTW: "ffmpeg可執行程序全路徑, 例如 C:\\Tools\\ffmpeg.exe",
|
||||
enUS: "Full path to the ffmpeg binary, like C:\\Tools\\ffmpeg.exe"
|
||||
zhCN: @"ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe",
|
||||
zhTW: @"ffmpeg可執行程序全路徑, 例如 C:\Tools\ffmpeg.exe",
|
||||
enUS: @"Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe"
|
||||
),
|
||||
["cmd_mkvmergeBinaryPath"] = new TextContainer
|
||||
(
|
||||
zhCN: "mkvmerge可执行程序全路径, 例如 C:\\Tools\\mkvmerge.exe",
|
||||
zhTW: "mkvmerge可執行程序全路徑, 例如 C:\\Tools\\mkvmerge.exe",
|
||||
enUS: "Full path to the mkvmerge binary, like C:\\Tools\\mkvmerge.exe"
|
||||
zhCN: @"mkvmerge可执行程序全路径, 例如 C:\Tools\mkvmerge.exe",
|
||||
zhTW: @"mkvmerge可執行程序全路徑, 例如 C:\Tools\mkvmerge.exe",
|
||||
enUS: @"Full path to the mkvmerge binary, like C:\Tools\mkvmerge.exe"
|
||||
),
|
||||
["cmd_liveFixVttByAudio"] = new TextContainer
|
||||
(
|
||||
|
|
|
@ -67,8 +67,7 @@ public static class GlobalUtil
|
|||
{
|
||||
var fileExt = OperatingSystem.IsWindows() ? ".exe" : "";
|
||||
var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) };
|
||||
var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ??
|
||||
Array.Empty<string>();
|
||||
var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
|
||||
return searchPath.Concat(envPath).Select(p => Path.Combine(p!, name + fileExt)).FirstOrDefault(File.Exists);
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@ public static class HTTPUtil
|
|||
private static bool CheckMPEG2TS(HttpResponseMessage? webResponse)
|
||||
{
|
||||
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower();
|
||||
return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg";
|
||||
return mediaType is "video/ts" or "video/mp2t" or "video/mpeg";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -7,11 +7,11 @@ namespace N_m3u8DL_RE.Parser.Config;
|
|||
|
||||
public class ParserConfig
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public string OriginalUrl { get; set; }
|
||||
public string OriginalUrl { get; set; } = string.Empty;
|
||||
|
||||
public string BaseUrl { get; set; }
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
public Dictionary<string, string> CustomParserArgs { get; } = new();
|
||||
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
namespace N_m3u8DL_RE.Parser.Constants;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Constants;
|
||||
|
||||
internal class DASHTags
|
||||
internal static class DASHTags
|
||||
{
|
||||
public static string TemplateRepresentationID = "$RepresentationID$";
|
||||
public static string TemplateBandwidth = "$Bandwidth$";
|
||||
public static string TemplateNumber = "$Number$";
|
||||
public static string TemplateTime = "$Time$";
|
||||
public const string TemplateRepresentationID = "$RepresentationID$";
|
||||
public const string TemplateBandwidth = "$Bandwidth$";
|
||||
public const string TemplateNumber = "$Number$";
|
||||
public const string TemplateTime = "$Time$";
|
||||
}
|
|
@ -1,37 +1,31 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
namespace N_m3u8DL_RE.Parser.Constants;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Constants;
|
||||
|
||||
internal class HLSTags
|
||||
internal static class HLSTags
|
||||
{
|
||||
public static string ext_m3u = "#EXTM3U";
|
||||
public static string ext_x_targetduration = "#EXT-X-TARGETDURATION";
|
||||
public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE";
|
||||
public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE";
|
||||
public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME";
|
||||
public static string ext_x_media = "#EXT-X-MEDIA";
|
||||
public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE";
|
||||
public static string ext_x_key = "#EXT-X-KEY";
|
||||
public static string ext_x_stream_inf = "#EXT-X-STREAM-INF";
|
||||
public static string ext_x_version = "#EXT-X-VERSION";
|
||||
public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE";
|
||||
public static string ext_x_endlist = "#EXT-X-ENDLIST";
|
||||
public static string extinf = "#EXTINF";
|
||||
public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY";
|
||||
public static string ext_x_byterange = "#EXT-X-BYTERANGE";
|
||||
public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF";
|
||||
public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY";
|
||||
public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT";
|
||||
public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT";
|
||||
public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35";
|
||||
public static string ext_x_cue_start = "#EXT-X-CUE-OUT";
|
||||
public static string ext_x_cue_end = "#EXT-X-CUE-IN";
|
||||
public static string ext_x_cue_span = "#EXT-X-CUE-SPAN";
|
||||
public static string ext_x_map = "#EXT-X-MAP";
|
||||
public static string ext_x_start = "#EXT-X-START";
|
||||
public const string ext_m3u = "#EXTM3U";
|
||||
public const string ext_x_targetduration = "#EXT-X-TARGETDURATION";
|
||||
public const string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE";
|
||||
public const string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE";
|
||||
public const string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME";
|
||||
public const string ext_x_media = "#EXT-X-MEDIA";
|
||||
public const string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE";
|
||||
public const string ext_x_key = "#EXT-X-KEY";
|
||||
public const string ext_x_stream_inf = "#EXT-X-STREAM-INF";
|
||||
public const string ext_x_version = "#EXT-X-VERSION";
|
||||
public const string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE";
|
||||
public const string ext_x_endlist = "#EXT-X-ENDLIST";
|
||||
public const string extinf = "#EXTINF";
|
||||
public const string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY";
|
||||
public const string ext_x_byterange = "#EXT-X-BYTERANGE";
|
||||
public const string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF";
|
||||
public const string ext_x_discontinuity = "#EXT-X-DISCONTINUITY";
|
||||
public const string ext_x_cue_out_start = "#EXT-X-CUE-OUT";
|
||||
public const string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT";
|
||||
public const string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
public const string ext_x_scte35 = "#EXT-OATCLS-SCTE35";
|
||||
public const string ext_x_cue_start = "#EXT-X-CUE-OUT";
|
||||
public const string ext_x_cue_end = "#EXT-X-CUE-IN";
|
||||
public const string ext_x_cue_span = "#EXT-X-CUE-SPAN";
|
||||
public const string ext_x_map = "#EXT-X-MAP";
|
||||
public const string ext_x_start = "#EXT-X-START";
|
||||
}
|
|
@ -1,15 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
namespace N_m3u8DL_RE.Parser.Constants;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Constants;
|
||||
|
||||
internal class MSSTags
|
||||
internal static class MSSTags
|
||||
{
|
||||
public static string Bitrate = "{Bitrate}";
|
||||
public static string Bitrate_BK = "{bitrate}";
|
||||
public static string StartTime = "{start_time}";
|
||||
public static string StartTime_BK = "{start time}";
|
||||
public const string Bitrate = "{Bitrate}";
|
||||
public const string Bitrate_BK = "{bitrate}";
|
||||
public const string StartTime = "{start_time}";
|
||||
public const string StartTime_BK = "{start time}";
|
||||
}
|
|
@ -4,19 +4,14 @@ using N_m3u8DL_RE.Common.Util;
|
|||
using N_m3u8DL_RE.Parser.Config;
|
||||
using N_m3u8DL_RE.Parser.Constants;
|
||||
using N_m3u8DL_RE.Parser.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Extractor;
|
||||
|
||||
// https://blog.csdn.net/leek5533/article/details/117750191
|
||||
internal class DASHExtractor2 : IExtractor
|
||||
internal partial class DASHExtractor2 : IExtractor
|
||||
{
|
||||
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;
|
||||
|
||||
|
@ -37,10 +32,7 @@ internal class DASHExtractor2 : IExtractor
|
|||
private void SetInitUrl()
|
||||
{
|
||||
this.MpdUrl = ParserConfig.Url ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl))
|
||||
this.BaseUrl = ParserConfig.BaseUrl;
|
||||
else
|
||||
this.BaseUrl = this.MpdUrl;
|
||||
this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.MpdUrl;
|
||||
}
|
||||
|
||||
private string ExtendBaseUrl(XElement element, string oriBaseUrl)
|
||||
|
@ -57,13 +49,11 @@ internal class DASHExtractor2 : IExtractor
|
|||
private double? GetFrameRate(XElement element)
|
||||
{
|
||||
var frameRate = element.Attribute("frameRate")?.Value;
|
||||
if (frameRate != null && frameRate.Contains('/'))
|
||||
{
|
||||
var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]);
|
||||
frameRate = d.ToString("0.000");
|
||||
return Convert.ToDouble(frameRate);
|
||||
}
|
||||
return null;
|
||||
if (frameRate == null || !frameRate.Contains('/')) return null;
|
||||
|
||||
var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]);
|
||||
frameRate = d.ToString("0.000");
|
||||
return Convert.ToDouble(frameRate);
|
||||
}
|
||||
|
||||
public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
||||
|
@ -180,7 +170,7 @@ internal class DASHExtractor2 : IExtractor
|
|||
streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null;
|
||||
}
|
||||
// 优化字幕场景识别
|
||||
if (streamSpec.Codecs == "stpp" || streamSpec.Codecs == "wvtt")
|
||||
if (streamSpec.Codecs is "stpp" or "wvtt")
|
||||
{
|
||||
streamSpec.MediaType = MediaType.SUBTITLES;
|
||||
}
|
||||
|
@ -493,7 +483,7 @@ internal class DASHExtractor2 : IExtractor
|
|||
else
|
||||
{
|
||||
// 修复mp4类型字幕
|
||||
if (streamSpec.MediaType == MediaType.SUBTITLES && streamSpec.Extension == "mp4")
|
||||
if (streamSpec is { MediaType: MediaType.SUBTITLES, Extension: "mp4" })
|
||||
{
|
||||
streamSpec.Extension = "m4s";
|
||||
}
|
||||
|
@ -513,20 +503,17 @@ internal class DASHExtractor2 : IExtractor
|
|||
}
|
||||
|
||||
// 为视频设置默认轨道
|
||||
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO);
|
||||
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES);
|
||||
foreach (var item in streamList)
|
||||
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();
|
||||
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();
|
||||
foreach (var item in streamList.Where(item => !string.IsNullOrEmpty(item.Resolution)))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Resolution))
|
||||
if (aL.Count != 0)
|
||||
{
|
||||
if (aL.Any())
|
||||
{
|
||||
item.AudioId = aL.OrderByDescending(x => x.Bandwidth).First().GroupId;
|
||||
}
|
||||
if (sL.Any())
|
||||
{
|
||||
item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId;
|
||||
}
|
||||
item.AudioId = aL.OrderByDescending(x => x.Bandwidth).First().GroupId;
|
||||
}
|
||||
if (sL.Count != 0)
|
||||
{
|
||||
item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -541,8 +528,7 @@ internal class DASHExtractor2 : IExtractor
|
|||
private string? FilterLanguage(string? v)
|
||||
{
|
||||
if (v == null) return null;
|
||||
if (Regex.IsMatch(v, "^[\\w_\\-\\d]+$")) return v;
|
||||
return "und";
|
||||
return LangCodeRegex().IsMatch(v) ? v : "und";
|
||||
}
|
||||
|
||||
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||
|
@ -581,22 +567,21 @@ internal class DASHExtractor2 : IExtractor
|
|||
|
||||
private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
|
||||
{
|
||||
for (int i = 0; i < streamSpecs.Count; i++)
|
||||
foreach (var streamSpec in streamSpecs)
|
||||
{
|
||||
var playlist = streamSpecs[i].Playlist;
|
||||
if (playlist != null)
|
||||
var playlist = streamSpec.Playlist;
|
||||
if (playlist == null) continue;
|
||||
|
||||
if (playlist.MediaInit != null)
|
||||
{
|
||||
if (playlist.MediaInit != null)
|
||||
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
|
||||
}
|
||||
for (var ii = 0; ii < playlist!.MediaParts.Count; ii++)
|
||||
{
|
||||
var part = playlist.MediaParts[ii];
|
||||
foreach (var mediaSegment in part.MediaSegments)
|
||||
{
|
||||
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
|
||||
}
|
||||
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++)
|
||||
{
|
||||
var part = playlist.MediaParts[ii];
|
||||
for (int iii = 0; iii < part.MediaSegments.Count; iii++)
|
||||
{
|
||||
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url);
|
||||
}
|
||||
mediaSegment.Url = PreProcessUrl(mediaSegment.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -633,4 +618,7 @@ internal class DASHExtractor2 : IExtractor
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^[\w_\-\d]+$")]
|
||||
private static partial Regex LangCodeRegex();
|
||||
}
|
|
@ -5,12 +5,6 @@ using N_m3u8DL_RE.Common.Log;
|
|||
using N_m3u8DL_RE.Common.Resource;
|
||||
using N_m3u8DL_RE.Parser.Util;
|
||||
using N_m3u8DL_RE.Parser.Constants;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using N_m3u8DL_RE.Common.Util;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Extractor;
|
||||
|
@ -26,8 +20,6 @@ internal class HLSExtractor : IExtractor
|
|||
|
||||
public ParserConfig ParserConfig { get; set; }
|
||||
|
||||
private HLSExtractor() { }
|
||||
|
||||
public HLSExtractor(ParserConfig parserConfig)
|
||||
{
|
||||
this.ParserConfig = parserConfig;
|
||||
|
@ -37,14 +29,7 @@ internal class HLSExtractor : IExtractor
|
|||
|
||||
private void SetBaseUrl()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl))
|
||||
{
|
||||
this.BaseUrl = ParserConfig.BaseUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.BaseUrl = this.M3u8Url;
|
||||
}
|
||||
this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.M3u8Url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -87,7 +72,7 @@ internal class HLSExtractor : IExtractor
|
|||
{
|
||||
MasterM3u8Flag = true;
|
||||
|
||||
List<StreamSpec> streams = new List<StreamSpec>();
|
||||
List<StreamSpec> streams = [];
|
||||
|
||||
using StringReader sr = new StringReader(M3u8Content);
|
||||
string? line;
|
||||
|
@ -201,7 +186,7 @@ internal class HLSExtractor : IExtractor
|
|||
|
||||
streams.Add(streamSpec);
|
||||
}
|
||||
else if (line.StartsWith("#"))
|
||||
else if (line.StartsWith('#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -237,22 +222,22 @@ internal class HLSExtractor : IExtractor
|
|||
long startIndex;
|
||||
|
||||
Playlist playlist = new();
|
||||
List<MediaPart> mediaParts = new();
|
||||
List<MediaPart> mediaParts = [];
|
||||
|
||||
// 当前的加密信息
|
||||
EncryptInfo currentEncryptInfo = new();
|
||||
if (ParserConfig.CustomMethod != null)
|
||||
currentEncryptInfo.Method = ParserConfig.CustomMethod.Value;
|
||||
if (ParserConfig.CustomeKey != null && ParserConfig.CustomeKey.Length > 0)
|
||||
if (ParserConfig.CustomeKey is { Length: > 0 })
|
||||
currentEncryptInfo.Key = ParserConfig.CustomeKey;
|
||||
if (ParserConfig.CustomeIV != null && ParserConfig.CustomeIV.Length > 0)
|
||||
if (ParserConfig.CustomeIV is { Length: > 0 })
|
||||
currentEncryptInfo.IV = ParserConfig.CustomeIV;
|
||||
// 上次读取到的加密行,#EXT-X-KEY:……
|
||||
string lastKeyLine = "";
|
||||
|
||||
MediaPart mediaPart = new();
|
||||
MediaSegment segment = new();
|
||||
List<MediaSegment> segments = new();
|
||||
List<MediaSegment> segments = [];
|
||||
|
||||
|
||||
while ((line = sr.ReadLine()) != null)
|
||||
|
@ -304,20 +289,19 @@ internal class HLSExtractor : IExtractor
|
|||
// 修复YK去除广告后的遗留问题
|
||||
if (hasAd && mediaParts.Count > 0)
|
||||
{
|
||||
segments = mediaParts[mediaParts.Count - 1].MediaSegments;
|
||||
segments = mediaParts[^1].MediaSegments;
|
||||
mediaParts.RemoveAt(mediaParts.Count - 1);
|
||||
hasAd = false;
|
||||
continue;
|
||||
}
|
||||
// 常规情况的#EXT-X-DISCONTINUITY标记,新建part
|
||||
if (!hasAd && segments.Count >= 1)
|
||||
if (hasAd || segments.Count < 1) continue;
|
||||
|
||||
mediaParts.Add(new MediaPart
|
||||
{
|
||||
mediaParts.Add(new MediaPart()
|
||||
{
|
||||
MediaSegments = segments,
|
||||
});
|
||||
segments = new();
|
||||
}
|
||||
MediaSegments = segments,
|
||||
});
|
||||
segments = new();
|
||||
}
|
||||
// 解析KEY
|
||||
else if (line.StartsWith(HLSTags.ext_x_key))
|
||||
|
@ -382,13 +366,11 @@ internal class HLSExtractor : IExtractor
|
|||
playlist.MediaInit.ExpectLength = n;
|
||||
playlist.MediaInit.StartRange = o ?? 0L;
|
||||
}
|
||||
// 是否有加密,有的话写入KEY和IV
|
||||
if (currentEncryptInfo.Method != EncryptMethod.NONE)
|
||||
{
|
||||
playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method;
|
||||
playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key;
|
||||
playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));
|
||||
}
|
||||
if (currentEncryptInfo.Method == EncryptMethod.NONE) continue;
|
||||
// 有加密的话写入KEY和IV
|
||||
playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method;
|
||||
playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key;
|
||||
playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));
|
||||
}
|
||||
// 遇到了其他的map,说明已经不是一个视频了,全部丢弃即可
|
||||
else
|
||||
|
@ -409,7 +391,7 @@ internal class HLSExtractor : IExtractor
|
|||
}
|
||||
}
|
||||
// 评论行不解析
|
||||
else if (line.StartsWith("#")) continue;
|
||||
else if (line.StartsWith('#')) continue;
|
||||
// 空白行不解析
|
||||
else if (line.StartsWith("\r\n")) continue;
|
||||
// 解析分片的地址
|
||||
|
@ -488,15 +470,15 @@ internal class HLSExtractor : IExtractor
|
|||
}
|
||||
|
||||
var playlist = await ParseListAsync();
|
||||
return new List<StreamSpec>
|
||||
{
|
||||
return
|
||||
[
|
||||
new()
|
||||
{
|
||||
Url = ParserConfig.Url,
|
||||
Playlist = playlist,
|
||||
Extension = playlist.MediaInit != null ? "mp4" : "ts"
|
||||
}
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
private async Task LoadM3u8FromUrlAsync(string url)
|
||||
|
@ -539,11 +521,10 @@ internal class HLSExtractor : IExtractor
|
|||
foreach (var l in lists)
|
||||
{
|
||||
var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()).ToList();
|
||||
if (match.Any())
|
||||
{
|
||||
Logger.DebugMarkUp($"{l.Url} => {match.First().Url}");
|
||||
l.Url = match.First().Url;
|
||||
}
|
||||
if (match.Count == 0) continue;
|
||||
|
||||
Logger.DebugMarkUp($"{l.Url} => {match.First().Url}");
|
||||
l.Url = match.First().Url;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -556,7 +537,7 @@ internal class HLSExtractor : IExtractor
|
|||
// 直接重新加载m3u8
|
||||
await LoadM3u8FromUrlAsync(lists[i].Url!);
|
||||
}
|
||||
catch (HttpRequestException) when (MasterM3u8Flag == true)
|
||||
catch (HttpRequestException) when (MasterM3u8Flag)
|
||||
{
|
||||
Logger.WarnMarkUp("Can not load m3u8. Try refreshing url from master url...");
|
||||
// 当前URL无法加载 尝试从Master链接中刷新URL
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
using N_m3u8DL_RE.Parser.Config;
|
||||
using N_m3u8DL_RE.Common.Entity;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using N_m3u8DL_RE.Common.Enum;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Extractor;
|
||||
|
|
|
@ -6,14 +6,7 @@ using N_m3u8DL_RE.Parser.Config;
|
|||
using N_m3u8DL_RE.Parser.Constants;
|
||||
using N_m3u8DL_RE.Parser.Mp4;
|
||||
using N_m3u8DL_RE.Parser.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Extractor;
|
||||
|
@ -46,10 +39,7 @@ internal partial class MSSExtractor : IExtractor
|
|||
private void SetInitUrl()
|
||||
{
|
||||
this.IsmUrl = ParserConfig.Url ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl))
|
||||
this.BaseUrl = ParserConfig.BaseUrl;
|
||||
else
|
||||
this.BaseUrl = this.IsmUrl;
|
||||
this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.IsmUrl;
|
||||
}
|
||||
|
||||
public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
||||
|
@ -248,20 +238,17 @@ internal partial class MSSExtractor : IExtractor
|
|||
}
|
||||
|
||||
// 为视频设置默认轨道
|
||||
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO);
|
||||
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES);
|
||||
foreach (var item in streamList)
|
||||
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();
|
||||
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();
|
||||
foreach (var item in streamList.Where(item => !string.IsNullOrEmpty(item.Resolution)))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Resolution))
|
||||
if (aL.Count != 0)
|
||||
{
|
||||
if (aL.Any())
|
||||
{
|
||||
item.AudioId = aL.First().GroupId;
|
||||
}
|
||||
if (sL.Any())
|
||||
{
|
||||
item.SubtitleId = sL.First().GroupId;
|
||||
}
|
||||
item.AudioId = aL.First().GroupId;
|
||||
}
|
||||
if (sL.Count != 0)
|
||||
{
|
||||
item.SubtitleId = sL.First().GroupId;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,22 +305,21 @@ internal partial class MSSExtractor : IExtractor
|
|||
|
||||
private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
|
||||
{
|
||||
for (int i = 0; i < streamSpecs.Count; i++)
|
||||
foreach (var streamSpec in streamSpecs)
|
||||
{
|
||||
var playlist = streamSpecs[i].Playlist;
|
||||
if (playlist != null)
|
||||
var playlist = streamSpec.Playlist;
|
||||
if (playlist == null) continue;
|
||||
|
||||
if (playlist.MediaInit != null)
|
||||
{
|
||||
if (playlist.MediaInit != null)
|
||||
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
|
||||
}
|
||||
for (var ii = 0; ii < playlist!.MediaParts.Count; ii++)
|
||||
{
|
||||
var part = playlist.MediaParts[ii];
|
||||
foreach (var segment in part.MediaSegments)
|
||||
{
|
||||
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
|
||||
}
|
||||
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++)
|
||||
{
|
||||
var part = playlist.MediaParts[ii];
|
||||
for (int iii = 0; iii < part.MediaSegments.Count; iii++)
|
||||
{
|
||||
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url);
|
||||
}
|
||||
segment.Url = PreProcessUrl(segment.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +1,62 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
namespace Mp4SubtitleParser;
|
||||
|
||||
namespace Mp4SubtitleParser
|
||||
// make BinaryReader in Big Endian
|
||||
class BinaryReader2 : BinaryReader
|
||||
{
|
||||
// make BinaryReader in Big Endian
|
||||
class BinaryReader2 : BinaryReader
|
||||
public BinaryReader2(System.IO.Stream stream) : base(stream) { }
|
||||
|
||||
public bool HasMoreData()
|
||||
{
|
||||
public BinaryReader2(System.IO.Stream stream) : base(stream) { }
|
||||
|
||||
public bool HasMoreData()
|
||||
{
|
||||
return BaseStream.Position < BaseStream.Length;
|
||||
}
|
||||
|
||||
public long GetLength()
|
||||
{
|
||||
return BaseStream.Length;
|
||||
}
|
||||
|
||||
public long GetPosition()
|
||||
{
|
||||
return BaseStream.Position;
|
||||
}
|
||||
|
||||
public override int ReadInt32()
|
||||
{
|
||||
var data = base.ReadBytes(4);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToInt32(data, 0);
|
||||
}
|
||||
|
||||
public override short ReadInt16()
|
||||
{
|
||||
var data = base.ReadBytes(2);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToInt16(data, 0);
|
||||
}
|
||||
|
||||
public override long ReadInt64()
|
||||
{
|
||||
var data = base.ReadBytes(8);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToInt64(data, 0);
|
||||
}
|
||||
|
||||
public override uint ReadUInt32()
|
||||
{
|
||||
var data = base.ReadBytes(4);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToUInt32(data, 0);
|
||||
}
|
||||
|
||||
public override ulong ReadUInt64()
|
||||
{
|
||||
var data = base.ReadBytes(8);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToUInt64(data, 0);
|
||||
}
|
||||
return BaseStream.Position < BaseStream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public long GetLength()
|
||||
{
|
||||
return BaseStream.Length;
|
||||
}
|
||||
|
||||
public long GetPosition()
|
||||
{
|
||||
return BaseStream.Position;
|
||||
}
|
||||
|
||||
public override int ReadInt32()
|
||||
{
|
||||
var data = base.ReadBytes(4);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToInt32(data, 0);
|
||||
}
|
||||
|
||||
public override short ReadInt16()
|
||||
{
|
||||
var data = base.ReadBytes(2);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToInt16(data, 0);
|
||||
}
|
||||
|
||||
public override long ReadInt64()
|
||||
{
|
||||
var data = base.ReadBytes(8);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToInt64(data, 0);
|
||||
}
|
||||
|
||||
public override uint ReadUInt32()
|
||||
{
|
||||
var data = base.ReadBytes(4);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToUInt32(data, 0);
|
||||
}
|
||||
|
||||
public override ulong ReadUInt64()
|
||||
{
|
||||
var data = base.ReadBytes(8);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(data);
|
||||
return BitConverter.ToUInt64(data, 0);
|
||||
}
|
||||
}
|
|
@ -1,89 +1,83 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
|
||||
namespace Mp4SubtitleParser
|
||||
namespace Mp4SubtitleParser;
|
||||
|
||||
// make BinaryWriter in Big Endian
|
||||
class BinaryWriter2 : BinaryWriter
|
||||
{
|
||||
// make BinaryWriter in Big Endian
|
||||
class BinaryWriter2 : BinaryWriter
|
||||
private static bool IsLittleEndian = BitConverter.IsLittleEndian;
|
||||
public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
|
||||
|
||||
|
||||
public void WriteUInt(decimal n, int offset = 0)
|
||||
{
|
||||
private static bool IsLittleEndian = BitConverter.IsLittleEndian;
|
||||
public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
|
||||
|
||||
|
||||
public void WriteUInt(decimal n, int offset = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((uint)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
if (offset != 0)
|
||||
arr = arr[offset..];
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public override void Write(string text)
|
||||
{
|
||||
BaseStream.Write(Encoding.ASCII.GetBytes(text));
|
||||
}
|
||||
|
||||
public void WriteInt(decimal n, int offset = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((int)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
if (offset != 0)
|
||||
arr = arr[offset..];
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteULong(decimal n, int offset = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((ulong)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
if (offset != 0)
|
||||
arr = arr[offset..];
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteUShort(decimal n, int padding = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((ushort)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
while (padding > 0)
|
||||
{
|
||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||
padding--;
|
||||
}
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteShort(decimal n, int padding = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((short)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
while (padding > 0)
|
||||
{
|
||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||
padding--;
|
||||
}
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteByte(byte n, int padding = 0)
|
||||
{
|
||||
var arr = new byte[] { n };
|
||||
while (padding > 0)
|
||||
{
|
||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||
padding--;
|
||||
}
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
var arr = BitConverter.GetBytes((uint)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
if (offset != 0)
|
||||
arr = arr[offset..];
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(string text)
|
||||
{
|
||||
BaseStream.Write(Encoding.ASCII.GetBytes(text));
|
||||
}
|
||||
|
||||
public void WriteInt(decimal n, int offset = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((int)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
if (offset != 0)
|
||||
arr = arr[offset..];
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteULong(decimal n, int offset = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((ulong)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
if (offset != 0)
|
||||
arr = arr[offset..];
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteUShort(decimal n, int padding = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((ushort)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
while (padding > 0)
|
||||
{
|
||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||
padding--;
|
||||
}
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteShort(decimal n, int padding = 0)
|
||||
{
|
||||
var arr = BitConverter.GetBytes((short)n);
|
||||
if (IsLittleEndian)
|
||||
Array.Reverse(arr);
|
||||
while (padding > 0)
|
||||
{
|
||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||
padding--;
|
||||
}
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
|
||||
public void WriteByte(byte n, int padding = 0)
|
||||
{
|
||||
var arr = new byte[] { n };
|
||||
while (padding > 0)
|
||||
{
|
||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||
padding--;
|
||||
}
|
||||
BaseStream.Write(arr);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
using N_m3u8DL_RE.Common.Util;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Mp4SubtitleParser
|
||||
{
|
||||
|
@ -11,10 +10,10 @@ namespace Mp4SubtitleParser
|
|||
public bool isMultiDRM;
|
||||
}
|
||||
|
||||
public class MP4InitUtil
|
||||
public static class MP4InitUtil
|
||||
{
|
||||
private static readonly byte[] SYSTEM_ID_WIDEVINE = { 0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED };
|
||||
private static readonly byte[] SYSTEM_ID_PLAYREADY = { 0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95 };
|
||||
private static readonly byte[] SYSTEM_ID_WIDEVINE = [0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED];
|
||||
private static readonly byte[] SYSTEM_ID_PLAYREADY = [0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95];
|
||||
|
||||
public static ParsedMP4Info ReadInit(byte[] data)
|
||||
{
|
||||
|
@ -28,22 +27,20 @@ namespace Mp4SubtitleParser
|
|||
.Box("minf", MP4Parser.Children)
|
||||
.Box("stbl", MP4Parser.Children)
|
||||
.FullBox("stsd", MP4Parser.SampleDescription)
|
||||
.FullBox("pssh", (box) =>
|
||||
.FullBox("pssh", box =>
|
||||
{
|
||||
if (!(box.Version == 0 || box.Version == 1))
|
||||
if (box.Version is not (0 or 1))
|
||||
throw new Exception("PSSH version can only be 0 or 1");
|
||||
var systemId = box.Reader.ReadBytes(16);
|
||||
if (SYSTEM_ID_WIDEVINE.SequenceEqual(systemId))
|
||||
{
|
||||
var dataSize = box.Reader.ReadUInt32();
|
||||
var psshData = box.Reader.ReadBytes((int)dataSize);
|
||||
info.PSSH = Convert.ToBase64String(psshData);
|
||||
if (info.KID == "00000000000000000000000000000000")
|
||||
{
|
||||
info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower();
|
||||
info.isMultiDRM = true;
|
||||
}
|
||||
}
|
||||
if (!SYSTEM_ID_WIDEVINE.SequenceEqual(systemId)) return;
|
||||
|
||||
var dataSize = box.Reader.ReadUInt32();
|
||||
var psshData = box.Reader.ReadBytes((int)dataSize);
|
||||
info.PSSH = Convert.ToBase64String(psshData);
|
||||
if (info.KID != "00000000000000000000000000000000") return;
|
||||
|
||||
info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower();
|
||||
info.isMultiDRM = true;
|
||||
})
|
||||
.FullBox("encv", MP4Parser.AllData(data => ReadBox(data, info)))
|
||||
.FullBox("enca", MP4Parser.AllData(data => ReadBox(data, info)))
|
||||
|
@ -57,11 +54,11 @@ namespace Mp4SubtitleParser
|
|||
private static void ReadBox(byte[] data, ParsedMP4Info info)
|
||||
{
|
||||
// find schm
|
||||
var schmBytes = new byte[4] { 0x73, 0x63, 0x68, 0x6d };
|
||||
byte[] schmBytes = [0x73, 0x63, 0x68, 0x6d];
|
||||
var schmIndex = 0;
|
||||
for (int i = 0; i < data.Length - 4; i++)
|
||||
for (var i = 0; i < data.Length - 4; i++)
|
||||
{
|
||||
if (new byte[4] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(schmBytes))
|
||||
if (new[] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(schmBytes))
|
||||
{
|
||||
schmIndex = i;
|
||||
break;
|
||||
|
@ -75,11 +72,11 @@ namespace Mp4SubtitleParser
|
|||
// if (info.Scheme != "cenc") return;
|
||||
|
||||
// find KID
|
||||
var tencBytes = new byte[4] { 0x74, 0x65, 0x6E, 0x63 };
|
||||
byte[] tencBytes = [0x74, 0x65, 0x6E, 0x6];
|
||||
var tencIndex = -1;
|
||||
for (int i = 0; i < data.Length - 4; i++)
|
||||
{
|
||||
if (new byte[4] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(tencBytes))
|
||||
if (new[] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(tencBytes))
|
||||
{
|
||||
tencIndex = i;
|
||||
break;
|
||||
|
|
|
@ -9,12 +9,12 @@ namespace Mp4SubtitleParser
|
|||
{
|
||||
class ParsedBox
|
||||
{
|
||||
public MP4Parser Parser { get; set; }
|
||||
public required MP4Parser Parser { get; set; }
|
||||
public bool PartialOkay { get; set; }
|
||||
public long Start { get; set; }
|
||||
public uint Version { get; set; } = 1000;
|
||||
public uint Flags { get; set; } = 1000;
|
||||
public BinaryReader2 Reader { get; set; }
|
||||
public required BinaryReader2 Reader { get; set; }
|
||||
public bool Has64BitSize { get; set; }
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ namespace Mp4SubtitleParser
|
|||
class TRUN
|
||||
{
|
||||
public uint SampleCount { get; set; }
|
||||
public List<Sample> SampleData { get; set; } = new List<Sample>();
|
||||
public List<Sample> SampleData { get; set; } = [];
|
||||
}
|
||||
|
||||
class Sample
|
||||
|
@ -55,7 +55,7 @@ namespace Mp4SubtitleParser
|
|||
|
||||
public static BoxHandler AllData(DataHandler handler)
|
||||
{
|
||||
return (box) =>
|
||||
return box =>
|
||||
{
|
||||
var all = box.Reader.GetLength() - box.Reader.GetPosition();
|
||||
handler(box.Reader.ReadBytes((int)all));
|
||||
|
@ -161,7 +161,7 @@ namespace Mp4SubtitleParser
|
|||
}
|
||||
|
||||
int payloadSize = (int)(end - reader.GetPosition());
|
||||
var payload = (payloadSize > 0) ? reader.ReadBytes(payloadSize) : new byte[0];
|
||||
var payload = (payloadSize > 0) ? reader.ReadBytes(payloadSize) : [];
|
||||
var box = new ParsedBox()
|
||||
{
|
||||
Parser = this,
|
||||
|
|
|
@ -3,75 +3,229 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
|
||||
namespace Mp4SubtitleParser
|
||||
namespace Mp4SubtitleParser;
|
||||
|
||||
class SubEntity
|
||||
{
|
||||
class SubEntity
|
||||
public required string Begin { get; set; }
|
||||
public required string End { get; set; }
|
||||
public required string Region { get; set; }
|
||||
public List<XmlElement> Contents { get; set; } = [];
|
||||
public List<string> ContentStrings { get; set; } = [];
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
public string Begin { get; set; }
|
||||
public string End { get; set; }
|
||||
public string Region { get; set; }
|
||||
public List<XmlElement> Contents { get; set; } = new();
|
||||
public List<string> ContentStrings { get; set; } = new();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is SubEntity entity &&
|
||||
Begin == entity.Begin &&
|
||||
End == entity.End &&
|
||||
Region == entity.Region &&
|
||||
ContentStrings.SequenceEqual(entity.ContentStrings);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Begin, End, Region, ContentStrings);
|
||||
}
|
||||
return obj is SubEntity entity &&
|
||||
Begin == entity.Begin &&
|
||||
End == entity.End &&
|
||||
Region == entity.Region &&
|
||||
ContentStrings.SequenceEqual(entity.ContentStrings);
|
||||
}
|
||||
|
||||
public partial class MP4TtmlUtil
|
||||
public override int GetHashCode()
|
||||
{
|
||||
[GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")]
|
||||
private static partial Regex AttrRegex();
|
||||
[GeneratedRegex("<p.*?>(.+?)<\\/p>")]
|
||||
private static partial Regex LabelFixRegex();
|
||||
[GeneratedRegex("\\<tt[\\s\\S]*?\\<\\/tt\\>")]
|
||||
private static partial Regex MultiElementsFixRegex();
|
||||
[GeneratedRegex("\\<smpte:image.*xml:id=\\\"(.*?)\\\".*\\>([\\s\\S]*?)<\\/smpte:image>")]
|
||||
private static partial Regex ImageRegex();
|
||||
return HashCode.Combine(Begin, End, Region, ContentStrings);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CheckInit(byte[] data)
|
||||
public static partial class MP4TtmlUtil
|
||||
{
|
||||
[GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")]
|
||||
private static partial Regex AttrRegex();
|
||||
[GeneratedRegex("<p.*?>(.+?)<\\/p>")]
|
||||
private static partial Regex LabelFixRegex();
|
||||
[GeneratedRegex(@"\<tt[\s\S]*?\<\/tt\>")]
|
||||
private static partial Regex MultiElementsFixRegex();
|
||||
[GeneratedRegex("\\<smpte:image.*xml:id=\\\"(.*?)\\\".*\\>([\\s\\S]*?)<\\/smpte:image>")]
|
||||
private static partial Regex ImageRegex();
|
||||
|
||||
public static bool CheckInit(byte[] data)
|
||||
{
|
||||
bool sawSTPP = false;
|
||||
|
||||
// parse init
|
||||
new MP4Parser()
|
||||
.Box("moov", MP4Parser.Children)
|
||||
.Box("trak", MP4Parser.Children)
|
||||
.Box("mdia", MP4Parser.Children)
|
||||
.Box("minf", MP4Parser.Children)
|
||||
.Box("stbl", MP4Parser.Children)
|
||||
.FullBox("stsd", MP4Parser.SampleDescription)
|
||||
.Box("stpp", box => {
|
||||
sawSTPP = true;
|
||||
})
|
||||
.Parse(data);
|
||||
|
||||
return sawSTPP;
|
||||
}
|
||||
|
||||
private static string ShiftTime(string xmlSrc, long segTimeMs, int index)
|
||||
{
|
||||
string Add(string xmlTime)
|
||||
{
|
||||
bool sawSTPP = false;
|
||||
|
||||
// parse init
|
||||
new MP4Parser()
|
||||
.Box("moov", MP4Parser.Children)
|
||||
.Box("trak", MP4Parser.Children)
|
||||
.Box("mdia", MP4Parser.Children)
|
||||
.Box("minf", MP4Parser.Children)
|
||||
.Box("stbl", MP4Parser.Children)
|
||||
.FullBox("stsd", MP4Parser.SampleDescription)
|
||||
.Box("stpp", (box) => {
|
||||
sawSTPP = true;
|
||||
})
|
||||
.Parse(data);
|
||||
|
||||
return sawSTPP;
|
||||
var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
|
||||
var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index);
|
||||
return $"{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}.{ts.Milliseconds:000}";
|
||||
}
|
||||
|
||||
private static string ShiftTime(string xmlSrc, long segTimeMs, int index)
|
||||
if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc;
|
||||
var xmlDoc = new XmlDocument();
|
||||
XmlNamespaceManager? nsMgr = null;
|
||||
xmlDoc.LoadXml(xmlSrc);
|
||||
var ttNode = xmlDoc.LastChild;
|
||||
if (nsMgr == null)
|
||||
{
|
||||
string Add(string xmlTime)
|
||||
{
|
||||
var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
|
||||
var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index);
|
||||
return string.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
|
||||
}
|
||||
var ns = ((XmlElement)ttNode!).GetAttribute("xmlns");
|
||||
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
|
||||
nsMgr.AddNamespace("ns", ns);
|
||||
}
|
||||
|
||||
if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc;
|
||||
var xmlDoc = new XmlDocument();
|
||||
XmlNamespaceManager? nsMgr = null;
|
||||
xmlDoc.LoadXml(xmlSrc);
|
||||
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
|
||||
if (bodyNode == null)
|
||||
return xmlSrc;
|
||||
|
||||
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
|
||||
// Parse <p> label
|
||||
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
|
||||
{
|
||||
var _begin = _p.GetAttribute("begin");
|
||||
var _end = _p.GetAttribute("end");
|
||||
// Handle namespace
|
||||
foreach (XmlAttribute attr in _p.Attributes)
|
||||
{
|
||||
if (attr.LocalName == "begin") _begin = attr.Value;
|
||||
else if (attr.LocalName == "end") _end = attr.Value;
|
||||
}
|
||||
_p.SetAttribute("begin", Add(_begin));
|
||||
_p.SetAttribute("end", Add(_end));
|
||||
// Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
|
||||
// Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
|
||||
}
|
||||
|
||||
return xmlDoc.OuterXml;
|
||||
}
|
||||
|
||||
private static string GetTextFromElement(XmlElement node)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (XmlNode item in node.ChildNodes)
|
||||
{
|
||||
if (item.NodeType == XmlNodeType.Text)
|
||||
{
|
||||
sb.Append(item.InnerText.Trim());
|
||||
}
|
||||
else if(item is { NodeType: XmlNodeType.Element, Name: "br" })
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static List<string> SplitMultipleRootElements(string xml)
|
||||
{
|
||||
return !MultiElementsFixRegex().IsMatch(xml) ? [] : MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList();
|
||||
}
|
||||
|
||||
public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
return ExtractFromMp4s([item], segTimeMs, baseTimestamp);
|
||||
}
|
||||
|
||||
private static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
// read ttmls
|
||||
List<string> xmls = [];
|
||||
int segIndex = 0;
|
||||
foreach (var item in items)
|
||||
{
|
||||
var dataSeg = File.ReadAllBytes(item);
|
||||
|
||||
var sawMDAT = false;
|
||||
// parse media
|
||||
new MP4Parser()
|
||||
.Box("mdat", MP4Parser.AllData(data =>
|
||||
{
|
||||
sawMDAT = true;
|
||||
// Join this to any previous payload, in case the mp4 has multiple
|
||||
// mdats.
|
||||
if (segTimeMs != 0)
|
||||
{
|
||||
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
||||
foreach (var item in datas)
|
||||
{
|
||||
xmls.Add(ShiftTime(item, segTimeMs, segIndex));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
||||
xmls.AddRange(datas);
|
||||
}
|
||||
}))
|
||||
.Parse(dataSeg,/* partialOkay= */ false);
|
||||
segIndex++;
|
||||
}
|
||||
|
||||
return ExtractSub(xmls, baseTimestamp);
|
||||
}
|
||||
|
||||
public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
return ExtractFromTTMLs([item], segTimeMs, baseTimestamp);
|
||||
}
|
||||
|
||||
public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
// read ttmls
|
||||
List<string> xmls = [];
|
||||
int segIndex = 0;
|
||||
foreach (var item in items)
|
||||
{
|
||||
var xml = File.ReadAllText(item);
|
||||
xmls.Add(segTimeMs != 0 ? ShiftTime(xml, segTimeMs, segIndex) : xml);
|
||||
segIndex++;
|
||||
}
|
||||
|
||||
return ExtractSub(xmls, baseTimestamp);
|
||||
}
|
||||
|
||||
private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp)
|
||||
{
|
||||
// parsing
|
||||
var xmlDoc = new XmlDocument();
|
||||
var finalSubs = new List<SubEntity>();
|
||||
XmlNode? headNode = null;
|
||||
XmlNamespaceManager? nsMgr = null;
|
||||
var regex = LabelFixRegex();
|
||||
var attrRegex = AttrRegex();
|
||||
foreach (var item in xmls)
|
||||
{
|
||||
var xmlContent = item;
|
||||
if (!xmlContent.Contains("<tt")) continue;
|
||||
|
||||
// fix non-standard xml
|
||||
var xmlContentFix = xmlContent;
|
||||
if (regex.IsMatch(xmlContent))
|
||||
{
|
||||
foreach (Match m in regex.Matches(xmlContentFix))
|
||||
{
|
||||
try
|
||||
{
|
||||
var inner = m.Groups[1].Value;
|
||||
if (attrRegex.IsMatch(inner))
|
||||
{
|
||||
inner = attrRegex.Replace(inner, "");
|
||||
}
|
||||
new XmlDocument().LoadXml($"<p>{inner}</p>");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
xmlContentFix = xmlContentFix.Replace(m.Groups[1].Value, System.Web.HttpUtility.HtmlEncode(m.Groups[1].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
xmlDoc.LoadXml(xmlContentFix);
|
||||
var ttNode = xmlDoc.LastChild;
|
||||
if (nsMgr == null)
|
||||
{
|
||||
|
@ -79,306 +233,142 @@ namespace Mp4SubtitleParser
|
|||
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
|
||||
nsMgr.AddNamespace("ns", ns);
|
||||
}
|
||||
if (headNode == null)
|
||||
headNode = ttNode!.SelectSingleNode("ns:head", nsMgr);
|
||||
|
||||
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
|
||||
if (bodyNode == null)
|
||||
return xmlSrc;
|
||||
continue;
|
||||
|
||||
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
|
||||
if (_div == null)
|
||||
continue;
|
||||
|
||||
|
||||
// PNG Subs
|
||||
var imageDic = new Dictionary<string, string>(); // id, Base64
|
||||
if (ImageRegex().IsMatch(xmlDoc.InnerXml))
|
||||
{
|
||||
foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))
|
||||
{
|
||||
imageDic.Add(img.Groups[1].Value.Trim(), img.Groups[2].Value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
// convert <div> to <p>
|
||||
if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0)
|
||||
{
|
||||
foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!)
|
||||
{
|
||||
var _p = xmlDoc.CreateDocumentFragment();
|
||||
_p.InnerXml = _tDiv.OuterXml.Replace("<div ", "<p ").Replace("</div>", "</p>");
|
||||
_div.AppendChild(_p);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse <p> label
|
||||
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
|
||||
{
|
||||
var _begin = _p.GetAttribute("begin");
|
||||
var _end = _p.GetAttribute("end");
|
||||
var _region = _p.GetAttribute("region");
|
||||
var _bgImg = _p.GetAttribute("smpte:backgroundImage");
|
||||
// Handle namespace
|
||||
foreach (XmlAttribute attr in _p.Attributes)
|
||||
{
|
||||
if (attr.LocalName == "begin") _begin = attr.Value;
|
||||
else if (attr.LocalName == "end") _end = attr.Value;
|
||||
else if (attr.LocalName == "region") _region = attr.Value;
|
||||
}
|
||||
_p.SetAttribute("begin", Add(_begin));
|
||||
_p.SetAttribute("end", Add(_end));
|
||||
// Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
|
||||
// Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
|
||||
}
|
||||
|
||||
return xmlDoc.OuterXml;
|
||||
}
|
||||
|
||||
private static string GetTextFromElement(XmlElement node)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (XmlNode item in node.ChildNodes)
|
||||
{
|
||||
if (item.NodeType == XmlNodeType.Text)
|
||||
var sub = new SubEntity
|
||||
{
|
||||
sb.Append(item.InnerText.Trim());
|
||||
}
|
||||
else if(item.NodeType == XmlNodeType.Element && item.Name == "br")
|
||||
Begin = _begin,
|
||||
End = _end,
|
||||
Region = _region
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(_bgImg))
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static List<string> SplitMultipleRootElements(string xml)
|
||||
{
|
||||
if (!MultiElementsFixRegex().IsMatch(xml)) return new List<string>();
|
||||
return MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList();
|
||||
}
|
||||
|
||||
public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
return ExtractFromMp4s(new string[] { item }, segTimeMs, baseTimestamp);
|
||||
}
|
||||
|
||||
public static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
// read ttmls
|
||||
List<string> xmls = new List<string>();
|
||||
int segIndex = 0;
|
||||
foreach (var item in items)
|
||||
{
|
||||
var dataSeg = File.ReadAllBytes(item);
|
||||
|
||||
var sawMDAT = false;
|
||||
// parse media
|
||||
new MP4Parser()
|
||||
.Box("mdat", MP4Parser.AllData((data) =>
|
||||
var _spans = _p.ChildNodes;
|
||||
// Collect <span>
|
||||
foreach (XmlNode _node in _spans)
|
||||
{
|
||||
sawMDAT = true;
|
||||
// Join this to any previous payload, in case the mp4 has multiple
|
||||
// mdats.
|
||||
if (segTimeMs != 0)
|
||||
if (_node.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
||||
foreach (var item in datas)
|
||||
{
|
||||
xmls.Add(ShiftTime(item, segTimeMs, segIndex));
|
||||
}
|
||||
var _span = (XmlElement)_node;
|
||||
if (string.IsNullOrEmpty(_span.InnerText))
|
||||
continue;
|
||||
sub.Contents.Add(_span);
|
||||
sub.ContentStrings.Add(_span.OuterXml);
|
||||
}
|
||||
else
|
||||
{
|
||||
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
||||
xmls.AddRange(datas);
|
||||
}
|
||||
}))
|
||||
.Parse(dataSeg,/* partialOkay= */ false);
|
||||
segIndex++;
|
||||
}
|
||||
|
||||
return ExtractSub(xmls, baseTimestamp);
|
||||
}
|
||||
|
||||
public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
return ExtractFromTTMLs(new string[] { item }, segTimeMs, baseTimestamp);
|
||||
}
|
||||
|
||||
public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
||||
{
|
||||
// read ttmls
|
||||
List<string> xmls = new List<string>();
|
||||
int segIndex = 0;
|
||||
foreach (var item in items)
|
||||
{
|
||||
var xml = File.ReadAllText(item);
|
||||
if (segTimeMs != 0)
|
||||
{
|
||||
xmls.Add(ShiftTime(xml, segTimeMs, segIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
xmls.Add(xml);
|
||||
}
|
||||
segIndex++;
|
||||
}
|
||||
|
||||
return ExtractSub(xmls, baseTimestamp);
|
||||
}
|
||||
|
||||
private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp)
|
||||
{
|
||||
// parsing
|
||||
var xmlDoc = new XmlDocument();
|
||||
var finalSubs = new List<SubEntity>();
|
||||
XmlNode? headNode = null;
|
||||
XmlNamespaceManager? nsMgr = null;
|
||||
var regex = LabelFixRegex();
|
||||
var attrRegex = AttrRegex();
|
||||
foreach (var item in xmls)
|
||||
{
|
||||
var xmlContent = item;
|
||||
if (!xmlContent.Contains("<tt")) continue;
|
||||
|
||||
// fix non-standard xml
|
||||
var xmlContentFix = xmlContent;
|
||||
if (regex.IsMatch(xmlContent))
|
||||
{
|
||||
foreach (Match m in regex.Matches(xmlContentFix))
|
||||
{
|
||||
try
|
||||
{
|
||||
var inner = m.Groups[1].Value;
|
||||
if (attrRegex.IsMatch(inner))
|
||||
{
|
||||
inner = attrRegex.Replace(inner, "");
|
||||
}
|
||||
new XmlDocument().LoadXml($"<p>{inner}</p>");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
xmlContentFix = xmlContentFix.Replace(m.Groups[1].Value, System.Web.HttpUtility.HtmlEncode(m.Groups[1].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
xmlDoc.LoadXml(xmlContentFix);
|
||||
var ttNode = xmlDoc.LastChild;
|
||||
if (nsMgr == null)
|
||||
{
|
||||
var ns = ((XmlElement)ttNode!).GetAttribute("xmlns");
|
||||
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
|
||||
nsMgr.AddNamespace("ns", ns);
|
||||
}
|
||||
if (headNode == null)
|
||||
headNode = ttNode!.SelectSingleNode("ns:head", nsMgr);
|
||||
|
||||
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
|
||||
if (bodyNode == null)
|
||||
continue;
|
||||
|
||||
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
|
||||
if (_div == null)
|
||||
continue;
|
||||
|
||||
|
||||
// PNG Subs
|
||||
var imageDic = new Dictionary<string, string>(); // id, Base64
|
||||
if (ImageRegex().IsMatch(xmlDoc.InnerXml))
|
||||
{
|
||||
foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))
|
||||
{
|
||||
imageDic.Add(img.Groups[1].Value.Trim(), img.Groups[2].Value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
// convert <div> to <p>
|
||||
if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0)
|
||||
{
|
||||
foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!)
|
||||
{
|
||||
var _p = xmlDoc.CreateDocumentFragment();
|
||||
_p.InnerXml = _tDiv.OuterXml.Replace("<div ", "<p ").Replace("</div>", "</p>");
|
||||
_div.AppendChild(_p);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse <p> label
|
||||
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
|
||||
{
|
||||
var _begin = _p.GetAttribute("begin");
|
||||
var _end = _p.GetAttribute("end");
|
||||
var _region = _p.GetAttribute("region");
|
||||
var _bgImg = _p.GetAttribute("smpte:backgroundImage");
|
||||
// Handle namespace
|
||||
foreach (XmlAttribute attr in _p.Attributes)
|
||||
{
|
||||
if (attr.LocalName == "begin") _begin = attr.Value;
|
||||
else if (attr.LocalName == "end") _end = attr.Value;
|
||||
else if (attr.LocalName == "region") _region = attr.Value;
|
||||
}
|
||||
var sub = new SubEntity
|
||||
{
|
||||
Begin = _begin,
|
||||
End = _end,
|
||||
Region = _region
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(_bgImg))
|
||||
{
|
||||
var _spans = _p.ChildNodes;
|
||||
// Collect <span>
|
||||
foreach (XmlNode _node in _spans)
|
||||
{
|
||||
if (_node.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
var _span = (XmlElement)_node;
|
||||
if (string.IsNullOrEmpty(_span.InnerText))
|
||||
continue;
|
||||
sub.Contents.Add(_span);
|
||||
sub.ContentStrings.Add(_span.OuterXml);
|
||||
}
|
||||
else if (_node.NodeType == XmlNodeType.Text)
|
||||
{
|
||||
var _span = new XmlDocument().CreateElement("span");
|
||||
_span.InnerText = _node.Value!;
|
||||
sub.Contents.Add(_span);
|
||||
sub.ContentStrings.Add(_span.OuterXml);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var id = _bgImg.Replace("#", "");
|
||||
if (imageDic.ContainsKey(id))
|
||||
else if (_node.NodeType == XmlNodeType.Text)
|
||||
{
|
||||
var _span = new XmlDocument().CreateElement("span");
|
||||
_span.InnerText = $"Base64::{imageDic[id]}";
|
||||
_span.InnerText = _node.Value!;
|
||||
sub.Contents.Add(_span);
|
||||
sub.ContentStrings.Add(_span.OuterXml);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if one <p> has been splitted
|
||||
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
|
||||
// Skip empty lines
|
||||
if (sub.ContentStrings.Count > 0)
|
||||
{
|
||||
// Extend <p> duration
|
||||
if (index != -1)
|
||||
finalSubs[index].End = sub.End;
|
||||
else if (!finalSubs.Contains(sub))
|
||||
finalSubs.Add(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var dic = new Dictionary<string, string>();
|
||||
foreach (var sub in finalSubs)
|
||||
{
|
||||
var key = $"{sub.Begin} --> {sub.End}";
|
||||
foreach (var item in sub.Contents)
|
||||
else
|
||||
{
|
||||
if (dic.ContainsKey(key))
|
||||
var id = _bgImg.Replace("#", "");
|
||||
if (imageDic.TryGetValue(id, out var value))
|
||||
{
|
||||
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
||||
dic[key] = $"{dic[key]}\r\n<i>{GetTextFromElement(item)}</i>";
|
||||
else
|
||||
dic[key] = $"{dic[key]}\r\n{GetTextFromElement(item)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
||||
dic.Add(key, $"<i>{GetTextFromElement(item)}</i>");
|
||||
else
|
||||
dic.Add(key, GetTextFromElement(item));
|
||||
var _span = new XmlDocument().CreateElement("span");
|
||||
_span.InnerText = $"Base64::{value}";
|
||||
sub.Contents.Add(_span);
|
||||
sub.ContentStrings.Add(_span.OuterXml);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if one <p> has been splitted
|
||||
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
|
||||
// Skip empty lines
|
||||
if (sub.ContentStrings.Count <= 0)
|
||||
continue;
|
||||
// Extend <p> duration
|
||||
if (index != -1)
|
||||
finalSubs[index].End = sub.End;
|
||||
else if (!finalSubs.Contains(sub))
|
||||
finalSubs.Add(sub);
|
||||
}
|
||||
|
||||
|
||||
var vtt = new StringBuilder();
|
||||
vtt.AppendLine("WEBVTT");
|
||||
foreach (var item in dic)
|
||||
{
|
||||
vtt.AppendLine(item.Key);
|
||||
vtt.AppendLine(item.Value);
|
||||
vtt.AppendLine();
|
||||
}
|
||||
|
||||
return WebVttSub.Parse(vtt.ToString(), baseTimestamp);
|
||||
}
|
||||
|
||||
|
||||
var dic = new Dictionary<string, string>();
|
||||
foreach (var sub in finalSubs)
|
||||
{
|
||||
var key = $"{sub.Begin} --> {sub.End}";
|
||||
foreach (var item in sub.Contents)
|
||||
{
|
||||
if (dic.ContainsKey(key))
|
||||
{
|
||||
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
||||
dic[key] = $"{dic[key]}\r\n<i>{GetTextFromElement(item)}</i>";
|
||||
else
|
||||
dic[key] = $"{dic[key]}\r\n{GetTextFromElement(item)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
||||
dic.Add(key, $"<i>{GetTextFromElement(item)}</i>");
|
||||
else
|
||||
dic.Add(key, GetTextFromElement(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var vtt = new StringBuilder();
|
||||
vtt.AppendLine("WEBVTT");
|
||||
foreach (var item in dic)
|
||||
{
|
||||
vtt.AppendLine(item.Key);
|
||||
vtt.AppendLine(item.Value);
|
||||
vtt.AppendLine();
|
||||
}
|
||||
|
||||
return WebVttSub.Parse(vtt.ToString(), baseTimestamp);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ using System.Text;
|
|||
|
||||
namespace Mp4SubtitleParser;
|
||||
|
||||
public class MP4VttUtil
|
||||
public static class MP4VttUtil
|
||||
{
|
||||
public static (bool, uint) CheckInit(byte[] data)
|
||||
{
|
||||
|
@ -15,16 +15,16 @@ public class MP4VttUtil
|
|||
.Box("moov", MP4Parser.Children)
|
||||
.Box("trak", MP4Parser.Children)
|
||||
.Box("mdia", MP4Parser.Children)
|
||||
.FullBox("mdhd", (box) =>
|
||||
.FullBox("mdhd", box =>
|
||||
{
|
||||
if (!(box.Version == 0 || box.Version == 1))
|
||||
if (box.Version is not (0 or 1))
|
||||
throw new Exception("MDHD version can only be 0 or 1");
|
||||
timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
|
||||
})
|
||||
.Box("minf", MP4Parser.Children)
|
||||
.Box("stbl", MP4Parser.Children)
|
||||
.FullBox("stsd", MP4Parser.SampleDescription)
|
||||
.Box("wvtt", (box) => {
|
||||
.Box("wvtt", _ => {
|
||||
// A valid vtt init segment, though we have no actual subtitles yet.
|
||||
sawWVTT = true;
|
||||
})
|
||||
|
@ -38,7 +38,7 @@ public class MP4VttUtil
|
|||
if (timescale == 0)
|
||||
throw new Exception("Missing timescale for VTT content!");
|
||||
|
||||
List<SubCue> cues = new();
|
||||
List<SubCue> cues = [];
|
||||
|
||||
foreach (var item in files)
|
||||
{
|
||||
|
@ -50,27 +50,27 @@ public class MP4VttUtil
|
|||
byte[]? rawPayload = null;
|
||||
ulong baseTime = 0;
|
||||
ulong defaultDuration = 0;
|
||||
List<Sample> presentations = new();
|
||||
List<Sample> presentations = [];
|
||||
|
||||
|
||||
// parse media
|
||||
new MP4Parser()
|
||||
.Box("moof", MP4Parser.Children)
|
||||
.Box("traf", MP4Parser.Children)
|
||||
.FullBox("tfdt", (box) =>
|
||||
.FullBox("tfdt", box =>
|
||||
{
|
||||
sawTFDT = true;
|
||||
if (!(box.Version == 0 || box.Version == 1))
|
||||
if (box.Version is not (0 or 1))
|
||||
throw new Exception("TFDT version can only be 0 or 1");
|
||||
baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
|
||||
})
|
||||
.FullBox("tfhd", (box) =>
|
||||
.FullBox("tfhd", box =>
|
||||
{
|
||||
if (box.Flags == 1000)
|
||||
throw new Exception("A TFHD box should have a valid flags value");
|
||||
defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
|
||||
})
|
||||
.FullBox("trun", (box) =>
|
||||
.FullBox("trun", box =>
|
||||
{
|
||||
sawTRUN = true;
|
||||
if (box.Version == 1000)
|
||||
|
@ -79,7 +79,7 @@ public class MP4VttUtil
|
|||
throw new Exception("A TRUN box should have a valid flags value");
|
||||
presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
|
||||
})
|
||||
.Box("mdat", MP4Parser.AllData((data) =>
|
||||
.Box("mdat", MP4Parser.AllData(data =>
|
||||
{
|
||||
if (sawMDAT)
|
||||
throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported");
|
||||
|
@ -139,8 +139,6 @@ public class MP4VttUtil
|
|||
{
|
||||
if (payload != null)
|
||||
{
|
||||
if (timescale == 0)
|
||||
throw new Exception("Timescale should not be zero!");
|
||||
var cue = ParseVTTC(
|
||||
payload,
|
||||
0 + (double)startTime / timescale,
|
||||
|
@ -192,15 +190,15 @@ public class MP4VttUtil
|
|||
string id = string.Empty;
|
||||
string settings = string.Empty;
|
||||
new MP4Parser()
|
||||
.Box("payl", MP4Parser.AllData((data) =>
|
||||
.Box("payl", MP4Parser.AllData(data =>
|
||||
{
|
||||
payload = Encoding.UTF8.GetString(data);
|
||||
}))
|
||||
.Box("iden", MP4Parser.AllData((data) =>
|
||||
.Box("iden", MP4Parser.AllData(data =>
|
||||
{
|
||||
id = Encoding.UTF8.GetString(data);
|
||||
}))
|
||||
.Box("sttg", MP4Parser.AllData((data) =>
|
||||
.Box("sttg", MP4Parser.AllData(data =>
|
||||
{
|
||||
settings = Encoding.UTF8.GetString(data);
|
||||
}))
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace N_m3u8DL_RE.Parser.Mp4;
|
|||
|
||||
public partial class MSSMoovProcessor
|
||||
{
|
||||
[GeneratedRegex("\\<KID\\>(.*?)\\<")]
|
||||
[GeneratedRegex(@"\<KID\>(.*?)\<")]
|
||||
private static partial Regex KIDRegex();
|
||||
|
||||
private static string StartCode = "00000001";
|
||||
|
@ -23,7 +23,7 @@ public partial class MSSMoovProcessor
|
|||
private string CodecPrivateData;
|
||||
private int Timesacle;
|
||||
private long Duration;
|
||||
private string Language { get => StreamSpec.Language ?? "und"; }
|
||||
private string Language => StreamSpec.Language ?? "und";
|
||||
private int Width => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First());
|
||||
private int Height => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last());
|
||||
private string StreamType;
|
||||
|
@ -36,8 +36,8 @@ public partial class MSSMoovProcessor
|
|||
private bool IsProtection;
|
||||
private string ProtectionSystemId;
|
||||
private string ProtectionData;
|
||||
private string ProtecitonKID;
|
||||
private string ProtecitonKID_PR;
|
||||
private string? ProtecitonKID;
|
||||
private string? ProtecitonKID_PR;
|
||||
private byte[] UnityMatrix
|
||||
{
|
||||
get
|
||||
|
@ -60,10 +60,9 @@ public partial class MSSMoovProcessor
|
|||
private static byte TRACK_IN_MOVIE = 0x2;
|
||||
private static byte TRACK_IN_PREVIEW = 0x4;
|
||||
private static byte SELF_CONTAINED = 0x1;
|
||||
private static List<string> SupportedFourCC = new()
|
||||
{
|
||||
"HVC1","HEV1","AACL","AACH","EC-3","H264","AVC1","DAVC","AVC1","TTML","DVHE","DVH1"
|
||||
};
|
||||
|
||||
private static List<string> SupportedFourCC =
|
||||
["HVC1", "HEV1", "AACL", "AACH", "EC-3", "H264", "AVC1", "DAVC", "AVC1", "TTML", "DVHE", "DVH1"];
|
||||
|
||||
public MSSMoovProcessor(StreamSpec streamSpec)
|
||||
{
|
||||
|
@ -95,7 +94,7 @@ public partial class MSSMoovProcessor
|
|||
}
|
||||
}
|
||||
|
||||
private static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = new string[] { "", "A", "B", "C" };
|
||||
private static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = ["", "A", "B", "C"];
|
||||
private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch
|
||||
{
|
||||
96000 => 0x0,
|
||||
|
@ -169,8 +168,8 @@ public partial class MSSMoovProcessor
|
|||
// save kid for playready
|
||||
this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes);
|
||||
// fix byte order
|
||||
var reverse1 = new byte[4] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] };
|
||||
var reverse2 = new byte[4] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] };
|
||||
var reverse1 = new[] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] };
|
||||
var reverse2 = new[] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] };
|
||||
Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length);
|
||||
Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length);
|
||||
this.ProtecitonKID = HexUtil.BytesToHex(kidBytes);
|
||||
|
@ -217,13 +216,13 @@ public partial class MSSMoovProcessor
|
|||
|
||||
var schmPayload = new List<byte>();
|
||||
schmPayload.AddRange(Encoding.ASCII.GetBytes("cenc")); // scheme_type 'cenc' => common encryption
|
||||
schmPayload.AddRange(new byte[] { 0, 1, 0, 0 }); // scheme_version Major version 1, Minor version 0
|
||||
schmPayload.AddRange([0, 1, 0, 0]); // scheme_version Major version 1, Minor version 0
|
||||
var schmBox = FullBox("schm", 0, 0, schmPayload.ToArray());
|
||||
|
||||
sinfPayload.AddRange(schmBox);
|
||||
|
||||
var tencPayload = new List<byte>();
|
||||
tencPayload.AddRange(new byte[] { 0, 0 });
|
||||
tencPayload.AddRange([0, 0]);
|
||||
tencPayload.Add(0x1); // default_IsProtected
|
||||
tencPayload.Add(0x8); // default_Per_Sample_IV_size
|
||||
tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); // default_KID
|
||||
|
@ -368,7 +367,7 @@ public partial class MSSMoovProcessor
|
|||
}
|
||||
else if (StreamType == "text")
|
||||
{
|
||||
minfPayload.AddRange(FullBox("sthd", 0, 0, new byte[0])); // Subtitle Media Header
|
||||
minfPayload.AddRange(FullBox("sthd", 0, 0, [])); // Subtitle Media Header
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -377,7 +376,7 @@ public partial class MSSMoovProcessor
|
|||
|
||||
var drefPayload = new List<byte>();
|
||||
drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); // entry count
|
||||
drefPayload.AddRange(FullBox("url ", 0, SELF_CONTAINED, new byte[0])); // Data Entry URL Box
|
||||
drefPayload.AddRange(FullBox("url ", 0, SELF_CONTAINED, [])); // Data Entry URL Box
|
||||
|
||||
var dinfPayload = FullBox("dref", 0, 0, drefPayload.ToArray()); // Data Reference Box
|
||||
minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); // Data Information Box
|
||||
|
@ -466,10 +465,7 @@ public partial class MSSMoovProcessor
|
|||
writer.Write(sinfBox);
|
||||
return Box("enca", stream.ToArray()); // Encrypted Audio
|
||||
}
|
||||
else
|
||||
{
|
||||
return Box("mp4a", stream.ToArray());
|
||||
}
|
||||
return Box("mp4a", stream.ToArray());
|
||||
}
|
||||
if (FourCC == "EC-3")
|
||||
{
|
||||
|
@ -479,10 +475,7 @@ public partial class MSSMoovProcessor
|
|||
writer.Write(sinfBox);
|
||||
return Box("enca", stream.ToArray()); // Encrypted Audio
|
||||
}
|
||||
else
|
||||
{
|
||||
return Box("ec-3", stream.ToArray());
|
||||
}
|
||||
return Box("ec-3", stream.ToArray());
|
||||
}
|
||||
}
|
||||
else if (StreamType == "video")
|
||||
|
@ -507,11 +500,11 @@ public partial class MSSMoovProcessor
|
|||
|
||||
var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData);
|
||||
|
||||
if (FourCC == "H264" || FourCC == "AVC1" || FourCC == "DAVC" || FourCC == "AVC1")
|
||||
if (FourCC is "H264" or "AVC1" or "DAVC")
|
||||
{
|
||||
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7).First());
|
||||
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8).First());
|
||||
var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
|
||||
var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7));
|
||||
var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8));
|
||||
// make avcC
|
||||
var avcC = GetAvcC(sps, pps);
|
||||
writer.Write(avcC);
|
||||
|
@ -521,17 +514,14 @@ public partial class MSSMoovProcessor
|
|||
writer.Write(sinfBox);
|
||||
return Box("encv", stream.ToArray()); // Encrypted Video
|
||||
}
|
||||
else
|
||||
{
|
||||
return Box("avc1", stream.ToArray()); // AVC Simple Entry
|
||||
}
|
||||
return Box("avc1", stream.ToArray()); // AVC Simple Entry
|
||||
}
|
||||
else if (FourCC == "HVC1" || FourCC == "HEV1")
|
||||
if (FourCC is "HVC1" or "HEV1")
|
||||
{
|
||||
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First());
|
||||
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First());
|
||||
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First());
|
||||
var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
|
||||
var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));
|
||||
var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));
|
||||
var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));
|
||||
// make hvcC
|
||||
var hvcC = GetHvcC(sps, pps, vps);
|
||||
writer.Write(hvcC);
|
||||
|
@ -541,18 +531,15 @@ public partial class MSSMoovProcessor
|
|||
writer.Write(sinfBox);
|
||||
return Box("encv", stream.ToArray()); // Encrypted Video
|
||||
}
|
||||
else
|
||||
{
|
||||
return Box("hvc1", stream.ToArray()); // HEVC Simple Entry
|
||||
}
|
||||
return Box("hvc1", stream.ToArray()); // HEVC Simple Entry
|
||||
}
|
||||
// 杜比视界也按照hevc处理
|
||||
else if (FourCC == "DVHE" || FourCC == "DVH1")
|
||||
if (FourCC is "DVHE" or "DVH1")
|
||||
{
|
||||
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First());
|
||||
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First());
|
||||
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First());
|
||||
var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
|
||||
var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));
|
||||
var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));
|
||||
var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));
|
||||
// make hvcC
|
||||
var hvcC = GetHvcC(sps, pps, vps, "dvh1");
|
||||
writer.Write(hvcC);
|
||||
|
@ -562,15 +549,10 @@ public partial class MSSMoovProcessor
|
|||
writer.Write(sinfBox);
|
||||
return Box("encv", stream.ToArray()); // Encrypted Video
|
||||
}
|
||||
else
|
||||
{
|
||||
return Box("dvh1", stream.ToArray()); // HEVC Simple Entry
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
return Box("dvh1", stream.ToArray()); // HEVC Simple Entry
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else if (StreamType == "text")
|
||||
{
|
||||
|
@ -581,10 +563,7 @@ public partial class MSSMoovProcessor
|
|||
writer.Write("\0"); // auxilary mime types(??)
|
||||
return Box("stpp", stream.ToArray()); // TTML Simple Entry
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -635,7 +614,7 @@ public partial class MSSMoovProcessor
|
|||
while (_reader.BaseStream.Position < _reader.BaseStream.Length)
|
||||
{
|
||||
encList.Add(_reader.ReadByte());
|
||||
if (encList.Count >= 3 && encList[encList.Count - 3] == 0x00 && encList[encList.Count - 2] == 0x00 && encList[encList.Count - 1] == 0x03)
|
||||
if (encList is [.., 0x00, 0x00, 0x03])
|
||||
{
|
||||
encList.RemoveAt(encList.Count - 1);
|
||||
}
|
||||
|
@ -805,7 +784,7 @@ public partial class MSSMoovProcessor
|
|||
new MP4Parser()
|
||||
.Box("moof", MP4Parser.Children)
|
||||
.Box("traf", MP4Parser.Children)
|
||||
.FullBox("tfhd", (box) =>
|
||||
.FullBox("tfhd", box =>
|
||||
{
|
||||
TrackId = (int)box.Reader.ReadUInt32();
|
||||
})
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using N_m3u8DL_RE.Parser.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Processor;
|
||||
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using N_m3u8DL_RE.Common.Log;
|
||||
using N_m3u8DL_RE.Parser.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Processor.DASH;
|
||||
|
||||
|
@ -18,11 +13,7 @@ public class DefaultDASHContentProcessor : ContentProcessor
|
|||
{
|
||||
if (extractorType != ExtractorType.MPEG_DASH) return false;
|
||||
|
||||
if (mpdContent.Contains("<mas:") && !mpdContent.Contains("xmlns:mas"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return mpdContent.Contains("<mas:") && !mpdContent.Contains("xmlns:mas");
|
||||
}
|
||||
|
||||
public override string Process(string mpdContent, ParserConfig parserConfig)
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using N_m3u8DL_RE.Common.Log;
|
||||
using N_m3u8DL_RE.Parser.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Processor;
|
||||
|
@ -17,29 +11,27 @@ public class DefaultUrlProcessor : UrlProcessor
|
|||
|
||||
public override string Process(string oriUrl, ParserConfig paserConfig)
|
||||
{
|
||||
if (oriUrl.StartsWith("http"))
|
||||
if (!oriUrl.StartsWith("http")) return oriUrl;
|
||||
|
||||
var uriFromConfig = new Uri(paserConfig.Url);
|
||||
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
|
||||
|
||||
var oldUri = new Uri(oriUrl);
|
||||
var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
|
||||
foreach (var item in uriFromConfigQuery.AllKeys)
|
||||
{
|
||||
var uriFromConfig = new Uri(paserConfig.Url);
|
||||
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
|
||||
|
||||
var oldUri = new Uri(oriUrl);
|
||||
var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
|
||||
foreach (var item in uriFromConfigQuery.AllKeys)
|
||||
{
|
||||
if (newQuery.AllKeys.Contains(item))
|
||||
newQuery.Set(item, uriFromConfigQuery.Get(item));
|
||||
else
|
||||
newQuery.Add(item, uriFromConfigQuery.Get(item));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(newQuery.ToString()))
|
||||
{
|
||||
Logger.Debug("Before: " + oriUrl);
|
||||
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery.ToString()).TrimEnd('?');
|
||||
Logger.Debug("After: " + oriUrl);
|
||||
}
|
||||
if (newQuery.AllKeys.Contains(item))
|
||||
newQuery.Set(item, uriFromConfigQuery.Get(item));
|
||||
else
|
||||
newQuery.Add(item, uriFromConfigQuery.Get(item));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(newQuery.ToString())) return oriUrl;
|
||||
|
||||
Logger.Debug("Before: " + oriUrl);
|
||||
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery).TrimEnd('?');
|
||||
Logger.Debug("After: " + oriUrl);
|
||||
|
||||
return oriUrl;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,7 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using N_m3u8DL_RE.Parser.Config;
|
||||
using N_m3u8DL_RE.Parser.Constants;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Processor.HLS;
|
||||
|
||||
|
@ -16,13 +11,13 @@ public partial class DefaultHLSContentProcessor : ContentProcessor
|
|||
private static partial Regex YkDVRegex();
|
||||
[GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
|
||||
private static partial Regex DNSPRegex();
|
||||
[GeneratedRegex("#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY")]
|
||||
[GeneratedRegex(@"#EXTINF:.*?,\s+.*BUMPER.*\s+?#EXT-X-DISCONTINUITY")]
|
||||
private static partial Regex DNSPSubRegex();
|
||||
[GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
|
||||
private static partial Regex OrderFixRegex();
|
||||
[GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")]
|
||||
[GeneratedRegex(@"#EXT-X-MAP.*\.apple\.com/")]
|
||||
private static partial Regex ATVRegex();
|
||||
[GeneratedRegex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
|
||||
[GeneratedRegex(@"(#EXT-X-KEY:[\s\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
|
||||
private static partial Regex ATVRegex2();
|
||||
|
||||
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
|
||||
|
@ -30,7 +25,7 @@ public partial class DefaultHLSContentProcessor : ContentProcessor
|
|||
public override string Process(string m3u8Content, ParserConfig parserConfig)
|
||||
{
|
||||
// 处理content以\r作为换行符的情况
|
||||
if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n"))
|
||||
if (m3u8Content.Contains('\r') && !m3u8Content.Contains('\n'))
|
||||
{
|
||||
m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
|
||||
}
|
||||
|
@ -51,7 +46,7 @@ public partial class DefaultHLSContentProcessor : ContentProcessor
|
|||
// 针对YK #EXT-X-VERSION:7杜比视界片源修正
|
||||
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
|
||||
{
|
||||
Regex ykmap = YkDVRegex();
|
||||
var ykmap = YkDVRegex();
|
||||
foreach (Match m in ykmap.Matches(m3u8Content))
|
||||
{
|
||||
m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
|
||||
|
|
|
@ -83,12 +83,11 @@ public class DefaultHLSKeyProcessor : KeyProcessor
|
|||
encryptInfo.Method = EncryptMethod.UNKNOWN;
|
||||
}
|
||||
|
||||
if (parserConfig.CustomMethod == null) return encryptInfo;
|
||||
|
||||
// 处理自定义加密方式
|
||||
if (parserConfig.CustomMethod != null)
|
||||
{
|
||||
encryptInfo.Method = parserConfig.CustomMethod.Value;
|
||||
Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
|
||||
}
|
||||
encryptInfo.Method = parserConfig.CustomMethod.Value;
|
||||
Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
|
||||
|
||||
return encryptInfo;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
using N_m3u8DL_RE.Common.Entity;
|
||||
using N_m3u8DL_RE.Common.Enum;
|
||||
using N_m3u8DL_RE.Parser.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Processor;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using N_m3u8DL_RE.Parser.Config;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using N_m3u8DL_RE.Parser.Config;
|
||||
using N_m3u8DL_RE.Common.Entity;
|
||||
using N_m3u8DL_RE.Common.Log;
|
||||
using N_m3u8DL_RE.Common.Resource;
|
||||
|
@ -19,11 +20,6 @@ public class StreamExtractor
|
|||
|
||||
public Dictionary<string, string> RawFiles { get; set; } = new(); // 存储(文件名,文件内容)
|
||||
|
||||
public StreamExtractor()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public StreamExtractor(ParserConfig parserConfig)
|
||||
{
|
||||
this.parserConfig = parserConfig;
|
||||
|
@ -54,7 +50,8 @@ public class StreamExtractor
|
|||
LoadSourceFromText(this.rawText);
|
||||
}
|
||||
|
||||
public void LoadSourceFromText(string rawText)
|
||||
[MemberNotNull(nameof(this.rawText), nameof(this.extractor))]
|
||||
private void LoadSourceFromText(string rawText)
|
||||
{
|
||||
var rawType = "txt";
|
||||
rawText = rawText.Trim();
|
||||
|
|
|
@ -3,9 +3,9 @@ using System.Text.RegularExpressions;
|
|||
|
||||
namespace N_m3u8DL_RE.Parser.Util;
|
||||
|
||||
public partial class ParserUtil
|
||||
public static partial class ParserUtil
|
||||
{
|
||||
[GeneratedRegex("\\$Number%([^$]+)d\\$")]
|
||||
[GeneratedRegex(@"\$Number%([^$]+)d\$")]
|
||||
private static partial Regex VarsNumberRegex();
|
||||
|
||||
/// <summary>
|
||||
|
@ -33,8 +33,7 @@ public partial class ParserUtil
|
|||
{
|
||||
var startIndex = index + (key + "=").Length;
|
||||
var endIndex = startIndex + line[startIndex..].IndexOf(',');
|
||||
if (endIndex >= startIndex) result = line[startIndex..endIndex];
|
||||
else result = line[startIndex..];
|
||||
result = endIndex >= startIndex ? line[startIndex..endIndex] : line[startIndex..];
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -49,18 +48,13 @@ public partial class ParserUtil
|
|||
public static (long, long?) GetRange(string input)
|
||||
{
|
||||
var t = input.Split('@');
|
||||
if (t.Length > 0)
|
||||
return t.Length switch
|
||||
{
|
||||
if (t.Length == 1)
|
||||
{
|
||||
return (Convert.ToInt64(t[0]), null);
|
||||
}
|
||||
if (t.Length == 2)
|
||||
{
|
||||
return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
|
||||
}
|
||||
}
|
||||
return (0, null);
|
||||
<= 0 => (0, null),
|
||||
1 => (Convert.ToInt64(t[0]), null),
|
||||
2 => (Convert.ToInt64(t[0]), Convert.ToInt64(t[1])),
|
||||
_ => (0, null)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -111,8 +105,8 @@ public partial class ParserUtil
|
|||
if (string.IsNullOrEmpty(baseurl))
|
||||
return url;
|
||||
|
||||
Uri uri1 = new Uri(baseurl); // 这里直接传完整的URL即可
|
||||
Uri uri2 = new Uri(uri1, url);
|
||||
var uri1 = new Uri(baseurl); // 这里直接传完整的URL即可
|
||||
var uri2 = new Uri(uri1, url);
|
||||
url = uri2.ToString();
|
||||
|
||||
return url;
|
||||
|
|
|
@ -27,7 +27,7 @@ internal sealed class DownloadSpeedColumn : ProgressColumn
|
|||
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
var flag = task.IsFinished || !task.IsStarted;
|
||||
// 单文件下载汇报进度
|
||||
if (!flag && speedContainer.SingleSegment && speedContainer.ResponseLength != null)
|
||||
if (!flag && speedContainer is { SingleSegment: true, ResponseLength: not null })
|
||||
{
|
||||
task.MaxValue = (double)speedContainer.ResponseLength;
|
||||
task.Value = speedContainer.RDownloaded;
|
||||
|
|
|
@ -25,9 +25,6 @@ internal class RecordingDurationColumn : ProgressColumn
|
|||
{
|
||||
if (_refreshedDurDic == null)
|
||||
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
|
||||
else
|
||||
{
|
||||
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
|
||||
}
|
||||
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
|
||||
}
|
||||
}
|
|
@ -15,14 +15,16 @@ using System.Text.RegularExpressions;
|
|||
|
||||
namespace N_m3u8DL_RE.CommandLine;
|
||||
|
||||
internal partial class CommandInvoker
|
||||
internal static partial class CommandInvoker
|
||||
{
|
||||
public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20241130";
|
||||
public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20241201";
|
||||
|
||||
[GeneratedRegex("((best|worst)\\d*|all)")]
|
||||
private static partial Regex ForStrRegex();
|
||||
[GeneratedRegex("(\\d*)-(\\d*)")]
|
||||
[GeneratedRegex(@"(\d*)-(\d*)")]
|
||||
private static partial Regex RangeRegex();
|
||||
[GeneratedRegex(@"([\d\\.]+)(M|K)")]
|
||||
private static partial Regex SpeedStrRegex();
|
||||
|
||||
private static readonly Argument<string> Input = new(name: "input", description: ResString.cmd_Input);
|
||||
private static readonly Option<string?> TmpDir = new(["--tmp-dir"], description: ResString.cmd_tmpDir);
|
||||
|
@ -110,23 +112,22 @@ internal partial class CommandInvoker
|
|||
private static readonly Option<StreamFilter?> DropSubtitleFilter = new(["-ds", "--drop-subtitle"], description: ResString.cmd_dropSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
|
||||
|
||||
/// <summary>
|
||||
/// 解析录制直播时长限制
|
||||
/// 解析下载速度限制
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
/// <returns></returns>
|
||||
private static long? ParseSpeedLimit(ArgumentResult result)
|
||||
{
|
||||
var input = result.Tokens.First().Value.ToUpper();
|
||||
var input = result.Tokens[0].Value.ToUpper();
|
||||
try
|
||||
{
|
||||
var reg = new Regex("([\\d\\\\.]+)(M|K)");
|
||||
if (!reg.IsMatch(input)) throw new ArgumentException();
|
||||
var reg = SpeedStrRegex();
|
||||
if (!reg.IsMatch(input)) throw new ArgumentException($"Invalid Speed Limit: {input}");
|
||||
|
||||
var number = double.Parse(reg.Match(input).Groups[1].Value);
|
||||
if (reg.Match(input).Groups[2].Value == "M")
|
||||
return (long)(number * 1024 * 1024);
|
||||
else
|
||||
return (long)(number * 1024);
|
||||
return (long)(number * 1024);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -143,7 +144,7 @@ internal partial class CommandInvoker
|
|||
/// <exception cref="ArgumentException"></exception>
|
||||
private static CustomRange? ParseCustomRange(ArgumentResult result)
|
||||
{
|
||||
var input = result.Tokens.First().Value;
|
||||
var input = result.Tokens[0].Value;
|
||||
// 支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00;
|
||||
try
|
||||
{
|
||||
|
@ -154,7 +155,7 @@ internal partial class CommandInvoker
|
|||
if (arr.Length != 2)
|
||||
throw new ArgumentException("Bad format!");
|
||||
|
||||
if (input.Contains(":"))
|
||||
if (input.Contains(':'))
|
||||
{
|
||||
return new CustomRange()
|
||||
{
|
||||
|
@ -163,7 +164,8 @@ internal partial class CommandInvoker
|
|||
EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds,
|
||||
};
|
||||
}
|
||||
else if (RangeRegex().IsMatch(input))
|
||||
|
||||
if (RangeRegex().IsMatch(input))
|
||||
{
|
||||
var left = RangeRegex().Match(input).Groups[1].Value;
|
||||
var right = RangeRegex().Match(input).Groups[2].Value;
|
||||
|
@ -192,7 +194,7 @@ internal partial class CommandInvoker
|
|||
/// <exception cref="ArgumentException"></exception>
|
||||
private static WebProxy? ParseProxy(ArgumentResult result)
|
||||
{
|
||||
var input = result.Tokens.First().Value;
|
||||
var input = result.Tokens[0].Value;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
|
@ -221,17 +223,16 @@ internal partial class CommandInvoker
|
|||
/// <returns></returns>
|
||||
private static byte[]? ParseHLSCustomKey(ArgumentResult result)
|
||||
{
|
||||
var input = result.Tokens.First().Value;
|
||||
var input = result.Tokens[0].Value;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return null;
|
||||
if (File.Exists(input))
|
||||
return File.ReadAllBytes(input);
|
||||
else if (HexUtil.TryParseHexString(input, out byte[]? bytes))
|
||||
if (HexUtil.TryParseHexString(input, out byte[]? bytes))
|
||||
return bytes;
|
||||
else
|
||||
return Convert.FromBase64String(input);
|
||||
return Convert.FromBase64String(input);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -247,7 +248,7 @@ internal partial class CommandInvoker
|
|||
/// <returns></returns>
|
||||
private static TimeSpan? ParseLiveLimit(ArgumentResult result)
|
||||
{
|
||||
var input = result.Tokens.First().Value;
|
||||
var input = result.Tokens[0].Value;
|
||||
try
|
||||
{
|
||||
return OtherUtil.ParseDur(input);
|
||||
|
@ -266,7 +267,7 @@ internal partial class CommandInvoker
|
|||
/// <returns></returns>
|
||||
private static DateTime? ParseStartTime(ArgumentResult result)
|
||||
{
|
||||
var input = result.Tokens.First().Value;
|
||||
var input = result.Tokens[0].Value;
|
||||
try
|
||||
{
|
||||
CultureInfo provider = CultureInfo.InvariantCulture;
|
||||
|
@ -281,7 +282,7 @@ internal partial class CommandInvoker
|
|||
|
||||
private static string? ParseSaveName(ArgumentResult result)
|
||||
{
|
||||
var input = result.Tokens.First().Value;
|
||||
var input = result.Tokens[0].Value;
|
||||
var newName = OtherUtil.GetValidFileName(input);
|
||||
if (string.IsNullOrEmpty(newName))
|
||||
{
|
||||
|
@ -299,7 +300,7 @@ internal partial class CommandInvoker
|
|||
private static StreamFilter? ParseStreamFilter(ArgumentResult result)
|
||||
{
|
||||
var streamFilter = new StreamFilter();
|
||||
var input = result.Tokens.First().Value;
|
||||
var input = result.Tokens[0].Value;
|
||||
var p = new ComplexParamParser(input);
|
||||
|
||||
|
||||
|
@ -437,7 +438,7 @@ internal partial class CommandInvoker
|
|||
/// <returns></returns>
|
||||
private static MuxOptions? ParseMuxAfterDone(ArgumentResult result)
|
||||
{
|
||||
var v = result.Tokens.First().Value;
|
||||
var v = result.Tokens[0].Value;
|
||||
var p = new ComplexParamParser(v);
|
||||
// 混流格式
|
||||
var format = p.GetValue("format") ?? v.Split(':')[0]; // 若未获取到,直接:前的字符串作为format解析
|
||||
|
@ -577,14 +578,12 @@ internal partial class CommandInvoker
|
|||
|
||||
// 混流设置
|
||||
var muxAfterDoneValue = bindingContext.ParseResult.GetValueForOption(MuxAfterDone);
|
||||
if (muxAfterDoneValue != null)
|
||||
{
|
||||
option.MuxAfterDone = true;
|
||||
option.MuxOptions = muxAfterDoneValue;
|
||||
if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath;
|
||||
else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath;
|
||||
}
|
||||
|
||||
if (muxAfterDoneValue == null) return option;
|
||||
|
||||
option.MuxAfterDone = true;
|
||||
option.MuxOptions = muxAfterDoneValue;
|
||||
if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath;
|
||||
else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath;
|
||||
|
||||
return option;
|
||||
}
|
||||
|
@ -626,7 +625,7 @@ internal partial class CommandInvoker
|
|||
};
|
||||
|
||||
rootCommand.TreatUnmatchedTokensAsErrors = true;
|
||||
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder());
|
||||
rootCommand.SetHandler(async myOption => await action(myOption), new MyOptionBinder());
|
||||
|
||||
var parser = new CommandLineBuilder(rootCommand)
|
||||
.UseDefaults()
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace N_m3u8DL_RE.CommandLine;
|
|||
|
||||
internal class ComplexParamParser
|
||||
{
|
||||
private string _arg;
|
||||
private readonly string _arg;
|
||||
public ComplexParamParser(string arg)
|
||||
{
|
||||
_arg = arg;
|
||||
|
@ -16,7 +16,7 @@ internal class ComplexParamParser
|
|||
|
||||
try
|
||||
{
|
||||
var index = _arg.IndexOf(key + "=");
|
||||
var index = _arg.IndexOf(key + "=", StringComparison.Ordinal);
|
||||
if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;
|
||||
|
||||
var chars = _arg[(index + key.Length + 1)..].ToCharArray();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace N_m3u8DL_RE.Crypto;
|
||||
|
||||
internal class AESUtil
|
||||
internal static class AESUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// AES-128解密,解密后原地替换文件
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace N_m3u8DL_RE.Crypto;
|
||||
|
||||
internal class ChaCha20Util
|
||||
internal static class ChaCha20Util
|
||||
{
|
||||
public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes)
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ internal class HTTPLiveRecordManager
|
|||
DownloaderConfig DownloaderConfig;
|
||||
StreamExtractor StreamExtractor;
|
||||
List<StreamSpec> SelectedSteams;
|
||||
List<OutputFile> OutputFiles = new();
|
||||
List<OutputFile> OutputFiles = [];
|
||||
DateTime NowDateTime;
|
||||
DateTime? PublishDateTime;
|
||||
bool STOP_FLAG = false;
|
||||
|
@ -107,7 +107,7 @@ internal class HTTPLiveRecordManager
|
|||
await Task.Delay(200);
|
||||
if (InfoBuffer.Count < 188 * 5000) continue;
|
||||
|
||||
UInt16 ConvertToUint16(IEnumerable<byte> bytes)
|
||||
ushort ConvertToUint16(IEnumerable<byte> bytes)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
bytes = bytes.Reverse();
|
||||
|
@ -217,7 +217,7 @@ internal class HTTPLiveRecordManager
|
|||
return (item, task);
|
||||
}).ToDictionary(item => item.item, item => item.task);
|
||||
|
||||
DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue;
|
||||
DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;
|
||||
var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
|
||||
if (limit != TimeSpan.MaxValue)
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");
|
||||
|
|
|
@ -23,7 +23,7 @@ internal class SimpleDownloadManager
|
|||
DownloaderConfig DownloaderConfig;
|
||||
StreamExtractor StreamExtractor;
|
||||
List<StreamSpec> SelectedSteams;
|
||||
List<OutputFile> OutputFiles = new();
|
||||
List<OutputFile> OutputFiles = [];
|
||||
|
||||
public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
|
||||
{
|
||||
|
@ -48,13 +48,13 @@ internal class SimpleDownloadManager
|
|||
|
||||
private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)
|
||||
{
|
||||
if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true))
|
||||
if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))
|
||||
{
|
||||
DownloaderConfig.MyOptions.BinaryMerge = true;
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]");
|
||||
}
|
||||
|
||||
if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true))
|
||||
if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))
|
||||
{
|
||||
DownloaderConfig.MyOptions.MuxAfterDone = false;
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
|
||||
|
@ -72,7 +72,7 @@ internal class SimpleDownloadManager
|
|||
else if (mediainfos.All(m => m.Type == "Subtitle"))
|
||||
{
|
||||
streamSpec.MediaType = MediaType.SUBTITLES;
|
||||
if (streamSpec.Extension == null || streamSpec.Extension == "ts")
|
||||
if (streamSpec.Extension is null or "ts")
|
||||
streamSpec.Extension = "vtt";
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ internal class SimpleDownloadManager
|
|||
{
|
||||
speedContainer.ResetVars();
|
||||
bool useAACFilter = false; // ffmpeg合并flag
|
||||
List<Mediainfo> mediaInfos = new();
|
||||
List<Mediainfo> mediaInfos = [];
|
||||
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
|
||||
|
||||
var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments);
|
||||
|
@ -158,7 +158,7 @@ internal class SimpleDownloadManager
|
|||
var path = Path.Combine(tmpDir, "_init.mp4.tmp");
|
||||
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
|
||||
FileDic[streamSpec.Playlist.MediaInit] = result;
|
||||
if (result == null || !result.Success)
|
||||
if (result is not { Success: true })
|
||||
{
|
||||
throw new Exception("Download init file failed!");
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ internal class SimpleDownloadManager
|
|||
task.Increment(1);
|
||||
|
||||
// 读取mp4信息
|
||||
if (result != null && result.Success)
|
||||
if (result is { Success: true })
|
||||
{
|
||||
mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath);
|
||||
currentKID = mp4Info.KID;
|
||||
|
@ -212,12 +212,12 @@ internal class SimpleDownloadManager
|
|||
var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
|
||||
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
|
||||
FileDic[seg] = result;
|
||||
if (result == null || !result.Success)
|
||||
if (result is not { Success: true })
|
||||
{
|
||||
throw new Exception("Download first segment failed!");
|
||||
}
|
||||
task.Increment(1);
|
||||
if (result != null && result.Success)
|
||||
if (result is { Success: true })
|
||||
{
|
||||
// 修复MSS init
|
||||
if (StreamExtractor.ExtractorType == ExtractorType.MSS)
|
||||
|
@ -284,10 +284,10 @@ internal class SimpleDownloadManager
|
|||
var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
|
||||
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
|
||||
FileDic[seg] = result;
|
||||
if (result != null && result.Success)
|
||||
if (result is { Success: true })
|
||||
task.Increment(1);
|
||||
// 实时解密
|
||||
if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID))
|
||||
if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result is { Success: true } && !string.IsNullOrEmpty(currentKID))
|
||||
{
|
||||
var enc = result.ActualFilePath;
|
||||
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
|
||||
|
@ -304,13 +304,12 @@ internal class SimpleDownloadManager
|
|||
// 修改输出后缀
|
||||
var outputExt = "." + streamSpec.Extension;
|
||||
if (streamSpec.Extension == null) outputExt = ".ts";
|
||||
else if (streamSpec.MediaType == MediaType.AUDIO && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".m4a";
|
||||
else if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".mp4";
|
||||
else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: "m4s" or "mp4" }) outputExt = ".m4a";
|
||||
else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension is "m4s" or "mp4") outputExt = ".mp4";
|
||||
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES)
|
||||
{
|
||||
if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt";
|
||||
else outputExt = ".vtt";
|
||||
outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? ".srt" : ".vtt";
|
||||
}
|
||||
var output = Path.Combine(saveDir, saveName + outputExt);
|
||||
|
||||
|
@ -320,7 +319,7 @@ internal class SimpleDownloadManager
|
|||
Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0 && mp4InitFile != "")
|
||||
if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, Keys.Length: > 0 } && mp4InitFile != "")
|
||||
{
|
||||
File.Delete(mp4InitFile);
|
||||
// shaka/ffmpeg实时解密不需要init文件用于合并
|
||||
|
@ -333,7 +332,7 @@ internal class SimpleDownloadManager
|
|||
// 校验分片数量
|
||||
if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null))
|
||||
{
|
||||
Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Where(s => s != null).Count());
|
||||
Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Count(s => s != null));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -351,8 +350,7 @@ internal class SimpleDownloadManager
|
|||
}
|
||||
|
||||
// 自动修复VTT raw字幕
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Extension != null && streamSpec.Extension.Contains("vtt"))
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("vtt"))
|
||||
{
|
||||
Logger.WarnMarkUp(ResString.fixingVTT);
|
||||
// 排序字幕并修正时间戳
|
||||
|
@ -398,7 +396,7 @@ internal class SimpleDownloadManager
|
|||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s"))
|
||||
{
|
||||
var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
|
||||
var initFile = FileDic.Values.FirstOrDefault(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init"));
|
||||
var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
|
||||
var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
|
||||
if (sawVtt)
|
||||
|
@ -432,8 +430,7 @@ internal class SimpleDownloadManager
|
|||
}
|
||||
|
||||
// 自动修复TTML raw字幕
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Extension != null && streamSpec.Extension.Contains("ttml"))
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("ttml"))
|
||||
{
|
||||
Logger.WarnMarkUp(ResString.fixingTTML);
|
||||
var first = true;
|
||||
|
@ -479,9 +476,8 @@ internal class SimpleDownloadManager
|
|||
}
|
||||
|
||||
// 自动修复TTML mp4字幕
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")
|
||||
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("m4s")
|
||||
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
|
||||
{
|
||||
Logger.WarnMarkUp(ResString.fixingTTMLmp4);
|
||||
// sawTtml暂时不判断
|
||||
|
@ -575,7 +571,7 @@ internal class SimpleDownloadManager
|
|||
}
|
||||
|
||||
// 删除临时文件夹
|
||||
if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && mergeSuccess)
|
||||
if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && mergeSuccess)
|
||||
{
|
||||
var files = FileDic.Values.Select(v => v!.ActualFilePath);
|
||||
foreach (var file in files)
|
||||
|
@ -598,7 +594,7 @@ internal class SimpleDownloadManager
|
|||
}
|
||||
|
||||
// 调用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 is { MP4RealTimeDecryption: false, Keys.Length: > 0 })
|
||||
{
|
||||
var enc = output;
|
||||
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
|
||||
|
@ -700,7 +696,7 @@ internal class SimpleDownloadManager
|
|||
var success = Results.Values.All(v => v == true);
|
||||
|
||||
// 删除临时文件夹
|
||||
if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success)
|
||||
if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && success)
|
||||
{
|
||||
foreach (var item in StreamExtractor.RawFiles)
|
||||
{
|
||||
|
|
|
@ -27,7 +27,7 @@ internal class SimpleLiveRecordManager2
|
|||
StreamExtractor StreamExtractor;
|
||||
List<StreamSpec> SelectedSteams;
|
||||
ConcurrentDictionary<int, string> PipeSteamNamesDic = new();
|
||||
List<OutputFile> OutputFiles = new();
|
||||
List<OutputFile> OutputFiles = [];
|
||||
DateTime? PublishDateTime;
|
||||
bool STOP_FLAG = false;
|
||||
int WAIT_SEC = 0; // 刷新间隔
|
||||
|
@ -41,7 +41,7 @@ internal class SimpleLiveRecordManager2
|
|||
ConcurrentDictionary<int, long> DateTimeDic = new(); // 上次下载的dateTime
|
||||
CancellationTokenSource CancellationTokenSource = new(); // 取消Wait
|
||||
|
||||
private readonly object lockObj = new object();
|
||||
private readonly Lock lockObj = new();
|
||||
TimeSpan? audioStart = null;
|
||||
|
||||
public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
|
||||
|
@ -60,9 +60,9 @@ internal class SimpleLiveRecordManager2
|
|||
if (_key != null)
|
||||
{
|
||||
if (DownloaderConfig.MyOptions.Keys == null)
|
||||
DownloaderConfig.MyOptions.Keys = new string[] { _key };
|
||||
DownloaderConfig.MyOptions.Keys = [_key];
|
||||
else
|
||||
DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray();
|
||||
DownloaderConfig.MyOptions.Keys = [..DownloaderConfig.MyOptions.Keys, _key];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,13 +111,13 @@ internal class SimpleLiveRecordManager2
|
|||
|
||||
private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)
|
||||
{
|
||||
if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true))
|
||||
if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))
|
||||
{
|
||||
DownloaderConfig.MyOptions.BinaryMerge = true;
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]");
|
||||
}
|
||||
|
||||
if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true))
|
||||
if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))
|
||||
{
|
||||
DownloaderConfig.MyOptions.MuxAfterDone = false;
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
|
||||
|
@ -136,7 +136,7 @@ internal class SimpleLiveRecordManager2
|
|||
{
|
||||
streamSpec.MediaType = MediaType.SUBTITLES;
|
||||
|
||||
if (streamSpec.Extension == null || streamSpec.Extension == "ts")
|
||||
if (streamSpec.Extension is null or "ts")
|
||||
streamSpec.Extension = "vtt";
|
||||
}
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ internal class SimpleLiveRecordManager2
|
|||
bool useAACFilter = false; // ffmpeg合并flag
|
||||
bool initDownloaded = false; // 是否下载过init文件
|
||||
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
|
||||
List<Mediainfo> mediaInfos = new();
|
||||
List<Mediainfo> mediaInfos = [];
|
||||
Stream? fileOutputStream = null;
|
||||
WebVttSub currentVtt = new(); // 字幕流始终维护一个实例
|
||||
bool firstSub = true;
|
||||
|
@ -197,7 +197,7 @@ internal class SimpleLiveRecordManager2
|
|||
var path = Path.Combine(tmpDir, "_init.mp4.tmp");
|
||||
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
|
||||
FileDic[streamSpec.Playlist.MediaInit] = result;
|
||||
if (result == null || !result.Success)
|
||||
if (result is not { Success: true })
|
||||
{
|
||||
throw new Exception("Download init file failed!");
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ internal class SimpleLiveRecordManager2
|
|||
task.Increment(1);
|
||||
|
||||
// 读取mp4信息
|
||||
if (result != null && result.Success)
|
||||
if (result is { Success: true })
|
||||
{
|
||||
currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;
|
||||
// 从文件读取KEY
|
||||
|
@ -257,12 +257,12 @@ internal class SimpleLiveRecordManager2
|
|||
var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp");
|
||||
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
|
||||
FileDic[seg] = result;
|
||||
if (result == null || !result.Success)
|
||||
if (result is not { Success: true })
|
||||
{
|
||||
throw new Exception("Download first segment failed!");
|
||||
}
|
||||
task.Increment(1);
|
||||
if (result != null && result.Success)
|
||||
if (result is { Success: true })
|
||||
{
|
||||
// 修复MSS init
|
||||
if (StreamExtractor.ExtractorType == ExtractorType.MSS)
|
||||
|
@ -330,10 +330,10 @@ internal class SimpleLiveRecordManager2
|
|||
var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp");
|
||||
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
|
||||
FileDic[seg] = result;
|
||||
if (result != null && result.Success)
|
||||
if (result is { Success: true })
|
||||
task.Increment(1);
|
||||
// 实时解密
|
||||
if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID))
|
||||
if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result is { Success: true } && !string.IsNullOrEmpty(currentKID))
|
||||
{
|
||||
var enc = result.ActualFilePath;
|
||||
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
|
||||
|
@ -347,14 +347,13 @@ internal class SimpleLiveRecordManager2
|
|||
});
|
||||
|
||||
// 自动修复VTT raw字幕
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Extension != null && streamSpec.Extension.Contains("vtt"))
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("vtt"))
|
||||
{
|
||||
// 排序字幕并修正时间戳
|
||||
var keys = FileDic.Keys.OrderBy(k => k.Index);
|
||||
var keys = FileDic.Keys.OrderBy(k => k.Index).ToList();
|
||||
foreach (var seg in keys)
|
||||
{
|
||||
var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath);
|
||||
var vttContent = await File.ReadAllTextAsync(FileDic[seg]!.ActualFilePath);
|
||||
var waitCount = 0;
|
||||
while (DownloaderConfig.MyOptions.LiveFixVttByAudio && audioStart == null && waitCount++ < 5)
|
||||
{
|
||||
|
@ -376,7 +375,7 @@ internal class SimpleLiveRecordManager2
|
|||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s"))
|
||||
{
|
||||
var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
|
||||
var initFile = FileDic.Values.FirstOrDefault(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init"));
|
||||
var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
|
||||
var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
|
||||
if (sawVtt)
|
||||
|
@ -396,10 +395,9 @@ internal class SimpleLiveRecordManager2
|
|||
}
|
||||
|
||||
// 自动修复TTML raw字幕
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Extension != null && streamSpec.Extension.Contains("ttml"))
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("ttml"))
|
||||
{
|
||||
var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key);
|
||||
var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key).ToList();
|
||||
if (firstSub)
|
||||
{
|
||||
if (baseTimestamp != 0)
|
||||
|
@ -437,9 +435,8 @@ internal class SimpleLiveRecordManager2
|
|||
}
|
||||
|
||||
// 自动修复TTML mp4字幕
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
|
||||
&& streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")
|
||||
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
|
||||
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("m4s")
|
||||
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
|
||||
{
|
||||
// sawTtml暂时不判断
|
||||
// var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
|
||||
|
@ -498,12 +495,11 @@ internal class SimpleLiveRecordManager2
|
|||
// 合并
|
||||
var outputExt = "." + streamSpec.Extension;
|
||||
if (streamSpec.Extension == null) outputExt = ".ts";
|
||||
else if (streamSpec.MediaType == MediaType.AUDIO && streamSpec.Extension == "m4s") outputExt = ".m4a";
|
||||
else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: "m4s" }) outputExt = ".m4a";
|
||||
else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension == "m4s") outputExt = ".mp4";
|
||||
else if (streamSpec.MediaType == MediaType.SUBTITLES)
|
||||
{
|
||||
if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt";
|
||||
else outputExt = ".vtt";
|
||||
outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? ".srt" : ".vtt";
|
||||
}
|
||||
|
||||
var output = Path.Combine(saveDir, saveName + outputExt);
|
||||
|
@ -536,7 +532,7 @@ internal class SimpleLiveRecordManager2
|
|||
fileOutputStream = PipeUtil.CreatePipe(pipeName);
|
||||
Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]");
|
||||
PipeSteamNamesDic[task.Id] = pipeName;
|
||||
if (PipeSteamNamesDic.Count == SelectedSteams.Where(x => x.MediaType != MediaType.SUBTITLES).Count())
|
||||
if (PipeSteamNamesDic.Count == SelectedSteams.Count(x => x.MediaType != MediaType.SUBTITLES))
|
||||
{
|
||||
var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray();
|
||||
Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]");
|
||||
|
@ -622,24 +618,23 @@ internal class SimpleLiveRecordManager2
|
|||
break;
|
||||
}
|
||||
|
||||
if (fileOutputStream != null)
|
||||
if (fileOutputStream == null) return true;
|
||||
|
||||
if (!DownloaderConfig.MyOptions.LivePipeMux)
|
||||
{
|
||||
if (!DownloaderConfig.MyOptions.LivePipeMux)
|
||||
// 记录所有文件信息
|
||||
OutputFiles.Add(new OutputFile()
|
||||
{
|
||||
// 记录所有文件信息
|
||||
OutputFiles.Add(new OutputFile()
|
||||
{
|
||||
Index = task.Id,
|
||||
FilePath = (fileOutputStream as FileStream)!.Name,
|
||||
LangCode = streamSpec.Language,
|
||||
Description = streamSpec.Name,
|
||||
Mediainfos = mediaInfos,
|
||||
MediaType = streamSpec.MediaType,
|
||||
});
|
||||
}
|
||||
fileOutputStream.Close();
|
||||
fileOutputStream.Dispose();
|
||||
Index = task.Id,
|
||||
FilePath = (fileOutputStream as FileStream)!.Name,
|
||||
LangCode = streamSpec.Language,
|
||||
Description = streamSpec.Name,
|
||||
Mediainfos = mediaInfos,
|
||||
MediaType = streamSpec.MediaType,
|
||||
});
|
||||
}
|
||||
fileOutputStream.Close();
|
||||
fileOutputStream.Dispose();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -648,78 +643,76 @@ internal class SimpleLiveRecordManager2
|
|||
{
|
||||
while (!STOP_FLAG)
|
||||
{
|
||||
if (WAIT_SEC != 0)
|
||||
if (WAIT_SEC == 0) continue;
|
||||
|
||||
// 1. MPD 所有URL相同 单次请求即可获得所有轨道的信息
|
||||
// 2. M3U8 所有URL不同 才需要多次请求
|
||||
await Parallel.ForEachAsync(dic, async (dic, _) =>
|
||||
{
|
||||
// 1. MPD 所有URL相同 单次请求即可获得所有轨道的信息
|
||||
// 2. M3U8 所有URL不同 才需要多次请求
|
||||
var streamSpec = dic.Key;
|
||||
var task = dic.Value;
|
||||
|
||||
await Parallel.ForEachAsync(dic, async (dic, _) =>
|
||||
// 达到上限时 不需要刷新了
|
||||
if (RecordLimitReachedDic[task.Id])
|
||||
return;
|
||||
|
||||
var allHasDatetime = streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null);
|
||||
if (!SamePathDic.ContainsKey(task.Id))
|
||||
{
|
||||
var streamSpec = dic.Key;
|
||||
var task = dic.Value;
|
||||
|
||||
// 达到上限时 不需要刷新了
|
||||
if (RecordLimitReachedDic[task.Id])
|
||||
return;
|
||||
|
||||
var allHasDatetime = streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null);
|
||||
if (!SamePathDic.ContainsKey(task.Id))
|
||||
{
|
||||
var allName = streamSpec.Playlist!.MediaParts[0].MediaSegments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false));
|
||||
var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1;
|
||||
SamePathDic[task.Id] = allSamePath;
|
||||
}
|
||||
// 过滤不需要下载的片段
|
||||
FilterMediaSegments(streamSpec, task, allHasDatetime, SamePathDic[task.Id]);
|
||||
var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments;
|
||||
if (newList.Count > 0)
|
||||
{
|
||||
task.MaxValue += newList.Count;
|
||||
// 推送给消费者
|
||||
await BlockDic[task.Id].SendAsync(newList);
|
||||
// 更新最新链接
|
||||
LastFileNameDic[task.Id] = GetSegmentName(newList.Last(), allHasDatetime, SamePathDic[task.Id]);
|
||||
// 尝试更新时间戳
|
||||
var dt = newList.Last().DateTime;
|
||||
DateTimeDic[task.Id] = dt != null ? GetUnixTimestamp(dt.Value) : 0L;
|
||||
// 累加已获取到的时长
|
||||
RefreshedDurDic[task.Id] += (int)newList.Sum(s => s.Duration);
|
||||
}
|
||||
|
||||
if (!STOP_FLAG && RefreshedDurDic[task.Id] >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds)
|
||||
{
|
||||
RecordLimitReachedDic[task.Id] = true;
|
||||
}
|
||||
|
||||
// 检测时长限制
|
||||
if (!STOP_FLAG && RecordLimitReachedDic.Values.All(x => x == true))
|
||||
{
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
|
||||
STOP_FLAG = true;
|
||||
CancellationTokenSource.Cancel();
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Logger.WarnMarkUp($"wait {waitSec}s");
|
||||
if (!STOP_FLAG) await Task.Delay(WAIT_SEC * 1000, CancellationTokenSource.Token);
|
||||
// 刷新列表
|
||||
if (!STOP_FLAG) await StreamExtractor.RefreshPlayListAsync(dic.Keys.ToList());
|
||||
var allName = streamSpec.Playlist!.MediaParts[0].MediaSegments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false));
|
||||
var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1;
|
||||
SamePathDic[task.Id] = allSamePath;
|
||||
}
|
||||
catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
|
||||
// 过滤不需要下载的片段
|
||||
FilterMediaSegments(streamSpec, task, allHasDatetime, SamePathDic[task.Id]);
|
||||
var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments;
|
||||
if (newList.Count > 0)
|
||||
{
|
||||
// 不需要做事
|
||||
task.MaxValue += newList.Count;
|
||||
// 推送给消费者
|
||||
await BlockDic[task.Id].SendAsync(newList);
|
||||
// 更新最新链接
|
||||
LastFileNameDic[task.Id] = GetSegmentName(newList.Last(), allHasDatetime, SamePathDic[task.Id]);
|
||||
// 尝试更新时间戳
|
||||
var dt = newList.Last().DateTime;
|
||||
DateTimeDic[task.Id] = dt != null ? GetUnixTimestamp(dt.Value) : 0L;
|
||||
// 累加已获取到的时长
|
||||
RefreshedDurDic[task.Id] += (int)newList.Sum(s => s.Duration);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
if (!STOP_FLAG && RefreshedDurDic[task.Id] >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds)
|
||||
{
|
||||
Logger.ErrorMarkUp(e);
|
||||
RecordLimitReachedDic[task.Id] = true;
|
||||
}
|
||||
|
||||
// 检测时长限制
|
||||
if (!STOP_FLAG && RecordLimitReachedDic.Values.All(x => x))
|
||||
{
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
|
||||
STOP_FLAG = true;
|
||||
// 停止所有Block
|
||||
foreach (var target in BlockDic.Values)
|
||||
{
|
||||
target.Complete();
|
||||
}
|
||||
CancellationTokenSource.Cancel();
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Logger.WarnMarkUp($"wait {waitSec}s");
|
||||
if (!STOP_FLAG) await Task.Delay(WAIT_SEC * 1000, CancellationTokenSource.Token);
|
||||
// 刷新列表
|
||||
if (!STOP_FLAG) await StreamExtractor.RefreshPlayListAsync(dic.Keys.ToList());
|
||||
}
|
||||
catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
|
||||
{
|
||||
// 不需要做事
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorMarkUp(e);
|
||||
STOP_FLAG = true;
|
||||
// 停止所有Block
|
||||
foreach (var target in BlockDic.Values)
|
||||
{
|
||||
target.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -783,7 +776,7 @@ internal class SimpleLiveRecordManager2
|
|||
Logger.WarnMarkUp($"set refresh interval to {WAIT_SEC} seconds");
|
||||
}
|
||||
// 如果没有选中音频 取消通过音频修复vtt时间轴
|
||||
if (!SelectedSteams.Any(x => x.MediaType == MediaType.AUDIO))
|
||||
if (SelectedSteams.All(x => x.MediaType != MediaType.AUDIO))
|
||||
{
|
||||
DownloaderConfig.MyOptions.LiveFixVttByAudio = false;
|
||||
}
|
||||
|
@ -839,7 +832,7 @@ internal class SimpleLiveRecordManager2
|
|||
|
||||
DownloaderConfig.MyOptions.ConcurrentDownload = true;
|
||||
DownloaderConfig.MyOptions.MP4RealTimeDecryption = true;
|
||||
DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue;
|
||||
DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;
|
||||
if (DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, DecryptionEngine: not DecryptEngine.SHAKA_PACKAGER, Keys.Length: > 0 })
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]");
|
||||
var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
|
||||
|
@ -865,7 +858,7 @@ internal class SimpleLiveRecordManager2
|
|||
var success = Results.Values.All(v => v == true);
|
||||
|
||||
// 删除临时文件夹
|
||||
if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success)
|
||||
if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && success)
|
||||
{
|
||||
foreach (var item in StreamExtractor.RawFiles)
|
||||
{
|
||||
|
|
|
@ -25,23 +25,25 @@ internal class SimpleDownloader : IDownloader
|
|||
{
|
||||
var url = segment.Url;
|
||||
var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount);
|
||||
if (dResult != null && dResult.Success && dResult.ActualFilePath != des)
|
||||
if (dResult is { Success: true } && dResult.ActualFilePath != des)
|
||||
{
|
||||
if (segment.EncryptInfo != null)
|
||||
switch (segment.EncryptInfo.Method)
|
||||
{
|
||||
if (segment.EncryptInfo.Method == EncryptMethod.AES_128)
|
||||
case EncryptMethod.AES_128:
|
||||
{
|
||||
var key = segment.EncryptInfo.Key;
|
||||
var iv = segment.EncryptInfo.IV;
|
||||
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!);
|
||||
break;
|
||||
}
|
||||
else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB)
|
||||
case EncryptMethod.AES_128_ECB:
|
||||
{
|
||||
var key = segment.EncryptInfo.Key;
|
||||
var iv = segment.EncryptInfo.IV;
|
||||
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB);
|
||||
break;
|
||||
}
|
||||
else if (segment.EncryptInfo.Method == EncryptMethod.CHACHA20)
|
||||
case EncryptMethod.CHACHA20:
|
||||
{
|
||||
var key = segment.EncryptInfo.Key;
|
||||
var nonce = segment.EncryptInfo.IV;
|
||||
|
@ -49,22 +51,22 @@ internal class SimpleDownloader : IDownloader
|
|||
var fileBytes = File.ReadAllBytes(dResult.ActualFilePath);
|
||||
var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!);
|
||||
await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted);
|
||||
break;
|
||||
}
|
||||
else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR)
|
||||
{
|
||||
case EncryptMethod.SAMPLE_AES_CTR:
|
||||
// throw new NotSupportedException("SAMPLE-AES-CTR");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Image头处理
|
||||
if (dResult.ImageHeader)
|
||||
{
|
||||
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
|
||||
}
|
||||
// Gzip解压
|
||||
if (dResult.GzipHeader)
|
||||
{
|
||||
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
|
||||
}
|
||||
// Image头处理
|
||||
if (dResult.ImageHeader)
|
||||
{
|
||||
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
|
||||
}
|
||||
// Gzip解压
|
||||
if (dResult.GzipHeader)
|
||||
{
|
||||
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
|
||||
}
|
||||
|
||||
// 处理完成后改名
|
||||
|
@ -99,14 +101,15 @@ internal class SimpleDownloader : IDownloader
|
|||
}
|
||||
|
||||
// 另起线程进行监控
|
||||
var cts = cancellationTokenSource;
|
||||
using var watcher = Task.Factory.StartNew(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break;
|
||||
if (cts.IsCancellationRequested) break;
|
||||
if (speedContainer.ShouldStop)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
cts.Cancel();
|
||||
Logger.DebugMarkUp("Cancel...");
|
||||
break;
|
||||
}
|
||||
|
@ -123,7 +126,7 @@ internal class SimpleDownloader : IDownloader
|
|||
catch (Exception ex)
|
||||
{
|
||||
Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
|
||||
Logger.Debug(url + " " + ex.ToString());
|
||||
Logger.Debug(url + " " + ex);
|
||||
Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
|
||||
if (retryCount-- > 0)
|
||||
{
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Entity;
|
||||
namespace N_m3u8DL_RE.Entity;
|
||||
|
||||
internal class DownloadResult
|
||||
{
|
||||
public bool Success { get => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength == null ? false : true); }
|
||||
public bool Success => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength != null);
|
||||
public long? RespContentLength { get; set; }
|
||||
public long? ActualContentLength { get; set; }
|
||||
public bool ImageHeader { get; set; } = false; // 图片伪装
|
||||
|
|
|
@ -9,5 +9,5 @@ internal class OutputFile
|
|||
public required string FilePath { get; set; }
|
||||
public string? LangCode { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public List<Mediainfo> Mediainfos { get; set; } = new();
|
||||
public List<Mediainfo> Mediainfos { get; set; } = [];
|
||||
}
|
|
@ -1,13 +1,4 @@
|
|||
using NiL.JS.Statements;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Entity;
|
||||
namespace N_m3u8DL_RE.Entity;
|
||||
|
||||
internal class SpeedContainer
|
||||
{
|
||||
|
@ -24,7 +15,7 @@ internal class SpeedContainer
|
|||
|
||||
private long _downloaded = 0;
|
||||
private long _Rdownloaded = 0;
|
||||
public long Downloaded { get => _downloaded; }
|
||||
public long Downloaded => _downloaded;
|
||||
|
||||
public int AddLowSpeedCount()
|
||||
{
|
||||
|
|
|
@ -46,6 +46,6 @@ public class StreamFilter
|
|||
if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} ");
|
||||
if (Role.HasValue) sb.Append($"Role: {Role} ");
|
||||
|
||||
return sb.ToString() + $"For: {For}";
|
||||
return sb + $"For: {For}";
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ using N_m3u8DL_RE.Util;
|
|||
using N_m3u8DL_RE.DownloadManager;
|
||||
using N_m3u8DL_RE.CommandLine;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using N_m3u8DL_RE.Enum;
|
||||
|
||||
namespace N_m3u8DL_RE;
|
||||
|
@ -26,7 +25,7 @@ internal class Program
|
|||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var osVersion = Environment.OSVersion.Version;
|
||||
if (osVersion.Major < 6 || (osVersion.Major == 6 && osVersion.Minor == 0))
|
||||
if (osVersion.Major < 6 || osVersion is { Major: 6, Minor: 0 })
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ internal class Program
|
|||
|
||||
string loc = ResString.CurrentLoc;
|
||||
string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
|
||||
if (currLoc == "zh-CN" || currLoc == "zh-SG") loc = "zh-CN";
|
||||
if (currLoc is "zh-CN" or "zh-SG") loc = "zh-CN";
|
||||
else if (currLoc.StartsWith("zh-")) loc = "zh-TW";
|
||||
|
||||
// 处理用户-h等请求
|
||||
|
@ -147,33 +146,36 @@ internal class Program
|
|||
// 预先检查
|
||||
if (option.Keys is { Length: > 0 } || option.KeyTextFile != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(option.DecryptionBinaryPath))
|
||||
if (!string.IsNullOrEmpty(option.DecryptionBinaryPath) && !File.Exists(option.DecryptionBinaryPath))
|
||||
{
|
||||
if (option.DecryptionEngine is DecryptEngine.SHAKA_PACKAGER)
|
||||
throw new FileNotFoundException(option.DecryptionBinaryPath);
|
||||
}
|
||||
switch (option.DecryptionEngine)
|
||||
{
|
||||
case DecryptEngine.SHAKA_PACKAGER:
|
||||
{
|
||||
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(ResString.shakaPackagerNotFound);
|
||||
if (file == null && file2 == null && file3 == null && file4 == null)
|
||||
throw new FileNotFoundException(ResString.shakaPackagerNotFound);
|
||||
option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
|
||||
Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}");
|
||||
break;
|
||||
}
|
||||
else if (option.DecryptionEngine is DecryptEngine.MP4DECRYPT)
|
||||
case DecryptEngine.MP4DECRYPT:
|
||||
{
|
||||
var file = GlobalUtil.FindExecutable("mp4decrypt");
|
||||
if (file == null) throw new FileNotFoundException(ResString.mp4decryptNotFound);
|
||||
option.DecryptionBinaryPath = file;
|
||||
Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}");
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
case DecryptEngine.FFMPEG:
|
||||
default:
|
||||
option.DecryptionBinaryPath = option.FFmpegBinaryPath;
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(option.DecryptionBinaryPath))
|
||||
{
|
||||
throw new FileNotFoundException(option.DecryptionBinaryPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,7 +285,7 @@ internal class Program
|
|||
|
||||
if (option.AutoSelect)
|
||||
{
|
||||
if (basicStreams.Any())
|
||||
if (basicStreams.Count != 0)
|
||||
selectedStreams.Add(basicStreams.First());
|
||||
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
|
||||
foreach (var lang in langs)
|
||||
|
@ -309,7 +311,7 @@ internal class Program
|
|||
selectedStreams = FilterUtil.SelectStreams(lists);
|
||||
}
|
||||
|
||||
if (!selectedStreams.Any())
|
||||
if (selectedStreams.Count == 0)
|
||||
throw new Exception(ResString.noStreamsToDownload);
|
||||
|
||||
// HLS: 选中流中若有没加载出playlist的,加载playlist
|
||||
|
@ -363,7 +365,7 @@ internal class Program
|
|||
Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]");
|
||||
|
||||
// 开始MuxAfterDone后自动使用二进制版
|
||||
if (!option.BinaryMerge && option.MuxAfterDone)
|
||||
if (option is { BinaryMerge: false, MuxAfterDone: true })
|
||||
{
|
||||
option.BinaryMerge = true;
|
||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]");
|
||||
|
@ -446,24 +448,21 @@ internal class Program
|
|||
static async Task<string> Get302Async(string url)
|
||||
{
|
||||
// this allows you to set the settings so that we can get the redirect url
|
||||
var handler = new HttpClientHandler()
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
};
|
||||
string redirectedUrl = "";
|
||||
using (HttpClient client = new(handler))
|
||||
using (HttpResponseMessage response = await client.GetAsync(url))
|
||||
using (HttpContent content = response.Content)
|
||||
var redirectedUrl = "";
|
||||
using var client = new HttpClient(handler);
|
||||
using var response = await client.GetAsync(url);
|
||||
using var content = response.Content;
|
||||
// ... Read the response to see if we have the redirected url
|
||||
if (response.StatusCode != HttpStatusCode.Found) return redirectedUrl;
|
||||
|
||||
var headers = response.Headers;
|
||||
if (headers.Location != null)
|
||||
{
|
||||
// ... Read the response to see if we have the redirected url
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Found)
|
||||
{
|
||||
HttpResponseHeaders headers = response.Headers;
|
||||
if (headers != null && headers.Location != null)
|
||||
{
|
||||
redirectedUrl = headers.Location.AbsoluteUri;
|
||||
}
|
||||
}
|
||||
redirectedUrl = headers.Location.AbsoluteUri;
|
||||
}
|
||||
|
||||
return redirectedUrl;
|
||||
|
|
|
@ -25,7 +25,7 @@ internal static class DownloadUtil
|
|||
{
|
||||
var buffer = new byte[expect];
|
||||
_ = await inputStream.ReadAsync(buffer);
|
||||
await outputStream.WriteAsync(buffer, 0, buffer.Length);
|
||||
await outputStream.WriteAsync(buffer);
|
||||
speedContainer.Add(buffer.Length);
|
||||
}
|
||||
return new DownloadResult()
|
||||
|
@ -81,7 +81,7 @@ internal static class DownloadUtil
|
|||
{
|
||||
HttpResponseHeaders respHeaders = response.Headers;
|
||||
Logger.Debug(respHeaders.ToString());
|
||||
if (respHeaders != null && respHeaders.Location != null)
|
||||
if (respHeaders.Location != null)
|
||||
{
|
||||
var redirectedUrl = "";
|
||||
if (!respHeaders.Location.IsAbsoluteUri)
|
||||
|
@ -108,7 +108,7 @@ internal static class DownloadUtil
|
|||
|
||||
size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);
|
||||
speedContainer.Add(size);
|
||||
await stream.WriteAsync(buffer, 0, size);
|
||||
await stream.WriteAsync(buffer.AsMemory(0, size));
|
||||
// 检测imageHeader
|
||||
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
|
||||
// 检测GZip(For DDP Audio)
|
||||
|
@ -117,7 +117,7 @@ internal static class DownloadUtil
|
|||
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
|
||||
{
|
||||
speedContainer.Add(size);
|
||||
await stream.WriteAsync(buffer, 0, size);
|
||||
await stream.WriteAsync(buffer.AsMemory(0, size));
|
||||
// 限速策略
|
||||
while (speedContainer.Downloaded > speedContainer.SpeedLimit)
|
||||
{
|
||||
|
|
|
@ -65,7 +65,7 @@ public static class FilterUtil
|
|||
|
||||
public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)
|
||||
{
|
||||
if (filter == null) return new List<StreamSpec>(lists);
|
||||
if (filter == null) return [..lists];
|
||||
|
||||
var inputs = lists.Where(_ => true);
|
||||
var selected = DoFilterKeep(lists, filter);
|
||||
|
@ -93,9 +93,8 @@ public static class FilterUtil
|
|||
.UseConverter(x =>
|
||||
{
|
||||
if (x.Name != null && x.Name.StartsWith("__"))
|
||||
return $"[darkslategray1]{x.Name.Substring(2)}[/]";
|
||||
else
|
||||
return x.ToString().EscapeMarkup().RemoveMarkup();
|
||||
return $"[darkslategray1]{x.Name[2..]}[/]";
|
||||
return x.ToString().EscapeMarkup().RemoveMarkup();
|
||||
})
|
||||
.Required()
|
||||
.PageSize(10)
|
||||
|
@ -107,12 +106,12 @@ public static class FilterUtil
|
|||
var first = streamSpecs.First();
|
||||
prompt.Select(first);
|
||||
|
||||
if (basicStreams.Any())
|
||||
if (basicStreams.Count != 0)
|
||||
{
|
||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams);
|
||||
}
|
||||
|
||||
if (audios.Any())
|
||||
if (audios.Count != 0)
|
||||
{
|
||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios);
|
||||
// 默认音轨
|
||||
|
@ -121,7 +120,7 @@ public static class FilterUtil
|
|||
prompt.Select(audios.First(a => a.GroupId == first.AudioId));
|
||||
}
|
||||
}
|
||||
if (subs.Any())
|
||||
if (subs.Count != 0)
|
||||
{
|
||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs);
|
||||
// 默认字幕轨
|
||||
|
|
|
@ -80,7 +80,7 @@ internal static class LargeSingleFileSplitUtil
|
|||
// 此函数主要是切片下载逻辑
|
||||
private static List<Clip> GetAllClips(string url, long fileSize)
|
||||
{
|
||||
List<Clip> clips = new();
|
||||
List<Clip> clips = [];
|
||||
int index = 0;
|
||||
long counter = 0;
|
||||
int perSize = 10 * 1024 * 1024;
|
||||
|
|
|
@ -7,7 +7,7 @@ using N_m3u8DL_RE.Enum;
|
|||
|
||||
namespace N_m3u8DL_RE.Util;
|
||||
|
||||
internal static class MP4DecryptUtil
|
||||
internal static partial class MP4DecryptUtil
|
||||
{
|
||||
private static readonly string ZeroKid = "00000000000000000000000000000000";
|
||||
public static async Task<bool> DecryptAsync(DecryptEngine decryptEngine, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false)
|
||||
|
@ -29,7 +29,7 @@ internal static class MP4DecryptUtil
|
|||
if (!string.IsNullOrEmpty(kid))
|
||||
{
|
||||
var test = keyPairs.Where(k => k.StartsWith(kid)).ToList();
|
||||
if (test.Any()) keyPair = test.First();
|
||||
if (test.Count != 0) keyPair = test.First();
|
||||
}
|
||||
|
||||
// Apple
|
||||
|
@ -159,11 +159,10 @@ internal static class MP4DecryptUtil
|
|||
using var reader = new StreamReader(stream);
|
||||
while (await reader.ReadLineAsync() is { } line)
|
||||
{
|
||||
if (line.Trim().StartsWith(kid))
|
||||
{
|
||||
Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]");
|
||||
return line.Trim();
|
||||
}
|
||||
if (!line.Trim().StartsWith(kid)) continue;
|
||||
|
||||
Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]");
|
||||
return line.Trim();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -192,7 +191,7 @@ internal static class MP4DecryptUtil
|
|||
|
||||
public static string? ReadInitShaka(string output, string bin)
|
||||
{
|
||||
Regex shakaKeyIdRegex = new("Key for key_id=([0-9a-f]+) was not found");
|
||||
Regex shakaKeyIdRegex = KidOutputRegex();
|
||||
|
||||
// TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid)
|
||||
// - stop process
|
||||
|
@ -214,4 +213,7 @@ internal static class MP4DecryptUtil
|
|||
p.WaitForExit();
|
||||
return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value;
|
||||
}
|
||||
|
||||
[GeneratedRegex("Key for key_id=([0-9a-f]+) was not found")]
|
||||
private static partial Regex KidOutputRegex();
|
||||
}
|
|
@ -8,23 +8,23 @@ internal static partial class MediainfoUtil
|
|||
{
|
||||
[GeneratedRegex(" Stream #.*")]
|
||||
private static partial Regex TextRegex();
|
||||
[GeneratedRegex("#0:\\d(\\[0x\\w+?\\])")]
|
||||
[GeneratedRegex(@"#0:\d(\[0x\w+?\])")]
|
||||
private static partial Regex IdRegex();
|
||||
[GeneratedRegex(": (\\w+): (.*)")]
|
||||
private static partial Regex TypeRegex();
|
||||
[GeneratedRegex("(.*?)(,|$)")]
|
||||
private static partial Regex BaseInfoRegex();
|
||||
[GeneratedRegex(" \\/ 0x\\w+")]
|
||||
[GeneratedRegex(@" \/ 0x\w+")]
|
||||
private static partial Regex ReplaceRegex();
|
||||
[GeneratedRegex("\\d{2,}x\\d+")]
|
||||
[GeneratedRegex(@"\d{2,}x\d+")]
|
||||
private static partial Regex ResRegex();
|
||||
[GeneratedRegex("\\d+ kb\\/s")]
|
||||
[GeneratedRegex(@"\d+ kb\/s")]
|
||||
private static partial Regex BitrateRegex();
|
||||
[GeneratedRegex("(\\d+(\\.\\d+)?) fps")]
|
||||
[GeneratedRegex(@"(\d+(\.\d+)?) fps")]
|
||||
private static partial Regex FpsRegex();
|
||||
[GeneratedRegex("DOVI configuration record.*profile: (\\d).*compatibility id: (\\d)")]
|
||||
[GeneratedRegex(@"DOVI configuration record.*profile: (\d).*compatibility id: (\d)")]
|
||||
private static partial Regex DoViRegex();
|
||||
[GeneratedRegex("Duration.*?start: (\\d+\\.?\\d{0,3})")]
|
||||
[GeneratedRegex(@"Duration.*?start: (\d+\.?\d{0,3})")]
|
||||
private static partial Regex StartRegex();
|
||||
|
||||
public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file)
|
||||
|
|
|
@ -68,20 +68,16 @@ internal static class MergeUtil
|
|||
public static string[] PartialCombineMultipleFiles(string[] files)
|
||||
{
|
||||
var newFiles = new List<string>();
|
||||
int div = 0;
|
||||
if (files.Length <= 90000)
|
||||
div = 100;
|
||||
else
|
||||
div = 200;
|
||||
var div = files.Length <= 90000 ? 100 : 200;
|
||||
|
||||
string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T");
|
||||
int index = 0; // 序号
|
||||
var outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T");
|
||||
var index = 0; // 序号
|
||||
|
||||
// 按照div的容量分割为小数组
|
||||
string[][] li = Enumerable.Range(0, files.Count() / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray();
|
||||
var li = Enumerable.Range(0, files.Length / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray();
|
||||
foreach (var items in li)
|
||||
{
|
||||
if (!items.Any())
|
||||
if (items.Length == 0)
|
||||
continue;
|
||||
var output = outputName + index.ToString("0000") + ".ts";
|
||||
CombineMultipleFilesIntoSingleFile(items, output);
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
|
|||
|
||||
namespace N_m3u8DL_RE.Util;
|
||||
|
||||
internal static class OtherUtil
|
||||
internal static partial class OtherUtil
|
||||
{
|
||||
public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers)
|
||||
{
|
||||
|
@ -25,13 +25,9 @@ internal static class OtherUtil
|
|||
|
||||
private static readonly char[] InvalidChars = "34,60,62,124,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,58,42,63,92,47"
|
||||
.Split(',').Select(s => (char)int.Parse(s)).ToArray();
|
||||
public static string GetValidFileName(string input, string re = ".", bool filterSlash = false)
|
||||
public static string GetValidFileName(string input, string re = "_", bool filterSlash = false)
|
||||
{
|
||||
string title = input;
|
||||
foreach (char invalidChar in InvalidChars)
|
||||
{
|
||||
title = title.Replace(invalidChar.ToString(), re);
|
||||
}
|
||||
var title = InvalidChars.Aggregate(input, (current, invalidChar) => current.Replace(invalidChar.ToString(), re));
|
||||
if (filterSlash)
|
||||
{
|
||||
title = title.Replace("/", re);
|
||||
|
@ -98,7 +94,7 @@ internal static class OtherUtil
|
|||
/// <exception cref="ArgumentException"></exception>
|
||||
public static double ParseSeconds(string timeStr)
|
||||
{
|
||||
var pattern = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$");
|
||||
var pattern = TimeStrRegex();
|
||||
|
||||
var match = pattern.Match(timeStr);
|
||||
|
||||
|
@ -171,4 +167,7 @@ internal static class OtherUtil
|
|||
_ => throw new ArgumentException($"unknown format: {muxFormat}")
|
||||
};
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$")]
|
||||
private static partial Regex TimeStrRegex();
|
||||
}
|
|
@ -14,24 +14,22 @@ internal static class PipeUtil
|
|||
{
|
||||
return new NamedPipeServerStream(pipeName, PipeDirection.InOut);
|
||||
}
|
||||
else
|
||||
|
||||
var path = Path.Combine(Path.GetTempPath(), pipeName);
|
||||
using var p = new Process();
|
||||
p.StartInfo = new ProcessStartInfo()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), pipeName);
|
||||
using var p = new Process();
|
||||
p.StartInfo = new ProcessStartInfo()
|
||||
{
|
||||
FileName = "mkfifo",
|
||||
Arguments = path,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
};
|
||||
p.Start();
|
||||
p.WaitForExit();
|
||||
Thread.Sleep(200);
|
||||
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
||||
}
|
||||
FileName = "mkfifo",
|
||||
Arguments = path,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
};
|
||||
p.Start();
|
||||
p.WaitForExit();
|
||||
Thread.Sleep(200);
|
||||
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
||||
}
|
||||
|
||||
public static async Task<bool> StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath)
|
||||
|
@ -77,7 +75,7 @@ internal static class PipeUtil
|
|||
|
||||
if (!string.IsNullOrEmpty(customDest))
|
||||
{
|
||||
if (customDest.Trim().StartsWith("-"))
|
||||
if (customDest.Trim().StartsWith('-'))
|
||||
command.Append(customDest);
|
||||
else
|
||||
command.Append($" -f mpegts -shortest \"{customDest}\"");
|
||||
|
|
Loading…
Reference in New Issue