From 9fc37d5b6176fbf63eb19f5064bc3f1af2f75e47 Mon Sep 17 00:00:00 2001 From: irodai-majom <181323284+irodai-majom@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:15:30 +0100 Subject: [PATCH] Marked util classes as static (#460) * Marked util classes as static * Used file-scoped namespaces --- src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs | 115 ++-- src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs | 243 ++++---- src/N_m3u8DL-RE.Common/Util/HexUtil.cs | 77 ++- src/N_m3u8DL-RE.Common/Util/RetryUtil.cs | 2 +- src/N_m3u8DL-RE/Util/DownloadUtil.cs | 251 ++++---- src/N_m3u8DL-RE/Util/FilterUtil.cs | 485 ++++++++-------- src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs | 130 ++--- src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs | 130 ++--- .../Util/LargeSingleFileSplitUtil.cs | 199 ++++--- src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs | 293 +++++----- src/N_m3u8DL-RE/Util/MediainfoUtil.cs | 153 +++-- src/N_m3u8DL-RE/Util/MergeUtil.cs | 547 +++++++++--------- src/N_m3u8DL-RE/Util/OtherUtil.cs | 303 +++++----- src/N_m3u8DL-RE/Util/PipeUtil.cs | 173 +++--- src/N_m3u8DL-RE/Util/SubtitleUtil.cs | 47 +- 15 files changed, 1560 insertions(+), 1588 deletions(-) diff --git a/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs b/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs index f25bc97..f41dce9 100644 --- a/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs @@ -8,73 +8,72 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Common.Util +namespace N_m3u8DL_RE.Common.Util; + +public static class GlobalUtil { - public class GlobalUtil + private static readonly JsonSerializerOptions Options = new JsonSerializerOptions { - private static readonly JsonSerializerOptions Options = new JsonSerializerOptions - { - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() } - }; - private static readonly JsonContext Context = new JsonContext(Options); + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() } + }; + private static readonly JsonContext Context = new JsonContext(Options); - public static string ConvertToJson(object o) + public static string ConvertToJson(object o) + { + if (o is StreamSpec s) { - if (o is StreamSpec s) - { - return JsonSerializer.Serialize(s, Context.StreamSpec); - } - else if (o is IOrderedEnumerable ss) - { - return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec); - } - else if (o is List sList) - { - return JsonSerializer.Serialize(sList, Context.ListStreamSpec); - } - else if (o is IEnumerable mList) - { - return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment); - } - return "{NOT SUPPORTED}"; + return JsonSerializer.Serialize(s, Context.StreamSpec); } - - public static string FormatFileSize(double fileSize) + else if (o is IOrderedEnumerable ss) { - 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) - }; + return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec); } - - //此函数用于格式化输出时长 - public static string FormatTime(int time) + else if (o is List sList) { - TimeSpan ts = new TimeSpan(0, 0, time); - string str = ""; - str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s"; - return str; + return JsonSerializer.Serialize(sList, Context.ListStreamSpec); } - - /// - /// 寻找可执行程序 - /// - /// - /// - public static string? FindExecutable(string name) + else if (o is IEnumerable mList) { - var fileExt = OperatingSystem.IsWindows() ? ".exe" : ""; - 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 JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment); } + return "{NOT SUPPORTED}"; } -} + + public static string FormatFileSize(double fileSize) + { + 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) + }; + } + + //此函数用于格式化输出时长 + public static string FormatTime(int time) + { + TimeSpan ts = new TimeSpan(0, 0, time); + string str = ""; + str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s"; + return str; + } + + /// + /// 寻找可执行程序 + /// + /// + /// + public static string? FindExecutable(string name) + { + var fileExt = OperatingSystem.IsWindows() ? ".exe" : ""; + 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); + } +} \ 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 b0f0def..973f5ee 100644 --- a/src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs @@ -3,139 +3,138 @@ using System.Net.Http.Headers; using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Resource; -namespace N_m3u8DL_RE.Common.Util +namespace N_m3u8DL_RE.Common.Util; + +public static class HTTPUtil { - public class HTTPUtil + public static readonly HttpClientHandler HttpClientHandler = new() { - public static readonly HttpClientHandler HttpClientHandler = new() - { - AllowAutoRedirect = false, - AutomaticDecompression = DecompressionMethods.All, - ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true, - MaxConnectionsPerServer = 1024, - }; + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.All, + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true, + MaxConnectionsPerServer = 1024, + }; - public static readonly HttpClient AppHttpClient = new(HttpClientHandler) - { - Timeout = TimeSpan.FromSeconds(100), - DefaultRequestVersion = HttpVersion.Version20, - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher, - }; + public static readonly HttpClient AppHttpClient = new(HttpClientHandler) + { + Timeout = TimeSpan.FromSeconds(100), + DefaultRequestVersion = HttpVersion.Version20, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher, + }; - private static async Task DoGetAsync(string url, Dictionary? headers = null) + private static async Task DoGetAsync(string url, Dictionary? headers = null) + { + Logger.Debug(ResString.fetch + url); + using var webRequest = new HttpRequestMessage(HttpMethod.Get, url); + webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate"); + webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache"); + webRequest.Headers.Connection.Clear(); + if (headers != null) { - Logger.Debug(ResString.fetch + url); - using var webRequest = new HttpRequestMessage(HttpMethod.Get, url); - webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate"); - webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache"); - webRequest.Headers.Connection.Clear(); - if (headers != null) + foreach (var item in headers) { - foreach (var item in headers) - { - webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value); - } + webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value); } - Logger.Debug(webRequest.Headers.ToString()); - //手动处理跳转,以免自定义Headers丢失 - var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead); - if (((int)webResponse.StatusCode).ToString().StartsWith("30")) + } + Logger.Debug(webRequest.Headers.ToString()); + //手动处理跳转,以免自定义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) { - HttpResponseHeaders respHeaders = webResponse.Headers; - Logger.Debug(respHeaders.ToString()); - if (respHeaders != null && respHeaders.Location != null) + var redirectedUrl = ""; + if (!respHeaders.Location.IsAbsoluteUri) { - var redirectedUrl = ""; - if (!respHeaders.Location.IsAbsoluteUri) - { - Uri uri1 = new Uri(url); - Uri uri2 = new Uri(uri1, respHeaders.Location); - redirectedUrl = uri2.ToString(); - } - else - { - redirectedUrl = respHeaders.Location.AbsoluteUri; - } + Uri uri1 = new Uri(url); + Uri uri2 = new Uri(uri1, respHeaders.Location); + redirectedUrl = uri2.ToString(); + } + else + { + redirectedUrl = respHeaders.Location.AbsoluteUri; + } - if (redirectedUrl != url) - { - Logger.Extra($"Redirected => {redirectedUrl}"); - return await DoGetAsync(redirectedUrl, headers); - } + if (redirectedUrl != url) + { + Logger.Extra($"Redirected => {redirectedUrl}"); + return await DoGetAsync(redirectedUrl, headers); } } - //手动将跳转后的URL设置进去, 用于后续取用 - webResponse.Headers.Location = new Uri(url); - webResponse.EnsureSuccessStatusCode(); - return webResponse; - } - - public static async Task GetBytesAsync(string url, Dictionary? headers = null) - { - if (url.StartsWith("file:")) - { - return await File.ReadAllBytesAsync(new Uri(url).LocalPath); - } - byte[] bytes = new byte[0]; - var webResponse = await DoGetAsync(url, headers); - bytes = await webResponse.Content.ReadAsByteArrayAsync(); - Logger.Debug(HexUtil.BytesToHex(bytes, " ")); - return bytes; - } - - /// - /// 获取网页源码 - /// - /// - /// - /// - public static async Task GetWebSourceAsync(string url, Dictionary? headers = null) - { - string htmlCode = string.Empty; - var webResponse = await DoGetAsync(url, headers); - htmlCode = await webResponse.Content.ReadAsStringAsync(); - Logger.Debug(htmlCode); - return htmlCode; - } - - private static bool CheckMPEG2TS(HttpResponseMessage? webResponse) - { - var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower(); - return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg"; - } - - /// - /// 获取网页源码和跳转后的URL - /// - /// - /// - /// (Source Code, RedirectedUrl) - public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary? headers = null) - { - string htmlCode = string.Empty; - var webResponse = await DoGetAsync(url, headers); - if (CheckMPEG2TS(webResponse)) - { - htmlCode = ResString.ReLiveTs; - } - else - { - htmlCode = await webResponse.Content.ReadAsStringAsync(); - } - Logger.Debug(htmlCode); - return (htmlCode, webResponse.Headers.Location != null ? webResponse.Headers.Location.AbsoluteUri : url); - } - - public static async Task GetPostResponseAsync(string Url, byte[] postData) - { - string htmlCode = string.Empty; - using HttpRequestMessage request = new(HttpMethod.Post, Url); - request.Headers.TryAddWithoutValidation("Content-Type", "application/json"); - request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString()); - request.Content = new ByteArrayContent(postData); - var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - htmlCode = await webResponse.Content.ReadAsStringAsync(); - return htmlCode; } + //手动将跳转后的URL设置进去, 用于后续取用 + webResponse.Headers.Location = new Uri(url); + webResponse.EnsureSuccessStatusCode(); + return webResponse; } -} + + public static async Task GetBytesAsync(string url, Dictionary? headers = null) + { + if (url.StartsWith("file:")) + { + return await File.ReadAllBytesAsync(new Uri(url).LocalPath); + } + byte[] bytes = new byte[0]; + var webResponse = await DoGetAsync(url, headers); + bytes = await webResponse.Content.ReadAsByteArrayAsync(); + Logger.Debug(HexUtil.BytesToHex(bytes, " ")); + return bytes; + } + + /// + /// 获取网页源码 + /// + /// + /// + /// + public static async Task GetWebSourceAsync(string url, Dictionary? headers = null) + { + string htmlCode = string.Empty; + var webResponse = await DoGetAsync(url, headers); + htmlCode = await webResponse.Content.ReadAsStringAsync(); + Logger.Debug(htmlCode); + return htmlCode; + } + + private static bool CheckMPEG2TS(HttpResponseMessage? webResponse) + { + var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower(); + return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg"; + } + + /// + /// 获取网页源码和跳转后的URL + /// + /// + /// + /// (Source Code, RedirectedUrl) + public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary? headers = null) + { + string htmlCode = string.Empty; + var webResponse = await DoGetAsync(url, headers); + if (CheckMPEG2TS(webResponse)) + { + htmlCode = ResString.ReLiveTs; + } + else + { + htmlCode = await webResponse.Content.ReadAsStringAsync(); + } + Logger.Debug(htmlCode); + return (htmlCode, webResponse.Headers.Location != null ? webResponse.Headers.Location.AbsoluteUri : url); + } + + public static async Task GetPostResponseAsync(string Url, byte[] postData) + { + string htmlCode = string.Empty; + using HttpRequestMessage request = new(HttpMethod.Post, Url); + request.Headers.TryAddWithoutValidation("Content-Type", "application/json"); + request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString()); + request.Content = new ByteArrayContent(postData); + var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + htmlCode = await webResponse.Content.ReadAsStringAsync(); + return htmlCode; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Util/HexUtil.cs b/src/N_m3u8DL-RE.Common/Util/HexUtil.cs index d20c636..2aea255 100644 --- a/src/N_m3u8DL-RE.Common/Util/HexUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/HexUtil.cs @@ -1,46 +1,39 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Common.Util; -namespace N_m3u8DL_RE.Common.Util +public static class HexUtil { - public class HexUtil + public static string BytesToHex(byte[] data, string split = "") { - public static string BytesToHex(byte[] data, string split = "") - { - return BitConverter.ToString(data).Replace("-", split); - } - - /// - /// 判断是不是HEX字符串 - /// - /// - /// - public static bool TryParseHexString(string input, out byte[]? bytes) - { - bytes = null; - input = input.ToUpper(); - if (input.StartsWith("0X")) - input = input[2..]; - if (input.Length % 2 != 0) - return false; - if (input.Any(c => !"0123456789ABCDEF".Contains(c))) - return false; - bytes = HexToBytes(input); - return true; - } - - public static byte[] HexToBytes(string hex) - { - var hexSpan = hex.AsSpan().Trim(); - if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X")) - { - hexSpan = hexSpan.Slice(2); - } - - return Convert.FromHexString(hexSpan); - } + return BitConverter.ToString(data).Replace("-", split); } -} + + /// + /// 判断是不是HEX字符串 + /// + /// + /// + public static bool TryParseHexString(string input, out byte[]? bytes) + { + bytes = null; + input = input.ToUpper(); + if (input.StartsWith("0X")) + input = input[2..]; + if (input.Length % 2 != 0) + return false; + if (input.Any(c => !"0123456789ABCDEF".Contains(c))) + return false; + bytes = HexToBytes(input); + return true; + } + + public static byte[] HexToBytes(string hex) + { + var hexSpan = hex.AsSpan().Trim(); + if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X")) + { + hexSpan = hexSpan.Slice(2); + } + + return Convert.FromHexString(hexSpan); + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE.Common/Util/RetryUtil.cs b/src/N_m3u8DL-RE.Common/Util/RetryUtil.cs index 096c6e3..2f42006 100644 --- a/src/N_m3u8DL-RE.Common/Util/RetryUtil.cs +++ b/src/N_m3u8DL-RE.Common/Util/RetryUtil.cs @@ -4,7 +4,7 @@ using Spectre.Console; namespace N_m3u8DL_RE.Common.Util; -public class RetryUtil +public static class RetryUtil { public static async Task WebRequestRetryAsync(Func> funcAsync, int maxRetries = 10, int retryDelayMilliseconds = 1500, int retryDelayIncrementMilliseconds = 0) { diff --git a/src/N_m3u8DL-RE/Util/DownloadUtil.cs b/src/N_m3u8DL-RE/Util/DownloadUtil.cs index b0604de..fd5d9d8 100644 --- a/src/N_m3u8DL-RE/Util/DownloadUtil.cs +++ b/src/N_m3u8DL-RE/Util/DownloadUtil.cs @@ -6,141 +6,140 @@ using System.IO; using System.Net; using System.Net.Http.Headers; -namespace N_m3u8DL_RE.Util -{ - internal class DownloadUtil - { - private static readonly HttpClient AppHttpClient = HTTPUtil.AppHttpClient; +namespace N_m3u8DL_RE.Util; - private static async Task CopyFileAsync(string sourceFile, string path, SpeedContainer speedContainer, long? fromPosition = null, long? toPosition = null) +internal static class DownloadUtil +{ + private static readonly HttpClient AppHttpClient = HTTPUtil.AppHttpClient; + + private static async Task CopyFileAsync(string sourceFile, string path, SpeedContainer speedContainer, long? fromPosition = null, long? toPosition = null) + { + using var inputStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read); + using var outputStream = new FileStream(path, FileMode.OpenOrCreate); + inputStream.Seek(fromPosition ?? 0L, SeekOrigin.Begin); + var expect = (toPosition ?? inputStream.Length) - inputStream.Position + 1; + if (expect == inputStream.Length + 1) { - using var inputStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read); - using var outputStream = new FileStream(path, FileMode.OpenOrCreate); - inputStream.Seek(fromPosition ?? 0L, SeekOrigin.Begin); - var expect = (toPosition ?? inputStream.Length) - inputStream.Position + 1; - if (expect == inputStream.Length + 1) - { - await inputStream.CopyToAsync(outputStream); - speedContainer.Add(inputStream.Length); - } - else - { - var buffer = new byte[expect]; - await inputStream.ReadAsync(buffer); - await outputStream.WriteAsync(buffer, 0, buffer.Length); - speedContainer.Add(buffer.Length); - } + await inputStream.CopyToAsync(outputStream); + speedContainer.Add(inputStream.Length); + } + else + { + var buffer = new byte[expect]; + await inputStream.ReadAsync(buffer); + await outputStream.WriteAsync(buffer, 0, buffer.Length); + speedContainer.Add(buffer.Length); + } + return new DownloadResult() + { + ActualContentLength = outputStream.Length, + ActualFilePath = path + }; + } + + public static async Task DownloadToFileAsync(string url, string path, SpeedContainer speedContainer, CancellationTokenSource cancellationTokenSource, Dictionary? headers = null, long? fromPosition = null, long? toPosition = null) + { + Logger.Debug(ResString.fetch + url); + if (url.StartsWith("file:")) + { + var file = new Uri(url).LocalPath; + return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition); + } + if (url.StartsWith("base64://")) + { + var bytes = Convert.FromBase64String(url[9..]); + await File.WriteAllBytesAsync(path, bytes); return new DownloadResult() { - ActualContentLength = outputStream.Length, - ActualFilePath = path + ActualContentLength = bytes.Length, + ActualFilePath = path, }; } - - public static async Task DownloadToFileAsync(string url, string path, SpeedContainer speedContainer, CancellationTokenSource cancellationTokenSource, Dictionary? headers = null, long? fromPosition = null, long? toPosition = null) + if (url.StartsWith("hex://")) { - Logger.Debug(ResString.fetch + url); - if (url.StartsWith("file:")) + var bytes = HexUtil.HexToBytes(url[6..]); + await File.WriteAllBytesAsync(path, bytes); + return new DownloadResult() { - var file = new Uri(url).LocalPath; - return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition); - } - if (url.StartsWith("base64://")) + ActualContentLength = bytes.Length, + ActualFilePath = path, + }; + } + using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); + if (fromPosition != null || toPosition != null) + request.Headers.Range = new(fromPosition, toPosition); + if (headers != null) + { + foreach (var item in headers) { - var bytes = Convert.FromBase64String(url[9..]); - await File.WriteAllBytesAsync(path, bytes); - return new DownloadResult() - { - ActualContentLength = bytes.Length, - ActualFilePath = path, - }; - } - if (url.StartsWith("hex://")) - { - var bytes = HexUtil.HexToBytes(url[6..]); - await File.WriteAllBytesAsync(path, bytes); - return new DownloadResult() - { - ActualContentLength = bytes.Length, - ActualFilePath = path, - }; - } - using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); - if (fromPosition != null || toPosition != null) - request.Headers.Range = new(fromPosition, toPosition); - if (headers != null) - { - foreach (var item in headers) - { - request.Headers.TryAddWithoutValidation(item.Key, item.Value); - } - } - Logger.Debug(request.Headers.ToString()); - try - { - using var response = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token); - if (((int)response.StatusCode).ToString().StartsWith("30")) - { - HttpResponseHeaders respHeaders = response.Headers; - Logger.Debug(respHeaders.ToString()); - if (respHeaders != null && respHeaders.Location != null) - { - var redirectedUrl = ""; - if (!respHeaders.Location.IsAbsoluteUri) - { - Uri uri1 = new Uri(url); - Uri uri2 = new Uri(uri1, respHeaders.Location); - redirectedUrl = uri2.ToString(); - } - else - { - redirectedUrl = respHeaders.Location.AbsoluteUri; - } - return await DownloadToFileAsync(redirectedUrl, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition); - } - } - response.EnsureSuccessStatusCode(); - var contentLength = response.Content.Headers.ContentLength; - if (speedContainer.SingleSegment) speedContainer.ResponseLength = contentLength; - - using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using var responseStream = await response.Content.ReadAsStreamAsync(cancellationTokenSource.Token); - var buffer = new byte[16 * 1024]; - var size = 0; - - size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token); - speedContainer.Add(size); - await stream.WriteAsync(buffer, 0, size); - //检测imageHeader - bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer); - //检测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); - } - } - - return new DownloadResult() - { - ActualContentLength = stream.Length, - RespContentLength = contentLength, - ActualFilePath = path, - ImageHeader= imageHeader, - GzipHeader = gZipHeader - }; - } - catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationTokenSource.Token) - { - speedContainer.ResetLowSpeedCount(); - throw new Exception("Download speed too slow!"); + request.Headers.TryAddWithoutValidation(item.Key, item.Value); } } + Logger.Debug(request.Headers.ToString()); + try + { + using var response = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token); + if (((int)response.StatusCode).ToString().StartsWith("30")) + { + HttpResponseHeaders respHeaders = response.Headers; + Logger.Debug(respHeaders.ToString()); + if (respHeaders != null && respHeaders.Location != null) + { + var redirectedUrl = ""; + if (!respHeaders.Location.IsAbsoluteUri) + { + Uri uri1 = new Uri(url); + Uri uri2 = new Uri(uri1, respHeaders.Location); + redirectedUrl = uri2.ToString(); + } + else + { + redirectedUrl = respHeaders.Location.AbsoluteUri; + } + return await DownloadToFileAsync(redirectedUrl, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition); + } + } + response.EnsureSuccessStatusCode(); + var contentLength = response.Content.Headers.ContentLength; + if (speedContainer.SingleSegment) speedContainer.ResponseLength = contentLength; + + using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using var responseStream = await response.Content.ReadAsStreamAsync(cancellationTokenSource.Token); + var buffer = new byte[16 * 1024]; + var size = 0; + + size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token); + speedContainer.Add(size); + await stream.WriteAsync(buffer, 0, size); + //检测imageHeader + bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer); + //检测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); + } + } + + return new DownloadResult() + { + ActualContentLength = stream.Length, + RespContentLength = contentLength, + ActualFilePath = path, + ImageHeader= imageHeader, + GzipHeader = gZipHeader + }; + } + catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationTokenSource.Token) + { + speedContainer.ResetLowSpeedCount(); + throw new Exception("Download speed too slow!"); + } } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/FilterUtil.cs b/src/N_m3u8DL-RE/Util/FilterUtil.cs index ec06e64..f876d00 100644 --- a/src/N_m3u8DL-RE/Util/FilterUtil.cs +++ b/src/N_m3u8DL-RE/Util/FilterUtil.cs @@ -11,274 +11,273 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +public static class FilterUtil { - public class FilterUtil + public static List DoFilterKeep(IEnumerable lists, StreamFilter? filter) { - public static List DoFilterKeep(IEnumerable lists, StreamFilter? filter) - { - if (filter == null) return new List(); + if (filter == null) return new List(); - var inputs = lists.Where(_ => true); - if (filter.GroupIdReg != null) - inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId)); - if (filter.LanguageReg != null) - inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language)); - if (filter.NameReg != null) - inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name)); - if (filter.CodecsReg != null) - inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs)); - if (filter.ResolutionReg != null) - inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution)); - if (filter.FrameRateReg != null) - inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($"{i.FrameRate}")); - if (filter.ChannelsReg != null) - inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels)); - if (filter.VideoRangeReg != null) - inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange)); - if (filter.UrlReg != null) - inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url)); - if (filter.SegmentsMaxCount != null && inputs.All(i => i.SegmentsCount > 0)) - inputs = inputs.Where(i => i.SegmentsCount < filter.SegmentsMaxCount); - if (filter.SegmentsMinCount != null && inputs.All(i => i.SegmentsCount > 0)) - inputs = inputs.Where(i => i.SegmentsCount > filter.SegmentsMinCount); - if (filter.PlaylistMinDur != null) - inputs = inputs.Where(i => i.Playlist?.TotalDuration > filter.PlaylistMinDur); - if (filter.PlaylistMaxDur != null) - inputs = inputs.Where(i => i.Playlist?.TotalDuration < filter.PlaylistMaxDur); - if (filter.BandwidthMin != null) - inputs = inputs.Where(i => i.Bandwidth >= filter.BandwidthMin); - if (filter.BandwidthMax != null) - inputs = inputs.Where(i => i.Bandwidth <= filter.BandwidthMax); - if (filter.Role.HasValue) - inputs = inputs.Where(i => i.Role == filter.Role); + var inputs = lists.Where(_ => true); + if (filter.GroupIdReg != null) + inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId)); + if (filter.LanguageReg != null) + inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language)); + if (filter.NameReg != null) + inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name)); + if (filter.CodecsReg != null) + inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs)); + if (filter.ResolutionReg != null) + inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution)); + if (filter.FrameRateReg != null) + inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($"{i.FrameRate}")); + if (filter.ChannelsReg != null) + inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels)); + if (filter.VideoRangeReg != null) + inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange)); + if (filter.UrlReg != null) + inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url)); + if (filter.SegmentsMaxCount != null && inputs.All(i => i.SegmentsCount > 0)) + inputs = inputs.Where(i => i.SegmentsCount < filter.SegmentsMaxCount); + if (filter.SegmentsMinCount != null && inputs.All(i => i.SegmentsCount > 0)) + inputs = inputs.Where(i => i.SegmentsCount > filter.SegmentsMinCount); + if (filter.PlaylistMinDur != null) + inputs = inputs.Where(i => i.Playlist?.TotalDuration > filter.PlaylistMinDur); + if (filter.PlaylistMaxDur != null) + inputs = inputs.Where(i => i.Playlist?.TotalDuration < filter.PlaylistMaxDur); + if (filter.BandwidthMin != null) + inputs = inputs.Where(i => i.Bandwidth >= filter.BandwidthMin); + if (filter.BandwidthMax != null) + inputs = inputs.Where(i => i.Bandwidth <= filter.BandwidthMax); + if (filter.Role.HasValue) + inputs = inputs.Where(i => i.Role == filter.Role); - var bestNumberStr = filter.For.Replace("best", ""); - var worstNumberStr = filter.For.Replace("worst", ""); + var bestNumberStr = filter.For.Replace("best", ""); + var worstNumberStr = filter.For.Replace("worst", ""); - if (filter.For == "best" && inputs.Count() > 0) - inputs = inputs.Take(1).ToList(); - else if (filter.For == "worst" && inputs.Count() > 0) - inputs = inputs.TakeLast(1).ToList(); - else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0) - inputs = inputs.Take(bestNumber).ToList(); - else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0) - inputs = inputs.TakeLast(worstNumber).ToList(); + if (filter.For == "best" && inputs.Count() > 0) + inputs = inputs.Take(1).ToList(); + else if (filter.For == "worst" && inputs.Count() > 0) + inputs = inputs.TakeLast(1).ToList(); + else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0) + inputs = inputs.Take(bestNumber).ToList(); + else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0) + inputs = inputs.TakeLast(worstNumber).ToList(); - return inputs.ToList(); - } + return inputs.ToList(); + } - public static List DoFilterDrop(IEnumerable lists, StreamFilter? filter) - { - if (filter == null) return new List(lists); + public static List DoFilterDrop(IEnumerable lists, StreamFilter? filter) + { + if (filter == null) return new List(lists); - var inputs = lists.Where(_ => true); - var selected = DoFilterKeep(lists, filter); + var inputs = lists.Where(_ => true); + var selected = DoFilterKeep(lists, filter); - inputs = inputs.Where(i => selected.All(s => s.ToString() != i.ToString())); + inputs = inputs.Where(i => selected.All(s => s.ToString() != i.ToString())); - return inputs.ToList(); - } + return inputs.ToList(); + } - public static List SelectStreams(IEnumerable lists) - { - if (lists.Count() == 1) - return new List(lists); + public static List SelectStreams(IEnumerable lists) + { + if (lists.Count() == 1) + return new List(lists); - //基本流 - var basicStreams = lists.Where(x => x.MediaType == null); - //可选音频轨道 - var audios = lists.Where(x => x.MediaType == MediaType.AUDIO); - //可选字幕轨道 - var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES); + //基本流 + var basicStreams = lists.Where(x => x.MediaType == null); + //可选音频轨道 + var audios = lists.Where(x => x.MediaType == MediaType.AUDIO); + //可选字幕轨道 + var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES); - var prompt = new MultiSelectionPrompt() - .Title(ResString.promptTitle) - .UseConverter(x => - { - if (x.Name != null && x.Name.StartsWith("__")) - return $"[darkslategray1]{x.Name.Substring(2)}[/]"; - else - return x.ToString().EscapeMarkup().RemoveMarkup(); - }) - .Required() - .PageSize(10) - .MoreChoicesText(ResString.promptChoiceText) - .InstructionsText(ResString.promptInfo) - ; - - //默认选中第一个 - var first = lists.First(); - prompt.Select(first); - - if (basicStreams.Any()) - { - prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams); - } - - if (audios.Any()) - { - prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios); - //默认音轨 - if (first.AudioId != null) + var prompt = new MultiSelectionPrompt() + .Title(ResString.promptTitle) + .UseConverter(x => { - prompt.Select(audios.First(a => a.GroupId == first.AudioId)); - } - } - if (subs.Any()) - { - prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs); - //默认字幕轨 - if (first.SubtitleId != null) - { - prompt.Select(subs.First(s => s.GroupId == first.SubtitleId)); - } - } - - //如果此时还是没有选中任何流,自动选择一个 - prompt.Select(basicStreams.Concat(audios).Concat(subs).First()); - - //多选 - var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt); - - return selectedStreams; - } - - /// - /// 直播使用。对齐各个轨道的起始。 - /// - /// - /// - public static void SyncStreams(List selectedSteams, int takeLastCount = 15) - { - //通过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))!; - foreach (var item in selectedSteams) - { - 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同步 - { - var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index)); - foreach (var item in selectedSteams) - { - foreach (var part in item.Playlist!.MediaParts) - { - part.MediaSegments = part.MediaSegments.Where(s => s.Index >= minIndex).ToList(); - } - } - } - - //取最新的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; - if (skipCount < 0) skipCount = 0; - foreach (var item in selectedSteams) - { - foreach (var part in item.Playlist!.MediaParts) - { - part.MediaSegments = part.MediaSegments.Skip(skipCount).ToList(); - } - } - } - } - - /// - /// 应用用户自定义的分片范围 - /// - /// - /// - 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; - - if (!filteByIndex && !filteByTime) - { - Logger.ErrorMarkUp(ResString.customRangeInvalid); - return; - } - - foreach (var stream in selectedSteams) - { - var skippedDur = 0d; - if (stream.Playlist == null) continue; - foreach (var part in stream.Playlist.MediaParts) - { - var newSegments = new List(); - if (filteByIndex) - newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList(); + if (x.Name != null && x.Name.StartsWith("__")) + return $"[darkslategray1]{x.Name.Substring(2)}[/]"; 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 - && stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) <= customRange.EndSec).ToList(); + return x.ToString().EscapeMarkup().RemoveMarkup(); + }) + .Required() + .PageSize(10) + .MoreChoicesText(ResString.promptChoiceText) + .InstructionsText(ResString.promptInfo) + ; - if (newSegments.Count > 0) - skippedDur += part.MediaSegments.Where(seg => seg.Index < newSegments.First().Index).Sum(x => x.Duration); - part.MediaSegments = newSegments; - } - stream.SkippedDuration = skippedDur; + //默认选中第一个 + var first = lists.First(); + prompt.Select(first); + + if (basicStreams.Any()) + { + prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams); + } + + if (audios.Any()) + { + prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios); + //默认音轨 + if (first.AudioId != null) + { + prompt.Select(audios.First(a => a.GroupId == first.AudioId)); + } + } + if (subs.Any()) + { + prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs); + //默认字幕轨 + if (first.SubtitleId != null) + { + prompt.Select(subs.First(s => s.GroupId == first.SubtitleId)); } } - /// - /// 根据用户输入,清除广告分片 - /// - /// - /// - public static void CleanAd(List selectedSteams, string[]? keywords) + //如果此时还是没有选中任何流,自动选择一个 + prompt.Select(basicStreams.Concat(audios).Concat(subs).First()); + + //多选 + var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt); + + return selectedStreams; + } + + /// + /// 直播使用。对齐各个轨道的起始。 + /// + /// + /// + public static void SyncStreams(List selectedSteams, int takeLastCount = 15) + { + //通过Date同步 + if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null))) { - if (keywords == null) return; - var regList = keywords.Select(s => new Regex(s)); - foreach ( var reg in regList) + var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!; + foreach (var item in selectedSteams) { - Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]"); - } - - foreach (var stream in selectedSteams) - { - if (stream.Playlist == null) continue; - - var countBefore = stream.SegmentsCount; - - foreach (var part in stream.Playlist.MediaParts) + foreach (var part in item.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(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList(); } - - //清理已经为空的 part - stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList(); - - var countAfter = stream.SegmentsCount; - - if (countBefore != countAfter) + } + } + else //通过index同步 + { + var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index)); + foreach (var item in selectedSteams) + { + foreach (var part in item.Playlist!.MediaParts) { - Logger.WarnMarkUp("[grey]{} segments => {} segments[/]", countBefore, countAfter); + part.MediaSegments = part.MediaSegments.Where(s => s.Index >= minIndex).ToList(); + } + } + } + + //取最新的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; + if (skipCount < 0) skipCount = 0; + foreach (var item in selectedSteams) + { + foreach (var part in item.Playlist!.MediaParts) + { + part.MediaSegments = part.MediaSegments.Skip(skipCount).ToList(); } } } } -} + + /// + /// 应用用户自定义的分片范围 + /// + /// + /// + 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; + + if (!filteByIndex && !filteByTime) + { + Logger.ErrorMarkUp(ResString.customRangeInvalid); + return; + } + + foreach (var stream in selectedSteams) + { + var skippedDur = 0d; + if (stream.Playlist == null) continue; + foreach (var part in stream.Playlist.MediaParts) + { + var newSegments = new List(); + if (filteByIndex) + 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 + && stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) <= customRange.EndSec).ToList(); + + if (newSegments.Count > 0) + skippedDur += part.MediaSegments.Where(seg => seg.Index < newSegments.First().Index).Sum(x => x.Duration); + part.MediaSegments = newSegments; + } + stream.SkippedDuration = skippedDur; + } + } + + /// + /// 根据用户输入,清除广告分片 + /// + /// + /// + public static void CleanAd(List selectedSteams, string[]? keywords) + { + if (keywords == null) return; + var regList = keywords.Select(s => new Regex(s)); + foreach ( var reg in regList) + { + Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]"); + } + + foreach (var stream in selectedSteams) + { + if (stream.Playlist == null) continue; + + var countBefore = stream.SegmentsCount; + + 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 + stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList(); + + var countAfter = stream.SegmentsCount; + + if (countBefore != countAfter) + { + Logger.WarnMarkUp("[grey]{} segments => {} segments[/]", countBefore, countAfter); + } + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs b/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs index 81b7f7f..9fe52e9 100644 --- a/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs +++ b/src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs @@ -1,74 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace N_m3u8DL_RE.Util; -namespace N_m3u8DL_RE.Util +internal static class ImageHeaderUtil { - internal class ImageHeaderUtil + public static bool IsImageHeader(byte[] bArr) { - public static bool IsImageHeader(byte[] bArr) - { - var size = bArr.Length; - //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]) - 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]) - return true; - //JPEG HEADER检测 - else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2]) - return true; - return false; - } + var size = bArr.Length; + //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]) + 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]) + return true; + //JPEG HEADER检测 + else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2]) + return true; + return false; + } - public static async Task ProcessAsync(string sourcePath) - { - var sourceData = await File.ReadAllBytesAsync(sourcePath); + public static async Task ProcessAsync(string sourcePath) + { + var sourceData = await File.ReadAllBytesAsync(sourcePath); - //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]) - sourceData = sourceData[120..]; - else if (sourceData.Length > 6102 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[6100] && 130 == sourceData[6101]) - sourceData = sourceData[6102..]; - else if (sourceData.Length > 69 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[67] && 130 == sourceData[68]) - sourceData = sourceData[69..]; - else if (sourceData.Length > 771 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[769] && 130 == sourceData[770]) - sourceData = sourceData[771..]; - else - { - //手动查询结尾标记 0x47 出现两次 - int skip = 0; - for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) - { - if (sourceData[i] == 0x47 && sourceData[i + 188] == 0x47 && sourceData[i + 188 + 188] == 0x47) - { - skip = i; - break; - } - } - sourceData = sourceData[skip..]; - } - } - //GIF HEADER - else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3]) - { - sourceData = sourceData[42..]; - } - //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检测 - else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2]) + //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]) + sourceData = sourceData[120..]; + else if (sourceData.Length > 6102 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[6100] && 130 == sourceData[6101]) + sourceData = sourceData[6102..]; + else if (sourceData.Length > 69 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[67] && 130 == sourceData[68]) + sourceData = sourceData[69..]; + else if (sourceData.Length > 771 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[769] && 130 == sourceData[770]) + sourceData = sourceData[771..]; + else { //手动查询结尾标记 0x47 出现两次 int skip = 0; @@ -82,8 +49,33 @@ namespace N_m3u8DL_RE.Util } sourceData = sourceData[skip..]; } - - await File.WriteAllBytesAsync(sourcePath, sourceData); } + //GIF HEADER + else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3]) + { + sourceData = sourceData[42..]; + } + //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检测 + else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2]) + { + //手动查询结尾标记 0x47 出现两次 + int skip = 0; + for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) + { + if (sourceData[i] == 0x47 && sourceData[i + 188] == 0x47 && sourceData[i + 188 + 188] == 0x47) + { + skip = i; + break; + } + } + sourceData = sourceData[skip..]; + } + + await File.WriteAllBytesAsync(sourcePath, sourceData); } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs b/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs index 543244a..01c19ac 100644 --- a/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs +++ b/src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs @@ -6,29 +6,28 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +class Language { - class Language - { - public string Code; - public string ExtendCode; - public string Description; - public string DescriptionAudio; + 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 Language(string extendCode, string code, string desc, string descA) + { + Code = code; + ExtendCode = extendCode; + Description = desc; + DescriptionAudio = descA; } +} - internal class LanguageCodeUtil - { - private LanguageCodeUtil() { } +internal static class LanguageCodeUtil +{ - private readonly static List ALL_LANGS = @" + private readonly static List ALL_LANGS = @" af;afr;Afrikaans;Afrikaans af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa) am;amh;Amharic;Amharic @@ -388,13 +387,13 @@ CC;chi;中文(繁體);中文 CZ;chi;中文(简体);中文 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()); -}).ToList(); + .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()); + }).ToList(); - private static Dictionary CODE_MAP = @" + private static Dictionary CODE_MAP = @" iv;IVL ar;ara bg;bul @@ -500,48 +499,47 @@ nn;nno bs;bos sr;srp " -.Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToDictionary(x => x.Split(';').First().Trim(), x => x.Split(';').Last().Trim()); + .Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToDictionary(x => x.Split(';').First().Trim(), x => x.Split(';').Last().Trim()); - private static string ConvertTwoToThree(string input) - { - if (CODE_MAP.TryGetValue(input, out var code)) return code; - return input; - } - - /// - /// 转换 ISO 639-1 => ISO 639-2 - /// 且当Description为空时将DisplayName写入 - /// - /// - public static void ConvertLangCodeAndDisplayName(OutputFile outputFile) - { - 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位 - var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First()); - lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase)); - } - - if (lang != null) - { - outputFile.LangCode = lang.Code; - if (string.IsNullOrEmpty(outputFile.Description)) - outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio; - } - else if (outputFile.LangCode == null) - { - outputFile.LangCode = "und"; //无法识别直接置为und - } - - //无描述,则把LangCode当作描述 - if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode; - } + private static string ConvertTwoToThree(string input) + { + if (CODE_MAP.TryGetValue(input, out var code)) return code; + return input; } -} + + /// + /// 转换 ISO 639-1 => ISO 639-2 + /// 且当Description为空时将DisplayName写入 + /// + /// + public static void ConvertLangCodeAndDisplayName(OutputFile outputFile) + { + 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位 + var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First()); + lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase)); + } + + if (lang != null) + { + outputFile.LangCode = lang.Code; + if (string.IsNullOrEmpty(outputFile.Description)) + outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio; + } + else if (outputFile.LangCode == null) + { + outputFile.LangCode = "und"; //无法识别直接置为und + } + + //无描述,则把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 2be5f91..3a6264f 100644 --- a/src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs +++ b/src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs @@ -9,114 +9,113 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +internal static class LargeSingleFileSplitUtil { - internal class LargeSingleFileSplitUtil + class Clip { - class Clip + public required int index; + public required long from; + public required long to; + } + + /// + /// URL大文件切片处理 + /// + /// + /// + /// + /// + public static async Task?> SplitUrlAsync(MediaSegment segment, Dictionary headers) + { + var url = segment.Url; + if (!await CanSplitAsync(url, headers)) return null; + + if (segment.StartRange != null) return null; + + long fileSize = await GetFileSizeAsync(url, headers); + if (fileSize == 0) return null; + + List allClips = GetAllClips(url, fileSize); + var splitSegments = new List(); + foreach (Clip clip in allClips) { - public required int index; - public required long from; - public required long to; + splitSegments.Add(new MediaSegment() + { + Index = clip.index, + Url = url, + StartRange = clip.from, + ExpectLength = clip.to == -1 ? null : clip.to - clip.from + 1, + EncryptInfo = segment.EncryptInfo, + }); } - /// - /// URL大文件切片处理 - /// - /// - /// - /// - /// - public static async Task?> SplitUrlAsync(MediaSegment segment, Dictionary headers) + return splitSegments; + } + + public static async Task CanSplitAsync(string url, Dictionary headers) + { + try { - var url = segment.Url; - if (!await CanSplitAsync(url, headers)) return null; + var request = new HttpRequestMessage(HttpMethod.Head, url); + var response = (await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode(); + bool supportsRangeRequests = response.Headers.Contains("Accept-Ranges"); - if (segment.StartRange != null) return null; - - long fileSize = await GetFileSizeAsync(url, headers); - if (fileSize == 0) return null; - - List allClips = GetAllClips(url, fileSize); - var splitSegments = new List(); - foreach (Clip clip in allClips) - { - splitSegments.Add(new MediaSegment() - { - Index = clip.index, - Url = url, - StartRange = clip.from, - ExpectLength = clip.to == -1 ? null : clip.to - clip.from + 1, - EncryptInfo = segment.EncryptInfo, - }); - } - - return splitSegments; + return supportsRangeRequests; } - - public static async Task CanSplitAsync(string url, Dictionary headers) + catch (Exception ex) { - try - { - var request = new HttpRequestMessage(HttpMethod.Head, url); - var response = (await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode(); - bool supportsRangeRequests = response.Headers.Contains("Accept-Ranges"); - - return supportsRangeRequests; - } - catch (Exception ex) - { - Logger.DebugMarkUp(ex.Message); - return false; - } - } - - private static async Task GetFileSizeAsync(string url, Dictionary headers) - { - using var httpRequestMessage = new HttpRequestMessage(); - httpRequestMessage.RequestUri = new(url); - foreach (var header in headers) - { - httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); - } - var response = (await HTTPUtil.AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode(); - long totalSizeBytes = response.Content.Headers.ContentLength ?? 0; - - return totalSizeBytes; - } - - //此函数主要是切片下载逻辑 - private static List GetAllClips(string url, long fileSize) - { - List clips = new(); - int index = 0; - long counter = 0; - int perSize = 10 * 1024 * 1024; - while (fileSize > 0) - { - Clip c = new() - { - index = index, - from = counter, - to = counter + perSize - }; - //没到最后 - if (fileSize - perSize > 0) - { - fileSize -= perSize; - counter += perSize + 1; - index++; - clips.Add(c); - } - //已到最后 - else - { - c.to = -1; - clips.Add(c); - break; - } - } - return clips; + Logger.DebugMarkUp(ex.Message); + return false; } } -} + + private static async Task GetFileSizeAsync(string url, Dictionary headers) + { + using var httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new(url); + foreach (var header in headers) + { + httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + var response = (await HTTPUtil.AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode(); + long totalSizeBytes = response.Content.Headers.ContentLength ?? 0; + + return totalSizeBytes; + } + + //此函数主要是切片下载逻辑 + private static List GetAllClips(string url, long fileSize) + { + List clips = new(); + int index = 0; + long counter = 0; + int perSize = 10 * 1024 * 1024; + while (fileSize > 0) + { + Clip c = new() + { + index = index, + from = counter, + to = counter + perSize + }; + //没到最后 + if (fileSize - perSize > 0) + { + fileSize -= perSize; + counter += perSize + 1; + index++; + clips.Add(c); + } + //已到最后 + else + { + c.to = -1; + clips.Add(c); + break; + } + } + return clips; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs index d8b38a8..b152295 100644 --- a/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs +++ b/src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs @@ -5,184 +5,183 @@ using N_m3u8DL_RE.Config; using System.Diagnostics; using System.Text.RegularExpressions; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +internal static class MP4DecryptUtil { - internal class MP4DecryptUtil + private static 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) { - private static 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; + + var keyPairs = keys.ToList(); + string? keyPair = null; + string? trackId = null; + + if (isMultiDRM) { - if (keys == null || keys.Length == 0) return false; + trackId = "1"; + } - var keyPairs = keys.ToList(); - string? keyPair = null; - string? trackId = null; + if (!string.IsNullOrEmpty(kid)) + { + var test = keyPairs.Where(k => k.StartsWith(kid)); + if (test.Any()) keyPair = test.First(); + } - if (isMultiDRM) - { - trackId = "1"; - } + // Apple + if (kid == ZeroKid) + { + keyPair = keyPairs.First(); + trackId = "1"; + } - if (!string.IsNullOrEmpty(kid)) - { - var test = keyPairs.Where(k => k.StartsWith(kid)); - if (test.Any()) keyPair = test.First(); - } - - // Apple - if (kid == ZeroKid) - { - keyPair = keyPairs.First(); - trackId = "1"; - } - - // user only input key, append kid - if (keyPair == null && keyPairs.Count == 1 && !keyPairs.First().Contains(':')) - { - keyPairs = keyPairs.Select(x => $"{kid}:{x}").ToList(); - keyPair = keyPairs.First(); - } + // user only input key, append kid + if (keyPair == null && keyPairs.Count == 1 && !keyPairs.First().Contains(':')) + { + keyPairs = keyPairs.Select(x => $"{kid}:{x}").ToList(); + keyPair = keyPairs.First(); + } - if (keyPair == null) return false; + if (keyPair == null) return false; - //shakaPackager 无法单独解密init文件 - if (source.EndsWith("_init.mp4") && shakaPackager) return false; + //shakaPackager 无法单独解密init文件 + if (source.EndsWith("_init.mp4") && shakaPackager) return false; - var cmd = ""; + var cmd = ""; - var tmpFile = ""; - if (shakaPackager) + var tmpFile = ""; + if (shakaPackager) + { + var enc = source; + //shakaPackager 手动构造文件 + if (init != "") { - var enc = source; - //shakaPackager 手动构造文件 - if (init != "") - { - tmpFile = Path.ChangeExtension(source, ".itmp"); - MergeUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile); - enc = tmpFile; - } + tmpFile = Path.ChangeExtension(source, ".itmp"); + MergeUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile); + enc = tmpFile; + } - cmd = $"--quiet --enable_raw_key_decryption input=\"{enc}\",stream=0,output=\"{dest}\" " + - $"--keys {(trackId != null ? $"label={trackId}:" : "")}key_id={(trackId != null ? ZeroKid : kid)}:key={keyPair.Split(':')[1]}"; + cmd = $"--quiet --enable_raw_key_decryption input=\"{enc}\",stream=0,output=\"{dest}\" " + + $"--keys {(trackId != null ? $"label={trackId}:" : "")}key_id={(trackId != null ? ZeroKid : kid)}:key={keyPair.Split(':')[1]}"; + } + else + { + if (trackId == null) + { + cmd = string.Join(" ", keyPairs.Select(k => $"--key {k}")); } else { - if (trackId == null) - { - cmd = string.Join(" ", keyPairs.Select(k => $"--key {k}")); - } - else - { - cmd = string.Join(" ", keyPairs.Select(k => $"--key {trackId}:{k.Split(':')[1]}")); - } - if (init != "") - { - cmd += $" --fragments-info \"{init}\" "; - } - cmd += $" \"{source}\" \"{dest}\""; + cmd = string.Join(" ", keyPairs.Select(k => $"--key {trackId}:{k.Split(':')[1]}")); } - - await RunCommandAsync(bin, cmd); - - if (File.Exists(dest) && new FileInfo(dest).Length > 0) + if (init != "") { - if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile); - return true; + cmd += $" --fragments-info \"{init}\" "; } - - return false; + cmd += $" \"{source}\" \"{dest}\""; } - private static async Task RunCommandAsync(string name, string arg) + await RunCommandAsync(bin, cmd); + + if (File.Exists(dest) && new FileInfo(dest).Length > 0) { - Logger.DebugMarkUp($"FileName: {name}"); - Logger.DebugMarkUp($"Arguments: {arg}"); - await Process.Start(new ProcessStartInfo() - { - FileName = name, - Arguments = arg, - //RedirectStandardOutput = true, - //RedirectStandardError = true, - CreateNoWindow = true, - UseShellExecute = false - })!.WaitForExitAsync(); + if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile); + return true; } - /// - /// 从文本文件中查询KID的KEY - /// - /// 文本文件 - /// 目标KID - /// - public static async Task SearchKeyFromFileAsync(string? file, string? kid) - { - try - { - if (string.IsNullOrEmpty(file) || !File.Exists(file) || string.IsNullOrEmpty(kid)) - return null; + return false; + } - 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) + private static async Task RunCommandAsync(string name, string arg) + { + Logger.DebugMarkUp($"FileName: {name}"); + Logger.DebugMarkUp($"Arguments: {arg}"); + await Process.Start(new ProcessStartInfo() + { + FileName = name, + Arguments = arg, + //RedirectStandardOutput = true, + //RedirectStandardError = true, + CreateNoWindow = true, + UseShellExecute = false + })!.WaitForExitAsync(); + } + + /// + /// 从文本文件中查询KID的KEY + /// + /// 文本文件 + /// 目标KID + /// + public static async Task SearchKeyFromFileAsync(string? file, string? kid) + { + try + { + if (string.IsNullOrEmpty(file) || !File.Exists(file) || string.IsNullOrEmpty(kid)) + return null; + + 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) + { + if (line.Trim().StartsWith(kid)) { - if (line.Trim().StartsWith(kid)) - { - Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]"); - return line.Trim(); - } + Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]"); + return line.Trim(); } } - catch (Exception ex) - { - Logger.ErrorMarkUp(ex.Message); - } - return null; } - - public static ParsedMP4Info GetMP4Info(byte[] data) + catch (Exception ex) { - var info = MP4InitUtil.ReadInit(data); - if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]"); - if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]"); - if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]"); - return info; + Logger.ErrorMarkUp(ex.Message); } + return null; + } - public static ParsedMP4Info GetMP4Info(string output) + public static ParsedMP4Info GetMP4Info(byte[] data) + { + var info = MP4InitUtil.ReadInit(data); + if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]"); + if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]"); + if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]"); + return info; + } + + public static ParsedMP4Info GetMP4Info(string output) + { + using (var fs = File.OpenRead(output)) { - using (var fs = File.OpenRead(output)) - { - var header = new byte[1 * 1024 * 1024]; //1MB - 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"); - - // TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid) - // - stop process - // - remove {output}.tmp.webm - var cmd = $"--quiet --enable_raw_key_decryption input=\"{output}\",stream=0,output=\"{output}.tmp.webm\" " + - $"--keys key_id={ZeroKid}:key={ZeroKid}"; - - using var p = new Process(); - p.StartInfo = new ProcessStartInfo() - { - FileName = bin, - Arguments = cmd, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - }; - p.Start(); - var errorOutput = p.StandardError.ReadToEnd(); - p.WaitForExit(); - return ShakaKeyIDRegex.Match(errorOutput).Groups[1].Value; + 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"); + + // TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid) + // - stop process + // - remove {output}.tmp.webm + var cmd = $"--quiet --enable_raw_key_decryption input=\"{output}\",stream=0,output=\"{output}.tmp.webm\" " + + $"--keys key_id={ZeroKid}:key={ZeroKid}"; + + using var p = new Process(); + p.StartInfo = new ProcessStartInfo() + { + FileName = bin, + Arguments = cmd, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + p.Start(); + var errorOutput = p.StandardError.ReadToEnd(); + p.WaitForExit(); + 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 2235be7..1ec6187 100644 --- a/src/N_m3u8DL-RE/Util/MediainfoUtil.cs +++ b/src/N_m3u8DL-RE/Util/MediainfoUtil.cs @@ -8,92 +8,91 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Linq; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +internal static partial class MediainfoUtil { - internal partial class MediainfoUtil + [GeneratedRegex(" Stream #.*")] + private static partial Regex TextRegex(); + [GeneratedRegex("#0:\\d(\\[0x\\w+?\\])")] + private static partial Regex IdRegex(); + [GeneratedRegex(": (\\w+): (.*)")] + private static partial Regex TypeRegex(); + [GeneratedRegex("(.*?)(,|$)")] + private static partial Regex BaseInfoRegex(); + [GeneratedRegex(" \\/ 0x\\w+")] + private static partial Regex ReplaceRegex(); + [GeneratedRegex("\\d{2,}x\\d+")] + private static partial Regex ResRegex(); + [GeneratedRegex("\\d+ kb\\/s")] + private static partial Regex BitrateRegex(); + [GeneratedRegex("(\\d+(\\.\\d+)?) fps")] + private static partial Regex FpsRegex(); + [GeneratedRegex("DOVI configuration record.*profile: (\\d).*compatibility id: (\\d)")] + private static partial Regex DoViRegex(); + [GeneratedRegex("Duration.*?start: (\\d+\\.?\\d{0,3})")] + private static partial Regex StartRegex(); + + public static async Task> ReadInfoAsync(string binary, string file) { - [GeneratedRegex(" Stream #.*")] - private static partial Regex TextRegex(); - [GeneratedRegex("#0:\\d(\\[0x\\w+?\\])")] - private static partial Regex IdRegex(); - [GeneratedRegex(": (\\w+): (.*)")] - private static partial Regex TypeRegex(); - [GeneratedRegex("(.*?)(,|$)")] - private static partial Regex BaseInfoRegex(); - [GeneratedRegex(" \\/ 0x\\w+")] - private static partial Regex ReplaceRegex(); - [GeneratedRegex("\\d{2,}x\\d+")] - private static partial Regex ResRegex(); - [GeneratedRegex("\\d+ kb\\/s")] - private static partial Regex BitrateRegex(); - [GeneratedRegex("(\\d+(\\.\\d+)?) fps")] - private static partial Regex FpsRegex(); - [GeneratedRegex("DOVI configuration record.*profile: (\\d).*compatibility id: (\\d)")] - private static partial Regex DoViRegex(); - [GeneratedRegex("Duration.*?start: (\\d+\\.?\\d{0,3})")] - private static partial Regex StartRegex(); + var result = new List(); - public static async Task> ReadInfoAsync(string binary, string file) + if (string.IsNullOrEmpty(file) || !File.Exists(file)) return result; + + string cmd = "-hide_banner -i \"" + file + "\""; + var p = Process.Start(new ProcessStartInfo() { - var result = new List(); + FileName = binary, + Arguments = cmd, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + })!; + var output = p.StandardError.ReadToEnd(); + await p.WaitForExitAsync(); - if (string.IsNullOrEmpty(file) || !File.Exists(file)) return result; - - string cmd = "-hide_banner -i \"" + file + "\""; - var p = Process.Start(new ProcessStartInfo() + foreach (Match stream in TextRegex().Matches(output)) + { + var info = new Mediainfo() { - FileName = binary, - Arguments = cmd, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - })!; - var output = p.StandardError.ReadToEnd(); - await p.WaitForExitAsync(); + Text = TypeRegex().Match(stream.Value).Groups[2].Value.TrimEnd(), + Id = IdRegex().Match(stream.Value).Groups[1].Value, + Type = TypeRegex().Match(stream.Value).Groups[1].Value, + }; - foreach (Match stream in TextRegex().Matches(output)) + info.Resolution = ResRegex().Match(info.Text).Value; + info.Bitrate = BitrateRegex().Match(info.Text).Value; + info.Fps = FpsRegex().Match(info.Text).Value; + info.BaseInfo = BaseInfoRegex().Match(info.Text).Groups[1].Value; + info.BaseInfo = ReplaceRegex().Replace(info.BaseInfo, ""); + info.HDR = info.Text.Contains("/bt2020/"); + + if (info.BaseInfo.Contains("dvhe") + || info.BaseInfo.Contains("dvh1") + || info.BaseInfo.Contains("DOVI") + || info.Type.Contains("dvvideo") + || (DoViRegex().IsMatch(output) && info.Type == "Video") + ) + info.DolbyVison = true; + + if (StartRegex().IsMatch(output)) { - var info = new Mediainfo() - { - Text = TypeRegex().Match(stream.Value).Groups[2].Value.TrimEnd(), - Id = IdRegex().Match(stream.Value).Groups[1].Value, - Type = TypeRegex().Match(stream.Value).Groups[1].Value, - }; - - info.Resolution = ResRegex().Match(info.Text).Value; - info.Bitrate = BitrateRegex().Match(info.Text).Value; - info.Fps = FpsRegex().Match(info.Text).Value; - info.BaseInfo = BaseInfoRegex().Match(info.Text).Groups[1].Value; - info.BaseInfo = ReplaceRegex().Replace(info.BaseInfo, ""); - info.HDR = info.Text.Contains("/bt2020/"); - - if (info.BaseInfo.Contains("dvhe") - || info.BaseInfo.Contains("dvh1") - || info.BaseInfo.Contains("DOVI") - || info.Type.Contains("dvvideo") - || (DoViRegex().IsMatch(output) && info.Type == "Video") - ) - info.DolbyVison = true; - - if (StartRegex().IsMatch(output)) - { - var f = StartRegex().Match(output).Groups[1].Value; - if (double.TryParse(f, out var d)) - info.StartTime = TimeSpan.FromSeconds(d); - } - - result.Add(info); + var f = StartRegex().Match(output).Groups[1].Value; + if (double.TryParse(f, out var d)) + info.StartTime = TimeSpan.FromSeconds(d); } - if (result.Count == 0) - { - result.Add(new Mediainfo() - { - Type = "Unknown" - }); - } - - return result; + result.Add(info); } + + if (result.Count == 0) + { + result.Add(new Mediainfo() + { + Type = "Unknown" + }); + } + + return result; } -} +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/MergeUtil.cs b/src/N_m3u8DL-RE/Util/MergeUtil.cs index 92b325c..1aaadfa 100644 --- a/src/N_m3u8DL-RE/Util/MergeUtil.cs +++ b/src/N_m3u8DL-RE/Util/MergeUtil.cs @@ -5,290 +5,289 @@ using System.Diagnostics; using System.Text; using N_m3u8DL_RE.Enum; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +internal static class MergeUtil { - internal class MergeUtil + /// + /// 输入一堆已存在的文件,合并到新文件 + /// + /// + /// + public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath) { - /// - /// 输入一堆已存在的文件,合并到新文件 - /// - /// - /// - public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath) + if (files.Length == 0) return; + if (files.Length == 1) { - if (files.Length == 0) return; - if (files.Length == 1) - { - FileInfo fi = new FileInfo(files[0]); - fi.CopyTo(outputFilePath, true); - return; - } - - if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) - Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); - - string[] inputFilePaths = files; - using (var outputStream = File.Create(outputFilePath)) - { - foreach (var inputFilePath in inputFilePaths) - { - if (inputFilePath == "") - continue; - using (var inputStream = File.OpenRead(inputFilePath)) - { - inputStream.CopyTo(outputStream); - } - } - } + FileInfo fi = new FileInfo(files[0]); + fi.CopyTo(outputFilePath, true); + return; } - private static int InvokeFFmpeg(string binary, string command, string workingDirectory) + if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) + Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); + + string[] inputFilePaths = files; + using (var outputStream = File.Create(outputFilePath)) { - Logger.DebugMarkUp($"{binary}: {command}"); - - using var p = new Process(); - p.StartInfo = new ProcessStartInfo() + foreach (var inputFilePath in inputFilePaths) { - WorkingDirectory = workingDirectory, - FileName = binary, - Arguments = command, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - }; - p.ErrorDataReceived += (sendProcess, output) => - { - if (!string.IsNullOrEmpty(output.Data)) - { - Logger.WarnMarkUp($"[grey]{output.Data.EscapeMarkup()}[/]"); - } - }; - p.Start(); - p.BeginErrorReadLine(); - p.WaitForExit(); - return p.ExitCode; - } - - public static string[] PartialCombineMultipleFiles(string[] files) - { - var newFiles = new List(); - int div = 0; - if (files.Length <= 90000) - div = 100; - else - div = 200; - - string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T"); - int index = 0; //序号 - - //按照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 (inputFilePath == "") continue; - var output = outputName + index.ToString("0000") + ".ts"; - CombineMultipleFilesIntoSingleFile(items, output); - newFiles.Add(output); - //合并后删除这些文件 - foreach (var item in items) + using (var inputStream = File.OpenRead(inputFilePath)) { - File.Delete(item); - } - index++; - } - - return newFiles.ToArray(); - } - - public static bool MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, bool useAACFilter, - bool fastStart = false, - 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; - - StringBuilder command = new StringBuilder("-loglevel warning -nostdin "); - string ddpAudio = string.Empty; - string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic"; - ddpAudio = (File.Exists($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") ? File.ReadAllText($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") : ""); - if (!string.IsNullOrEmpty(ddpAudio)) useAACFilter = false; - - if (useConcatDemuxer) - { - // 使用 concat demuxer合并 - var text = string.Join(Environment.NewLine, files.Select(f => $"file '{f}'")); - var tempFile = Path.GetTempFileName(); - File.WriteAllText(tempFile, text); - command.Append($" -f concat -safe 0 -i \"{tempFile}"); - } - else - { - command.Append(" -i concat:\""); - foreach (string t in files) - { - command.Append(Path.GetFileName(t) + "|"); + inputStream.CopyTo(outputStream); } } - - - switch (muxFormat.ToUpper()) - { - case ("MP4"): - command.Append("\" " + (string.IsNullOrEmpty(poster) ? "" : "-i \"" + poster + "\"")); - command.Append(" " + (string.IsNullOrEmpty(ddpAudio) ? "" : "-i \"" + ddpAudio + "\"")); - command.Append( - $" -map 0:v? {(string.IsNullOrEmpty(ddpAudio) ? "-map 0:a?" : $"-map {(string.IsNullOrEmpty(poster) ? "1" : "2")}:a -map 0:a?")} -map 0:s? " + (string.IsNullOrEmpty(poster) ? "" : addPoster) - + (writeDate ? " -metadata date=\"" + dateString + "\"" : "") + - " -metadata encoding_tool=\"" + encodingTool + "\" -metadata title=\"" + title + - "\" -metadata copyright=\"" + copyright + "\" -metadata comment=\"" + comment + - $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} title=\"" + audioName + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler=\"" + audioName + "\" "); - command.Append(string.IsNullOrEmpty(ddpAudio) ? "" : " -metadata:s:a:0 title=\"DD+\" -metadata:s:a:0 handler=\"DD+\" "); - if (fastStart) - command.Append("-movflags +faststart"); - command.Append(" -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mp4\""); - break; - case ("MKV"): - command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mkv\""); - break; - case ("FLV"): - command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".flv\""); - break; - case ("M4A"): - command.Append("\" -map 0 -c copy -f mp4 -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".m4a\""); - break; - case ("TS"): - command.Append("\" -map 0 -c copy -y -f mpegts -bsf:v h264_mp4toannexb \"" + outputPath + ".ts\""); - break; - case ("EAC3"): - command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".eac3\""); - break; - case ("AAC"): - command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".m4a\""); - break; - case ("AC3"): - command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".ac3\""); - break; - } - - var code = InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0])!); - - return code == 0; - } - - public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath, MuxFormat muxFormat, bool dateinfo) - { - var ext = OtherUtil.GetMuxExtension(muxFormat); - string dateString = DateTime.Now.ToString("o"); - StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn "); - - //INPUT - foreach (var item in files) - { - command.Append($" -i \"{item.FilePath}\" "); - } - - //MAP - for (int i = 0; i < files.Length; i++) - { - command.Append($" -map {i} "); - } - - 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字幕,必须转换格式 - 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 - command.Append(" -map_metadata -1 "); - - //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)) - { - command.Append($" -metadata:s:{streamIndex} title=\"{files[i].Description}\" "); - } - /** - * -metadata:s:xx标记的是 输出的第xx个流的metadata, - * 若输入文件存在不止一个流时,这里单纯使用files的index - * 就有可能出现metadata错位的情况,所以加了如下逻辑 - */ - if (files[i].Mediainfos.Count > 0) - streamIndex += files[i].Mediainfos.Count; - else - streamIndex++; - } - - var videoTracks = files.Where(x => x.MediaType != Common.Enum.MediaType.AUDIO && x.MediaType != Common.Enum.MediaType.SUBTITLES); - 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++) - { - command.Append($" -disposition:a:{i} 0 "); - } - } - - if (dateinfo) command.Append($" -metadata date=\"{dateString}\" "); - command.Append($" -ignore_unknown -copy_unknown "); - command.Append($" \"{outputPath}{ext}\""); - - var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory); - - return code == 0; - } - - public static bool MuxInputsByMkvmerge(string binary, OutputFile[] files, string outputPath) - { - StringBuilder command = new StringBuilder($"-q --output \"{outputPath}.mkv\" "); - - command.Append(" --no-chapters "); - - var dFlag = false; - - //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) - command.Append($" --default-track 0:no "); - dFlag = true; - } - if (!string.IsNullOrEmpty(files[i].Description)) - command.Append($" --track-name 0:\"{files[i].Description}\" "); - command.Append($" \"{files[i].FilePath}\" "); - } - - var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory); - - return code == 0; } } -} + + private static int InvokeFFmpeg(string binary, string command, string workingDirectory) + { + Logger.DebugMarkUp($"{binary}: {command}"); + + using var p = new Process(); + p.StartInfo = new ProcessStartInfo() + { + WorkingDirectory = workingDirectory, + FileName = binary, + Arguments = command, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + p.ErrorDataReceived += (sendProcess, output) => + { + if (!string.IsNullOrEmpty(output.Data)) + { + Logger.WarnMarkUp($"[grey]{output.Data.EscapeMarkup()}[/]"); + } + }; + p.Start(); + p.BeginErrorReadLine(); + p.WaitForExit(); + return p.ExitCode; + } + + public static string[] PartialCombineMultipleFiles(string[] files) + { + var newFiles = new List(); + int div = 0; + if (files.Length <= 90000) + div = 100; + else + div = 200; + + string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T"); + int index = 0; //序号 + + //按照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) + continue; + var output = outputName + index.ToString("0000") + ".ts"; + CombineMultipleFilesIntoSingleFile(items, output); + newFiles.Add(output); + //合并后删除这些文件 + foreach (var item in items) + { + File.Delete(item); + } + index++; + } + + return newFiles.ToArray(); + } + + public static bool MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, bool useAACFilter, + bool fastStart = false, + 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; + + StringBuilder command = new StringBuilder("-loglevel warning -nostdin "); + string ddpAudio = string.Empty; + string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic"; + ddpAudio = (File.Exists($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") ? File.ReadAllText($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") : ""); + if (!string.IsNullOrEmpty(ddpAudio)) useAACFilter = false; + + if (useConcatDemuxer) + { + // 使用 concat demuxer合并 + var text = string.Join(Environment.NewLine, files.Select(f => $"file '{f}'")); + var tempFile = Path.GetTempFileName(); + File.WriteAllText(tempFile, text); + command.Append($" -f concat -safe 0 -i \"{tempFile}"); + } + else + { + command.Append(" -i concat:\""); + foreach (string t in files) + { + command.Append(Path.GetFileName(t) + "|"); + } + } + + + switch (muxFormat.ToUpper()) + { + case ("MP4"): + command.Append("\" " + (string.IsNullOrEmpty(poster) ? "" : "-i \"" + poster + "\"")); + command.Append(" " + (string.IsNullOrEmpty(ddpAudio) ? "" : "-i \"" + ddpAudio + "\"")); + command.Append( + $" -map 0:v? {(string.IsNullOrEmpty(ddpAudio) ? "-map 0:a?" : $"-map {(string.IsNullOrEmpty(poster) ? "1" : "2")}:a -map 0:a?")} -map 0:s? " + (string.IsNullOrEmpty(poster) ? "" : addPoster) + + (writeDate ? " -metadata date=\"" + dateString + "\"" : "") + + " -metadata encoding_tool=\"" + encodingTool + "\" -metadata title=\"" + title + + "\" -metadata copyright=\"" + copyright + "\" -metadata comment=\"" + comment + + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} title=\"" + audioName + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler=\"" + audioName + "\" "); + command.Append(string.IsNullOrEmpty(ddpAudio) ? "" : " -metadata:s:a:0 title=\"DD+\" -metadata:s:a:0 handler=\"DD+\" "); + if (fastStart) + command.Append("-movflags +faststart"); + command.Append(" -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mp4\""); + break; + case ("MKV"): + command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mkv\""); + break; + case ("FLV"): + command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".flv\""); + break; + case ("M4A"): + command.Append("\" -map 0 -c copy -f mp4 -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".m4a\""); + break; + case ("TS"): + command.Append("\" -map 0 -c copy -y -f mpegts -bsf:v h264_mp4toannexb \"" + outputPath + ".ts\""); + break; + case ("EAC3"): + command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".eac3\""); + break; + case ("AAC"): + command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".m4a\""); + break; + case ("AC3"): + command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".ac3\""); + break; + } + + var code = InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0])!); + + return code == 0; + } + + public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath, MuxFormat muxFormat, bool dateinfo) + { + var ext = OtherUtil.GetMuxExtension(muxFormat); + string dateString = DateTime.Now.ToString("o"); + StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn "); + + //INPUT + foreach (var item in files) + { + command.Append($" -i \"{item.FilePath}\" "); + } + + //MAP + for (int i = 0; i < files.Length; i++) + { + command.Append($" -map {i} "); + } + + 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字幕,必须转换格式 + 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 + command.Append(" -map_metadata -1 "); + + //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)) + { + command.Append($" -metadata:s:{streamIndex} title=\"{files[i].Description}\" "); + } + /** + * -metadata:s:xx标记的是 输出的第xx个流的metadata, + * 若输入文件存在不止一个流时,这里单纯使用files的index + * 就有可能出现metadata错位的情况,所以加了如下逻辑 + */ + if (files[i].Mediainfos.Count > 0) + streamIndex += files[i].Mediainfos.Count; + else + streamIndex++; + } + + var videoTracks = files.Where(x => x.MediaType != Common.Enum.MediaType.AUDIO && x.MediaType != Common.Enum.MediaType.SUBTITLES); + 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++) + { + command.Append($" -disposition:a:{i} 0 "); + } + } + + if (dateinfo) command.Append($" -metadata date=\"{dateString}\" "); + command.Append($" -ignore_unknown -copy_unknown "); + command.Append($" \"{outputPath}{ext}\""); + + var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory); + + return code == 0; + } + + public static bool MuxInputsByMkvmerge(string binary, OutputFile[] files, string outputPath) + { + StringBuilder command = new StringBuilder($"-q --output \"{outputPath}.mkv\" "); + + command.Append(" --no-chapters "); + + var dFlag = false; + + //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) + command.Append($" --default-track 0:no "); + dFlag = true; + } + if (!string.IsNullOrEmpty(files[i].Description)) + command.Append($" --track-name 0:\"{files[i].Description}\" "); + command.Append($" \"{files[i].FilePath}\" "); + } + + var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory); + + return code == 0; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/OtherUtil.cs b/src/N_m3u8DL-RE/Util/OtherUtil.cs index 70855d3..ece61c8 100644 --- a/src/N_m3u8DL-RE/Util/OtherUtil.cs +++ b/src/N_m3u8DL-RE/Util/OtherUtil.cs @@ -6,175 +6,174 @@ using System.IO.Compression; using System.Text; using System.Text.RegularExpressions; -namespace N_m3u8DL_RE.Util -{ - internal class OtherUtil - { - public static Dictionary SplitHeaderArrayToDic(string[]? headers) - { - Dictionary dic = new(); +namespace N_m3u8DL_RE.Util; - if (headers != null) +internal class OtherUtil +{ + public static Dictionary SplitHeaderArrayToDic(string[]? headers) + { + Dictionary dic = new(); + + if (headers != null) + { + 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" - .Split(',').Select(s => (char)int.Parse(s)).ToArray(); - public static string GetValidFileName(string input, string re = ".", bool filterSlash = false) + 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" + .Split(',').Select(s => (char)int.Parse(s)).ToArray(); + public static string GetValidFileName(string input, string re = ".", bool filterSlash = false) + { + string title = input; + foreach (char invalidChar in InvalidChars) { - string title = input; - foreach (char invalidChar in InvalidChars) - { - title = title.Replace(invalidChar.ToString(), re); - } - if (filterSlash) - { - title = title.Replace("/", re); - title = title.Replace("\\", re); - } - return title.Trim('.'); + title = title.Replace(invalidChar.ToString(), re); + } + if (filterSlash) + { + title = title.Replace("/", re); + title = title.Replace("\\", re); + } + return title.Trim('.'); + } + + /// + /// 从输入自动获取文件名 + /// + /// + /// + public static string GetFileNameFromInput(string input, bool addSuffix = true) + { + var saveName = addSuffix ? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") : string.Empty; + if (File.Exists(input)) + { + saveName = Path.GetFileNameWithoutExtension(input) + "_" + saveName; + } + else + { + var uri = new Uri(input.Split('?').First()); + var name = Path.GetFileNameWithoutExtension(uri.LocalPath); + saveName = GetValidFileName(name) + "_" + saveName; + } + return saveName; + } + + /// + /// 从 hh:mm:ss 解析TimeSpan + /// + /// + /// + public static TimeSpan ParseDur(string timeStr) + { + var arr = timeStr.Replace(":", ":").Split(':'); + var days = -1; + var hours = -1; + var mins = -1; + var secs = -1; + arr.Reverse().Select(i => Convert.ToInt32(i)).ToList().ForEach(item => + { + if (secs == -1) secs = item; + else if (mins == -1) mins = item; + else if (hours == -1) hours = item; + else if (days == -1) days = item; + }); + + if (days == -1) days = 0; + if (hours == -1) hours = 0; + if (mins == -1) mins = 0; + if (secs == -1) secs = 0; + + return new TimeSpan(days, hours, mins, secs); + } + + /// + /// 从1h3m20s解析出总秒数 + /// + /// + /// + /// + public static double ParseSeconds(string timeStr) + { + var pattern = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$"); + + var match = pattern.Match(timeStr); + + if (!match.Success) + { + throw new ArgumentException("时间格式无效"); } - /// - /// 从输入自动获取文件名 - /// - /// - /// - public static string GetFileNameFromInput(string input, bool addSuffix = true) + int hours = match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0; + int minutes = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0; + int seconds = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0; + + return hours * 3600 + minutes * 60 + seconds; + } + + //若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录 + public static void SafeDeleteDir(string dirPath) + { + if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath)) + return; + + var parent = Path.GetDirectoryName(dirPath)!; + if (!Directory.EnumerateFileSystemEntries(dirPath).Any()) { - var saveName = addSuffix ? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") : string.Empty; - if (File.Exists(input)) - { - saveName = Path.GetFileNameWithoutExtension(input) + "_" + saveName; - } - else - { - var uri = new Uri(input.Split('?').First()); - var name = Path.GetFileNameWithoutExtension(uri.LocalPath); - saveName = GetValidFileName(name) + "_" + saveName; - } - return saveName; + Directory.Delete(dirPath); } - - /// - /// 从 hh:mm:ss 解析TimeSpan - /// - /// - /// - public static TimeSpan ParseDur(string timeStr) + else { - var arr = timeStr.Replace(":", ":").Split(':'); - var days = -1; - var hours = -1; - var mins = -1; - var secs = -1; - arr.Reverse().Select(i => Convert.ToInt32(i)).ToList().ForEach(item => - { - if (secs == -1) secs = item; - else if (mins == -1) mins = item; - else if (hours == -1) hours = item; - else if (days == -1) days = item; - }); - - if (days == -1) days = 0; - if (hours == -1) hours = 0; - if (mins == -1) mins = 0; - if (secs == -1) secs = 0; - - return new TimeSpan(days, hours, mins, secs); + return; } + SafeDeleteDir(parent); + } - /// - /// 从1h3m20s解析出总秒数 - /// - /// - /// - /// - public static double ParseSeconds(string timeStr) + /// + /// 解压并替换原文件 + /// + /// + public static async Task DeGzipFileAsync(string filePath) + { + var deGzipFile = Path.ChangeExtension(filePath, ".dezip_tmp"); + try { - var pattern = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$"); - - var match = pattern.Match(timeStr); - - if (!match.Success) + await using (var fileToDecompressAsStream = File.OpenRead(filePath)) { - throw new ArgumentException("时间格式无效"); - } - - int hours = match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0; - int minutes = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0; - int seconds = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0; - - return hours * 3600 + minutes * 60 + seconds; - } - - //若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录 - public static void SafeDeleteDir(string dirPath) - { - if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath)) - return; - - var parent = Path.GetDirectoryName(dirPath)!; - if (!Directory.EnumerateFileSystemEntries(dirPath).Any()) - { - Directory.Delete(dirPath); - } - else - { - return; - } - SafeDeleteDir(parent); - } - - /// - /// 解压并替换原文件 - /// - /// - public static async Task DeGzipFileAsync(string filePath) - { - var deGzipFile = Path.ChangeExtension(filePath, ".dezip_tmp"); - try - { - await using (var fileToDecompressAsStream = File.OpenRead(filePath)) - { - await using var decompressedStream = File.Create(deGzipFile); - await using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress); - await decompressionStream.CopyToAsync(decompressedStream); - }; - File.Delete(filePath); - File.Move(deGzipFile, filePath); - } - catch - { - if (File.Exists(deGzipFile)) File.Delete(deGzipFile); - } - } - - public static string GetEnvironmentVariable(string key, string defaultValue = "") - { - return Environment.GetEnvironmentVariable(key) ?? defaultValue; - } - - public static string GetMuxExtension(MuxFormat muxFormat) - { - return muxFormat switch - { - MuxFormat.MP4 => ".mp4", - MuxFormat.MKV => ".mkv", - MuxFormat.TS => ".ts", - _ => throw new ArgumentException($"unknown format: {muxFormat}") + await using var decompressedStream = File.Create(deGzipFile); + await using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress); + await decompressionStream.CopyToAsync(decompressedStream); }; + File.Delete(filePath); + File.Move(deGzipFile, filePath); + } + catch + { + if (File.Exists(deGzipFile)) File.Delete(deGzipFile); } } -} + + public static string GetEnvironmentVariable(string key, string defaultValue = "") + { + return Environment.GetEnvironmentVariable(key) ?? defaultValue; + } + + public static string GetMuxExtension(MuxFormat muxFormat) + { + return muxFormat switch + { + MuxFormat.MP4 => ".mp4", + MuxFormat.MKV => ".mkv", + MuxFormat.TS => ".ts", + _ => throw new ArgumentException($"unknown format: {muxFormat}") + }; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/PipeUtil.cs b/src/N_m3u8DL-RE/Util/PipeUtil.cs index d51df0e..0d99e5d 100644 --- a/src/N_m3u8DL-RE/Util/PipeUtil.cs +++ b/src/N_m3u8DL-RE/Util/PipeUtil.cs @@ -10,104 +10,103 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +internal static class PipeUtil { - internal class PipeUtil + public static Stream CreatePipe(string pipeName) { - public static Stream CreatePipe(string pipeName) + if (OperatingSystem.IsWindows()) { - if (OperatingSystem.IsWindows()) - { - return new NamedPipeServerStream(pipeName, PipeDirection.InOut); - } - else - { - var path = Path.Combine(Path.GetTempPath(), pipeName); - using var p = new Process(); - p.StartInfo = new ProcessStartInfo() - { - FileName = "mkfifo", - Arguments = path, - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true, - }; - p.Start(); - p.WaitForExit(); - Thread.Sleep(200); - return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); - } + return new NamedPipeServerStream(pipeName, PipeDirection.InOut); } - - public static async Task StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath) + else { - return await Task.Run(async () => - { - await Task.Delay(1000); - return StartPipeMux(binary, pipeNames, outputPath); - }); - } - - public static bool StartPipeMux(string binary, string[] pipeNames, string outputPath) - { - string dateString = DateTime.Now.ToString("o"); - StringBuilder command = new StringBuilder("-y -fflags +genpts -loglevel quiet "); - - string customDest = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_OPTIONS"); - string pipeDir = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_TMP_DIR", Path.GetTempPath()); - - if (!string.IsNullOrEmpty(customDest)) - { - command.Append(" -re "); - } - - foreach (var item in pipeNames) - { - if (OperatingSystem.IsWindows()) - command.Append($" -i \"\\\\.\\pipe\\{item}\" "); - else - //command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" "); - command.Append($" -i \"{Path.Combine(pipeDir, item)}\" "); - } - - for (int i = 0; i < pipeNames.Length; i++) - { - command.Append($" -map {i} "); - } - - command.Append(" -strict unofficial -c copy "); - command.Append($" -metadata date=\"{dateString}\" "); - command.Append($" -ignore_unknown -copy_unknown "); - - - if (!string.IsNullOrEmpty(customDest)) - { - if (customDest.Trim().StartsWith("-")) - command.Append(customDest); - else - command.Append($" -f mpegts -shortest \"{customDest}\""); - Logger.WarnMarkUp($"[deepskyblue1]{command.ToString().EscapeMarkup()}[/]"); - } - else - { - command.Append($" -f mpegts -shortest \"{outputPath}\""); - } - + var path = Path.Combine(Path.GetTempPath(), pipeName); using var p = new Process(); p.StartInfo = new ProcessStartInfo() { - WorkingDirectory = Environment.CurrentDirectory, - FileName = binary, - Arguments = command.ToString(), + FileName = "mkfifo", + Arguments = path, CreateNoWindow = true, - UseShellExecute = false + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, }; - //p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42"); p.Start(); p.WaitForExit(); - - return p.ExitCode == 0; + Thread.Sleep(200); + return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); } } -} + + public static async Task StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath) + { + return await Task.Run(async () => + { + await Task.Delay(1000); + return StartPipeMux(binary, pipeNames, outputPath); + }); + } + + public static bool StartPipeMux(string binary, string[] pipeNames, string outputPath) + { + string dateString = DateTime.Now.ToString("o"); + StringBuilder command = new StringBuilder("-y -fflags +genpts -loglevel quiet "); + + string customDest = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_OPTIONS"); + string pipeDir = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_TMP_DIR", Path.GetTempPath()); + + if (!string.IsNullOrEmpty(customDest)) + { + command.Append(" -re "); + } + + foreach (var item in pipeNames) + { + if (OperatingSystem.IsWindows()) + command.Append($" -i \"\\\\.\\pipe\\{item}\" "); + else + //command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" "); + command.Append($" -i \"{Path.Combine(pipeDir, item)}\" "); + } + + for (int i = 0; i < pipeNames.Length; i++) + { + command.Append($" -map {i} "); + } + + command.Append(" -strict unofficial -c copy "); + command.Append($" -metadata date=\"{dateString}\" "); + command.Append($" -ignore_unknown -copy_unknown "); + + + if (!string.IsNullOrEmpty(customDest)) + { + if (customDest.Trim().StartsWith("-")) + command.Append(customDest); + else + command.Append($" -f mpegts -shortest \"{customDest}\""); + Logger.WarnMarkUp($"[deepskyblue1]{command.ToString().EscapeMarkup()}[/]"); + } + else + { + command.Append($" -f mpegts -shortest \"{outputPath}\""); + } + + using var p = new Process(); + p.StartInfo = new ProcessStartInfo() + { + WorkingDirectory = Environment.CurrentDirectory, + FileName = binary, + Arguments = command.ToString(), + CreateNoWindow = true, + UseShellExecute = false + }; + //p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42"); + p.Start(); + p.WaitForExit(); + + return p.ExitCode == 0; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Util/SubtitleUtil.cs b/src/N_m3u8DL-RE/Util/SubtitleUtil.cs index 1876f18..44028e9 100644 --- a/src/N_m3u8DL-RE/Util/SubtitleUtil.cs +++ b/src/N_m3u8DL-RE/Util/SubtitleUtil.cs @@ -7,34 +7,33 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Util +namespace N_m3u8DL_RE.Util; + +internal static class SubtitleUtil { - internal class SubtitleUtil + /// + /// 写出图形字幕PNG文件 + /// + /// + /// 临时目录 + /// + public static async Task TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir) { - /// - /// 写出图形字幕PNG文件 - /// - /// - /// 临时目录 - /// - public static async Task TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir) + if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::"))) { - if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::"))) + Logger.WarnMarkUp(ResString.processImageSub); + var _i = 0; + foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::"))) { - Logger.WarnMarkUp(ResString.processImageSub); - var _i = 0; - foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::"))) - { - var name = $"{_i++}.png"; - var dest = ""; - 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; + var name = $"{_i++}.png"; + var dest = ""; + 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; } - else return false; + return true; } + else return false; } -} +} \ No newline at end of file