mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-12-25 11:54:43 +08:00
267 lines
9.3 KiB
C#
267 lines
9.3 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 = workDirectory is null ? 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 = workDirectory is null ? 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[^1] == "")
|
|
{
|
|
parentSegments = parentSegments[..^1];
|
|
}
|
|
|
|
var childSegments = fullChild.Split(separator, StringSplitOptions.None).AsSpan();
|
|
if (childSegments[^1] == "")
|
|
{
|
|
childSegments = childSegments[..^1];
|
|
}
|
|
|
|
if (parentSegments.Length >= childSegments.Length)
|
|
return false;
|
|
|
|
return childSegments[..parentSegments.Length].SequenceEqual(parentSegments);
|
|
}
|
|
}
|
|
}
|