完成m3u8基本解析
This commit is contained in:
parent
35e8dac90e
commit
6828d11952
|
@ -0,0 +1,35 @@
|
|||
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; }
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Entity
|
||||
{
|
||||
public class EncryptInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 加密方式,默认无加密
|
||||
/// </summary>
|
||||
public EncryptMethod Method { get; set; } = EncryptMethod.NONE;
|
||||
|
||||
public byte[]? Key { get; set; }
|
||||
public byte[]? IV { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Entity
|
||||
{
|
||||
//主要处理 EXT-X-DISCONTINUITY
|
||||
public class MediaPart
|
||||
{
|
||||
public List<MediaSegment> MediaSegments { get; set; } = new List<MediaSegment>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Entity
|
||||
{
|
||||
public class MediaSegment
|
||||
{
|
||||
public int Index { get; set; }
|
||||
|
||||
public int TargetDuration { get; set; }
|
||||
public double Duration { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
public long StartRange { get; set; } = 0L;
|
||||
public long StopRange { get => StartRange + ExpectLength - 1; }
|
||||
public long ExpectLength { get; set; } = -1L;
|
||||
|
||||
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo();
|
||||
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Entity
|
||||
{
|
||||
public class Playlist
|
||||
{
|
||||
//对应Url信息
|
||||
public string Url { get; set; }
|
||||
//是否直播
|
||||
public bool IsLive { get; set; }
|
||||
//INIT信息
|
||||
public MediaSegment? MediaInit { get; set; }
|
||||
//分片信息
|
||||
public List<MediaPart> MediaParts { get; set; } = new List<MediaPart>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using N_m3u8DL_RE.Common.Enum;
|
||||
using Spectre.Console;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Entity
|
||||
{
|
||||
public class StreamSpec
|
||||
{
|
||||
public MediaType? MediaType { get; set; }
|
||||
public string? GroupId { get; set; }
|
||||
public string? Language { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public Choise? Default { get; set; }
|
||||
|
||||
//基本信息
|
||||
public int? Bandwidth { get; set; }
|
||||
public string? Codecs { get; set; }
|
||||
public string? Resolution { get; set; }
|
||||
public double? FrameRate { get; set; }
|
||||
public string? Channels { get; set; }
|
||||
|
||||
|
||||
//外部轨道GroupId (后续寻找对应轨道信息)
|
||||
public string? AudioId { get; set; }
|
||||
public string? VideoId { get; set; }
|
||||
public string? SubtitleId { get; set; }
|
||||
|
||||
public string Url { get; set; }
|
||||
|
||||
public Playlist Playlist { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var encStr = string.Empty;
|
||||
|
||||
//增加加密标志
|
||||
if (Playlist != null && Playlist.MediaParts.Any(m => m.MediaSegments.Any(s => s.EncryptInfo.Method != EncryptMethod.NONE)))
|
||||
{
|
||||
encStr = "[red]*[/] ";
|
||||
}
|
||||
|
||||
if (MediaType == Enum.MediaType.AUDIO)
|
||||
{
|
||||
var d = $"{GroupId} | {Name} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}".Replace("| |", "|");
|
||||
return $"[deepskyblue3]Aud[/] {encStr}" + d.EscapeMarkup().Trim().Trim('|').Trim();
|
||||
}
|
||||
else if (MediaType == Enum.MediaType.SUBTITLES)
|
||||
{
|
||||
var d = $"{GroupId} | {Language} | {Name} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}".Replace("| |", "|");
|
||||
return $"[deepskyblue3_1]Sub[/] {encStr}" + d.EscapeMarkup().Trim().Trim('|').Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {FrameRate} | {Codecs} | {(Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) + " Segments" : "")}".Replace("| |", "|");
|
||||
return $"[aqua]Vid[/] {encStr}" + d.EscapeMarkup().Trim().Trim('|').Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Enum
|
||||
{
|
||||
public enum Choise
|
||||
{
|
||||
YES = 1,
|
||||
NO = 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Enum
|
||||
{
|
||||
public enum EncryptMethod
|
||||
{
|
||||
NONE,
|
||||
AES_128,
|
||||
AES_128_ECB,
|
||||
SAMPLE_AES,
|
||||
UNKNOWN
|
||||
}
|
||||
}
|
|
@ -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.Enum
|
||||
{
|
||||
public enum MediaType
|
||||
{
|
||||
AUDIO = 0,
|
||||
VIDEO = 1,
|
||||
SUBTITLES = 2,
|
||||
CLOSED_CAPTIONS = 3
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.JsonConverter
|
||||
{
|
||||
internal class BytesBase64Converter : JsonConverter<byte[]>
|
||||
{
|
||||
public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetBytesFromBase64();
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) => writer.WriteStringValue(Convert.ToBase64String(value));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>N_m3u8DL_RE.Common</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Spectre.Console" Version="0.44.1-preview.0.17" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Resource\ResString.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>ResString.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resource\ResString.en-US.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resource\ResString.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>ResString.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resource\ResString.zh-TW.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,180 @@
|
|||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
//
|
||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||
// 重新生成代码,这些更改将会丢失。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Resource {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 一个强类型的资源类,用于查找本地化的字符串等。
|
||||
/// </summary>
|
||||
// 此类是由 StronglyTypedResourceBuilder
|
||||
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
|
||||
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
|
||||
// (以 /str 作为命令选项),或重新生成 VS 项目。
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class ResString {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal ResString() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回此类使用的缓存的 ResourceManager 实例。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("N_m3u8DL_RE.Common.Resource.ResString", typeof(ResString).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写当前线程的 CurrentUICulture 属性,对
|
||||
/// 使用此强类型资源类的所有资源查找执行重写。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 错误的m3u8 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string badM3u8 {
|
||||
get {
|
||||
return ResourceManager.GetString("badM3u8", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 获取: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string fetch {
|
||||
get {
|
||||
return ResourceManager.GetString("fetch", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 检测到直播流 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string liveFound {
|
||||
get {
|
||||
return ResourceManager.GetString("liveFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 加载URL: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string loadingUrl {
|
||||
get {
|
||||
return ResourceManager.GetString("loadingUrl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 检测到Master列表,开始解析全部流信息 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string masterM3u8Found {
|
||||
get {
|
||||
return ResourceManager.GetString("masterM3u8Found", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 内容匹配: [white on deepskyblue1]HTTP Live Streaming[/] 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string matchHLS {
|
||||
get {
|
||||
return ResourceManager.GetString("matchHLS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 当前输入不受支持: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string notSupported {
|
||||
get {
|
||||
return ResourceManager.GetString("notSupported", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 [grey](按键盘上下键以浏览更多内容)[/] 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string promptChoiceText {
|
||||
get {
|
||||
return ResourceManager.GetString("promptChoiceText", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 (按 [blue]空格键[/] 选择流, [green]回车键[/] 完成选择) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string promptInfo {
|
||||
get {
|
||||
return ResourceManager.GetString("promptInfo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 请选择 [green]你要下载的内容[/]: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string promptTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("promptTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 已选择的流: 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string selectedStream {
|
||||
get {
|
||||
return ResourceManager.GetString("selectedStream", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 已解析, 共计 {} 条媒体流, 基本流 {} 条, 可选音频流 {} 条, 可选字幕流 {} 条 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string streamsInfo {
|
||||
get {
|
||||
return ResourceManager.GetString("streamsInfo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 写出meta.json 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string writeJson {
|
||||
get {
|
||||
return ResourceManager.GetString("writeJson", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 1.3
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">1.3</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1">this is my long string</data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
[base64 mime encoded serialized .NET Framework object]
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="badM3u8" xml:space="preserve">
|
||||
<value>Bad m3u8</value>
|
||||
</data>
|
||||
<data name="notSupported" xml:space="preserve">
|
||||
<value>Input not supported: </value>
|
||||
</data>
|
||||
<data name="loadingUrl" xml:space="preserve">
|
||||
<value>Loading URL: </value>
|
||||
</data>
|
||||
<data name="matchHLS" xml:space="preserve">
|
||||
<value>Content Matched: [white on deepskyblue1]HTTP Live Streaming[/]</value>
|
||||
</data>
|
||||
<data name="masterM3u8Found" xml:space="preserve">
|
||||
<value>Master List detected, try parse all streams</value>
|
||||
</data>
|
||||
<data name="fetch" xml:space="preserve">
|
||||
<value>Fetch: </value>
|
||||
</data>
|
||||
<data name="promptTitle" xml:space="preserve">
|
||||
<value>Please select [green]what you want to download[/]:</value>
|
||||
</data>
|
||||
<data name="promptChoiceText" xml:space="preserve">
|
||||
<value>[grey](Move up and down to reveal more streams)[/]</value>
|
||||
</data>
|
||||
<data name="promptInfo" xml:space="preserve">
|
||||
<value>(Press [blue]<space>[/] to toggle a stream, [green]<enter>[/] to accept)</value>
|
||||
</data>
|
||||
<data name="streamsInfo" xml:space="preserve">
|
||||
<value>Extracted, there are {} streams, with {} basic streams, {} audio streams, {} subtitle streams</value>
|
||||
</data>
|
||||
<data name="liveFound" xml:space="preserve">
|
||||
<value>Live stream found</value>
|
||||
</data>
|
||||
<data name="selectedStream" xml:space="preserve">
|
||||
<value>Selected Streams:</value>
|
||||
</data>
|
||||
<data name="writeJson" xml:space="preserve">
|
||||
<value>Writing meta.json</value>
|
||||
</data>
|
||||
</root>
|
|
@ -0,0 +1,139 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 1.3
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">1.3</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1">this is my long string</data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
[base64 mime encoded serialized .NET Framework object]
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="badM3u8" xml:space="preserve">
|
||||
<value>错误的m3u8</value>
|
||||
</data>
|
||||
<data name="notSupported" xml:space="preserve">
|
||||
<value>当前输入不受支持: </value>
|
||||
</data>
|
||||
<data name="loadingUrl" xml:space="preserve">
|
||||
<value>加载URL: </value>
|
||||
</data>
|
||||
<data name="matchHLS" xml:space="preserve">
|
||||
<value>内容匹配: [white on deepskyblue1]HTTP Live Streaming[/]</value>
|
||||
</data>
|
||||
<data name="masterM3u8Found" xml:space="preserve">
|
||||
<value>检测到Master列表,开始解析全部流信息</value>
|
||||
</data>
|
||||
<data name="fetch" xml:space="preserve">
|
||||
<value>获取: </value>
|
||||
</data>
|
||||
<data name="promptTitle" xml:space="preserve">
|
||||
<value>请选择 [green]你要下载的内容[/]:</value>
|
||||
</data>
|
||||
<data name="promptChoiceText" xml:space="preserve">
|
||||
<value>[grey](按键盘上下键以浏览更多内容)[/]</value>
|
||||
</data>
|
||||
<data name="promptInfo" xml:space="preserve">
|
||||
<value>(按 [blue]空格键[/] 选择流, [green]回车键[/] 完成选择)</value>
|
||||
</data>
|
||||
<data name="streamsInfo" xml:space="preserve">
|
||||
<value>已解析, 共计 {} 条媒体流, 基本流 {} 条, 可选音频流 {} 条, 可选字幕流 {} 条</value>
|
||||
</data>
|
||||
<data name="liveFound" xml:space="preserve">
|
||||
<value>检测到直播流</value>
|
||||
</data>
|
||||
<data name="selectedStream" xml:space="preserve">
|
||||
<value>已选择的流:</value>
|
||||
</data>
|
||||
<data name="writeJson" xml:space="preserve">
|
||||
<value>写出meta.json</value>
|
||||
</data>
|
||||
</root>
|
|
@ -0,0 +1,139 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 1.3
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">1.3</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1">this is my long string</data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
[base64 mime encoded serialized .NET Framework object]
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="badM3u8" xml:space="preserve">
|
||||
<value>錯誤的m3u8</value>
|
||||
</data>
|
||||
<data name="notSupported" xml:space="preserve">
|
||||
<value>當前輸入不受支持: </value>
|
||||
</data>
|
||||
<data name="loadingUrl" xml:space="preserve">
|
||||
<value>加載URL: </value>
|
||||
</data>
|
||||
<data name="matchHLS" xml:space="preserve">
|
||||
<value>內容匹配: [white on deepskyblue1]HTTP Live Streaming[/]</value>
|
||||
</data>
|
||||
<data name="masterM3u8Found" xml:space="preserve">
|
||||
<value>檢測到Master列表,開始解析全部流信息</value>
|
||||
</data>
|
||||
<data name="fetch" xml:space="preserve">
|
||||
<value>獲取: </value>
|
||||
</data>
|
||||
<data name="promptTitle" xml:space="preserve">
|
||||
<value>請選擇 [green]你要下載的內容[/]:</value>
|
||||
</data>
|
||||
<data name="promptChoiceText" xml:space="preserve">
|
||||
<value>[grey](按鍵盤上下鍵以瀏覽更多內容)[/]</value>
|
||||
</data>
|
||||
<data name="promptInfo" xml:space="preserve">
|
||||
<value>(按 [blue]空格鍵[/] 選擇流, [green]回車鍵[/] 完成選擇)</value>
|
||||
</data>
|
||||
<data name="streamsInfo" xml:space="preserve">
|
||||
<value>已解析, 共計 {} 條媒體流, 基本流 {} 條, 可選音頻流 {} 條, 可選字幕流 {} 條</value>
|
||||
</data>
|
||||
<data name="liveFound" xml:space="preserve">
|
||||
<value>檢測到直播流</value>
|
||||
</data>
|
||||
<data name="selectedStream" xml:space="preserve">
|
||||
<value>已選擇的流:</value>
|
||||
</data>
|
||||
<data name="writeJson" xml:space="preserve">
|
||||
<value>寫出meta.json</value>
|
||||
</data>
|
||||
</root>
|
|
@ -0,0 +1,26 @@
|
|||
using N_m3u8DL_RE.Common.JsonConverter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Util
|
||||
{
|
||||
public class GlobalUtil
|
||||
{
|
||||
public static string ConvertToJson(object o)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() }
|
||||
};
|
||||
return JsonSerializer.Serialize(o, options);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Cache;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using N_m3u8DL_RE.Common.Log;
|
||||
using N_m3u8DL_RE.Common.Resource;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Util
|
||||
{
|
||||
public class HTTPUtil
|
||||
{
|
||||
|
||||
public static readonly HttpClient AppHttpClient = new(new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = true,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
|
||||
})
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(2)
|
||||
};
|
||||
|
||||
public static async Task<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null)
|
||||
{
|
||||
Logger.Debug(ResString.fetch + url);
|
||||
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
|
||||
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
|
||||
webRequest.Headers.Connection.Clear();
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var item in headers)
|
||||
{
|
||||
webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
Logger.Debug(webRequest.Headers.ToString());
|
||||
var webResponse = (await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
|
||||
return webResponse;
|
||||
}
|
||||
|
||||
public static async Task<byte[]> GetBytesAsync(string url, Dictionary<string, string>? headers = null)
|
||||
{
|
||||
byte[] bytes = new byte[0];
|
||||
var webResponse = await DoGetAsync(url, headers);
|
||||
bytes = await webResponse.Content.ReadAsByteArrayAsync();
|
||||
Logger.Debug(HexUtil.BytesToHex(bytes, " "));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
|
||||
{
|
||||
string htmlCode = string.Empty;
|
||||
var webResponse = await DoGetAsync(url, headers);
|
||||
htmlCode = await webResponse.Content.ReadAsStringAsync();
|
||||
Logger.Debug(htmlCode);
|
||||
return htmlCode;
|
||||
}
|
||||
|
||||
public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)
|
||||
{
|
||||
string htmlCode = string.Empty;
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, Url);
|
||||
request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
|
||||
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());
|
||||
request.Content = new ByteArrayContent(postData);
|
||||
var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
htmlCode = await webResponse.Content.ReadAsStringAsync();
|
||||
return htmlCode;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Common.Util
|
||||
{
|
||||
public class HexUtil
|
||||
{
|
||||
public static string BytesToHex(byte[] data, string split = "")
|
||||
{
|
||||
return BitConverter.ToString(data).Replace("-", split);
|
||||
}
|
||||
|
||||
public static byte[] HexToBytes(string hex)
|
||||
{
|
||||
hex = hex.Trim();
|
||||
if (hex.StartsWith("0x") || hex.StartsWith("0X"))
|
||||
hex = hex.Substring(2);
|
||||
byte[] bytes = new byte[hex.Length / 2];
|
||||
|
||||
for (int i = 0; i < hex.Length; i += 2)
|
||||
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Constants
|
||||
{
|
||||
internal class HLSTags
|
||||
{
|
||||
public static string ext_m3u = "#EXTM3U";
|
||||
public static string ext_x_targetduration = "#EXT-X-TARGETDURATION";
|
||||
public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE";
|
||||
public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE";
|
||||
public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME";
|
||||
public static string ext_x_media = "#EXT-X-MEDIA";
|
||||
public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE";
|
||||
public static string ext_x_key = "#EXT-X-KEY";
|
||||
public static string ext_x_stream_inf = "#EXT-X-STREAM-INF";
|
||||
public static string ext_x_version = "#EXT-X-VERSION";
|
||||
public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE";
|
||||
public static string ext_x_endlist = "#EXT-X-ENDLIST";
|
||||
public static string extinf = "#EXTINF";
|
||||
public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY";
|
||||
public static string ext_x_byterange = "#EXT-X-BYTERANGE";
|
||||
public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF";
|
||||
public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY";
|
||||
public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT";
|
||||
public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT";
|
||||
public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35";
|
||||
public static string ext_x_cue_start = "#EXT-X-CUE-OUT";
|
||||
public static string ext_x_cue_end = "#EXT-X-CUE-IN";
|
||||
public static string ext_x_cue_span = "#EXT-X-CUE-SPAN";
|
||||
public static string ext_x_map = "#EXT-X-MAP";
|
||||
public static string ext_x_start = "#EXT-X-START";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,534 @@
|
|||
using N_m3u8DL_RE.Common.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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Extractor
|
||||
{
|
||||
internal class HLSExtractor : IExtractor
|
||||
{
|
||||
private string M3u8Url = string.Empty;
|
||||
private string BaseUrl = string.Empty;
|
||||
private string M3u8Content = string.Empty;
|
||||
|
||||
public ParserConfig ParserConfig { get; set; }
|
||||
|
||||
private HLSExtractor() { }
|
||||
|
||||
public HLSExtractor(ParserConfig parserConfig)
|
||||
{
|
||||
this.ParserConfig = parserConfig;
|
||||
this.M3u8Url = parserConfig.Url ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(parserConfig.BaseUrl))
|
||||
this.BaseUrl = parserConfig.BaseUrl;
|
||||
else
|
||||
this.BaseUrl = this.M3u8Url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预处理m3u8内容
|
||||
/// </summary>
|
||||
private void PreProcessContent()
|
||||
{
|
||||
M3u8Content = M3u8Content.Trim();
|
||||
if (!M3u8Content.StartsWith(HLSTags.ext_m3u))
|
||||
{
|
||||
throw new Exception(ResString.badM3u8);
|
||||
}
|
||||
|
||||
//央视频回放
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预处理URL
|
||||
/// </summary>
|
||||
private string PreProcessUrl(string url)
|
||||
{
|
||||
if (url.Contains("?__gda__"))
|
||||
{
|
||||
url += new Regex("\\?__gda__.*").Match(M3u8Url).Value;
|
||||
}
|
||||
if (M3u8Url.Contains("//dlsc.hcs.cmvideo.cn") && (url.EndsWith(".ts") || url.EndsWith(".mp4")))
|
||||
{
|
||||
url += new Regex("\\?.*").Match(M3u8Url).Value;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private bool IsMaster()
|
||||
{
|
||||
return M3u8Content.Contains(HLSTags.ext_x_stream_inf);
|
||||
}
|
||||
|
||||
private async Task<List<StreamSpec>> ParseMasterListAsync()
|
||||
{
|
||||
List<StreamSpec> streams = new List<StreamSpec>();
|
||||
|
||||
using StringReader sr = new StringReader(M3u8Content);
|
||||
string line;
|
||||
bool expectPlaylist = false;
|
||||
StreamSpec streamSpec = new();
|
||||
|
||||
while ((line = sr.ReadLine()) != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
if (line.StartsWith(HLSTags.ext_x_stream_inf))
|
||||
{
|
||||
streamSpec = new();
|
||||
var bandwidth = string.IsNullOrEmpty(ParserUtil.GetAttribute(line, "BANDWIDTH")) ? ParserUtil.GetAttribute(line, "AVERAGE-BANDWIDTH") : ParserUtil.GetAttribute(line, "BANDWIDTH");
|
||||
streamSpec.Bandwidth = Convert.ToInt32(bandwidth);
|
||||
streamSpec.Codecs = ParserUtil.GetAttribute(line, "CODECS");
|
||||
streamSpec.Resolution = ParserUtil.GetAttribute(line, "RESOLUTION");
|
||||
|
||||
var frameRate = ParserUtil.GetAttribute(line, "FRAME-RATE");
|
||||
if (!string.IsNullOrEmpty(frameRate))
|
||||
streamSpec.FrameRate = Convert.ToDouble(frameRate);
|
||||
|
||||
var audioId = ParserUtil.GetAttribute(line, "AUDIO");
|
||||
if (!string.IsNullOrEmpty(audioId))
|
||||
streamSpec.AudioId = audioId;
|
||||
|
||||
var videoId = ParserUtil.GetAttribute(line, "VIDEO");
|
||||
if (!string.IsNullOrEmpty(videoId))
|
||||
streamSpec.VideoId = videoId;
|
||||
|
||||
var subtitleId = ParserUtil.GetAttribute(line, "SUBTITLES");
|
||||
if (!string.IsNullOrEmpty(subtitleId))
|
||||
streamSpec.SubtitleId = subtitleId;
|
||||
|
||||
expectPlaylist = true;
|
||||
}
|
||||
else if (line.StartsWith(HLSTags.ext_x_media))
|
||||
{
|
||||
streamSpec = new();
|
||||
var type = ParserUtil.GetAttribute(line, "TYPE").Replace("-", "_");
|
||||
if (Enum.TryParse<MediaType>(type, out var mediaType))
|
||||
{
|
||||
streamSpec.MediaType = mediaType;
|
||||
}
|
||||
|
||||
//跳过CLOSED_CAPTIONS类型(目前不支持)
|
||||
if (streamSpec.MediaType == MediaType.CLOSED_CAPTIONS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var url = ParserUtil.GetAttribute(line, "URI");
|
||||
|
||||
/**
|
||||
* The URI attribute of the EXT-X-MEDIA tag is REQUIRED if the media
|
||||
type is SUBTITLES, but OPTIONAL if the media type is VIDEO or AUDIO.
|
||||
If the media type is VIDEO or AUDIO, a missing URI attribute
|
||||
indicates that the media data for this Rendition is included in the
|
||||
Media Playlist of any EXT-X-STREAM-INF tag referencing this EXT-
|
||||
X-MEDIA tag. If the media TYPE is AUDIO and the URI attribute is
|
||||
missing, clients MUST assume that the audio data for this Rendition
|
||||
is present in every video Rendition specified by the EXT-X-STREAM-INF
|
||||
tag.
|
||||
|
||||
此处直接忽略URI属性为空的情况
|
||||
*/
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
url = ParserUtil.CombineURL(BaseUrl, url);
|
||||
streamSpec.Url = PreProcessUrl(url);
|
||||
|
||||
var groupId = ParserUtil.GetAttribute(line, "GROUP-ID");
|
||||
streamSpec.GroupId = groupId;
|
||||
|
||||
var lang = ParserUtil.GetAttribute(line, "LANGUAGE");
|
||||
if (!string.IsNullOrEmpty(lang))
|
||||
streamSpec.Language = lang;
|
||||
|
||||
var name = ParserUtil.GetAttribute(line, "NAME");
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
streamSpec.Name = name;
|
||||
|
||||
var def = ParserUtil.GetAttribute(line, "DEFAULT");
|
||||
if (Enum.TryParse<Choise>(type, out var defaultChoise))
|
||||
{
|
||||
streamSpec.Default = defaultChoise;
|
||||
}
|
||||
|
||||
var channels = ParserUtil.GetAttribute(line, "CHANNELS");
|
||||
if (!string.IsNullOrEmpty(channels))
|
||||
streamSpec.Channels = channels;
|
||||
|
||||
streams.Add(streamSpec);
|
||||
}
|
||||
else if (line.StartsWith("#"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (expectPlaylist)
|
||||
{
|
||||
var url = ParserUtil.CombineURL(BaseUrl, line);
|
||||
streamSpec.Url = PreProcessUrl(url);
|
||||
expectPlaylist = false;
|
||||
streams.Add(streamSpec);
|
||||
}
|
||||
}
|
||||
|
||||
return streams;
|
||||
}
|
||||
|
||||
private async Task<Playlist> ParseListAsync()
|
||||
{
|
||||
//标记是否已清除优酷广告分片
|
||||
bool hasAd = false;
|
||||
|
||||
using StringReader sr = new StringReader(M3u8Content);
|
||||
string line;
|
||||
bool expectSegment = false;
|
||||
bool isEndlist = false;
|
||||
int segIndex = 0;
|
||||
bool isAd = false;
|
||||
int startIndex;
|
||||
|
||||
Playlist playlist = new();
|
||||
List<MediaPart> mediaParts = new();
|
||||
|
||||
//当前的加密信息
|
||||
EncryptInfo currentEncryptInfo = new();
|
||||
//上次读取到的加密行,#EXT-X-KEY:……
|
||||
string lastKeyLine = "";
|
||||
|
||||
MediaPart mediaPart = new();
|
||||
MediaSegment segment = new();
|
||||
List<MediaSegment> segments = new();
|
||||
|
||||
|
||||
while ((line = sr.ReadLine()) != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
//只下载部分字节
|
||||
if (line.StartsWith(HLSTags.ext_x_byterange))
|
||||
{
|
||||
var p = ParserUtil.GetAttribute(line);
|
||||
var (n, o) = ParserUtil.GetRange(p);
|
||||
segment.ExpectLength = n;
|
||||
segment.StartRange = o ?? segments.Last().StartRange + segments.Last().ExpectLength;
|
||||
expectSegment = true;
|
||||
}
|
||||
//国家地理去广告
|
||||
else if (line.StartsWith("#UPLYNK-SEGMENT"))
|
||||
{
|
||||
if (line.Contains(",ad"))
|
||||
isAd = true;
|
||||
else if (line.Contains(",segment"))
|
||||
isAd = false;
|
||||
}
|
||||
//国家地理去广告
|
||||
else if (isAd)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//解析定义的分段长度
|
||||
else if (line.StartsWith(HLSTags.ext_x_targetduration))
|
||||
{
|
||||
segment.Duration = Convert.ToDouble(ParserUtil.GetAttribute(line));
|
||||
}
|
||||
//解析起始编号
|
||||
else if (line.StartsWith(HLSTags.ext_x_media_sequence))
|
||||
{
|
||||
segIndex = Convert.ToInt32(ParserUtil.GetAttribute(line));
|
||||
startIndex = segIndex;
|
||||
}
|
||||
//program date time
|
||||
else if (line.StartsWith(HLSTags.ext_x_program_date_time))
|
||||
{
|
||||
//
|
||||
}
|
||||
//解析不连续标记,需要单独合并(timestamp不同)
|
||||
else if (line.StartsWith(HLSTags.ext_x_discontinuity))
|
||||
{
|
||||
//修复优酷去除广告后的遗留问题
|
||||
if (hasAd && mediaParts.Count > 0)
|
||||
{
|
||||
segments = mediaParts[mediaParts.Count - 1].MediaSegments;
|
||||
mediaParts.RemoveAt(mediaParts.Count - 1);
|
||||
hasAd = false;
|
||||
continue;
|
||||
}
|
||||
//常规情况的#EXT-X-DISCONTINUITY标记,新建part
|
||||
if (!hasAd && segments.Count > 1)
|
||||
{
|
||||
mediaParts.Add(new MediaPart()
|
||||
{
|
||||
MediaSegments = segments,
|
||||
});
|
||||
segments = new();
|
||||
}
|
||||
}
|
||||
//解析KEY
|
||||
else if (line.StartsWith(HLSTags.ext_x_key))
|
||||
{
|
||||
var iv = ParserUtil.GetAttribute(line, "IV");
|
||||
var method = ParserUtil.GetAttribute(line, "METHOD");
|
||||
var uri = ParserUtil.GetAttribute(line, "URI");
|
||||
var uri_last = ParserUtil.GetAttribute(lastKeyLine, "URI");
|
||||
|
||||
//自定义KEY情况 判断是否需要读取IV
|
||||
if (line.Contains("IV=0x") && ParserConfig.CustomeKey != null && ParserConfig.CustomeIV == null)
|
||||
{
|
||||
currentEncryptInfo.Method = ParserConfig.CustomMethod;
|
||||
currentEncryptInfo.Key = ParserConfig.CustomeKey;
|
||||
currentEncryptInfo.IV = HexUtil.HexToBytes(iv);
|
||||
}
|
||||
//如果KEY URL相同,不进行重复解析
|
||||
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);
|
||||
}
|
||||
}
|
||||
lastKeyLine = line;
|
||||
}
|
||||
//解析分片时长
|
||||
else if (line.StartsWith(HLSTags.extinf))
|
||||
{
|
||||
string[] tmp = ParserUtil.GetAttribute(line).Split(',');
|
||||
segment.Duration = Convert.ToDouble(tmp[0]);
|
||||
segment.Index = segIndex;
|
||||
//是否有加密,有的话写入KEY和IV
|
||||
if (currentEncryptInfo.Method != EncryptMethod.NONE)
|
||||
{
|
||||
segment.EncryptInfo.Method = currentEncryptInfo.Method;
|
||||
segment.EncryptInfo.Key = currentEncryptInfo.Key;
|
||||
segment.EncryptInfo.IV = currentEncryptInfo.IV;
|
||||
}
|
||||
expectSegment = true;
|
||||
segIndex++;
|
||||
}
|
||||
//m3u8主体结束
|
||||
else if (line.StartsWith(HLSTags.ext_x_endlist))
|
||||
{
|
||||
if (segments.Count > 0)
|
||||
{
|
||||
mediaParts.Add(new MediaPart()
|
||||
{
|
||||
MediaSegments = segments
|
||||
});
|
||||
}
|
||||
segments = new();
|
||||
isEndlist = true;
|
||||
}
|
||||
//#EXT-X-MAP
|
||||
else if (line.StartsWith(HLSTags.ext_x_map))
|
||||
{
|
||||
if (playlist.MediaInit == null)
|
||||
{
|
||||
playlist.MediaInit = new MediaSegment()
|
||||
{
|
||||
Url = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, ParserUtil.GetAttribute(line, "URI"))),
|
||||
};
|
||||
if (line.Contains("BYTERANGE"))
|
||||
{
|
||||
var p = ParserUtil.GetAttribute(line, "BYTERANGE");
|
||||
var (n, o) = ParserUtil.GetRange(p);
|
||||
segment.ExpectLength = n;
|
||||
segment.StartRange = o ?? 0L;
|
||||
}
|
||||
}
|
||||
//遇到了其他的map,说明已经不是一个视频了,全部丢弃即可
|
||||
else
|
||||
{
|
||||
if (segments.Count > 0)
|
||||
{
|
||||
mediaParts.Add(new MediaPart()
|
||||
{
|
||||
MediaSegments = segments
|
||||
});
|
||||
}
|
||||
segments = new();
|
||||
isEndlist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//评论行不解析
|
||||
else if (line.StartsWith("#")) continue;
|
||||
//空白行不解析
|
||||
else if (line.StartsWith("\r\n")) continue;
|
||||
//解析分片的地址
|
||||
else if (expectSegment)
|
||||
{
|
||||
var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, line));
|
||||
segment.Url = segUrl;
|
||||
segments.Add(segment);
|
||||
segment = new();
|
||||
//优酷的广告分段则清除此分片
|
||||
//需要注意,遇到广告说明程序对上文的#EXT-X-DISCONTINUITY做出的动作是不必要的,
|
||||
//其实上下文是同一种编码,需要恢复到原先的part上
|
||||
if (segUrl.Contains("ccode=") && segUrl.Contains("/ad/") && segUrl.Contains("duration="))
|
||||
{
|
||||
segments.RemoveAt(segments.Count - 1);
|
||||
segIndex--;
|
||||
hasAd = true;
|
||||
}
|
||||
//优酷广告(4K分辨率测试)
|
||||
if (segUrl.Contains("ccode=0902") && segUrl.Contains("duration="))
|
||||
{
|
||||
segments.RemoveAt(segments.Count - 1);
|
||||
segIndex--;
|
||||
hasAd = true;
|
||||
}
|
||||
expectSegment = false;
|
||||
}
|
||||
}
|
||||
|
||||
//直播的情况,无法遇到m3u8结束标记,需要手动将segments加入parts
|
||||
if (!isEndlist)
|
||||
{
|
||||
mediaParts.Add(new MediaPart()
|
||||
{
|
||||
MediaSegments = segments
|
||||
});
|
||||
}
|
||||
|
||||
playlist.MediaParts = mediaParts;
|
||||
playlist.IsLive = !isEndlist;
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
private async Task<byte[]> ParseKeyAsync(string uri)
|
||||
{
|
||||
var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, uri));
|
||||
var bytes = await HTTPUtil.GetBytesAsync(segUrl, ParserConfig.Headers);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
||||
{
|
||||
this.M3u8Content = rawText;
|
||||
this.PreProcessContent();
|
||||
if (IsMaster())
|
||||
{
|
||||
Logger.Warn(ResString.masterM3u8Found);
|
||||
var lists = await ParseMasterListAsync();
|
||||
lists = lists.DistinctBy(p => p.Url).ToList();
|
||||
for (int i = 0; i < lists.Count; i++)
|
||||
{
|
||||
//重新加载m3u8
|
||||
await LoadM3u8FromUrlAsync(lists[i].Url);
|
||||
lists[i].Playlist = await ParseListAsync();
|
||||
}
|
||||
return lists;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new List<StreamSpec>()
|
||||
{
|
||||
new StreamSpec()
|
||||
{
|
||||
Url = ParserConfig.Url,
|
||||
Playlist = await ParseListAsync()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadM3u8FromUrlAsync(string url)
|
||||
{
|
||||
//Logger.Info(ResString.loadingUrl + url);
|
||||
if (url.StartsWith("file:"))
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
this.M3u8Content = File.ReadAllText(uri.LocalPath);
|
||||
}
|
||||
else if (url.StartsWith("http"))
|
||||
{
|
||||
this.M3u8Content = await HTTPUtil.GetWebSourceAsync(url, ParserConfig.Headers);
|
||||
}
|
||||
this.M3u8Url = this.BaseUrl = url;
|
||||
this.PreProcessContent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using N_m3u8DL_RE.Common.Config;
|
||||
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.Parser.Extractor
|
||||
{
|
||||
internal interface IExtractor
|
||||
{
|
||||
ParserConfig ParserConfig { get; set; }
|
||||
|
||||
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>N_m3u8DL_RE.Parser</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\N_m3u8DL-RE.Common\N_m3u8DL-RE.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,69 @@
|
|||
using N_m3u8DL_RE.Common.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.Constants;
|
||||
using N_m3u8DL_RE.Parser.Extractor;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser
|
||||
{
|
||||
public class StreamExtractor
|
||||
{
|
||||
private IExtractor extractor;
|
||||
private ParserConfig parserConfig = new ParserConfig();
|
||||
private string rawText;
|
||||
|
||||
public StreamExtractor(ParserConfig parserConfig)
|
||||
{
|
||||
this.parserConfig = parserConfig;
|
||||
}
|
||||
|
||||
public void LoadSourceFromUrl(string url)
|
||||
{
|
||||
Logger.Info(ResString.loadingUrl + url);
|
||||
if (url.StartsWith("file:"))
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
this.rawText = File.ReadAllText(uri.LocalPath);
|
||||
parserConfig.Url = url;
|
||||
}
|
||||
else if (url.StartsWith("http"))
|
||||
{
|
||||
this.rawText = HTTPUtil.GetWebSourceAsync(url, parserConfig.Headers).Result;
|
||||
parserConfig.Url = url;
|
||||
}
|
||||
this.rawText = rawText.Trim();
|
||||
LoadSourceFromText(this.rawText);
|
||||
}
|
||||
|
||||
public void LoadSourceFromText(string rawText)
|
||||
{
|
||||
rawText = rawText.Trim();
|
||||
this.rawText = rawText;
|
||||
if (rawText.StartsWith(HLSTags.ext_m3u))
|
||||
{
|
||||
Logger.InfoMarkUp(ResString.matchHLS);
|
||||
extractor = new HLSExtractor(parserConfig);
|
||||
}
|
||||
else if (rawText.StartsWith(".."))
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(ResString.notSupported);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<StreamSpec>> ExtractStreamsAsync()
|
||||
{
|
||||
return extractor.ExtractStreamsAsync(rawText);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
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.Util
|
||||
{
|
||||
internal class ParserUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 从以下文本中获取参数
|
||||
/// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
|
||||
/// </summary>
|
||||
/// <param name="line">等待被解析的一行文本</param>
|
||||
/// <param name="key">留空则获取第一个英文冒号后的全部字符</param>
|
||||
/// <returns></returns>
|
||||
public static string GetAttribute(string line, string key = "")
|
||||
{
|
||||
line = line.Trim();
|
||||
if (key == "")
|
||||
return line.Substring(line.IndexOf(':') + 1);
|
||||
|
||||
if (line.Contains(key + "=\""))
|
||||
{
|
||||
return Regex.Match(line, key + "=\"([^\"]*)\"").Groups[1].Value;
|
||||
}
|
||||
else if (line.Contains(key + "="))
|
||||
{
|
||||
return Regex.Match(line, key + "=([^,]*)").Groups[1].Value;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从如下文本中提取
|
||||
/// <n>[@<o>]
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns>n(length) o(start)</returns>
|
||||
public static (long, long?) GetRange(string input)
|
||||
{
|
||||
var t = input.Split('@');
|
||||
if (t.Length > 0)
|
||||
{
|
||||
if (t.Length == 1)
|
||||
{
|
||||
return (Convert.ToInt64(t[0]), null);
|
||||
}
|
||||
if (t.Length == 2)
|
||||
{
|
||||
return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
|
||||
}
|
||||
}
|
||||
return (0, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拼接Baseurl和RelativeUrl
|
||||
/// </summary>
|
||||
/// <param name="baseurl">Baseurl</param>
|
||||
/// <param name="url">RelativeUrl</param>
|
||||
/// <returns></returns>
|
||||
public static string CombineURL(string baseurl, string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseurl))
|
||||
return url;
|
||||
|
||||
Uri uri1 = new Uri(baseurl); //这里直接传完整的URL即可
|
||||
Uri uri2 = new Uri(uri1, url);
|
||||
url = uri2.ToString();
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using N_m3u8DL_RE.Common.Entity;
|
||||
using N_m3u8DL_RE.Common.Enum;
|
||||
using N_m3u8DL_RE.Common.Resource;
|
||||
using Spectre.Console;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace N_m3u8DL_RE.Parser.Util
|
||||
{
|
||||
public class PromptUtil
|
||||
{
|
||||
public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
|
||||
{
|
||||
//基本流
|
||||
var basicStreams = lists.Where(x => x.MediaType == null);
|
||||
//可选音频轨道
|
||||
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
|
||||
//可选字幕轨道
|
||||
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
|
||||
|
||||
var prompt = new MultiSelectionPrompt<StreamSpec>()
|
||||
.Title(ResString.promptTitle)
|
||||
.UseConverter(x =>
|
||||
{
|
||||
if (x.Name != null && x.Name.StartsWith("__"))
|
||||
return $"[darkslategray1]{x.Name.Substring(2)}[/]";
|
||||
else
|
||||
return x.ToString().EscapeMarkup().RemoveMarkup();
|
||||
})
|
||||
.Required()
|
||||
.PageSize(10)
|
||||
.MoreChoicesText(ResString.promptChoiceText)
|
||||
.InstructionsText(ResString.promptInfo)
|
||||
.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams);
|
||||
|
||||
//默认选中第一个
|
||||
var first = lists.First();
|
||||
prompt.Select(first);
|
||||
|
||||
if (audios.Any())
|
||||
{
|
||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios);
|
||||
//默认音轨
|
||||
if (first.AudioId != null)
|
||||
{
|
||||
prompt.Select(audios.First(a => a.GroupId == first.AudioId));
|
||||
}
|
||||
}
|
||||
if (subs.Any())
|
||||
{
|
||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs);
|
||||
//默认字幕轨
|
||||
if (first.SubtitleId != null)
|
||||
{
|
||||
prompt.Select(subs.First(s => s.GroupId == first.SubtitleId));
|
||||
}
|
||||
}
|
||||
//多选
|
||||
var selectedStreams = AnsiConsole.Prompt(prompt);
|
||||
|
||||
return selectedStreams;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
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}"
|
||||
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}"
|
||||
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}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{E6915BF9-8306-4F62-B357-23430F0D80B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E6915BF9-8306-4F62-B357-23430F0D80B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6915BF9-8306-4F62-B357-23430F0D80B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6915BF9-8306-4F62-B357-23430F0D80B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {87F963D4-EA06-413D-9372-C726711C32B5}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,24 @@
|
|||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
|
||||
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
|
||||
<StaticallyLinked Condition="$(RuntimeIdentifier.StartsWith('win'))">true</StaticallyLinked>
|
||||
<TrimMode>Link</TrimMode>
|
||||
<TrimmerDefaultAction>link</TrimmerDefaultAction>
|
||||
<NativeAotCompilerVersion>7.0.0-*</NativeAotCompilerVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
|
||||
<!-- Cross-compilation for Windows x64-arm64 and Linux x64-arm64 -->
|
||||
<PackageReference Condition="'$(RuntimeIdentifier)'=='win-arm64'" Include="runtime.win-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
|
||||
<PackageReference Condition="'$(RuntimeIdentifier)'=='linux-arm64'" Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<IlcArg Include="--reflectedonly" />
|
||||
<RdXmlFile Include="rd.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>N_m3u8DL_RE</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,102 @@
|
|||
using N_m3u8DL_RE.Common.Config;
|
||||
using N_m3u8DL_RE.Common.Entity;
|
||||
using N_m3u8DL_RE.Common.Enum;
|
||||
using N_m3u8DL_RE.Common.Util;
|
||||
using N_m3u8DL_RE.Parser;
|
||||
using Spectre.Console;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using N_m3u8DL_RE.Common.Resource;
|
||||
using N_m3u8DL_RE.Common.Log;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using N_m3u8DL_RE.Parser.Util;
|
||||
|
||||
namespace N_m3u8DL_RE
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
string loc = "en-US";
|
||||
string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
|
||||
if (currLoc == "zh-TW" || currLoc == "zh-HK" || currLoc == "zh-MO") loc = "zh-TW";
|
||||
else if (currLoc == "zh-CN" || currLoc == "zh-SG") loc = "zh-CN";
|
||||
//设置语言
|
||||
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc);
|
||||
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(loc);
|
||||
|
||||
try
|
||||
{
|
||||
//Logger.LogLevel = LogLevel.DEBUG;
|
||||
var config = new ParserConfig();
|
||||
var url = string.Empty;
|
||||
//url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8";
|
||||
url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8";
|
||||
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
url = AnsiConsole.Ask<string>("Input [green]URL[/]: ");
|
||||
}
|
||||
|
||||
//流提取器配置
|
||||
var extractor = new StreamExtractor(config);
|
||||
extractor.LoadSourceFromUrl(url);
|
||||
|
||||
var streams = await extractor.ExtractStreamsAsync();
|
||||
//全部媒体
|
||||
var lists = streams.OrderByDescending(p => p.Bandwidth);
|
||||
//基本流
|
||||
var basicStreams = lists.Where(x => x.MediaType == null);
|
||||
//可选音频轨道
|
||||
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
|
||||
//可选字幕轨道
|
||||
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
|
||||
|
||||
Logger.Warn(ResString.writeJson);
|
||||
await File.WriteAllTextAsync("meta.json", GlobalUtil.ConvertToJson(lists), Encoding.UTF8);
|
||||
|
||||
Logger.Info(ResString.streamsInfo, lists.Count(), basicStreams.Count(), audios.Count(), subs.Count());
|
||||
|
||||
if (streams.Count > 1)
|
||||
{
|
||||
|
||||
foreach (var item in lists) Logger.InfoMarkUp(item.ToString());
|
||||
|
||||
var selectedStreams = PromptUtil.SelectStreams(lists);
|
||||
|
||||
Logger.Info(ResString.selectedStream);
|
||||
await File.WriteAllTextAsync("meta_selected.json", GlobalUtil.ConvertToJson(selectedStreams), Encoding.UTF8);
|
||||
foreach (var item in selectedStreams)
|
||||
{
|
||||
Logger.InfoMarkUp(item.ToString());
|
||||
}
|
||||
}
|
||||
else if (streams.Count == 1)
|
||||
{
|
||||
var playlist = streams.First().Playlist;
|
||||
if (playlist.IsLive)
|
||||
{
|
||||
Logger.Warn(ResString.liveFound);
|
||||
}
|
||||
//Print(playlist);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("解析失败");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex.ToString());
|
||||
}
|
||||
//Console.ReadKey();
|
||||
}
|
||||
|
||||
static void Print(object o)
|
||||
{
|
||||
Console.WriteLine(GlobalUtil.ConvertToJson(o));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<Directives>
|
||||
<Application>
|
||||
<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>
|
Loading…
Reference in New Issue