Bump version to 1.0.3

- Suffix release tag support: folder name ending in -TAG
  prepends [TAG] to output filenames
- Show name injection from parent folder for episode-only
  filenames (e.g. S01E01-Name.mkv)
- Queue and logs display predicted output filename
- Run button label reflects rename when applicable
- Gray out main UI while strip/rename is in progress
This commit is contained in:
2026-07-05 16:42:55 +02:00
parent e61cb5e709
commit 02ce7bb872
6 changed files with 170 additions and 30 deletions
+132
View File
@@ -0,0 +1,132 @@
using System.IO;
using System.Text.RegularExpressions;
namespace Futonizer.Services;
/// <summary>
/// Pure static helpers for computing the final output filename from a source
/// file path, applying release-tag prefixing and show-name injection.
/// </summary>
public static class FileNameHelper
{
// Suffix release tag: "Show Name-TTGA" → tag = "TTGA"
// Must be 2-8 uppercase letters/digits at the very end, preceded by a dash.
private static readonly Regex SuffixTagRegex =
new(@"-([A-Z][A-Z0-9]{1,7})$", RegexOptions.Compiled);
// Prefix release tag: "[KAA] Show Name" → tag = "KAA"
private static readonly Regex PrefixTagRegex =
new(@"^\[(.+?)\]", RegexOptions.Compiled);
// Episode-only filename: starts with S01E01, S1E1, etc. (no show name present)
private static readonly Regex EpisodeOnlyRegex =
new(@"^S\d+E\d+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Individual metadata-token patterns used to find where the show name ends
// inside a folder name.
private static readonly Regex[] MetadataPatterns =
[
new(@"^S\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // S01
new(@"^S\d+E\d+", RegexOptions.IgnoreCase | RegexOptions.Compiled), // S01E01...
new(@"^E[Pp]?\d+$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // E01, EP01
new(@"^\d{3,4}[pP]$", RegexOptions.Compiled), // 1080p, 720p
new(@"^(4K|UHD|2160p)$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // 4K, UHD
new(@"^(BD|BluRay|Blu-Ray|WEB|WEB-DL|WEBRip|HDTV|AMZN|NF|DSNP|HULU|MAX)$",RegexOptions.IgnoreCase | RegexOptions.Compiled), // sources
new(@"^(Remux|Encode)$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // processing
new(@"^(HEVC|AVC|x264|x265|H\.?264|H\.?265|VP9)$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // video codecs
new(@"^(FLAC|AAC|DTS|TrueHD|Atmos|AC3|EAC3|Opus|MP3)$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // audio codecs
new(@"^(HDR|SDR|10bit|8bit|HDR10|DV|DoVi)$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // HDR / bit-depth
];
// -------------------------------------------------------------------------
/// <summary>
/// Computes the final output filename for a given source file path, applying
/// (in order):
/// <list type="number">
/// <item>Show-name injection — if the filename starts with an episode code
/// (e.g. S01E01) the show name is derived from the parent folder and
/// prepended.</item>
/// <item>Release-tag prefix — a <c>[TAG]</c> bracket prefix derived from
/// the parent folder name, either from a suffix pattern (<c>-TAG</c>)
/// or a prefix pattern (<c>[TAG]</c>).</item>
/// </list>
/// Returns the original filename unchanged when the parent folder provides
/// no actionable information.
/// </summary>
public static string ComputeOutputFileName(string sourceFilePath)
{
string fileName = Path.GetFileName(sourceFilePath);
string? folder = Path.GetDirectoryName(sourceFilePath);
if (string.IsNullOrEmpty(folder)) return fileName;
string folderName = Path.GetFileName(folder);
if (string.IsNullOrEmpty(folderName)) return fileName;
// 1. Inject show name when only an episode code is present.
string fileNameNoExt = Path.GetFileNameWithoutExtension(fileName);
if (EpisodeOnlyRegex.IsMatch(fileNameNoExt))
{
string showName = ExtractShowName(folderName);
if (!string.IsNullOrEmpty(showName))
fileName = showName + " " + fileName;
}
// 2. Prepend release tag if not already present.
string? tag = ExtractReleaseTag(folderName);
if (!string.IsNullOrEmpty(tag))
{
string tagPrefix = $"[{tag}]";
if (!fileName.StartsWith(tagPrefix, StringComparison.OrdinalIgnoreCase))
fileName = tagPrefix + " " + fileName;
}
return fileName;
}
/// <summary>
/// Extracts the release tag string (without brackets) from a folder name.
/// Suffix pattern (<c>-TAG</c>) takes priority over prefix pattern
/// (<c>[TAG]</c>). Returns <c>null</c> when neither is present.
/// </summary>
public static string? ExtractReleaseTag(string folderName)
{
var suffixMatch = SuffixTagRegex.Match(folderName);
if (suffixMatch.Success) return suffixMatch.Groups[1].Value;
var prefixMatch = PrefixTagRegex.Match(folderName);
if (prefixMatch.Success) return prefixMatch.Groups[1].Value;
return null;
}
/// <summary>
/// Derives the show name from a folder name by stripping any release tag
/// (prefix or suffix) and then taking all words that precede the first
/// recognised metadata token (season, quality, source, codec, etc.).
/// </summary>
public static string ExtractShowName(string folderName)
{
// Strip release tags first.
string name = SuffixTagRegex.Replace(folderName, "").Trim();
name = PrefixTagRegex.Replace(name, "").Trim();
// Collect words up to the first metadata token.
var words = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var showWords = new List<string>();
foreach (var word in words)
{
if (IsMetadataToken(word)) break;
showWords.Add(word);
}
return string.Join(" ", showWords).Trim();
}
private static bool IsMetadataToken(string token)
{
foreach (var pattern in MetadataPatterns)
if (pattern.IsMatch(token)) return true;
return false;
}
}