初步支持直播录制
This commit is contained in:
parent
e3bbdde016
commit
1429f3c14b
|
@ -52,8 +52,15 @@ namespace N_m3u8DL_RE.Common.Resource
|
||||||
public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); }
|
public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); }
|
||||||
public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); }
|
public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); }
|
||||||
public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); }
|
public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); }
|
||||||
|
public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); }
|
||||||
|
public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); }
|
||||||
|
public static string cmd_liveRealTimeMerge { get => GetText("cmd_liveRealTimeMerge"); }
|
||||||
|
public static string cmd_livePerformAsVod { get => GetText("cmd_livePerformAsVod"); }
|
||||||
public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); }
|
public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); }
|
||||||
public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); }
|
public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); }
|
||||||
|
public static string liveLimit { get => GetText("liveLimit"); }
|
||||||
|
public static string liveLimitReached { get => GetText("liveLimitReached"); }
|
||||||
|
public static string saveName { get => GetText("saveName"); }
|
||||||
public static string fetch { get => GetText("fetch"); }
|
public static string fetch { get => GetText("fetch"); }
|
||||||
public static string ffmpegMerge { get => GetText("ffmpegMerge"); }
|
public static string ffmpegMerge { get => GetText("ffmpegMerge"); }
|
||||||
public static string ffmpegNotFound { get => GetText("ffmpegNotFound"); }
|
public static string ffmpegNotFound { get => GetText("ffmpegNotFound"); }
|
||||||
|
|
|
@ -232,6 +232,30 @@ namespace N_m3u8DL_RE.Common.Resource
|
||||||
zhTW: "此字符串將直接傳遞給URL Processor",
|
zhTW: "此字符串將直接傳遞給URL Processor",
|
||||||
enUS: "Give these arguments to the URL Processors."
|
enUS: "Give these arguments to the URL Processors."
|
||||||
),
|
),
|
||||||
|
["cmd_liveRealTimeMerge"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "录制直播时实时合并",
|
||||||
|
zhTW: "錄製直播時即時合併",
|
||||||
|
enUS: "Real-time merge into file when recording live"
|
||||||
|
),
|
||||||
|
["cmd_livePerformAsVod"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "以点播方式下载直播流",
|
||||||
|
zhTW: "以點播方式下載直播流",
|
||||||
|
enUS: "Download live streams as vod"
|
||||||
|
),
|
||||||
|
["cmd_liveKeepSegments"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "录制直播并开启实时合并时依然保留分片",
|
||||||
|
zhTW: "錄製直播並開啟即時合併時依然保留分片",
|
||||||
|
enUS: "Keep segments when recording a live broadcast and enable liveRealTimeMerge"
|
||||||
|
),
|
||||||
|
["cmd_liveRecordLimit"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "录制直播时的录制时长限制",
|
||||||
|
zhTW: "錄製直播時的錄製時長限制",
|
||||||
|
enUS: "Recording time limit when recording live"
|
||||||
|
),
|
||||||
["cmd_useShakaPackager"] = new TextContainer
|
["cmd_useShakaPackager"] = new TextContainer
|
||||||
(
|
(
|
||||||
zhCN: "解密时使用shaka-packager替代mp4decrypt",
|
zhCN: "解密时使用shaka-packager替代mp4decrypt",
|
||||||
|
@ -397,6 +421,24 @@ namespace N_m3u8DL_RE.Common.Resource
|
||||||
zhTW: "解析後的訊息是否輸出json文件",
|
zhTW: "解析後的訊息是否輸出json文件",
|
||||||
enUS: "Write meta json after parsed"
|
enUS: "Write meta json after parsed"
|
||||||
),
|
),
|
||||||
|
["liveLimit"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "本次直播录制时长上限: ",
|
||||||
|
zhTW: "本次直播錄製時長上限: ",
|
||||||
|
enUS: "Live recording duration limit: "
|
||||||
|
),
|
||||||
|
["liveLimitReached"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "到达直播录制上限,即将停止录制",
|
||||||
|
zhTW: "到達直播錄製上限,即將停止錄製",
|
||||||
|
enUS: "Live recording limit reached, will stop recording soon"
|
||||||
|
),
|
||||||
|
["saveName"] = new TextContainer
|
||||||
|
(
|
||||||
|
zhCN: "保存文件名: ",
|
||||||
|
zhTW: "保存檔案名: ",
|
||||||
|
enUS: "Save Name: "
|
||||||
|
),
|
||||||
["fetch"] = new TextContainer
|
["fetch"] = new TextContainer
|
||||||
(
|
(
|
||||||
zhCN: "获取: ",
|
zhCN: "获取: ",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using N_m3u8DL_RE.Common.Entity;
|
using N_m3u8DL_RE.Common.Entity;
|
||||||
using N_m3u8DL_RE.Common.Enum;
|
using N_m3u8DL_RE.Common.Enum;
|
||||||
|
using N_m3u8DL_RE.Common.Util;
|
||||||
using N_m3u8DL_RE.Parser.Config;
|
using N_m3u8DL_RE.Parser.Config;
|
||||||
using N_m3u8DL_RE.Parser.Constants;
|
using N_m3u8DL_RE.Parser.Constants;
|
||||||
using N_m3u8DL_RE.Parser.Util;
|
using N_m3u8DL_RE.Parser.Util;
|
||||||
|
@ -471,10 +472,23 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
return streamList;
|
return streamList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
|
||||||
{
|
{
|
||||||
|
if (streamSpecs.Count == 0) return;
|
||||||
|
var rawText = await HTTPUtil.GetWebSourceAsync(ParserConfig.Url);
|
||||||
|
var newStreams = await ExtractStreamsAsync(rawText);
|
||||||
|
foreach (var streamSpec in streamSpecs)
|
||||||
|
{
|
||||||
|
var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());
|
||||||
|
if (match.Any())
|
||||||
|
streamSpec.Playlist = match.First().Playlist;
|
||||||
|
}
|
||||||
//这里才调用URL预处理器,节省开销
|
//这里才调用URL预处理器,节省开销
|
||||||
|
await ProcessUrlAsync(streamSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
for (int i = 0; i < streamSpecs.Count; i++)
|
for (int i = 0; i < streamSpecs.Count; i++)
|
||||||
{
|
{
|
||||||
var playlist = streamSpecs[i].Playlist;
|
var playlist = streamSpecs[i].Playlist;
|
||||||
|
@ -496,6 +510,12 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
//这里才调用URL预处理器,节省开销
|
||||||
|
await ProcessUrlAsync(streamSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
public string PreProcessUrl(string url)
|
public string PreProcessUrl(string url)
|
||||||
{
|
{
|
||||||
foreach (var p in ParserConfig.UrlProcessors)
|
foreach (var p in ParserConfig.UrlProcessors)
|
||||||
|
|
|
@ -536,5 +536,10 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
await FetchPlayListAsync(streamSpecs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
|
||||||
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
|
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
|
||||||
|
|
||||||
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
|
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
|
||||||
|
Task RefreshPlayListAsync(List<StreamSpec> streamSpecs);
|
||||||
|
|
||||||
string PreProcessUrl(string url);
|
string PreProcessUrl(string url);
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@ using N_m3u8DL_RE.Common.Resource;
|
||||||
using N_m3u8DL_RE.Parser.Constants;
|
using N_m3u8DL_RE.Parser.Constants;
|
||||||
using N_m3u8DL_RE.Parser.Extractor;
|
using N_m3u8DL_RE.Parser.Extractor;
|
||||||
using N_m3u8DL_RE.Common.Util;
|
using N_m3u8DL_RE.Common.Util;
|
||||||
|
using N_m3u8DL_RE.Common.Enum;
|
||||||
|
|
||||||
namespace N_m3u8DL_RE.Parser
|
namespace N_m3u8DL_RE.Parser
|
||||||
{
|
{
|
||||||
public class StreamExtractor
|
public class StreamExtractor
|
||||||
{
|
{
|
||||||
public IExtractor Extractor { get; private set; }
|
public ExtractorType ExtractorType { get => extractor.ExtractorType; }
|
||||||
|
private IExtractor extractor;
|
||||||
private ParserConfig parserConfig = new ParserConfig();
|
private ParserConfig parserConfig = new ParserConfig();
|
||||||
private string rawText;
|
private string rawText;
|
||||||
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
|
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
|
||||||
|
@ -56,13 +58,13 @@ namespace N_m3u8DL_RE.Parser
|
||||||
if (rawText.StartsWith(HLSTags.ext_m3u))
|
if (rawText.StartsWith(HLSTags.ext_m3u))
|
||||||
{
|
{
|
||||||
Logger.InfoMarkUp(ResString.matchHLS);
|
Logger.InfoMarkUp(ResString.matchHLS);
|
||||||
Extractor = new HLSExtractor(parserConfig);
|
extractor = new HLSExtractor(parserConfig);
|
||||||
}
|
}
|
||||||
else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD"))
|
else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD"))
|
||||||
{
|
{
|
||||||
Logger.InfoMarkUp(ResString.matchDASH);
|
Logger.InfoMarkUp(ResString.matchDASH);
|
||||||
//extractor = new DASHExtractor(parserConfig);
|
//extractor = new DASHExtractor(parserConfig);
|
||||||
Extractor = new DASHExtractor2(parserConfig);
|
extractor = new DASHExtractor2(parserConfig);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -80,7 +82,7 @@ namespace N_m3u8DL_RE.Parser
|
||||||
{
|
{
|
||||||
await semaphore.WaitAsync();
|
await semaphore.WaitAsync();
|
||||||
Logger.Info(ResString.parsingStream);
|
Logger.Info(ResString.parsingStream);
|
||||||
return await Extractor.ExtractStreamsAsync(rawText);
|
return await extractor.ExtractStreamsAsync(rawText);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@ -98,7 +100,20 @@ namespace N_m3u8DL_RE.Parser
|
||||||
{
|
{
|
||||||
await semaphore.WaitAsync();
|
await semaphore.WaitAsync();
|
||||||
Logger.Info(ResString.parsingStream);
|
Logger.Info(ResString.parsingStream);
|
||||||
await Extractor.FetchPlayListAsync(streamSpecs);
|
await extractor.FetchPlayListAsync(streamSpecs);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
await extractor.RefreshPlayListAsync(streamSpecs);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
using N_m3u8DL_RE.Common.Util;
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Rendering;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace N_m3u8DL_RE.Column
|
||||||
|
{
|
||||||
|
internal class RecordingDurationColumn : ProgressColumn
|
||||||
|
{
|
||||||
|
protected override bool NoWrap => true;
|
||||||
|
private ConcurrentDictionary<int, int> _recodingDurDic;
|
||||||
|
public Style MyStyle { get; set; } = new Style(foreground: Color.Grey);
|
||||||
|
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic)
|
||||||
|
{
|
||||||
|
_recodingDurDic = recodingDurDic;
|
||||||
|
}
|
||||||
|
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
return new Text(GlobalUtil.FormatTime(_recodingDurDic[task.Id]), MyStyle).LeftAligned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Rendering;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace N_m3u8DL_RE.Column
|
||||||
|
{
|
||||||
|
internal class RecordingStatusColumn : ProgressColumn
|
||||||
|
{
|
||||||
|
protected override bool NoWrap => true;
|
||||||
|
public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
|
||||||
|
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
|
||||||
|
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
if (task.IsFinished)
|
||||||
|
return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftAligned();
|
||||||
|
return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftAligned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ using N_m3u8DL_RE.Common.Resource;
|
||||||
using N_m3u8DL_RE.Entity;
|
using N_m3u8DL_RE.Entity;
|
||||||
using N_m3u8DL_RE.Enum;
|
using N_m3u8DL_RE.Enum;
|
||||||
using N_m3u8DL_RE.Util;
|
using N_m3u8DL_RE.Util;
|
||||||
|
using NiL.JS.Expressions;
|
||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using System.CommandLine.Binding;
|
using System.CommandLine.Binding;
|
||||||
using System.CommandLine.Parsing;
|
using System.CommandLine.Parsing;
|
||||||
|
@ -47,6 +48,13 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
private readonly static Option<string?> BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl);
|
private readonly static Option<string?> BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl);
|
||||||
private readonly static Option<bool> ConcurrentDownload = new(new string[] { "-mt", "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false);
|
private readonly static Option<bool> ConcurrentDownload = new(new string[] { "-mt", "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false);
|
||||||
|
|
||||||
|
//直播相关
|
||||||
|
private readonly static Option<bool> LivePerformAsVod = new(new string[] { "--live-perform-as-vod" }, description: ResString.cmd_livePerformAsVod, getDefaultValue: () => false);
|
||||||
|
private readonly static Option<bool> LiveRealTimeMerge = new(new string[] { "--live-real-time-merge" }, description: ResString.cmd_liveRealTimeMerge, getDefaultValue: () => false);
|
||||||
|
private readonly static Option<bool> LiveKeepSegments = new(new string[] { "--live-keep-segments" }, description: ResString.cmd_liveKeepSegments, getDefaultValue: () => true);
|
||||||
|
private readonly static Option<TimeSpan?> LiveRecordLimit = new(new string[] { "--live-record-limit" }, description: ResString.cmd_liveRecordLimit, parseArgument: ParseLiveLimit) { ArgumentHelpName = "HH:mm:ss" };
|
||||||
|
|
||||||
|
|
||||||
//复杂命令行如下
|
//复杂命令行如下
|
||||||
private readonly static Option<MuxOptions?> MuxAfterDone = new(new string[] { "-M", "--mux-after-done" }, description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" };
|
private readonly static Option<MuxOptions?> MuxAfterDone = new(new string[] { "-M", "--mux-after-done" }, description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" };
|
||||||
private readonly static Option<List<OutputFile>> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" };
|
private readonly static Option<List<OutputFile>> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" };
|
||||||
|
@ -54,11 +62,29 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
private readonly static Option<StreamFilter?> AudioFilter = new(new string[] { "-sa", "--select-audio" }, description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
|
private readonly static Option<StreamFilter?> AudioFilter = new(new string[] { "-sa", "--select-audio" }, description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
|
||||||
private readonly static Option<StreamFilter?> SubtitleFilter = new(new string[] { "-ss", "--select-subtitle" }, description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
|
private readonly static Option<StreamFilter?> SubtitleFilter = new(new string[] { "-ss", "--select-subtitle" }, description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析录制直播时长限制
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static TimeSpan? ParseLiveLimit(ArgumentResult result)
|
||||||
|
{
|
||||||
|
var input = result.Tokens.First().Value;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return OtherUtil.ParseDur(input);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
result.ErrorMessage = "error in parse LiveRecordLimit: " + input;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string? ParseSaveName(ArgumentResult result)
|
private static string? ParseSaveName(ArgumentResult result)
|
||||||
{
|
{
|
||||||
var input = result.Tokens.First().Value;
|
var input = result.Tokens.First().Value;
|
||||||
var newName = ConvertUtil.GetValidFileName(input);
|
var newName = OtherUtil.GetValidFileName(input);
|
||||||
if (string.IsNullOrEmpty(newName))
|
if (string.IsNullOrEmpty(newName))
|
||||||
{
|
{
|
||||||
result.ErrorMessage = "Invalid save name!";
|
result.ErrorMessage = "Invalid save name!";
|
||||||
|
@ -143,7 +169,7 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
private static Dictionary<string, string> ParseHeaders(ArgumentResult result)
|
private static Dictionary<string, string> ParseHeaders(ArgumentResult result)
|
||||||
{
|
{
|
||||||
var array = result.Tokens.Select(t => t.Value).ToArray();
|
var array = result.Tokens.Select(t => t.Value).ToArray();
|
||||||
return ConvertUtil.SplitHeaderArrayToDic(array);
|
return OtherUtil.SplitHeaderArrayToDic(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -269,6 +295,10 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
VideoFilter = bindingContext.ParseResult.GetValueForOption(VideoFilter),
|
VideoFilter = bindingContext.ParseResult.GetValueForOption(VideoFilter),
|
||||||
AudioFilter = bindingContext.ParseResult.GetValueForOption(AudioFilter),
|
AudioFilter = bindingContext.ParseResult.GetValueForOption(AudioFilter),
|
||||||
SubtitleFilter = bindingContext.ParseResult.GetValueForOption(SubtitleFilter),
|
SubtitleFilter = bindingContext.ParseResult.GetValueForOption(SubtitleFilter),
|
||||||
|
LiveRealTimeMerge = bindingContext.ParseResult.GetValueForOption(LiveRealTimeMerge),
|
||||||
|
LiveKeepSegments = bindingContext.ParseResult.GetValueForOption(LiveKeepSegments),
|
||||||
|
LiveRecordLimit = bindingContext.ParseResult.GetValueForOption(LiveRecordLimit),
|
||||||
|
LivePerformAsVod = bindingContext.ParseResult.GetValueForOption(LivePerformAsVod),
|
||||||
};
|
};
|
||||||
|
|
||||||
var parsedHeaders = bindingContext.ParseResult.GetValueForOption(Headers);
|
var parsedHeaders = bindingContext.ParseResult.GetValueForOption(Headers);
|
||||||
|
@ -304,13 +334,15 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
|
|
||||||
public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> action)
|
public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> action)
|
||||||
{
|
{
|
||||||
var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220914")
|
var rootCommand = new RootCommand("N_m3u8DL-RE (Beta version) 20220917")
|
||||||
{
|
{
|
||||||
Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,
|
Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,
|
||||||
BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix,
|
BinaryMerge, DelAfterDone, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix,
|
||||||
FFmpegBinaryPath,
|
FFmpegBinaryPath,
|
||||||
LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption,
|
LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption,
|
||||||
MuxAfterDone, MuxImports, VideoFilter, AudioFilter, SubtitleFilter
|
MuxAfterDone,
|
||||||
|
LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LiveRecordLimit,
|
||||||
|
MuxImports, VideoFilter, AudioFilter, SubtitleFilter
|
||||||
};
|
};
|
||||||
rootCommand.TreatUnmatchedTokensAsErrors = true;
|
rootCommand.TreatUnmatchedTokensAsErrors = true;
|
||||||
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder());
|
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder());
|
||||||
|
|
|
@ -51,6 +51,10 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int DownloadRetryCount { get; set; }
|
public int DownloadRetryCount { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// See: <see cref="CommandInvoker.LiveRecordLimit"/>.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? LiveRecordLimit { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// See: <see cref="CommandInvoker.SkipMerge"/>.
|
/// See: <see cref="CommandInvoker.SkipMerge"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SkipMerge { get; set; }
|
public bool SkipMerge { get; set; }
|
||||||
|
@ -107,6 +111,18 @@ namespace N_m3u8DL_RE.CommandLine
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ConcurrentDownload { get; set; }
|
public bool ConcurrentDownload { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// See: <see cref="CommandInvoker.LiveRealTimeMerge"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool LiveRealTimeMerge { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// See: <see cref="CommandInvoker.LiveKeepSegments"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool LiveKeepSegments { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// See: <see cref="CommandInvoker.LivePerformAsVod"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool LivePerformAsVod { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// See: <see cref="CommandInvoker.SubtitleFormat"/>.
|
/// See: <see cref="CommandInvoker.SubtitleFormat"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SubtitleFormat SubtitleFormat { get; set; }
|
public SubtitleFormat SubtitleFormat { get; set; }
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<IlcTrimMetadata>true</IlcTrimMetadata>
|
<IlcTrimMetadata>true</IlcTrimMetadata>
|
||||||
<IlcGenerateStackTraceData>true</IlcGenerateStackTraceData>
|
<IlcGenerateStackTraceData>true</IlcGenerateStackTraceData>
|
||||||
<SatelliteResourceLanguages>zh-CN;zh-TW;en-US</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>zh-CN;zh-TW;en-US</SatelliteResourceLanguages>
|
||||||
<NativeAotCompilerVersion>7.0.0-*</NativeAotCompilerVersion>
|
|
||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
@ -367,7 +367,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
||||||
{
|
{
|
||||||
path = Path.ChangeExtension(path, ".srt");
|
path = Path.ChangeExtension(path, ".srt");
|
||||||
subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
||||||
output = Path.ChangeExtension(output, ".srt");
|
output = Path.ChangeExtension(output, ".srt");
|
||||||
}
|
}
|
||||||
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
||||||
|
@ -402,7 +402,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
||||||
{
|
{
|
||||||
path = Path.ChangeExtension(path, ".srt");
|
path = Path.ChangeExtension(path, ".srt");
|
||||||
subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
||||||
output = Path.ChangeExtension(output, ".srt");
|
output = Path.ChangeExtension(output, ".srt");
|
||||||
}
|
}
|
||||||
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
||||||
|
@ -435,7 +435,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
||||||
{
|
{
|
||||||
path = Path.ChangeExtension(path, ".srt");
|
path = Path.ChangeExtension(path, ".srt");
|
||||||
subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
||||||
output = Path.ChangeExtension(output, ".srt");
|
output = Path.ChangeExtension(output, ".srt");
|
||||||
}
|
}
|
||||||
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
||||||
|
@ -472,7 +472,7 @@ namespace N_m3u8DL_RE.DownloadManager
|
||||||
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
|
||||||
{
|
{
|
||||||
path = Path.ChangeExtension(path, ".srt");
|
path = Path.ChangeExtension(path, ".srt");
|
||||||
subContentFixed = ConvertUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
subContentFixed = OtherUtil.WebVtt2Other(finalVtt, DownloaderConfig.MyOptions.SubtitleFormat);
|
||||||
output = Path.ChangeExtension(output, ".srt");
|
output = Path.ChangeExtension(output, ".srt");
|
||||||
}
|
}
|
||||||
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
await File.WriteAllTextAsync(path, subContentFixed, new UTF8Encoding(false));
|
||||||
|
|
|
@ -186,7 +186,7 @@ namespace N_m3u8DL_RE
|
||||||
var streams = await extractor.ExtractStreamsAsync();
|
var streams = await extractor.ExtractStreamsAsync();
|
||||||
|
|
||||||
//直播检测
|
//直播检测
|
||||||
var livingFlag = streams.Any(s => s.Playlist?.IsLive == true);
|
var livingFlag = streams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod;
|
||||||
if (livingFlag)
|
if (livingFlag)
|
||||||
{
|
{
|
||||||
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]");
|
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]");
|
||||||
|
@ -248,7 +248,7 @@ namespace N_m3u8DL_RE
|
||||||
|
|
||||||
//HLS: 选中流中若有没加载出playlist的,加载playlist
|
//HLS: 选中流中若有没加载出playlist的,加载playlist
|
||||||
//DASH: 加载playlist (调用url预处理器)
|
//DASH: 加载playlist (调用url预处理器)
|
||||||
if (selectedStreams.Any(s => s.Playlist == null) || extractor.Extractor.ExtractorType == ExtractorType.MPEG_DASH)
|
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH)
|
||||||
await extractor.FetchPlayListAsync(selectedStreams);
|
await extractor.FetchPlayListAsync(selectedStreams);
|
||||||
|
|
||||||
//无法识别的加密方式,自动开启二进制合并
|
//无法识别的加密方式,自动开启二进制合并
|
||||||
|
@ -281,19 +281,11 @@ namespace N_m3u8DL_RE
|
||||||
//尝试从URL或文件读取文件名
|
//尝试从URL或文件读取文件名
|
||||||
if (string.IsNullOrEmpty(option.SaveName))
|
if (string.IsNullOrEmpty(option.SaveName))
|
||||||
{
|
{
|
||||||
if (File.Exists(option.Input))
|
option.SaveName = OtherUtil.GetFileNameFromInput(option.Input);
|
||||||
{
|
|
||||||
option.SaveName = Path.GetFileNameWithoutExtension(option.Input) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var uri = new Uri(option.Input);
|
|
||||||
var name = uri.GetLeftPart(UriPartial.Path).Split('/').Last();
|
|
||||||
name = string.Join(".", name.Split('.').SkipLast(1)).Trim('.');
|
|
||||||
option.SaveName = ConvertUtil.GetValidFileName(name) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]");
|
||||||
|
|
||||||
//下载配置
|
//下载配置
|
||||||
var downloadConfig = new DownloaderConfig()
|
var downloadConfig = new DownloaderConfig()
|
||||||
{
|
{
|
||||||
|
@ -313,7 +305,12 @@ namespace N_m3u8DL_RE
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Live not supported yet.");
|
var sldm = new SimpleLiveRecordManager(downloadConfig, selectedStreams, extractor);
|
||||||
|
var result = await sldm.StartRecordAsync();
|
||||||
|
if (result)
|
||||||
|
Logger.InfoMarkUp("[white on green]Done[/]");
|
||||||
|
else
|
||||||
|
Logger.ErrorMarkUp("[white on red]Faild[/]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
using N_m3u8DL_RE.Common.Entity;
|
using N_m3u8DL_RE.Common.Entity;
|
||||||
using N_m3u8DL_RE.Common.Log;
|
using N_m3u8DL_RE.Common.Log;
|
||||||
using N_m3u8DL_RE.Enum;
|
using N_m3u8DL_RE.Enum;
|
||||||
|
using System.CommandLine;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace N_m3u8DL_RE.Util
|
namespace N_m3u8DL_RE.Util
|
||||||
{
|
{
|
||||||
internal class ConvertUtil
|
internal class OtherUtil
|
||||||
{
|
{
|
||||||
public static Dictionary<string,string> SplitHeaderArrayToDic(string[]? headers)
|
public static Dictionary<string,string> SplitHeaderArrayToDic(string[]? headers)
|
||||||
{
|
{
|
||||||
|
@ -66,5 +67,55 @@ namespace N_m3u8DL_RE.Util
|
||||||
}
|
}
|
||||||
return title.Trim('.');
|
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);
|
||||||
|
var name = uri.GetLeftPart(UriPartial.Path).Split('/').Last();
|
||||||
|
name = string.Join(".", name.Split('.').SkipLast(1)).Trim('.');
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue