完成m3u8基本解析

This commit is contained in:
nilaoda 2022-06-19 00:49:17 +08:00
parent 35e8dac90e
commit 6828d11952
30 changed files with 2035 additions and 0 deletions

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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>();
}
}

View File

@ -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; }
}
}

View File

@ -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>();
}
}

View File

@ -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();
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

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.Enum
{
public enum MediaType
{
AUDIO = 0,
VIDEO = 1,
SUBTITLES = 2,
CLOSED_CAPTIONS = 3
}
}

View File

@ -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));
}
}

View File

@ -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>

View File

@ -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);
}
}
}
}

View File

@ -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]&lt;space&gt;[/] to toggle a stream, [green]&lt;enter&gt;[/] 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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

37
src/N_m3u8DL-RE.sln Normal file
View File

@ -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

View File

@ -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>

View File

@ -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>

102
src/N_m3u8DL-RE/Program.cs Normal file
View File

@ -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));
}
}
}

21
src/N_m3u8DL-RE/rd.xml Normal file
View File

@ -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>