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