完成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