Adds a season-agnostic fallback to subtitle-episode matching so dropped subtitle files no longer need a full SxxEyy code to be attached to the right video. Recognises bare numbers (06, 6, 16), explicit markers without a season (E06, EP06, Episode 06), and numbers cleanly delimited by separators, including the common anime release style 'Show - 06 - Title'. Guards against false positives from resolutions (1080p), years, and decimal-looking audio tags (DD5.1) by requiring clean separator boundaries and capping bare numbers at a few digits. Full season+episode codes still take priority and match exactly as before; the new bare-number matching only kicks in when one side of the match lacks season info. Note: dotnet is not reachable in this environment, so the required Release build validation could not be run before this commit.
179 lines
6.1 KiB
C#
179 lines
6.1 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using Futonizer.Services;
|
|
|
|
namespace Futonizer.Models;
|
|
|
|
/// <summary>
|
|
/// The four states shown as an icon in the file queue list.
|
|
/// </summary>
|
|
public enum FileLoadState
|
|
{
|
|
/// <summary>Currently being scanned with mkvmerge -J.</summary>
|
|
Loading,
|
|
/// <summary>Scan finished (or failed) but track selection isn't evaluated as correct/incorrect yet.</summary>
|
|
Loaded,
|
|
/// <summary>Exactly one audio track and one subtitle track are selected to be kept.</summary>
|
|
Correct,
|
|
/// <summary>Anything other than exactly one audio + one subtitle selected.</summary>
|
|
Incorrect,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents one MKV file in the processing queue.
|
|
/// </summary>
|
|
public class QueuedFileItem : INotifyPropertyChanged
|
|
{
|
|
private string _status = string.Empty;
|
|
private bool _isScanning;
|
|
private bool _hasError;
|
|
|
|
public string FilePath { get; }
|
|
public string FileName => Path.GetFileName(FilePath);
|
|
|
|
/// <summary>
|
|
/// The filename as it will appear after rename adjustments (release-tag prefix
|
|
/// and/or show-name injection). Equals <see cref="FileName"/> when no
|
|
/// changes would be applied.
|
|
/// </summary>
|
|
public string DisplayFileName => FileNameHelper.ComputeOutputFileName(FilePath);
|
|
public string FileSizeDisplay { get; }
|
|
public ObservableCollection<TrackItem> Tracks { get; } = new();
|
|
|
|
/// <summary>
|
|
/// External subtitle files (dropped separately and matched to this file by
|
|
/// episode — e.g. "S04E06", or just a bare episode number like "06") that
|
|
/// will be muxed in as additional subtitle tracks when this file is
|
|
/// processed.
|
|
/// </summary>
|
|
public ObservableCollection<ExternalSubtitleTrack> ExternalSubtitles { get; } = new();
|
|
|
|
public bool HasExternalSubtitles => ExternalSubtitles.Count > 0;
|
|
|
|
public string ExternalSubtitlesTooltip => ExternalSubtitles.Count == 0
|
|
? string.Empty
|
|
: "External subtitles to mux in:\n" + string.Join("\n", ExternalSubtitles.Select(s => $"- {s.FileName} ({s.LanguageDisplay})"));
|
|
|
|
public string Status
|
|
{
|
|
get => _status;
|
|
set { _status = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
public bool IsScanning
|
|
{
|
|
get => _isScanning;
|
|
set { _isScanning = value; OnPropertyChanged(); NotifyLoadState(); }
|
|
}
|
|
|
|
public bool HasError
|
|
{
|
|
get => _hasError;
|
|
set { _hasError = value; OnPropertyChanged(); NotifyLoadState(); }
|
|
}
|
|
|
|
public FileLoadState LoadState
|
|
{
|
|
get
|
|
{
|
|
if (_isScanning) return FileLoadState.Loading;
|
|
if (_hasError || Tracks.Count == 0) return FileLoadState.Loaded;
|
|
|
|
int audioSelected = Tracks.Count(t => t.Type == "audio" && t.Copy);
|
|
int subtitleSelected = Tracks.Count(t => t.Type == "subtitles" && t.Copy);
|
|
|
|
// A subtitle requirement is met either by picking exactly one
|
|
// internal track, or by having at least one matched external
|
|
// subtitle file (which can also be combined with one internal track).
|
|
bool subtitleOk = subtitleSelected == 1 || (subtitleSelected == 0 && ExternalSubtitles.Count > 0);
|
|
|
|
return audioSelected == 1 && subtitleOk
|
|
? FileLoadState.Correct
|
|
: FileLoadState.Incorrect;
|
|
}
|
|
}
|
|
|
|
public bool IsLoading => LoadState == FileLoadState.Loading;
|
|
public bool IsLoadedNeutral => LoadState == FileLoadState.Loaded;
|
|
public bool IsCorrect => LoadState == FileLoadState.Correct;
|
|
public bool IsIncorrect => LoadState == FileLoadState.Incorrect;
|
|
|
|
public QueuedFileItem(string filePath)
|
|
{
|
|
FilePath = filePath;
|
|
FileSizeDisplay = FormatFileSize(filePath);
|
|
Tracks.CollectionChanged += OnTracksChanged;
|
|
ExternalSubtitles.CollectionChanged += OnExternalSubtitlesChanged;
|
|
}
|
|
|
|
private static string FormatFileSize(string filePath)
|
|
{
|
|
try
|
|
{
|
|
long bytes = new FileInfo(filePath).Length;
|
|
string[] units = { "B", "KB", "MB", "GB", "TB" };
|
|
double size = bytes;
|
|
int unitIndex = 0;
|
|
while (size >= 1024 && unitIndex < units.Length - 1)
|
|
{
|
|
size /= 1024;
|
|
unitIndex++;
|
|
}
|
|
return unitIndex == 0
|
|
? $"{size:0} {units[unitIndex]}"
|
|
: $"{size:0.#} {units[unitIndex]}";
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private void OnExternalSubtitlesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
OnPropertyChanged(nameof(HasExternalSubtitles));
|
|
OnPropertyChanged(nameof(ExternalSubtitlesTooltip));
|
|
NotifyLoadState();
|
|
}
|
|
|
|
private void OnTracksChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.NewItems != null)
|
|
{
|
|
foreach (TrackItem t in e.NewItems)
|
|
t.PropertyChanged += TrackPropertyChanged;
|
|
}
|
|
|
|
if (e.OldItems != null)
|
|
{
|
|
foreach (TrackItem t in e.OldItems)
|
|
t.PropertyChanged -= TrackPropertyChanged;
|
|
}
|
|
|
|
NotifyLoadState();
|
|
}
|
|
|
|
private void TrackPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(TrackItem.Copy))
|
|
NotifyLoadState();
|
|
}
|
|
|
|
private void NotifyLoadState()
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LoadState)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoading)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoadedNeutral)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsCorrect)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsIncorrect)));
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
}
|