调整部分代码结构

This commit is contained in:
nilaoda 2024-11-10 18:19:04 +08:00
parent 7d8e7c6402
commit b9d3b57b39
88 changed files with 8778 additions and 9160 deletions

View File

@ -5,39 +5,38 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity namespace N_m3u8DL_RE.Common.Entity;
public class EncryptInfo
{ {
public class EncryptInfo /// <summary>
/// 加密方式,默认无加密
/// </summary>
public EncryptMethod Method { get; set; } = EncryptMethod.NONE;
public byte[]? Key { get; set; }
public byte[]? IV { get; set; }
public EncryptInfo() { }
/// <summary>
/// 创建EncryptInfo并尝试自动解析Method
/// </summary>
/// <param name="method"></param>
public EncryptInfo(string method)
{ {
/// <summary> Method = ParseMethod(method);
/// 加密方式,默认无加密 }
/// </summary>
public EncryptMethod Method { get; set; } = EncryptMethod.NONE;
public byte[]? Key { get; set; } public static EncryptMethod ParseMethod(string? method)
public byte[]? IV { get; set; } {
if (method != null && System.Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m))
public EncryptInfo() { }
/// <summary>
/// 创建EncryptInfo并尝试自动解析Method
/// </summary>
/// <param name="method"></param>
public EncryptInfo(string method)
{ {
Method = ParseMethod(method); return m;
} }
else
public static EncryptMethod ParseMethod(string? method)
{ {
if (method != null && System.Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m)) return EncryptMethod.UNKNOWN;
{
return m;
}
else
{
return EncryptMethod.UNKNOWN;
}
} }
} }
} }

View File

@ -1,25 +1,18 @@
using System; namespace N_m3u8DL_RE.Common.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity public class MSSData
{ {
public class MSSData public string FourCC { get; set; } = "";
{ public string CodecPrivateData { get; set; } = "";
public string FourCC { get; set; } = ""; public string Type { get; set; } = "";
public string CodecPrivateData { get; set; } = ""; public int Timesacle { get; set; }
public string Type { get; set; } = ""; public int SamplingRate { get; set; }
public int Timesacle { get; set; } public int Channels { get; set; }
public int SamplingRate { get; set; } public int BitsPerSample { get; set; }
public int Channels { get; set; } public int NalUnitLengthField { get; set; }
public int BitsPerSample { get; set; } public long Duration { get; set; }
public int NalUnitLengthField { get; set; }
public long Duration { get; set; }
public bool IsProtection { get; set; } = false; public bool IsProtection { get; set; } = false;
public string ProtectionSystemID { get; set; } = ""; public string ProtectionSystemID { get; set; } = "";
public string ProtectionData { get; set; } = ""; public string ProtectionData { get; set; } = "";
}
} }

View File

@ -1,14 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Entity;
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
{ {
//主要处理 EXT-X-DISCONTINUITY public List<MediaSegment> MediaSegments { get; set; } = new List<MediaSegment>();
public class MediaPart
{
public List<MediaSegment> MediaSegments { get; set; } = new List<MediaSegment>();
}
} }

View File

@ -1,43 +1,36 @@
using System; namespace N_m3u8DL_RE.Common.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity public class MediaSegment
{ {
public class MediaSegment public long Index { get; set; }
public double Duration { get; set; }
public string? Title { get; set; }
public DateTime? DateTime { get; set; }
public long? StartRange { get; set; }
public long? StopRange => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null;
public long? ExpectLength { get; set; }
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo();
public string Url { get; set; }
public string? NameFromVar { get; set; } // MPD分段文件名
public override bool Equals(object? obj)
{ {
public long Index { get; set; } return obj is MediaSegment segment &&
public double Duration { get; set; } Index == segment.Index &&
public string? Title { get; set; } Math.Abs(Duration - segment.Duration) < 0.001 &&
public DateTime? DateTime { get; set; } Title == segment.Title &&
StartRange == segment.StartRange &&
StopRange == segment.StopRange &&
ExpectLength == segment.ExpectLength &&
Url == segment.Url;
}
public long? StartRange { get; set; } public override int GetHashCode()
public long? StopRange { get => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null; } {
public long? ExpectLength { get; set; } return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url);
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo();
public string Url { get; set; }
public string? NameFromVar { get; set; } //MPD分段文件名
public override bool Equals(object? obj)
{
return obj is MediaSegment segment &&
Index == segment.Index &&
Duration == segment.Duration &&
Title == segment.Title &&
StartRange == segment.StartRange &&
StopRange == segment.StopRange &&
ExpectLength == segment.ExpectLength &&
Url == segment.Url;
}
public override int GetHashCode()
{
return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url);
}
} }
} }

View File

@ -1,27 +1,19 @@
using N_m3u8DL_RE.Common.Enum; namespace 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.Common.Entity public class Playlist
{ {
public class Playlist // 对应Url信息
{ public string Url { get; set; }
//对应Url信息 // 是否直播
public string Url { get; set; } public bool IsLive { get; set; } = false;
//是否直播 // 直播刷新间隔毫秒默认15秒
public bool IsLive { get; set; } = false; public double RefreshIntervalMs { get; set; } = 15000;
//直播刷新间隔毫秒默认15秒 // 所有分片时长总和
public double RefreshIntervalMs { get; set; } = 15000; public double TotalDuration { get => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); }
//所有分片时长总和 // 所有分片中最长时长
public double TotalDuration { get => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); } public double? TargetDuration { get; set; }
//所有分片中最长时长 // INIT信息
public double? TargetDuration { get; set; } public MediaSegment? MediaInit { get; set; }
//INIT信息 // 分片信息
public MediaSegment? MediaInit { get; set; } public List<MediaPart> MediaParts { get; set; } = new List<MediaPart>();
//分片信息
public List<MediaPart> MediaParts { get; set; } = new List<MediaPart>();
}
} }

View File

@ -1,188 +1,182 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using Spectre.Console; 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 namespace N_m3u8DL_RE.Common.Entity;
public class StreamSpec
{ {
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 double? SkippedDuration { get; set; }
// MSS信息
public MSSData? MSSData { 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; }
public string? Extension { get; set; }
// Dash
public RoleType? Role { get; set; }
// 补充信息-色域
public string? VideoRange { get; set; }
// 补充信息-特征
public string? Characteristics { get; set; }
// 发布时间仅MPD需要
public DateTime? PublishTime { get; set; }
// 外部轨道GroupId (后续寻找对应轨道信息)
public string? AudioId { get; set; }
public string? VideoId { get; set; }
public string? SubtitleId { get; set; }
public string? PeriodId { get; set; }
/// <summary>
/// URL
/// </summary>
public string Url { get; set; }
/// <summary>
/// 原始URL
/// </summary>
public string OriginalUrl { get; set; }
public Playlist? Playlist { get; set; }
public int SegmentsCount
{ {
public MediaType? MediaType { get; set; } get
public string? GroupId { get; set; }
public string? Language { get; set; }
public string? Name { get; set; }
public Choise? Default { get; set; }
//由于用户选择 被跳过的分片总时长
public double? SkippedDuration { get; set; }
//MSS信息
public MSSData? MSSData { 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; }
public string? Extension { get; set; }
//Dash
public RoleType? Role { get; set; }
//补充信息-色域
public string? VideoRange { get; set; }
//补充信息-特征
public string? Characteristics { get; set; }
//发布时间仅MPD需要
public DateTime? PublishTime { get; set; }
//外部轨道GroupId (后续寻找对应轨道信息)
public string? AudioId { get; set; }
public string? VideoId { get; set; }
public string? SubtitleId { get; set; }
public string? PeriodId { get; set; }
/// <summary>
/// URL
/// </summary>
public string Url { get; set; }
/// <summary>
/// 原始URL
/// </summary>
public string OriginalUrl { get; set; }
public Playlist? Playlist { get; set; }
public int SegmentsCount
{ {
get return Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) : 0;
{
return Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) : 0;
}
}
public string ToShortString()
{
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty;
if (MediaType == Enum.MediaType.AUDIO)
{
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
returnStr = d.EscapeMarkup();
}
else if (MediaType == Enum.MediaType.SUBTITLES)
{
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Role}";
returnStr = d.EscapeMarkup();
}
else
{
prefixStr = $"[aqua]Vid[/] {encStr}";
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {Role}";
returnStr = d.EscapeMarkup();
}
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
}
public string ToShortShortString()
{
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty;
if (MediaType == Enum.MediaType.AUDIO)
{
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
var d = $"{(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
returnStr = d.EscapeMarkup();
}
else if (MediaType == Enum.MediaType.SUBTITLES)
{
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
var d = $"{Language} | {Name} | {Codecs} | {Role}";
returnStr = d.EscapeMarkup();
}
else
{
prefixStr = $"[aqua]Vid[/] {encStr}";
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {FrameRate} | {VideoRange} | {Role}";
returnStr = d.EscapeMarkup();
}
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
}
public override string ToString()
{
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty;
var segmentsCountStr = SegmentsCount == 0 ? "" : (SegmentsCount > 1 ? $"{SegmentsCount} Segments" : $"{SegmentsCount} Segment");
//增加加密标志
if (Playlist != null && Playlist.MediaParts.Any(m => m.MediaSegments.Any(s => s.EncryptInfo.Method != EncryptMethod.NONE)))
{
var ms = Playlist.MediaParts.SelectMany(m => m.MediaSegments.Select(s => s.EncryptInfo.Method)).Where(e => e != EncryptMethod.NONE).Distinct();
encStr = $"[red]*{string.Join(",", ms).EscapeMarkup()}[/] ";
}
if (MediaType == Enum.MediaType.AUDIO)
{
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {segmentsCountStr} | {Role}";
returnStr = d.EscapeMarkup();
}
else if (MediaType == Enum.MediaType.SUBTITLES)
{
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Characteristics} | {segmentsCountStr} | {Role}";
returnStr = d.EscapeMarkup();
}
else
{
prefixStr = $"[aqua]Vid[/] {encStr}";
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {segmentsCountStr} | {Role}";
returnStr = d.EscapeMarkup();
}
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
//计算时长
if (Playlist != null)
{
var total = Playlist.TotalDuration;
returnStr += " | ~" + GlobalUtil.FormatTime((int)total);
}
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
} }
} }
public string ToShortString()
{
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty;
if (MediaType == Enum.MediaType.AUDIO)
{
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
returnStr = d.EscapeMarkup();
}
else if (MediaType == Enum.MediaType.SUBTITLES)
{
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Role}";
returnStr = d.EscapeMarkup();
}
else
{
prefixStr = $"[aqua]Vid[/] {encStr}";
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {Role}";
returnStr = d.EscapeMarkup();
}
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
}
public string ToShortShortString()
{
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty;
if (MediaType == Enum.MediaType.AUDIO)
{
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
var d = $"{(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
returnStr = d.EscapeMarkup();
}
else if (MediaType == Enum.MediaType.SUBTITLES)
{
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
var d = $"{Language} | {Name} | {Codecs} | {Role}";
returnStr = d.EscapeMarkup();
}
else
{
prefixStr = $"[aqua]Vid[/] {encStr}";
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {FrameRate} | {VideoRange} | {Role}";
returnStr = d.EscapeMarkup();
}
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
}
public override string ToString()
{
var prefixStr = "";
var returnStr = "";
var encStr = string.Empty;
var segmentsCountStr = SegmentsCount == 0 ? "" : (SegmentsCount > 1 ? $"{SegmentsCount} Segments" : $"{SegmentsCount} Segment");
// 增加加密标志
if (Playlist != null && Playlist.MediaParts.Any(m => m.MediaSegments.Any(s => s.EncryptInfo.Method != EncryptMethod.NONE)))
{
var ms = Playlist.MediaParts.SelectMany(m => m.MediaSegments.Select(s => s.EncryptInfo.Method)).Where(e => e != EncryptMethod.NONE).Distinct();
encStr = $"[red]*{string.Join(",", ms).EscapeMarkup()}[/] ";
}
if (MediaType == Enum.MediaType.AUDIO)
{
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {segmentsCountStr} | {Role}";
returnStr = d.EscapeMarkup();
}
else if (MediaType == Enum.MediaType.SUBTITLES)
{
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Characteristics} | {segmentsCountStr} | {Role}";
returnStr = d.EscapeMarkup();
}
else
{
prefixStr = $"[aqua]Vid[/] {encStr}";
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {segmentsCountStr} | {Role}";
returnStr = d.EscapeMarkup();
}
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
while (returnStr.Contains("| |"))
{
returnStr = returnStr.Replace("| |", "|");
}
// 计算时长
if (Playlist != null)
{
var total = Playlist.TotalDuration;
returnStr += " | ~" + GlobalUtil.FormatTime((int)total);
}
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
}
} }

View File

@ -1,30 +1,23 @@
using System; namespace N_m3u8DL_RE.Common.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity public class SubCue
{ {
public class SubCue public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
public required string Payload { get; set; }
public required string Settings { get; set; }
public override bool Equals(object? obj)
{ {
public TimeSpan StartTime { get; set; } return obj is SubCue cue &&
public TimeSpan EndTime { get; set; } StartTime.Equals(cue.StartTime) &&
public required string Payload { get; set; } EndTime.Equals(cue.EndTime) &&
public required string Settings { get; set; } Payload == cue.Payload &&
Settings == cue.Settings;
}
public override bool Equals(object? obj) public override int GetHashCode()
{ {
return obj is SubCue cue && return HashCode.Combine(StartTime, EndTime, Payload, Settings);
StartTime.Equals(cue.StartTime) &&
EndTime.Equals(cue.EndTime) &&
Payload == cue.Payload &&
Settings == cue.Settings;
}
public override int GetHashCode()
{
return HashCode.Combine(StartTime, EndTime, Payload, Settings);
}
} }
} }

View File

@ -1,276 +1,271 @@
using System; using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity namespace N_m3u8DL_RE.Common.Entity;
public partial class WebVttSub
{ {
public partial class WebVttSub [GeneratedRegex("X-TIMESTAMP-MAP.*")]
private static partial Regex TSMapRegex();
[GeneratedRegex("MPEGTS:(\\d+)")]
private static partial Regex TSValueRegex();
[GeneratedRegex("\\s")]
private static partial Regex SplitRegex();
[GeneratedRegex("<c\\..*?>([\\s\\S]*?)<\\/c>")]
private static partial Regex VttClassRegex();
public List<SubCue> Cues { get; set; } = new List<SubCue>();
public long MpegtsTimestamp { get; set; } = 0L;
/// <summary>
/// 从字节数组解析WEBVTT
/// </summary>
/// <param name="textBytes"></param>
/// <returns></returns>
public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L)
{ {
[GeneratedRegex("X-TIMESTAMP-MAP.*")] return Parse(Encoding.UTF8.GetString(textBytes), BaseTimestamp);
private static partial Regex TSMapRegex(); }
[GeneratedRegex("MPEGTS:(\\d+)")]
private static partial Regex TSValueRegex();
[GeneratedRegex("\\s")]
private static partial Regex SplitRegex();
[GeneratedRegex("<c\\..*?>([\\s\\S]*?)<\\/c>")]
private static partial Regex VttClassRegex();
public List<SubCue> Cues { get; set; } = new List<SubCue>(); /// <summary>
public long MpegtsTimestamp { get; set; } = 0L; /// 从字节数组解析WEBVTT
/// </summary>
/// <param name="textBytes"></param>
/// <param name="encoding"></param>
/// <returns></returns>
public static WebVttSub Parse(byte[] textBytes, Encoding encoding, long BaseTimestamp = 0L)
{
return Parse(encoding.GetString(textBytes), BaseTimestamp);
}
/// <summary> /// <summary>
/// 从字节数组解析WEBVTT /// 从字符串解析WEBVTT
/// </summary> /// </summary>
/// <param name="textBytes"></param> /// <param name="text"></param>
/// <returns></returns> /// <returns></returns>
public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L) public static WebVttSub Parse(string text, long BaseTimestamp = 0L)
{
if (!text.Trim().StartsWith("WEBVTT"))
throw new Exception("Bad vtt!");
text += Environment.NewLine;
var webSub = new WebVttSub();
var needPayload = false;
var timeLine = "";
var regex1 = TSMapRegex();
if (regex1.IsMatch(text))
{ {
return Parse(Encoding.UTF8.GetString(textBytes), BaseTimestamp); var timestamp = TSValueRegex().Match(regex1.Match(text).Value).Groups[1].Value;
webSub.MpegtsTimestamp = Convert.ToInt64(timestamp);
} }
/// <summary> var payloads = new List<string>();
/// 从字节数组解析WEBVTT foreach (var line in text.Split('\n'))
/// </summary>
/// <param name="textBytes"></param>
/// <param name="encoding"></param>
/// <returns></returns>
public static WebVttSub Parse(byte[] textBytes, Encoding encoding, long BaseTimestamp = 0L)
{ {
return Parse(encoding.GetString(textBytes), BaseTimestamp); if (line.Contains(" --> "))
}
/// <summary>
/// 从字符串解析WEBVTT
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static WebVttSub Parse(string text, long BaseTimestamp = 0L)
{
if (!text.Trim().StartsWith("WEBVTT"))
throw new Exception("Bad vtt!");
text += Environment.NewLine;
var webSub = new WebVttSub();
var needPayload = false;
var timeLine = "";
var regex1 = TSMapRegex();
if (regex1.IsMatch(text))
{ {
var timestamp = TSValueRegex().Match(regex1.Match(text).Value).Groups[1].Value; needPayload = true;
webSub.MpegtsTimestamp = Convert.ToInt64(timestamp); timeLine = line.Trim();
continue;
} }
var payloads = new List<string>(); if (needPayload)
foreach (var line in text.Split('\n'))
{ {
if (line.Contains(" --> ")) if (string.IsNullOrEmpty(line.Trim()))
{ {
needPayload = true; var payload = string.Join(Environment.NewLine, payloads);
timeLine = line.Trim(); if (string.IsNullOrEmpty(payload.Trim())) continue; // 没获取到payload 跳过添加
continue;
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
var startTime = ConvertToTS(arr[0]);
var endTime = ConvertToTS(arr[1]);
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
webSub.Cues.Add(new SubCue()
{
StartTime = startTime,
EndTime = endTime,
Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), // Remove Zero Width Space!
Settings = style
});
payloads.Clear();
needPayload = false;
} }
else
if (needPayload)
{ {
if (string.IsNullOrEmpty(line.Trim())) payloads.Add(line.Trim());
{
var payload = string.Join(Environment.NewLine, payloads);
if (string.IsNullOrEmpty(payload.Trim())) continue; //没获取到payload 跳过添加
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
var startTime = ConvertToTS(arr[0]);
var endTime = ConvertToTS(arr[1]);
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
webSub.Cues.Add(new SubCue()
{
StartTime = startTime,
EndTime = endTime,
Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), //Remove Zero Width Space!
Settings = style
});
payloads.Clear();
needPayload = false;
}
else
{
payloads.Add(line.Trim());
}
} }
} }
if (BaseTimestamp != 0)
{
foreach (var item in webSub.Cues)
{
if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)
{
item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp);
item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp);
}
else
{
break;
}
}
}
return webSub;
} }
private static string RemoveClassTag(string text) if (BaseTimestamp != 0)
{ {
if (VttClassRegex().IsMatch(text))
{
return string.Join(Environment.NewLine, text.Split('\n').Select(line => line.TrimEnd()).Select(line =>
{
return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " "));
})).TrimEnd();
}
else return text;
}
/// <summary>
/// 从另一个字幕中获取所有Cue并加载此字幕中且自动修正偏移
/// </summary>
/// <param name="webSub"></param>
/// <returns></returns>
public WebVttSub AddCuesFromOne(WebVttSub webSub)
{
FixTimestamp(webSub, this.MpegtsTimestamp);
foreach (var item in webSub.Cues) foreach (var item in webSub.Cues)
{ {
if (!this.Cues.Contains(item)) if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)
{ {
//如果相差只有1ms且payload相同则拼接 item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp);
var last = this.Cues.LastOrDefault(); item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp);
if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload)
{
last.EndTime = item.EndTime;
}
else
{
this.Cues.Add(item);
}
} }
} else
return this;
}
private void FixTimestamp(WebVttSub sub, long baseTimestamp)
{
if (sub.MpegtsTimestamp == 0)
{
return;
}
//确实存在时间轴错误的情况,才修复
if ((this.Cues.Count > 0 && sub.Cues.Count > 0 && sub.Cues.First().StartTime < this.Cues.Last().EndTime && sub.Cues.First().EndTime != this.Cues.Last().EndTime) || this.Cues.Count == 0)
{
//The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second
var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;
var offset = TimeSpan.FromSeconds(seconds);
//当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒而字幕起始却是2秒),才修复
if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset)
{ {
for (int i = 0; i < sub.Cues.Count; i++) break;
{
sub.Cues[i].StartTime += offset;
sub.Cues[i].EndTime += offset;
}
} }
} }
} }
private IEnumerable<SubCue> GetCues() return webSub;
}
private static string RemoveClassTag(string text)
{
if (VttClassRegex().IsMatch(text))
{ {
return this.Cues.Where(c => !string.IsNullOrEmpty(c.Payload)); return string.Join(Environment.NewLine, text.Split('\n').Select(line => line.TrimEnd()).Select(line =>
{
return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " "));
})).TrimEnd();
} }
else return text;
}
private static TimeSpan ConvertToTS(string str) /// <summary>
/// 从另一个字幕中获取所有Cue并加载此字幕中且自动修正偏移
/// </summary>
/// <param name="webSub"></param>
/// <returns></returns>
public WebVttSub AddCuesFromOne(WebVttSub webSub)
{
FixTimestamp(webSub, this.MpegtsTimestamp);
foreach (var item in webSub.Cues)
{ {
//17.0s if (!this.Cues.Contains(item))
if (str.EndsWith('s'))
{ {
double sec = Convert.ToDouble(str[..^1]); // 如果相差只有1ms且payload相同则拼接
return TimeSpan.FromSeconds(sec); var last = this.Cues.LastOrDefault();
} if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload)
{
str = str.Replace(',', '.'); last.EndTime = item.EndTime;
long time = 0; }
string[] parts = str.Split('.'); else
if (parts.Length > 1) {
{ this.Cues.Add(item);
time += Convert.ToInt32(parts.Last().PadRight(3, '0')); }
str = parts.First();
}
var t = str.Split(':').Reverse().ToList();
for (int i = 0; i < t.Count(); i++)
{
time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
}
return TimeSpan.FromMilliseconds(time);
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (var c in GetCues()) //输出时去除空串
{
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);
sb.AppendLine(c.Payload);
sb.AppendLine();
}
sb.AppendLine();
return sb.ToString();
}
/// <summary>
/// 字幕向前平移指定时间
/// </summary>
/// <param name="time"></param>
public void LeftShiftTime(TimeSpan time)
{
foreach (var cue in this.Cues)
{
if (cue.StartTime.TotalSeconds - time.TotalSeconds > 0) cue.StartTime -= time;
else cue.StartTime = TimeSpan.FromSeconds(0);
if (cue.EndTime.TotalSeconds - time.TotalSeconds > 0) cue.EndTime -= time;
else cue.EndTime = TimeSpan.FromSeconds(0);
} }
} }
return this;
}
public string ToVtt() private void FixTimestamp(WebVttSub sub, long baseTimestamp)
{
if (sub.MpegtsTimestamp == 0)
{ {
return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString(); return;
} }
public string ToSrt() // 确实存在时间轴错误的情况,才修复
if ((this.Cues.Count > 0 && sub.Cues.Count > 0 && sub.Cues.First().StartTime < this.Cues.Last().EndTime && sub.Cues.First().EndTime != this.Cues.Last().EndTime) || this.Cues.Count == 0)
{ {
StringBuilder sb = new StringBuilder(); // The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second
int index = 1; var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;
foreach (var c in GetCues()) var offset = TimeSpan.FromSeconds(seconds);
// 当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒而字幕起始却是2秒),才修复
if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset)
{ {
sb.AppendLine($"{index++}"); for (int i = 0; i < sub.Cues.Count; i++)
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\,fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\,fff")); {
sb.AppendLine(c.Payload); sub.Cues[i].StartTime += offset;
sb.AppendLine(); sub.Cues[i].EndTime += offset;
}
} }
sb.AppendLine();
var srt = sb.ToString();
if (string.IsNullOrEmpty(srt.Trim()))
{
srt = "1\r\n00:00:00,000 --> 00:00:01,000"; //空字幕
}
return srt;
} }
} }
private IEnumerable<SubCue> GetCues()
{
return this.Cues.Where(c => !string.IsNullOrEmpty(c.Payload));
}
private static TimeSpan ConvertToTS(string str)
{
// 17.0s
if (str.EndsWith('s'))
{
double sec = Convert.ToDouble(str[..^1]);
return TimeSpan.FromSeconds(sec);
}
str = str.Replace(',', '.');
long time = 0;
string[] parts = str.Split('.');
if (parts.Length > 1)
{
time += Convert.ToInt32(parts.Last().PadRight(3, '0'));
str = parts.First();
}
var t = str.Split(':').Reverse().ToList();
for (int i = 0; i < t.Count(); i++)
{
time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
}
return TimeSpan.FromMilliseconds(time);
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (var c in GetCues()) // 输出时去除空串
{
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);
sb.AppendLine(c.Payload);
sb.AppendLine();
}
sb.AppendLine();
return sb.ToString();
}
/// <summary>
/// 字幕向前平移指定时间
/// </summary>
/// <param name="time"></param>
public void LeftShiftTime(TimeSpan time)
{
foreach (var cue in this.Cues)
{
if (cue.StartTime.TotalSeconds - time.TotalSeconds > 0) cue.StartTime -= time;
else cue.StartTime = TimeSpan.FromSeconds(0);
if (cue.EndTime.TotalSeconds - time.TotalSeconds > 0) cue.EndTime -= time;
else cue.EndTime = TimeSpan.FromSeconds(0);
}
}
public string ToVtt()
{
return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString();
}
public string ToSrt()
{
StringBuilder sb = new StringBuilder();
int index = 1;
foreach (var c in GetCues())
{
sb.AppendLine($"{index++}");
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\,fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\,fff"));
sb.AppendLine(c.Payload);
sb.AppendLine();
}
sb.AppendLine();
var srt = sb.ToString();
if (string.IsNullOrEmpty(srt.Trim()))
{
srt = "1\r\n00:00:00,000 --> 00:00:01,000"; // 空字幕
}
return srt;
}
} }

View File

@ -1,14 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum Choise
{ {
public enum Choise YES = 1,
{ NO = 0
YES = 1,
NO = 0
}
} }

View File

@ -1,20 +1,13 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum EncryptMethod
{ {
public enum EncryptMethod NONE,
{ AES_128,
NONE, AES_128_ECB,
AES_128, SAMPLE_AES,
AES_128_ECB, SAMPLE_AES_CTR,
SAMPLE_AES, CENC,
SAMPLE_AES_CTR, CHACHA20,
CENC, UNKNOWN
CHACHA20,
UNKNOWN
}
} }

View File

@ -1,16 +1,9 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum ExtractorType
{ {
public enum ExtractorType MPEG_DASH,
{ HLS,
MPEG_DASH, HTTP_LIVE,
HLS, MSS
HTTP_LIVE,
MSS
}
} }

View File

@ -1,16 +1,9 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum MediaType
{ {
public enum MediaType AUDIO = 0,
{ VIDEO = 1,
AUDIO = 0, SUBTITLES = 2,
VIDEO = 1, CLOSED_CAPTIONS = 3
SUBTITLES = 2,
CLOSED_CAPTIONS = 3
}
} }

View File

@ -1,15 +1,14 @@
namespace N_m3u8DL_RE.Common.Enum namespace N_m3u8DL_RE.Common.Enum;
public enum RoleType
{ {
public enum RoleType Subtitle = 0,
{ Main = 1,
Subtitle = 0, Alternate = 2,
Main = 1, Supplementary = 3,
Alternate = 2, Commentary = 4,
Supplementary = 3, Dub = 5,
Commentary = 4, Description = 6,
Dub = 5, Sign = 7,
Description = 6, Metadata = 8,
Sign = 7,
Metadata = 8,
}
} }

View File

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

View File

@ -6,12 +6,11 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.JsonConverter 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)); 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

@ -8,27 +8,27 @@ public class NonAnsiWriter : TextWriter
{ {
public override Encoding Encoding => Console.OutputEncoding; public override Encoding Encoding => Console.OutputEncoding;
private string lastOut = ""; private string? _lastOut = "";
public override void Write(char value) public override void Write(char value)
{ {
Console.Write(value); Console.Write(value);
} }
public override void Write(string value) public override void Write(string? value)
{ {
if (lastOut == value) if (_lastOut == value)
{ {
return; return;
} }
lastOut = value; _lastOut = value;
RemoveAnsiEscapeSequences(value); RemoveAnsiEscapeSequences(value);
} }
private void RemoveAnsiEscapeSequences(string input) private void RemoveAnsiEscapeSequences(string? input)
{ {
// Use regular expression to remove ANSI escape sequences // Use regular expression to remove ANSI escape sequences
string output = Regex.Replace(input, @"\x1B\[(\d+;?)+m", ""); string output = Regex.Replace(input ?? "", @"\x1B\[(\d+;?)+m", "");
output = Regex.Replace(output, @"\[\??\d+[AKlh]", ""); output = Regex.Replace(output, @"\[\??\d+[AKlh]", "");
output = Regex.Replace(output,"[\r\n] +",""); output = Regex.Replace(output,"[\r\n] +","");
if (string.IsNullOrWhiteSpace(output)) if (string.IsNullOrWhiteSpace(output))

View File

@ -4,14 +4,13 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Log namespace N_m3u8DL_RE.Common.Log;
public enum LogLevel
{ {
public enum LogLevel OFF,
{ ERROR,
OFF, WARN,
ERROR, INFO,
WARN, DEBUG,
INFO,
DEBUG,
}
} }

View File

@ -1,243 +1,236 @@
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace N_m3u8DL_RE.Common.Log namespace N_m3u8DL_RE.Common.Log;
public partial class Logger
{ {
public partial class Logger [GeneratedRegex("{}")]
private static partial Regex VarsRepRegex();
/// <summary>
/// 日志级别默认为INFO
/// </summary>
public static LogLevel LogLevel { get; set; } = LogLevel.INFO;
/// <summary>
/// 是否写出日志文件
/// </summary>
public static bool IsWriteFile { get; set; } = true;
/// <summary>
/// 本次运行日志文件所在位置
/// </summary>
private static string? LogFilePath { get; set; }
// 读写锁
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
public static void InitLogFile()
{ {
[GeneratedRegex("{}")] if (!IsWriteFile) return;
private static partial Regex VarsRepRegex();
/// <summary> try
/// 日志级别默认为INFO
/// </summary>
public static LogLevel LogLevel { get; set; } = LogLevel.INFO;
/// <summary>
/// 是否写出日志文件
/// </summary>
public static bool IsWriteFile { get; set; } = true;
/// <summary>
/// 本次运行日志文件所在位置
/// </summary>
private static string? LogFilePath { get; set; }
//读写锁
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
public static void InitLogFile()
{ {
if (!IsWriteFile) return; var logDir = Path.GetDirectoryName(Environment.ProcessPath) + "/Logs";
if (!Directory.Exists(logDir))
try
{ {
var logDir = Path.GetDirectoryName(Environment.ProcessPath) + "/Logs"; Directory.CreateDirectory(logDir);
if (!Directory.Exists(logDir)) }
{
Directory.CreateDirectory(logDir);
}
var now = DateTime.Now; var now = DateTime.Now;
LogFilePath = Path.Combine(logDir, now.ToString("yyyy-MM-dd_HH-mm-ss-fff") + ".log"); LogFilePath = Path.Combine(logDir, now.ToString("yyyy-MM-dd_HH-mm-ss-fff") + ".log");
int index = 1; int index = 1;
var fileName = Path.GetFileNameWithoutExtension(LogFilePath); var fileName = Path.GetFileNameWithoutExtension(LogFilePath);
string init = "LOG " + now.ToString("yyyy/MM/dd") + Environment.NewLine string init = "LOG " + now.ToString("yyyy/MM/dd") + Environment.NewLine
+ "Save Path: " + Path.GetDirectoryName(LogFilePath) + Environment.NewLine + "Save Path: " + Path.GetDirectoryName(LogFilePath) + Environment.NewLine
+ "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine + "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine
+ "Task CommandLine: " + Environment.CommandLine; + "Task CommandLine: " + Environment.CommandLine;
init += $"{Environment.NewLine}{Environment.NewLine}"; init += $"{Environment.NewLine}{Environment.NewLine}";
//若文件存在则加序号 // 若文件存在则加序号
while (File.Exists(LogFilePath)) while (File.Exists(LogFilePath))
{
LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log");
}
File.WriteAllText(LogFilePath, init, Encoding.UTF8);
}
catch (Exception ex)
{ {
Error($"Init log failed! {ex.Message.RemoveMarkup()}"); LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log");
} }
File.WriteAllText(LogFilePath, init, Encoding.UTF8);
} }
catch (Exception ex)
private static string GetCurrTime()
{ {
return DateTime.Now.ToString("HH:mm:ss.fff"); Error($"Init log failed! {ex.Message.RemoveMarkup()}");
} }
}
private static void HandleLog(string write, string subWrite = "") private static string GetCurrTime()
{
return DateTime.Now.ToString("HH:mm:ss.fff");
}
private static void HandleLog(string write, string subWrite = "")
{
try
{ {
try if (subWrite == "")
{ {
if (subWrite == "") CustomAnsiConsole.MarkupLine(write);
{
CustomAnsiConsole.MarkupLine(write);
}
else
{
CustomAnsiConsole.Markup(write);
Console.WriteLine(subWrite);
}
if (IsWriteFile && File.Exists(LogFilePath))
{
var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();
try
{
//进入写入
LogWriteLock.EnterWriteLock();
using (StreamWriter sw = File.AppendText(LogFilePath))
{
sw.WriteLine(plain);
}
}
finally
{
//释放占用
LogWriteLock.ExitWriteLock();
}
}
} }
catch (Exception) else
{ {
Console.WriteLine("Failed to write: " + write); CustomAnsiConsole.Markup(write);
} Console.WriteLine(subWrite);
}
private static string ReplaceVars(string data, params object[] ps)
{
for (int i = 0; i < ps.Length; i++)
{
data = VarsRepRegex().Replace(data, $"{ps[i]}", 1);
} }
return data;
}
public static void Info(string data, params object[] ps)
{
if (LogLevel >= LogLevel.INFO)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
HandleLog(write, data);
}
}
public static void InfoMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.INFO)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
HandleLog(write);
}
}
public static void Debug(string data, params object[] ps)
{
if (LogLevel >= LogLevel.DEBUG)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
HandleLog(write, data);
}
}
public static void DebugMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.DEBUG)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
HandleLog(write);
}
}
public static void Warn(string data, params object[] ps)
{
if (LogLevel >= LogLevel.WARN)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
HandleLog(write, data);
}
}
public static void WarnMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.WARN)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
HandleLog(write);
}
}
public static void Error(string data, params object[] ps)
{
if (LogLevel >= LogLevel.ERROR)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
HandleLog(write, data);
}
}
public static void ErrorMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.ERROR)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
HandleLog(write);
}
}
public static void ErrorMarkUp(Exception exception)
{
string data = exception.Message.EscapeMarkup();
if (LogLevel >= LogLevel.ERROR)
{
data = exception.ToString().EscapeMarkup();
}
ErrorMarkUp(data);
}
/// <summary>
/// This thing will only write to the log file.
/// </summary>
/// <param name="data"></param>
/// <param name="ps"></param>
public static void Extra(string data, params object[] ps)
{
if (IsWriteFile && File.Exists(LogFilePath)) if (IsWriteFile && File.Exists(LogFilePath))
{ {
data = ReplaceVars(data, ps); var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();
var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup();
try try
{ {
//进入写入 // 进入写入
LogWriteLock.EnterWriteLock(); LogWriteLock.EnterWriteLock();
using (StreamWriter sw = File.AppendText(LogFilePath)) using (StreamWriter sw = File.AppendText(LogFilePath))
{ {
sw.WriteLine(plain, Encoding.UTF8); sw.WriteLine(plain);
} }
} }
finally finally
{ {
//释放占用 // 释放占用
LogWriteLock.ExitWriteLock(); LogWriteLock.ExitWriteLock();
} }
} }
} }
catch (Exception)
{
Console.WriteLine("Failed to write: " + write);
}
}
private static string ReplaceVars(string data, params object[] ps)
{
for (int i = 0; i < ps.Length; i++)
{
data = VarsRepRegex().Replace(data, $"{ps[i]}", 1);
}
return data;
}
public static void Info(string data, params object[] ps)
{
if (LogLevel >= LogLevel.INFO)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
HandleLog(write, data);
}
}
public static void InfoMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.INFO)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
HandleLog(write);
}
}
public static void Debug(string data, params object[] ps)
{
if (LogLevel >= LogLevel.DEBUG)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
HandleLog(write, data);
}
}
public static void DebugMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.DEBUG)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
HandleLog(write);
}
}
public static void Warn(string data, params object[] ps)
{
if (LogLevel >= LogLevel.WARN)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
HandleLog(write, data);
}
}
public static void WarnMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.WARN)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
HandleLog(write);
}
}
public static void Error(string data, params object[] ps)
{
if (LogLevel >= LogLevel.ERROR)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
HandleLog(write, data);
}
}
public static void ErrorMarkUp(string data, params object[] ps)
{
if (LogLevel >= LogLevel.ERROR)
{
data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
HandleLog(write);
}
}
public static void ErrorMarkUp(Exception exception)
{
string data = exception.Message.EscapeMarkup();
if (LogLevel >= LogLevel.ERROR)
{
data = exception.ToString().EscapeMarkup();
}
ErrorMarkUp(data);
}
/// <summary>
/// This thing will only write to the log file.
/// </summary>
/// <param name="data"></param>
/// <param name="ps"></param>
public static void Extra(string data, params object[] ps)
{
if (IsWriteFile && File.Exists(LogFilePath))
{
data = ReplaceVars(data, ps);
var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup();
try
{
// 进入写入
LogWriteLock.EnterWriteLock();
using (StreamWriter sw = File.AppendText(LogFilePath))
{
sw.WriteLine(plain, Encoding.UTF8);
}
}
finally
{
// 释放占用
LogWriteLock.ExitWriteLock();
}
}
} }
} }

View File

@ -1,151 +1,142 @@
using System; namespace N_m3u8DL_RE.Common.Resource;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Resource public class ResString
{ {
public class ResString public static readonly string ReLiveTs = "<RE_LIVE_TS>";
public static string singleFileRealtimeDecryptWarn => GetText("singleFileRealtimeDecryptWarn");
public static string singleFileSplitWarn => GetText("singleFileSplitWarn");
public static string customRangeWarn => GetText("customRangeWarn");
public static string customRangeFound => GetText("customRangeFound");
public static string customAdKeywordsFound => GetText("customAdKeywordsFound");
public static string customRangeInvalid => GetText("customRangeInvalid");
public static string consoleRedirected => GetText("consoleRedirected");
public static string autoBinaryMerge => GetText("autoBinaryMerge");
public static string autoBinaryMerge2 => GetText("autoBinaryMerge2");
public static string autoBinaryMerge3 => GetText("autoBinaryMerge3");
public static string autoBinaryMerge4 => GetText("autoBinaryMerge4");
public static string autoBinaryMerge5 => GetText("autoBinaryMerge5");
public static string autoBinaryMerge6 => GetText("autoBinaryMerge6");
public static string badM3u8 => GetText("badM3u8");
public static string binaryMerge => GetText("binaryMerge");
public static string checkingLast => GetText("checkingLast");
public static string cmd_appendUrlParams => GetText("cmd_appendUrlParams");
public static string cmd_autoSelect => GetText("cmd_autoSelect");
public static string cmd_binaryMerge => GetText("cmd_binaryMerge");
public static string cmd_useFFmpegConcatDemuxer => GetText("cmd_useFFmpegConcatDemuxer");
public static string cmd_checkSegmentsCount => GetText("cmd_checkSegmentsCount");
public static string cmd_decryptionBinaryPath => GetText("cmd_decryptionBinaryPath");
public static string cmd_delAfterDone => GetText("cmd_delAfterDone");
public static string cmd_ffmpegBinaryPath => GetText("cmd_ffmpegBinaryPath");
public static string cmd_mkvmergeBinaryPath => GetText("cmd_mkvmergeBinaryPath");
public static string cmd_baseUrl => GetText("cmd_baseUrl");
public static string cmd_maxSpeed => GetText("cmd_maxSpeed");
public static string cmd_adKeyword => GetText("cmd_adKeyword");
public static string cmd_moreHelp => GetText("cmd_moreHelp");
public static string cmd_header => GetText("cmd_header");
public static string cmd_muxImport => GetText("cmd_muxImport");
public static string cmd_muxImport_more => GetText("cmd_muxImport_more");
public static string cmd_selectVideo => GetText("cmd_selectVideo");
public static string cmd_dropVideo => GetText("cmd_dropVideo");
public static string cmd_selectVideo_more => GetText("cmd_selectVideo_more");
public static string cmd_selectAudio => GetText("cmd_selectAudio");
public static string cmd_dropAudio => GetText("cmd_dropAudio");
public static string cmd_selectAudio_more => GetText("cmd_selectAudio_more");
public static string cmd_selectSubtitle => GetText("cmd_selectSubtitle");
public static string cmd_dropSubtitle => GetText("cmd_dropSubtitle");
public static string cmd_selectSubtitle_more => GetText("cmd_selectSubtitle_more");
public static string cmd_custom_range => GetText("cmd_custom_range");
public static string cmd_customHLSMethod => GetText("cmd_customHLSMethod");
public static string cmd_customHLSKey => GetText("cmd_customHLSKey");
public static string cmd_customHLSIv => GetText("cmd_customHLSIv");
public static string cmd_Input => GetText("cmd_Input");
public static string cmd_forceAnsiConsole => GetText("cmd_forceAnsiConsole");
public static string cmd_noAnsiColor => GetText("cmd_noAnsiColor");
public static string cmd_keys => GetText("cmd_keys");
public static string cmd_keyText => GetText("cmd_keyText");
public static string cmd_loadKeyFailed => GetText("cmd_loadKeyFailed");
public static string cmd_logLevel => GetText("cmd_logLevel");
public static string cmd_MP4RealTimeDecryption => GetText("cmd_MP4RealTimeDecryption");
public static string cmd_saveDir => GetText("cmd_saveDir");
public static string cmd_saveName => GetText("cmd_saveName");
public static string cmd_savePattern => GetText("cmd_savePattern");
public static string cmd_skipDownload => GetText("cmd_skipDownload");
public static string cmd_noDateInfo => GetText("cmd_noDateInfo");
public static string cmd_noLog => GetText("cmd_noLog");
public static string cmd_skipMerge => GetText("cmd_skipMerge");
public static string cmd_subFormat => GetText("cmd_subFormat");
public static string cmd_subOnly => GetText("cmd_subOnly");
public static string cmd_subtitleFix => GetText("cmd_subtitleFix");
public static string cmd_threadCount => GetText("cmd_threadCount");
public static string cmd_downloadRetryCount => GetText("cmd_downloadRetryCount");
public static string cmd_tmpDir => GetText("cmd_tmpDir");
public static string cmd_uiLanguage => GetText("cmd_uiLanguage");
public static string cmd_urlProcessorArgs => GetText("cmd_urlProcessorArgs");
public static string cmd_useShakaPackager => GetText("cmd_useShakaPackager");
public static string cmd_concurrentDownload => GetText("cmd_concurrentDownload");
public static string cmd_useSystemProxy => GetText("cmd_useSystemProxy");
public static string cmd_customProxy => GetText("cmd_customProxy");
public static string cmd_customRange => GetText("cmd_customRange");
public static string cmd_liveKeepSegments => GetText("cmd_liveKeepSegments");
public static string cmd_livePipeMux => GetText("cmd_livePipeMux");
public static string cmd_liveRecordLimit => GetText("cmd_liveRecordLimit");
public static string cmd_taskStartAt => GetText("cmd_taskStartAt");
public static string cmd_liveWaitTime => GetText("cmd_liveWaitTime");
public static string cmd_liveTakeCount => GetText("cmd_liveTakeCount");
public static string cmd_liveFixVttByAudio => GetText("cmd_liveFixVttByAudio");
public static string cmd_liveRealTimeMerge => GetText("cmd_liveRealTimeMerge");
public static string cmd_livePerformAsVod => GetText("cmd_livePerformAsVod");
public static string cmd_muxAfterDone => GetText("cmd_muxAfterDone");
public static string cmd_muxAfterDone_more => GetText("cmd_muxAfterDone_more");
public static string cmd_writeMetaJson => GetText("cmd_writeMetaJson");
public static string liveLimit => GetText("liveLimit");
public static string realTimeDecMessage => GetText("realTimeDecMessage");
public static string liveLimitReached => GetText("liveLimitReached");
public static string saveName => GetText("saveName");
public static string taskStartAt => GetText("taskStartAt");
public static string namedPipeCreated => GetText("namedPipeCreated");
public static string namedPipeMux => GetText("namedPipeMux");
public static string partMerge => GetText("partMerge");
public static string fetch => GetText("fetch");
public static string ffmpegMerge => GetText("ffmpegMerge");
public static string ffmpegNotFound => GetText("ffmpegNotFound");
public static string fixingTTML => GetText("fixingTTML");
public static string fixingTTMLmp4 => GetText("fixingTTMLmp4");
public static string fixingVTT => GetText("fixingVTT");
public static string fixingVTTmp4 => GetText("fixingVTTmp4");
public static string keyProcessorNotFound => GetText("keyProcessorNotFound");
public static string liveFound => GetText("liveFound");
public static string loadingUrl => GetText("loadingUrl");
public static string masterM3u8Found => GetText("masterM3u8Found");
public static string matchDASH => GetText("matchDASH");
public static string matchMSS => GetText("matchMSS");
public static string matchTS => GetText("matchTS");
public static string matchHLS => GetText("matchHLS");
public static string notSupported => GetText("notSupported");
public static string parsingStream => GetText("parsingStream");
public static string promptChoiceText => GetText("promptChoiceText");
public static string promptInfo => GetText("promptInfo");
public static string promptTitle => GetText("promptTitle");
public static string readingInfo => GetText("readingInfo");
public static string searchKey => GetText("searchKey");
public static string segmentCountCheckNotPass => GetText("segmentCountCheckNotPass");
public static string selectedStream => GetText("selectedStream");
public static string startDownloading => GetText("startDownloading");
public static string streamsInfo => GetText("streamsInfo");
public static string writeJson => GetText("writeJson");
public static string noStreamsToDownload => GetText("noStreamsToDownload");
public static string newVersionFound => GetText("newVersionFound");
public static string processImageSub => GetText("processImageSub");
private static string GetText(string key)
{ {
public readonly static string ReLiveTs = "<RE_LIVE_TS>"; if (!StaticText.LANG_DIC.ContainsKey(key))
public static string singleFileRealtimeDecryptWarn { get => GetText("singleFileRealtimeDecryptWarn"); } return "<...LANG TEXT MISSING...>";
public static string singleFileSplitWarn { get => GetText("singleFileSplitWarn"); }
public static string customRangeWarn { get => GetText("customRangeWarn"); }
public static string customRangeFound { get => GetText("customRangeFound"); }
public static string customAdKeywordsFound { get => GetText("customAdKeywordsFound"); }
public static string customRangeInvalid { get => GetText("customRangeInvalid"); }
public static string consoleRedirected { get => GetText("consoleRedirected"); }
public static string autoBinaryMerge { get => GetText("autoBinaryMerge"); }
public static string autoBinaryMerge2 { get => GetText("autoBinaryMerge2"); }
public static string autoBinaryMerge3 { get => GetText("autoBinaryMerge3"); }
public static string autoBinaryMerge4 { get => GetText("autoBinaryMerge4"); }
public static string autoBinaryMerge5 { get => GetText("autoBinaryMerge5"); }
public static string autoBinaryMerge6 { get => GetText("autoBinaryMerge6"); }
public static string badM3u8 { get => GetText("badM3u8"); }
public static string binaryMerge { get => GetText("binaryMerge"); }
public static string checkingLast { get => GetText("checkingLast"); }
public static string cmd_appendUrlParams { get => GetText("cmd_appendUrlParams"); }
public static string cmd_autoSelect { get => GetText("cmd_autoSelect"); }
public static string cmd_binaryMerge { get => GetText("cmd_binaryMerge"); }
public static string cmd_useFFmpegConcatDemuxer { get => GetText("cmd_useFFmpegConcatDemuxer"); }
public static string cmd_checkSegmentsCount { get => GetText("cmd_checkSegmentsCount"); }
public static string cmd_decryptionBinaryPath { get => GetText("cmd_decryptionBinaryPath"); }
public static string cmd_delAfterDone { get => GetText("cmd_delAfterDone"); }
public static string cmd_ffmpegBinaryPath { get => GetText("cmd_ffmpegBinaryPath"); }
public static string cmd_mkvmergeBinaryPath { get => GetText("cmd_mkvmergeBinaryPath"); }
public static string cmd_baseUrl { get => GetText("cmd_baseUrl"); }
public static string cmd_maxSpeed { get => GetText("cmd_maxSpeed"); }
public static string cmd_adKeyword { get => GetText("cmd_adKeyword"); }
public static string cmd_moreHelp { get => GetText("cmd_moreHelp"); }
public static string cmd_header { get => GetText("cmd_header"); }
public static string cmd_muxImport { get => GetText("cmd_muxImport"); }
public static string cmd_muxImport_more { get => GetText("cmd_muxImport_more"); }
public static string cmd_selectVideo { get => GetText("cmd_selectVideo"); }
public static string cmd_dropVideo { get => GetText("cmd_dropVideo"); }
public static string cmd_selectVideo_more { get => GetText("cmd_selectVideo_more"); }
public static string cmd_selectAudio { get => GetText("cmd_selectAudio"); }
public static string cmd_dropAudio { get => GetText("cmd_dropAudio"); }
public static string cmd_selectAudio_more { get => GetText("cmd_selectAudio_more"); }
public static string cmd_selectSubtitle { get => GetText("cmd_selectSubtitle"); }
public static string cmd_dropSubtitle { get => GetText("cmd_dropSubtitle"); }
public static string cmd_selectSubtitle_more { get => GetText("cmd_selectSubtitle_more"); }
public static string cmd_custom_range { get => GetText("cmd_custom_range"); }
public static string cmd_customHLSMethod { get => GetText("cmd_customHLSMethod"); }
public static string cmd_customHLSKey { get => GetText("cmd_customHLSKey"); }
public static string cmd_customHLSIv { get => GetText("cmd_customHLSIv"); }
public static string cmd_Input { get => GetText("cmd_Input"); }
public static string cmd_forceAnsiConsole { get => GetText("cmd_forceAnsiConsole"); }
public static string cmd_noAnsiColor { get => GetText("cmd_noAnsiColor"); }
public static string cmd_keys { get => GetText("cmd_keys"); }
public static string cmd_keyText { get => GetText("cmd_keyText"); }
public static string cmd_loadKeyFailed { get => GetText("cmd_loadKeyFailed"); }
public static string cmd_logLevel { get => GetText("cmd_logLevel"); }
public static string cmd_MP4RealTimeDecryption { get => GetText("cmd_MP4RealTimeDecryption"); }
public static string cmd_saveDir { get => GetText("cmd_saveDir"); }
public static string cmd_saveName { get => GetText("cmd_saveName"); }
public static string cmd_savePattern { get => GetText("cmd_savePattern"); }
public static string cmd_skipDownload { get => GetText("cmd_skipDownload"); }
public static string cmd_noDateInfo { get => GetText("cmd_noDateInfo"); }
public static string cmd_noLog { get => GetText("cmd_noLog"); }
public static string cmd_skipMerge { get => GetText("cmd_skipMerge"); }
public static string cmd_subFormat { get => GetText("cmd_subFormat"); }
public static string cmd_subOnly { get => GetText("cmd_subOnly"); }
public static string cmd_subtitleFix { get => GetText("cmd_subtitleFix"); }
public static string cmd_threadCount { get => GetText("cmd_threadCount"); }
public static string cmd_downloadRetryCount { get => GetText("cmd_downloadRetryCount"); }
public static string cmd_tmpDir { get => GetText("cmd_tmpDir"); }
public static string cmd_uiLanguage { get => GetText("cmd_uiLanguage"); }
public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); }
public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); }
public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); }
public static string cmd_useSystemProxy { get => GetText("cmd_useSystemProxy"); }
public static string cmd_customProxy { get => GetText("cmd_customProxy"); }
public static string cmd_customRange { get => GetText("cmd_customRange"); }
public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); }
public static string cmd_livePipeMux { get => GetText("cmd_livePipeMux"); }
public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); }
public static string cmd_taskStartAt { get => GetText("cmd_taskStartAt"); }
public static string cmd_liveWaitTime { get => GetText("cmd_liveWaitTime"); }
public static string cmd_liveTakeCount { get => GetText("cmd_liveTakeCount"); }
public static string cmd_liveFixVttByAudio { get => GetText("cmd_liveFixVttByAudio"); }
public static string cmd_liveRealTimeMerge { get => GetText("cmd_liveRealTimeMerge"); }
public static string cmd_livePerformAsVod { get => GetText("cmd_livePerformAsVod"); }
public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); }
public static string cmd_muxAfterDone_more { get => GetText("cmd_muxAfterDone_more"); }
public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); }
public static string liveLimit { get => GetText("liveLimit"); }
public static string realTimeDecMessage { get => GetText("realTimeDecMessage"); }
public static string liveLimitReached { get => GetText("liveLimitReached"); }
public static string saveName { get => GetText("saveName"); }
public static string taskStartAt { get => GetText("taskStartAt"); }
public static string namedPipeCreated { get => GetText("namedPipeCreated"); }
public static string namedPipeMux { get => GetText("namedPipeMux"); }
public static string partMerge { get => GetText("partMerge"); }
public static string fetch { get => GetText("fetch"); }
public static string ffmpegMerge { get => GetText("ffmpegMerge"); }
public static string ffmpegNotFound { get => GetText("ffmpegNotFound"); }
public static string fixingTTML { get => GetText("fixingTTML"); }
public static string fixingTTMLmp4 { get => GetText("fixingTTMLmp4"); }
public static string fixingVTT { get => GetText("fixingVTT"); }
public static string fixingVTTmp4 { get => GetText("fixingVTTmp4"); }
public static string keyProcessorNotFound { get => GetText("keyProcessorNotFound"); }
public static string liveFound { get => GetText("liveFound"); }
public static string loadingUrl { get => GetText("loadingUrl"); }
public static string masterM3u8Found { get => GetText("masterM3u8Found"); }
public static string matchDASH { get => GetText("matchDASH"); }
public static string matchMSS { get => GetText("matchMSS"); }
public static string matchTS { get => GetText("matchTS"); }
public static string matchHLS { get => GetText("matchHLS"); }
public static string notSupported { get => GetText("notSupported"); }
public static string parsingStream { get => GetText("parsingStream"); }
public static string promptChoiceText { get => GetText("promptChoiceText"); }
public static string promptInfo { get => GetText("promptInfo"); }
public static string promptTitle { get => GetText("promptTitle"); }
public static string readingInfo { get => GetText("readingInfo"); }
public static string searchKey { get => GetText("searchKey"); }
public static string segmentCountCheckNotPass { get => GetText("segmentCountCheckNotPass"); }
public static string selectedStream { get => GetText("selectedStream"); }
public static string startDownloading { get => GetText("startDownloading"); }
public static string streamsInfo { get => GetText("streamsInfo"); }
public static string writeJson { get => GetText("writeJson"); }
public static string noStreamsToDownload { get => GetText("noStreamsToDownload"); }
public static string newVersionFound { get => GetText("newVersionFound"); }
public static string processImageSub { get => GetText("processImageSub"); }
private static string GetText(string key) var current = Thread.CurrentThread.CurrentUICulture.Name;
{ if (current == "zh-CN" || current == "zh-SG" || current == "zh-Hans")
if (!StaticText.LANG_DIC.ContainsKey(key)) return StaticText.LANG_DIC[key].ZH_CN;
return "<...LANG TEXT MISSING...>"; if (current.StartsWith("zh-"))
return StaticText.LANG_DIC[key].ZH_TW;
var current = Thread.CurrentThread.CurrentUICulture.Name; return StaticText.LANG_DIC[key].EN_US;
if (current == "zh-CN" || current == "zh-SG" || current == "zh-Hans")
return StaticText.LANG_DIC[key].ZH_CN;
else if (current.StartsWith("zh-"))
return StaticText.LANG_DIC[key].ZH_TW;
else
return StaticText.LANG_DIC[key].EN_US;
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,15 @@
using System; namespace N_m3u8DL_RE.Common.Resource;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Resource internal class TextContainer
{ {
internal class TextContainer public string ZH_CN { get; }
{ public string ZH_TW { get; }
public string ZH_CN { get; set; } public string EN_US { get; }
public string ZH_TW { get; set; }
public string EN_US { get; set; }
public TextContainer(string zhCN, string zhTW, string enUS) public TextContainer(string zhCN, string zhTW, string enUS)
{ {
ZH_CN = zhCN; ZH_CN = zhCN;
ZH_TW = zhTW; ZH_TW = zhTW;
EN_US = enUS; EN_US = enUS;
}
} }
} }

View File

@ -1,18 +1,13 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.JsonConverter; 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;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Util; namespace N_m3u8DL_RE.Common.Util;
public static class GlobalUtil public static class GlobalUtil
{ {
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions private static readonly JsonSerializerOptions Options = new()
{ {
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = true, WriteIndented = true,
@ -27,15 +22,15 @@ public static class GlobalUtil
{ {
return JsonSerializer.Serialize(s, Context.StreamSpec); return JsonSerializer.Serialize(s, Context.StreamSpec);
} }
else if (o is IOrderedEnumerable<StreamSpec> ss) if (o is IOrderedEnumerable<StreamSpec> ss)
{ {
return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec); return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);
} }
else if (o is List<StreamSpec> sList) if (o is List<StreamSpec> sList)
{ {
return JsonSerializer.Serialize(sList, Context.ListStreamSpec); return JsonSerializer.Serialize(sList, Context.ListStreamSpec);
} }
else if (o is IEnumerable<MediaSegment> mList) if (o is IEnumerable<MediaSegment> mList)
{ {
return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment); return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment);
} }
@ -47,14 +42,14 @@ public static class GlobalUtil
return fileSize switch return fileSize switch
{ {
< 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)), < 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),
>= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GB", (double)fileSize / (1024 * 1024 * 1024)), >= 1024 * 1024 * 1024 => $"{fileSize / (1024 * 1024 * 1024):########0.00}GB",
>= 1024 * 1024 => string.Format("{0:####0.00}MB", (double)fileSize / (1024 * 1024)), >= 1024 * 1024 => $"{fileSize / (1024 * 1024):####0.00}MB",
>= 1024 => string.Format("{0:####0.00}KB", (double)fileSize / 1024), >= 1024 => $"{fileSize / 1024:####0.00}KB",
_ => string.Format("{0:####0.00}B", fileSize) _ => $"{fileSize:####0.00}B"
}; };
} }
//此函数用于格式化输出时长 // 此函数用于格式化输出时长
public static string FormatTime(int time) public static string FormatTime(int time)
{ {
TimeSpan ts = new TimeSpan(0, 0, time); TimeSpan ts = new TimeSpan(0, 0, time);
@ -74,6 +69,6 @@ public static class GlobalUtil
var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) }; var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) };
var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ??
Array.Empty<string>(); Array.Empty<string>();
return searchPath.Concat(envPath).Select(p => Path.Combine(p, name + fileExt)).FirstOrDefault(File.Exists); return searchPath.Concat(envPath).Select(p => Path.Combine(p!, name + fileExt)).FirstOrDefault(File.Exists);
} }
} }

View File

@ -37,13 +37,13 @@ public static class HTTPUtil
} }
} }
Logger.Debug(webRequest.Headers.ToString()); Logger.Debug(webRequest.Headers.ToString());
//手动处理跳转以免自定义Headers丢失 // 手动处理跳转以免自定义Headers丢失
var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead); var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead);
if (((int)webResponse.StatusCode).ToString().StartsWith("30")) if (((int)webResponse.StatusCode).ToString().StartsWith("30"))
{ {
HttpResponseHeaders respHeaders = webResponse.Headers; HttpResponseHeaders respHeaders = webResponse.Headers;
Logger.Debug(respHeaders.ToString()); Logger.Debug(respHeaders.ToString());
if (respHeaders != null && respHeaders.Location != null) if (respHeaders.Location != null)
{ {
var redirectedUrl = ""; var redirectedUrl = "";
if (!respHeaders.Location.IsAbsoluteUri) if (!respHeaders.Location.IsAbsoluteUri)
@ -64,7 +64,7 @@ public static class HTTPUtil
} }
} }
} }
//手动将跳转后的URL设置进去, 用于后续取用 // 手动将跳转后的URL设置进去, 用于后续取用
webResponse.Headers.Location = new Uri(url); webResponse.Headers.Location = new Uri(url);
webResponse.EnsureSuccessStatusCode(); webResponse.EnsureSuccessStatusCode();
return webResponse; return webResponse;
@ -76,9 +76,8 @@ public static class HTTPUtil
{ {
return await File.ReadAllBytesAsync(new Uri(url).LocalPath); return await File.ReadAllBytesAsync(new Uri(url).LocalPath);
} }
byte[] bytes = new byte[0];
var webResponse = await DoGetAsync(url, headers); var webResponse = await DoGetAsync(url, headers);
bytes = await webResponse.Content.ReadAsByteArrayAsync(); var bytes = await webResponse.Content.ReadAsByteArrayAsync();
Logger.Debug(HexUtil.BytesToHex(bytes, " ")); Logger.Debug(HexUtil.BytesToHex(bytes, " "));
return bytes; return bytes;
} }
@ -91,9 +90,8 @@ public static class HTTPUtil
/// <returns></returns> /// <returns></returns>
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null) public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
{ {
string htmlCode = string.Empty;
var webResponse = await DoGetAsync(url, headers); var webResponse = await DoGetAsync(url, headers);
htmlCode = await webResponse.Content.ReadAsStringAsync(); string htmlCode = await webResponse.Content.ReadAsStringAsync();
Logger.Debug(htmlCode); Logger.Debug(htmlCode);
return htmlCode; return htmlCode;
} }
@ -112,7 +110,7 @@ public static class HTTPUtil
/// <returns>(Source Code, RedirectedUrl)</returns> /// <returns>(Source Code, RedirectedUrl)</returns>
public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null) public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null)
{ {
string htmlCode = string.Empty; string htmlCode;
var webResponse = await DoGetAsync(url, headers); var webResponse = await DoGetAsync(url, headers);
if (CheckMPEG2TS(webResponse)) if (CheckMPEG2TS(webResponse))
{ {
@ -128,7 +126,7 @@ public static class HTTPUtil
public static async Task<string> GetPostResponseAsync(string Url, byte[] postData) public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)
{ {
string htmlCode = string.Empty; string htmlCode;
using HttpRequestMessage request = new(HttpMethod.Post, Url); using HttpRequestMessage request = new(HttpMethod.Post, Url);
request.Headers.TryAddWithoutValidation("Content-Type", "application/json"); request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString()); request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());

View File

@ -31,7 +31,7 @@ public static class HexUtil
var hexSpan = hex.AsSpan().Trim(); var hexSpan = hex.AsSpan().Trim();
if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X")) if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X"))
{ {
hexSpan = hexSpan.Slice(2); hexSpan = hexSpan[2..];
} }
return Convert.FromHexString(hexSpan); return Convert.FromHexString(hexSpan);

View File

@ -3,66 +3,65 @@ using N_m3u8DL_RE.Parser.Processor;
using N_m3u8DL_RE.Parser.Processor.DASH; using N_m3u8DL_RE.Parser.Processor.DASH;
using N_m3u8DL_RE.Parser.Processor.HLS; using N_m3u8DL_RE.Parser.Processor.HLS;
namespace N_m3u8DL_RE.Parser.Config namespace N_m3u8DL_RE.Parser.Config;
public class ParserConfig
{ {
public class ParserConfig public string Url { get; set; }
{
public string Url { get; set; }
public string OriginalUrl { get; set; } public string OriginalUrl { get; set; }
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
/// <summary> /// <summary>
/// 内容前置处理器. 调用顺序与列表顺序相同 /// 内容前置处理器. 调用顺序与列表顺序相同
/// </summary> /// </summary>
public IList<ContentProcessor> ContentProcessors { get; } = new List<ContentProcessor>() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() }; public IList<ContentProcessor> ContentProcessors { get; } = new List<ContentProcessor>() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() };
/// <summary> /// <summary>
/// 添加分片URL前置处理器. 调用顺序与列表顺序相同 /// 添加分片URL前置处理器. 调用顺序与列表顺序相同
/// </summary> /// </summary>
public IList<UrlProcessor> UrlProcessors { get; } = new List<UrlProcessor>() { new DefaultUrlProcessor() }; public IList<UrlProcessor> UrlProcessors { get; } = new List<UrlProcessor>() { new DefaultUrlProcessor() };
/// <summary> /// <summary>
/// KEY解析器. 调用顺序与列表顺序相同 /// KEY解析器. 调用顺序与列表顺序相同
/// </summary> /// </summary>
public IList<KeyProcessor> KeyProcessors { get; } = new List<KeyProcessor>() { new DefaultHLSKeyProcessor() }; public IList<KeyProcessor> KeyProcessors { get; } = new List<KeyProcessor>() { new DefaultHLSKeyProcessor() };
/// <summary> /// <summary>
/// 自定义的加密方式 /// 自定义的加密方式
/// </summary> /// </summary>
public EncryptMethod? CustomMethod { get; set; } public EncryptMethod? CustomMethod { get; set; }
/// <summary> /// <summary>
/// 自定义的解密KEY /// 自定义的解密KEY
/// </summary> /// </summary>
public byte[]? CustomeKey { get; set; } public byte[]? CustomeKey { get; set; }
/// <summary> /// <summary>
/// 自定义的解密IV /// 自定义的解密IV
/// </summary> /// </summary>
public byte[]? CustomeIV { get; set; } public byte[]? CustomeIV { get; set; }
/// <summary> /// <summary>
/// 组装视频分段的URL时是否要把原本URL后的参数也加上去 /// 组装视频分段的URL时是否要把原本URL后的参数也加上去
/// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx" /// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx"
/// 相对路径 = clip_01.ts /// 相对路径 = clip_01.ts
/// 如果 AppendUrlParams=false得 http://xxx.com/clip_01.ts /// 如果 AppendUrlParams=false得 http://xxx.com/clip_01.ts
/// 如果 AppendUrlParams=true得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx /// 如果 AppendUrlParams=true得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx
/// </summary> /// </summary>
public bool AppendUrlParams { get; set; } = false; public bool AppendUrlParams { get; set; } = false;
/// <summary> /// <summary>
/// 此参数将会传递给URL Processor中 /// 此参数将会传递给URL Processor中
/// </summary> /// </summary>
public string? UrlProcessorArgs { get; set; } public string? UrlProcessorArgs { get; set; }
/// <summary> /// <summary>
/// KEY重试次数 /// KEY重试次数
/// </summary> /// </summary>
public int KeyRetryCount { get; set; } = 3; public int KeyRetryCount { get; set; } = 3;
}
} }

View File

@ -4,13 +4,12 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants namespace N_m3u8DL_RE.Parser.Constants;
internal class DASHTags
{ {
internal class DASHTags public static string TemplateRepresentationID = "$RepresentationID$";
{ public static string TemplateBandwidth = "$Bandwidth$";
public static string TemplateRepresentationID = "$RepresentationID$"; public static string TemplateNumber = "$Number$";
public static string TemplateBandwidth = "$Bandwidth$"; public static string TemplateTime = "$Time$";
public static string TemplateNumber = "$Number$";
public static string TemplateTime = "$Time$";
}
} }

View File

@ -4,35 +4,34 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants namespace N_m3u8DL_RE.Parser.Constants;
internal class HLSTags
{ {
internal class HLSTags public static string ext_m3u = "#EXTM3U";
{ public static string ext_x_targetduration = "#EXT-X-TARGETDURATION";
public static string ext_m3u = "#EXTM3U"; public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE";
public static string ext_x_targetduration = "#EXT-X-TARGETDURATION"; public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE";
public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"; public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME";
public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"; public static string ext_x_media = "#EXT-X-MEDIA";
public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"; public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE";
public static string ext_x_media = "#EXT-X-MEDIA"; public static string ext_x_key = "#EXT-X-KEY";
public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"; public static string ext_x_stream_inf = "#EXT-X-STREAM-INF";
public static string ext_x_key = "#EXT-X-KEY"; public static string ext_x_version = "#EXT-X-VERSION";
public static string ext_x_stream_inf = "#EXT-X-STREAM-INF"; public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE";
public static string ext_x_version = "#EXT-X-VERSION"; public static string ext_x_endlist = "#EXT-X-ENDLIST";
public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"; public static string extinf = "#EXTINF";
public static string ext_x_endlist = "#EXT-X-ENDLIST"; public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY";
public static string extinf = "#EXTINF"; public static string ext_x_byterange = "#EXT-X-BYTERANGE";
public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"; public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF";
public static string ext_x_byterange = "#EXT-X-BYTERANGE"; public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY";
public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"; public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT";
public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY"; public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT";
public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT"; public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS";
public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT"; public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35";
public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"; public static string ext_x_cue_start = "#EXT-X-CUE-OUT";
public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35"; public static string ext_x_cue_end = "#EXT-X-CUE-IN";
public static string ext_x_cue_start = "#EXT-X-CUE-OUT"; public static string ext_x_cue_span = "#EXT-X-CUE-SPAN";
public static string ext_x_cue_end = "#EXT-X-CUE-IN"; public static string ext_x_map = "#EXT-X-MAP";
public static string ext_x_cue_span = "#EXT-X-CUE-SPAN"; public static string ext_x_start = "#EXT-X-START";
public static string ext_x_map = "#EXT-X-MAP";
public static string ext_x_start = "#EXT-X-START";
}
} }

View File

@ -4,13 +4,12 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants namespace N_m3u8DL_RE.Parser.Constants;
internal class MSSTags
{ {
internal class MSSTags public static string Bitrate = "{Bitrate}";
{ public static string Bitrate_BK = "{bitrate}";
public static string Bitrate = "{Bitrate}"; public static string StartTime = "{start_time}";
public static string Bitrate_BK = "{bitrate}"; public static string StartTime_BK = "{start time}";
public static string StartTime = "{start_time}";
public static string StartTime_BK = "{start time}";
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,21 +7,20 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
public interface IExtractor
{ {
public interface IExtractor ExtractorType ExtractorType { get; }
{
ExtractorType ExtractorType { get; }
ParserConfig ParserConfig { get; set; } ParserConfig ParserConfig { get; set; }
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText); Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
Task FetchPlayListAsync(List<StreamSpec> streamSpecs); Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
Task RefreshPlayListAsync(List<StreamSpec> streamSpecs); Task RefreshPlayListAsync(List<StreamSpec> streamSpecs);
string PreProcessUrl(string url); string PreProcessUrl(string url);
void PreProcessContent(); void PreProcessContent();
}
} }

View File

@ -3,51 +3,50 @@ using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
internal class LiveTSExtractor : IExtractor
{ {
internal class LiveTSExtractor : IExtractor public ExtractorType ExtractorType => ExtractorType.HTTP_LIVE;
public ParserConfig ParserConfig {get; set;}
public LiveTSExtractor(ParserConfig parserConfig)
{ {
public ExtractorType ExtractorType => ExtractorType.HTTP_LIVE; this.ParserConfig = parserConfig;
}
public ParserConfig ParserConfig {get; set;} public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{
public LiveTSExtractor(ParserConfig parserConfig) return Task.FromResult(new List<StreamSpec>
{ {
this.ParserConfig = parserConfig; new()
}
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{
return new List<StreamSpec>()
{ {
new StreamSpec() OriginalUrl = ParserConfig.OriginalUrl,
{ Url = ParserConfig.Url,
OriginalUrl = ParserConfig.OriginalUrl, Playlist = new Playlist(),
Url = ParserConfig.Url, GroupId = ResString.ReLiveTs
Playlist = new Playlist(), }
GroupId = ResString.ReLiveTs });
} }
};
}
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs) public Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async void PreProcessContent() public void PreProcessContent()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public string PreProcessUrl(string url) public string PreProcessUrl(string url)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task RefreshPlayListAsync(List<StreamSpec> streamSpecs) public Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
}
} }
} }

View File

@ -16,386 +16,386 @@ using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
// Microsoft Smooth Streaming
// https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/manifest
// file:///C:/Users/nilaoda/Downloads/[MS-SSTR]-180316.pdf
internal partial class MSSExtractor : IExtractor
{ {
//Microsoft Smooth Streaming [GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")]
//https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/manifest private static partial Regex VCodecsRegex();
//file:///C:/Users/nilaoda/Downloads/[MS-SSTR]-180316.pdf
internal partial class MSSExtractor : IExtractor ////////////////////////////////////////
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;
public ExtractorType ExtractorType => ExtractorType.MSS;
private string IsmUrl = string.Empty;
private string BaseUrl = string.Empty;
private string IsmContent = string.Empty;
public ParserConfig ParserConfig { get; set; }
public MSSExtractor(ParserConfig parserConfig)
{ {
[GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")] this.ParserConfig = parserConfig;
private static partial Regex VCodecsRegex(); SetInitUrl();
}
//////////////////////////////////////// private void SetInitUrl()
{
this.IsmUrl = ParserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl))
this.BaseUrl = ParserConfig.BaseUrl;
else
this.BaseUrl = this.IsmUrl;
}
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC; public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{
var streamList = new List<StreamSpec>();
this.IsmContent = rawText;
this.PreProcessContent();
public ExtractorType ExtractorType => ExtractorType.MSS; var xmlDocument = XDocument.Parse(IsmContent);
private string IsmUrl = string.Empty; // 选中第一个SmoothStreamingMedia节点
private string BaseUrl = string.Empty; var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia");
private string IsmContent = string.Empty; var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000";
public ParserConfig ParserConfig { get; set; } var durationStr = ssmElement.Attribute("Duration")?.Value;
var timescale = Convert.ToInt32(timeScaleStr);
var isLiveStr = ssmElement.Attribute("IsLive")?.Value;
bool isLive = Convert.ToBoolean(isLiveStr ?? "FALSE");
public MSSExtractor(ParserConfig parserConfig) var isProtection = false;
var protectionSystemId = "";
var protectionData = "";
// 加密检测
var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Protection");
if (protectElement != null)
{ {
this.ParserConfig = parserConfig; var protectionHeader = protectElement.Element("ProtectionHeader");
SetInitUrl(); if (protectionHeader != null)
}
private void SetInitUrl()
{
this.IsmUrl = ParserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl))
this.BaseUrl = ParserConfig.BaseUrl;
else
this.BaseUrl = this.IsmUrl;
}
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{
var streamList = new List<StreamSpec>();
this.IsmContent = rawText;
this.PreProcessContent();
var xmlDocument = XDocument.Parse(IsmContent);
//选中第一个SmoothStreamingMedia节点
var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia");
var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000";
var durationStr = ssmElement.Attribute("Duration")?.Value;
var timescale = Convert.ToInt32(timeScaleStr);
var isLiveStr = ssmElement.Attribute("IsLive")?.Value;
bool isLive = Convert.ToBoolean(isLiveStr ?? "FALSE");
var isProtection = false;
var protectionSystemId = "";
var protectionData = "";
//加密检测
var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Protection");
if (protectElement != null)
{ {
var protectionHeader = protectElement.Element("ProtectionHeader"); isProtection = true;
if (protectionHeader != null) protectionSystemId = protectionHeader.Attribute("SystemID")?.Value ?? "9A04F079-9840-4286-AB92-E65BE0885F95";
{ protectionData = HexUtil.BytesToHex(Convert.FromBase64String(protectionHeader.Value));
isProtection = true;
protectionSystemId = protectionHeader.Attribute("SystemID")?.Value ?? "9A04F079-9840-4286-AB92-E65BE0885F95";
protectionData = HexUtil.BytesToHex(Convert.FromBase64String(protectionHeader.Value));
}
} }
}
//所有StreamIndex节点 // 所有StreamIndex节点
var streamIndexElements = ssmElement.Elements().Where(e => e.Name.LocalName == "StreamIndex"); var streamIndexElements = ssmElement.Elements().Where(e => e.Name.LocalName == "StreamIndex");
foreach (var streamIndex in streamIndexElements) foreach (var streamIndex in streamIndexElements)
{
var type = streamIndex.Attribute("Type")?.Value; // "video" / "audio" / "text"
var name = streamIndex.Attribute("Name")?.Value;
var subType = streamIndex.Attribute("Subtype")?.Value; // text track
// 如果有则不从QualityLevel读取
// Bitrate = "{bitrate}" / "{Bitrate}"
// StartTimeSubstitution = "{start time}" / "{start_time}"
var urlPattern = streamIndex.Attribute("Url")?.Value;
var language = streamIndex.Attribute("Language")?.Value;
// 去除不规范的语言标签
if (language?.Length != 3) language = null;
// 所有c节点
var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == "c");
// 所有QualityLevel节点
var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == "QualityLevel");
foreach (var qualityLevel in qualityLevelElements)
{ {
var type = streamIndex.Attribute("Type")?.Value; //"video" / "audio" / "text" urlPattern = (qualityLevel.Attribute("Url")?.Value ?? urlPattern)!
var name = streamIndex.Attribute("Name")?.Value; .Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime);
var subType = streamIndex.Attribute("Subtype")?.Value; //text track var fourCC = qualityLevel.Attribute("FourCC")!.Value.ToUpper();
//如果有则不从QualityLevel读取 var samplingRateStr = qualityLevel.Attribute("SamplingRate")?.Value;
//Bitrate = "{bitrate}" / "{Bitrate}" var bitsPerSampleStr = qualityLevel.Attribute("BitsPerSample")?.Value;
//StartTimeSubstitution = "{start time}" / "{start_time}" var nalUnitLengthFieldStr = qualityLevel.Attribute("NALUnitLengthField")?.Value;
var urlPattern = streamIndex.Attribute("Url")?.Value; var indexStr = qualityLevel.Attribute("Index")?.Value;
var language = streamIndex.Attribute("Language")?.Value; var codecPrivateData = qualityLevel.Attribute("CodecPrivateData")?.Value ?? "";
//去除不规范的语言标签 var audioTag = qualityLevel.Attribute("AudioTag")?.Value;
if (language?.Length != 3) language = null; var bitrate = Convert.ToInt32(qualityLevel.Attribute("Bitrate")?.Value ?? "0");
var width = Convert.ToInt32(qualityLevel.Attribute("MaxWidth")?.Value ?? "0");
var height = Convert.ToInt32(qualityLevel.Attribute("MaxHeight")?.Value ?? "0");
var channels = qualityLevel.Attribute("Channels")?.Value;
//所有c节点 StreamSpec streamSpec = new();
var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == "c"); streamSpec.PublishTime = DateTime.Now; // 发布时间默认现在
streamSpec.Extension = "m4s";
//所有QualityLevel节点 streamSpec.OriginalUrl = ParserConfig.OriginalUrl;
var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == "QualityLevel"); streamSpec.PeriodId = indexStr;
streamSpec.Playlist = new Playlist();
foreach (var qualityLevel in qualityLevelElements) streamSpec.Playlist.IsLive = isLive;
streamSpec.Playlist.MediaParts.Add(new MediaPart());
streamSpec.GroupId = name ?? indexStr;
streamSpec.Bandwidth = bitrate;
streamSpec.Codecs = ParseCodecs(fourCC, codecPrivateData);
streamSpec.Language = language;
streamSpec.Resolution = width == 0 ? null : $"{width}x{height}";
streamSpec.Url = IsmUrl;
streamSpec.Channels = channels;
streamSpec.MediaType = type switch
{ {
urlPattern = (qualityLevel.Attribute("Url")?.Value ?? urlPattern)! "text" => MediaType.SUBTITLES,
.Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime); "audio" => MediaType.AUDIO,
var fourCC = qualityLevel.Attribute("FourCC")!.Value.ToUpper(); _ => null
var samplingRateStr = qualityLevel.Attribute("SamplingRate")?.Value; };
var bitsPerSampleStr = qualityLevel.Attribute("BitsPerSample")?.Value;
var nalUnitLengthFieldStr = qualityLevel.Attribute("NALUnitLengthField")?.Value;
var indexStr = qualityLevel.Attribute("Index")?.Value;
var codecPrivateData = qualityLevel.Attribute("CodecPrivateData")?.Value ?? "";
var audioTag = qualityLevel.Attribute("AudioTag")?.Value;
var bitrate = Convert.ToInt32(qualityLevel.Attribute("Bitrate")?.Value ?? "0");
var width = Convert.ToInt32(qualityLevel.Attribute("MaxWidth")?.Value ?? "0");
var height = Convert.ToInt32(qualityLevel.Attribute("MaxHeight")?.Value ?? "0");
var channels = qualityLevel.Attribute("Channels")?.Value;
StreamSpec streamSpec = new(); streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.PublishTime = DateTime.Now; //发布时间默认现在 if (!string.IsNullOrEmpty(codecPrivateData))
streamSpec.Extension = "m4s"; {
streamSpec.OriginalUrl = ParserConfig.OriginalUrl; streamSpec.Playlist.MediaInit.Index = -1; // 便于排序
streamSpec.PeriodId = indexStr; streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}";
streamSpec.Playlist = new Playlist(); }
streamSpec.Playlist.IsLive = isLive;
streamSpec.Playlist.MediaParts.Add(new MediaPart());
streamSpec.GroupId = name ?? indexStr;
streamSpec.Bandwidth = bitrate;
streamSpec.Codecs = ParseCodecs(fourCC, codecPrivateData);
streamSpec.Language = language;
streamSpec.Resolution = width == 0 ? null : $"{width}x{height}";
streamSpec.Url = IsmUrl;
streamSpec.Channels = channels;
streamSpec.MediaType = type switch
{
"text" => MediaType.SUBTITLES,
"audio" => MediaType.AUDIO,
_ => null
};
streamSpec.Playlist.MediaInit = new MediaSegment(); var currentTime = 0L;
if (!string.IsNullOrEmpty(codecPrivateData)) var segIndex = 0;
var varDic = new Dictionary<string, object?>();
varDic[MSSTags.Bitrate] = bitrate;
foreach (var c in cElements)
{
// 每个C元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration)
var _startTimeStr = c.Attribute("t")?.Value;
var _durationStr = c.Attribute("d")?.Value;
var _repeatCountStr = c.Attribute("r")?.Value;
if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr);
var _duration = Convert.ToInt64(_durationStr);
var _repeatCount = Convert.ToInt64(_repeatCountStr);
if (_repeatCount > 0)
{ {
streamSpec.Playlist.MediaInit.Index = -1; //便于排序 // This value is one-based. (A value of 2 means two fragments in the contiguous series).
streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}"; _repeatCount -= 1;
} }
var currentTime = 0L; varDic[MSSTags.StartTime] = currentTime;
var segIndex = 0; var oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
var varDic = new Dictionary<string, object?>(); var mediaUrl = ParserUtil.ReplaceVars(oriUrl, varDic);
varDic[MSSTags.Bitrate] = bitrate; MediaSegment mediaSegment = new();
mediaSegment.Url = mediaUrl;
foreach (var c in cElements) if (oriUrl.Contains(MSSTags.StartTime))
mediaSegment.NameFromVar = currentTime.ToString();
mediaSegment.Duration = _duration / (double)timescale;
mediaSegment.Index = segIndex++;
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
if (_repeatCount < 0)
{
// 负数表示一直重复 直到period结束 注意减掉已经加入的1个片段
_repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1;
}
for (long i = 0; i < _repeatCount; i++)
{ {
//每个C元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration)
var _startTimeStr = c.Attribute("t")?.Value;
var _durationStr = c.Attribute("d")?.Value;
var _repeatCountStr = c.Attribute("r")?.Value;
if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr);
var _duration = Convert.ToInt64(_durationStr);
var _repeatCount = Convert.ToInt64(_repeatCountStr);
if (_repeatCount > 0)
{
// This value is one-based. (A value of 2 means two fragments in the contiguous series).
_repeatCount -= 1;
}
varDic[MSSTags.StartTime] = currentTime;
var oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
var mediaUrl = ParserUtil.ReplaceVars(oriUrl, varDic);
MediaSegment mediaSegment = new();
mediaSegment.Url = mediaUrl;
if (oriUrl.Contains(MSSTags.StartTime))
mediaSegment.NameFromVar = currentTime.ToString();
mediaSegment.Duration = _duration / (double)timescale;
mediaSegment.Index = segIndex++;
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
if (_repeatCount < 0)
{
//负数表示一直重复 直到period结束 注意减掉已经加入的1个片段
_repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1;
}
for (long i = 0; i < _repeatCount; i++)
{
currentTime += _duration;
MediaSegment _mediaSegment = new();
varDic[MSSTags.StartTime] = currentTime;
var _oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
var _mediaUrl = ParserUtil.ReplaceVars(_oriUrl, varDic);
_mediaSegment.Url = _mediaUrl;
_mediaSegment.Index = segIndex++;
_mediaSegment.Duration = _duration / (double)timescale;
if (_oriUrl.Contains(MSSTags.StartTime))
_mediaSegment.NameFromVar = currentTime.ToString();
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment);
}
currentTime += _duration; currentTime += _duration;
MediaSegment _mediaSegment = new();
varDic[MSSTags.StartTime] = currentTime;
var _oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
var _mediaUrl = ParserUtil.ReplaceVars(_oriUrl, varDic);
_mediaSegment.Url = _mediaUrl;
_mediaSegment.Index = segIndex++;
_mediaSegment.Duration = _duration / (double)timescale;
if (_oriUrl.Contains(MSSTags.StartTime))
_mediaSegment.NameFromVar = currentTime.ToString();
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment);
} }
currentTime += _duration;
}
//生成MOOV数据 // 生成MOOV数据
if (MSSMoovProcessor.CanHandle(fourCC!)) if (MSSMoovProcessor.CanHandle(fourCC!))
{
streamSpec.MSSData = new MSSData()
{ {
streamSpec.MSSData = new MSSData() FourCC = fourCC!,
CodecPrivateData = codecPrivateData,
Type = type!,
Timesacle = Convert.ToInt32(timeScaleStr),
Duration = Convert.ToInt64(durationStr),
SamplingRate = Convert.ToInt32(samplingRateStr ?? "48000"),
Channels = Convert.ToInt32(channels ?? "2"),
BitsPerSample = Convert.ToInt32(bitsPerSampleStr ?? "16"),
NalUnitLengthField = Convert.ToInt32(nalUnitLengthFieldStr ?? "4"),
IsProtection = isProtection,
ProtectionData = protectionData,
ProtectionSystemID = protectionSystemId,
};
var processor = new MSSMoovProcessor(streamSpec);
var header = processor.GenHeader(); // trackId可能不正确
streamSpec.Playlist!.MediaInit!.Url = $"base64://{Convert.ToBase64String(header)}";
// 为音视频写入加密信息
if (isProtection && type != "text")
{
if (streamSpec.Playlist.MediaInit != null)
{ {
FourCC = fourCC!, streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD;
CodecPrivateData = codecPrivateData,
Type = type!,
Timesacle = Convert.ToInt32(timeScaleStr),
Duration = Convert.ToInt64(durationStr),
SamplingRate = Convert.ToInt32(samplingRateStr ?? "48000"),
Channels = Convert.ToInt32(channels ?? "2"),
BitsPerSample = Convert.ToInt32(bitsPerSampleStr ?? "16"),
NalUnitLengthField = Convert.ToInt32(nalUnitLengthFieldStr ?? "4"),
IsProtection = isProtection,
ProtectionData = protectionData,
ProtectionSystemID = protectionSystemId,
};
var processor = new MSSMoovProcessor(streamSpec);
var header = processor.GenHeader(); //trackId可能不正确
streamSpec.Playlist!.MediaInit!.Url = $"base64://{Convert.ToBase64String(header)}";
//为音视频写入加密信息
if (isProtection && type != "text")
{
if (streamSpec.Playlist.MediaInit != null)
{
streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD;
}
foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)
{
item.EncryptInfo.Method = DEFAULT_METHOD;
}
} }
streamList.Add(streamSpec); foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)
}
else
{
Logger.WarnMarkUp($"[green]{fourCC}[/] not supported! Skiped.");
continue;
}
}
}
//为视频设置默认轨道
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO);
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES);
foreach (var item in streamList)
{
if (!string.IsNullOrEmpty(item.Resolution))
{
if (aL.Any())
{
item.AudioId = aL.First().GroupId;
}
if (sL.Any())
{
item.SubtitleId = sL.First().GroupId;
}
}
}
return streamList;
}
/// <summary>
/// 解析编码
/// </summary>
/// <param name="fourCC"></param>
/// <returns></returns>
private static string? ParseCodecs(string fourCC, string? privateData)
{
if (fourCC == "TTML") return "stpp";
if (string.IsNullOrEmpty(privateData)) return null;
return fourCC switch
{
//AVC视频
"H264" or "X264" or "DAVC" or "AVC1" => ParseAVCCodecs(privateData),
//AAC音频
"AAC" or "AACL" or "AACH" or "AACP" => ParseAACCodecs(fourCC, privateData),
//默认返回fourCC本身
_ => fourCC.ToLower()
};
}
private static string ParseAVCCodecs(string privateData)
{
var result = VCodecsRegex().Match(privateData).Groups[1].Value;
return string.IsNullOrEmpty(result) ? "avc1.4D401E" : $"avc1.{result}";
}
private static string ParseAACCodecs(string fourCC, string privateData)
{
var mpProfile = 2;
if (fourCC == "AACH")
{
mpProfile = 5; // High Efficiency AAC Profile
}
else if (!string.IsNullOrEmpty(privateData))
{
mpProfile = (Convert.ToByte(privateData[..2], 16) & 0xF8) >> 3;
}
return $"mp4a.40.{mpProfile}";
}
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{
//这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs);
}
private async Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
{
for (int i = 0; i < streamSpecs.Count; i++)
{
var playlist = streamSpecs[i].Playlist;
if (playlist != null)
{
if (playlist.MediaInit != null)
{
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
}
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++)
{
var part = playlist.MediaParts[ii];
for (int iii = 0; iii < part.MediaSegments.Count; iii++)
{ {
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url); item.EncryptInfo.Method = DEFAULT_METHOD;
} }
} }
streamList.Add(streamSpec);
} }
} else
}
public string PreProcessUrl(string url)
{
foreach (var p in ParserConfig.UrlProcessors)
{
if (p.CanProcess(ExtractorType, url, ParserConfig))
{ {
url = p.Process(url, ParserConfig); Logger.WarnMarkUp($"[green]{fourCC}[/] not supported! Skiped.");
} }
} }
return url;
} }
public void PreProcessContent() // 为视频设置默认轨道
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO);
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES);
foreach (var item in streamList)
{ {
foreach (var p in ParserConfig.ContentProcessors) if (!string.IsNullOrEmpty(item.Resolution))
{ {
if (p.CanProcess(ExtractorType, IsmContent, ParserConfig)) if (aL.Any())
{ {
IsmContent = p.Process(IsmContent, ParserConfig); item.AudioId = aL.First().GroupId;
}
if (sL.Any())
{
item.SubtitleId = sL.First().GroupId;
} }
} }
} }
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs) return Task.FromResult(streamList);
}
/// <summary>
/// 解析编码
/// </summary>
/// <param name="fourCC"></param>
/// <returns></returns>
private static string? ParseCodecs(string fourCC, string? privateData)
{
if (fourCC == "TTML") return "stpp";
if (string.IsNullOrEmpty(privateData)) return null;
return fourCC switch
{ {
if (streamSpecs.Count == 0) return; // AVC视频
"H264" or "X264" or "DAVC" or "AVC1" => ParseAVCCodecs(privateData),
// AAC音频
"AAC" or "AACL" or "AACH" or "AACP" => ParseAACCodecs(fourCC, privateData),
// 默认返回fourCC本身
_ => fourCC.ToLower()
};
}
var (rawText, url) = ("", ParserConfig.Url); private static string ParseAVCCodecs(string privateData)
try {
var result = VCodecsRegex().Match(privateData).Groups[1].Value;
return string.IsNullOrEmpty(result) ? "avc1.4D401E" : $"avc1.{result}";
}
private static string ParseAACCodecs(string fourCC, string privateData)
{
var mpProfile = 2;
if (fourCC == "AACH")
{
mpProfile = 5; // High Efficiency AAC Profile
}
else if (!string.IsNullOrEmpty(privateData))
{
mpProfile = (Convert.ToByte(privateData[..2], 16) & 0xF8) >> 3;
}
return $"mp4a.40.{mpProfile}";
}
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{
// 这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs);
}
private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
{
for (int i = 0; i < streamSpecs.Count; i++)
{
var playlist = streamSpecs[i].Playlist;
if (playlist != null)
{ {
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.Url, ParserConfig.Headers); if (playlist.MediaInit != null)
{
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
}
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++)
{
var part = playlist.MediaParts[ii];
for (int iii = 0; iii < part.MediaSegments.Count; iii++)
{
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url);
}
}
} }
catch (HttpRequestException) when (ParserConfig.Url != ParserConfig.OriginalUrl) }
return Task.CompletedTask;
}
public string PreProcessUrl(string url)
{
foreach (var p in ParserConfig.UrlProcessors)
{
if (p.CanProcess(ExtractorType, url, ParserConfig))
{ {
//当URL无法访问时再请求原始URL url = p.Process(url, ParserConfig);
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);
} }
}
ParserConfig.Url = url; return url;
SetInitUrl(); }
var newStreams = await ExtractStreamsAsync(rawText); public void PreProcessContent()
foreach (var streamSpec in streamSpecs) {
foreach (var p in ParserConfig.ContentProcessors)
{
if (p.CanProcess(ExtractorType, IsmContent, ParserConfig))
{ {
//有的网站每次请求MPD返回的码率不一致导致ToShortString()无法匹配 无法更新playlist IsmContent = p.Process(IsmContent, ParserConfig);
//故增加通过init url来匹配 (如果有的话)
var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());
if (!match.Any())
match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);
if (match.Any())
streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; //不更新init
} }
//这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs);
} }
} }
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
{
if (streamSpecs.Count == 0) return;
var (rawText, url) = ("", ParserConfig.Url);
try
{
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.Url, ParserConfig.Headers);
}
catch (HttpRequestException) when (ParserConfig.Url != ParserConfig.OriginalUrl)
{
// 当URL无法访问时再请求原始URL
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);
}
ParserConfig.Url = url;
SetInitUrl();
var newStreams = await ExtractStreamsAsync(rawText);
foreach (var streamSpec in streamSpecs)
{
// 有的网站每次请求MPD返回的码率不一致导致ToShortString()无法匹配 无法更新playlist
// 故增加通过init url来匹配 (如果有的话)
var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());
if (!match.Any())
match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);
if (match.Any())
streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; // 不更新init
}
// 这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs);
}
} }

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser
{ {
//make BinaryReader in Big Endian // make BinaryReader in Big Endian
class BinaryReader2 : BinaryReader class BinaryReader2 : BinaryReader
{ {
public BinaryReader2(System.IO.Stream stream) : base(stream) { } public BinaryReader2(System.IO.Stream stream) : base(stream) { }

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser
{ {
//make BinaryWriter in Big Endian // make BinaryWriter in Big Endian
class BinaryWriter2 : BinaryWriter class BinaryWriter2 : BinaryWriter
{ {
private static bool IsLittleEndian = BitConverter.IsLittleEndian; private static bool IsLittleEndian = BitConverter.IsLittleEndian;

View File

@ -20,7 +20,7 @@ namespace Mp4SubtitleParser
{ {
var info = new ParsedMP4Info(); var info = new ParsedMP4Info();
//parse init // parse init
new MP4Parser() new MP4Parser()
.Box("moov", MP4Parser.Children) .Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children) .Box("trak", MP4Parser.Children)
@ -56,7 +56,7 @@ namespace Mp4SubtitleParser
private static void ReadBox(byte[] data, ParsedMP4Info info) private static void ReadBox(byte[] data, ParsedMP4Info info)
{ {
//find schm // find schm
var schmBytes = new byte[4] { 0x73, 0x63, 0x68, 0x6d }; var schmBytes = new byte[4] { 0x73, 0x63, 0x68, 0x6d };
var schmIndex = 0; var schmIndex = 0;
for (int i = 0; i < data.Length - 4; i++) for (int i = 0; i < data.Length - 4; i++)
@ -72,9 +72,9 @@ namespace Mp4SubtitleParser
info.Scheme = System.Text.Encoding.UTF8.GetString(data[schmIndex..][8..12]); info.Scheme = System.Text.Encoding.UTF8.GetString(data[schmIndex..][8..12]);
} }
//if (info.Scheme != "cenc") return; // if (info.Scheme != "cenc") return;
//find KID // find KID
var tencBytes = new byte[4] { 0x74, 0x65, 0x6E, 0x63 }; var tencBytes = new byte[4] { 0x74, 0x65, 0x6E, 0x63 };
var tencIndex = -1; var tencIndex = -1;
for (int i = 0; i < data.Length - 4; i++) for (int i = 0; i < data.Length - 4; i++)

View File

@ -111,7 +111,7 @@ namespace Mp4SubtitleParser
var name = TypeToString(type); var name = TypeToString(type);
var has64BitSize = false; var has64BitSize = false;
//Console.WriteLine($"Parsing MP4 box: {name}"); // Console.WriteLine($"Parsing MP4 box: {name}");
switch (size) switch (size)
{ {
@ -129,8 +129,7 @@ namespace Mp4SubtitleParser
break; break;
} }
BoxHandler boxDefinition = null; this.BoxDefinitions.TryGetValue(type, out BoxHandler? boxDefinition);
this.BoxDefinitions.TryGetValue(type, out boxDefinition);
if (boxDefinition != null) if (boxDefinition != null)
{ {

View File

@ -10,8 +10,8 @@ namespace Mp4SubtitleParser
public string Begin { get; set; } public string Begin { get; set; }
public string End { get; set; } public string End { get; set; }
public string Region { get; set; } public string Region { get; set; }
public List<XmlElement> Contents { get; set; } = new List<XmlElement>(); public List<XmlElement> Contents { get; set; } = new();
public List<string> ContentStrings { get; set; } = new List<string>(); public List<string> ContentStrings { get; set; } = new();
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
@ -43,7 +43,7 @@ namespace Mp4SubtitleParser
{ {
bool sawSTPP = false; bool sawSTPP = false;
//parse init // parse init
new MP4Parser() new MP4Parser()
.Box("moov", MP4Parser.Children) .Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children) .Box("trak", MP4Parser.Children)
@ -85,12 +85,12 @@ namespace Mp4SubtitleParser
return xmlSrc; return xmlSrc;
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr); var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
//Parse <p> label // Parse <p> label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!) foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{ {
var _begin = _p.GetAttribute("begin"); var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end"); var _end = _p.GetAttribute("end");
//Handle namespace // Handle namespace
foreach (XmlAttribute attr in _p.Attributes) foreach (XmlAttribute attr in _p.Attributes)
{ {
if (attr.LocalName == "begin") _begin = attr.Value; if (attr.LocalName == "begin") _begin = attr.Value;
@ -98,8 +98,8 @@ namespace Mp4SubtitleParser
} }
_p.SetAttribute("begin", Add(_begin)); _p.SetAttribute("begin", Add(_begin));
_p.SetAttribute("end", Add(_end)); _p.SetAttribute("end", Add(_end));
//Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}"); // Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
//Console.WriteLine($"{_end} {_p.GetAttribute("begin")}"); // Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
} }
return xmlDoc.OuterXml; return xmlDoc.OuterXml;
@ -135,7 +135,7 @@ namespace Mp4SubtitleParser
public static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L) public static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
{ {
//read ttmls // read ttmls
List<string> xmls = new List<string>(); List<string> xmls = new List<string>();
int segIndex = 0; int segIndex = 0;
foreach (var item in items) foreach (var item in items)
@ -143,7 +143,7 @@ namespace Mp4SubtitleParser
var dataSeg = File.ReadAllBytes(item); var dataSeg = File.ReadAllBytes(item);
var sawMDAT = false; var sawMDAT = false;
//parse media // parse media
new MP4Parser() new MP4Parser()
.Box("mdat", MP4Parser.AllData((data) => .Box("mdat", MP4Parser.AllData((data) =>
{ {
@ -161,10 +161,7 @@ namespace Mp4SubtitleParser
else else
{ {
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data)); var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
foreach (var item in datas) xmls.AddRange(datas);
{
xmls.Add(item);
}
} }
})) }))
.Parse(dataSeg,/* partialOkay= */ false); .Parse(dataSeg,/* partialOkay= */ false);
@ -181,7 +178,7 @@ namespace Mp4SubtitleParser
public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L) public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
{ {
//read ttmls // read ttmls
List<string> xmls = new List<string>(); List<string> xmls = new List<string>();
int segIndex = 0; int segIndex = 0;
foreach (var item in items) foreach (var item in items)
@ -203,7 +200,7 @@ namespace Mp4SubtitleParser
private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp) private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp)
{ {
//parsing // parsing
var xmlDoc = new XmlDocument(); var xmlDoc = new XmlDocument();
var finalSubs = new List<SubEntity>(); var finalSubs = new List<SubEntity>();
XmlNode? headNode = null; XmlNode? headNode = null;
@ -215,7 +212,7 @@ namespace Mp4SubtitleParser
var xmlContent = item; var xmlContent = item;
if (!xmlContent.Contains("<tt")) continue; if (!xmlContent.Contains("<tt")) continue;
//fix non-standard xml // fix non-standard xml
var xmlContentFix = xmlContent; var xmlContentFix = xmlContent;
if (regex.IsMatch(xmlContent)) if (regex.IsMatch(xmlContent))
{ {
@ -256,8 +253,8 @@ namespace Mp4SubtitleParser
continue; continue;
//PNG Subs // PNG Subs
var imageDic = new Dictionary<string, string>(); //id, Base64 var imageDic = new Dictionary<string, string>(); // id, Base64
if (ImageRegex().IsMatch(xmlDoc.InnerXml)) if (ImageRegex().IsMatch(xmlDoc.InnerXml))
{ {
foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml)) foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))
@ -266,7 +263,7 @@ namespace Mp4SubtitleParser
} }
} }
//convert <div> to <p> // convert <div> to <p>
if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0) if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0)
{ {
foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!) foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!)
@ -277,14 +274,14 @@ namespace Mp4SubtitleParser
} }
} }
//Parse <p> label // Parse <p> label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!) foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{ {
var _begin = _p.GetAttribute("begin"); var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end"); var _end = _p.GetAttribute("end");
var _region = _p.GetAttribute("region"); var _region = _p.GetAttribute("region");
var _bgImg = _p.GetAttribute("smpte:backgroundImage"); var _bgImg = _p.GetAttribute("smpte:backgroundImage");
//Handle namespace // Handle namespace
foreach (XmlAttribute attr in _p.Attributes) foreach (XmlAttribute attr in _p.Attributes)
{ {
if (attr.LocalName == "begin") _begin = attr.Value; if (attr.LocalName == "begin") _begin = attr.Value;
@ -301,7 +298,7 @@ namespace Mp4SubtitleParser
if (string.IsNullOrEmpty(_bgImg)) if (string.IsNullOrEmpty(_bgImg))
{ {
var _spans = _p.ChildNodes; var _spans = _p.ChildNodes;
//Collect <span> // Collect <span>
foreach (XmlNode _node in _spans) foreach (XmlNode _node in _spans)
{ {
if (_node.NodeType == XmlNodeType.Element) if (_node.NodeType == XmlNodeType.Element)
@ -333,12 +330,12 @@ namespace Mp4SubtitleParser
} }
} }
//Check if one <p> has been splitted // Check if one <p> has been splitted
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings)); var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
//Skip empty lines // Skip empty lines
if (sub.ContentStrings.Count > 0) if (sub.ContentStrings.Count > 0)
{ {
//Extend <p> duration // Extend <p> duration
if (index != -1) if (index != -1)
finalSubs[index].End = sub.End; finalSubs[index].End = sub.End;
else if (!finalSubs.Contains(sub)) else if (!finalSubs.Contains(sub))
@ -372,7 +369,7 @@ namespace Mp4SubtitleParser
} }
StringBuilder vtt = new StringBuilder(); var vtt = new StringBuilder();
vtt.AppendLine("WEBVTT"); vtt.AppendLine("WEBVTT");
foreach (var item in dic) foreach (var item in dic)
{ {

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -6,11 +6,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public abstract class ContentProcessor
{ {
public abstract class ContentProcessor public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig);
{ public abstract string Process(string rawText, ParserConfig parserConfig);
public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig);
public abstract string Process(string rawText, ParserConfig parserConfig);
}
} }

View File

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

View File

@ -9,38 +9,37 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public class DefaultUrlProcessor : UrlProcessor
{ {
public class DefaultUrlProcessor : UrlProcessor public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams;
public override string Process(string oriUrl, ParserConfig paserConfig)
{ {
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams; if (oriUrl.StartsWith("http"))
public override string Process(string oriUrl, ParserConfig paserConfig)
{ {
if (oriUrl.StartsWith("http")) var uriFromConfig = new Uri(paserConfig.Url);
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
var oldUri = new Uri(oriUrl);
var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
foreach (var item in uriFromConfigQuery.AllKeys)
{ {
var uriFromConfig = new Uri(paserConfig.Url); if (newQuery.AllKeys.Contains(item))
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query); newQuery.Set(item, uriFromConfigQuery.Get(item));
else
var oldUri = new Uri(oriUrl); newQuery.Add(item, uriFromConfigQuery.Get(item));
var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
foreach (var item in uriFromConfigQuery.AllKeys)
{
if (newQuery.AllKeys.Contains(item))
newQuery.Set(item, uriFromConfigQuery.Get(item));
else
newQuery.Add(item, uriFromConfigQuery.Get(item));
}
if (!string.IsNullOrEmpty(newQuery.ToString()))
{
Logger.Debug("Before: " + oriUrl);
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery.ToString()).TrimEnd('?');
Logger.Debug("After: " + oriUrl);
}
} }
return oriUrl; if (!string.IsNullOrEmpty(newQuery.ToString()))
{
Logger.Debug("Before: " + oriUrl);
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery.ToString()).TrimEnd('?');
Logger.Debug("After: " + oriUrl);
}
} }
return oriUrl;
} }
} }

View File

@ -8,101 +8,94 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS namespace N_m3u8DL_RE.Parser.Processor.HLS;
public partial class DefaultHLSContentProcessor : ContentProcessor
{ {
public partial class DefaultHLSContentProcessor : ContentProcessor [GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")]
private static partial Regex YkDVRegex();
[GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPRegex();
[GeneratedRegex("#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPSubRegex();
[GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
private static partial Regex OrderFixRegex();
[GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")]
private static partial Regex ATVRegex();
[GeneratedRegex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
private static partial Regex ATVRegex2();
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
public override string Process(string m3u8Content, ParserConfig parserConfig)
{ {
[GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")] // 处理content以\r作为换行符的情况
private static partial Regex YkDVRegex(); if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n"))
[GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPRegex();
[GeneratedRegex("#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPSubRegex();
[GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
private static partial Regex OrderFixRegex();
[GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")]
private static partial Regex ATVRegex();
[GeneratedRegex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
private static partial Regex ATVRegex2();
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
public override string Process(string m3u8Content, ParserConfig parserConfig)
{ {
//处理content以\r作为换行符的情况 m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n"))
{
m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
}
var m3u8Url = parserConfig.Url;
//央视频回放
if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime="))
{
m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
}
//IMOOC
if (m3u8Url.Contains("imooc.com/"))
{
//M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
}
//iqy
if (m3u8Content.StartsWith("{\"payload\""))
{
//
}
//针对优酷#EXT-X-VERSION:7杜比视界片源修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
{
Regex ykmap = YkDVRegex();
foreach (Match m in ykmap.Matches(m3u8Content))
{
m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
}
}
//针对Disney+修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
{
Regex ykmap = DNSPRegex();
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
}
}
//针对Disney+字幕修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/"))
{
Regex ykmap = DNSPSubRegex();
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/") || ATVRegex().IsMatch(m3u8Content)))
{
//只取加密部分即可
Regex ykmap = ATVRegex2();
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
}
}
//修复#EXT-X-KEY与#EXTINF出现次序异常问题
var regex = OrderFixRegex();
if (regex.IsMatch(m3u8Content))
{
m3u8Content = regex.Replace(m3u8Content, "$3$2$1");
}
return m3u8Content;
} }
var m3u8Url = parserConfig.Url;
// YSP回放
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);
}
// 针对YK #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 = YkDVRegex();
foreach (Match m in ykmap.Matches(m3u8Content))
{
m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
}
}
// 针对Disney+修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
{
Regex ykmap = DNSPRegex();
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
}
}
// 针对Disney+字幕修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/"))
{
Regex ykmap = DNSPSubRegex();
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/") || ATVRegex().IsMatch(m3u8Content)))
{
// 只取加密部分即可
Regex ykmap = ATVRegex2();
if (ykmap.IsMatch(m3u8Content))
{
m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
}
}
// 修复#EXT-X-KEY与#EXTINF出现次序异常问题
var regex = OrderFixRegex();
if (regex.IsMatch(m3u8Content))
{
m3u8Content = regex.Replace(m3u8Content, "$3$2$1");
}
return m3u8Content;
} }
} }

View File

@ -6,112 +6,106 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Util;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS namespace N_m3u8DL_RE.Parser.Processor.HLS;
public class DefaultHLSKeyProcessor : KeyProcessor
{ {
public class DefaultHLSKeyProcessor : KeyProcessor public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS;
public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
{ {
public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS; var iv = ParserUtil.GetAttribute(keyLine, "IV");
var method = ParserUtil.GetAttribute(keyLine, "METHOD");
var uri = ParserUtil.GetAttribute(keyLine, "URI");
Logger.Debug("METHOD:{},URI:{},IV:{}", method, uri, iv);
public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig) var encryptInfo = new EncryptInfo(method);
// IV
if (!string.IsNullOrEmpty(iv))
{ {
var iv = ParserUtil.GetAttribute(keyLine, "IV"); encryptInfo.IV = HexUtil.HexToBytes(iv);
var method = ParserUtil.GetAttribute(keyLine, "METHOD"); }
var uri = ParserUtil.GetAttribute(keyLine, "URI"); // 自定义IV
if (parserConfig.CustomeIV is { Length: > 0 })
{
encryptInfo.IV = parserConfig.CustomeIV;
}
Logger.Debug("METHOD:{},URI:{},IV:{}", method, uri, iv); // KEY
try
var encryptInfo = new EncryptInfo(method); {
if (parserConfig.CustomeKey is { Length: > 0 })
//IV
if (!string.IsNullOrEmpty(iv))
{ {
encryptInfo.IV = HexUtil.HexToBytes(iv); encryptInfo.Key = parserConfig.CustomeKey;
} }
//自定义IV else if (uri.ToLower().StartsWith("base64:"))
if (parserConfig.CustomeIV != null && parserConfig.CustomeIV.Length > 0)
{ {
encryptInfo.IV = parserConfig.CustomeIV; encryptInfo.Key = Convert.FromBase64String(uri[7..]);
} }
else if (uri.ToLower().StartsWith("data:;base64,"))
//KEY
try
{ {
if (parserConfig.CustomeKey != null && parserConfig.CustomeKey.Length > 0) encryptInfo.Key = Convert.FromBase64String(uri[13..]);
{ }
encryptInfo.Key = parserConfig.CustomeKey; else if (uri.ToLower().StartsWith("data:text/plain;base64,"))
} {
else if (uri.ToLower().StartsWith("base64:")) encryptInfo.Key = Convert.FromBase64String(uri[23..]);
{ }
encryptInfo.Key = Convert.FromBase64String(uri[7..]); else if (File.Exists(uri))
} {
else if (uri.ToLower().StartsWith("data:;base64,")) encryptInfo.Key = File.ReadAllBytes(uri);
{ }
encryptInfo.Key = Convert.FromBase64String(uri[13..]); else if (!string.IsNullOrEmpty(uri))
} {
else if (uri.ToLower().StartsWith("data:text/plain;base64,")) var retryCount = parserConfig.KeyRetryCount;
{ var segUrl = PreProcessUrl(ParserUtil.CombineURL(m3u8Url, uri), parserConfig);
encryptInfo.Key = Convert.FromBase64String(uri[23..]);
}
else if (File.Exists(uri))
{
encryptInfo.Key = File.ReadAllBytes(uri);
}
else if (!string.IsNullOrEmpty(uri))
{
var retryCount = parserConfig.KeyRetryCount;
var segUrl = PreProcessUrl(ParserUtil.CombineURL(m3u8Url, uri), parserConfig);
getHttpKey: getHttpKey:
try try
{
var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;
encryptInfo.Key = bytes;
}
catch (Exception _ex) when (!_ex.Message.Contains("scheme is not supported."))
{
Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
Thread.Sleep(1000);
if (retryCount-- > 0) goto getHttpKey;
else throw;
}
}
}
catch (Exception ex)
{
Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message);
encryptInfo.Method = EncryptMethod.UNKNOWN;
}
//处理自定义加密方式
if (parserConfig.CustomMethod != null)
{
encryptInfo.Method = parserConfig.CustomMethod.Value;
Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
}
return encryptInfo;
}
/// <summary>
/// 预处理URL
/// </summary>
private string PreProcessUrl(string url, ParserConfig parserConfig)
{
foreach (var p in parserConfig.UrlProcessors)
{
if (p.CanProcess(ExtractorType.HLS, url, parserConfig))
{ {
url = p.Process(url, parserConfig); var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;
encryptInfo.Key = bytes;
}
catch (Exception _ex) when (!_ex.Message.Contains("scheme is not supported."))
{
Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
Thread.Sleep(1000);
if (retryCount-- > 0) goto getHttpKey;
throw;
} }
} }
return url;
} }
catch (Exception ex)
{
Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message);
encryptInfo.Method = EncryptMethod.UNKNOWN;
}
// 处理自定义加密方式
if (parserConfig.CustomMethod != null)
{
encryptInfo.Method = parserConfig.CustomMethod.Value;
Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
}
return encryptInfo;
}
/// <summary>
/// 预处理URL
/// </summary>
private string PreProcessUrl(string url, ParserConfig parserConfig)
{
foreach (var p in parserConfig.UrlProcessors)
{
if (p.CanProcess(ExtractorType.HLS, url, parserConfig))
{
url = p.Process(url, parserConfig);
}
}
return url;
} }
} }

View File

@ -7,11 +7,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public abstract class KeyProcessor
{ {
public abstract class KeyProcessor public abstract bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
{ public abstract EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
public abstract bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
public abstract EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
}
} }

View File

@ -1,16 +1,10 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public abstract class UrlProcessor
{ {
public abstract class UrlProcessor public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig);
{ public abstract string Process(string oriUrl, ParserConfig parserConfig);
public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig);
public abstract string Process(string oriUrl, ParserConfig parserConfig);
}
} }

View File

@ -6,144 +6,142 @@ using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Extractor; using N_m3u8DL_RE.Parser.Extractor;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using Spectre.Console;
namespace N_m3u8DL_RE.Parser namespace N_m3u8DL_RE.Parser;
public class StreamExtractor
{ {
public class StreamExtractor public ExtractorType ExtractorType => extractor.ExtractorType;
private IExtractor extractor;
private ParserConfig parserConfig = new();
private string rawText;
private static SemaphoreSlim semaphore = new(1, 1);
public Dictionary<string, string> RawFiles { get; set; } = new(); // 存储(文件名,文件内容)
public StreamExtractor()
{ {
public ExtractorType ExtractorType { get => extractor.ExtractorType; }
private IExtractor extractor;
private ParserConfig parserConfig = new ParserConfig();
private string rawText;
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
public Dictionary<string, string> RawFiles { get; set; } = new(); //存储(文件名,文件内容) }
public StreamExtractor() public StreamExtractor(ParserConfig parserConfig)
{
this.parserConfig = parserConfig;
}
public async Task LoadSourceFromUrlAsync(string url)
{
Logger.Info(ResString.loadingUrl + url);
if (url.StartsWith("file:"))
{ {
var uri = new Uri(url);
this.rawText = await File.ReadAllTextAsync(uri.LocalPath);
parserConfig.OriginalUrl = parserConfig.Url = url;
}
else if (url.StartsWith("http"))
{
parserConfig.OriginalUrl = url;
(this.rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers);
parserConfig.Url = url;
}
else if (File.Exists(url))
{
url = Path.GetFullPath(url);
this.rawText = await File.ReadAllTextAsync(url);
parserConfig.OriginalUrl = parserConfig.Url = new Uri(url).AbsoluteUri;
}
this.rawText = rawText.Trim();
LoadSourceFromText(this.rawText);
}
public void LoadSourceFromText(string rawText)
{
var rawType = "txt";
rawText = rawText.Trim();
this.rawText = rawText;
if (rawText.StartsWith(HLSTags.ext_m3u))
{
Logger.InfoMarkUp(ResString.matchHLS);
extractor = new HLSExtractor(parserConfig);
rawType = "m3u8";
}
else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD"))
{
Logger.InfoMarkUp(ResString.matchDASH);
// extractor = new DASHExtractor(parserConfig);
extractor = new DASHExtractor2(parserConfig);
rawType = "mpd";
}
else if (rawText.Contains("</SmoothStreamingMedia>") && rawText.Contains("<SmoothStreamingMedia"))
{
Logger.InfoMarkUp(ResString.matchMSS);
// extractor = new DASHExtractor(parserConfig);
extractor = new MSSExtractor(parserConfig);
rawType = "ism";
}
else if (rawText == ResString.ReLiveTs)
{
Logger.InfoMarkUp(ResString.matchTS);
extractor = new LiveTSExtractor(parserConfig);
}
else
{
throw new NotSupportedException(ResString.notSupported);
} }
public StreamExtractor(ParserConfig parserConfig) RawFiles[$"raw.{rawType}"] = rawText;
}
/// <summary>
/// 开始解析流媒体信息
/// </summary>
/// <returns></returns>
public async Task<List<StreamSpec>> ExtractStreamsAsync()
{
try
{ {
this.parserConfig = parserConfig; await semaphore.WaitAsync();
Logger.Info(ResString.parsingStream);
return await extractor.ExtractStreamsAsync(rawText);
} }
finally
public async Task LoadSourceFromUrlAsync(string url)
{ {
Logger.Info(ResString.loadingUrl + url); semaphore.Release();
if (url.StartsWith("file:"))
{
var uri = new Uri(url);
this.rawText = await File.ReadAllTextAsync(uri.LocalPath);
parserConfig.OriginalUrl = parserConfig.Url = url;
}
else if (url.StartsWith("http"))
{
parserConfig.OriginalUrl = url;
(this.rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers);
parserConfig.Url = url;
}
else if (File.Exists(url))
{
url = Path.GetFullPath(url);
this.rawText = await File.ReadAllTextAsync(url);
parserConfig.OriginalUrl = parserConfig.Url = new Uri(url).AbsoluteUri;
}
this.rawText = rawText.Trim();
LoadSourceFromText(this.rawText);
} }
}
public void LoadSourceFromText(string rawText) /// <summary>
/// 根据规格说明填充媒体播放列表信息
/// </summary>
/// <param name="streamSpecs"></param>
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{
try
{ {
var rawType = "txt"; await semaphore.WaitAsync();
rawText = rawText.Trim(); Logger.Info(ResString.parsingStream);
this.rawText = rawText; await extractor.FetchPlayListAsync(streamSpecs);
if (rawText.StartsWith(HLSTags.ext_m3u))
{
Logger.InfoMarkUp(ResString.matchHLS);
extractor = new HLSExtractor(parserConfig);
rawType = "m3u8";
}
else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD"))
{
Logger.InfoMarkUp(ResString.matchDASH);
//extractor = new DASHExtractor(parserConfig);
extractor = new DASHExtractor2(parserConfig);
rawType = "mpd";
}
else if (rawText.Contains("</SmoothStreamingMedia>") && rawText.Contains("<SmoothStreamingMedia"))
{
Logger.InfoMarkUp(ResString.matchMSS);
//extractor = new DASHExtractor(parserConfig);
extractor = new MSSExtractor(parserConfig);
rawType = "ism";
}
else if (rawText == ResString.ReLiveTs)
{
Logger.InfoMarkUp(ResString.matchTS);
extractor = new LiveTSExtractor(parserConfig);
}
else
{
throw new NotSupportedException(ResString.notSupported);
}
RawFiles[$"raw.{rawType}"] = rawText;
} }
finally
/// <summary>
/// 开始解析流媒体信息
/// </summary>
/// <returns></returns>
public async Task<List<StreamSpec>> ExtractStreamsAsync()
{ {
try semaphore.Release();
{
await semaphore.WaitAsync();
Logger.Info(ResString.parsingStream);
return await extractor.ExtractStreamsAsync(rawText);
}
finally
{
semaphore.Release();
}
} }
}
/// <summary> public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
/// 根据规格说明填充媒体播放列表信息 {
/// </summary> try
/// <param name="streamSpecs"></param>
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{ {
try await semaphore.WaitAsync();
await RetryUtil.WebRequestRetryAsync(async () =>
{ {
await semaphore.WaitAsync(); await extractor.RefreshPlayListAsync(streamSpecs);
Logger.Info(ResString.parsingStream); return true;
await extractor.FetchPlayListAsync(streamSpecs); }, retryDelayMilliseconds: 1000, maxRetries: 5);
}
finally
{
semaphore.Release();
}
} }
finally
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
{ {
try semaphore.Release();
{
await semaphore.WaitAsync();
await RetryUtil.WebRequestRetryAsync(async () =>
{
await extractor.RefreshPlayListAsync(streamSpecs);
return true;
}, retryDelayMilliseconds: 1000, maxRetries: 5);
}
finally
{
semaphore.Release();
}
} }
} }
} }

View File

@ -1,126 +1,120 @@
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Util namespace N_m3u8DL_RE.Parser.Util;
public partial class ParserUtil
{ {
public partial class ParserUtil [GeneratedRegex("\\$Number%([^$]+)d\\$")]
private static partial Regex VarsNumberRegex();
/// <summary>
/// 从以下文本中获取参数
/// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
/// </summary>
/// <param name="line">等待被解析的一行文本</param>
/// <param name="key">留空则获取第一个英文冒号后的全部字符</param>
/// <returns></returns>
public static string GetAttribute(string line, string key = "")
{ {
[GeneratedRegex("\\$Number%([^$]+)d\\$")] line = line.Trim();
private static partial Regex VarsNumberRegex(); if (key == "")
return line[(line.IndexOf(':') + 1)..];
/// <summary> var index = -1;
/// 从以下文本中获取参数 var result = string.Empty;
/// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720" if ((index = line.IndexOf(key + "=\"", StringComparison.Ordinal)) > -1)
/// </summary>
/// <param name="line">等待被解析的一行文本</param>
/// <param name="key">留空则获取第一个英文冒号后的全部字符</param>
/// <returns></returns>
public static string GetAttribute(string line, string key = "")
{ {
line = line.Trim(); var startIndex = index + (key + "=\"").Length;
if (key == "") var endIndex = startIndex + line[startIndex..].IndexOf('\"');
return line[(line.IndexOf(':') + 1)..]; result = line[startIndex..endIndex];
}
var index = -1; else if ((index = line.IndexOf(key + "=", StringComparison.Ordinal)) > -1)
var result = string.Empty; {
if ((index = line.IndexOf(key + "=\"")) > -1) var startIndex = index + (key + "=").Length;
{ var endIndex = startIndex + line[startIndex..].IndexOf(',');
var startIndex = index + (key + "=\"").Length; if (endIndex >= startIndex) result = line[startIndex..endIndex];
var endIndex = startIndex + line[startIndex..].IndexOf('\"'); else result = line[startIndex..];
result = line[startIndex..endIndex];
}
else if ((index = line.IndexOf(key + "=")) > -1)
{
var startIndex = index + (key + "=").Length;
var endIndex = startIndex + line[startIndex..].IndexOf(',');
if (endIndex >= startIndex) result = line[startIndex..endIndex];
else result = line[startIndex..];
}
return result;
} }
/// <summary> return result;
/// 从如下文本中提取 }
/// <n>[@<o>]
/// </summary> /// <summary>
/// <param name="input"></param> /// 从如下文本中提取
/// <returns>n(length) o(start)</returns> /// <n>[@<o>]
public static (long, long?) GetRange(string input) /// </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)
{ {
var t = input.Split('@'); if (t.Length == 1)
if (t.Length > 0)
{ {
if (t.Length == 1) return (Convert.ToInt64(t[0]), null);
{
return (Convert.ToInt64(t[0]), null);
}
if (t.Length == 2)
{
return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
}
} }
return (0, null); if (t.Length == 2)
}
/// <summary>
/// 从100-300这种字符串中获取StartRange, ExpectLength信息
/// </summary>
/// <param name="range"></param>
/// <returns>StartRange, ExpectLength</returns>
public static (long, long) ParseRange(string range)
{
var start = Convert.ToInt64(range.Split('-')[0]);
var end = Convert.ToInt64(range.Split('-')[1]);
return (start, end - start + 1);
}
/// <summary>
/// MPD SegmentTemplate替换
/// </summary>
/// <param name="text"></param>
/// <param name="keyValuePairs"></param>
/// <returns></returns>
public static string ReplaceVars(string text, Dictionary<string, object?> keyValuePairs)
{
foreach (var item in keyValuePairs)
if (text.Contains(item.Key))
text = text.Replace(item.Key, item.Value!.ToString());
//处理特殊形式数字 如 $Number%05d$
var regex = VarsNumberRegex();
if (regex.IsMatch(text) && keyValuePairs.ContainsKey(DASHTags.TemplateNumber))
{ {
foreach (Match m in regex.Matches(text)) return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
{
text = text.Replace(m.Value, keyValuePairs[DASHTags.TemplateNumber]?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));
}
} }
}
return (0, null);
}
return text; /// <summary>
/// 从100-300这种字符串中获取StartRange, ExpectLength信息
/// </summary>
/// <param name="range"></param>
/// <returns>StartRange, ExpectLength</returns>
public static (long, long) ParseRange(string range)
{
var start = Convert.ToInt64(range.Split('-')[0]);
var end = Convert.ToInt64(range.Split('-')[1]);
return (start, end - start + 1);
}
/// <summary>
/// MPD SegmentTemplate替换
/// </summary>
/// <param name="text"></param>
/// <param name="keyValuePairs"></param>
/// <returns></returns>
public static string ReplaceVars(string text, Dictionary<string, object?> keyValuePairs)
{
foreach (var item in keyValuePairs)
if (text.Contains(item.Key))
text = text.Replace(item.Key, item.Value!.ToString());
// 处理特殊形式数字 如 $Number%05d$
var regex = VarsNumberRegex();
if (regex.IsMatch(text) && keyValuePairs.TryGetValue(DASHTags.TemplateNumber, out var keyValuePair))
{
foreach (Match m in regex.Matches(text))
{
text = text.Replace(m.Value, keyValuePair?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));
}
} }
/// <summary> return text;
/// 拼接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();
/// <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; return url;
}
Uri uri1 = new Uri(baseurl); // 这里直接传完整的URL即可
Uri uri2 = new Uri(uri1, url);
url = uri2.ToString();
return url;
} }
} }

View File

@ -1,66 +1,48 @@
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using N_m3u8DL_RE.Common.Util;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal sealed class DownloadSpeedColumn : ProgressColumn
{ {
internal sealed class DownloadSpeedColumn : ProgressColumn private long _stopSpeed = 0;
private ConcurrentDictionary<int, string> DateTimeStringDic = new();
protected override bool NoWrap => true;
private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
public DownloadSpeedColumn(ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic)
{ {
private long _stopSpeed = 0; this.SpeedContainerDic = SpeedContainerDic;
private ConcurrentDictionary<int, string> DateTimeStringDic = new(); }
protected override bool NoWrap => true;
private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
public DownloadSpeedColumn(ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic) public Style MyStyle { get; set; } = new Style(foreground: Color.Green);
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var taskId = task.Id;
var speedContainer = SpeedContainerDic[taskId];
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var flag = task.IsFinished || !task.IsStarted;
// 单文件下载汇报进度
if (!flag && speedContainer.SingleSegment && speedContainer.ResponseLength != null)
{ {
this.SpeedContainerDic = SpeedContainerDic; task.MaxValue = (double)speedContainer.ResponseLength;
task.Value = speedContainer.RDownloaded;
} }
// 一秒汇报一次即可
public Style MyStyle { get; set; } = new Style(foreground: Color.Green); if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{ {
var taskId = task.Id; speedContainer.NowSpeed = speedContainer.Downloaded;
var speedContainer = SpeedContainerDic[taskId]; // 速度为0计数增加
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
var flag = task.IsFinished || !task.IsStarted; else speedContainer.ResetLowSpeedCount();
//单文件下载汇报进度 speedContainer.Reset();
if (!flag && speedContainer.SingleSegment && speedContainer.ResponseLength != null)
{
task.MaxValue = (double)speedContainer.ResponseLength;
task.Value = speedContainer.RDownloaded;
}
//一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
{
speedContainer.NowSpeed = speedContainer.Downloaded;
//速度为0计数增加
if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
else speedContainer.ResetLowSpeedCount();
speedContainer.Reset();
}
DateTimeStringDic[taskId] = now;
var style = flag ? Style.Plain : MyStyle;
return flag ? new Text("-", style).Centered() : new Text(FormatFileSize(speedContainer.NowSpeed) + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered();
}
private static string FormatFileSize(double fileSize)
{
return fileSize switch
{
< 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),
>= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GBps", (double)fileSize / (1024 * 1024 * 1024)),
>= 1024 * 1024 => string.Format("{0:####0.00}MBps", (double)fileSize / (1024 * 1024)),
>= 1024 => string.Format("{0:####0.00}KBps", (double)fileSize / 1024),
_ => string.Format("{0:####0.00}Bps", fileSize)
};
} }
DateTimeStringDic[taskId] = now;
var style = flag ? Style.Plain : MyStyle;
return flag ? new Text("-", style).Centered() : new Text(GlobalUtil.FormatFileSize(speedContainer.NowSpeed) + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered();
} }
} }

View File

@ -2,48 +2,42 @@
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class DownloadStatusColumn : ProgressColumn
{ {
internal class DownloadStatusColumn : ProgressColumn private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
private ConcurrentDictionary<int, string> DateTimeStringDic = new();
private ConcurrentDictionary<int, string> SizeDic = new();
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
public DownloadStatusColumn(ConcurrentDictionary<int, SpeedContainer> speedContainerDic)
{ {
private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; } this.SpeedContainerDic = speedContainerDic;
private ConcurrentDictionary<int, string> DateTimeStringDic = new(); }
private ConcurrentDictionary<int, string> SizeDic = new();
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
public DownloadStatusColumn(ConcurrentDictionary<int, SpeedContainer> speedContainerDic) public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
if (task.Value == 0) return new Text("-", MyStyle).RightJustified();
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var speedContainer = SpeedContainerDic[task.Id];
var size = speedContainer.RDownloaded;
// 一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
{ {
this.SpeedContainerDic = speedContainerDic; var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
SizeDic[task.Id] = $"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}";
} }
DateTimeStringDic[task.Id] = now;
SizeDic.TryGetValue(task.Id, out var sizeStr);
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);
{
if (task.Value == 0) return new Text("-", MyStyle).RightJustified();
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var speedContainer = SpeedContainerDic[task.Id]; return new Text(sizeStr ?? "-", MyStyle).RightJustified();
var size = speedContainer.RDownloaded;
//一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
{
var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
SizeDic[task.Id] = $"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}";
}
DateTimeStringDic[task.Id] = now;
SizeDic.TryGetValue(task.Id, out var sizeStr);
if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);
return new Text(sizeStr ?? "-", MyStyle).RightJustified();
}
} }
} }

View File

@ -1,31 +1,25 @@
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class MyPercentageColumn : ProgressColumn
{ {
internal class MyPercentageColumn : ProgressColumn /// <summary>
/// Gets or sets the style for a non-complete task.
/// </summary>
public Style Style { get; set; } = Style.Plain;
/// <summary>
/// Gets or sets the style for a completed task.
/// </summary>
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
/// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{ {
/// <summary> var percentage = task.Percentage;
/// Gets or sets the style for a non-complete task. var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
/// </summary> return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
public Style Style { get; set; } = Style.Plain;
/// <summary>
/// Gets or sets the style for a completed task.
/// </summary>
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
/// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var percentage = task.Percentage;
var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
}
} }
} }

View File

@ -1,39 +1,33 @@
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class RecordingDurationColumn : ProgressColumn
{ {
internal class RecordingDurationColumn : ProgressColumn protected override bool NoWrap => true;
private ConcurrentDictionary<int, int> _recodingDurDic;
private ConcurrentDictionary<int, int>? _refreshedDurDic;
public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey);
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen);
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic)
{ {
protected override bool NoWrap => true; _recodingDurDic = recodingDurDic;
private ConcurrentDictionary<int, int> _recodingDurDic; }
private ConcurrentDictionary<int, int>? _refreshedDurDic; public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic, ConcurrentDictionary<int, int> refreshedDurDic)
public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey); {
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen); _recodingDurDic = recodingDurDic;
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic) _refreshedDurDic = refreshedDurDic;
}
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
if (_refreshedDurDic == null)
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
else
{ {
_recodingDurDic = recodingDurDic; return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
}
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic, ConcurrentDictionary<int, int> refreshedDurDic)
{
_recodingDurDic = recodingDurDic;
_refreshedDurDic = refreshedDurDic;
}
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
if (_refreshedDurDic == null)
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
else
{
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
}
} }
} }
} }

View File

@ -1,39 +1,32 @@
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class RecordingSizeColumn : ProgressColumn
{ {
internal class RecordingSizeColumn : ProgressColumn protected override bool NoWrap => true;
private ConcurrentDictionary<int, double> RecodingSizeDic = new(); // 临时的大小 每秒刷新用
private ConcurrentDictionary<int, double> _recodingSizeDic;
private ConcurrentDictionary<int, string> DateTimeStringDic = new();
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
public RecordingSizeColumn(ConcurrentDictionary<int, double> recodingSizeDic)
{ {
protected override bool NoWrap => true; _recodingSizeDic = recodingSizeDic;
private ConcurrentDictionary<int, double> RecodingSizeDic = new(); //临时的大小 每秒刷新用 }
private ConcurrentDictionary<int, double> _recodingSizeDic; public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
private ConcurrentDictionary<int, string> DateTimeStringDic = new(); {
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan); var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
public RecordingSizeColumn(ConcurrentDictionary<int, double> recodingSizeDic) var taskId = task.Id;
// 一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
{ {
_recodingSizeDic = recodingSizeDic; RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
}
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var taskId = task.Id;
//一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
{
RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
}
DateTimeStringDic[taskId] = now;
var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
} }
DateTimeStringDic[taskId] = now;
var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
} }
} }

View File

@ -1,23 +1,17 @@
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class RecordingStatusColumn : ProgressColumn
{ {
internal class RecordingStatusColumn : ProgressColumn protected override bool NoWrap => true;
public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{ {
protected override bool NoWrap => true; if (task.IsFinished)
public Style MyStyle { get; set; } = new Style(foreground: Color.Default); return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified();
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow); return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
if (task.IsFinished)
return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified();
return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,61 +1,56 @@
using System; using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.CommandLine namespace N_m3u8DL_RE.CommandLine;
internal class ComplexParamParser
{ {
internal class ComplexParamParser private string _arg;
public ComplexParamParser(string arg)
{ {
private string _arg; _arg = arg;
public ComplexParamParser(string arg) }
{
_arg = arg;
}
public string? GetValue(string key) public string? GetValue(string key)
{ {
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(_arg)) return null; if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(_arg)) return null;
try try
{
var index = _arg.IndexOf(key + "=");
if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;
var chars = _arg[(index + key.Length + 1)..].ToCharArray();
var result = new StringBuilder();
char last = '\0';
for (int i = 0; i < chars.Length; i++)
{ {
var index = _arg.IndexOf(key + "="); if (chars[i] == ':')
if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;
var chars = _arg[(index + key.Length + 1)..].ToCharArray();
var result = new StringBuilder();
char last = '\0';
for (int i = 0; i < chars.Length; i++)
{ {
if (chars[i] == ':') if (last == '\\')
{
if (last == '\\')
{
result.Replace("\\", "");
last = chars[i];
result.Append(chars[i]);
}
else break;
}
else
{ {
result.Replace("\\", "");
last = chars[i]; last = chars[i];
result.Append(chars[i]); result.Append(chars[i]);
} }
else break;
}
else
{
last = chars[i];
result.Append(chars[i]);
} }
var resultStr = result.ToString().Trim().Trim('\"').Trim('\'');
//不应该有引号出现
if (resultStr.Contains('\"') || resultStr.Contains('\'')) throw new Exception();
return resultStr;
}
catch (Exception)
{
throw new ArgumentException($"Parse Argument [{key}] failed!");
} }
var resultStr = result.ToString().Trim().Trim('\"').Trim('\'');
// 不应该有引号出现
if (resultStr.Contains('\"') || resultStr.Contains('\'')) throw new Exception();
return resultStr;
}
catch (Exception)
{
throw new ArgumentException($"Parse Argument [{key}] failed!");
} }
} }
} }

View File

@ -4,255 +4,254 @@ using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Enum; using N_m3u8DL_RE.Enum;
using System.Net; using System.Net;
namespace N_m3u8DL_RE.CommandLine namespace N_m3u8DL_RE.CommandLine;
internal class MyOption
{ {
internal class MyOption /// <summary>
{ /// See: <see cref="CommandInvoker.Input"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.Input"/>. public string Input { get; set; } = default!;
/// </summary> /// <summary>
public string Input { get; set; } = default!; /// See: <see cref="CommandInvoker.Headers"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.Headers"/>. public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
/// </summary> /// <summary>
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); /// See: <see cref="CommandInvoker.AdKeywords"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.AdKeywords"/>. public string[]? AdKeywords { get; set; }
/// </summary> /// <summary>
public string[]? AdKeywords { get; set; } /// See: <see cref="CommandInvoker.MaxSpeed"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.MaxSpeed"/>. public long? MaxSpeed { get; set; }
/// </summary> /// <summary>
public long? MaxSpeed { get; set; } /// See: <see cref="CommandInvoker.Keys"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.Keys"/>. public string[]? Keys { get; set; }
/// </summary> /// <summary>
public string[]? Keys { get; set; } /// See: <see cref="CommandInvoker.BaseUrl"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.BaseUrl"/>. public string? BaseUrl { get; set; }
/// </summary> /// <summary>
public string? BaseUrl { get; set; } /// See: <see cref="CommandInvoker.KeyTextFile"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.KeyTextFile"/>. public string? KeyTextFile { get; set; }
/// </summary> /// <summary>
public string? KeyTextFile { get; set; } /// See: <see cref="CommandInvoker.UrlProcessorArgs"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.UrlProcessorArgs"/>. public string? UrlProcessorArgs { get; set; }
/// </summary> /// <summary>
public string? UrlProcessorArgs { get; set; } /// See: <see cref="CommandInvoker.LogLevel"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LogLevel"/>. public LogLevel LogLevel { get; set; }
/// </summary> /// <summary>
public LogLevel LogLevel { get; set; } /// See: <see cref="CommandInvoker.NoDateInfo"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.NoDateInfo"/>. public bool NoDateInfo { get; set; }
/// </summary> /// <summary>
public bool NoDateInfo { get; set; } /// See: <see cref="CommandInvoker.NoLog"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.NoLog"/>. public bool NoLog { get; set; }
/// </summary> /// <summary>
public bool NoLog { get; set; } /// See: <see cref="CommandInvoker.AutoSelect"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.AutoSelect"/>. public bool AutoSelect { get; set; }
/// </summary> /// <summary>
public bool AutoSelect { get; set; } /// See: <see cref="CommandInvoker.SubOnly"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SubOnly"/>. public bool SubOnly { get; set; }
/// </summary> /// <summary>
public bool SubOnly { get; set; } /// See: <see cref="CommandInvoker.ThreadCount"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.ThreadCount"/>. public int ThreadCount { get; set; }
/// </summary> /// <summary>
public int ThreadCount { get; set; } /// See: <see cref="CommandInvoker.DownloadRetryCount"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.DownloadRetryCount"/>. public int DownloadRetryCount { get; set; }
/// </summary> /// <summary>
public int DownloadRetryCount { get; set; } /// See: <see cref="CommandInvoker.LiveRecordLimit"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LiveRecordLimit"/>. public TimeSpan? LiveRecordLimit { get; set; }
/// </summary> /// <summary>
public TimeSpan? LiveRecordLimit { get; set; } /// See: <see cref="CommandInvoker.TaskStartAt"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.TaskStartAt"/>. public DateTime? TaskStartAt { get; set; }
/// </summary> /// <summary>
public DateTime? TaskStartAt { get; set; } /// See: <see cref="CommandInvoker.SkipMerge"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SkipMerge"/>. public bool SkipMerge { get; set; }
/// </summary> /// <summary>
public bool SkipMerge { get; set; } /// See: <see cref="CommandInvoker.BinaryMerge"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.BinaryMerge"/>. public bool BinaryMerge { get; set; }
/// </summary> /// <summary>
public bool BinaryMerge { get; set; } /// See: <see cref="CommandInvoker.ForceAnsiConsole"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.ForceAnsiConsole"/>. public bool ForceAnsiConsole { get; set; }
/// </summary> /// <summary>
public bool ForceAnsiConsole { get; set; } /// See: <see cref="CommandInvoker.NoAnsiColor"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.NoAnsiColor"/>. public bool NoAnsiColor { get; set; }
/// </summary> /// <summary>
public bool NoAnsiColor { get; set; } /// See: <see cref="CommandInvoker.UseFFmpegConcatDemuxer"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.UseFFmpegConcatDemuxer"/>. public bool UseFFmpegConcatDemuxer { get; set; }
/// </summary> /// <summary>
public bool UseFFmpegConcatDemuxer { get; set; } /// See: <see cref="CommandInvoker.DelAfterDone"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.DelAfterDone"/>. public bool DelAfterDone { get; set; }
/// </summary> /// <summary>
public bool DelAfterDone { get; set; } /// See: <see cref="CommandInvoker.AutoSubtitleFix"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.AutoSubtitleFix"/>. public bool AutoSubtitleFix { get; set; }
/// </summary> /// <summary>
public bool AutoSubtitleFix { get; set; } /// See: <see cref="CommandInvoker.CheckSegmentsCount"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.CheckSegmentsCount"/>. public bool CheckSegmentsCount { get; set; }
/// </summary> /// <summary>
public bool CheckSegmentsCount { get; set; } /// See: <see cref="CommandInvoker.SkipDownload"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SkipDownload"/>. public bool SkipDownload { get; set; }
/// </summary> /// <summary>
public bool SkipDownload { get; set; } /// See: <see cref="CommandInvoker.WriteMetaJson"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.WriteMetaJson"/>. public bool WriteMetaJson { get; set; }
/// </summary> /// <summary>
public bool WriteMetaJson { get; set; } /// See: <see cref="CommandInvoker.AppendUrlParams"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.AppendUrlParams"/>. public bool AppendUrlParams { get; set; }
/// </summary> /// <summary>
public bool AppendUrlParams { get; set; } /// See: <see cref="CommandInvoker.MP4RealTimeDecryption"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.MP4RealTimeDecryption"/>. public bool MP4RealTimeDecryption { get; set; }
/// </summary> /// <summary>
public bool MP4RealTimeDecryption { get; set; } /// See: <see cref="CommandInvoker.UseShakaPackager"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.UseShakaPackager"/>. public bool UseShakaPackager { get; set; }
/// </summary> /// <summary>
public bool UseShakaPackager { get; set; } /// See: <see cref="CommandInvoker.MuxAfterDone"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.MuxAfterDone"/>. public bool MuxAfterDone { get; set; }
/// </summary> /// <summary>
public bool MuxAfterDone { get; set; } /// See: <see cref="CommandInvoker.ConcurrentDownload"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.ConcurrentDownload"/>. public bool ConcurrentDownload { get; set; }
/// </summary> /// <summary>
public bool ConcurrentDownload { get; set; } /// See: <see cref="CommandInvoker.LiveRealTimeMerge"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LiveRealTimeMerge"/>. public bool LiveRealTimeMerge { get; set; }
/// </summary> /// <summary>
public bool LiveRealTimeMerge { get; set; } /// See: <see cref="CommandInvoker.LiveKeepSegments"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LiveKeepSegments"/>. public bool LiveKeepSegments { get; set; }
/// </summary> /// <summary>
public bool LiveKeepSegments { get; set; } /// See: <see cref="CommandInvoker.LivePerformAsVod"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LivePerformAsVod"/>. public bool LivePerformAsVod { get; set; }
/// </summary> /// <summary>
public bool LivePerformAsVod { get; set; } /// See: <see cref="CommandInvoker.UseSystemProxy"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.UseSystemProxy"/>. public bool UseSystemProxy { get; set; }
/// </summary> /// <summary>
public bool UseSystemProxy { get; set; } /// See: <see cref="CommandInvoker.SubtitleFormat"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SubtitleFormat"/>. public SubtitleFormat SubtitleFormat { get; set; }
/// </summary> /// <summary>
public SubtitleFormat SubtitleFormat { get; set; } /// See: <see cref="CommandInvoker.TmpDir"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.TmpDir"/>. public string? TmpDir { get; set; }
/// </summary> /// <summary>
public string? TmpDir { get; set; } /// See: <see cref="CommandInvoker.SaveDir"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SaveDir"/>. public string? SaveDir { get; set; }
/// </summary> /// <summary>
public string? SaveDir { get; set; } /// See: <see cref="CommandInvoker.SaveName"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SaveName"/>. public string? SaveName { get; set; }
/// </summary> /// <summary>
public string? SaveName { get; set; } /// See: <see cref="CommandInvoker.SavePattern"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SavePattern"/>. public string? SavePattern { get; set; }
/// </summary> /// <summary>
public string? SavePattern { get; set; } /// See: <see cref="CommandInvoker.UILanguage"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.UILanguage"/>. public string? UILanguage { get; set; }
/// </summary> /// <summary>
public string? UILanguage { get; set; } /// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>. public string? DecryptionBinaryPath { get; set; }
/// </summary> /// <summary>
public string? DecryptionBinaryPath { get; set; } /// See: <see cref="CommandInvoker.FFmpegBinaryPath"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.FFmpegBinaryPath"/>. public string? FFmpegBinaryPath { get; set; }
/// </summary> /// <summary>
public string? FFmpegBinaryPath { get; set; } /// See: <see cref="CommandInvoker.MkvmergeBinaryPath"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.MkvmergeBinaryPath"/>. public string? MkvmergeBinaryPath { get; set; }
/// </summary> /// <summary>
public string? MkvmergeBinaryPath { get; set; } /// See: <see cref="CommandInvoker.MuxImports"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.MuxImports"/>. public List<OutputFile>? MuxImports { get; set; }
/// </summary> /// <summary>
public List<OutputFile>? MuxImports { get; set; } /// See: <see cref="CommandInvoker.VideoFilter"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.VideoFilter"/>. public StreamFilter? VideoFilter { get; set; }
/// </summary> /// <summary>
public StreamFilter? VideoFilter { get; set; } /// See: <see cref="CommandInvoker.DropVideoFilter"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.DropVideoFilter"/>. public StreamFilter? DropVideoFilter { get; set; }
/// </summary> /// <summary>
public StreamFilter? DropVideoFilter { get; set; } /// See: <see cref="CommandInvoker.AudioFilter"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.AudioFilter"/>. public StreamFilter? AudioFilter { get; set; }
/// </summary> /// <summary>
public StreamFilter? AudioFilter { get; set; } /// See: <see cref="CommandInvoker.DropAudioFilter"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.DropAudioFilter"/>. public StreamFilter? DropAudioFilter { get; set; }
/// </summary> /// <summary>
public StreamFilter? DropAudioFilter { get; set; } /// See: <see cref="CommandInvoker.SubtitleFilter"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.SubtitleFilter"/>. public StreamFilter? SubtitleFilter { get; set; }
/// </summary> /// <summary>
public StreamFilter? SubtitleFilter { get; set; } /// See: <see cref="CommandInvoker.DropSubtitleFilter"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.DropSubtitleFilter"/>. public StreamFilter? DropSubtitleFilter { get; set; }
/// </summary> /// <summary>
public StreamFilter? DropSubtitleFilter { get; set; } /// See: <see cref="CommandInvoker.CustomHLSMethod"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.CustomHLSMethod"/>. public EncryptMethod? CustomHLSMethod { get; set; }
/// </summary> /// <summary>
public EncryptMethod? CustomHLSMethod { get; set; } /// See: <see cref="CommandInvoker.CustomHLSKey"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.CustomHLSKey"/>. public byte[]? CustomHLSKey { get; set; }
/// </summary> /// <summary>
public byte[]? CustomHLSKey { get; set; } /// See: <see cref="CommandInvoker.CustomHLSIv"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.CustomHLSIv"/>. public byte[]? CustomHLSIv { get; set; }
/// </summary> /// <summary>
public byte[]? CustomHLSIv { get; set; } /// See: <see cref="CommandInvoker.CustomProxy"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.CustomProxy"/>. public WebProxy? CustomProxy { get; set; }
/// </summary> /// <summary>
public WebProxy? CustomProxy { get; set; } /// See: <see cref="CommandInvoker.CustomRange"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.CustomRange"/>. public CustomRange? CustomRange { get; set; }
/// </summary> /// <summary>
public CustomRange? CustomRange { get; set; } /// See: <see cref="CommandInvoker.LiveWaitTime"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LiveWaitTime"/>. public int? LiveWaitTime { get; set; }
/// </summary> /// <summary>
public int? LiveWaitTime { get; set; } /// See: <see cref="CommandInvoker.LiveTakeCount"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LiveTakeCount"/>. public int LiveTakeCount { get; set; }
/// </summary> public MuxOptions? MuxOptions { get; set; }
public int LiveTakeCount { get; set; } // public bool LiveWriteHLS { get; set; } = true;
public MuxOptions MuxOptions { get; set; } /// <summary>
//public bool LiveWriteHLS { get; set; } = true; /// See: <see cref="CommandInvoker.LivePipeMux"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LivePipeMux"/>. public bool LivePipeMux { get; set; }
/// </summary> /// <summary>
public bool LivePipeMux { get; set; } /// See: <see cref="CommandInvoker.LiveFixVttByAudio"/>.
/// <summary> /// </summary>
/// See: <see cref="CommandInvoker.LiveFixVttByAudio"/>. public bool LiveFixVttByAudio { get; set; }
/// </summary>
public bool LiveFixVttByAudio { get; set; }
}
} }

View File

@ -1,33 +1,25 @@
using N_m3u8DL_RE.CommandLine; using N_m3u8DL_RE.CommandLine;
using N_m3u8DL_RE.Enum;
using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Config namespace N_m3u8DL_RE.Config;
internal class DownloaderConfig
{ {
internal class DownloaderConfig public required MyOption MyOptions { get; set; }
{
public required MyOption MyOptions { get; set; }
/// <summary> /// <summary>
/// 前置阶段生成的文件夹名 /// 前置阶段生成的文件夹名
/// </summary> /// </summary>
public required string DirPrefix { get; set; } public required string DirPrefix { get; set; }
/// <summary> /// <summary>
/// 文件名模板 /// 文件名模板
/// </summary> /// </summary>
public string? SavePattern { get; set; } public string? SavePattern { get; set; }
/// <summary> /// <summary>
/// 校验响应头的文件大小和实际大小 /// 校验响应头的文件大小和实际大小
/// </summary> /// </summary>
public bool CheckContentLength { get; set; } = true; public bool CheckContentLength { get; set; } = true;
/// <summary> /// <summary>
/// 请求头 /// 请求头
/// </summary> /// </summary>
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
}
} }

View File

@ -1,44 +1,38 @@
using System; using System.Security.Cryptography;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Crypto namespace N_m3u8DL_RE.Crypto;
internal class AESUtil
{ {
internal class AESUtil /// <summary>
/// AES-128解密解密后原地替换文件
/// </summary>
/// <param name="filePath"></param>
/// <param name="keyByte"></param>
/// <param name="ivByte"></param>
/// <param name="mode"></param>
/// <param name="padding"></param>
public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
{ {
/// <summary> var fileBytes = File.ReadAllBytes(filePath);
/// AES-128解密解密后原地替换文件 var decrypted = AES128Decrypt(fileBytes, keyByte, ivByte, mode, padding);
/// </summary> File.WriteAllBytes(filePath, decrypted);
/// <param name="filePath"></param> }
/// <param name="keyByte"></param>
/// <param name="ivByte"></param>
/// <param name="mode"></param>
/// <param name="padding"></param>
public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
{
var fileBytes = File.ReadAllBytes(filePath);
var decrypted = AES128Decrypt(fileBytes, keyByte, ivByte, mode, padding);
File.WriteAllBytes(filePath, decrypted);
}
public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
{ {
byte[] inBuff = encryptedBuff; byte[] inBuff = encryptedBuff;
Aes dcpt = Aes.Create(); Aes dcpt = Aes.Create();
dcpt.BlockSize = 128; dcpt.BlockSize = 128;
dcpt.KeySize = 128; dcpt.KeySize = 128;
dcpt.Key = keyByte; dcpt.Key = keyByte;
dcpt.IV = ivByte; dcpt.IV = ivByte;
dcpt.Mode = mode; dcpt.Mode = mode;
dcpt.Padding = padding; dcpt.Padding = padding;
ICryptoTransform cTransform = dcpt.CreateDecryptor(); ICryptoTransform cTransform = dcpt.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length);
return resultArray; return resultArray;
}
} }
} }

View File

@ -1,26 +1,21 @@
using CSChaCha20; using CSChaCha20;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Crypto namespace N_m3u8DL_RE.Crypto;
internal class ChaCha20Util
{ {
internal class ChaCha20Util public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes)
{ {
public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes) if (keyBytes.Length != 32)
{ throw new Exception("Key must be 32 bytes!");
if (keyBytes.Length != 32) if (nonceBytes.Length != 12 && nonceBytes.Length != 8)
throw new Exception("Key must be 32 bytes!"); throw new Exception("Key must be 12 or 8 bytes!");
if (nonceBytes.Length != 12 && nonceBytes.Length != 8) if (nonceBytes.Length == 8)
throw new Exception("Key must be 12 or 8 bytes!"); nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray();
if (nonceBytes.Length == 8)
nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray();
var decStream = new MemoryStream(); var decStream = new MemoryStream();
using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff)); using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff));
using (BinaryWriter writer = new BinaryWriter(decStream)) using (BinaryWriter writer = new BinaryWriter(decStream))
while (true) while (true)
{ {
var buffer = reader.ReadBytes(1024); var buffer = reader.ReadBytes(1024);
@ -37,7 +32,6 @@ namespace N_m3u8DL_RE.Crypto
} }
} }
return decStream.ToArray(); return decStream.ToArray();
}
} }
} }

View File

@ -1,7 +1,5 @@
using Mp4SubtitleParser; using N_m3u8DL_RE.Column;
using N_m3u8DL_RE.Column;
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
@ -11,244 +9,234 @@ using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Parser; using N_m3u8DL_RE.Parser;
using N_m3u8DL_RE.Util; using N_m3u8DL_RE.Util;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO;
using System.Net.Http.Headers;
using System.Reflection.PortableExecutable;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Xml.Linq;
namespace N_m3u8DL_RE.DownloadManager namespace N_m3u8DL_RE.DownloadManager;
internal class HTTPLiveRecordManager
{ {
internal class HTTPLiveRecordManager IDownloader Downloader;
DownloaderConfig DownloaderConfig;
StreamExtractor StreamExtractor;
List<StreamSpec> SelectedSteams;
List<OutputFile> OutputFiles = new();
DateTime NowDateTime;
DateTime? PublishDateTime;
bool STOP_FLAG = false;
bool READ_IFO = false;
ConcurrentDictionary<int, int> RecordingDurDic = new(); // 已录制时长
ConcurrentDictionary<int, double> RecordingSizeDic = new(); // 已录制大小
CancellationTokenSource CancellationTokenSource = new(); // 取消Wait
List<byte> InfoBuffer = new List<byte>(188 * 5000); // 5000个分包中解析信息没有就算了
public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
{ {
IDownloader Downloader; this.DownloaderConfig = downloaderConfig;
DownloaderConfig DownloaderConfig; Downloader = new SimpleDownloader(DownloaderConfig);
StreamExtractor StreamExtractor; NowDateTime = DateTime.Now;
List<StreamSpec> SelectedSteams; PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;
List<OutputFile> OutputFiles = new(); StreamExtractor = streamExtractor;
DateTime NowDateTime; SelectedSteams = selectedSteams;
DateTime? PublishDateTime; }
bool STOP_FLAG = false;
bool READ_IFO = false;
ConcurrentDictionary<int, int> RecordingDurDic = new(); //已录制时长
ConcurrentDictionary<int, double> RecordingSizeDic = new(); //已录制大小
CancellationTokenSource CancellationTokenSource = new(); //取消Wait
List<byte> InfoBuffer = new List<byte>(188 * 5000); //5000个分包中解析信息没有就算了
public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)
{
task.MaxValue = 1;
task.StartTask();
var name = streamSpec.ToShortString();
var dirName = $"{DownloaderConfig.MyOptions.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}";
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName;
Logger.Debug($"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}");
// 创建文件夹
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url));
request.Headers.ConnectionClose = false;
foreach (var item in DownloaderConfig.Headers)
{ {
this.DownloaderConfig = downloaderConfig; request.Headers.TryAddWithoutValidation(item.Key, item.Value);
Downloader = new SimpleDownloader(DownloaderConfig); }
NowDateTime = DateTime.Now; Logger.Debug(request.Headers.ToString());
PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;
StreamExtractor = streamExtractor; using var response = await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationTokenSource.Token);
SelectedSteams = selectedSteams; response.EnsureSuccessStatusCode();
var output = Path.Combine(saveDir, saveName + ".ts");
using var stream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
using var responseStream = await response.Content.ReadAsStreamAsync(CancellationTokenSource.Token);
var buffer = new byte[16 * 1024];
var size = 0;
// 计时器
_ = TimeCounterAsync();
// 读取INFO
_ = ReadInfoAsync();
try
{
while ((size = await responseStream.ReadAsync(buffer, CancellationTokenSource.Token)) > 0)
{
if (!READ_IFO && InfoBuffer.Count < 188 * 5000)
{
InfoBuffer.AddRange(buffer);
}
speedContainer.Add(size);
RecordingSizeDic[task.Id] += size;
await stream.WriteAsync(buffer, 0, size);
}
}
catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
{
;
} }
private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer) Logger.InfoMarkUp("File Size: " + GlobalUtil.FormatFileSize(RecordingSizeDic[task.Id]));
return true;
}
public async Task ReadInfoAsync()
{
while (!STOP_FLAG && !READ_IFO)
{ {
task.MaxValue = 1; await Task.Delay(200);
task.StartTask(); if (InfoBuffer.Count < 188 * 5000) continue;
var name = streamSpec.ToShortString(); UInt16 ConvertToUint16(IEnumerable<byte> bytes)
var dirName = $"{DownloaderConfig.MyOptions.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}";
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName;
Logger.Debug($"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}");
//创建文件夹
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url));
request.Headers.ConnectionClose = false;
foreach (var item in DownloaderConfig.Headers)
{ {
request.Headers.TryAddWithoutValidation(item.Key, item.Value); if (BitConverter.IsLittleEndian)
bytes = bytes.Reverse();
return BitConverter.ToUInt16(bytes.ToArray());
} }
Logger.Debug(request.Headers.ToString());
using var response = await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationTokenSource.Token); var data = InfoBuffer.ToArray();
response.EnsureSuccessStatusCode(); var programId = "";
var serviceProvider = "";
var output = Path.Combine(saveDir, saveName + ".ts"); var serviceName = "";
using var stream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); for (int i = 0; i < data.Length; i++)
using var responseStream = await response.Content.ReadAsStreamAsync(CancellationTokenSource.Token);
var buffer = new byte[16 * 1024];
var size = 0;
//计时器
TimeCounterAsync();
//读取INFO
ReadInfoAsync();
try
{ {
while ((size = await responseStream.ReadAsync(buffer, CancellationTokenSource.Token)) > 0) if (data[i] == 0x47 && (i + 188) < data.Length && data[i + 188] == 0x47)
{ {
if (!READ_IFO && InfoBuffer.Count < 188 * 5000) var tsData = data.Skip(i).Take(188);
var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0);
var pid = (tsHeaderInt & 0x1fff00) >> 8;
var tsPayload = tsData.Skip(4);
// PAT
if (pid == 0x0000)
{ {
InfoBuffer.AddRange(buffer); programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString();
} }
speedContainer.Add(size); // SDT, BAT, ST
RecordingSizeDic[task.Id] += size; else if (pid == 0x0011)
await stream.WriteAsync(buffer, 0, size);
}
}
catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
{
;
}
Logger.InfoMarkUp("File Size: " + GlobalUtil.FormatFileSize(RecordingSizeDic[task.Id]));
return true;
}
public async Task ReadInfoAsync()
{
while (!STOP_FLAG && !READ_IFO)
{
await Task.Delay(200);
if (InfoBuffer.Count < 188 * 5000) continue;
UInt16 ConvertToUint16(IEnumerable<byte> bytes)
{
if (BitConverter.IsLittleEndian)
bytes = bytes.Reverse();
return BitConverter.ToUInt16(bytes.ToArray());
}
var data = InfoBuffer.ToArray();
var programId = "";
var serviceProvider = "";
var serviceName = "";
for (int i = 0; i < data.Length; i++)
{
if (data[i] == 0x47 && (i + 188) < data.Length && data[i + 188] == 0x47)
{ {
var tsData = data.Skip(i).Take(188); var tableId = (int)tsPayload.Skip(1).First();
var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0); // Current TS Info
var pid = (tsHeaderInt & 0x1fff00) >> 8; if (tableId == 0x42)
var tsPayload = tsData.Skip(4);
//PAT
if (pid == 0x0000)
{ {
programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString(); var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff;
var sectionData = tsPayload.Skip(4).Take(sectionLength);
var dscripData = sectionData.Skip(8);
var descriptorsLoopLength = (ConvertToUint16(dscripData.Skip(3).Take(2))) & 0xfff;
var descriptorsData = dscripData.Skip(5).Take(descriptorsLoopLength);
var serviceProviderLength = (int)descriptorsData.Skip(3).First();
serviceProvider = Encoding.UTF8.GetString(descriptorsData.Skip(4).Take(serviceProviderLength).ToArray());
var serviceNameLength = (int)descriptorsData.Skip(4 + serviceProviderLength).First();
serviceName = Encoding.UTF8.GetString(descriptorsData.Skip(5 + serviceProviderLength).Take(serviceNameLength).ToArray());
} }
//SDT, BAT, ST
else if (pid == 0x0011)
{
var tableId = (int)tsPayload.Skip(1).First();
//Current TS Info
if (tableId == 0x42)
{
var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff;
var sectionData = tsPayload.Skip(4).Take(sectionLength);
var dscripData = sectionData.Skip(8);
var descriptorsLoopLength = (ConvertToUint16(dscripData.Skip(3).Take(2))) & 0xfff;
var descriptorsData = dscripData.Skip(5).Take(descriptorsLoopLength);
var serviceProviderLength = (int)descriptorsData.Skip(3).First();
serviceProvider = Encoding.UTF8.GetString(descriptorsData.Skip(4).Take(serviceProviderLength).ToArray());
var serviceNameLength = (int)descriptorsData.Skip(4 + serviceProviderLength).First();
serviceName = Encoding.UTF8.GetString(descriptorsData.Skip(5 + serviceProviderLength).Take(serviceNameLength).ToArray());
}
}
if (programId != "" && (serviceName != "" || serviceProvider != ""))
break;
} }
} if (programId != "" && (serviceName != "" || serviceProvider != ""))
break;
if (!string.IsNullOrEmpty(programId))
{
Logger.InfoMarkUp($"Program Id: [cyan]{programId.EscapeMarkup()}[/]");
if (!string.IsNullOrEmpty(serviceName)) Logger.InfoMarkUp($"Service Name: [cyan]{serviceName.EscapeMarkup()}[/]");
if (!string.IsNullOrEmpty(serviceProvider)) Logger.InfoMarkUp($"Service Provider: [cyan]{serviceProvider.EscapeMarkup()}[/]");
READ_IFO = true;
} }
} }
}
public async Task TimeCounterAsync() if (!string.IsNullOrEmpty(programId))
{
while (!STOP_FLAG)
{ {
await Task.Delay(1000); Logger.InfoMarkUp($"Program Id: [cyan]{programId.EscapeMarkup()}[/]");
RecordingDurDic[0]++; if (!string.IsNullOrEmpty(serviceName)) Logger.InfoMarkUp($"Service Name: [cyan]{serviceName.EscapeMarkup()}[/]");
if (!string.IsNullOrEmpty(serviceProvider)) Logger.InfoMarkUp($"Service Provider: [cyan]{serviceProvider.EscapeMarkup()}[/]");
//检测时长限制 READ_IFO = true;
if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds))
{
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
STOP_FLAG = true;
CancellationTokenSource.Cancel();
}
} }
} }
}
public async Task<bool> StartRecordAsync()
{ public async Task TimeCounterAsync()
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); //速度计算 {
ConcurrentDictionary<StreamSpec, bool?> Results = new(); while (!STOP_FLAG)
{
var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); await Task.Delay(1000);
progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; RecordingDurDic[0]++;
//进度条的列定义 // 检测时长限制
var progressColumns = new ProgressColumn[] if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds))
{ {
new TaskDescriptionColumn() { Alignment = Justify.Left }, Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
new RecordingDurationColumn(RecordingDurDic), //时长显示 STOP_FLAG = true;
new RecordingSizeColumn(RecordingSizeDic), //大小显示 CancellationTokenSource.Cancel();
new RecordingStatusColumn(), }
new DownloadSpeedColumn(SpeedContainerDic), //速度计算 }
new SpinnerColumn(), }
};
if (DownloaderConfig.MyOptions.NoAnsiColor) public async Task<bool> StartRecordAsync()
{ {
progressColumns = progressColumns.SkipLast(1).ToArray(); ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算
} ConcurrentDictionary<StreamSpec, bool?> Results = new();
progress.Columns(progressColumns);
var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
await progress.StartAsync(async ctx => progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;
{
//创建任务 // 进度条的列定义
var dic = SelectedSteams.Select(item => var progressColumns = new ProgressColumn[]
{ {
var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0); new TaskDescriptionColumn() { Alignment = Justify.Left },
SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 new RecordingDurationColumn(RecordingDurDic), // 时长显示
RecordingDurDic[task.Id] = 0; new RecordingSizeColumn(RecordingSizeDic), // 大小显示
RecordingSizeDic[task.Id] = 0; new RecordingStatusColumn(),
return (item, task); new DownloadSpeedColumn(SpeedContainerDic), // 速度计算
}).ToDictionary(item => item.item, item => item.task); new SpinnerColumn(),
};
DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; if (DownloaderConfig.MyOptions.NoAnsiColor)
var limit = DownloaderConfig.MyOptions.LiveRecordLimit; {
if (limit != TimeSpan.MaxValue) progressColumns = progressColumns.SkipLast(1).ToArray();
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]"); }
//录制直播时,用户选了几个流就并发录几个 progress.Columns(progressColumns);
var options = new ParallelOptions()
{ await progress.StartAsync(async ctx =>
MaxDegreeOfParallelism = SelectedSteams.Count {
}; // 创建任务
//并发下载 var dic = SelectedSteams.Select(item =>
await Parallel.ForEachAsync(dic, options, async (kp, _) => {
{ var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0);
var task = kp.Value; SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算
var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]); RecordingDurDic[task.Id] = 0;
Results[kp.Key] = await consumerTask; RecordingSizeDic[task.Id] = 0;
}); return (item, task);
}); }).ToDictionary(item => item.item, item => item.task);
var success = Results.Values.All(v => v == true); DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue;
var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
return success; if (limit != TimeSpan.MaxValue)
} Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");
// 录制直播时,用户选了几个流就并发录几个
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = SelectedSteams.Count
};
// 并发下载
await Parallel.ForEachAsync(dic, options, async (kp, _) =>
{
var task = kp.Value;
var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);
Results[kp.Key] = await consumerTask;
});
});
var success = Results.Values.All(v => v == true);
return success;
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -6,155 +6,145 @@ using N_m3u8DL_RE.Crypto;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Util; using N_m3u8DL_RE.Util;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Downloader namespace N_m3u8DL_RE.Downloader;
/// <summary>
/// 简单下载器
/// </summary>
internal class SimpleDownloader : IDownloader
{ {
/// <summary> DownloaderConfig DownloaderConfig;
/// 简单下载器
/// </summary> public SimpleDownloader(DownloaderConfig config)
internal class SimpleDownloader : IDownloader
{ {
DownloaderConfig DownloaderConfig; DownloaderConfig = config;
}
public SimpleDownloader(DownloaderConfig config) public async Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null)
{
var url = segment.Url;
var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount);
if (dResult != null && dResult.Success && dResult.ActualFilePath != des)
{ {
DownloaderConfig = config; if (segment.EncryptInfo != null)
}
public async Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null)
{
var url = segment.Url;
var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount);
if (dResult != null && dResult.Success && dResult.ActualFilePath != des)
{ {
if (segment.EncryptInfo != null) if (segment.EncryptInfo.Method == EncryptMethod.AES_128)
{ {
if (segment.EncryptInfo.Method == EncryptMethod.AES_128) var key = segment.EncryptInfo.Key;
{ var iv = segment.EncryptInfo.IV;
var key = segment.EncryptInfo.Key; AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!);
var iv = segment.EncryptInfo.IV; }
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!); else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB)
} {
else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB) var key = segment.EncryptInfo.Key;
{ var iv = segment.EncryptInfo.IV;
var key = segment.EncryptInfo.Key; AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB);
var iv = segment.EncryptInfo.IV; }
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB); else if (segment.EncryptInfo.Method == EncryptMethod.CHACHA20)
} {
else if (segment.EncryptInfo.Method == EncryptMethod.CHACHA20) var key = segment.EncryptInfo.Key;
{ var nonce = segment.EncryptInfo.IV;
var key = segment.EncryptInfo.Key;
var nonce = segment.EncryptInfo.IV;
var fileBytes = File.ReadAllBytes(dResult.ActualFilePath); var fileBytes = File.ReadAllBytes(dResult.ActualFilePath);
var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!); var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!);
await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted); await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted);
} }
else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR) else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR)
{ {
//throw new NotSupportedException("SAMPLE-AES-CTR"); // throw new NotSupportedException("SAMPLE-AES-CTR");
}
//Image头处理
if (dResult.ImageHeader)
{
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
}
//Gzip解压
if (dResult.GzipHeader)
{
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
}
} }
//处理完成后改名 // Image头处理
File.Move(dResult.ActualFilePath, des); if (dResult.ImageHeader)
dResult.ActualFilePath = des; {
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
}
// Gzip解压
if (dResult.GzipHeader)
{
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
}
} }
return dResult;
}
private async Task<(string des, DownloadResult? dResult)> DownClipAsync(string url, string path, SpeedContainer speedContainer, long? fromPosition, long? toPosition, Dictionary<string, string>? headers = null, int retryCount = 3) // 处理完成后改名
{ File.Move(dResult.ActualFilePath, des);
CancellationTokenSource? cancellationTokenSource = null; dResult.ActualFilePath = des;
}
return dResult;
}
private async Task<(string des, DownloadResult? dResult)> DownClipAsync(string url, string path, SpeedContainer speedContainer, long? fromPosition, long? toPosition, Dictionary<string, string>? headers = null, int retryCount = 3)
{
CancellationTokenSource? cancellationTokenSource = null;
retry: retry:
try try
{
cancellationTokenSource = new();
var des = Path.ChangeExtension(path, null);
// 已下载跳过
if (File.Exists(des))
{ {
cancellationTokenSource = new(); speedContainer.Add(new FileInfo(des).Length);
var des = Path.ChangeExtension(path, null); return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des });
}
//已下载跳过 // 已解密跳过
if (File.Exists(des)) var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + "_dec" + Path.GetExtension(des));
{ if (File.Exists(dec))
speedContainer.Add(new FileInfo(des).Length); {
return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des }); speedContainer.Add(new FileInfo(dec).Length);
} return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec });
}
//已解密跳过 // 另起线程进行监控
var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + "_dec" + Path.GetExtension(des)); using var watcher = Task.Factory.StartNew(async () =>
if (File.Exists(dec)) {
while (true)
{ {
speedContainer.Add(new FileInfo(dec).Length); if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break;
return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec }); if (speedContainer.ShouldStop)
}
//另起线程进行监控
using var watcher = Task.Factory.StartNew(async () =>
{
while (true)
{ {
if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break; cancellationTokenSource.Cancel();
if (speedContainer.ShouldStop) Logger.DebugMarkUp("Cancel...");
{ break;
cancellationTokenSource.Cancel();
Logger.DebugMarkUp("Cancel...");
break;
}
await Task.Delay(500);
} }
}); await Task.Delay(500);
}
});
//调用下载 // 调用下载
var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition); var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition);
return (des, result); return (des, result);
throw new Exception("please retry"); throw new Exception("please retry");
} }
catch (Exception ex) catch (Exception ex)
{
Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
Logger.Debug(url + " " + ex.ToString());
Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
if (retryCount-- > 0)
{ {
Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]"); await Task.Delay(1000);
Logger.Debug(url + " " + ex.ToString()); goto retry;
Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
if (retryCount-- > 0)
{
await Task.Delay(1000);
goto retry;
}
else
{
Logger.Extra($"The retry attempts have been exhausted and the download of this segment has failed.{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
Logger.WarnMarkUp($"[grey]{ex.Message.EscapeMarkup()}[/]");
}
//throw new Exception("download failed", ex);
return default;
} }
finally else
{ {
if (cancellationTokenSource != null) Logger.Extra($"The retry attempts have been exhausted and the download of this segment has failed.{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
{ Logger.WarnMarkUp($"[grey]{ex.Message.EscapeMarkup()}[/]");
//调用后销毁 }
cancellationTokenSource.Dispose(); // throw new Exception("download failed", ex);
cancellationTokenSource = null; return default;
} }
finally
{
if (cancellationTokenSource != null)
{
// 调用后销毁
cancellationTokenSource.Dispose();
cancellationTokenSource = null;
} }
} }
} }

View File

@ -1,23 +1,16 @@
using System; namespace N_m3u8DL_RE.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity public class CustomRange
{ {
public class CustomRange public required string InputStr { get; set; }
public double? StartSec { get; set; }
public double? EndSec { get; set; }
public long? StartSegIndex { get; set; }
public long? EndSegIndex { get; set;}
public override string? ToString()
{ {
public required string InputStr { get; set; } return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}";
public double? StartSec { get; set; }
public double? EndSec { get; set; }
public long? StartSegIndex { get; set; }
public long? EndSegIndex { get; set;}
public override string? ToString()
{
return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}";
}
} }
} }

View File

@ -5,15 +5,14 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
internal class DownloadResult
{ {
internal class DownloadResult public bool Success { get => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength == null ? false : true); }
{ public long? RespContentLength { get; set; }
public bool Success { get => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength == null ? false : true); } public long? ActualContentLength { get; set; }
public long? RespContentLength { get; set; } public bool ImageHeader { get; set; } = false; // 图片伪装
public long? ActualContentLength { get; set; } public bool GzipHeader { get; set; } = false; // GZip压缩
public bool ImageHeader { get; set; } = false; //图片伪装 public required string ActualFilePath { get; set; }
public bool GzipHeader { get; set; } = false; //GZip压缩
public required string ActualFilePath { get; set; }
}
} }

View File

@ -1,33 +1,27 @@
using System; using Spectre.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spectre.Console;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
internal class Mediainfo
{ {
internal class Mediainfo public string? Id { get; set; }
public string? Text { get; set; }
public string? BaseInfo { get; set; }
public string? Bitrate { get; set; }
public string? Resolution { get; set; }
public string? Fps { get; set; }
public string? Type { get; set; }
public TimeSpan? StartTime { get; set; }
public bool DolbyVison { get; set; }
public bool HDR { get; set; }
public override string? ToString()
{ {
public string? Id { get; set; } return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List<string?> { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i)));
public string? Text { get; set; } }
public string? BaseInfo { get; set; }
public string? Bitrate { get; set; }
public string? Resolution { get; set; }
public string? Fps { get; set; }
public string? Type { get; set; }
public TimeSpan? StartTime { get; set; }
public bool DolbyVison { get; set; }
public bool HDR { get; set; }
public override string? ToString() public string ToStringMarkUp()
{ {
return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List<string?> { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i))); return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]";
}
public string ToStringMarkUp()
{
return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]";
}
} }
} }

View File

@ -1,13 +1,12 @@
using N_m3u8DL_RE.Enum; using N_m3u8DL_RE.Enum;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
internal class MuxOptions
{ {
internal class MuxOptions public bool UseMkvmerge { get; set; } = false;
{ public MuxFormat MuxFormat { get; set; } = MuxFormat.MP4;
public bool UseMkvmerge { get; set; } = false; public bool KeepFiles { get; set; } = false;
public MuxFormat MuxFormat { get; set; } = MuxFormat.MP4; public bool SkipSubtitle { get; set; } = false;
public bool KeepFiles { get; set; } = false; public string? BinPath { get; set; }
public bool SkipSubtitle { get; set; } = false;
public string? BinPath { get; set; }
}
} }

View File

@ -1,19 +1,13 @@
using N_m3u8DL_RE.Common.Enum; 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.Entity namespace N_m3u8DL_RE.Entity;
internal class OutputFile
{ {
internal class OutputFile public MediaType? MediaType { get; set; }
{ public required int Index { get; set; }
public MediaType? MediaType { get; set; } public required string FilePath { get; set; }
public required int Index { get; set; } public string? LangCode { get; set; }
public required string FilePath { get; set; } public string? Description { get; set; }
public string? LangCode { get; set; } public List<Mediainfo> Mediainfos { get; set; } = new();
public string? Description { get; set; }
public List<Mediainfo> Mediainfos { get; set; } = new();
}
} }

View File

@ -7,53 +7,52 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
internal class SpeedContainer
{ {
internal class SpeedContainer public bool SingleSegment { get; set; } = false;
public long NowSpeed { get; set; } = 0L; // 当前每秒速度
public long SpeedLimit { get; set; } = long.MaxValue; // 限速设置
public long? ResponseLength { get; set; }
public long RDownloaded => _Rdownloaded;
private int _zeroSpeedCount = 0;
public int LowSpeedCount => _zeroSpeedCount;
public bool ShouldStop => LowSpeedCount >= 20;
///////////////////////////////////////////////////
private long _downloaded = 0;
private long _Rdownloaded = 0;
public long Downloaded { get => _downloaded; }
public int AddLowSpeedCount()
{ {
public bool SingleSegment { get; set; } = false; return Interlocked.Add(ref _zeroSpeedCount, 1);
public long NowSpeed { get; set; } = 0L; //当前每秒速度 }
public long SpeedLimit { get; set; } = long.MaxValue; //限速设置
public long? ResponseLength { get; set; }
public long RDownloaded { get => _Rdownloaded; }
private int _zeroSpeedCount = 0;
public int LowSpeedCount { get => _zeroSpeedCount; }
public bool ShouldStop { get => LowSpeedCount >= 20; }
/////////////////////////////////////////////////// public int ResetLowSpeedCount()
{
return Interlocked.Exchange(ref _zeroSpeedCount, 0);
}
private long _downloaded = 0; public long Add(long size)
private long _Rdownloaded = 0; {
public long Downloaded { get => _downloaded; } Interlocked.Add(ref _Rdownloaded, size);
return Interlocked.Add(ref _downloaded, size);
}
public int AddLowSpeedCount() public void Reset()
{ {
return Interlocked.Add(ref _zeroSpeedCount, 1); Interlocked.Exchange(ref _downloaded, 0);
} }
public int ResetLowSpeedCount() public void ResetVars()
{ {
return Interlocked.Exchange(ref _zeroSpeedCount, 0); Reset();
} ResetLowSpeedCount();
SingleSegment = false;
public long Add(long size) ResponseLength = null;
{ _Rdownloaded = 0L;
Interlocked.Add(ref _Rdownloaded, size);
return Interlocked.Add(ref _downloaded, size);
}
public void Reset()
{
Interlocked.Exchange(ref _downloaded, 0);
}
public void ResetVars()
{
Reset();
ResetLowSpeedCount();
SingleSegment = false;
ResponseLength = null;
_Rdownloaded = 0L;
}
} }
} }

View File

@ -1,56 +1,51 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
public class StreamFilter
{ {
public class StreamFilter public Regex? GroupIdReg { get; set; }
public Regex? LanguageReg { get; set; }
public Regex? NameReg { get; set; }
public Regex? CodecsReg { get; set; }
public Regex? ResolutionReg { get; set; }
public Regex? FrameRateReg { get; set; }
public Regex? ChannelsReg { get; set; }
public Regex? VideoRangeReg { get; set; }
public Regex? UrlReg { get; set; }
public long? SegmentsMinCount { get; set; }
public long? SegmentsMaxCount { get; set; }
public double? PlaylistMinDur { get; set; }
public double? PlaylistMaxDur { get; set; }
public int? BandwidthMin { get; set; }
public int? BandwidthMax { get; set; }
public RoleType? Role { get; set; }
public string For { get; set; } = "best";
public override string? ToString()
{ {
public Regex? GroupIdReg { get; set; } var sb = new StringBuilder();
public Regex? LanguageReg { get; set; }
public Regex? NameReg { get; set; }
public Regex? CodecsReg { get; set; }
public Regex? ResolutionReg { get; set; }
public Regex? FrameRateReg { get; set; }
public Regex? ChannelsReg { get; set; }
public Regex? VideoRangeReg { get; set; }
public Regex? UrlReg { get; set; }
public long? SegmentsMinCount { get; set; }
public long? SegmentsMaxCount { get; set; }
public double? PlaylistMinDur { get; set; }
public double? PlaylistMaxDur { get; set; }
public int? BandwidthMin { get; set; }
public int? BandwidthMax { get; set; }
public RoleType? Role { get; set; }
public string For { get; set; } = "best"; if (GroupIdReg != null) sb.Append($"GroupIdReg: {GroupIdReg} ");
if (LanguageReg != null) sb.Append($"LanguageReg: {LanguageReg} ");
if (NameReg != null) sb.Append($"NameReg: {NameReg} ");
if (CodecsReg != null) sb.Append($"CodecsReg: {CodecsReg} ");
if (ResolutionReg != null) sb.Append($"ResolutionReg: {ResolutionReg} ");
if (FrameRateReg != null) sb.Append($"FrameRateReg: {FrameRateReg} ");
if (ChannelsReg != null) sb.Append($"ChannelsReg: {ChannelsReg} ");
if (VideoRangeReg != null) sb.Append($"VideoRangeReg: {VideoRangeReg} ");
if (UrlReg != null) sb.Append($"UrlReg: {UrlReg} ");
if (SegmentsMinCount != null) sb.Append($"SegmentsMinCount: {SegmentsMinCount} ");
if (SegmentsMaxCount != null) sb.Append($"SegmentsMaxCount: {SegmentsMaxCount} ");
if (PlaylistMinDur != null) sb.Append($"PlaylistMinDur: {PlaylistMinDur} ");
if (PlaylistMaxDur != null) sb.Append($"PlaylistMaxDur: {PlaylistMaxDur} ");
if (BandwidthMin != null) sb.Append($"{nameof(BandwidthMin)}: {BandwidthMin} ");
if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} ");
if (Role.HasValue) sb.Append($"Role: {Role} ");
public override string? ToString() return sb.ToString() + $"For: {For}";
{
var sb = new StringBuilder();
if (GroupIdReg != null) sb.Append($"GroupIdReg: {GroupIdReg} ");
if (LanguageReg != null) sb.Append($"LanguageReg: {LanguageReg} ");
if (NameReg != null) sb.Append($"NameReg: {NameReg} ");
if (CodecsReg != null) sb.Append($"CodecsReg: {CodecsReg} ");
if (ResolutionReg != null) sb.Append($"ResolutionReg: {ResolutionReg} ");
if (FrameRateReg != null) sb.Append($"FrameRateReg: {FrameRateReg} ");
if (ChannelsReg != null) sb.Append($"ChannelsReg: {ChannelsReg} ");
if (VideoRangeReg != null) sb.Append($"VideoRangeReg: {VideoRangeReg} ");
if (UrlReg != null) sb.Append($"UrlReg: {UrlReg} ");
if (SegmentsMinCount != null) sb.Append($"SegmentsMinCount: {SegmentsMinCount} ");
if (SegmentsMaxCount != null) sb.Append($"SegmentsMaxCount: {SegmentsMaxCount} ");
if (PlaylistMinDur != null) sb.Append($"PlaylistMinDur: {PlaylistMinDur} ");
if (PlaylistMaxDur != null) sb.Append($"PlaylistMaxDur: {PlaylistMaxDur} ");
if (BandwidthMin != null) sb.Append($"{nameof(BandwidthMin)}: {BandwidthMin} ");
if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} ");
if (Role.HasValue) sb.Append($"Role: {Role} ");
return sb.ToString() + $"For: {For}";
}
} }
} }

View File

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

View File

@ -5,27 +5,21 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Processor; using N_m3u8DL_RE.Parser.Processor;
using N_m3u8DL_RE.Parser.Processor.HLS; using N_m3u8DL_RE.Parser.Processor.HLS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Processor namespace N_m3u8DL_RE.Processor;
internal class DemoProcessor2 : KeyProcessor
{ {
internal class DemoProcessor2 : KeyProcessor public override bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
{ {
public override bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig) return extractorType == ExtractorType.HLS && parserConfig.Url.Contains("playertest.longtailvideo.com");
{ }
return extractorType == ExtractorType.HLS && parserConfig.Url.Contains("playertest.longtailvideo.com");
}
public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig) public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
{ {
Logger.InfoMarkUp($"[white on green]My Key Processor => {keyLine}[/]"); Logger.InfoMarkUp($"[white on green]My Key Processor => {keyLine}[/]");
var info = new DefaultHLSKeyProcessor().Process(keyLine, m3u8Url, m3u8Content, parserConfig); var info = new DefaultHLSKeyProcessor().Process(keyLine, m3u8Url, m3u8Content, parserConfig);
Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]"); Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]");
return info; return info;
}
} }
} }

View File

@ -6,219 +6,211 @@ using N_m3u8DL_RE.Parser.Util;
using NiL.JS.BaseLibrary; using NiL.JS.BaseLibrary;
using NiL.JS.Core; using NiL.JS.Core;
using NiL.JS.Extensions; using NiL.JS.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Processor namespace N_m3u8DL_RE.Processor;
// "https://1429754964.rsc.cdn77.org/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/h264.mpd?secure=mSvVfvuciJt9wufUyzuBnA==,1658505709774" --urlprocessor-args "nowehoryzonty:timeDifference=-2274,filminfo.secureToken=vx54axqjal4f0yy2"
internal class NowehoryzontyUrlProcessor : UrlProcessor
{ {
//"https://1429754964.rsc.cdn77.org/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/h264.mpd?secure=mSvVfvuciJt9wufUyzuBnA==,1658505709774" --urlprocessor-args "nowehoryzonty:timeDifference=-2274,filminfo.secureToken=vx54axqjal4f0yy2" private static string START = "nowehoryzonty:";
internal class NowehoryzontyUrlProcessor : UrlProcessor private static string? TimeDifferenceStr = null;
private static int? TimeDifference = null;
private static string? SecureToken = null;
private static bool LOG = false;
private static Function? Function = null;
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig)
{ {
private static string START = "nowehoryzonty:"; if (extractorType == ExtractorType.MPEG_DASH && parserConfig.UrlProcessorArgs != null && parserConfig.UrlProcessorArgs.StartsWith(START))
private static string? TimeDifferenceStr = null;
private static int? TimeDifference = null;
private static string? SecureToken = null;
private static bool LOG = false;
private static Function? Function = null;
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig)
{ {
if (extractorType == ExtractorType.MPEG_DASH && parserConfig.UrlProcessorArgs != null && parserConfig.UrlProcessorArgs.StartsWith(START)) if (!LOG)
{ {
if (!LOG) Logger.WarnMarkUp($"[white on green]www.nowehoryzonty.pl[/] matched! waiting for calc...");
{ LOG = true;
Logger.WarnMarkUp($"[white on green]www.nowehoryzonty.pl[/] matched! waiting for calc...");
LOG = true;
}
var context = new Context();
context.Eval(JS);
Function = context.GetVariable("md5").As<Function>();
var argLine = parserConfig.UrlProcessorArgs![START.Length..];
TimeDifferenceStr = ParserUtil.GetAttribute(argLine, "timeDifference");
SecureToken = ParserUtil.GetAttribute(argLine, "filminfo.secureToken");
if (TimeDifferenceStr != null && SecureToken != null)
{
TimeDifference = Convert.ToInt32(TimeDifferenceStr);
}
return true;
} }
return false; var context = new Context();
context.Eval(JS);
Function = context.GetVariable("md5").As<Function>();
var argLine = parserConfig.UrlProcessorArgs![START.Length..];
TimeDifferenceStr = ParserUtil.GetAttribute(argLine, "timeDifference");
SecureToken = ParserUtil.GetAttribute(argLine, "filminfo.secureToken");
if (TimeDifferenceStr != null && SecureToken != null)
{
TimeDifference = Convert.ToInt32(TimeDifferenceStr);
}
return true;
} }
return false;
public override string Process(string oriUrl, ParserConfig parserConfig)
{
var a = new Uri(oriUrl).AbsolutePath;
var n = oriUrl + "?secure=" + Calc(a);
return n;
}
private static string Calc(string a)
{
string returnStr = Function!.Call(new Arguments { a, SecureToken, TimeDifference }).ToString();
return returnStr;
}
////https://www.nowehoryzonty.pl/packed/videonho.js?v=1114377281:formatted
private static readonly string JS = """
var p = function(f, e) {
var d = f[0]
, a = f[1]
, b = f[2]
, c = f[3];
d = h(d, a, b, c, e[0], 7, -680876936);
c = h(c, d, a, b, e[1], 12, -389564586);
b = h(b, c, d, a, e[2], 17, 606105819);
a = h(a, b, c, d, e[3], 22, -1044525330);
d = h(d, a, b, c, e[4], 7, -176418897);
c = h(c, d, a, b, e[5], 12, 1200080426);
b = h(b, c, d, a, e[6], 17, -1473231341);
a = h(a, b, c, d, e[7], 22, -45705983);
d = h(d, a, b, c, e[8], 7, 1770035416);
c = h(c, d, a, b, e[9], 12, -1958414417);
b = h(b, c, d, a, e[10], 17, -42063);
a = h(a, b, c, d, e[11], 22, -1990404162);
d = h(d, a, b, c, e[12], 7, 1804603682);
c = h(c, d, a, b, e[13], 12, -40341101);
b = h(b, c, d, a, e[14], 17, -1502002290);
a = h(a, b, c, d, e[15], 22, 1236535329);
d = k(d, a, b, c, e[1], 5, -165796510);
c = k(c, d, a, b, e[6], 9, -1069501632);
b = k(b, c, d, a, e[11], 14, 643717713);
a = k(a, b, c, d, e[0], 20, -373897302);
d = k(d, a, b, c, e[5], 5, -701558691);
c = k(c, d, a, b, e[10], 9, 38016083);
b = k(b, c, d, a, e[15], 14, -660478335);
a = k(a, b, c, d, e[4], 20, -405537848);
d = k(d, a, b, c, e[9], 5, 568446438);
c = k(c, d, a, b, e[14], 9, -1019803690);
b = k(b, c, d, a, e[3], 14, -187363961);
a = k(a, b, c, d, e[8], 20, 1163531501);
d = k(d, a, b, c, e[13], 5, -1444681467);
c = k(c, d, a, b, e[2], 9, -51403784);
b = k(b, c, d, a, e[7], 14, 1735328473);
a = k(a, b, c, d, e[12], 20, -1926607734);
d = g(a ^ b ^ c, d, a, e[5], 4, -378558);
c = g(d ^ a ^ b, c, d, e[8], 11, -2022574463);
b = g(c ^ d ^ a, b, c, e[11], 16, 1839030562);
a = g(b ^ c ^ d, a, b, e[14], 23, -35309556);
d = g(a ^ b ^ c, d, a, e[1], 4, -1530992060);
c = g(d ^ a ^ b, c, d, e[4], 11, 1272893353);
b = g(c ^ d ^ a, b, c, e[7], 16, -155497632);
a = g(b ^ c ^ d, a, b, e[10], 23, -1094730640);
d = g(a ^ b ^ c, d, a, e[13], 4, 681279174);
c = g(d ^ a ^ b, c, d, e[0], 11, -358537222);
b = g(c ^ d ^ a, b, c, e[3], 16, -722521979);
a = g(b ^ c ^ d, a, b, e[6], 23, 76029189);
d = g(a ^ b ^ c, d, a, e[9], 4, -640364487);
c = g(d ^ a ^ b, c, d, e[12], 11, -421815835);
b = g(c ^ d ^ a, b, c, e[15], 16, 530742520);
a = g(b ^ c ^ d, a, b, e[2], 23, -995338651);
d = l(d, a, b, c, e[0], 6, -198630844);
c = l(c, d, a, b, e[7], 10, 1126891415);
b = l(b, c, d, a, e[14], 15, -1416354905);
a = l(a, b, c, d, e[5], 21, -57434055);
d = l(d, a, b, c, e[12], 6, 1700485571);
c = l(c, d, a, b, e[3], 10, -1894986606);
b = l(b, c, d, a, e[10], 15, -1051523);
a = l(a, b, c, d, e[1], 21, -2054922799);
d = l(d, a, b, c, e[8], 6, 1873313359);
c = l(c, d, a, b, e[15], 10, -30611744);
b = l(b, c, d, a, e[6], 15, -1560198380);
a = l(a, b, c, d, e[13], 21, 1309151649);
d = l(d, a, b, c, e[4], 6, -145523070);
c = l(c, d, a, b, e[11], 10, -1120210379);
b = l(b, c, d, a, e[2], 15, 718787259);
a = l(a, b, c, d, e[9], 21, -343485551);
f[0] = m(d, f[0]);
f[1] = m(a, f[1]);
f[2] = m(b, f[2]);
f[3] = m(c, f[3])
}, g = function(f, e, d, a, b, c) {
e = m(m(e, f), m(a, c));
return m(e << b | e >>> 32 - b, d)
}
, h = function(f, e, d, a, b, c, n) {
return g(e & d | ~e & a, f, e, b, c, n)
}
, k = function(f, e, d, a, b, c, n) {
return g(e & a | d & ~a, f, e, b, c, n)
}
, l = function(f, e, d, a, b, c, n) {
return g(d ^ (e | ~a), f, e, b, c, n)
}, r = "0123456789abcdef".split("");
var m = function(f, e) {
return f + e & 4294967295
};
var q = function(f) {
var e = f.length, d = [1732584193, -271733879, -1732584194, 271733878], a;
for (a = 64; a <= f.length; a += 64) {
var b, c = f.substring(a - 64, a), g = [];
for (b = 0; 64 > b; b += 4)
g[b >> 2] = c.charCodeAt(b) + (c.charCodeAt(b + 1) << 8) + (c.charCodeAt(b + 2) << 16) + (c.charCodeAt(b + 3) << 24);
p(d, g)
}
f = f.substring(a - 64);
b = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for (a = 0; a < f.length; a++)
b[a >> 2] |= f.charCodeAt(a) << (a % 4 << 3);
b[a >> 2] |= 128 << (a % 4 << 3);
if (55 < a)
for (p(d, b),
a = 0; 16 > a; a++)
b[a] = 0;
b[14] = 8 * e;
p(d, b);
return d
};
var md5 = function(f, e, timeDifference) {
var d = Date.now() + 6E4 + timeDifference;
e = q(d + f + e);
f = [];
for (var a = 0; a < e.length; a++) {
var b = e[a];
var c = []
, g = 4;
do
c[--g] = b & 255,
b >>= 8;
while (g);
b = c;
for (c = b.length - 1; 0 <= c; c--)
f.push(b[c])
}
g = void 0;
c = "";
for (e = a = b = 0; e < 4 * f.length / 3; g = b >> 2 * (++e & 3) & 63,
c += String.fromCharCode(g + 71 - (26 > g ? 6 : 52 > g ? 0 : 62 > g ? 75 : g ^ 63 ? 90 : 87)) + (75 == (e - 1) % 76 ? "\r\n" : ""))
e & 3 ^ 3 && (b = b << 8 ^ f[a++]);
for (; e++ & 3; )
c += "\x3d";
return c.replace(/\+/g, "-").replace(/\//g, "_") + "," + d
};
"5d41402abc4b2a76b9719d911017c592" != function(f) {
for (var e = 0; e < f.length; e++) {
for (var d = e, a = f[e], b = "", c = 0; 4 > c; c++)
b += r[a >> 8 * c + 4 & 15] + r[a >> 8 * c & 15];
f[d] = b
}
return f.join("")
}(q("hello")) && (m = function(f, e) {
var d = (f & 65535) + (e & 65535);
return (f >> 16) + (e >> 16) + (d >> 16) << 16 | d & 65535
}
)
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/h264.mpd','vx54axqjal4f0yy2',-2274));
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/subtitle_pl/34.m4s','vx54axqjal4f0yy2',-2274));
""";
} }
public override string Process(string oriUrl, ParserConfig parserConfig)
{
var a = new Uri(oriUrl).AbsolutePath;
var n = oriUrl + "?secure=" + Calc(a);
return n;
}
private static string Calc(string a)
{
string returnStr = Function!.Call(new Arguments { a, SecureToken, TimeDifference }).ToString();
return returnStr;
}
////https://www.nowehoryzonty.pl/packed/videonho.js?v=1114377281:formatted
private static readonly string JS = """
var p = function(f, e) {
var d = f[0]
, a = f[1]
, b = f[2]
, c = f[3];
d = h(d, a, b, c, e[0], 7, -680876936);
c = h(c, d, a, b, e[1], 12, -389564586);
b = h(b, c, d, a, e[2], 17, 606105819);
a = h(a, b, c, d, e[3], 22, -1044525330);
d = h(d, a, b, c, e[4], 7, -176418897);
c = h(c, d, a, b, e[5], 12, 1200080426);
b = h(b, c, d, a, e[6], 17, -1473231341);
a = h(a, b, c, d, e[7], 22, -45705983);
d = h(d, a, b, c, e[8], 7, 1770035416);
c = h(c, d, a, b, e[9], 12, -1958414417);
b = h(b, c, d, a, e[10], 17, -42063);
a = h(a, b, c, d, e[11], 22, -1990404162);
d = h(d, a, b, c, e[12], 7, 1804603682);
c = h(c, d, a, b, e[13], 12, -40341101);
b = h(b, c, d, a, e[14], 17, -1502002290);
a = h(a, b, c, d, e[15], 22, 1236535329);
d = k(d, a, b, c, e[1], 5, -165796510);
c = k(c, d, a, b, e[6], 9, -1069501632);
b = k(b, c, d, a, e[11], 14, 643717713);
a = k(a, b, c, d, e[0], 20, -373897302);
d = k(d, a, b, c, e[5], 5, -701558691);
c = k(c, d, a, b, e[10], 9, 38016083);
b = k(b, c, d, a, e[15], 14, -660478335);
a = k(a, b, c, d, e[4], 20, -405537848);
d = k(d, a, b, c, e[9], 5, 568446438);
c = k(c, d, a, b, e[14], 9, -1019803690);
b = k(b, c, d, a, e[3], 14, -187363961);
a = k(a, b, c, d, e[8], 20, 1163531501);
d = k(d, a, b, c, e[13], 5, -1444681467);
c = k(c, d, a, b, e[2], 9, -51403784);
b = k(b, c, d, a, e[7], 14, 1735328473);
a = k(a, b, c, d, e[12], 20, -1926607734);
d = g(a ^ b ^ c, d, a, e[5], 4, -378558);
c = g(d ^ a ^ b, c, d, e[8], 11, -2022574463);
b = g(c ^ d ^ a, b, c, e[11], 16, 1839030562);
a = g(b ^ c ^ d, a, b, e[14], 23, -35309556);
d = g(a ^ b ^ c, d, a, e[1], 4, -1530992060);
c = g(d ^ a ^ b, c, d, e[4], 11, 1272893353);
b = g(c ^ d ^ a, b, c, e[7], 16, -155497632);
a = g(b ^ c ^ d, a, b, e[10], 23, -1094730640);
d = g(a ^ b ^ c, d, a, e[13], 4, 681279174);
c = g(d ^ a ^ b, c, d, e[0], 11, -358537222);
b = g(c ^ d ^ a, b, c, e[3], 16, -722521979);
a = g(b ^ c ^ d, a, b, e[6], 23, 76029189);
d = g(a ^ b ^ c, d, a, e[9], 4, -640364487);
c = g(d ^ a ^ b, c, d, e[12], 11, -421815835);
b = g(c ^ d ^ a, b, c, e[15], 16, 530742520);
a = g(b ^ c ^ d, a, b, e[2], 23, -995338651);
d = l(d, a, b, c, e[0], 6, -198630844);
c = l(c, d, a, b, e[7], 10, 1126891415);
b = l(b, c, d, a, e[14], 15, -1416354905);
a = l(a, b, c, d, e[5], 21, -57434055);
d = l(d, a, b, c, e[12], 6, 1700485571);
c = l(c, d, a, b, e[3], 10, -1894986606);
b = l(b, c, d, a, e[10], 15, -1051523);
a = l(a, b, c, d, e[1], 21, -2054922799);
d = l(d, a, b, c, e[8], 6, 1873313359);
c = l(c, d, a, b, e[15], 10, -30611744);
b = l(b, c, d, a, e[6], 15, -1560198380);
a = l(a, b, c, d, e[13], 21, 1309151649);
d = l(d, a, b, c, e[4], 6, -145523070);
c = l(c, d, a, b, e[11], 10, -1120210379);
b = l(b, c, d, a, e[2], 15, 718787259);
a = l(a, b, c, d, e[9], 21, -343485551);
f[0] = m(d, f[0]);
f[1] = m(a, f[1]);
f[2] = m(b, f[2]);
f[3] = m(c, f[3])
}, g = function(f, e, d, a, b, c) {
e = m(m(e, f), m(a, c));
return m(e << b | e >>> 32 - b, d)
}
, h = function(f, e, d, a, b, c, n) {
return g(e & d | ~e & a, f, e, b, c, n)
}
, k = function(f, e, d, a, b, c, n) {
return g(e & a | d & ~a, f, e, b, c, n)
}
, l = function(f, e, d, a, b, c, n) {
return g(d ^ (e | ~a), f, e, b, c, n)
}, r = "0123456789abcdef".split("");
var m = function(f, e) {
return f + e & 4294967295
};
var q = function(f) {
var e = f.length, d = [1732584193, -271733879, -1732584194, 271733878], a;
for (a = 64; a <= f.length; a += 64) {
var b, c = f.substring(a - 64, a), g = [];
for (b = 0; 64 > b; b += 4)
g[b >> 2] = c.charCodeAt(b) + (c.charCodeAt(b + 1) << 8) + (c.charCodeAt(b + 2) << 16) + (c.charCodeAt(b + 3) << 24);
p(d, g)
}
f = f.substring(a - 64);
b = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for (a = 0; a < f.length; a++)
b[a >> 2] |= f.charCodeAt(a) << (a % 4 << 3);
b[a >> 2] |= 128 << (a % 4 << 3);
if (55 < a)
for (p(d, b),
a = 0; 16 > a; a++)
b[a] = 0;
b[14] = 8 * e;
p(d, b);
return d
};
var md5 = function(f, e, timeDifference) {
var d = Date.now() + 6E4 + timeDifference;
e = q(d + f + e);
f = [];
for (var a = 0; a < e.length; a++) {
var b = e[a];
var c = []
, g = 4;
do
c[--g] = b & 255,
b >>= 8;
while (g);
b = c;
for (c = b.length - 1; 0 <= c; c--)
f.push(b[c])
}
g = void 0;
c = "";
for (e = a = b = 0; e < 4 * f.length / 3; g = b >> 2 * (++e & 3) & 63,
c += String.fromCharCode(g + 71 - (26 > g ? 6 : 52 > g ? 0 : 62 > g ? 75 : g ^ 63 ? 90 : 87)) + (75 == (e - 1) % 76 ? "\r\n" : ""))
e & 3 ^ 3 && (b = b << 8 ^ f[a++]);
for (; e++ & 3; )
c += "\x3d";
return c.replace(/\+/g, "-").replace(/\//g, "_") + "," + d
};
"5d41402abc4b2a76b9719d911017c592" != function(f) {
for (var e = 0; e < f.length; e++) {
for (var d = e, a = f[e], b = "", c = 0; 4 > c; c++)
b += r[a >> 8 * c + 4 & 15] + r[a >> 8 * c & 15];
f[d] = b
}
return f.join("")
}(q("hello")) && (m = function(f, e) {
var d = (f & 65535) + (e & 65535);
return (f >> 16) + (e >> 16) + (d >> 16) << 16 | d & 65535
}
)
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/h264.mpd','vx54axqjal4f0yy2',-2274));
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/subtitle_pl/34.m4s','vx54axqjal4f0yy2',-2274));
""";
} }

View File

@ -16,437 +16,436 @@ using N_m3u8DL_RE.CommandLine;
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
namespace N_m3u8DL_RE namespace N_m3u8DL_RE;
internal class Program
{ {
internal class Program static async Task Main(string[] args)
{ {
static async Task Main(string[] args) Console.CancelKeyPress += Console_CancelKeyPress;
ServicePointManager.DefaultConnectionLimit = 1024;
try { Console.CursorVisible = true; } catch { }
string loc = "en-US";
string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
if (currLoc == "zh-CN" || currLoc == "zh-SG") loc = "zh-CN";
else if (currLoc.StartsWith("zh-")) loc = "zh-TW";
// 处理用户-h等请求
var index = -1;
var list = new List<string>(args);
if ((index = list.IndexOf("--ui-language")) != -1 && list.Count > index + 1 && new List<string> { "en-US", "zh-CN", "zh-TW" }.Contains(list[index + 1]))
{ {
Console.CancelKeyPress += Console_CancelKeyPress; loc = list[index + 1];
ServicePointManager.DefaultConnectionLimit = 1024;
try { Console.CursorVisible = true; } catch { }
string loc = "en-US";
string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
if (currLoc == "zh-CN" || currLoc == "zh-SG") loc = "zh-CN";
else if (currLoc.StartsWith("zh-")) loc = "zh-TW";
//处理用户-h等请求
var index = -1;
var list = new List<string>(args);
if ((index = list.IndexOf("--ui-language")) != -1 && list.Count > index + 1 && new List<string> { "en-US", "zh-CN", "zh-TW" }.Contains(list[index + 1]))
{
loc = list[index + 1];
}
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(loc);
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(loc);
await CommandInvoker.InvokeArgs(args, DoWorkAsync);
} }
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(loc);
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(loc);
await CommandInvoker.InvokeArgs(args, DoWorkAsync);
}
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
Logger.WarnMarkUp("Force Exit...");
try
{ {
Logger.WarnMarkUp("Force Exit..."); Console.CursorVisible = true;
try if (!OperatingSystem.IsWindows())
{ System.Diagnostics.Process.Start("stty", "echo");
Console.CursorVisible = true; } catch { }
if (!OperatingSystem.IsWindows()) Environment.Exit(0);
System.Diagnostics.Process.Start("stty", "echo"); }
} catch { }
Environment.Exit(0); static int GetOrder(StreamSpec streamSpec)
{
if (streamSpec.Channels == null) return 0;
var str = streamSpec.Channels.Split('/')[0];
return int.TryParse(str, out var order) ? order : 0;
}
static async Task DoWorkAsync(MyOption option)
{
if (Console.IsOutputRedirected || Console.IsErrorRedirected)
{
option.ForceAnsiConsole = true;
option.NoAnsiColor = true;
Logger.Info(ResString.consoleRedirected);
}
CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor);
// 检测更新
_ = CheckUpdateAsync();
Logger.IsWriteFile = !option.NoLog;
Logger.InitLogFile();
Logger.LogLevel = option.LogLevel;
Logger.Info(CommandInvoker.VERSION_INFO);
if (option.UseSystemProxy == false)
{
HTTPUtil.HttpClientHandler.UseProxy = false;
} }
static int GetOrder(StreamSpec streamSpec) if (option.CustomProxy != null)
{ {
if (streamSpec.Channels == null) return 0; HTTPUtil.HttpClientHandler.Proxy = option.CustomProxy;
HTTPUtil.HttpClientHandler.UseProxy = true;
var str = streamSpec.Channels.Split('/')[0];
return int.TryParse(str, out var order) ? order : 0;
} }
static async Task DoWorkAsync(MyOption option) // 检查互斥的选项
if (!option.MuxAfterDone && option.MuxImports != null && option.MuxImports.Count > 0)
{ {
throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!");
}
if (Console.IsOutputRedirected || Console.IsErrorRedirected) // LivePipeMux开启时 LiveRealTimeMerge必须开启
if (option.LivePipeMux && !option.LiveRealTimeMerge)
{
Logger.WarnMarkUp("LivePipeMux detected, forced enable LiveRealTimeMerge");
option.LiveRealTimeMerge = true;
}
// 预先检查ffmpeg
if (option.FFmpegBinaryPath == null)
option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg");
if (string.IsNullOrEmpty(option.FFmpegBinaryPath) || !File.Exists(option.FFmpegBinaryPath))
{
throw new FileNotFoundException(ResString.ffmpegNotFound);
}
Logger.Extra($"ffmpeg => {option.FFmpegBinaryPath}");
// 预先检查mkvmerge
if (option.MuxOptions != null && option.MuxOptions.UseMkvmerge && option.MuxAfterDone)
{
if (option.MkvmergeBinaryPath == null)
option.MkvmergeBinaryPath = GlobalUtil.FindExecutable("mkvmerge");
if (string.IsNullOrEmpty(option.MkvmergeBinaryPath) || !File.Exists(option.MkvmergeBinaryPath))
{ {
option.ForceAnsiConsole = true; throw new FileNotFoundException("mkvmerge not found");
option.NoAnsiColor = true;
Logger.Info(ResString.consoleRedirected);
} }
CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor); Logger.Extra($"mkvmerge => {option.MkvmergeBinaryPath}");
//检测更新 }
CheckUpdateAsync();
Logger.IsWriteFile = !option.NoLog; // 预先检查
Logger.InitLogFile(); if ((option.Keys != null && option.Keys.Length > 0) || option.KeyTextFile != null)
Logger.LogLevel = option.LogLevel; {
Logger.Info(CommandInvoker.VERSION_INFO); if (string.IsNullOrEmpty(option.DecryptionBinaryPath))
if (option.UseSystemProxy == false)
{ {
HTTPUtil.HttpClientHandler.UseProxy = false; if (option.UseShakaPackager)
}
if (option.CustomProxy != null)
{
HTTPUtil.HttpClientHandler.Proxy = option.CustomProxy;
HTTPUtil.HttpClientHandler.UseProxy = true;
}
//检查互斥的选项
if (!option.MuxAfterDone && option.MuxImports != null && option.MuxImports.Count > 0)
{
throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!");
}
//LivePipeMux开启时 LiveRealTimeMerge必须开启
if (option.LivePipeMux && !option.LiveRealTimeMerge)
{
Logger.WarnMarkUp("LivePipeMux detected, forced enable LiveRealTimeMerge");
option.LiveRealTimeMerge = true;
}
//预先检查ffmpeg
if (option.FFmpegBinaryPath == null)
option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg");
if (string.IsNullOrEmpty(option.FFmpegBinaryPath) || !File.Exists(option.FFmpegBinaryPath))
{
throw new FileNotFoundException(ResString.ffmpegNotFound);
}
Logger.Extra($"ffmpeg => {option.FFmpegBinaryPath}");
//预先检查mkvmerge
if (option.MuxOptions != null && option.MuxOptions.UseMkvmerge && option.MuxAfterDone)
{
if (option.MkvmergeBinaryPath == null)
option.MkvmergeBinaryPath = GlobalUtil.FindExecutable("mkvmerge");
if (string.IsNullOrEmpty(option.MkvmergeBinaryPath) || !File.Exists(option.MkvmergeBinaryPath))
{ {
throw new FileNotFoundException("mkvmerge not found"); var file = GlobalUtil.FindExecutable("shaka-packager");
var file2 = GlobalUtil.FindExecutable("packager-linux-x64");
var file3 = GlobalUtil.FindExecutable("packager-osx-x64");
var file4 = GlobalUtil.FindExecutable("packager-win-x64");
if (file == null && file2 == null && file3 == null && file4 == null) throw new FileNotFoundException("shaka-packager not found!");
option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}");
} }
Logger.Extra($"mkvmerge => {option.MkvmergeBinaryPath}"); else
}
//预先检查
if ((option.Keys != null && option.Keys.Length > 0) || option.KeyTextFile != null)
{
if (string.IsNullOrEmpty(option.DecryptionBinaryPath))
{ {
if (option.UseShakaPackager) var file = GlobalUtil.FindExecutable("mp4decrypt");
{ if (file == null) throw new FileNotFoundException("mp4decrypt not found!");
var file = GlobalUtil.FindExecutable("shaka-packager"); option.DecryptionBinaryPath = file;
var file2 = GlobalUtil.FindExecutable("packager-linux-x64"); Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}");
var file3 = GlobalUtil.FindExecutable("packager-osx-x64");
var file4 = GlobalUtil.FindExecutable("packager-win-x64");
if (file == null && file2 == null && file3 == null && file4 == null) throw new FileNotFoundException("shaka-packager not found!");
option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}");
}
else
{
var file = GlobalUtil.FindExecutable("mp4decrypt");
if (file == null) throw new FileNotFoundException("mp4decrypt not found!");
option.DecryptionBinaryPath = file;
Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}");
}
}
else if (!File.Exists(option.DecryptionBinaryPath))
{
throw new FileNotFoundException(option.DecryptionBinaryPath);
} }
} }
else if (!File.Exists(option.DecryptionBinaryPath))
//默认的headers
var headers = 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" throw new FileNotFoundException(option.DecryptionBinaryPath);
};
//添加或替换用户输入的headers
foreach (var item in option.Headers)
{
headers[item.Key] = item.Value;
Logger.Extra($"User-Defined Header => {item.Key}: {item.Value}");
} }
}
var parserConfig = new ParserConfig() // 默认的headers
var headers = 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"
};
// 添加或替换用户输入的headers
foreach (var item in option.Headers)
{
headers[item.Key] = item.Value;
Logger.Extra($"User-Defined Header => {item.Key}: {item.Value}");
}
var parserConfig = new ParserConfig()
{
AppendUrlParams = option.AppendUrlParams,
UrlProcessorArgs = option.UrlProcessorArgs,
BaseUrl = option.BaseUrl!,
Headers = headers,
CustomMethod = option.CustomHLSMethod,
CustomeKey = option.CustomHLSKey,
CustomeIV = option.CustomHLSIv,
};
// demo1
parserConfig.ContentProcessors.Insert(0, new DemoProcessor());
// demo2
parserConfig.KeyProcessors.Insert(0, new DemoProcessor2());
// for www.nowehoryzonty.pl
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
// 等待任务开始时间
if (option.TaskStartAt != null && option.TaskStartAt > DateTime.Now)
{
Logger.InfoMarkUp(ResString.taskStartAt + option.TaskStartAt);
while (option.TaskStartAt > DateTime.Now)
{ {
AppendUrlParams = option.AppendUrlParams, await Task.Delay(1000);
UrlProcessorArgs = option.UrlProcessorArgs,
BaseUrl = option.BaseUrl!,
Headers = headers,
CustomMethod = option.CustomHLSMethod,
CustomeKey = option.CustomHLSKey,
CustomeIV = option.CustomHLSIv,
};
//demo1
parserConfig.ContentProcessors.Insert(0, new DemoProcessor());
//demo2
parserConfig.KeyProcessors.Insert(0, new DemoProcessor2());
//for www.nowehoryzonty.pl
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
//等待任务开始时间
if (option.TaskStartAt != null && option.TaskStartAt > DateTime.Now)
{
Logger.InfoMarkUp(ResString.taskStartAt + option.TaskStartAt);
while (option.TaskStartAt > DateTime.Now)
{
await Task.Delay(1000);
}
} }
}
var url = option.Input; var url = option.Input;
//流提取器配置 // 流提取器配置
var extractor = new StreamExtractor(parserConfig); var extractor = new StreamExtractor(parserConfig);
// 从链接加载内容 // 从链接加载内容
await RetryUtil.WebRequestRetryAsync(async () => await RetryUtil.WebRequestRetryAsync(async () =>
{
await extractor.LoadSourceFromUrlAsync(url);
return true;
});
// 解析流信息
var streams = await extractor.ExtractStreamsAsync();
// 全部媒体
var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder);
// 基本流
var basicStreams = lists.Where(x => x.MediaType == null || x.MediaType == MediaType.VIDEO);
// 可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
// 可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
// 尝试从URL或文件读取文件名
if (string.IsNullOrEmpty(option.SaveName))
{
option.SaveName = OtherUtil.GetFileNameFromInput(option.Input);
}
// 生成文件夹
var tmpDir = Path.Combine(option.TmpDir ?? Environment.CurrentDirectory, $"{option.SaveName ?? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}");
// 记录文件
extractor.RawFiles["meta.json"] = GlobalUtil.ConvertToJson(lists);
// 写出文件
await WriteRawFilesAsync(option, extractor, tmpDir);
Logger.Info(ResString.streamsInfo, lists.Count(), basicStreams.Count(), audios.Count(), subs.Count());
foreach (var item in lists)
{
Logger.InfoMarkUp(item.ToString());
}
var selectedStreams = new List<StreamSpec>();
if (option.DropVideoFilter != null || option.DropAudioFilter != null || option.DropSubtitleFilter != null)
{
basicStreams = FilterUtil.DoFilterDrop(basicStreams, option.DropVideoFilter);
audios = FilterUtil.DoFilterDrop(audios, option.DropAudioFilter);
subs = FilterUtil.DoFilterDrop(subs, option.DropSubtitleFilter);
lists = basicStreams.Concat(audios).Concat(subs).OrderBy(x => true);
}
if (option.DropVideoFilter != null) Logger.Extra($"DropVideoFilter => {option.DropVideoFilter}");
if (option.DropAudioFilter != null) Logger.Extra($"DropAudioFilter => {option.DropAudioFilter}");
if (option.DropSubtitleFilter != null) Logger.Extra($"DropSubtitleFilter => {option.DropSubtitleFilter}");
if (option.VideoFilter != null) Logger.Extra($"VideoFilter => {option.VideoFilter}");
if (option.AudioFilter != null) Logger.Extra($"AudioFilter => {option.AudioFilter}");
if (option.SubtitleFilter != null) Logger.Extra($"SubtitleFilter => {option.SubtitleFilter}");
if (option.AutoSelect)
{
if (basicStreams.Any())
selectedStreams.Add(basicStreams.First());
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
foreach (var lang in langs)
{ {
await extractor.LoadSourceFromUrlAsync(url); selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).ThenByDescending(GetOrder).First());
return true;
});
//解析流信息
var streams = await extractor.ExtractStreamsAsync();
//全部媒体
var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder);
//基本流
var basicStreams = lists.Where(x => x.MediaType == null || x.MediaType == MediaType.VIDEO);
//可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
//可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
//尝试从URL或文件读取文件名
if (string.IsNullOrEmpty(option.SaveName))
{
option.SaveName = OtherUtil.GetFileNameFromInput(option.Input);
} }
selectedStreams.AddRange(subs);
}
else if (option.SubOnly)
{
selectedStreams.AddRange(subs);
}
else if (option.VideoFilter != null || option.AudioFilter != null || option.SubtitleFilter != null)
{
basicStreams = FilterUtil.DoFilterKeep(basicStreams, option.VideoFilter);
audios = FilterUtil.DoFilterKeep(audios, option.AudioFilter);
subs = FilterUtil.DoFilterKeep(subs, option.SubtitleFilter);
selectedStreams = basicStreams.Concat(audios).Concat(subs).ToList();
}
else
{
// 展示交互式选择框
selectedStreams = FilterUtil.SelectStreams(lists);
}
//生成文件夹 if (!selectedStreams.Any())
var tmpDir = Path.Combine(option.TmpDir ?? Environment.CurrentDirectory, $"{option.SaveName ?? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}"); throw new Exception(ResString.noStreamsToDownload);
//记录文件
extractor.RawFiles["meta.json"] = GlobalUtil.ConvertToJson(lists);
//写出文件
await WriteRawFilesAsync(option, extractor, tmpDir);
Logger.Info(ResString.streamsInfo, lists.Count(), basicStreams.Count(), audios.Count(), subs.Count()); // HLS: 选中流中若有没加载出playlist的加载playlist
// DASH/MSS: 加载playlist (调用url预处理器)
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS)
await extractor.FetchPlayListAsync(selectedStreams);
foreach (var item in lists) // 直播检测
{ var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod;
Logger.InfoMarkUp(item.ToString()); if (livingFlag)
} {
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]");
}
var selectedStreams = new List<StreamSpec>(); // 无法识别的加密方式,自动开启二进制合并
if (option.DropVideoFilter != null || option.DropAudioFilter != null || option.DropSubtitleFilter != null) if (selectedStreams.Any(s => s.Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN))))
{ {
basicStreams = FilterUtil.DoFilterDrop(basicStreams, option.DropVideoFilter); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]");
audios = FilterUtil.DoFilterDrop(audios, option.DropAudioFilter); option.BinaryMerge = true;
subs = FilterUtil.DoFilterDrop(subs, option.DropSubtitleFilter); }
lists = basicStreams.Concat(audios).Concat(subs).OrderBy(x => true);
}
if (option.DropVideoFilter != null) Logger.Extra($"DropVideoFilter => {option.DropVideoFilter}"); // 应用用户自定义的分片范围
if (option.DropAudioFilter != null) Logger.Extra($"DropAudioFilter => {option.DropAudioFilter}"); if (!livingFlag)
if (option.DropSubtitleFilter != null) Logger.Extra($"DropSubtitleFilter => {option.DropSubtitleFilter}"); FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange);
if (option.VideoFilter != null) Logger.Extra($"VideoFilter => {option.VideoFilter}");
if (option.AudioFilter != null) Logger.Extra($"AudioFilter => {option.AudioFilter}");
if (option.SubtitleFilter != null) Logger.Extra($"SubtitleFilter => {option.SubtitleFilter}");
if (option.AutoSelect) // 应用用户自定义的广告分片关键字
{ FilterUtil.CleanAd(selectedStreams, option.AdKeywords);
if (basicStreams.Any())
selectedStreams.Add(basicStreams.First());
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
foreach (var lang in langs)
{
selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).ThenByDescending(GetOrder).First());
}
selectedStreams.AddRange(subs);
}
else if (option.SubOnly)
{
selectedStreams.AddRange(subs);
}
else if (option.VideoFilter != null || option.AudioFilter != null || option.SubtitleFilter != null)
{
basicStreams = FilterUtil.DoFilterKeep(basicStreams, option.VideoFilter);
audios = FilterUtil.DoFilterKeep(audios, option.AudioFilter);
subs = FilterUtil.DoFilterKeep(subs, option.SubtitleFilter);
selectedStreams = basicStreams.Concat(audios).Concat(subs).ToList();
}
else
{
//展示交互式选择框
selectedStreams = FilterUtil.SelectStreams(lists);
}
if (!selectedStreams.Any()) // 记录文件
throw new Exception(ResString.noStreamsToDownload); extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams);
//HLS: 选中流中若有没加载出playlist的加载playlist Logger.Info(ResString.selectedStream);
//DASH/MSS: 加载playlist (调用url预处理器) foreach (var item in selectedStreams)
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS) {
await extractor.FetchPlayListAsync(selectedStreams); Logger.InfoMarkUp(item.ToString());
}
//直播检测 // 写出文件
var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod; await WriteRawFilesAsync(option, extractor, tmpDir);
if (livingFlag)
{
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]");
}
//无法识别的加密方式,自动开启二进制合并 if (option.SkipDownload)
if (selectedStreams.Any(s => s.Playlist.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN)))) {
{ return;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]"); }
option.BinaryMerge = true;
}
//应用用户自定义的分片范围
if (!livingFlag)
FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange);
//应用用户自定义的广告分片关键字
FilterUtil.CleanAd(selectedStreams, option.AdKeywords);
//记录文件
extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams);
Logger.Info(ResString.selectedStream);
foreach (var item in selectedStreams)
{
Logger.InfoMarkUp(item.ToString());
}
//写出文件
await WriteRawFilesAsync(option, extractor, tmpDir);
if (option.SkipDownload)
{
return;
}
#if DEBUG #if DEBUG
Console.WriteLine("Press any key to continue..."); Console.WriteLine("Press any key to continue...");
Console.ReadKey(); Console.ReadKey();
#endif #endif
Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]"); Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]");
//开始MuxAfterDone后自动使用二进制版 // 开始MuxAfterDone后自动使用二进制版
if (!option.BinaryMerge && option.MuxAfterDone) if (!option.BinaryMerge && option.MuxAfterDone)
{ {
option.BinaryMerge = true; option.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]");
}
//下载配置
var downloadConfig = new DownloaderConfig()
{
MyOptions = option,
DirPrefix = tmpDir,
Headers = parserConfig.Headers, //使用命令行解析得到的Headers
};
var result = false;
if (extractor.ExtractorType == ExtractorType.HTTP_LIVE)
{
var sldm = new HTTPLiveRecordManager(downloadConfig, selectedStreams, extractor);
result = await sldm.StartRecordAsync();
}
else if (!livingFlag)
{
//开始下载
var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor);
result = await sdm.StartDownloadAsync();
}
else
{
var sldm = new SimpleLiveRecordManager2(downloadConfig, selectedStreams, extractor);
result = await sldm.StartRecordAsync();
}
if (result)
{
Logger.InfoMarkUp("[white on green]Done[/]");
}
else
{
Logger.ErrorMarkUp("[white on red]Failed[/]");
Environment.ExitCode = 1;
}
} }
private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir) // 下载配置
var downloadConfig = new DownloaderConfig()
{ {
//写出json文件 MyOptions = option,
if (option.WriteMetaJson) DirPrefix = tmpDir,
{ Headers = parserConfig.Headers, // 使用命令行解析得到的Headers
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); };
Logger.Warn(ResString.writeJson);
foreach (var item in extractor.RawFiles) var result = false;
{
var file = Path.Combine(tmpDir, item.Key); if (extractor.ExtractorType == ExtractorType.HTTP_LIVE)
if (!File.Exists(file)) await File.WriteAllTextAsync(file, item.Value, Encoding.UTF8); {
} var sldm = new HTTPLiveRecordManager(downloadConfig, selectedStreams, extractor);
} result = await sldm.StartRecordAsync();
}
else if (!livingFlag)
{
// 开始下载
var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor);
result = await sdm.StartDownloadAsync();
}
else
{
var sldm = new SimpleLiveRecordManager2(downloadConfig, selectedStreams, extractor);
result = await sldm.StartRecordAsync();
} }
static async Task CheckUpdateAsync() if (result)
{ {
try Logger.InfoMarkUp("[white on green]Done[/]");
{
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!;
string nowVer = $"v{ver.Major}.{ver.Minor}.{ver.Build}";
string redirctUrl = await Get302Async("https://github.com/nilaoda/N_m3u8DL-RE/releases/latest");
string latestVer = redirctUrl.Replace("https://github.com/nilaoda/N_m3u8DL-RE/releases/tag/", "");
if (!latestVer.StartsWith(nowVer) && !latestVer.StartsWith("https"))
{
Console.Title = $"{ResString.newVersionFound} {latestVer}";
Logger.InfoMarkUp($"[cyan]{ResString.newVersionFound}[/] [red]{latestVer}[/]");
}
}
catch (Exception)
{
;
}
} }
else
//重定向
static async Task<string> Get302Async(string url)
{ {
//this allows you to set the settings so that we can get the redirect url Logger.ErrorMarkUp("[white on red]Failed[/]");
var handler = new HttpClientHandler() Environment.ExitCode = 1;
{
AllowAutoRedirect = false
};
string redirectedUrl = "";
using (HttpClient client = new(handler))
using (HttpResponseMessage response = await client.GetAsync(url))
using (HttpContent content = response.Content)
{
// ... Read the response to see if we have the redirected url
if (response.StatusCode == System.Net.HttpStatusCode.Found)
{
HttpResponseHeaders headers = response.Headers;
if (headers != null && headers.Location != null)
{
redirectedUrl = headers.Location.AbsoluteUri;
}
}
}
return redirectedUrl;
} }
} }
private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir)
{
// 写出json文件
if (option.WriteMetaJson)
{
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
Logger.Warn(ResString.writeJson);
foreach (var item in extractor.RawFiles)
{
var file = Path.Combine(tmpDir, item.Key);
if (!File.Exists(file)) await File.WriteAllTextAsync(file, item.Value, Encoding.UTF8);
}
}
}
static async Task CheckUpdateAsync()
{
try
{
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!;
string nowVer = $"v{ver.Major}.{ver.Minor}.{ver.Build}";
string redirctUrl = await Get302Async("https://github.com/nilaoda/N_m3u8DL-RE/releases/latest");
string latestVer = redirctUrl.Replace("https://github.com/nilaoda/N_m3u8DL-RE/releases/tag/", "");
if (!latestVer.StartsWith(nowVer) && !latestVer.StartsWith("https"))
{
Console.Title = $"{ResString.newVersionFound} {latestVer}";
Logger.InfoMarkUp($"[cyan]{ResString.newVersionFound}[/] [red]{latestVer}[/]");
}
}
catch (Exception)
{
;
}
}
// 重定向
static async Task<string> Get302Async(string url)
{
// this allows you to set the settings so that we can get the redirect url
var handler = new HttpClientHandler()
{
AllowAutoRedirect = false
};
string redirectedUrl = "";
using (HttpClient client = new(handler))
using (HttpResponseMessage response = await client.GetAsync(url))
using (HttpContent content = response.Content)
{
// ... Read the response to see if we have the redirected url
if (response.StatusCode == System.Net.HttpStatusCode.Found)
{
HttpResponseHeaders headers = response.Headers;
if (headers != null && headers.Location != null)
{
redirectedUrl = headers.Location.AbsoluteUri;
}
}
}
return redirectedUrl;
}
} }

View File

@ -2,8 +2,6 @@
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using System.IO;
using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
@ -111,16 +109,16 @@ internal static class DownloadUtil
size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token); size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);
speedContainer.Add(size); speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size); await stream.WriteAsync(buffer, 0, size);
//检测imageHeader // 检测imageHeader
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer); bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
//检测GZipFor DDP Audio // 检测GZipFor DDP Audio
bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b; bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0) while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
{ {
speedContainer.Add(size); speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size); await stream.WriteAsync(buffer, 0, size);
//限速策略 // 限速策略
while (speedContainer.Downloaded > speedContainer.SpeedLimit) while (speedContainer.Downloaded > speedContainer.SpeedLimit)
{ {
await Task.Delay(1); await Task.Delay(1);

View File

@ -4,12 +4,7 @@ using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
@ -17,7 +12,7 @@ public static class FilterUtil
{ {
public static List<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> lists, StreamFilter? filter) public static List<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{ {
if (filter == null) return new List<StreamSpec>(); if (filter == null) return [];
var inputs = lists.Where(_ => true); var inputs = lists.Where(_ => true);
if (filter.GroupIdReg != null) if (filter.GroupIdReg != null)
@ -56,13 +51,13 @@ public static class FilterUtil
var bestNumberStr = filter.For.Replace("best", ""); var bestNumberStr = filter.For.Replace("best", "");
var worstNumberStr = filter.For.Replace("worst", ""); var worstNumberStr = filter.For.Replace("worst", "");
if (filter.For == "best" && inputs.Count() > 0) if (filter.For == "best" && inputs.Any())
inputs = inputs.Take(1).ToList(); inputs = inputs.Take(1).ToList();
else if (filter.For == "worst" && inputs.Count() > 0) else if (filter.For == "worst" && inputs.Any())
inputs = inputs.TakeLast(1).ToList(); inputs = inputs.TakeLast(1).ToList();
else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0) else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Any())
inputs = inputs.Take(bestNumber).ToList(); inputs = inputs.Take(bestNumber).ToList();
else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0) else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Any())
inputs = inputs.TakeLast(worstNumber).ToList(); inputs = inputs.TakeLast(worstNumber).ToList();
return inputs.ToList(); return inputs.ToList();
@ -82,15 +77,16 @@ public static class FilterUtil
public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists) public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
{ {
if (lists.Count() == 1) var streamSpecs = lists.ToList();
return new List<StreamSpec>(lists); if (streamSpecs.Count == 1)
return [..streamSpecs];
//基本流 // 基本流
var basicStreams = lists.Where(x => x.MediaType == null); var basicStreams = streamSpecs.Where(x => x.MediaType == null).ToList();
//可选音频轨道 // 可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO); var audios = streamSpecs.Where(x => x.MediaType == MediaType.AUDIO).ToList();
//可选字幕轨道 // 可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES); var subs = streamSpecs.Where(x => x.MediaType == MediaType.SUBTITLES).ToList();
var prompt = new MultiSelectionPrompt<StreamSpec>() var prompt = new MultiSelectionPrompt<StreamSpec>()
.Title(ResString.promptTitle) .Title(ResString.promptTitle)
@ -107,8 +103,8 @@ public static class FilterUtil
.InstructionsText(ResString.promptInfo) .InstructionsText(ResString.promptInfo)
; ;
//默认选中第一个 // 默认选中第一个
var first = lists.First(); var first = streamSpecs.First();
prompt.Select(first); prompt.Select(first);
if (basicStreams.Any()) if (basicStreams.Any())
@ -119,7 +115,7 @@ public static class FilterUtil
if (audios.Any()) if (audios.Any())
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios);
//默认音轨 // 默认音轨
if (first.AudioId != null) if (first.AudioId != null)
{ {
prompt.Select(audios.First(a => a.GroupId == first.AudioId)); prompt.Select(audios.First(a => a.GroupId == first.AudioId));
@ -128,17 +124,17 @@ public static class FilterUtil
if (subs.Any()) if (subs.Any())
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs);
//默认字幕轨 // 默认字幕轨
if (first.SubtitleId != null) if (first.SubtitleId != null)
{ {
prompt.Select(subs.First(s => s.GroupId == first.SubtitleId)); prompt.Select(subs.First(s => s.GroupId == first.SubtitleId));
} }
} }
//如果此时还是没有选中任何流,自动选择一个 // 如果此时还是没有选中任何流,自动选择一个
prompt.Select(basicStreams.Concat(audios).Concat(subs).First()); prompt.Select(basicStreams.Concat(audios).Concat(subs).First());
//多选 // 多选
var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt); var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt);
return selectedStreams; return selectedStreams;
@ -147,11 +143,11 @@ public static class FilterUtil
/// <summary> /// <summary>
/// 直播使用。对齐各个轨道的起始。 /// 直播使用。对齐各个轨道的起始。
/// </summary> /// </summary>
/// <param name="streams"></param> /// <param name="selectedSteams"></param>
/// <param name="takeLastCount"></param> /// <param name="takeLastCount"></param>
public static void SyncStreams(List<StreamSpec> selectedSteams, int takeLastCount = 15) public static void SyncStreams(List<StreamSpec> selectedSteams, int takeLastCount = 15)
{ {
//通过Date同步 // 通过Date同步
if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null))) if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null)))
{ {
var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!; var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!;
@ -159,12 +155,12 @@ public static class FilterUtil
{ {
foreach (var part in item.Playlist!.MediaParts) foreach (var part in item.Playlist!.MediaParts)
{ {
//秒级同步 忽略毫秒 // 秒级同步 忽略毫秒
part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList(); part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList();
} }
} }
} }
else //通过index同步 else // 通过index同步
{ {
var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index)); var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index));
foreach (var item in selectedSteams) foreach (var item in selectedSteams)
@ -176,7 +172,7 @@ public static class FilterUtil
} }
} }
//取最新的N个分片 // 取最新的N个分片
if (selectedSteams.Any(x => x.Playlist!.MediaParts[0].MediaSegments.Count > takeLastCount)) if (selectedSteams.Any(x => x.Playlist!.MediaParts[0].MediaSegments.Count > takeLastCount))
{ {
var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1; var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1;
@ -198,17 +194,15 @@ public static class FilterUtil
/// <param name="customRange"></param> /// <param name="customRange"></param>
public static void ApplyCustomRange(List<StreamSpec> selectedSteams, CustomRange? customRange) public static void ApplyCustomRange(List<StreamSpec> selectedSteams, CustomRange? customRange)
{ {
var resultList = selectedSteams.Select(x => 0d).ToList();
if (customRange == null) return; if (customRange == null) return;
Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]"); Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]");
Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]");
var filteByIndex = customRange.StartSegIndex != null && customRange.EndSegIndex != null; var filterByIndex = customRange is { StartSegIndex: not null, EndSegIndex: not null };
var filteByTime = customRange.StartSec != null && customRange.EndSec != null; var filterByTime = customRange is { StartSec: not null, EndSec: not null };
if (!filteByIndex && !filteByTime) if (!filterByIndex && !filterByTime)
{ {
Logger.ErrorMarkUp(ResString.customRangeInvalid); Logger.ErrorMarkUp(ResString.customRangeInvalid);
return; return;
@ -220,8 +214,8 @@ public static class FilterUtil
if (stream.Playlist == null) continue; if (stream.Playlist == null) continue;
foreach (var part in stream.Playlist.MediaParts) foreach (var part in stream.Playlist.MediaParts)
{ {
var newSegments = new List<MediaSegment>(); List<MediaSegment> newSegments;
if (filteByIndex) if (filterByIndex)
newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList(); newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList();
else else
newSegments = part.MediaSegments.Where(seg => stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) >= customRange.StartSec newSegments = part.MediaSegments.Where(seg => stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) >= customRange.StartSec
@ -239,11 +233,11 @@ public static class FilterUtil
/// 根据用户输入,清除广告分片 /// 根据用户输入,清除广告分片
/// </summary> /// </summary>
/// <param name="selectedSteams"></param> /// <param name="selectedSteams"></param>
/// <param name="customRange"></param> /// <param name="keywords"></param>
public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords) public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords)
{ {
if (keywords == null) return; if (keywords == null) return;
var regList = keywords.Select(s => new Regex(s)); var regList = keywords.Select(s => new Regex(s)).ToList();
foreach ( var reg in regList) foreach ( var reg in regList)
{ {
Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]"); Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]");
@ -257,19 +251,16 @@ public static class FilterUtil
foreach (var part in stream.Playlist.MediaParts) foreach (var part in stream.Playlist.MediaParts)
{ {
//没有找到广告分片 // 没有找到广告分片
if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url)))) if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url))))
{ {
continue; continue;
} }
//找到广告分片 清理 // 找到广告分片 清理
else part.MediaSegments = part.MediaSegments.Where(x => regList.All(reg => !reg.IsMatch(x.Url))).ToList();
{
part.MediaSegments = part.MediaSegments.Where(x => regList.All(reg => !reg.IsMatch(x.Url))).ToList();
}
} }
//清理已经为空的 part // 清理已经为空的 part
stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList(); stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList();
var countAfter = stream.SegmentsCount; var countAfter = stream.SegmentsCount;

View File

@ -5,17 +5,17 @@ internal static class ImageHeaderUtil
public static bool IsImageHeader(byte[] bArr) public static bool IsImageHeader(byte[] bArr)
{ {
var size = bArr.Length; var size = bArr.Length;
//PNG HEADER检测 // PNG HEADER检测
if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3]) if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3])
return true; return true;
//GIF HEADER检测 // GIF HEADER检测
else if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3]) if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3])
return true; return true;
//BMP HEADER检测 // BMP HEADER检测
else if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8]) if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8])
return true; return true;
//JPEG HEADER检测 // JPEG HEADER检测
else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2]) if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2])
return true; return true;
return false; return false;
} }
@ -24,7 +24,7 @@ internal static class ImageHeaderUtil
{ {
var sourceData = await File.ReadAllBytesAsync(sourcePath); var sourceData = await File.ReadAllBytesAsync(sourcePath);
//PNG HEADER // PNG HEADER
if (137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3]) if (137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3])
{ {
if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119]) if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119])
@ -37,7 +37,7 @@ internal static class ImageHeaderUtil
sourceData = sourceData[771..]; sourceData = sourceData[771..];
else else
{ {
//手动查询结尾标记 0x47 出现两次 // 手动查询结尾标记 0x47 出现两次
int skip = 0; int skip = 0;
for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)
{ {
@ -50,20 +50,20 @@ internal static class ImageHeaderUtil
sourceData = sourceData[skip..]; sourceData = sourceData[skip..];
} }
} }
//GIF HEADER // GIF HEADER
else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3]) else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3])
{ {
sourceData = sourceData[42..]; sourceData = sourceData[42..];
} }
//BMP HEADER // BMP HEADER
else if (0x42 == sourceData[0] && 0x4D == sourceData[1] && 0x00 == sourceData[5] && 0x00 == sourceData[6] && 0x00 == sourceData[7] && 0x00 == sourceData[8]) else if (0x42 == sourceData[0] && 0x4D == sourceData[1] && 0x00 == sourceData[5] && 0x00 == sourceData[6] && 0x00 == sourceData[7] && 0x00 == sourceData[8])
{ {
sourceData = sourceData[0x3E..]; sourceData = sourceData[0x3E..];
} }
//JPEG HEADER检测 // JPEG HEADER检测
else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2]) else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2])
{ {
//手动查询结尾标记 0x47 出现两次 // 手动查询结尾标记 0x47 出现两次
int skip = 0; int skip = 0;
for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)
{ {

View File

@ -1,33 +1,19 @@
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
class Language internal class Language(string extendCode, string code, string desc, string descA)
{ {
public string Code; public readonly string Code = code;
public string ExtendCode; public readonly string ExtendCode = extendCode;
public string Description; public readonly string Description = desc;
public string DescriptionAudio; public readonly string DescriptionAudio = descA;
public Language(string extendCode, string code, string desc, string descA)
{
Code = code;
ExtendCode = extendCode;
Description = desc;
DescriptionAudio = descA;
}
} }
internal static class LanguageCodeUtil internal static class LanguageCodeUtil
{ {
private readonly static List<Language> ALL_LANGS = @" private static readonly List<Language> ALL_LANGS = @"
af;afr;Afrikaans;Afrikaans af;afr;Afrikaans;Afrikaans
af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa) af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa)
am;amh;Amharic;Amharic am;amh;Amharic;Amharic
@ -389,8 +375,8 @@ MA;msa;Melayu;Melayu
" "
.Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => .Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x =>
{ {
var arr = x.Trim().Split(';'); var arr = x.Trim().Split(';', StringSplitOptions.TrimEntries);
return new Language(arr[0].Trim(), arr[1].Trim(), arr[2].Trim(), arr[3].Trim()); return new Language(arr[0], arr[1], arr[2], arr[3]);
}).ToList(); }).ToList();
private static Dictionary<string, string> CODE_MAP = @" private static Dictionary<string, string> CODE_MAP = @"
@ -504,8 +490,7 @@ sr;srp
private static string ConvertTwoToThree(string input) private static string ConvertTwoToThree(string input)
{ {
if (CODE_MAP.TryGetValue(input, out var code)) return code; return CODE_MAP.GetValueOrDefault(input, input);
return input;
} }
/// <summary> /// <summary>
@ -518,12 +503,12 @@ sr;srp
if (string.IsNullOrEmpty(outputFile.LangCode)) return; if (string.IsNullOrEmpty(outputFile.LangCode)) return;
var originalLangCode = outputFile.LangCode; var originalLangCode = outputFile.LangCode;
//先直接查找 // 先直接查找
var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase)); var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase));
//处理特殊的扩展语言标记 // 处理特殊的扩展语言标记
if (lang == null) if (lang == null)
{ {
//2位转3位 // 2位转3位
var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First()); var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First());
lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase)); lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase));
} }
@ -536,10 +521,10 @@ sr;srp
} }
else if (outputFile.LangCode == null) else if (outputFile.LangCode == null)
{ {
outputFile.LangCode = "und"; //无法识别直接置为und outputFile.LangCode = "und"; // 无法识别直接置为und
} }
//无描述则把LangCode当作描述 // 无描述则把LangCode当作描述
if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode; if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode;
} }
} }

View File

@ -1,13 +1,6 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using NiL.JS.Expressions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
@ -15,17 +8,16 @@ internal static class LargeSingleFileSplitUtil
{ {
class Clip class Clip
{ {
public required int index; public required int Index;
public required long from; public required long From;
public required long to; public required long To;
} }
/// <summary> /// <summary>
/// URL大文件切片处理 /// URL大文件切片处理
/// </summary> /// </summary>
/// <param name="url"></param> /// <param name="segment"></param>
/// <param name="headers"></param> /// <param name="headers"></param>
/// <param name="splitSegments"></param>
/// <returns></returns> /// <returns></returns>
public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers) public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers)
{ {
@ -43,10 +35,10 @@ internal static class LargeSingleFileSplitUtil
{ {
splitSegments.Add(new MediaSegment() splitSegments.Add(new MediaSegment()
{ {
Index = clip.index, Index = clip.Index,
Url = url, Url = url,
StartRange = clip.from, StartRange = clip.From,
ExpectLength = clip.to == -1 ? null : clip.to - clip.from + 1, ExpectLength = clip.To == -1 ? null : clip.To - clip.From + 1,
EncryptInfo = segment.EncryptInfo, EncryptInfo = segment.EncryptInfo,
}); });
} }
@ -85,7 +77,7 @@ internal static class LargeSingleFileSplitUtil
return totalSizeBytes; return totalSizeBytes;
} }
//此函数主要是切片下载逻辑 // 此函数主要是切片下载逻辑
private static List<Clip> GetAllClips(string url, long fileSize) private static List<Clip> GetAllClips(string url, long fileSize)
{ {
List<Clip> clips = new(); List<Clip> clips = new();
@ -96,11 +88,11 @@ internal static class LargeSingleFileSplitUtil
{ {
Clip c = new() Clip c = new()
{ {
index = index, Index = index,
from = counter, From = counter,
to = counter + perSize To = counter + perSize
}; };
//没到最后 // 没到最后
if (fileSize - perSize > 0) if (fileSize - perSize > 0)
{ {
fileSize -= perSize; fileSize -= perSize;
@ -108,10 +100,10 @@ internal static class LargeSingleFileSplitUtil
index++; index++;
clips.Add(c); clips.Add(c);
} }
//已到最后 // 已到最后
else else
{ {
c.to = -1; c.To = -1;
clips.Add(c); clips.Add(c);
break; break;
} }

View File

@ -9,7 +9,7 @@ namespace N_m3u8DL_RE.Util;
internal static class MP4DecryptUtil internal static class MP4DecryptUtil
{ {
private static string ZeroKid = "00000000000000000000000000000000"; private static readonly string ZeroKid = "00000000000000000000000000000000";
public static async Task<bool> DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false) public static async Task<bool> DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false)
{ {
if (keys == null || keys.Length == 0) return false; if (keys == null || keys.Length == 0) return false;
@ -25,7 +25,7 @@ internal static class MP4DecryptUtil
if (!string.IsNullOrEmpty(kid)) if (!string.IsNullOrEmpty(kid))
{ {
var test = keyPairs.Where(k => k.StartsWith(kid)); var test = keyPairs.Where(k => k.StartsWith(kid)).ToList();
if (test.Any()) keyPair = test.First(); if (test.Any()) keyPair = test.First();
} }
@ -45,16 +45,16 @@ internal static class MP4DecryptUtil
if (keyPair == null) return false; if (keyPair == null) return false;
//shakaPackager 无法单独解密init文件 // shakaPackager 无法单独解密init文件
if (source.EndsWith("_init.mp4") && shakaPackager) return false; if (source.EndsWith("_init.mp4") && shakaPackager) return false;
var cmd = ""; string cmd;
var tmpFile = ""; var tmpFile = "";
if (shakaPackager) if (shakaPackager)
{ {
var enc = source; var enc = source;
//shakaPackager 手动构造文件 // shakaPackager 手动构造文件
if (init != "") if (init != "")
{ {
tmpFile = Path.ChangeExtension(source, ".itmp"); tmpFile = Path.ChangeExtension(source, ".itmp");
@ -101,8 +101,8 @@ internal static class MP4DecryptUtil
{ {
FileName = name, FileName = name,
Arguments = arg, Arguments = arg,
//RedirectStandardOutput = true, // RedirectStandardOutput = true,
//RedirectStandardError = true, // RedirectStandardError = true,
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false UseShellExecute = false
})!.WaitForExitAsync(); })!.WaitForExitAsync();
@ -124,8 +124,7 @@ internal static class MP4DecryptUtil
Logger.InfoMarkUp(ResString.searchKey); Logger.InfoMarkUp(ResString.searchKey);
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
var line = ""; while (await reader.ReadLineAsync() is { } line)
while ((line = await reader.ReadLineAsync()) != null)
{ {
if (line.Trim().StartsWith(kid)) if (line.Trim().StartsWith(kid))
{ {
@ -152,17 +151,15 @@ internal static class MP4DecryptUtil
public static ParsedMP4Info GetMP4Info(string output) public static ParsedMP4Info GetMP4Info(string output)
{ {
using (var fs = File.OpenRead(output)) using var fs = File.OpenRead(output);
{ var header = new byte[1 * 1024 * 1024]; // 1MB
var header = new byte[1 * 1024 * 1024]; //1MB fs.Read(header);
fs.Read(header); return GetMP4Info(header);
return GetMP4Info(header);
}
} }
public static string? ReadInitShaka(string output, string bin) public static string? ReadInitShaka(string output, string bin)
{ {
Regex ShakaKeyIDRegex = new Regex("Key for key_id=([0-9a-f]+) was not found"); Regex shakaKeyIdRegex = new("Key for key_id=([0-9a-f]+) was not found");
// TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid) // TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid)
// - stop process // - stop process
@ -182,6 +179,6 @@ internal static class MP4DecryptUtil
p.Start(); p.Start();
var errorOutput = p.StandardError.ReadToEnd(); var errorOutput = p.StandardError.ReadToEnd();
p.WaitForExit(); p.WaitForExit();
return ShakaKeyIDRegex.Match(errorOutput).Groups[1].Value; return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value;
} }
} }

View File

@ -1,12 +1,6 @@
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
@ -48,7 +42,7 @@ internal static partial class MediainfoUtil
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false UseShellExecute = false
})!; })!;
var output = p.StandardError.ReadToEnd(); var output = await p.StandardError.ReadToEndAsync();
await p.WaitForExitAsync(); await p.WaitForExitAsync();
foreach (Match stream in TextRegex().Matches(output)) foreach (Match stream in TextRegex().Matches(output))
@ -87,7 +81,7 @@ internal static partial class MediainfoUtil
if (result.Count == 0) if (result.Count == 0)
{ {
result.Add(new Mediainfo() result.Add(new Mediainfo
{ {
Type = "Unknown" Type = "Unknown"
}); });

View File

@ -27,18 +27,14 @@ internal static class MergeUtil
if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) if (!Directory.Exists(Path.GetDirectoryName(outputFilePath)))
Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!);
string[] inputFilePaths = files; var inputFilePaths = files;
using (var outputStream = File.Create(outputFilePath)) using var outputStream = File.Create(outputFilePath);
foreach (var inputFilePath in inputFilePaths)
{ {
foreach (var inputFilePath in inputFilePaths) if (inputFilePath == "")
{ continue;
if (inputFilePath == "") using var inputStream = File.OpenRead(inputFilePath);
continue; inputStream.CopyTo(outputStream);
using (var inputStream = File.OpenRead(inputFilePath))
{
inputStream.CopyTo(outputStream);
}
}
} }
} }
@ -79,18 +75,18 @@ internal static class MergeUtil
div = 200; div = 200;
string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T"); string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T");
int index = 0; //序号 int index = 0; // 序号
//按照div的容量分割为小数组 // 按照div的容量分割为小数组
string[][] li = Enumerable.Range(0, files.Count() / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray(); string[][] li = Enumerable.Range(0, files.Count() / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray();
foreach (var items in li) foreach (var items in li)
{ {
if (items.Count() == 0) if (!items.Any())
continue; continue;
var output = outputName + index.ToString("0000") + ".ts"; var output = outputName + index.ToString("0000") + ".ts";
CombineMultipleFilesIntoSingleFile(items, output); CombineMultipleFilesIntoSingleFile(items, output);
newFiles.Add(output); newFiles.Add(output);
//合并后删除这些文件 // 合并后删除这些文件
foreach (var item in items) foreach (var item in items)
{ {
File.Delete(item); File.Delete(item);
@ -106,7 +102,7 @@ internal static class MergeUtil
bool writeDate = true, bool useConcatDemuxer = false, string poster = "", string audioName = "", string title = "", bool writeDate = true, bool useConcatDemuxer = false, string poster = "", string audioName = "", string title = "",
string copyright = "", string comment = "", string encodingTool = "", string recTime = "") string copyright = "", string comment = "", string encodingTool = "", string recTime = "")
{ {
//改为绝对路径 // 改为绝对路径
outputPath = Path.GetFullPath(outputPath); outputPath = Path.GetFullPath(outputPath);
string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime; string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime;
@ -185,13 +181,13 @@ internal static class MergeUtil
string dateString = DateTime.Now.ToString("o"); string dateString = DateTime.Now.ToString("o");
StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn "); StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn ");
//INPUT // INPUT
foreach (var item in files) foreach (var item in files)
{ {
command.Append($" -i \"{item.FilePath}\" "); command.Append($" -i \"{item.FilePath}\" ");
} }
//MAP // MAP
for (int i = 0; i < files.Length; i++) for (int i = 0; i < files.Length; i++)
{ {
command.Append($" -map {i} "); command.Append($" -map {i} ");
@ -200,21 +196,21 @@ internal static class MergeUtil
var srt = files.Any(x => x.FilePath.EndsWith(".srt")); var srt = files.Any(x => x.FilePath.EndsWith(".srt"));
if (muxFormat == MuxFormat.MP4) if (muxFormat == MuxFormat.MP4)
command.Append($" -strict unofficial -c:a copy -c:v copy -c:s mov_text "); //mp4不支持vtt/srt字幕必须转换格式 command.Append($" -strict unofficial -c:a copy -c:v copy -c:s mov_text "); // mp4不支持vtt/srt字幕必须转换格式
else if (muxFormat == MuxFormat.TS) else if (muxFormat == MuxFormat.TS)
command.Append($" -strict unofficial -c:a copy -c:v copy "); command.Append($" -strict unofficial -c:a copy -c:v copy ");
else if (muxFormat == MuxFormat.MKV) else if (muxFormat == MuxFormat.MKV)
command.Append($" -strict unofficial -c:a copy -c:v copy -c:s {(srt ? "srt" : "webvtt")} "); command.Append($" -strict unofficial -c:a copy -c:v copy -c:s {(srt ? "srt" : "webvtt")} ");
else throw new ArgumentException($"unknown format: {muxFormat}"); else throw new ArgumentException($"unknown format: {muxFormat}");
//CLEAN // CLEAN
command.Append(" -map_metadata -1 "); command.Append(" -map_metadata -1 ");
//LANG and NAME // LANG and NAME
var streamIndex = 0; var streamIndex = 0;
for (int i = 0; i < files.Length; i++) for (int i = 0; i < files.Length; i++)
{ {
//转换语言代码 // 转换语言代码
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]); LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
command.Append($" -metadata:s:{streamIndex} language=\"{files[i].LangCode ?? "und"}\" "); command.Append($" -metadata:s:{streamIndex} language=\"{files[i].LangCode ?? "und"}\" ");
if (!string.IsNullOrEmpty(files[i].Description)) if (!string.IsNullOrEmpty(files[i].Description))
@ -236,11 +232,11 @@ internal static class MergeUtil
var audioTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO); var audioTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO);
var subTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO); var subTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO);
if (videoTracks.Any()) command.Append(" -disposition:v:0 default "); if (videoTracks.Any()) command.Append(" -disposition:v:0 default ");
//字幕都不设置默认 // 字幕都不设置默认
if (subTracks.Any()) command.Append(" -disposition:s 0 "); if (subTracks.Any()) command.Append(" -disposition:s 0 ");
if (audioTracks.Any()) if (audioTracks.Any())
{ {
//音频除了第一个音轨 都不设置默认 // 音频除了第一个音轨 都不设置默认
command.Append(" -disposition:a:0 default "); command.Append(" -disposition:a:0 default ");
for (int i = 1; i < audioTracks.Count(); i++) for (int i = 1; i < audioTracks.Count(); i++)
{ {
@ -265,16 +261,16 @@ internal static class MergeUtil
var dFlag = false; var dFlag = false;
//LANG and NAME // LANG and NAME
for (int i = 0; i < files.Length; i++) for (int i = 0; i < files.Length; i++)
{ {
//转换语言代码 // 转换语言代码
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]); LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
command.Append($" --language 0:\"{files[i].LangCode ?? "und"}\" "); command.Append($" --language 0:\"{files[i].LangCode ?? "und"}\" ");
//字幕都不设置默认 // 字幕都不设置默认
if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES) if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES)
command.Append($" --default-track 0:no "); command.Append($" --default-track 0:no ");
//音频除了第一个音轨 都不设置默认 // 音频除了第一个音轨 都不设置默认
if (files[i].MediaType == Common.Enum.MediaType.AUDIO) if (files[i].MediaType == Common.Enum.MediaType.AUDIO)
{ {
if (dFlag) if (dFlag)

View File

@ -1,35 +1,29 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Enum;
using System.CommandLine;
using System.IO.Compression; using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
internal class OtherUtil internal static class OtherUtil
{ {
public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers) public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers)
{ {
Dictionary<string, string> dic = new(); Dictionary<string, string> dic = new();
if (headers == null) return dic;
if (headers != null) foreach (string header in headers)
{ {
foreach (string header in headers) var index = header.IndexOf(':');
if (index != -1)
{ {
var index = header.IndexOf(':'); dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim();
if (index != -1)
{
dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim();
}
} }
} }
return dic; return dic;
} }
private static char[] InvalidChars = "34,60,62,124,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,58,42,63,92,47" private static readonly char[] InvalidChars = "34,60,62,124,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,58,42,63,92,47"
.Split(',').Select(s => (char)int.Parse(s)).ToArray(); .Split(',').Select(s => (char)int.Parse(s)).ToArray();
public static string GetValidFileName(string input, string re = ".", bool filterSlash = false) public static string GetValidFileName(string input, string re = ".", bool filterSlash = false)
{ {
@ -50,6 +44,7 @@ internal class OtherUtil
/// 从输入自动获取文件名 /// 从输入自动获取文件名
/// </summary> /// </summary>
/// <param name="input"></param> /// <param name="input"></param>
/// <param name="addSuffix"></param>
/// <returns></returns> /// <returns></returns>
public static string GetFileNameFromInput(string input, bool addSuffix = true) public static string GetFileNameFromInput(string input, bool addSuffix = true)
{ {
@ -119,7 +114,7 @@ internal class OtherUtil
return hours * 3600 + minutes * 60 + seconds; return hours * 3600 + minutes * 60 + seconds;
} }
//若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录 // 若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录
public static void SafeDeleteDir(string dirPath) public static void SafeDeleteDir(string dirPath)
{ {
if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath)) if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath))

View File

@ -1,14 +1,8 @@
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Diagnostics; using System.Diagnostics;
using System.IO.Pipes; using System.IO.Pipes;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
@ -67,7 +61,7 @@ internal static class PipeUtil
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
command.Append($" -i \"\\\\.\\pipe\\{item}\" "); command.Append($" -i \"\\\\.\\pipe\\{item}\" ");
else else
//command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" "); // command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" ");
command.Append($" -i \"{Path.Combine(pipeDir, item)}\" "); command.Append($" -i \"{Path.Combine(pipeDir, item)}\" ");
} }
@ -103,7 +97,7 @@ internal static class PipeUtil
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false UseShellExecute = false
}; };
//p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42"); // p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42");
p.Start(); p.Start();
p.WaitForExit(); p.WaitForExit();

View File

@ -1,11 +1,6 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util; namespace N_m3u8DL_RE.Util;
@ -17,23 +12,21 @@ internal static class SubtitleUtil
/// <param name="finalVtt"></param> /// <param name="finalVtt"></param>
/// <param name="tmpDir">临时目录</param> /// <param name="tmpDir">临时目录</param>
/// <returns></returns> /// <returns></returns>
public static async Task<bool> TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir) public static async Task TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir)
{ {
if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::"))) if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::")))
{ {
Logger.WarnMarkUp(ResString.processImageSub); Logger.WarnMarkUp(ResString.processImageSub);
var _i = 0; var i = 0;
foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::"))) foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::")))
{ {
var name = $"{_i++}.png"; var name = $"{i++}.png";
var dest = ""; var dest = "";
for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{_i++}.png") ; for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{i++}.png") ;
var base64 = img.Payload[8..]; var base64 = img.Payload[8..];
await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64)); await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64));
img.Payload = name; img.Payload = name;
} }
return true;
} }
else return false;
} }
} }