mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 03:32:20 +08:00
Core: Refactor file name templateing
This commit is contained in:
parent
bbada97219
commit
0b8cf27ce9
|
@ -18,7 +18,7 @@ namespace BililiveRecorder.Core.Config.V3
|
||||||
public bool DisableConfigSave { get; set; } = false; // for CLI
|
public bool DisableConfigSave { get; set; } = false; // for CLI
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class RoomConfig
|
public partial class RoomConfig : IFileNameConfig
|
||||||
{
|
{
|
||||||
public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
|
public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
|
||||||
{ }
|
{ }
|
||||||
|
@ -28,7 +28,7 @@ namespace BililiveRecorder.Core.Config.V3
|
||||||
public string? WorkDirectory => this.GetPropertyValue<string>();
|
public string? WorkDirectory => this.GetPropertyValue<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class GlobalConfig
|
public partial class GlobalConfig : IFileNameConfig
|
||||||
{
|
{
|
||||||
public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
|
public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
|
||||||
{
|
{
|
||||||
|
|
9
BililiveRecorder.Core/Config/V3/IFileNameConfig.cs
Normal file
9
BililiveRecorder.Core/Config/V3/IFileNameConfig.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace BililiveRecorder.Core.Config.V3
|
||||||
|
{
|
||||||
|
public interface IFileNameConfig
|
||||||
|
{
|
||||||
|
public string? FileNameRecordTemplate { get; }
|
||||||
|
|
||||||
|
public string? WorkDirectory { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ using BililiveRecorder.Core.Config.V3;
|
||||||
using BililiveRecorder.Core.Danmaku;
|
using BililiveRecorder.Core.Danmaku;
|
||||||
using BililiveRecorder.Core.Recording;
|
using BililiveRecorder.Core.Recording;
|
||||||
using BililiveRecorder.Core.Scripting;
|
using BililiveRecorder.Core.Scripting;
|
||||||
using BililiveRecorder.Core.Templating;
|
|
||||||
using BililiveRecorder.Flv;
|
using BililiveRecorder.Flv;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Polly.Registry;
|
using Polly.Registry;
|
||||||
|
@ -46,7 +45,6 @@ namespace BililiveRecorder.DependencyInjection
|
||||||
;
|
;
|
||||||
|
|
||||||
public static IServiceCollection AddRecorderRecording(this IServiceCollection services) => services
|
public static IServiceCollection AddRecorderRecording(this IServiceCollection services) => services
|
||||||
.AddSingleton<FileNameGenerator>()
|
|
||||||
.AddScoped<IRecordTaskFactory, RecordTaskFactory>()
|
.AddScoped<IRecordTaskFactory, RecordTaskFactory>()
|
||||||
.AddScoped<IFlvProcessingContextWriterFactory, FlvProcessingContextWriterWithFileWriterFactory>()
|
.AddScoped<IFlvProcessingContextWriterFactory, FlvProcessingContextWriterWithFileWriterFactory>()
|
||||||
.AddScoped<IFlvTagReaderFactory, FlvTagReaderFactory>()
|
.AddScoped<IFlvTagReaderFactory, FlvTagReaderFactory>()
|
||||||
|
|
|
@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||||
using BililiveRecorder.Core.Api;
|
using BililiveRecorder.Core.Api;
|
||||||
using BililiveRecorder.Core.Event;
|
using BililiveRecorder.Core.Event;
|
||||||
using BililiveRecorder.Core.Scripting;
|
using BililiveRecorder.Core.Scripting;
|
||||||
using BililiveRecorder.Core.Templating;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace BililiveRecorder.Core.Recording
|
namespace BililiveRecorder.Core.Recording
|
||||||
|
@ -17,12 +16,10 @@ namespace BililiveRecorder.Core.Recording
|
||||||
public RawDataRecordTask(IRoom room,
|
public RawDataRecordTask(IRoom room,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
IApiClient apiClient,
|
IApiClient apiClient,
|
||||||
FileNameGenerator fileNameGenerator,
|
|
||||||
UserScriptRunner userScriptRunner)
|
UserScriptRunner userScriptRunner)
|
||||||
: base(room: room,
|
: base(room: room,
|
||||||
logger: logger?.ForContext<RawDataRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
|
logger: logger?.ForContext<RawDataRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
|
||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
fileNameGenerator: fileNameGenerator,
|
|
||||||
userScriptRunner: userScriptRunner)
|
userScriptRunner: userScriptRunner)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,13 +52,14 @@ namespace BililiveRecorder.Core.Recording
|
||||||
private DateTimeOffset ioStatsLastTrigger;
|
private DateTimeOffset ioStatsLastTrigger;
|
||||||
private TimeSpan durationSinceNoDataReceived;
|
private TimeSpan durationSinceNoDataReceived;
|
||||||
|
|
||||||
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, FileNameGenerator fileNameGenerator, UserScriptRunner userScriptRunner)
|
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, UserScriptRunner userScriptRunner)
|
||||||
{
|
{
|
||||||
this.room = room ?? throw new ArgumentNullException(nameof(room));
|
this.room = room ?? throw new ArgumentNullException(nameof(room));
|
||||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||||
this.fileNameGenerator = fileNameGenerator ?? throw new ArgumentNullException(nameof(fileNameGenerator));
|
|
||||||
this.userScriptRunner = userScriptRunner ?? throw new ArgumentNullException(nameof(userScriptRunner));
|
this.userScriptRunner = userScriptRunner ?? throw new ArgumentNullException(nameof(userScriptRunner));
|
||||||
|
|
||||||
|
this.fileNameGenerator = new FileNameGenerator(room.RoomConfig, logger);
|
||||||
this.ct = this.cts.Token;
|
this.ct = this.cts.Token;
|
||||||
|
|
||||||
this.timer.Elapsed += this.Timer_Elapsed_TriggerIOStats;
|
this.timer.Elapsed += this.Timer_Elapsed_TriggerIOStats;
|
||||||
|
@ -174,7 +175,9 @@ namespace BililiveRecorder.Core.Recording
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected (string fullPath, string relativePath) CreateFileName() => this.fileNameGenerator.CreateFilePath(new FileNameTemplateContext
|
protected (string fullPath, string relativePath) CreateFileName()
|
||||||
|
{
|
||||||
|
var output = this.fileNameGenerator.CreateFilePath(new FileNameTemplateContext
|
||||||
{
|
{
|
||||||
Name = FileNameGenerator.RemoveInvalidFileName(this.room.Name, ignore_slash: false),
|
Name = FileNameGenerator.RemoveInvalidFileName(this.room.Name, ignore_slash: false),
|
||||||
Title = FileNameGenerator.RemoveInvalidFileName(this.room.Title, ignore_slash: false),
|
Title = FileNameGenerator.RemoveInvalidFileName(this.room.Title, ignore_slash: false),
|
||||||
|
@ -186,6 +189,9 @@ namespace BililiveRecorder.Core.Recording
|
||||||
Json = this.room.RawBilibiliApiJsonData,
|
Json = this.room.RawBilibiliApiJsonData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return (output.FullPath!, output.RelativePath);
|
||||||
|
}
|
||||||
|
|
||||||
#region Api Requests
|
#region Api Requests
|
||||||
|
|
||||||
private HttpClient CreateHttpClient()
|
private HttpClient CreateHttpClient()
|
||||||
|
|
|
@ -10,7 +10,6 @@ using BililiveRecorder.Core.Config;
|
||||||
using BililiveRecorder.Core.Event;
|
using BililiveRecorder.Core.Event;
|
||||||
using BililiveRecorder.Core.ProcessingRules;
|
using BililiveRecorder.Core.ProcessingRules;
|
||||||
using BililiveRecorder.Core.Scripting;
|
using BililiveRecorder.Core.Scripting;
|
||||||
using BililiveRecorder.Core.Templating;
|
|
||||||
using BililiveRecorder.Flv;
|
using BililiveRecorder.Flv;
|
||||||
using BililiveRecorder.Flv.Amf;
|
using BililiveRecorder.Flv.Amf;
|
||||||
using BililiveRecorder.Flv.Pipeline;
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
@ -44,12 +43,10 @@ namespace BililiveRecorder.Core.Recording
|
||||||
IFlvTagReaderFactory flvTagReaderFactory,
|
IFlvTagReaderFactory flvTagReaderFactory,
|
||||||
ITagGroupReaderFactory tagGroupReaderFactory,
|
ITagGroupReaderFactory tagGroupReaderFactory,
|
||||||
IFlvProcessingContextWriterFactory writerFactory,
|
IFlvProcessingContextWriterFactory writerFactory,
|
||||||
FileNameGenerator fileNameGenerator,
|
|
||||||
UserScriptRunner userScriptRunner)
|
UserScriptRunner userScriptRunner)
|
||||||
: base(room: room,
|
: base(room: room,
|
||||||
logger: logger?.ForContext<StandardRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
|
logger: logger?.ForContext<StandardRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
|
||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
fileNameGenerator: fileNameGenerator,
|
|
||||||
userScriptRunner: userScriptRunner)
|
userScriptRunner: userScriptRunner)
|
||||||
{
|
{
|
||||||
this.flvTagReaderFactory = flvTagReaderFactory ?? throw new ArgumentNullException(nameof(flvTagReaderFactory));
|
this.flvTagReaderFactory = flvTagReaderFactory ?? throw new ArgumentNullException(nameof(flvTagReaderFactory));
|
||||||
|
|
|
@ -7,6 +7,7 @@ using Fluid.Ast;
|
||||||
using Fluid.Values;
|
using Fluid.Values;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Serilog.Core;
|
||||||
|
|
||||||
namespace BililiveRecorder.Core.Templating
|
namespace BililiveRecorder.Core.Templating
|
||||||
{
|
{
|
||||||
|
@ -15,8 +16,6 @@ namespace BililiveRecorder.Core.Templating
|
||||||
// TODO: 需要改得更通用一些
|
// TODO: 需要改得更通用一些
|
||||||
// 日志不应该一定绑定到一个直播间上
|
// 日志不应该一定绑定到一个直播间上
|
||||||
|
|
||||||
private static readonly ILogger logger = Log.Logger.ForContext<FileNameGenerator>();
|
|
||||||
|
|
||||||
private static readonly FluidParser parser;
|
private static readonly FluidParser parser;
|
||||||
private static readonly IFluidTemplate defaultTemplate;
|
private static readonly IFluidTemplate defaultTemplate;
|
||||||
|
|
||||||
|
@ -39,8 +38,8 @@ namespace BililiveRecorder.Core.Templating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly GlobalConfig config;
|
private readonly IFileNameConfig config;
|
||||||
private IFluidTemplate? template;
|
private readonly ILogger logger;
|
||||||
|
|
||||||
static FileNameGenerator()
|
static FileNameGenerator()
|
||||||
{
|
{
|
||||||
|
@ -72,32 +71,22 @@ namespace BililiveRecorder.Core.Templating
|
||||||
defaultTemplate = parser.Parse(DefaultConfig.Instance.FileNameRecordTemplate);
|
defaultTemplate = parser.Parse(DefaultConfig.Instance.FileNameRecordTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileNameGenerator(GlobalConfig config)
|
public FileNameGenerator(IFileNameConfig config, ILogger? logger)
|
||||||
{
|
{
|
||||||
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
this.logger = logger?.ForContext<FileNameGenerator>() ?? Logger.None;
|
||||||
config.PropertyChanged += (s, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(config.FileNameRecordTemplate))
|
|
||||||
{
|
|
||||||
this.UpdateTemplate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.UpdateTemplate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateTemplate()
|
public FileNameTemplateOutput CreateFilePath(FileNameTemplateContext data)
|
||||||
{
|
{
|
||||||
if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error))
|
var status = FileNameTemplateStatus.Success;
|
||||||
{
|
string? errorMessage = null;
|
||||||
logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error);
|
string relativePath;
|
||||||
}
|
string? fullPath;
|
||||||
this.template = template;
|
|
||||||
}
|
var workDirectory = this.config.WorkDirectory;
|
||||||
|
var skipFullPath = workDirectory is null;
|
||||||
|
|
||||||
public (string fullPath, string relativePath) CreateFilePath(FileNameTemplateContext data)
|
|
||||||
{
|
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
var templateOptions = new TemplateOptions
|
var templateOptions = new TemplateOptions
|
||||||
{
|
{
|
||||||
|
@ -105,49 +94,57 @@ namespace BililiveRecorder.Core.Templating
|
||||||
};
|
};
|
||||||
templateOptions.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;
|
templateOptions.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;
|
||||||
templateOptions.ValueConverters.Add(o => o is JContainer j ? new JContainerValue(j) : null);
|
templateOptions.ValueConverters.Add(o => o is JContainer j ? new JContainerValue(j) : null);
|
||||||
templateOptions.Filters.AddFilter("format_qn", static (FluidValue input, FilterArguments arguments, TemplateContext context)
|
templateOptions.Filters.AddFilter("format_qn",
|
||||||
=> new StringValue(StreamQualityNumber.MapToString((int)input.ToNumberValue()))
|
static (FluidValue input, FilterArguments arguments, TemplateContext context) => new StringValue(StreamQualityNumber.MapToString((int)input.ToNumberValue())));
|
||||||
);
|
|
||||||
|
|
||||||
var context = new TemplateContext(data, templateOptions);
|
var context = new TemplateContext(data, templateOptions);
|
||||||
|
|
||||||
var workDirectory = this.config.WorkDirectory!;
|
if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error))
|
||||||
|
|
||||||
if (this.template is not { } t)
|
|
||||||
{
|
{
|
||||||
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("文件名模板格式不正确,请检查设置。将写入到默认路径。");
|
this.logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error);
|
||||||
|
errorMessage = "文件名模板格式不正确,请修改: " + error;
|
||||||
|
status = FileNameTemplateStatus.TemplateError;
|
||||||
goto returnDefaultPath;
|
goto returnDefaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
var relativePath = t.Render(context);
|
relativePath = template.Render(context);
|
||||||
relativePath = RemoveInvalidFileName(relativePath);
|
relativePath = RemoveInvalidFileName(relativePath);
|
||||||
var fullPath = Path.GetFullPath(Path.Combine(workDirectory, relativePath));
|
|
||||||
|
|
||||||
if (!CheckIsWithinPath(workDirectory!, Path.GetDirectoryName(fullPath)))
|
fullPath = skipFullPath ? null : Path.GetFullPath(Path.Combine(workDirectory, relativePath));
|
||||||
|
|
||||||
|
if (!skipFullPath && !CheckIsWithinPath(workDirectory!, Path.GetDirectoryName(fullPath)))
|
||||||
{
|
{
|
||||||
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
|
this.logger.Warning("录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
|
||||||
|
status = FileNameTemplateStatus.OutOfRange;
|
||||||
|
errorMessage = "录制文件位置超出允许范围";
|
||||||
goto returnDefaultPath;
|
goto returnDefaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = Path.GetExtension(fullPath);
|
var ext = Path.GetExtension(relativePath);
|
||||||
if (!ext.Equals(".flv", StringComparison.OrdinalIgnoreCase))
|
if (!ext.Equals(".flv", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录播姬只支持 FLV 文件格式,将在录制文件后缀名 {ExtensionName} 后添加 .flv。", ext);
|
this.logger.Warning("录播姬只支持 FLV 文件格式,将在录制文件后缀名 {ExtensionName} 后添加 {DotFlv}。", ext, ".flv");
|
||||||
relativePath += ".flv";
|
relativePath += ".flv";
|
||||||
|
|
||||||
|
if (!skipFullPath)
|
||||||
fullPath += ".flv";
|
fullPath += ".flv";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(fullPath))
|
if (!skipFullPath && File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录制文件名冲突,请检查设置。将写入到默认路径。");
|
this.logger.Warning("录制文件名冲突,将写入到默认路径。");
|
||||||
|
status = FileNameTemplateStatus.FileConflict;
|
||||||
|
errorMessage = "录制文件名冲突";
|
||||||
goto returnDefaultPath;
|
goto returnDefaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (fullPath, relativePath);
|
return new FileNameTemplateOutput(status, errorMessage, relativePath, fullPath);
|
||||||
|
|
||||||
returnDefaultPath:
|
returnDefaultPath:
|
||||||
var defaultRelativePath = RemoveInvalidFileName(defaultTemplate.Render(context));
|
var defaultRelativePath = RemoveInvalidFileName(defaultTemplate.Render(context));
|
||||||
return (Path.GetFullPath(Path.Combine(this.config.WorkDirectory, defaultRelativePath)), defaultRelativePath);
|
var defaultFullPath = skipFullPath ? null : Path.GetFullPath(Path.Combine(workDirectory, defaultRelativePath));
|
||||||
|
|
||||||
|
return new FileNameTemplateOutput(status, errorMessage, defaultRelativePath, defaultFullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class JContainerValue : ObjectValueBase
|
private class JContainerValue : ObjectValueBase
|
||||||
|
@ -238,22 +235,29 @@ namespace BililiveRecorder.Core.Templating
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly char[] separator = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
|
||||||
|
|
||||||
internal static bool CheckIsWithinPath(string parent, string child)
|
internal static bool CheckIsWithinPath(string parent, string child)
|
||||||
{
|
{
|
||||||
if (parent is null || child is null)
|
var fullParent = Path.GetFullPath(parent);
|
||||||
|
var fullChild = Path.GetFullPath(child);
|
||||||
|
|
||||||
|
var parentSegments = fullParent.Split(separator, StringSplitOptions.None).AsSpan();
|
||||||
|
if (parentSegments[parentSegments.Length - 1] == "")
|
||||||
|
{
|
||||||
|
parentSegments = parentSegments.Slice(0, parentSegments.Length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var childSegments = fullChild.Split(separator, StringSplitOptions.None).AsSpan();
|
||||||
|
if (childSegments[childSegments.Length - 1] == "")
|
||||||
|
{
|
||||||
|
childSegments = childSegments.Slice(0, childSegments.Length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentSegments.Length >= childSegments.Length)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
parent = parent.Replace('/', '\\');
|
return childSegments.Slice(0, parentSegments.Length).SequenceEqual(parentSegments);
|
||||||
if (!parent.EndsWith("\\"))
|
|
||||||
parent += "\\";
|
|
||||||
parent = Path.GetFullPath(parent);
|
|
||||||
|
|
||||||
child = child.Replace('/', '\\');
|
|
||||||
if (!child.EndsWith("\\"))
|
|
||||||
child += "\\";
|
|
||||||
child = Path.GetFullPath(child);
|
|
||||||
|
|
||||||
return child.StartsWith(parent, StringComparison.Ordinal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
BililiveRecorder.Core/Templating/FileNameTemplateOutput.cs
Normal file
23
BililiveRecorder.Core/Templating/FileNameTemplateOutput.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Core.Templating
|
||||||
|
{
|
||||||
|
public readonly struct FileNameTemplateOutput
|
||||||
|
{
|
||||||
|
public FileNameTemplateOutput(FileNameTemplateStatus status, string? errorMessage, string relativePath, string? fullPath)
|
||||||
|
{
|
||||||
|
this.Status = status;
|
||||||
|
this.ErrorMessage = errorMessage;
|
||||||
|
this.RelativePath = relativePath ?? throw new ArgumentNullException(nameof(relativePath));
|
||||||
|
this.FullPath = fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileNameTemplateStatus Status { get; }
|
||||||
|
|
||||||
|
public string? ErrorMessage { get; }
|
||||||
|
|
||||||
|
public string RelativePath { get; }
|
||||||
|
|
||||||
|
public string? FullPath { get; }
|
||||||
|
}
|
||||||
|
}
|
10
BililiveRecorder.Core/Templating/FileNameTemplateStatus.cs
Normal file
10
BililiveRecorder.Core/Templating/FileNameTemplateStatus.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace BililiveRecorder.Core.Templating
|
||||||
|
{
|
||||||
|
public enum FileNameTemplateStatus
|
||||||
|
{
|
||||||
|
Success = 0,
|
||||||
|
TemplateError,
|
||||||
|
OutOfRange,
|
||||||
|
FileConflict,
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@
|
||||||
l:ResxLocalizationProvider.DefaultDictionary="Strings"
|
l:ResxLocalizationProvider.DefaultDictionary="Strings"
|
||||||
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
|
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||||
xmlns:m="clr-namespace:BililiveRecorder.WPF.Models"
|
xmlns:m="clr-namespace:BililiveRecorder.WPF.Models"
|
||||||
|
xmlns:t="clr-namespace:BililiveRecorder.Core.Templating;assembly=BililiveRecorder.Core"
|
||||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
||||||
xmlns:configv3="clr-namespace:BililiveRecorder.Core.Config.V3;assembly=BililiveRecorder.Core"
|
xmlns:configv3="clr-namespace:BililiveRecorder.Core.Config.V3;assembly=BililiveRecorder.Core"
|
||||||
|
@ -96,7 +97,14 @@
|
||||||
<TextBox Text="{Binding FileNameRecordTemplate,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
<TextBox Text="{Binding FileNameRecordTemplate,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||||
</c:SettingWithDefault>
|
</c:SettingWithDefault>
|
||||||
<Button Margin="0,10,0,5" Content="测试" Click="TestFileNameTemplate_Button_Click"/>
|
<Button Margin="0,10,0,5" Content="测试" Click="TestFileNameTemplate_Button_Click"/>
|
||||||
<TextBox Visibility="Collapsed" IsReadOnly="True" x:Name="FileNameTestResult"/>
|
<Border x:Name="FileNameTestResultArea" d:DataContext="{d:DesignInstance Type=t:FileNameTemplateOutput}" Visibility="Collapsed"
|
||||||
|
BorderBrush="{DynamicResource SystemControlBackgroundBaseMediumBrush}" CornerRadius="5" BorderThickness="1" Padding="5">
|
||||||
|
<StackPanel Orientation="Vertical" Margin="0">
|
||||||
|
<TextBlock Text="测试结果:"/>
|
||||||
|
<TextBlock Text="{Binding ErrorMessage,Mode=OneWay}" Margin="0,5"/>
|
||||||
|
<TextBox IsReadOnly="True" Text="{Binding RelativePath,Mode=OneWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
<GroupBox Header="录制画质">
|
<GroupBox Header="录制画质">
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -23,19 +23,19 @@ namespace BililiveRecorder.Web.Api
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("generateFileName")]
|
[HttpPost("generateFileName")]
|
||||||
public ActionResult<string> GenerateFileName([FromBody] GenerateFileNameInput input)
|
public ActionResult<FileNameTemplateOutput> GenerateFileName([FromBody] GenerateFileNameInput input)
|
||||||
{
|
{
|
||||||
var config = new GlobalConfig()
|
var config = new GlobalConfig()
|
||||||
{
|
{
|
||||||
WorkDirectory = "/",
|
WorkDirectory = "/",
|
||||||
FileNameRecordTemplate = input.Template
|
FileNameRecordTemplate = input.Template
|
||||||
};
|
};
|
||||||
var generator = new FileNameGenerator(config);
|
var generator = new FileNameGenerator(config, null);
|
||||||
|
|
||||||
var context = this.mapper.Map<FileNameTemplateContext>(input.Context);
|
var context = this.mapper.Map<FileNameTemplateContext>(input.Context);
|
||||||
|
|
||||||
var (_, relativePath) = generator.CreateFilePath(context);
|
var output = generator.CreateFilePath(context);
|
||||||
return relativePath;
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ namespace BililiveRecorder.Core.Config.V3
|
||||||
public bool WpfShowTitleAndArea { get; }
|
public bool WpfShowTitleAndArea { get; }
|
||||||
}
|
}
|
||||||
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
|
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
|
||||||
public sealed class GlobalConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.DefaultConfig, BililiveRecorder.Core.Config.V3.GlobalConfig>
|
public sealed class GlobalConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.DefaultConfig, BililiveRecorder.Core.Config.V3.GlobalConfig>, BililiveRecorder.Core.Config.V3.IFileNameConfig
|
||||||
{
|
{
|
||||||
public GlobalConfig() { }
|
public GlobalConfig() { }
|
||||||
public string? Cookie { get; set; }
|
public string? Cookie { get; set; }
|
||||||
|
@ -201,8 +201,13 @@ namespace BililiveRecorder.Core.Config.V3
|
||||||
public string? WorkDirectory { get; set; }
|
public string? WorkDirectory { get; set; }
|
||||||
public bool WpfShowTitleAndArea { get; set; }
|
public bool WpfShowTitleAndArea { get; set; }
|
||||||
}
|
}
|
||||||
|
public interface IFileNameConfig
|
||||||
|
{
|
||||||
|
string? FileNameRecordTemplate { get; }
|
||||||
|
string? WorkDirectory { get; }
|
||||||
|
}
|
||||||
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
|
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
|
||||||
public sealed class RoomConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.GlobalConfig, BililiveRecorder.Core.Config.V3.RoomConfig>
|
public sealed class RoomConfig : HierarchicalPropertyDefault.HierarchicalObject<BililiveRecorder.Core.Config.V3.GlobalConfig, BililiveRecorder.Core.Config.V3.RoomConfig>, BililiveRecorder.Core.Config.V3.IFileNameConfig
|
||||||
{
|
{
|
||||||
public RoomConfig() { }
|
public RoomConfig() { }
|
||||||
public bool AutoRecord { get; set; }
|
public bool AutoRecord { get; set; }
|
||||||
|
@ -474,11 +479,8 @@ namespace BililiveRecorder.Core.Templating
|
||||||
{
|
{
|
||||||
public sealed class FileNameGenerator
|
public sealed class FileNameGenerator
|
||||||
{
|
{
|
||||||
public FileNameGenerator(BililiveRecorder.Core.Config.V3.GlobalConfig config) { }
|
public FileNameGenerator(BililiveRecorder.Core.Config.V3.IFileNameConfig config, Serilog.ILogger? logger) { }
|
||||||
[return: System.Runtime.CompilerServices.TupleElementNames(new string[] {
|
public BililiveRecorder.Core.Templating.FileNameTemplateOutput CreateFilePath(BililiveRecorder.Core.Templating.FileNameTemplateContext data) { }
|
||||||
"fullPath",
|
|
||||||
"relativePath"})]
|
|
||||||
public System.ValueTuple<string, string> CreateFilePath(BililiveRecorder.Core.Templating.FileNameTemplateContext data) { }
|
|
||||||
}
|
}
|
||||||
public class FileNameTemplateContext
|
public class FileNameTemplateContext
|
||||||
{
|
{
|
||||||
|
@ -492,6 +494,21 @@ namespace BililiveRecorder.Core.Templating
|
||||||
public int ShortId { get; set; }
|
public int ShortId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
}
|
}
|
||||||
|
public readonly struct FileNameTemplateOutput
|
||||||
|
{
|
||||||
|
public FileNameTemplateOutput(BililiveRecorder.Core.Templating.FileNameTemplateStatus status, string? errorMessage, string relativePath, string? fullPath) { }
|
||||||
|
public string? ErrorMessage { get; }
|
||||||
|
public string? FullPath { get; }
|
||||||
|
public string RelativePath { get; }
|
||||||
|
public BililiveRecorder.Core.Templating.FileNameTemplateStatus Status { get; }
|
||||||
|
}
|
||||||
|
public enum FileNameTemplateStatus
|
||||||
|
{
|
||||||
|
Success = 0,
|
||||||
|
TemplateError = 1,
|
||||||
|
OutOfRange = 2,
|
||||||
|
FileConflict = 3,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
namespace BililiveRecorder.DependencyInjection
|
namespace BililiveRecorder.DependencyInjection
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,38 +1,70 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using BililiveRecorder.Core.Templating;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace BililiveRecorder.Core.UnitTests.Recording
|
namespace BililiveRecorder.Core.UnitTests.Recording
|
||||||
{
|
{
|
||||||
public class CheckIsWithinPathTests
|
public class CheckIsWithinPathTests
|
||||||
{
|
{
|
||||||
[Theory(Skip = "Path 差异")]
|
[Theory, MemberData(nameof(GetTestData))]
|
||||||
[InlineData(@"C:\", @"C:\", false)]
|
public void RunTest(bool expectation, string parent, string child)
|
||||||
[InlineData(@"C:", @"C:\foo", true)]
|
|
||||||
[InlineData(@"C:\", @"C:\foo", true)]
|
|
||||||
[InlineData(@"C:\foo", @"C:\foo", false)]
|
|
||||||
[InlineData(@"C:\foo\", @"C:\foo", false)]
|
|
||||||
[InlineData(@"C:\foo", @"C:\foo\", true)]
|
|
||||||
[InlineData(@"C:\foo\", @"C:\foo\bar\", true)]
|
|
||||||
[InlineData(@"C:\foo\", @"C:\foo\bar", true)]
|
|
||||||
[InlineData(@"C:\foo", @"C:\FOO\bar", false)]
|
|
||||||
[InlineData(@"C:\foo", @"C:/foo/bar", true)]
|
|
||||||
[InlineData(@"C:\foo", @"C:\foobar", false)]
|
|
||||||
[InlineData(@"C:\foo", @"C:\foobar\baz", false)]
|
|
||||||
[InlineData(@"C:\foo\", @"C:\foobar\baz", false)]
|
|
||||||
[InlineData(@"C:\foobar", @"C:\foo\bar", false)]
|
|
||||||
[InlineData(@"C:\foobar\", @"C:\foo\bar", false)]
|
|
||||||
[InlineData(@"C:\foo", @"C:\foo\..\bar\baz", false)]
|
|
||||||
[InlineData(@"C:\bar", @"C:\foo\..\bar\baz", true)]
|
|
||||||
[InlineData(@"C:\barr", @"C:\foo\..\bar\baz", false)]
|
|
||||||
[InlineData(@"C:\foo\", @"D:\foo\bar", false)]
|
|
||||||
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\foo", false)]
|
|
||||||
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\bar", false)]
|
|
||||||
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\foo\bar", true)]
|
|
||||||
[InlineData(@"\\server1\vol1\foo", @"\\server1\vol1\foo\..\bar", false)]
|
|
||||||
public void Test(string parent, string child, bool result)
|
|
||||||
{
|
{
|
||||||
// TODO fix path tests
|
Assert.Equal(expectation, FileNameGenerator.CheckIsWithinPath(parent, child));
|
||||||
Assert.Equal(result, Core.Templating.FileNameGenerator.CheckIsWithinPath(parent, Path.GetDirectoryName(child)!));
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> GetTestData()
|
||||||
|
{
|
||||||
|
yield return new object[] { true, @"/path/a/", "/path/a/file.flv" };
|
||||||
|
yield return new object[] { true, @"/path/a", "/path/a/file.flv" };
|
||||||
|
|
||||||
|
yield return new object[] { true, @"/path/a/", "/path/a/b/file.flv" };
|
||||||
|
yield return new object[] { true, @"/path/a", "/path/a/b/file.flv" };
|
||||||
|
|
||||||
|
yield return new object[] { true, @"/path/a/", "/path/a/../a/file.flv" };
|
||||||
|
yield return new object[] { true, @"/path/a", "/path/a/../a/file.flv" };
|
||||||
|
|
||||||
|
yield return new object[] { false, @"/path/a/", "/path/a/../b/file.flv" };
|
||||||
|
yield return new object[] { false, @"/path/a", "/path/a/../b/file.flv" };
|
||||||
|
|
||||||
|
yield return new object[] { true, @"/", "/path/a/file.flv" };
|
||||||
|
yield return new object[] { true, @"/", "/file.flv" };
|
||||||
|
|
||||||
|
yield return new object[] { false, @"/path", "/path/a/../../../../file.flv" };
|
||||||
|
yield return new object[] { false, @"/path", "/path../../../../file.flv" };
|
||||||
|
|
||||||
|
yield return new object[] { true, @"/path/a/", "/path////a/file.flv" };
|
||||||
|
yield return new object[] { true, @"/path/a", "/path////a/file.flv" };
|
||||||
|
|
||||||
|
yield return new object[] { false, @"/path/", "/path/" };
|
||||||
|
yield return new object[] { false, @"/path", "/path" };
|
||||||
|
yield return new object[] { false, @"/path/", "/path" };
|
||||||
|
yield return new object[] { false, @"/path", "/path/" };
|
||||||
|
|
||||||
|
var isWindows = Path.DirectorySeparatorChar == '\\';
|
||||||
|
|
||||||
|
yield return new object[] { isWindows && false, @"C:\", @"C:\" };
|
||||||
|
yield return new object[] { isWindows && true, @"C:\", @"C:\foo" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foo" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo\", @"C:\foo" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foo\" };
|
||||||
|
yield return new object[] { isWindows && true, @"C:\foo\", @"C:\foo\bar\" };
|
||||||
|
yield return new object[] { isWindows && true, @"C:\foo\", @"C:\foo\bar" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo", @"C:\FOO\bar" };
|
||||||
|
yield return new object[] { isWindows && true, @"C:\foo", @"C:/foo/bar" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foobar" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foobar\baz" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo\", @"C:\foobar\baz" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foobar", @"C:\foo\bar" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foobar\", @"C:\foo\bar" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo", @"C:\foo\..\bar\baz" };
|
||||||
|
yield return new object[] { isWindows && true, @"C:\bar", @"C:\foo\..\bar\baz" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\barr", @"C:\foo\..\bar\baz" };
|
||||||
|
yield return new object[] { isWindows && false, @"C:\foo\", @"D:\foo\bar" };
|
||||||
|
yield return new object[] { isWindows && false, @"\\server1\vol1\foo", @"\\server1\vol1\foo" };
|
||||||
|
yield return new object[] { isWindows && false, @"\\server1\vol1\foo", @"\\server1\vol1\bar" };
|
||||||
|
yield return new object[] { isWindows && true, @"\\server1\vol1\foo", @"\\server1\vol1\foo\bar" };
|
||||||
|
yield return new object[] { isWindows && false, @"\\server1\vol1\foo", @"\\server1\vol1\foo\..\bar" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user