升级到.NET7 并开始支持基本的下载功能

This commit is contained in:
nilaoda 2022-07-16 22:50:41 +08:00
parent 71988ddbca
commit 99cf887a70
40 changed files with 2071 additions and 264 deletions

View File

@ -19,5 +19,23 @@ namespace N_m3u8DL_RE.Common.Entity
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo();
public string Url { get; set; }
public override bool Equals(object? obj)
{
return obj is MediaSegment segment &&
Index == segment.Index &&
Duration == segment.Duration &&
Title == segment.Title &&
StartRange == segment.StartRange &&
StopRange == segment.StopRange &&
ExpectLength == segment.ExpectLength &&
EqualityComparer<EncryptInfo>.Default.Equals(EncryptInfo, segment.EncryptInfo) &&
Url == segment.Url;
}
public override int GetHashCode()
{
return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, EncryptInfo, Url);
}
}
}

View File

@ -23,6 +23,7 @@ namespace N_m3u8DL_RE.Common.Entity
public string? Resolution { get; set; }
public double? FrameRate { get; set; }
public string? Channels { get; set; }
public string? Extension { get; set; }
//外部轨道GroupId (后续寻找对应轨道信息)
@ -34,6 +35,40 @@ namespace N_m3u8DL_RE.Common.Entity
public Playlist? Playlist { get; set; }
public string ToShortString()
{
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty;
if (MediaType == Enum.MediaType.AUDIO)
{
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")}";
returnStr = d.EscapeMarkup();
}
else if (MediaType == Enum.MediaType.SUBTITLES)
{
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
var d = $"{GroupId} | {Language} | {Name} | {Codecs}";
returnStr = d.EscapeMarkup();
}
else
{
prefixStr = $"[aqua]Vid[/] {encStr}";
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs}";
returnStr = d.EscapeMarkup();
}
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
}
public override string ToString()
{
var prefixStr = "";

View File

@ -10,7 +10,21 @@ namespace N_m3u8DL_RE.Common.Entity
{
public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
public string Payload { get; set; }
public string Settings { get; set; }
public required string Payload { get; set; }
public required string Settings { get; set; }
public override bool Equals(object? obj)
{
return obj is SubCue cue &&
StartTime.Equals(cue.StartTime) &&
EndTime.Equals(cue.EndTime) &&
Payload == cue.Payload &&
Settings == cue.Settings;
}
public override int GetHashCode()
{
return HashCode.Combine(StartTime, EndTime, Payload, Settings);
}
}
}

View File

@ -1,115 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity
{
public class WebSub
{
public List<SubCue> Cues { get; set; } = new List<SubCue>();
public long MpegtsTimestamp { get; set; } = 0L;
/// <summary>
/// 从字节数组解析WEBVTT
/// </summary>
/// <param name="textBytes"></param>
/// <returns></returns>
public static WebSub Parse(byte[] textBytes)
{
return Parse(Encoding.UTF8.GetString(textBytes));
}
/// <summary>
/// 从字节数组解析WEBVTT
/// </summary>
/// <param name="textBytes"></param>
/// <param name="encoding"></param>
/// <returns></returns>
public static WebSub Parse(byte[] textBytes, Encoding encoding)
{
return Parse(encoding.GetString(textBytes));
}
/// <summary>
/// 从字符串解析WEBVTT
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static WebSub Parse(string text)
{
if (!text.Trim().StartsWith("WEBVTT"))
throw new Exception("Bad vtt!");
var webSub = new WebSub();
var needPayload = false;
var timeLine = "";
var regex1 = new Regex("X-TIMESTAMP-MAP.*");
if (regex1.IsMatch(text))
{
var timestamp = Regex.Match(regex1.Match(text).Value, "MPEGTS:(\\d+)").Groups[1].Value;
webSub.MpegtsTimestamp = Convert.ToInt64(timestamp);
}
foreach (var line in text.Split('\n'))
{
if (string.IsNullOrEmpty(line)) continue;
if (!needPayload && line.Contains(" --> "))
{
needPayload = true;
timeLine = line.Trim();
continue;
}
if (needPayload)
{
var payload = line.Trim();
var arr = Regex.Split(timeLine.Replace("-->", ""), "\\s").Where(s => !string.IsNullOrEmpty(s)).ToList();
var startTime = ConvertToTS(arr[0]);
var endTime = ConvertToTS(arr[1]);
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
webSub.Cues.Add(new SubCue()
{
StartTime = startTime,
EndTime = endTime,
Payload = string.Join("", payload.Where(c => c != 8203)), //Remove Zero Width Space!
Settings = style
}) ;
needPayload = false;
}
}
return webSub;
}
private static TimeSpan ConvertToTS(string str)
{
var ms = Convert.ToInt32(str.Split('.').Last());
var o = str.Split('.').First();
var t = o.Split(':').Reverse().ToList();
var time = 0L + ms;
for (int i = 0; i < t.Count(); i++)
{
time += (int)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
}
return TimeSpan.FromMilliseconds(time);
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (var c in this.Cues)
{
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);
sb.AppendLine(c.Payload);
sb.AppendLine();
}
return sb.ToString();
}
}
}

View File

@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity
{
public partial class WebVttSub
{
[RegexGenerator("X-TIMESTAMP-MAP.*")]
private static partial Regex TSMapRegex();
[RegexGenerator("MPEGTS:(\\d+)")]
private static partial Regex TSValueRegex();
[RegexGenerator("\\s")]
private static partial Regex SplitRegex();
public List<SubCue> Cues { get; set; } = new List<SubCue>();
public long MpegtsTimestamp { get; set; } = 0L;
/// <summary>
/// 从字节数组解析WEBVTT
/// </summary>
/// <param name="textBytes"></param>
/// <returns></returns>
public static WebVttSub Parse(byte[] textBytes)
{
return Parse(Encoding.UTF8.GetString(textBytes));
}
/// <summary>
/// 从字节数组解析WEBVTT
/// </summary>
/// <param name="textBytes"></param>
/// <param name="encoding"></param>
/// <returns></returns>
public static WebVttSub Parse(byte[] textBytes, Encoding encoding)
{
return Parse(encoding.GetString(textBytes));
}
/// <summary>
/// 从字符串解析WEBVTT
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static WebVttSub Parse(string text)
{
if (!text.Trim().StartsWith("WEBVTT"))
throw new Exception("Bad vtt!");
var webSub = new WebVttSub();
var needPayload = false;
var timeLine = "";
var regex1 = TSMapRegex();
if (regex1.IsMatch(text))
{
var timestamp = TSValueRegex().Match(regex1.Match(text).Value).Groups[1].Value;
webSub.MpegtsTimestamp = Convert.ToInt64(timestamp);
}
var payloads = new List<string>();
foreach (var line in text.Split('\n'))
{
if (line.Contains(" --> "))
{
needPayload = true;
timeLine = line.Trim();
continue;
}
if (needPayload)
{
if (string.IsNullOrEmpty(line.Trim()))
{
var payload = string.Join(Environment.NewLine, payloads);
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
var startTime = ConvertToTS(arr[0]);
var endTime = ConvertToTS(arr[1]);
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
webSub.Cues.Add(new SubCue()
{
StartTime = startTime,
EndTime = endTime,
Payload = string.Join("", payload.Where(c => c != 8203)), //Remove Zero Width Space!
Settings = style
});
payloads.Clear();
needPayload = false;
}
else
{
payloads.Add(line.Trim());
}
}
}
return webSub;
}
/// <summary>
/// 从另一个字幕中获取所有Cue并加载此字幕中且自动修正偏移
/// </summary>
/// <param name="webSub"></param>
/// <returns></returns>
public WebVttSub AddCuesFromOne(WebVttSub webSub)
{
FixTimestamp(webSub, this.MpegtsTimestamp);
foreach (var item in webSub.Cues)
{
if (!this.Cues.Contains(item)) this.Cues.Add(item);
}
return this;
}
public static void FixTimestamp(WebVttSub sub, long baseTimestamp)
{
if (baseTimestamp == 0 || sub.MpegtsTimestamp == 0)
{
return;
}
//The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second
var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;
for (int i = 0; i < sub.Cues.Count; i++)
{
sub.Cues[i].StartTime += TimeSpan.FromSeconds(seconds);
sub.Cues[i].EndTime += TimeSpan.FromSeconds(seconds);
}
}
private static TimeSpan ConvertToTS(string str)
{
var ms = Convert.ToInt32(str.Split('.').Last());
var o = str.Split('.').First();
var t = o.Split(':').Reverse().ToList();
var time = 0L + ms;
for (int i = 0; i < t.Count(); i++)
{
time += (int)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
}
return TimeSpan.FromMilliseconds(time);
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (var c in this.Cues)
{
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);
sb.AppendLine(c.Payload);
sb.AppendLine();
}
sb.AppendLine();
return sb.ToString();
}
public string ToStringWithHeader()
{
return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString();
}
}
}

View File

@ -0,0 +1,20 @@
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using System.Text.Json.Serialization;
namespace N_m3u8DL_RE.Common
{
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(MediaType))]
[JsonSerializable(typeof(EncryptMethod))]
[JsonSerializable(typeof(ExtractorType))]
[JsonSerializable(typeof(Choise))]
[JsonSerializable(typeof(StreamSpec))]
[JsonSerializable(typeof(IOrderedEnumerable<StreamSpec>))]
[JsonSerializable(typeof(List<StreamSpec>))]
[JsonSerializable(typeof(Dictionary<string, string>))]
internal partial class JsonContext : JsonSerializerContext { }
}

View File

@ -8,8 +8,11 @@ using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Log
{
public class Logger
public partial class Logger
{
[RegexGenerator("{}")]
private static partial Regex VarsRepRegex();
/// <summary>
/// 日志级别默认为INFO
/// </summary>
@ -46,7 +49,7 @@ namespace N_m3u8DL_RE.Common.Log
{
for (int i = 0; i < ps.Length; i++)
{
data = new Regex("{}").Replace(data, $"{ps[i]}", 1);
data = VarsRepRegex().Replace(data, $"{ps[i]}", 1);
}
return data;
}

View File

@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<OutputType>library</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>N_m3u8DL_RE.Common</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -69,6 +69,15 @@ namespace N_m3u8DL_RE.Common.Resource {
}
}
/// <summary>
/// 查找类似 二进制合并中... 的本地化字符串。
/// </summary>
public static string binaryMerge {
get {
return ResourceManager.GetString("binaryMerge", resourceCulture);
}
}
/// <summary>
/// 查找类似 验证最后一个分片有效性 的本地化字符串。
/// </summary>
@ -87,6 +96,42 @@ namespace N_m3u8DL_RE.Common.Resource {
}
}
/// <summary>
/// 查找类似 正在提取TTML(raw)字幕... 的本地化字符串。
/// </summary>
public static string fixingTTML {
get {
return ResourceManager.GetString("fixingTTML", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在提取TTML(mp4)字幕... 的本地化字符串。
/// </summary>
public static string fixingTTMLmp4 {
get {
return ResourceManager.GetString("fixingTTMLmp4", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在提取VTT(raw)字幕... 的本地化字符串。
/// </summary>
public static string fixingVTT {
get {
return ResourceManager.GetString("fixingVTT", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在提取VTT(mp4)字幕... 的本地化字符串。
/// </summary>
public static string fixingVTTmp4 {
get {
return ResourceManager.GetString("fixingVTTmp4", resourceCulture);
}
}
/// <summary>
/// 查找类似 找不到支持的Processor 的本地化字符串。
/// </summary>
@ -186,6 +231,15 @@ namespace N_m3u8DL_RE.Common.Resource {
}
}
/// <summary>
/// 查找类似 分片数量校验不通过, 共{}个,已下载{}. 的本地化字符串。
/// </summary>
public static string segmentCountCheckNotPass {
get {
return ResourceManager.GetString("segmentCountCheckNotPass", resourceCulture);
}
}
/// <summary>
/// 查找类似 已选择的流: 的本地化字符串。
/// </summary>
@ -195,6 +249,15 @@ namespace N_m3u8DL_RE.Common.Resource {
}
}
/// <summary>
/// 查找类似 开始下载... 的本地化字符串。
/// </summary>
public static string startDownloading {
get {
return ResourceManager.GetString("startDownloading", resourceCulture);
}
}
/// <summary>
/// 查找类似 已解析, 共计 {} 条媒体流, 基本流 {} 条, 可选音频流 {} 条, 可选字幕流 {} 条 的本地化字符串。
/// </summary>

View File

@ -131,7 +131,7 @@
<value>Live stream found</value>
</data>
<data name="selectedStream" xml:space="preserve">
<value>Selected Streams:</value>
<value>Selected streams:</value>
</data>
<data name="writeJson" xml:space="preserve">
<value>Writing meta.json</value>
@ -148,4 +148,25 @@
<data name="keyProcessorNotFound" xml:space="preserve">
<value>No Processor matched</value>
</data>
<data name="startDownloading" xml:space="preserve">
<value>Start downloading...</value>
</data>
<data name="segmentCountCheckNotPass" xml:space="preserve">
<value>Segment count check not pass, total: {}, downloaded: {}.</value>
</data>
<data name="fixingVTT" xml:space="preserve">
<value>Extracting VTT(raw) subtitle...</value>
</data>
<data name="fixingVTTmp4" xml:space="preserve">
<value>Extracting VTT(mp4) subtitle...</value>
</data>
<data name="binaryMerge" xml:space="preserve">
<value>Binary merging...</value>
</data>
<data name="fixingTTMLmp4" xml:space="preserve">
<value>Extracting TTML(mp4) subtitle...</value>
</data>
<data name="fixingTTML" xml:space="preserve">
<value>Extracting TTML(raw) subtitle...</value>
</data>
</root>

View File

@ -148,4 +148,25 @@
<data name="keyProcessorNotFound" xml:space="preserve">
<value>找不到支持的Processor</value>
</data>
<data name="startDownloading" xml:space="preserve">
<value>开始下载...</value>
</data>
<data name="segmentCountCheckNotPass" xml:space="preserve">
<value>分片数量校验不通过, 共{}个,已下载{}.</value>
</data>
<data name="fixingVTT" xml:space="preserve">
<value>正在提取VTT(raw)字幕...</value>
</data>
<data name="fixingVTTmp4" xml:space="preserve">
<value>正在提取VTT(mp4)字幕...</value>
</data>
<data name="binaryMerge" xml:space="preserve">
<value>二进制合并中...</value>
</data>
<data name="fixingTTMLmp4" xml:space="preserve">
<value>正在提取TTML(mp4)字幕...</value>
</data>
<data name="fixingTTML" xml:space="preserve">
<value>正在提取TTML(raw)字幕...</value>
</data>
</root>

View File

@ -148,4 +148,25 @@
<data name="keyProcessorNotFound" xml:space="preserve">
<value>找不到支持的Processor</value>
</data>
<data name="startDownloading" xml:space="preserve">
<value>開始下載...</value>
</data>
<data name="segmentCountCheckNotPass" xml:space="preserve">
<value>分片數量校驗不通過, 共{}個,已下載{}.</value>
</data>
<data name="fixingVTT" xml:space="preserve">
<value>正在提取VTT(raw)字幕...</value>
</data>
<data name="fixingVTTmp4" xml:space="preserve">
<value>正在提取VTT(mp4)字幕...</value>
</data>
<data name="binaryMerge" xml:space="preserve">
<value>二進製合併中...</value>
</data>
<data name="fixingTTMLmp4" xml:space="preserve">
<value>正在提取TTML(mp4)字幕...</value>
</data>
<data name="fixingTTML" xml:space="preserve">
<value>正在提取TTML(raw)字幕...</value>
</data>
</root>

View File

@ -1,4 +1,5 @@
using N_m3u8DL_RE.Common.JsonConverter;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.JsonConverter;
using System;
using System.Collections.Generic;
using System.Linq;
@ -11,16 +12,30 @@ namespace N_m3u8DL_RE.Common.Util
{
public class GlobalUtil
{
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() }
};
private static readonly JsonContext Context = new JsonContext(Options);
public static string ConvertToJson(object o)
{
var options = new JsonSerializerOptions
if (o is StreamSpec s)
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() }
};
return JsonSerializer.Serialize(o, options);
return JsonSerializer.Serialize(s, Context.StreamSpec);
}
else if (o is IOrderedEnumerable<StreamSpec> ss)
{
return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);
}
else if (o is List<StreamSpec> sList)
{
return JsonSerializer.Serialize(sList, Context.ListStreamSpec);
}
return JsonSerializer.Serialize(o, Options);
}
//此函数用于格式化输出时长

View File

@ -126,7 +126,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
if (mimeType == null)
{
mimeType = representation.Attribute("contentType")?.Value ?? representation.Attribute("mimeType")?.Value ?? "";
mimeType = representation.Attribute("contentType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value ?? "";
}
var bandwidth = representation.Attribute("bandwidth");
StreamSpec streamSpec = new();
@ -139,17 +139,34 @@ namespace N_m3u8DL_RE.Parser.Extractor
streamSpec.FrameRate = frameRate ?? GetFrameRate(representation);
streamSpec.Resolution = representation.Attribute("width")?.Value != null ? $"{representation.Attribute("width")?.Value}x{representation.Attribute("height")?.Value}" : null;
streamSpec.Url = MpdUrl;
streamSpec.MediaType = mimeType.Split("/")[0] switch
streamSpec.MediaType = mimeType.Split('/')[0] switch
{
"text" => MediaType.SUBTITLES,
"audio" => MediaType.AUDIO,
_ => null
};
//推测后缀名
var mType = representation.Attribute("mimeType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value;
if (mType != null)
{
var mTypeSplit = mType.Split('/');
streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null;
}
//优化字幕场景识别
if (streamSpec.Codecs == "stpp" || streamSpec.Codecs == "wvtt")
{
streamSpec.MediaType = MediaType.SUBTITLES;
}
//优化字幕场景识别
var role = representation.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault() ?? adaptationSet.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault();
if (role != null)
{
var v = role.Attribute("value")?.Value;
if (v == "subtitle")
streamSpec.MediaType = MediaType.SUBTITLES;
if (mType != null && mType.Contains("ttml"))
streamSpec.Extension = "ttml";
}
streamSpec.Playlist.IsLive = isLive;
//设置刷新间隔 timeShiftBufferDepth / 2
if (timeShiftBufferDepth != null)
@ -188,7 +205,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
}
else
{
var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value);
var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!);
var initRange = initialization.Attribute("range")?.Value;
streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.Playlist.MediaInit.Url = PreProcessUrl(initUrl);
@ -211,7 +228,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var initialization = segmentList.Elements().Where(e => e.Name.LocalName == "Initialization").FirstOrDefault();
if (initialization != null)
{
var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value);
var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!);
var initRange = initialization.Attribute("range")?.Value;
streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.Playlist.MediaInit.Url = PreProcessUrl(initUrl);
@ -227,7 +244,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
for (int segmentIndex = 0; segmentIndex < segmentURLs.Count(); segmentIndex++)
{
var segmentURL = segmentURLs.ElementAt(segmentIndex);
var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute("media")?.Value);
var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute("media")?.Value!);
var mediaRange = segmentURL.Attribute("range")?.Value;
MediaSegment mediaSegment = new();
mediaSegment.Duration = Convert.ToDouble(duration);
@ -253,20 +270,23 @@ namespace N_m3u8DL_RE.Parser.Extractor
if (segmentTemplateElements.Any() || segmentTemplateElementsOuter.Any())
{
//优先使用最近的元素
var segmentTemplate = segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault();
var segmentTemplateOuter = segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault();
var segmentTemplate = (segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault())!;
var segmentTemplateOuter = (segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault())!;
var varDic = new Dictionary<string, object?>();
varDic[DASHTags.TemplateRepresentationID] = streamSpec.GroupId;
varDic[DASHTags.TemplateBandwidth] = bandwidth?.Value;
//timesacle
var timescaleStr = segmentTemplate.Attribute("timescale")?.Value ?? segmentTemplateOuter.Attribute("timescale")?.Value ?? "1";
var durationStr = segmentTemplate.Attribute("duration")?.Value ?? segmentTemplateOuter.Attribute("duration")?.Value;
var startNumberStr = segmentTemplate.Attribute("startNumber")?.Value ?? segmentTemplateOuter.Attribute("startNumber")?.Value ?? "0";
var startNumberStr = segmentTemplate.Attribute("startNumber")?.Value ?? segmentTemplateOuter.Attribute("startNumber")?.Value ?? "1";
//处理init url
var initialization = segmentTemplate.Attribute("initialization")?.Value ?? segmentTemplateOuter.Attribute("initialization")?.Value;
var initUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, initialization), varDic);
streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.Playlist.MediaInit.Url = PreProcessUrl(initUrl);
if (initialization != null)
{
var initUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, initialization), varDic);
streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.Playlist.MediaInit.Url = PreProcessUrl(initUrl);
}
//处理分片
var media = segmentTemplate.Attribute("media")?.Value ?? segmentTemplateOuter.Attribute("media")?.Value;
var segmentTimeline = segmentTemplate.Elements().Where(e => e.Name.LocalName == "SegmentTimeline").FirstOrDefault();
@ -290,7 +310,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var _repeatCount = Convert.ToInt64(_repeatCountStr);
varDic[DASHTags.TemplateTime] = currentTime;
varDic[DASHTags.TemplateNumber] = segNumber++;
var mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media), varDic);
var mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media!), varDic);
MediaSegment mediaSegment = new();
mediaSegment.Url = PreProcessUrl(mediaUrl);
mediaSegment.Duration = _duration / (double)timescale;
@ -307,7 +327,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
MediaSegment _mediaSegment = new();
varDic[DASHTags.TemplateTime] = currentTime;
varDic[DASHTags.TemplateNumber] = segNumber++;
var _mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media), varDic);
var _mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media!), varDic);
_mediaSegment.Url = PreProcessUrl(_mediaUrl);
_mediaSegment.Index = segIndex++;
_mediaSegment.Duration = _duration / (double)timescale;
@ -327,9 +347,9 @@ namespace N_m3u8DL_RE.Parser.Extractor
if (totalNumber == 0 && isLive)
{
var now = publishTime == null ? DateTime.Now : DateTime.Parse(publishTime);
var availableTime = DateTime.Parse(availabilityStartTime);
var availableTime = DateTime.Parse(availabilityStartTime!);
var ts = now - availableTime;
var updateTs = XmlConvert.ToTimeSpan(timeShiftBufferDepth);
var updateTs = XmlConvert.ToTimeSpan(timeShiftBufferDepth!);
//(当前时间到发布时间的时间差 - 最小刷新间隔) / 分片时长
startNumber += (long)((ts.TotalSeconds - updateTs.TotalSeconds) * timescale / duration);
totalNumber = (long)(updateTs.TotalSeconds * timescale / duration);
@ -337,7 +357,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
for (long index = startNumber, segIndex = 0; index < startNumber + totalNumber; index++, segIndex++)
{
varDic[DASHTags.TemplateNumber] = index;
var mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media), varDic);
var mediaUrl = ParserUtil.ReplaceVars(ParserUtil.CombineURL(segBaseUrl, media!), varDic);
MediaSegment mediaSegment = new();
mediaSegment.Url = PreProcessUrl(mediaUrl);
mediaSegment.Index = isLive ? index : segIndex; //直播直接用startNumber
@ -389,7 +409,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var startIndex = streamList[_index].Playlist?.MediaParts.Last().MediaSegments.Last().Index + 1;
foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)
{
item.Index = item.Index + startIndex.Value;
item.Index = item.Index + startIndex!.Value;
}
streamList[_index].Playlist?.MediaParts.Add(new MediaPart()
{
@ -400,6 +420,11 @@ namespace N_m3u8DL_RE.Parser.Extractor
}
else
{
//分片默认后缀m4s
if (streamSpec.Extension == null || streamSpec.Playlist.MediaParts.Sum(x => x.MediaSegments.Count) > 1)
{
streamSpec.Extension = "m4s";
}
streamList.Add(streamSpec);
//将segBaseUrl恢复 (重要)
segBaseUrl = this.BaseUrl;

View File

@ -87,7 +87,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
List<StreamSpec> streams = new List<StreamSpec>();
using StringReader sr = new StringReader(M3u8Content);
string line;
string? line;
bool expectPlaylist = false;
StreamSpec streamSpec = new();
@ -205,7 +205,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
bool hasAd = false;
using StringReader sr = new StringReader(M3u8Content);
string line;
string? line;
bool expectSegment = false;
bool isEndlist = false;
long segIndex = 0;
@ -449,7 +449,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
return playlist;
}
private byte[] ParseKey(string method, string uriText)
private byte[]? ParseKey(string method, string uriText)
{
foreach (var p in ParserConfig.KeyProcessors)
{
@ -476,12 +476,14 @@ namespace N_m3u8DL_RE.Parser.Extractor
}
else
{
var playlist = await ParseListAsync();
return new List<StreamSpec>()
{
new StreamSpec()
{
Url = ParserConfig.Url,
Playlist = await ParseListAsync()
Playlist = playlist,
Extension = playlist.MediaInit != null ? "mp4" : "ts"
}
};
}
@ -508,8 +510,9 @@ namespace N_m3u8DL_RE.Parser.Extractor
for (int i = 0; i < lists.Count; i++)
{
//重新加载m3u8
await LoadM3u8FromUrlAsync(lists[i].Url);
await LoadM3u8FromUrlAsync(lists[i].Url!);
lists[i].Playlist = await ParseListAsync();
lists[i].Extension = lists[i].Playlist!.MediaInit != null ? "mp4" : "ts";
}
}
}

View File

@ -0,0 +1,63 @@
using System;
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
{
public BinaryReader2(System.IO.Stream stream) : base(stream) { }
public bool HasMoreData()
{
return BaseStream.Position < BaseStream.Length;
}
public long GetLength()
{
return BaseStream.Length;
}
public long GetPosition()
{
return BaseStream.Position;
}
public override int ReadInt32()
{
var data = base.ReadBytes(4);
if (BitConverter.IsLittleEndian)
Array.Reverse(data);
return BitConverter.ToInt32(data, 0);
}
public override short ReadInt16()
{
var data = base.ReadBytes(2);
if (BitConverter.IsLittleEndian)
Array.Reverse(data);
return BitConverter.ToInt16(data, 0);
}
public override long ReadInt64()
{
var data = base.ReadBytes(8);
if (BitConverter.IsLittleEndian)
Array.Reverse(data);
return BitConverter.ToInt64(data, 0);
}
public override uint ReadUInt32()
{
var data = base.ReadBytes(4);
if (BitConverter.IsLittleEndian)
Array.Reverse(data);
return BitConverter.ToUInt32(data, 0);
}
}
}

View File

@ -0,0 +1,345 @@
using System.Text;
/**
* Translated from shaka-player project
* https://github.com/nilaoda/Mp4SubtitleParser
* https://github.com/shaka-project/shaka-player
*/
namespace Mp4SubtitleParser
{
class ParsedBox
{
public MP4Parser Parser { get; set; }
public bool PartialOkay { get; set; }
public long Start { get; set; }
public uint Version { get; set; } = 1000;
public uint Flags { get; set; } = 1000;
public BinaryReader2 Reader { get; set; }
public bool Has64BitSize { get; set; }
}
class TFHD
{
public uint TrackId { get; set; }
public uint DefaultSampleDuration { get; set; }
public uint DefaultSampleSize { get; set; }
}
class TRUN
{
public uint SampleCount { get; set; }
public List<Sample> SampleData { get; set; } = new List<Sample>();
}
class Sample
{
public uint SampleDuration { get; set; }
public uint SampleSize { get; set; }
public uint SampleCompositionTimeOffset { get; set; }
}
enum BoxType
{
BASIC_BOX = 0,
FULL_BOX = 1
};
class MP4Parser
{
public bool Done { get; set; } = false;
public Dictionary<long, int> Headers { get; set; } = new Dictionary<long, int>();
public Dictionary<long, BoxHandler> BoxDefinitions { get; set; } = new Dictionary<long, BoxHandler>();
public delegate void BoxHandler(ParsedBox box);
public delegate void DataHandler(byte[] data);
public static BoxHandler AllData(DataHandler handler)
{
return (box) =>
{
var all = box.Reader.GetLength() - box.Reader.GetPosition();
handler(box.Reader.ReadBytes((int)all));
};
}
public static void Children(ParsedBox box)
{
var headerSize = HeaderSize(box);
while (box.Reader.HasMoreData() && !box.Parser.Done)
{
box.Parser.ParseNext(box.Start + headerSize, box.Reader, box.PartialOkay);
}
}
public static void SampleDescription(ParsedBox box)
{
var headerSize = HeaderSize(box);
var count = box.Reader.ReadUInt32();
for (int i = 0; i < count; i++)
{
box.Parser.ParseNext(box.Start + headerSize, box.Reader, box.PartialOkay);
if (box.Parser.Done)
{
break;
}
}
}
public void Parse(byte[] data, bool partialOkay = false, bool stopOnPartial = false)
{
var reader = new BinaryReader2(new MemoryStream(data));
this.Done = false;
while (reader.HasMoreData() && !this.Done)
{
this.ParseNext(0, reader, partialOkay, stopOnPartial);
}
}
private void ParseNext(long absStart, BinaryReader2 reader, bool partialOkay, bool stopOnPartial = false)
{
var start = reader.GetPosition();
// size(4 bytes) + type(4 bytes) = 8 bytes
if (stopOnPartial && start + 8 > reader.GetLength())
{
this.Done = true;
return;
}
long size = reader.ReadUInt32();
long type = reader.ReadUInt32();
var name = TypeToString(type);
var has64BitSize = false;
//Console.WriteLine($"Parsing MP4 box: {name}");
switch (size)
{
case 0:
size = reader.GetLength() - start;
break;
case 1:
if (stopOnPartial && reader.GetPosition() + 8 > reader.GetLength())
{
this.Done = true;
return;
}
size = (long)reader.ReadUInt64();
has64BitSize = true;
break;
}
BoxHandler boxDefinition = null;
this.BoxDefinitions.TryGetValue(type, out boxDefinition);
if (boxDefinition != null)
{
uint version = 1000;
uint flags = 1000;
if (this.Headers[type] == (int)BoxType.FULL_BOX)
{
if (stopOnPartial && reader.GetPosition() + 4 > reader.GetLength())
{
this.Done = true;
return;
}
var versionAndFlags = reader.ReadUInt32();
version = versionAndFlags >> 24;
flags = versionAndFlags & 0xFFFFFF;
}
var end = start + size;
if (partialOkay && end > reader.GetLength())
{
// For partial reads, truncate the payload if we must.
end = reader.GetLength();
}
if (stopOnPartial && end > reader.GetLength())
{
this.Done = true;
return;
}
int payloadSize = (int)(end - reader.GetPosition());
var payload = (payloadSize > 0) ? reader.ReadBytes(payloadSize) : new byte[0];
var box = new ParsedBox()
{
Parser = this,
PartialOkay = partialOkay || false,
Version = version,
Flags = flags,
Reader = new BinaryReader2(new MemoryStream(payload)),
Start = start + absStart,
Has64BitSize = has64BitSize,
};
boxDefinition(box);
}
else
{
// Move the read head to be at the end of the box.
// If the box is longer than the remaining parts of the file, e.g. the
// mp4 is improperly formatted, or this was a partial range request that
// ended in the middle of a box, just skip to the end.
var skipLength = Math.Min(
start + size - reader.GetPosition(),
reader.GetLength() - reader.GetPosition());
reader.ReadBytes((int)skipLength);
}
}
private static int HeaderSize(ParsedBox box)
{
return /* basic header */ 8
+ /* additional 64-bit size field */ (box.Has64BitSize ? 8 : 0)
+ /* version and flags for a "full" box */ (box.Flags != 0 ? 4 : 0);
}
public static string TypeToString(long type)
{
return Encoding.UTF8.GetString(new byte[]
{
(byte)((type >> 24) & 0xff),
(byte)((type >> 16) & 0xff),
(byte)((type >> 8) & 0xff),
(byte)(type & 0xff)
});
}
private static int TypeFromString(string name)
{
if (name.Length != 4)
throw new Exception("Mp4 box names must be 4 characters long");
var code = 0;
foreach (var chr in name) {
code = (code << 8) | chr;
}
return code;
}
public MP4Parser Box(string type, BoxHandler handler)
{
var typeCode = TypeFromString(type);
this.Headers[typeCode] = (int)BoxType.BASIC_BOX;
this.BoxDefinitions[typeCode] = handler;
return this;
}
public MP4Parser FullBox(string type, BoxHandler handler)
{
var typeCode = TypeFromString(type);
this.Headers[typeCode] = (int)BoxType.FULL_BOX;
this.BoxDefinitions[typeCode] = handler;
return this;
}
public static uint ParseMDHD(BinaryReader2 reader, uint version)
{
if (version == 1)
{
reader.ReadBytes(8); // Skip "creation_time"
reader.ReadBytes(8); // Skip "modification_time"
}
else
{
reader.ReadBytes(4); // Skip "creation_time"
reader.ReadBytes(4); // Skip "modification_time"
}
return reader.ReadUInt32();
}
public static ulong ParseTFDT(BinaryReader2 reader, uint version)
{
return version == 1 ? reader.ReadUInt64() : reader.ReadUInt32();
}
public static TFHD ParseTFHD(BinaryReader2 reader, uint flags)
{
var trackId = reader.ReadUInt32();
uint defaultSampleDuration = 0;
uint defaultSampleSize = 0;
// Skip "base_data_offset" if present.
if ((flags & 0x000001) != 0)
{
reader.ReadBytes(8);
}
// Skip "sample_description_index" if present.
if ((flags & 0x000002) != 0)
{
reader.ReadBytes(4);
}
// Read "default_sample_duration" if present.
if ((flags & 0x000008) != 0)
{
defaultSampleDuration = reader.ReadUInt32();
}
// Read "default_sample_size" if present.
if ((flags & 0x000010) != 0)
{
defaultSampleSize = reader.ReadUInt32();
}
return new TFHD() { TrackId = trackId, DefaultSampleDuration = defaultSampleDuration, DefaultSampleSize = defaultSampleSize };
}
public static TRUN ParseTRUN(BinaryReader2 reader, uint version, uint flags)
{
var trun = new TRUN();
trun.SampleCount = reader.ReadUInt32();
// Skip "data_offset" if present.
if ((flags & 0x000001) != 0)
{
reader.ReadBytes(4);
}
// Skip "first_sample_flags" if present.
if ((flags & 0x000004) != 0)
{
reader.ReadBytes(4);
}
for (int i = 0; i < trun.SampleCount; i++)
{
var sample = new Sample();
// Read "sample duration" if present.
if ((flags & 0x000100) != 0)
{
sample.SampleDuration = reader.ReadUInt32();
}
// Read "sample_size" if present.
if ((flags & 0x000200) != 0)
{
sample.SampleSize = reader.ReadUInt32();
}
// Skip "sample_flags" if present.
if ((flags & 0x000400) != 0)
{
reader.ReadBytes(4);
}
// Read "sample_time_offset" if present.
if ((flags & 0x000800) != 0)
{
sample.SampleCompositionTimeOffset = version == 0 ?
reader.ReadUInt32() :
(uint)reader.ReadInt32();
}
trun.SampleData.Add(sample);
}
return trun;
}
}
}

View File

@ -0,0 +1,290 @@
using N_m3u8DL_RE.Common.Entity;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace Mp4SubtitleParser
{
class SubEntity
{
public string Begin { get; set; }
public string End { get; set; }
public string Region { get; set; }
public List<XmlElement> Contents { get; set; } = new List<XmlElement>();
public List<string> ContentStrings { get; set; } = new List<string>();
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
{
[RegexGenerator(">(.+?)<\\/p>")]
private static partial Regex LabelFixRegex();
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)
{
var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index);
return string.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
}
if (!xmlSrc.Contains("<?xml") || !xmlSrc.Contains("<head>")) return xmlSrc;
var xmlDoc = new XmlDocument();
XmlNamespaceManager? nsMgr = null;
xmlDoc.LoadXml(xmlSrc);
var ttNode = xmlDoc.LastChild;
if (nsMgr == null)
{
var ns = ((XmlElement)ttNode!).GetAttribute("xmlns");
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsMgr.AddNamespace("ns", ns);
}
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
if (bodyNode == null)
return xmlSrc;
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
//Parse <p> label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{
var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end");
_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.NodeType == XmlNodeType.Element && item.Name == "br")
{
sb.AppendLine();
}
}
return sb.ToString();
}
public static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs)
{
//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;
// Join this to any previous payload, in case the mp4 has multiple
// mdats.
if (segTimeMs != 0)
{
xmls.Add(ShiftTime(Encoding.UTF8.GetString(data), segTimeMs, segIndex));
}
else
{
xmls.Add(Encoding.UTF8.GetString(data));
}
}))
.Parse(dataSeg,/* partialOkay= */ false);
segIndex++;
}
return ExtractSub(xmls);
}
public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs)
{
//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);
}
private static WebVttSub ExtractSub(List<string> xmls)
{
//parsing
var xmlDoc = new XmlDocument();
var finalSubs = new List<SubEntity>();
XmlNode? headNode = null;
XmlNamespaceManager? nsMgr = null;
var regex = LabelFixRegex();
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))
{
if (!m.Groups[1].Value.StartsWith("<span"))
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);
//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 sub = new SubEntity
{
Begin = _begin,
End = _end,
Region = _region
};
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);
}
}
//Check if one <p> has been splitted
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
//Skip empty lines
if (sub.ContentStrings.Count > 0)
{
//Extend <p> duration
if (index != -1)
finalSubs[index].End = sub.End;
else if (!finalSubs.Contains(sub))
finalSubs.Add(sub);
}
}
}
var dic = new Dictionary<string, string>();
foreach (var sub in finalSubs)
{
var key = $"{sub.Begin} --> {sub.End}";
foreach (var item in sub.Contents)
{
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));
}
}
}
StringBuilder 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());
}
}
}

View File

@ -0,0 +1,216 @@
using N_m3u8DL_RE.Common.Entity;
using System.Text;
namespace Mp4SubtitleParser
{
public class MP4VttUtil
{
public static (bool, uint) CheckInit(byte[] data)
{
uint timescale = 0;
bool sawWVTT = false;
//parse init
new MP4Parser()
.Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children)
.Box("mdia", MP4Parser.Children)
.FullBox("mdhd", (box) =>
{
if (!(box.Version == 0 || box.Version == 1))
throw new Exception("MDHD version can only be 0 or 1");
timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
})
.Box("minf", MP4Parser.Children)
.Box("stbl", MP4Parser.Children)
.FullBox("stsd", MP4Parser.SampleDescription)
.Box("wvtt", (box) => {
// A valid vtt init segment, though we have no actual subtitles yet.
sawWVTT = true;
})
.Parse(data);
return (sawWVTT, timescale);
}
public static WebVttSub ExtractSub(IEnumerable<string> files, uint timescale)
{
if (timescale == 0)
throw new Exception("Missing timescale for VTT content!");
List<SubCue> cues = new();
foreach (var item in files)
{
var dataSeg = File.ReadAllBytes(item);
bool sawTFDT = false;
bool sawTRUN = false;
bool sawMDAT = false;
byte[]? rawPayload = null;
ulong baseTime = 0;
ulong defaultDuration = 0;
List<Sample> presentations = new();
//parse media
new MP4Parser()
.Box("moof", MP4Parser.Children)
.Box("traf", MP4Parser.Children)
.FullBox("tfdt", (box) =>
{
sawTFDT = true;
if (!(box.Version == 0 || box.Version == 1))
throw new Exception("TFDT version can only be 0 or 1");
baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
})
.FullBox("tfhd", (box) =>
{
if (box.Flags == 1000)
throw new Exception("A TFHD box should have a valid flags value");
defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
})
.FullBox("trun", (box) =>
{
sawTRUN = true;
if (box.Version == 1000)
throw new Exception("A TRUN box should have a valid version value");
if (box.Flags == 1000)
throw new Exception("A TRUN box should have a valid flags value");
presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
})
.Box("mdat", MP4Parser.AllData((data) =>
{
if (sawMDAT)
throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported");
sawMDAT = true;
rawPayload = data;
}))
.Parse(dataSeg,/* partialOkay= */ false);
if (!sawMDAT && !sawTFDT && !sawTRUN)
{
throw new Exception("A required box is missing");
}
var currentTime = baseTime;
var reader = new BinaryReader2(new MemoryStream(rawPayload!));
foreach (var presentation in presentations)
{
var duration = presentation.SampleDuration == 0 ? defaultDuration : presentation.SampleDuration;
var startTime = presentation.SampleCompositionTimeOffset != 0 ?
baseTime + presentation.SampleCompositionTimeOffset :
currentTime;
currentTime = startTime + duration;
var totalSize = 0;
do
{
// Read the payload size.
var payloadSize = (int)reader.ReadUInt32();
totalSize += payloadSize;
// Skip the type.
var payloadType = reader.ReadUInt32();
var payloadName = MP4Parser.TypeToString(payloadType);
// Read the data payload.
byte[]? payload = null;
if (payloadName == "vttc")
{
if (payloadSize > 8)
{
payload = reader.ReadBytes(payloadSize - 8);
}
}
else if (payloadName == "vtte")
{
// It's a vtte, which is a vtt cue that is empty. Ignore any data that
// does exist.
reader.ReadBytes(payloadSize - 8);
}
else
{
Console.WriteLine($"Unknown box {payloadName}! Skipping!");
reader.ReadBytes(payloadSize - 8);
}
if (duration != 0)
{
if (payload != null)
{
if (timescale == 0)
throw new Exception("Timescale should not be zero!");
var cue = ParseVTTC(
payload,
0 + (double)startTime / timescale,
0 + (double)currentTime / timescale);
//Check if same subtitle has been splitted
if (cue != null)
{
var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload);
if (index != -1)
{
cues[index].EndTime = cue.EndTime;
}
else
{
cues.Add(cue);
}
}
}
}
else
{
throw new Exception("WVTT sample duration unknown, and no default found!");
}
if (!(presentation.SampleSize == 0 || totalSize <= presentation.SampleSize))
{
throw new Exception("The samples do not fit evenly into the sample sizes given in the TRUN box!");
}
} while (presentation.SampleSize != 0 && (totalSize < presentation.SampleSize));
if (reader.HasMoreData())
{
//throw new Exception("MDAT which contain VTT cues and non-VTT data are not currently supported!");
}
}
}
if (cues.Count > 0)
{
return new WebVttSub() { Cues = cues };
}
return new WebVttSub();
}
private static SubCue? ParseVTTC(byte[] data, double startTime, double endTime)
{
string payload = string.Empty;
string id = string.Empty;
string settings = string.Empty;
new MP4Parser()
.Box("payl", MP4Parser.AllData((data) =>
{
payload = Encoding.UTF8.GetString(data);
}))
.Box("iden", MP4Parser.AllData((data) =>
{
id = Encoding.UTF8.GetString(data);
}))
.Box("sttg", MP4Parser.AllData((data) =>
{
settings = Encoding.UTF8.GetString(data);
}))
.Parse(data);
if (!string.IsNullOrEmpty(payload))
{
return new SubCue() { StartTime = TimeSpan.FromSeconds(startTime), EndTime = TimeSpan.FromSeconds(endTime), Payload = payload, Settings = settings };
}
return null;
}
}
}

View File

@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<OutputType>library</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>N_m3u8DL_RE.Parser</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -10,8 +10,10 @@ using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor
{
public class DefaultUrlProcessor : UrlProcessor
public partial class DefaultUrlProcessor : UrlProcessor
{
[RegexGenerator("\\?.*")]
private static partial Regex ParaRegex();
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => true;
@ -20,7 +22,7 @@ namespace N_m3u8DL_RE.Parser.Processor
if (paserConfig.AppendUrlParams)
{
Logger.Debug("Before: " + oriUrl);
oriUrl += new Regex("\\?.*").Match(paserConfig.Url).Value;
oriUrl += ParaRegex().Match(paserConfig.Url).Value;
Logger.Debug("After: " + oriUrl);
}

View File

@ -10,8 +10,17 @@ using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS
{
public class DefaultHLSContentProcessor : ContentProcessor
public partial class DefaultHLSContentProcessor : ContentProcessor
{
[RegexGenerator("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")]
private static partial Regex YkDVRegex();
[RegexGenerator("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPRegex();
[RegexGenerator("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
private static partial Regex OrderFixRegex();
[RegexGenerator("#EXT-X-MAP.*\\.apple\\.com/")]
private static partial Regex ATVRegex();
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
public override string Process(string m3u8Content, ParserConfig parserConfig)
@ -38,7 +47,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
//针对优酷#EXT-X-VERSION:7杜比视界片源修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
{
Regex ykmap = new Regex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"");
Regex ykmap = YkDVRegex();
foreach (Match m in ykmap.Matches(m3u8Content))
{
m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
@ -48,7 +57,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
//针对Disney+修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
{
Regex ykmap = new Regex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY");
Regex ykmap = DNSPRegex();
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
@ -56,10 +65,10 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
}
//针对AppleTv修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || Regex.IsMatch(m3u8Content, "#EXT-X-MAP.*\\.apple\\.com/")))
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || ATVRegex().IsMatch(m3u8Content)))
{
//只取加密部分即可
Regex ykmap = new Regex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)");
Regex ykmap = DNSPRegex();
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
@ -67,9 +76,10 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
}
//修复#EXT-X-KEY与#EXTINF出现次序异常问题
if (Regex.IsMatch(m3u8Content, "(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)"))
var regex = OrderFixRegex();
if (regex.IsMatch(m3u8Content))
{
m3u8Content = Regex.Replace(m3u8Content, "(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)", "$3$2$1");
m3u8Content = regex.Replace(m3u8Content, "$3$2$1");
}
return m3u8Content;

View File

@ -16,7 +16,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
public override bool CanProcess(ExtractorType extractorType, string method, string uriText, ParserConfig paserConfig) => extractorType == ExtractorType.HLS;
public override byte[] Process(string method, string uriText, ParserConfig parserConfig)
public override byte[]? Process(string method, string uriText, ParserConfig parserConfig)
{
var encryptInfo = new EncryptInfo();

View File

@ -12,6 +12,6 @@ namespace N_m3u8DL_RE.Parser.Processor
public abstract class KeyProcessor
{
public abstract bool CanProcess(ExtractorType extractorType, string method, string uriText, ParserConfig parserConfig);
public abstract byte[] Process(string method, string uriText, ParserConfig parserConfig);
public abstract byte[]? Process(string method, string uriText, ParserConfig parserConfig);
}
}

View File

@ -72,7 +72,7 @@ namespace N_m3u8DL_RE.Parser
}
else
{
throw new Exception(ResString.notSupported);
throw new NotSupportedException(ResString.notSupported);
}
}

View File

@ -8,8 +8,11 @@ using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Util
{
internal class ParserUtil
internal partial class ParserUtil
{
[RegexGenerator("\\$Number%([^$]+)d\\$")]
private static partial Regex VarsNumberRegex();
/// <summary>
/// 从以下文本中获取参数
/// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
@ -21,18 +24,25 @@ namespace N_m3u8DL_RE.Parser.Util
{
line = line.Trim();
if (key == "")
return line.Substring(line.IndexOf(':') + 1);
return line[(line.IndexOf(':') + 1)..];
if (line.Contains(key + "=\""))
var index = -1;
var result = string.Empty;
if ((index = line.IndexOf(key + "=\"")) > -1)
{
return Regex.Match(line, key + "=\"([^\"]*)\"").Groups[1].Value;
var startIndex = index + (key + "=\"").Length;
var endIndex = startIndex + line[startIndex..].IndexOf('\"');
result = line[startIndex..endIndex];
}
else if (line.Contains(key + "="))
else if ((index = line.IndexOf(key + "=")) > -1)
{
return Regex.Match(line, key + "=([^,]*)").Groups[1].Value;
var startIndex = index + (key + "=").Length;
var endIndex = startIndex + line[startIndex..].IndexOf(',');
if (endIndex >= startIndex) result = line[startIndex..endIndex];
else result = line[startIndex..];
}
return string.Empty;
return result;
}
/// <summary>
@ -80,10 +90,10 @@ namespace N_m3u8DL_RE.Parser.Util
{
foreach (var item in keyValuePairs)
if (text.Contains(item.Key))
text = text.Replace(item.Key, item.Value.ToString());
text = text.Replace(item.Key, item.Value!.ToString());
//处理特殊形式数字 如 $Number%05d$
var regex = new Regex("\\$Number%([^$]+)d\\$");
var regex = VarsNumberRegex();
if (regex.IsMatch(text) && keyValuePairs.ContainsKey(DASHTags.TemplateNumber))
{
foreach (Match m in regex.Matches(text))

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Config
{
internal class DownloaderConfig
{
/// <summary>
/// 临时文件存储目录
/// </summary>
public string? TmpDir { get; set; }
/// <summary>
/// 文件存储目录
/// </summary>
public string? SaveDir { get; set; }
/// <summary>
/// 文件名
/// </summary>
public string? SaveName { get; set; }
/// <summary>
/// 线程数
/// </summary>
public int ThreadCount { get; set; } = 8;
/// <summary>
/// 跳过合并
/// </summary>
public bool SkipMerge { get; set; } = false;
/// <summary>
/// 二进制合并
/// </summary>
public bool BinaryMerge { get; set; } = false;
/// <summary>
/// 完成后是否删除临时文件
/// </summary>
public bool DelAfterDone { get; set; } = false;
/// <summary>
/// 校验有没有下完全部分片
/// </summary>
public bool CheckSegmentsCount { get; set; } = true;
/// <summary>
/// 校验响应头的文件大小和实际大小
/// </summary>
public bool CheckContentLength { get; set; } = true;
/// <summary>
/// 自动修复字幕
/// </summary>
public bool AutoSubtitleFix { get; set; } = true;
/// <summary>
/// 请求头
/// </summary>
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>()
{
["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
};
}
}

View File

@ -9,26 +9,19 @@ namespace N_m3u8DL_RE.Crypto
{
internal class AESUtil
{
public static byte[] AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
/// <summary>
/// AES-128解密解密后原地替换文件
/// </summary>
/// <param name="filePath"></param>
/// <param name="keyByte"></param>
/// <param name="ivByte"></param>
/// <param name="mode"></param>
/// <param name="padding"></param>
public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
{
FileStream fs = new FileStream(filePath, FileMode.Open);
//获取文件大小
long size = fs.Length;
byte[] inBuff = new byte[size];
fs.Read(inBuff, 0, inBuff.Length);
fs.Close();
Aes dcpt = Aes.Create();
dcpt.BlockSize = 128;
dcpt.KeySize = 128;
dcpt.Key = keyByte;
dcpt.IV = ivByte;
dcpt.Mode = mode;
dcpt.Padding = padding;
ICryptoTransform cTransform = dcpt.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length);
return resultArray;
var fileBytes = File.ReadAllBytes(filePath);
var decrypted = AES128Decrypt(fileBytes, keyByte, ivByte, mode, padding);
File.WriteAllBytes(filePath, decrypted);
}
public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)

View File

@ -4,8 +4,7 @@
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<StaticallyLinked Condition="$(RuntimeIdentifier.StartsWith('win'))">true</StaticallyLinked>
<TrimMode>Link</TrimMode>
<TrimmerDefaultAction>link</TrimmerDefaultAction>
<TrimMode>full</TrimMode>
<NativeAotCompilerVersion>7.0.0-*</NativeAotCompilerVersion>
</PropertyGroup>
@ -17,7 +16,6 @@
</ItemGroup>
<ItemGroup>
<IlcArg Include="--reflectedonly" />
<RdXmlFile Include="rd.xml" />
</ItemGroup>

View File

@ -0,0 +1,303 @@
using Mp4SubtitleParser;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Config;
using N_m3u8DL_RE.Downloader;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Util;
using Spectre.Console;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.DownloadManager
{
internal class SimpleDownloadManager
{
IDownloader Downloader;
DownloaderConfig DownloaderConfig;
public SimpleDownloadManager(DownloaderConfig downloaderConfig)
{
this.DownloaderConfig = downloaderConfig;
Downloader = new SimpleDownloader(DownloaderConfig);
}
private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task)
{
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments);
if (segments == null) return false;
var dirName = $"{streamSpec.GroupId}_{streamSpec.Codecs}_{streamSpec.Language}";
var tmpDir = DownloaderConfig.TmpDir ?? Path.Combine(Environment.CurrentDirectory, dirName);
var saveDir = DownloaderConfig.SaveDir ?? Environment.CurrentDirectory;
var saveName = DownloaderConfig.SaveName ?? dirName;
var headers = DownloaderConfig.Headers;
var output = Path.Combine(saveDir, saveName + $".{streamSpec.Extension ?? "ts"}");
Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}; output: {output}");
//创建文件夹
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
var totalCount = segments.Count();
if (streamSpec.Playlist?.MediaInit != null)
{
totalCount++;
}
task.MaxValue = totalCount;
task.StartTask();
//开始下载
Logger.InfoMarkUp(ResString.startDownloading + streamSpec.ToShortString());
//下载init
if (streamSpec.Playlist?.MediaInit != null)
{
totalCount++;
var path = Path.Combine(tmpDir, "_init.mp4.tmp");
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, headers);
FileDic[streamSpec.Playlist.MediaInit] = result;
task.Increment(1);
//修改输出后缀
output = Path.ChangeExtension(output, ".mp4");
}
//开始下载
var pad = "0".PadLeft(segments.Count().ToString().Length, '0');
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = DownloaderConfig.ThreadCount
};
await Parallel.ForEachAsync(segments, options, async (seg, _) =>
{
var index = seg.Index;
var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, headers);
FileDic[seg] = result;
task.Increment(1);
});
//校验分片数量
if (DownloaderConfig.CheckSegmentsCount && FileDic.Values.Any(s => s == null))
{
Logger.WarnMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Where(s => s != null).Count());
return false;
}
//移除无效片段
var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key);
foreach (var badKey in badKeys)
{
FileDic!.Remove(badKey, out _);
}
//校验完整性
if (DownloaderConfig.CheckContentLength && FileDic.Values.Any(a => a!.Success == false))
{
return false;
}
//自动修复VTT raw字幕
if (DownloaderConfig.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Extension != null && streamSpec.Extension.Contains("vtt"))
{
Logger.WarnMarkUp(ResString.fixingVTT);
//排序字幕并修正时间戳
bool first = true;
var finalVtt = new WebVttSub();
var keys = FileDic.Keys.OrderBy(k => k.Index);
foreach (var seg in keys)
{
var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath);
var vtt = WebVttSub.Parse(vttContent);
if (first)
{
finalVtt = vtt;
first = false;
}
else
{
finalVtt.AddCuesFromOne(vtt);
}
}
//写出字幕
var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray();
foreach (var item in files) File.Delete(item);
FileDic.Clear();
var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + $".fix.{streamSpec.Extension ?? "clip"}");
var vttContentFixed = finalVtt.ToStringWithHeader();
await File.WriteAllTextAsync(path, vttContentFixed, new UTF8Encoding(false));
FileDic[keys.First()] = new DownloadResult()
{
ActualContentLength = vttContentFixed.Length,
ActualFilePath = path
};
}
//自动修复VTT mp4字幕
if (DownloaderConfig.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s"))
{
var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
if (sawVtt)
{
Logger.WarnMarkUp(ResString.fixingVTTmp4);
var mp4s = FileDic.Values.Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).OrderBy(s => s).ToArray();
var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale);
//写出字幕
var firstKey = FileDic.Keys.First();
var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray();
foreach (var item in files) File.Delete(item);
FileDic.Clear();
var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
var vttContentFixed = finalVtt.ToStringWithHeader();
await File.WriteAllTextAsync(path, vttContentFixed, new UTF8Encoding(false));
FileDic[firstKey] = new DownloadResult()
{
ActualContentLength = vttContentFixed.Length,
ActualFilePath = path
};
//修改输出后缀
output = Path.ChangeExtension(output, ".vtt");
}
}
//自动修复TTML raw字幕
if (DownloaderConfig.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Extension != null && streamSpec.Extension.Contains("ttml"))
{
Logger.WarnMarkUp(ResString.fixingTTML);
var mp4s = FileDic.Values.Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".ttml")).OrderBy(s => s).ToArray();
var finalVtt = MP4TtmlUtil.ExtractFromTTMLs(mp4s, 0);
//写出字幕
var firstKey = FileDic.Keys.First();
var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray();
foreach (var item in files) File.Delete(item);
FileDic.Clear();
var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
var vttContentFixed = finalVtt.ToStringWithHeader();
await File.WriteAllTextAsync(path, vttContentFixed, new UTF8Encoding(false));
FileDic[firstKey] = new DownloadResult()
{
ActualContentLength = vttContentFixed.Length,
ActualFilePath = path
};
//修改输出后缀
output = Path.ChangeExtension(output, ".vtt");
}
//自动修复TTML mp4字幕
if (DownloaderConfig.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
{
Logger.WarnMarkUp(ResString.fixingTTMLmp4);
//sawTtml暂时不判断
//var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
//var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
//var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);
var mp4s = FileDic.Values.Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).OrderBy(s => s).ToArray();
var finalVtt = MP4TtmlUtil.ExtractFromMp4s(mp4s, 0);
//写出字幕
var firstKey = FileDic.Keys.First();
var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray();
foreach (var item in files) File.Delete(item);
FileDic.Clear();
var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
var vttContentFixed = finalVtt.ToStringWithHeader();
await File.WriteAllTextAsync(path, vttContentFixed, new UTF8Encoding(false));
FileDic[firstKey] = new DownloadResult()
{
ActualContentLength = vttContentFixed.Length,
ActualFilePath = path
};
//修改输出后缀
output = Path.ChangeExtension(output, ".vtt");
}
//合并
if (!DownloaderConfig.SkipMerge)
{
if (DownloaderConfig.BinaryMerge)
{
Logger.InfoMarkUp(ResString.binaryMerge);
var files = FileDic.Values.Select(v => v!.ActualFilePath).OrderBy(s => s).ToArray();
DownloadUtil.CombineMultipleFilesIntoSingleFile(files, output);
}
else
{
throw new NotImplementedException();
}
}
//删除临时文件夹
if (DownloaderConfig.DelAfterDone)
{
var files = FileDic.Values.Select(v => v!.ActualFilePath);
foreach (var file in files)
{
File.Delete(file);
}
if (!Directory.EnumerateFiles(tmpDir).Any())
{
Directory.Delete(tmpDir);
}
}
return true;
}
public async Task<bool> StartDownloadAsync(IEnumerable<StreamSpec> streamSpecs)
{
ConcurrentDictionary<StreamSpec, bool?> Results = new();
var progress = AnsiConsole.Progress().AutoClear(true);
//进度条的列定义
progress.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn() { Alignment = Justify.Left },
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn(),
});
await progress.StartAsync(async ctx =>
{
//创建任务
var dic = streamSpecs.Select(item =>
{
var task = ctx.AddTask(item.ToShortString(), autoStart: false);
return (item, task);
}).ToDictionary(item => item.item, item => item.task);
//遍历,顺序下载
foreach (var kp in dic)
{
var task = kp.Value;
var result = await DownloadStreamAsync(kp.Key, task);
Results[kp.Key] = result;
}
});
return Results.Values.All(v => v == true);
}
}
}

View File

@ -0,0 +1,15 @@
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Downloader
{
internal interface IDownloader
{
Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, Dictionary<string, string>? headers = null);
}
}

View File

@ -0,0 +1,90 @@
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Config;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Util;
using Spectre.Console;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Downloader
{
/// <summary>
/// 简单下载器
/// </summary>
internal class SimpleDownloader : IDownloader
{
DownloaderConfig DownloaderConfig;
public SimpleDownloader(DownloaderConfig config)
{
DownloaderConfig = config;
}
public async Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, Dictionary<string, string>? headers = null)
{
var url = segment.Url;
var dResult = await DownClipAsync(url, savePath, segment.StartRange, segment.StopRange, headers);
if (dResult != null && dResult.Success && segment.EncryptInfo != null)
{
if (segment.EncryptInfo.Method == EncryptMethod.AES_128)
{
var key = segment.EncryptInfo.Key;
var iv = segment.EncryptInfo.IV;
Crypto.AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!);
}
else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB)
{
var key = segment.EncryptInfo.Key;
var iv = segment.EncryptInfo.IV;
Crypto.AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB);
}
else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR)
{
throw new NotSupportedException("SAMPLE-AES-CTR");
}
}
return dResult;
}
private async Task<DownloadResult?> DownClipAsync(string url, string path, long? fromPosition, long? toPosition, Dictionary<string, string>? headers = null, int retryCount = 3)
{
retry:
try
{
var des = Path.ChangeExtension(path, null);
//已下载过跳过
if (File.Exists(des))
{
return new DownloadResult() { ActualContentLength = 0, ActualFilePath = des };
}
var result = await DownloadUtil.DownloadToFileAsync(url, path, headers, fromPosition, toPosition);
//下载完成后改名
if (result.Success || !DownloaderConfig.CheckContentLength)
{
File.Move(path, des);
result.ActualFilePath = des;
return result;
}
throw new Exception("please retry");
}
catch (Exception ex)
{
Logger.Debug(ex.ToString());
if (retryCount-- > 0)
{
await Task.Delay(200);
goto retry;
}
//throw new Exception("download failed", ex);
return null;
}
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity
{
internal class DownloadResult
{
public bool Success { get => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength == null ? false : true); }
public long? RespContentLength { get; set; }
public long? ActualContentLength { get; set; }
public required string ActualFilePath { get; set; }
}
}

View File

@ -2,9 +2,10 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>N_m3u8DL_RE</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
@ -17,7 +18,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Downloader\" />
<Folder Include="Subtitle\" />
</ItemGroup>
</Project>

View File

@ -20,11 +20,11 @@ namespace N_m3u8DL_RE.Processor
return extractorType == ExtractorType.HLS && parserConfig.Url.Contains("playertest.longtailvideo.com");
}
public override byte[] Process(string method, string uriText, ParserConfig parserConfig)
public override byte[]? Process(string method, string uriText, ParserConfig parserConfig)
{
Logger.InfoMarkUp($"[white on green]My Key Processor => {uriText}[/]");
var key = new DefaultHLSKeyProcessor().Process(method, uriText, parserConfig);
Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(key, " ") + "[/]");
Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(key!, " ") + "[/]");
return key;
}
}

View File

@ -11,10 +11,14 @@ using N_m3u8DL_RE.Common.Log;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using N_m3u8DL_RE.Subtitle;
using System.Collections.Concurrent;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Processor;
using N_m3u8DL_RE.Downloader;
using N_m3u8DL_RE.Config;
using N_m3u8DL_RE.Util;
using System.Diagnostics;
using N_m3u8DL_RE.DownloadManager;
namespace N_m3u8DL_RE
{
@ -34,18 +38,30 @@ namespace N_m3u8DL_RE
try
{
var config = new ParserConfig();
var parserConfig = new ParserConfig();
//demo1
config.ContentProcessors.Insert(0, new DemoProcessor());
parserConfig.ContentProcessors.Insert(0, new DemoProcessor());
//demo2
config.KeyProcessors.Insert(0, new DemoProcessor2());
parserConfig.KeyProcessors.Insert(0, new DemoProcessor2());
var url = string.Empty;
//url = "http://livesim.dashif.org/livesim/mup_300/tsbd_500/testpic_2s/Manifest.mpd";
url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8";
//url = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd";
//url = "https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd"; //直播
//url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8";
//url = "https://vod.sdn.wavve.com/hls/S01/S01_E461382925.1/1/5000/chunklist.m3u8";
url = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd";
//url = "http://tv-live.ynkmit.com/tv/anning.m3u8?txSecret=7528f35fb4b62bd24d55b891899db68f&txTime=632C8680"; //直播
//url = "https://rest-as.ott.kaltura.com/api_v3/service/assetFile/action/playManifest/partnerId/147/assetId/1304099/assetType/media/assetFileId/16136929/contextType/PLAYBACK/isAltUrl/False/ks/djJ8MTQ3fMusTFH6PCZpcrfKLQwI-pPm9ex6b6r49wioe32WH2udXeM4reyWIkSDpi7HhvhxBHAHAKiHrcnkmIJQpyAt4MuDBG0ywGQ-jOeqQFcTRQ8BGJGw6g-smSBLwSbo4CCx9M9vWNJX3GkOfhoMAY4yRU-ur3okHiVq1mUJ82XBd_iVqLuzodnc9sJEtcHH0zc5CoPiTq2xor-dq3yDURnZm3isfSN3t9uLIJEW09oE-SJ84DM5GUuFUdbnIV8bdcWUsPicUg-Top1G2D3WcWXq4EvPnwvD8jrC_vsiOpLHf5akAwtdGsJ6__cXUmT7a-QlfjdvaZ5T8UhDLnttHmsxYs2E5c0lh4uOvvJou8dD8iYxUexlPI2j4QUkBRxqOEVLSNV3Y82-5TTRqgnK_uGYXHwk7EAmDws7hbLj2-DJ1heXDcye3OJYdunJgAS-9ma5zmQQNiY_HYh6wj2N1HpCTNAtWWga6R9fC0VgBTZbidW-YwMSGzIvMQfIfWKe15X7Oc_hCs-zGfW9XeRJZrutcWKK_D_HlzpQVBF2vIF3XgaI/a.mpd";
//url = "https://dash.akamaized.net/dash264/TestCases/2c/qualcomm/1/MultiResMPEG2.mpd";
//url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8";
//url = "https://cmaf.lln.latam.hbomaxcdn.com/videos/GYPGKMQjoDkVLBQEAAAAo/1/1b5ad5/1_single_J8sExA_1080hi.mpd";
//url = "https://livesim.dashif.org/dash/vod/testpic_2s/multi_subs.mpd"; //ttml + mp4
//url = "http://media.axprod.net/TestVectors/v6-Clear/Manifest_1080p.mpd"; //vtt + mp4
url = "https://livesim.dashif.org/dash/vod/testpic_2s/xml_subs.mpd"; //ttml
if (args.Length > 0)
{
url = args[0];
}
if (string.IsNullOrEmpty(url))
{
@ -53,7 +69,7 @@ namespace N_m3u8DL_RE
}
//流提取器配置
var extractor = new StreamExtractor(config);
var extractor = new StreamExtractor(parserConfig);
extractor.LoadSourceFromUrl(url);
//解析流信息
@ -91,8 +107,23 @@ namespace N_m3u8DL_RE
Logger.InfoMarkUp(item.ToString());
}
Logger.Info("按任意键继续");
Console.ReadKey();
//下载配置
var downloadConfig = new DownloaderConfig()
{
Headers = parserConfig.Headers,
BinaryMerge = true,
DelAfterDone = true,
CheckSegmentsCount = true
};
//开始下载
var sdm = new SimpleDownloadManager(downloadConfig);
var result = await sdm.StartDownloadAsync(selectedStreams);
if (result)
Logger.InfoMarkUp("[white on green]成功[/]");
else
Logger.ErrorMarkUp("[white on red]失败[/]");
}
catch (Exception ex)
{
@ -100,10 +131,5 @@ namespace N_m3u8DL_RE
}
//Console.ReadKey();
}
static void Print(object o)
{
Console.WriteLine(GlobalUtil.ConvertToJson(o));
}
}
}

View File

@ -1,35 +0,0 @@
using N_m3u8DL_RE.Common.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Subtitle
{
public class WebVTTUtil
{
/// <summary>
/// 修复VTT起始时间戳 <br/>
/// X-TIMESTAMP-MAP=MPEGTS:8528254208,LOCAL:00:00:00.000
/// </summary>
/// <param name="sub"></param>
/// <param name="baseTimestamp">基础时间戳</param>
/// <returns></returns>
public static void FixTimestamp(WebSub sub, long baseTimestamp)
{
if (baseTimestamp == 0 || sub.MpegtsTimestamp == 0)
{
return;
}
//The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second
var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;
for (int i = 0; i < sub.Cues.Count; i++)
{
sub.Cues[i].StartTime += TimeSpan.FromSeconds(seconds);
sub.Cues[i].EndTime += TimeSpan.FromSeconds(seconds);
}
}
}
}

View File

@ -0,0 +1,104 @@
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util
{
internal class DownloadUtil
{
private static readonly HttpClient AppHttpClient = new(new HttpClientHandler
{
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.All,
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
})
{
Timeout = TimeSpan.FromMinutes(2)
};
public static async Task<DownloadResult> DownloadToFileAsync(string url, string path, Dictionary<string, string>? headers = null, long? fromPosition = null, long? toPosition = null)
{
Logger.Debug(ResString.fetch + url);
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
if (fromPosition != null || toPosition != null)
request.Headers.Range = new(fromPosition, toPosition);
if (headers != null)
{
foreach (var item in headers)
{
request.Headers.TryAddWithoutValidation(item.Key, item.Value);
}
}
Logger.Debug(request.Headers.ToString());
using var response = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.Moved)
{
HttpResponseHeaders respHeaders = response.Headers;
Logger.Debug(respHeaders.ToString());
if (respHeaders != null && respHeaders.Location != null)
{
var redirectedUrl = respHeaders.Location.AbsoluteUri;
return await DownloadToFileAsync(redirectedUrl, path, headers);
}
}
response.EnsureSuccessStatusCode();
var contentLength = response.Content.Headers.ContentLength;
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
using var responseStream = await response.Content.ReadAsStreamAsync();
var buffer = new byte[16 * 1024];
var size = 0;
while ((size = await responseStream.ReadAsync(buffer)) > 0)
{
await stream.WriteAsync(buffer, 0, size);
}
return new DownloadResult()
{
ActualContentLength = stream.Length,
RespContentLength = contentLength,
ActualFilePath = path
};
}
/// <summary>
/// 输入一堆已存在的文件,合并到新文件
/// </summary>
/// <param name="files"></param>
/// <param name="outputFilePath"></param>
public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath)
{
if (files.Length == 0) return;
if (files.Length == 1)
{
FileInfo fi = new FileInfo(files[0]);
fi.CopyTo(outputFilePath, true);
return;
}
if (!Directory.Exists(Path.GetDirectoryName(outputFilePath)))
Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!);
string[] inputFilePaths = files;
using (var outputStream = File.Create(outputFilePath))
{
foreach (var inputFilePath in inputFilePaths)
{
if (inputFilePath == "")
continue;
using (var inputStream = File.OpenRead(inputFilePath))
{
inputStream.CopyTo(outputStream);
}
}
}
}
}
}

View File

@ -8,7 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Util
namespace N_m3u8DL_RE.Util
{
public class PromptUtil
{

View File

@ -3,19 +3,5 @@
<Assembly Name="N_m3u8DL-RE" Dynamic="Required All"/>
<Assembly Name="N_m3u8DL-RE.Common" Dynamic="Required All"/>
<Assembly Name="N_m3u8DL-RE.Parser" Dynamic="Required All"/>
<Assembly Name="System.Text.Json" Dynamic="Required All">
<Type Name="System.Text.Json.Serialization.Converters.EnumConverter`1[[N_m3u8DL_RE.Common.Enum.MediaType,N_m3u8DL-RE.Common]]" Dynamic="Required All" />
<Type Name="System.Text.Json.Serialization.Converters.NullableConverter`1[[N_m3u8DL_RE.Common.Enum.MediaType,N_m3u8DL-RE.Common]]" Dynamic="Required All" />
<Type Name="System.Text.Json.Serialization.Converters.EnumConverter`1[[N_m3u8DL_RE.Common.Enum.EncryptMethod,N_m3u8DL-RE.Common]]" Dynamic="Required All" />
<Type Name="System.Text.Json.Serialization.Converters.NullableConverter`1[[N_m3u8DL_RE.Common.Enum.EncryptMethod,N_m3u8DL-RE.Common]]" Dynamic="Required All" />
<Type Name="System.Text.Json.Serialization.Converters.EnumConverter`1[[N_m3u8DL_RE.Common.Enum.Choise,N_m3u8DL-RE.Common]]" Dynamic="Required All" />
<Type Name="System.Text.Json.Serialization.Converters.NullableConverter`1[[N_m3u8DL_RE.Common.Enum.Choise,N_m3u8DL-RE.Common]]" Dynamic="Required All" />
<Type Name="System.Text.Json.Serialization.Converters.NullableConverter`1[[System.Int32,System.Private.CoreLib]]" Dynamic="Required All" />
<Type Name="System.Text.Json.Serialization.Converters.NullableConverter`1[[System.Double,System.Private.CoreLib]]" Dynamic="Required All" />
</Assembly>
</Application>
</Directives>