优化部分代码

This commit is contained in:
nilaoda 2024-12-01 00:30:53 +08:00
parent cacf9b0ff0
commit adbe376ae0
60 changed files with 1140 additions and 1368 deletions

View File

@ -1,9 +1,4 @@
using N_m3u8DL_RE.Common.Enum; 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; namespace N_m3u8DL_RE.Common.Entity;
@ -34,9 +29,6 @@ public class EncryptInfo
{ {
return m; return m;
} }
else return EncryptMethod.UNKNOWN;
{
return EncryptMethod.UNKNOWN;
}
} }
} }

View File

@ -3,5 +3,5 @@
// 主要处理 EXT-X-DISCONTINUITY // 主要处理 EXT-X-DISCONTINUITY
public class MediaPart public class MediaPart
{ {
public List<MediaSegment> MediaSegments { get; set; } = new List<MediaSegment>(); public List<MediaSegment> MediaSegments { get; set; } = [];
} }

View File

@ -17,7 +17,7 @@ public class MediaSegment
public bool IsEncrypted => EncryptInfo.Method != EncryptMethod.NONE; 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分段文件名 public string? NameFromVar { get; set; } // MPD分段文件名

View File

@ -3,17 +3,18 @@
public class Playlist public class Playlist
{ {
// 对应Url信息 // 对应Url信息
public string Url { get; set; } public string Url { get; set; } = string.Empty;
// 是否直播 // 是否直播
public bool IsLive { get; set; } = false; public bool IsLive { get; set; } = false;
// 直播刷新间隔毫秒默认15秒 // 直播刷新间隔毫秒默认15秒
public double RefreshIntervalMs { get; set; } = 15000; 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; } public double? TargetDuration { get; set; }
// INIT信息 // INIT信息
public MediaSegment? MediaInit { get; set; } public MediaSegment? MediaInit { get; set; }
// 分片信息 // 分片信息
public List<MediaPart> MediaParts { get; set; } = new List<MediaPart>(); public List<MediaPart> MediaParts { get; set; } = [];
} }

View File

@ -46,12 +46,12 @@ public class StreamSpec
/// <summary> /// <summary>
/// URL /// URL
/// </summary> /// </summary>
public string Url { get; set; } public string Url { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 原始URL /// 原始URL
/// </summary> /// </summary>
public string OriginalUrl { get; set; } public string OriginalUrl { get; set; } = string.Empty;
public Playlist? Playlist { get; set; } public Playlist? Playlist { get; set; }

View File

@ -11,10 +11,10 @@ public partial class WebVttSub
private static partial Regex TSValueRegex(); private static partial Regex TSValueRegex();
[GeneratedRegex("\\s")] [GeneratedRegex("\\s")]
private static partial Regex SplitRegex(); private static partial Regex SplitRegex();
[GeneratedRegex("<c\\..*?>([\\s\\S]*?)<\\/c>")] [GeneratedRegex(@"<c\..*?>([\s\S]*?)<\/c>")]
private static partial Regex VttClassRegex(); 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; public long MpegtsTimestamp { get; set; } = 0L;
/// <summary> /// <summary>
@ -71,47 +71,45 @@ public partial class WebVttSub
continue; 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 arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
var startTime = ConvertToTS(arr[0]); var startTime = ConvertToTS(arr[0]);
var endTime = ConvertToTS(arr[1]); var endTime = ConvertToTS(arr[1]);
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : ""; var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
webSub.Cues.Add(new SubCue() 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
{ {
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);
item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp); }
item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp); else
} {
else break;
{
break;
}
} }
} }
@ -127,7 +125,7 @@ public partial class WebVttSub
return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " ")); return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " "));
})).TrimEnd(); })).TrimEnd();
} }
else return text; return text;
} }
/// <summary> /// <summary>
@ -140,18 +138,17 @@ public partial class WebVttSub
FixTimestamp(webSub, this.MpegtsTimestamp); FixTimestamp(webSub, this.MpegtsTimestamp);
foreach (var item in webSub.Cues) 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相同则拼接 last.EndTime = item.EndTime;
var last = this.Cues.LastOrDefault(); }
if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload) else
{ {
last.EndTime = item.EndTime; this.Cues.Add(item);
}
else
{
this.Cues.Add(item);
}
} }
} }
return this; return this;
@ -173,10 +170,10 @@ public partial class WebVttSub
// 当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒而字幕起始却是2秒),才修复 // 当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒而字幕起始却是2秒),才修复
if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset) 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; subCue.StartTime += offset;
sub.Cues[i].EndTime += offset; subCue.EndTime += offset;
} }
} }
} }
@ -205,7 +202,7 @@ public partial class WebVttSub
str = parts.First(); str = parts.First();
} }
var t = str.Split(':').Reverse().ToList(); 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; time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
} }
@ -214,7 +211,7 @@ public partial class WebVttSub
public override string ToString() public override string ToString()
{ {
StringBuilder sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var c in GetCues()) // 输出时去除空串 foreach (var c in GetCues()) // 输出时去除空串
{ {
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings); sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);

View File

@ -1,10 +1,5 @@
using System; using System.Text.Json;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.JsonConverter; namespace N_m3u8DL_RE.Common.JsonConverter;

View File

@ -4,7 +4,7 @@ using Spectre.Console;
namespace N_m3u8DL_RE.Common.Log; namespace N_m3u8DL_RE.Common.Log;
public class NonAnsiWriter : TextWriter public partial class NonAnsiWriter : TextWriter
{ {
public override Encoding Encoding => Console.OutputEncoding; public override Encoding Encoding => Console.OutputEncoding;
@ -28,15 +28,22 @@ public class NonAnsiWriter : TextWriter
private void RemoveAnsiEscapeSequences(string? input) private void RemoveAnsiEscapeSequences(string? input)
{ {
// Use regular expression to remove ANSI escape sequences // Use regular expression to remove ANSI escape sequences
string output = Regex.Replace(input ?? "", @"\x1B\[(\d+;?)+m", ""); var output = MyRegex().Replace(input ?? "", "");
output = Regex.Replace(output, @"\[\??\d+[AKlh]", ""); output = MyRegex1().Replace(output, "");
output = Regex.Replace(output,"[\r\n] +",""); output = MyRegex2().Replace(output, "");
if (string.IsNullOrWhiteSpace(output)) if (string.IsNullOrWhiteSpace(output))
{ {
return; return;
} }
Console.Write(output); 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> /// <summary>

View File

@ -1,10 +1,4 @@
using System; namespace N_m3u8DL_RE.Common.Log;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Log;
public enum LogLevel public enum LogLevel
{ {

View File

@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.Common.Log; namespace N_m3u8DL_RE.Common.Log;
public partial class Logger public static partial class Logger
{ {
[GeneratedRegex("{}")] [GeneratedRegex("{}")]
private static partial Regex VarsRepRegex(); private static partial Regex VarsRepRegex();
@ -80,24 +80,23 @@ public partial class Logger
Console.WriteLine(subWrite); 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))
{ {
// 进入写入 sw.WriteLine(plain);
LogWriteLock.EnterWriteLock();
using (StreamWriter sw = File.AppendText(LogFilePath))
{
sw.WriteLine(plain);
}
}
finally
{
// 释放占用
LogWriteLock.ExitWriteLock();
} }
} }
finally
{
// 释放占用
LogWriteLock.ExitWriteLock();
}
} }
catch (Exception) catch (Exception)
{ {
@ -117,82 +116,74 @@ public partial class Logger
public static void Info(string data, params object[] ps) public static void Info(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.INFO) if (LogLevel < LogLevel.INFO) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : "; var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
HandleLog(write, data); HandleLog(write, data);
}
} }
public static void InfoMarkUp(string data, params object[] ps) public static void InfoMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.INFO) if (LogLevel < LogLevel.INFO) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data; var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
HandleLog(write); HandleLog(write);
}
} }
public static void Debug(string data, params object[] ps) public static void Debug(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.DEBUG) if (LogLevel < LogLevel.DEBUG) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: "; var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
HandleLog(write, data); HandleLog(write, data);
}
} }
public static void DebugMarkUp(string data, params object[] ps) public static void DebugMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.DEBUG) if (LogLevel < LogLevel.DEBUG) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data; var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
HandleLog(write); HandleLog(write);
}
} }
public static void Warn(string data, params object[] ps) public static void Warn(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.WARN) if (LogLevel < LogLevel.WARN) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : "; var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
HandleLog(write, data); HandleLog(write, data);
}
} }
public static void WarnMarkUp(string data, params object[] ps) public static void WarnMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.WARN) if (LogLevel < LogLevel.WARN) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data; var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
HandleLog(write); HandleLog(write);
}
} }
public static void Error(string data, params object[] ps) public static void Error(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.ERROR) if (LogLevel < LogLevel.ERROR) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: "; var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
HandleLog(write, data); HandleLog(write, data);
}
} }
public static void ErrorMarkUp(string data, params object[] ps) public static void ErrorMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.ERROR) if (LogLevel < LogLevel.ERROR) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data; var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
HandleLog(write); HandleLog(write);
}
} }
public static void ErrorMarkUp(Exception exception) public static void ErrorMarkUp(Exception exception)
@ -213,24 +204,23 @@ public partial class Logger
/// <param name="ps"></param> /// <param name="ps"></param>
public static void Extra(string data, params object[] ps) 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(); LogWriteLock.EnterWriteLock();
try using (StreamWriter sw = File.AppendText(LogFilePath))
{ {
// 进入写入 sw.WriteLine(plain, Encoding.UTF8);
LogWriteLock.EnterWriteLock();
using (StreamWriter sw = File.AppendText(LogFilePath))
{
sw.WriteLine(plain, Encoding.UTF8);
}
}
finally
{
// 释放占用
LogWriteLock.ExitWriteLock();
} }
} }
finally
{
// 释放占用
LogWriteLock.ExitWriteLock();
}
} }
} }

View File

@ -2,7 +2,7 @@
public static class ResString 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 readonly string ReLiveTs = "<RE_LIVE_TS>";
public static string singleFileRealtimeDecryptWarn => GetText("singleFileRealtimeDecryptWarn"); public static string singleFileRealtimeDecryptWarn => GetText("singleFileRealtimeDecryptWarn");
@ -140,13 +140,11 @@ public static class ResString
private static string GetText(string key) 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...>"; return "<...LANG TEXT MISSING...>";
if (CurrentLoc == "zh-CN" || CurrentLoc == "zh-SG" || CurrentLoc == "zh-Hans") if (CurrentLoc is "zh-CN" or "zh-SG" or "zh-Hans")
return StaticText.LANG_DIC[key].ZH_CN; return textObj.ZH_CN;
if (CurrentLoc.StartsWith("zh-")) return CurrentLoc.StartsWith("zh-") ? textObj.ZH_TW : textObj.EN_US;
return StaticText.LANG_DIC[key].ZH_TW;
return StaticText.LANG_DIC[key].EN_US;
} }
} }

View File

@ -1,14 +1,8 @@
using System; namespace N_m3u8DL_RE.Common.Resource;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Resource; internal static class StaticText
internal class StaticText
{ {
public static Dictionary<string, TextContainer> LANG_DIC = new() public static readonly Dictionary<string, TextContainer> LANG_DIC = new()
{ {
["singleFileSplitWarn"] = new TextContainer ["singleFileSplitWarn"] = new TextContainer
( (
@ -228,9 +222,9 @@ internal class StaticText
), ),
["cmd_decryptionBinaryPath"] = new TextContainer ["cmd_decryptionBinaryPath"] = new TextContainer
( (
zhCN: "MP4解密所用工具的全路径, 例如 C:\\Tools\\mp4decrypt.exe", zhCN: @"MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe",
zhTW: "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" enUS: @"Full path to the tool used for MP4 decryption, like C:\Tools\mp4decrypt.exe"
), ),
["cmd_delAfterDone"] = new TextContainer ["cmd_delAfterDone"] = new TextContainer
( (
@ -240,15 +234,15 @@ internal class StaticText
), ),
["cmd_ffmpegBinaryPath"] = new TextContainer ["cmd_ffmpegBinaryPath"] = new TextContainer
( (
zhCN: "ffmpeg可执行程序全路径, 例如 C:\\Tools\\ffmpeg.exe", zhCN: @"ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe",
zhTW: "ffmpeg可執行程序全路徑, 例如 C:\\Tools\\ffmpeg.exe", zhTW: @"ffmpeg可執行程序全路徑, 例如 C:\Tools\ffmpeg.exe",
enUS: "Full path to the ffmpeg binary, like C:\\Tools\\ffmpeg.exe" enUS: @"Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe"
), ),
["cmd_mkvmergeBinaryPath"] = new TextContainer ["cmd_mkvmergeBinaryPath"] = new TextContainer
( (
zhCN: "mkvmerge可执行程序全路径, 例如 C:\\Tools\\mkvmerge.exe", zhCN: @"mkvmerge可执行程序全路径, 例如 C:\Tools\mkvmerge.exe",
zhTW: "mkvmerge可執行程序全路徑, 例如 C:\\Tools\\mkvmerge.exe", zhTW: @"mkvmerge可執行程序全路徑, 例如 C:\Tools\mkvmerge.exe",
enUS: "Full path to the mkvmerge binary, like C:\\Tools\\mkvmerge.exe" enUS: @"Full path to the mkvmerge binary, like C:\Tools\mkvmerge.exe"
), ),
["cmd_liveFixVttByAudio"] = new TextContainer ["cmd_liveFixVttByAudio"] = new TextContainer
( (

View File

@ -67,8 +67,7 @@ public static class GlobalUtil
{ {
var fileExt = OperatingSystem.IsWindows() ? ".exe" : ""; var fileExt = OperatingSystem.IsWindows() ? ".exe" : "";
var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) }; var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) };
var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
Array.Empty<string>();
return searchPath.Concat(envPath).Select(p => Path.Combine(p!, name + fileExt)).FirstOrDefault(File.Exists); return searchPath.Concat(envPath).Select(p => Path.Combine(p!, name + fileExt)).FirstOrDefault(File.Exists);
} }
} }

View File

@ -99,7 +99,7 @@ public static class HTTPUtil
private static bool CheckMPEG2TS(HttpResponseMessage? webResponse) private static bool CheckMPEG2TS(HttpResponseMessage? webResponse)
{ {
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower(); 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> /// <summary>

View File

@ -7,11 +7,11 @@ namespace N_m3u8DL_RE.Parser.Config;
public class ParserConfig 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(); public Dictionary<string, string> CustomParserArgs { get; } = new();

View File

@ -1,15 +1,9 @@
using System; namespace N_m3u8DL_RE.Parser.Constants;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants; internal static class DASHTags
internal class DASHTags
{ {
public static string TemplateRepresentationID = "$RepresentationID$"; public const string TemplateRepresentationID = "$RepresentationID$";
public static string TemplateBandwidth = "$Bandwidth$"; public const string TemplateBandwidth = "$Bandwidth$";
public static string TemplateNumber = "$Number$"; public const string TemplateNumber = "$Number$";
public static string TemplateTime = "$Time$"; public const string TemplateTime = "$Time$";
} }

View File

@ -1,37 +1,31 @@
using System; namespace N_m3u8DL_RE.Parser.Constants;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants; internal static class HLSTags
internal class HLSTags
{ {
public static string ext_m3u = "#EXTM3U"; public const string ext_m3u = "#EXTM3U";
public static string ext_x_targetduration = "#EXT-X-TARGETDURATION"; public const string ext_x_targetduration = "#EXT-X-TARGETDURATION";
public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"; public const string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE";
public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"; public const string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE";
public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"; public const string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME";
public static string ext_x_media = "#EXT-X-MEDIA"; public const string ext_x_media = "#EXT-X-MEDIA";
public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"; public const string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE";
public static string ext_x_key = "#EXT-X-KEY"; public const string ext_x_key = "#EXT-X-KEY";
public static string ext_x_stream_inf = "#EXT-X-STREAM-INF"; public const string ext_x_stream_inf = "#EXT-X-STREAM-INF";
public static string ext_x_version = "#EXT-X-VERSION"; public const string ext_x_version = "#EXT-X-VERSION";
public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"; public const string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE";
public static string ext_x_endlist = "#EXT-X-ENDLIST"; public const string ext_x_endlist = "#EXT-X-ENDLIST";
public static string extinf = "#EXTINF"; public const string extinf = "#EXTINF";
public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"; public const string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY";
public static string ext_x_byterange = "#EXT-X-BYTERANGE"; public const string ext_x_byterange = "#EXT-X-BYTERANGE";
public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"; public const string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF";
public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY"; public const string ext_x_discontinuity = "#EXT-X-DISCONTINUITY";
public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT"; public const string ext_x_cue_out_start = "#EXT-X-CUE-OUT";
public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT"; public const string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT";
public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"; public const string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS";
public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35"; public const string ext_x_scte35 = "#EXT-OATCLS-SCTE35";
public static string ext_x_cue_start = "#EXT-X-CUE-OUT"; public const string ext_x_cue_start = "#EXT-X-CUE-OUT";
public static string ext_x_cue_end = "#EXT-X-CUE-IN"; public const string ext_x_cue_end = "#EXT-X-CUE-IN";
public static string ext_x_cue_span = "#EXT-X-CUE-SPAN"; public const string ext_x_cue_span = "#EXT-X-CUE-SPAN";
public static string ext_x_map = "#EXT-X-MAP"; public const string ext_x_map = "#EXT-X-MAP";
public static string ext_x_start = "#EXT-X-START"; public const string ext_x_start = "#EXT-X-START";
} }

View File

@ -1,15 +1,9 @@
using System; namespace N_m3u8DL_RE.Parser.Constants;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants; internal static class MSSTags
internal class MSSTags
{ {
public static string Bitrate = "{Bitrate}"; public const string Bitrate = "{Bitrate}";
public static string Bitrate_BK = "{bitrate}"; public const string Bitrate_BK = "{bitrate}";
public static string StartTime = "{start_time}"; public const string StartTime = "{start_time}";
public static string StartTime_BK = "{start time}"; public const string StartTime_BK = "{start time}";
} }

View File

@ -4,19 +4,14 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
namespace N_m3u8DL_RE.Parser.Extractor; namespace N_m3u8DL_RE.Parser.Extractor;
// https://blog.csdn.net/leek5533/article/details/117750191 // https://blog.csdn.net/leek5533/article/details/117750191
internal class DASHExtractor2 : IExtractor internal partial class DASHExtractor2 : IExtractor
{ {
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC; private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;
@ -37,10 +32,7 @@ internal class DASHExtractor2 : IExtractor
private void SetInitUrl() private void SetInitUrl()
{ {
this.MpdUrl = ParserConfig.Url ?? string.Empty; this.MpdUrl = ParserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.MpdUrl;
this.BaseUrl = ParserConfig.BaseUrl;
else
this.BaseUrl = this.MpdUrl;
} }
private string ExtendBaseUrl(XElement element, string oriBaseUrl) private string ExtendBaseUrl(XElement element, string oriBaseUrl)
@ -57,13 +49,11 @@ internal class DASHExtractor2 : IExtractor
private double? GetFrameRate(XElement element) private double? GetFrameRate(XElement element)
{ {
var frameRate = element.Attribute("frameRate")?.Value; var frameRate = element.Attribute("frameRate")?.Value;
if (frameRate != null && frameRate.Contains('/')) if (frameRate == null || !frameRate.Contains('/')) return null;
{
var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]); var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]);
frameRate = d.ToString("0.000"); frameRate = d.ToString("0.000");
return Convert.ToDouble(frameRate); return Convert.ToDouble(frameRate);
}
return null;
} }
public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText) public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
@ -180,7 +170,7 @@ internal class DASHExtractor2 : IExtractor
streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null; 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; streamSpec.MediaType = MediaType.SUBTITLES;
} }
@ -493,7 +483,7 @@ internal class DASHExtractor2 : IExtractor
else else
{ {
// 修复mp4类型字幕 // 修复mp4类型字幕
if (streamSpec.MediaType == MediaType.SUBTITLES && streamSpec.Extension == "mp4") if (streamSpec is { MediaType: MediaType.SUBTITLES, Extension: "mp4" })
{ {
streamSpec.Extension = "m4s"; streamSpec.Extension = "m4s";
} }
@ -513,20 +503,17 @@ internal class DASHExtractor2 : IExtractor
} }
// 为视频设置默认轨道 // 为视频设置默认轨道
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO); var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES); var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();
foreach (var item in streamList) 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;
{ }
item.AudioId = aL.OrderByDescending(x => x.Bandwidth).First().GroupId; if (sL.Count != 0)
} {
if (sL.Any()) item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId;
{
item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId;
}
} }
} }
@ -541,8 +528,7 @@ internal class DASHExtractor2 : IExtractor
private string? FilterLanguage(string? v) private string? FilterLanguage(string? v)
{ {
if (v == null) return null; if (v == null) return null;
if (Regex.IsMatch(v, "^[\\w_\\-\\d]+$")) return v; return LangCodeRegex().IsMatch(v) ? v : "und";
return "und";
} }
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs) public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
@ -581,22 +567,21 @@ internal class DASHExtractor2 : IExtractor
private Task ProcessUrlAsync(List<StreamSpec> streamSpecs) private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
{ {
for (int i = 0; i < streamSpecs.Count; i++) foreach (var streamSpec in streamSpecs)
{ {
var playlist = streamSpecs[i].Playlist; var playlist = streamSpec.Playlist;
if (playlist != null) 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); mediaSegment.Url = PreProcessUrl(mediaSegment.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);
}
} }
} }
} }
@ -633,4 +618,7 @@ internal class DASHExtractor2 : IExtractor
} }
} }
} }
[GeneratedRegex(@"^[\w_\-\d]+$")]
private static partial Regex LangCodeRegex();
} }

View File

@ -5,12 +5,6 @@ using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Util;
using N_m3u8DL_RE.Parser.Constants; 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; using N_m3u8DL_RE.Common.Util;
namespace N_m3u8DL_RE.Parser.Extractor; namespace N_m3u8DL_RE.Parser.Extractor;
@ -26,8 +20,6 @@ internal class HLSExtractor : IExtractor
public ParserConfig ParserConfig { get; set; } public ParserConfig ParserConfig { get; set; }
private HLSExtractor() { }
public HLSExtractor(ParserConfig parserConfig) public HLSExtractor(ParserConfig parserConfig)
{ {
this.ParserConfig = parserConfig; this.ParserConfig = parserConfig;
@ -37,14 +29,7 @@ internal class HLSExtractor : IExtractor
private void SetBaseUrl() private void SetBaseUrl()
{ {
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.M3u8Url;
{
this.BaseUrl = ParserConfig.BaseUrl;
}
else
{
this.BaseUrl = this.M3u8Url;
}
} }
/// <summary> /// <summary>
@ -87,7 +72,7 @@ internal class HLSExtractor : IExtractor
{ {
MasterM3u8Flag = true; MasterM3u8Flag = true;
List<StreamSpec> streams = new List<StreamSpec>(); List<StreamSpec> streams = [];
using StringReader sr = new StringReader(M3u8Content); using StringReader sr = new StringReader(M3u8Content);
string? line; string? line;
@ -201,7 +186,7 @@ internal class HLSExtractor : IExtractor
streams.Add(streamSpec); streams.Add(streamSpec);
} }
else if (line.StartsWith("#")) else if (line.StartsWith('#'))
{ {
continue; continue;
} }
@ -237,22 +222,22 @@ internal class HLSExtractor : IExtractor
long startIndex; long startIndex;
Playlist playlist = new(); Playlist playlist = new();
List<MediaPart> mediaParts = new(); List<MediaPart> mediaParts = [];
// 当前的加密信息 // 当前的加密信息
EncryptInfo currentEncryptInfo = new(); EncryptInfo currentEncryptInfo = new();
if (ParserConfig.CustomMethod != null) if (ParserConfig.CustomMethod != null)
currentEncryptInfo.Method = ParserConfig.CustomMethod.Value; currentEncryptInfo.Method = ParserConfig.CustomMethod.Value;
if (ParserConfig.CustomeKey != null && ParserConfig.CustomeKey.Length > 0) if (ParserConfig.CustomeKey is { Length: > 0 })
currentEncryptInfo.Key = ParserConfig.CustomeKey; currentEncryptInfo.Key = ParserConfig.CustomeKey;
if (ParserConfig.CustomeIV != null && ParserConfig.CustomeIV.Length > 0) if (ParserConfig.CustomeIV is { Length: > 0 })
currentEncryptInfo.IV = ParserConfig.CustomeIV; currentEncryptInfo.IV = ParserConfig.CustomeIV;
// 上次读取到的加密行,#EXT-X-KEY:…… // 上次读取到的加密行,#EXT-X-KEY:……
string lastKeyLine = ""; string lastKeyLine = "";
MediaPart mediaPart = new(); MediaPart mediaPart = new();
MediaSegment segment = new(); MediaSegment segment = new();
List<MediaSegment> segments = new(); List<MediaSegment> segments = [];
while ((line = sr.ReadLine()) != null) while ((line = sr.ReadLine()) != null)
@ -304,20 +289,19 @@ internal class HLSExtractor : IExtractor
// 修复YK去除广告后的遗留问题 // 修复YK去除广告后的遗留问题
if (hasAd && mediaParts.Count > 0) if (hasAd && mediaParts.Count > 0)
{ {
segments = mediaParts[mediaParts.Count - 1].MediaSegments; segments = mediaParts[^1].MediaSegments;
mediaParts.RemoveAt(mediaParts.Count - 1); mediaParts.RemoveAt(mediaParts.Count - 1);
hasAd = false; hasAd = false;
continue; continue;
} }
// 常规情况的#EXT-X-DISCONTINUITY标记新建part // 常规情况的#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,
{ });
MediaSegments = segments, segments = new();
});
segments = new();
}
} }
// 解析KEY // 解析KEY
else if (line.StartsWith(HLSTags.ext_x_key)) else if (line.StartsWith(HLSTags.ext_x_key))
@ -382,13 +366,11 @@ internal class HLSExtractor : IExtractor
playlist.MediaInit.ExpectLength = n; playlist.MediaInit.ExpectLength = n;
playlist.MediaInit.StartRange = o ?? 0L; playlist.MediaInit.StartRange = o ?? 0L;
} }
// 是否有加密有的话写入KEY和IV if (currentEncryptInfo.Method == EncryptMethod.NONE) continue;
if (currentEncryptInfo.Method != EncryptMethod.NONE) // 有加密的话写入KEY和IV
{ playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method;
playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method; playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key;
playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key; playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));
playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));
}
} }
// 遇到了其他的map说明已经不是一个视频了全部丢弃即可 // 遇到了其他的map说明已经不是一个视频了全部丢弃即可
else 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; else if (line.StartsWith("\r\n")) continue;
// 解析分片的地址 // 解析分片的地址
@ -488,15 +470,15 @@ internal class HLSExtractor : IExtractor
} }
var playlist = await ParseListAsync(); var playlist = await ParseListAsync();
return new List<StreamSpec> return
{ [
new() new()
{ {
Url = ParserConfig.Url, Url = ParserConfig.Url,
Playlist = playlist, Playlist = playlist,
Extension = playlist.MediaInit != null ? "mp4" : "ts" Extension = playlist.MediaInit != null ? "mp4" : "ts"
} }
}; ];
} }
private async Task LoadM3u8FromUrlAsync(string url) private async Task LoadM3u8FromUrlAsync(string url)
@ -539,11 +521,10 @@ internal class HLSExtractor : IExtractor
foreach (var l in lists) foreach (var l in lists)
{ {
var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()).ToList(); var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()).ToList();
if (match.Any()) if (match.Count == 0) continue;
{
Logger.DebugMarkUp($"{l.Url} => {match.First().Url}"); Logger.DebugMarkUp($"{l.Url} => {match.First().Url}");
l.Url = match.First().Url; l.Url = match.First().Url;
}
} }
} }
@ -556,7 +537,7 @@ internal class HLSExtractor : IExtractor
// 直接重新加载m3u8 // 直接重新加载m3u8
await LoadM3u8FromUrlAsync(lists[i].Url!); 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..."); Logger.WarnMarkUp("Can not load m3u8. Try refreshing url from master url...");
// 当前URL无法加载 尝试从Master链接中刷新URL // 当前URL无法加载 尝试从Master链接中刷新URL

View File

@ -1,10 +1,5 @@
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity; 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; using N_m3u8DL_RE.Common.Enum;
namespace N_m3u8DL_RE.Parser.Extractor; namespace N_m3u8DL_RE.Parser.Extractor;

View File

@ -6,14 +6,7 @@ using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Mp4; using N_m3u8DL_RE.Parser.Mp4;
using N_m3u8DL_RE.Parser.Util; 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.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
namespace N_m3u8DL_RE.Parser.Extractor; namespace N_m3u8DL_RE.Parser.Extractor;
@ -46,10 +39,7 @@ internal partial class MSSExtractor : IExtractor
private void SetInitUrl() private void SetInitUrl()
{ {
this.IsmUrl = ParserConfig.Url ?? string.Empty; this.IsmUrl = ParserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.IsmUrl;
this.BaseUrl = ParserConfig.BaseUrl;
else
this.BaseUrl = this.IsmUrl;
} }
public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText) 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 aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES); var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();
foreach (var item in streamList) 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;
{ }
item.AudioId = aL.First().GroupId; if (sL.Count != 0)
} {
if (sL.Any()) item.SubtitleId = sL.First().GroupId;
{
item.SubtitleId = sL.First().GroupId;
}
} }
} }
@ -318,22 +305,21 @@ internal partial class MSSExtractor : IExtractor
private Task ProcessUrlAsync(List<StreamSpec> streamSpecs) private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
{ {
for (int i = 0; i < streamSpecs.Count; i++) foreach (var streamSpec in streamSpecs)
{ {
var playlist = streamSpecs[i].Playlist; var playlist = streamSpec.Playlist;
if (playlist != null) 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); segment.Url = PreProcessUrl(segment.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);
}
} }
} }
} }

View File

@ -1,70 +1,62 @@
using System; namespace Mp4SubtitleParser;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mp4SubtitleParser // make BinaryReader in Big Endian
class BinaryReader2 : BinaryReader
{ {
// make BinaryReader in Big Endian public BinaryReader2(System.IO.Stream stream) : base(stream) { }
class BinaryReader2 : BinaryReader
public bool HasMoreData()
{ {
public BinaryReader2(System.IO.Stream stream) : base(stream) { } return BaseStream.Position < BaseStream.Length;
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);
}
} }
}
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);
}
}

View File

@ -1,89 +1,83 @@
using System; using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser;
// make BinaryWriter in Big Endian
class BinaryWriter2 : BinaryWriter
{ {
// make BinaryWriter in Big Endian private static bool IsLittleEndian = BitConverter.IsLittleEndian;
class BinaryWriter2 : BinaryWriter public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
public void WriteUInt(decimal n, int offset = 0)
{ {
private static bool IsLittleEndian = BitConverter.IsLittleEndian; var arr = BitConverter.GetBytes((uint)n);
public BinaryWriter2(System.IO.Stream stream) : base(stream) { } if (IsLittleEndian)
Array.Reverse(arr);
if (offset != 0)
public void WriteUInt(decimal n, int offset = 0) arr = arr[offset..];
{ 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);
}
} }
}
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);
}
}

View File

@ -1,5 +1,4 @@
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using System.Security.Cryptography;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser
{ {
@ -11,10 +10,10 @@ namespace Mp4SubtitleParser
public bool isMultiDRM; 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_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_PLAYREADY = [0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95];
public static ParsedMP4Info ReadInit(byte[] data) public static ParsedMP4Info ReadInit(byte[] data)
{ {
@ -28,22 +27,20 @@ namespace Mp4SubtitleParser
.Box("minf", MP4Parser.Children) .Box("minf", MP4Parser.Children)
.Box("stbl", MP4Parser.Children) .Box("stbl", MP4Parser.Children)
.FullBox("stsd", MP4Parser.SampleDescription) .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"); throw new Exception("PSSH version can only be 0 or 1");
var systemId = box.Reader.ReadBytes(16); var systemId = box.Reader.ReadBytes(16);
if (SYSTEM_ID_WIDEVINE.SequenceEqual(systemId)) if (!SYSTEM_ID_WIDEVINE.SequenceEqual(systemId)) return;
{
var dataSize = box.Reader.ReadUInt32(); var dataSize = box.Reader.ReadUInt32();
var psshData = box.Reader.ReadBytes((int)dataSize); var psshData = box.Reader.ReadBytes((int)dataSize);
info.PSSH = Convert.ToBase64String(psshData); info.PSSH = Convert.ToBase64String(psshData);
if (info.KID == "00000000000000000000000000000000") if (info.KID != "00000000000000000000000000000000") return;
{
info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower(); info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower();
info.isMultiDRM = true; info.isMultiDRM = true;
}
}
}) })
.FullBox("encv", MP4Parser.AllData(data => ReadBox(data, info))) .FullBox("encv", MP4Parser.AllData(data => ReadBox(data, info)))
.FullBox("enca", 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) private static void ReadBox(byte[] data, ParsedMP4Info info)
{ {
// find schm // find schm
var schmBytes = new byte[4] { 0x73, 0x63, 0x68, 0x6d }; byte[] schmBytes = [0x73, 0x63, 0x68, 0x6d];
var schmIndex = 0; 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; schmIndex = i;
break; break;
@ -75,11 +72,11 @@ namespace Mp4SubtitleParser
// if (info.Scheme != "cenc") return; // if (info.Scheme != "cenc") return;
// find KID // find KID
var tencBytes = new byte[4] { 0x74, 0x65, 0x6E, 0x63 }; byte[] tencBytes = [0x74, 0x65, 0x6E, 0x6];
var tencIndex = -1; var tencIndex = -1;
for (int i = 0; i < data.Length - 4; i++) 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; tencIndex = i;
break; break;

View File

@ -9,12 +9,12 @@ namespace Mp4SubtitleParser
{ {
class ParsedBox class ParsedBox
{ {
public MP4Parser Parser { get; set; } public required MP4Parser Parser { get; set; }
public bool PartialOkay { get; set; } public bool PartialOkay { get; set; }
public long Start { get; set; } public long Start { get; set; }
public uint Version { get; set; } = 1000; public uint Version { get; set; } = 1000;
public uint Flags { 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; } public bool Has64BitSize { get; set; }
} }
@ -28,7 +28,7 @@ namespace Mp4SubtitleParser
class TRUN class TRUN
{ {
public uint SampleCount { get; set; } public uint SampleCount { get; set; }
public List<Sample> SampleData { get; set; } = new List<Sample>(); public List<Sample> SampleData { get; set; } = [];
} }
class Sample class Sample
@ -55,7 +55,7 @@ namespace Mp4SubtitleParser
public static BoxHandler AllData(DataHandler handler) public static BoxHandler AllData(DataHandler handler)
{ {
return (box) => return box =>
{ {
var all = box.Reader.GetLength() - box.Reader.GetPosition(); var all = box.Reader.GetLength() - box.Reader.GetPosition();
handler(box.Reader.ReadBytes((int)all)); handler(box.Reader.ReadBytes((int)all));
@ -161,7 +161,7 @@ namespace Mp4SubtitleParser
} }
int payloadSize = (int)(end - reader.GetPosition()); 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() var box = new ParsedBox()
{ {
Parser = this, Parser = this,

View File

@ -3,75 +3,229 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml; 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; } return obj is SubEntity entity &&
public string End { get; set; } Begin == entity.Begin &&
public string Region { get; set; } End == entity.End &&
public List<XmlElement> Contents { get; set; } = new(); Region == entity.Region &&
public List<string> ContentStrings { get; set; } = new(); ContentStrings.SequenceEqual(entity.ContentStrings);
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);
}
} }
public partial class MP4TtmlUtil public override int GetHashCode()
{ {
[GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")] return HashCode.Combine(Begin, End, Region, ContentStrings);
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) 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; var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index);
// parse init return $"{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}.{ts.Milliseconds:000}";
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) 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 ns = ((XmlElement)ttNode!).GetAttribute("xmlns");
{ nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture); nsMgr.AddNamespace("ns", ns);
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);
}
if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc; var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
var xmlDoc = new XmlDocument(); if (bodyNode == null)
XmlNamespaceManager? nsMgr = null; return xmlSrc;
xmlDoc.LoadXml(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; var ttNode = xmlDoc.LastChild;
if (nsMgr == null) if (nsMgr == null)
{ {
@ -79,306 +233,142 @@ namespace Mp4SubtitleParser
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable); nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsMgr.AddNamespace("ns", ns); nsMgr.AddNamespace("ns", ns);
} }
if (headNode == null)
headNode = ttNode!.SelectSingleNode("ns:head", nsMgr);
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr); var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
if (bodyNode == null) if (bodyNode == null)
return xmlSrc; continue;
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr); 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 // Parse <p> label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!) foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{ {
var _begin = _p.GetAttribute("begin"); var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end"); var _end = _p.GetAttribute("end");
var _region = _p.GetAttribute("region");
var _bgImg = _p.GetAttribute("smpte:backgroundImage");
// Handle namespace // Handle namespace
foreach (XmlAttribute attr in _p.Attributes) foreach (XmlAttribute attr in _p.Attributes)
{ {
if (attr.LocalName == "begin") _begin = attr.Value; if (attr.LocalName == "begin") _begin = attr.Value;
else if (attr.LocalName == "end") _end = attr.Value; else if (attr.LocalName == "end") _end = attr.Value;
else if (attr.LocalName == "region") _region = attr.Value;
} }
_p.SetAttribute("begin", Add(_begin)); var sub = new SubEntity
_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()); Begin = _begin,
} End = _end,
else if(item.NodeType == XmlNodeType.Element && item.Name == "br") Region = _region
};
if (string.IsNullOrEmpty(_bgImg))
{ {
sb.AppendLine(); var _spans = _p.ChildNodes;
} // Collect <span>
} foreach (XmlNode _node in _spans)
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) =>
{ {
sawMDAT = true; if (_node.NodeType == XmlNodeType.Element)
// Join this to any previous payload, in case the mp4 has multiple
// mdats.
if (segTimeMs != 0)
{ {
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data)); var _span = (XmlElement)_node;
foreach (var item in datas) if (string.IsNullOrEmpty(_span.InnerText))
{ continue;
xmls.Add(ShiftTime(item, segTimeMs, segIndex)); sub.Contents.Add(_span);
} sub.ContentStrings.Add(_span.OuterXml);
} }
else else if (_node.NodeType == XmlNodeType.Text)
{
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))
{ {
var _span = new XmlDocument().CreateElement("span"); var _span = new XmlDocument().CreateElement("span");
_span.InnerText = $"Base64::{imageDic[id]}"; _span.InnerText = _node.Value!;
sub.Contents.Add(_span); sub.Contents.Add(_span);
sub.ContentStrings.Add(_span.OuterXml); 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);
}
} }
} else
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)) var id = _bgImg.Replace("#", "");
if (imageDic.TryGetValue(id, out var value))
{ {
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique") var _span = new XmlDocument().CreateElement("span");
dic[key] = $"{dic[key]}\r\n<i>{GetTextFromElement(item)}</i>"; _span.InnerText = $"Base64::{value}";
else sub.Contents.Add(_span);
dic[key] = $"{dic[key]}\r\n{GetTextFromElement(item)}"; sub.ContentStrings.Add(_span.OuterXml);
}
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));
} }
} }
// 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);
} }
} }

View File

@ -3,7 +3,7 @@ using System.Text;
namespace Mp4SubtitleParser; namespace Mp4SubtitleParser;
public class MP4VttUtil public static class MP4VttUtil
{ {
public static (bool, uint) CheckInit(byte[] data) public static (bool, uint) CheckInit(byte[] data)
{ {
@ -15,16 +15,16 @@ public class MP4VttUtil
.Box("moov", MP4Parser.Children) .Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children) .Box("trak", MP4Parser.Children)
.Box("mdia", 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"); throw new Exception("MDHD version can only be 0 or 1");
timescale = MP4Parser.ParseMDHD(box.Reader, box.Version); timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
}) })
.Box("minf", MP4Parser.Children) .Box("minf", MP4Parser.Children)
.Box("stbl", MP4Parser.Children) .Box("stbl", MP4Parser.Children)
.FullBox("stsd", MP4Parser.SampleDescription) .FullBox("stsd", MP4Parser.SampleDescription)
.Box("wvtt", (box) => { .Box("wvtt", _ => {
// A valid vtt init segment, though we have no actual subtitles yet. // A valid vtt init segment, though we have no actual subtitles yet.
sawWVTT = true; sawWVTT = true;
}) })
@ -38,7 +38,7 @@ public class MP4VttUtil
if (timescale == 0) if (timescale == 0)
throw new Exception("Missing timescale for VTT content!"); throw new Exception("Missing timescale for VTT content!");
List<SubCue> cues = new(); List<SubCue> cues = [];
foreach (var item in files) foreach (var item in files)
{ {
@ -50,27 +50,27 @@ public class MP4VttUtil
byte[]? rawPayload = null; byte[]? rawPayload = null;
ulong baseTime = 0; ulong baseTime = 0;
ulong defaultDuration = 0; ulong defaultDuration = 0;
List<Sample> presentations = new(); List<Sample> presentations = [];
// parse media // parse media
new MP4Parser() new MP4Parser()
.Box("moof", MP4Parser.Children) .Box("moof", MP4Parser.Children)
.Box("traf", MP4Parser.Children) .Box("traf", MP4Parser.Children)
.FullBox("tfdt", (box) => .FullBox("tfdt", box =>
{ {
sawTFDT = true; 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"); throw new Exception("TFDT version can only be 0 or 1");
baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version); baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
}) })
.FullBox("tfhd", (box) => .FullBox("tfhd", box =>
{ {
if (box.Flags == 1000) if (box.Flags == 1000)
throw new Exception("A TFHD box should have a valid flags value"); throw new Exception("A TFHD box should have a valid flags value");
defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration; defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
}) })
.FullBox("trun", (box) => .FullBox("trun", box =>
{ {
sawTRUN = true; sawTRUN = true;
if (box.Version == 1000) if (box.Version == 1000)
@ -79,7 +79,7 @@ public class MP4VttUtil
throw new Exception("A TRUN box should have a valid flags value"); throw new Exception("A TRUN box should have a valid flags value");
presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData; presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
}) })
.Box("mdat", MP4Parser.AllData((data) => .Box("mdat", MP4Parser.AllData(data =>
{ {
if (sawMDAT) if (sawMDAT)
throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported"); 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 (payload != null)
{ {
if (timescale == 0)
throw new Exception("Timescale should not be zero!");
var cue = ParseVTTC( var cue = ParseVTTC(
payload, payload,
0 + (double)startTime / timescale, 0 + (double)startTime / timescale,
@ -192,15 +190,15 @@ public class MP4VttUtil
string id = string.Empty; string id = string.Empty;
string settings = string.Empty; string settings = string.Empty;
new MP4Parser() new MP4Parser()
.Box("payl", MP4Parser.AllData((data) => .Box("payl", MP4Parser.AllData(data =>
{ {
payload = Encoding.UTF8.GetString(data); payload = Encoding.UTF8.GetString(data);
})) }))
.Box("iden", MP4Parser.AllData((data) => .Box("iden", MP4Parser.AllData(data =>
{ {
id = Encoding.UTF8.GetString(data); id = Encoding.UTF8.GetString(data);
})) }))
.Box("sttg", MP4Parser.AllData((data) => .Box("sttg", MP4Parser.AllData(data =>
{ {
settings = Encoding.UTF8.GetString(data); settings = Encoding.UTF8.GetString(data);
})) }))

View File

@ -13,7 +13,7 @@ namespace N_m3u8DL_RE.Parser.Mp4;
public partial class MSSMoovProcessor public partial class MSSMoovProcessor
{ {
[GeneratedRegex("\\<KID\\>(.*?)\\<")] [GeneratedRegex(@"\<KID\>(.*?)\<")]
private static partial Regex KIDRegex(); private static partial Regex KIDRegex();
private static string StartCode = "00000001"; private static string StartCode = "00000001";
@ -23,7 +23,7 @@ public partial class MSSMoovProcessor
private string CodecPrivateData; private string CodecPrivateData;
private int Timesacle; private int Timesacle;
private long Duration; 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 Width => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First());
private int Height => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last()); private int Height => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last());
private string StreamType; private string StreamType;
@ -36,8 +36,8 @@ public partial class MSSMoovProcessor
private bool IsProtection; private bool IsProtection;
private string ProtectionSystemId; private string ProtectionSystemId;
private string ProtectionData; private string ProtectionData;
private string ProtecitonKID; private string? ProtecitonKID;
private string ProtecitonKID_PR; private string? ProtecitonKID_PR;
private byte[] UnityMatrix private byte[] UnityMatrix
{ {
get get
@ -60,10 +60,9 @@ public partial class MSSMoovProcessor
private static byte TRACK_IN_MOVIE = 0x2; private static byte TRACK_IN_MOVIE = 0x2;
private static byte TRACK_IN_PREVIEW = 0x4; private static byte TRACK_IN_PREVIEW = 0x4;
private static byte SELF_CONTAINED = 0x1; private static byte SELF_CONTAINED = 0x1;
private static List<string> SupportedFourCC = new()
{ private static List<string> SupportedFourCC =
"HVC1","HEV1","AACL","AACH","EC-3","H264","AVC1","DAVC","AVC1","TTML","DVHE","DVH1" ["HVC1", "HEV1", "AACL", "AACH", "EC-3", "H264", "AVC1", "DAVC", "AVC1", "TTML", "DVHE", "DVH1"];
};
public MSSMoovProcessor(StreamSpec streamSpec) 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 private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch
{ {
96000 => 0x0, 96000 => 0x0,
@ -169,8 +168,8 @@ public partial class MSSMoovProcessor
// save kid for playready // save kid for playready
this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes); this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes);
// fix byte order // fix byte order
var reverse1 = new byte[4] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] }; var reverse1 = new[] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] };
var reverse2 = new byte[4] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] }; var reverse2 = new[] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] };
Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length); Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length);
Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length); Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length);
this.ProtecitonKID = HexUtil.BytesToHex(kidBytes); this.ProtecitonKID = HexUtil.BytesToHex(kidBytes);
@ -217,13 +216,13 @@ public partial class MSSMoovProcessor
var schmPayload = new List<byte>(); var schmPayload = new List<byte>();
schmPayload.AddRange(Encoding.ASCII.GetBytes("cenc")); // scheme_type 'cenc' => common encryption 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()); var schmBox = FullBox("schm", 0, 0, schmPayload.ToArray());
sinfPayload.AddRange(schmBox); sinfPayload.AddRange(schmBox);
var tencPayload = new List<byte>(); var tencPayload = new List<byte>();
tencPayload.AddRange(new byte[] { 0, 0 }); tencPayload.AddRange([0, 0]);
tencPayload.Add(0x1); // default_IsProtected tencPayload.Add(0x1); // default_IsProtected
tencPayload.Add(0x8); // default_Per_Sample_IV_size tencPayload.Add(0x8); // default_Per_Sample_IV_size
tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); // default_KID tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); // default_KID
@ -368,7 +367,7 @@ public partial class MSSMoovProcessor
} }
else if (StreamType == "text") 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 else
{ {
@ -377,7 +376,7 @@ public partial class MSSMoovProcessor
var drefPayload = new List<byte>(); var drefPayload = new List<byte>();
drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); // entry count 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 var dinfPayload = FullBox("dref", 0, 0, drefPayload.ToArray()); // Data Reference Box
minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); // Data Information Box minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); // Data Information Box
@ -466,10 +465,7 @@ public partial class MSSMoovProcessor
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("enca", stream.ToArray()); // Encrypted Audio return Box("enca", stream.ToArray()); // Encrypted Audio
} }
else return Box("mp4a", stream.ToArray());
{
return Box("mp4a", stream.ToArray());
}
} }
if (FourCC == "EC-3") if (FourCC == "EC-3")
{ {
@ -479,10 +475,7 @@ public partial class MSSMoovProcessor
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("enca", stream.ToArray()); // Encrypted Audio return Box("enca", stream.ToArray()); // Encrypted Audio
} }
else return Box("ec-3", stream.ToArray());
{
return Box("ec-3", stream.ToArray());
}
} }
} }
else if (StreamType == "video") else if (StreamType == "video")
@ -507,11 +500,11 @@ public partial class MSSMoovProcessor
var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData); 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 arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7).First()); var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7));
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8).First()); var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8));
// make avcC // make avcC
var avcC = GetAvcC(sps, pps); var avcC = GetAvcC(sps, pps);
writer.Write(avcC); writer.Write(avcC);
@ -521,17 +514,14 @@ public partial class MSSMoovProcessor
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("encv", stream.ToArray()); // Encrypted Video 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 arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First()); var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First()); var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First()); var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));
// make hvcC // make hvcC
var hvcC = GetHvcC(sps, pps, vps); var hvcC = GetHvcC(sps, pps, vps);
writer.Write(hvcC); writer.Write(hvcC);
@ -541,18 +531,15 @@ public partial class MSSMoovProcessor
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("encv", stream.ToArray()); // Encrypted Video 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处理 // 杜比视界也按照hevc处理
else if (FourCC == "DVHE" || FourCC == "DVH1") if (FourCC is "DVHE" or "DVH1")
{ {
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First()); var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First()); var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First()); var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));
// make hvcC // make hvcC
var hvcC = GetHvcC(sps, pps, vps, "dvh1"); var hvcC = GetHvcC(sps, pps, vps, "dvh1");
writer.Write(hvcC); writer.Write(hvcC);
@ -562,15 +549,10 @@ public partial class MSSMoovProcessor
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("encv", stream.ToArray()); // Encrypted Video return Box("encv", stream.ToArray()); // Encrypted Video
} }
else return Box("dvh1", stream.ToArray()); // HEVC Simple Entry
{
return Box("dvh1", stream.ToArray()); // HEVC Simple Entry
}
}
else
{
throw new NotSupportedException();
} }
throw new NotSupportedException();
} }
else if (StreamType == "text") else if (StreamType == "text")
{ {
@ -581,10 +563,7 @@ public partial class MSSMoovProcessor
writer.Write("\0"); // auxilary mime types(??) writer.Write("\0"); // auxilary mime types(??)
return Box("stpp", stream.ToArray()); // TTML Simple Entry return Box("stpp", stream.ToArray()); // TTML Simple Entry
} }
else throw new NotSupportedException();
{
throw new NotSupportedException();
}
} }
else else
{ {
@ -635,7 +614,7 @@ public partial class MSSMoovProcessor
while (_reader.BaseStream.Position < _reader.BaseStream.Length) while (_reader.BaseStream.Position < _reader.BaseStream.Length)
{ {
encList.Add(_reader.ReadByte()); 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); encList.RemoveAt(encList.Count - 1);
} }
@ -805,7 +784,7 @@ public partial class MSSMoovProcessor
new MP4Parser() new MP4Parser()
.Box("moof", MP4Parser.Children) .Box("moof", MP4Parser.Children)
.Box("traf", MP4Parser.Children) .Box("traf", MP4Parser.Children)
.FullBox("tfhd", (box) => .FullBox("tfhd", box =>
{ {
TrackId = (int)box.Reader.ReadUInt32(); TrackId = (int)box.Reader.ReadUInt32();
}) })

View File

@ -1,10 +1,5 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; 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; namespace N_m3u8DL_RE.Parser.Processor;

View File

@ -1,11 +1,6 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config; 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; namespace N_m3u8DL_RE.Parser.Processor.DASH;
@ -18,11 +13,7 @@ public class DefaultDASHContentProcessor : ContentProcessor
{ {
if (extractorType != ExtractorType.MPEG_DASH) return false; if (extractorType != ExtractorType.MPEG_DASH) return false;
if (mpdContent.Contains("<mas:") && !mpdContent.Contains("xmlns:mas")) return mpdContent.Contains("<mas:") && !mpdContent.Contains("xmlns:mas");
{
return true;
}
return false;
} }
public override string Process(string mpdContent, ParserConfig parserConfig) public override string Process(string mpdContent, ParserConfig parserConfig)

View File

@ -1,12 +1,6 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config; 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; using System.Web;
namespace N_m3u8DL_RE.Parser.Processor; namespace N_m3u8DL_RE.Parser.Processor;
@ -17,29 +11,27 @@ public class DefaultUrlProcessor : UrlProcessor
public override string Process(string oriUrl, ParserConfig paserConfig) 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); if (newQuery.AllKeys.Contains(item))
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query); newQuery.Set(item, uriFromConfigQuery.Get(item));
else
var oldUri = new Uri(oriUrl); newQuery.Add(item, uriFromConfigQuery.Get(item));
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 (string.IsNullOrEmpty(newQuery.ToString())) return oriUrl;
Logger.Debug("Before: " + oriUrl);
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery).TrimEnd('?');
Logger.Debug("After: " + oriUrl);
return oriUrl; return oriUrl;
} }
} }

View File

@ -1,12 +1,7 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS; namespace N_m3u8DL_RE.Parser.Processor.HLS;
@ -16,13 +11,13 @@ public partial class DefaultHLSContentProcessor : ContentProcessor
private static partial Regex YkDVRegex(); private static partial Regex YkDVRegex();
[GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")] [GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPRegex(); 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(); private static partial Regex DNSPSubRegex();
[GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")] [GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
private static partial Regex OrderFixRegex(); private static partial Regex OrderFixRegex();
[GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")] [GeneratedRegex(@"#EXT-X-MAP.*\.apple\.com/")]
private static partial Regex ATVRegex(); 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(); private static partial Regex ATVRegex2();
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS; 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) public override string Process(string m3u8Content, ParserConfig parserConfig)
{ {
// 处理content以\r作为换行符的情况 // 处理content以\r作为换行符的情况
if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n")) if (m3u8Content.Contains('\r') && !m3u8Content.Contains('\n'))
{ {
m3u8Content = m3u8Content.Replace("\r", Environment.NewLine); m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
} }
@ -51,7 +46,7 @@ public partial class DefaultHLSContentProcessor : ContentProcessor
// 针对YK #EXT-X-VERSION:7杜比视界片源修正 // 针对YK #EXT-X-VERSION:7杜比视界片源修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode=")) if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
{ {
Regex ykmap = YkDVRegex(); var ykmap = YkDVRegex();
foreach (Match m in ykmap.Matches(m3u8Content)) 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}"); m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");

View File

@ -83,12 +83,11 @@ public class DefaultHLSKeyProcessor : KeyProcessor
encryptInfo.Method = EncryptMethod.UNKNOWN; 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; return encryptInfo;
} }

View File

@ -1,11 +1,6 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; 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; namespace N_m3u8DL_RE.Parser.Processor;

View File

@ -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.Entity;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
@ -19,11 +20,6 @@ public class StreamExtractor
public Dictionary<string, string> RawFiles { get; set; } = new(); // 存储(文件名,文件内容) public Dictionary<string, string> RawFiles { get; set; } = new(); // 存储(文件名,文件内容)
public StreamExtractor()
{
}
public StreamExtractor(ParserConfig parserConfig) public StreamExtractor(ParserConfig parserConfig)
{ {
this.parserConfig = parserConfig; this.parserConfig = parserConfig;
@ -54,7 +50,8 @@ public class StreamExtractor
LoadSourceFromText(this.rawText); LoadSourceFromText(this.rawText);
} }
public void LoadSourceFromText(string rawText) [MemberNotNull(nameof(this.rawText), nameof(this.extractor))]
private void LoadSourceFromText(string rawText)
{ {
var rawType = "txt"; var rawType = "txt";
rawText = rawText.Trim(); rawText = rawText.Trim();

View File

@ -3,9 +3,9 @@ using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.Parser.Util; 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(); private static partial Regex VarsNumberRegex();
/// <summary> /// <summary>
@ -33,8 +33,7 @@ public partial class ParserUtil
{ {
var startIndex = index + (key + "=").Length; var startIndex = index + (key + "=").Length;
var endIndex = startIndex + line[startIndex..].IndexOf(','); var endIndex = startIndex + line[startIndex..].IndexOf(',');
if (endIndex >= startIndex) result = line[startIndex..endIndex]; result = endIndex >= startIndex ? line[startIndex..endIndex] : line[startIndex..];
else result = line[startIndex..];
} }
return result; return result;
@ -49,18 +48,13 @@ public partial class ParserUtil
public static (long, long?) GetRange(string input) public static (long, long?) GetRange(string input)
{ {
var t = input.Split('@'); var t = input.Split('@');
if (t.Length > 0) return t.Length switch
{ {
if (t.Length == 1) <= 0 => (0, null),
{ 1 => (Convert.ToInt64(t[0]), null),
return (Convert.ToInt64(t[0]), null); 2 => (Convert.ToInt64(t[0]), Convert.ToInt64(t[1])),
} _ => (0, null)
if (t.Length == 2) };
{
return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
}
}
return (0, null);
} }
/// <summary> /// <summary>
@ -111,8 +105,8 @@ public partial class ParserUtil
if (string.IsNullOrEmpty(baseurl)) if (string.IsNullOrEmpty(baseurl))
return url; return url;
Uri uri1 = new Uri(baseurl); // 这里直接传完整的URL即可 var uri1 = new Uri(baseurl); // 这里直接传完整的URL即可
Uri uri2 = new Uri(uri1, url); var uri2 = new Uri(uri1, url);
url = uri2.ToString(); url = uri2.ToString();
return url; return url;

View File

@ -27,7 +27,7 @@ internal sealed class DownloadSpeedColumn : ProgressColumn
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var flag = task.IsFinished || !task.IsStarted; 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.MaxValue = (double)speedContainer.ResponseLength;
task.Value = speedContainer.RDownloaded; task.Value = speedContainer.RDownloaded;

View File

@ -25,9 +25,6 @@ internal class RecordingDurationColumn : ProgressColumn
{ {
if (_refreshedDurDic == null) if (_refreshedDurDic == null)
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified(); 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);
}
} }
} }

View File

@ -15,14 +15,16 @@ using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.CommandLine; 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)")] [GeneratedRegex("((best|worst)\\d*|all)")]
private static partial Regex ForStrRegex(); private static partial Regex ForStrRegex();
[GeneratedRegex("(\\d*)-(\\d*)")] [GeneratedRegex(@"(\d*)-(\d*)")]
private static partial Regex RangeRegex(); 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 Argument<string> Input = new(name: "input", description: ResString.cmd_Input);
private static readonly Option<string?> TmpDir = new(["--tmp-dir"], description: ResString.cmd_tmpDir); 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" }; private static readonly Option<StreamFilter?> DropSubtitleFilter = new(["-ds", "--drop-subtitle"], description: ResString.cmd_dropSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
/// <summary> /// <summary>
/// 解析录制直播时长限制 /// 解析下载速度限制
/// </summary> /// </summary>
/// <param name="result"></param> /// <param name="result"></param>
/// <returns></returns> /// <returns></returns>
private static long? ParseSpeedLimit(ArgumentResult result) private static long? ParseSpeedLimit(ArgumentResult result)
{ {
var input = result.Tokens.First().Value.ToUpper(); var input = result.Tokens[0].Value.ToUpper();
try try
{ {
var reg = new Regex("([\\d\\\\.]+)(M|K)"); var reg = SpeedStrRegex();
if (!reg.IsMatch(input)) throw new ArgumentException(); if (!reg.IsMatch(input)) throw new ArgumentException($"Invalid Speed Limit: {input}");
var number = double.Parse(reg.Match(input).Groups[1].Value); var number = double.Parse(reg.Match(input).Groups[1].Value);
if (reg.Match(input).Groups[2].Value == "M") if (reg.Match(input).Groups[2].Value == "M")
return (long)(number * 1024 * 1024); return (long)(number * 1024 * 1024);
else return (long)(number * 1024);
return (long)(number * 1024);
} }
catch (Exception) catch (Exception)
{ {
@ -143,7 +144,7 @@ internal partial class CommandInvoker
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
private static CustomRange? ParseCustomRange(ArgumentResult result) 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; // 支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00;
try try
{ {
@ -154,7 +155,7 @@ internal partial class CommandInvoker
if (arr.Length != 2) if (arr.Length != 2)
throw new ArgumentException("Bad format!"); throw new ArgumentException("Bad format!");
if (input.Contains(":")) if (input.Contains(':'))
{ {
return new CustomRange() return new CustomRange()
{ {
@ -163,7 +164,8 @@ internal partial class CommandInvoker
EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds, 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 left = RangeRegex().Match(input).Groups[1].Value;
var right = RangeRegex().Match(input).Groups[2].Value; var right = RangeRegex().Match(input).Groups[2].Value;
@ -192,7 +194,7 @@ internal partial class CommandInvoker
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
private static WebProxy? ParseProxy(ArgumentResult result) private static WebProxy? ParseProxy(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
@ -221,17 +223,16 @@ internal partial class CommandInvoker
/// <returns></returns> /// <returns></returns>
private static byte[]? ParseHLSCustomKey(ArgumentResult result) private static byte[]? ParseHLSCustomKey(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
return null; return null;
if (File.Exists(input)) if (File.Exists(input))
return File.ReadAllBytes(input); return File.ReadAllBytes(input);
else if (HexUtil.TryParseHexString(input, out byte[]? bytes)) if (HexUtil.TryParseHexString(input, out byte[]? bytes))
return bytes; return bytes;
else return Convert.FromBase64String(input);
return Convert.FromBase64String(input);
} }
catch (Exception) catch (Exception)
{ {
@ -247,7 +248,7 @@ internal partial class CommandInvoker
/// <returns></returns> /// <returns></returns>
private static TimeSpan? ParseLiveLimit(ArgumentResult result) private static TimeSpan? ParseLiveLimit(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
return OtherUtil.ParseDur(input); return OtherUtil.ParseDur(input);
@ -266,7 +267,7 @@ internal partial class CommandInvoker
/// <returns></returns> /// <returns></returns>
private static DateTime? ParseStartTime(ArgumentResult result) private static DateTime? ParseStartTime(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
CultureInfo provider = CultureInfo.InvariantCulture; CultureInfo provider = CultureInfo.InvariantCulture;
@ -281,7 +282,7 @@ internal partial class CommandInvoker
private static string? ParseSaveName(ArgumentResult result) private static string? ParseSaveName(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
var newName = OtherUtil.GetValidFileName(input); var newName = OtherUtil.GetValidFileName(input);
if (string.IsNullOrEmpty(newName)) if (string.IsNullOrEmpty(newName))
{ {
@ -299,7 +300,7 @@ internal partial class CommandInvoker
private static StreamFilter? ParseStreamFilter(ArgumentResult result) private static StreamFilter? ParseStreamFilter(ArgumentResult result)
{ {
var streamFilter = new StreamFilter(); var streamFilter = new StreamFilter();
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
var p = new ComplexParamParser(input); var p = new ComplexParamParser(input);
@ -437,7 +438,7 @@ internal partial class CommandInvoker
/// <returns></returns> /// <returns></returns>
private static MuxOptions? ParseMuxAfterDone(ArgumentResult result) private static MuxOptions? ParseMuxAfterDone(ArgumentResult result)
{ {
var v = result.Tokens.First().Value; var v = result.Tokens[0].Value;
var p = new ComplexParamParser(v); var p = new ComplexParamParser(v);
// 混流格式 // 混流格式
var format = p.GetValue("format") ?? v.Split(':')[0]; // 若未获取到,直接:前的字符串作为format解析 var format = p.GetValue("format") ?? v.Split(':')[0]; // 若未获取到,直接:前的字符串作为format解析
@ -577,14 +578,12 @@ internal partial class CommandInvoker
// 混流设置 // 混流设置
var muxAfterDoneValue = bindingContext.ParseResult.GetValueForOption(MuxAfterDone); var muxAfterDoneValue = bindingContext.ParseResult.GetValueForOption(MuxAfterDone);
if (muxAfterDoneValue != null) if (muxAfterDoneValue == null) return option;
{
option.MuxAfterDone = true; option.MuxAfterDone = true;
option.MuxOptions = muxAfterDoneValue; option.MuxOptions = muxAfterDoneValue;
if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath; if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath;
else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath; else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath;
}
return option; return option;
} }
@ -626,7 +625,7 @@ internal partial class CommandInvoker
}; };
rootCommand.TreatUnmatchedTokensAsErrors = true; 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) var parser = new CommandLineBuilder(rootCommand)
.UseDefaults() .UseDefaults()

View File

@ -4,7 +4,7 @@ namespace N_m3u8DL_RE.CommandLine;
internal class ComplexParamParser internal class ComplexParamParser
{ {
private string _arg; private readonly string _arg;
public ComplexParamParser(string arg) public ComplexParamParser(string arg)
{ {
_arg = arg; _arg = arg;
@ -16,7 +16,7 @@ internal class ComplexParamParser
try 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; if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;
var chars = _arg[(index + key.Length + 1)..].ToCharArray(); var chars = _arg[(index + key.Length + 1)..].ToCharArray();

View File

@ -2,7 +2,7 @@
namespace N_m3u8DL_RE.Crypto; namespace N_m3u8DL_RE.Crypto;
internal class AESUtil internal static class AESUtil
{ {
/// <summary> /// <summary>
/// AES-128解密解密后原地替换文件 /// AES-128解密解密后原地替换文件

View File

@ -2,7 +2,7 @@
namespace N_m3u8DL_RE.Crypto; namespace N_m3u8DL_RE.Crypto;
internal class ChaCha20Util internal static class ChaCha20Util
{ {
public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes) public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes)
{ {

View File

@ -20,7 +20,7 @@ internal class HTTPLiveRecordManager
DownloaderConfig DownloaderConfig; DownloaderConfig DownloaderConfig;
StreamExtractor StreamExtractor; StreamExtractor StreamExtractor;
List<StreamSpec> SelectedSteams; List<StreamSpec> SelectedSteams;
List<OutputFile> OutputFiles = new(); List<OutputFile> OutputFiles = [];
DateTime NowDateTime; DateTime NowDateTime;
DateTime? PublishDateTime; DateTime? PublishDateTime;
bool STOP_FLAG = false; bool STOP_FLAG = false;
@ -107,7 +107,7 @@ internal class HTTPLiveRecordManager
await Task.Delay(200); await Task.Delay(200);
if (InfoBuffer.Count < 188 * 5000) continue; if (InfoBuffer.Count < 188 * 5000) continue;
UInt16 ConvertToUint16(IEnumerable<byte> bytes) ushort ConvertToUint16(IEnumerable<byte> bytes)
{ {
if (BitConverter.IsLittleEndian) if (BitConverter.IsLittleEndian)
bytes = bytes.Reverse(); bytes = bytes.Reverse();
@ -217,7 +217,7 @@ internal class HTTPLiveRecordManager
return (item, task); return (item, task);
}).ToDictionary(item => item.item, item => 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; var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
if (limit != TimeSpan.MaxValue) if (limit != TimeSpan.MaxValue)
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");

View File

@ -23,7 +23,7 @@ internal class SimpleDownloadManager
DownloaderConfig DownloaderConfig; DownloaderConfig DownloaderConfig;
StreamExtractor StreamExtractor; StreamExtractor StreamExtractor;
List<StreamSpec> SelectedSteams; List<StreamSpec> SelectedSteams;
List<OutputFile> OutputFiles = new(); List<OutputFile> OutputFiles = [];
public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) 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) 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; DownloaderConfig.MyOptions.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); 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; DownloaderConfig.MyOptions.MuxAfterDone = false;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
@ -72,7 +72,7 @@ internal class SimpleDownloadManager
else if (mediainfos.All(m => m.Type == "Subtitle")) else if (mediainfos.All(m => m.Type == "Subtitle"))
{ {
streamSpec.MediaType = MediaType.SUBTITLES; streamSpec.MediaType = MediaType.SUBTITLES;
if (streamSpec.Extension == null || streamSpec.Extension == "ts") if (streamSpec.Extension is null or "ts")
streamSpec.Extension = "vtt"; streamSpec.Extension = "vtt";
} }
} }
@ -81,7 +81,7 @@ internal class SimpleDownloadManager
{ {
speedContainer.ResetVars(); speedContainer.ResetVars();
bool useAACFilter = false; // ffmpeg合并flag bool useAACFilter = false; // ffmpeg合并flag
List<Mediainfo> mediaInfos = new(); List<Mediainfo> mediaInfos = [];
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new(); ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments); 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 path = Path.Combine(tmpDir, "_init.mp4.tmp");
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
FileDic[streamSpec.Playlist.MediaInit] = result; FileDic[streamSpec.Playlist.MediaInit] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download init file failed!"); throw new Exception("Download init file failed!");
} }
@ -166,7 +166,7 @@ internal class SimpleDownloadManager
task.Increment(1); task.Increment(1);
// 读取mp4信息 // 读取mp4信息
if (result != null && result.Success) if (result is { Success: true })
{ {
mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath); mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath);
currentKID = mp4Info.KID; currentKID = mp4Info.KID;
@ -212,12 +212,12 @@ internal class SimpleDownloadManager
var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download first segment failed!"); throw new Exception("Download first segment failed!");
} }
task.Increment(1); task.Increment(1);
if (result != null && result.Success) if (result is { Success: true })
{ {
// 修复MSS init // 修复MSS init
if (StreamExtractor.ExtractorType == ExtractorType.MSS) 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 path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result != null && result.Success) if (result is { Success: true })
task.Increment(1); 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 enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); 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; var outputExt = "." + streamSpec.Extension;
if (streamSpec.Extension == null) outputExt = ".ts"; if (streamSpec.Extension == null) outputExt = ".ts";
else if (streamSpec.MediaType == MediaType.AUDIO && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".m4a"; else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: "m4s" or "mp4" }) outputExt = ".m4a";
else if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".mp4"; 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.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES)
{ {
if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? ".srt" : ".vtt";
else outputExt = ".vtt";
} }
var output = Path.Combine(saveDir, saveName + outputExt); 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)))}"); 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); File.Delete(mp4InitFile);
// shaka/ffmpeg实时解密不需要init文件用于合并 // shaka/ffmpeg实时解密不需要init文件用于合并
@ -333,7 +332,7 @@ internal class SimpleDownloadManager
// 校验分片数量 // 校验分片数量
if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null)) 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; return false;
} }
@ -351,8 +350,7 @@ internal class SimpleDownloadManager
} }
// 自动修复VTT raw字幕 // 自动修复VTT raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("vtt"))
&& streamSpec.Extension != null && streamSpec.Extension.Contains("vtt"))
{ {
Logger.WarnMarkUp(ResString.fixingVTT); Logger.WarnMarkUp(ResString.fixingVTT);
// 排序字幕并修正时间戳 // 排序字幕并修正时间戳
@ -398,7 +396,7 @@ internal class SimpleDownloadManager
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) && 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 iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
if (sawVtt) if (sawVtt)
@ -432,8 +430,7 @@ internal class SimpleDownloadManager
} }
// 自动修复TTML raw字幕 // 自动修复TTML raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("ttml"))
&& streamSpec.Extension != null && streamSpec.Extension.Contains("ttml"))
{ {
Logger.WarnMarkUp(ResString.fixingTTML); Logger.WarnMarkUp(ResString.fixingTTML);
var first = true; var first = true;
@ -479,9 +476,8 @@ internal class SimpleDownloadManager
} }
// 自动修复TTML mp4字幕 // 自动修复TTML mp4字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("m4s")
&& streamSpec.Extension != null && streamSpec.Extension.Contains("m4s") && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
{ {
Logger.WarnMarkUp(ResString.fixingTTMLmp4); Logger.WarnMarkUp(ResString.fixingTTMLmp4);
// sawTtml暂时不判断 // 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); var files = FileDic.Values.Select(v => v!.ActualFilePath);
foreach (var file in files) foreach (var file in files)
@ -598,7 +594,7 @@ internal class SimpleDownloadManager
} }
// 调用mp4decrypt解密 // 调用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 enc = output;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); 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); 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) foreach (var item in StreamExtractor.RawFiles)
{ {

View File

@ -27,7 +27,7 @@ internal class SimpleLiveRecordManager2
StreamExtractor StreamExtractor; StreamExtractor StreamExtractor;
List<StreamSpec> SelectedSteams; List<StreamSpec> SelectedSteams;
ConcurrentDictionary<int, string> PipeSteamNamesDic = new(); ConcurrentDictionary<int, string> PipeSteamNamesDic = new();
List<OutputFile> OutputFiles = new(); List<OutputFile> OutputFiles = [];
DateTime? PublishDateTime; DateTime? PublishDateTime;
bool STOP_FLAG = false; bool STOP_FLAG = false;
int WAIT_SEC = 0; // 刷新间隔 int WAIT_SEC = 0; // 刷新间隔
@ -41,7 +41,7 @@ internal class SimpleLiveRecordManager2
ConcurrentDictionary<int, long> DateTimeDic = new(); // 上次下载的dateTime ConcurrentDictionary<int, long> DateTimeDic = new(); // 上次下载的dateTime
CancellationTokenSource CancellationTokenSource = new(); // 取消Wait CancellationTokenSource CancellationTokenSource = new(); // 取消Wait
private readonly object lockObj = new object(); private readonly Lock lockObj = new();
TimeSpan? audioStart = null; TimeSpan? audioStart = null;
public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
@ -60,9 +60,9 @@ internal class SimpleLiveRecordManager2
if (_key != null) if (_key != null)
{ {
if (DownloaderConfig.MyOptions.Keys == null) if (DownloaderConfig.MyOptions.Keys == null)
DownloaderConfig.MyOptions.Keys = new string[] { _key }; DownloaderConfig.MyOptions.Keys = [_key];
else 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) 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; DownloaderConfig.MyOptions.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); 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; DownloaderConfig.MyOptions.MuxAfterDone = false;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
@ -136,7 +136,7 @@ internal class SimpleLiveRecordManager2
{ {
streamSpec.MediaType = MediaType.SUBTITLES; streamSpec.MediaType = MediaType.SUBTITLES;
if (streamSpec.Extension == null || streamSpec.Extension == "ts") if (streamSpec.Extension is null or "ts")
streamSpec.Extension = "vtt"; streamSpec.Extension = "vtt";
} }
} }
@ -151,7 +151,7 @@ internal class SimpleLiveRecordManager2
bool useAACFilter = false; // ffmpeg合并flag bool useAACFilter = false; // ffmpeg合并flag
bool initDownloaded = false; // 是否下载过init文件 bool initDownloaded = false; // 是否下载过init文件
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new(); ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
List<Mediainfo> mediaInfos = new(); List<Mediainfo> mediaInfos = [];
Stream? fileOutputStream = null; Stream? fileOutputStream = null;
WebVttSub currentVtt = new(); // 字幕流始终维护一个实例 WebVttSub currentVtt = new(); // 字幕流始终维护一个实例
bool firstSub = true; bool firstSub = true;
@ -197,7 +197,7 @@ internal class SimpleLiveRecordManager2
var path = Path.Combine(tmpDir, "_init.mp4.tmp"); var path = Path.Combine(tmpDir, "_init.mp4.tmp");
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
FileDic[streamSpec.Playlist.MediaInit] = result; FileDic[streamSpec.Playlist.MediaInit] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download init file failed!"); throw new Exception("Download init file failed!");
} }
@ -205,7 +205,7 @@ internal class SimpleLiveRecordManager2
task.Increment(1); task.Increment(1);
// 读取mp4信息 // 读取mp4信息
if (result != null && result.Success) if (result is { Success: true })
{ {
currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;
// 从文件读取KEY // 从文件读取KEY
@ -257,12 +257,12 @@ internal class SimpleLiveRecordManager2
var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download first segment failed!"); throw new Exception("Download first segment failed!");
} }
task.Increment(1); task.Increment(1);
if (result != null && result.Success) if (result is { Success: true })
{ {
// 修复MSS init // 修复MSS init
if (StreamExtractor.ExtractorType == ExtractorType.MSS) if (StreamExtractor.ExtractorType == ExtractorType.MSS)
@ -330,10 +330,10 @@ internal class SimpleLiveRecordManager2
var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result != null && result.Success) if (result is { Success: true })
task.Increment(1); 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 enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
@ -347,14 +347,13 @@ internal class SimpleLiveRecordManager2
}); });
// 自动修复VTT raw字幕 // 自动修复VTT raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("vtt"))
&& streamSpec.Extension != 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) foreach (var seg in keys)
{ {
var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); var vttContent = await File.ReadAllTextAsync(FileDic[seg]!.ActualFilePath);
var waitCount = 0; var waitCount = 0;
while (DownloaderConfig.MyOptions.LiveFixVttByAudio && audioStart == null && waitCount++ < 5) 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 if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) && 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 iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
if (sawVtt) if (sawVtt)
@ -396,10 +395,9 @@ internal class SimpleLiveRecordManager2
} }
// 自动修复TTML raw字幕 // 自动修复TTML raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("ttml"))
&& streamSpec.Extension != 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 (firstSub)
{ {
if (baseTimestamp != 0) if (baseTimestamp != 0)
@ -437,9 +435,8 @@ internal class SimpleLiveRecordManager2
} }
// 自动修复TTML mp4字幕 // 自动修复TTML mp4字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("m4s")
&& streamSpec.Extension != null && streamSpec.Extension.Contains("m4s") && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
{ {
// sawTtml暂时不判断 // sawTtml暂时不判断
// var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
@ -498,12 +495,11 @@ internal class SimpleLiveRecordManager2
// 合并 // 合并
var outputExt = "." + streamSpec.Extension; var outputExt = "." + streamSpec.Extension;
if (streamSpec.Extension == null) outputExt = ".ts"; 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 && streamSpec.Extension == "m4s") outputExt = ".mp4";
else if (streamSpec.MediaType == MediaType.SUBTITLES) else if (streamSpec.MediaType == MediaType.SUBTITLES)
{ {
if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? ".srt" : ".vtt";
else outputExt = ".vtt";
} }
var output = Path.Combine(saveDir, saveName + outputExt); var output = Path.Combine(saveDir, saveName + outputExt);
@ -536,7 +532,7 @@ internal class SimpleLiveRecordManager2
fileOutputStream = PipeUtil.CreatePipe(pipeName); fileOutputStream = PipeUtil.CreatePipe(pipeName);
Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]"); Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]");
PipeSteamNamesDic[task.Id] = pipeName; 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(); var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray();
Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]"); Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]");
@ -622,24 +618,23 @@ internal class SimpleLiveRecordManager2
break; break;
} }
if (fileOutputStream != null) if (fileOutputStream == null) return true;
if (!DownloaderConfig.MyOptions.LivePipeMux)
{ {
if (!DownloaderConfig.MyOptions.LivePipeMux) // 记录所有文件信息
OutputFiles.Add(new OutputFile()
{ {
// 记录所有文件信息 Index = task.Id,
OutputFiles.Add(new OutputFile() FilePath = (fileOutputStream as FileStream)!.Name,
{ LangCode = streamSpec.Language,
Index = task.Id, Description = streamSpec.Name,
FilePath = (fileOutputStream as FileStream)!.Name, Mediainfos = mediaInfos,
LangCode = streamSpec.Language, MediaType = streamSpec.MediaType,
Description = streamSpec.Name, });
Mediainfos = mediaInfos,
MediaType = streamSpec.MediaType,
});
}
fileOutputStream.Close();
fileOutputStream.Dispose();
} }
fileOutputStream.Close();
fileOutputStream.Dispose();
return true; return true;
} }
@ -648,78 +643,76 @@ internal class SimpleLiveRecordManager2
{ {
while (!STOP_FLAG) 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相同 单次请求即可获得所有轨道的信息 var streamSpec = dic.Key;
// 2. M3U8 所有URL不同 才需要多次请求 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 allName = streamSpec.Playlist!.MediaParts[0].MediaSegments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false));
var task = dic.Value; var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1;
SamePathDic[task.Id] = allSamePath;
// 达到上限时 不需要刷新了
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());
} }
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; STOP_FLAG = true;
// 停止所有Block CancellationTokenSource.Cancel();
foreach (var target in BlockDic.Values) }
{ });
target.Complete();
} 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"); Logger.WarnMarkUp($"set refresh interval to {WAIT_SEC} seconds");
} }
// 如果没有选中音频 取消通过音频修复vtt时间轴 // 如果没有选中音频 取消通过音频修复vtt时间轴
if (!SelectedSteams.Any(x => x.MediaType == MediaType.AUDIO)) if (SelectedSteams.All(x => x.MediaType != MediaType.AUDIO))
{ {
DownloaderConfig.MyOptions.LiveFixVttByAudio = false; DownloaderConfig.MyOptions.LiveFixVttByAudio = false;
} }
@ -839,7 +832,7 @@ internal class SimpleLiveRecordManager2
DownloaderConfig.MyOptions.ConcurrentDownload = true; DownloaderConfig.MyOptions.ConcurrentDownload = true;
DownloaderConfig.MyOptions.MP4RealTimeDecryption = 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 }) if (DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, DecryptionEngine: not DecryptEngine.SHAKA_PACKAGER, Keys.Length: > 0 })
Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]");
var limit = DownloaderConfig.MyOptions.LiveRecordLimit; var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
@ -865,7 +858,7 @@ internal class SimpleLiveRecordManager2
var success = Results.Values.All(v => v == true); 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) foreach (var item in StreamExtractor.RawFiles)
{ {

View File

@ -25,23 +25,25 @@ internal class SimpleDownloader : IDownloader
{ {
var url = segment.Url; var url = segment.Url;
var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount); 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 key = segment.EncryptInfo.Key;
var iv = segment.EncryptInfo.IV; var iv = segment.EncryptInfo.IV;
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, 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 key = segment.EncryptInfo.Key;
var iv = segment.EncryptInfo.IV; var iv = segment.EncryptInfo.IV;
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB); 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 key = segment.EncryptInfo.Key;
var nonce = segment.EncryptInfo.IV; var nonce = segment.EncryptInfo.IV;
@ -49,22 +51,22 @@ internal class SimpleDownloader : IDownloader
var fileBytes = File.ReadAllBytes(dResult.ActualFilePath); var fileBytes = File.ReadAllBytes(dResult.ActualFilePath);
var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!); var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!);
await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted); 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"); // throw new NotSupportedException("SAMPLE-AES-CTR");
} break;
}
// Image头处理 // Image头处理
if (dResult.ImageHeader) if (dResult.ImageHeader)
{ {
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath); await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
} }
// Gzip解压 // Gzip解压
if (dResult.GzipHeader) if (dResult.GzipHeader)
{ {
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath); await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
}
} }
// 处理完成后改名 // 处理完成后改名
@ -99,14 +101,15 @@ internal class SimpleDownloader : IDownloader
} }
// 另起线程进行监控 // 另起线程进行监控
var cts = cancellationTokenSource;
using var watcher = Task.Factory.StartNew(async () => using var watcher = Task.Factory.StartNew(async () =>
{ {
while (true) while (true)
{ {
if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break; if (cts.IsCancellationRequested) break;
if (speedContainer.ShouldStop) if (speedContainer.ShouldStop)
{ {
cancellationTokenSource.Cancel(); cts.Cancel();
Logger.DebugMarkUp("Cancel..."); Logger.DebugMarkUp("Cancel...");
break; break;
} }
@ -123,7 +126,7 @@ internal class SimpleDownloader : IDownloader
catch (Exception ex) catch (Exception ex)
{ {
Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]"); 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}"); Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
if (retryCount-- > 0) if (retryCount-- > 0)
{ {

View File

@ -1,15 +1,8 @@
using System; namespace N_m3u8DL_RE.Entity;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity;
internal class DownloadResult 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? RespContentLength { get; set; }
public long? ActualContentLength { get; set; } public long? ActualContentLength { get; set; }
public bool ImageHeader { get; set; } = false; // 图片伪装 public bool ImageHeader { get; set; } = false; // 图片伪装

View File

@ -9,5 +9,5 @@ internal class OutputFile
public required string FilePath { get; set; } public required string FilePath { get; set; }
public string? LangCode { get; set; } public string? LangCode { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public List<Mediainfo> Mediainfos { get; set; } = new(); public List<Mediainfo> Mediainfos { get; set; } = [];
} }

View File

@ -1,13 +1,4 @@
using NiL.JS.Statements; namespace N_m3u8DL_RE.Entity;
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;
internal class SpeedContainer internal class SpeedContainer
{ {
@ -24,7 +15,7 @@ internal class SpeedContainer
private long _downloaded = 0; private long _downloaded = 0;
private long _Rdownloaded = 0; private long _Rdownloaded = 0;
public long Downloaded { get => _downloaded; } public long Downloaded => _downloaded;
public int AddLowSpeedCount() public int AddLowSpeedCount()
{ {

View File

@ -46,6 +46,6 @@ public class StreamFilter
if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} "); if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} ");
if (Role.HasValue) sb.Append($"Role: {Role} "); if (Role.HasValue) sb.Append($"Role: {Role} ");
return sb.ToString() + $"For: {For}"; return sb + $"For: {For}";
} }
} }

View File

@ -13,7 +13,6 @@ using N_m3u8DL_RE.Util;
using N_m3u8DL_RE.DownloadManager; using N_m3u8DL_RE.DownloadManager;
using N_m3u8DL_RE.CommandLine; using N_m3u8DL_RE.CommandLine;
using System.Net; using System.Net;
using System.Net.Http.Headers;
using N_m3u8DL_RE.Enum; using N_m3u8DL_RE.Enum;
namespace N_m3u8DL_RE; namespace N_m3u8DL_RE;
@ -26,7 +25,7 @@ internal class Program
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
var osVersion = Environment.OSVersion.Version; 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"); Environment.SetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
} }
@ -38,7 +37,7 @@ internal class Program
string loc = ResString.CurrentLoc; string loc = ResString.CurrentLoc;
string currLoc = Thread.CurrentThread.CurrentUICulture.Name; 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"; else if (currLoc.StartsWith("zh-")) loc = "zh-TW";
// 处理用户-h等请求 // 处理用户-h等请求
@ -147,33 +146,36 @@ internal class Program
// 预先检查 // 预先检查
if (option.Keys is { Length: > 0 } || option.KeyTextFile != null) 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 file = GlobalUtil.FindExecutable("shaka-packager");
var file2 = GlobalUtil.FindExecutable("packager-linux-x64"); var file2 = GlobalUtil.FindExecutable("packager-linux-x64");
var file3 = GlobalUtil.FindExecutable("packager-osx-x64"); var file3 = GlobalUtil.FindExecutable("packager-osx-x64");
var file4 = GlobalUtil.FindExecutable("packager-win-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; option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}"); Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}");
break;
} }
else if (option.DecryptionEngine is DecryptEngine.MP4DECRYPT) case DecryptEngine.MP4DECRYPT:
{ {
var file = GlobalUtil.FindExecutable("mp4decrypt"); var file = GlobalUtil.FindExecutable("mp4decrypt");
if (file == null) throw new FileNotFoundException(ResString.mp4decryptNotFound); if (file == null) throw new FileNotFoundException(ResString.mp4decryptNotFound);
option.DecryptionBinaryPath = file; option.DecryptionBinaryPath = file;
Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}"); Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}");
break;
} }
else case DecryptEngine.FFMPEG:
{ default:
option.DecryptionBinaryPath = option.FFmpegBinaryPath; option.DecryptionBinaryPath = option.FFmpegBinaryPath;
} break;
}
else if (!File.Exists(option.DecryptionBinaryPath))
{
throw new FileNotFoundException(option.DecryptionBinaryPath);
} }
} }
@ -283,7 +285,7 @@ internal class Program
if (option.AutoSelect) if (option.AutoSelect)
{ {
if (basicStreams.Any()) if (basicStreams.Count != 0)
selectedStreams.Add(basicStreams.First()); selectedStreams.Add(basicStreams.First());
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language); var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
foreach (var lang in langs) foreach (var lang in langs)
@ -309,7 +311,7 @@ internal class Program
selectedStreams = FilterUtil.SelectStreams(lists); selectedStreams = FilterUtil.SelectStreams(lists);
} }
if (!selectedStreams.Any()) if (selectedStreams.Count == 0)
throw new Exception(ResString.noStreamsToDownload); throw new Exception(ResString.noStreamsToDownload);
// HLS: 选中流中若有没加载出playlist的加载playlist // HLS: 选中流中若有没加载出playlist的加载playlist
@ -363,7 +365,7 @@ internal class Program
Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]"); Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]");
// 开始MuxAfterDone后自动使用二进制版 // 开始MuxAfterDone后自动使用二进制版
if (!option.BinaryMerge && option.MuxAfterDone) if (option is { BinaryMerge: false, MuxAfterDone: true })
{ {
option.BinaryMerge = true; option.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]");
@ -446,24 +448,21 @@ internal class Program
static async Task<string> Get302Async(string url) static async Task<string> Get302Async(string url)
{ {
// this allows you to set the settings so that we can get the redirect 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 AllowAutoRedirect = false
}; };
string redirectedUrl = ""; var redirectedUrl = "";
using (HttpClient client = new(handler)) using var client = new HttpClient(handler);
using (HttpResponseMessage response = await client.GetAsync(url)) using var response = await client.GetAsync(url);
using (HttpContent content = response.Content) 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 redirectedUrl = headers.Location.AbsoluteUri;
if (response.StatusCode == System.Net.HttpStatusCode.Found)
{
HttpResponseHeaders headers = response.Headers;
if (headers != null && headers.Location != null)
{
redirectedUrl = headers.Location.AbsoluteUri;
}
}
} }
return redirectedUrl; return redirectedUrl;

View File

@ -25,7 +25,7 @@ internal static class DownloadUtil
{ {
var buffer = new byte[expect]; var buffer = new byte[expect];
_ = await inputStream.ReadAsync(buffer); _ = await inputStream.ReadAsync(buffer);
await outputStream.WriteAsync(buffer, 0, buffer.Length); await outputStream.WriteAsync(buffer);
speedContainer.Add(buffer.Length); speedContainer.Add(buffer.Length);
} }
return new DownloadResult() return new DownloadResult()
@ -81,7 +81,7 @@ internal static class DownloadUtil
{ {
HttpResponseHeaders respHeaders = response.Headers; HttpResponseHeaders respHeaders = response.Headers;
Logger.Debug(respHeaders.ToString()); Logger.Debug(respHeaders.ToString());
if (respHeaders != null && respHeaders.Location != null) if (respHeaders.Location != null)
{ {
var redirectedUrl = ""; var redirectedUrl = "";
if (!respHeaders.Location.IsAbsoluteUri) if (!respHeaders.Location.IsAbsoluteUri)
@ -108,7 +108,7 @@ internal static class DownloadUtil
size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token); size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);
speedContainer.Add(size); speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size); await stream.WriteAsync(buffer.AsMemory(0, size));
// 检测imageHeader // 检测imageHeader
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer); bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
// 检测GZipFor DDP Audio // 检测GZipFor DDP Audio
@ -117,7 +117,7 @@ internal static class DownloadUtil
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0) while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
{ {
speedContainer.Add(size); speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size); await stream.WriteAsync(buffer.AsMemory(0, size));
// 限速策略 // 限速策略
while (speedContainer.Downloaded > speedContainer.SpeedLimit) while (speedContainer.Downloaded > speedContainer.SpeedLimit)
{ {

View File

@ -65,7 +65,7 @@ public static class FilterUtil
public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter) 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 inputs = lists.Where(_ => true);
var selected = DoFilterKeep(lists, filter); var selected = DoFilterKeep(lists, filter);
@ -93,9 +93,8 @@ public static class FilterUtil
.UseConverter(x => .UseConverter(x =>
{ {
if (x.Name != null && x.Name.StartsWith("__")) if (x.Name != null && x.Name.StartsWith("__"))
return $"[darkslategray1]{x.Name.Substring(2)}[/]"; return $"[darkslategray1]{x.Name[2..]}[/]";
else return x.ToString().EscapeMarkup().RemoveMarkup();
return x.ToString().EscapeMarkup().RemoveMarkup();
}) })
.Required() .Required()
.PageSize(10) .PageSize(10)
@ -107,12 +106,12 @@ public static class FilterUtil
var first = streamSpecs.First(); var first = streamSpecs.First();
prompt.Select(first); prompt.Select(first);
if (basicStreams.Any()) if (basicStreams.Count != 0)
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams);
} }
if (audios.Any()) if (audios.Count != 0)
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios);
// 默认音轨 // 默认音轨
@ -121,7 +120,7 @@ public static class FilterUtil
prompt.Select(audios.First(a => a.GroupId == first.AudioId)); prompt.Select(audios.First(a => a.GroupId == first.AudioId));
} }
} }
if (subs.Any()) if (subs.Count != 0)
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs);
// 默认字幕轨 // 默认字幕轨

View File

@ -80,7 +80,7 @@ internal static class LargeSingleFileSplitUtil
// 此函数主要是切片下载逻辑 // 此函数主要是切片下载逻辑
private static List<Clip> GetAllClips(string url, long fileSize) private static List<Clip> GetAllClips(string url, long fileSize)
{ {
List<Clip> clips = new(); List<Clip> clips = [];
int index = 0; int index = 0;
long counter = 0; long counter = 0;
int perSize = 10 * 1024 * 1024; int perSize = 10 * 1024 * 1024;

View File

@ -7,7 +7,7 @@ using N_m3u8DL_RE.Enum;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
internal static class MP4DecryptUtil internal static partial class MP4DecryptUtil
{ {
private static readonly string ZeroKid = "00000000000000000000000000000000"; 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) 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)) if (!string.IsNullOrEmpty(kid))
{ {
var test = keyPairs.Where(k => k.StartsWith(kid)).ToList(); var test = keyPairs.Where(k => k.StartsWith(kid)).ToList();
if (test.Any()) keyPair = test.First(); if (test.Count != 0) keyPair = test.First();
} }
// Apple // Apple
@ -159,11 +159,10 @@ internal static class MP4DecryptUtil
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
while (await reader.ReadLineAsync() is { } line) while (await reader.ReadLineAsync() is { } line)
{ {
if (line.Trim().StartsWith(kid)) if (!line.Trim().StartsWith(kid)) continue;
{
Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]"); Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]");
return line.Trim(); return line.Trim();
}
} }
} }
catch (Exception ex) catch (Exception ex)
@ -192,7 +191,7 @@ internal static class MP4DecryptUtil
public static string? ReadInitShaka(string output, string bin) 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) // TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid)
// - stop process // - stop process
@ -214,4 +213,7 @@ internal static class MP4DecryptUtil
p.WaitForExit(); p.WaitForExit();
return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value; return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value;
} }
[GeneratedRegex("Key for key_id=([0-9a-f]+) was not found")]
private static partial Regex KidOutputRegex();
} }

View File

@ -8,23 +8,23 @@ internal static partial class MediainfoUtil
{ {
[GeneratedRegex(" Stream #.*")] [GeneratedRegex(" Stream #.*")]
private static partial Regex TextRegex(); private static partial Regex TextRegex();
[GeneratedRegex("#0:\\d(\\[0x\\w+?\\])")] [GeneratedRegex(@"#0:\d(\[0x\w+?\])")]
private static partial Regex IdRegex(); private static partial Regex IdRegex();
[GeneratedRegex(": (\\w+): (.*)")] [GeneratedRegex(": (\\w+): (.*)")]
private static partial Regex TypeRegex(); private static partial Regex TypeRegex();
[GeneratedRegex("(.*?)(,|$)")] [GeneratedRegex("(.*?)(,|$)")]
private static partial Regex BaseInfoRegex(); private static partial Regex BaseInfoRegex();
[GeneratedRegex(" \\/ 0x\\w+")] [GeneratedRegex(@" \/ 0x\w+")]
private static partial Regex ReplaceRegex(); private static partial Regex ReplaceRegex();
[GeneratedRegex("\\d{2,}x\\d+")] [GeneratedRegex(@"\d{2,}x\d+")]
private static partial Regex ResRegex(); private static partial Regex ResRegex();
[GeneratedRegex("\\d+ kb\\/s")] [GeneratedRegex(@"\d+ kb\/s")]
private static partial Regex BitrateRegex(); private static partial Regex BitrateRegex();
[GeneratedRegex("(\\d+(\\.\\d+)?) fps")] [GeneratedRegex(@"(\d+(\.\d+)?) fps")]
private static partial Regex FpsRegex(); 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(); private static partial Regex DoViRegex();
[GeneratedRegex("Duration.*?start: (\\d+\\.?\\d{0,3})")] [GeneratedRegex(@"Duration.*?start: (\d+\.?\d{0,3})")]
private static partial Regex StartRegex(); private static partial Regex StartRegex();
public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file) public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file)

View File

@ -68,20 +68,16 @@ internal static class MergeUtil
public static string[] PartialCombineMultipleFiles(string[] files) public static string[] PartialCombineMultipleFiles(string[] files)
{ {
var newFiles = new List<string>(); var newFiles = new List<string>();
int div = 0; var div = files.Length <= 90000 ? 100 : 200;
if (files.Length <= 90000)
div = 100;
else
div = 200;
string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T"); var outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T");
int index = 0; // 序号 var index = 0; // 序号
// 按照div的容量分割为小数组 // 按照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) foreach (var items in li)
{ {
if (!items.Any()) if (items.Length == 0)
continue; continue;
var output = outputName + index.ToString("0000") + ".ts"; var output = outputName + index.ToString("0000") + ".ts";
CombineMultipleFilesIntoSingleFile(items, output); CombineMultipleFilesIntoSingleFile(items, output);

View File

@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
internal static class OtherUtil internal static partial class OtherUtil
{ {
public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers) 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" 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(); .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; var title = InvalidChars.Aggregate(input, (current, invalidChar) => current.Replace(invalidChar.ToString(), re));
foreach (char invalidChar in InvalidChars)
{
title = title.Replace(invalidChar.ToString(), re);
}
if (filterSlash) if (filterSlash)
{ {
title = title.Replace("/", re); title = title.Replace("/", re);
@ -98,7 +94,7 @@ internal static class OtherUtil
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
public static double ParseSeconds(string timeStr) public static double ParseSeconds(string timeStr)
{ {
var pattern = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$"); var pattern = TimeStrRegex();
var match = pattern.Match(timeStr); var match = pattern.Match(timeStr);
@ -171,4 +167,7 @@ internal static class OtherUtil
_ => throw new ArgumentException($"unknown format: {muxFormat}") _ => throw new ArgumentException($"unknown format: {muxFormat}")
}; };
} }
[GeneratedRegex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$")]
private static partial Regex TimeStrRegex();
} }

View File

@ -14,24 +14,22 @@ internal static class PipeUtil
{ {
return new NamedPipeServerStream(pipeName, PipeDirection.InOut); 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); FileName = "mkfifo",
using var p = new Process(); Arguments = path,
p.StartInfo = new ProcessStartInfo() CreateNoWindow = true,
{ UseShellExecute = false,
FileName = "mkfifo", RedirectStandardError = true,
Arguments = path, RedirectStandardOutput = true,
CreateNoWindow = true, };
UseShellExecute = false, p.Start();
RedirectStandardError = true, p.WaitForExit();
RedirectStandardOutput = true, Thread.Sleep(200);
}; return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
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) 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 (!string.IsNullOrEmpty(customDest))
{ {
if (customDest.Trim().StartsWith("-")) if (customDest.Trim().StartsWith('-'))
command.Append(customDest); command.Append(customDest);
else else
command.Append($" -f mpegts -shortest \"{customDest}\""); command.Append($" -f mpegts -shortest \"{customDest}\"");