Marked util classes as static (#460)
* Marked util classes as static * Used file-scoped namespaces
This commit is contained in:
parent
8a25815c1f
commit
9fc37d5b61
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
|
//检测GZip(For DDP Audio)
|
||||||
|
bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;
|
||||||
|
|
||||||
|
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
|
||||||
|
{
|
||||||
speedContainer.Add(size);
|
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)
|
||||||
//检测GZip(For DDP Audio)
|
|
||||||
bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;
|
|
||||||
|
|
||||||
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
|
|
||||||
{
|
{
|
||||||
speedContainer.Add(size);
|
await 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!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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}")
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue