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.Text.Json.Serialization;
using System.Threading.Tasks; 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, return JsonSerializer.Serialize(s, Context.StreamSpec);
WriteIndented = true, }
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, else if (o is IOrderedEnumerable<StreamSpec> ss)
Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() } {
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) //此函数用于格式化输出时长
{ public static string FormatTime(int time)
if (o is StreamSpec s) {
{ TimeSpan ts = new TimeSpan(0, 0, time);
return JsonSerializer.Serialize(s, Context.StreamSpec); string str = "";
} str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s";
else if (o is IOrderedEnumerable<StreamSpec> ss) return str;
{ }
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) /// <summary>
{ /// 寻找可执行程序
return fileSize switch /// </summary>
{ /// <param name="name"></param>
< 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)), /// <returns></returns>
>= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GB", (double)fileSize / (1024 * 1024 * 1024)), public static string? FindExecutable(string name)
>= 1024 * 1024 => string.Format("{0:####0.00}MB", (double)fileSize / (1024 * 1024)), {
>= 1024 => string.Format("{0:####0.00}KB", (double)fileSize / 1024), var fileExt = OperatingSystem.IsWindows() ? ".exe" : "";
_ => string.Format("{0:####0.00}B", fileSize) 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);
//此函数用于格式化输出时长
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);
}
} }
} }

View File

@ -3,139 +3,138 @@ using System.Net.Http.Headers;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; 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,
AllowAutoRedirect = false, ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true,
AutomaticDecompression = DecompressionMethods.All, MaxConnectionsPerServer = 1024,
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true, };
MaxConnectionsPerServer = 1024,
};
public static readonly HttpClient AppHttpClient = new(HttpClientHandler) public static readonly HttpClient AppHttpClient = new(HttpClientHandler)
{ {
Timeout = TimeSpan.FromSeconds(100), Timeout = TimeSpan.FromSeconds(100),
DefaultRequestVersion = HttpVersion.Version20, DefaultRequestVersion = HttpVersion.Version20,
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher, 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); foreach (var item in headers)
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
webRequest.Headers.Connection.Clear();
if (headers != null)
{ {
foreach (var item in headers) webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value);
}
}
Logger.Debug(webRequest.Headers.ToString());
//手动处理跳转以免自定义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);
{
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;
} }
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>
/// 获取网页源码 /// 获取网页源码
/// </summary> /// </summary>
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="headers"></param> /// <param name="headers"></param>
/// <returns></returns> /// <returns></returns>
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null) public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
{
string htmlCode = string.Empty;
var webResponse = await DoGetAsync(url, headers);
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(); 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) public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)
{ {
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower(); string htmlCode = string.Empty;
return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg"; using HttpRequestMessage request = new(HttpMethod.Post, Url);
} request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());
/// <summary> request.Content = new ByteArrayContent(postData);
/// 获取网页源码和跳转后的URL var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
/// </summary> htmlCode = await webResponse.Content.ReadAsStringAsync();
/// <param name="url"></param> return htmlCode;
/// <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;
}
} }
} }

View File

@ -1,46 +1,39 @@
using System; namespace N_m3u8DL_RE.Common.Util;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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> return Convert.FromHexString(hexSpan);
/// 判断是不是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);
}
} }
} }

View File

@ -4,7 +4,7 @@ using Spectre.Console;
namespace N_m3u8DL_RE.Common.Util; 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) 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;
using System.Net.Http.Headers; using System.Net.Http.Headers;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
{
internal 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) 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); await inputStream.CopyToAsync(outputStream);
using var outputStream = new FileStream(path, FileMode.OpenOrCreate); speedContainer.Add(inputStream.Length);
inputStream.Seek(fromPosition ?? 0L, SeekOrigin.Begin); }
var expect = (toPosition ?? inputStream.Length) - inputStream.Position + 1; else
if (expect == inputStream.Length + 1) {
{ var buffer = new byte[expect];
await inputStream.CopyToAsync(outputStream); await inputStream.ReadAsync(buffer);
speedContainer.Add(inputStream.Length); await outputStream.WriteAsync(buffer, 0, buffer.Length);
} speedContainer.Add(buffer.Length);
else }
{ return new DownloadResult()
var buffer = new byte[expect]; {
await inputStream.ReadAsync(buffer); ActualContentLength = outputStream.Length,
await outputStream.WriteAsync(buffer, 0, buffer.Length); ActualFilePath = path
speedContainer.Add(buffer.Length); };
} }
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() return new DownloadResult()
{ {
ActualContentLength = outputStream.Length, ActualContentLength = bytes.Length,
ActualFilePath = path ActualFilePath = path,
}; };
} }
if (url.StartsWith("hex://"))
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); var bytes = HexUtil.HexToBytes(url[6..]);
if (url.StartsWith("file:")) await File.WriteAllBytesAsync(path, bytes);
return new DownloadResult()
{ {
var file = new Uri(url).LocalPath; ActualContentLength = bytes.Length,
return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition); 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..]); HttpResponseHeaders respHeaders = response.Headers;
await File.WriteAllBytesAsync(path, bytes); Logger.Debug(respHeaders.ToString());
return new DownloadResult() if (respHeaders != null && respHeaders.Location != null)
{ {
ActualContentLength = bytes.Length, var redirectedUrl = "";
ActualFilePath = path, if (!respHeaders.Location.IsAbsoluteUri)
};
}
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 = ""; Uri uri1 = new Uri(url);
if (!respHeaders.Location.IsAbsoluteUri) Uri uri2 = new Uri(uri1, respHeaders.Location);
{ redirectedUrl = uri2.ToString();
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);
} }
else
{
redirectedUrl = respHeaders.Location.AbsoluteUri;
}
return await DownloadToFileAsync(redirectedUrl, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition);
} }
response.EnsureSuccessStatusCode(); }
var contentLength = response.Content.Headers.ContentLength; response.EnsureSuccessStatusCode();
if (speedContainer.SingleSegment) speedContainer.ResponseLength = contentLength; 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 stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
using var responseStream = await response.Content.ReadAsStreamAsync(cancellationTokenSource.Token); using var responseStream = await response.Content.ReadAsStreamAsync(cancellationTokenSource.Token);
var buffer = new byte[16 * 1024]; var buffer = new byte[16 * 1024];
var size = 0; 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); speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size); await stream.WriteAsync(buffer, 0, size);
//检测imageHeader //限速策略
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer); while (speedContainer.Downloaded > speedContainer.SpeedLimit)
//检测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 Task.Delay(1);
await stream.WriteAsync(buffer, 0, size);
//限速策略
while (speedContainer.Downloaded > speedContainer.SpeedLimit)
{
await Task.Delay(1);
}
} }
}
return new DownloadResult() return new DownloadResult()
{
ActualContentLength = stream.Length,
RespContentLength = contentLength,
ActualFilePath = path,
ImageHeader= imageHeader,
GzipHeader = gZipHeader
};
}
catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationTokenSource.Token)
{ {
speedContainer.ResetLowSpeedCount(); ActualContentLength = stream.Length,
throw new Exception("Download speed too slow!"); 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.Text.RegularExpressions;
using System.Threading.Tasks; 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); var inputs = lists.Where(_ => true);
if (filter.GroupIdReg != null) if (filter.GroupIdReg != null)
inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId)); inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId));
if (filter.LanguageReg != null) if (filter.LanguageReg != null)
inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language)); inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language));
if (filter.NameReg != null) if (filter.NameReg != null)
inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name)); inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name));
if (filter.CodecsReg != null) if (filter.CodecsReg != null)
inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs)); inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs));
if (filter.ResolutionReg != null) if (filter.ResolutionReg != null)
inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution)); inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution));
if (filter.FrameRateReg != null) if (filter.FrameRateReg != null)
inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($"{i.FrameRate}")); inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($"{i.FrameRate}"));
if (filter.ChannelsReg != null) if (filter.ChannelsReg != null)
inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels)); inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels));
if (filter.VideoRangeReg != null) if (filter.VideoRangeReg != null)
inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange)); inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange));
if (filter.UrlReg != null) if (filter.UrlReg != null)
inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url)); inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url));
if (filter.SegmentsMaxCount != null && inputs.All(i => i.SegmentsCount > 0)) if (filter.SegmentsMaxCount != null && inputs.All(i => i.SegmentsCount > 0))
inputs = inputs.Where(i => i.SegmentsCount < filter.SegmentsMaxCount); inputs = inputs.Where(i => i.SegmentsCount < filter.SegmentsMaxCount);
if (filter.SegmentsMinCount != null && inputs.All(i => i.SegmentsCount > 0)) if (filter.SegmentsMinCount != null && inputs.All(i => i.SegmentsCount > 0))
inputs = inputs.Where(i => i.SegmentsCount > filter.SegmentsMinCount); inputs = inputs.Where(i => i.SegmentsCount > filter.SegmentsMinCount);
if (filter.PlaylistMinDur != null) if (filter.PlaylistMinDur != null)
inputs = inputs.Where(i => i.Playlist?.TotalDuration > filter.PlaylistMinDur); inputs = inputs.Where(i => i.Playlist?.TotalDuration > filter.PlaylistMinDur);
if (filter.PlaylistMaxDur != null) if (filter.PlaylistMaxDur != null)
inputs = inputs.Where(i => i.Playlist?.TotalDuration < filter.PlaylistMaxDur); inputs = inputs.Where(i => i.Playlist?.TotalDuration < filter.PlaylistMaxDur);
if (filter.BandwidthMin != null) if (filter.BandwidthMin != null)
inputs = inputs.Where(i => i.Bandwidth >= filter.BandwidthMin); inputs = inputs.Where(i => i.Bandwidth >= filter.BandwidthMin);
if (filter.BandwidthMax != null) if (filter.BandwidthMax != null)
inputs = inputs.Where(i => i.Bandwidth <= filter.BandwidthMax); inputs = inputs.Where(i => i.Bandwidth <= filter.BandwidthMax);
if (filter.Role.HasValue) if (filter.Role.HasValue)
inputs = inputs.Where(i => i.Role == filter.Role); inputs = inputs.Where(i => i.Role == filter.Role);
var bestNumberStr = filter.For.Replace("best", ""); var bestNumberStr = filter.For.Replace("best", "");
var worstNumberStr = filter.For.Replace("worst", ""); var worstNumberStr = filter.For.Replace("worst", "");
if (filter.For == "best" && inputs.Count() > 0) if (filter.For == "best" && inputs.Count() > 0)
inputs = inputs.Take(1).ToList(); inputs = inputs.Take(1).ToList();
else if (filter.For == "worst" && inputs.Count() > 0) else if (filter.For == "worst" && inputs.Count() > 0)
inputs = inputs.TakeLast(1).ToList(); inputs = inputs.TakeLast(1).ToList();
else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0) else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0)
inputs = inputs.Take(bestNumber).ToList(); inputs = inputs.Take(bestNumber).ToList();
else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0) else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0)
inputs = inputs.TakeLast(worstNumber).ToList(); inputs = inputs.TakeLast(worstNumber).ToList();
return inputs.ToList(); return inputs.ToList();
} }
public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter) public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{ {
if (filter == null) return new List<StreamSpec>(lists); if (filter == null) return new List<StreamSpec>(lists);
var inputs = lists.Where(_ => true); var inputs = lists.Where(_ => true);
var selected = DoFilterKeep(lists, filter); 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) public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
{ {
if (lists.Count() == 1) if (lists.Count() == 1)
return new List<StreamSpec>(lists); return new List<StreamSpec>(lists);
//基本流 //基本流
var basicStreams = lists.Where(x => x.MediaType == null); var basicStreams = lists.Where(x => x.MediaType == null);
//可选音频轨道 //可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO); var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
//可选字幕轨道 //可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES); var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
var prompt = new MultiSelectionPrompt<StreamSpec>() var prompt = new MultiSelectionPrompt<StreamSpec>()
.Title(ResString.promptTitle) .Title(ResString.promptTitle)
.UseConverter(x => .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)
{ {
prompt.Select(audios.First(a => a.GroupId == first.AudioId)); if (x.Name != null && x.Name.StartsWith("__"))
} return $"[darkslategray1]{x.Name.Substring(2)}[/]";
}
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();
else else
newSegments = part.MediaSegments.Where(seg => stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) >= customRange.StartSec return x.ToString().EscapeMarkup().RemoveMarkup();
&& stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) <= customRange.EndSec).ToList(); })
.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); var first = lists.First();
part.MediaSegments = newSegments; prompt.Select(first);
}
stream.SkippedDuration = skippedDur; 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> //如果此时还是没有选中任何流,自动选择一个
/// 根据用户输入,清除广告分片 prompt.Select(basicStreams.Concat(audios).Concat(subs).First());
/// </summary>
/// <param name="selectedSteams"></param> //多选
/// <param name="customRange"></param> var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt);
public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords)
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 minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!;
var regList = keywords.Select(s => new Regex(s)); foreach (var item in selectedSteams)
foreach ( var reg in regList)
{ {
Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]"); foreach (var part in item.Playlist!.MediaParts)
}
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)))) part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList();
{
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(); else //通过index同步
{
var countAfter = stream.SegmentsCount; var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index));
foreach (var item in selectedSteams)
if (countBefore != countAfter) {
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; namespace N_m3u8DL_RE.Util;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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检测
var size = bArr.Length; if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3])
//PNG HEADER检测 return true;
if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3]) //GIF HEADER检测
return true; else if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3])
//GIF HEADER检测 return true;
else if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3]) //BMP HEADER检测
return true; else if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8])
//BMP HEADER检测 return true;
else if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8]) //JPEG HEADER检测
return true; else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2])
//JPEG HEADER检测 return true;
else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2]) return false;
return true; }
return false;
}
public static async Task ProcessAsync(string sourcePath) public static async Task ProcessAsync(string sourcePath)
{ {
var sourceData = await File.ReadAllBytesAsync(sourcePath); var sourceData = await File.ReadAllBytesAsync(sourcePath);
//PNG HEADER //PNG HEADER
if (137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3]) if (137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3])
{ {
if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119]) if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119])
sourceData = sourceData[120..]; 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]) 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..]; 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]) 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..]; 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]) 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..]; sourceData = sourceData[771..];
else 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])
{ {
//手动查询结尾标记 0x47 出现两次 //手动查询结尾标记 0x47 出现两次
int skip = 0; int skip = 0;
@ -82,8 +49,33 @@ namespace N_m3u8DL_RE.Util
} }
sourceData = sourceData[skip..]; 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.Text;
using System.Threading.Tasks; 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 Code; public string Description;
public string ExtendCode; public string DescriptionAudio;
public string Description;
public string DescriptionAudio;
public Language(string extendCode, string code, string desc, string descA) public Language(string extendCode, string code, string desc, string descA)
{ {
Code = code; Code = code;
ExtendCode = extendCode; ExtendCode = extendCode;
Description = desc; Description = desc;
DescriptionAudio = descA; DescriptionAudio = descA;
}
} }
}
internal class LanguageCodeUtil internal static class LanguageCodeUtil
{ {
private LanguageCodeUtil() { }
private readonly static List<Language> ALL_LANGS = @" private readonly static List<Language> ALL_LANGS = @"
af;afr;Afrikaans;Afrikaans af;afr;Afrikaans;Afrikaans
af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa) af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa)
am;amh;Amharic;Amharic am;amh;Amharic;Amharic
@ -388,13 +387,13 @@ CC;chi;中文(繁體);中文
CZ;chi;; CZ;chi;;
MA;msa;Melayu;Melayu MA;msa;Melayu;Melayu
" "
.Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => .Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x =>
{ {
var arr = x.Trim().Split(';'); var arr = x.Trim().Split(';');
return new Language(arr[0].Trim(), arr[1].Trim(), arr[2].Trim(), arr[3].Trim()); return new Language(arr[0].Trim(), arr[1].Trim(), arr[2].Trim(), arr[3].Trim());
}).ToList(); }).ToList();
private static Dictionary<string, string> CODE_MAP = @" private static Dictionary<string, string> CODE_MAP = @"
iv;IVL iv;IVL
ar;ara ar;ara
bg;bul bg;bul
@ -500,48 +499,47 @@ nn;nno
bs;bos bs;bos
sr;srp 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; //2位转3位
return input; 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> if (lang != null)
/// 转换 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; outputFile.LangCode = lang.Code;
var originalLangCode = outputFile.LangCode; if (string.IsNullOrEmpty(outputFile.Description))
outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio;
//先直接查找
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;
} }
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.Text;
using System.Threading.Tasks; 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; splitSegments.Add(new MediaSegment()
public required long from; {
public required long to; Index = clip.index,
Url = url,
StartRange = clip.from,
ExpectLength = clip.to == -1 ? null : clip.to - clip.from + 1,
EncryptInfo = segment.EncryptInfo,
});
} }
/// <summary> return splitSegments;
/// URL大文件切片处理 }
/// </summary>
/// <param name="url"></param> public static async Task<bool> CanSplitAsync(string url, Dictionary<string, string> headers)
/// <param name="headers"></param> {
/// <param name="splitSegments"></param> try
/// <returns></returns>
public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers)
{ {
var url = segment.Url; var request = new HttpRequestMessage(HttpMethod.Head, url);
if (!await CanSplitAsync(url, headers)) return null; var response = (await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
bool supportsRangeRequests = response.Headers.Contains("Accept-Ranges");
if (segment.StartRange != null) return null; return supportsRangeRequests;
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;
} }
catch (Exception ex)
public static async Task<bool> CanSplitAsync(string url, Dictionary<string, string> headers)
{ {
try Logger.DebugMarkUp(ex.Message);
{ return false;
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;
} }
} }
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.Diagnostics;
using System.Text.RegularExpressions; 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"; if (keys == null || keys.Length == 0) return false;
public static async Task<bool> DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=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(); if (!string.IsNullOrEmpty(kid))
string? keyPair = null; {
string? trackId = null; 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)); cmd = string.Join(" ", keyPairs.Select(k => $"--key {k}"));
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]}";
} }
else else
{ {
if (trackId == null) cmd = string.Join(" ", keyPairs.Select(k => $"--key {trackId}:{k.Split(':')[1]}"));
{
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}\"";
} }
if (init != "")
await RunCommandAsync(bin, cmd);
if (File.Exists(dest) && new FileInfo(dest).Length > 0)
{ {
if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile); cmd += $" --fragments-info \"{init}\" ";
return true;
} }
cmd += $" \"{source}\" \"{dest}\"";
return false;
} }
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}"); if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile);
Logger.DebugMarkUp($"Arguments: {arg}"); return true;
await Process.Start(new ProcessStartInfo()
{
FileName = name,
Arguments = arg,
//RedirectStandardOutput = true,
//RedirectStandardError = true,
CreateNoWindow = true,
UseShellExecute = false
})!.WaitForExitAsync();
} }
/// <summary> return false;
/// 从文本文件中查询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); private static async Task RunCommandAsync(string name, string arg)
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); {
using var reader = new StreamReader(stream); Logger.DebugMarkUp($"FileName: {name}");
var line = ""; Logger.DebugMarkUp($"Arguments: {arg}");
while ((line = await reader.ReadLineAsync()) != null) 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;
} }
catch (Exception ex)
public static ParsedMP4Info GetMP4Info(byte[] data)
{ {
var info = MP4InitUtil.ReadInit(data); Logger.ErrorMarkUp(ex.Message);
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;
} }
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);
var header = new byte[1 * 1024 * 1024]; //1MB return GetMP4Info(header);
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;
} }
} }
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.Threading.Tasks;
using System.Xml.Linq; 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 #.*")] var result = new List<Mediainfo>();
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) 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; foreach (Match stream in TextRegex().Matches(output))
{
string cmd = "-hide_banner -i \"" + file + "\""; var info = new Mediainfo()
var p = Process.Start(new ProcessStartInfo()
{ {
FileName = binary, Text = TypeRegex().Match(stream.Value).Groups[2].Value.TrimEnd(),
Arguments = cmd, Id = IdRegex().Match(stream.Value).Groups[1].Value,
RedirectStandardOutput = true, Type = TypeRegex().Match(stream.Value).Groups[1].Value,
RedirectStandardError = true, };
UseShellExecute = false
})!;
var output = p.StandardError.ReadToEnd();
await p.WaitForExitAsync();
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() var f = StartRegex().Match(output).Groups[1].Value;
{ if (double.TryParse(f, out var d))
Text = TypeRegex().Match(stream.Value).Groups[2].Value.TrimEnd(), info.StartTime = TimeSpan.FromSeconds(d);
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);
} }
if (result.Count == 0) result.Add(info);
{
result.Add(new Mediainfo()
{
Type = "Unknown"
});
}
return result;
} }
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 System.Text;
using N_m3u8DL_RE.Enum; 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> if (files.Length == 0) return;
/// 输入一堆已存在的文件,合并到新文件 if (files.Length == 1)
/// </summary>
/// <param name="files"></param>
/// <param name="outputFilePath"></param>
public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath)
{ {
if (files.Length == 0) return; FileInfo fi = new FileInfo(files[0]);
if (files.Length == 1) fi.CopyTo(outputFilePath, true);
{ return;
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);
}
}
}
} }
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}"); foreach (var inputFilePath in inputFilePaths)
using var p = new Process();
p.StartInfo = new ProcessStartInfo()
{ {
WorkingDirectory = workingDirectory, if (inputFilePath == "")
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; continue;
var output = outputName + index.ToString("0000") + ".ts"; using (var inputStream = File.OpenRead(inputFilePath))
CombineMultipleFilesIntoSingleFile(items, output);
newFiles.Add(output);
//合并后删除这些文件
foreach (var item in items)
{ {
File.Delete(item); inputStream.CopyTo(outputStream);
}
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;
} }
} }
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;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
{
internal class OtherUtil
{
public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers)
{
Dictionary<string, string> dic = new();
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(':'); dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim();
if (index != -1)
{
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" return dic;
.Split(',').Select(s => (char)int.Parse(s)).ToArray(); }
public static string GetValidFileName(string input, string re = ".", bool filterSlash = false)
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; title = title.Replace(invalidChar.ToString(), re);
foreach (char invalidChar in InvalidChars) }
{ if (filterSlash)
title = title.Replace(invalidChar.ToString(), re); {
} title = title.Replace("/", re);
if (filterSlash) title = title.Replace("\\", re);
{ }
title = title.Replace("/", re); return title.Trim('.');
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> 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;
/// </summary> int seconds = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0;
/// <param name="input"></param>
/// <returns></returns> return hours * 3600 + minutes * 60 + seconds;
public static string GetFileNameFromInput(string input, bool addSuffix = true) }
//若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录
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; Directory.Delete(dirPath);
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;
} }
else
/// <summary>
/// 从 hh:mm:ss 解析TimeSpan
/// </summary>
/// <param name="timeStr"></param>
/// <returns></returns>
public static TimeSpan ParseDur(string timeStr)
{ {
var arr = timeStr.Replace("", ":").Split(':'); return;
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);
} }
SafeDeleteDir(parent);
}
/// <summary> /// <summary>
/// 从1h3m20s解析出总秒数 /// 解压并替换原文件
/// </summary> /// </summary>
/// <param name="timeStr"></param> /// <param name="filePath"></param>
/// <returns></returns> public static async Task DeGzipFileAsync(string filePath)
/// <exception cref="ArgumentException"></exception> {
public static double ParseSeconds(string timeStr) var deGzipFile = Path.ChangeExtension(filePath, ".dezip_tmp");
try
{ {
var pattern = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$"); await using (var fileToDecompressAsStream = File.OpenRead(filePath))
var match = pattern.Match(timeStr);
if (!match.Success)
{ {
throw new ArgumentException("时间格式无效"); await using var decompressedStream = File.Create(deGzipFile);
} await using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress);
await decompressionStream.CopyToAsync(decompressedStream);
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}")
}; };
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.Text;
using System.Threading.Tasks; 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);
{
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);
}
} }
else
public static async Task<bool> StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath)
{ {
return await Task.Run(async () => var path = Path.Combine(Path.GetTempPath(), pipeName);
{
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(); using var p = new Process();
p.StartInfo = new ProcessStartInfo() p.StartInfo = new ProcessStartInfo()
{ {
WorkingDirectory = Environment.CurrentDirectory, FileName = "mkfifo",
FileName = binary, Arguments = path,
Arguments = command.ToString(),
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
}; };
//p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42");
p.Start(); p.Start();
p.WaitForExit(); p.WaitForExit();
Thread.Sleep(200);
return p.ExitCode == 0; 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.Text;
using System.Threading.Tasks; 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> if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::")))
/// 写出图形字幕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::"))) Logger.WarnMarkUp(ResString.processImageSub);
var _i = 0;
foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::")))
{ {
Logger.WarnMarkUp(ResString.processImageSub); var name = $"{_i++}.png";
var _i = 0; var dest = "";
foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::"))) for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{_i++}.png") ;
{ var base64 = img.Payload[8..];
var name = $"{_i++}.png"; await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64));
var dest = ""; img.Payload = name;
for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{_i++}.png") ;
var base64 = img.Payload[8..];
await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64));
img.Payload = name;
}
return true;
} }
else return false; return true;
} }
else return false;
} }
} }