BililiveRecorder/BililiveRecorder.Core/Templating/FileNameGenerator.cs

267 lines
9.4 KiB
C#

using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BililiveRecorder.Core.Config.V3;
using Fluid;
using Fluid.Ast;
using Fluid.Values;
using Newtonsoft.Json.Linq;
using Serilog;
using Serilog.Core;
namespace BililiveRecorder.Core.Templating
{
public sealed class FileNameGenerator
{
private static readonly FluidParser parser;
private static readonly IFluidTemplate defaultTemplate;
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 IFileNameConfig config;
private readonly ILogger logger;
static FileNameGenerator()
{
parser = new FluidParser();
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 = parser.Compile();
defaultTemplate = parser.Parse(DefaultConfig.Instance.FileNameRecordTemplate);
}
public FileNameGenerator(IFileNameConfig config, ILogger? logger)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.logger = logger?.ForContext<FileNameGenerator>() ?? Logger.None;
}
public FileNameTemplateOutput CreateFilePath(FileNameTemplateContext data)
{
var status = FileNameTemplateStatus.Success;
string? errorMessage = null;
string relativePath;
string? fullPath;
var workDirectory = this.config.WorkDirectory;
var skipFullPath = workDirectory is null;
var now = DateTimeOffset.Now;
var templateOptions = new TemplateOptions
{
Now = () => now,
};
templateOptions.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;
templateOptions.ValueConverters.Add(o => o is JContainer j ? new JContainerValue(j) : null);
templateOptions.Filters.AddFilter("format_qn",
static (FluidValue input, FilterArguments arguments, TemplateContext context) => new StringValue(StreamQualityNumber.MapToString((int)input.ToNumberValue())));
var context = new TemplateContext(data, templateOptions);
if (!parser.TryParse(this.config.FileNameRecordTemplate, out var template, out var error))
{
this.logger.Warning("文件名模板格式不正确,请修改: {ParserError}", error);
errorMessage = "文件名模板格式不正确,请修改: " + error;
status = FileNameTemplateStatus.TemplateError;
goto returnDefaultPath;
}
relativePath = template.Render(context);
relativePath = RemoveInvalidFileName(relativePath);
fullPath = skipFullPath ? null : Path.GetFullPath(Path.Combine(workDirectory, relativePath));
if (!skipFullPath && !CheckIsWithinPath(workDirectory!, fullPath!))
{
this.logger.Warning("录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
status = FileNameTemplateStatus.OutOfRange;
errorMessage = "录制文件位置超出允许范围";
goto returnDefaultPath;
}
var ext = Path.GetExtension(relativePath);
if (!ext.Equals(".flv", StringComparison.OrdinalIgnoreCase))
{
this.logger.Warning("录播姬只支持 FLV 文件格式,将在录制文件后缀名 {ExtensionName} 后添加 {DotFlv}。", ext, ".flv");
relativePath += ".flv";
if (!skipFullPath)
fullPath += ".flv";
}
if (!skipFullPath && File.Exists(fullPath))
{
this.logger.Warning("录制文件名冲突,将写入到默认路径。");
status = FileNameTemplateStatus.FileConflict;
errorMessage = "录制文件名冲突";
goto returnDefaultPath;
}
return new FileNameTemplateOutput(status, errorMessage, relativePath, fullPath);
returnDefaultPath:
var defaultRelativePath = RemoveInvalidFileName(defaultTemplate.Render(context));
var defaultFullPath = skipFullPath ? null : Path.GetFullPath(Path.Combine(workDirectory, defaultRelativePath));
return new FileNameTemplateOutput(status, errorMessage, defaultRelativePath, defaultFullPath);
}
private class JContainerValue : ObjectValueBase
{
public JContainerValue(JContainer value) : base(value)
{
}
public override ValueTask<FluidValue> GetValueAsync(string name, TemplateContext context)
{
var j = (JContainer)this.Value;
JToken? value;
if (j is JObject jobject)
{
value = jobject[name];
}
else
{
return NilValue.Instance;
}
if (value is null)
{
return NilValue.Instance;
}
else if (value is JContainer)
{
return Create(value, context.Options);
}
else if (value is JValue jValue)
{
return Create(jValue.Value, context.Options);
}
else
{
// WHAT ARE YOU !?
return NilValue.Instance;
}
}
public override ValueTask<FluidValue> GetIndexAsync(FluidValue index, TemplateContext context)
{
var j = (JContainer)this.Value;
JToken? value;
try
{
if (index.Type == FluidValues.Number)
{
value = j[(int)index.ToNumberValue()];
}
else
{
value = j[index.ToStringValue()];
}
}
catch (Exception)
{
return NilValue.Instance;
}
if (value is null)
{
return NilValue.Instance;
}
else if (value is JContainer)
{
return Create(value, context.Options);
}
else if (value is JValue jValue)
{
return Create(jValue.Value, context.Options);
}
else
{
// WHAT ARE YOU !?
return NilValue.Instance;
}
}
}
private static readonly Regex invalidDirectoryNameRegex = new Regex(@"([ .])([\\\/])", RegexOptions.Compiled);
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, '_');
input = invalidDirectoryNameRegex.Replace(input, "$1_$2");
return input;
}
private static readonly char[] separator = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
internal static bool CheckIsWithinPath(string parent, string child)
{
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 childSegments.Slice(0, parentSegments.Length).SequenceEqual(parentSegments);
}
}
}