diff --git a/Futonizer.csproj b/Futonizer.csproj
index 335c131..57af9dd 100644
--- a/Futonizer.csproj
+++ b/Futonizer.csproj
@@ -9,8 +9,8 @@
enable
Futonizer
Futonizer
- 1.0.2
- 1.0.2.0
+ 1.0.3
+ 1.0.3.0
Assets\app.ico
diff --git a/MainWindow.xaml b/MainWindow.xaml
index ccc8770..3ba68d5 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -175,7 +175,7 @@
Foreground="#F44336" VerticalAlignment="Center"
Visibility="{Binding IsIncorrect, Converter={StaticResource BoolToVis}}"/>
-
@@ -331,6 +331,15 @@
+
+
+
+ !string.Equals(f.DisplayFileName, f.FileName, StringComparison.Ordinal));
+ RunButton.Content = anyRename ? "Rename and Strip Tracks" : "Strip Tracks";
}
private void QueuedFileItem_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -500,6 +509,7 @@ public partial class MainWindow : FluentWindow
_isProcessing = true;
RunButton.IsEnabled = false;
StatusText.Text = string.Empty;
+ ProcessingOverlay.Visibility = Visibility.Visible;
var progressWin = new ProgressWindow(readyFiles, _settings.MkvMergePath, _settings.OutputFolder, MaxConcurrentStrips)
{
@@ -508,6 +518,7 @@ public partial class MainWindow : FluentWindow
progressWin.Closed += (_, _) =>
{
_isProcessing = false;
+ ProcessingOverlay.Visibility = Visibility.Collapsed;
if (progressWin.ShouldClearQueue)
{
foreach (var item in _fileQueue)
diff --git a/Models/QueuedFileItem.cs b/Models/QueuedFileItem.cs
index ec90669..cf21fb6 100644
--- a/Models/QueuedFileItem.cs
+++ b/Models/QueuedFileItem.cs
@@ -3,6 +3,7 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
+using Futonizer.Services;
namespace Futonizer.Models;
@@ -32,6 +33,13 @@ public class QueuedFileItem : INotifyPropertyChanged
public string FilePath { get; }
public string FileName => Path.GetFileName(FilePath);
+
+ ///
+ /// The filename as it will appear after rename adjustments (release-tag prefix
+ /// and/or show-name injection). Equals when no
+ /// changes would be applied.
+ ///
+ public string DisplayFileName => FileNameHelper.ComputeOutputFileName(FilePath);
public string FileSizeDisplay { get; }
public ObservableCollection Tracks { get; } = new();
diff --git a/Services/FileNameHelper.cs b/Services/FileNameHelper.cs
new file mode 100644
index 0000000..60e4f28
--- /dev/null
+++ b/Services/FileNameHelper.cs
@@ -0,0 +1,132 @@
+using System.IO;
+using System.Text.RegularExpressions;
+
+namespace Futonizer.Services;
+
+///
+/// Pure static helpers for computing the final output filename from a source
+/// file path, applying release-tag prefixing and show-name injection.
+///
+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
+ ];
+
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Computes the final output filename for a given source file path, applying
+ /// (in order):
+ ///
+ /// - 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.
+ /// - Release-tag prefix — a [TAG] bracket prefix derived from
+ /// the parent folder name, either from a suffix pattern (-TAG)
+ /// or a prefix pattern ([TAG]).
+ ///
+ /// Returns the original filename unchanged when the parent folder provides
+ /// no actionable information.
+ ///
+ 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;
+ }
+
+ ///
+ /// Extracts the release tag string (without brackets) from a folder name.
+ /// Suffix pattern (-TAG) takes priority over prefix pattern
+ /// ([TAG]). Returns null when neither is present.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.).
+ ///
+ 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();
+ 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;
+ }
+}
diff --git a/Views/ProgressWindow.xaml.cs b/Views/ProgressWindow.xaml.cs
index 4b4a22a..a509f57 100644
--- a/Views/ProgressWindow.xaml.cs
+++ b/Views/ProgressWindow.xaml.cs
@@ -1,7 +1,6 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
-using System.Text.RegularExpressions;
using System.Windows;
using Futonizer.Models;
using Futonizer.Services;
@@ -111,28 +110,6 @@ public partial class ProgressWindow : FluentWindow
TotalBytesText.Text = $"{doneMb:F0} / {totalMb:F0} MB copied";
}
- ///
- /// If the source file's parent folder starts with a bracketed tag
- /// (e.g. "[KAA]" or "[neoHEVC]") and the file name doesn't already
- /// start with that tag, returns the file name with the tag prepended
- /// (e.g. "[KAA] episode.mkv"). Otherwise returns the file name unchanged.
- /// Only applied to the output file — the source is never touched.
- ///
- private static string PrefixedFileName(string sourceFilePath, string fileName)
- {
- string? folder = Path.GetDirectoryName(sourceFilePath);
- if (string.IsNullOrEmpty(folder)) return fileName;
-
- string folderName = Path.GetFileName(folder);
- var match = Regex.Match(folderName, @"^\[.+?\]");
- if (!match.Success) return fileName;
-
- string prefix = match.Value + " "; // e.g. "[KAA] "
- if (fileName.StartsWith(match.Value, StringComparison.Ordinal)) return fileName;
-
- return prefix + fileName;
- }
-
private static long SafeFileLength(string path)
{
try { return new FileInfo(path).Length; }
@@ -167,7 +144,8 @@ public partial class ProgressWindow : FluentWindow
{
await semaphore.WaitAsync(ct);
- var job = new JobProgressItem { FileName = file.FileName, FileSizeBytes = SafeFileLength(file.FilePath) };
+ string computedFileName = FileNameHelper.ComputeOutputFileName(file.FilePath);
+ var job = new JobProgressItem { FileName = computedFileName, FileSizeBytes = SafeFileLength(file.FilePath) };
job.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(JobProgressItem.MegabytesPerSecond))
@@ -180,9 +158,11 @@ public partial class ProgressWindow : FluentWindow
try
{
+ string sourceDir = Path.GetDirectoryName(file.FilePath) ?? "";
+
string outputFilePath = overwritingInPlace
- ? file.FilePath
- : Path.Combine(_outputFolder, PrefixedFileName(file.FilePath, file.FileName));
+ ? Path.Combine(sourceDir, computedFileName)
+ : Path.Combine(_outputFolder, computedFileName);
var videoIds = file.Tracks
.Where(t => t.Type == "video" && t.Copy)
@@ -210,7 +190,7 @@ public partial class ProgressWindow : FluentWindow
progress, ct);
string tag = result.Success ? "[DONE]" : "[FAIL]";
- AppendLog($"{tag} {result.FileName} — {result.Message}");
+ AppendLog($"{tag} {Path.GetFileName(result.OutputFilePath)} — {result.Message}");
HistoryService.Add(new HistoryEntry
{