Files
ADOFAIRenderer/Util/RenderFileNameUtil.cs

193 lines
6.6 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using ADOFAIRenderer.Core.Data;
using ADOFAIRenderer.Data;
namespace ADOFAIRenderer.Util;
/// <summary>
/// Formats and sanitizes render output file names according to customizable template patterns.
/// </summary>
internal static class RenderFileNameUtil
{
private static readonly Regex RichTextRegex = new(@"<[^>]+>", RegexOptions.Compiled);
private static readonly Regex TokenRegex = new(@"\{([a-zA-Z0-9_]+)\}", RegexOptions.Compiled);
private static readonly HashSet<string> ReservedWindowsNames = new(StringComparer.OrdinalIgnoreCase)
{
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
};
/// <summary>
/// Removes Unity rich text tags (such as &lt;color=...&gt;, &lt;b&gt;, etc.) from the given text.
/// </summary>
public static string StripRichText(string? input)
{
if (string.IsNullOrEmpty(input)) return string.Empty;
return RichTextRegex.Replace(input, string.Empty);
}
/// <summary>
/// Replaces invalid file name characters within a single token with an underscore.
/// </summary>
public static string SanitizeToken(string? token)
{
if (string.IsNullOrEmpty(token)) return string.Empty;
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(token.Length);
foreach (var c in token)
{
sb.Append(invalidChars.Contains(c) ? '_' : c);
}
return sb.ToString().Trim(' ', '.');
}
/// <summary>
/// Sanitizes the final formatted file name, ensuring no illegal characters or Windows reserved device names.
/// </summary>
public static string SanitizeFileName(string? fileName)
{
if (string.IsNullOrWhiteSpace(fileName)) return string.Empty;
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(invalidChars.Contains(c) ? '_' : c);
}
var result = sb.ToString().Trim(' ', '.');
if (ReservedWindowsNames.Contains(result))
{
result = "_" + result;
}
return result;
}
/// <summary>
/// Formats a file name without extension by substituting template variables.
/// Available tokens: {song}, {title}, {artist}, {author}, {level}, {preset},
/// {date}, {time}, {year}, {month}, {day}, {hour}, {minute}, {second},
/// {resolution}, {width}, {height}, {fps}.
/// </summary>
public static string FormatFileName(
string? formatTemplate,
DateTime startTime,
RenderConfig config,
string? presetName = null,
string? levelPath = null)
{
var scn = scnGame.instance;
string rawSong = "";
string rawArtist = "";
string rawAuthor = "";
if (scn != null && scn.levelData != null)
{
rawSong = scn.levelData.song ?? "";
rawArtist = scn.levelData.artist ?? "";
rawAuthor = scn.levelData.author ?? "";
}
if (string.IsNullOrWhiteSpace(rawSong) && !string.IsNullOrWhiteSpace(levelPath))
{
var dir = Path.GetDirectoryName(levelPath);
rawSong = !string.IsNullOrWhiteSpace(dir)
? Path.GetFileName(dir)
: Path.GetFileNameWithoutExtension(levelPath);
}
var song = SanitizeToken(StripRichText(rawSong));
var artist = SanitizeToken(StripRichText(rawArtist));
var author = SanitizeToken(StripRichText(rawAuthor));
var level = !string.IsNullOrWhiteSpace(levelPath)
? SanitizeToken(Path.GetFileNameWithoutExtension(levelPath))
: song;
var preset = SanitizeToken(presetName ?? "");
var date = startTime.ToString("yyyy-MM-dd");
var time = startTime.ToString("HH-mm-ss");
var year = startTime.ToString("yyyy");
var month = startTime.ToString("MM");
var day = startTime.ToString("dd");
var hour = startTime.ToString("HH");
var minute = startTime.ToString("mm");
var second = startTime.ToString("ss");
var width = config.RenderWidth.ToString();
var height = config.RenderHeight.ToString();
var resolution = $"{config.RenderWidth}x{config.RenderHeight}";
var effectiveFps = Math.Min(config.VideoFPS, config.RenderFPS);
var fps = Math.Abs(effectiveFps % 1) < 0.001
? ((int)effectiveFps).ToString()
: effectiveFps.ToString("0.##");
var tokens = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "song", song },
{ "title", song },
{ "artist", artist },
{ "author", author },
{ "level", level },
{ "preset", preset },
{ "date", date },
{ "time", time },
{ "year", year },
{ "month", month },
{ "day", day },
{ "hour", hour },
{ "minute", minute },
{ "second", second },
{ "width", width },
{ "height", height },
{ "resolution", resolution },
{ "fps", fps }
};
var template = string.IsNullOrWhiteSpace(formatTemplate)
? GlobalSettings.DefaultFileNameFormat
: formatTemplate;
var replaced = TokenRegex.Replace(template, match =>
{
var key = match.Groups[1].Value;
return tokens.TryGetValue(key, out var val) ? val : match.Value;
});
var sanitized = SanitizeFileName(replaced);
return string.IsNullOrWhiteSpace(sanitized)
? startTime.ToString("yyyy-MM-dd HH-mm-ss")
: sanitized;
}
/// <summary>
/// Returns a collision-free output file path by appending (1), (2), etc. if a file with the given name already exists.
/// </summary>
public static string GetUniqueFilePath(string directory, string fileNameWithoutExt, string ext)
{
var normalizedExt = "." + ext.TrimStart('.').ToLowerInvariant();
var fileName = $"{fileNameWithoutExt}{normalizedExt}";
var fullPath = Path.Combine(directory, fileName);
var counter = 1;
while (File.Exists(fullPath))
{
fileName = $"{fileNameWithoutExt} ({counter}){normalizedExt}";
fullPath = Path.Combine(directory, fileName);
counter++;
}
return fullPath;
}
}