From 02ce7bb8720e4d30339da2b4a138cba00c45891f Mon Sep 17 00:00:00 2001 From: l4kr Date: Sun, 5 Jul 2026 16:42:55 +0200 Subject: [PATCH] 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 --- Futonizer.csproj | 4 +- MainWindow.xaml | 11 ++- MainWindow.xaml.cs | 11 +++ Models/QueuedFileItem.cs | 8 +++ Services/FileNameHelper.cs | 132 +++++++++++++++++++++++++++++++++++ Views/ProgressWindow.xaml.cs | 34 ++------- 6 files changed, 170 insertions(+), 30 deletions(-) create mode 100644 Services/FileNameHelper.cs 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 {