增强可定制性

This commit is contained in:
nilaoda 2022-06-27 20:16:38 +08:00
parent 9722079c65
commit d21d3924cc
29 changed files with 702 additions and 158 deletions

View File

@ -1,44 +0,0 @@
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.Config
{
public class ParserConfig
{
public string Url { get; set; }
public string BaseUrl { get; set; }
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"
};
/// <summary>
/// 自定义的加密方式 默认AES_128_CBC
/// </summary>
public EncryptMethod CustomMethod { get; set; } = EncryptMethod.AES_128;
/// <summary>
/// 自定义的解密KEY
/// </summary>
public byte[]? CustomeKey { get; set; }
/// <summary>
/// 自定义的解密IV
/// </summary>
public byte[]? CustomeIV { get; set; }
/// <summary>
/// 组装视频分段的URL时是否要把原本URL后的参数也加上去
/// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx"
/// 相对路径 = clip_01.ts
/// 如果 AppendUrlParams=false得 http://xxx.com/clip_01.ts
/// 如果 AppendUrlParams=true得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx
/// </summary>
public bool AppendUrlParams { get; set; } = false;
}
}

View File

@ -1,4 +1,5 @@
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Util;
using Spectre.Console;
using System;
using System.Collections.Generic;
@ -70,6 +71,13 @@ namespace N_m3u8DL_RE.Common.Entity
returnStr = returnStr.Replace("| |", "|");
}
//计算时长
if (Playlist != null)
{
var total = Playlist.MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration));
returnStr += " | " + GlobalUtil.FormatTime((int)total);
}
return returnStr;
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity
{
public class SubCue
{
public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
public string Payload { get; set; }
public string Settings { get; set; }
}
}

View File

@ -0,0 +1,104 @@
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="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 = payload,
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

@ -87,6 +87,15 @@ namespace N_m3u8DL_RE.Common.Resource {
}
}
/// <summary>
/// 查找类似 找不到支持的Processor 的本地化字符串。
/// </summary>
public static string keyProcessorNotFound {
get {
return ResourceManager.GetString("keyProcessorNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 检测到直播流 的本地化字符串。
/// </summary>

View File

@ -145,4 +145,7 @@
<data name="parsingStream" xml:space="preserve">
<value>Parsing streams...</value>
</data>
<data name="keyProcessorNotFound" xml:space="preserve">
<value>No Processor matched</value>
</data>
</root>

View File

@ -145,4 +145,7 @@
<data name="parsingStream" xml:space="preserve">
<value>正在解析媒体信息...</value>
</data>
<data name="keyProcessorNotFound" xml:space="preserve">
<value>找不到支持的Processor</value>
</data>
</root>

View File

@ -145,4 +145,7 @@
<data name="parsingStream" xml:space="preserve">
<value>正在解析媒體信息...</value>
</data>
<data name="keyProcessorNotFound" xml:space="preserve">
<value>找不到支持的Processor</value>
</data>
</root>

View File

@ -22,5 +22,14 @@ namespace N_m3u8DL_RE.Common.Util
};
return JsonSerializer.Serialize(o, options);
}
//此函数用于格式化输出时长
public static string FormatTime(int time)
{
TimeSpan ts = new TimeSpan(0, 0, time);
string str = "";
str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s";
return str;
}
}
}

View File

@ -53,6 +53,43 @@ namespace N_m3u8DL_RE.Common.Util
return webResponse;
}
//重定向
public static async Task<string> Get302Async(string url, Dictionary<string, string>? headers = null)
{
Logger.Debug(ResString.fetch + url);
var handler = new HttpClientHandler()
{
AllowAutoRedirect = false
};
string redirectedUrl = url;
using (HttpClient client = new HttpClient(handler))
{
if (headers != null)
{
foreach (var item in headers)
{
client.DefaultRequestHeaders.TryAddWithoutValidation(item.Key, item.Value);
}
}
using (HttpResponseMessage response = await client.GetAsync(url))
using (HttpContent content = response.Content)
{
Logger.Debug(ResString.fetch + response.Headers);
if (response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.Moved)
{
HttpResponseHeaders respHeaders = response.Headers;
if (respHeaders != null && respHeaders.Location != null)
{
redirectedUrl = respHeaders.Location.AbsoluteUri;
}
}
}
}
return redirectedUrl;
}
public static async Task<byte[]> GetBytesAsync(string url, Dictionary<string, string>? headers = null)
{
byte[] bytes = new byte[0];

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>N_m3u8DL_RE.Extends</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\N_m3u8DL-RE.Common\N_m3u8DL-RE.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Crypto\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,35 @@
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.Extends.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,69 @@
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Processor;
using N_m3u8DL_RE.Parser.Processor.DASH;
using N_m3u8DL_RE.Parser.Processor.HLS;
namespace N_m3u8DL_RE.Parser.Config
{
public class ParserConfig
{
public string Url { get; set; }
public string BaseUrl { get; set; }
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"
};
/// <summary>
/// HLS内容前置处理器. 调用顺序与列表顺序相同
/// </summary>
public IList<ContentProcessor> HLSContentProcessors { get; } = new List<ContentProcessor>() { new DefaultHLSContentProcessor() };
/// <summary>
/// DASH内容前置处理器. 调用顺序与列表顺序相同
/// </summary>
public IList<ContentProcessor> DASHContentProcessors { get; } = new List<ContentProcessor>() { new DefaultDASHContentProcessor() };
/// <summary>
/// 添加分片URL前置处理器. 调用顺序与列表顺序相同
/// </summary>
public IList<UrlProcessor> HLSUrlProcessors { get; } = new List<UrlProcessor>() { new DefaultUrlProcessor() };
/// <summary>
/// DASH内容前置处理器. 调用顺序与列表顺序相同
/// </summary>
public IList<UrlProcessor> DASHUrlProcessors { get; } = new List<UrlProcessor>() { new DefaultUrlProcessor() };
/// <summary>
/// HLS-KEY解析器. 调用顺序与列表顺序相同
/// </summary>
public IList<KeyProcessor> HLSKeyProcessors { get; } = new List<KeyProcessor>() { new DefaultHLSKeyProcessor() };
/// <summary>
/// 自定义的加密方式
/// </summary>
public EncryptMethod? CustomMethod { get; set; }
/// <summary>
/// 自定义的解密KEY
/// </summary>
public byte[]? CustomeKey { get; set; }
/// <summary>
/// 自定义的解密IV
/// </summary>
public byte[]? CustomeIV { get; set; }
/// <summary>
/// 组装视频分段的URL时是否要把原本URL后的参数也加上去
/// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx"
/// 相对路径 = clip_01.ts
/// 如果 AppendUrlParams=false得 http://xxx.com/clip_01.ts
/// 如果 AppendUrlParams=true得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx
/// </summary>
public bool AppendUrlParams { get; set; } = false;
}
}

View File

@ -1,9 +1,8 @@
using N_m3u8DL_RE.Common.Config;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Util;
using System;
using System.Collections.Generic;
@ -723,9 +722,12 @@ namespace N_m3u8DL_RE.Parser.Extractor
/// </summary>
private string PreProcessUrl(string url)
{
if (ParserConfig.AppendUrlParams)
foreach (var p in ParserConfig.DASHUrlProcessors)
{
url += new Regex("\\?.*").Match(MpdUrl).Value;
if (p.CanProcess(url, ParserConfig))
{
url = p.Process(url, ParserConfig);
}
}
return url;
@ -733,10 +735,12 @@ namespace N_m3u8DL_RE.Parser.Extractor
private void PreProcessContent()
{
//XiGua
if (this.MpdContent.Contains("<mas:") && !this.MpdContent.Contains("xmlns:mas"))
foreach (var p in ParserConfig.DASHContentProcessors)
{
this.MpdContent = this.MpdContent.Replace("<MPD ", "<MPD xmlns:mas=\"urn:marlin:mas:1-0:services:schemas:mpd\" ");
if (p.CanProcess(MpdContent, ParserConfig))
{
MpdContent = p.Process(MpdContent, ParserConfig);
}
}
}

View File

@ -1,17 +1,17 @@
using N_m3u8DL_RE.Common.Config;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Util;
using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using N_m3u8DL_RE.Common.Util;
namespace N_m3u8DL_RE.Parser.Extractor
{
@ -30,9 +30,13 @@ namespace N_m3u8DL_RE.Parser.Extractor
this.ParserConfig = parserConfig;
this.M3u8Url = parserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(parserConfig.BaseUrl))
{
this.BaseUrl = parserConfig.BaseUrl;
}
else
this.BaseUrl = this.M3u8Url;
{
this.BaseUrl = parserConfig.BaseUrl = this.M3u8Url;
}
}
/// <summary>
@ -46,60 +50,13 @@ namespace N_m3u8DL_RE.Parser.Extractor
throw new Exception(ResString.badM3u8);
}
//央视频回放
if (M3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && M3u8Url.Contains("endtime="))
foreach (var p in ParserConfig.HLSContentProcessors)
{
M3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
}
//IMOOC
if (M3u8Url.Contains("imooc.com/"))
{
//M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
}
//iqy
if (M3u8Content.StartsWith("{\"payload\""))
{
//
}
//针对优酷#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=\\\"(.*?)\\\"");
foreach (Match m in ykmap.Matches(M3u8Content))
if (p.CanProcess(M3u8Content, ParserConfig))
{
M3u8Content = M3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
M3u8Content = p.Process(M3u8Content, ParserConfig);
}
}
//针对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");
if (ykmap.IsMatch(M3u8Content))
{
M3u8Content = M3u8Content.Replace(ykmap.Match(M3u8Content).Value, "#XXX");
}
}
//针对AppleTv修正
if (M3u8Content.Contains("#EXT-X-DISCONTINUITY") && M3u8Content.Contains("#EXT-X-MAP") && (M3u8Url.Contains(".apple.com/") || Regex.IsMatch(M3u8Content, "#EXT-X-MAP.*\\.apple\\.com/")))
{
//只取加密部分即可
Regex ykmap = new Regex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)");
if (ykmap.IsMatch(M3u8Content))
{
M3u8Content = "#EXTM3U\r\n" + ykmap.Match(M3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
}
}
//修复#EXT-X-KEY与#EXTINF出现次序异常问题
if (Regex.IsMatch(M3u8Content, "(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)"))
{
M3u8Content = Regex.Replace(M3u8Content, "(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)", "$3$2$1");
}
}
/// <summary>
@ -107,9 +64,12 @@ namespace N_m3u8DL_RE.Parser.Extractor
/// </summary>
private string PreProcessUrl(string url)
{
if (ParserConfig.AppendUrlParams)
foreach (var p in ParserConfig.HLSUrlProcessors)
{
url += new Regex("\\?.*").Match(M3u8Url).Value;
if (p.CanProcess(url, ParserConfig))
{
url = p.Process(url, ParserConfig);
}
}
return url;
@ -338,7 +298,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
//自定义KEY情况 判断是否需要读取IV
if (line.Contains("IV=0x") && ParserConfig.CustomeKey != null && ParserConfig.CustomeIV == null)
{
currentEncryptInfo.Method = ParserConfig.CustomMethod;
currentEncryptInfo.Method = ParserConfig.CustomMethod ?? EncryptMethod.AES_128;
currentEncryptInfo.Key = ParserConfig.CustomeKey;
currentEncryptInfo.IV = HexUtil.HexToBytes(iv);
}
@ -346,25 +306,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
if (uri != uri_last)
{
//解析key
currentEncryptInfo.Key = await ParseKeyAsync(uri);
//加密方式
if (Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m))
{
currentEncryptInfo.Method = m;
}
else
{
currentEncryptInfo.Method = EncryptMethod.UNKNOWN;
}
//没有读取到IV自己生成
if (string.IsNullOrEmpty(iv))
{
currentEncryptInfo.IV = HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));
}
else
{
currentEncryptInfo.IV = HexUtil.HexToBytes(iv);
}
currentEncryptInfo = ParseKey(method, uri, iv, segIndex);
}
lastKeyLine = line;
}
@ -475,22 +417,17 @@ namespace N_m3u8DL_RE.Parser.Extractor
return playlist;
}
private async Task<byte[]> ParseKeyAsync(string uri)
private EncryptInfo ParseKey(string method, string uriText, string ivText, int segIndex)
{
if (uri.ToLower().StartsWith("base64:"))
foreach (var p in ParserConfig.HLSKeyProcessors)
{
return Convert.FromBase64String(uri.Substring(7));
}
else if (uri.ToLower().StartsWith("data:text/plain;base64,"))
{
return Convert.FromBase64String(uri.Substring(23));
}
else
{
var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, uri));
var bytes = await HTTPUtil.GetBytesAsync(segUrl, ParserConfig.Headers);
return bytes;
if (p.CanProcess(method, uriText, ivText, ParserConfig))
{
return p.Process(method, uriText, ivText, segIndex, ParserConfig);
}
}
throw new Exception(ResString.keyProcessorNotFound);
}
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)

View File

@ -1,4 +1,4 @@
using N_m3u8DL_RE.Common.Config;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity;
using System;
using System.Collections.Generic;

View File

@ -0,0 +1,15 @@
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
{
public abstract class ContentProcessor
{
public abstract bool CanProcess(string rawText, ParserConfig parserConfig);
public abstract string Process(string rawText, ParserConfig parserConfig);
}
}

View File

@ -0,0 +1,33 @@
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.DASH
{
/// <summary>
/// 西瓜视频处理
/// </summary>
public class DefaultDASHContentProcessor : ContentProcessor
{
public override bool CanProcess(string mpdContent, ParserConfig parserConfig)
{
if (mpdContent.Contains("<mas:") && !mpdContent.Contains("xmlns:mas"))
{
return true;
}
return false;
}
public override string Process(string mpdContent, ParserConfig parserConfig)
{
Logger.Debug("Fix xigua mpd...");
mpdContent = mpdContent.Replace("<MPD ", "<MPD xmlns:mas=\"urn:marlin:mas:1-0:services:schemas:mpd\" ");
return mpdContent;
}
}
}

View File

@ -0,0 +1,28 @@
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor
{
public class DefaultUrlProcessor : UrlProcessor
{
public override bool CanProcess(string oriUrl, ParserConfig paserConfig) => true;
public override string Process(string oriUrl, ParserConfig paserConfig)
{
if (paserConfig.AppendUrlParams)
{
Logger.Debug("Before: " + oriUrl);
oriUrl += new Regex("\\?.*").Match(paserConfig.Url).Value;
Logger.Debug("After: " + oriUrl);
}
return oriUrl;
}
}
}

View File

@ -0,0 +1,77 @@
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS
{
public class DefaultHLSContentProcessor : ContentProcessor
{
public override bool CanProcess(string rawText, ParserConfig parserConfig) => true;
public override string Process(string m3u8Content, ParserConfig parserConfig)
{
var m3u8Url = parserConfig.Url;
//央视频回放
if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime="))
{
m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
}
//IMOOC
if (m3u8Url.Contains("imooc.com/"))
{
//M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
}
//iqy
if (m3u8Content.StartsWith("{\"payload\""))
{
//
}
//针对优酷#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=\\\"(.*?)\\\"");
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}");
}
}
//针对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");
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
}
}
//针对AppleTv修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || Regex.IsMatch(m3u8Content, "#EXT-X-MAP.*\\.apple\\.com/")))
{
//只取加密部分即可
Regex ykmap = new Regex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)");
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
}
}
//修复#EXT-X-KEY与#EXTINF出现次序异常问题
if (Regex.IsMatch(m3u8Content, "(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)"))
{
m3u8Content = Regex.Replace(m3u8Content, "(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)", "$3$2$1");
}
return m3u8Content;
}
}
}

View File

@ -0,0 +1,76 @@
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS
{
public class DefaultHLSKeyProcessor : KeyProcessor
{
public override bool CanProcess(string method, string uriText, string ivText, ParserConfig paserConfig) => true;
public override EncryptInfo Process(string method, string uriText, string ivText, int segIndex, ParserConfig parserConfig)
{
var encryptInfo = new EncryptInfo();
if (uriText.ToLower().StartsWith("base64:"))
{
encryptInfo.Key = Convert.FromBase64String(uriText.Substring(7));
}
else if (uriText.ToLower().StartsWith("data:text/plain;base64,"))
{
encryptInfo.Key = Convert.FromBase64String(uriText.Substring(23));
}
else
{
var segUrl = PreProcessUrl(ParserUtil.CombineURL(parserConfig.BaseUrl, uriText), parserConfig);
var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;
encryptInfo.Key = bytes;
}
//加密方式
if (Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m))
{
encryptInfo.Method = m;
}
else
{
encryptInfo.Method = EncryptMethod.UNKNOWN;
}
//没有读取到IV自己生成
if (string.IsNullOrEmpty(ivText))
{
encryptInfo.IV = HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));
}
else
{
encryptInfo.IV = HexUtil.HexToBytes(ivText);
}
return encryptInfo;
}
/// <summary>
/// 预处理URL
/// </summary>
private string PreProcessUrl(string url, ParserConfig parserConfig)
{
foreach (var p in parserConfig.HLSUrlProcessors)
{
if (p.CanProcess(url, parserConfig))
{
url = p.Process(url, parserConfig);
}
}
return url;
}
}
}

View File

@ -0,0 +1,16 @@
using N_m3u8DL_RE.Common.Entity;
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
{
public abstract class KeyProcessor
{
public abstract bool CanProcess(string method, string uriText, string ivText, ParserConfig parserConfig);
public abstract EncryptInfo Process(string method, string uriText, string ivText, int segIndex, ParserConfig parserConfig);
}
}

View File

@ -0,0 +1,15 @@
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
{
public abstract class UrlProcessor
{
public abstract bool CanProcess(string oriUrl, ParserConfig parserConfig);
public abstract string Process(string oriUrl, ParserConfig parserConfig);
}
}

View File

@ -1,8 +1,8 @@
using N_m3u8DL_RE.Common.Config;
using N_m3u8DL_RE.Parser.Config;
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.Parser.Util;
using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Extractor;
using System;
@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using N_m3u8DL_RE.Common.Util;
namespace N_m3u8DL_RE.Parser
{
@ -19,6 +20,11 @@ namespace N_m3u8DL_RE.Parser
private ParserConfig parserConfig = new ParserConfig();
private string rawText;
public StreamExtractor()
{
}
public StreamExtractor(ParserConfig parserConfig)
{
this.parserConfig = parserConfig;

View File

@ -3,11 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32505.426
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N_m3u8DL-RE", "N_m3u8DL-RE\N_m3u8DL-RE.csproj", "{E6915BF9-8306-4F62-B357-23430F0D80B5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "N_m3u8DL-RE", "N_m3u8DL-RE\N_m3u8DL-RE.csproj", "{E6915BF9-8306-4F62-B357-23430F0D80B5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N_m3u8DL-RE.Common", "N_m3u8DL-RE.Common\N_m3u8DL-RE.Common.csproj", "{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "N_m3u8DL-RE.Common", "N_m3u8DL-RE.Common\N_m3u8DL-RE.Common.csproj", "{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N_m3u8DL-RE.Parser", "N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj", "{0DA02925-AF3A-4598-AF01-91AE5539FCA1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "N_m3u8DL-RE.Parser", "N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj", "{0DA02925-AF3A-4598-AF01-91AE5539FCA1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N_m3u8DL-RE.Extends", "N_m3u8DL-RE.Extends\N_m3u8DL-RE.Extends.csproj", "{99175570-6FE1-45C0-87BD-D2E1B52A35CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -27,6 +29,10 @@ Global
{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Release|Any CPU.Build.0 = Release|Any CPU
{99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\N_m3u8DL-RE.Extends\N_m3u8DL-RE.Extends.csproj" />
<ProjectReference Include="..\N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj" />
</ItemGroup>

View File

@ -0,0 +1,25 @@
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Processor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Processor
{
internal class DemoProcessor : ContentProcessor
{
public override bool CanProcess(string rawText, ParserConfig parserConfig)
{
return parserConfig.Url.Contains("bitmovin");
}
public override string Process(string rawText, ParserConfig parserConfig)
{
Logger.InfoMarkUp("[red]Match bitmovin![/]");
return rawText;
}
}
}

View File

@ -0,0 +1,27 @@
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Processor;
using N_m3u8DL_RE.Parser.Processor.HLS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Processor
{
internal class DemoProcessor2 : KeyProcessor
{
public override bool CanProcess(string method, string uriText, string ivText, ParserConfig parserConfig)
{
return parserConfig.Url.Contains("playertest.longtailvideo.com");
}
public override EncryptInfo Process(string method, string uriText, string ivText, int segIndex, ParserConfig parserConfig)
{
Logger.InfoMarkUp("[white on green]My Key Processor![/]");
return new DefaultHLSKeyProcessor().Process(method, uriText, ivText, segIndex, parserConfig);
}
}
}

View File

@ -1,7 +1,7 @@
using N_m3u8DL_RE.Common.Config;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Util;
using N_m3u8DL_RE.Parser;
using Spectre.Console;
using System.Text.Json;
@ -10,7 +10,11 @@ using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Log;
using System.Globalization;
using System.Text;
using N_m3u8DL_RE.Parser.Util;
using System.Text.RegularExpressions;
using N_m3u8DL_RE.Extends.Subtitle;
using System.Collections.Concurrent;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Processor;
namespace N_m3u8DL_RE
{
@ -26,18 +30,25 @@ namespace N_m3u8DL_RE
//设置语言
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(loc);
//Logger.LogLevel = LogLevel.DEBUG;
try
{
//Logger.LogLevel = LogLevel.DEBUG;
var config = new ParserConfig();
//demo1
config.DASHContentProcessors.Insert(0, new DemoProcessor());
//demo2
config.HLSKeyProcessors.Insert(0, new DemoProcessor2());
var url = string.Empty;
//url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8";
url = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.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";
if (string.IsNullOrEmpty(url))
{
url = AnsiConsole.Ask<string>("Input [green]URL[/]: ");
url = AnsiConsole.Ask<string>("请输入 [green]URL[/]: ");
}
//流提取器配置
@ -47,11 +58,6 @@ namespace N_m3u8DL_RE
//解析流信息
var streams = await extractor.ExtractStreamsAsync();
if (streams.Count == 0)
{
throw new Exception("解析失败");
}
//全部媒体
var lists = streams.OrderByDescending(p => p.Bandwidth);
//基本流
@ -83,6 +89,8 @@ namespace N_m3u8DL_RE
Logger.InfoMarkUp(item.ToString());
}
Logger.Info("按任意键继续");
Console.ReadKey();
}
catch (Exception ex)
{