Core: Use Fluid.Core to generate file names

This commit is contained in:
genteure 2021-12-19 23:13:30 +08:00
parent 441343e92e
commit 20c7a1efb0
12 changed files with 216 additions and 93 deletions

View File

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fluid.Core" Version="2.2.8" />
<PackageReference Include="JsonSubTypes" Version="1.8.0" />
<PackageReference Include="HierarchicalPropertyDefault" Version="0.1.4-beta-g75fdf624b1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />

View File

@ -86,12 +86,19 @@ namespace BililiveRecorder.Core.Config
// 如果用户设置了自定义的文件名格式才需要转换,否则使用全局默认
if (v2.Global.HasRecordFilenameFormat && v2.Global.RecordFilenameFormat is not null)
{
v3.Global.FileNameRecordTemplate = v2.Global.RecordFilenameFormat.Replace("", ""); // TODO
v3.Global.FileNameRecordTemplate = v2.Global.RecordFilenameFormat
.Replace("{date}", "{{ \"now\" | format_date: \"yyyyMMdd\" }}")
.Replace("{time}", "{{ \"now\" | format_date: \"HHmmss\" }}")
.Replace("{ms}", "{{ \"now\" | format_date: \"fff\" }}")
.Replace("{random}", "{% random 3 %}")
.Replace("{roomid}", "{{ roomId }}")
.Replace("{title}", "{{ title }}")
.Replace("{name}", "{{ name }}")
.Replace("{parea}", "{{ areaParent }}")
.Replace("{area}", "{{ areaChild }}")
;
}
throw new NotImplementedException("配置转换还未完成");
return v3;
}

View File

@ -372,7 +372,7 @@ namespace BililiveRecorder.Core.Config.V3
public string RecordingQuality => "10000";
public string FileNameRecordTemplate => @"{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv";
public string FileNameRecordTemplate => "{{ roomId }}-{{ name }}/录制-{{ roomId }}-{{ \"now\" | format_date: \"yyyyMMdd-HHmmss-fff\" }}-{{ title }}.flv";
public string WebHookUrls => string.Empty;

View File

@ -5,6 +5,7 @@ using BililiveRecorder.Core.Api.Http;
using BililiveRecorder.Core.Config.V3;
using BililiveRecorder.Core.Danmaku;
using BililiveRecorder.Core.Recording;
using BililiveRecorder.Core.Templating;
using BililiveRecorder.Flv;
using Microsoft.Extensions.DependencyInjection;
using Polly.Registry;
@ -42,6 +43,7 @@ namespace BililiveRecorder.DependencyInjection
;
public static IServiceCollection AddRecorderRecording(this IServiceCollection services) => services
.AddSingleton<FileNameGenerator>()
.AddScoped<IRecordTaskFactory, RecordTaskFactory>()
.AddScoped<IFlvProcessingContextWriterFactory, FlvProcessingContextWriterWithFileWriterFactory>()
.AddScoped<IFlvTagReaderFactory, FlvTagReaderFactory>()

View File

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using BililiveRecorder.Core.Api;
using BililiveRecorder.Core.Event;
using BililiveRecorder.Core.Templating;
using Serilog;
namespace BililiveRecorder.Core.Recording
@ -14,10 +15,12 @@ namespace BililiveRecorder.Core.Recording
public RawDataRecordTask(IRoom room,
ILogger logger,
IApiClient apiClient)
IApiClient apiClient,
FileNameGenerator fileNameGenerator)
: base(room: room,
logger: logger?.ForContext<RawDataRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
apiClient: apiClient)
apiClient: apiClient,
fileNameGenerator: fileNameGenerator)
{
}

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System.Timers;
using BililiveRecorder.Core.Api;
using BililiveRecorder.Core.Event;
using BililiveRecorder.Core.Templating;
using Serilog;
using Timer = System.Timers.Timer;
@ -28,6 +29,7 @@ namespace BililiveRecorder.Core.Recording
protected readonly IRoom room;
protected readonly ILogger logger;
protected readonly IApiClient apiClient;
private readonly FileNameGenerator fileNameGenerator;
protected string? streamHost;
protected bool started = false;
@ -38,12 +40,12 @@ namespace BililiveRecorder.Core.Recording
private DateTimeOffset fillerStatsLastTrigger;
private TimeSpan durationSinceNoDataReceived;
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient)
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, FileNameGenerator fileNameGenerator)
{
this.room = room ?? throw new ArgumentNullException(nameof(room));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.fileNameGenerator = fileNameGenerator ?? throw new ArgumentNullException(nameof(fileNameGenerator));
this.ct = this.cts.Token;
this.timer.Elapsed += this.Timer_Elapsed_TriggerNetworkStats;
@ -152,82 +154,15 @@ namespace BililiveRecorder.Core.Recording
}
}
#region File Name
protected (string fullPath, string relativePath) CreateFileName()
protected (string fullPath, string relativePath) CreateFileName() => this.fileNameGenerator.CreateFilePath(new FileNameGenerator.FileNameContextData
{
var formatString = this.room.RoomConfig.FileNameRecordTemplate!; // TODO
var now = DateTime.Now;
var date = now.ToString("yyyyMMdd");
var time = now.ToString("HHmmss");
var ms = now.ToString("fff");
var randomStr = this.random.Next(100, 999).ToString();
var relativePath = formatString
.Replace(@"{date}", date)
.Replace(@"{time}", time)
.Replace(@"{ms}", ms)
.Replace(@"{random}", randomStr)
.Replace(@"{roomid}", this.room.RoomConfig.RoomId.ToString())
.Replace(@"{title}", RemoveInvalidFileName(this.room.Title))
.Replace(@"{name}", RemoveInvalidFileName(this.room.Name))
.Replace(@"{parea}", RemoveInvalidFileName(this.room.AreaNameParent))
.Replace(@"{area}", RemoveInvalidFileName(this.room.AreaNameChild))
;
if (!relativePath.EndsWith(".flv", StringComparison.OrdinalIgnoreCase))
relativePath += ".flv";
relativePath = RemoveInvalidFileName(relativePath, ignore_slash: true);
var workDirectory = this.room.RoomConfig.WorkDirectory;
var fullPath = Path.Combine(workDirectory, relativePath);
fullPath = Path.GetFullPath(fullPath);
if (!CheckIsWithinPath(workDirectory!, Path.GetDirectoryName(fullPath)))
{
this.logger.Warning("录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
relativePath = Path.Combine(this.room.RoomConfig.RoomId.ToString(), $"{this.room.RoomConfig.RoomId}-{date}-{time}-{randomStr}.flv");
fullPath = Path.Combine(workDirectory, relativePath);
}
if (File.Exists(fullPath))
{
this.logger.Warning("录制文件名冲突,请检查设置。将写入到默认路径。");
relativePath = Path.Combine(this.room.RoomConfig.RoomId.ToString(), $"{this.room.RoomConfig.RoomId}-{date}-{time}-{randomStr}.flv");
fullPath = Path.Combine(workDirectory, relativePath);
}
return (fullPath, relativePath);
}
internal static string RemoveInvalidFileName(string input, bool ignore_slash = false)
{
foreach (var c in Path.GetInvalidFileNameChars())
if (!ignore_slash || c != '\\' && c != '/')
input = input.Replace(c, '_');
return input;
}
internal static bool CheckIsWithinPath(string parent, string child)
{
if (parent is null || child is null)
return false;
parent = parent.Replace('/', '\\');
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);
}
#endregion
Name = this.room.Name,
Title = this.room.Title,
RoomId = this.room.RoomConfig.RoomId,
ShortId = this.room.ShortId,
AreaParent = this.room.AreaNameParent,
AreaChild = this.room.AreaNameChild,
});
#region Api Requests

View File

@ -9,6 +9,7 @@ using BililiveRecorder.Core.Api;
using BililiveRecorder.Core.Config;
using BililiveRecorder.Core.Event;
using BililiveRecorder.Core.ProcessingRules;
using BililiveRecorder.Core.Templating;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Pipeline;
@ -40,10 +41,12 @@ namespace BililiveRecorder.Core.Recording
IApiClient apiClient,
IFlvTagReaderFactory flvTagReaderFactory,
ITagGroupReaderFactory tagGroupReaderFactory,
IFlvProcessingContextWriterFactory writerFactory)
IFlvProcessingContextWriterFactory writerFactory,
FileNameGenerator fileNameGenerator)
: base(room: room,
logger: logger?.ForContext<StandardRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
apiClient: apiClient)
apiClient: apiClient,
fileNameGenerator: fileNameGenerator)
{
this.flvTagReaderFactory = flvTagReaderFactory ?? throw new ArgumentNullException(nameof(flvTagReaderFactory));
this.tagGroupReaderFactory = tagGroupReaderFactory ?? throw new ArgumentNullException(nameof(tagGroupReaderFactory));

View File

@ -0,0 +1,171 @@
using System;
using System.IO;
using BililiveRecorder.Core.Config.V3;
using Fluid;
using Fluid.Ast;
using Serilog;
namespace BililiveRecorder.Core.Templating
{
public class FileNameGenerator
{
private static readonly ILogger logger = Log.Logger.ForContext<FileNameGenerator>();
private static readonly FluidParser parser = new();
private static readonly IFluidTemplate defaultTemplate = parser.Parse(DefaultConfig.Instance.FileNameRecordTemplate);
private static readonly Random _globalRandom = new();
[ThreadStatic] private static Random? _localRandom;
private static Random Random
{
get
{
if (_localRandom == null)
{
int seed;
lock (_globalRandom)
{
seed = _globalRandom.Next();
}
_localRandom = new Random(seed);
}
return _localRandom;
}
}
private readonly GlobalConfig config;
private IFluidTemplate? template;
static FileNameGenerator()
{
parser.RegisterExpressionTag("random", async static (expression, writer, encoder, context) =>
{
var value = await expression.EvaluateAsync(context);
var valueStr = value.ToStringValue();
if (!int.TryParse(valueStr, out var count))
count = 3;
var text = string.Empty;
while (count > 0)
{
var step = count > 9 ? 9 : count;
count -= step;
var num = Random.Next((int)Math.Pow(10, step));
text += num.ToString("D" + step);
}
await writer.WriteAsync(text);
return Completion.Normal;
});
parser.Compile();
}
public FileNameGenerator(GlobalConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
config.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(config.FileNameRecordTemplate))
{
this.UpdateTemplate();
}
};
this.UpdateTemplate();
}
private void UpdateTemplate()
{
if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error))
{
logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error);
}
this.template = template;
}
public (string fullPath, string relativePath) CreateFilePath(FileNameContextData data)
{
var now = DateTimeOffset.Now;
var templateOptions = new TemplateOptions
{
Now = () => now,
};
templateOptions.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;
var context = new TemplateContext(data, templateOptions);
var workDirectory = this.config.WorkDirectory!;
if (this.template is not { } t)
{
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("文件名模板格式不正确,请检查设置。将写入到默认路径。");
goto returnDefaultPath;
}
var relativePath = t.Render(context);
relativePath = RemoveInvalidFileName(relativePath);
var fullPath = Path.GetFullPath(Path.Combine(workDirectory, relativePath));
if (!CheckIsWithinPath(workDirectory!, Path.GetDirectoryName(fullPath)))
{
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
goto returnDefaultPath;
}
if (File.Exists(fullPath))
{
logger.ForContext(LoggingContext.RoomId, data.RoomId).Warning("录制文件名冲突,请检查设置。将写入到默认路径。");
goto returnDefaultPath;
}
return (fullPath, relativePath);
returnDefaultPath:
var defaultRelativePath = RemoveInvalidFileName(defaultTemplate.Render(context));
return (Path.GetFullPath(Path.Combine(this.config.WorkDirectory, defaultRelativePath)), defaultRelativePath);
}
public class FileNameContextData
{
public int RoomId { get; set; }
public int ShortId { get; set; }
public string Name { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string AreaParent { get; set; } = string.Empty;
public string AreaChild { get; set; } = string.Empty;
}
internal static string RemoveInvalidFileName(string input, bool ignore_slash = true)
{
foreach (var c in Path.GetInvalidFileNameChars())
if (!ignore_slash || c != '\\' && c != '/')
input = input.Replace(c, '_');
return input;
}
internal static bool CheckIsWithinPath(string parent, string child)
{
if (parent is null || child is null)
return false;
parent = parent.Replace('/', '\\');
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);
}
}
}

View File

@ -87,8 +87,8 @@
<ui:PathIcon Margin="2,0" VerticalAlignment="Center" Height="15" Style="{StaticResource PathIconDataOpenInNew}"/>
</Button>
</StackPanel>
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordFilenameFormat}" Header="{l:Loc Settings_FileName_Record}">
<TextBox Text="{Binding RecordFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasFileNameRecordTemplate}" Header="{l:Loc Settings_FileName_Record}">
<TextBox Text="{Binding FileNameRecordTemplate,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
</c:SettingWithDefault>
</StackPanel>
</GroupBox>

View File

@ -7,8 +7,8 @@
"additionalProperties": false,
"properties": {
"FileNameRecordTemplate": {
"description": "录制文件名模板\n默认: {roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv",
"markdownDescription": "录制文件名模板 \n默认: `{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv `\n\nTODO: config v3 新的文件名模板系统的文档还没有写",
"description": "录制文件名模板\n默认: \"{{ roomId }}-{{ name }}/录制-{{ roomId }}-{{ \"now\" | format_date: \"yyyyMMdd-HHmmss-fff\" }}-{{ title }}.flv\"",
"markdownDescription": "录制文件名模板 \n默认: `\"{{ roomId }}-{{ name }}/录制-{{ roomId }}-{{ \"now\" | format_date: \"yyyyMMdd-HHmmss-fff\" }}-{{ title }}.flv\" `\n\nTODO: config v3 新的文件名模板系统的文档还没有写",
"type": "object",
"additionalProperties": false,
"properties": {
@ -18,7 +18,7 @@
},
"Value": {
"type": "string",
"default": "{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv"
"default": "{{ roomId }}-{{ name }}/录制-{{ roomId }}-{{ \"now\" | format_date: \"yyyyMMdd-HHmmss-fff\" }}-{{ title }}.flv"
}
}
},

View File

@ -96,7 +96,8 @@ export const data: Array<ConfigEntry> = [
description: "录制文件名模板",
type: "string?",
configType: "globalOnly",
defaultValue: "@\"{roomid}-{name}/录制-{roomid}-{date}-{time}-{ms}-{title}.flv\"",
defaultValue: "\"{{ roomId }}-{{ name }}/录制-{{ roomId }}-{{ \\\"now\\\" | format_date: \\\"yyyyMMdd-HHmmss-fff\\\" }}-{{ title }}.flv\"",
defaultValueDescription: "\"{{ roomId }}-{{ name }}/录制-{{ roomId }}-{{ \"now\" | format_date: \"yyyyMMdd-HHmmss-fff\" }}-{{ title }}.flv\"",
markdown: "TODO: config v3 新的文件名模板系统的文档还没有写"
},
{

View File

@ -32,7 +32,7 @@ namespace BililiveRecorder.Core.UnitTests.Recording
public void Test(string parent, string child, bool result)
{
// TODO fix path tests
Assert.Equal(result, Core.Recording.RecordTaskBase.CheckIsWithinPath(parent, Path.GetDirectoryName(child)!));
Assert.Equal(result, Core.Templating.FileNameGenerator.CheckIsWithinPath(parent, Path.GetDirectoryName(child)!));
}
}
}