Marked util classes as static (#460)

* Marked util classes as static

* Used file-scoped namespaces
This commit is contained in:
irodai-majom 2024-11-10 09:15:30 +01:00 committed by GitHub
parent 8a25815c1f
commit 9fc37d5b61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1560 additions and 1588 deletions

View File

@ -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);
public static string ConvertToJson(object o)
{
if (o is StreamSpec s)
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() }
return JsonSerializer.Serialize(s, Context.StreamSpec);
}
else if (o is IOrderedEnumerable<StreamSpec> ss)
{
return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);
}
else if (o is List<StreamSpec> sList)
{
return JsonSerializer.Serialize(sList, Context.ListStreamSpec);
}
else if (o is IEnumerable<MediaSegment> mList)
{
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)
};
private static readonly JsonContext Context = new JsonContext(Options);
}
public static string ConvertToJson(object o)
{
if (o is StreamSpec s)
{
return JsonSerializer.Serialize(s, Context.StreamSpec);
}
else if (o is IOrderedEnumerable<StreamSpec> ss)
{
return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);
}
else if (o is List<StreamSpec> sList)
{
return JsonSerializer.Serialize(sList, Context.ListStreamSpec);
}
else if (o is IEnumerable<MediaSegment> mList)
{
return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment);
}
return "{NOT SUPPORTED}";
}
//此函数用于格式化输出时长
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 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;
}
/// <summary>
/// 寻找可执行程序
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
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<string>();
return searchPath.Concat(envPath).Select(p => Path.Combine(p, name + fileExt)).FirstOrDefault(File.Exists);
}
/// <summary>
/// 寻找可执行程序
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
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<string>();
return searchPath.Concat(envPath).Select(p => Path.Combine(p, name + fileExt)).FirstOrDefault(File.Exists);
}
}

View File

@ -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<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null)
private static async Task<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null)
{
Logger.Debug(ResString.fetch + url);
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
webRequest.Headers.Connection.Clear();
if (headers != null)
{
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);
}
}
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)
{
var redirectedUrl = "";
if (!respHeaders.Location.IsAbsoluteUri)
{
webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value);
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);
}
}
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)
{
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;
}
if (redirectedUrl != url)
{
Logger.Extra($"Redirected => {redirectedUrl}");
return await DoGetAsync(redirectedUrl, headers);
}
}
}
//手动将跳转后的URL设置进去, 用于后续取用
webResponse.Headers.Location = new Uri(url);
webResponse.EnsureSuccessStatusCode();
return webResponse;
}
//手动将跳转后的URL设置进去, 用于后续取用
webResponse.Headers.Location = new Uri(url);
webResponse.EnsureSuccessStatusCode();
return webResponse;
}
public static async Task<byte[]> GetBytesAsync(string url, Dictionary<string, string>? headers = null)
public static async Task<byte[]> GetBytesAsync(string url, Dictionary<string, string>? headers = null)
{
if (url.StartsWith("file:"))
{
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;
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;
}
/// <summary>
/// 获取网页源码
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <returns></returns>
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
/// <summary>
/// 获取网页源码
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <returns></returns>
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
{
string htmlCode = string.Empty;
var webResponse = await DoGetAsync(url, headers);
htmlCode = await webResponse.Content.ReadAsStringAsync();
Logger.Debug(htmlCode);
return htmlCode;
}
private static bool CheckMPEG2TS(HttpResponseMessage? webResponse)
{
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower();
return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg";
}
/// <summary>
/// 获取网页源码和跳转后的URL
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <returns>(Source Code, RedirectedUrl)</returns>
public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null)
{
string htmlCode = string.Empty;
var webResponse = await DoGetAsync(url, headers);
if (CheckMPEG2TS(webResponse))
{
htmlCode = ResString.ReLiveTs;
}
else
{
string htmlCode = string.Empty;
var webResponse = await DoGetAsync(url, headers);
htmlCode = await webResponse.Content.ReadAsStringAsync();
Logger.Debug(htmlCode);
return htmlCode;
}
Logger.Debug(htmlCode);
return (htmlCode, webResponse.Headers.Location != null ? webResponse.Headers.Location.AbsoluteUri : url);
}
private static bool CheckMPEG2TS(HttpResponseMessage? webResponse)
{
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower();
return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg";
}
/// <summary>
/// 获取网页源码和跳转后的URL
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <returns>(Source Code, RedirectedUrl)</returns>
public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? 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<string> GetPostResponseAsync(string Url, byte[] postData)
{
string htmlCode = string.Empty;
using HttpRequestMessage request = new(HttpMethod.Post, Url);
request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());
request.Content = new ByteArrayContent(postData);
var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
htmlCode = await webResponse.Content.ReadAsStringAsync();
return htmlCode;
}
public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)
{
string htmlCode = string.Empty;
using HttpRequestMessage request = new(HttpMethod.Post, Url);
request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());
request.Content = new ByteArrayContent(postData);
var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
htmlCode = await webResponse.Content.ReadAsStringAsync();
return htmlCode;
}
}

View File

@ -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);
}
/// <summary>
/// 判断是不是HEX字符串
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
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"))
{
return BitConverter.ToString(data).Replace("-", split);
hexSpan = hexSpan.Slice(2);
}
/// <summary>
/// 判断是不是HEX字符串
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
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 Convert.FromHexString(hexSpan);
}
}

View File

@ -4,7 +4,7 @@ using Spectre.Console;
namespace N_m3u8DL_RE.Common.Util;
public class RetryUtil
public static class RetryUtil
{
public static async Task<T?> WebRequestRetryAsync<T>(Func<Task<T>> funcAsync, int maxRetries = 10, int retryDelayMilliseconds = 1500, int retryDelayIncrementMilliseconds = 0)
{

View File

@ -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<DownloadResult> 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<DownloadResult> 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<DownloadResult> DownloadToFileAsync(string url, string path, SpeedContainer speedContainer, CancellationTokenSource cancellationTokenSource, Dictionary<string, string>? 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<DownloadResult> DownloadToFileAsync(string url, string path, SpeedContainer speedContainer, CancellationTokenSource cancellationTokenSource, Dictionary<string, string>? 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);
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);
}
if (url.StartsWith("base64://"))
}
Logger.Debug(request.Headers.ToString());
try
{
using var response = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token);
if (((int)response.StatusCode).ToString().StartsWith("30"))
{
var bytes = Convert.FromBase64String(url[9..]);
await File.WriteAllBytesAsync(path, bytes);
return new DownloadResult()
HttpResponseHeaders respHeaders = response.Headers;
Logger.Debug(respHeaders.ToString());
if (respHeaders != null && respHeaders.Location != null)
{
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)
{
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);
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;
}
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;
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);
size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);
speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size);
//检测imageHeader
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
//检测GZipFor 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);
//检测imageHeader
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
//检测GZipFor DDP Audio
bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
//限速策略
while (speedContainer.Downloaded > speedContainer.SpeedLimit)
{
speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size);
//限速策略
while (speedContainer.Downloaded > speedContainer.SpeedLimit)
{
await Task.Delay(1);
}
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)
return new DownloadResult()
{
speedContainer.ResetLowSpeedCount();
throw new Exception("Download speed too slow!");
}
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!");
}
}
}

View File

@ -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<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{
public static List<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{
if (filter == null) return new List<StreamSpec>();
if (filter == null) return new List<StreamSpec>();
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<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{
if (filter == null) return new List<StreamSpec>(lists);
public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{
if (filter == null) return new List<StreamSpec>(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<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
{
if (lists.Count() == 1)
return new List<StreamSpec>(lists);
public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
{
if (lists.Count() == 1)
return new List<StreamSpec>(lists);
//基本流
var basicStreams = lists.Where(x => x.MediaType == null);
//可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
//可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
//基本流
var basicStreams = lists.Where(x => x.MediaType == null);
//可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
//可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
var prompt = new MultiSelectionPrompt<StreamSpec>()
.Title(ResString.promptTitle)
.UseConverter(x =>
{
if (x.Name != null && x.Name.StartsWith("__"))
return $"[darkslategray1]{x.Name.Substring(2)}[/]";
else
return x.ToString().EscapeMarkup().RemoveMarkup();
})
.Required()
.PageSize(10)
.MoreChoicesText(ResString.promptChoiceText)
.InstructionsText(ResString.promptInfo)
;
//默认选中第一个
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<StreamSpec>()
.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;
}
/// <summary>
/// 直播使用。对齐各个轨道的起始。
/// </summary>
/// <param name="streams"></param>
/// <param name="takeLastCount"></param>
public static void SyncStreams(List<StreamSpec> 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();
}
}
}
}
/// <summary>
/// 应用用户自定义的分片范围
/// </summary>
/// <param name="selectedSteams"></param>
/// <param name="customRange"></param>
public static void ApplyCustomRange(List<StreamSpec> 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<MediaSegment>();
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));
}
}
/// <summary>
/// 根据用户输入,清除广告分片
/// </summary>
/// <param name="selectedSteams"></param>
/// <param name="customRange"></param>
public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords)
//如果此时还是没有选中任何流,自动选择一个
prompt.Select(basicStreams.Concat(audios).Concat(subs).First());
//多选
var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt);
return selectedStreams;
}
/// <summary>
/// 直播使用。对齐各个轨道的起始。
/// </summary>
/// <param name="streams"></param>
/// <param name="takeLastCount"></param>
public static void SyncStreams(List<StreamSpec> 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();
}
}
}
}
/// <summary>
/// 应用用户自定义的分片范围
/// </summary>
/// <param name="selectedSteams"></param>
/// <param name="customRange"></param>
public static void ApplyCustomRange(List<StreamSpec> 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<MediaSegment>();
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;
}
}
/// <summary>
/// 根据用户输入,清除广告分片
/// </summary>
/// <param name="selectedSteams"></param>
/// <param name="customRange"></param>
public static void CleanAd(List<StreamSpec> 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);
}
}
}
}

View File

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

View File

@ -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<Language> ALL_LANGS = @"
private readonly static List<Language> 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<string, string> CODE_MAP = @"
private static Dictionary<string, string> 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)
private static string ConvertTwoToThree(string input)
{
if (CODE_MAP.TryGetValue(input, out var code)) return code;
return input;
}
/// <summary>
/// 转换 ISO 639-1 => ISO 639-2
/// 且当Description为空时将DisplayName写入
/// </summary>
/// <param name="outputFile"></param>
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)
{
if (CODE_MAP.TryGetValue(input, out var code)) return code;
return input;
//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));
}
/// <summary>
/// 转换 ISO 639-1 => ISO 639-2
/// 且当Description为空时将DisplayName写入
/// </summary>
/// <param name="outputFile"></param>
public static void ConvertLangCodeAndDisplayName(OutputFile outputFile)
if (lang != null)
{
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;
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;
}
}

View File

@ -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;
}
/// <summary>
/// URL大文件切片处理
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <param name="splitSegments"></param>
/// <returns></returns>
public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> 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<Clip> allClips = GetAllClips(url, fileSize);
var splitSegments = new List<MediaSegment>();
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,
});
}
/// <summary>
/// URL大文件切片处理
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <param name="splitSegments"></param>
/// <returns></returns>
public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers)
return splitSegments;
}
public static async Task<bool> CanSplitAsync(string url, Dictionary<string, string> 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<Clip> allClips = GetAllClips(url, fileSize);
var splitSegments = new List<MediaSegment>();
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<bool> CanSplitAsync(string url, Dictionary<string, string> 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<long> GetFileSizeAsync(string url, Dictionary<string, string> 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<Clip> GetAllClips(string url, long fileSize)
{
List<Clip> 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<long> GetFileSizeAsync(string url, Dictionary<string, string> 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<Clip> GetAllClips(string url, long fileSize)
{
List<Clip> 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;
}
}

View File

@ -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<bool> 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<bool> DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false)
if (keys == null || keys.Length == 0) return false;
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)
// 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();
}
if (keyPair == null) return false;
//shakaPackager 无法单独解密init文件
if (source.EndsWith("_init.mp4") && shakaPackager) return false;
var cmd = "";
var tmpFile = "";
if (shakaPackager)
{
var enc = source;
//shakaPackager 手动构造文件
if (init != "")
{
trackId = "1";
tmpFile = Path.ChangeExtension(source, ".itmp");
MergeUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile);
enc = tmpFile;
}
if (!string.IsNullOrEmpty(kid))
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)
{
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();
}
if (keyPair == null) return false;
//shakaPackager 无法单独解密init文件
if (source.EndsWith("_init.mp4") && shakaPackager) return false;
var cmd = "";
var tmpFile = "";
if (shakaPackager)
{
var enc = source;
//shakaPackager 手动构造文件
if (init != "")
{
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 = 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;
}
/// <summary>
/// 从文本文件中查询KID的KEY
/// </summary>
/// <param name="file">文本文件</param>
/// <param name="kid">目标KID</param>
/// <returns></returns>
public static async Task<string?> 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();
}
/// <summary>
/// 从文本文件中查询KID的KEY
/// </summary>
/// <param name="file">文本文件</param>
/// <param name="kid">目标KID</param>
/// <returns></returns>
public static async Task<string?> 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;
}
}

View File

@ -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<List<Mediainfo>> 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<Mediainfo>();
public static async Task<List<Mediainfo>> 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<Mediainfo>();
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;
}
}

View File

@ -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
/// <summary>
/// 输入一堆已存在的文件,合并到新文件
/// </summary>
/// <param name="files"></param>
/// <param name="outputFilePath"></param>
public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath)
{
/// <summary>
/// 输入一堆已存在的文件,合并到新文件
/// </summary>
/// <param name="files"></param>
/// <param name="outputFilePath"></param>
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<string>();
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<string>();
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;
}
}

View File

@ -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<string, string> SplitHeaderArrayToDic(string[]? headers)
{
Dictionary<string, string> dic = new();
namespace N_m3u8DL_RE.Util;
if (headers != null)
internal class OtherUtil
{
public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers)
{
Dictionary<string, string> 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('.');
}
/// <summary>
/// 从输入自动获取文件名
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 从 hh:mm:ss 解析TimeSpan
/// </summary>
/// <param name="timeStr"></param>
/// <returns></returns>
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);
}
/// <summary>
/// 从1h3m20s解析出总秒数
/// </summary>
/// <param name="timeStr"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
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("时间格式无效");
}
/// <summary>
/// 从输入自动获取文件名
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
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);
}
/// <summary>
/// 从 hh:mm:ss 解析TimeSpan
/// </summary>
/// <param name="timeStr"></param>
/// <returns></returns>
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);
}
/// <summary>
/// 从1h3m20s解析出总秒数
/// </summary>
/// <param name="timeStr"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static double ParseSeconds(string timeStr)
/// <summary>
/// 解压并替换原文件
/// </summary>
/// <param name="filePath"></param>
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);
}
/// <summary>
/// 解压并替换原文件
/// </summary>
/// <param name="filePath"></param>
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}")
};
}
}

View File

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

View File

@ -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
/// <summary>
/// 写出图形字幕PNG文件
/// </summary>
/// <param name="finalVtt"></param>
/// <param name="tmpDir">临时目录</param>
/// <returns></returns>
public static async Task<bool> TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir)
{
/// <summary>
/// 写出图形字幕PNG文件
/// </summary>
/// <param name="finalVtt"></param>
/// <param name="tmpDir">临时目录</param>
/// <returns></returns>
public static async Task<bool> 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;
}
}