diff --git a/src/N_m3u8DL-RE.Common/Entity/EncryptInfo.cs b/src/N_m3u8DL-RE.Common/Entity/EncryptInfo.cs index dedfee8..3ce359b 100644 --- a/src/N_m3u8DL-RE.Common/Entity/EncryptInfo.cs +++ b/src/N_m3u8DL-RE.Common/Entity/EncryptInfo.cs @@ -5,39 +5,38 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Common.Entity +namespace N_m3u8DL_RE.Common.Entity; + +public class EncryptInfo { - public class EncryptInfo + /// + /// 加密方式,默认无加密 + /// + public EncryptMethod Method { get; set; } = EncryptMethod.NONE; + + public byte[]? Key { get; set; } + public byte[]? IV { get; set; } + + public EncryptInfo() { } + + /// + /// 创建EncryptInfo并尝试自动解析Method + /// + /// + public EncryptInfo(string method) { - /// - /// 加密方式,默认无加密 - /// - public EncryptMethod Method { get; set; } = EncryptMethod.NONE; + Method = ParseMethod(method); + } - public byte[]? Key { get; set; } - public byte[]? IV { get; set; } - - public EncryptInfo() { } - - /// - /// 创建EncryptInfo并尝试自动解析Method - /// - /// - public EncryptInfo(string method) + public static EncryptMethod ParseMethod(string? method) + { + if (method != null && System.Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m)) { - Method = ParseMethod(method); + return m; } - - public static EncryptMethod ParseMethod(string? method) + else { - if (method != null && System.Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m)) - { - return m; - } - else - { - return EncryptMethod.UNKNOWN; - } + return EncryptMethod.UNKNOWN; } } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Entity/MSSData.cs b/src/N_m3u8DL-RE.Common/Entity/MSSData.cs index 96a0ae2..370293f 100644 --- a/src/N_m3u8DL-RE.Common/Entity/MSSData.cs +++ b/src/N_m3u8DL-RE.Common/Entity/MSSData.cs @@ -1,25 +1,18 @@ -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 MSSData { - public class MSSData - { - public string FourCC { get; set; } = ""; - public string CodecPrivateData { get; set; } = ""; - public string Type { get; set; } = ""; - public int Timesacle { get; set; } - public int SamplingRate { get; set; } - public int Channels { get; set; } - public int BitsPerSample { get; set; } - public int NalUnitLengthField { get; set; } - public long Duration { get; set; } + public string FourCC { get; set; } = ""; + public string CodecPrivateData { get; set; } = ""; + public string Type { get; set; } = ""; + public int Timesacle { get; set; } + public int SamplingRate { get; set; } + public int Channels { get; set; } + public int BitsPerSample { get; set; } + public int NalUnitLengthField { get; set; } + public long Duration { get; set; } - public bool IsProtection { get; set; } = false; - public string ProtectionSystemID { get; set; } = ""; - public string ProtectionData { get; set; } = ""; - } -} + public bool IsProtection { get; set; } = false; + public string ProtectionSystemID { get; set; } = ""; + public string ProtectionData { get; set; } = ""; +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Entity/MediaPart.cs b/src/N_m3u8DL-RE.Common/Entity/MediaPart.cs index b86fa64..c417524 100644 --- a/src/N_m3u8DL-RE.Common/Entity/MediaPart.cs +++ b/src/N_m3u8DL-RE.Common/Entity/MediaPart.cs @@ -1,14 +1,7 @@ -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 +// 主要处理 EXT-X-DISCONTINUITY +public class MediaPart { - //主要处理 EXT-X-DISCONTINUITY - public class MediaPart - { - public List MediaSegments { get; set; } = new List(); - } -} + public List MediaSegments { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs index 918b016..4055ee1 100644 --- a/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs +++ b/src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs @@ -1,43 +1,36 @@ -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 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; } - public double Duration { get; set; } - public string? Title { get; set; } - public DateTime? DateTime { get; set; } - - public long? StartRange { get; set; } - public long? StopRange { get => (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) - { - 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); - } + return obj is MediaSegment segment && + Index == segment.Index && + Math.Abs(Duration - segment.Duration) < 0.001 && + 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); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Entity/Playlist.cs b/src/N_m3u8DL-RE.Common/Entity/Playlist.cs index 65a8bb6..6cc194a 100644 --- a/src/N_m3u8DL-RE.Common/Entity/Playlist.cs +++ b/src/N_m3u8DL-RE.Common/Entity/Playlist.cs @@ -1,27 +1,19 @@ -using N_m3u8DL_RE.Common.Enum; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Entity; -namespace N_m3u8DL_RE.Common.Entity +public class Playlist { - public class Playlist - { - //对应Url信息 - public string Url { get; set; } - //是否直播 - public bool IsLive { get; set; } = false; - //直播刷新间隔毫秒(默认15秒) - public double RefreshIntervalMs { get; set; } = 15000; - //所有分片时长总和 - public double TotalDuration { get => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); } - //所有分片中最长时长 - public double? TargetDuration { get; set; } - //INIT信息 - public MediaSegment? MediaInit { get; set; } - //分片信息 - public List MediaParts { get; set; } = new List(); - } -} + // 对应Url信息 + public string Url { get; set; } + // 是否直播 + public bool IsLive { get; set; } = false; + // 直播刷新间隔毫秒(默认15秒) + public double RefreshIntervalMs { get; set; } = 15000; + // 所有分片时长总和 + public double TotalDuration { get => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); } + // 所有分片中最长时长 + public double? TargetDuration { get; set; } + // INIT信息 + public MediaSegment? MediaInit { get; set; } + // 分片信息 + public List MediaParts { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs index 20b29a0..4cec950 100644 --- a/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs +++ b/src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs @@ -1,188 +1,182 @@ using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Util; 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; } + + /// + /// URL + /// + public string Url { get; set; } + + /// + /// 原始URL + /// + public string OriginalUrl { get; set; } + + public Playlist? Playlist { get; set; } + + public int SegmentsCount { - 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; } - - /// - /// URL - /// - public string Url { get; set; } - - /// - /// 原始URL - /// - public string OriginalUrl { get; set; } - - public Playlist? Playlist { get; set; } - - public int SegmentsCount + get { - get - { - 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(); + 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(); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Entity/SubCue.cs b/src/N_m3u8DL-RE.Common/Entity/SubCue.cs index 2e22911..5e0549a 100644 --- a/src/N_m3u8DL-RE.Common/Entity/SubCue.cs +++ b/src/N_m3u8DL-RE.Common/Entity/SubCue.cs @@ -1,30 +1,23 @@ -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 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; } - public TimeSpan EndTime { get; set; } - public required string Payload { get; set; } - public required string Settings { get; set; } - - public override bool Equals(object? obj) - { - return obj is SubCue cue && - 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); - } + return obj is SubCue cue && + 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); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs b/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs index 2f1171e..c3b255f 100644 --- a/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs +++ b/src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs @@ -1,276 +1,271 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; 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("([\\s\\S]*?)<\\/c>")] + private static partial Regex VttClassRegex(); + + public List Cues { get; set; } = new List(); + public long MpegtsTimestamp { get; set; } = 0L; + + /// + /// 从字节数组解析WEBVTT + /// + /// + /// + public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L) { - [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("([\\s\\S]*?)<\\/c>")] - private static partial Regex VttClassRegex(); + return Parse(Encoding.UTF8.GetString(textBytes), BaseTimestamp); + } - public List Cues { get; set; } = new List(); - public long MpegtsTimestamp { get; set; } = 0L; + /// + /// 从字节数组解析WEBVTT + /// + /// + /// + /// + public static WebVttSub Parse(byte[] textBytes, Encoding encoding, long BaseTimestamp = 0L) + { + return Parse(encoding.GetString(textBytes), BaseTimestamp); + } - /// - /// 从字节数组解析WEBVTT - /// - /// - /// - public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L) + /// + /// 从字符串解析WEBVTT + /// + /// + /// + 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); } - /// - /// 从字节数组解析WEBVTT - /// - /// - /// - /// - public static WebVttSub Parse(byte[] textBytes, Encoding encoding, long BaseTimestamp = 0L) + var payloads = new List(); + foreach (var line in text.Split('\n')) { - return Parse(encoding.GetString(textBytes), BaseTimestamp); - } - - /// - /// 从字符串解析WEBVTT - /// - /// - /// - 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)) + if (line.Contains(" --> ")) { - var timestamp = TSValueRegex().Match(regex1.Match(text).Value).Groups[1].Value; - webSub.MpegtsTimestamp = Convert.ToInt64(timestamp); + needPayload = true; + timeLine = line.Trim(); + continue; } - var payloads = new List(); - foreach (var line in text.Split('\n')) + if (needPayload) { - if (line.Contains(" --> ")) + if (string.IsNullOrEmpty(line.Trim())) { - needPayload = true; - timeLine = line.Trim(); - continue; + 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; } - - if (needPayload) + else { - if (string.IsNullOrEmpty(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()); - } + 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; - } - - /// - /// 从另一个字幕中获取所有Cue,并加载此字幕中,且自动修正偏移 - /// - /// - /// - public WebVttSub AddCuesFromOne(WebVttSub webSub) - { - FixTimestamp(webSub, this.MpegtsTimestamp); foreach (var item in webSub.Cues) { - if (!this.Cues.Contains(item)) + if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0) { - //如果相差只有1ms,且payload相同,则拼接 - var last = this.Cues.LastOrDefault(); - 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); - } + item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp); + item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp); } - } - 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) + else { - for (int i = 0; i < sub.Cues.Count; i++) - { - sub.Cues[i].StartTime += offset; - sub.Cues[i].EndTime += offset; - } + break; } } } - private IEnumerable 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) + /// + /// 从另一个字幕中获取所有Cue,并加载此字幕中,且自动修正偏移 + /// + /// + /// + public WebVttSub AddCuesFromOne(WebVttSub webSub) + { + FixTimestamp(webSub, this.MpegtsTimestamp); + foreach (var item in webSub.Cues) { - //17.0s - if (str.EndsWith('s')) + if (!this.Cues.Contains(item)) { - 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(); - } - - /// - /// 字幕向前平移指定时间 - /// - /// - 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); + // 如果相差只有1ms,且payload相同,则拼接 + var last = this.Cues.LastOrDefault(); + 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); + } } } + 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(); - int index = 1; - foreach (var c in GetCues()) + // 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) { - 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(); + for (int i = 0; i < sub.Cues.Count; i++) + { + sub.Cues[i].StartTime += offset; + 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 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(); + } + + /// + /// 字幕向前平移指定时间 + /// + /// + 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; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Enum/Choise.cs b/src/N_m3u8DL-RE.Common/Enum/Choise.cs index edba103..73f7072 100644 --- a/src/N_m3u8DL-RE.Common/Enum/Choise.cs +++ b/src/N_m3u8DL-RE.Common/Enum/Choise.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Enum; -namespace N_m3u8DL_RE.Common.Enum +public enum Choise { - public enum Choise - { - YES = 1, - NO = 0 - } -} + YES = 1, + NO = 0 +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs b/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs index 319a466..e87942b 100644 --- a/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs +++ b/src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs @@ -1,20 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Enum; -namespace N_m3u8DL_RE.Common.Enum +public enum EncryptMethod { - public enum EncryptMethod - { - NONE, - AES_128, - AES_128_ECB, - SAMPLE_AES, - SAMPLE_AES_CTR, - CENC, - CHACHA20, - UNKNOWN - } -} + NONE, + AES_128, + AES_128_ECB, + SAMPLE_AES, + SAMPLE_AES_CTR, + CENC, + CHACHA20, + UNKNOWN +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs b/src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs index 27e2616..6df2aec 100644 --- a/src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs +++ b/src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs @@ -1,16 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Enum; -namespace N_m3u8DL_RE.Common.Enum +public enum ExtractorType { - public enum ExtractorType - { - MPEG_DASH, - HLS, - HTTP_LIVE, - MSS - } -} + MPEG_DASH, + HLS, + HTTP_LIVE, + MSS +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Enum/MediaType.cs b/src/N_m3u8DL-RE.Common/Enum/MediaType.cs index 3857763..bd1f458 100644 --- a/src/N_m3u8DL-RE.Common/Enum/MediaType.cs +++ b/src/N_m3u8DL-RE.Common/Enum/MediaType.cs @@ -1,16 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Enum; -namespace N_m3u8DL_RE.Common.Enum +public enum MediaType { - public enum MediaType - { - AUDIO = 0, - VIDEO = 1, - SUBTITLES = 2, - CLOSED_CAPTIONS = 3 - } -} + AUDIO = 0, + VIDEO = 1, + SUBTITLES = 2, + CLOSED_CAPTIONS = 3 +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Enum/RoleType.cs b/src/N_m3u8DL-RE.Common/Enum/RoleType.cs index 787801f..4dbd1cf 100644 --- a/src/N_m3u8DL-RE.Common/Enum/RoleType.cs +++ b/src/N_m3u8DL-RE.Common/Enum/RoleType.cs @@ -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, - Alternate = 2, - Supplementary = 3, - Commentary = 4, - Dub = 5, - Description = 6, - Sign = 7, - Metadata = 8, - } -} + Subtitle = 0, + Main = 1, + Alternate = 2, + Supplementary = 3, + Commentary = 4, + Dub = 5, + Description = 6, + Sign = 7, + Metadata = 8, +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/JsonContext/JsonContext.cs b/src/N_m3u8DL-RE.Common/JsonContext/JsonContext.cs index 617bc8d..3b7ea0c 100644 --- a/src/N_m3u8DL-RE.Common/JsonContext/JsonContext.cs +++ b/src/N_m3u8DL-RE.Common/JsonContext/JsonContext.cs @@ -2,21 +2,20 @@ using N_m3u8DL_RE.Common.Enum; using System.Text.Json.Serialization; -namespace N_m3u8DL_RE.Common -{ - [JsonSourceGenerationOptions( +namespace N_m3u8DL_RE.Common; + +[JsonSourceGenerationOptions( WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, GenerationMode = JsonSourceGenerationMode.Metadata)] - [JsonSerializable(typeof(MediaType))] - [JsonSerializable(typeof(EncryptMethod))] - [JsonSerializable(typeof(ExtractorType))] - [JsonSerializable(typeof(Choise))] - [JsonSerializable(typeof(StreamSpec))] - [JsonSerializable(typeof(IOrderedEnumerable))] - [JsonSerializable(typeof(IEnumerable))] - [JsonSerializable(typeof(List))] - [JsonSerializable(typeof(List))] - [JsonSerializable(typeof(Dictionary))] - internal partial class JsonContext : JsonSerializerContext { } -} +[JsonSerializable(typeof(MediaType))] +[JsonSerializable(typeof(EncryptMethod))] +[JsonSerializable(typeof(ExtractorType))] +[JsonSerializable(typeof(Choise))] +[JsonSerializable(typeof(StreamSpec))] +[JsonSerializable(typeof(IOrderedEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Dictionary))] +internal partial class JsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/JsonConverter/BytesBase64Converter.cs b/src/N_m3u8DL-RE.Common/JsonConverter/BytesBase64Converter.cs index f0ae02a..4097aed 100644 --- a/src/N_m3u8DL-RE.Common/JsonConverter/BytesBase64Converter.cs +++ b/src/N_m3u8DL-RE.Common/JsonConverter/BytesBase64Converter.cs @@ -6,12 +6,11 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Common.JsonConverter -{ - internal class BytesBase64Converter : JsonConverter - { - public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetBytesFromBase64(); +namespace N_m3u8DL_RE.Common.JsonConverter; - public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) => writer.WriteStringValue(Convert.ToBase64String(value)); - } -} +internal class BytesBase64Converter : JsonConverter +{ + 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)); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Log/CustomAnsiConsole.cs b/src/N_m3u8DL-RE.Common/Log/CustomAnsiConsole.cs index 417bcf8..9337497 100644 --- a/src/N_m3u8DL-RE.Common/Log/CustomAnsiConsole.cs +++ b/src/N_m3u8DL-RE.Common/Log/CustomAnsiConsole.cs @@ -8,27 +8,27 @@ public class NonAnsiWriter : TextWriter { public override Encoding Encoding => Console.OutputEncoding; - private string lastOut = ""; + private string? _lastOut = ""; public override void Write(char value) { Console.Write(value); } - public override void Write(string value) + public override void Write(string? value) { - if (lastOut == value) + if (_lastOut == value) { return; } - lastOut = value; + _lastOut = value; RemoveAnsiEscapeSequences(value); } - private void RemoveAnsiEscapeSequences(string input) + private void RemoveAnsiEscapeSequences(string? input) { // 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,"[\r\n] +",""); if (string.IsNullOrWhiteSpace(output)) diff --git a/src/N_m3u8DL-RE.Common/Log/LogLevel.cs b/src/N_m3u8DL-RE.Common/Log/LogLevel.cs index e971658..11c0387 100644 --- a/src/N_m3u8DL-RE.Common/Log/LogLevel.cs +++ b/src/N_m3u8DL-RE.Common/Log/LogLevel.cs @@ -4,14 +4,13 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Common.Log +namespace N_m3u8DL_RE.Common.Log; + +public enum LogLevel { - public enum LogLevel - { - OFF, - ERROR, - WARN, - INFO, - DEBUG, - } -} + OFF, + ERROR, + WARN, + INFO, + DEBUG, +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Log/Logger.cs b/src/N_m3u8DL-RE.Common/Log/Logger.cs index a5d69b1..d2f7cd8 100644 --- a/src/N_m3u8DL-RE.Common/Log/Logger.cs +++ b/src/N_m3u8DL-RE.Common/Log/Logger.cs @@ -1,243 +1,236 @@ using Spectre.Console; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; 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(); + + /// + /// 日志级别,默认为INFO + /// + public static LogLevel LogLevel { get; set; } = LogLevel.INFO; + + /// + /// 是否写出日志文件 + /// + public static bool IsWriteFile { get; set; } = true; + + /// + /// 本次运行日志文件所在位置 + /// + private static string? LogFilePath { get; set; } + + // 读写锁 + static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); + + public static void InitLogFile() { - [GeneratedRegex("{}")] - private static partial Regex VarsRepRegex(); + if (!IsWriteFile) return; - /// - /// 日志级别,默认为INFO - /// - public static LogLevel LogLevel { get; set; } = LogLevel.INFO; - - /// - /// 是否写出日志文件 - /// - public static bool IsWriteFile { get; set; } = true; - - /// - /// 本次运行日志文件所在位置 - /// - private static string? LogFilePath { get; set; } - - //读写锁 - static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); - - public static void InitLogFile() + try { - if (!IsWriteFile) return; - - try + var logDir = Path.GetDirectoryName(Environment.ProcessPath) + "/Logs"; + if (!Directory.Exists(logDir)) { - var logDir = Path.GetDirectoryName(Environment.ProcessPath) + "/Logs"; - if (!Directory.Exists(logDir)) - { - Directory.CreateDirectory(logDir); - } + Directory.CreateDirectory(logDir); + } - var now = DateTime.Now; - LogFilePath = Path.Combine(logDir, now.ToString("yyyy-MM-dd_HH-mm-ss-fff") + ".log"); - int index = 1; - var fileName = Path.GetFileNameWithoutExtension(LogFilePath); - string init = "LOG " + now.ToString("yyyy/MM/dd") + Environment.NewLine - + "Save Path: " + Path.GetDirectoryName(LogFilePath) + Environment.NewLine - + "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine - + "Task CommandLine: " + Environment.CommandLine; - init += $"{Environment.NewLine}{Environment.NewLine}"; - //若文件存在则加序号 - while (File.Exists(LogFilePath)) - { - LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log"); - } - File.WriteAllText(LogFilePath, init, Encoding.UTF8); - } - catch (Exception ex) + var now = DateTime.Now; + LogFilePath = Path.Combine(logDir, now.ToString("yyyy-MM-dd_HH-mm-ss-fff") + ".log"); + int index = 1; + var fileName = Path.GetFileNameWithoutExtension(LogFilePath); + string init = "LOG " + now.ToString("yyyy/MM/dd") + Environment.NewLine + + "Save Path: " + Path.GetDirectoryName(LogFilePath) + Environment.NewLine + + "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine + + "Task CommandLine: " + Environment.CommandLine; + init += $"{Environment.NewLine}{Environment.NewLine}"; + // 若文件存在则加序号 + while (File.Exists(LogFilePath)) { - Error($"Init log failed! {ex.Message.RemoveMarkup()}"); + LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log"); } + File.WriteAllText(LogFilePath, init, Encoding.UTF8); } - - private static string GetCurrTime() + catch (Exception ex) { - 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); - } - 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(); - } - } + CustomAnsiConsole.MarkupLine(write); } - catch (Exception) + else { - 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); + CustomAnsiConsole.Markup(write); + Console.WriteLine(subWrite); } - 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); - } - - /// - /// This thing will only write to the log file. - /// - /// - /// - public static void Extra(string data, params object[] ps) - { if (IsWriteFile && File.Exists(LogFilePath)) { - data = ReplaceVars(data, ps); - var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup(); + var plain = write.RemoveMarkup() + subWrite.RemoveMarkup(); try { - //进入写入 + // 进入写入 LogWriteLock.EnterWriteLock(); using (StreamWriter sw = File.AppendText(LogFilePath)) { - sw.WriteLine(plain, Encoding.UTF8); + sw.WriteLine(plain); } } finally { - //释放占用 + // 释放占用 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); + } + + /// + /// This thing will only write to the log file. + /// + /// + /// + 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(); + } + } } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index a97f0d4..8c97681 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -1,151 +1,142 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Resource; -namespace N_m3u8DL_RE.Common.Resource +public class ResString { - public class ResString + public static readonly string ReLiveTs = ""; + 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 = ""; - public static string singleFileRealtimeDecryptWarn { get => GetText("singleFileRealtimeDecryptWarn"); } - 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"); } + if (!StaticText.LANG_DIC.ContainsKey(key)) + return "<...LANG TEXT MISSING...>"; - private static string GetText(string key) - { - if (!StaticText.LANG_DIC.ContainsKey(key)) - return "<...LANG TEXT MISSING...>"; - - var current = Thread.CurrentThread.CurrentUICulture.Name; - 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; - } + var current = Thread.CurrentThread.CurrentUICulture.Name; + if (current == "zh-CN" || current == "zh-SG" || current == "zh-Hans") + return StaticText.LANG_DIC[key].ZH_CN; + if (current.StartsWith("zh-")) + return StaticText.LANG_DIC[key].ZH_TW; + return StaticText.LANG_DIC[key].EN_US; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index 23f84f3..1739d53 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -4,919 +4,918 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Common.Resource -{ - internal class StaticText - { - public static Dictionary LANG_DIC = new() - { - ["singleFileSplitWarn"] = new TextContainer - ( - zhCN: "整段文件已被自动切割为小分片以加速下载", - zhTW: "整段文件已被自動切割為小分片以加速下載", - enUS: "The entire file has been cut into small segments to accelerate" - ), - ["singleFileRealtimeDecryptWarn"] = new TextContainer - ( - zhCN: "实时解密已被强制关闭", - zhTW: "即時解密已被強制關閉", - enUS: "Real-time decryption has been disabled" - ), - ["cmd_forceAnsiConsole"] = new TextContainer - ( - zhCN: "强制认定终端为支持ANSI且可交互的终端", - zhTW: "強制認定終端為支援ANSI且可交往的終端", - enUS: "Force assuming the terminal is ANSI-compatible and interactive" - ), - ["cmd_noAnsiColor"] = new TextContainer - ( - zhCN: "去除ANSI颜色", - zhTW: "關閉ANSI顏色", - enUS: "Remove ANSI colors" - ), - ["customRangeWarn"] = new TextContainer - ( - zhCN: "请注意,自定义下载范围有时会导致音画不同步", - zhTW: "請注意,自定義下載範圍有時會導致音畫不同步", - enUS: "Please note that custom range may sometimes result in audio and video being out of sync" - ), - ["customRangeInvalid"] = new TextContainer - ( - zhCN: "自定义下载范围无效", - zhTW: "自定義下載範圍無效", - enUS: "User customed range invalid" - ), - ["customAdKeywordsFound"] = new TextContainer - ( - zhCN: "用户自定义广告分片URL关键字:", - zhTW: "用戶自定義廣告分片URL關鍵字:", - enUS: "User customed Ad keyword: " - ), - ["customRangeFound"] = new TextContainer - ( - zhCN: "用户自定义下载范围:", - zhTW: "用戶自定義下載範圍:", - enUS: "User customed range: " - ), - ["consoleRedirected"] = new TextContainer - ( - zhCN: "输出被重定向, 将清除ANSI颜色", - zhTW: "輸出被重定向, 將清除ANSI顏色", - enUS: "Output is redirected, ANSI colors are cleared." - ), - ["processImageSub"] = new TextContainer - ( - zhCN: "正在处理图形字幕", - zhTW: "正在處理圖形字幕", - enUS: "Processing Image Sub" - ), - ["newVersionFound"] = new TextContainer - ( - zhCN: "检测到新版本,请尽快升级!", - zhTW: "檢測到新版本,請盡快升級!", - enUS: "New version detected!" - ), - ["namedPipeCreated"] = new TextContainer - ( - zhCN: "已创建命名管道:", - zhTW: "已創建命名管道:", - enUS: "Named pipe created: " - ), - ["namedPipeMux"] = new TextContainer - ( - zhCN: "通过命名管道混流到", - zhTW: "通過命名管道混流到", - enUS: "Mux with named pipe, to" - ), - ["taskStartAt"] = new TextContainer - ( - zhCN: "程序将等待,直到:", - zhTW: "程序將等待,直到:", - enUS: "The program will wait until: " - ), - ["autoBinaryMerge"] = new TextContainer - ( - zhCN: "检测到fMP4,自动开启二进制合并", - zhTW: "檢測到fMP4,自動開啟二進位制合併", - enUS: "fMP4 is detected, binary merging is automatically enabled" - ), - ["autoBinaryMerge2"] = new TextContainer - ( - zhCN: "检测到杜比视界内容,自动开启二进制合并", - zhTW: "檢測到杜比視界內容,自動開啟二進位制合併", - enUS: "Dolby Vision content is detected, binary merging is automatically enabled" - ), - ["autoBinaryMerge3"] = new TextContainer - ( - zhCN: "检测到无法识别的加密方式,自动开启二进制合并", - zhTW: "檢測到無法識別的加密方式,自動開啟二進位制合併", - enUS: "An unrecognized encryption method is detected, binary merging is automatically enabled" - ), - ["autoBinaryMerge4"] = new TextContainer - ( - zhCN: "检测到CENC加密方式,自动开启二进制合并", - zhTW: "檢測到CENC加密方式,自動開啟二進位制合併", - enUS: "When CENC encryption is detected, binary merging is automatically enabled" - ), - ["autoBinaryMerge5"] = new TextContainer - ( - zhCN: "检测到杜比视界内容,混流功能已禁用", - zhTW: "檢測到杜比視界內容,混流功能已禁用", - enUS: "Dolby Vision content is detected, mux after done is automatically disabled" - ), - ["autoBinaryMerge6"] = new TextContainer - ( - zhCN: "你已开启下载完成后混流,自动开启二进制合并", - zhTW: "你已開啟下載完成後混流,自動開啟二進制合併", - enUS: "MuxAfterDone is detected, binary merging is automatically enabled" - ), - ["badM3u8"] = new TextContainer - ( - zhCN: "错误的m3u8", - zhTW: "錯誤的m3u8", - enUS: "Bad m3u8" - ), - ["binaryMerge"] = new TextContainer - ( - zhCN: "二进制合并中...", - zhTW: "二進位制合併中...", - enUS: "Binary merging..." - ), - ["checkingLast"] = new TextContainer - ( - zhCN: "验证最后一个分片有效性", - zhTW: "驗證最後一個分片有效性", - enUS: "Verifying the validity of the last segment" - ), - ["cmd_baseUrl"] = new TextContainer - ( - zhCN: "设置BaseURL", - zhTW: "設置BaseURL", - enUS: "Set BaseURL" - ), - ["cmd_maxSpeed"] = new TextContainer - ( - zhCN: "设置限速,单位支持 Mbps 或 Kbps,如:15M 100K", - zhTW: "設置限速,單位支持 Mbps 或 Kbps,如:15M 100K", - enUS: "Set speed limit, Mbps or Kbps, for example: 15M 100K." - ), - ["cmd_noDateInfo"] = new TextContainer - ( - zhCN: "混流时不写入日期信息", - zhTW: "混流時不寫入日期訊息", - enUS: "Date information is not written during muxing" - ), - ["cmd_noLog"] = new TextContainer - ( - zhCN: "关闭日志文件输出", - zhTW: "關閉日誌文件輸出", - enUS: "Disable log file output" - ), - ["cmd_appendUrlParams"] = new TextContainer - ( - zhCN: "将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com", - zhTW: "將輸入Url的Params添加至分片, 對某些網站很有用, 例如 kakao.com", - enUS: "Add Params of input Url to segments, useful for some websites, such as kakao.com" - ), - ["cmd_autoSelect"] = new TextContainer - ( - zhCN: "自动选择所有类型的最佳轨道", - zhTW: "自動選擇所有類型的最佳軌道", - enUS: "Automatically selects the best tracks of all types" - ), - ["cmd_binaryMerge"] = new TextContainer - ( - zhCN: "二进制合并", - zhTW: "二進位制合併", - enUS: "Binary merge" - ), - ["cmd_useFFmpegConcatDemuxer"] = new TextContainer - ( - zhCN: "使用 ffmpeg 合并时,使用 concat 分离器而非 concat 协议", - zhTW: "使用 ffmpeg 合併時,使用 concat 分離器而非 concat 協議", - enUS: "When merging with ffmpeg, use the concat demuxer instead of the concat protocol" - ), - ["cmd_checkSegmentsCount"] = new TextContainer - ( - zhCN: "检测实际下载的分片数量和预期数量是否匹配", - zhTW: "檢測實際下載的分片數量和預期數量是否匹配", - enUS: "Check if the actual number of segments downloaded matches the expected number" - ), - ["cmd_downloadRetryCount"] = new TextContainer - ( - zhCN: "每个分片下载异常时的重试次数", - zhTW: "每個分片下載異常時的重試次數", - enUS: "The number of retries when download segment error" - ), - ["cmd_decryptionBinaryPath"] = new TextContainer - ( - zhCN: "MP4解密所用工具的全路径, 例如 C:\\Tools\\mp4decrypt.exe", - zhTW: "MP4解密所用工具的全路徑, 例如 C:\\Tools\\mp4decrypt.exe", - enUS: "Full path to the tool used for MP4 decryption, like C:\\Tools\\mp4decrypt.exe" - ), - ["cmd_delAfterDone"] = new TextContainer - ( - zhCN: "完成后删除临时文件", - zhTW: "完成後刪除臨時文件", - enUS: "Delete temporary files when done" - ), - ["cmd_ffmpegBinaryPath"] = new TextContainer - ( - zhCN: "ffmpeg可执行程序全路径, 例如 C:\\Tools\\ffmpeg.exe", - zhTW: "ffmpeg可執行程序全路徑, 例如 C:\\Tools\\ffmpeg.exe", - enUS: "Full path to the ffmpeg binary, like C:\\Tools\\ffmpeg.exe" - ), - ["cmd_mkvmergeBinaryPath"] = new TextContainer - ( - zhCN: "mkvmerge可执行程序全路径, 例如 C:\\Tools\\mkvmerge.exe", - zhTW: "mkvmerge可執行程序全路徑, 例如 C:\\Tools\\mkvmerge.exe", - enUS: "Full path to the mkvmerge binary, like C:\\Tools\\mkvmerge.exe" - ), - ["cmd_liveFixVttByAudio"] = new TextContainer - ( - zhCN: "通过读取音频文件的起始时间修正VTT字幕", - zhTW: "透過讀取音訊檔案的起始時間修正VTT字幕", - enUS: "Correct VTT sub by reading the start time of the audio file" - ), - ["cmd_header"] = new TextContainer - ( - zhCN: "为HTTP请求设置特定的请求头, 例如:\r\n-H \"Cookie: mycookie\" -H \"User-Agent: iOS\"", - zhTW: "為HTTP請求設置特定的請求頭, 例如:\r\n-H \"Cookie: mycookie\" -H \"User-Agent: iOS\"", - enUS: "Pass custom header(s) to server, Example:\r\n-H \"Cookie: mycookie\" -H \"User-Agent: iOS\"" - ), - ["cmd_Input"] = new TextContainer - ( - zhCN: "链接或文件", - zhTW: "連結或文件", - enUS: "Input Url or File" - ), - ["cmd_keys"] = new TextContainer - ( - zhCN: "设置解密密钥, 程序调用mp4decrpyt/shaka-packager进行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2\r\n对于KEY相同的情况可以直接输入 --key KEY", - zhTW: "設置解密密鑰, 程序調用mp4decrpyt/shaka-packager進行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2\r\n對於KEY相同的情況可以直接輸入 --key KEY", - enUS: "Set decryption key(s) to mp4decrypt/shaka-packager. format:\r\n--key KID1:KEY1 --key KID2:KEY2\r\nor use --key KEY if all tracks share the same key." - ), - ["cmd_keyText"] = new TextContainer - ( - zhCN: "设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)", - zhTW: "設置密鑰文件,程序將從文件中按KID搜尋KEY以解密.(不建議使用特大文件)", - enUS: "Set the kid-key file, the program will search the KEY with KID from the file.(Very large file are not recommended)" - ), - ["cmd_loadKeyFailed"] = new TextContainer - ( - zhCN: "获取KEY失败,忽略读取.", - zhTW: "獲取KEY失敗,忽略讀取.", - enUS: "Failed to get KEY, ignore." - ), - ["cmd_logLevel"] = new TextContainer - ( - zhCN: "设置日志级别", - zhTW: "設置日誌級別", - enUS: "Set log level" - ), - ["cmd_MP4RealTimeDecryption"] = new TextContainer - ( - zhCN: "实时解密MP4分片", - zhTW: "即時解密MP4分片", - enUS: "Decrypt MP4 segments in real time" - ), - ["cmd_saveDir"] = new TextContainer - ( - zhCN: "设置输出目录", - zhTW: "設置輸出目錄", - enUS: "Set output directory" - ), - ["cmd_saveName"] = new TextContainer - ( - zhCN: "设置保存文件名", - zhTW: "設置保存檔案名", - enUS: "Set output filename" - ), - ["cmd_savePattern"] = new TextContainer - ( - zhCN: "设置保存文件命名模板, 支持使用变量", - zhTW: "", - enUS: "" - ), - ["cmd_skipDownload"] = new TextContainer - ( - zhCN: "跳过下载", - zhTW: "跳過下載", - enUS: "Skip download" - ), - ["cmd_skipMerge"] = new TextContainer - ( - zhCN: "跳过合并分片", - zhTW: "跳過合併分片", - enUS: "Skip segments merge" - ), - ["cmd_subFormat"] = new TextContainer - ( - zhCN: "字幕输出类型", - zhTW: "字幕輸出類型", - enUS: "Subtitle output format" - ), - ["cmd_subOnly"] = new TextContainer - ( - zhCN: "只选取字幕轨道", - zhTW: "只選取字幕軌道", - enUS: "Select only subtitle tracks" - ), - ["cmd_subtitleFix"] = new TextContainer - ( - zhCN: "自动修正字幕", - zhTW: "自動修正字幕", - enUS: "Automatically fix subtitles" - ), - ["cmd_threadCount"] = new TextContainer - ( - zhCN: "设置下载线程数", - zhTW: "設置下載執行緒數", - enUS: "Set download thread count" - ), - ["cmd_tmpDir"] = new TextContainer - ( - zhCN: "设置临时文件存储目录", - zhTW: "設置臨時文件儲存目錄", - enUS: "Set temporary file directory" - ), - ["cmd_uiLanguage"] = new TextContainer - ( - zhCN: "设置UI语言", - zhTW: "設置UI語言", - enUS: "Set UI language" - ), - ["cmd_moreHelp"] = new TextContainer - ( - zhCN: "查看某个选项的详细帮助信息", - zhTW: "查看某個選項的詳細幫助訊息", - enUS: "Set more help info about one option" - ), - ["cmd_urlProcessorArgs"] = new TextContainer - ( - zhCN: "此字符串将直接传递给URL Processor", - zhTW: "此字符串將直接傳遞給URL Processor", - enUS: "Give these arguments to the URL Processors." - ), - ["cmd_liveRealTimeMerge"] = new TextContainer - ( - zhCN: "录制直播时实时合并", - zhTW: "錄製直播時即時合併", - enUS: "Real-time merge into file when recording live" - ), - ["cmd_customProxy"] = new TextContainer - ( - zhCN: "设置请求代理, 如 http://127.0.0.1:8888", - zhTW: "設置請求代理, 如 http://127.0.0.1:8888", - enUS: "Set web request proxy, like http://127.0.0.1:8888" - ), - ["cmd_customRange"] = new TextContainer - ( - zhCN: "仅下载部分分片. 输入 \"--morehelp custom-range\" 以查看详细信息", - zhTW: "僅下載部分分片. 輸入 \"--morehelp custom-range\" 以查看詳細訊息", - enUS: "Download only part of the segments. Use \"--morehelp custom-range\" for more details" - ), - ["cmd_useSystemProxy"] = new TextContainer - ( - zhCN: "使用系统默认代理", - zhTW: "使用系統默認代理", - enUS: "Use system default proxy" - ), - ["cmd_livePerformAsVod"] = new TextContainer - ( - zhCN: "以点播方式下载直播流", - zhTW: "以點播方式下載直播流", - enUS: "Download live streams as vod" - ), - ["cmd_liveWaitTime"] = new TextContainer - ( - zhCN: "手动设置直播列表刷新间隔", - zhTW: "手動設置直播列表刷新間隔", - enUS: "Manually set the live playlist refresh interval" - ), - ["cmd_adKeyword"] = new TextContainer - ( - zhCN: "设置广告分片的URL关键字(正则表达式)", - zhTW: "設置廣告分片的URL關鍵字(正則表達式)", - enUS: "Set URL keywords (regular expressions) for AD segments" - ), - ["cmd_liveTakeCount"] = new TextContainer - ( - zhCN: "手动设置录制直播时首次获取分片的数量", - zhTW: "手動設置錄製直播時首次獲取分片的數量", - enUS: "Manually set the number of segments downloaded for the first time when recording live" - ), - ["cmd_customHLSMethod"] = new TextContainer - ( - zhCN: "指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)", - zhTW: "指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)", - enUS: "Set HLS encryption method (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)" - ), - ["cmd_customHLSKey"] = new TextContainer - ( - zhCN: "指定HLS解密KEY. 可以是文件, HEX或Base64", - zhTW: "指定HLS解密KEY. 可以是文件, HEX或Base64", - enUS: "Set the HLS decryption key. Can be file, HEX or Base64" - ), - ["cmd_customHLSIv"] = new TextContainer - ( - zhCN: "指定HLS解密IV. 可以是文件, HEX或Base64", - zhTW: "指定HLS解密IV. 可以是文件, HEX或Base64", - enUS: "Set the HLS decryption iv. Can be file, HEX or Base64" - ), - ["cmd_livePipeMux"] = new TextContainer - ( - zhCN: "录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件", - zhTW: "錄製直播並開啟即時合併時通過管道+ffmpeg即時混流到TS文件", - enUS: "Real-time muxing to TS file through pipeline + ffmpeg (liveRealTimeMerge enabled)" - ), - ["cmd_liveKeepSegments"] = new TextContainer - ( - zhCN: "录制直播并开启实时合并时依然保留分片", - zhTW: "錄製直播並開啟即時合併時依然保留分片", - enUS: "Keep segments when recording a live (liveRealTimeMerge enabled)" - ), - ["cmd_liveRecordLimit"] = new TextContainer - ( - zhCN: "录制直播时的录制时长限制", - zhTW: "錄製直播時的錄製時長限制", - enUS: "Recording time limit when recording live" - ), - ["cmd_taskStartAt"] = new TextContainer - ( - zhCN: "在此时间之前不会开始执行任务", - zhTW: "在此時間之前不會開始執行任務", - enUS: "Task execution will not start before this time" - ), - ["cmd_useShakaPackager"] = new TextContainer - ( - zhCN: "解密时使用shaka-packager替代mp4decrypt", - zhTW: "解密時使用shaka-packager替代mp4decrypt", - enUS: "Use shaka-packager instead of mp4decrypt to decrypt" - ), - ["cmd_concurrentDownload"] = new TextContainer - ( - zhCN: "并发下载已选择的音频、视频和字幕", - zhTW: "並發下載已選擇的音訊、影片和字幕", - enUS: "Concurrently download the selected audio, video and subtitles" - ), - ["cmd_selectVideo"] = new TextContainer - ( - zhCN: "通过正则表达式选择符合要求的视频流. 输入 \"--morehelp select-video\" 以查看详细信息", - zhTW: "通過正則表達式選擇符合要求的影片軌. 輸入 \"--morehelp select-video\" 以查看詳細訊息", - enUS: "Select video streams by regular expressions. Use \"--morehelp select-video\" for more details" - ), - ["cmd_dropVideo"] = new TextContainer - ( - zhCN: "通过正则表达式去除符合要求的视频流.", - zhTW: "通過正則表達式去除符合要求的影片串流.", - enUS: "Drop video streams by regular expressions." - ), - ["cmd_selectVideo_more"] = new TextContainer - ( - zhCN: "通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数:\r\n\r\n" + - "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + - "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + - "plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\r\n\r\n" + - "* for=FOR: 选择方式. best[number], worst[number], all (默认: best)\r\n\r\n" + - "例如: \r\n" + - "# 选择最佳视频\r\n" + - "-sv best\r\n" + - "# 选择4K+HEVC视频\r\n" + - "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + - "# 选择长度大于1小时20分钟30秒的视频\r\n" + - "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + - "-sv role=\"main\":for=best\r\n" + - "# 选择码率在800Kbps至1Mbps之间的视频\r\n" + - "-sv bwMin=800:bwMax=1000\r\n", - zhTW: "通過正則表達式選擇符合要求的影片軌. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + - "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + - "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + - "plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\r\n\r\n" + - "* for=FOR: 選擇方式. best[number], worst[number], all (默認: best)\r\n\r\n" + - "例如: \r\n" + - "# 選擇最佳影片\r\n" + - "-sv best\r\n" + - "# 選擇4K+HEVC影片\r\n" + - "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + - "# 選擇長度大於1小時20分鐘30秒的影片\r\n" + - "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + - "-sv role=\"main\":for=best\r\n" + - "# 選擇碼率在800Kbps至1Mbps之間的影片\r\n" + - "-sv bwMin=800:bwMax=1000\r\n", - enUS: "Select video streams by regular expressions. OPTIONS is a colon separated list of:\r\n\r\n" + - "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + - "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + - "plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\r\n\r\n" + - "* for=FOR: Select type. best[number], worst[number], all (Default: best)\r\n\r\n" + - "Examples: \r\n" + - "# select best video\r\n" + - "-sv best\r\n" + - "# select 4K+HEVC video\r\n" + - "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + - "# Select best video with duration longer than 1 hour 20 minutes 30 seconds\r\n" + - "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + - "-sv role=\"main\":for=best\r\n" + - "# Select video with bandwidth between 800Kbps and 1Mbps\r\n" + - "-sv bwMin=800:bwMax=1000\r\n" - ), - ["cmd_selectAudio"] = new TextContainer - ( - zhCN: "通过正则表达式选择符合要求的音频流. 输入 \"--morehelp select-audio\" 以查看详细信息", - zhTW: "通過正則表達式選擇符合要求的音軌. 輸入 \"--morehelp select-audio\" 以查看詳細訊息", - enUS: "Select audio streams by regular expressions. Use \"--morehelp select-audio\" for more details" - ), - ["cmd_dropAudio"] = new TextContainer - ( - zhCN: "通过正则表达式去除符合要求的音频流.", - zhTW: "通過正則表達式去除符合要求的音軌.", - enUS: "Drop audio streams by regular expressions." - ), - ["cmd_selectAudio_more"] = new TextContainer - ( - zhCN: "通过正则表达式选择符合要求的音频流. 参考 --select-video\r\n\r\n" + - "例如: \r\n" + - "# 选择所有音频\r\n" + - "-sa all\r\n" + - "# 选择最佳英语音轨\r\n" + - "-sa lang=en:for=best\r\n" + - "# 选择最佳的2条英语(或日语)音轨\r\n" + - "-sa lang=\"ja|en\":for=best2\r\n" + - "-sa role=\"main\":for=best\r\n", - zhTW: "通過正則表達式選擇符合要求的音軌. 參考 --select-video\r\n\r\n" + - "例如: \r\n" + - "# 選擇所有音訊\r\n" + - "-sa all\r\n" + - "# 選擇最佳英語音軌\r\n" + - "-sa lang=en:for=best\r\n" + - "# 選擇最佳的2條英語(或日語)音軌\r\n" + - "-sa lang=\"ja|en\":for=best2\r\n" + - "-sa role=\"main\":for=best\r\n", - enUS: "Select audio streams by regular expressions. ref --select-video\r\n\r\n" + - "Examples: \r\n" + - "# select all\r\n" + - "-sa all\r\n" + - "# select best eng audio\r\n" + - "-sa lang=en:for=best\r\n" + - "# select best 2, and language is ja or en\r\n" + - "-sa lang=\"ja|en\":for=best2\r\n" + - "-sa role=\"main\":for=best\r\n" - ), - ["cmd_selectSubtitle"] = new TextContainer - ( - zhCN: "通过正则表达式选择符合要求的字幕流. 输入 \"--morehelp select-subtitle\" 以查看详细信息", - zhTW: "通過正則表達式選擇符合要求的字幕流. 輸入 \"--morehelp select-subtitle\" 以查看詳細訊息", - enUS: "Select subtitle streams by regular expressions. Use \"--morehelp select-subtitle\" for more details" - ), - ["cmd_dropSubtitle"] = new TextContainer - ( - zhCN: "通过正则表达式去除符合要求的字幕流.", - zhTW: "通過正則表達式去除符合要求的字幕流.", - enUS: "Drop subtitle streams by regular expressions." - ), - ["cmd_custom_range"] = new TextContainer - ( - zhCN: "下载点播内容时, 仅下载部分分片.\r\n\r\n" + - "例如: \r\n" + - "# 下载[0,10]共11个分片\r\n" + - "--custom-range 0-10\r\n" + - "# 下载从序号10开始的后续分片\r\n" + - "--custom-range 10-\r\n" + - "# 下载前100个分片\r\n" + - "--custom-range -99\r\n" + - "# 下载第5分钟到20分钟的内容\r\n" + - "--custom-range 05:00-20:00\r\n", - zhTW: "下載點播內容時, 僅下載部分分片.\r\n\r\n" + - "例如: \r\n" + - "# 下載[0,10]共11個分片\r\n" + - "--custom-range 0-10\r\n" + - "# 下載從序號10開始的後續分片\r\n" + - "--custom-range 10-\r\n" + - "# 下載前100個分片\r\n" + - "--custom-range -99\r\n" + - "# 下載第5分鐘到20分鐘的內容\r\n" + - "--custom-range 05:00-20:00\r\n", - enUS: "Download only part of the segments when downloading vod content.\r\n\r\n" + - "Examples: \r\n" + - "# Download [0,10], a total of 11 segments\r\n" + - "--custom-range 0-10\r\n" + - "# Download subsequent segments starting from index 10\r\n" + - "--custom-range 10-\r\n" + - "# Download the first 100 segments\r\n" + - "--custom-range -99\r\n" + - "# Download content from the 05:00 to 20:00\r\n" + - "--custom-range 05:00-20:00\r\n" - ), - ["cmd_selectSubtitle_more"] = new TextContainer - ( - zhCN: "通过正则表达式选择符合要求的字幕流. 参考 --select-video\r\n\r\n" + - "例如: \r\n" + - "# 选择所有字幕\r\n" + - "-ss all\r\n" + - "# 选择所有带有\"中文\"的字幕\r\n" + - "-ss name=\"中文\":for=all\r\n", - zhTW: "通過正則表達式選擇符合要求的字幕流. 參考 --select-video\r\n\r\n" + - "例如: \r\n" + - "# 選擇所有字幕\r\n" + - "-ss all\r\n" + - "# 選擇所有帶有\"中文\"的字幕\r\n" + - "-ss name=\"中文\":for=all\r\n", - enUS: "Select subtitle streams by regular expressions. ref --select-video\r\n\r\n" + - "Examples: \r\n" + - "# select all subs\r\n" + - "-ss all\r\n" + - "# select all subs containing \"English\"\r\n" + - "-ss name=\"English\":for=all\r\n" - ), - ["cmd_muxAfterDone_more"] = new TextContainer - ( - zhCN: "所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\r\n\r\n" + - "* format=FORMAT: 指定混流容器 mkv, mp4, ts\r\n" + - "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\r\n" + - "* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" + - "* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\r\n" + - "* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)\r\n\r\n" + - "例如: \r\n" + - "# 混流为mp4容器\r\n" + - "-M format=mp4\r\n" + - "# 使用mkvmerge, 自动寻找程序\r\n" + - "-M format=mkv:muxer=mkvmerge\r\n" + - "# 使用mkvmerge, 自定义程序路径\r\n" + - "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", - zhTW: "所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + - "* format=FORMAT: 指定混流容器 mkv, mp4, ts\r\n" + - "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默認: ffmpeg)\r\n" + - "* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" + - "* skip_sub=BOOL: 是否忽略字幕文件 (默認: false)\r\n" + - "* keep=BOOL: 混流完成是否保留文件 true, false (默認: false)\r\n\r\n" + - "例如: \r\n" + - "# 混流為mp4容器\r\n" + - "-M format=mp4\r\n" + - "# 使用mkvmerge, 自動尋找程序\r\n" + - "-M format=mkv:muxer=mkvmerge\r\n" + - "# 使用mkvmerge, 自訂程序路徑\r\n" + - "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", - enUS: "When all works is done, try to mux the downloaded streams. OPTIONS is a colon separated list of:\r\n\r\n" + - "* format=FORMAT: set container. mkv, mp4, ts\r\n" + - "* muxer=MUXER: set muxer. ffmpeg, mkvmerge (Default: ffmpeg)\r\n" + - "* bin_path=PATH: set binary file path. (Default: auto)\r\n" + - "* skip_sub=BOOL: set whether or not skip subtitle files (Default: false)\r\n" + - "* keep=BOOL: set whether or not keep files. true, false (Default: false)\r\n\r\n" + - "Examples: \r\n" + - "# mux to mp4\r\n" + - "-M format=mp4\r\n" + - "# use mkvmerge, auto detect bin path\r\n" + - "-M format=mkv:muxer=mkvmerge\r\n" + - "# use mkvmerge, set bin path\r\n" + - "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n" - ), - ["cmd_muxAfterDone"] = new TextContainer - ( - zhCN: "所有工作完成时尝试混流分离的音视频. 输入 \"--morehelp mux-after-done\" 以查看详细信息", - zhTW: "所有工作完成時嘗試混流分離的影音. 輸入 \"--morehelp mux-after-done\" 以查看詳細訊息", - enUS: "When all works is done, try to mux the downloaded streams. Use \"--morehelp mux-after-done\" for more details" - ), - ["cmd_muxImport"] = new TextContainer - ( - zhCN: "混流时引入外部媒体文件. 输入 \"--morehelp mux-import\" 以查看详细信息", - zhTW: "混流時引入外部媒體檔案. 輸入 \"--morehelp mux-import\" 以查看詳細訊息", - enUS: "When MuxAfterDone enabled, allow to import local media files. Use \"--morehelp mux-import\" for more details" - ), - ["cmd_muxImport_more"] = new TextContainer - ( - zhCN: "混流时引入外部媒体文件. 你能够以:分隔形式指定如下参数:\r\n\r\n" + - "* path=PATH: 指定媒体文件路径\r\n" + - "* lang=CODE: 指定媒体文件语言代码 (非必须)\r\n" + - "* name=NAME: 指定媒体文件描述信息 (非必须)\r\n\r\n" + - "例如: \r\n" + - "# 引入外部字幕\r\n" + - "--mux-import path=zh-Hans.srt:lang=chi:name=\"中文 (简体)\"\r\n" + - "# 引入外部音轨+字幕\r\n" + - "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"", - zhTW: "混流時引入外部媒體檔案. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + - "* path=PATH: 指定媒體檔案路徑\r\n" + - "* lang=CODE: 指定媒體檔案語言代碼 (非必須)\r\n" + - "* name=NAME: 指定媒體檔案描述訊息 (非必須)\r\n\r\n" + - "例如: \r\n" + - "# 引入外部字幕\r\n" + - "--mux-import path=zh-Hant.srt:lang=chi:name=\"中文 (繁體)\"\r\n" + - "# 引入外部音軌+字幕\r\n" + - "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"", - enUS: "When MuxAfterDone enabled, allow to import local media files. OPTIONS is a colon separated list of:\r\n\r\n" + - "* path=PATH: set file path\r\n" + - "* lang=CODE: set media language code (not required)\r\n" + - "* name=NAME: set description (not required)\r\n\r\n" + - "Examples: \r\n" + - "# import subtitle\r\n" + - "--mux-import path=en-US.srt:lang=eng:name=\"English (Original)\"\r\n" + - "# import audio and subtitle\r\n" + - "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"" - ), - ["cmd_writeMetaJson"] = new TextContainer - ( - zhCN: "解析后的信息是否输出json文件", - zhTW: "解析後的訊息是否輸出json文件", - enUS: "Write meta json after parsed" - ), - ["liveLimit"] = new TextContainer - ( - zhCN: "本次直播录制时长上限: ", - zhTW: "本次直播錄製時長上限: ", - enUS: "Live recording duration limit: " - ), - ["realTimeDecMessage"] = new TextContainer - ( - zhCN: "启用实时解密时,建议用shaka-packager而非mp4decrypt", - zhTW: "啟用即時解密時,建議用shaka-packager而非mp4decrypt", - enUS: "When enabling real-time decryption, it is recommended to use shaka-packager instead of mp4decrypt" - ), - ["liveLimitReached"] = new TextContainer - ( - zhCN: "到达直播录制上限,即将停止录制", - zhTW: "到達直播錄製上限,即將停止錄製", - enUS: "Live recording limit reached, will stop recording soon" - ), - ["saveName"] = new TextContainer - ( - zhCN: "保存文件名: ", - zhTW: "保存檔案名: ", - enUS: "Save Name: " - ), - ["fetch"] = new TextContainer - ( - zhCN: "获取: ", - zhTW: "獲取: ", - enUS: "Fetch: " - ), - ["ffmpegMerge"] = new TextContainer - ( - zhCN: "调用ffmpeg合并中...", - zhTW: "調用ffmpeg合併中...", - enUS: "ffmpeg merging..." - ), - ["ffmpegNotFound"] = new TextContainer - ( - zhCN: "找不到ffmpeg,请自行下载:https://ffmpeg.org/download.html", - zhTW: "找不到ffmpeg,請自行下載:https://ffmpeg.org/download.html", - enUS: "ffmpeg not found, please download at: https://ffmpeg.org/download.html" - ), - ["fixingTTML"] = new TextContainer - ( - zhCN: "正在提取TTML(raw)字幕...", - zhTW: "正在提取TTML(raw)字幕...", - enUS: "Extracting TTML(raw) subtitle..." - ), - ["fixingTTMLmp4"] = new TextContainer - ( - zhCN: "正在提取TTML(mp4)字幕...", - zhTW: "正在提取TTML(mp4)字幕...", - enUS: "Extracting TTML(mp4) subtitle..." - ), - ["fixingVTT"] = new TextContainer - ( - zhCN: "正在提取VTT(raw)字幕...", - zhTW: "正在提取VTT(raw)字幕...", - enUS: "Extracting VTT(raw) subtitle..." - ), - ["fixingVTTmp4"] = new TextContainer - ( - zhCN: "正在提取VTT(mp4)字幕...", - zhTW: "正在提取VTT(mp4)字幕...", - enUS: "Extracting VTT(mp4) subtitle..." - ), - ["keyProcessorNotFound"] = new TextContainer - ( - zhCN: "找不到支持的Processor", - zhTW: "找不到支持的Processor", - enUS: "No Processor matched" - ), - ["liveFound"] = new TextContainer - ( - zhCN: "检测到直播流", - zhTW: "檢測到直播流", - enUS: "Live stream found" - ), - ["loadingUrl"] = new TextContainer - ( - zhCN: "加载URL: ", - zhTW: "載入URL: ", - enUS: "Loading URL: " - ), - ["masterM3u8Found"] = new TextContainer - ( - zhCN: "检测到Master列表,开始解析全部流信息", - zhTW: "檢測到Master列表,開始解析全部流訊息", - enUS: "Master List detected, try parse all streams" - ), - ["matchTS"] = new TextContainer - ( - zhCN: "内容匹配: [white on green3]HTTP Live MPEG2-TS[/]", - zhTW: "內容匹配: [white on green3]HTTP Live MPEG2-TS[/]", - enUS: "Content Matched: [white on green3]HTTP Live MPEG2-TS[/]" - ), - ["matchDASH"] = new TextContainer - ( - zhCN: "内容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]", - zhTW: "內容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]", - enUS: "Content Matched: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]" - ), - ["matchMSS"] = new TextContainer - ( - zhCN: "内容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]", - zhTW: "內容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]", - enUS: "Content Matched: [white on steelblue1]Microsoft Smooth Streaming[/]" - ), - ["matchHLS"] = new TextContainer - ( - zhCN: "内容匹配: [white on deepskyblue1]HTTP Live Streaming[/]", - zhTW: "內容匹配: [white on deepskyblue1]HTTP Live Streaming[/]", - enUS: "Content Matched: [white on deepskyblue1]HTTP Live Streaming[/]" - ), - ["partMerge"] = new TextContainer - ( - zhCN: "分片数量大于1800个,开始分块合并...", - zhTW: "分片數量大於1800個,開始分塊合併...", - enUS: "Segments more than 1800, start partial merge..." - ), - ["notSupported"] = new TextContainer - ( - zhCN: "当前输入不受支持: ", - zhTW: "當前輸入不受支援: ", - enUS: "Input not supported: " - ), - ["parsingStream"] = new TextContainer - ( - zhCN: "正在解析媒体信息...", - zhTW: "正在解析媒體信息...", - enUS: "Parsing streams..." - ), - ["promptChoiceText"] = new TextContainer - ( - zhCN: "[grey](按键盘上下键以浏览更多内容)[/]", - zhTW: "[grey](按鍵盤上下鍵以瀏覽更多內容)[/]", - enUS: "[grey](Move up and down to reveal more streams)[/]" - ), - ["promptInfo"] = new TextContainer - ( - zhCN: "(按 [blue]空格键[/] 选择流, [green]回车键[/] 完成选择)", - zhTW: "(按 [blue]空格鍵[/] 選擇流, [green]確認鍵[/] 完成選擇)", - enUS: "(Press [blue][/] to toggle a stream, [green][/] to accept)" - ), - ["promptTitle"] = new TextContainer - ( - zhCN: "请选择 [green]你要下载的内容[/]:", - zhTW: "請選擇 [green]你要下載的內容[/]:", - enUS: "Please select [green]what you want to download[/]:" - ), - ["readingInfo"] = new TextContainer - ( - zhCN: "读取媒体信息...", - zhTW: "讀取媒體訊息...", - enUS: "Reading media info..." - ), - ["searchKey"] = new TextContainer - ( - zhCN: "正在尝试从文本文件搜索KEY...", - zhTW: "正在嘗試從文本文件搜尋KEY...", - enUS: "Trying to search for KEY from text file..." - ), - ["segmentCountCheckNotPass"] = new TextContainer - ( - zhCN: "分片数量校验不通过, 共{}个,已下载{}.", - zhTW: "分片數量校驗不通過, 共{}個,已下載{}.", - enUS: "Segment count check not pass, total: {}, downloaded: {}." - ), - ["selectedStream"] = new TextContainer - ( - zhCN: "已选择的流:", - zhTW: "已選擇的流:", - enUS: "Selected streams:" - ), - ["startDownloading"] = new TextContainer - ( - zhCN: "开始下载...", - zhTW: "開始下載...", - enUS: "Start downloading..." - ), - ["streamsInfo"] = new TextContainer - ( - zhCN: "已解析, 共计 {} 条媒体流, 基本流 {} 条, 可选音频流 {} 条, 可选字幕流 {} 条", - zhTW: "已解析, 共計 {} 條媒體流, 基本流 {} 條, 可選音頻流 {} 條, 可選字幕流 {} 條", - enUS: "Extracted, there are {} streams, with {} basic streams, {} audio streams, {} subtitle streams" - ), - ["writeJson"] = new TextContainer - ( - zhCN: "写出meta json", - zhTW: "寫出meta json", - enUS: "Writing meta json" - ), - ["noStreamsToDownload"] = new TextContainer - ( - zhCN: "没有找到需要下载的流", - zhTW: "沒有找到需要下載的流", - enUS: "No stream found to download" - ), +namespace N_m3u8DL_RE.Common.Resource; - }; - } -} +internal class StaticText +{ + public static Dictionary LANG_DIC = new() + { + ["singleFileSplitWarn"] = new TextContainer + ( + zhCN: "整段文件已被自动切割为小分片以加速下载", + zhTW: "整段文件已被自動切割為小分片以加速下載", + enUS: "The entire file has been cut into small segments to accelerate" + ), + ["singleFileRealtimeDecryptWarn"] = new TextContainer + ( + zhCN: "实时解密已被强制关闭", + zhTW: "即時解密已被強制關閉", + enUS: "Real-time decryption has been disabled" + ), + ["cmd_forceAnsiConsole"] = new TextContainer + ( + zhCN: "强制认定终端为支持ANSI且可交互的终端", + zhTW: "強制認定終端為支援ANSI且可交往的終端", + enUS: "Force assuming the terminal is ANSI-compatible and interactive" + ), + ["cmd_noAnsiColor"] = new TextContainer + ( + zhCN: "去除ANSI颜色", + zhTW: "關閉ANSI顏色", + enUS: "Remove ANSI colors" + ), + ["customRangeWarn"] = new TextContainer + ( + zhCN: "请注意,自定义下载范围有时会导致音画不同步", + zhTW: "請注意,自定義下載範圍有時會導致音畫不同步", + enUS: "Please note that custom range may sometimes result in audio and video being out of sync" + ), + ["customRangeInvalid"] = new TextContainer + ( + zhCN: "自定义下载范围无效", + zhTW: "自定義下載範圍無效", + enUS: "User customed range invalid" + ), + ["customAdKeywordsFound"] = new TextContainer + ( + zhCN: "用户自定义广告分片URL关键字:", + zhTW: "用戶自定義廣告分片URL關鍵字:", + enUS: "User customed Ad keyword: " + ), + ["customRangeFound"] = new TextContainer + ( + zhCN: "用户自定义下载范围:", + zhTW: "用戶自定義下載範圍:", + enUS: "User customed range: " + ), + ["consoleRedirected"] = new TextContainer + ( + zhCN: "输出被重定向, 将清除ANSI颜色", + zhTW: "輸出被重定向, 將清除ANSI顏色", + enUS: "Output is redirected, ANSI colors are cleared." + ), + ["processImageSub"] = new TextContainer + ( + zhCN: "正在处理图形字幕", + zhTW: "正在處理圖形字幕", + enUS: "Processing Image Sub" + ), + ["newVersionFound"] = new TextContainer + ( + zhCN: "检测到新版本,请尽快升级!", + zhTW: "檢測到新版本,請盡快升級!", + enUS: "New version detected!" + ), + ["namedPipeCreated"] = new TextContainer + ( + zhCN: "已创建命名管道:", + zhTW: "已創建命名管道:", + enUS: "Named pipe created: " + ), + ["namedPipeMux"] = new TextContainer + ( + zhCN: "通过命名管道混流到", + zhTW: "通過命名管道混流到", + enUS: "Mux with named pipe, to" + ), + ["taskStartAt"] = new TextContainer + ( + zhCN: "程序将等待,直到:", + zhTW: "程序將等待,直到:", + enUS: "The program will wait until: " + ), + ["autoBinaryMerge"] = new TextContainer + ( + zhCN: "检测到fMP4,自动开启二进制合并", + zhTW: "檢測到fMP4,自動開啟二進位制合併", + enUS: "fMP4 is detected, binary merging is automatically enabled" + ), + ["autoBinaryMerge2"] = new TextContainer + ( + zhCN: "检测到杜比视界内容,自动开启二进制合并", + zhTW: "檢測到杜比視界內容,自動開啟二進位制合併", + enUS: "Dolby Vision content is detected, binary merging is automatically enabled" + ), + ["autoBinaryMerge3"] = new TextContainer + ( + zhCN: "检测到无法识别的加密方式,自动开启二进制合并", + zhTW: "檢測到無法識別的加密方式,自動開啟二進位制合併", + enUS: "An unrecognized encryption method is detected, binary merging is automatically enabled" + ), + ["autoBinaryMerge4"] = new TextContainer + ( + zhCN: "检测到CENC加密方式,自动开启二进制合并", + zhTW: "檢測到CENC加密方式,自動開啟二進位制合併", + enUS: "When CENC encryption is detected, binary merging is automatically enabled" + ), + ["autoBinaryMerge5"] = new TextContainer + ( + zhCN: "检测到杜比视界内容,混流功能已禁用", + zhTW: "檢測到杜比視界內容,混流功能已禁用", + enUS: "Dolby Vision content is detected, mux after done is automatically disabled" + ), + ["autoBinaryMerge6"] = new TextContainer + ( + zhCN: "你已开启下载完成后混流,自动开启二进制合并", + zhTW: "你已開啟下載完成後混流,自動開啟二進制合併", + enUS: "MuxAfterDone is detected, binary merging is automatically enabled" + ), + ["badM3u8"] = new TextContainer + ( + zhCN: "错误的m3u8", + zhTW: "錯誤的m3u8", + enUS: "Bad m3u8" + ), + ["binaryMerge"] = new TextContainer + ( + zhCN: "二进制合并中...", + zhTW: "二進位制合併中...", + enUS: "Binary merging..." + ), + ["checkingLast"] = new TextContainer + ( + zhCN: "验证最后一个分片有效性", + zhTW: "驗證最後一個分片有效性", + enUS: "Verifying the validity of the last segment" + ), + ["cmd_baseUrl"] = new TextContainer + ( + zhCN: "设置BaseURL", + zhTW: "設置BaseURL", + enUS: "Set BaseURL" + ), + ["cmd_maxSpeed"] = new TextContainer + ( + zhCN: "设置限速,单位支持 Mbps 或 Kbps,如:15M 100K", + zhTW: "設置限速,單位支持 Mbps 或 Kbps,如:15M 100K", + enUS: "Set speed limit, Mbps or Kbps, for example: 15M 100K." + ), + ["cmd_noDateInfo"] = new TextContainer + ( + zhCN: "混流时不写入日期信息", + zhTW: "混流時不寫入日期訊息", + enUS: "Date information is not written during muxing" + ), + ["cmd_noLog"] = new TextContainer + ( + zhCN: "关闭日志文件输出", + zhTW: "關閉日誌文件輸出", + enUS: "Disable log file output" + ), + ["cmd_appendUrlParams"] = new TextContainer + ( + zhCN: "将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com", + zhTW: "將輸入Url的Params添加至分片, 對某些網站很有用, 例如 kakao.com", + enUS: "Add Params of input Url to segments, useful for some websites, such as kakao.com" + ), + ["cmd_autoSelect"] = new TextContainer + ( + zhCN: "自动选择所有类型的最佳轨道", + zhTW: "自動選擇所有類型的最佳軌道", + enUS: "Automatically selects the best tracks of all types" + ), + ["cmd_binaryMerge"] = new TextContainer + ( + zhCN: "二进制合并", + zhTW: "二進位制合併", + enUS: "Binary merge" + ), + ["cmd_useFFmpegConcatDemuxer"] = new TextContainer + ( + zhCN: "使用 ffmpeg 合并时,使用 concat 分离器而非 concat 协议", + zhTW: "使用 ffmpeg 合併時,使用 concat 分離器而非 concat 協議", + enUS: "When merging with ffmpeg, use the concat demuxer instead of the concat protocol" + ), + ["cmd_checkSegmentsCount"] = new TextContainer + ( + zhCN: "检测实际下载的分片数量和预期数量是否匹配", + zhTW: "檢測實際下載的分片數量和預期數量是否匹配", + enUS: "Check if the actual number of segments downloaded matches the expected number" + ), + ["cmd_downloadRetryCount"] = new TextContainer + ( + zhCN: "每个分片下载异常时的重试次数", + zhTW: "每個分片下載異常時的重試次數", + enUS: "The number of retries when download segment error" + ), + ["cmd_decryptionBinaryPath"] = new TextContainer + ( + zhCN: "MP4解密所用工具的全路径, 例如 C:\\Tools\\mp4decrypt.exe", + zhTW: "MP4解密所用工具的全路徑, 例如 C:\\Tools\\mp4decrypt.exe", + enUS: "Full path to the tool used for MP4 decryption, like C:\\Tools\\mp4decrypt.exe" + ), + ["cmd_delAfterDone"] = new TextContainer + ( + zhCN: "完成后删除临时文件", + zhTW: "完成後刪除臨時文件", + enUS: "Delete temporary files when done" + ), + ["cmd_ffmpegBinaryPath"] = new TextContainer + ( + zhCN: "ffmpeg可执行程序全路径, 例如 C:\\Tools\\ffmpeg.exe", + zhTW: "ffmpeg可執行程序全路徑, 例如 C:\\Tools\\ffmpeg.exe", + enUS: "Full path to the ffmpeg binary, like C:\\Tools\\ffmpeg.exe" + ), + ["cmd_mkvmergeBinaryPath"] = new TextContainer + ( + zhCN: "mkvmerge可执行程序全路径, 例如 C:\\Tools\\mkvmerge.exe", + zhTW: "mkvmerge可執行程序全路徑, 例如 C:\\Tools\\mkvmerge.exe", + enUS: "Full path to the mkvmerge binary, like C:\\Tools\\mkvmerge.exe" + ), + ["cmd_liveFixVttByAudio"] = new TextContainer + ( + zhCN: "通过读取音频文件的起始时间修正VTT字幕", + zhTW: "透過讀取音訊檔案的起始時間修正VTT字幕", + enUS: "Correct VTT sub by reading the start time of the audio file" + ), + ["cmd_header"] = new TextContainer + ( + zhCN: "为HTTP请求设置特定的请求头, 例如:\r\n-H \"Cookie: mycookie\" -H \"User-Agent: iOS\"", + zhTW: "為HTTP請求設置特定的請求頭, 例如:\r\n-H \"Cookie: mycookie\" -H \"User-Agent: iOS\"", + enUS: "Pass custom header(s) to server, Example:\r\n-H \"Cookie: mycookie\" -H \"User-Agent: iOS\"" + ), + ["cmd_Input"] = new TextContainer + ( + zhCN: "链接或文件", + zhTW: "連結或文件", + enUS: "Input Url or File" + ), + ["cmd_keys"] = new TextContainer + ( + zhCN: "设置解密密钥, 程序调用mp4decrpyt/shaka-packager进行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2\r\n对于KEY相同的情况可以直接输入 --key KEY", + zhTW: "設置解密密鑰, 程序調用mp4decrpyt/shaka-packager進行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2\r\n對於KEY相同的情況可以直接輸入 --key KEY", + enUS: "Set decryption key(s) to mp4decrypt/shaka-packager. format:\r\n--key KID1:KEY1 --key KID2:KEY2\r\nor use --key KEY if all tracks share the same key." + ), + ["cmd_keyText"] = new TextContainer + ( + zhCN: "设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)", + zhTW: "設置密鑰文件,程序將從文件中按KID搜尋KEY以解密.(不建議使用特大文件)", + enUS: "Set the kid-key file, the program will search the KEY with KID from the file.(Very large file are not recommended)" + ), + ["cmd_loadKeyFailed"] = new TextContainer + ( + zhCN: "获取KEY失败,忽略读取.", + zhTW: "獲取KEY失敗,忽略讀取.", + enUS: "Failed to get KEY, ignore." + ), + ["cmd_logLevel"] = new TextContainer + ( + zhCN: "设置日志级别", + zhTW: "設置日誌級別", + enUS: "Set log level" + ), + ["cmd_MP4RealTimeDecryption"] = new TextContainer + ( + zhCN: "实时解密MP4分片", + zhTW: "即時解密MP4分片", + enUS: "Decrypt MP4 segments in real time" + ), + ["cmd_saveDir"] = new TextContainer + ( + zhCN: "设置输出目录", + zhTW: "設置輸出目錄", + enUS: "Set output directory" + ), + ["cmd_saveName"] = new TextContainer + ( + zhCN: "设置保存文件名", + zhTW: "設置保存檔案名", + enUS: "Set output filename" + ), + ["cmd_savePattern"] = new TextContainer + ( + zhCN: "设置保存文件命名模板, 支持使用变量", + zhTW: "", + enUS: "" + ), + ["cmd_skipDownload"] = new TextContainer + ( + zhCN: "跳过下载", + zhTW: "跳過下載", + enUS: "Skip download" + ), + ["cmd_skipMerge"] = new TextContainer + ( + zhCN: "跳过合并分片", + zhTW: "跳過合併分片", + enUS: "Skip segments merge" + ), + ["cmd_subFormat"] = new TextContainer + ( + zhCN: "字幕输出类型", + zhTW: "字幕輸出類型", + enUS: "Subtitle output format" + ), + ["cmd_subOnly"] = new TextContainer + ( + zhCN: "只选取字幕轨道", + zhTW: "只選取字幕軌道", + enUS: "Select only subtitle tracks" + ), + ["cmd_subtitleFix"] = new TextContainer + ( + zhCN: "自动修正字幕", + zhTW: "自動修正字幕", + enUS: "Automatically fix subtitles" + ), + ["cmd_threadCount"] = new TextContainer + ( + zhCN: "设置下载线程数", + zhTW: "設置下載執行緒數", + enUS: "Set download thread count" + ), + ["cmd_tmpDir"] = new TextContainer + ( + zhCN: "设置临时文件存储目录", + zhTW: "設置臨時文件儲存目錄", + enUS: "Set temporary file directory" + ), + ["cmd_uiLanguage"] = new TextContainer + ( + zhCN: "设置UI语言", + zhTW: "設置UI語言", + enUS: "Set UI language" + ), + ["cmd_moreHelp"] = new TextContainer + ( + zhCN: "查看某个选项的详细帮助信息", + zhTW: "查看某個選項的詳細幫助訊息", + enUS: "Set more help info about one option" + ), + ["cmd_urlProcessorArgs"] = new TextContainer + ( + zhCN: "此字符串将直接传递给URL Processor", + zhTW: "此字符串將直接傳遞給URL Processor", + enUS: "Give these arguments to the URL Processors." + ), + ["cmd_liveRealTimeMerge"] = new TextContainer + ( + zhCN: "录制直播时实时合并", + zhTW: "錄製直播時即時合併", + enUS: "Real-time merge into file when recording live" + ), + ["cmd_customProxy"] = new TextContainer + ( + zhCN: "设置请求代理, 如 http://127.0.0.1:8888", + zhTW: "設置請求代理, 如 http://127.0.0.1:8888", + enUS: "Set web request proxy, like http://127.0.0.1:8888" + ), + ["cmd_customRange"] = new TextContainer + ( + zhCN: "仅下载部分分片. 输入 \"--morehelp custom-range\" 以查看详细信息", + zhTW: "僅下載部分分片. 輸入 \"--morehelp custom-range\" 以查看詳細訊息", + enUS: "Download only part of the segments. Use \"--morehelp custom-range\" for more details" + ), + ["cmd_useSystemProxy"] = new TextContainer + ( + zhCN: "使用系统默认代理", + zhTW: "使用系統默認代理", + enUS: "Use system default proxy" + ), + ["cmd_livePerformAsVod"] = new TextContainer + ( + zhCN: "以点播方式下载直播流", + zhTW: "以點播方式下載直播流", + enUS: "Download live streams as vod" + ), + ["cmd_liveWaitTime"] = new TextContainer + ( + zhCN: "手动设置直播列表刷新间隔", + zhTW: "手動設置直播列表刷新間隔", + enUS: "Manually set the live playlist refresh interval" + ), + ["cmd_adKeyword"] = new TextContainer + ( + zhCN: "设置广告分片的URL关键字(正则表达式)", + zhTW: "設置廣告分片的URL關鍵字(正則表達式)", + enUS: "Set URL keywords (regular expressions) for AD segments" + ), + ["cmd_liveTakeCount"] = new TextContainer + ( + zhCN: "手动设置录制直播时首次获取分片的数量", + zhTW: "手動設置錄製直播時首次獲取分片的數量", + enUS: "Manually set the number of segments downloaded for the first time when recording live" + ), + ["cmd_customHLSMethod"] = new TextContainer + ( + zhCN: "指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)", + zhTW: "指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)", + enUS: "Set HLS encryption method (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)" + ), + ["cmd_customHLSKey"] = new TextContainer + ( + zhCN: "指定HLS解密KEY. 可以是文件, HEX或Base64", + zhTW: "指定HLS解密KEY. 可以是文件, HEX或Base64", + enUS: "Set the HLS decryption key. Can be file, HEX or Base64" + ), + ["cmd_customHLSIv"] = new TextContainer + ( + zhCN: "指定HLS解密IV. 可以是文件, HEX或Base64", + zhTW: "指定HLS解密IV. 可以是文件, HEX或Base64", + enUS: "Set the HLS decryption iv. Can be file, HEX or Base64" + ), + ["cmd_livePipeMux"] = new TextContainer + ( + zhCN: "录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件", + zhTW: "錄製直播並開啟即時合併時通過管道+ffmpeg即時混流到TS文件", + enUS: "Real-time muxing to TS file through pipeline + ffmpeg (liveRealTimeMerge enabled)" + ), + ["cmd_liveKeepSegments"] = new TextContainer + ( + zhCN: "录制直播并开启实时合并时依然保留分片", + zhTW: "錄製直播並開啟即時合併時依然保留分片", + enUS: "Keep segments when recording a live (liveRealTimeMerge enabled)" + ), + ["cmd_liveRecordLimit"] = new TextContainer + ( + zhCN: "录制直播时的录制时长限制", + zhTW: "錄製直播時的錄製時長限制", + enUS: "Recording time limit when recording live" + ), + ["cmd_taskStartAt"] = new TextContainer + ( + zhCN: "在此时间之前不会开始执行任务", + zhTW: "在此時間之前不會開始執行任務", + enUS: "Task execution will not start before this time" + ), + ["cmd_useShakaPackager"] = new TextContainer + ( + zhCN: "解密时使用shaka-packager替代mp4decrypt", + zhTW: "解密時使用shaka-packager替代mp4decrypt", + enUS: "Use shaka-packager instead of mp4decrypt to decrypt" + ), + ["cmd_concurrentDownload"] = new TextContainer + ( + zhCN: "并发下载已选择的音频、视频和字幕", + zhTW: "並發下載已選擇的音訊、影片和字幕", + enUS: "Concurrently download the selected audio, video and subtitles" + ), + ["cmd_selectVideo"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的视频流. 输入 \"--morehelp select-video\" 以查看详细信息", + zhTW: "通過正則表達式選擇符合要求的影片軌. 輸入 \"--morehelp select-video\" 以查看詳細訊息", + enUS: "Select video streams by regular expressions. Use \"--morehelp select-video\" for more details" + ), + ["cmd_dropVideo"] = new TextContainer + ( + zhCN: "通过正则表达式去除符合要求的视频流.", + zhTW: "通過正則表達式去除符合要求的影片串流.", + enUS: "Drop video streams by regular expressions." + ), + ["cmd_selectVideo_more"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数:\r\n\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + + "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + + "plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\r\n\r\n" + + "* for=FOR: 选择方式. best[number], worst[number], all (默认: best)\r\n\r\n" + + "例如: \r\n" + + "# 选择最佳视频\r\n" + + "-sv best\r\n" + + "# 选择4K+HEVC视频\r\n" + + "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + + "# 选择长度大于1小时20分钟30秒的视频\r\n" + + "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + + "-sv role=\"main\":for=best\r\n" + + "# 选择码率在800Kbps至1Mbps之间的视频\r\n" + + "-sv bwMin=800:bwMax=1000\r\n", + zhTW: "通過正則表達式選擇符合要求的影片軌. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + + "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + + "plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\r\n\r\n" + + "* for=FOR: 選擇方式. best[number], worst[number], all (默認: best)\r\n\r\n" + + "例如: \r\n" + + "# 選擇最佳影片\r\n" + + "-sv best\r\n" + + "# 選擇4K+HEVC影片\r\n" + + "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + + "# 選擇長度大於1小時20分鐘30秒的影片\r\n" + + "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + + "-sv role=\"main\":for=best\r\n" + + "# 選擇碼率在800Kbps至1Mbps之間的影片\r\n" + + "-sv bwMin=800:bwMax=1000\r\n", + enUS: "Select video streams by regular expressions. OPTIONS is a colon separated list of:\r\n\r\n" + + "id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\r\n" + + "segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\r\n" + + "plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\r\n\r\n" + + "* for=FOR: Select type. best[number], worst[number], all (Default: best)\r\n\r\n" + + "Examples: \r\n" + + "# select best video\r\n" + + "-sv best\r\n" + + "# select 4K+HEVC video\r\n" + + "-sv res=\"3840*\":codecs=hvc1:for=best\r\n" + + "# Select best video with duration longer than 1 hour 20 minutes 30 seconds\r\n" + + "-sv plistDurMin=\"1h20m30s\":for=best\r\n" + + "-sv role=\"main\":for=best\r\n" + + "# Select video with bandwidth between 800Kbps and 1Mbps\r\n" + + "-sv bwMin=800:bwMax=1000\r\n" + ), + ["cmd_selectAudio"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的音频流. 输入 \"--morehelp select-audio\" 以查看详细信息", + zhTW: "通過正則表達式選擇符合要求的音軌. 輸入 \"--morehelp select-audio\" 以查看詳細訊息", + enUS: "Select audio streams by regular expressions. Use \"--morehelp select-audio\" for more details" + ), + ["cmd_dropAudio"] = new TextContainer + ( + zhCN: "通过正则表达式去除符合要求的音频流.", + zhTW: "通過正則表達式去除符合要求的音軌.", + enUS: "Drop audio streams by regular expressions." + ), + ["cmd_selectAudio_more"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的音频流. 参考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 选择所有音频\r\n" + + "-sa all\r\n" + + "# 选择最佳英语音轨\r\n" + + "-sa lang=en:for=best\r\n" + + "# 选择最佳的2条英语(或日语)音轨\r\n" + + "-sa lang=\"ja|en\":for=best2\r\n" + + "-sa role=\"main\":for=best\r\n", + zhTW: "通過正則表達式選擇符合要求的音軌. 參考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 選擇所有音訊\r\n" + + "-sa all\r\n" + + "# 選擇最佳英語音軌\r\n" + + "-sa lang=en:for=best\r\n" + + "# 選擇最佳的2條英語(或日語)音軌\r\n" + + "-sa lang=\"ja|en\":for=best2\r\n" + + "-sa role=\"main\":for=best\r\n", + enUS: "Select audio streams by regular expressions. ref --select-video\r\n\r\n" + + "Examples: \r\n" + + "# select all\r\n" + + "-sa all\r\n" + + "# select best eng audio\r\n" + + "-sa lang=en:for=best\r\n" + + "# select best 2, and language is ja or en\r\n" + + "-sa lang=\"ja|en\":for=best2\r\n" + + "-sa role=\"main\":for=best\r\n" + ), + ["cmd_selectSubtitle"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的字幕流. 输入 \"--morehelp select-subtitle\" 以查看详细信息", + zhTW: "通過正則表達式選擇符合要求的字幕流. 輸入 \"--morehelp select-subtitle\" 以查看詳細訊息", + enUS: "Select subtitle streams by regular expressions. Use \"--morehelp select-subtitle\" for more details" + ), + ["cmd_dropSubtitle"] = new TextContainer + ( + zhCN: "通过正则表达式去除符合要求的字幕流.", + zhTW: "通過正則表達式去除符合要求的字幕流.", + enUS: "Drop subtitle streams by regular expressions." + ), + ["cmd_custom_range"] = new TextContainer + ( + zhCN: "下载点播内容时, 仅下载部分分片.\r\n\r\n" + + "例如: \r\n" + + "# 下载[0,10]共11个分片\r\n" + + "--custom-range 0-10\r\n" + + "# 下载从序号10开始的后续分片\r\n" + + "--custom-range 10-\r\n" + + "# 下载前100个分片\r\n" + + "--custom-range -99\r\n" + + "# 下载第5分钟到20分钟的内容\r\n" + + "--custom-range 05:00-20:00\r\n", + zhTW: "下載點播內容時, 僅下載部分分片.\r\n\r\n" + + "例如: \r\n" + + "# 下載[0,10]共11個分片\r\n" + + "--custom-range 0-10\r\n" + + "# 下載從序號10開始的後續分片\r\n" + + "--custom-range 10-\r\n" + + "# 下載前100個分片\r\n" + + "--custom-range -99\r\n" + + "# 下載第5分鐘到20分鐘的內容\r\n" + + "--custom-range 05:00-20:00\r\n", + enUS: "Download only part of the segments when downloading vod content.\r\n\r\n" + + "Examples: \r\n" + + "# Download [0,10], a total of 11 segments\r\n" + + "--custom-range 0-10\r\n" + + "# Download subsequent segments starting from index 10\r\n" + + "--custom-range 10-\r\n" + + "# Download the first 100 segments\r\n" + + "--custom-range -99\r\n" + + "# Download content from the 05:00 to 20:00\r\n" + + "--custom-range 05:00-20:00\r\n" + ), + ["cmd_selectSubtitle_more"] = new TextContainer + ( + zhCN: "通过正则表达式选择符合要求的字幕流. 参考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 选择所有字幕\r\n" + + "-ss all\r\n" + + "# 选择所有带有\"中文\"的字幕\r\n" + + "-ss name=\"中文\":for=all\r\n", + zhTW: "通過正則表達式選擇符合要求的字幕流. 參考 --select-video\r\n\r\n" + + "例如: \r\n" + + "# 選擇所有字幕\r\n" + + "-ss all\r\n" + + "# 選擇所有帶有\"中文\"的字幕\r\n" + + "-ss name=\"中文\":for=all\r\n", + enUS: "Select subtitle streams by regular expressions. ref --select-video\r\n\r\n" + + "Examples: \r\n" + + "# select all subs\r\n" + + "-ss all\r\n" + + "# select all subs containing \"English\"\r\n" + + "-ss name=\"English\":for=all\r\n" + ), + ["cmd_muxAfterDone_more"] = new TextContainer + ( + zhCN: "所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\r\n\r\n" + + "* format=FORMAT: 指定混流容器 mkv, mp4, ts\r\n" + + "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\r\n" + + "* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" + + "* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\r\n" + + "* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)\r\n\r\n" + + "例如: \r\n" + + "# 混流为mp4容器\r\n" + + "-M format=mp4\r\n" + + "# 使用mkvmerge, 自动寻找程序\r\n" + + "-M format=mkv:muxer=mkvmerge\r\n" + + "# 使用mkvmerge, 自定义程序路径\r\n" + + "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", + zhTW: "所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + + "* format=FORMAT: 指定混流容器 mkv, mp4, ts\r\n" + + "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默認: ffmpeg)\r\n" + + "* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" + + "* skip_sub=BOOL: 是否忽略字幕文件 (默認: false)\r\n" + + "* keep=BOOL: 混流完成是否保留文件 true, false (默認: false)\r\n\r\n" + + "例如: \r\n" + + "# 混流為mp4容器\r\n" + + "-M format=mp4\r\n" + + "# 使用mkvmerge, 自動尋找程序\r\n" + + "-M format=mkv:muxer=mkvmerge\r\n" + + "# 使用mkvmerge, 自訂程序路徑\r\n" + + "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", + enUS: "When all works is done, try to mux the downloaded streams. OPTIONS is a colon separated list of:\r\n\r\n" + + "* format=FORMAT: set container. mkv, mp4, ts\r\n" + + "* muxer=MUXER: set muxer. ffmpeg, mkvmerge (Default: ffmpeg)\r\n" + + "* bin_path=PATH: set binary file path. (Default: auto)\r\n" + + "* skip_sub=BOOL: set whether or not skip subtitle files (Default: false)\r\n" + + "* keep=BOOL: set whether or not keep files. true, false (Default: false)\r\n\r\n" + + "Examples: \r\n" + + "# mux to mp4\r\n" + + "-M format=mp4\r\n" + + "# use mkvmerge, auto detect bin path\r\n" + + "-M format=mkv:muxer=mkvmerge\r\n" + + "# use mkvmerge, set bin path\r\n" + + "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n" + ), + ["cmd_muxAfterDone"] = new TextContainer + ( + zhCN: "所有工作完成时尝试混流分离的音视频. 输入 \"--morehelp mux-after-done\" 以查看详细信息", + zhTW: "所有工作完成時嘗試混流分離的影音. 輸入 \"--morehelp mux-after-done\" 以查看詳細訊息", + enUS: "When all works is done, try to mux the downloaded streams. Use \"--morehelp mux-after-done\" for more details" + ), + ["cmd_muxImport"] = new TextContainer + ( + zhCN: "混流时引入外部媒体文件. 输入 \"--morehelp mux-import\" 以查看详细信息", + zhTW: "混流時引入外部媒體檔案. 輸入 \"--morehelp mux-import\" 以查看詳細訊息", + enUS: "When MuxAfterDone enabled, allow to import local media files. Use \"--morehelp mux-import\" for more details" + ), + ["cmd_muxImport_more"] = new TextContainer + ( + zhCN: "混流时引入外部媒体文件. 你能够以:分隔形式指定如下参数:\r\n\r\n" + + "* path=PATH: 指定媒体文件路径\r\n" + + "* lang=CODE: 指定媒体文件语言代码 (非必须)\r\n" + + "* name=NAME: 指定媒体文件描述信息 (非必须)\r\n\r\n" + + "例如: \r\n" + + "# 引入外部字幕\r\n" + + "--mux-import path=zh-Hans.srt:lang=chi:name=\"中文 (简体)\"\r\n" + + "# 引入外部音轨+字幕\r\n" + + "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"", + zhTW: "混流時引入外部媒體檔案. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + + "* path=PATH: 指定媒體檔案路徑\r\n" + + "* lang=CODE: 指定媒體檔案語言代碼 (非必須)\r\n" + + "* name=NAME: 指定媒體檔案描述訊息 (非必須)\r\n\r\n" + + "例如: \r\n" + + "# 引入外部字幕\r\n" + + "--mux-import path=zh-Hant.srt:lang=chi:name=\"中文 (繁體)\"\r\n" + + "# 引入外部音軌+字幕\r\n" + + "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"", + enUS: "When MuxAfterDone enabled, allow to import local media files. OPTIONS is a colon separated list of:\r\n\r\n" + + "* path=PATH: set file path\r\n" + + "* lang=CODE: set media language code (not required)\r\n" + + "* name=NAME: set description (not required)\r\n\r\n" + + "Examples: \r\n" + + "# import subtitle\r\n" + + "--mux-import path=en-US.srt:lang=eng:name=\"English (Original)\"\r\n" + + "# import audio and subtitle\r\n" + + "--mux-import path=\"D\\:\\media\\atmos.m4a\":lang=eng:name=\"English Description Audio\" --mux-import path=\"D\\:\\media\\eng.vtt\":lang=eng:name=\"English (Description)\"" + ), + ["cmd_writeMetaJson"] = new TextContainer + ( + zhCN: "解析后的信息是否输出json文件", + zhTW: "解析後的訊息是否輸出json文件", + enUS: "Write meta json after parsed" + ), + ["liveLimit"] = new TextContainer + ( + zhCN: "本次直播录制时长上限: ", + zhTW: "本次直播錄製時長上限: ", + enUS: "Live recording duration limit: " + ), + ["realTimeDecMessage"] = new TextContainer + ( + zhCN: "启用实时解密时,建议用shaka-packager而非mp4decrypt", + zhTW: "啟用即時解密時,建議用shaka-packager而非mp4decrypt", + enUS: "When enabling real-time decryption, it is recommended to use shaka-packager instead of mp4decrypt" + ), + ["liveLimitReached"] = new TextContainer + ( + zhCN: "到达直播录制上限,即将停止录制", + zhTW: "到達直播錄製上限,即將停止錄製", + enUS: "Live recording limit reached, will stop recording soon" + ), + ["saveName"] = new TextContainer + ( + zhCN: "保存文件名: ", + zhTW: "保存檔案名: ", + enUS: "Save Name: " + ), + ["fetch"] = new TextContainer + ( + zhCN: "获取: ", + zhTW: "獲取: ", + enUS: "Fetch: " + ), + ["ffmpegMerge"] = new TextContainer + ( + zhCN: "调用ffmpeg合并中...", + zhTW: "調用ffmpeg合併中...", + enUS: "ffmpeg merging..." + ), + ["ffmpegNotFound"] = new TextContainer + ( + zhCN: "找不到ffmpeg,请自行下载:https://ffmpeg.org/download.html", + zhTW: "找不到ffmpeg,請自行下載:https://ffmpeg.org/download.html", + enUS: "ffmpeg not found, please download at: https://ffmpeg.org/download.html" + ), + ["fixingTTML"] = new TextContainer + ( + zhCN: "正在提取TTML(raw)字幕...", + zhTW: "正在提取TTML(raw)字幕...", + enUS: "Extracting TTML(raw) subtitle..." + ), + ["fixingTTMLmp4"] = new TextContainer + ( + zhCN: "正在提取TTML(mp4)字幕...", + zhTW: "正在提取TTML(mp4)字幕...", + enUS: "Extracting TTML(mp4) subtitle..." + ), + ["fixingVTT"] = new TextContainer + ( + zhCN: "正在提取VTT(raw)字幕...", + zhTW: "正在提取VTT(raw)字幕...", + enUS: "Extracting VTT(raw) subtitle..." + ), + ["fixingVTTmp4"] = new TextContainer + ( + zhCN: "正在提取VTT(mp4)字幕...", + zhTW: "正在提取VTT(mp4)字幕...", + enUS: "Extracting VTT(mp4) subtitle..." + ), + ["keyProcessorNotFound"] = new TextContainer + ( + zhCN: "找不到支持的Processor", + zhTW: "找不到支持的Processor", + enUS: "No Processor matched" + ), + ["liveFound"] = new TextContainer + ( + zhCN: "检测到直播流", + zhTW: "檢測到直播流", + enUS: "Live stream found" + ), + ["loadingUrl"] = new TextContainer + ( + zhCN: "加载URL: ", + zhTW: "載入URL: ", + enUS: "Loading URL: " + ), + ["masterM3u8Found"] = new TextContainer + ( + zhCN: "检测到Master列表,开始解析全部流信息", + zhTW: "檢測到Master列表,開始解析全部流訊息", + enUS: "Master List detected, try parse all streams" + ), + ["matchTS"] = new TextContainer + ( + zhCN: "内容匹配: [white on green3]HTTP Live MPEG2-TS[/]", + zhTW: "內容匹配: [white on green3]HTTP Live MPEG2-TS[/]", + enUS: "Content Matched: [white on green3]HTTP Live MPEG2-TS[/]" + ), + ["matchDASH"] = new TextContainer + ( + zhCN: "内容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]", + zhTW: "內容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]", + enUS: "Content Matched: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]" + ), + ["matchMSS"] = new TextContainer + ( + zhCN: "内容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]", + zhTW: "內容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]", + enUS: "Content Matched: [white on steelblue1]Microsoft Smooth Streaming[/]" + ), + ["matchHLS"] = new TextContainer + ( + zhCN: "内容匹配: [white on deepskyblue1]HTTP Live Streaming[/]", + zhTW: "內容匹配: [white on deepskyblue1]HTTP Live Streaming[/]", + enUS: "Content Matched: [white on deepskyblue1]HTTP Live Streaming[/]" + ), + ["partMerge"] = new TextContainer + ( + zhCN: "分片数量大于1800个,开始分块合并...", + zhTW: "分片數量大於1800個,開始分塊合併...", + enUS: "Segments more than 1800, start partial merge..." + ), + ["notSupported"] = new TextContainer + ( + zhCN: "当前输入不受支持: ", + zhTW: "當前輸入不受支援: ", + enUS: "Input not supported: " + ), + ["parsingStream"] = new TextContainer + ( + zhCN: "正在解析媒体信息...", + zhTW: "正在解析媒體信息...", + enUS: "Parsing streams..." + ), + ["promptChoiceText"] = new TextContainer + ( + zhCN: "[grey](按键盘上下键以浏览更多内容)[/]", + zhTW: "[grey](按鍵盤上下鍵以瀏覽更多內容)[/]", + enUS: "[grey](Move up and down to reveal more streams)[/]" + ), + ["promptInfo"] = new TextContainer + ( + zhCN: "(按 [blue]空格键[/] 选择流, [green]回车键[/] 完成选择)", + zhTW: "(按 [blue]空格鍵[/] 選擇流, [green]確認鍵[/] 完成選擇)", + enUS: "(Press [blue][/] to toggle a stream, [green][/] to accept)" + ), + ["promptTitle"] = new TextContainer + ( + zhCN: "请选择 [green]你要下载的内容[/]:", + zhTW: "請選擇 [green]你要下載的內容[/]:", + enUS: "Please select [green]what you want to download[/]:" + ), + ["readingInfo"] = new TextContainer + ( + zhCN: "读取媒体信息...", + zhTW: "讀取媒體訊息...", + enUS: "Reading media info..." + ), + ["searchKey"] = new TextContainer + ( + zhCN: "正在尝试从文本文件搜索KEY...", + zhTW: "正在嘗試從文本文件搜尋KEY...", + enUS: "Trying to search for KEY from text file..." + ), + ["segmentCountCheckNotPass"] = new TextContainer + ( + zhCN: "分片数量校验不通过, 共{}个,已下载{}.", + zhTW: "分片數量校驗不通過, 共{}個,已下載{}.", + enUS: "Segment count check not pass, total: {}, downloaded: {}." + ), + ["selectedStream"] = new TextContainer + ( + zhCN: "已选择的流:", + zhTW: "已選擇的流:", + enUS: "Selected streams:" + ), + ["startDownloading"] = new TextContainer + ( + zhCN: "开始下载...", + zhTW: "開始下載...", + enUS: "Start downloading..." + ), + ["streamsInfo"] = new TextContainer + ( + zhCN: "已解析, 共计 {} 条媒体流, 基本流 {} 条, 可选音频流 {} 条, 可选字幕流 {} 条", + zhTW: "已解析, 共計 {} 條媒體流, 基本流 {} 條, 可選音頻流 {} 條, 可選字幕流 {} 條", + enUS: "Extracted, there are {} streams, with {} basic streams, {} audio streams, {} subtitle streams" + ), + ["writeJson"] = new TextContainer + ( + zhCN: "写出meta json", + zhTW: "寫出meta json", + enUS: "Writing meta json" + ), + ["noStreamsToDownload"] = new TextContainer + ( + zhCN: "没有找到需要下载的流", + zhTW: "沒有找到需要下載的流", + enUS: "No stream found to download" + ), + + }; +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Resource/TextContainer.cs b/src/N_m3u8DL-RE.Common/Resource/TextContainer.cs index 13ae7cc..d4a24c0 100644 --- a/src/N_m3u8DL-RE.Common/Resource/TextContainer.cs +++ b/src/N_m3u8DL-RE.Common/Resource/TextContainer.cs @@ -1,22 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Resource; -namespace N_m3u8DL_RE.Common.Resource +internal class TextContainer { - internal class TextContainer - { - public string ZH_CN { get; set; } - public string ZH_TW { get; set; } - public string EN_US { get; set; } + public string ZH_CN { get; } + public string ZH_TW { get; } + public string EN_US { get; } - public TextContainer(string zhCN, string zhTW, string enUS) - { - ZH_CN = zhCN; - ZH_TW = zhTW; - EN_US = enUS; - } + public TextContainer(string zhCN, string zhTW, string enUS) + { + ZH_CN = zhCN; + ZH_TW = zhTW; + EN_US = enUS; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs b/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs index f41dce9..b8e3ba7 100644 --- a/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs @@ -1,18 +1,13 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.JsonConverter; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace N_m3u8DL_RE.Common.Util; public static class GlobalUtil { - private static readonly JsonSerializerOptions Options = new JsonSerializerOptions + private static readonly JsonSerializerOptions Options = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = true, @@ -27,15 +22,15 @@ public static class GlobalUtil { return JsonSerializer.Serialize(s, Context.StreamSpec); } - else if (o is IOrderedEnumerable ss) + if (o is IOrderedEnumerable ss) { return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec); } - else if (o is List sList) + if (o is List sList) { return JsonSerializer.Serialize(sList, Context.ListStreamSpec); } - else if (o is IEnumerable mList) + if (o is IEnumerable mList) { return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment); } @@ -47,14 +42,14 @@ public static class GlobalUtil return fileSize switch { < 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)), - >= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GB", (double)fileSize / (1024 * 1024 * 1024)), - >= 1024 * 1024 => string.Format("{0:####0.00}MB", (double)fileSize / (1024 * 1024)), - >= 1024 => string.Format("{0:####0.00}KB", (double)fileSize / 1024), - _ => string.Format("{0:####0.00}B", fileSize) + >= 1024 * 1024 * 1024 => $"{fileSize / (1024 * 1024 * 1024):########0.00}GB", + >= 1024 * 1024 => $"{fileSize / (1024 * 1024):####0.00}MB", + >= 1024 => $"{fileSize / 1024:####0.00}KB", + _ => $"{fileSize:####0.00}B" }; } - //此函数用于格式化输出时长 + // 此函数用于格式化输出时长 public static string FormatTime(int 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 envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty(); - 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); } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs b/src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs index 973f5ee..c04fdb4 100644 --- a/src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs @@ -37,13 +37,13 @@ public static class HTTPUtil } } Logger.Debug(webRequest.Headers.ToString()); - //手动处理跳转,以免自定义Headers丢失 + // 手动处理跳转,以免自定义Headers丢失 var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead); if (((int)webResponse.StatusCode).ToString().StartsWith("30")) { HttpResponseHeaders respHeaders = webResponse.Headers; Logger.Debug(respHeaders.ToString()); - if (respHeaders != null && respHeaders.Location != null) + if (respHeaders.Location != null) { var redirectedUrl = ""; if (!respHeaders.Location.IsAbsoluteUri) @@ -64,7 +64,7 @@ public static class HTTPUtil } } } - //手动将跳转后的URL设置进去, 用于后续取用 + // 手动将跳转后的URL设置进去, 用于后续取用 webResponse.Headers.Location = new Uri(url); webResponse.EnsureSuccessStatusCode(); return webResponse; @@ -76,9 +76,8 @@ public static class HTTPUtil { return await File.ReadAllBytesAsync(new Uri(url).LocalPath); } - byte[] bytes = new byte[0]; var webResponse = await DoGetAsync(url, headers); - bytes = await webResponse.Content.ReadAsByteArrayAsync(); + var bytes = await webResponse.Content.ReadAsByteArrayAsync(); Logger.Debug(HexUtil.BytesToHex(bytes, " ")); return bytes; } @@ -91,9 +90,8 @@ public static class HTTPUtil /// public static async Task GetWebSourceAsync(string url, Dictionary? headers = null) { - string htmlCode = string.Empty; var webResponse = await DoGetAsync(url, headers); - htmlCode = await webResponse.Content.ReadAsStringAsync(); + string htmlCode = await webResponse.Content.ReadAsStringAsync(); Logger.Debug(htmlCode); return htmlCode; } @@ -112,7 +110,7 @@ public static class HTTPUtil /// (Source Code, RedirectedUrl) public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary? headers = null) { - string htmlCode = string.Empty; + string htmlCode; var webResponse = await DoGetAsync(url, headers); if (CheckMPEG2TS(webResponse)) { @@ -128,7 +126,7 @@ public static class HTTPUtil public static async Task GetPostResponseAsync(string Url, byte[] postData) { - string htmlCode = string.Empty; + string htmlCode; using HttpRequestMessage request = new(HttpMethod.Post, Url); request.Headers.TryAddWithoutValidation("Content-Type", "application/json"); request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString()); diff --git a/src/N_m3u8DL-RE.Common/Util/HexUtil.cs b/src/N_m3u8DL-RE.Common/Util/HexUtil.cs index 2aea255..a2506ad 100644 --- a/src/N_m3u8DL-RE.Common/Util/HexUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/HexUtil.cs @@ -31,7 +31,7 @@ public static class HexUtil var hexSpan = hex.AsSpan().Trim(); if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X")) { - hexSpan = hexSpan.Slice(2); + hexSpan = hexSpan[2..]; } return Convert.FromHexString(hexSpan); diff --git a/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs b/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs index 52e2831..fc21386 100644 --- a/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs +++ b/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs @@ -3,66 +3,65 @@ using N_m3u8DL_RE.Parser.Processor; using N_m3u8DL_RE.Parser.Processor.DASH; using N_m3u8DL_RE.Parser.Processor.HLS; -namespace N_m3u8DL_RE.Parser.Config +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 Headers { get; set; } = new Dictionary(); + public Dictionary Headers { get; set; } = new Dictionary(); - /// - /// 内容前置处理器. 调用顺序与列表顺序相同 - /// - public IList ContentProcessors { get; } = new List() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() }; + /// + /// 内容前置处理器. 调用顺序与列表顺序相同 + /// + public IList ContentProcessors { get; } = new List() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() }; - /// - /// 添加分片URL前置处理器. 调用顺序与列表顺序相同 - /// - public IList UrlProcessors { get; } = new List() { new DefaultUrlProcessor() }; + /// + /// 添加分片URL前置处理器. 调用顺序与列表顺序相同 + /// + public IList UrlProcessors { get; } = new List() { new DefaultUrlProcessor() }; - /// - /// KEY解析器. 调用顺序与列表顺序相同 - /// - public IList KeyProcessors { get; } = new List() { new DefaultHLSKeyProcessor() }; + /// + /// KEY解析器. 调用顺序与列表顺序相同 + /// + public IList KeyProcessors { get; } = new List() { new DefaultHLSKeyProcessor() }; - /// - /// 自定义的加密方式 - /// - public EncryptMethod? CustomMethod { get; set; } + /// + /// 自定义的加密方式 + /// + public EncryptMethod? CustomMethod { get; set; } - /// - /// 自定义的解密KEY - /// - public byte[]? CustomeKey { get; set; } + /// + /// 自定义的解密KEY + /// + public byte[]? CustomeKey { get; set; } - /// - /// 自定义的解密IV - /// - public byte[]? CustomeIV { get; set; } + /// + /// 自定义的解密IV + /// + public byte[]? CustomeIV { get; set; } - /// - /// 组装视频分段的URL时,是否要把原本URL后的参数也加上去 - /// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx" - /// 相对路径 = clip_01.ts - /// 如果 AppendUrlParams=false,得 http://xxx.com/clip_01.ts - /// 如果 AppendUrlParams=true,得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx - /// - public bool AppendUrlParams { get; set; } = false; + /// + /// 组装视频分段的URL时,是否要把原本URL后的参数也加上去 + /// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx" + /// 相对路径 = clip_01.ts + /// 如果 AppendUrlParams=false,得 http://xxx.com/clip_01.ts + /// 如果 AppendUrlParams=true,得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx + /// + public bool AppendUrlParams { get; set; } = false; - /// - /// 此参数将会传递给URL Processor中 - /// - public string? UrlProcessorArgs { get; set; } + /// + /// 此参数将会传递给URL Processor中 + /// + public string? UrlProcessorArgs { get; set; } - /// - /// KEY重试次数 - /// - public int KeyRetryCount { get; set; } = 3; - } -} + /// + /// KEY重试次数 + /// + public int KeyRetryCount { get; set; } = 3; +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs b/src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs index 4a1ebbe..3bba8fb 100644 --- a/src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs +++ b/src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs @@ -4,13 +4,12 @@ using System.Linq; using System.Text; 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 TemplateNumber = "$Number$"; - public static string TemplateTime = "$Time$"; - } -} + public static string TemplateRepresentationID = "$RepresentationID$"; + public static string TemplateBandwidth = "$Bandwidth$"; + public static string TemplateNumber = "$Number$"; + public static string TemplateTime = "$Time$"; +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Constants/HLSTags.cs b/src/N_m3u8DL-RE.Parser/Constants/HLSTags.cs index d27d3c8..ef59755 100644 --- a/src/N_m3u8DL-RE.Parser/Constants/HLSTags.cs +++ b/src/N_m3u8DL-RE.Parser/Constants/HLSTags.cs @@ -4,35 +4,34 @@ using System.Linq; using System.Text; 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_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"; - public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"; - public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"; - public static string ext_x_media = "#EXT-X-MEDIA"; - public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"; - public static string ext_x_key = "#EXT-X-KEY"; - public static string ext_x_stream_inf = "#EXT-X-STREAM-INF"; - public static string ext_x_version = "#EXT-X-VERSION"; - public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"; - public static string ext_x_endlist = "#EXT-X-ENDLIST"; - public static string extinf = "#EXTINF"; - public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"; - public static string ext_x_byterange = "#EXT-X-BYTERANGE"; - public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"; - public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY"; - public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT"; - public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT"; - public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"; - public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35"; - public static string ext_x_cue_start = "#EXT-X-CUE-OUT"; - public static string ext_x_cue_end = "#EXT-X-CUE-IN"; - public static string ext_x_cue_span = "#EXT-X-CUE-SPAN"; - public static string ext_x_map = "#EXT-X-MAP"; - public static string ext_x_start = "#EXT-X-START"; - } -} + public static string ext_m3u = "#EXTM3U"; + public static string ext_x_targetduration = "#EXT-X-TARGETDURATION"; + public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"; + public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"; + public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"; + public static string ext_x_media = "#EXT-X-MEDIA"; + public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"; + public static string ext_x_key = "#EXT-X-KEY"; + public static string ext_x_stream_inf = "#EXT-X-STREAM-INF"; + public static string ext_x_version = "#EXT-X-VERSION"; + public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"; + public static string ext_x_endlist = "#EXT-X-ENDLIST"; + public static string extinf = "#EXTINF"; + public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"; + public static string ext_x_byterange = "#EXT-X-BYTERANGE"; + public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"; + public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY"; + public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT"; + public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT"; + public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"; + public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35"; + public static string ext_x_cue_start = "#EXT-X-CUE-OUT"; + public static string ext_x_cue_end = "#EXT-X-CUE-IN"; + public static string ext_x_cue_span = "#EXT-X-CUE-SPAN"; + public static string ext_x_map = "#EXT-X-MAP"; + public static string ext_x_start = "#EXT-X-START"; +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs b/src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs index bf7e32c..35b894b 100644 --- a/src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs +++ b/src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs @@ -4,13 +4,12 @@ using System.Linq; using System.Text; 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 StartTime = "{start_time}"; - public static string StartTime_BK = "{start time}"; - } -} + public static string Bitrate = "{Bitrate}"; + public static string Bitrate_BK = "{bitrate}"; + public static string StartTime = "{start_time}"; + public static string StartTime_BK = "{start time}"; +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs index b84021d..f9f8c4f 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs @@ -13,266 +13,242 @@ using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; -namespace N_m3u8DL_RE.Parser.Extractor +namespace N_m3u8DL_RE.Parser.Extractor; + +// https://blog.csdn.net/leek5533/article/details/117750191 +internal class DASHExtractor2 : IExtractor { - //https://blog.csdn.net/leek5533/article/details/117750191 - internal class DASHExtractor2 : IExtractor + private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC; + + public ExtractorType ExtractorType => ExtractorType.MPEG_DASH; + + private string MpdUrl = string.Empty; + private string BaseUrl = string.Empty; + private string MpdContent = string.Empty; + public ParserConfig ParserConfig { get; set; } + + public DASHExtractor2(ParserConfig parserConfig) { - private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC; + this.ParserConfig = parserConfig; + SetInitUrl(); + } - public ExtractorType ExtractorType => ExtractorType.MPEG_DASH; - private string MpdUrl = string.Empty; - private string BaseUrl = string.Empty; - private string MpdContent = string.Empty; - public ParserConfig ParserConfig { get; set; } + private void SetInitUrl() + { + this.MpdUrl = ParserConfig.Url ?? string.Empty; + if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) + this.BaseUrl = ParserConfig.BaseUrl; + else + this.BaseUrl = this.MpdUrl; + } - public DASHExtractor2(ParserConfig parserConfig) + private string ExtendBaseUrl(XElement element, string oriBaseUrl) + { + var target = element.Elements().Where(e => e.Name.LocalName == "BaseURL").FirstOrDefault(); + if (target != null) { - this.ParserConfig = parserConfig; - SetInitUrl(); + oriBaseUrl = ParserUtil.CombineURL(oriBaseUrl, target.Value); } + return oriBaseUrl; + } - private void SetInitUrl() + private double? GetFrameRate(XElement element) + { + var frameRate = element.Attribute("frameRate")?.Value; + if (frameRate != null && frameRate.Contains("/")) { - this.MpdUrl = ParserConfig.Url ?? string.Empty; - if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) - this.BaseUrl = ParserConfig.BaseUrl; - else - this.BaseUrl = this.MpdUrl; + var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]); + frameRate = d.ToString("0.000"); + return Convert.ToDouble(frameRate); + } + return null; + } + + public Task> ExtractStreamsAsync(string rawText) + { + var streamList = new List(); + + this.MpdContent = rawText; + this.PreProcessContent(); + + + var xmlDocument = XDocument.Parse(MpdContent); + + // 选中第一个MPD节点 + var mpdElement = xmlDocument.Elements().First(e => e.Name.LocalName == "MPD"); + + // 类型 static点播, dynamic直播 + var type = mpdElement.Attribute("type")?.Value; + bool isLive = type == "dynamic"; + + // 分片最大时长 + var maxSegmentDuration = mpdElement.Attribute("maxSegmentDuration")?.Value; + // 分片从该时间起可用 + var availabilityStartTime = mpdElement.Attribute("availabilityStartTime")?.Value; + // 在availabilityStartTime的前XX段时间,分片有效 + var timeShiftBufferDepth = mpdElement.Attribute("timeShiftBufferDepth")?.Value; + if (string.IsNullOrEmpty(timeShiftBufferDepth)) + { + // 如果没有 默认一分钟有效 + timeShiftBufferDepth = "PT1M"; + } + // MPD发布时间 + var publishTime = mpdElement.Attribute("publishTime")?.Value; + // MPD总时长 + var mediaPresentationDuration = mpdElement.Attribute("mediaPresentationDuration")?.Value; + + // 读取在MPD开头定义的,并替换本身的URL + var baseUrlElement = mpdElement.Elements().Where(e => e.Name.LocalName == "BaseURL").FirstOrDefault(); + if (baseUrlElement != null) + { + var baseUrl = baseUrlElement.Value; + if (baseUrl.Contains("kkbox.com.tw/")) baseUrl = baseUrl.Replace("//https:%2F%2F", "//"); + this.BaseUrl = ParserUtil.CombineURL(this.MpdUrl, baseUrl); } - private string ExtendBaseUrl(XElement element, string oriBaseUrl) + // 全部Period + var periods = mpdElement.Elements().Where(e => e.Name.LocalName == "Period"); + foreach (var period in periods) { - var target = element.Elements().Where(e => e.Name.LocalName == "BaseURL").FirstOrDefault(); - if (target != null) + // 本Period时长 + var periodDuration = period.Attribute("duration")?.Value; + + // 本Period ID + var periodId = period.Attribute("id")?.Value; + + // 最终分片会使用的baseurl + var segBaseUrl = this.BaseUrl; + + // 处理baseurl嵌套 + segBaseUrl = ExtendBaseUrl(period, segBaseUrl); + + var adaptationSetsBaseUrl = segBaseUrl; + + // 本Period中的全部AdaptationSet + var adaptationSets = period.Elements().Where(e => e.Name.LocalName == "AdaptationSet"); + foreach (var adaptationSet in adaptationSets) { - oriBaseUrl = ParserUtil.CombineURL(oriBaseUrl, target.Value); - } + // 处理baseurl嵌套 + segBaseUrl = ExtendBaseUrl(adaptationSet, segBaseUrl); - return oriBaseUrl; - } + var representationsBaseUrl = segBaseUrl; - private double? GetFrameRate(XElement element) - { - var frameRate = element.Attribute("frameRate")?.Value; - if (frameRate != null && frameRate.Contains("/")) - { - var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]); - frameRate = d.ToString("0.000"); - return Convert.ToDouble(frameRate); - } - return null; - } - - public async Task> ExtractStreamsAsync(string rawText) - { - var streamList = new List(); - - this.MpdContent = rawText; - this.PreProcessContent(); - - - var xmlDocument = XDocument.Parse(MpdContent); - - //选中第一个MPD节点 - var mpdElement = xmlDocument.Elements().First(e => e.Name.LocalName == "MPD"); - - //类型 static点播, dynamic直播 - var type = mpdElement.Attribute("type")?.Value; - bool isLive = type == "dynamic"; - - //分片最大时长 - var maxSegmentDuration = mpdElement.Attribute("maxSegmentDuration")?.Value; - //分片从该时间起可用 - var availabilityStartTime = mpdElement.Attribute("availabilityStartTime")?.Value; - //在availabilityStartTime的前XX段时间,分片有效 - var timeShiftBufferDepth = mpdElement.Attribute("timeShiftBufferDepth")?.Value; - if (string.IsNullOrEmpty(timeShiftBufferDepth)) - { - //如果没有 默认一分钟有效 - timeShiftBufferDepth = "PT1M"; - } - //MPD发布时间 - var publishTime = mpdElement.Attribute("publishTime")?.Value; - //MPD总时长 - var mediaPresentationDuration = mpdElement.Attribute("mediaPresentationDuration")?.Value; - - //读取在MPD开头定义的,并替换本身的URL - var baseUrlElement = mpdElement.Elements().Where(e => e.Name.LocalName == "BaseURL").FirstOrDefault(); - if (baseUrlElement != null) - { - var baseUrl = baseUrlElement.Value; - if (baseUrl.Contains("kkbox.com.tw/")) baseUrl = baseUrl.Replace("//https:%2F%2F", "//"); - this.BaseUrl = ParserUtil.CombineURL(this.MpdUrl, baseUrl); - } - - //全部Period - var periods = mpdElement.Elements().Where(e => e.Name.LocalName == "Period"); - foreach (var period in periods) - { - //本Period时长 - var periodDuration = period.Attribute("duration")?.Value; - - //本Period ID - var periodId = period.Attribute("id")?.Value; - - //最终分片会使用的baseurl - var segBaseUrl = this.BaseUrl; - - //处理baseurl嵌套 - segBaseUrl = ExtendBaseUrl(period, segBaseUrl); - - var adaptationSetsBaseUrl = segBaseUrl; - - //本Period中的全部AdaptationSet - var adaptationSets = period.Elements().Where(e => e.Name.LocalName == "AdaptationSet"); - foreach (var adaptationSet in adaptationSets) + var mimeType = adaptationSet.Attribute("contentType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value; + var frameRate = GetFrameRate(adaptationSet); + // 本AdaptationSet中的全部Representation + var representations = adaptationSet.Elements().Where(e => e.Name.LocalName == "Representation"); + foreach (var representation in representations) { - //处理baseurl嵌套 - segBaseUrl = ExtendBaseUrl(adaptationSet, segBaseUrl); + // 处理baseurl嵌套 + segBaseUrl = ExtendBaseUrl(representation, segBaseUrl); - var representationsBaseUrl = segBaseUrl; - - var mimeType = adaptationSet.Attribute("contentType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value; - var frameRate = GetFrameRate(adaptationSet); - //本AdaptationSet中的全部Representation - var representations = adaptationSet.Elements().Where(e => e.Name.LocalName == "Representation"); - foreach (var representation in representations) + if (mimeType == null) { - //处理baseurl嵌套 - segBaseUrl = ExtendBaseUrl(representation, segBaseUrl); + mimeType = representation.Attribute("contentType")?.Value ?? representation.Attribute("mimeType")?.Value ?? ""; + } + var bandwidth = representation.Attribute("bandwidth"); + StreamSpec streamSpec = new(); + streamSpec.OriginalUrl = ParserConfig.OriginalUrl; + streamSpec.PeriodId = periodId; + streamSpec.Playlist = new Playlist(); + streamSpec.Playlist.MediaParts.Add(new MediaPart()); + streamSpec.GroupId = representation.Attribute("id")?.Value; + streamSpec.Bandwidth = Convert.ToInt32(bandwidth?.Value ?? "0"); + streamSpec.Codecs = representation.Attribute("codecs")?.Value ?? adaptationSet.Attribute("codecs")?.Value; + streamSpec.Language = FilterLanguage(representation.Attribute("lang")?.Value ?? adaptationSet.Attribute("lang")?.Value); + streamSpec.FrameRate = frameRate ?? GetFrameRate(representation); + streamSpec.Resolution = representation.Attribute("width")?.Value != null ? $"{representation.Attribute("width")?.Value}x{representation.Attribute("height")?.Value}" : null; + streamSpec.Url = MpdUrl; + streamSpec.MediaType = mimeType.Split('/')[0] switch + { + "text" => MediaType.SUBTITLES, + "audio" => MediaType.AUDIO, + _ => null + }; + // 特殊处理 + if (representation.Attribute("volumeAdjust") != null) + { + streamSpec.GroupId += "-" + representation.Attribute("volumeAdjust")?.Value; + } + // 推测后缀名 + var mType = representation.Attribute("mimeType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value; + if (mType != null) + { + var mTypeSplit = mType.Split('/'); + streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null; + } + // 优化字幕场景识别 + if (streamSpec.Codecs == "stpp" || streamSpec.Codecs == "wvtt") + { + streamSpec.MediaType = MediaType.SUBTITLES; + } + // 优化字幕场景识别 + var role = representation.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault() ?? adaptationSet.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault(); + if (role != null) + { + var v = role.Attribute("value")?.Value; + if (Enum.TryParse(v, true, out RoleType roleType)) + { + streamSpec.Role = roleType; - if (mimeType == null) - { - mimeType = representation.Attribute("contentType")?.Value ?? representation.Attribute("mimeType")?.Value ?? ""; - } - var bandwidth = representation.Attribute("bandwidth"); - StreamSpec streamSpec = new(); - streamSpec.OriginalUrl = ParserConfig.OriginalUrl; - streamSpec.PeriodId = periodId; - streamSpec.Playlist = new Playlist(); - streamSpec.Playlist.MediaParts.Add(new MediaPart()); - streamSpec.GroupId = representation.Attribute("id")?.Value; - streamSpec.Bandwidth = Convert.ToInt32(bandwidth?.Value ?? "0"); - streamSpec.Codecs = representation.Attribute("codecs")?.Value ?? adaptationSet.Attribute("codecs")?.Value; - streamSpec.Language = FilterLanguage(representation.Attribute("lang")?.Value ?? adaptationSet.Attribute("lang")?.Value); - streamSpec.FrameRate = frameRate ?? GetFrameRate(representation); - streamSpec.Resolution = representation.Attribute("width")?.Value != null ? $"{representation.Attribute("width")?.Value}x{representation.Attribute("height")?.Value}" : null; - streamSpec.Url = MpdUrl; - streamSpec.MediaType = mimeType.Split('/')[0] switch - { - "text" => MediaType.SUBTITLES, - "audio" => MediaType.AUDIO, - _ => null - }; - //特殊处理 - if (representation.Attribute("volumeAdjust") != null) - { - streamSpec.GroupId += "-" + representation.Attribute("volumeAdjust")?.Value; - } - //推测后缀名 - var mType = representation.Attribute("mimeType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value; - if (mType != null) - { - var mTypeSplit = mType.Split('/'); - streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null; - } - //优化字幕场景识别 - if (streamSpec.Codecs == "stpp" || streamSpec.Codecs == "wvtt") - { - streamSpec.MediaType = MediaType.SUBTITLES; - } - //优化字幕场景识别 - var role = representation.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault() ?? adaptationSet.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault(); - if (role != null) - { - var v = role.Attribute("value")?.Value; - if (Enum.TryParse(v, true, out RoleType roleType)) + if (roleType == RoleType.Subtitle) { - streamSpec.Role = roleType; - - if (roleType == RoleType.Subtitle) - { - streamSpec.MediaType = MediaType.SUBTITLES; - if (mType != null && mType.Contains("ttml")) - streamSpec.Extension = "ttml"; - } + streamSpec.MediaType = MediaType.SUBTITLES; + if (mType != null && mType.Contains("ttml")) + streamSpec.Extension = "ttml"; } } - streamSpec.Playlist.IsLive = isLive; - //设置刷新间隔 timeShiftBufferDepth / 2 - if (timeShiftBufferDepth != null) - { - streamSpec.Playlist.RefreshIntervalMs = XmlConvert.ToTimeSpan(timeShiftBufferDepth).TotalMilliseconds / 2; - } + } + streamSpec.Playlist.IsLive = isLive; + // 设置刷新间隔 timeShiftBufferDepth / 2 + if (timeShiftBufferDepth != null) + { + streamSpec.Playlist.RefreshIntervalMs = XmlConvert.ToTimeSpan(timeShiftBufferDepth).TotalMilliseconds / 2; + } - //读取声道数量 - var audioChannelConfiguration = adaptationSet.Elements().Concat(representation.Elements()).Where(e => e.Name.LocalName == "AudioChannelConfiguration").FirstOrDefault(); - if (audioChannelConfiguration != null) - { - streamSpec.Channels = audioChannelConfiguration.Attribute("value")?.Value; - } + // 读取声道数量 + var audioChannelConfiguration = adaptationSet.Elements().Concat(representation.Elements()).Where(e => e.Name.LocalName == "AudioChannelConfiguration").FirstOrDefault(); + if (audioChannelConfiguration != null) + { + streamSpec.Channels = audioChannelConfiguration.Attribute("value")?.Value; + } - //发布时间 - if (!string.IsNullOrEmpty(publishTime)) - { - streamSpec.PublishTime = DateTime.Parse(publishTime); - } + // 发布时间 + if (!string.IsNullOrEmpty(publishTime)) + { + streamSpec.PublishTime = DateTime.Parse(publishTime); + } - //第一种形式 SegmentBase - var segmentBaseElement = representation.Elements().Where(e => e.Name.LocalName == "SegmentBase").FirstOrDefault(); - if (segmentBaseElement != null) + // 第一种形式 SegmentBase + var segmentBaseElement = representation.Elements().Where(e => e.Name.LocalName == "SegmentBase").FirstOrDefault(); + if (segmentBaseElement != null) + { + // 处理init url + var initialization = segmentBaseElement.Elements().Where(e => e.Name.LocalName == "Initialization").FirstOrDefault(); + if (initialization != null) { - //处理init url - var initialization = segmentBaseElement.Elements().Where(e => e.Name.LocalName == "Initialization").FirstOrDefault(); - if (initialization != null) + var sourceURL = initialization.Attribute("sourceURL")?.Value; + if (sourceURL == null) { - var sourceURL = initialization.Attribute("sourceURL")?.Value; - if (sourceURL == null) - { - streamSpec.Playlist.MediaParts[0].MediaSegments.Add - ( - new MediaSegment() - { - Index = 0, - Url = segBaseUrl, - Duration = XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds - } - ); - } - else - { - var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!); - var initRange = initialization.Attribute("range")?.Value; - streamSpec.Playlist.MediaInit = new MediaSegment(); - streamSpec.Playlist.MediaInit.Index = -1; //便于排序 - streamSpec.Playlist.MediaInit.Url = initUrl; - if (initRange != null) + streamSpec.Playlist.MediaParts[0].MediaSegments.Add + ( + new MediaSegment() { - var (start, expect) = ParserUtil.ParseRange(initRange); - streamSpec.Playlist.MediaInit.StartRange = start; - streamSpec.Playlist.MediaInit.ExpectLength = expect; + Index = 0, + Url = segBaseUrl, + Duration = XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds } - } + ); } - } - - //第二种形式 SegmentList.SegmentList - var segmentList = representation.Elements().Where(e => e.Name.LocalName == "SegmentList").FirstOrDefault(); - if (segmentList != null) - { - var durationStr = segmentList.Attribute("duration")?.Value; - //处理init url - var initialization = segmentList.Elements().Where(e => e.Name.LocalName == "Initialization").FirstOrDefault(); - if (initialization != null) + else { var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!); var initRange = initialization.Attribute("range")?.Value; streamSpec.Playlist.MediaInit = new MediaSegment(); - streamSpec.Playlist.MediaInit.Index = -1; //便于排序 + streamSpec.Playlist.MediaInit.Index = -1; // 便于排序 streamSpec.Playlist.MediaInit.Url = initUrl; if (initRange != null) { @@ -281,355 +257,380 @@ namespace N_m3u8DL_RE.Parser.Extractor streamSpec.Playlist.MediaInit.ExpectLength = expect; } } - //处理分片 - var segmentURLs = segmentList.Elements().Where(e => e.Name.LocalName == "SegmentURL"); - var timescaleStr = segmentList.Attribute("timescale")?.Value ?? "1"; - for (int segmentIndex = 0; segmentIndex < segmentURLs.Count(); segmentIndex++) + } + } + + // 第二种形式 SegmentList.SegmentList + var segmentList = representation.Elements().Where(e => e.Name.LocalName == "SegmentList").FirstOrDefault(); + if (segmentList != null) + { + var durationStr = segmentList.Attribute("duration")?.Value; + // 处理init url + var initialization = segmentList.Elements().Where(e => e.Name.LocalName == "Initialization").FirstOrDefault(); + if (initialization != null) + { + var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!); + var initRange = initialization.Attribute("range")?.Value; + streamSpec.Playlist.MediaInit = new MediaSegment(); + streamSpec.Playlist.MediaInit.Index = -1; // 便于排序 + streamSpec.Playlist.MediaInit.Url = initUrl; + if (initRange != null) { - var segmentURL = segmentURLs.ElementAt(segmentIndex); - var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute("media")?.Value!); - var mediaRange = segmentURL.Attribute("mediaRange")?.Value; - var timesacle = Convert.ToInt32(timescaleStr); - var duration = Convert.ToInt64(durationStr); - MediaSegment mediaSegment = new(); - mediaSegment.Duration = duration / (double)timesacle; - mediaSegment.Url = mediaUrl; - mediaSegment.Index = segmentIndex; - if (mediaRange != null) - { - var (start, expect) = ParserUtil.ParseRange(mediaRange); - mediaSegment.StartRange = start; - mediaSegment.ExpectLength = expect; - } - streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); + var (start, expect) = ParserUtil.ParseRange(initRange); + streamSpec.Playlist.MediaInit.StartRange = start; + streamSpec.Playlist.MediaInit.ExpectLength = expect; } } - - //第三种形式 SegmentTemplate+SegmentTimeline - //通配符有$RepresentationID$ $Bandwidth$ $Number$ $Time$ - - //adaptationSets中的segmentTemplate - var segmentTemplateElementsOuter = adaptationSet.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); - //representation中的segmentTemplate - var segmentTemplateElements = representation.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); - if (segmentTemplateElements.Any() || segmentTemplateElementsOuter.Any()) + // 处理分片 + var segmentURLs = segmentList.Elements().Where(e => e.Name.LocalName == "SegmentURL"); + var timescaleStr = segmentList.Attribute("timescale")?.Value ?? "1"; + for (int segmentIndex = 0; segmentIndex < segmentURLs.Count(); segmentIndex++) { - //优先使用最近的元素 - var segmentTemplate = (segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault())!; - var segmentTemplateOuter = (segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault())!; - var varDic = new Dictionary(); - varDic[DASHTags.TemplateRepresentationID] = streamSpec.GroupId; - varDic[DASHTags.TemplateBandwidth] = bandwidth?.Value; - //presentationTimeOffset - var presentationTimeOffsetStr = segmentTemplate.Attribute("presentationTimeOffset")?.Value ?? segmentTemplateOuter.Attribute("presentationTimeOffset")?.Value ?? "0"; - //timesacle - var timescaleStr = segmentTemplate.Attribute("timescale")?.Value ?? segmentTemplateOuter.Attribute("timescale")?.Value ?? "1"; - var durationStr = segmentTemplate.Attribute("duration")?.Value ?? segmentTemplateOuter.Attribute("duration")?.Value; - var startNumberStr = segmentTemplate.Attribute("startNumber")?.Value ?? segmentTemplateOuter.Attribute("startNumber")?.Value ?? "1"; - //处理init url - var initialization = segmentTemplate.Attribute("initialization")?.Value ?? segmentTemplateOuter.Attribute("initialization")?.Value; - if (initialization != null) + var segmentURL = segmentURLs.ElementAt(segmentIndex); + var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute("media")?.Value!); + var mediaRange = segmentURL.Attribute("mediaRange")?.Value; + var timesacle = Convert.ToInt32(timescaleStr); + var duration = Convert.ToInt64(durationStr); + MediaSegment mediaSegment = new(); + mediaSegment.Duration = duration / (double)timesacle; + mediaSegment.Url = mediaUrl; + mediaSegment.Index = segmentIndex; + if (mediaRange != null) { - var _init = ParserUtil.ReplaceVars(initialization, varDic); - var initUrl = ParserUtil.CombineURL(segBaseUrl, _init); - streamSpec.Playlist.MediaInit = new MediaSegment(); - streamSpec.Playlist.MediaInit.Index = -1; //便于排序 - streamSpec.Playlist.MediaInit.Url = initUrl; + var (start, expect) = ParserUtil.ParseRange(mediaRange); + mediaSegment.StartRange = start; + mediaSegment.ExpectLength = expect; } - //处理分片 - var mediaTemplate = segmentTemplate.Attribute("media")?.Value ?? segmentTemplateOuter.Attribute("media")?.Value; - var segmentTimeline = segmentTemplate.Elements().Where(e => e.Name.LocalName == "SegmentTimeline").FirstOrDefault(); - if (segmentTimeline != null) - { - //使用了SegmentTimeline 结果精确 - var segNumber = Convert.ToInt64(startNumberStr); - var Ss = segmentTimeline.Elements().Where(e => e.Name.LocalName == "S"); - var currentTime = 0L; - var segIndex = 0; - foreach (var S in Ss) - { - //每个S元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration) - var _startTimeStr = S.Attribute("t")?.Value; - var _durationStr = S.Attribute("d")?.Value; - var _repeatCountStr = S.Attribute("r")?.Value; + streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); + } + } - if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr); - var _duration = Convert.ToInt64(_durationStr); - var timescale = Convert.ToInt32(timescaleStr); - var _repeatCount = Convert.ToInt64(_repeatCountStr); + // 第三种形式 SegmentTemplate+SegmentTimeline + // 通配符有$RepresentationID$ $Bandwidth$ $Number$ $Time$ + + // adaptationSets中的segmentTemplate + var segmentTemplateElementsOuter = adaptationSet.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); + // representation中的segmentTemplate + var segmentTemplateElements = representation.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); + if (segmentTemplateElements.Any() || segmentTemplateElementsOuter.Any()) + { + // 优先使用最近的元素 + var segmentTemplate = (segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault())!; + var segmentTemplateOuter = (segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault())!; + var varDic = new Dictionary(); + varDic[DASHTags.TemplateRepresentationID] = streamSpec.GroupId; + varDic[DASHTags.TemplateBandwidth] = bandwidth?.Value; + // presentationTimeOffset + var presentationTimeOffsetStr = segmentTemplate.Attribute("presentationTimeOffset")?.Value ?? segmentTemplateOuter.Attribute("presentationTimeOffset")?.Value ?? "0"; + // timesacle + var timescaleStr = segmentTemplate.Attribute("timescale")?.Value ?? segmentTemplateOuter.Attribute("timescale")?.Value ?? "1"; + var durationStr = segmentTemplate.Attribute("duration")?.Value ?? segmentTemplateOuter.Attribute("duration")?.Value; + var startNumberStr = segmentTemplate.Attribute("startNumber")?.Value ?? segmentTemplateOuter.Attribute("startNumber")?.Value ?? "1"; + // 处理init url + var initialization = segmentTemplate.Attribute("initialization")?.Value ?? segmentTemplateOuter.Attribute("initialization")?.Value; + if (initialization != null) + { + var _init = ParserUtil.ReplaceVars(initialization, varDic); + var initUrl = ParserUtil.CombineURL(segBaseUrl, _init); + streamSpec.Playlist.MediaInit = new MediaSegment(); + streamSpec.Playlist.MediaInit.Index = -1; // 便于排序 + streamSpec.Playlist.MediaInit.Url = initUrl; + } + // 处理分片 + var mediaTemplate = segmentTemplate.Attribute("media")?.Value ?? segmentTemplateOuter.Attribute("media")?.Value; + var segmentTimeline = segmentTemplate.Elements().Where(e => e.Name.LocalName == "SegmentTimeline").FirstOrDefault(); + if (segmentTimeline != null) + { + // 使用了SegmentTimeline 结果精确 + var segNumber = Convert.ToInt64(startNumberStr); + var Ss = segmentTimeline.Elements().Where(e => e.Name.LocalName == "S"); + var currentTime = 0L; + var segIndex = 0; + foreach (var S in Ss) + { + // 每个S元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration) + var _startTimeStr = S.Attribute("t")?.Value; + var _durationStr = S.Attribute("d")?.Value; + var _repeatCountStr = S.Attribute("r")?.Value; + + if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr); + var _duration = Convert.ToInt64(_durationStr); + var timescale = Convert.ToInt32(timescaleStr); + var _repeatCount = Convert.ToInt64(_repeatCountStr); + varDic[DASHTags.TemplateTime] = currentTime; + varDic[DASHTags.TemplateNumber] = segNumber++; + var hasTime = mediaTemplate!.Contains(DASHTags.TemplateTime); + var media = ParserUtil.ReplaceVars(mediaTemplate!, varDic); + var mediaUrl = ParserUtil.CombineURL(segBaseUrl, media!); + MediaSegment mediaSegment = new(); + mediaSegment.Url = mediaUrl; + if (hasTime) + 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(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / _duration) - 1; + } + for (long i = 0; i < _repeatCount; i++) + { + currentTime += _duration; + MediaSegment _mediaSegment = new(); varDic[DASHTags.TemplateTime] = currentTime; varDic[DASHTags.TemplateNumber] = segNumber++; - var hasTime = mediaTemplate!.Contains(DASHTags.TemplateTime); - var media = ParserUtil.ReplaceVars(mediaTemplate!, varDic); - var mediaUrl = ParserUtil.CombineURL(segBaseUrl, media!); - MediaSegment mediaSegment = new(); - mediaSegment.Url = mediaUrl; - if (hasTime) - 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(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / _duration) - 1; - } - for (long i = 0; i < _repeatCount; i++) - { - currentTime += _duration; - MediaSegment _mediaSegment = new(); - varDic[DASHTags.TemplateTime] = currentTime; - varDic[DASHTags.TemplateNumber] = segNumber++; - var _hashTime = mediaTemplate!.Contains(DASHTags.TemplateTime); - var _media = ParserUtil.ReplaceVars(mediaTemplate!, varDic); - var _mediaUrl = ParserUtil.CombineURL(segBaseUrl, _media); - _mediaSegment.Url = _mediaUrl; - _mediaSegment.Index = segIndex++; - _mediaSegment.Duration = _duration / (double)timescale; - if (_hashTime) - _mediaSegment.NameFromVar = currentTime.ToString(); - streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment); - } - currentTime += _duration; - } - } - else - { - //没用SegmentTimeline 需要计算总分片数量 不精确 - var timescale = Convert.ToInt32(timescaleStr); - var startNumber = Convert.ToInt64(startNumberStr); - var duration = Convert.ToInt32(durationStr); - var totalNumber = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / duration); - //直播的情况,需要自己计算totalNumber - if (totalNumber == 0 && isLive) - { - var now = DateTime.Now; - var availableTime = DateTime.Parse(availabilityStartTime!); - //可用时间+偏移量 - var offsetMs = TimeSpan.FromMilliseconds(Convert.ToInt64(presentationTimeOffsetStr) / 1000); - availableTime = availableTime.Add(offsetMs); - var ts = now - availableTime; - var updateTs = XmlConvert.ToTimeSpan(timeShiftBufferDepth!); - //(当前时间到发布时间的时间差 - 最小刷新间隔) / 分片时长 - startNumber += (long)((ts.TotalSeconds - updateTs.TotalSeconds) * timescale / duration); - totalNumber = (long)(updateTs.TotalSeconds * timescale / duration); - } - for (long index = startNumber, segIndex = 0; index < startNumber + totalNumber; index++, segIndex++) - { - varDic[DASHTags.TemplateNumber] = index; - var hasNumber = mediaTemplate!.Contains(DASHTags.TemplateNumber); - var media = ParserUtil.ReplaceVars(mediaTemplate!, varDic); - var mediaUrl = ParserUtil.CombineURL(segBaseUrl, media!); - MediaSegment mediaSegment = new(); - mediaSegment.Url = mediaUrl; - if (hasNumber) - mediaSegment.NameFromVar = index.ToString(); - mediaSegment.Index = isLive ? index : segIndex; //直播直接用startNumber - mediaSegment.Duration = duration / (double)timescale; - streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); - } - } - } - - //如果依旧没被添加分片,直接把BaseUrl塞进去就好 - if (streamSpec.Playlist.MediaParts[0].MediaSegments.Count == 0) - { - streamSpec.Playlist.MediaParts[0].MediaSegments.Add - ( - new MediaSegment() - { - Index = 0, - Url = segBaseUrl, - Duration = XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds - } - ); - } - - //判断加密情况 - if (adaptationSet.Elements().Concat(representation.Elements()).Any(e => e.Name.LocalName == "ContentProtection")) - { - 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; - } - } - - //处理同一ID分散在不同Period的情况 - var _index = streamList.FindIndex(_f => _f.PeriodId != streamSpec.PeriodId && _f.GroupId == streamSpec.GroupId && _f.Resolution == streamSpec.Resolution && _f.MediaType == streamSpec.MediaType); - if (_index > -1) - { - if (isLive) - { - //直播,这种情况直接略过新的 - } - else - { - //点播,这种情况如果URL不同则作为新的part出现,否则仅把时间加起来 - var url1 = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Url; - var url2 = streamSpec.Playlist.MediaParts[0].MediaSegments.LastOrDefault()?.Url; - if (url1 != url2) - { - var startIndex = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Index + 1; - var enumerator = streamSpec.Playlist.MediaParts[0].MediaSegments.GetEnumerator(); - while (enumerator.MoveNext()) - { - enumerator.Current.Index += startIndex; - } - streamList[_index].Playlist!.MediaParts.Add(new MediaPart() - { - MediaSegments = streamSpec.Playlist.MediaParts[0].MediaSegments - }); - } - else - { - streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Duration += streamSpec.Playlist.MediaParts[0].MediaSegments.Sum(x => x.Duration); + var _hashTime = mediaTemplate!.Contains(DASHTags.TemplateTime); + var _media = ParserUtil.ReplaceVars(mediaTemplate!, varDic); + var _mediaUrl = ParserUtil.CombineURL(segBaseUrl, _media); + _mediaSegment.Url = _mediaUrl; + _mediaSegment.Index = segIndex++; + _mediaSegment.Duration = _duration / (double)timescale; + if (_hashTime) + _mediaSegment.NameFromVar = currentTime.ToString(); + streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment); } + currentTime += _duration; } } else { - //修复mp4类型字幕 - if (streamSpec.MediaType == MediaType.SUBTITLES && streamSpec.Extension == "mp4") + // 没用SegmentTimeline 需要计算总分片数量 不精确 + var timescale = Convert.ToInt32(timescaleStr); + var startNumber = Convert.ToInt64(startNumberStr); + var duration = Convert.ToInt32(durationStr); + var totalNumber = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / duration); + // 直播的情况,需要自己计算totalNumber + if (totalNumber == 0 && isLive) { - streamSpec.Extension = "m4s"; + var now = DateTime.Now; + var availableTime = DateTime.Parse(availabilityStartTime!); + // 可用时间+偏移量 + var offsetMs = TimeSpan.FromMilliseconds(Convert.ToInt64(presentationTimeOffsetStr) / 1000); + availableTime = availableTime.Add(offsetMs); + var ts = now - availableTime; + var updateTs = XmlConvert.ToTimeSpan(timeShiftBufferDepth!); + // (当前时间到发布时间的时间差 - 最小刷新间隔) / 分片时长 + startNumber += (long)((ts.TotalSeconds - updateTs.TotalSeconds) * timescale / duration); + totalNumber = (long)(updateTs.TotalSeconds * timescale / duration); } - //分片默认后缀m4s - if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == null || streamSpec.Playlist.MediaParts.Sum(x => x.MediaSegments.Count) > 1)) + for (long index = startNumber, segIndex = 0; index < startNumber + totalNumber; index++, segIndex++) { - streamSpec.Extension = "m4s"; + varDic[DASHTags.TemplateNumber] = index; + var hasNumber = mediaTemplate!.Contains(DASHTags.TemplateNumber); + var media = ParserUtil.ReplaceVars(mediaTemplate!, varDic); + var mediaUrl = ParserUtil.CombineURL(segBaseUrl, media!); + MediaSegment mediaSegment = new(); + mediaSegment.Url = mediaUrl; + if (hasNumber) + mediaSegment.NameFromVar = index.ToString(); + mediaSegment.Index = isLive ? index : segIndex; // 直播直接用startNumber + mediaSegment.Duration = duration / (double)timescale; + streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); } - streamList.Add(streamSpec); } - //恢复BaseURL相对位置 - segBaseUrl = representationsBaseUrl; } - //恢复BaseURL相对位置 - segBaseUrl = adaptationSetsBaseUrl; - } - } - //为视频设置默认轨道 - 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()) + // 如果依旧没被添加分片,直接把BaseUrl塞进去就好 + if (streamSpec.Playlist.MediaParts[0].MediaSegments.Count == 0) { - item.AudioId = aL.OrderByDescending(x => x.Bandwidth).First().GroupId; + streamSpec.Playlist.MediaParts[0].MediaSegments.Add + ( + new MediaSegment() + { + Index = 0, + Url = segBaseUrl, + Duration = XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds + } + ); } - if (sL.Any()) + + // 判断加密情况 + if (adaptationSet.Elements().Concat(representation.Elements()).Any(e => e.Name.LocalName == "ContentProtection")) { - item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId; - } - } - } - - return streamList; - } - - /// - /// 如果有非法字符 返回und - /// - /// - /// - private string? FilterLanguage(string? v) - { - if (v == null) return null; - if (Regex.IsMatch(v, "^[\\w_\\-\\d]+$")) return v; - return "und"; - } - - public async Task RefreshPlayListAsync(List 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); - } - - private async Task ProcessUrlAsync(List 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++) + if (streamSpec.Playlist.MediaInit != null) { - part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url); + streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD; } + foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments) + { + item.EncryptInfo.Method = DEFAULT_METHOD; + } + } + + // 处理同一ID分散在不同Period的情况 + var _index = streamList.FindIndex(_f => _f.PeriodId != streamSpec.PeriodId && _f.GroupId == streamSpec.GroupId && _f.Resolution == streamSpec.Resolution && _f.MediaType == streamSpec.MediaType); + if (_index > -1) + { + if (isLive) + { + // 直播,这种情况直接略过新的 + } + else + { + // 点播,这种情况如果URL不同则作为新的part出现,否则仅把时间加起来 + var url1 = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Url; + var url2 = streamSpec.Playlist.MediaParts[0].MediaSegments.LastOrDefault()?.Url; + if (url1 != url2) + { + var startIndex = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Index + 1; + var enumerator = streamSpec.Playlist.MediaParts[0].MediaSegments.GetEnumerator(); + while (enumerator.MoveNext()) + { + enumerator.Current.Index += startIndex; + } + streamList[_index].Playlist!.MediaParts.Add(new MediaPart() + { + MediaSegments = streamSpec.Playlist.MediaParts[0].MediaSegments + }); + } + else + { + streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Duration += streamSpec.Playlist.MediaParts[0].MediaSegments.Sum(x => x.Duration); + } + } + } + else + { + // 修复mp4类型字幕 + if (streamSpec.MediaType == MediaType.SUBTITLES && streamSpec.Extension == "mp4") + { + streamSpec.Extension = "m4s"; + } + // 分片默认后缀m4s + if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == null || streamSpec.Playlist.MediaParts.Sum(x => x.MediaSegments.Count) > 1)) + { + streamSpec.Extension = "m4s"; + } + streamList.Add(streamSpec); + } + // 恢复BaseURL相对位置 + segBaseUrl = representationsBaseUrl; + } + // 恢复BaseURL相对位置 + segBaseUrl = adaptationSetsBaseUrl; + } + } + + // 为视频设置默认轨道 + 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.OrderByDescending(x => x.Bandwidth).First().GroupId; + } + if (sL.Any()) + { + item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId; + } + } + } + + return Task.FromResult(streamList); + } + + /// + /// 如果有非法字符 返回und + /// + /// + /// + private string? FilterLanguage(string? v) + { + if (v == null) return null; + if (Regex.IsMatch(v, "^[\\w_\\-\\d]+$")) return v; + return "und"; + } + + public async Task RefreshPlayListAsync(List 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); + } + + private Task ProcessUrlAsync(List 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); } } } } - public async Task FetchPlayListAsync(List streamSpecs) - { - //这里才调用URL预处理器,节省开销 - await ProcessUrlAsync(streamSpecs); - } + return Task.CompletedTask; + } - public string PreProcessUrl(string url) + public async Task FetchPlayListAsync(List streamSpecs) + { + // 这里才调用URL预处理器,节省开销 + await ProcessUrlAsync(streamSpecs); + } + + public string PreProcessUrl(string url) + { + foreach (var p in ParserConfig.UrlProcessors) { - foreach (var p in ParserConfig.UrlProcessors) + if (p.CanProcess(ExtractorType, url, ParserConfig)) { - if (p.CanProcess(ExtractorType, url, ParserConfig)) - { - url = p.Process(url, ParserConfig); - } + url = p.Process(url, ParserConfig); } - - return url; } - public void PreProcessContent() + return url; + } + + public void PreProcessContent() + { + foreach (var p in ParserConfig.ContentProcessors) { - foreach (var p in ParserConfig.ContentProcessors) + if (p.CanProcess(ExtractorType, MpdContent, ParserConfig)) { - if (p.CanProcess(ExtractorType, MpdContent, ParserConfig)) - { - MpdContent = p.Process(MpdContent, ParserConfig); - } + MpdContent = p.Process(MpdContent, ParserConfig); } } } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs index 4f49da6..3f60fe5 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs @@ -13,341 +13,379 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using N_m3u8DL_RE.Common.Util; -namespace N_m3u8DL_RE.Parser.Extractor +namespace N_m3u8DL_RE.Parser.Extractor; + +internal class HLSExtractor : IExtractor { - internal class HLSExtractor : IExtractor + public ExtractorType ExtractorType => ExtractorType.HLS; + + private string M3u8Url = string.Empty; + private string BaseUrl = string.Empty; + private string M3u8Content = string.Empty; + private bool MasterM3u8Flag = false; + + public ParserConfig ParserConfig { get; set; } + + private HLSExtractor() { } + + public HLSExtractor(ParserConfig parserConfig) { - public ExtractorType ExtractorType => ExtractorType.HLS; + this.ParserConfig = parserConfig; + this.M3u8Url = parserConfig.Url ?? string.Empty; + this.SetBaseUrl(); + } - private string M3u8Url = string.Empty; - private string BaseUrl = string.Empty; - private string M3u8Content = string.Empty; - private bool MasterM3u8Flag = false; - - public ParserConfig ParserConfig { get; set; } - - private HLSExtractor() { } - - public HLSExtractor(ParserConfig parserConfig) + private void SetBaseUrl() + { + if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) { - this.ParserConfig = parserConfig; - this.M3u8Url = parserConfig.Url ?? string.Empty; - this.SetBaseUrl(); + this.BaseUrl = ParserConfig.BaseUrl; + } + else + { + this.BaseUrl = this.M3u8Url; + } + } + + /// + /// 预处理m3u8内容 + /// + public void PreProcessContent() + { + M3u8Content = M3u8Content.Trim(); + if (!M3u8Content.StartsWith(HLSTags.ext_m3u)) + { + throw new Exception(ResString.badM3u8); } - private void SetBaseUrl() + foreach (var p in ParserConfig.ContentProcessors) { - if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) + if (p.CanProcess(ExtractorType, M3u8Content, ParserConfig)) { - this.BaseUrl = ParserConfig.BaseUrl; + M3u8Content = p.Process(M3u8Content, ParserConfig); } - else + } + } + + /// + /// 预处理URL + /// + public string PreProcessUrl(string url) + { + foreach (var p in ParserConfig.UrlProcessors) + { + if (p.CanProcess(ExtractorType, url, ParserConfig)) { - this.BaseUrl = this.M3u8Url; + url = p.Process(url, ParserConfig); } } - /// - /// 预处理m3u8内容 - /// - public void PreProcessContent() + return url; + } + + private Task> ParseMasterListAsync() + { + MasterM3u8Flag = true; + + List streams = new List(); + + using StringReader sr = new StringReader(M3u8Content); + string? line; + bool expectPlaylist = false; + StreamSpec streamSpec = new(); + + while ((line = sr.ReadLine()) != null) { - M3u8Content = M3u8Content.Trim(); - if (!M3u8Content.StartsWith(HLSTags.ext_m3u)) + if (string.IsNullOrEmpty(line)) + continue; + + if (line.StartsWith(HLSTags.ext_x_stream_inf)) { - throw new Exception(ResString.badM3u8); + streamSpec = new(); + streamSpec.OriginalUrl = ParserConfig.OriginalUrl; + var bandwidth = string.IsNullOrEmpty(ParserUtil.GetAttribute(line, "AVERAGE-BANDWIDTH")) ? ParserUtil.GetAttribute(line, "BANDWIDTH") : ParserUtil.GetAttribute(line, "AVERAGE-BANDWIDTH"); + streamSpec.Bandwidth = Convert.ToInt32(bandwidth); + streamSpec.Codecs = ParserUtil.GetAttribute(line, "CODECS"); + streamSpec.Resolution = ParserUtil.GetAttribute(line, "RESOLUTION"); + + var frameRate = ParserUtil.GetAttribute(line, "FRAME-RATE"); + if (!string.IsNullOrEmpty(frameRate)) + streamSpec.FrameRate = Convert.ToDouble(frameRate); + + var audioId = ParserUtil.GetAttribute(line, "AUDIO"); + if (!string.IsNullOrEmpty(audioId)) + streamSpec.AudioId = audioId; + + var videoId = ParserUtil.GetAttribute(line, "VIDEO"); + if (!string.IsNullOrEmpty(videoId)) + streamSpec.VideoId = videoId; + + var subtitleId = ParserUtil.GetAttribute(line, "SUBTITLES"); + if (!string.IsNullOrEmpty(subtitleId)) + streamSpec.SubtitleId = subtitleId; + + var videoRange = ParserUtil.GetAttribute(line, "VIDEO-RANGE"); + if (!string.IsNullOrEmpty(videoRange)) + streamSpec.VideoRange = videoRange; + + // 清除多余的编码信息 dvh1.05.06,ec-3 => dvh1.05.06 + if (!string.IsNullOrEmpty(streamSpec.Codecs) && !string.IsNullOrEmpty(streamSpec.AudioId)) + { + streamSpec.Codecs = streamSpec.Codecs.Split(',')[0]; + } + + expectPlaylist = true; } - - foreach (var p in ParserConfig.ContentProcessors) + else if (line.StartsWith(HLSTags.ext_x_media)) { - if (p.CanProcess(ExtractorType, M3u8Content, ParserConfig)) + streamSpec = new(); + var type = ParserUtil.GetAttribute(line, "TYPE").Replace("-", "_"); + if (Enum.TryParse(type, out var mediaType)) { - M3u8Content = p.Process(M3u8Content, ParserConfig); + streamSpec.MediaType = mediaType; } - } - } - /// - /// 预处理URL - /// - public string PreProcessUrl(string url) - { - foreach (var p in ParserConfig.UrlProcessors) - { - if (p.CanProcess(ExtractorType, url, ParserConfig)) - { - url = p.Process(url, ParserConfig); - } - } - - return url; - } - - private async Task> ParseMasterListAsync() - { - MasterM3u8Flag = true; - - List streams = new List(); - - using StringReader sr = new StringReader(M3u8Content); - string? line; - bool expectPlaylist = false; - StreamSpec streamSpec = new(); - - while ((line = sr.ReadLine()) != null) - { - if (string.IsNullOrEmpty(line)) - continue; - - if (line.StartsWith(HLSTags.ext_x_stream_inf)) - { - streamSpec = new(); - streamSpec.OriginalUrl = ParserConfig.OriginalUrl; - var bandwidth = string.IsNullOrEmpty(ParserUtil.GetAttribute(line, "AVERAGE-BANDWIDTH")) ? ParserUtil.GetAttribute(line, "BANDWIDTH") : ParserUtil.GetAttribute(line, "AVERAGE-BANDWIDTH"); - streamSpec.Bandwidth = Convert.ToInt32(bandwidth); - streamSpec.Codecs = ParserUtil.GetAttribute(line, "CODECS"); - streamSpec.Resolution = ParserUtil.GetAttribute(line, "RESOLUTION"); - - var frameRate = ParserUtil.GetAttribute(line, "FRAME-RATE"); - if (!string.IsNullOrEmpty(frameRate)) - streamSpec.FrameRate = Convert.ToDouble(frameRate); - - var audioId = ParserUtil.GetAttribute(line, "AUDIO"); - if (!string.IsNullOrEmpty(audioId)) - streamSpec.AudioId = audioId; - - var videoId = ParserUtil.GetAttribute(line, "VIDEO"); - if (!string.IsNullOrEmpty(videoId)) - streamSpec.VideoId = videoId; - - var subtitleId = ParserUtil.GetAttribute(line, "SUBTITLES"); - if (!string.IsNullOrEmpty(subtitleId)) - streamSpec.SubtitleId = subtitleId; - - var videoRange = ParserUtil.GetAttribute(line, "VIDEO-RANGE"); - if (!string.IsNullOrEmpty(videoRange)) - streamSpec.VideoRange = videoRange; - - //清除多余的编码信息 dvh1.05.06,ec-3 => dvh1.05.06 - if (!string.IsNullOrEmpty(streamSpec.Codecs) && !string.IsNullOrEmpty(streamSpec.AudioId)) - { - streamSpec.Codecs = streamSpec.Codecs.Split(',')[0]; - } - - expectPlaylist = true; - } - else if (line.StartsWith(HLSTags.ext_x_media)) - { - streamSpec = new(); - var type = ParserUtil.GetAttribute(line, "TYPE").Replace("-", "_"); - if (Enum.TryParse(type, out var mediaType)) - { - streamSpec.MediaType = mediaType; - } - - //跳过CLOSED_CAPTIONS类型(目前不支持) - if (streamSpec.MediaType == MediaType.CLOSED_CAPTIONS) - { - continue; - } - - var url = ParserUtil.GetAttribute(line, "URI"); - - /** - * The URI attribute of the EXT-X-MEDIA tag is REQUIRED if the media - type is SUBTITLES, but OPTIONAL if the media type is VIDEO or AUDIO. - If the media type is VIDEO or AUDIO, a missing URI attribute - indicates that the media data for this Rendition is included in the - Media Playlist of any EXT-X-STREAM-INF tag referencing this EXT- - X-MEDIA tag. If the media TYPE is AUDIO and the URI attribute is - missing, clients MUST assume that the audio data for this Rendition - is present in every video Rendition specified by the EXT-X-STREAM-INF - tag. - - 此处直接忽略URI属性为空的情况 - */ - if (string.IsNullOrEmpty(url)) - { - continue; - } - - url = ParserUtil.CombineURL(BaseUrl, url); - streamSpec.Url = PreProcessUrl(url); - - var groupId = ParserUtil.GetAttribute(line, "GROUP-ID"); - streamSpec.GroupId = groupId; - - var lang = ParserUtil.GetAttribute(line, "LANGUAGE"); - if (!string.IsNullOrEmpty(lang)) - streamSpec.Language = lang; - - var name = ParserUtil.GetAttribute(line, "NAME"); - if (!string.IsNullOrEmpty(name)) - streamSpec.Name = name; - - var def = ParserUtil.GetAttribute(line, "DEFAULT"); - if (Enum.TryParse(type, out var defaultChoise)) - { - streamSpec.Default = defaultChoise; - } - - var channels = ParserUtil.GetAttribute(line, "CHANNELS"); - if (!string.IsNullOrEmpty(channels)) - streamSpec.Channels = channels; - - var characteristics = ParserUtil.GetAttribute(line, "CHARACTERISTICS"); - if (!string.IsNullOrEmpty(characteristics)) - streamSpec.Characteristics = characteristics.Split(',').Last().Split('.').Last(); - - streams.Add(streamSpec); - } - else if (line.StartsWith("#")) + // 跳过CLOSED_CAPTIONS类型(目前不支持) + if (streamSpec.MediaType == MediaType.CLOSED_CAPTIONS) { continue; } - else if (expectPlaylist) - { - var url = ParserUtil.CombineURL(BaseUrl, line); - streamSpec.Url = PreProcessUrl(url); - expectPlaylist = false; - streams.Add(streamSpec); - } - } - return streams; + var url = ParserUtil.GetAttribute(line, "URI"); + + /** + * The URI attribute of the EXT-X-MEDIA tag is REQUIRED if the media + type is SUBTITLES, but OPTIONAL if the media type is VIDEO or AUDIO. + If the media type is VIDEO or AUDIO, a missing URI attribute + indicates that the media data for this Rendition is included in the + Media Playlist of any EXT-X-STREAM-INF tag referencing this EXT- + X-MEDIA tag. If the media TYPE is AUDIO and the URI attribute is + missing, clients MUST assume that the audio data for this Rendition + is present in every video Rendition specified by the EXT-X-STREAM-INF + tag. + + 此处直接忽略URI属性为空的情况 + */ + if (string.IsNullOrEmpty(url)) + { + continue; + } + + url = ParserUtil.CombineURL(BaseUrl, url); + streamSpec.Url = PreProcessUrl(url); + + var groupId = ParserUtil.GetAttribute(line, "GROUP-ID"); + streamSpec.GroupId = groupId; + + var lang = ParserUtil.GetAttribute(line, "LANGUAGE"); + if (!string.IsNullOrEmpty(lang)) + streamSpec.Language = lang; + + var name = ParserUtil.GetAttribute(line, "NAME"); + if (!string.IsNullOrEmpty(name)) + streamSpec.Name = name; + + var def = ParserUtil.GetAttribute(line, "DEFAULT"); + if (Enum.TryParse(type, out var defaultChoise)) + { + streamSpec.Default = defaultChoise; + } + + var channels = ParserUtil.GetAttribute(line, "CHANNELS"); + if (!string.IsNullOrEmpty(channels)) + streamSpec.Channels = channels; + + var characteristics = ParserUtil.GetAttribute(line, "CHARACTERISTICS"); + if (!string.IsNullOrEmpty(characteristics)) + streamSpec.Characteristics = characteristics.Split(',').Last().Split('.').Last(); + + streams.Add(streamSpec); + } + else if (line.StartsWith("#")) + { + continue; + } + else if (expectPlaylist) + { + var url = ParserUtil.CombineURL(BaseUrl, line); + streamSpec.Url = PreProcessUrl(url); + expectPlaylist = false; + streams.Add(streamSpec); + } } - private async Task ParseListAsync() + return Task.FromResult(streams); + } + + private Task ParseListAsync() + { + // 标记是否已清除广告分片 + bool hasAd = false; + + using StringReader sr = new StringReader(M3u8Content); + string? line; + bool expectSegment = false; + bool isEndlist = false; + long segIndex = 0; + bool isAd = false; + long startIndex; + + Playlist playlist = new(); + List mediaParts = new(); + + // 当前的加密信息 + EncryptInfo currentEncryptInfo = new(); + if (ParserConfig.CustomMethod != null) + currentEncryptInfo.Method = ParserConfig.CustomMethod.Value; + if (ParserConfig.CustomeKey != null && ParserConfig.CustomeKey.Length > 0) + currentEncryptInfo.Key = ParserConfig.CustomeKey; + if (ParserConfig.CustomeIV != null && ParserConfig.CustomeIV.Length > 0) + currentEncryptInfo.IV = ParserConfig.CustomeIV; + // 上次读取到的加密行,#EXT-X-KEY:…… + string lastKeyLine = ""; + + MediaPart mediaPart = new(); + MediaSegment segment = new(); + List segments = new(); + + + while ((line = sr.ReadLine()) != null) { - //标记是否已清除优酷广告分片 - bool hasAd = false; + if (string.IsNullOrEmpty(line)) + continue; - using StringReader sr = new StringReader(M3u8Content); - string? line; - bool expectSegment = false; - bool isEndlist = false; - long segIndex = 0; - bool isAd = false; - long startIndex; - - Playlist playlist = new(); - List mediaParts = new(); - - //当前的加密信息 - EncryptInfo currentEncryptInfo = new(); - if (ParserConfig.CustomMethod != null) - currentEncryptInfo.Method = ParserConfig.CustomMethod.Value; - if (ParserConfig.CustomeKey != null && ParserConfig.CustomeKey.Length > 0) - currentEncryptInfo.Key = ParserConfig.CustomeKey; - if (ParserConfig.CustomeIV != null && ParserConfig.CustomeIV.Length > 0) - currentEncryptInfo.IV = ParserConfig.CustomeIV; - //上次读取到的加密行,#EXT-X-KEY:…… - string lastKeyLine = ""; - - MediaPart mediaPart = new(); - MediaSegment segment = new(); - List segments = new(); - - - while ((line = sr.ReadLine()) != null) + // 只下载部分字节 + if (line.StartsWith(HLSTags.ext_x_byterange)) { - if (string.IsNullOrEmpty(line)) - continue; - - //只下载部分字节 - if (line.StartsWith(HLSTags.ext_x_byterange)) - { - var p = ParserUtil.GetAttribute(line); - var (n, o) = ParserUtil.GetRange(p); - segment.ExpectLength = n; - segment.StartRange = o ?? segments.Last().StartRange + segments.Last().ExpectLength; - expectSegment = true; - } - //国家地理去广告 - else if (line.StartsWith("#UPLYNK-SEGMENT")) - { - if (line.Contains(",ad")) - isAd = true; - else if (line.Contains(",segment")) - isAd = false; - } - //国家地理去广告 - else if (isAd) + var p = ParserUtil.GetAttribute(line); + var (n, o) = ParserUtil.GetRange(p); + segment.ExpectLength = n; + segment.StartRange = o ?? segments.Last().StartRange + segments.Last().ExpectLength; + expectSegment = true; + } + // 国家地理去广告 + else if (line.StartsWith("#UPLYNK-SEGMENT")) + { + if (line.Contains(",ad")) + isAd = true; + else if (line.Contains(",segment")) + isAd = false; + } + // 国家地理去广告 + else if (isAd) + { + continue; + } + // 解析定义的分段长度 + else if (line.StartsWith(HLSTags.ext_x_targetduration)) + { + playlist.TargetDuration = Convert.ToDouble(ParserUtil.GetAttribute(line)); + } + // 解析起始编号 + else if (line.StartsWith(HLSTags.ext_x_media_sequence)) + { + segIndex = Convert.ToInt64(ParserUtil.GetAttribute(line)); + startIndex = segIndex; + } + // program date time + else if (line.StartsWith(HLSTags.ext_x_program_date_time)) + { + segment.DateTime = DateTime.Parse(ParserUtil.GetAttribute(line)); + } + // 解析不连续标记,需要单独合并(timestamp不同) + else if (line.StartsWith(HLSTags.ext_x_discontinuity)) + { + // 修复YK去除广告后的遗留问题 + if (hasAd && mediaParts.Count > 0) { + segments = mediaParts[mediaParts.Count - 1].MediaSegments; + mediaParts.RemoveAt(mediaParts.Count - 1); + hasAd = false; continue; } - //解析定义的分段长度 - else if (line.StartsWith(HLSTags.ext_x_targetduration)) + // 常规情况的#EXT-X-DISCONTINUITY标记,新建part + if (!hasAd && segments.Count >= 1) { - playlist.TargetDuration = Convert.ToDouble(ParserUtil.GetAttribute(line)); - } - //解析起始编号 - else if (line.StartsWith(HLSTags.ext_x_media_sequence)) - { - segIndex = Convert.ToInt64(ParserUtil.GetAttribute(line)); - startIndex = segIndex; - } - //program date time - else if (line.StartsWith(HLSTags.ext_x_program_date_time)) - { - segment.DateTime = DateTime.Parse(ParserUtil.GetAttribute(line)); - } - //解析不连续标记,需要单独合并(timestamp不同) - else if (line.StartsWith(HLSTags.ext_x_discontinuity)) - { - //修复优酷去除广告后的遗留问题 - if (hasAd && mediaParts.Count > 0) + mediaParts.Add(new MediaPart() { - segments = mediaParts[mediaParts.Count - 1].MediaSegments; - mediaParts.RemoveAt(mediaParts.Count - 1); - hasAd = false; - continue; - } - //常规情况的#EXT-X-DISCONTINUITY标记,新建part - if (!hasAd && segments.Count >= 1) - { - mediaParts.Add(new MediaPart() - { - MediaSegments = segments, - }); - segments = new(); - } + MediaSegments = segments, + }); + segments = new(); } - //解析KEY - else if (line.StartsWith(HLSTags.ext_x_key)) - { - var uri = ParserUtil.GetAttribute(line, "URI"); - var uri_last = ParserUtil.GetAttribute(lastKeyLine, "URI"); + } + // 解析KEY + else if (line.StartsWith(HLSTags.ext_x_key)) + { + var uri = ParserUtil.GetAttribute(line, "URI"); + var uri_last = ParserUtil.GetAttribute(lastKeyLine, "URI"); - //如果KEY URL相同,不进行重复解析 - if (uri != uri_last) - { - //调用处理器进行解析 - var parsedInfo = ParseKey(line); - currentEncryptInfo.Method = parsedInfo.Method; - currentEncryptInfo.Key = parsedInfo.Key; - currentEncryptInfo.IV = parsedInfo.IV; - } - lastKeyLine = line; - } - //解析分片时长 - else if (line.StartsWith(HLSTags.extinf)) + // 如果KEY URL相同,不进行重复解析 + if (uri != uri_last) { - string[] tmp = ParserUtil.GetAttribute(line).Split(','); - segment.Duration = Convert.ToDouble(tmp[0]); - segment.Index = segIndex; - //是否有加密,有的话写入KEY和IV + // 调用处理器进行解析 + var parsedInfo = ParseKey(line); + currentEncryptInfo.Method = parsedInfo.Method; + currentEncryptInfo.Key = parsedInfo.Key; + currentEncryptInfo.IV = parsedInfo.IV; + } + lastKeyLine = line; + } + // 解析分片时长 + else if (line.StartsWith(HLSTags.extinf)) + { + string[] tmp = ParserUtil.GetAttribute(line).Split(','); + segment.Duration = Convert.ToDouble(tmp[0]); + segment.Index = segIndex; + // 是否有加密,有的话写入KEY和IV + if (currentEncryptInfo.Method != EncryptMethod.NONE) + { + segment.EncryptInfo.Method = currentEncryptInfo.Method; + segment.EncryptInfo.Key = currentEncryptInfo.Key; + segment.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0')); + } + expectSegment = true; + segIndex++; + } + // m3u8主体结束 + else if (line.StartsWith(HLSTags.ext_x_endlist)) + { + if (segments.Count > 0) + { + mediaParts.Add(new MediaPart() + { + MediaSegments = segments + }); + } + segments = new(); + isEndlist = true; + } + // #EXT-X-MAP + else if (line.StartsWith(HLSTags.ext_x_map)) + { + if (playlist.MediaInit == null) + { + playlist.MediaInit = new MediaSegment() + { + Url = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, ParserUtil.GetAttribute(line, "URI"))), + Index = -1, // 便于排序 + }; + if (line.Contains("BYTERANGE")) + { + var p = ParserUtil.GetAttribute(line, "BYTERANGE"); + var (n, o) = ParserUtil.GetRange(p); + playlist.MediaInit.ExpectLength = n; + playlist.MediaInit.StartRange = o ?? 0L; + } + // 是否有加密,有的话写入KEY和IV if (currentEncryptInfo.Method != EncryptMethod.NONE) { - segment.EncryptInfo.Method = currentEncryptInfo.Method; - segment.EncryptInfo.Key = currentEncryptInfo.Key; - segment.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0')); + playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method; + playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key; + playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0')); } - expectSegment = true; - segIndex++; } - //m3u8主体结束 - else if (line.StartsWith(HLSTags.ext_x_endlist)) + // 遇到了其他的map,说明已经不是一个视频了,全部丢弃即可 + else { if (segments.Count > 0) { @@ -358,228 +396,187 @@ namespace N_m3u8DL_RE.Parser.Extractor } segments = new(); isEndlist = true; - } - //#EXT-X-MAP - else if (line.StartsWith(HLSTags.ext_x_map)) - { - if (playlist.MediaInit == null) - { - playlist.MediaInit = new MediaSegment() - { - Url = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, ParserUtil.GetAttribute(line, "URI"))), - Index = -1, //便于排序 - }; - if (line.Contains("BYTERANGE")) - { - var p = ParserUtil.GetAttribute(line, "BYTERANGE"); - var (n, o) = ParserUtil.GetRange(p); - playlist.MediaInit.ExpectLength = n; - playlist.MediaInit.StartRange = o ?? 0L; - } - //是否有加密,有的话写入KEY和IV - if (currentEncryptInfo.Method != EncryptMethod.NONE) - { - playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method; - playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key; - playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0')); - } - } - //遇到了其他的map,说明已经不是一个视频了,全部丢弃即可 - else - { - if (segments.Count > 0) - { - mediaParts.Add(new MediaPart() - { - MediaSegments = segments - }); - } - segments = new(); - isEndlist = true; - break; - } - } - //评论行不解析 - else if (line.StartsWith("#")) continue; - //空白行不解析 - else if (line.StartsWith("\r\n")) continue; - //解析分片的地址 - else if (expectSegment) - { - var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, line)); - segment.Url = segUrl; - segments.Add(segment); - segment = new(); - //优酷的广告分段则清除此分片 - //需要注意,遇到广告说明程序对上文的#EXT-X-DISCONTINUITY做出的动作是不必要的, - //其实上下文是同一种编码,需要恢复到原先的part上 - if (segUrl.Contains("ccode=") && segUrl.Contains("/ad/") && segUrl.Contains("duration=")) - { - segments.RemoveAt(segments.Count - 1); - segIndex--; - hasAd = true; - } - //优酷广告(4K分辨率测试) - if (segUrl.Contains("ccode=0902") && segUrl.Contains("duration=")) - { - segments.RemoveAt(segments.Count - 1); - segIndex--; - hasAd = true; - } - expectSegment = false; + break; } } - - //直播的情况,无法遇到m3u8结束标记,需要手动将segments加入parts - if (!isEndlist) + // 评论行不解析 + else if (line.StartsWith("#")) continue; + // 空白行不解析 + else if (line.StartsWith("\r\n")) continue; + // 解析分片的地址 + else if (expectSegment) { - mediaParts.Add(new MediaPart() + var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, line)); + segment.Url = segUrl; + segments.Add(segment); + segment = new(); + // YK的广告分段则清除此分片 + // 需要注意,遇到广告说明程序对上文的#EXT-X-DISCONTINUITY做出的动作是不必要的, + // 其实上下文是同一种编码,需要恢复到原先的part上 + if (segUrl.Contains("ccode=") && segUrl.Contains("/ad/") && segUrl.Contains("duration=")) { - MediaSegments = segments - }); + segments.RemoveAt(segments.Count - 1); + segIndex--; + hasAd = true; + } + // YK广告(4K分辨率测试) + if (segUrl.Contains("ccode=0902") && segUrl.Contains("duration=")) + { + segments.RemoveAt(segments.Count - 1); + segIndex--; + hasAd = true; + } + expectSegment = false; } - - playlist.MediaParts = mediaParts; - playlist.IsLive = !isEndlist; - - //直播刷新间隔 - if (playlist.IsLive) - { - //由于播放器默认从最后3个分片开始播放 此处设置刷新间隔为TargetDuration的2倍 - playlist.RefreshIntervalMs = (int)((playlist.TargetDuration ?? 5) * 2 * 1000); - } - - return playlist; } - private EncryptInfo ParseKey(string keyLine) + // 直播的情况,无法遇到m3u8结束标记,需要手动将segments加入parts + if (!isEndlist) { - foreach (var p in ParserConfig.KeyProcessors) + mediaParts.Add(new MediaPart() { - if (p.CanProcess(ExtractorType, keyLine, M3u8Url, M3u8Content, ParserConfig)) - { - //匹配到对应处理器后不再继续 - return p.Process(keyLine, M3u8Url, M3u8Content, ParserConfig); - } - } - - throw new Exception(ResString.keyProcessorNotFound); + MediaSegments = segments + }); } - public async Task> ExtractStreamsAsync(string rawText) + playlist.MediaParts = mediaParts; + playlist.IsLive = !isEndlist; + + // 直播刷新间隔 + if (playlist.IsLive) { - this.M3u8Content = rawText; - this.PreProcessContent(); - if (M3u8Content.Contains(HLSTags.ext_x_stream_inf)) + // 由于播放器默认从最后3个分片开始播放 此处设置刷新间隔为TargetDuration的2倍 + playlist.RefreshIntervalMs = (int)((playlist.TargetDuration ?? 5) * 2 * 1000); + } + + return Task.FromResult(playlist); + } + + private EncryptInfo ParseKey(string keyLine) + { + foreach (var p in ParserConfig.KeyProcessors) + { + if (p.CanProcess(ExtractorType, keyLine, M3u8Url, M3u8Content, ParserConfig)) { - Logger.Warn(ResString.masterM3u8Found); - var lists = await ParseMasterListAsync(); - lists = lists.DistinctBy(p => p.Url).ToList(); - return lists; + // 匹配到对应处理器后不再继续 + return p.Process(keyLine, M3u8Url, M3u8Content, ParserConfig); + } + } + + throw new Exception(ResString.keyProcessorNotFound); + } + + public async Task> ExtractStreamsAsync(string rawText) + { + this.M3u8Content = rawText; + this.PreProcessContent(); + if (M3u8Content.Contains(HLSTags.ext_x_stream_inf)) + { + Logger.Warn(ResString.masterM3u8Found); + var lists = await ParseMasterListAsync(); + lists = lists.DistinctBy(p => p.Url).ToList(); + return lists; + } + + var playlist = await ParseListAsync(); + return new List + { + new() + { + Url = ParserConfig.Url, + Playlist = playlist, + Extension = playlist.MediaInit != null ? "mp4" : "ts" + } + }; + } + + private async Task LoadM3u8FromUrlAsync(string url) + { + // Logger.Info(ResString.loadingUrl + url); + if (url.StartsWith("file:")) + { + var uri = new Uri(url); + this.M3u8Content = File.ReadAllText(uri.LocalPath); + } + else if (url.StartsWith("http")) + { + try + { + (this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, ParserConfig.Headers); + } + catch (HttpRequestException) when (url != ParserConfig.OriginalUrl) + { + // 当URL无法访问时,再请求原始URL + (this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers); + } + } + + this.M3u8Url = url; + this.SetBaseUrl(); + this.PreProcessContent(); + } + + /// + /// 从Master链接中刷新各个流的URL + /// + /// + /// + private async Task RefreshUrlFromMaster(List lists) + { + // 重新加载master m3u8, 刷新选中流的URL + await LoadM3u8FromUrlAsync(ParserConfig.Url); + var newStreams = await ParseMasterListAsync(); + newStreams = newStreams.DistinctBy(p => p.Url).ToList(); + foreach (var l in lists) + { + var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()).ToList(); + if (match.Any()) + { + Logger.DebugMarkUp($"{l.Url} => {match.First().Url}"); + l.Url = match.First().Url; + } + } + } + + public async Task FetchPlayListAsync(List lists) + { + for (int i = 0; i < lists.Count; i++) + { + try + { + // 直接重新加载m3u8 + await LoadM3u8FromUrlAsync(lists[i].Url!); + } + catch (HttpRequestException) when (MasterM3u8Flag == true) + { + Logger.WarnMarkUp("Can not load m3u8. Try refreshing url from master url..."); + // 当前URL无法加载 尝试从Master链接中刷新URL + await RefreshUrlFromMaster(lists); + await LoadM3u8FromUrlAsync(lists[i].Url!); + } + + var newPlaylist = await ParseListAsync(); + if (lists[i].Playlist?.MediaInit != null) + lists[i].Playlist!.MediaParts = newPlaylist.MediaParts; // 不更新init + else + lists[i].Playlist = newPlaylist; + + if (lists[i].MediaType == MediaType.SUBTITLES) + { + var a = lists[i].Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.Url.Contains(".ttml"))); + var b = lists[i].Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.Url.Contains(".vtt") || m.Url.Contains(".webvtt"))); + if (a) lists[i].Extension = "ttml"; + if (b) lists[i].Extension = "vtt"; } else { - var playlist = await ParseListAsync(); - return new List() - { - new StreamSpec() - { - Url = ParserConfig.Url, - Playlist = playlist, - Extension = playlist.MediaInit != null ? "mp4" : "ts" - } - }; + lists[i].Extension = lists[i].Playlist!.MediaInit != null ? "m4s" : "ts"; } } - - private async Task LoadM3u8FromUrlAsync(string url) - { - //Logger.Info(ResString.loadingUrl + url); - if (url.StartsWith("file:")) - { - var uri = new Uri(url); - this.M3u8Content = File.ReadAllText(uri.LocalPath); - } - else if (url.StartsWith("http")) - { - try - { - (this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, ParserConfig.Headers); - } - catch (HttpRequestException) when (url != ParserConfig.OriginalUrl) - { - //当URL无法访问时,再请求原始URL - (this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers); - } - } - - this.M3u8Url = url; - this.SetBaseUrl(); - this.PreProcessContent(); - } - - /// - /// 从Master链接中刷新各个流的URL - /// - /// - /// - private async Task RefreshUrlFromMaster(List lists) - { - //重新加载master m3u8, 刷新选中流的URL - await LoadM3u8FromUrlAsync(ParserConfig.Url); - var newStreams = await ParseMasterListAsync(); - newStreams = newStreams.DistinctBy(p => p.Url).ToList(); - foreach (var l in lists) - { - var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()); - if (match.Any()) - { - Logger.DebugMarkUp($"{l.Url} => {match.First().Url}"); - l.Url = match.First().Url; - } - } - } - - public async Task FetchPlayListAsync(List lists) - { - for (int i = 0; i < lists.Count; i++) - { - try - { - //直接重新加载m3u8 - await LoadM3u8FromUrlAsync(lists[i].Url!); - } - catch (HttpRequestException) when (MasterM3u8Flag == true) - { - Logger.WarnMarkUp("Can not load m3u8. Try refreshing url from master url..."); - //当前URL无法加载 尝试从Master链接中刷新URL - await RefreshUrlFromMaster(lists); - await LoadM3u8FromUrlAsync(lists[i].Url!); - } - - var newPlaylist = await ParseListAsync(); - if (lists[i].Playlist?.MediaInit != null) - lists[i].Playlist!.MediaParts = newPlaylist.MediaParts; //不更新init - else - lists[i].Playlist = newPlaylist; - - if (lists[i].MediaType == MediaType.SUBTITLES) - { - var a = lists[i].Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.Url.Contains(".ttml"))); - var b = lists[i].Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.Url.Contains(".vtt") || m.Url.Contains(".webvtt"))); - if (a) lists[i].Extension = "ttml"; - if (b) lists[i].Extension = "vtt"; - } - else - { - lists[i].Extension = lists[i].Playlist!.MediaInit != null ? "m4s" : "ts"; - } - } - } - - public async Task RefreshPlayListAsync(List streamSpecs) - { - await FetchPlayListAsync(streamSpecs); - } } -} + + public async Task RefreshPlayListAsync(List streamSpecs) + { + await FetchPlayListAsync(streamSpecs); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs index ad6fb73..708f5b9 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs @@ -7,21 +7,20 @@ using System.Text; using System.Threading.Tasks; 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> ExtractStreamsAsync(string rawText); + Task> ExtractStreamsAsync(string rawText); - Task FetchPlayListAsync(List streamSpecs); - Task RefreshPlayListAsync(List streamSpecs); + Task FetchPlayListAsync(List streamSpecs); + Task RefreshPlayListAsync(List streamSpecs); - string PreProcessUrl(string url); + string PreProcessUrl(string url); - void PreProcessContent(); - } -} + void PreProcessContent(); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Extractor/LiveTSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/LiveTSExtractor.cs index eae7cc0..7e36fd5 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/LiveTSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/LiveTSExtractor.cs @@ -3,51 +3,50 @@ using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Resource; 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; - - public ParserConfig ParserConfig {get; set;} - - public LiveTSExtractor(ParserConfig parserConfig) - { - this.ParserConfig = parserConfig; - } - - public async Task> ExtractStreamsAsync(string rawText) - { - return new List() - { - new StreamSpec() - { - OriginalUrl = ParserConfig.OriginalUrl, - Url = ParserConfig.Url, - Playlist = new Playlist(), - GroupId = ResString.ReLiveTs - } - }; - } - - public async Task FetchPlayListAsync(List streamSpecs) - { - throw new NotImplementedException(); - } - - public async void PreProcessContent() - { - throw new NotImplementedException(); - } - - public string PreProcessUrl(string url) - { - throw new NotImplementedException(); - } - - public Task RefreshPlayListAsync(List streamSpecs) - { - throw new NotImplementedException(); - } + this.ParserConfig = parserConfig; } -} + + public Task> ExtractStreamsAsync(string rawText) + { + return Task.FromResult(new List + { + new() + { + OriginalUrl = ParserConfig.OriginalUrl, + Url = ParserConfig.Url, + Playlist = new Playlist(), + GroupId = ResString.ReLiveTs + } + }); + } + + public Task FetchPlayListAsync(List streamSpecs) + { + throw new NotImplementedException(); + } + + public void PreProcessContent() + { + throw new NotImplementedException(); + } + + public string PreProcessUrl(string url) + { + throw new NotImplementedException(); + } + + public Task RefreshPlayListAsync(List streamSpecs) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs index e6639a8..9c2b5b4 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs @@ -16,386 +16,386 @@ using System.Threading.Tasks; using System.Xml; 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 - //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 - { - [GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")] - private static partial Regex VCodecsRegex(); + [GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")] + private static partial Regex VCodecsRegex(); - //////////////////////////////////////// + //////////////////////////////////////// - private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC; + private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC; - public ExtractorType ExtractorType => ExtractorType.MSS; + 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; } + private string IsmUrl = string.Empty; + private string BaseUrl = string.Empty; + private string IsmContent = string.Empty; + public ParserConfig ParserConfig { get; set; } - public MSSExtractor(ParserConfig parserConfig) + public MSSExtractor(ParserConfig parserConfig) + { + this.ParserConfig = parserConfig; + SetInitUrl(); + } + + private void SetInitUrl() + { + this.IsmUrl = ParserConfig.Url ?? string.Empty; + if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) + this.BaseUrl = ParserConfig.BaseUrl; + else + this.BaseUrl = this.IsmUrl; + } + + public Task> ExtractStreamsAsync(string rawText) + { + var streamList = new List(); + 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) { - this.ParserConfig = parserConfig; - SetInitUrl(); - } - - 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> ExtractStreamsAsync(string rawText) - { - var streamList = new List(); - 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"); + if (protectionHeader != null) { - var protectionHeader = protectElement.Element("ProtectionHeader"); - if (protectionHeader != null) - { - isProtection = true; - 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节点 - var streamIndexElements = ssmElement.Elements().Where(e => e.Name.LocalName == "StreamIndex"); + // 所有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" - 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; + urlPattern = (qualityLevel.Attribute("Url")?.Value ?? urlPattern)! + .Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime); + var fourCC = qualityLevel.Attribute("FourCC")!.Value.ToUpper(); + 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; - //所有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) + StreamSpec streamSpec = new(); + streamSpec.PublishTime = DateTime.Now; // 发布时间默认现在 + streamSpec.Extension = "m4s"; + streamSpec.OriginalUrl = ParserConfig.OriginalUrl; + streamSpec.PeriodId = indexStr; + 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 { - urlPattern = (qualityLevel.Attribute("Url")?.Value ?? urlPattern)! - .Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime); - var fourCC = qualityLevel.Attribute("FourCC")!.Value.ToUpper(); - 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; + "text" => MediaType.SUBTITLES, + "audio" => MediaType.AUDIO, + _ => null + }; - StreamSpec streamSpec = new(); - streamSpec.PublishTime = DateTime.Now; //发布时间默认现在 - streamSpec.Extension = "m4s"; - streamSpec.OriginalUrl = ParserConfig.OriginalUrl; - streamSpec.PeriodId = indexStr; - 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(); + if (!string.IsNullOrEmpty(codecPrivateData)) + { + streamSpec.Playlist.MediaInit.Index = -1; // 便于排序 + streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}"; + } - streamSpec.Playlist.MediaInit = new MediaSegment(); - if (!string.IsNullOrEmpty(codecPrivateData)) + var currentTime = 0L; + var segIndex = 0; + var varDic = new Dictionary(); + 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; //便于排序 - streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}"; + // This value is one-based. (A value of 2 means two fragments in the contiguous series). + _repeatCount -= 1; } - var currentTime = 0L; - var segIndex = 0; - var varDic = new Dictionary(); - varDic[MSSTags.Bitrate] = bitrate; - - foreach (var c in cElements) + 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++) { - //每个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; + 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数据 - if (MSSMoovProcessor.CanHandle(fourCC!)) + // 生成MOOV数据 + 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!, - 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; - } + streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD; } - streamList.Add(streamSpec); - } - 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; - } - - /// - /// 解析编码 - /// - /// - /// - 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 streamSpecs) - { - //这里才调用URL预处理器,节省开销 - await ProcessUrlAsync(streamSpecs); - } - - private async Task ProcessUrlAsync(List 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++) + foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments) { - part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url); + item.EncryptInfo.Method = DEFAULT_METHOD; } } + streamList.Add(streamSpec); } - } - } - - public string PreProcessUrl(string url) - { - foreach (var p in ParserConfig.UrlProcessors) - { - if (p.CanProcess(ExtractorType, url, ParserConfig)) + else { - 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 streamSpecs) + return Task.FromResult(streamList); + } + + /// + /// 解析编码 + /// + /// + /// + 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); - try + 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 streamSpecs) + { + // 这里才调用URL预处理器,节省开销 + await ProcessUrlAsync(streamSpecs); + } + + private Task ProcessUrlAsync(List 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 - (rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers); + url = p.Process(url, ParserConfig); } + } - ParserConfig.Url = url; - SetInitUrl(); + return url; + } - var newStreams = await ExtractStreamsAsync(rawText); - foreach (var streamSpec in streamSpecs) + public void PreProcessContent() + { + foreach (var p in ParserConfig.ContentProcessors) + { + if (p.CanProcess(ExtractorType, IsmContent, ParserConfig)) { - //有的网站每次请求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 + IsmContent = p.Process(IsmContent, ParserConfig); } - //这里才调用URL预处理器,节省开销 - await ProcessUrlAsync(streamSpecs); } } -} + + public async Task RefreshPlayListAsync(List 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); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Mp4/BinaryReader2.cs b/src/N_m3u8DL-RE.Parser/Mp4/BinaryReader2.cs index e0eceda..4d66979 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/BinaryReader2.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/BinaryReader2.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Mp4SubtitleParser { - //make BinaryReader in Big Endian + // make BinaryReader in Big Endian class BinaryReader2 : BinaryReader { public BinaryReader2(System.IO.Stream stream) : base(stream) { } diff --git a/src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs b/src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs index 3ddd3ea..f185779 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Mp4SubtitleParser { - //make BinaryWriter in Big Endian + // make BinaryWriter in Big Endian class BinaryWriter2 : BinaryWriter { private static bool IsLittleEndian = BitConverter.IsLittleEndian; diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs index 3e5ea62..864672e 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs @@ -20,7 +20,7 @@ namespace Mp4SubtitleParser { var info = new ParsedMP4Info(); - //parse init + // parse init new MP4Parser() .Box("moov", MP4Parser.Children) .Box("trak", MP4Parser.Children) @@ -56,7 +56,7 @@ namespace Mp4SubtitleParser private static void ReadBox(byte[] data, ParsedMP4Info info) { - //find schm + // find schm var schmBytes = new byte[4] { 0x73, 0x63, 0x68, 0x6d }; var schmIndex = 0; 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]); } - //if (info.Scheme != "cenc") return; + // if (info.Scheme != "cenc") return; - //find KID + // find KID var tencBytes = new byte[4] { 0x74, 0x65, 0x6E, 0x63 }; var tencIndex = -1; for (int i = 0; i < data.Length - 4; i++) diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4Parser.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4Parser.cs index fd1ef78..e5ae4b5 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4Parser.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4Parser.cs @@ -111,7 +111,7 @@ namespace Mp4SubtitleParser var name = TypeToString(type); var has64BitSize = false; - //Console.WriteLine($"Parsing MP4 box: {name}"); + // Console.WriteLine($"Parsing MP4 box: {name}"); switch (size) { @@ -129,8 +129,7 @@ namespace Mp4SubtitleParser break; } - BoxHandler boxDefinition = null; - this.BoxDefinitions.TryGetValue(type, out boxDefinition); + this.BoxDefinitions.TryGetValue(type, out BoxHandler? boxDefinition); if (boxDefinition != null) { diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs index 427da8d..378a085 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs @@ -10,8 +10,8 @@ namespace Mp4SubtitleParser public string Begin { get; set; } public string End { get; set; } public string Region { get; set; } - public List Contents { get; set; } = new List(); - public List ContentStrings { get; set; } = new List(); + public List Contents { get; set; } = new(); + public List ContentStrings { get; set; } = new(); public override bool Equals(object? obj) { @@ -43,7 +43,7 @@ namespace Mp4SubtitleParser { bool sawSTPP = false; - //parse init + // parse init new MP4Parser() .Box("moov", MP4Parser.Children) .Box("trak", MP4Parser.Children) @@ -85,12 +85,12 @@ namespace Mp4SubtitleParser return xmlSrc; var _div = bodyNode.SelectSingleNode("ns:div", nsMgr); - //Parse

label + // Parse

label foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!) { var _begin = _p.GetAttribute("begin"); var _end = _p.GetAttribute("end"); - //Handle namespace + // Handle namespace foreach (XmlAttribute attr in _p.Attributes) { if (attr.LocalName == "begin") _begin = attr.Value; @@ -98,8 +98,8 @@ namespace Mp4SubtitleParser } _p.SetAttribute("begin", Add(_begin)); _p.SetAttribute("end", Add(_end)); - //Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}"); - //Console.WriteLine($"{_end} {_p.GetAttribute("begin")}"); + // Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}"); + // Console.WriteLine($"{_end} {_p.GetAttribute("begin")}"); } return xmlDoc.OuterXml; @@ -135,7 +135,7 @@ namespace Mp4SubtitleParser public static WebVttSub ExtractFromMp4s(IEnumerable items, long segTimeMs, long baseTimestamp = 0L) { - //read ttmls + // read ttmls List xmls = new List(); int segIndex = 0; foreach (var item in items) @@ -143,7 +143,7 @@ namespace Mp4SubtitleParser var dataSeg = File.ReadAllBytes(item); var sawMDAT = false; - //parse media + // parse media new MP4Parser() .Box("mdat", MP4Parser.AllData((data) => { @@ -161,10 +161,7 @@ namespace Mp4SubtitleParser else { var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data)); - foreach (var item in datas) - { - xmls.Add(item); - } + xmls.AddRange(datas); } })) .Parse(dataSeg,/* partialOkay= */ false); @@ -181,7 +178,7 @@ namespace Mp4SubtitleParser public static WebVttSub ExtractFromTTMLs(IEnumerable items, long segTimeMs, long baseTimestamp = 0L) { - //read ttmls + // read ttmls List xmls = new List(); int segIndex = 0; foreach (var item in items) @@ -203,7 +200,7 @@ namespace Mp4SubtitleParser private static WebVttSub ExtractSub(List xmls, long baseTimestamp) { - //parsing + // parsing var xmlDoc = new XmlDocument(); var finalSubs = new List(); XmlNode? headNode = null; @@ -215,7 +212,7 @@ namespace Mp4SubtitleParser var xmlContent = item; if (!xmlContent.Contains("(); //id, Base64 + // PNG Subs + var imageDic = new Dictionary(); // id, Base64 if (ImageRegex().IsMatch(xmlDoc.InnerXml)) { foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml)) @@ -266,7 +263,7 @@ namespace Mp4SubtitleParser } } - //convert

to

+ // convert

to

if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0) { foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!) @@ -277,14 +274,14 @@ namespace Mp4SubtitleParser } } - //Parse

label + // Parse

label foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!) { var _begin = _p.GetAttribute("begin"); var _end = _p.GetAttribute("end"); var _region = _p.GetAttribute("region"); var _bgImg = _p.GetAttribute("smpte:backgroundImage"); - //Handle namespace + // Handle namespace foreach (XmlAttribute attr in _p.Attributes) { if (attr.LocalName == "begin") _begin = attr.Value; @@ -301,7 +298,7 @@ namespace Mp4SubtitleParser if (string.IsNullOrEmpty(_bgImg)) { var _spans = _p.ChildNodes; - //Collect + // Collect foreach (XmlNode _node in _spans) { if (_node.NodeType == XmlNodeType.Element) @@ -333,12 +330,12 @@ namespace Mp4SubtitleParser } } - //Check if one

has been splitted + // Check if one

has been splitted 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) { - //Extend

duration + // Extend

duration if (index != -1) finalSubs[index].End = sub.End; else if (!finalSubs.Contains(sub)) @@ -372,7 +369,7 @@ namespace Mp4SubtitleParser } - StringBuilder vtt = new StringBuilder(); + var vtt = new StringBuilder(); vtt.AppendLine("WEBVTT"); foreach (var item in dic) { diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs b/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs index fdb62e3..d51842f 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs @@ -1,216 +1,215 @@ using N_m3u8DL_RE.Common.Entity; 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 - new MP4Parser() - .Box("moov", MP4Parser.Children) - .Box("trak", MP4Parser.Children) - .Box("mdia", MP4Parser.Children) - .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 files, uint timescale) - { - if (timescale == 0) - throw new Exception("Missing timescale for VTT content!"); - - List cues = new(); - - foreach (var item in files) + // parse init + new MP4Parser() + .Box("moov", MP4Parser.Children) + .Box("trak", MP4Parser.Children) + .Box("mdia", MP4Parser.Children) + .FullBox("mdhd", (box) => { - 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; - bool sawTRUN = false; - bool sawMDAT = false; - byte[]? rawPayload = null; - ulong baseTime = 0; - ulong defaultDuration = 0; - List presentations = new(); + return (sawWVTT, timescale); + } + + public static WebVttSub ExtractSub(IEnumerable files, uint timescale) + { + if (timescale == 0) + throw new Exception("Missing timescale for VTT content!"); + + List 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 presentations = new(); - //parse media - new MP4Parser() - .Box("moof", MP4Parser.Children) - .Box("traf", MP4Parser.Children) - .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) + // parse media + new MP4Parser() + .Box("moof", MP4Parser.Children) + .Box("traf", MP4Parser.Children) + .FullBox("tfdt", (box) => { - throw new Exception("A required box is missing"); - } - - var currentTime = baseTime; - var reader = new BinaryReader2(new MemoryStream(rawPayload!)); - - foreach (var presentation in presentations) + 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) => { - var duration = presentation.SampleDuration == 0 ? defaultDuration : presentation.SampleDuration; - var startTime = presentation.SampleCompositionTimeOffset != 0 ? - baseTime + presentation.SampleCompositionTimeOffset : - currentTime; - currentTime = startTime + duration; - var totalSize = 0; - do + 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"); + } + + 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. - 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) { - 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); - } - } - 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); + if (index != -1) { - var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload); - if (index != -1) - { - cues[index].EndTime = cue.EndTime; - } - else - { - cues.Add(cue); - } + cues[index].EndTime = cue.EndTime; + } + 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; - 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() { Cues = cues }; } + 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; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs b/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs index 82f6428..e8b6571 100644 --- a/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs @@ -4,888 +4,887 @@ using N_m3u8DL_RE.Common.Util; using System.Text; using System.Text.RegularExpressions; -//https://github.com/canalplus/rx-player/blob/48d1f845064cea5c5a3546d2c53b1855c2be149d/src/parsers/manifest/smooth/get_codecs.ts -//https://github.dev/Dash-Industry-Forum/dash.js/blob/2aad3e79079b4de0bcd961ce6b4957103d98a621/src/mss/MssFragmentMoovProcessor.js -//https://github.com/yt-dlp/yt-dlp/blob/3639df54c3298e35b5ae2a96a25bc4d3c38950d0/yt_dlp/downloader/ism.py -//https://github.com/google/ExoPlayer/blob/a9444c880230d2c2c79097e89259ce0b9f80b87d/library/extractor/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java#L38 -//https://github.com/sannies/mp4parser/blob/master/isoparser/src/main/java/org/mp4parser/boxes/iso14496/part15/HevcDecoderConfigurationRecord.java -namespace N_m3u8DL_RE.Parser.Mp4 +// https://github.com/canalplus/rx-player/blob/48d1f845064cea5c5a3546d2c53b1855c2be149d/src/parsers/manifest/smooth/get_codecs.ts +// https://github.dev/Dash-Industry-Forum/dash.js/blob/2aad3e79079b4de0bcd961ce6b4957103d98a621/src/mss/MssFragmentMoovProcessor.js +// https://github.com/yt-dlp/yt-dlp/blob/3639df54c3298e35b5ae2a96a25bc4d3c38950d0/yt_dlp/downloader/ism.py +// https://github.com/google/ExoPlayer/blob/a9444c880230d2c2c79097e89259ce0b9f80b87d/library/extractor/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java#L38 +// https://github.com/sannies/mp4parser/blob/master/isoparser/src/main/java/org/mp4parser/boxes/iso14496/part15/HevcDecoderConfigurationRecord.java +namespace N_m3u8DL_RE.Parser.Mp4; + +public partial class MSSMoovProcessor { - public partial class MSSMoovProcessor + [GeneratedRegex("\\(.*?)\\<")] + private static partial Regex KIDRegex(); + + private static string StartCode = "00000001"; + private StreamSpec StreamSpec; + private int TrackId = 2; + private string FourCC; + private string CodecPrivateData; + private int Timesacle; + private long Duration; + private string Language { get => StreamSpec.Language ?? "und"; } + private int Width => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First()); + private int Height => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last()); + private string StreamType; + private int Channels; + private int BitsPerSample; + private int SamplingRate; + private int NalUnitLengthField; + private long CreationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + private bool IsProtection; + private string ProtectionSystemId; + private string ProtectionData; + private string ProtecitonKID; + private string ProtecitonKID_PR; + private byte[] UnityMatrix { - [GeneratedRegex("\\(.*?)\\<")] - private static partial Regex KIDRegex(); - - private static string StartCode = "00000001"; - private StreamSpec StreamSpec; - private int TrackId = 2; - private string FourCC; - private string CodecPrivateData; - private int Timesacle; - private long Duration; - private string Language { get => StreamSpec.Language ?? "und"; } - private int Width { get => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First()); } - private int Height { get => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last()); } - private string StreamType; - private int Channels; - private int BitsPerSample; - private int SamplingRate; - private int NalUnitLengthField; - private long CreationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - - private bool IsProtection; - private string ProtectionSystemId; - private string ProtectionData; - private string ProtecitonKID; - private string ProtecitonKID_PR; - private byte[] UnityMatrix - { - get - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - writer.WriteInt(0x10000); - writer.WriteInt(0); - writer.WriteInt(0); - writer.WriteInt(0); - writer.WriteInt(0x10000); - writer.WriteInt(0); - writer.WriteInt(0); - writer.WriteInt(0); - writer.WriteInt(0x40000000); - return stream.ToArray(); - } - } - private static byte TRACK_ENABLED = 0x1; - private static byte TRACK_IN_MOVIE = 0x2; - private static byte TRACK_IN_PREVIEW = 0x4; - private static byte SELF_CONTAINED = 0x1; - private static List SupportedFourCC = new List() - { - "HVC1","HEV1","AACL","AACH","EC-3","H264","AVC1","DAVC","AVC1","TTML","DVHE","DVH1" - }; - - public MSSMoovProcessor(StreamSpec streamSpec) - { - this.StreamSpec = streamSpec; - var data = streamSpec.MSSData!; - this.NalUnitLengthField = data.NalUnitLengthField; - this.CodecPrivateData = data.CodecPrivateData; - this.FourCC = data.FourCC; - this.Timesacle = data.Timesacle; - this.Duration = data.Duration; - this.StreamType = data.Type; - this.Channels = data.Channels; - this.SamplingRate = data.SamplingRate; - this.BitsPerSample = data.BitsPerSample; - this.IsProtection = data.IsProtection; - this.ProtectionData = data.ProtectionData; - this.ProtectionSystemId = data.ProtectionSystemID; - - //需要手动生成CodecPrivateData - if (string.IsNullOrEmpty(CodecPrivateData)) - { - GenCodecPrivateDataForAAC(); - } - - //解析KID - if (IsProtection) - { - ExtractKID(); - } - } - - private static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = new string[] { "", "A", "B", "C" }; - private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch - { - 96000 => 0x0, - 88200 => 0x1, - 64000 => 0x2, - 48000 => 0x3, - 44100 => 0x4, - 32000 => 0x5, - 24000 => 0x6, - 22050 => 0x7, - 16000 => 0x8, - 12000 => 0x9, - 11025 => 0xA, - 8000 => 0xB, - 7350 => 0xC, - _ => 0x0 - }; - - private void GenCodecPrivateDataForAAC() - { - var objectType = 0x02; //AAC Main Low Complexity => object Type = 2 - var indexFreq = SamplingFrequencyIndex(SamplingRate); - - if (FourCC == "AACH") - { - // 4 bytes : XXXXX XXXX XXXX XXXX XXXXX XXX XXXXXXX - // ' ObjectType' 'Freq Index' 'Channels value' 'Extens Sampl Freq' 'ObjectType' 'GAS' 'alignment = 0' - objectType = 0x05; // High Efficiency AAC Profile = object Type = 5 SBR - var codecPrivateData = new byte[4]; - var extensionSamplingFrequencyIndex = SamplingFrequencyIndex(SamplingRate * 2); // in HE AAC Extension Sampling frequence - // equals to SamplingRate*2 - //Freq Index is present for 3 bits in the first byte, last bit is in the second - codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1)); - codecPrivateData[1] = (byte)((indexFreq << 7) | (Channels << 3) | (extensionSamplingFrequencyIndex >> 1)); - codecPrivateData[2] = (byte)((extensionSamplingFrequencyIndex << 7) | (0x02 << 2)); // origin object type equals to 2 => AAC Main Low Complexity - codecPrivateData[3] = 0x0; //alignment bits - - var arr16 = new ushort[2]; - arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]); - arr16[1] = (ushort)((codecPrivateData[2] << 8) + codecPrivateData[3]); - - //convert decimal to hex value - this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0'); - this.CodecPrivateData += HexUtil.BytesToHex(BitConverter.GetBytes(arr16[1])).PadLeft(16, '0'); - } - else if (FourCC.StartsWith("AAC")) - { - // 2 bytes : XXXXX XXXX XXXX XXX - // ' ObjectType' 'Freq Index' 'Channels value' 'GAS = 000' - var codecPrivateData = new byte[2]; - //Freq Index is present for 3 bits in the first byte, last bit is in the second - codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1)); - codecPrivateData[1] = (byte)((indexFreq << 7) | Channels << 3); - // put the 2 bytes in an 16 bits array - var arr16 = new ushort[1]; - arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]); - - //convert decimal to hex value - this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0'); - } - } - - private void ExtractKID() - { - //playready - if (ProtectionSystemId.ToUpper() == "9A04F079-9840-4286-AB92-E65BE0885F95") - { - var bytes = HexUtil.HexToBytes(ProtectionData.Replace("00", "")); - var text = Encoding.ASCII.GetString(bytes); - var kidBytes = Convert.FromBase64String(KIDRegex().Match(text).Groups[1].Value); - //save kid for playready - this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes); - //fix byte order - var reverse1 = new byte[4] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] }; - var reverse2 = new byte[4] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] }; - Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length); - Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length); - this.ProtecitonKID = HexUtil.BytesToHex(kidBytes); - } - //widevine - else if (ProtectionSystemId.ToUpper() == "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED") - { - throw new NotSupportedException(); - } - } - - public static bool CanHandle(string fourCC) => SupportedFourCC.Contains(fourCC); - - private byte[] Box(string boxType, byte[] payload) + get { using var stream = new MemoryStream(); using var writer = new BinaryWriter2(stream); - - writer.WriteUInt(8 + (uint)payload.Length); - writer.Write(boxType); - writer.Write(payload); - - return stream.ToArray(); - } - - private byte[] FullBox(string boxType, byte version, uint flags, byte[] payload) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.Write(version); - writer.WriteUInt(flags, offset: 1); - writer.Write(payload); - - return Box(boxType, stream.ToArray()); - } - - private byte[] GenSinf(string codec) - { - var frmaBox = Box("frma", Encoding.ASCII.GetBytes(codec)); - - var sinfPayload = new List(); - sinfPayload.AddRange(frmaBox); - - var schmPayload = new List(); - schmPayload.AddRange(Encoding.ASCII.GetBytes("cenc")); //scheme_type 'cenc' => common encryption - schmPayload.AddRange(new byte[] { 0, 1, 0, 0 }); //scheme_version Major version 1, Minor version 0 - var schmBox = FullBox("schm", 0, 0, schmPayload.ToArray()); - - sinfPayload.AddRange(schmBox); - - var tencPayload = new List(); - tencPayload.AddRange(new byte[] { 0, 0 }); - tencPayload.Add(0x1); //default_IsProtected - tencPayload.Add(0x8); //default_Per_Sample_IV_size - tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); //default_KID - //tencPayload.Add(0x8);//default_constant_IV_size - //tencPayload.AddRange(new byte[8]);//default_constant_IV - var tencBox = FullBox("tenc", 0, 0, tencPayload.ToArray()); - - var schiBox = Box("schi", tencBox); - sinfPayload.AddRange(schiBox); - - var sinfBox = Box("sinf", sinfPayload.ToArray()); - - return sinfBox; - } - - private byte[] GenFtyp() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.Write("isml"); //major brand - writer.WriteUInt(1); //minor version - writer.Write("iso5"); //compatible brand - writer.Write("iso6"); //compatible brand - writer.Write("piff"); //compatible brand - writer.Write("msdh"); //compatible brand - - return Box("ftyp", stream.ToArray()); - } - - private byte[] GenMvhd() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteULong(CreationTime); //creation_time - writer.WriteULong(CreationTime); //modification_time - writer.WriteUInt(Timesacle); //timescale - writer.WriteULong(Duration); //duration - writer.WriteUShort(1, padding: 2); //rate - writer.WriteByte(1, padding: 1); //volume - writer.WriteUShort(0); //reserved - writer.WriteUInt(0); - writer.WriteUInt(0); - - writer.Write(UnityMatrix); - - writer.WriteUInt(0); //pre defined - writer.WriteUInt(0); - writer.WriteUInt(0); - writer.WriteUInt(0); - writer.WriteUInt(0); - writer.WriteUInt(0); - - writer.WriteUInt(0xffffffff); //next track id - - - return FullBox("mvhd", 1, 0, stream.ToArray()); - } - - private byte[] GenTkhd() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteULong(CreationTime); //creation_time - writer.WriteULong(CreationTime); //modification_time - writer.WriteUInt(TrackId); //track id - writer.WriteUInt(0); //reserved - writer.WriteULong(Duration); //duration - writer.WriteUInt(0); //reserved - writer.WriteUInt(0); - writer.WriteShort(0); //layer - writer.WriteShort(0); //alternate group - writer.WriteByte(StreamType == "audio" ? (byte)1 : (byte)0, padding: 1); //volume - writer.WriteUShort(0); //reserved - - writer.Write(UnityMatrix); - - writer.WriteUShort(Width, padding: 2); //width - writer.WriteUShort(Height, padding: 2); //height - - return FullBox("tkhd", 1, (uint)TRACK_ENABLED | TRACK_IN_MOVIE | TRACK_IN_PREVIEW, stream.ToArray()); - } - - - private byte[] GenMdhd() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteULong(CreationTime); //creation_time - writer.WriteULong(CreationTime); //modification_time - writer.WriteUInt(Timesacle); //timescale - writer.WriteULong(Duration); //duration - writer.WriteUShort((Language[0] - 0x60) << 10 | (Language[1] - 0x60) << 5 | (Language[2] - 0x60)); //language - writer.WriteUShort(0); //pre defined - - return FullBox("mdhd", 1, 0, stream.ToArray()); - } - - private byte[] GenHdlr() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteUInt(0); //pre defined - if (StreamType == "audio") writer.Write("soun"); - else if (StreamType == "video") writer.Write("vide"); - else if (StreamType == "text") writer.Write("subt"); - else throw new NotSupportedException(); - - writer.WriteUInt(0); //reserved - writer.WriteUInt(0); - writer.WriteUInt(0); - writer.Write($"{StreamSpec.GroupId ?? "RE Handler"}\0"); //name - - return FullBox("hdlr", 0, 0, stream.ToArray()); - } - - private byte[] GenMinf() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - var minfPayload = new List(); - if (StreamType == "audio") - { - var smhd = new List(); - smhd.Add(0); smhd.Add(0); //balance - smhd.Add(0); smhd.Add(0); //reserved - - minfPayload.AddRange(FullBox("smhd", 0, 0, smhd.ToArray())); //Sound Media Header - } - else if (StreamType == "video") - { - var vmhd = new List(); - vmhd.Add(0); vmhd.Add(0); //graphics mode - vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0);//opcolor - - minfPayload.AddRange(FullBox("vmhd", 0, 1, vmhd.ToArray())); //Video Media Header - } - else if (StreamType == "text") - { - minfPayload.AddRange(FullBox("sthd", 0, 0, new byte[0])); //Subtitle Media Header - } - else - { - throw new NotSupportedException(); - } - - var drefPayload = new List(); - drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); //entry count - drefPayload.AddRange(FullBox("url ", 0, SELF_CONTAINED, new byte[0])); //Data Entry URL Box - - var dinfPayload = FullBox("dref", 0, 0, drefPayload.ToArray()); //Data Reference Box - minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); //Data Information Box - - return minfPayload.ToArray(); - } - - private byte[] GenEsds(byte[] audioSpecificConfig) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - // ESDS length = esds box header length (= 12) + - // ES_Descriptor header length (= 5) + - // DecoderConfigDescriptor header length (= 15) + - // decoderSpecificInfo header length (= 2) + - // AudioSpecificConfig length (= codecPrivateData length) - // esdsLength = 34 + len(audioSpecificConfig) - - // ES_Descriptor (see ISO/IEC 14496-1 (Systems)) - writer.WriteByte(0x03); //tag = 0x03 (ES_DescrTag) - writer.WriteByte((byte)(20 + audioSpecificConfig.Length)); //size - writer.WriteByte((byte)((TrackId & 0xFF00) >> 8)); //ES_ID = track_id - writer.WriteByte((byte)(TrackId & 0x00FF)); - writer.WriteByte(0); //flags and streamPriority - - // DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems)) - writer.WriteByte(0x04); //tag = 0x04 (DecoderConfigDescrTag) - writer.WriteByte((byte)(15 + audioSpecificConfig.Length)); //size - writer.WriteByte(0x40); //objectTypeIndication = 0x40 (MPEG-4 AAC) - writer.WriteByte((0x05 << 2) | (0 << 1) | 1); //reserved = 1 - writer.WriteByte(0xFF); //buffersizeDB = undefined - writer.WriteByte(0xFF); - writer.WriteByte(0xFF); - - var bandwidth = StreamSpec.Bandwidth!; - writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); //maxBitrate - writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16)); - writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8)); - writer.WriteByte((byte)(bandwidth & 0x000000FF)); - writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); //avgbitrate - writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16)); - writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8)); - writer.WriteByte((byte)(bandwidth & 0x000000FF)); - - // DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems)) - writer.WriteByte(0x05); //tag = 0x05 (DecSpecificInfoTag) - writer.WriteByte((byte)audioSpecificConfig.Length); //size - writer.Write(audioSpecificConfig); //AudioSpecificConfig bytes - - return FullBox("esds", 0, 0, stream.ToArray()); - } - - private byte[] GetSampleEntryBox() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteByte(0); //reserved - writer.WriteByte(0); - writer.WriteByte(0); - writer.WriteByte(0); - writer.WriteByte(0); - writer.WriteByte(0); - writer.WriteUShort(1); //data reference index - - if (StreamType == "audio") - { - writer.WriteUInt(0); //reserved2 - writer.WriteUInt(0); - writer.WriteUShort(Channels); //channels - writer.WriteUShort(BitsPerSample); //bits_per_sample - writer.WriteUShort(0); //pre defined - writer.WriteUShort(0); //reserved3 - writer.WriteUShort(SamplingRate, padding: 2); //sampling_rate - - var audioSpecificConfig = HexUtil.HexToBytes(CodecPrivateData); - var esdsBox = GenEsds(audioSpecificConfig); - writer.Write(esdsBox); - - if (FourCC.StartsWith("AAC")) - { - if (IsProtection) - { - var sinfBox = GenSinf("mp4a"); - writer.Write(sinfBox); - return Box("enca", stream.ToArray()); //Encrypted Audio - } - else - { - return Box("mp4a", stream.ToArray()); - } - } - if (FourCC == "EC-3") - { - if (IsProtection) - { - var sinfBox = GenSinf("ec-3"); - writer.Write(sinfBox); - return Box("enca", stream.ToArray()); //Encrypted Audio - } - else - { - return Box("ec-3", stream.ToArray()); - } - } - } - else if (StreamType == "video") - { - writer.WriteUShort(0); //pre defined - writer.WriteUShort(0); //reserved - writer.WriteUInt(0); //pre defined - writer.WriteUInt(0); - writer.WriteUInt(0); - writer.WriteUShort(Width); //width - writer.WriteUShort(Height); //height - writer.WriteUShort(0x48, padding: 2); //horiz resolution 72 dpi - writer.WriteUShort(0x48, padding: 2); //vert resolution 72 dpi - writer.WriteUInt(0); //reserved - writer.WriteUShort(1); //frame count - for (int i = 0; i < 32; i++) //compressor name - { - writer.WriteByte(0); - } - writer.WriteUShort(0x18); //depth - writer.WriteUShort(65535); //pre defined - - var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData); - - if (FourCC == "H264" || FourCC == "AVC1" || FourCC == "DAVC" || FourCC == "AVC1") - { - var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); - var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7).First()); - var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8).First()); - //make avcC - var avcC = GetAvcC(sps, pps); - writer.Write(avcC); - if (IsProtection) - { - var sinfBox = GenSinf("avc1"); - writer.Write(sinfBox); - return Box("encv", stream.ToArray()); //Encrypted Video - } - else - { - return Box("avc1", stream.ToArray()); //AVC Simple Entry - } - } - else if (FourCC == "HVC1" || FourCC == "HEV1") - { - var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); - var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First()); - var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First()); - var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First()); - //make hvcC - var hvcC = GetHvcC(sps, pps, vps); - writer.Write(hvcC); - if (IsProtection) - { - var sinfBox = GenSinf("hvc1"); - writer.Write(sinfBox); - return Box("encv", stream.ToArray()); //Encrypted Video - } - else - { - return Box("hvc1", stream.ToArray()); //HEVC Simple Entry - } - } - // 杜比视界也按照hevc处理 - else if (FourCC == "DVHE" || FourCC == "DVH1") - { - var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); - var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First()); - var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First()); - var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First()); - //make hvcC - var hvcC = GetHvcC(sps, pps, vps, "dvh1"); - writer.Write(hvcC); - if (IsProtection) - { - var sinfBox = GenSinf("dvh1"); - writer.Write(sinfBox); - return Box("encv", stream.ToArray()); //Encrypted Video - } - else - { - return Box("dvh1", stream.ToArray()); //HEVC Simple Entry - } - } - else - { - throw new NotSupportedException(); - } - } - else if (StreamType == "text") - { - if (FourCC == "TTML") - { - writer.Write("http://www.w3.org/ns/ttml\0"); //namespace - writer.Write("\0"); //schema location - writer.Write("\0"); //auxilary mime types(??) - return Box("stpp", stream.ToArray()); //TTML Simple Entry - } - else - { - throw new NotSupportedException(); - } - } - else - { - throw new NotSupportedException(); - } - - throw new NotSupportedException(); - } - - private byte[] GetAvcC(byte[] sps, byte[] pps) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteByte(1); //configuration version - writer.Write(sps[1..4]); //avc profile indication + profile compatibility + avc level indication - writer.WriteByte((byte)(0xfc | (NalUnitLengthField - 1))); //complete representation (1) + reserved (11111) + length size minus one - writer.WriteByte(1); //reserved (0) + number of sps (0000001) - writer.WriteUShort(sps.Length); - writer.Write(sps); - writer.WriteByte(1); //number of pps - writer.WriteUShort(pps.Length); - writer.Write(pps); - - return Box("avcC", stream.ToArray()); //AVC Decoder Configuration Record - } - - private byte[] GetHvcC(byte[] sps, byte[] pps, byte[] vps, string code = "hvc1") - { - var oriSps = new List(sps); - //https://www.itu.int/rec/dologin.asp?lang=f&id=T-REC-H.265-201504-S!!PDF-E&type=items - //Read generalProfileSpace, generalTierFlag, generalProfileIdc, - //generalProfileCompatibilityFlags, constraintBytes, generalLevelIdc - //from sps - var encList = new List(); - /** - * 处理payload, 有00 00 03 0,1,2,3的情况 统一换成00 00 XX 即丢弃03 - * 注意:此处采用的逻辑是直接简单粗暴地判断列表末尾3字节,如果是0x000003就删掉最后的0x03,可能会导致以下情况 - * 00 00 03 03 03 03 03 01 会被直接处理成 => 00 00 01 - * 此处经过测试只有直接跳过才正常,如果处理成 00 00 03 03 03 03 01 是有问题的 - * - * 测试的数据如下: - * 原始:42 01 01 01 60 00 00 03 00 90 00 00 03 00 00 03 00 96 a0 01 e0 20 06 61 65 95 9a 49 30 bf fc 0c 7c 0c 81 a8 08 08 08 20 00 00 03 00 20 00 00 03 03 01 - * 处理后:42 01 01 01 60 00 00 00 90 00 00 00 00 00 96 A0 01 E0 20 06 61 65 95 9A 49 30 BF FC 0C 7C 0C 81 A8 08 08 08 20 00 00 00 20 00 00 01 - */ - using (var _reader = new BinaryReader(new MemoryStream(sps))) - { - while (_reader.BaseStream.Position < _reader.BaseStream.Length) - { - encList.Add(_reader.ReadByte()); - if (encList.Count >= 3 && encList[encList.Count - 3] == 0x00 && encList[encList.Count - 2] == 0x00 && encList[encList.Count - 1] == 0x03) - { - encList.RemoveAt(encList.Count - 1); - } - } - } - sps = encList.ToArray(); - - using var reader = new BinaryReader2(new MemoryStream(sps)); - reader.ReadBytes(2); //Skip 2 bytes unit header - var firstByte = reader.ReadByte(); - var maxSubLayersMinus1 = (firstByte & 0xe) >> 1; - var nextByte = reader.ReadByte(); - var generalProfileSpace = (nextByte & 0xc0) >> 6; - var generalTierFlag = (nextByte & 0x20) >> 5; - var generalProfileIdc = nextByte & 0x1f; - var generalProfileCompatibilityFlags = reader.ReadUInt32(); - var constraintBytes = reader.ReadBytes(6); - var generalLevelIdc = reader.ReadByte(); - - /*var skipBit = 0; - for (int i = 0; i < maxSubLayersMinus1; i++) - { - skipBit += 2; //sub_layer_profile_present_flag sub_layer_level_present_flag - } - if (maxSubLayersMinus1 > 0) - { - for (int i = maxSubLayersMinus1; i < 8; i++) - { - skipBit += 2; //reserved_zero_2bits - } - } - for (int i = 0; i < maxSubLayersMinus1; i++) - { - skipBit += 2; //sub_layer_profile_present_flag sub_layer_level_present_flag - }*/ - - //生成编码信息 - var codecs = code + - $".{HEVC_GENERAL_PROFILE_SPACE_STRINGS[generalProfileSpace]}{generalProfileIdc}" + - $".{Convert.ToString(generalProfileCompatibilityFlags, 16)}" + - $".{(generalTierFlag == 1 ? 'H' : 'L')}{generalLevelIdc}" + - $".{HexUtil.BytesToHex(constraintBytes.Where(b => b != 0).ToArray())}"; - StreamSpec.Codecs = codecs; - - - /////////////////////// - - - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - //var reserved1 = 0xF; - - writer.WriteByte(1); //configuration version - writer.WriteByte((byte)((generalProfileSpace << 6) + (generalTierFlag == 1 ? 0x20 : 0) | generalProfileIdc)); //general_profile_space + general_tier_flag + general_profile_idc - writer.WriteUInt(generalProfileCompatibilityFlags); //general_profile_compatibility_flags - writer.Write(constraintBytes); //general_constraint_indicator_flags - writer.WriteByte((byte)generalProfileIdc); //general_level_idc - writer.WriteUShort(0xf000); //reserved + min_spatial_segmentation_idc - writer.WriteByte(0xfc); //reserved + parallelismType - writer.WriteByte(0 | 0xfc); //reserved + chromaFormat - writer.WriteByte(0 | 0xf8); //reserved + bitDepthLumaMinus8 - writer.WriteByte(0 | 0xf8); //reserved + bitDepthChromaMinus8 - writer.WriteUShort(0); //avgFrameRate - writer.WriteByte((byte)(0 << 6 | 0 << 3 | 0 << 2 | (NalUnitLengthField - 1))); //constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne - writer.WriteByte(0x03); //numOfArrays (vps sps pps) - - sps = oriSps.ToArray(); - writer.WriteByte(0x20); //array_completeness + reserved + NAL_unit_type - writer.WriteUShort(1); //numNalus - writer.WriteUShort(vps.Length); - writer.Write(vps); - writer.WriteByte(0x21); - writer.WriteUShort(1); //numNalus - writer.WriteUShort(sps.Length); - writer.Write(sps); - writer.WriteByte(0x22); - writer.WriteUShort(1); //numNalus - writer.WriteUShort(pps.Length); - writer.Write(pps); - - return Box("hvcC", stream.ToArray()); //HEVC Decoder Configuration Record - } - - private byte[] GetStsd() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteUInt(1); //entry count - var sampleEntryData = GetSampleEntryBox(); - writer.Write(sampleEntryData); - - return stream.ToArray(); - } - - private byte[] GetMehd() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteULong(Duration); - - return FullBox("mehd", 1, 0, stream.ToArray()); //Movie Extends Header Box - } - private byte[] GetTrex() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - writer.WriteUInt(TrackId); //track id - writer.WriteUInt(1); //default sample description index - writer.WriteUInt(0); //default sample duration - writer.WriteUInt(0); //default sample size - writer.WriteUInt(0); //default sample flags - - return FullBox("trex", 0, 0, stream.ToArray()); //Track Extends Box - } - - private byte[] GenPsshBoxForPlayReady() - { - using var _stream = new MemoryStream(); - using var _writer = new BinaryWriter2(_stream); - var sysIdData = HexUtil.HexToBytes(ProtectionSystemId.Replace("-", "")); - var psshData = HexUtil.HexToBytes(ProtectionData); - - _writer.Write(sysIdData); // SystemID 16 bytes - _writer.WriteUInt(psshData.Length); //Size of Data 4 bytes - _writer.Write(psshData); //Data - var psshBox = FullBox("pssh", 0, 0, _stream.ToArray()); - return psshBox; - } - - private byte[] GenPsshBoxForWideVine() - { - using var _stream = new MemoryStream(); - using var _writer = new BinaryWriter2(_stream); - var sysIdData = HexUtil.HexToBytes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed".Replace("-", "")); - //var kid = HexUtil.HexToBytes(ProtecitonKID); - - _writer.Write(sysIdData); // SystemID 16 bytes - var psshData = HexUtil.HexToBytes($"08011210{ProtecitonKID}1A046E647265220400000000"); - _writer.WriteUInt(psshData.Length); //Size of Data 4 bytes - _writer.Write(psshData); //Data - var psshBox = FullBox("pssh", 0, 0, _stream.ToArray()); - return psshBox; - } - - private byte[] GenMoof() - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter2(stream); - - //make senc - writer.WriteUInt(1); //sample_count - writer.Write(new byte[8]); //8 bytes IV - - var sencBox = FullBox("senc", 1, 0, stream.ToArray()); - - var moofBox = Box("moof", sencBox); //Movie Extends Box - - return moofBox; - } - - public byte[] GenHeader(byte[] firstSegment) - { - new MP4Parser() - .Box("moof", MP4Parser.Children) - .Box("traf", MP4Parser.Children) - .FullBox("tfhd", (box) => - { - TrackId = (int)box.Reader.ReadUInt32(); - }) - .Parse(firstSegment); - - return GenHeader(); - } - - public byte[] GenHeader() - { - using var stream = new MemoryStream(); - - var ftyp = GenFtyp(); // File Type Box - stream.Write(ftyp); - - var moovPayload = GenMvhd(); // Movie Header Box - - var trakPayload = GenTkhd(); // Track Header Box - - var mdhdPayload = GenMdhd(); // Media Header Box - - var hdlrPayload = GenHdlr(); // Handler Reference Box - - var mdiaPayload = mdhdPayload.Concat(hdlrPayload).ToArray(); - - var minfPayload = GenMinf(); - - - var sttsPayload = new byte[] { 0, 0, 0, 0 }; //entry count - var stblPayload = FullBox("stts", 0, 0, sttsPayload); //Decoding Time to Sample Box - - var stscPayload = new byte[] { 0, 0, 0, 0 }; //entry count - var stscBox = FullBox("stsc", 0, 0, stscPayload); //Sample To Chunk Box - - var stcoPayload = new byte[] { 0, 0, 0, 0 }; //entry count - var stcoBox = FullBox("stco", 0, 0, stcoPayload); //Chunk Offset Box - - var stszPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; //sample size, sample count - var stszBox = FullBox("stsz", 0, 0, stszPayload); //Sample Size Box - - var stsdPayload = GetStsd(); - var stsdBox = FullBox("stsd", 0, 0, stsdPayload); //Sample Description Box - - stblPayload = stblPayload.Concat(stscBox).Concat(stcoBox).Concat(stszBox).Concat(stsdBox).ToArray(); - - - var stblBox = Box("stbl", stblPayload); //Sample Table Box - minfPayload = minfPayload.Concat(stblBox).ToArray(); - - var minfBox = Box("minf", minfPayload); //Media Information Box - mdiaPayload = mdiaPayload.Concat(minfBox).ToArray(); - - var mdiaBox = Box("mdia", mdiaPayload); //Media Box - trakPayload = trakPayload.Concat(mdiaBox).ToArray(); - - var trakBox = Box("trak", trakPayload); //Track Box - moovPayload = moovPayload.Concat(trakBox).ToArray(); - - var mvexPayload = GetMehd(); - var trexBox = GetTrex(); - mvexPayload = mvexPayload.Concat(trexBox).ToArray(); - - var mvexBox = Box("mvex", mvexPayload); //Movie Extends Box - moovPayload = moovPayload.Concat(mvexBox).ToArray(); - - if (IsProtection) - { - var psshBox1 = GenPsshBoxForPlayReady(); - var psshBox2 = GenPsshBoxForWideVine(); - moovPayload = moovPayload.Concat(psshBox1).Concat(psshBox2).ToArray(); - } - - var moovBox = Box("moov", moovPayload); //Movie Box - - stream.Write(moovBox); - - //var moofBox = GenMoof(); //Movie Extends Box - //stream.Write(moofBox); - + writer.WriteInt(0x10000); + writer.WriteInt(0); + writer.WriteInt(0); + writer.WriteInt(0); + writer.WriteInt(0x10000); + writer.WriteInt(0); + writer.WriteInt(0); + writer.WriteInt(0); + writer.WriteInt(0x40000000); return stream.ToArray(); } } -} + private static byte TRACK_ENABLED = 0x1; + private static byte TRACK_IN_MOVIE = 0x2; + private static byte TRACK_IN_PREVIEW = 0x4; + private static byte SELF_CONTAINED = 0x1; + private static List SupportedFourCC = new() + { + "HVC1","HEV1","AACL","AACH","EC-3","H264","AVC1","DAVC","AVC1","TTML","DVHE","DVH1" + }; + + public MSSMoovProcessor(StreamSpec streamSpec) + { + this.StreamSpec = streamSpec; + var data = streamSpec.MSSData!; + this.NalUnitLengthField = data.NalUnitLengthField; + this.CodecPrivateData = data.CodecPrivateData; + this.FourCC = data.FourCC; + this.Timesacle = data.Timesacle; + this.Duration = data.Duration; + this.StreamType = data.Type; + this.Channels = data.Channels; + this.SamplingRate = data.SamplingRate; + this.BitsPerSample = data.BitsPerSample; + this.IsProtection = data.IsProtection; + this.ProtectionData = data.ProtectionData; + this.ProtectionSystemId = data.ProtectionSystemID; + + // 需要手动生成CodecPrivateData + if (string.IsNullOrEmpty(CodecPrivateData)) + { + GenCodecPrivateDataForAAC(); + } + + // 解析KID + if (IsProtection) + { + ExtractKID(); + } + } + + private static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = new string[] { "", "A", "B", "C" }; + private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch + { + 96000 => 0x0, + 88200 => 0x1, + 64000 => 0x2, + 48000 => 0x3, + 44100 => 0x4, + 32000 => 0x5, + 24000 => 0x6, + 22050 => 0x7, + 16000 => 0x8, + 12000 => 0x9, + 11025 => 0xA, + 8000 => 0xB, + 7350 => 0xC, + _ => 0x0 + }; + + private void GenCodecPrivateDataForAAC() + { + var objectType = 0x02; // AAC Main Low Complexity => object Type = 2 + var indexFreq = SamplingFrequencyIndex(SamplingRate); + + if (FourCC == "AACH") + { + // 4 bytes : XXXXX XXXX XXXX XXXX XXXXX XXX XXXXXXX + // ' ObjectType' 'Freq Index' 'Channels value' 'Extens Sampl Freq' 'ObjectType' 'GAS' 'alignment = 0' + objectType = 0x05; // High Efficiency AAC Profile = object Type = 5 SBR + var codecPrivateData = new byte[4]; + var extensionSamplingFrequencyIndex = SamplingFrequencyIndex(SamplingRate * 2); // in HE AAC Extension Sampling frequence + // equals to SamplingRate*2 + // Freq Index is present for 3 bits in the first byte, last bit is in the second + codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1)); + codecPrivateData[1] = (byte)((indexFreq << 7) | (Channels << 3) | (extensionSamplingFrequencyIndex >> 1)); + codecPrivateData[2] = (byte)((extensionSamplingFrequencyIndex << 7) | (0x02 << 2)); // origin object type equals to 2 => AAC Main Low Complexity + codecPrivateData[3] = 0x0; // alignment bits + + var arr16 = new ushort[2]; + arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]); + arr16[1] = (ushort)((codecPrivateData[2] << 8) + codecPrivateData[3]); + + // convert decimal to hex value + this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0'); + this.CodecPrivateData += HexUtil.BytesToHex(BitConverter.GetBytes(arr16[1])).PadLeft(16, '0'); + } + else if (FourCC.StartsWith("AAC")) + { + // 2 bytes : XXXXX XXXX XXXX XXX + // ' ObjectType' 'Freq Index' 'Channels value' 'GAS = 000' + var codecPrivateData = new byte[2]; + // Freq Index is present for 3 bits in the first byte, last bit is in the second + codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1)); + codecPrivateData[1] = (byte)((indexFreq << 7) | Channels << 3); + // put the 2 bytes in an 16 bits array + var arr16 = new ushort[1]; + arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]); + + // convert decimal to hex value + this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0'); + } + } + + private void ExtractKID() + { + // playready + if (ProtectionSystemId.ToUpper() == "9A04F079-9840-4286-AB92-E65BE0885F95") + { + var bytes = HexUtil.HexToBytes(ProtectionData.Replace("00", "")); + var text = Encoding.ASCII.GetString(bytes); + var kidBytes = Convert.FromBase64String(KIDRegex().Match(text).Groups[1].Value); + // save kid for playready + this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes); + // fix byte order + var reverse1 = new byte[4] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] }; + var reverse2 = new byte[4] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] }; + Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length); + Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length); + this.ProtecitonKID = HexUtil.BytesToHex(kidBytes); + } + // widevine + else if (ProtectionSystemId.ToUpper() == "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED") + { + throw new NotSupportedException(); + } + } + + public static bool CanHandle(string fourCC) => SupportedFourCC.Contains(fourCC); + + private byte[] Box(string boxType, byte[] payload) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteUInt(8 + (uint)payload.Length); + writer.Write(boxType); + writer.Write(payload); + + return stream.ToArray(); + } + + private byte[] FullBox(string boxType, byte version, uint flags, byte[] payload) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.Write(version); + writer.WriteUInt(flags, offset: 1); + writer.Write(payload); + + return Box(boxType, stream.ToArray()); + } + + private byte[] GenSinf(string codec) + { + var frmaBox = Box("frma", Encoding.ASCII.GetBytes(codec)); + + var sinfPayload = new List(); + sinfPayload.AddRange(frmaBox); + + var schmPayload = new List(); + schmPayload.AddRange(Encoding.ASCII.GetBytes("cenc")); // scheme_type 'cenc' => common encryption + schmPayload.AddRange(new byte[] { 0, 1, 0, 0 }); // scheme_version Major version 1, Minor version 0 + var schmBox = FullBox("schm", 0, 0, schmPayload.ToArray()); + + sinfPayload.AddRange(schmBox); + + var tencPayload = new List(); + tencPayload.AddRange(new byte[] { 0, 0 }); + tencPayload.Add(0x1); // default_IsProtected + tencPayload.Add(0x8); // default_Per_Sample_IV_size + tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); // default_KID + // tencPayload.Add(0x8);// default_constant_IV_size + // tencPayload.AddRange(new byte[8]);// default_constant_IV + var tencBox = FullBox("tenc", 0, 0, tencPayload.ToArray()); + + var schiBox = Box("schi", tencBox); + sinfPayload.AddRange(schiBox); + + var sinfBox = Box("sinf", sinfPayload.ToArray()); + + return sinfBox; + } + + private byte[] GenFtyp() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.Write("isml"); // major brand + writer.WriteUInt(1); // minor version + writer.Write("iso5"); // compatible brand + writer.Write("iso6"); // compatible brand + writer.Write("piff"); // compatible brand + writer.Write("msdh"); // compatible brand + + return Box("ftyp", stream.ToArray()); + } + + private byte[] GenMvhd() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteULong(CreationTime); // creation_time + writer.WriteULong(CreationTime); // modification_time + writer.WriteUInt(Timesacle); // timescale + writer.WriteULong(Duration); // duration + writer.WriteUShort(1, padding: 2); // rate + writer.WriteByte(1, padding: 1); // volume + writer.WriteUShort(0); // reserved + writer.WriteUInt(0); + writer.WriteUInt(0); + + writer.Write(UnityMatrix); + + writer.WriteUInt(0); // pre defined + writer.WriteUInt(0); + writer.WriteUInt(0); + writer.WriteUInt(0); + writer.WriteUInt(0); + writer.WriteUInt(0); + + writer.WriteUInt(0xffffffff); // next track id + + + return FullBox("mvhd", 1, 0, stream.ToArray()); + } + + private byte[] GenTkhd() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteULong(CreationTime); // creation_time + writer.WriteULong(CreationTime); // modification_time + writer.WriteUInt(TrackId); // track id + writer.WriteUInt(0); // reserved + writer.WriteULong(Duration); // duration + writer.WriteUInt(0); // reserved + writer.WriteUInt(0); + writer.WriteShort(0); // layer + writer.WriteShort(0); // alternate group + writer.WriteByte(StreamType == "audio" ? (byte)1 : (byte)0, padding: 1); // volume + writer.WriteUShort(0); // reserved + + writer.Write(UnityMatrix); + + writer.WriteUShort(Width, padding: 2); // width + writer.WriteUShort(Height, padding: 2); // height + + return FullBox("tkhd", 1, (uint)TRACK_ENABLED | TRACK_IN_MOVIE | TRACK_IN_PREVIEW, stream.ToArray()); + } + + + private byte[] GenMdhd() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteULong(CreationTime); // creation_time + writer.WriteULong(CreationTime); // modification_time + writer.WriteUInt(Timesacle); // timescale + writer.WriteULong(Duration); // duration + writer.WriteUShort((Language[0] - 0x60) << 10 | (Language[1] - 0x60) << 5 | (Language[2] - 0x60)); // language + writer.WriteUShort(0); // pre defined + + return FullBox("mdhd", 1, 0, stream.ToArray()); + } + + private byte[] GenHdlr() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteUInt(0); // pre defined + if (StreamType == "audio") writer.Write("soun"); + else if (StreamType == "video") writer.Write("vide"); + else if (StreamType == "text") writer.Write("subt"); + else throw new NotSupportedException(); + + writer.WriteUInt(0); // reserved + writer.WriteUInt(0); + writer.WriteUInt(0); + writer.Write($"{StreamSpec.GroupId ?? "RE Handler"}\0"); // name + + return FullBox("hdlr", 0, 0, stream.ToArray()); + } + + private byte[] GenMinf() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + var minfPayload = new List(); + if (StreamType == "audio") + { + var smhd = new List(); + smhd.Add(0); smhd.Add(0); // balance + smhd.Add(0); smhd.Add(0); // reserved + + minfPayload.AddRange(FullBox("smhd", 0, 0, smhd.ToArray())); // Sound Media Header + } + else if (StreamType == "video") + { + var vmhd = new List(); + vmhd.Add(0); vmhd.Add(0); // graphics mode + vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0);// opcolor + + minfPayload.AddRange(FullBox("vmhd", 0, 1, vmhd.ToArray())); // Video Media Header + } + else if (StreamType == "text") + { + minfPayload.AddRange(FullBox("sthd", 0, 0, new byte[0])); // Subtitle Media Header + } + else + { + throw new NotSupportedException(); + } + + var drefPayload = new List(); + drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); // entry count + drefPayload.AddRange(FullBox("url ", 0, SELF_CONTAINED, new byte[0])); // Data Entry URL Box + + var dinfPayload = FullBox("dref", 0, 0, drefPayload.ToArray()); // Data Reference Box + minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); // Data Information Box + + return minfPayload.ToArray(); + } + + private byte[] GenEsds(byte[] audioSpecificConfig) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + // ESDS length = esds box header length (= 12) + + // ES_Descriptor header length (= 5) + + // DecoderConfigDescriptor header length (= 15) + + // decoderSpecificInfo header length (= 2) + + // AudioSpecificConfig length (= codecPrivateData length) + // esdsLength = 34 + len(audioSpecificConfig) + + // ES_Descriptor (see ISO/IEC 14496-1 (Systems)) + writer.WriteByte(0x03); // tag = 0x03 (ES_DescrTag) + writer.WriteByte((byte)(20 + audioSpecificConfig.Length)); // size + writer.WriteByte((byte)((TrackId & 0xFF00) >> 8)); // ES_ID = track_id + writer.WriteByte((byte)(TrackId & 0x00FF)); + writer.WriteByte(0); // flags and streamPriority + + // DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems)) + writer.WriteByte(0x04); // tag = 0x04 (DecoderConfigDescrTag) + writer.WriteByte((byte)(15 + audioSpecificConfig.Length)); // size + writer.WriteByte(0x40); // objectTypeIndication = 0x40 (MPEG-4 AAC) + writer.WriteByte((0x05 << 2) | (0 << 1) | 1); // reserved = 1 + writer.WriteByte(0xFF); // buffersizeDB = undefined + writer.WriteByte(0xFF); + writer.WriteByte(0xFF); + + var bandwidth = StreamSpec.Bandwidth!; + writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); // maxBitrate + writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16)); + writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8)); + writer.WriteByte((byte)(bandwidth & 0x000000FF)); + writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); // avgbitrate + writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16)); + writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8)); + writer.WriteByte((byte)(bandwidth & 0x000000FF)); + + // DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems)) + writer.WriteByte(0x05); // tag = 0x05 (DecSpecificInfoTag) + writer.WriteByte((byte)audioSpecificConfig.Length); // size + writer.Write(audioSpecificConfig); // AudioSpecificConfig bytes + + return FullBox("esds", 0, 0, stream.ToArray()); + } + + private byte[] GetSampleEntryBox() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteByte(0); // reserved + writer.WriteByte(0); + writer.WriteByte(0); + writer.WriteByte(0); + writer.WriteByte(0); + writer.WriteByte(0); + writer.WriteUShort(1); // data reference index + + if (StreamType == "audio") + { + writer.WriteUInt(0); // reserved2 + writer.WriteUInt(0); + writer.WriteUShort(Channels); // channels + writer.WriteUShort(BitsPerSample); // bits_per_sample + writer.WriteUShort(0); // pre defined + writer.WriteUShort(0); // reserved3 + writer.WriteUShort(SamplingRate, padding: 2); // sampling_rate + + var audioSpecificConfig = HexUtil.HexToBytes(CodecPrivateData); + var esdsBox = GenEsds(audioSpecificConfig); + writer.Write(esdsBox); + + if (FourCC.StartsWith("AAC")) + { + if (IsProtection) + { + var sinfBox = GenSinf("mp4a"); + writer.Write(sinfBox); + return Box("enca", stream.ToArray()); // Encrypted Audio + } + else + { + return Box("mp4a", stream.ToArray()); + } + } + if (FourCC == "EC-3") + { + if (IsProtection) + { + var sinfBox = GenSinf("ec-3"); + writer.Write(sinfBox); + return Box("enca", stream.ToArray()); // Encrypted Audio + } + else + { + return Box("ec-3", stream.ToArray()); + } + } + } + else if (StreamType == "video") + { + writer.WriteUShort(0); // pre defined + writer.WriteUShort(0); // reserved + writer.WriteUInt(0); // pre defined + writer.WriteUInt(0); + writer.WriteUInt(0); + writer.WriteUShort(Width); // width + writer.WriteUShort(Height); // height + writer.WriteUShort(0x48, padding: 2); // horiz resolution 72 dpi + writer.WriteUShort(0x48, padding: 2); // vert resolution 72 dpi + writer.WriteUInt(0); // reserved + writer.WriteUShort(1); // frame count + for (int i = 0; i < 32; i++) // compressor name + { + writer.WriteByte(0); + } + writer.WriteUShort(0x18); // depth + writer.WriteUShort(65535); // pre defined + + var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData); + + if (FourCC == "H264" || FourCC == "AVC1" || FourCC == "DAVC" || FourCC == "AVC1") + { + var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); + var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7).First()); + var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8).First()); + // make avcC + var avcC = GetAvcC(sps, pps); + writer.Write(avcC); + if (IsProtection) + { + var sinfBox = GenSinf("avc1"); + writer.Write(sinfBox); + return Box("encv", stream.ToArray()); // Encrypted Video + } + else + { + return Box("avc1", stream.ToArray()); // AVC Simple Entry + } + } + else if (FourCC == "HVC1" || FourCC == "HEV1") + { + var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); + var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First()); + var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First()); + var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First()); + // make hvcC + var hvcC = GetHvcC(sps, pps, vps); + writer.Write(hvcC); + if (IsProtection) + { + var sinfBox = GenSinf("hvc1"); + writer.Write(sinfBox); + return Box("encv", stream.ToArray()); // Encrypted Video + } + else + { + return Box("hvc1", stream.ToArray()); // HEVC Simple Entry + } + } + // 杜比视界也按照hevc处理 + else if (FourCC == "DVHE" || FourCC == "DVH1") + { + var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); + var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First()); + var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First()); + var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First()); + // make hvcC + var hvcC = GetHvcC(sps, pps, vps, "dvh1"); + writer.Write(hvcC); + if (IsProtection) + { + var sinfBox = GenSinf("dvh1"); + writer.Write(sinfBox); + return Box("encv", stream.ToArray()); // Encrypted Video + } + else + { + return Box("dvh1", stream.ToArray()); // HEVC Simple Entry + } + } + else + { + throw new NotSupportedException(); + } + } + else if (StreamType == "text") + { + if (FourCC == "TTML") + { + writer.Write("http://www.w3.org/ns/ttml\0"); // namespace + writer.Write("\0"); // schema location + writer.Write("\0"); // auxilary mime types(??) + return Box("stpp", stream.ToArray()); // TTML Simple Entry + } + else + { + throw new NotSupportedException(); + } + } + else + { + throw new NotSupportedException(); + } + + throw new NotSupportedException(); + } + + private byte[] GetAvcC(byte[] sps, byte[] pps) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteByte(1); // configuration version + writer.Write(sps[1..4]); // avc profile indication + profile compatibility + avc level indication + writer.WriteByte((byte)(0xfc | (NalUnitLengthField - 1))); // complete representation (1) + reserved (11111) + length size minus one + writer.WriteByte(1); // reserved (0) + number of sps (0000001) + writer.WriteUShort(sps.Length); + writer.Write(sps); + writer.WriteByte(1); // number of pps + writer.WriteUShort(pps.Length); + writer.Write(pps); + + return Box("avcC", stream.ToArray()); // AVC Decoder Configuration Record + } + + private byte[] GetHvcC(byte[] sps, byte[] pps, byte[] vps, string code = "hvc1") + { + var oriSps = new List(sps); + // https://www.itu.int/rec/dologin.asp?lang=f&id=T-REC-H.265-201504-S!!PDF-E&type=items + // Read generalProfileSpace, generalTierFlag, generalProfileIdc, + // generalProfileCompatibilityFlags, constraintBytes, generalLevelIdc + // from sps + var encList = new List(); + /** + * 处理payload, 有00 00 03 0,1,2,3的情况 统一换成00 00 XX 即丢弃03 + * 注意:此处采用的逻辑是直接简单粗暴地判断列表末尾3字节,如果是0x000003就删掉最后的0x03,可能会导致以下情况 + * 00 00 03 03 03 03 03 01 会被直接处理成 => 00 00 01 + * 此处经过测试只有直接跳过才正常,如果处理成 00 00 03 03 03 03 01 是有问题的 + * + * 测试的数据如下: + * 原始:42 01 01 01 60 00 00 03 00 90 00 00 03 00 00 03 00 96 a0 01 e0 20 06 61 65 95 9a 49 30 bf fc 0c 7c 0c 81 a8 08 08 08 20 00 00 03 00 20 00 00 03 03 01 + * 处理后:42 01 01 01 60 00 00 00 90 00 00 00 00 00 96 A0 01 E0 20 06 61 65 95 9A 49 30 BF FC 0C 7C 0C 81 A8 08 08 08 20 00 00 00 20 00 00 01 + */ + using (var _reader = new BinaryReader(new MemoryStream(sps))) + { + while (_reader.BaseStream.Position < _reader.BaseStream.Length) + { + encList.Add(_reader.ReadByte()); + if (encList.Count >= 3 && encList[encList.Count - 3] == 0x00 && encList[encList.Count - 2] == 0x00 && encList[encList.Count - 1] == 0x03) + { + encList.RemoveAt(encList.Count - 1); + } + } + } + sps = encList.ToArray(); + + using var reader = new BinaryReader2(new MemoryStream(sps)); + reader.ReadBytes(2); // Skip 2 bytes unit header + var firstByte = reader.ReadByte(); + var maxSubLayersMinus1 = (firstByte & 0xe) >> 1; + var nextByte = reader.ReadByte(); + var generalProfileSpace = (nextByte & 0xc0) >> 6; + var generalTierFlag = (nextByte & 0x20) >> 5; + var generalProfileIdc = nextByte & 0x1f; + var generalProfileCompatibilityFlags = reader.ReadUInt32(); + var constraintBytes = reader.ReadBytes(6); + var generalLevelIdc = reader.ReadByte(); + + /*var skipBit = 0; + for (int i = 0; i < maxSubLayersMinus1; i++) + { + skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag + } + if (maxSubLayersMinus1 > 0) + { + for (int i = maxSubLayersMinus1; i < 8; i++) + { + skipBit += 2; // reserved_zero_2bits + } + } + for (int i = 0; i < maxSubLayersMinus1; i++) + { + skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag + }*/ + + // 生成编码信息 + var codecs = code + + $".{HEVC_GENERAL_PROFILE_SPACE_STRINGS[generalProfileSpace]}{generalProfileIdc}" + + $".{Convert.ToString(generalProfileCompatibilityFlags, 16)}" + + $".{(generalTierFlag == 1 ? 'H' : 'L')}{generalLevelIdc}" + + $".{HexUtil.BytesToHex(constraintBytes.Where(b => b != 0).ToArray())}"; + StreamSpec.Codecs = codecs; + + + /////////////////////// + + + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + // var reserved1 = 0xF; + + writer.WriteByte(1); // configuration version + writer.WriteByte((byte)((generalProfileSpace << 6) + (generalTierFlag == 1 ? 0x20 : 0) | generalProfileIdc)); // general_profile_space + general_tier_flag + general_profile_idc + writer.WriteUInt(generalProfileCompatibilityFlags); // general_profile_compatibility_flags + writer.Write(constraintBytes); // general_constraint_indicator_flags + writer.WriteByte((byte)generalProfileIdc); // general_level_idc + writer.WriteUShort(0xf000); // reserved + min_spatial_segmentation_idc + writer.WriteByte(0xfc); // reserved + parallelismType + writer.WriteByte(0 | 0xfc); // reserved + chromaFormat + writer.WriteByte(0 | 0xf8); // reserved + bitDepthLumaMinus8 + writer.WriteByte(0 | 0xf8); // reserved + bitDepthChromaMinus8 + writer.WriteUShort(0); // avgFrameRate + writer.WriteByte((byte)(0 << 6 | 0 << 3 | 0 << 2 | (NalUnitLengthField - 1))); // constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne + writer.WriteByte(0x03); // numOfArrays (vps sps pps) + + sps = oriSps.ToArray(); + writer.WriteByte(0x20); // array_completeness + reserved + NAL_unit_type + writer.WriteUShort(1); // numNalus + writer.WriteUShort(vps.Length); + writer.Write(vps); + writer.WriteByte(0x21); + writer.WriteUShort(1); // numNalus + writer.WriteUShort(sps.Length); + writer.Write(sps); + writer.WriteByte(0x22); + writer.WriteUShort(1); // numNalus + writer.WriteUShort(pps.Length); + writer.Write(pps); + + return Box("hvcC", stream.ToArray()); // HEVC Decoder Configuration Record + } + + private byte[] GetStsd() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteUInt(1); // entry count + var sampleEntryData = GetSampleEntryBox(); + writer.Write(sampleEntryData); + + return stream.ToArray(); + } + + private byte[] GetMehd() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteULong(Duration); + + return FullBox("mehd", 1, 0, stream.ToArray()); // Movie Extends Header Box + } + private byte[] GetTrex() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + writer.WriteUInt(TrackId); // track id + writer.WriteUInt(1); // default sample description index + writer.WriteUInt(0); // default sample duration + writer.WriteUInt(0); // default sample size + writer.WriteUInt(0); // default sample flags + + return FullBox("trex", 0, 0, stream.ToArray()); // Track Extends Box + } + + private byte[] GenPsshBoxForPlayReady() + { + using var _stream = new MemoryStream(); + using var _writer = new BinaryWriter2(_stream); + var sysIdData = HexUtil.HexToBytes(ProtectionSystemId.Replace("-", "")); + var psshData = HexUtil.HexToBytes(ProtectionData); + + _writer.Write(sysIdData); // SystemID 16 bytes + _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes + _writer.Write(psshData); // Data + var psshBox = FullBox("pssh", 0, 0, _stream.ToArray()); + return psshBox; + } + + private byte[] GenPsshBoxForWideVine() + { + using var _stream = new MemoryStream(); + using var _writer = new BinaryWriter2(_stream); + var sysIdData = HexUtil.HexToBytes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed".Replace("-", "")); + // var kid = HexUtil.HexToBytes(ProtecitonKID); + + _writer.Write(sysIdData); // SystemID 16 bytes + var psshData = HexUtil.HexToBytes($"08011210{ProtecitonKID}1A046E647265220400000000"); + _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes + _writer.Write(psshData); // Data + var psshBox = FullBox("pssh", 0, 0, _stream.ToArray()); + return psshBox; + } + + private byte[] GenMoof() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter2(stream); + + // make senc + writer.WriteUInt(1); // sample_count + writer.Write(new byte[8]); // 8 bytes IV + + var sencBox = FullBox("senc", 1, 0, stream.ToArray()); + + var moofBox = Box("moof", sencBox); // Movie Extends Box + + return moofBox; + } + + public byte[] GenHeader(byte[] firstSegment) + { + new MP4Parser() + .Box("moof", MP4Parser.Children) + .Box("traf", MP4Parser.Children) + .FullBox("tfhd", (box) => + { + TrackId = (int)box.Reader.ReadUInt32(); + }) + .Parse(firstSegment); + + return GenHeader(); + } + + public byte[] GenHeader() + { + using var stream = new MemoryStream(); + + var ftyp = GenFtyp(); // File Type Box + stream.Write(ftyp); + + var moovPayload = GenMvhd(); // Movie Header Box + + var trakPayload = GenTkhd(); // Track Header Box + + var mdhdPayload = GenMdhd(); // Media Header Box + + var hdlrPayload = GenHdlr(); // Handler Reference Box + + var mdiaPayload = mdhdPayload.Concat(hdlrPayload).ToArray(); + + var minfPayload = GenMinf(); + + + var sttsPayload = new byte[] { 0, 0, 0, 0 }; // entry count + var stblPayload = FullBox("stts", 0, 0, sttsPayload); // Decoding Time to Sample Box + + var stscPayload = new byte[] { 0, 0, 0, 0 }; // entry count + var stscBox = FullBox("stsc", 0, 0, stscPayload); // Sample To Chunk Box + + var stcoPayload = new byte[] { 0, 0, 0, 0 }; // entry count + var stcoBox = FullBox("stco", 0, 0, stcoPayload); // Chunk Offset Box + + var stszPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; // sample size, sample count + var stszBox = FullBox("stsz", 0, 0, stszPayload); // Sample Size Box + + var stsdPayload = GetStsd(); + var stsdBox = FullBox("stsd", 0, 0, stsdPayload); // Sample Description Box + + stblPayload = stblPayload.Concat(stscBox).Concat(stcoBox).Concat(stszBox).Concat(stsdBox).ToArray(); + + + var stblBox = Box("stbl", stblPayload); // Sample Table Box + minfPayload = minfPayload.Concat(stblBox).ToArray(); + + var minfBox = Box("minf", minfPayload); // Media Information Box + mdiaPayload = mdiaPayload.Concat(minfBox).ToArray(); + + var mdiaBox = Box("mdia", mdiaPayload); // Media Box + trakPayload = trakPayload.Concat(mdiaBox).ToArray(); + + var trakBox = Box("trak", trakPayload); // Track Box + moovPayload = moovPayload.Concat(trakBox).ToArray(); + + var mvexPayload = GetMehd(); + var trexBox = GetTrex(); + mvexPayload = mvexPayload.Concat(trexBox).ToArray(); + + var mvexBox = Box("mvex", mvexPayload); // Movie Extends Box + moovPayload = moovPayload.Concat(mvexBox).ToArray(); + + if (IsProtection) + { + var psshBox1 = GenPsshBoxForPlayReady(); + var psshBox2 = GenPsshBoxForWideVine(); + moovPayload = moovPayload.Concat(psshBox1).Concat(psshBox2).ToArray(); + } + + var moovBox = Box("moov", moovPayload); // Movie Box + + stream.Write(moovBox); + + // var moofBox = GenMoof(); // Movie Extends Box + // stream.Write(moofBox); + + return stream.ToArray(); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs index 60b0a87..ba1a6ea 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs @@ -6,11 +6,10 @@ 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 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); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs index aa69a94..97ede43 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs @@ -7,30 +7,29 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Parser.Processor.DASH +namespace N_m3u8DL_RE.Parser.Processor.DASH; + +///

+/// XG视频处理 +/// +public class DefaultDASHContentProcessor : ContentProcessor { - /// - /// 西瓜视频处理 - /// - public class DefaultDASHContentProcessor : ContentProcessor + public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig) { - public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig) + if (extractorType != ExtractorType.MPEG_DASH) return false; + + if (mpdContent.Contains(" paserConfig.AppendUrlParams; + + public override string Process(string oriUrl, ParserConfig paserConfig) { - public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams; - - public override string Process(string oriUrl, ParserConfig paserConfig) + if (oriUrl.StartsWith("http")) { - 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); - var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query); - - var oldUri = new Uri(oriUrl); - 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); - } + if (newQuery.AllKeys.Contains(item)) + newQuery.Set(item, uriFromConfigQuery.Get(item)); + else + newQuery.Add(item, uriFromConfigQuery.Get(item)); } - 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; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs index 3e69ee0..2e99e61 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs @@ -8,101 +8,94 @@ using System.Text; using System.Text.RegularExpressions; 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=\\\"(.*?)\\\"")] - 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) + // 处理content以\r作为换行符的情况 + if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n")) { - //处理content以\r作为换行符的情况 - 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; + m3u8Content = m3u8Content.Replace("\r", Environment.NewLine); } + + 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; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs index 556bdcc..9f8826c 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs @@ -6,112 +6,106 @@ using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Util; 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"); - var method = ParserUtil.GetAttribute(keyLine, "METHOD"); - var uri = ParserUtil.GetAttribute(keyLine, "URI"); + encryptInfo.IV = HexUtil.HexToBytes(iv); + } + // 自定义IV + if (parserConfig.CustomeIV is { Length: > 0 }) + { + encryptInfo.IV = parserConfig.CustomeIV; + } - Logger.Debug("METHOD:{},URI:{},IV:{}", method, uri, iv); - - var encryptInfo = new EncryptInfo(method); - - //IV - if (!string.IsNullOrEmpty(iv)) + // KEY + try + { + if (parserConfig.CustomeKey is { Length: > 0 }) { - encryptInfo.IV = HexUtil.HexToBytes(iv); + encryptInfo.Key = parserConfig.CustomeKey; } - //自定义IV - if (parserConfig.CustomeIV != null && parserConfig.CustomeIV.Length > 0) + else if (uri.ToLower().StartsWith("base64:")) { - encryptInfo.IV = parserConfig.CustomeIV; + encryptInfo.Key = Convert.FromBase64String(uri[7..]); } - - //KEY - try + else if (uri.ToLower().StartsWith("data:;base64,")) { - if (parserConfig.CustomeKey != null && parserConfig.CustomeKey.Length > 0) - { - encryptInfo.Key = parserConfig.CustomeKey; - } - else if (uri.ToLower().StartsWith("base64:")) - { - encryptInfo.Key = Convert.FromBase64String(uri[7..]); - } - else if (uri.ToLower().StartsWith("data:;base64,")) - { - encryptInfo.Key = Convert.FromBase64String(uri[13..]); - } - else if (uri.ToLower().StartsWith("data:text/plain;base64,")) - { - 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); + encryptInfo.Key = Convert.FromBase64String(uri[13..]); + } + else if (uri.ToLower().StartsWith("data:text/plain;base64,")) + { + 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: - 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; - } - - /// - /// 预处理URL - /// - private string PreProcessUrl(string url, ParserConfig parserConfig) - { - foreach (var p in parserConfig.UrlProcessors) - { - if (p.CanProcess(ExtractorType.HLS, url, parserConfig)) + try { - 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; } -} + + /// + /// 预处理URL + /// + 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; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs index ec1438d..66dd612 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs @@ -7,11 +7,10 @@ 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 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); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs index 6f3818e..1fa2f62 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs @@ -1,16 +1,10 @@ using N_m3u8DL_RE.Common.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.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); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs index 81215ca..a7ef235 100644 --- a/src/N_m3u8DL-RE.Parser/StreamExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/StreamExtractor.cs @@ -6,144 +6,142 @@ using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Extractor; using N_m3u8DL_RE.Common.Util; 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 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 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("") && rawText.Contains("") && rawText.Contains(" + /// 开始解析流媒体信息 + /// + /// + public async Task> ExtractStreamsAsync() + { + try { - this.parserConfig = parserConfig; + await semaphore.WaitAsync(); + Logger.Info(ResString.parsingStream); + return await extractor.ExtractStreamsAsync(rawText); } - - public async Task LoadSourceFromUrlAsync(string url) + finally { - 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("") && rawText.Contains("") && rawText.Contains(" - /// 开始解析流媒体信息 - /// - /// - public async Task> ExtractStreamsAsync() - { - try - { - await semaphore.WaitAsync(); - Logger.Info(ResString.parsingStream); - return await extractor.ExtractStreamsAsync(rawText); - } - finally - { - semaphore.Release(); - } - } - - /// - /// 根据规格说明填充媒体播放列表信息 - /// - /// - public async Task FetchPlayListAsync(List streamSpecs) - { - try - { - await semaphore.WaitAsync(); - Logger.Info(ResString.parsingStream); - await extractor.FetchPlayListAsync(streamSpecs); - } - finally - { - semaphore.Release(); - } - } - - public async Task RefreshPlayListAsync(List streamSpecs) - { - try - { - await semaphore.WaitAsync(); - await RetryUtil.WebRequestRetryAsync(async () => - { - await extractor.RefreshPlayListAsync(streamSpecs); - return true; - }, retryDelayMilliseconds: 1000, maxRetries: 5); - } - finally - { - semaphore.Release(); - } + semaphore.Release(); } } -} + + /// + /// 根据规格说明填充媒体播放列表信息 + /// + /// + public async Task FetchPlayListAsync(List streamSpecs) + { + try + { + await semaphore.WaitAsync(); + Logger.Info(ResString.parsingStream); + await extractor.FetchPlayListAsync(streamSpecs); + } + finally + { + semaphore.Release(); + } + } + + public async Task RefreshPlayListAsync(List streamSpecs) + { + try + { + await semaphore.WaitAsync(); + await RetryUtil.WebRequestRetryAsync(async () => + { + await extractor.RefreshPlayListAsync(streamSpecs); + return true; + }, retryDelayMilliseconds: 1000, maxRetries: 5); + } + finally + { + semaphore.Release(); + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs b/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs index 0215c77..4186e9b 100644 --- a/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs +++ b/src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs @@ -1,126 +1,120 @@ using N_m3u8DL_RE.Parser.Constants; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace N_m3u8DL_RE.Parser.Util +namespace N_m3u8DL_RE.Parser.Util; + +public partial class ParserUtil { - public partial class ParserUtil + [GeneratedRegex("\\$Number%([^$]+)d\\$")] + private static partial Regex VarsNumberRegex(); + + /// + /// 从以下文本中获取参数 + /// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720" + /// + /// 等待被解析的一行文本 + /// 留空则获取第一个英文冒号后的全部字符 + /// + public static string GetAttribute(string line, string key = "") { - [GeneratedRegex("\\$Number%([^$]+)d\\$")] - private static partial Regex VarsNumberRegex(); + line = line.Trim(); + if (key == "") + return line[(line.IndexOf(':') + 1)..]; - /// - /// 从以下文本中获取参数 - /// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720" - /// - /// 等待被解析的一行文本 - /// 留空则获取第一个英文冒号后的全部字符 - /// - public static string GetAttribute(string line, string key = "") + var index = -1; + var result = string.Empty; + if ((index = line.IndexOf(key + "=\"", StringComparison.Ordinal)) > -1) { - line = line.Trim(); - if (key == "") - return line[(line.IndexOf(':') + 1)..]; - - var index = -1; - var result = string.Empty; - if ((index = line.IndexOf(key + "=\"")) > -1) - { - var startIndex = index + (key + "=\"").Length; - var endIndex = startIndex + line[startIndex..].IndexOf('\"'); - 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; + var startIndex = index + (key + "=\"").Length; + var endIndex = startIndex + line[startIndex..].IndexOf('\"'); + result = line[startIndex..endIndex]; + } + else if ((index = line.IndexOf(key + "=", StringComparison.Ordinal)) > -1) + { + var startIndex = index + (key + "=").Length; + var endIndex = startIndex + line[startIndex..].IndexOf(','); + if (endIndex >= startIndex) result = line[startIndex..endIndex]; + else result = line[startIndex..]; } - /// - /// 从如下文本中提取 - /// [@] - /// - /// - /// n(length) o(start) - public static (long, long?) GetRange(string input) - { - var t = input.Split('@'); - if (t.Length > 0) - { - if (t.Length == 1) - { - return (Convert.ToInt64(t[0]), null); - } - if (t.Length == 2) - { - return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1])); - } - } - return (0, null); - } - - /// - /// 从100-300这种字符串中获取StartRange, ExpectLength信息 - /// - /// - /// StartRange, ExpectLength - 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); - } - - /// - /// MPD SegmentTemplate替换 - /// - /// - /// - /// - public static string ReplaceVars(string text, Dictionary 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)) - { - text = text.Replace(m.Value, keyValuePairs[DASHTags.TemplateNumber]?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0')); - } - } - - return text; - } - - /// - /// 拼接Baseurl和RelativeUrl - /// - /// Baseurl - /// RelativeUrl - /// - public static string CombineURL(string baseurl, string url) - { - if (string.IsNullOrEmpty(baseurl)) - return url; - - Uri uri1 = new Uri(baseurl); //这里直接传完整的URL即可 - Uri uri2 = new Uri(uri1, url); - url = uri2.ToString(); - - return url; - } + return result; } -} + + /// + /// 从如下文本中提取 + /// [@] + /// + /// + /// n(length) o(start) + public static (long, long?) GetRange(string input) + { + var t = input.Split('@'); + if (t.Length > 0) + { + if (t.Length == 1) + { + return (Convert.ToInt64(t[0]), null); + } + if (t.Length == 2) + { + return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1])); + } + } + return (0, null); + } + + /// + /// 从100-300这种字符串中获取StartRange, ExpectLength信息 + /// + /// + /// StartRange, ExpectLength + 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); + } + + /// + /// MPD SegmentTemplate替换 + /// + /// + /// + /// + public static string ReplaceVars(string text, Dictionary 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')); + } + } + + return text; + } + + /// + /// 拼接Baseurl和RelativeUrl + /// + /// Baseurl + /// RelativeUrl + /// + public static string CombineURL(string baseurl, string url) + { + if (string.IsNullOrEmpty(baseurl)) + return url; + + Uri uri1 = new Uri(baseurl); // 这里直接传完整的URL即可 + Uri uri2 = new Uri(uri1, url); + url = uri2.ToString(); + + return url; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs b/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs index cc0f522..565eef6 100644 --- a/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs +++ b/src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs @@ -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.Rendering; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using N_m3u8DL_RE.Common.Util; -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 DateTimeStringDic = new(); + protected override bool NoWrap => true; + private ConcurrentDictionary SpeedContainerDic { get; set; } + + public DownloadSpeedColumn(ConcurrentDictionary SpeedContainerDic) { - private long _stopSpeed = 0; - private ConcurrentDictionary DateTimeStringDic = new(); - protected override bool NoWrap => true; - private ConcurrentDictionary SpeedContainerDic { get; set; } - - public DownloadSpeedColumn(ConcurrentDictionary SpeedContainerDic) - { - this.SpeedContainerDic = 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) - { - 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) - }; - } + this.SpeedContainerDic = 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) + { + 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(GlobalUtil.FormatFileSize(speedContainer.NowSpeed) + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered(); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs b/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs index 60edcc4..cbedb9d 100644 --- a/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs +++ b/src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs @@ -2,48 +2,42 @@ using N_m3u8DL_RE.Entity; using Spectre.Console; using Spectre.Console.Rendering; -using System; 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 SpeedContainerDic { get; set; } + private ConcurrentDictionary DateTimeStringDic = new(); + private ConcurrentDictionary 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 speedContainerDic) { - private ConcurrentDictionary SpeedContainerDic { get; set; } - private ConcurrentDictionary DateTimeStringDic = new(); - private ConcurrentDictionary 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 speedContainerDic) - { - this.SpeedContainerDic = 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) - { - 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(); - } + this.SpeedContainerDic = 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) + { + 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(); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs b/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs index 849821b..2c74aae 100644 --- a/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs +++ b/src/N_m3u8DL-RE/Column/MyPercentageColumn.cs @@ -1,31 +1,25 @@ using Spectre.Console.Rendering; 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 + /// + /// Gets or sets the style for a non-complete task. + /// + public Style Style { get; set; } = Style.Plain; + + /// + /// Gets or sets the style for a completed task. + /// + public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green); + + /// + public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) { - /// - /// Gets or sets the style for a non-complete task. - /// - public Style Style { get; set; } = Style.Plain; - - /// - /// Gets or sets the style for a completed task. - /// - public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green); - - /// - 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(); - } + var percentage = task.Percentage; + var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain; + return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified(); } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs b/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs index 461e834..4bb3190 100644 --- a/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs +++ b/src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs @@ -1,39 +1,33 @@ using N_m3u8DL_RE.Common.Util; using Spectre.Console; using Spectre.Console.Rendering; -using System; 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 _recodingDurDic; + private ConcurrentDictionary? _refreshedDurDic; + public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey); + public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen); + public RecordingDurationColumn(ConcurrentDictionary recodingDurDic) { - protected override bool NoWrap => true; - private ConcurrentDictionary _recodingDurDic; - private ConcurrentDictionary? _refreshedDurDic; - public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey); - public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen); - public RecordingDurationColumn(ConcurrentDictionary recodingDurDic) - { - _recodingDurDic = recodingDurDic; - } - public RecordingDurationColumn(ConcurrentDictionary recodingDurDic, ConcurrentDictionary 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); - } + _recodingDurDic = recodingDurDic; + } + public RecordingDurationColumn(ConcurrentDictionary recodingDurDic, ConcurrentDictionary 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); } } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs b/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs index 21b76f4..9d56934 100644 --- a/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs +++ b/src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs @@ -1,39 +1,32 @@ using N_m3u8DL_RE.Common.Util; -using N_m3u8DL_RE.Entity; using Spectre.Console; using Spectre.Console.Rendering; -using System; 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 RecodingSizeDic = new(); // 临时的大小 每秒刷新用 + private ConcurrentDictionary _recodingSizeDic; + private ConcurrentDictionary DateTimeStringDic = new(); + public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan); + public RecordingSizeColumn(ConcurrentDictionary recodingSizeDic) { - protected override bool NoWrap => true; - private ConcurrentDictionary RecodingSizeDic = new(); //临时的大小 每秒刷新用 - private ConcurrentDictionary _recodingSizeDic; - private ConcurrentDictionary DateTimeStringDic = new(); - public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan); - public RecordingSizeColumn(ConcurrentDictionary recodingSizeDic) - { - _recodingSizeDic = recodingSizeDic; - } - 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(); - } + _recodingSizeDic = recodingSizeDic; } -} + 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(); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs b/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs index aa0a057..2925d22 100644 --- a/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs +++ b/src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs @@ -1,23 +1,17 @@ using Spectre.Console; 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; - 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) - { - if (task.IsFinished) - return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified(); - return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified(); - } + if (task.IsFinished) + return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified(); + return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified(); } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index fbb78b8..23915ef 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -5,7 +5,6 @@ using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Enum; using N_m3u8DL_RE.Util; -using NiL.JS.Expressions; using System.CommandLine; using System.CommandLine.Binding; using System.CommandLine.Builder; @@ -14,630 +13,629 @@ using System.Globalization; using System.Net; using System.Text.RegularExpressions; -namespace N_m3u8DL_RE.CommandLine +namespace N_m3u8DL_RE.CommandLine; + +internal partial class CommandInvoker { - internal partial class CommandInvoker + public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20241110"; + + [GeneratedRegex("((best|worst)\\d*|all)")] + private static partial Regex ForStrRegex(); + [GeneratedRegex("(\\d*)-(\\d*)")] + private static partial Regex RangeRegex(); + + private static readonly Argument Input = new(name: "input", description: ResString.cmd_Input); + private static readonly Option TmpDir = new(["--tmp-dir"], description: ResString.cmd_tmpDir); + private static readonly Option SaveDir = new(["--save-dir"], description: ResString.cmd_saveDir); + private static readonly Option SaveName = new(["--save-name"], description: ResString.cmd_saveName, parseArgument: ParseSaveName); + private static readonly Option SavePattern = new(["--save-pattern"], description: ResString.cmd_savePattern, getDefaultValue: () => "____"); + private static readonly Option UILanguage = new Option(["--ui-language"], description: ResString.cmd_uiLanguage).FromAmong("en-US", "zh-CN", "zh-TW"); + private static readonly Option UrlProcessorArgs = new(["--urlprocessor-args"], description: ResString.cmd_urlProcessorArgs); + private static readonly Option Keys = new(["--key"], description: ResString.cmd_keys) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false }; + private static readonly Option KeyTextFile = new(["--key-text-file"], description: ResString.cmd_keyText); + private static readonly Option> Headers = new(["-H", "--header"], description: ResString.cmd_header, parseArgument: ParseHeaders) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false }; + private static readonly Option LogLevel = new(name: "--log-level", description: ResString.cmd_logLevel, getDefaultValue: () => Common.Log.LogLevel.INFO); + private static readonly Option SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.SRT); + private static readonly Option AutoSelect = new(["--auto-select"], description: ResString.cmd_autoSelect, getDefaultValue: () => false); + private static readonly Option SubOnly = new(["--sub-only"], description: ResString.cmd_subOnly, getDefaultValue: () => false); + private static readonly Option ThreadCount = new(["--thread-count"], description: ResString.cmd_threadCount, getDefaultValue: () => Environment.ProcessorCount) { ArgumentHelpName = "number" }; + private static readonly Option DownloadRetryCount = new(["--download-retry-count"], description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3) { ArgumentHelpName = "number" }; + private static readonly Option SkipMerge = new(["--skip-merge"], description: ResString.cmd_skipMerge, getDefaultValue: () => false); + private static readonly Option SkipDownload = new(["--skip-download"], description: ResString.cmd_skipDownload, getDefaultValue: () => false); + private static readonly Option NoDateInfo = new(["--no-date-info"], description: ResString.cmd_noDateInfo, getDefaultValue: () => false); + private static readonly Option BinaryMerge = new(["--binary-merge"], description: ResString.cmd_binaryMerge, getDefaultValue: () => false); + private static readonly Option UseFFmpegConcatDemuxer = new(["--use-ffmpeg-concat-demuxer"], description: ResString.cmd_useFFmpegConcatDemuxer, getDefaultValue: () => false); + private static readonly Option DelAfterDone = new(["--del-after-done"], description: ResString.cmd_delAfterDone, getDefaultValue: () => true); + private static readonly Option AutoSubtitleFix = new(["--auto-subtitle-fix"], description: ResString.cmd_subtitleFix, getDefaultValue: () => true); + private static readonly Option CheckSegmentsCount = new(["--check-segments-count"], description: ResString.cmd_checkSegmentsCount, getDefaultValue: () => true); + private static readonly Option WriteMetaJson = new(["--write-meta-json"], description: ResString.cmd_writeMetaJson, getDefaultValue: () => true); + private static readonly Option AppendUrlParams = new(["--append-url-params"], description: ResString.cmd_appendUrlParams, getDefaultValue: () => false); + private static readonly Option MP4RealTimeDecryption = new (["--mp4-real-time-decryption"], description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); + private static readonly Option UseShakaPackager = new (["--use-shaka-packager"], description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); + private static readonly Option ForceAnsiConsole = new(["--force-ansi-console"], description: ResString.cmd_forceAnsiConsole); + private static readonly Option NoAnsiColor = new(["--no-ansi-color"], description: ResString.cmd_noAnsiColor); + private static readonly Option DecryptionBinaryPath = new(["--decryption-binary-path"], description: ResString.cmd_decryptionBinaryPath) { ArgumentHelpName = "PATH" }; + private static readonly Option FFmpegBinaryPath = new(["--ffmpeg-binary-path"], description: ResString.cmd_ffmpegBinaryPath) { ArgumentHelpName = "PATH" }; + private static readonly Option BaseUrl = new(["--base-url"], description: ResString.cmd_baseUrl); + private static readonly Option ConcurrentDownload = new(["-mt", "--concurrent-download"], description: ResString.cmd_concurrentDownload, getDefaultValue: () => false); + private static readonly Option NoLog = new(["--no-log"], description: ResString.cmd_noLog, getDefaultValue: () => false); + private static readonly Option AdKeywords = new(["--ad-keyword"], description: ResString.cmd_adKeyword) { ArgumentHelpName = "REG" }; + private static readonly Option MaxSpeed = new(["-R", "--max-speed"], description: ResString.cmd_maxSpeed, parseArgument: ParseSpeedLimit) { ArgumentHelpName = "SPEED" }; + + + // 代理选项 + private static readonly Option UseSystemProxy = new(["--use-system-proxy"], description: ResString.cmd_useSystemProxy, getDefaultValue: () => true); + private static readonly Option CustomProxy = new(["--custom-proxy"], description: ResString.cmd_customProxy, parseArgument: ParseProxy) { ArgumentHelpName = "URL" }; + + // 只下载部分分片 + private static readonly Option CustomRange = new(["--custom-range"], description: ResString.cmd_customRange, parseArgument: ParseCustomRange) { ArgumentHelpName = "RANGE" }; + + + // morehelp + private static readonly Option MoreHelp = new(["--morehelp"], description: ResString.cmd_moreHelp) { ArgumentHelpName = "OPTION" }; + + // 自定义KEY等 + private static readonly Option CustomHLSMethod = new(name: "--custom-hls-method", description: ResString.cmd_customHLSMethod) { ArgumentHelpName = "METHOD" }; + private static readonly Option CustomHLSKey = new(name: "--custom-hls-key", description: ResString.cmd_customHLSKey, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" }; + private static readonly Option CustomHLSIv = new(name: "--custom-hls-iv", description: ResString.cmd_customHLSIv, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" }; + + // 任务开始时间 + private static readonly Option TaskStartAt = new(["--task-start-at"], description: ResString.cmd_taskStartAt, parseArgument: ParseStartTime) { ArgumentHelpName = "yyyyMMddHHmmss" }; + + + // 直播相关 + private static readonly Option LivePerformAsVod = new(["--live-perform-as-vod"], description: ResString.cmd_livePerformAsVod, getDefaultValue: () => false); + private static readonly Option LiveRealTimeMerge = new(["--live-real-time-merge"], description: ResString.cmd_liveRealTimeMerge, getDefaultValue: () => false); + private static readonly Option LiveKeepSegments = new(["--live-keep-segments"], description: ResString.cmd_liveKeepSegments, getDefaultValue: () => true); + private static readonly Option LivePipeMux = new(["--live-pipe-mux"], description: ResString.cmd_livePipeMux, getDefaultValue: () => false); + private static readonly Option LiveRecordLimit = new(["--live-record-limit"], description: ResString.cmd_liveRecordLimit, parseArgument: ParseLiveLimit) { ArgumentHelpName = "HH:mm:ss" }; + private static readonly Option LiveWaitTime = new(["--live-wait-time"], description: ResString.cmd_liveWaitTime) { ArgumentHelpName = "SEC" }; + private static readonly Option LiveTakeCount = new(["--live-take-count"], description: ResString.cmd_liveTakeCount, getDefaultValue: () => 16) { ArgumentHelpName = "NUM" }; + private static readonly Option LiveFixVttByAudio = new(["--live-fix-vtt-by-audio"], description: ResString.cmd_liveFixVttByAudio, getDefaultValue: () => false); + + + // 复杂命令行如下 + private static readonly Option MuxAfterDone = new(["-M", "--mux-after-done"], description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" }; + private static readonly Option> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" }; + private static readonly Option VideoFilter = new(["-sv", "--select-video"], description: ResString.cmd_selectVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + private static readonly Option AudioFilter = new(["-sa", "--select-audio"], description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + private static readonly Option SubtitleFilter = new(["-ss", "--select-subtitle"], description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + + private static readonly Option DropVideoFilter = new(["-dv", "--drop-video"], description: ResString.cmd_dropVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + private static readonly Option DropAudioFilter = new(["-da", "--drop-audio"], description: ResString.cmd_dropAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + private static readonly Option DropSubtitleFilter = new(["-ds", "--drop-subtitle"], description: ResString.cmd_dropSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; + + /// + /// 解析录制直播时长限制 + /// + /// + /// + private static long? ParseSpeedLimit(ArgumentResult result) { - public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20241020"; - - [GeneratedRegex("((best|worst)\\d*|all)")] - private static partial Regex ForStrRegex(); - [GeneratedRegex("(\\d*)-(\\d*)")] - private static partial Regex RangeRegex(); - - private readonly static Argument Input = new(name: "input", description: ResString.cmd_Input); - private readonly static Option TmpDir = new(new string[] { "--tmp-dir" }, description: ResString.cmd_tmpDir); - private readonly static Option SaveDir = new(new string[] { "--save-dir" }, description: ResString.cmd_saveDir); - private readonly static Option SaveName = new(new string[] { "--save-name" }, description: ResString.cmd_saveName, parseArgument: ParseSaveName); - private readonly static Option SavePattern = new(new string[] { "--save-pattern" }, description: ResString.cmd_savePattern, getDefaultValue: () => "____"); - private readonly static Option UILanguage = new Option(new string[] { "--ui-language" }, description: ResString.cmd_uiLanguage).FromAmong("en-US", "zh-CN", "zh-TW"); - private readonly static Option UrlProcessorArgs = new(new string[] { "--urlprocessor-args" }, description: ResString.cmd_urlProcessorArgs); - private readonly static Option Keys = new(new string[] { "--key" }, description: ResString.cmd_keys) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false }; - private readonly static Option KeyTextFile = new(new string[] { "--key-text-file" }, description: ResString.cmd_keyText); - private readonly static Option> Headers = new(new string[] { "-H", "--header" }, description: ResString.cmd_header, parseArgument: ParseHeaders) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false }; - private readonly static Option LogLevel = new(name: "--log-level", description: ResString.cmd_logLevel, getDefaultValue: () => Common.Log.LogLevel.INFO); - private readonly static Option SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.SRT); - private readonly static Option AutoSelect = new(new string[] { "--auto-select" }, description: ResString.cmd_autoSelect, getDefaultValue: () => false); - private readonly static Option SubOnly = new(new string[] { "--sub-only" }, description: ResString.cmd_subOnly, getDefaultValue: () => false); - private readonly static Option ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => Environment.ProcessorCount) { ArgumentHelpName = "number" }; - private readonly static Option DownloadRetryCount = new(new string[] { "--download-retry-count" }, description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3) { ArgumentHelpName = "number" }; - private readonly static Option SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false); - private readonly static Option SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false); - private readonly static Option NoDateInfo = new(new string[] { "--no-date-info" }, description: ResString.cmd_noDateInfo, getDefaultValue: () => false); - private readonly static Option BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => false); - private readonly static Option UseFFmpegConcatDemuxer = new(new string[] { "--use-ffmpeg-concat-demuxer" }, description: ResString.cmd_useFFmpegConcatDemuxer, getDefaultValue: () => false); - private readonly static Option DelAfterDone = new(new string[] { "--del-after-done" }, description: ResString.cmd_delAfterDone, getDefaultValue: () => true); - private readonly static Option AutoSubtitleFix = new(new string[] { "--auto-subtitle-fix" }, description: ResString.cmd_subtitleFix, getDefaultValue: () => true); - private readonly static Option CheckSegmentsCount = new(new string[] { "--check-segments-count" }, description: ResString.cmd_checkSegmentsCount, getDefaultValue: () => true); - private readonly static Option WriteMetaJson = new(new string[] { "--write-meta-json" }, description: ResString.cmd_writeMetaJson, getDefaultValue: () => true); - private readonly static Option AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, getDefaultValue: () => false); - private readonly static Option MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); - private readonly static Option UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); - private readonly static Option ForceAnsiConsole = new(new string[] { "--force-ansi-console" }, description: ResString.cmd_forceAnsiConsole); - private readonly static Option NoAnsiColor = new(new string[] { "--no-ansi-color" }, description: ResString.cmd_noAnsiColor); - private readonly static Option DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath) { ArgumentHelpName = "PATH" }; - private readonly static Option FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath) { ArgumentHelpName = "PATH" }; - private readonly static Option BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl); - private readonly static Option ConcurrentDownload = new(new string[] { "-mt", "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false); - private readonly static Option NoLog = new(new string[] { "--no-log" }, description: ResString.cmd_noLog, getDefaultValue: () => false); - private readonly static Option AdKeywords = new(new string[] { "--ad-keyword" }, description: ResString.cmd_adKeyword) { ArgumentHelpName = "REG" }; - private readonly static Option MaxSpeed = new(new string[] { "-R", "--max-speed" }, description: ResString.cmd_maxSpeed, parseArgument: ParseSpeedLimit) { ArgumentHelpName = "SPEED" }; - - - //代理选项 - private readonly static Option UseSystemProxy = new(new string[] { "--use-system-proxy" }, description: ResString.cmd_useSystemProxy, getDefaultValue: () => true); - private readonly static Option CustomProxy = new(new string[] { "--custom-proxy" }, description: ResString.cmd_customProxy, parseArgument: ParseProxy) { ArgumentHelpName = "URL" }; - - //只下载部分分片 - private readonly static Option CustomRange = new(new string[] { "--custom-range" }, description: ResString.cmd_customRange, parseArgument: ParseCustomRange) { ArgumentHelpName = "RANGE" }; - - - //morehelp - private readonly static Option MoreHelp = new(new string[] { "--morehelp" }, description: ResString.cmd_moreHelp) { ArgumentHelpName = "OPTION" }; - - //自定义KEY等 - private readonly static Option CustomHLSMethod = new(name: "--custom-hls-method", description: ResString.cmd_customHLSMethod) { ArgumentHelpName = "METHOD" }; - private readonly static Option CustomHLSKey = new(name: "--custom-hls-key", description: ResString.cmd_customHLSKey, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" }; - private readonly static Option CustomHLSIv = new(name: "--custom-hls-iv", description: ResString.cmd_customHLSIv, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" }; - - //任务开始时间 - private readonly static Option TaskStartAt = new(new string[] { "--task-start-at" }, description: ResString.cmd_taskStartAt, parseArgument: ParseStartTime) { ArgumentHelpName = "yyyyMMddHHmmss" }; - - - //直播相关 - private readonly static Option LivePerformAsVod = new(new string[] { "--live-perform-as-vod" }, description: ResString.cmd_livePerformAsVod, getDefaultValue: () => false); - private readonly static Option LiveRealTimeMerge = new(new string[] { "--live-real-time-merge" }, description: ResString.cmd_liveRealTimeMerge, getDefaultValue: () => false); - private readonly static Option LiveKeepSegments = new(new string[] { "--live-keep-segments" }, description: ResString.cmd_liveKeepSegments, getDefaultValue: () => true); - private readonly static Option LivePipeMux = new(new string[] { "--live-pipe-mux" }, description: ResString.cmd_livePipeMux, getDefaultValue: () => false); - private readonly static Option LiveRecordLimit = new(new string[] { "--live-record-limit" }, description: ResString.cmd_liveRecordLimit, parseArgument: ParseLiveLimit) { ArgumentHelpName = "HH:mm:ss" }; - private readonly static Option LiveWaitTime = new(new string[] { "--live-wait-time" }, description: ResString.cmd_liveWaitTime) { ArgumentHelpName = "SEC" }; - private readonly static Option LiveTakeCount = new(new string[] { "--live-take-count" }, description: ResString.cmd_liveTakeCount, getDefaultValue: () => 16) { ArgumentHelpName = "NUM" }; - private readonly static Option LiveFixVttByAudio = new(new string[] { "--live-fix-vtt-by-audio" }, description: ResString.cmd_liveFixVttByAudio, getDefaultValue: () => false); - - - //复杂命令行如下 - private readonly static Option MuxAfterDone = new(new string[] { "-M", "--mux-after-done" }, description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" }; - private readonly static Option> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" }; - private readonly static Option VideoFilter = new(new string[] { "-sv", "--select-video" }, description: ResString.cmd_selectVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; - private readonly static Option AudioFilter = new(new string[] { "-sa", "--select-audio" }, description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; - private readonly static Option SubtitleFilter = new(new string[] { "-ss", "--select-subtitle" }, description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; - - private readonly static Option DropVideoFilter = new(new string[] { "-dv", "--drop-video" }, description: ResString.cmd_dropVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; - private readonly static Option DropAudioFilter = new(new string[] { "-da", "--drop-audio" }, description: ResString.cmd_dropAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; - private readonly static Option DropSubtitleFilter = new(new string[] { "-ds", "--drop-subtitle" }, description: ResString.cmd_dropSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; - - /// - /// 解析录制直播时长限制 - /// - /// - /// - private static long? ParseSpeedLimit(ArgumentResult result) + var input = result.Tokens.First().Value.ToUpper(); + try { - var input = result.Tokens.First().Value.ToUpper(); - try - { - var reg = new Regex("([\\d\\\\.]+)(M|K)"); - if (!reg.IsMatch(input)) throw new ArgumentException(); + var reg = new Regex("([\\d\\\\.]+)(M|K)"); + if (!reg.IsMatch(input)) throw new ArgumentException(); - var number = double.Parse(reg.Match(input).Groups[1].Value); - if (reg.Match(input).Groups[2].Value == "M") - return (long)(number * 1024 * 1024); - else - return (long)(number * 1024); - } - catch (Exception) - { - result.ErrorMessage = "error in parse SpeedLimit: " + input; - return null; - } - } - - /// - /// 解析用户定义的下载范围 - /// - /// - /// - /// - private static CustomRange? ParseCustomRange(ArgumentResult result) - { - var input = result.Tokens.First().Value; - //支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00; - try - { - if (string.IsNullOrEmpty(input)) - return null; - - var arr = input.Split('-'); - if (arr.Length != 2) - throw new ArgumentException("Bad format!"); - - if (input.Contains(":")) - { - return new CustomRange() - { - InputStr = input, - StartSec = arr[0] == "" ? 0 : OtherUtil.ParseDur(arr[0]).TotalSeconds, - EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds, - }; - } - else if (RangeRegex().IsMatch(input)) - { - var left = RangeRegex().Match(input).Groups[1].Value; - var right = RangeRegex().Match(input).Groups[2].Value; - return new CustomRange() - { - InputStr = input, - StartSegIndex = left == "" ? 0 : long.Parse(left), - EndSegIndex = right == "" ? long.MaxValue : long.Parse(right), - }; - } - - throw new ArgumentException("Bad format!"); - } - catch (Exception ex) - { - result.ErrorMessage = $"error in parse CustomRange: " + ex.Message; - return null; - } - } - - /// - /// 解析用户代理 - /// - /// - /// - /// - private static WebProxy? ParseProxy(ArgumentResult result) - { - var input = result.Tokens.First().Value; - try - { - if (string.IsNullOrEmpty(input)) - return null; - - var uri = new Uri(input); - var proxy = new WebProxy(uri, true); - if (!string.IsNullOrEmpty(uri.UserInfo)) - { - var infos = uri.UserInfo.Split(':'); - proxy.Credentials = new NetworkCredential(infos.First(), infos.Last()); - } - return proxy; - } - catch (Exception ex) - { - result.ErrorMessage = $"error in parse proxy: " + ex.Message; - return null; - } - } - - /// - /// 解析自定义KEY - /// - /// - /// - private static byte[]? ParseHLSCustomKey(ArgumentResult result) - { - var input = result.Tokens.First().Value; - try - { - if (string.IsNullOrEmpty(input)) - return null; - if (File.Exists(input)) - return File.ReadAllBytes(input); - else if (HexUtil.TryParseHexString(input, out byte[]? bytes)) - return bytes; - else - return Convert.FromBase64String(input); - } - catch (Exception) - { - result.ErrorMessage = "error in parse hls custom key: " + input; - return null; - } - } - - /// - /// 解析录制直播时长限制 - /// - /// - /// - private static TimeSpan? ParseLiveLimit(ArgumentResult result) - { - var input = result.Tokens.First().Value; - try - { - return OtherUtil.ParseDur(input); - } - catch (Exception) - { - result.ErrorMessage = "error in parse LiveRecordLimit: " + input; - return null; - } - } - - /// - /// 解析任务开始时间 - /// - /// - /// - private static DateTime? ParseStartTime(ArgumentResult result) - { - var input = result.Tokens.First().Value; - try - { - CultureInfo provider = CultureInfo.InvariantCulture; - return DateTime.ParseExact(input, "yyyyMMddHHmmss", provider); - } - catch (Exception) - { - result.ErrorMessage = "error in parse TaskStartTime: " + input; - return null; - } - } - - private static string? ParseSaveName(ArgumentResult result) - { - var input = result.Tokens.First().Value; - var newName = OtherUtil.GetValidFileName(input); - if (string.IsNullOrEmpty(newName)) - { - result.ErrorMessage = "Invalid save name!"; - return null; - } - return newName; - } - - /// - /// 流过滤器 - /// - /// - /// - private static StreamFilter? ParseStreamFilter(ArgumentResult result) - { - var streamFilter = new StreamFilter(); - var input = result.Tokens.First().Value; - var p = new ComplexParamParser(input); - - - //目标范围 - var forStr = ""; - if (input == ForStrRegex().Match(input).Value) - { - forStr = input; - } + var number = double.Parse(reg.Match(input).Groups[1].Value); + if (reg.Match(input).Groups[2].Value == "M") + return (long)(number * 1024 * 1024); else - { - forStr = p.GetValue("for") ?? "best"; - if (forStr != ForStrRegex().Match(forStr).Value) - { - result.ErrorMessage = $"for={forStr} not valid"; - return null; - } - } - streamFilter.For = forStr; - - var id = p.GetValue("id"); - if (!string.IsNullOrEmpty(id)) - streamFilter.GroupIdReg = new Regex(id); - - var lang = p.GetValue("lang"); - if (!string.IsNullOrEmpty(lang)) - streamFilter.LanguageReg = new Regex(lang); - - var name = p.GetValue("name"); - if (!string.IsNullOrEmpty(name)) - streamFilter.NameReg = new Regex(name); - - var codecs = p.GetValue("codecs"); - if (!string.IsNullOrEmpty(codecs)) - streamFilter.CodecsReg = new Regex(codecs); - - var res = p.GetValue("res"); - if (!string.IsNullOrEmpty(res)) - streamFilter.ResolutionReg = new Regex(res); - - var frame = p.GetValue("frame"); - if (!string.IsNullOrEmpty(frame)) - streamFilter.FrameRateReg = new Regex(frame); - - var channel = p.GetValue("channel"); - if (!string.IsNullOrEmpty(channel)) - streamFilter.ChannelsReg = new Regex(channel); - - var range = p.GetValue("range"); - if (!string.IsNullOrEmpty(range)) - streamFilter.VideoRangeReg = new Regex(range); - - var url = p.GetValue("url"); - if (!string.IsNullOrEmpty(url)) - streamFilter.UrlReg = new Regex(url); - - var segsMin = p.GetValue("segsMin"); - if (!string.IsNullOrEmpty(segsMin)) - streamFilter.SegmentsMinCount = long.Parse(segsMin); - - var segsMax = p.GetValue("segsMax"); - if (!string.IsNullOrEmpty(segsMax)) - streamFilter.SegmentsMaxCount = long.Parse(segsMax); - - var plistDurMin = p.GetValue("plistDurMin"); - if (!string.IsNullOrEmpty(plistDurMin)) - streamFilter.PlaylistMinDur = OtherUtil.ParseSeconds(plistDurMin); - - var plistDurMax = p.GetValue("plistDurMax"); - if (!string.IsNullOrEmpty(plistDurMax)) - streamFilter.PlaylistMaxDur = OtherUtil.ParseSeconds(plistDurMax); - - var bwMin = p.GetValue("bwMin"); - if (!string.IsNullOrEmpty(bwMin)) - streamFilter.BandwidthMin = int.Parse(bwMin) * 1000; - - var bwMax = p.GetValue("bwMax"); - if (!string.IsNullOrEmpty(bwMax)) - streamFilter.BandwidthMax = int.Parse(bwMax) * 1000; - - var role = p.GetValue("role"); - if (System.Enum.TryParse(role, true, out RoleType roleType)) - streamFilter.Role = roleType; - - return streamFilter; + return (long)(number * 1024); } - - /// - /// 分割Header - /// - /// - /// - private static Dictionary ParseHeaders(ArgumentResult result) + catch (Exception) { - var array = result.Tokens.Select(t => t.Value).ToArray(); - return OtherUtil.SplitHeaderArrayToDic(array); - } - - /// - /// 解析混流引入的外部文件 - /// - /// - /// - private static List ParseImports(ArgumentResult result) - { - var imports = new List(); - - foreach (var item in result.Tokens) - { - var p = new ComplexParamParser(item.Value); - var path = p.GetValue("path") ?? item.Value; //若未获取到,直接整个字符串作为path - var lang = p.GetValue("lang"); - var name = p.GetValue("name"); - if (string.IsNullOrEmpty(path) || !File.Exists(path)) - { - result.ErrorMessage = "path empty or file not exists!"; - return imports; - } - imports.Add(new OutputFile() - { - Index = 999, - FilePath = path, - LangCode = lang, - Description = name - }); - } - - return imports; - } - - /// - /// 解析混流选项 - /// - /// - /// - private static MuxOptions? ParseMuxAfterDone(ArgumentResult result) - { - var v = result.Tokens.First().Value; - var p = new ComplexParamParser(v); - //混流格式 - var format = p.GetValue("format") ?? v.Split(':')[0]; //若未获取到,直接:前的字符串作为format解析 - var parseResult = System.Enum.TryParse(format.ToUpperInvariant(), out MuxFormat muxFormat); - if (!parseResult) - { - result.ErrorMessage = $"format={format} not valid"; - return null; - } - //混流器 - var muxer = p.GetValue("muxer") ?? "ffmpeg"; - if (muxer != "ffmpeg" && muxer != "mkvmerge") - { - result.ErrorMessage = $"muxer={muxer} not valid"; - return null; - } - //混流器路径 - var bin_path = p.GetValue("bin_path") ?? "auto"; - if (string.IsNullOrEmpty(bin_path)) - { - result.ErrorMessage = $"bin_path={bin_path} not valid"; - return null; - } - //是否删除 - var keep = p.GetValue("keep") ?? "false"; - if (keep != "true" && keep != "false") - { - result.ErrorMessage = $"keep={keep} not valid"; - return null; - } - //是否忽略字幕 - var skipSub = p.GetValue("skip_sub") ?? "false"; - if (skipSub != "true" && skipSub != "false") - { - result.ErrorMessage = $"skip_sub={keep} not valid"; - return null; - } - //冲突检测 - if (muxer == "mkvmerge" && format == "mp4") - { - result.ErrorMessage = $"mkvmerge can not do mp4"; - return null; - } - return new MuxOptions() - { - UseMkvmerge = muxer == "mkvmerge", - MuxFormat = muxFormat, - KeepFiles = keep == "true", - SkipSubtitle = skipSub == "true", - BinPath = bin_path == "auto" ? null : bin_path - }; - } - - class MyOptionBinder : BinderBase - { - protected override MyOption GetBoundValue(BindingContext bindingContext) - { - var option = new MyOption - { - Input = bindingContext.ParseResult.GetValueForArgument(Input), - ForceAnsiConsole = bindingContext.ParseResult.GetValueForOption(ForceAnsiConsole), - NoAnsiColor = bindingContext.ParseResult.GetValueForOption(NoAnsiColor), - LogLevel = bindingContext.ParseResult.GetValueForOption(LogLevel), - AutoSelect = bindingContext.ParseResult.GetValueForOption(AutoSelect), - SkipMerge = bindingContext.ParseResult.GetValueForOption(SkipMerge), - BinaryMerge = bindingContext.ParseResult.GetValueForOption(BinaryMerge), - UseFFmpegConcatDemuxer = bindingContext.ParseResult.GetValueForOption(UseFFmpegConcatDemuxer), - DelAfterDone = bindingContext.ParseResult.GetValueForOption(DelAfterDone), - AutoSubtitleFix = bindingContext.ParseResult.GetValueForOption(AutoSubtitleFix), - CheckSegmentsCount = bindingContext.ParseResult.GetValueForOption(CheckSegmentsCount), - SubtitleFormat = bindingContext.ParseResult.GetValueForOption(SubtitleFormat), - SubOnly = bindingContext.ParseResult.GetValueForOption(SubOnly), - TmpDir = bindingContext.ParseResult.GetValueForOption(TmpDir), - SaveDir = bindingContext.ParseResult.GetValueForOption(SaveDir), - SaveName = bindingContext.ParseResult.GetValueForOption(SaveName), - ThreadCount = bindingContext.ParseResult.GetValueForOption(ThreadCount), - UILanguage = bindingContext.ParseResult.GetValueForOption(UILanguage), - SkipDownload = bindingContext.ParseResult.GetValueForOption(SkipDownload), - WriteMetaJson = bindingContext.ParseResult.GetValueForOption(WriteMetaJson), - AppendUrlParams = bindingContext.ParseResult.GetValueForOption(AppendUrlParams), - SavePattern = bindingContext.ParseResult.GetValueForOption(SavePattern), - Keys = bindingContext.ParseResult.GetValueForOption(Keys), - UrlProcessorArgs = bindingContext.ParseResult.GetValueForOption(UrlProcessorArgs), - MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption), - UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager), - DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath), - FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath), - KeyTextFile = bindingContext.ParseResult.GetValueForOption(KeyTextFile), - DownloadRetryCount = bindingContext.ParseResult.GetValueForOption(DownloadRetryCount), - BaseUrl = bindingContext.ParseResult.GetValueForOption(BaseUrl), - MuxImports = bindingContext.ParseResult.GetValueForOption(MuxImports), - ConcurrentDownload = bindingContext.ParseResult.GetValueForOption(ConcurrentDownload), - VideoFilter = bindingContext.ParseResult.GetValueForOption(VideoFilter), - AudioFilter = bindingContext.ParseResult.GetValueForOption(AudioFilter), - SubtitleFilter = bindingContext.ParseResult.GetValueForOption(SubtitleFilter), - DropVideoFilter = bindingContext.ParseResult.GetValueForOption(DropVideoFilter), - DropAudioFilter = bindingContext.ParseResult.GetValueForOption(DropAudioFilter), - DropSubtitleFilter = bindingContext.ParseResult.GetValueForOption(DropSubtitleFilter), - LiveRealTimeMerge = bindingContext.ParseResult.GetValueForOption(LiveRealTimeMerge), - LiveKeepSegments = bindingContext.ParseResult.GetValueForOption(LiveKeepSegments), - LiveRecordLimit = bindingContext.ParseResult.GetValueForOption(LiveRecordLimit), - TaskStartAt = bindingContext.ParseResult.GetValueForOption(TaskStartAt), - LivePerformAsVod = bindingContext.ParseResult.GetValueForOption(LivePerformAsVod), - LivePipeMux = bindingContext.ParseResult.GetValueForOption(LivePipeMux), - LiveFixVttByAudio = bindingContext.ParseResult.GetValueForOption(LiveFixVttByAudio), - UseSystemProxy = bindingContext.ParseResult.GetValueForOption(UseSystemProxy), - CustomProxy = bindingContext.ParseResult.GetValueForOption(CustomProxy), - CustomRange = bindingContext.ParseResult.GetValueForOption(CustomRange), - LiveWaitTime = bindingContext.ParseResult.GetValueForOption(LiveWaitTime), - LiveTakeCount = bindingContext.ParseResult.GetValueForOption(LiveTakeCount), - NoDateInfo = bindingContext.ParseResult.GetValueForOption(NoDateInfo), - NoLog = bindingContext.ParseResult.GetValueForOption(NoLog), - AdKeywords = bindingContext.ParseResult.GetValueForOption(AdKeywords), - MaxSpeed = bindingContext.ParseResult.GetValueForOption(MaxSpeed), - }; - - if (bindingContext.ParseResult.HasOption(CustomHLSMethod)) option.CustomHLSMethod = bindingContext.ParseResult.GetValueForOption(CustomHLSMethod); - if (bindingContext.ParseResult.HasOption(CustomHLSKey)) option.CustomHLSKey = bindingContext.ParseResult.GetValueForOption(CustomHLSKey); - if (bindingContext.ParseResult.HasOption(CustomHLSIv)) option.CustomHLSIv = bindingContext.ParseResult.GetValueForOption(CustomHLSIv); - - var parsedHeaders = bindingContext.ParseResult.GetValueForOption(Headers); - if (parsedHeaders != null) - option.Headers = parsedHeaders; - - - //以用户选择语言为准优先 - if (option.UILanguage != null) - { - CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(option.UILanguage); - Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(option.UILanguage); - Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(option.UILanguage); - } - - //混流设置 - var muxAfterDoneValue = bindingContext.ParseResult.GetValueForOption(MuxAfterDone); - if (muxAfterDoneValue != null) - { - option.MuxAfterDone = true; - option.MuxOptions = muxAfterDoneValue; - if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath; - else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath; - } - - - return option; - } - } - - - public static async Task InvokeArgs(string[] args, Func action) - { - var argList = new List(args); - var index = -1; - if ((index = argList.IndexOf("--morehelp")) >= 0 && argList.Count > index + 1) - { - var option = argList[index + 1]; - var msg = option switch - { - "mux-after-done" => ResString.cmd_muxAfterDone_more, - "mux-import" => ResString.cmd_muxImport_more, - "select-video" => ResString.cmd_selectVideo_more, - "select-audio" => ResString.cmd_selectAudio_more, - "select-subtitle" => ResString.cmd_selectSubtitle_more, - "custom-range" => ResString.cmd_custom_range, - _ => $"Option=\"{option}\" not found" - }; - Console.WriteLine($"More Help:\r\n\r\n --{option}\r\n\r\n" + msg); - Environment.Exit(0); - } - - var rootCommand = new RootCommand(VERSION_INFO) - { - Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, - BinaryMerge, UseFFmpegConcatDemuxer, DelAfterDone, NoDateInfo, NoLog, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, - FFmpegBinaryPath, - LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, - MaxSpeed, - MuxAfterDone, - CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, CustomRange, TaskStartAt, - LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveFixVttByAudio, LiveRecordLimit, LiveWaitTime, LiveTakeCount, - MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, AdKeywords, MoreHelp - }; - - rootCommand.TreatUnmatchedTokensAsErrors = true; - rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); - - var parser = new CommandLineBuilder(rootCommand) - .UseDefaults() - .EnablePosixBundling(false) - .UseExceptionHandler((ex, context) => - { - try { Console.CursorVisible = true; } catch { } - string msg = Logger.LogLevel == Common.Log.LogLevel.DEBUG ? ex.ToString() : ex.Message; -#if DEBUG - msg = ex.ToString(); -#endif - Logger.Error(msg); - Thread.Sleep(3000); - Environment.Exit(1); - }, 1) - .Build(); - - return await parser.InvokeAsync(args); + result.ErrorMessage = "error in parse SpeedLimit: " + input; + return null; } } -} + + /// + /// 解析用户定义的下载范围 + /// + /// + /// + /// + private static CustomRange? ParseCustomRange(ArgumentResult result) + { + var input = result.Tokens.First().Value; + // 支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00; + try + { + if (string.IsNullOrEmpty(input)) + return null; + + var arr = input.Split('-'); + if (arr.Length != 2) + throw new ArgumentException("Bad format!"); + + if (input.Contains(":")) + { + return new CustomRange() + { + InputStr = input, + StartSec = arr[0] == "" ? 0 : OtherUtil.ParseDur(arr[0]).TotalSeconds, + EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds, + }; + } + else if (RangeRegex().IsMatch(input)) + { + var left = RangeRegex().Match(input).Groups[1].Value; + var right = RangeRegex().Match(input).Groups[2].Value; + return new CustomRange() + { + InputStr = input, + StartSegIndex = left == "" ? 0 : long.Parse(left), + EndSegIndex = right == "" ? long.MaxValue : long.Parse(right), + }; + } + + throw new ArgumentException("Bad format!"); + } + catch (Exception ex) + { + result.ErrorMessage = $"error in parse CustomRange: " + ex.Message; + return null; + } + } + + /// + /// 解析用户代理 + /// + /// + /// + /// + private static WebProxy? ParseProxy(ArgumentResult result) + { + var input = result.Tokens.First().Value; + try + { + if (string.IsNullOrEmpty(input)) + return null; + + var uri = new Uri(input); + var proxy = new WebProxy(uri, true); + if (!string.IsNullOrEmpty(uri.UserInfo)) + { + var infos = uri.UserInfo.Split(':'); + proxy.Credentials = new NetworkCredential(infos.First(), infos.Last()); + } + return proxy; + } + catch (Exception ex) + { + result.ErrorMessage = $"error in parse proxy: " + ex.Message; + return null; + } + } + + /// + /// 解析自定义KEY + /// + /// + /// + private static byte[]? ParseHLSCustomKey(ArgumentResult result) + { + var input = result.Tokens.First().Value; + try + { + if (string.IsNullOrEmpty(input)) + return null; + if (File.Exists(input)) + return File.ReadAllBytes(input); + else if (HexUtil.TryParseHexString(input, out byte[]? bytes)) + return bytes; + else + return Convert.FromBase64String(input); + } + catch (Exception) + { + result.ErrorMessage = "error in parse hls custom key: " + input; + return null; + } + } + + /// + /// 解析录制直播时长限制 + /// + /// + /// + private static TimeSpan? ParseLiveLimit(ArgumentResult result) + { + var input = result.Tokens.First().Value; + try + { + return OtherUtil.ParseDur(input); + } + catch (Exception) + { + result.ErrorMessage = "error in parse LiveRecordLimit: " + input; + return null; + } + } + + /// + /// 解析任务开始时间 + /// + /// + /// + private static DateTime? ParseStartTime(ArgumentResult result) + { + var input = result.Tokens.First().Value; + try + { + CultureInfo provider = CultureInfo.InvariantCulture; + return DateTime.ParseExact(input, "yyyyMMddHHmmss", provider); + } + catch (Exception) + { + result.ErrorMessage = "error in parse TaskStartTime: " + input; + return null; + } + } + + private static string? ParseSaveName(ArgumentResult result) + { + var input = result.Tokens.First().Value; + var newName = OtherUtil.GetValidFileName(input); + if (string.IsNullOrEmpty(newName)) + { + result.ErrorMessage = "Invalid save name!"; + return null; + } + return newName; + } + + /// + /// 流过滤器 + /// + /// + /// + private static StreamFilter? ParseStreamFilter(ArgumentResult result) + { + var streamFilter = new StreamFilter(); + var input = result.Tokens.First().Value; + var p = new ComplexParamParser(input); + + + // 目标范围 + var forStr = ""; + if (input == ForStrRegex().Match(input).Value) + { + forStr = input; + } + else + { + forStr = p.GetValue("for") ?? "best"; + if (forStr != ForStrRegex().Match(forStr).Value) + { + result.ErrorMessage = $"for={forStr} not valid"; + return null; + } + } + streamFilter.For = forStr; + + var id = p.GetValue("id"); + if (!string.IsNullOrEmpty(id)) + streamFilter.GroupIdReg = new Regex(id); + + var lang = p.GetValue("lang"); + if (!string.IsNullOrEmpty(lang)) + streamFilter.LanguageReg = new Regex(lang); + + var name = p.GetValue("name"); + if (!string.IsNullOrEmpty(name)) + streamFilter.NameReg = new Regex(name); + + var codecs = p.GetValue("codecs"); + if (!string.IsNullOrEmpty(codecs)) + streamFilter.CodecsReg = new Regex(codecs); + + var res = p.GetValue("res"); + if (!string.IsNullOrEmpty(res)) + streamFilter.ResolutionReg = new Regex(res); + + var frame = p.GetValue("frame"); + if (!string.IsNullOrEmpty(frame)) + streamFilter.FrameRateReg = new Regex(frame); + + var channel = p.GetValue("channel"); + if (!string.IsNullOrEmpty(channel)) + streamFilter.ChannelsReg = new Regex(channel); + + var range = p.GetValue("range"); + if (!string.IsNullOrEmpty(range)) + streamFilter.VideoRangeReg = new Regex(range); + + var url = p.GetValue("url"); + if (!string.IsNullOrEmpty(url)) + streamFilter.UrlReg = new Regex(url); + + var segsMin = p.GetValue("segsMin"); + if (!string.IsNullOrEmpty(segsMin)) + streamFilter.SegmentsMinCount = long.Parse(segsMin); + + var segsMax = p.GetValue("segsMax"); + if (!string.IsNullOrEmpty(segsMax)) + streamFilter.SegmentsMaxCount = long.Parse(segsMax); + + var plistDurMin = p.GetValue("plistDurMin"); + if (!string.IsNullOrEmpty(plistDurMin)) + streamFilter.PlaylistMinDur = OtherUtil.ParseSeconds(plistDurMin); + + var plistDurMax = p.GetValue("plistDurMax"); + if (!string.IsNullOrEmpty(plistDurMax)) + streamFilter.PlaylistMaxDur = OtherUtil.ParseSeconds(plistDurMax); + + var bwMin = p.GetValue("bwMin"); + if (!string.IsNullOrEmpty(bwMin)) + streamFilter.BandwidthMin = int.Parse(bwMin) * 1000; + + var bwMax = p.GetValue("bwMax"); + if (!string.IsNullOrEmpty(bwMax)) + streamFilter.BandwidthMax = int.Parse(bwMax) * 1000; + + var role = p.GetValue("role"); + if (System.Enum.TryParse(role, true, out RoleType roleType)) + streamFilter.Role = roleType; + + return streamFilter; + } + + /// + /// 分割Header + /// + /// + /// + private static Dictionary ParseHeaders(ArgumentResult result) + { + var array = result.Tokens.Select(t => t.Value).ToArray(); + return OtherUtil.SplitHeaderArrayToDic(array); + } + + /// + /// 解析混流引入的外部文件 + /// + /// + /// + private static List ParseImports(ArgumentResult result) + { + var imports = new List(); + + foreach (var item in result.Tokens) + { + var p = new ComplexParamParser(item.Value); + var path = p.GetValue("path") ?? item.Value; // 若未获取到,直接整个字符串作为path + var lang = p.GetValue("lang"); + var name = p.GetValue("name"); + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + { + result.ErrorMessage = "path empty or file not exists!"; + return imports; + } + imports.Add(new OutputFile() + { + Index = 999, + FilePath = path, + LangCode = lang, + Description = name + }); + } + + return imports; + } + + /// + /// 解析混流选项 + /// + /// + /// + private static MuxOptions? ParseMuxAfterDone(ArgumentResult result) + { + var v = result.Tokens.First().Value; + var p = new ComplexParamParser(v); + // 混流格式 + var format = p.GetValue("format") ?? v.Split(':')[0]; // 若未获取到,直接:前的字符串作为format解析 + var parseResult = System.Enum.TryParse(format.ToUpperInvariant(), out MuxFormat muxFormat); + if (!parseResult) + { + result.ErrorMessage = $"format={format} not valid"; + return null; + } + // 混流器 + var muxer = p.GetValue("muxer") ?? "ffmpeg"; + if (muxer != "ffmpeg" && muxer != "mkvmerge") + { + result.ErrorMessage = $"muxer={muxer} not valid"; + return null; + } + // 混流器路径 + var bin_path = p.GetValue("bin_path") ?? "auto"; + if (string.IsNullOrEmpty(bin_path)) + { + result.ErrorMessage = $"bin_path={bin_path} not valid"; + return null; + } + // 是否删除 + var keep = p.GetValue("keep") ?? "false"; + if (keep != "true" && keep != "false") + { + result.ErrorMessage = $"keep={keep} not valid"; + return null; + } + // 是否忽略字幕 + var skipSub = p.GetValue("skip_sub") ?? "false"; + if (skipSub != "true" && skipSub != "false") + { + result.ErrorMessage = $"skip_sub={keep} not valid"; + return null; + } + // 冲突检测 + if (muxer == "mkvmerge" && format == "mp4") + { + result.ErrorMessage = $"mkvmerge can not do mp4"; + return null; + } + return new MuxOptions() + { + UseMkvmerge = muxer == "mkvmerge", + MuxFormat = muxFormat, + KeepFiles = keep == "true", + SkipSubtitle = skipSub == "true", + BinPath = bin_path == "auto" ? null : bin_path + }; + } + + class MyOptionBinder : BinderBase + { + protected override MyOption GetBoundValue(BindingContext bindingContext) + { + var option = new MyOption + { + Input = bindingContext.ParseResult.GetValueForArgument(Input), + ForceAnsiConsole = bindingContext.ParseResult.GetValueForOption(ForceAnsiConsole), + NoAnsiColor = bindingContext.ParseResult.GetValueForOption(NoAnsiColor), + LogLevel = bindingContext.ParseResult.GetValueForOption(LogLevel), + AutoSelect = bindingContext.ParseResult.GetValueForOption(AutoSelect), + SkipMerge = bindingContext.ParseResult.GetValueForOption(SkipMerge), + BinaryMerge = bindingContext.ParseResult.GetValueForOption(BinaryMerge), + UseFFmpegConcatDemuxer = bindingContext.ParseResult.GetValueForOption(UseFFmpegConcatDemuxer), + DelAfterDone = bindingContext.ParseResult.GetValueForOption(DelAfterDone), + AutoSubtitleFix = bindingContext.ParseResult.GetValueForOption(AutoSubtitleFix), + CheckSegmentsCount = bindingContext.ParseResult.GetValueForOption(CheckSegmentsCount), + SubtitleFormat = bindingContext.ParseResult.GetValueForOption(SubtitleFormat), + SubOnly = bindingContext.ParseResult.GetValueForOption(SubOnly), + TmpDir = bindingContext.ParseResult.GetValueForOption(TmpDir), + SaveDir = bindingContext.ParseResult.GetValueForOption(SaveDir), + SaveName = bindingContext.ParseResult.GetValueForOption(SaveName), + ThreadCount = bindingContext.ParseResult.GetValueForOption(ThreadCount), + UILanguage = bindingContext.ParseResult.GetValueForOption(UILanguage), + SkipDownload = bindingContext.ParseResult.GetValueForOption(SkipDownload), + WriteMetaJson = bindingContext.ParseResult.GetValueForOption(WriteMetaJson), + AppendUrlParams = bindingContext.ParseResult.GetValueForOption(AppendUrlParams), + SavePattern = bindingContext.ParseResult.GetValueForOption(SavePattern), + Keys = bindingContext.ParseResult.GetValueForOption(Keys), + UrlProcessorArgs = bindingContext.ParseResult.GetValueForOption(UrlProcessorArgs), + MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption), + UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager), + DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath), + FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath), + KeyTextFile = bindingContext.ParseResult.GetValueForOption(KeyTextFile), + DownloadRetryCount = bindingContext.ParseResult.GetValueForOption(DownloadRetryCount), + BaseUrl = bindingContext.ParseResult.GetValueForOption(BaseUrl), + MuxImports = bindingContext.ParseResult.GetValueForOption(MuxImports), + ConcurrentDownload = bindingContext.ParseResult.GetValueForOption(ConcurrentDownload), + VideoFilter = bindingContext.ParseResult.GetValueForOption(VideoFilter), + AudioFilter = bindingContext.ParseResult.GetValueForOption(AudioFilter), + SubtitleFilter = bindingContext.ParseResult.GetValueForOption(SubtitleFilter), + DropVideoFilter = bindingContext.ParseResult.GetValueForOption(DropVideoFilter), + DropAudioFilter = bindingContext.ParseResult.GetValueForOption(DropAudioFilter), + DropSubtitleFilter = bindingContext.ParseResult.GetValueForOption(DropSubtitleFilter), + LiveRealTimeMerge = bindingContext.ParseResult.GetValueForOption(LiveRealTimeMerge), + LiveKeepSegments = bindingContext.ParseResult.GetValueForOption(LiveKeepSegments), + LiveRecordLimit = bindingContext.ParseResult.GetValueForOption(LiveRecordLimit), + TaskStartAt = bindingContext.ParseResult.GetValueForOption(TaskStartAt), + LivePerformAsVod = bindingContext.ParseResult.GetValueForOption(LivePerformAsVod), + LivePipeMux = bindingContext.ParseResult.GetValueForOption(LivePipeMux), + LiveFixVttByAudio = bindingContext.ParseResult.GetValueForOption(LiveFixVttByAudio), + UseSystemProxy = bindingContext.ParseResult.GetValueForOption(UseSystemProxy), + CustomProxy = bindingContext.ParseResult.GetValueForOption(CustomProxy), + CustomRange = bindingContext.ParseResult.GetValueForOption(CustomRange), + LiveWaitTime = bindingContext.ParseResult.GetValueForOption(LiveWaitTime), + LiveTakeCount = bindingContext.ParseResult.GetValueForOption(LiveTakeCount), + NoDateInfo = bindingContext.ParseResult.GetValueForOption(NoDateInfo), + NoLog = bindingContext.ParseResult.GetValueForOption(NoLog), + AdKeywords = bindingContext.ParseResult.GetValueForOption(AdKeywords), + MaxSpeed = bindingContext.ParseResult.GetValueForOption(MaxSpeed), + }; + + if (bindingContext.ParseResult.HasOption(CustomHLSMethod)) option.CustomHLSMethod = bindingContext.ParseResult.GetValueForOption(CustomHLSMethod); + if (bindingContext.ParseResult.HasOption(CustomHLSKey)) option.CustomHLSKey = bindingContext.ParseResult.GetValueForOption(CustomHLSKey); + if (bindingContext.ParseResult.HasOption(CustomHLSIv)) option.CustomHLSIv = bindingContext.ParseResult.GetValueForOption(CustomHLSIv); + + var parsedHeaders = bindingContext.ParseResult.GetValueForOption(Headers); + if (parsedHeaders != null) + option.Headers = parsedHeaders; + + + // 以用户选择语言为准优先 + if (option.UILanguage != null) + { + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(option.UILanguage); + Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(option.UILanguage); + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(option.UILanguage); + } + + // 混流设置 + var muxAfterDoneValue = bindingContext.ParseResult.GetValueForOption(MuxAfterDone); + if (muxAfterDoneValue != null) + { + option.MuxAfterDone = true; + option.MuxOptions = muxAfterDoneValue; + if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath; + else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath; + } + + + return option; + } + } + + + public static async Task InvokeArgs(string[] args, Func action) + { + var argList = new List(args); + var index = -1; + if ((index = argList.IndexOf("--morehelp")) >= 0 && argList.Count > index + 1) + { + var option = argList[index + 1]; + var msg = option switch + { + "mux-after-done" => ResString.cmd_muxAfterDone_more, + "mux-import" => ResString.cmd_muxImport_more, + "select-video" => ResString.cmd_selectVideo_more, + "select-audio" => ResString.cmd_selectAudio_more, + "select-subtitle" => ResString.cmd_selectSubtitle_more, + "custom-range" => ResString.cmd_custom_range, + _ => $"Option=\"{option}\" not found" + }; + Console.WriteLine($"More Help:\r\n\r\n --{option}\r\n\r\n" + msg); + Environment.Exit(0); + } + + var rootCommand = new RootCommand(VERSION_INFO) + { + Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, + BinaryMerge, UseFFmpegConcatDemuxer, DelAfterDone, NoDateInfo, NoLog, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, + FFmpegBinaryPath, + LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, + MaxSpeed, + MuxAfterDone, + CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, CustomRange, TaskStartAt, + LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveFixVttByAudio, LiveRecordLimit, LiveWaitTime, LiveTakeCount, + MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, AdKeywords, MoreHelp + }; + + rootCommand.TreatUnmatchedTokensAsErrors = true; + rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); + + var parser = new CommandLineBuilder(rootCommand) + .UseDefaults() + .EnablePosixBundling(false) + .UseExceptionHandler((ex, context) => + { + try { Console.CursorVisible = true; } catch { } + string msg = Logger.LogLevel == Common.Log.LogLevel.DEBUG ? ex.ToString() : ex.Message; +#if DEBUG + msg = ex.ToString(); +#endif + Logger.Error(msg); + Thread.Sleep(3000); + Environment.Exit(1); + }, 1) + .Build(); + + return await parser.InvokeAsync(args); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs b/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs index 8674dd4..4ee32f6 100644 --- a/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs +++ b/src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs @@ -1,61 +1,56 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Text; -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; - public ComplexParamParser(string arg) - { - _arg = arg; - } + _arg = arg; + } - public string? GetValue(string key) - { - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(_arg)) return null; + public string? GetValue(string key) + { + 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 (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 (chars[i] == ':') - { - if (last == '\\') - { - result.Replace("\\", ""); - last = chars[i]; - result.Append(chars[i]); - } - else break; - } - else + if (last == '\\') { + result.Replace("\\", ""); last = 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!"); } } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 9b926ce..0cb77e4 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -4,255 +4,254 @@ using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Enum; using System.Net; -namespace N_m3u8DL_RE.CommandLine +namespace N_m3u8DL_RE.CommandLine; + +internal class MyOption { - internal class MyOption - { - /// - /// See: . - /// - public string Input { get; set; } = default!; - /// - /// See: . - /// - public Dictionary Headers { get; set; } = new Dictionary(); - /// - /// See: . - /// - public string[]? AdKeywords { get; set; } - /// - /// See: . - /// - public long? MaxSpeed { get; set; } - /// - /// See: . - /// - public string[]? Keys { get; set; } - /// - /// See: . - /// - public string? BaseUrl { get; set; } - /// - /// See: . - /// - public string? KeyTextFile { get; set; } - /// - /// See: . - /// - public string? UrlProcessorArgs { get; set; } - /// - /// See: . - /// - public LogLevel LogLevel { get; set; } - /// - /// See: . - /// - public bool NoDateInfo { get; set; } - /// - /// See: . - /// - public bool NoLog { get; set; } - /// - /// See: . - /// - public bool AutoSelect { get; set; } - /// - /// See: . - /// - public bool SubOnly { get; set; } - /// - /// See: . - /// - public int ThreadCount { get; set; } - /// - /// See: . - /// - public int DownloadRetryCount { get; set; } - /// - /// See: . - /// - public TimeSpan? LiveRecordLimit { get; set; } - /// - /// See: . - /// - public DateTime? TaskStartAt { get; set; } - /// - /// See: . - /// - public bool SkipMerge { get; set; } - /// - /// See: . - /// - public bool BinaryMerge { get; set; } - /// - /// See: . - /// - public bool ForceAnsiConsole { get; set; } - /// - /// See: . - /// - public bool NoAnsiColor { get; set; } - /// - /// See: . - /// - public bool UseFFmpegConcatDemuxer { get; set; } - /// - /// See: . - /// - public bool DelAfterDone { get; set; } - /// - /// See: . - /// - public bool AutoSubtitleFix { get; set; } - /// - /// See: . - /// - public bool CheckSegmentsCount { get; set; } - /// - /// See: . - /// - public bool SkipDownload { get; set; } - /// - /// See: . - /// - public bool WriteMetaJson { get; set; } - /// - /// See: . - /// - public bool AppendUrlParams { get; set; } - /// - /// See: . - /// - public bool MP4RealTimeDecryption { get; set; } - /// - /// See: . - /// - public bool UseShakaPackager { get; set; } - /// - /// See: . - /// - public bool MuxAfterDone { get; set; } - /// - /// See: . - /// - public bool ConcurrentDownload { get; set; } - /// - /// See: . - /// - public bool LiveRealTimeMerge { get; set; } - /// - /// See: . - /// - public bool LiveKeepSegments { get; set; } - /// - /// See: . - /// - public bool LivePerformAsVod { get; set; } - /// - /// See: . - /// - public bool UseSystemProxy { get; set; } - /// - /// See: . - /// - public SubtitleFormat SubtitleFormat { get; set; } - /// - /// See: . - /// - public string? TmpDir { get; set; } - /// - /// See: . - /// - public string? SaveDir { get; set; } - /// - /// See: . - /// - public string? SaveName { get; set; } - /// - /// See: . - /// - public string? SavePattern { get; set; } - /// - /// See: . - /// - public string? UILanguage { get; set; } - /// - /// See: . - /// - public string? DecryptionBinaryPath { get; set; } - /// - /// See: . - /// - public string? FFmpegBinaryPath { get; set; } - /// - /// See: . - /// - public string? MkvmergeBinaryPath { get; set; } - /// - /// See: . - /// - public List? MuxImports { get; set; } - /// - /// See: . - /// - public StreamFilter? VideoFilter { get; set; } - /// - /// See: . - /// - public StreamFilter? DropVideoFilter { get; set; } - /// - /// See: . - /// - public StreamFilter? AudioFilter { get; set; } - /// - /// See: . - /// - public StreamFilter? DropAudioFilter { get; set; } - /// - /// See: . - /// - public StreamFilter? SubtitleFilter { get; set; } - /// - /// See: . - /// - public StreamFilter? DropSubtitleFilter { get; set; } - /// - /// See: . - /// - public EncryptMethod? CustomHLSMethod { get; set; } - /// - /// See: . - /// - public byte[]? CustomHLSKey { get; set; } - /// - /// See: . - /// - public byte[]? CustomHLSIv { get; set; } - /// - /// See: . - /// - public WebProxy? CustomProxy { get; set; } - /// - /// See: . - /// - public CustomRange? CustomRange { get; set; } - /// - /// See: . - /// - public int? LiveWaitTime { get; set; } - /// - /// See: . - /// - public int LiveTakeCount { get; set; } - public MuxOptions MuxOptions { get; set; } - //public bool LiveWriteHLS { get; set; } = true; - /// - /// See: . - /// - public bool LivePipeMux { get; set; } - /// - /// See: . - /// - public bool LiveFixVttByAudio { get; set; } - } + /// + /// See: . + /// + public string Input { get; set; } = default!; + /// + /// See: . + /// + public Dictionary Headers { get; set; } = new Dictionary(); + /// + /// See: . + /// + public string[]? AdKeywords { get; set; } + /// + /// See: . + /// + public long? MaxSpeed { get; set; } + /// + /// See: . + /// + public string[]? Keys { get; set; } + /// + /// See: . + /// + public string? BaseUrl { get; set; } + /// + /// See: . + /// + public string? KeyTextFile { get; set; } + /// + /// See: . + /// + public string? UrlProcessorArgs { get; set; } + /// + /// See: . + /// + public LogLevel LogLevel { get; set; } + /// + /// See: . + /// + public bool NoDateInfo { get; set; } + /// + /// See: . + /// + public bool NoLog { get; set; } + /// + /// See: . + /// + public bool AutoSelect { get; set; } + /// + /// See: . + /// + public bool SubOnly { get; set; } + /// + /// See: . + /// + public int ThreadCount { get; set; } + /// + /// See: . + /// + public int DownloadRetryCount { get; set; } + /// + /// See: . + /// + public TimeSpan? LiveRecordLimit { get; set; } + /// + /// See: . + /// + public DateTime? TaskStartAt { get; set; } + /// + /// See: . + /// + public bool SkipMerge { get; set; } + /// + /// See: . + /// + public bool BinaryMerge { get; set; } + /// + /// See: . + /// + public bool ForceAnsiConsole { get; set; } + /// + /// See: . + /// + public bool NoAnsiColor { get; set; } + /// + /// See: . + /// + public bool UseFFmpegConcatDemuxer { get; set; } + /// + /// See: . + /// + public bool DelAfterDone { get; set; } + /// + /// See: . + /// + public bool AutoSubtitleFix { get; set; } + /// + /// See: . + /// + public bool CheckSegmentsCount { get; set; } + /// + /// See: . + /// + public bool SkipDownload { get; set; } + /// + /// See: . + /// + public bool WriteMetaJson { get; set; } + /// + /// See: . + /// + public bool AppendUrlParams { get; set; } + /// + /// See: . + /// + public bool MP4RealTimeDecryption { get; set; } + /// + /// See: . + /// + public bool UseShakaPackager { get; set; } + /// + /// See: . + /// + public bool MuxAfterDone { get; set; } + /// + /// See: . + /// + public bool ConcurrentDownload { get; set; } + /// + /// See: . + /// + public bool LiveRealTimeMerge { get; set; } + /// + /// See: . + /// + public bool LiveKeepSegments { get; set; } + /// + /// See: . + /// + public bool LivePerformAsVod { get; set; } + /// + /// See: . + /// + public bool UseSystemProxy { get; set; } + /// + /// See: . + /// + public SubtitleFormat SubtitleFormat { get; set; } + /// + /// See: . + /// + public string? TmpDir { get; set; } + /// + /// See: . + /// + public string? SaveDir { get; set; } + /// + /// See: . + /// + public string? SaveName { get; set; } + /// + /// See: . + /// + public string? SavePattern { get; set; } + /// + /// See: . + /// + public string? UILanguage { get; set; } + /// + /// See: . + /// + public string? DecryptionBinaryPath { get; set; } + /// + /// See: . + /// + public string? FFmpegBinaryPath { get; set; } + /// + /// See: . + /// + public string? MkvmergeBinaryPath { get; set; } + /// + /// See: . + /// + public List? MuxImports { get; set; } + /// + /// See: . + /// + public StreamFilter? VideoFilter { get; set; } + /// + /// See: . + /// + public StreamFilter? DropVideoFilter { get; set; } + /// + /// See: . + /// + public StreamFilter? AudioFilter { get; set; } + /// + /// See: . + /// + public StreamFilter? DropAudioFilter { get; set; } + /// + /// See: . + /// + public StreamFilter? SubtitleFilter { get; set; } + /// + /// See: . + /// + public StreamFilter? DropSubtitleFilter { get; set; } + /// + /// See: . + /// + public EncryptMethod? CustomHLSMethod { get; set; } + /// + /// See: . + /// + public byte[]? CustomHLSKey { get; set; } + /// + /// See: . + /// + public byte[]? CustomHLSIv { get; set; } + /// + /// See: . + /// + public WebProxy? CustomProxy { get; set; } + /// + /// See: . + /// + public CustomRange? CustomRange { get; set; } + /// + /// See: . + /// + public int? LiveWaitTime { get; set; } + /// + /// See: . + /// + public int LiveTakeCount { get; set; } + public MuxOptions? MuxOptions { get; set; } + // public bool LiveWriteHLS { get; set; } = true; + /// + /// See: . + /// + public bool LivePipeMux { get; set; } + /// + /// See: . + /// + public bool LiveFixVttByAudio { get; set; } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs index c953878..ca1716e 100644 --- a/src/N_m3u8DL-RE/Config/DownloaderConfig.cs +++ b/src/N_m3u8DL-RE/Config/DownloaderConfig.cs @@ -1,33 +1,25 @@ 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; } - /// - /// 前置阶段生成的文件夹名 - /// - public required string DirPrefix { get; set; } - /// - /// 文件名模板 - /// - public string? SavePattern { get; set; } - /// - /// 校验响应头的文件大小和实际大小 - /// - public bool CheckContentLength { get; set; } = true; - /// - /// 请求头 - /// - public Dictionary Headers { get; set; } = new Dictionary(); - } -} + /// + /// 前置阶段生成的文件夹名 + /// + public required string DirPrefix { get; set; } + /// + /// 文件名模板 + /// + public string? SavePattern { get; set; } + /// + /// 校验响应头的文件大小和实际大小 + /// + public bool CheckContentLength { get; set; } = true; + /// + /// 请求头 + /// + public Dictionary Headers { get; set; } = new Dictionary(); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Crypto/AESUtil.cs b/src/N_m3u8DL-RE/Crypto/AESUtil.cs index b2b270d..3b9d299 100644 --- a/src/N_m3u8DL-RE/Crypto/AESUtil.cs +++ b/src/N_m3u8DL-RE/Crypto/AESUtil.cs @@ -1,44 +1,38 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; +using System.Security.Cryptography; -namespace N_m3u8DL_RE.Crypto +namespace N_m3u8DL_RE.Crypto; + +internal class AESUtil { - internal class AESUtil + /// + /// AES-128解密,解密后原地替换文件 + /// + /// + /// + /// + /// + /// + public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) { - /// - /// AES-128解密,解密后原地替换文件 - /// - /// - /// - /// - /// - /// - 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) - { - byte[] inBuff = encryptedBuff; - - Aes dcpt = Aes.Create(); - dcpt.BlockSize = 128; - dcpt.KeySize = 128; - dcpt.Key = keyByte; - dcpt.IV = ivByte; - dcpt.Mode = mode; - dcpt.Padding = padding; - - ICryptoTransform cTransform = dcpt.CreateDecryptor(); - byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); - return resultArray; - } + 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) + { + byte[] inBuff = encryptedBuff; + + Aes dcpt = Aes.Create(); + dcpt.BlockSize = 128; + dcpt.KeySize = 128; + dcpt.Key = keyByte; + dcpt.IV = ivByte; + dcpt.Mode = mode; + dcpt.Padding = padding; + + ICryptoTransform cTransform = dcpt.CreateDecryptor(); + byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); + return resultArray; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs b/src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs index 0c02179..2edab26 100644 --- a/src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs +++ b/src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs @@ -1,26 +1,21 @@ 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 (nonceBytes.Length != 12 && nonceBytes.Length != 8) - throw new Exception("Key must be 12 or 8 bytes!"); - if (nonceBytes.Length == 8) - nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray(); + if (keyBytes.Length != 32) + throw new Exception("Key must be 32 bytes!"); + if (nonceBytes.Length != 12 && nonceBytes.Length != 8) + throw new Exception("Key must be 12 or 8 bytes!"); + if (nonceBytes.Length == 8) + nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray(); - var decStream = new MemoryStream(); - using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff)); - using (BinaryWriter writer = new BinaryWriter(decStream)) + var decStream = new MemoryStream(); + using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff)); + using (BinaryWriter writer = new BinaryWriter(decStream)) while (true) { var buffer = reader.ReadBytes(1024); @@ -37,7 +32,6 @@ namespace N_m3u8DL_RE.Crypto } } - return decStream.ToArray(); - } + return decStream.ToArray(); } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs b/src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs index be6c39d..206b78e 100644 --- a/src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs @@ -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.Enum; using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Util; @@ -11,244 +9,234 @@ using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Parser; using N_m3u8DL_RE.Util; using Spectre.Console; -using Spectre.Console.Rendering; -using System; using System.Collections.Concurrent; -using System.IO; -using System.Net.Http.Headers; -using System.Reflection.PortableExecutable; 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 SelectedSteams; + List OutputFiles = new(); + DateTime NowDateTime; + DateTime? PublishDateTime; + bool STOP_FLAG = false; + bool READ_IFO = false; + ConcurrentDictionary RecordingDurDic = new(); // 已录制时长 + ConcurrentDictionary RecordingSizeDic = new(); // 已录制大小 + CancellationTokenSource CancellationTokenSource = new(); // 取消Wait + List InfoBuffer = new List(188 * 5000); // 5000个分包中解析信息,没有就算了 + + public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) { - IDownloader Downloader; - DownloaderConfig DownloaderConfig; - StreamExtractor StreamExtractor; - List SelectedSteams; - List OutputFiles = new(); - DateTime NowDateTime; - DateTime? PublishDateTime; - bool STOP_FLAG = false; - bool READ_IFO = false; - ConcurrentDictionary RecordingDurDic = new(); //已录制时长 - ConcurrentDictionary RecordingSizeDic = new(); //已录制大小 - CancellationTokenSource CancellationTokenSource = new(); //取消Wait - List InfoBuffer = new List(188 * 5000); //5000个分包中解析信息,没有就算了 + this.DownloaderConfig = downloaderConfig; + Downloader = new SimpleDownloader(DownloaderConfig); + NowDateTime = DateTime.Now; + PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime; + StreamExtractor = streamExtractor; + SelectedSteams = selectedSteams; + } - public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) + private async Task 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; - Downloader = new SimpleDownloader(DownloaderConfig); - NowDateTime = DateTime.Now; - PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime; - StreamExtractor = streamExtractor; - SelectedSteams = selectedSteams; + request.Headers.TryAddWithoutValidation(item.Key, item.Value); + } + Logger.Debug(request.Headers.ToString()); + + using var response = await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationTokenSource.Token); + 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 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; - task.StartTask(); + await Task.Delay(200); + if (InfoBuffer.Count < 188 * 5000) continue; - 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) + UInt16 ConvertToUint16(IEnumerable bytes) { - 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); - 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 + var data = InfoBuffer.ToArray(); + var programId = ""; + var serviceProvider = ""; + var serviceName = ""; + for (int i = 0; i < data.Length; i++) { - 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); - RecordingSizeDic[task.Id] += size; - 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 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) + // SDT, BAT, ST + else if (pid == 0x0011) { - 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) + var tableId = (int)tsPayload.Skip(1).First(); + // Current TS Info + if (tableId == 0x42) { - 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 (!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; + if (programId != "" && (serviceName != "" || serviceProvider != "")) + break; } } - } - public async Task TimeCounterAsync() - { - while (!STOP_FLAG) + if (!string.IsNullOrEmpty(programId)) { - await Task.Delay(1000); - RecordingDurDic[0]++; - - //检测时长限制 - if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds)) - { - Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]"); - STOP_FLAG = true; - CancellationTokenSource.Cancel(); - } + 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 StartRecordAsync() - { - ConcurrentDictionary SpeedContainerDic = new(); //速度计算 - ConcurrentDictionary Results = new(); - - var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); - progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; - - //进度条的列定义 - var progressColumns = new ProgressColumn[] - { - new TaskDescriptionColumn() { Alignment = Justify.Left }, - new RecordingDurationColumn(RecordingDurDic), //时长显示 - new RecordingSizeColumn(RecordingSizeDic), //大小显示 - new RecordingStatusColumn(), - new DownloadSpeedColumn(SpeedContainerDic), //速度计算 - new SpinnerColumn(), - }; - if (DownloaderConfig.MyOptions.NoAnsiColor) - { - progressColumns = progressColumns.SkipLast(1).ToArray(); - } - progress.Columns(progressColumns); - - await progress.StartAsync(async ctx => - { - //创建任务 - var dic = SelectedSteams.Select(item => - { - var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0); - SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 - RecordingDurDic[task.Id] = 0; - RecordingSizeDic[task.Id] = 0; - return (item, task); - }).ToDictionary(item => item.item, item => item.task); - - DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; - var limit = DownloaderConfig.MyOptions.LiveRecordLimit; - 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; - } } -} + + public async Task TimeCounterAsync() + { + while (!STOP_FLAG) + { + await Task.Delay(1000); + RecordingDurDic[0]++; + + // 检测时长限制 + if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds)) + { + Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]"); + STOP_FLAG = true; + CancellationTokenSource.Cancel(); + } + } + } + + public async Task StartRecordAsync() + { + ConcurrentDictionary SpeedContainerDic = new(); // 速度计算 + ConcurrentDictionary Results = new(); + + var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); + progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; + + // 进度条的列定义 + var progressColumns = new ProgressColumn[] + { + new TaskDescriptionColumn() { Alignment = Justify.Left }, + new RecordingDurationColumn(RecordingDurDic), // 时长显示 + new RecordingSizeColumn(RecordingSizeDic), // 大小显示 + new RecordingStatusColumn(), + new DownloadSpeedColumn(SpeedContainerDic), // 速度计算 + new SpinnerColumn(), + }; + if (DownloaderConfig.MyOptions.NoAnsiColor) + { + progressColumns = progressColumns.SkipLast(1).ToArray(); + } + progress.Columns(progressColumns); + + await progress.StartAsync(async ctx => + { + // 创建任务 + var dic = SelectedSteams.Select(item => + { + var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0); + SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算 + RecordingDurDic[task.Id] = 0; + RecordingSizeDic[task.Id] = 0; + return (item, task); + }).ToDictionary(item => item.item, item => item.task); + + DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; + var limit = DownloaderConfig.MyOptions.LiveRecordLimit; + 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; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs index 118b86a..abd4c17 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs @@ -14,279 +14,241 @@ using Spectre.Console; using System.Collections.Concurrent; using System.Text; -namespace N_m3u8DL_RE.DownloadManager +namespace N_m3u8DL_RE.DownloadManager; + +internal class SimpleDownloadManager { - internal class SimpleDownloadManager + IDownloader Downloader; + DownloaderConfig DownloaderConfig; + StreamExtractor StreamExtractor; + List SelectedSteams; + List OutputFiles = new(); + + public SimpleDownloadManager(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) + { + this.DownloaderConfig = downloaderConfig; + this.SelectedSteams = selectedSteams; + this.StreamExtractor = streamExtractor; + Downloader = new SimpleDownloader(DownloaderConfig); + } + + // 从文件读取KEY + private async Task SearchKeyAsync(string? currentKID) { - IDownloader Downloader; - DownloaderConfig DownloaderConfig; - StreamExtractor StreamExtractor; - List SelectedSteams; - List OutputFiles = new(); + var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID); + if (_key != null) + { + if (DownloaderConfig.MyOptions.Keys == null) + DownloaderConfig.MyOptions.Keys = new string[] { _key }; + else + DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray(); + } + } - public SimpleDownloadManager(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) - { - this.DownloaderConfig = downloaderConfig; - this.SelectedSteams = selectedSteams; - this.StreamExtractor = streamExtractor; - Downloader = new SimpleDownloader(DownloaderConfig); + private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos, ref bool useAACFilter) + { + if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) + { + DownloaderConfig.MyOptions.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); } - //从文件读取KEY - private async Task SearchKeyAsync(string? currentKID) + if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true)) { - var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID); - if (_key != null) + DownloaderConfig.MyOptions.MuxAfterDone = false; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); + } + + if (mediainfos.Where(m => m.Type == "Audio").All(m => m.BaseInfo!.Contains("aac"))) + { + useAACFilter = true; + } + + if (mediainfos.All(m => m.Type == "Audio")) + { + streamSpec.MediaType = MediaType.AUDIO; + } + else if (mediainfos.All(m => m.Type == "Subtitle")) + { + streamSpec.MediaType = MediaType.SUBTITLES; + if (streamSpec.Extension == null || streamSpec.Extension == "ts") + streamSpec.Extension = "vtt"; + } + } + + private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer) + { + speedContainer.ResetVars(); + bool useAACFilter = false; // ffmpeg合并flag + List mediaInfos = new(); + ConcurrentDictionary FileDic = new(); + + var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments); + if (segments == null || !segments.Any()) return false; + // 单分段尝试切片并行下载 + if (segments.Count() == 1) + { + var splitSegments = await LargeSingleFileSplitUtil.SplitUrlAsync(segments.First(), DownloaderConfig.Headers); + if (splitSegments != null) { - if (DownloaderConfig.MyOptions.Keys == null) - DownloaderConfig.MyOptions.Keys = new string[] { _key }; - else - DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray(); + segments = splitSegments; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.singleFileSplitWarn}[/]"); + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption) + { + DownloaderConfig.MyOptions.MP4RealTimeDecryption = false; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.singleFileRealtimeDecryptWarn}[/]"); + } } + else speedContainer.SingleSegment = true; } - private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos, ref bool useAACFilter) + var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO; + var dirName = $"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}"; + var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName); + var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; + var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName; + var headers = DownloaderConfig.Headers; + + // mp4decrypt + var mp4decrypt = DownloaderConfig.MyOptions.DecryptionBinaryPath!; + var mp4InitFile = ""; + var currentKID = ""; + var readInfo = false; // 是否读取过 + var mp4Info = new ParsedMP4Info(); + + // 用户自定义范围导致被跳过的时长 计算字幕偏移使用 + var skippedDur = streamSpec.SkippedDuration ?? 0d; + + Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); + + // 创建文件夹 + if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); + if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); + + var totalCount = segments.Count(); + if (streamSpec.Playlist?.MediaInit != null) { - if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) + totalCount++; + } + + task.MaxValue = totalCount; + task.StartTask(); + + // 开始下载 + Logger.InfoMarkUp(ResString.startDownloading + streamSpec.ToShortString()); + + // 对于CENC,全部自动开启二进制合并 + if (!DownloaderConfig.MyOptions.BinaryMerge && totalCount >= 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method == Common.Enum.EncryptMethod.CENC) + { + DownloaderConfig.MyOptions.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge4}[/]"); + } + + // 下载init + if (streamSpec.Playlist?.MediaInit != null) + { + // 对于fMP4,自动开启二进制合并 + if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) { DownloaderConfig.MyOptions.BinaryMerge = true; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]"); } - if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true)) + var path = Path.Combine(tmpDir, "_init.mp4.tmp"); + var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); + FileDic[streamSpec.Playlist.MediaInit] = result; + if (result == null || !result.Success) { - DownloaderConfig.MyOptions.MuxAfterDone = false; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); + throw new Exception("Download init file failed!"); } + mp4InitFile = result.ActualFilePath; + task.Increment(1); - if (mediainfos.Where(m => m.Type == "Audio").All(m => m.BaseInfo!.Contains("aac"))) + // 读取mp4信息 + if (result != null && result.Success) { - useAACFilter = true; - } - - if (mediainfos.All(m => m.Type == "Audio")) - { - streamSpec.MediaType = MediaType.AUDIO; - } - else if (mediainfos.All(m => m.Type == "Subtitle")) - { - streamSpec.MediaType = MediaType.SUBTITLES; - if (streamSpec.Extension == null || streamSpec.Extension == "ts") - streamSpec.Extension = "vtt"; + mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath); + currentKID = mp4Info.KID; + // try shaka packager, which can handle WebM + if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { + currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); + } + // 从文件读取KEY + await SearchKeyAsync(currentKID); + // 实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS) + { + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); + if (dResult) + { + FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; + } + } + // ffmpeg读取信息 + if (!readInfo) + { + Logger.WarnMarkUp(ResString.readingInfo); + mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath); + mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); + readInfo = true; + } } } - private async Task DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer) + // 计算填零个数 + var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); + + // 下载第一个分片 + if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS) { - speedContainer.ResetVars(); - bool useAACFilter = false; //ffmpeg合并flag - List mediaInfos = new(); - ConcurrentDictionary FileDic = new(); + var seg = segments.First(); + segments = segments.Skip(1); - var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments); - if (segments == null || !segments.Any()) return false; - //单分段尝试切片并行下载 - if (segments.Count() == 1) + var index = seg.Index; + var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); + var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); + FileDic[seg] = result; + if (result == null || !result.Success) { - var splitSegments = await LargeSingleFileSplitUtil.SplitUrlAsync(segments.First(), DownloaderConfig.Headers); - if (splitSegments != null) - { - segments = splitSegments; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.singleFileSplitWarn}[/]"); - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption) - { - DownloaderConfig.MyOptions.MP4RealTimeDecryption = false; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.singleFileRealtimeDecryptWarn}[/]"); - } - } - else speedContainer.SingleSegment = true; + throw new Exception("Download first segment failed!"); } - - var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO; - var dirName = $"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}"; - var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName); - var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName; - var headers = DownloaderConfig.Headers; - - //mp4decrypt - var mp4decrypt = DownloaderConfig.MyOptions.DecryptionBinaryPath!; - var mp4InitFile = ""; - var currentKID = ""; - var readInfo = false; //是否读取过 - var mp4Info = new ParsedMP4Info(); - - //用户自定义范围导致被跳过的时长 计算字幕偏移使用 - var skippedDur = streamSpec.SkippedDuration ?? 0d; - - Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); - - //创建文件夹 - if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); - if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); - - var totalCount = segments.Count(); - if (streamSpec.Playlist?.MediaInit != null) + task.Increment(1); + if (result != null && result.Success) { - totalCount++; - } - - task.MaxValue = totalCount; - task.StartTask(); - - //开始下载 - Logger.InfoMarkUp(ResString.startDownloading + streamSpec.ToShortString()); - - //对于CENC,全部自动开启二进制合并 - if (!DownloaderConfig.MyOptions.BinaryMerge && totalCount >= 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method == Common.Enum.EncryptMethod.CENC) - { - DownloaderConfig.MyOptions.BinaryMerge = true; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge4}[/]"); - } - - //下载init - if (streamSpec.Playlist?.MediaInit != null) - { - //对于fMP4,自动开启二进制合并 - if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) + // 修复MSS init + if (StreamExtractor.ExtractorType == ExtractorType.MSS) { - DownloaderConfig.MyOptions.BinaryMerge = true; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]"); - } - - var path = Path.Combine(tmpDir, "_init.mp4.tmp"); - var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); - FileDic[streamSpec.Playlist.MediaInit] = result; - if (result == null || !result.Success) - { - throw new Exception("Download init file failed!"); - } - mp4InitFile = result.ActualFilePath; - task.Increment(1); - - //读取mp4信息 - if (result != null && result.Success) - { - mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath); - currentKID = mp4Info.KID; - // try shaka packager, which can handle WebM - if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { - currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); - } - //从文件读取KEY - await SearchKeyAsync(currentKID); - //实时解密 - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS) - { - var enc = result.ActualFilePath; - var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); - if (dResult) - { - FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; - } - } - //ffmpeg读取信息 - if (!readInfo) - { - Logger.WarnMarkUp(ResString.readingInfo); - mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath); - mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); - ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); - readInfo = true; - } - } - } - - //计算填零个数 - var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); - - //下载第一个分片 - if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS) - { - var seg = segments.First(); - segments = segments.Skip(1); - - var index = seg.Index; - var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); - var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); - FileDic[seg] = result; - if (result == null || !result.Success) - { - throw new Exception("Download first segment failed!"); - } - task.Increment(1); - if (result != null && result.Success) - { - //修复MSS init - if (StreamExtractor.ExtractorType == ExtractorType.MSS) - { - var processor = new MSSMoovProcessor(streamSpec); - var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath)); - await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header); - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) - { - //需要重新解密init - var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath; - var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); - if (dResult) - { - FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec; - } - } - } - //读取init信息 - if (string.IsNullOrEmpty(currentKID)) - { - currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; - } - // try shaka packager, which can handle WebM - if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { - currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); - } - //从文件读取KEY - await SearchKeyAsync(currentKID); - //实时解密 + var processor = new MSSMoovProcessor(streamSpec); + var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath)); + await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header); if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) { - var enc = result.ActualFilePath; + // 需要重新解密init + var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - mp4Info = MP4DecryptUtil.GetMP4Info(enc); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); if (dResult) { - File.Delete(enc); - result.ActualFilePath = dec; + FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec; } } - if (!readInfo) - { - //ffmpeg读取信息 - Logger.WarnMarkUp(ResString.readingInfo); - mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); - mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); - ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); - readInfo = true; - } } - } - - //开始下载 - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount - }; - await Parallel.ForEachAsync(segments, options, async (seg, _) => - { - var index = seg.Index; - var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); - var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); - FileDic[seg] = result; - if (result != null && result.Success) - task.Increment(1); - //实时解密 - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID)) + // 读取init信息 + if (string.IsNullOrEmpty(currentKID)) + { + currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; + } + // try shaka packager, which can handle WebM + if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { + currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); + } + // 从文件读取KEY + await SearchKeyAsync(currentKID); + // 实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); @@ -298,467 +260,504 @@ namespace N_m3u8DL_RE.DownloadManager result.ActualFilePath = dec; } } - }); - - //修改输出后缀 - var outputExt = "." + streamSpec.Extension; - if (streamSpec.Extension == null) outputExt = ".ts"; - else if (streamSpec.MediaType == MediaType.AUDIO && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".m4a"; - else if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".mp4"; - - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES) - { - if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; - else outputExt = ".vtt"; - } - var output = Path.Combine(saveDir, saveName + outputExt); - - //检测目标文件是否存在 - while (File.Exists(output)) - { - Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); - } - - if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0 && mp4InitFile != "") - { - File.Delete(mp4InitFile); - //shaka实时解密不需要init文件用于合并 - if (DownloaderConfig.MyOptions.UseShakaPackager) + if (!readInfo) { - FileDic!.Remove(streamSpec.Playlist!.MediaInit, out _); + // ffmpeg读取信息 + Logger.WarnMarkUp(ResString.readingInfo); + mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); + mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); + readInfo = true; } } + } - //校验分片数量 - if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null)) + // 开始下载 + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount + }; + await Parallel.ForEachAsync(segments, options, async (seg, _) => + { + var index = seg.Index; + var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); + var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); + FileDic[seg] = result; + if (result != null && result.Success) + task.Increment(1); + // 实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID)) { - Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Where(s => s != null).Count()); - return false; - } - - //移除无效片段 - var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key); - foreach (var badKey in badKeys) - { - FileDic!.Remove(badKey, out _); - } - - //校验完整性 - if (DownloaderConfig.CheckContentLength && FileDic.Values.Any(a => a!.Success == false)) - { - return false; - } - - //自动修复VTT raw字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Extension != null && streamSpec.Extension.Contains("vtt")) - { - Logger.WarnMarkUp(ResString.fixingVTT); - //排序字幕并修正时间戳 - bool first = true; - var finalVtt = new WebVttSub(); - var keys = FileDic.Keys.OrderBy(k => k.Index); - foreach (var seg in keys) - { - var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); - var vtt = WebVttSub.Parse(vttContent); - //手动计算MPEGTS - if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) - { - vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); - } - if (first) { finalVtt = vtt; first = false; } - else finalVtt.AddCuesFromOne(vtt); - } - //写出字幕 - var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); - foreach (var item in files) File.Delete(item); - FileDic.Clear(); - var index = 0; - var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - //设置字幕偏移 - finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); - var subContentFixed = finalVtt.ToVtt(); - //转换字幕格式 - if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) - { - path = Path.ChangeExtension(path, ".srt"); - subContentFixed = finalVtt.ToSrt(); - } - await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); - FileDic[keys.First()] = new DownloadResult() - { - ActualContentLength = subContentFixed.Length, - ActualFilePath = path - }; - } - - //自动修复VTT mp4字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) - { - var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); - var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); - var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); - if (sawVtt) - { - Logger.WarnMarkUp(ResString.fixingVTTmp4); - var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray(); - var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale); - //写出字幕 - var firstKey = FileDic.Keys.First(); - var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); - foreach (var item in files) File.Delete(item); - FileDic.Clear(); - var index = 0; - var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - //设置字幕偏移 - finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); - var subContentFixed = finalVtt.ToVtt(); - //转换字幕格式 - if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) - { - path = Path.ChangeExtension(path, ".srt"); - subContentFixed = finalVtt.ToSrt(); - } - await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); - FileDic[firstKey] = new DownloadResult() - { - ActualContentLength = subContentFixed.Length, - ActualFilePath = path - }; - } - } - - //自动修复TTML raw字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Extension != null && streamSpec.Extension.Contains("ttml")) - { - Logger.WarnMarkUp(ResString.fixingTTML); - var first = true; - var finalVtt = new WebVttSub(); - var keys = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Key); - foreach (var seg in keys) - { - var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0); - //手动计算MPEGTS - if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) - { - vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); - } - if (first) { finalVtt = vtt; first = false; } - else finalVtt.AddCuesFromOne(vtt); - } - //写出字幕 - var firstKey = FileDic.Keys.First(); - var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); - - //处理图形字幕 - await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir); - - foreach (var item in files) File.Delete(item); - FileDic.Clear(); - var index = 0; - var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - //设置字幕偏移 - finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); - var subContentFixed = finalVtt.ToVtt(); - //转换字幕格式 - if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) - { - path = Path.ChangeExtension(path, ".srt"); - subContentFixed = finalVtt.ToSrt(); - } - await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); - FileDic[firstKey] = new DownloadResult() - { - ActualContentLength = subContentFixed.Length, - ActualFilePath = path - }; - } - - //自动修复TTML mp4字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s") - && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) - { - Logger.WarnMarkUp(ResString.fixingTTMLmp4); - //sawTtml暂时不判断 - //var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); - //var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); - //var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); - var first = true; - var finalVtt = new WebVttSub(); - var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); - foreach (var seg in keys) - { - var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0); - //手动计算MPEGTS - if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) - { - vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); - } - if (first) { finalVtt = vtt; first = false; } - else finalVtt.AddCuesFromOne(vtt); - } - - //写出字幕 - var firstKey = FileDic.Keys.First(); - var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); - - //处理图形字幕 - await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir); - - foreach (var item in files) File.Delete(item); - FileDic.Clear(); - var index = 0; - var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); - //设置字幕偏移 - finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); - var subContentFixed = finalVtt.ToVtt(); - //转换字幕格式 - if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) - { - path = Path.ChangeExtension(path, ".srt"); - subContentFixed = finalVtt.ToSrt(); - } - await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); - FileDic[firstKey] = new DownloadResult() - { - ActualContentLength = subContentFixed.Length, - ActualFilePath = path - }; - } - - bool mergeSuccess = false; - //合并 - if (!DownloaderConfig.MyOptions.SkipMerge) - { - //字幕也使用二进制合并 - if (DownloaderConfig.MyOptions.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES) - { - Logger.InfoMarkUp(ResString.binaryMerge); - var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); - MergeUtil.CombineMultipleFilesIntoSingleFile(files, output); - mergeSuccess = true; - } - else - { - //ffmpeg合并 - var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); - Logger.InfoMarkUp(ResString.ffmpegMerge); - var ext = streamSpec.MediaType == MediaType.AUDIO ? "m4a" : "mp4"; - var ffOut = Path.Combine(Path.GetDirectoryName(output)!, Path.GetFileNameWithoutExtension(output) + $".{ext}"); - //检测目标文件是否存在 - while (File.Exists(ffOut)) - { - Logger.WarnMarkUp($"{Path.GetFileName(ffOut)} => {Path.GetFileName(ffOut = Path.ChangeExtension(ffOut, $"copy" + Path.GetExtension(ffOut)))}"); - } - //大于1800分片,需要分步骤合并 - if (files.Length >= 1800) - { - Logger.WarnMarkUp(ResString.partMerge); - files = MergeUtil.PartialCombineMultipleFiles(files); - FileDic.Clear(); - foreach (var item in files) - { - FileDic[new MediaSegment() { Url = item }] = new DownloadResult() - { - ActualFilePath = item - }; - } - } - mergeSuccess = MergeUtil.MergeByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, files, Path.ChangeExtension(ffOut, null), ext, useAACFilter, writeDate: !DownloaderConfig.MyOptions.NoDateInfo, useConcatDemuxer: DownloaderConfig.MyOptions.UseFFmpegConcatDemuxer); - if (mergeSuccess) output = ffOut; - } - } - - //删除临时文件夹 - if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && mergeSuccess) - { - var files = FileDic.Values.Select(v => v!.ActualFilePath); - foreach (var file in files) - { - File.Delete(file); - } - OtherUtil.SafeDeleteDir(tmpDir); - } - - //重新读取init信息 - if (mergeSuccess && totalCount >= 1 && string.IsNullOrEmpty(currentKID) && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) - { - currentKID = MP4DecryptUtil.GetMP4Info(output).KID; - // try shaka packager, which can handle WebM - if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { - currentKID = MP4DecryptUtil.ReadInitShaka(output, mp4decrypt); - } - //从文件读取KEY - await SearchKeyAsync(currentKID); - } - - //调用mp4decrypt解密 - if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && !DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) - { - var enc = output; + var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); mp4Info = MP4DecryptUtil.GetMP4Info(enc); - Logger.InfoMarkUp($"[grey]Decrypting...[/]"); - var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); - if (result) + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM); + if (dResult) { File.Delete(enc); - File.Move(dec, enc); + result.ActualFilePath = dec; } } + }); - //记录所有文件信息 - if (File.Exists(output)) + // 修改输出后缀 + var outputExt = "." + streamSpec.Extension; + if (streamSpec.Extension == null) outputExt = ".ts"; + else if (streamSpec.MediaType == MediaType.AUDIO && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".m4a"; + else if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".mp4"; + + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES) + { + if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; + else outputExt = ".vtt"; + } + var output = Path.Combine(saveDir, saveName + outputExt); + + // 检测目标文件是否存在 + while (File.Exists(output)) + { + Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); + } + + if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0 && mp4InitFile != "") + { + File.Delete(mp4InitFile); + // shaka实时解密不需要init文件用于合并 + if (DownloaderConfig.MyOptions.UseShakaPackager) { - OutputFiles.Add(new OutputFile() + FileDic!.Remove(streamSpec.Playlist!.MediaInit, out _); + } + } + + // 校验分片数量 + if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null)) + { + Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Where(s => s != null).Count()); + return false; + } + + // 移除无效片段 + var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key); + foreach (var badKey in badKeys) + { + FileDic!.Remove(badKey, out _); + } + + // 校验完整性 + if (DownloaderConfig.CheckContentLength && FileDic.Values.Any(a => a!.Success == false)) + { + return false; + } + + // 自动修复VTT raw字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("vtt")) + { + Logger.WarnMarkUp(ResString.fixingVTT); + // 排序字幕并修正时间戳 + bool first = true; + var finalVtt = new WebVttSub(); + var keys = FileDic.Keys.OrderBy(k => k.Index); + foreach (var seg in keys) + { + var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); + var vtt = WebVttSub.Parse(vttContent); + // 手动计算MPEGTS + if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { - Index = task.Id, - FilePath = output, - LangCode = streamSpec.Language, - Description = streamSpec.Name, - Mediainfos = mediaInfos, - MediaType = streamSpec.MediaType, + vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); + } + if (first) { finalVtt = vtt; first = false; } + else finalVtt.AddCuesFromOne(vtt); + } + // 写出字幕 + var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); + foreach (var item in files) File.Delete(item); + FileDic.Clear(); + var index = 0; + var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + // 设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); + var subContentFixed = finalVtt.ToVtt(); + // 转换字幕格式 + if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) + { + path = Path.ChangeExtension(path, ".srt"); + subContentFixed = finalVtt.ToSrt(); + } + await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); + FileDic[keys.First()] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + + // 自动修复VTT mp4字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) + { + var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); + var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); + var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); + if (sawVtt) + { + Logger.WarnMarkUp(ResString.fixingVTTmp4); + var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray(); + var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale); + // 写出字幕 + var firstKey = FileDic.Keys.First(); + var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); + foreach (var item in files) File.Delete(item); + FileDic.Clear(); + var index = 0; + var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + // 设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); + var subContentFixed = finalVtt.ToVtt(); + // 转换字幕格式 + if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) + { + path = Path.ChangeExtension(path, ".srt"); + subContentFixed = finalVtt.ToSrt(); + } + await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); + FileDic[firstKey] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + } + + // 自动修复TTML raw字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("ttml")) + { + Logger.WarnMarkUp(ResString.fixingTTML); + var first = true; + var finalVtt = new WebVttSub(); + var keys = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Key); + foreach (var seg in keys) + { + var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0); + // 手动计算MPEGTS + if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); + } + if (first) { finalVtt = vtt; first = false; } + else finalVtt.AddCuesFromOne(vtt); + } + // 写出字幕 + var firstKey = FileDic.Keys.First(); + var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); + + // 处理图形字幕 + await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir); + + foreach (var item in files) File.Delete(item); + FileDic.Clear(); + var index = 0; + var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + // 设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); + var subContentFixed = finalVtt.ToVtt(); + // 转换字幕格式 + if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) + { + path = Path.ChangeExtension(path, ".srt"); + subContentFixed = finalVtt.ToSrt(); + } + await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); + FileDic[firstKey] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + + // 自动修复TTML mp4字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s") + && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) + { + Logger.WarnMarkUp(ResString.fixingTTMLmp4); + // sawTtml暂时不判断 + // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); + // var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); + // var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); + var first = true; + var finalVtt = new WebVttSub(); + var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); + foreach (var seg in keys) + { + var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0); + // 手动计算MPEGTS + if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); + } + if (first) { finalVtt = vtt; first = false; } + else finalVtt.AddCuesFromOne(vtt); + } + + // 写出字幕 + var firstKey = FileDic.Keys.First(); + var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); + + // 处理图形字幕 + await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir); + + foreach (var item in files) File.Delete(item); + FileDic.Clear(); + var index = 0; + var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); + // 设置字幕偏移 + finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); + var subContentFixed = finalVtt.ToVtt(); + // 转换字幕格式 + if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) + { + path = Path.ChangeExtension(path, ".srt"); + subContentFixed = finalVtt.ToSrt(); + } + await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8); + FileDic[firstKey] = new DownloadResult() + { + ActualContentLength = subContentFixed.Length, + ActualFilePath = path + }; + } + + bool mergeSuccess = false; + // 合并 + if (!DownloaderConfig.MyOptions.SkipMerge) + { + // 字幕也使用二进制合并 + if (DownloaderConfig.MyOptions.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES) + { + Logger.InfoMarkUp(ResString.binaryMerge); + var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); + MergeUtil.CombineMultipleFilesIntoSingleFile(files, output); + mergeSuccess = true; + } + else + { + // ffmpeg合并 + var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); + Logger.InfoMarkUp(ResString.ffmpegMerge); + var ext = streamSpec.MediaType == MediaType.AUDIO ? "m4a" : "mp4"; + var ffOut = Path.Combine(Path.GetDirectoryName(output)!, Path.GetFileNameWithoutExtension(output) + $".{ext}"); + // 检测目标文件是否存在 + while (File.Exists(ffOut)) + { + Logger.WarnMarkUp($"{Path.GetFileName(ffOut)} => {Path.GetFileName(ffOut = Path.ChangeExtension(ffOut, $"copy" + Path.GetExtension(ffOut)))}"); + } + // 大于1800分片,需要分步骤合并 + if (files.Length >= 1800) + { + Logger.WarnMarkUp(ResString.partMerge); + files = MergeUtil.PartialCombineMultipleFiles(files); + FileDic.Clear(); + foreach (var item in files) + { + FileDic[new MediaSegment() { Url = item }] = new DownloadResult() + { + ActualFilePath = item + }; + } + } + mergeSuccess = MergeUtil.MergeByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, files, Path.ChangeExtension(ffOut, null), ext, useAACFilter, writeDate: !DownloaderConfig.MyOptions.NoDateInfo, useConcatDemuxer: DownloaderConfig.MyOptions.UseFFmpegConcatDemuxer); + if (mergeSuccess) output = ffOut; + } + } + + // 删除临时文件夹 + if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && mergeSuccess) + { + var files = FileDic.Values.Select(v => v!.ActualFilePath); + foreach (var file in files) + { + File.Delete(file); + } + OtherUtil.SafeDeleteDir(tmpDir); + } + + // 重新读取init信息 + if (mergeSuccess && totalCount >= 1 && string.IsNullOrEmpty(currentKID) && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) + { + currentKID = MP4DecryptUtil.GetMP4Info(output).KID; + // try shaka packager, which can handle WebM + if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { + currentKID = MP4DecryptUtil.ReadInitShaka(output, mp4decrypt); + } + // 从文件读取KEY + await SearchKeyAsync(currentKID); + } + + // 调用mp4decrypt解密 + if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && !DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) + { + var enc = output; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + mp4Info = MP4DecryptUtil.GetMP4Info(enc); + Logger.InfoMarkUp($"[grey]Decrypting...[/]"); + var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); + if (result) + { + File.Delete(enc); + File.Move(dec, enc); + } + } + + // 记录所有文件信息 + if (File.Exists(output)) + { + OutputFiles.Add(new OutputFile() + { + Index = task.Id, + FilePath = output, + LangCode = streamSpec.Language, + Description = streamSpec.Name, + Mediainfos = mediaInfos, + MediaType = streamSpec.MediaType, + }); + } + + return true; + } + + public async Task StartDownloadAsync() + { + ConcurrentDictionary SpeedContainerDic = new(); // 速度计算 + ConcurrentDictionary Results = new(); + + var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); + progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; + + // 进度条的列定义 + var progressColumns = new ProgressColumn[] + { + new TaskDescriptionColumn() { Alignment = Justify.Left }, + new ProgressBarColumn(){ Width = 30 }, + new MyPercentageColumn(), + new DownloadStatusColumn(SpeedContainerDic), + new DownloadSpeedColumn(SpeedContainerDic), // 速度计算 + new RemainingTimeColumn(), + new SpinnerColumn(), + }; + if (DownloaderConfig.MyOptions.NoAnsiColor) + { + progressColumns = progressColumns.SkipLast(1).ToArray(); + } + progress.Columns(progressColumns); + + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !DownloaderConfig.MyOptions.UseShakaPackager + && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) + Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]"); + + await progress.StartAsync(async ctx => + { + // 创建任务 + var dic = SelectedSteams.Select(item => + { + var description = item.ToShortShortString(); + var task = ctx.AddTask(description, autoStart: false); + SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算 + // 限速设置 + if (DownloaderConfig.MyOptions.MaxSpeed != null) + { + SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value; + } + return (item, task); + }).ToDictionary(item => item.item, item => item.task); + + if (!DownloaderConfig.MyOptions.ConcurrentDownload) + { + // 遍历,顺序下载 + foreach (var kp in dic) + { + var task = kp.Value; + var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]); + Results[kp.Key] = result; + // 失败不再下载后续 + if (!result) break; + } + } + else + { + // 并发下载 + await Parallel.ForEachAsync(dic, async (kp, _) => + { + var task = kp.Value; + var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]); + Results[kp.Key] = result; }); } + }); - return true; - } + var success = Results.Values.All(v => v == true); - public async Task StartDownloadAsync() + // 删除临时文件夹 + if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success) { - ConcurrentDictionary SpeedContainerDic = new(); //速度计算 - ConcurrentDictionary Results = new(); - - var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); - progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; - - //进度条的列定义 - var progressColumns = new ProgressColumn[] + foreach (var item in StreamExtractor.RawFiles) { - new TaskDescriptionColumn() { Alignment = Justify.Left }, - new ProgressBarColumn(){ Width = 30 }, - new MyPercentageColumn(), - new DownloadStatusColumn(SpeedContainerDic), - new DownloadSpeedColumn(SpeedContainerDic), //速度计算 - new RemainingTimeColumn(), - new SpinnerColumn(), - }; - if (DownloaderConfig.MyOptions.NoAnsiColor) - { - progressColumns = progressColumns.SkipLast(1).ToArray(); + var file = Path.Combine(DownloaderConfig.DirPrefix, item.Key); + if (File.Exists(file)) File.Delete(file); } - progress.Columns(progressColumns); - - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !DownloaderConfig.MyOptions.UseShakaPackager - && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) - Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]"); - - await progress.StartAsync(async ctx => - { - //创建任务 - var dic = SelectedSteams.Select(item => - { - var description = item.ToShortShortString(); - var task = ctx.AddTask(description, autoStart: false); - SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 - //限速设置 - if (DownloaderConfig.MyOptions.MaxSpeed != null) - { - SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value; - } - return (item, task); - }).ToDictionary(item => item.item, item => item.task); - - if (!DownloaderConfig.MyOptions.ConcurrentDownload) - { - //遍历,顺序下载 - foreach (var kp in dic) - { - var task = kp.Value; - var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]); - Results[kp.Key] = result; - //失败不再下载后续 - if (!result) break; - } - } - else - { - //并发下载 - await Parallel.ForEachAsync(dic, async (kp, _) => - { - var task = kp.Value; - var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]); - Results[kp.Key] = result; - }); - } - }); - - var success = Results.Values.All(v => v == true); - - //删除临时文件夹 - if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success) - { - foreach (var item in StreamExtractor.RawFiles) - { - var file = Path.Combine(DownloaderConfig.DirPrefix, item.Key); - if (File.Exists(file)) File.Delete(file); - } - OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix); - } - - //混流 - if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) - { - OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); - //是否跳过字幕 - if (DownloaderConfig.MyOptions.MuxOptions.SkipSubtitle) - { - OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); - } - if (DownloaderConfig.MyOptions.MuxImports != null) - { - OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports); - } - OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); - var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat); - var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); - var outName = $"{dirName}.MUX"; - var outPath = Path.Combine(saveDir, outName); - Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); - var result = false; - if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); - else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo); - //完成后删除各轨道文件 - if (result) - { - if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) - { - Logger.WarnMarkUp("[grey]Cleaning files...[/]"); - OutputFiles.ForEach(f => File.Delete(f.FilePath)); - var tmpDir = DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory; - OtherUtil.SafeDeleteDir(tmpDir); - } - } - else - { - success = false; - Logger.ErrorMarkUp($"Mux failed"); - } - //判断是否要改名 - var newPath = Path.ChangeExtension(outPath, ext); - if (result && !File.Exists(newPath)) - { - Logger.WarnMarkUp($"Rename to [grey]{Path.GetFileName(newPath).EscapeMarkup()}[/]"); - File.Move(outPath + ext, newPath); - } - } - - return success; + OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix); } + + // 混流 + if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) + { + OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); + // 是否跳过字幕 + if (DownloaderConfig.MyOptions.MuxOptions!.SkipSubtitle) + { + OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); + } + if (DownloaderConfig.MyOptions.MuxImports != null) + { + OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports); + } + OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); + var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; + var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat); + var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); + var outName = $"{dirName}.MUX"; + var outPath = Path.Combine(saveDir, outName); + Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); + var result = false; + if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); + else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo); + // 完成后删除各轨道文件 + if (result) + { + if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) + { + Logger.WarnMarkUp("[grey]Cleaning files...[/]"); + OutputFiles.ForEach(f => File.Delete(f.FilePath)); + var tmpDir = DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory; + OtherUtil.SafeDeleteDir(tmpDir); + } + } + else + { + success = false; + Logger.ErrorMarkUp($"Mux failed"); + } + // 判断是否要改名 + var newPath = Path.ChangeExtension(outPath, ext); + if (result && !File.Exists(newPath)) + { + Logger.WarnMarkUp($"Rename to [grey]{Path.GetFileName(newPath).EscapeMarkup()}[/]"); + File.Move(outPath + ext, newPath); + } + } + + return success; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs index 80e62e0..e5c07ef 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs @@ -12,333 +12,284 @@ using N_m3u8DL_RE.Parser; using N_m3u8DL_RE.Parser.Mp4; using N_m3u8DL_RE.Util; using Spectre.Console; -using Spectre.Console.Rendering; using System.Collections.Concurrent; -using System.Diagnostics; -using System.IO; using System.IO.Pipes; -using System.Linq; using System.Text; -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 SimpleLiveRecordManager2 { - internal class SimpleLiveRecordManager2 + IDownloader Downloader; + DownloaderConfig DownloaderConfig; + StreamExtractor StreamExtractor; + List SelectedSteams; + ConcurrentDictionary PipeSteamNamesDic = new(); + List OutputFiles = new(); + DateTime? PublishDateTime; + bool STOP_FLAG = false; + int WAIT_SEC = 0; // 刷新间隔 + ConcurrentDictionary RecordedDurDic = new(); // 已录制时长 + ConcurrentDictionary RefreshedDurDic = new(); // 已刷新出的时长 + ConcurrentDictionary>> BlockDic = new(); // 各流的Block + ConcurrentDictionary SamePathDic = new(); // 各流是否allSamePath + ConcurrentDictionary RecordLimitReachedDic = new(); // 各流是否达到上限 + ConcurrentDictionary LastFileNameDic = new(); // 上次下载的文件名 + ConcurrentDictionary MaxIndexDic = new(); // 最大Index + ConcurrentDictionary DateTimeDic = new(); // 上次下载的dateTime + CancellationTokenSource CancellationTokenSource = new(); // 取消Wait + + private readonly object lockObj = new object(); + TimeSpan? audioStart = null; + + public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) { - IDownloader Downloader; - DownloaderConfig DownloaderConfig; - StreamExtractor StreamExtractor; - List SelectedSteams; - ConcurrentDictionary PipeSteamNamesDic = new(); - List OutputFiles = new(); - DateTime? PublishDateTime; - bool STOP_FLAG = false; - int WAIT_SEC = 0; //刷新间隔 - ConcurrentDictionary RecordedDurDic = new(); //已录制时长 - ConcurrentDictionary RefreshedDurDic = new(); //已刷新出的时长 - ConcurrentDictionary>> BlockDic = new(); //各流的Block - ConcurrentDictionary SamePathDic = new(); //各流是否allSamePath - ConcurrentDictionary RecordLimitReachedDic = new(); //各流是否达到上限 - ConcurrentDictionary LastFileNameDic = new(); //上次下载的文件名 - ConcurrentDictionary MaxIndexDic = new(); //最大Index - ConcurrentDictionary DateTimeDic = new(); //上次下载的dateTime - CancellationTokenSource CancellationTokenSource = new(); //取消Wait + this.DownloaderConfig = downloaderConfig; + Downloader = new SimpleDownloader(DownloaderConfig); + PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime; + StreamExtractor = streamExtractor; + SelectedSteams = selectedSteams; + } - private readonly object lockObj = new object(); - TimeSpan? audioStart = null; - - public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List selectedSteams, StreamExtractor streamExtractor) + // 从文件读取KEY + private async Task SearchKeyAsync(string? currentKID) + { + var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID); + if (_key != null) { - this.DownloaderConfig = downloaderConfig; - Downloader = new SimpleDownloader(DownloaderConfig); - PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime; - StreamExtractor = streamExtractor; - SelectedSteams = selectedSteams; + if (DownloaderConfig.MyOptions.Keys == null) + DownloaderConfig.MyOptions.Keys = new string[] { _key }; + else + DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray(); + } + } + + /// + /// 获取时间戳 + /// + /// + /// + private long GetUnixTimestamp(DateTime dateTime) + { + return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds(); + } + + /// + /// 获取分段文件夹 + /// + /// + /// + /// + private string GetSegmentName(MediaSegment segment, bool allHasDatetime, bool allSamePath) + { + if (!string.IsNullOrEmpty(segment.NameFromVar)) + { + return segment.NameFromVar; } - //从文件读取KEY - private async Task SearchKeyAsync(string? currentKID) + bool hls = StreamExtractor.ExtractorType == ExtractorType.HLS; + + string name = OtherUtil.GetFileNameFromInput(segment.Url, false); + if (allSamePath) { - var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID); - if (_key != null) - { - if (DownloaderConfig.MyOptions.Keys == null) - DownloaderConfig.MyOptions.Keys = new string[] { _key }; - else - DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray(); - } + name = OtherUtil.GetValidFileName(segment.Url.Split('?').Last(), "_"); } - /// - /// 获取时间戳 - /// - /// - /// - private long GetUnixTimestamp(DateTime dateTime) + if (hls && allHasDatetime) { - return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds(); + name = GetUnixTimestamp(segment.DateTime!.Value).ToString(); + } + else if (hls) + { + name = segment.Index.ToString(); } - /// - /// 获取分段文件夹 - /// - /// - /// - /// - private string GetSegmentName(MediaSegment segment, bool allHasDatetime, bool allSamePath) + return name; + } + + private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos, ref bool useAACFilter) + { + if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) { - if (!string.IsNullOrEmpty(segment.NameFromVar)) - { - return segment.NameFromVar; - } - - bool hls = StreamExtractor.ExtractorType == ExtractorType.HLS; - - string name = OtherUtil.GetFileNameFromInput(segment.Url, false); - if (allSamePath) - { - name = OtherUtil.GetValidFileName(segment.Url.Split('?').Last(), "_"); - } - - if (hls && allHasDatetime) - { - name = GetUnixTimestamp(segment.DateTime!.Value).ToString(); - } - else if (hls) - { - name = segment.Index.ToString(); - } - - return name; + DownloaderConfig.MyOptions.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); } - private void ChangeSpecInfo(StreamSpec streamSpec, List mediainfos, ref bool useAACFilter) + if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true)) { - if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) - { - DownloaderConfig.MyOptions.BinaryMerge = true; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); - } - - if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true)) - { - DownloaderConfig.MyOptions.MuxAfterDone = false; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); - } - - if (mediainfos.Where(m => m.Type == "Audio").All(m => m.BaseInfo!.Contains("aac"))) - { - useAACFilter = true; - } - - if (mediainfos.All(m => m.Type == "Audio") && streamSpec.MediaType != MediaType.AUDIO) - { - streamSpec.MediaType = MediaType.AUDIO; - } - else if (mediainfos.All(m => m.Type == "Subtitle") && streamSpec.MediaType != MediaType.SUBTITLES) - { - streamSpec.MediaType = MediaType.SUBTITLES; - - if (streamSpec.Extension == null || streamSpec.Extension == "ts") - streamSpec.Extension = "vtt"; - } + DownloaderConfig.MyOptions.MuxAfterDone = false; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); } - private async Task RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer, BufferBlock> source) + if (mediainfos.Where(m => m.Type == "Audio").All(m => m.BaseInfo!.Contains("aac"))) { - var baseTimestamp = PublishDateTime == null ? 0L : (long)(PublishDateTime.Value.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds; - //mp4decrypt - var mp4decrypt = DownloaderConfig.MyOptions.DecryptionBinaryPath!; - var mp4InitFile = ""; - var currentKID = ""; - var readInfo = false; //是否读取过 - bool useAACFilter = false; //ffmpeg合并flag - bool initDownloaded = false; //是否下载过init文件 - ConcurrentDictionary FileDic = new(); - List mediaInfos = new(); - Stream? fileOutputStream = null; - WebVttSub currentVtt = new(); //字幕流始终维护一个实例 - bool firstSub = true; - task.StartTask(); + useAACFilter = true; + } - var name = streamSpec.ToShortString(); - var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO; - var dirName = $"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}"; - var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName); - var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName; - var headers = DownloaderConfig.Headers; + if (mediainfos.All(m => m.Type == "Audio") && streamSpec.MediaType != MediaType.AUDIO) + { + streamSpec.MediaType = MediaType.AUDIO; + } + else if (mediainfos.All(m => m.Type == "Subtitle") && streamSpec.MediaType != MediaType.SUBTITLES) + { + streamSpec.MediaType = MediaType.SUBTITLES; - Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); + if (streamSpec.Extension == null || streamSpec.Extension == "ts") + streamSpec.Extension = "vtt"; + } + } - //创建文件夹 - if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); - if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); + private async Task RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer, BufferBlock> source) + { + var baseTimestamp = PublishDateTime == null ? 0L : (long)(PublishDateTime.Value.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds; + // mp4decrypt + var mp4decrypt = DownloaderConfig.MyOptions.DecryptionBinaryPath!; + var mp4InitFile = ""; + var currentKID = ""; + var readInfo = false; // 是否读取过 + bool useAACFilter = false; // ffmpeg合并flag + bool initDownloaded = false; // 是否下载过init文件 + ConcurrentDictionary FileDic = new(); + List mediaInfos = new(); + Stream? fileOutputStream = null; + WebVttSub currentVtt = new(); // 字幕流始终维护一个实例 + bool firstSub = true; + task.StartTask(); - while (true && await source.OutputAvailableAsync()) + var name = streamSpec.ToShortString(); + var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO; + var dirName = $"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}"; + var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName); + var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; + var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName; + var headers = DownloaderConfig.Headers; + + Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); + + // 创建文件夹 + if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); + if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); + + while (true && await source.OutputAvailableAsync()) + { + // 接收新片段 且总是拿全部未处理的片段 + // 有时每次只有很少的片段,但是之前的片段下载慢,导致后面还没下载的片段都失效了 + // TryReceiveAll可以稍微缓解一下 + source.TryReceiveAll(out IList>? segmentsList); + var segments = segmentsList!.SelectMany(s => s); + if (segments == null || !segments.Any()) continue; + var segmentsDuration = segments.Sum(s => s.Duration); + Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false)))); + + // 下载init + if (!initDownloaded && streamSpec.Playlist?.MediaInit != null) { - //接收新片段 且总是拿全部未处理的片段 - //有时每次只有很少的片段,但是之前的片段下载慢,导致后面还没下载的片段都失效了 - //TryReceiveAll可以稍微缓解一下 - source.TryReceiveAll(out IList>? segmentsList); - var segments = segmentsList!.SelectMany(s => s); - if (segments == null || !segments.Any()) continue; - var segmentsDuration = segments.Sum(s => s.Duration); - Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false)))); - - //下载init - if (!initDownloaded && streamSpec.Playlist?.MediaInit != null) + task.MaxValue += 1; + // 对于fMP4,自动开启二进制合并 + if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) { - task.MaxValue += 1; - //对于fMP4,自动开启二进制合并 - if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) - { - DownloaderConfig.MyOptions.BinaryMerge = true; - Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]"); - } + DownloaderConfig.MyOptions.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]"); + } - var path = Path.Combine(tmpDir, "_init.mp4.tmp"); - var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); - FileDic[streamSpec.Playlist.MediaInit] = result; - if (result == null || !result.Success) - { - throw new Exception("Download init file failed!"); - } - mp4InitFile = result.ActualFilePath; - task.Increment(1); + var path = Path.Combine(tmpDir, "_init.mp4.tmp"); + var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); + FileDic[streamSpec.Playlist.MediaInit] = result; + if (result == null || !result.Success) + { + throw new Exception("Download init file failed!"); + } + mp4InitFile = result.ActualFilePath; + task.Increment(1); - //读取mp4信息 - if (result != null && result.Success) + // 读取mp4信息 + if (result != null && result.Success) + { + currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; + // 从文件读取KEY + await SearchKeyAsync(currentKID); + // 实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS) { - currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; - //从文件读取KEY - await SearchKeyAsync(currentKID); - //实时解密 - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS) + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); + if (dResult) { - var enc = result.ActualFilePath; + FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; + } + } + // ffmpeg读取信息 + if (!readInfo) + { + Logger.WarnMarkUp(ResString.readingInfo); + mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath); + mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + lock (lockObj) + { + if (audioStart == null) audioStart = mediaInfos.FirstOrDefault(x => x.Type == "Audio")?.StartTime; + } + ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); + readInfo = true; + } + initDownloaded = true; + } + } + + var allHasDatetime = segments.All(s => s.DateTime != null); + if (!SamePathDic.ContainsKey(task.Id)) + { + var allName = segments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false)); + var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1; + SamePathDic[task.Id] = allSamePath; + } + + // 下载第一个分片 + if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS) + { + var seg = segments.First(); + segments = segments.Skip(1); + // 获取文件名 + var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]); + var index = seg.Index; + var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); + var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); + FileDic[seg] = result; + if (result == null || !result.Success) + { + throw new Exception("Download first segment failed!"); + } + task.Increment(1); + if (result != null && result.Success) + { + // 修复MSS init + if (StreamExtractor.ExtractorType == ExtractorType.MSS) + { + var processor = new MSSMoovProcessor(streamSpec); + var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath)); + await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header); + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) + { + // 需要重新解密init + var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); if (dResult) { - FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; + FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec; } } - //ffmpeg读取信息 - if (!readInfo) - { - Logger.WarnMarkUp(ResString.readingInfo); - mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath); - mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); - lock (lockObj) - { - if (audioStart == null) audioStart = mediaInfos.FirstOrDefault(x => x.Type == "Audio")?.StartTime; - } - ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); - readInfo = true; - } - initDownloaded = true; } - } - - var allHasDatetime = segments.All(s => s.DateTime != null); - if (!SamePathDic.ContainsKey(task.Id)) - { - var allName = segments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false)); - var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1; - SamePathDic[task.Id] = allSamePath; - } - - //下载第一个分片 - if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS) - { - var seg = segments.First(); - segments = segments.Skip(1); - //获取文件名 - var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]); - var index = seg.Index; - var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); - var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); - FileDic[seg] = result; - if (result == null || !result.Success) + // 读取init信息 + if (string.IsNullOrEmpty(currentKID)) { - throw new Exception("Download first segment failed!"); + currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; } - task.Increment(1); - if (result != null && result.Success) - { - //修复MSS init - if (StreamExtractor.ExtractorType == ExtractorType.MSS) - { - var processor = new MSSMoovProcessor(streamSpec); - var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath)); - await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header); - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) - { - //需要重新解密init - var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath; - var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); - if (dResult) - { - FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec; - } - } - } - //读取init信息 - if (string.IsNullOrEmpty(currentKID)) - { - currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; - } - //从文件读取KEY - await SearchKeyAsync(currentKID); - //实时解密 - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) - { - var enc = result.ActualFilePath; - var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); - var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); - if (dResult) - { - File.Delete(enc); - result.ActualFilePath = dec; - } - } - if (!readInfo) - { - //ffmpeg读取信息 - Logger.WarnMarkUp(ResString.readingInfo); - mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); - mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); - lock (lockObj) - { - if (audioStart == null) audioStart = mediaInfos.FirstOrDefault(x => x.Type == "Audio")?.StartTime; - } - ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); - readInfo = true; - } - } - } - - //开始下载 - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount - }; - await Parallel.ForEachAsync(segments, options, async (seg, _) => - { - //获取文件名 - var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]); - var index = seg.Index; - var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); - var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); - FileDic[seg] = result; - if (result != null && result.Success) - task.Increment(1); - //实时解密 - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID)) + // 从文件读取KEY + await SearchKeyAsync(currentKID); + // 实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) { var enc = result.ActualFilePath; var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); @@ -349,587 +300,629 @@ namespace N_m3u8DL_RE.DownloadManager result.ActualFilePath = dec; } } - }); + if (!readInfo) + { + // ffmpeg读取信息 + Logger.WarnMarkUp(ResString.readingInfo); + mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); + mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); + lock (lockObj) + { + if (audioStart == null) audioStart = mediaInfos.FirstOrDefault(x => x.Type == "Audio")?.StartTime; + } + ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter); + readInfo = true; + } + } + } - //自动修复VTT raw字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Extension != null && streamSpec.Extension.Contains("vtt")) + // 开始下载 + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount + }; + await Parallel.ForEachAsync(segments, options, async (seg, _) => + { + // 获取文件名 + var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]); + var index = seg.Index; + var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); + var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); + FileDic[seg] = result; + if (result != null && result.Success) + task.Increment(1); + // 实时解密 + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID)) { - //排序字幕并修正时间戳 - var keys = FileDic.Keys.OrderBy(k => k.Index); + var enc = result.ActualFilePath; + var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); + var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); + if (dResult) + { + File.Delete(enc); + result.ActualFilePath = dec; + } + } + }); + + // 自动修复VTT raw字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("vtt")) + { + // 排序字幕并修正时间戳 + var keys = FileDic.Keys.OrderBy(k => k.Index); + foreach (var seg in keys) + { + var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); + var waitCount = 0; + while (DownloaderConfig.MyOptions.LiveFixVttByAudio && audioStart == null && waitCount++ < 5) + { + await Task.Delay(1000); + } + var subOffset = audioStart != null ? (long)audioStart.Value.TotalMilliseconds : 0L; + var vtt = WebVttSub.Parse(vttContent, subOffset); + // 手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); + } + if (firstSub) { currentVtt = vtt; firstSub = false; } + else currentVtt.AddCuesFromOne(vtt); + } + } + + // 自动修复VTT mp4字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) + { + var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); + var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); + var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); + if (sawVtt) + { + var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray(); + if (firstSub) + { + currentVtt = MP4VttUtil.ExtractSub(mp4s, timescale); + firstSub = false; + } + else + { + var vtt = MP4VttUtil.ExtractSub(mp4s, timescale); + currentVtt.AddCuesFromOne(vtt); + } + } + } + + // 自动修复TTML raw字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("ttml")) + { + var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); + if (firstSub) + { + if (baseTimestamp != 0) + { + var total = segmentsDuration; + baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; + } + var first = true; foreach (var seg in keys) { - var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); - var waitCount = 0; - while (DownloaderConfig.MyOptions.LiveFixVttByAudio && audioStart == null && waitCount++ < 5) - { - await Task.Delay(1000); - } - var subOffset = audioStart != null ? (long)audioStart.Value.TotalMilliseconds : 0L; - var vtt = WebVttSub.Parse(vttContent, subOffset); - //手动计算MPEGTS + var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); + // 手动计算MPEGTS if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); } - if (firstSub) { currentVtt = vtt; firstSub = false; } + if (first) { currentVtt = vtt; first = false; } else currentVtt.AddCuesFromOne(vtt); } + firstSub = false; } - - //自动修复VTT mp4字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) + else { - var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); - var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); - var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); - if (sawVtt) + foreach (var seg in keys) { - var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray(); - if (firstSub) + var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); + // 手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { - currentVtt = MP4VttUtil.ExtractSub(mp4s, timescale); - firstSub = false; - } - else - { - var vtt = MP4VttUtil.ExtractSub(mp4s, timescale); - currentVtt.AddCuesFromOne(vtt); + vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); } + currentVtt.AddCuesFromOne(vtt); } } - - //自动修复TTML raw字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Extension != null && streamSpec.Extension.Contains("ttml")) - { - var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); - if (firstSub) - { - if (baseTimestamp != 0) - { - var total = segmentsDuration; - baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; - } - var first = true; - foreach (var seg in keys) - { - var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); - //手动计算MPEGTS - if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) - { - vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); - } - if (first) { currentVtt = vtt; first = false; } - else currentVtt.AddCuesFromOne(vtt); - } - firstSub = false; - } - else - { - foreach (var seg in keys) - { - var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); - //手动计算MPEGTS - if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) - { - vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); - } - currentVtt.AddCuesFromOne(vtt); - } - } - } - - //自动修复TTML mp4字幕 - if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES - && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s") - && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) - { - //sawTtml暂时不判断 - //var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); - //var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); - //var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); - var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); - if (firstSub) - { - if (baseTimestamp != 0) - { - var total = segmentsDuration; - baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; - } - var first = true; - foreach (var seg in keys) - { - var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); - //手动计算MPEGTS - if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) - { - vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); - } - if (first) { currentVtt = vtt; first = false; } - else currentVtt.AddCuesFromOne(vtt); - } - firstSub = false; - } - else - { - foreach (var seg in keys) - { - var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); - //手动计算MPEGTS - if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) - { - vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); - } - currentVtt.AddCuesFromOne(vtt); - } - } - } - - RecordedDurDic[task.Id] += (int)segmentsDuration; - - /*//写出m3u8 - if (DownloaderConfig.MyOptions.LiveWriteHLS) - { - var _saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var _saveName = DownloaderConfig.MyOptions.SaveName ?? DateTime.Now.ToString("yyyyMMddHHmmss"); - await StreamingUtil.WriteStreamListAsync(FileDic, task.Id, 0, _saveName, _saveDir); - }*/ - - //合并逻辑 - if (DownloaderConfig.MyOptions.LiveRealTimeMerge) - { - //合并 - var outputExt = "." + streamSpec.Extension; - if (streamSpec.Extension == null) outputExt = ".ts"; - else if (streamSpec.MediaType == MediaType.AUDIO && streamSpec.Extension == "m4s") outputExt = ".m4a"; - else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension == "m4s") outputExt = ".mp4"; - else if (streamSpec.MediaType == MediaType.SUBTITLES) - { - if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; - else outputExt = ".vtt"; - } - - var output = Path.Combine(saveDir, saveName + outputExt); - - //移除无效片段 - var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key); - foreach (var badKey in badKeys) - { - FileDic!.Remove(badKey, out _); - } - - //设置输出流 - if (fileOutputStream == null) - { - //检测目标文件是否存在 - while (File.Exists(output)) - { - Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); - } - - if (!DownloaderConfig.MyOptions.LivePipeMux || streamSpec.MediaType == MediaType.SUBTITLES) - { - fileOutputStream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); - } - else - { - //创建管道 - output = Path.ChangeExtension(output, ".ts"); - var pipeName = $"RE_pipe_{Guid.NewGuid()}"; - fileOutputStream = PipeUtil.CreatePipe(pipeName); - Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]"); - PipeSteamNamesDic[task.Id] = pipeName; - if (PipeSteamNamesDic.Count == SelectedSteams.Where(x => x.MediaType != MediaType.SUBTITLES).Count()) - { - var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray(); - Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]"); - var t = PipeUtil.StartPipeMuxAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, names, output); - } - - //Windows only - if (OperatingSystem.IsWindows()) - await (fileOutputStream as NamedPipeServerStream)!.WaitForConnectionAsync(); - } - } - - if (streamSpec.MediaType != MediaType.SUBTITLES) - { - var initResult = streamSpec.Playlist!.MediaInit != null ? FileDic[streamSpec.Playlist!.MediaInit!]! : null; - var files = FileDic.Where(f => f.Key != streamSpec.Playlist!.MediaInit).OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray(); - if (initResult != null && mp4InitFile != "") - { - //shaka实时解密不需要init文件用于合并,mp4decrpyt需要 - if (!DownloaderConfig.MyOptions.UseShakaPackager) - { - files = new string[] { initResult.ActualFilePath }.Concat(files).ToArray(); - } - } - foreach (var inputFilePath in files) - { - using (var inputStream = File.OpenRead(inputFilePath)) - { - inputStream.CopyTo(fileOutputStream); - } - } - if (!DownloaderConfig.MyOptions.LiveKeepSegments) - { - foreach (var inputFilePath in files.Where(x => !Path.GetFileName(x).StartsWith("_init"))) - { - File.Delete(inputFilePath); - } - } - FileDic.Clear(); - if (initResult != null) - { - FileDic[streamSpec.Playlist!.MediaInit!] = initResult; - } - } - else - { - var initResult = streamSpec.Playlist!.MediaInit != null ? FileDic[streamSpec.Playlist!.MediaInit!]! : null; - var files = FileDic.OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray(); - foreach (var inputFilePath in files) - { - if (!DownloaderConfig.MyOptions.LiveKeepSegments && !Path.GetFileName(inputFilePath).StartsWith("_init")) - { - File.Delete(inputFilePath); - } - } - - //处理图形字幕 - await SubtitleUtil.TryWriteImagePngsAsync(currentVtt, tmpDir); - - var subText = currentVtt.ToVtt(); - if (outputExt == ".srt") - { - subText = currentVtt.ToSrt(); - } - var subBytes = Encoding.UTF8.GetBytes(subText); - fileOutputStream.Position = 0; - fileOutputStream.Write(subBytes); - FileDic.Clear(); - if (initResult != null) - { - FileDic[streamSpec.Playlist!.MediaInit!] = initResult; - } - } - - //刷新buffer - if (fileOutputStream != null) - { - fileOutputStream.Flush(); - } - } - - if (STOP_FLAG && source.Count == 0) - break; } - if (fileOutputStream != null) + // 自动修复TTML mp4字幕 + if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES + && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s") + && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) { - if (!DownloaderConfig.MyOptions.LivePipeMux) + // sawTtml暂时不判断 + // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); + // var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); + // var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); + var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); + if (firstSub) { - //记录所有文件信息 - OutputFiles.Add(new OutputFile() + if (baseTimestamp != 0) { - Index = task.Id, - FilePath = (fileOutputStream as FileStream)!.Name, - LangCode = streamSpec.Language, - Description = streamSpec.Name, - Mediainfos = mediaInfos, - MediaType = streamSpec.MediaType, - }); + var total = segmentsDuration; + baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds; + } + var first = true; + foreach (var seg in keys) + { + var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); + // 手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) + { + vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); + } + if (first) { currentVtt = vtt; first = false; } + else currentVtt.AddCuesFromOne(vtt); + } + firstSub = false; } - fileOutputStream.Close(); - fileOutputStream.Dispose(); - } - - return true; - } - - private async Task PlayListProduceAsync(Dictionary dic) - { - while (!STOP_FLAG) - { - if (WAIT_SEC != 0) + else { - //1. MPD 所有URL相同 单次请求即可获得所有轨道的信息 - //2. M3U8 所有URL不同 才需要多次请求 - - await Parallel.ForEachAsync(dic, async (dic, _) => + foreach (var seg in keys) { - var streamSpec = dic.Key; - var task = dic.Value; - - //达到上限时 不需要刷新了 - if (RecordLimitReachedDic[task.Id]) - return; - - var allHasDatetime = streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null); - if (!SamePathDic.ContainsKey(task.Id)) + var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); + // 手动计算MPEGTS + if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) { - var allName = streamSpec.Playlist!.MediaParts[0].MediaSegments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false)); - var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1; - SamePathDic[task.Id] = allSamePath; - } - //过滤不需要下载的片段 - FilterMediaSegments(streamSpec, task, allHasDatetime, SamePathDic[task.Id]); - var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments; - if (newList.Count > 0) - { - task.MaxValue += newList.Count; - //推送给消费者 - await BlockDic[task.Id].SendAsync(newList); - //更新最新链接 - LastFileNameDic[task.Id] = GetSegmentName(newList.Last(), allHasDatetime, SamePathDic[task.Id]); - //尝试更新时间戳 - var dt = newList.Last().DateTime; - DateTimeDic[task.Id] = dt != null ? GetUnixTimestamp(dt.Value) : 0L; - //累加已获取到的时长 - RefreshedDurDic[task.Id] += (int)newList.Sum(s => s.Duration); - } - - if (!STOP_FLAG && RefreshedDurDic[task.Id] >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds) - { - RecordLimitReachedDic[task.Id] = true; - } - - //检测时长限制 - if (!STOP_FLAG && RecordLimitReachedDic.Values.All(x => x == true)) - { - Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]"); - STOP_FLAG = true; - CancellationTokenSource.Cancel(); - } - }); - - try - { - //Logger.WarnMarkUp($"wait {waitSec}s"); - if (!STOP_FLAG) await Task.Delay(WAIT_SEC * 1000, CancellationTokenSource.Token); - //刷新列表 - if (!STOP_FLAG) await StreamExtractor.RefreshPlayListAsync(dic.Keys.ToList()); - } - catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token) - { - //不需要做事 - } - catch (Exception e) - { - Logger.ErrorMarkUp(e); - STOP_FLAG = true; - //停止所有Block - foreach (var target in BlockDic.Values) - { - target.Complete(); + vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); } + currentVtt.AddCuesFromOne(vtt); } } } - } - private void FilterMediaSegments(StreamSpec streamSpec, ProgressTask task, bool allHasDatetime, bool allSamePath) - { - if (string.IsNullOrEmpty(LastFileNameDic[task.Id]) && DateTimeDic[task.Id] == 0) return; + RecordedDurDic[task.Id] += (int)segmentsDuration; - var index = -1; - var dateTime = DateTimeDic[task.Id]; - var lastName = LastFileNameDic[task.Id]; - - //优先使用dateTime判断 - if (dateTime != 0 && streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null)) - { - index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetUnixTimestamp(s.DateTime!.Value) == dateTime); - } - else - { - index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetSegmentName(s, allHasDatetime, allSamePath) == lastName); - } - - if (index > -1) - { - //修正Index - var list = streamSpec.Playlist!.MediaParts[0].MediaSegments.Skip(index + 1).ToList(); - if (list.Count > 0) - { - var newMin = list.Min(s => s.Index); - var oldMax = MaxIndexDic[task.Id]; - if (newMin < oldMax) - { - var offset = oldMax - newMin + 1; - foreach (var item in list) - { - item.Index += offset; - } - } - MaxIndexDic[task.Id] = list.Max(s => s.Index); - } - streamSpec.Playlist!.MediaParts[0].MediaSegments = list; - } - } - - public async Task StartRecordAsync() - { - var takeLastCount = DownloaderConfig.MyOptions.LiveTakeCount; - ConcurrentDictionary SpeedContainerDic = new(); //速度计算 - ConcurrentDictionary Results = new(); - //同步流 - FilterUtil.SyncStreams(SelectedSteams, takeLastCount); - //设置等待时间 - if (WAIT_SEC == 0) - { - WAIT_SEC = (int)(SelectedSteams.Min(s => s.Playlist!.MediaParts[0].MediaSegments.Sum(s => s.Duration)) / 2); - WAIT_SEC -= 2; //再提前两秒吧 留出冗余 - if (DownloaderConfig.MyOptions.LiveWaitTime != null) - WAIT_SEC = DownloaderConfig.MyOptions.LiveWaitTime.Value; - if (WAIT_SEC <= 0) WAIT_SEC = 1; - Logger.WarnMarkUp($"set refresh interval to {WAIT_SEC} seconds"); - } - //如果没有选中音频 取消通过音频修复vtt时间轴 - if (!SelectedSteams.Any(x => x.MediaType == MediaType.AUDIO)) - { - DownloaderConfig.MyOptions.LiveFixVttByAudio = false; - } - - /*//写出master + /*// 写出m3u8 if (DownloaderConfig.MyOptions.LiveWriteHLS) { - var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var saveName = DownloaderConfig.MyOptions.SaveName ?? DateTime.Now.ToString("yyyyMMddHHmmss"); - await StreamingUtil.WriteMasterListAsync(SelectedSteams, saveName, saveDir); + var _saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; + var _saveName = DownloaderConfig.MyOptions.SaveName ?? DateTime.Now.ToString("yyyyMMddHHmmss"); + await StreamingUtil.WriteStreamListAsync(FileDic, task.Id, 0, _saveName, _saveDir); }*/ - var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); - progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; - - //进度条的列定义 - var progressColumns = new ProgressColumn[] + // 合并逻辑 + if (DownloaderConfig.MyOptions.LiveRealTimeMerge) { - new TaskDescriptionColumn() { Alignment = Justify.Left }, - new RecordingDurationColumn(RecordedDurDic, RefreshedDurDic), //时长显示 - new RecordingStatusColumn(), - new PercentageColumn(), - new DownloadSpeedColumn(SpeedContainerDic), //速度计算 - new SpinnerColumn(), - }; - if (DownloaderConfig.MyOptions.NoAnsiColor) - { - progressColumns = progressColumns.SkipLast(1).ToArray(); - } - progress.Columns(progressColumns); - - await progress.StartAsync(async ctx => - { - //创建任务 - var dic = SelectedSteams.Select(item => + // 合并 + var outputExt = "." + streamSpec.Extension; + if (streamSpec.Extension == null) outputExt = ".ts"; + else if (streamSpec.MediaType == MediaType.AUDIO && streamSpec.Extension == "m4s") outputExt = ".m4a"; + else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension == "m4s") outputExt = ".mp4"; + else if (streamSpec.MediaType == MediaType.SUBTITLES) { - var task = ctx.AddTask(item.ToShortShortString(), autoStart: false, maxValue: 0); - SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 - //限速设置 - if (DownloaderConfig.MyOptions.MaxSpeed != null) + if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; + else outputExt = ".vtt"; + } + + var output = Path.Combine(saveDir, saveName + outputExt); + + // 移除无效片段 + var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key); + foreach (var badKey in badKeys) + { + FileDic!.Remove(badKey, out _); + } + + // 设置输出流 + if (fileOutputStream == null) + { + // 检测目标文件是否存在 + while (File.Exists(output)) { - SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value; + Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); } - LastFileNameDic[task.Id] = ""; - RecordLimitReachedDic[task.Id] = false; - DateTimeDic[task.Id] = 0L; - RecordedDurDic[task.Id] = 0; - RefreshedDurDic[task.Id] = 0; - MaxIndexDic[task.Id] = item.Playlist?.MediaParts[0].MediaSegments.LastOrDefault()?.Index ?? 0L; //最大Index - BlockDic[task.Id] = new BufferBlock>(); - return (item, task); - }).ToDictionary(item => item.item, item => item.task); - DownloaderConfig.MyOptions.ConcurrentDownload = true; - DownloaderConfig.MyOptions.MP4RealTimeDecryption = true; - DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; - if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !DownloaderConfig.MyOptions.UseShakaPackager - && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) - Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]"); - var limit = DownloaderConfig.MyOptions.LiveRecordLimit; - if (limit != TimeSpan.MaxValue) - Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]"); - //录制直播时,用户选了几个流就并发录几个 - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = SelectedSteams.Count - }; - //开始刷新 - var producerTask = PlayListProduceAsync(dic); - await Task.Delay(200); - //并发下载 - await Parallel.ForEachAsync(dic, options, async (kp, _) => - { - var task = kp.Value; - var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id], BlockDic[task.Id]); - Results[kp.Key] = await consumerTask; - }); - }); - - var success = Results.Values.All(v => v == true); - - //删除临时文件夹 - if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success) - { - foreach (var item in StreamExtractor.RawFiles) - { - var file = Path.Combine(DownloaderConfig.DirPrefix, item.Key); - if (File.Exists(file)) File.Delete(file); - } - OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix); - } - - //混流 - if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) - { - OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); - //是否跳过字幕 - if (DownloaderConfig.MyOptions.MuxOptions.SkipSubtitle) - { - OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); - } - if (DownloaderConfig.MyOptions.MuxImports != null) - { - OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports); - } - OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); - var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; - var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat); - var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); - var outName = $"{dirName}.MUX"; - var outPath = Path.Combine(saveDir, outName); - Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); - var result = false; - if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); - else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo); - //完成后删除各轨道文件 - if (result) - { - if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) + if (!DownloaderConfig.MyOptions.LivePipeMux || streamSpec.MediaType == MediaType.SUBTITLES) { - Logger.WarnMarkUp("[grey]Cleaning files...[/]"); - OutputFiles.ForEach(f => File.Delete(f.FilePath)); - var tmpDir = DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory; - OtherUtil.SafeDeleteDir(tmpDir); + fileOutputStream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + } + else + { + // 创建管道 + output = Path.ChangeExtension(output, ".ts"); + var pipeName = $"RE_pipe_{Guid.NewGuid()}"; + fileOutputStream = PipeUtil.CreatePipe(pipeName); + Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]"); + PipeSteamNamesDic[task.Id] = pipeName; + if (PipeSteamNamesDic.Count == SelectedSteams.Where(x => x.MediaType != MediaType.SUBTITLES).Count()) + { + var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray(); + Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]"); + var t = PipeUtil.StartPipeMuxAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, names, output); + } + + // Windows only + if (OperatingSystem.IsWindows()) + await (fileOutputStream as NamedPipeServerStream)!.WaitForConnectionAsync(); + } + } + + if (streamSpec.MediaType != MediaType.SUBTITLES) + { + var initResult = streamSpec.Playlist!.MediaInit != null ? FileDic[streamSpec.Playlist!.MediaInit!]! : null; + var files = FileDic.Where(f => f.Key != streamSpec.Playlist!.MediaInit).OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray(); + if (initResult != null && mp4InitFile != "") + { + // shaka实时解密不需要init文件用于合并,mp4decrpyt需要 + if (!DownloaderConfig.MyOptions.UseShakaPackager) + { + files = new string[] { initResult.ActualFilePath }.Concat(files).ToArray(); + } + } + foreach (var inputFilePath in files) + { + using (var inputStream = File.OpenRead(inputFilePath)) + { + inputStream.CopyTo(fileOutputStream); + } + } + if (!DownloaderConfig.MyOptions.LiveKeepSegments) + { + foreach (var inputFilePath in files.Where(x => !Path.GetFileName(x).StartsWith("_init"))) + { + File.Delete(inputFilePath); + } + } + FileDic.Clear(); + if (initResult != null) + { + FileDic[streamSpec.Playlist!.MediaInit!] = initResult; } } else { - success = false; - Logger.ErrorMarkUp($"Mux failed"); + var initResult = streamSpec.Playlist!.MediaInit != null ? FileDic[streamSpec.Playlist!.MediaInit!]! : null; + var files = FileDic.OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray(); + foreach (var inputFilePath in files) + { + if (!DownloaderConfig.MyOptions.LiveKeepSegments && !Path.GetFileName(inputFilePath).StartsWith("_init")) + { + File.Delete(inputFilePath); + } + } + + // 处理图形字幕 + await SubtitleUtil.TryWriteImagePngsAsync(currentVtt, tmpDir); + + var subText = currentVtt.ToVtt(); + if (outputExt == ".srt") + { + subText = currentVtt.ToSrt(); + } + var subBytes = Encoding.UTF8.GetBytes(subText); + fileOutputStream.Position = 0; + fileOutputStream.Write(subBytes); + FileDic.Clear(); + if (initResult != null) + { + FileDic[streamSpec.Playlist!.MediaInit!] = initResult; + } } - //判断是否要改名 - var newPath = Path.ChangeExtension(outPath, ext); - if (result && !File.Exists(newPath)) + + // 刷新buffer + if (fileOutputStream != null) { - Logger.WarnMarkUp($"Rename to [grey]{Path.GetFileName(newPath).EscapeMarkup()}[/]"); - File.Move(outPath + ext, newPath); + fileOutputStream.Flush(); } } - return success; + if (STOP_FLAG && source.Count == 0) + break; + } + + if (fileOutputStream != null) + { + if (!DownloaderConfig.MyOptions.LivePipeMux) + { + // 记录所有文件信息 + OutputFiles.Add(new OutputFile() + { + Index = task.Id, + FilePath = (fileOutputStream as FileStream)!.Name, + LangCode = streamSpec.Language, + Description = streamSpec.Name, + Mediainfos = mediaInfos, + MediaType = streamSpec.MediaType, + }); + } + fileOutputStream.Close(); + fileOutputStream.Dispose(); + } + + return true; + } + + private async Task PlayListProduceAsync(Dictionary dic) + { + while (!STOP_FLAG) + { + if (WAIT_SEC != 0) + { + // 1. MPD 所有URL相同 单次请求即可获得所有轨道的信息 + // 2. M3U8 所有URL不同 才需要多次请求 + + await Parallel.ForEachAsync(dic, async (dic, _) => + { + var streamSpec = dic.Key; + var task = dic.Value; + + // 达到上限时 不需要刷新了 + if (RecordLimitReachedDic[task.Id]) + return; + + var allHasDatetime = streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null); + if (!SamePathDic.ContainsKey(task.Id)) + { + var allName = streamSpec.Playlist!.MediaParts[0].MediaSegments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false)); + var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1; + SamePathDic[task.Id] = allSamePath; + } + // 过滤不需要下载的片段 + FilterMediaSegments(streamSpec, task, allHasDatetime, SamePathDic[task.Id]); + var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments; + if (newList.Count > 0) + { + task.MaxValue += newList.Count; + // 推送给消费者 + await BlockDic[task.Id].SendAsync(newList); + // 更新最新链接 + LastFileNameDic[task.Id] = GetSegmentName(newList.Last(), allHasDatetime, SamePathDic[task.Id]); + // 尝试更新时间戳 + var dt = newList.Last().DateTime; + DateTimeDic[task.Id] = dt != null ? GetUnixTimestamp(dt.Value) : 0L; + // 累加已获取到的时长 + RefreshedDurDic[task.Id] += (int)newList.Sum(s => s.Duration); + } + + if (!STOP_FLAG && RefreshedDurDic[task.Id] >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds) + { + RecordLimitReachedDic[task.Id] = true; + } + + // 检测时长限制 + if (!STOP_FLAG && RecordLimitReachedDic.Values.All(x => x == true)) + { + Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]"); + STOP_FLAG = true; + CancellationTokenSource.Cancel(); + } + }); + + try + { + // Logger.WarnMarkUp($"wait {waitSec}s"); + if (!STOP_FLAG) await Task.Delay(WAIT_SEC * 1000, CancellationTokenSource.Token); + // 刷新列表 + if (!STOP_FLAG) await StreamExtractor.RefreshPlayListAsync(dic.Keys.ToList()); + } + catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token) + { + // 不需要做事 + } + catch (Exception e) + { + Logger.ErrorMarkUp(e); + STOP_FLAG = true; + // 停止所有Block + foreach (var target in BlockDic.Values) + { + target.Complete(); + } + } + } } } -} + + private void FilterMediaSegments(StreamSpec streamSpec, ProgressTask task, bool allHasDatetime, bool allSamePath) + { + if (string.IsNullOrEmpty(LastFileNameDic[task.Id]) && DateTimeDic[task.Id] == 0) return; + + var index = -1; + var dateTime = DateTimeDic[task.Id]; + var lastName = LastFileNameDic[task.Id]; + + // 优先使用dateTime判断 + if (dateTime != 0 && streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null)) + { + index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetUnixTimestamp(s.DateTime!.Value) == dateTime); + } + else + { + index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetSegmentName(s, allHasDatetime, allSamePath) == lastName); + } + + if (index > -1) + { + // 修正Index + var list = streamSpec.Playlist!.MediaParts[0].MediaSegments.Skip(index + 1).ToList(); + if (list.Count > 0) + { + var newMin = list.Min(s => s.Index); + var oldMax = MaxIndexDic[task.Id]; + if (newMin < oldMax) + { + var offset = oldMax - newMin + 1; + foreach (var item in list) + { + item.Index += offset; + } + } + MaxIndexDic[task.Id] = list.Max(s => s.Index); + } + streamSpec.Playlist!.MediaParts[0].MediaSegments = list; + } + } + + public async Task StartRecordAsync() + { + var takeLastCount = DownloaderConfig.MyOptions.LiveTakeCount; + ConcurrentDictionary SpeedContainerDic = new(); // 速度计算 + ConcurrentDictionary Results = new(); + // 同步流 + FilterUtil.SyncStreams(SelectedSteams, takeLastCount); + // 设置等待时间 + if (WAIT_SEC == 0) + { + WAIT_SEC = (int)(SelectedSteams.Min(s => s.Playlist!.MediaParts[0].MediaSegments.Sum(s => s.Duration)) / 2); + WAIT_SEC -= 2; // 再提前两秒吧 留出冗余 + if (DownloaderConfig.MyOptions.LiveWaitTime != null) + WAIT_SEC = DownloaderConfig.MyOptions.LiveWaitTime.Value; + if (WAIT_SEC <= 0) WAIT_SEC = 1; + Logger.WarnMarkUp($"set refresh interval to {WAIT_SEC} seconds"); + } + // 如果没有选中音频 取消通过音频修复vtt时间轴 + if (!SelectedSteams.Any(x => x.MediaType == MediaType.AUDIO)) + { + DownloaderConfig.MyOptions.LiveFixVttByAudio = false; + } + + /*// 写出master + if (DownloaderConfig.MyOptions.LiveWriteHLS) + { + var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; + var saveName = DownloaderConfig.MyOptions.SaveName ?? DateTime.Now.ToString("yyyyMMddHHmmss"); + await StreamingUtil.WriteMasterListAsync(SelectedSteams, saveName, saveDir); + }*/ + + var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); + progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; + + // 进度条的列定义 + var progressColumns = new ProgressColumn[] + { + new TaskDescriptionColumn() { Alignment = Justify.Left }, + new RecordingDurationColumn(RecordedDurDic, RefreshedDurDic), // 时长显示 + new RecordingStatusColumn(), + new PercentageColumn(), + new DownloadSpeedColumn(SpeedContainerDic), // 速度计算 + new SpinnerColumn(), + }; + if (DownloaderConfig.MyOptions.NoAnsiColor) + { + progressColumns = progressColumns.SkipLast(1).ToArray(); + } + progress.Columns(progressColumns); + + await progress.StartAsync(async ctx => + { + // 创建任务 + var dic = SelectedSteams.Select(item => + { + var task = ctx.AddTask(item.ToShortShortString(), autoStart: false, maxValue: 0); + SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算 + // 限速设置 + if (DownloaderConfig.MyOptions.MaxSpeed != null) + { + SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value; + } + LastFileNameDic[task.Id] = ""; + RecordLimitReachedDic[task.Id] = false; + DateTimeDic[task.Id] = 0L; + RecordedDurDic[task.Id] = 0; + RefreshedDurDic[task.Id] = 0; + MaxIndexDic[task.Id] = item.Playlist?.MediaParts[0].MediaSegments.LastOrDefault()?.Index ?? 0L; // 最大Index + BlockDic[task.Id] = new BufferBlock>(); + return (item, task); + }).ToDictionary(item => item.item, item => item.task); + + DownloaderConfig.MyOptions.ConcurrentDownload = true; + DownloaderConfig.MyOptions.MP4RealTimeDecryption = true; + DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; + if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !DownloaderConfig.MyOptions.UseShakaPackager + && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) + Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]"); + var limit = DownloaderConfig.MyOptions.LiveRecordLimit; + if (limit != TimeSpan.MaxValue) + Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]"); + // 录制直播时,用户选了几个流就并发录几个 + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = SelectedSteams.Count + }; + // 开始刷新 + var producerTask = PlayListProduceAsync(dic); + await Task.Delay(200); + // 并发下载 + await Parallel.ForEachAsync(dic, options, async (kp, _) => + { + var task = kp.Value; + var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id], BlockDic[task.Id]); + Results[kp.Key] = await consumerTask; + }); + }); + + var success = Results.Values.All(v => v == true); + + // 删除临时文件夹 + if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success) + { + foreach (var item in StreamExtractor.RawFiles) + { + var file = Path.Combine(DownloaderConfig.DirPrefix, item.Key); + if (File.Exists(file)) File.Delete(file); + } + OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix); + } + + // 混流 + if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) + { + OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); + // 是否跳过字幕 + if (DownloaderConfig.MyOptions.MuxOptions!.SkipSubtitle) + { + OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); + } + if (DownloaderConfig.MyOptions.MuxImports != null) + { + OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports); + } + OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); + var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; + var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat); + var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); + var outName = $"{dirName}.MUX"; + var outPath = Path.Combine(saveDir, outName); + Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); + var result = false; + if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); + else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo); + // 完成后删除各轨道文件 + if (result) + { + if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) + { + Logger.WarnMarkUp("[grey]Cleaning files...[/]"); + OutputFiles.ForEach(f => File.Delete(f.FilePath)); + var tmpDir = DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory; + OtherUtil.SafeDeleteDir(tmpDir); + } + } + else + { + success = false; + Logger.ErrorMarkUp($"Mux failed"); + } + // 判断是否要改名 + var newPath = Path.ChangeExtension(outPath, ext); + if (result && !File.Exists(newPath)) + { + Logger.WarnMarkUp($"Rename to [grey]{Path.GetFileName(newPath).EscapeMarkup()}[/]"); + File.Move(outPath + ext, newPath); + } + } + + return success; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Downloader/IDownloader.cs b/src/N_m3u8DL-RE/Downloader/IDownloader.cs index 9db7289..0d4e444 100644 --- a/src/N_m3u8DL-RE/Downloader/IDownloader.cs +++ b/src/N_m3u8DL-RE/Downloader/IDownloader.cs @@ -1,15 +1,9 @@ using N_m3u8DL_RE.Common.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 DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary? headers = null); - } -} + Task DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary? headers = null); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs b/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs index b9405fc..f8a4c1c 100644 --- a/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs +++ b/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs @@ -6,156 +6,146 @@ using N_m3u8DL_RE.Crypto; using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Util; 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; + +/// +/// 简单下载器 +/// +internal class SimpleDownloader : IDownloader { - /// - /// 简单下载器 - /// - internal class SimpleDownloader : IDownloader + DownloaderConfig DownloaderConfig; + + public SimpleDownloader(DownloaderConfig config) { - DownloaderConfig DownloaderConfig; + DownloaderConfig = config; + } - public SimpleDownloader(DownloaderConfig config) + public async Task DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary? 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; - } - - public async Task DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary? 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 != 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; - AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!); - } - else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB) - { - var key = segment.EncryptInfo.Key; - var iv = segment.EncryptInfo.IV; - AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB); - } - else if (segment.EncryptInfo.Method == EncryptMethod.CHACHA20) - { - var key = segment.EncryptInfo.Key; - var nonce = segment.EncryptInfo.IV; + var key = segment.EncryptInfo.Key; + var iv = segment.EncryptInfo.IV; + AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!); + } + else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB) + { + var key = segment.EncryptInfo.Key; + var iv = segment.EncryptInfo.IV; + AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB); + } + else if (segment.EncryptInfo.Method == EncryptMethod.CHACHA20) + { + var key = segment.EncryptInfo.Key; + var nonce = segment.EncryptInfo.IV; - var fileBytes = File.ReadAllBytes(dResult.ActualFilePath); - var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!); - await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted); - } - else if (segment.EncryptInfo.Method == EncryptMethod.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); - } + var fileBytes = File.ReadAllBytes(dResult.ActualFilePath); + var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!); + await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted); + } + else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR) + { + // throw new NotSupportedException("SAMPLE-AES-CTR"); } - //处理完成后改名 - File.Move(dResult.ActualFilePath, des); - dResult.ActualFilePath = des; + // Image头处理 + if (dResult.ImageHeader) + { + 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? headers = null, int retryCount = 3) - { - CancellationTokenSource? cancellationTokenSource = null; + // 处理完成后改名 + File.Move(dResult.ActualFilePath, des); + dResult.ActualFilePath = des; + } + return dResult; + } + + private async Task<(string des, DownloadResult? dResult)> DownClipAsync(string url, string path, SpeedContainer speedContainer, long? fromPosition, long? toPosition, Dictionary? headers = null, int retryCount = 3) + { + CancellationTokenSource? cancellationTokenSource = null; retry: - try + try + { + cancellationTokenSource = new(); + var des = Path.ChangeExtension(path, null); + + // 已下载跳过 + if (File.Exists(des)) { - cancellationTokenSource = new(); - var des = Path.ChangeExtension(path, null); + speedContainer.Add(new FileInfo(des).Length); + return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des }); + } - //已下载跳过 - if (File.Exists(des)) - { - speedContainer.Add(new FileInfo(des).Length); - return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des }); - } + // 已解密跳过 + var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + "_dec" + Path.GetExtension(des)); + if (File.Exists(dec)) + { + 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)); - if (File.Exists(dec)) + // 另起线程进行监控 + using var watcher = Task.Factory.StartNew(async () => + { + while (true) { - speedContainer.Add(new FileInfo(dec).Length); - return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec }); - } - - //另起线程进行监控 - using var watcher = Task.Factory.StartNew(async () => - { - while (true) + if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break; + if (speedContainer.ShouldStop) { - if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break; - if (speedContainer.ShouldStop) - { - cancellationTokenSource.Cancel(); - Logger.DebugMarkUp("Cancel..."); - break; - } - await Task.Delay(500); + cancellationTokenSource.Cancel(); + Logger.DebugMarkUp("Cancel..."); + break; } - }); + await Task.Delay(500); + } + }); - //调用下载 - var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition); - return (des, result); + // 调用下载 + var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition); + return (des, result); - throw new Exception("please retry"); - } - catch (Exception ex) + throw new Exception("please retry"); + } + 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}[/]"); - 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) - { - 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; + await Task.Delay(1000); + goto retry; } - finally + else { - if (cancellationTokenSource != null) - { - //调用后销毁 - cancellationTokenSource.Dispose(); - 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()}[/]"); + } + // throw new Exception("download failed", ex); + return default; + } + finally + { + if (cancellationTokenSource != null) + { + // 调用后销毁 + cancellationTokenSource.Dispose(); + cancellationTokenSource = null; } } } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/CustomRange.cs b/src/N_m3u8DL-RE/Entity/CustomRange.cs index e59b4c1..95c68a1 100644 --- a/src/N_m3u8DL-RE/Entity/CustomRange.cs +++ b/src/N_m3u8DL-RE/Entity/CustomRange.cs @@ -1,23 +1,16 @@ -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 +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; } - 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}"; - } + return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}"; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/DownloadResult.cs b/src/N_m3u8DL-RE/Entity/DownloadResult.cs index 04bdd79..ac085f8 100644 --- a/src/N_m3u8DL-RE/Entity/DownloadResult.cs +++ b/src/N_m3u8DL-RE/Entity/DownloadResult.cs @@ -5,15 +5,14 @@ using System.Linq; using System.Text; 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 long? ActualContentLength { get; set; } - public bool ImageHeader { get; set; } = false; //图片伪装 - public bool GzipHeader { get; set; } = false; //GZip压缩 - public required string ActualFilePath { get; set; } - } -} + public bool Success { get => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength == null ? false : true); } + public long? RespContentLength { get; set; } + public long? ActualContentLength { get; set; } + public bool ImageHeader { get; set; } = false; // 图片伪装 + public bool GzipHeader { get; set; } = false; // GZip压缩 + public required string ActualFilePath { get; set; } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/Mediainfo.cs b/src/N_m3u8DL-RE/Entity/Mediainfo.cs index 5bf440f..795becd 100644 --- a/src/N_m3u8DL-RE/Entity/Mediainfo.cs +++ b/src/N_m3u8DL-RE/Entity/Mediainfo.cs @@ -1,33 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Spectre.Console; +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; } - 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() - { - return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i))); - } - - public string ToStringMarkUp() - { - return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]"; - } + return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i))); } -} + + public string ToStringMarkUp() + { + return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]"; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/MuxOptions.cs b/src/N_m3u8DL-RE/Entity/MuxOptions.cs index 5e27134..163e0cf 100644 --- a/src/N_m3u8DL-RE/Entity/MuxOptions.cs +++ b/src/N_m3u8DL-RE/Entity/MuxOptions.cs @@ -1,13 +1,12 @@ 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 KeepFiles { get; set; } = false; - public bool SkipSubtitle { get; set; } = false; - public string? BinPath { get; set; } - } -} + public bool UseMkvmerge { get; set; } = false; + public MuxFormat MuxFormat { get; set; } = MuxFormat.MP4; + public bool KeepFiles { get; set; } = false; + public bool SkipSubtitle { get; set; } = false; + public string? BinPath { get; set; } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/OutputFile.cs b/src/N_m3u8DL-RE/Entity/OutputFile.cs index cc37050..849ad78 100644 --- a/src/N_m3u8DL-RE/Entity/OutputFile.cs +++ b/src/N_m3u8DL-RE/Entity/OutputFile.cs @@ -1,19 +1,13 @@ 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 required string FilePath { get; set; } - public string? LangCode { get; set; } - public string? Description { get; set; } - public List Mediainfos { get; set; } = new(); - } -} + public MediaType? MediaType { get; set; } + public required int Index { get; set; } + public required string FilePath { get; set; } + public string? LangCode { get; set; } + public string? Description { get; set; } + public List Mediainfos { get; set; } = new(); +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/SpeedContainer.cs b/src/N_m3u8DL-RE/Entity/SpeedContainer.cs index 93ef2d5..13f4365 100644 --- a/src/N_m3u8DL-RE/Entity/SpeedContainer.cs +++ b/src/N_m3u8DL-RE/Entity/SpeedContainer.cs @@ -7,53 +7,52 @@ using System.Linq; using System.Text; 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; - 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; } - - /////////////////////////////////////////////////// - - private long _downloaded = 0; - private long _Rdownloaded = 0; - public long Downloaded { get => _downloaded; } - - public int AddLowSpeedCount() - { - return Interlocked.Add(ref _zeroSpeedCount, 1); - } - - public int ResetLowSpeedCount() - { - return Interlocked.Exchange(ref _zeroSpeedCount, 0); - } - - public long Add(long size) - { - 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; - } + return Interlocked.Add(ref _zeroSpeedCount, 1); } -} + + public int ResetLowSpeedCount() + { + return Interlocked.Exchange(ref _zeroSpeedCount, 0); + } + + public long Add(long size) + { + 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; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Entity/StreamFilter.cs b/src/N_m3u8DL-RE/Entity/StreamFilter.cs index d76d862..54e5bf2 100644 --- a/src/N_m3u8DL-RE/Entity/StreamFilter.cs +++ b/src/N_m3u8DL-RE/Entity/StreamFilter.cs @@ -1,56 +1,51 @@ using N_m3u8DL_RE.Common.Enum; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; 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; } - 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; } + var sb = new StringBuilder(); - 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() - { - 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}"; - } + return sb.ToString() + $"For: {For}"; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Processor/DemoProcessor.cs b/src/N_m3u8DL-RE/Processor/DemoProcessor.cs index 71ab689..7e9d356 100644 --- a/src/N_m3u8DL-RE/Processor/DemoProcessor.cs +++ b/src/N_m3u8DL-RE/Processor/DemoProcessor.cs @@ -2,26 +2,20 @@ using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Processor; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace N_m3u8DL_RE.Processor +namespace N_m3u8DL_RE.Processor; + +internal class DemoProcessor : ContentProcessor { - internal class DemoProcessor : ContentProcessor + + public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) { - - public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) - { - return extractorType == ExtractorType.MPEG_DASH && parserConfig.Url.Contains("bitmovin"); - } - - public override string Process(string rawText, ParserConfig parserConfig) - { - Logger.InfoMarkUp("[red]Match bitmovin![/]"); - return rawText; - } + return extractorType == ExtractorType.MPEG_DASH && parserConfig.Url.Contains("bitmovin"); } -} + + public override string Process(string rawText, ParserConfig parserConfig) + { + Logger.InfoMarkUp("[red]Match bitmovin![/]"); + return rawText; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs b/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs index 5825c51..6b480b1 100644 --- a/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs +++ b/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs @@ -5,27 +5,21 @@ using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Processor; using N_m3u8DL_RE.Parser.Processor.HLS; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace N_m3u8DL_RE.Processor +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"); - } - - public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig) - { - Logger.InfoMarkUp($"[white on green]My Key Processor => {keyLine}[/]"); - var info = new DefaultHLSKeyProcessor().Process(keyLine, m3u8Url, m3u8Content, parserConfig); - Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]"); - return info; - } + return extractorType == ExtractorType.HLS && parserConfig.Url.Contains("playertest.longtailvideo.com"); } -} + + public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig) + { + Logger.InfoMarkUp($"[white on green]My Key Processor => {keyLine}[/]"); + var info = new DefaultHLSKeyProcessor().Process(keyLine, m3u8Url, m3u8Content, parserConfig); + Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]"); + return info; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Processor/NowehoryzontyUrlProcessor.cs b/src/N_m3u8DL-RE/Processor/NowehoryzontyUrlProcessor.cs index 482f5dc..ab58807 100644 --- a/src/N_m3u8DL-RE/Processor/NowehoryzontyUrlProcessor.cs +++ b/src/N_m3u8DL-RE/Processor/NowehoryzontyUrlProcessor.cs @@ -6,219 +6,211 @@ using N_m3u8DL_RE.Parser.Util; using NiL.JS.BaseLibrary; using NiL.JS.Core; 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" - internal class NowehoryzontyUrlProcessor : UrlProcessor + private static string START = "nowehoryzonty:"; + 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:"; - 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 (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; - } - var context = new Context(); - context.Eval(JS); - Function = context.GetVariable("md5").As(); - 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; + Logger.WarnMarkUp($"[white on green]www.nowehoryzonty.pl[/] matched! waiting for calc..."); + LOG = true; } - return false; + var context = new Context(); + context.Eval(JS); + Function = context.GetVariable("md5").As(); + 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; } - - 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)); - - """; + 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)); + + """; +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index 2f0c0b5..0d350fd 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -16,437 +16,436 @@ using N_m3u8DL_RE.CommandLine; using System.Net; 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(args); + if ((index = list.IndexOf("--ui-language")) != -1 && list.Count > index + 1 && new List { "en-US", "zh-CN", "zh-TW" }.Contains(list[index + 1])) { - 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(args); - if ((index = list.IndexOf("--ui-language")) != -1 && list.Count > index + 1 && new List { "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); + loc = list[index + 1]; } - private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) - { - Logger.WarnMarkUp("Force Exit..."); - try - { - Console.CursorVisible = true; - if (!OperatingSystem.IsWindows()) - System.Diagnostics.Process.Start("stty", "echo"); - } catch { } - Environment.Exit(0); - } + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc); + Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(loc); + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(loc); - static int GetOrder(StreamSpec streamSpec) - { - if (streamSpec.Channels == null) return 0; + + await CommandInvoker.InvokeArgs(args, DoWorkAsync); + } + + private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) + { + Logger.WarnMarkUp("Force Exit..."); + try + { + Console.CursorVisible = true; + if (!OperatingSystem.IsWindows()) + 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; + 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 async Task DoWorkAsync(MyOption option) + if (option.CustomProxy != null) { - - if (Console.IsOutputRedirected || Console.IsErrorRedirected) + 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)) { - option.ForceAnsiConsole = true; - option.NoAnsiColor = true; - Logger.Info(ResString.consoleRedirected); + throw new FileNotFoundException("mkvmerge not found"); } - CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor); - //检测更新 - CheckUpdateAsync(); + Logger.Extra($"mkvmerge => {option.MkvmergeBinaryPath}"); + } - Logger.IsWriteFile = !option.NoLog; - Logger.InitLogFile(); - Logger.LogLevel = option.LogLevel; - Logger.Info(CommandInvoker.VERSION_INFO); - - if (option.UseSystemProxy == false) + // 预先检查 + if ((option.Keys != null && option.Keys.Length > 0) || option.KeyTextFile != null) + { + if (string.IsNullOrEmpty(option.DecryptionBinaryPath)) { - HTTPUtil.HttpClientHandler.UseProxy = false; - } - - 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)) + if (option.UseShakaPackager) { - 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}"); - } - - //预先检查 - if ((option.Keys != null && option.Keys.Length > 0) || option.KeyTextFile != null) - { - if (string.IsNullOrEmpty(option.DecryptionBinaryPath)) + else { - if (option.UseShakaPackager) - { - 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}"); - } - 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); + var file = GlobalUtil.FindExecutable("mp4decrypt"); + if (file == null) throw new FileNotFoundException("mp4decrypt not found!"); + option.DecryptionBinaryPath = file; + Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}"); } } - - //默认的headers - var headers = new Dictionary() + else if (!File.Exists(option.DecryptionBinaryPath)) { - ["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}"); + throw new FileNotFoundException(option.DecryptionBinaryPath); } + } - var parserConfig = new ParserConfig() + // 默认的headers + var headers = new Dictionary() + { + ["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, - 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); - } + await Task.Delay(1000); } + } - var url = option.Input; + var url = option.Input; - //流提取器配置 - var extractor = new StreamExtractor(parserConfig); - // 从链接加载内容 - await RetryUtil.WebRequestRetryAsync(async () => + // 流提取器配置 + var extractor = new StreamExtractor(parserConfig); + // 从链接加载内容 + 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(); + 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); - 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.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); + } - //生成文件夹 - 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); + if (!selectedStreams.Any()) + throw new Exception(ResString.noStreamsToDownload); - 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) - { - Logger.InfoMarkUp(item.ToString()); - } + // 直播检测 + var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod; + if (livingFlag) + { + Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]"); + } - var selectedStreams = new List(); - 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 (selectedStreams.Any(s => s.Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN)))) + { + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]"); + option.BinaryMerge = 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 (!livingFlag) + FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange); - 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) - { - 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); - } + // 应用用户自定义的广告分片关键字 + FilterUtil.CleanAd(selectedStreams, option.AdKeywords); - if (!selectedStreams.Any()) - throw new Exception(ResString.noStreamsToDownload); + // 记录文件 + extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams); - //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); + Logger.Info(ResString.selectedStream); + foreach (var item in selectedStreams) + { + Logger.InfoMarkUp(item.ToString()); + } - //直播检测 - var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod; - if (livingFlag) - { - Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]"); - } + // 写出文件 + await WriteRawFilesAsync(option, extractor, tmpDir); - //无法识别的加密方式,自动开启二进制合并 - if (selectedStreams.Any(s => s.Playlist.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN)))) - { - 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 (option.SkipDownload) + { + return; + } #if DEBUG - Console.WriteLine("Press any key to continue..."); - Console.ReadKey(); + Console.WriteLine("Press any key to continue..."); + Console.ReadKey(); #endif - Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]"); + Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]"); - //开始MuxAfterDone后自动使用二进制版 - if (!option.BinaryMerge && option.MuxAfterDone) - { - option.BinaryMerge = true; - 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; - } + // 开始MuxAfterDone后自动使用二进制版 + if (!option.BinaryMerge && option.MuxAfterDone) + { + option.BinaryMerge = true; + Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]"); } - private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir) + // 下载配置 + var downloadConfig = new DownloaderConfig() { - //写出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); - } - } + 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(); } - static async Task CheckUpdateAsync() + if (result) { - 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) - { - ; - } + Logger.InfoMarkUp("[white on green]Done[/]"); } - - //重定向 - static async Task Get302Async(string url) + else { - //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; + Logger.ErrorMarkUp("[white on red]Failed[/]"); + Environment.ExitCode = 1; } } + + 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 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; + } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/DownloadUtil.cs b/src/N_m3u8DL-RE/Util/DownloadUtil.cs index fd5d9d8..63e29e3 100644 --- a/src/N_m3u8DL-RE/Util/DownloadUtil.cs +++ b/src/N_m3u8DL-RE/Util/DownloadUtil.cs @@ -2,8 +2,6 @@ using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Entity; -using System.IO; -using System.Net; using System.Net.Http.Headers; namespace N_m3u8DL_RE.Util; @@ -111,16 +109,16 @@ internal static class DownloadUtil size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token); speedContainer.Add(size); await stream.WriteAsync(buffer, 0, size); - //检测imageHeader + // 检测imageHeader bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer); - //检测GZip(For DDP Audio) + // 检测GZip(For DDP Audio) bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b; while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0) { speedContainer.Add(size); await stream.WriteAsync(buffer, 0, size); - //限速策略 + // 限速策略 while (speedContainer.Downloaded > speedContainer.SpeedLimit) { await Task.Delay(1); diff --git a/src/N_m3u8DL-RE/Util/FilterUtil.cs b/src/N_m3u8DL-RE/Util/FilterUtil.cs index f876d00..1a30e5c 100644 --- a/src/N_m3u8DL-RE/Util/FilterUtil.cs +++ b/src/N_m3u8DL-RE/Util/FilterUtil.cs @@ -4,12 +4,7 @@ using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Entity; using Spectre.Console; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace N_m3u8DL_RE.Util; @@ -17,7 +12,7 @@ public static class FilterUtil { public static List DoFilterKeep(IEnumerable lists, StreamFilter? filter) { - if (filter == null) return new List(); + if (filter == null) return []; var inputs = lists.Where(_ => true); if (filter.GroupIdReg != null) @@ -56,13 +51,13 @@ public static class FilterUtil var bestNumberStr = filter.For.Replace("best", ""); var worstNumberStr = filter.For.Replace("worst", ""); - if (filter.For == "best" && inputs.Count() > 0) + if (filter.For == "best" && inputs.Any()) 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(); - 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(); - 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(); return inputs.ToList(); @@ -82,15 +77,16 @@ public static class FilterUtil public static List SelectStreams(IEnumerable lists) { - if (lists.Count() == 1) - return new List(lists); + var streamSpecs = lists.ToList(); + if (streamSpecs.Count == 1) + return [..streamSpecs]; - //基本流 - var basicStreams = lists.Where(x => x.MediaType == null); - //可选音频轨道 - var audios = lists.Where(x => x.MediaType == MediaType.AUDIO); - //可选字幕轨道 - var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES); + // 基本流 + var basicStreams = streamSpecs.Where(x => x.MediaType == null).ToList(); + // 可选音频轨道 + var audios = streamSpecs.Where(x => x.MediaType == MediaType.AUDIO).ToList(); + // 可选字幕轨道 + var subs = streamSpecs.Where(x => x.MediaType == MediaType.SUBTITLES).ToList(); var prompt = new MultiSelectionPrompt() .Title(ResString.promptTitle) @@ -107,8 +103,8 @@ public static class FilterUtil .InstructionsText(ResString.promptInfo) ; - //默认选中第一个 - var first = lists.First(); + // 默认选中第一个 + var first = streamSpecs.First(); prompt.Select(first); if (basicStreams.Any()) @@ -119,7 +115,7 @@ public static class FilterUtil if (audios.Any()) { prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios); - //默认音轨 + // 默认音轨 if (first.AudioId != null) { prompt.Select(audios.First(a => a.GroupId == first.AudioId)); @@ -128,17 +124,17 @@ public static class FilterUtil if (subs.Any()) { prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs); - //默认字幕轨 + // 默认字幕轨 if (first.SubtitleId != null) { prompt.Select(subs.First(s => s.GroupId == first.SubtitleId)); } } - //如果此时还是没有选中任何流,自动选择一个 + // 如果此时还是没有选中任何流,自动选择一个 prompt.Select(basicStreams.Concat(audios).Concat(subs).First()); - //多选 + // 多选 var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt); return selectedStreams; @@ -147,11 +143,11 @@ public static class FilterUtil /// /// 直播使用。对齐各个轨道的起始。 /// - /// + /// /// public static void SyncStreams(List selectedSteams, int takeLastCount = 15) { - //通过Date同步 + // 通过Date同步 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))!; @@ -159,12 +155,12 @@ public static class FilterUtil { 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(); } } } - else //通过index同步 + else // 通过index同步 { var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index)); 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)) { var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1; @@ -198,17 +194,15 @@ public static class FilterUtil /// public static void ApplyCustomRange(List selectedSteams, CustomRange? customRange) { - var resultList = selectedSteams.Select(x => 0d).ToList(); - if (customRange == null) return; Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]"); - var filteByIndex = customRange.StartSegIndex != null && customRange.EndSegIndex != null; - var filteByTime = customRange.StartSec != null && customRange.EndSec != null; + var filterByIndex = customRange is { StartSegIndex: not null, EndSegIndex: not null }; + var filterByTime = customRange is { StartSec: not null, EndSec: not null }; - if (!filteByIndex && !filteByTime) + if (!filterByIndex && !filterByTime) { Logger.ErrorMarkUp(ResString.customRangeInvalid); return; @@ -220,8 +214,8 @@ public static class FilterUtil if (stream.Playlist == null) continue; foreach (var part in stream.Playlist.MediaParts) { - var newSegments = new List(); - if (filteByIndex) + List newSegments; + if (filterByIndex) newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList(); 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 @@ -239,11 +233,11 @@ public static class FilterUtil /// 根据用户输入,清除广告分片 /// /// - /// + /// public static void CleanAd(List selectedSteams, string[]? keywords) { 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) { Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]"); @@ -257,19 +251,16 @@ public static class FilterUtil foreach (var part in stream.Playlist.MediaParts) { - //没有找到广告分片 + // 没有找到广告分片 if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url)))) { 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(); var countAfter = stream.SegmentsCount; diff --git a/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs b/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs index 9fe52e9..fd3537a 100644 --- a/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs +++ b/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs @@ -5,17 +5,17 @@ internal static class ImageHeaderUtil public static bool IsImageHeader(byte[] bArr) { var size = bArr.Length; - //PNG HEADER检测 + // PNG HEADER检测 if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3]) return true; - //GIF HEADER检测 - else if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3]) + // GIF HEADER检测 + if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3]) return true; - //BMP HEADER检测 - else if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8]) + // BMP HEADER检测 + if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8]) return true; - //JPEG HEADER检测 - else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2]) + // JPEG HEADER检测 + if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2]) return true; return false; } @@ -24,7 +24,7 @@ internal static class ImageHeaderUtil { var sourceData = await File.ReadAllBytesAsync(sourcePath); - //PNG HEADER + // PNG HEADER 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]) @@ -37,7 +37,7 @@ internal static class ImageHeaderUtil sourceData = sourceData[771..]; else { - //手动查询结尾标记 0x47 出现两次 + // 手动查询结尾标记 0x47 出现两次 int skip = 0; for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) { @@ -50,20 +50,20 @@ internal static class ImageHeaderUtil sourceData = sourceData[skip..]; } } - //GIF HEADER + // GIF HEADER else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3]) { 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]) { sourceData = sourceData[0x3E..]; } - //JPEG HEADER检测 + // JPEG HEADER检测 else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2]) { - //手动查询结尾标记 0x47 出现两次 + // 手动查询结尾标记 0x47 出现两次 int skip = 0; for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) { diff --git a/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs b/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs index 01c19ac..c79876c 100644 --- a/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs +++ b/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs @@ -1,33 +1,19 @@ 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; -class Language +internal class Language(string extendCode, string code, string desc, string descA) { - public string Code; - public string ExtendCode; - public string Description; - public string DescriptionAudio; - - public Language(string extendCode, string code, string desc, string descA) - { - Code = code; - ExtendCode = extendCode; - Description = desc; - DescriptionAudio = descA; - } + public readonly string Code = code; + public readonly string ExtendCode = extendCode; + public readonly string Description = desc; + public readonly string DescriptionAudio = descA; } internal static class LanguageCodeUtil { - private readonly static List ALL_LANGS = @" + private static readonly List ALL_LANGS = @" af;afr;Afrikaans;Afrikaans af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa) am;amh;Amharic;Amharic @@ -389,8 +375,8 @@ MA;msa;Melayu;Melayu " .Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => { - var arr = x.Trim().Split(';'); - return new Language(arr[0].Trim(), arr[1].Trim(), arr[2].Trim(), arr[3].Trim()); + var arr = x.Trim().Split(';', StringSplitOptions.TrimEntries); + return new Language(arr[0], arr[1], arr[2], arr[3]); }).ToList(); private static Dictionary CODE_MAP = @" @@ -504,8 +490,7 @@ sr;srp private static string ConvertTwoToThree(string input) { - if (CODE_MAP.TryGetValue(input, out var code)) return code; - return input; + return CODE_MAP.GetValueOrDefault(input, input); } /// @@ -518,12 +503,12 @@ sr;srp if (string.IsNullOrEmpty(outputFile.LangCode)) return; var originalLangCode = outputFile.LangCode; - //先直接查找 + // 先直接查找 var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase)); - //处理特殊的扩展语言标记 + // 处理特殊的扩展语言标记 if (lang == null) { - //2位转3位 + // 2位转3位 var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First()); 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) { - outputFile.LangCode = "und"; //无法识别直接置为und + outputFile.LangCode = "und"; // 无法识别直接置为und } - //无描述,则把LangCode当作描述 + // 无描述,则把LangCode当作描述 if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode; } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs b/src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs index 3a6264f..c4870c5 100644 --- a/src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs +++ b/src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs @@ -1,13 +1,6 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Log; 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; @@ -15,17 +8,16 @@ internal static class LargeSingleFileSplitUtil { class Clip { - public required int index; - public required long from; - public required long to; + public required int Index; + public required long From; + public required long To; } /// /// URL大文件切片处理 /// - /// + /// /// - /// /// public static async Task?> SplitUrlAsync(MediaSegment segment, Dictionary headers) { @@ -43,10 +35,10 @@ internal static class LargeSingleFileSplitUtil { splitSegments.Add(new MediaSegment() { - Index = clip.index, + Index = clip.Index, Url = url, - StartRange = clip.from, - ExpectLength = clip.to == -1 ? null : clip.to - clip.from + 1, + StartRange = clip.From, + ExpectLength = clip.To == -1 ? null : clip.To - clip.From + 1, EncryptInfo = segment.EncryptInfo, }); } @@ -85,7 +77,7 @@ internal static class LargeSingleFileSplitUtil return totalSizeBytes; } - //此函数主要是切片下载逻辑 + // 此函数主要是切片下载逻辑 private static List GetAllClips(string url, long fileSize) { List clips = new(); @@ -96,11 +88,11 @@ internal static class LargeSingleFileSplitUtil { Clip c = new() { - index = index, - from = counter, - to = counter + perSize + Index = index, + From = counter, + To = counter + perSize }; - //没到最后 + // 没到最后 if (fileSize - perSize > 0) { fileSize -= perSize; @@ -108,10 +100,10 @@ internal static class LargeSingleFileSplitUtil index++; clips.Add(c); } - //已到最后 + // 已到最后 else { - c.to = -1; + c.To = -1; clips.Add(c); break; } diff --git a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs index b152295..9b6bdd6 100644 --- a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs +++ b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs @@ -9,7 +9,7 @@ namespace N_m3u8DL_RE.Util; internal static class MP4DecryptUtil { - private static string ZeroKid = "00000000000000000000000000000000"; + private static readonly string ZeroKid = "00000000000000000000000000000000"; public static async Task 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; @@ -25,7 +25,7 @@ internal static class MP4DecryptUtil 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(); } @@ -45,16 +45,16 @@ internal static class MP4DecryptUtil if (keyPair == null) return false; - //shakaPackager 无法单独解密init文件 + // shakaPackager 无法单独解密init文件 if (source.EndsWith("_init.mp4") && shakaPackager) return false; - var cmd = ""; + string cmd; var tmpFile = ""; if (shakaPackager) { var enc = source; - //shakaPackager 手动构造文件 + // shakaPackager 手动构造文件 if (init != "") { tmpFile = Path.ChangeExtension(source, ".itmp"); @@ -101,8 +101,8 @@ internal static class MP4DecryptUtil { FileName = name, Arguments = arg, - //RedirectStandardOutput = true, - //RedirectStandardError = true, + // RedirectStandardOutput = true, + // RedirectStandardError = true, CreateNoWindow = true, UseShellExecute = false })!.WaitForExitAsync(); @@ -124,8 +124,7 @@ internal static class MP4DecryptUtil Logger.InfoMarkUp(ResString.searchKey); using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); using var reader = new StreamReader(stream); - var line = ""; - while ((line = await reader.ReadLineAsync()) != null) + while (await reader.ReadLineAsync() is { } line) { if (line.Trim().StartsWith(kid)) { @@ -152,17 +151,15 @@ internal static class MP4DecryptUtil public static ParsedMP4Info GetMP4Info(string output) { - using (var fs = File.OpenRead(output)) - { - var header = new byte[1 * 1024 * 1024]; //1MB - fs.Read(header); - return GetMP4Info(header); - } + using var fs = File.OpenRead(output); + var header = new byte[1 * 1024 * 1024]; // 1MB + fs.Read(header); + return GetMP4Info(header); } 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) // - stop process @@ -182,6 +179,6 @@ internal static class MP4DecryptUtil p.Start(); var errorOutput = p.StandardError.ReadToEnd(); p.WaitForExit(); - return ShakaKeyIDRegex.Match(errorOutput).Groups[1].Value; + return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value; } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/MediainfoUtil.cs b/src/N_m3u8DL-RE/Util/MediainfoUtil.cs index 1ec6187..1e89664 100644 --- a/src/N_m3u8DL-RE/Util/MediainfoUtil.cs +++ b/src/N_m3u8DL-RE/Util/MediainfoUtil.cs @@ -1,12 +1,6 @@ using N_m3u8DL_RE.Entity; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Xml.Linq; namespace N_m3u8DL_RE.Util; @@ -48,7 +42,7 @@ internal static partial class MediainfoUtil RedirectStandardError = true, UseShellExecute = false })!; - var output = p.StandardError.ReadToEnd(); + var output = await p.StandardError.ReadToEndAsync(); await p.WaitForExitAsync(); foreach (Match stream in TextRegex().Matches(output)) @@ -87,7 +81,7 @@ internal static partial class MediainfoUtil if (result.Count == 0) { - result.Add(new Mediainfo() + result.Add(new Mediainfo { Type = "Unknown" }); diff --git a/src/N_m3u8DL-RE/Util/MergeUtil.cs b/src/N_m3u8DL-RE/Util/MergeUtil.cs index 1aaadfa..240acbe 100644 --- a/src/N_m3u8DL-RE/Util/MergeUtil.cs +++ b/src/N_m3u8DL-RE/Util/MergeUtil.cs @@ -27,18 +27,14 @@ internal static class MergeUtil if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); - string[] inputFilePaths = files; - using (var outputStream = File.Create(outputFilePath)) + var inputFilePaths = files; + using var outputStream = File.Create(outputFilePath); + foreach (var inputFilePath in inputFilePaths) { - foreach (var inputFilePath in inputFilePaths) - { - if (inputFilePath == "") - continue; - using (var inputStream = File.OpenRead(inputFilePath)) - { - inputStream.CopyTo(outputStream); - } - } + if (inputFilePath == "") + continue; + using var inputStream = File.OpenRead(inputFilePath); + inputStream.CopyTo(outputStream); } } @@ -79,18 +75,18 @@ internal static class MergeUtil div = 200; 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(); foreach (var items in li) { - if (items.Count() == 0) + if (!items.Any()) continue; var output = outputName + index.ToString("0000") + ".ts"; CombineMultipleFilesIntoSingleFile(items, output); newFiles.Add(output); - //合并后删除这些文件 + // 合并后删除这些文件 foreach (var item in items) { File.Delete(item); @@ -106,7 +102,7 @@ internal static class MergeUtil bool writeDate = true, bool useConcatDemuxer = false, string poster = "", string audioName = "", string title = "", string copyright = "", string comment = "", string encodingTool = "", string recTime = "") { - //改为绝对路径 + // 改为绝对路径 outputPath = Path.GetFullPath(outputPath); string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime; @@ -185,13 +181,13 @@ internal static class MergeUtil string dateString = DateTime.Now.ToString("o"); StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn "); - //INPUT + // INPUT foreach (var item in files) { command.Append($" -i \"{item.FilePath}\" "); } - //MAP + // MAP for (int i = 0; i < files.Length; i++) { command.Append($" -map {i} "); @@ -200,21 +196,21 @@ internal static class MergeUtil var srt = files.Any(x => x.FilePath.EndsWith(".srt")); 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) command.Append($" -strict unofficial -c:a copy -c:v copy "); else if (muxFormat == MuxFormat.MKV) command.Append($" -strict unofficial -c:a copy -c:v copy -c:s {(srt ? "srt" : "webvtt")} "); else throw new ArgumentException($"unknown format: {muxFormat}"); - //CLEAN + // CLEAN command.Append(" -map_metadata -1 "); - //LANG and NAME + // LANG and NAME var streamIndex = 0; for (int i = 0; i < files.Length; i++) { - //转换语言代码 + // 转换语言代码 LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]); command.Append($" -metadata:s:{streamIndex} language=\"{files[i].LangCode ?? "und"}\" "); 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 subTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO); if (videoTracks.Any()) command.Append(" -disposition:v:0 default "); - //字幕都不设置默认 + // 字幕都不设置默认 if (subTracks.Any()) command.Append(" -disposition:s 0 "); if (audioTracks.Any()) { - //音频除了第一个音轨 都不设置默认 + // 音频除了第一个音轨 都不设置默认 command.Append(" -disposition:a:0 default "); for (int i = 1; i < audioTracks.Count(); i++) { @@ -265,16 +261,16 @@ internal static class MergeUtil var dFlag = false; - //LANG and NAME + // LANG and NAME for (int i = 0; i < files.Length; i++) { - //转换语言代码 + // 转换语言代码 LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]); command.Append($" --language 0:\"{files[i].LangCode ?? "und"}\" "); - //字幕都不设置默认 + // 字幕都不设置默认 if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES) command.Append($" --default-track 0:no "); - //音频除了第一个音轨 都不设置默认 + // 音频除了第一个音轨 都不设置默认 if (files[i].MediaType == Common.Enum.MediaType.AUDIO) { if (dFlag) diff --git a/src/N_m3u8DL-RE/Util/OtherUtil.cs b/src/N_m3u8DL-RE/Util/OtherUtil.cs index ece61c8..9ea336c 100644 --- a/src/N_m3u8DL-RE/Util/OtherUtil.cs +++ b/src/N_m3u8DL-RE/Util/OtherUtil.cs @@ -1,35 +1,29 @@ -using N_m3u8DL_RE.Common.Entity; -using N_m3u8DL_RE.Common.Log; -using N_m3u8DL_RE.Enum; -using System.CommandLine; +using N_m3u8DL_RE.Enum; using System.IO.Compression; -using System.Text; using System.Text.RegularExpressions; namespace N_m3u8DL_RE.Util; -internal class OtherUtil +internal static class OtherUtil { public static Dictionary SplitHeaderArrayToDic(string[]? headers) { Dictionary dic = new(); - - if (headers != null) + if (headers == null) return dic; + + foreach (string header in headers) { - foreach (string header in headers) + var index = header.IndexOf(':'); + if (index != -1) { - var index = header.IndexOf(':'); - if (index != -1) - { - dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim(); - } + dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim(); } } 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(); public static string GetValidFileName(string input, string re = ".", bool filterSlash = false) { @@ -50,6 +44,7 @@ internal class OtherUtil /// 从输入自动获取文件名 /// /// + /// /// public static string GetFileNameFromInput(string input, bool addSuffix = true) { @@ -119,7 +114,7 @@ internal class OtherUtil return hours * 3600 + minutes * 60 + seconds; } - //若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录 + // 若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录 public static void SafeDeleteDir(string dirPath) { if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath)) diff --git a/src/N_m3u8DL-RE/Util/PipeUtil.cs b/src/N_m3u8DL-RE/Util/PipeUtil.cs index 0d99e5d..e3452a4 100644 --- a/src/N_m3u8DL-RE/Util/PipeUtil.cs +++ b/src/N_m3u8DL-RE/Util/PipeUtil.cs @@ -1,14 +1,8 @@ using N_m3u8DL_RE.Common.Log; -using N_m3u8DL_RE.Common.Resource; using Spectre.Console; -using System; -using System.Collections.Generic; -using System.CommandLine; using System.Diagnostics; using System.IO.Pipes; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace N_m3u8DL_RE.Util; @@ -67,7 +61,7 @@ internal static class PipeUtil if (OperatingSystem.IsWindows()) command.Append($" -i \"\\\\.\\pipe\\{item}\" "); 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)}\" "); } @@ -103,7 +97,7 @@ internal static class PipeUtil CreateNoWindow = true, 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.WaitForExit(); diff --git a/src/N_m3u8DL-RE/Util/SubtitleUtil.cs b/src/N_m3u8DL-RE/Util/SubtitleUtil.cs index 44028e9..52e253a 100644 --- a/src/N_m3u8DL-RE/Util/SubtitleUtil.cs +++ b/src/N_m3u8DL-RE/Util/SubtitleUtil.cs @@ -1,11 +1,6 @@ using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Log; 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; @@ -17,23 +12,21 @@ internal static class SubtitleUtil /// /// 临时目录 /// - public static async Task 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::"))) { Logger.WarnMarkUp(ResString.processImageSub); - var _i = 0; + var i = 0; foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::"))) { - var name = $"{_i++}.png"; + var name = $"{i++}.png"; 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..]; await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64)); img.Payload = name; } - return true; } - else return false; } } \ No newline at end of file