diff --git a/src/MatroskaBatchFlow.Core/Models/FileTrackConfiguration.cs b/src/MatroskaBatchFlow.Core/Models/FileTrackConfiguration.cs
index 25771ae..fe53338 100644
--- a/src/MatroskaBatchFlow.Core/Models/FileTrackConfiguration.cs
+++ b/src/MatroskaBatchFlow.Core/Models/FileTrackConfiguration.cs
@@ -1,6 +1,5 @@
using System.Collections.ObjectModel;
using MatroskaBatchFlow.Core.Enums;
-using MatroskaBatchFlow.Core.Services;
namespace MatroskaBatchFlow.Core.Models;
@@ -15,26 +14,26 @@ public sealed class FileTrackConfiguration
public string FilePath { get; set; } = string.Empty;
///
- /// Audio track configurations for this file.
+ /// Audio track values for this file.
///
- public ObservableCollection AudioTracks { get; set; } = [];
+ public ObservableCollection AudioTracks { get; set; } = [];
///
- /// Video track configurations for this file.
+ /// Video track values for this file.
///
- public ObservableCollection VideoTracks { get; set; } = [];
+ public ObservableCollection VideoTracks { get; set; } = [];
///
- /// Subtitle track configurations for this file.
+ /// Subtitle track values for this file.
///
- public ObservableCollection SubtitleTracks { get; set; } = [];
+ public ObservableCollection SubtitleTracks { get; set; } = [];
///
/// Gets the track list for a specific track type.
///
/// The track type to retrieve.
- /// Observable collection of track configurations for the specified type.
- public ObservableCollection GetTrackListForType(TrackType trackType)
+ /// Observable collection of track values for the specified type.
+ public ObservableCollection GetTrackListForType(TrackType trackType)
{
return trackType switch
{
diff --git a/src/MatroskaBatchFlow.Core/Models/FileTrackValues.cs b/src/MatroskaBatchFlow.Core/Models/FileTrackValues.cs
new file mode 100644
index 0000000..90a6fd1
--- /dev/null
+++ b/src/MatroskaBatchFlow.Core/Models/FileTrackValues.cs
@@ -0,0 +1,68 @@
+using MatroskaBatchFlow.Core.Enums;
+
+namespace MatroskaBatchFlow.Core.Models;
+
+///
+/// Stores per-file track values that are initially populated from the MediaInfo scan.
+/// The original scanned data is always available via .
+///
+///
+/// The batch configuration uses a dual-model approach for tracks:
+///
+/// -
+/// - one per track index in the global collection.
+/// Carries modification intent (ShouldModify* flags) and the effective values
+/// (Name, Language, flags) that should be written, and is shared across all files.
+///
+/// -
+/// - one per track per file in .
+/// Represents the scanned/current per-file values and indicates whether a given file actually
+/// has a track at a particular index.
+///
+///
+/// During command generation, reads both
+/// ShouldModify* and the values to write from the global track configuration, and uses
+/// only to determine per-file track existence and indexing.
+///
+public sealed class FileTrackValues
+{
+ ///
+ /// The raw track information as returned by MediaInfo.
+ ///
+ public required MediaInfoResult.MediaInfo.TrackInfo ScannedTrackInfo { get; init; }
+
+ ///
+ /// The type of this track (Audio, Video, Text).
+ ///
+ public TrackType Type { get; init; }
+
+ ///
+ /// Zero-based index of this track within its type (e.g. 0 = first audio track).
+ ///
+ public int Index { get; init; }
+
+ ///
+ /// The track name as scanned from the file.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// The track language as scanned from the file.
+ ///
+ public MatroskaLanguageOption Language { get; set; } = MatroskaLanguageOption.Undetermined;
+
+ ///
+ /// Whether this track has the default flag set.
+ ///
+ public bool Default { get; set; }
+
+ ///
+ /// Whether this track has the forced flag set.
+ ///
+ public bool Forced { get; set; }
+
+ ///
+ /// Whether this track is enabled (corresponds to the Matroska FlagEnabled element).
+ ///
+ public bool Enabled { get; set; }
+}
diff --git a/src/MatroskaBatchFlow.Core/Services/BatchConfiguration.cs b/src/MatroskaBatchFlow.Core/Services/BatchConfiguration.cs
index f144db7..e032a6c 100644
--- a/src/MatroskaBatchFlow.Core/Services/BatchConfiguration.cs
+++ b/src/MatroskaBatchFlow.Core/Services/BatchConfiguration.cs
@@ -321,7 +321,7 @@ public IList GetTrackListForType(TrackType trackType)
///
/// Thrown if no per-file track configuration exists for the specified file ID.
- public IList GetTrackListForFile(Guid fileId, TrackType trackType)
+ public IList GetTrackListForFile(Guid fileId, TrackType trackType)
{
// Prefer per-file configuration. If none exists, fail fast by throwing an exception.
if (FileConfigurations.TryGetValue(fileId, out var fileConfig))
@@ -656,7 +656,7 @@ public bool ShouldModifyEnabledFlag
}
}
- protected void OnPropertyChanged(string propertyName)
+ private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
diff --git a/src/MatroskaBatchFlow.Core/Services/BatchTrackConfigurationInitializer.cs b/src/MatroskaBatchFlow.Core/Services/BatchTrackConfigurationInitializer.cs
index 6406f63..e21b06b 100644
--- a/src/MatroskaBatchFlow.Core/Services/BatchTrackConfigurationInitializer.cs
+++ b/src/MatroskaBatchFlow.Core/Services/BatchTrackConfigurationInitializer.cs
@@ -7,18 +7,20 @@ namespace MatroskaBatchFlow.Core.Services;
/// Initializes per-file track configurations based on scanned file information.
///
///
-/// This class orchestrates the creation of individual track configurations for each file:
+/// This class orchestrates track initialization for each file:
///
-/// - Delegates track configuration creation to
+/// - Creates per-file directly from scanned track metadata
+/// - Delegates global creation to when expanding to match maximum track counts
/// - Populates file-specific track lists in
-/// - Updates global track collections to reflect maximum track counts for UI display
///
///
/// The batch configuration to be modified.
-/// The factory for creating track configurations.
+/// The factory for creating global track configurations.
+/// The language provider for resolving track language codes.
public class BatchTrackConfigurationInitializer(
IBatchConfiguration batchConfig,
- ITrackConfigurationFactory trackConfigFactory) : IBatchTrackConfigurationInitializer
+ ITrackConfigurationFactory trackConfigFactory,
+ ILanguageProvider languageProvider) : IBatchTrackConfigurationInitializer
{
///
public void Initialize(ScannedFileInfo scannedFile, params TrackType[] trackTypes)
@@ -36,7 +38,7 @@ public void Initialize(ScannedFileInfo scannedFile, params TrackType[] trackType
batchConfig.FileConfigurations.Add(scannedFile.Id, fileConfig);
}
- // Populate file-specific track configurations based on what this file has
+ // Populate file-specific track values based on what this file has
foreach (var trackType in trackTypes)
{
// Ordering tracks by StreamKindID as it represents the track order in the file
@@ -52,7 +54,17 @@ public void Initialize(ScannedFileInfo scannedFile, params TrackType[] trackType
for (int i = existingCount; i < scannedCount; i++)
{
- fileTracks.Add(trackConfigFactory.Create(scannedTracks[i], trackType, i));
+ var scannedTrack = scannedTracks[i];
+ fileTracks.Add(new FileTrackValues
+ {
+ ScannedTrackInfo = scannedTrack,
+ Type = trackType,
+ Index = i,
+ Name = scannedTrack.Title ?? string.Empty,
+ Language = languageProvider.Resolve(scannedTrack.Language),
+ Default = scannedTrack.Default,
+ Forced = scannedTrack.Forced,
+ });
}
}
diff --git a/src/MatroskaBatchFlow.Core/Services/IBatchConfiguration.cs b/src/MatroskaBatchFlow.Core/Services/IBatchConfiguration.cs
index e497798..96155fe 100644
--- a/src/MatroskaBatchFlow.Core/Services/IBatchConfiguration.cs
+++ b/src/MatroskaBatchFlow.Core/Services/IBatchConfiguration.cs
@@ -9,8 +9,9 @@ namespace MatroskaBatchFlow.Core.Services;
///
/// Defines the contract for batch configuration of media files.
///
-/// Note: The TrackConfiguration items in the collections also implement INotifyPropertyChanged,
-/// so property changes within tracks can be observed.
+/// Note: The TrackConfiguration items in the global collections implement INotifyPropertyChanged,
+/// so property changes within global tracks can be observed.
+/// Per-file track values are stored as which carry no modification intent.
///
public interface IBatchConfiguration : INotifyPropertyChanged
{
@@ -51,13 +52,14 @@ public interface IBatchConfiguration : INotifyPropertyChanged
public IList GetTrackListForType(TrackType trackType);
///
- /// Gets track configuration for a specific file and track type.
- /// Always uses per-file configurations. Falls back to global if file config not found.
+ /// Gets the per-file track values for a specific file and track type.
+ /// These carry only scanned values (Name, Language, flags) — no modification intent.
+ /// Modification flags (ShouldModify*) live on the global objects returned by .
///
/// The ScannedFileInfo.Id (Guid) of the file.
/// Type of track to retrieve.
- /// List of track configurations for the specified file and track type.
- public IList GetTrackListForFile(Guid fileId, TrackType trackType);
+ /// List of per-file track values for the specified file and track type.
+ public IList GetTrackListForFile(Guid fileId, TrackType trackType);
///
/// Migrates file configuration from an old file ID to a new file ID.
diff --git a/src/MatroskaBatchFlow.Core/Services/ILanguageProvider.cs b/src/MatroskaBatchFlow.Core/Services/ILanguageProvider.cs
index 9deb1a3..1d387f5 100644
--- a/src/MatroskaBatchFlow.Core/Services/ILanguageProvider.cs
+++ b/src/MatroskaBatchFlow.Core/Services/ILanguageProvider.cs
@@ -8,4 +8,11 @@ public interface ILanguageProvider
public ImmutableList Languages { get; }
public void LoadLanguages();
+
+ ///
+ /// Resolves a raw language string from MediaInfo into a typed .
+ ///
+ /// The language code (e.g., "en", "eng", or "English").
+ /// The matching language option, or if no match found.
+ MatroskaLanguageOption Resolve(string? languageCode);
}
diff --git a/src/MatroskaBatchFlow.Core/Services/LanguageProvider.cs b/src/MatroskaBatchFlow.Core/Services/LanguageProvider.cs
index dd7ec3e..7fc4fef 100644
--- a/src/MatroskaBatchFlow.Core/Services/LanguageProvider.cs
+++ b/src/MatroskaBatchFlow.Core/Services/LanguageProvider.cs
@@ -66,4 +66,21 @@ public void LoadLanguages()
Languages = [];
}
}
+
+ ///
+ public MatroskaLanguageOption Resolve(string? languageCode)
+ {
+ if (string.IsNullOrWhiteSpace(languageCode))
+ return MatroskaLanguageOption.Undetermined;
+
+ var matchedLanguage = Languages.FirstOrDefault(lang =>
+ string.Equals(lang.Iso639_2_b, languageCode, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(lang.Iso639_2_t, languageCode, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(lang.Iso639_1, languageCode, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(lang.Iso639_3, languageCode, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(lang.Name, languageCode, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(lang.Code, languageCode, StringComparison.OrdinalIgnoreCase));
+
+ return matchedLanguage ?? MatroskaLanguageOption.Undetermined;
+ }
}
diff --git a/src/MatroskaBatchFlow.Core/Services/MkvPropeditArgumentsGenerator.Logging.cs b/src/MatroskaBatchFlow.Core/Services/MkvPropeditArgumentsGenerator.Logging.cs
new file mode 100644
index 0000000..6c9e6f5
--- /dev/null
+++ b/src/MatroskaBatchFlow.Core/Services/MkvPropeditArgumentsGenerator.Logging.cs
@@ -0,0 +1,14 @@
+using MatroskaBatchFlow.Core.Enums;
+using Microsoft.Extensions.Logging;
+
+namespace MatroskaBatchFlow.Core.Services;
+
+///
+/// LoggerMessage definitions for .
+///
+public sealed partial class MkvPropeditArgumentsGenerator
+{
+ [LoggerMessage(Level = LogLevel.Warning,
+ Message = "Per-file {TrackType} track index {TrackIndex} exceeds global track count {GlobalTrackCount} for file: {FilePath}")]
+ private partial void LogPerFileTrackExceedsGlobalCount(string filePath, TrackType trackType, int trackIndex, int globalTrackCount);
+}
diff --git a/src/MatroskaBatchFlow.Core/Services/MkvPropeditArgumentsGenerator.cs b/src/MatroskaBatchFlow.Core/Services/MkvPropeditArgumentsGenerator.cs
index c136e8b..f6a038e 100644
--- a/src/MatroskaBatchFlow.Core/Services/MkvPropeditArgumentsGenerator.cs
+++ b/src/MatroskaBatchFlow.Core/Services/MkvPropeditArgumentsGenerator.cs
@@ -1,13 +1,14 @@
using MatroskaBatchFlow.Core.Builders.MkvPropeditArguments;
using MatroskaBatchFlow.Core.Enums;
using MatroskaBatchFlow.Core.Models;
+using Microsoft.Extensions.Logging;
namespace MatroskaBatchFlow.Core.Services;
///
/// Provides reusable logic for generating mkvpropedit command-line arguments from an .
///
-public sealed class MkvPropeditArgumentsGenerator : IMkvPropeditArgumentsGenerator
+public sealed partial class MkvPropeditArgumentsGenerator(ILogger logger) : IMkvPropeditArgumentsGenerator
{
///
public string[] BuildBatchArguments(IBatchConfiguration batchConfiguration)
@@ -42,7 +43,7 @@ public string BuildFileArgumentString(ScannedFileInfo file, IBatchConfiguration
/// Contains global title settings and per-file track configurations.
/// Token array suitable for joining. Returns an empty array if no modifications are requested
/// (to signal "no-op").
- private static string[] BuildFileArgumentTokens(ScannedFileInfo file, IBatchConfiguration batchConfiguration)
+ private string[] BuildFileArgumentTokens(ScannedFileInfo file, IBatchConfiguration batchConfiguration)
{
var builder = new MkvPropeditArgumentsBuilder();
@@ -84,11 +85,16 @@ private static string[] BuildFileArgumentTokens(ScannedFileInfo file, IBatchConf
/// Adds track-specific modifications to the builder for a specific file, filtering out tracks that have
/// no requested changes or don't exist in the file.
///
+ ///
+ /// Both the modification intent (ShouldModify*) and the actual values to write (Name, Language,
+ /// flags) are read from the global at the matching index. Per-file
+ /// are used only to determine which tracks exist in each file.
+ ///
/// The accumulating mkvpropedit argument builder.
/// The file being processed.
/// The track type (must map to a Matroska track element).
/// The batch configuration containing track availability data.
- private static void AddTracksForFile(
+ private void AddTracksForFile(
MkvPropeditArgumentsBuilder builder,
ScannedFileInfo file,
TrackType type,
@@ -99,17 +105,30 @@ private static void AddTracksForFile(
return;
}
- // Get file-specific track configuration.
- var tracks = batchConfig.GetTrackListForFile(file.Id, type);
+ // Per-file values (Name, Language, flags as scanned from this file).
+ var perFileTracks = batchConfig.GetTrackListForFile(file.Id, type);
- foreach (var track in tracks)
+ // Global tracks carry the modification intent (ShouldModify* flags).
+ var globalTracks = batchConfig.GetTrackListForType(type);
+
+ foreach (var track in perFileTracks)
{
+ // Defensive: the initializer always expands global tracks to the maximum
+ // count, so this should not be true under normal operation.
+ if (track.Index >= globalTracks.Count)
+ {
+ LogPerFileTrackExceedsGlobalCount(file.Path, type, track.Index, globalTracks.Count);
+ continue;
+ }
+
+ var globalTrack = globalTracks[track.Index];
+
// Skip inert tracks (no requested modifications).
- if (!(track.ShouldModifyLanguage ||
- track.ShouldModifyName ||
- track.ShouldModifyDefaultFlag ||
- track.ShouldModifyForcedFlag ||
- track.ShouldModifyEnabledFlag))
+ if (!(globalTrack.ShouldModifyLanguage ||
+ globalTrack.ShouldModifyName ||
+ globalTrack.ShouldModifyDefaultFlag ||
+ globalTrack.ShouldModifyForcedFlag ||
+ globalTrack.ShouldModifyEnabledFlag))
{
continue;
}
@@ -126,29 +145,29 @@ private static void AddTracksForFile(
// Track ID converted to 1-based indexing for mkvpropedit conventions.
tb.SetTrackId(track.Index + 1).SetTrackType(type);
- if (track.ShouldModifyLanguage)
+ if (globalTrack.ShouldModifyLanguage)
{
- tb.WithLanguage(track.Language.Code);
+ tb.WithLanguage(globalTrack.Language.Code);
}
- if (track.ShouldModifyName)
+ if (globalTrack.ShouldModifyName)
{
- tb.WithName(track.Name);
+ tb.WithName(globalTrack.Name);
}
- if (track.ShouldModifyDefaultFlag)
+ if (globalTrack.ShouldModifyDefaultFlag)
{
- tb.WithIsDefault(track.Default);
+ tb.WithIsDefault(globalTrack.Default);
}
- if (track.ShouldModifyForcedFlag)
+ if (globalTrack.ShouldModifyForcedFlag)
{
- tb.WithIsForced(track.Forced);
+ tb.WithIsForced(globalTrack.Forced);
}
- if (track.ShouldModifyEnabledFlag)
+ if (globalTrack.ShouldModifyEnabledFlag)
{
- tb.WithIsEnabled(track.Enabled);
+ tb.WithIsEnabled(globalTrack.Enabled);
}
return tb;
diff --git a/src/MatroskaBatchFlow.Core/Services/TrackConfigurationFactory.cs b/src/MatroskaBatchFlow.Core/Services/TrackConfigurationFactory.cs
index f7512a7..977cf42 100644
--- a/src/MatroskaBatchFlow.Core/Services/TrackConfigurationFactory.cs
+++ b/src/MatroskaBatchFlow.Core/Services/TrackConfigurationFactory.cs
@@ -22,34 +22,9 @@ public TrackConfiguration Create(
Type = trackType,
Index = index,
Name = scannedTrackInfo.Title ?? string.Empty,
- Language = ParseLanguageFromCode(scannedTrackInfo.Language),
+ Language = languageProvider.Resolve(scannedTrackInfo.Language),
Default = scannedTrackInfo.Default,
Forced = scannedTrackInfo.Forced
};
}
-
- ///
- /// Parses a language code from scanned track info and returns the matching .
- ///
- ///
- /// Performs case-insensitive comparison against ISO 639-1, ISO 639-2 (both bibliographic and terminologic),
- /// ISO 639-3 codes, the language name, and a custom code field.
- ///
- /// The language code from MediaInfo (e.g., "en", "eng", "jpn", or "English").
- /// The matching language option, or if no match found.
- private MatroskaLanguageOption ParseLanguageFromCode(string? languageCode)
- {
- if (string.IsNullOrWhiteSpace(languageCode))
- return MatroskaLanguageOption.Undetermined;
-
- var matchedLanguage = languageProvider.Languages.FirstOrDefault(lang =>
- string.Equals(lang.Iso639_2_b, languageCode, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(lang.Iso639_2_t, languageCode, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(lang.Iso639_1, languageCode, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(lang.Iso639_3, languageCode, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(lang.Name, languageCode, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(lang.Code, languageCode, StringComparison.OrdinalIgnoreCase));
-
- return matchedLanguage ?? MatroskaLanguageOption.Undetermined;
- }
}
diff --git a/src/MatroskaBatchFlow.Uno/Presentation/AudioViewModel.cs b/src/MatroskaBatchFlow.Uno/Presentation/AudioViewModel.cs
index e9e7eeb..721fc07 100644
--- a/src/MatroskaBatchFlow.Uno/Presentation/AudioViewModel.cs
+++ b/src/MatroskaBatchFlow.Uno/Presentation/AudioViewModel.cs
@@ -28,8 +28,8 @@ public ObservableCollection AudioTracks
///
public bool IsFileListPopulated => _batchConfiguration.FileList.Count > 0;
- public AudioViewModel(ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
- : base(languageProvider, batchConfiguration, uiPreferences)
+ public AudioViewModel(ILogger logger, ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
+ : base(logger, languageProvider, batchConfiguration, uiPreferences)
{
AudioTracks = [.. _batchConfiguration.AudioTracks];
diff --git a/src/MatroskaBatchFlow.Uno/Presentation/SubtitleViewModel.cs b/src/MatroskaBatchFlow.Uno/Presentation/SubtitleViewModel.cs
index 54395b9..bce831b 100644
--- a/src/MatroskaBatchFlow.Uno/Presentation/SubtitleViewModel.cs
+++ b/src/MatroskaBatchFlow.Uno/Presentation/SubtitleViewModel.cs
@@ -28,8 +28,8 @@ public ObservableCollection SubtitleTracks
///
public bool IsFileListPopulated => _batchConfiguration.FileList.Count > 0;
- public SubtitleViewModel(ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
- : base(languageProvider, batchConfiguration, uiPreferences)
+ public SubtitleViewModel(ILogger logger, ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
+ : base(logger, languageProvider, batchConfiguration, uiPreferences)
{
SubtitleTracks = [.. _batchConfiguration.SubtitleTracks];
diff --git a/src/MatroskaBatchFlow.Uno/Presentation/TrackViewModelBase.Logging.cs b/src/MatroskaBatchFlow.Uno/Presentation/TrackViewModelBase.Logging.cs
new file mode 100644
index 0000000..23312e4
--- /dev/null
+++ b/src/MatroskaBatchFlow.Uno/Presentation/TrackViewModelBase.Logging.cs
@@ -0,0 +1,10 @@
+namespace MatroskaBatchFlow.Uno.Presentation;
+
+///
+/// LoggerMessage definitions for .
+///
+public abstract partial class TrackViewModelBase
+{
+ [LoggerMessage(Level = LogLevel.Warning, Message = "SelectedLanguage was set to null; falling back to Undetermined.")]
+ private partial void LogSelectedLanguageReceivedNull();
+}
diff --git a/src/MatroskaBatchFlow.Uno/Presentation/TrackViewModelBase.cs b/src/MatroskaBatchFlow.Uno/Presentation/TrackViewModelBase.cs
index 89aa3bc..6bdf566 100644
--- a/src/MatroskaBatchFlow.Uno/Presentation/TrackViewModelBase.cs
+++ b/src/MatroskaBatchFlow.Uno/Presentation/TrackViewModelBase.cs
@@ -8,6 +8,7 @@ namespace MatroskaBatchFlow.Uno.Presentation;
public abstract partial class TrackViewModelBase : ObservableObject
{
+ private readonly ILogger _logger;
protected bool _suppressBatchConfigUpdate = false;
protected ObservableCollection _tracks = [];
@@ -53,7 +54,7 @@ public bool IsDefaultTrack
{
_isDefaultTrack = value;
OnPropertyChanged(nameof(IsDefaultTrack));
- UpdateBatchConfigTrackProperty(tc => tc.Default = value);
+ UpdateBatchConfigTrackProperty(tc => tc.Default = value, ftv => ftv.Default = value);
}
}
}
@@ -69,7 +70,7 @@ public bool IsDefaultFlagModificationEnabled
{
_isDefaultFlagModificationEnabled = value;
OnPropertyChanged(nameof(IsDefaultFlagModificationEnabled));
- UpdateBatchConfigTrackProperty(tc => tc.ShouldModifyDefaultFlag = value);
+ UpdateGlobalModificationFlag(tc => tc.ShouldModifyDefaultFlag = value);
}
}
}
@@ -85,7 +86,7 @@ public bool IsEnabledTrack
{
_isEnabledTrack = value;
OnPropertyChanged(nameof(IsEnabledTrack));
- UpdateBatchConfigTrackProperty(tc => tc.Enabled = value);
+ UpdateBatchConfigTrackProperty(tc => tc.Enabled = value, ftv => ftv.Enabled = value);
}
}
}
@@ -101,7 +102,7 @@ public bool IsEnabledFlagModificationEnabled
{
_isEnabledFlagModificationEnabled = value;
OnPropertyChanged(nameof(IsEnabledFlagModificationEnabled));
- UpdateBatchConfigTrackProperty(tc => tc.ShouldModifyEnabledFlag = value);
+ UpdateGlobalModificationFlag(tc => tc.ShouldModifyEnabledFlag = value);
}
}
}
@@ -117,7 +118,7 @@ public bool IsForcedTrack
{
_isForcedTrack = value;
OnPropertyChanged(nameof(IsForcedTrack));
- UpdateBatchConfigTrackProperty(tc => tc.Forced = value);
+ UpdateBatchConfigTrackProperty(tc => tc.Forced = value, ftv => ftv.Forced = value);
}
}
}
@@ -133,7 +134,7 @@ public bool IsForcedFlagModificationEnabled
{
_isForcedFlagModificationEnabled = value;
OnPropertyChanged(nameof(IsForcedFlagModificationEnabled));
- UpdateBatchConfigTrackProperty(tc => tc.ShouldModifyForcedFlag = value);
+ UpdateGlobalModificationFlag(tc => tc.ShouldModifyForcedFlag = value);
}
}
}
@@ -149,7 +150,7 @@ public string TrackName
{
_trackName = value;
OnPropertyChanged(nameof(TrackName));
- UpdateBatchConfigTrackProperty(tc => tc.Name = value);
+ UpdateBatchConfigTrackProperty(tc => tc.Name = value, ftv => ftv.Name = value);
}
}
}
@@ -165,7 +166,7 @@ public bool IsTrackNameModificationEnabled
{
_isTrackNameModificationEnabled = value;
OnPropertyChanged(nameof(IsTrackNameModificationEnabled));
- UpdateBatchConfigTrackProperty(tc => tc.ShouldModifyName = value);
+ UpdateGlobalModificationFlag(tc => tc.ShouldModifyName = value);
}
}
}
@@ -181,7 +182,16 @@ public MatroskaLanguageOption? SelectedLanguage
{
_selectedLanguage = value;
OnPropertyChanged(nameof(SelectedLanguage));
- UpdateBatchConfigTrackProperty(tc => tc.Language = value);
+
+ // Only warn when null comes from outside the internal sync path (e.g., ComboBox clearing its selection).
+ // During internal resets (ApplyTrackProperties), _suppressBatchConfigUpdate is true and null is expected.
+ if (value is null && !_suppressBatchConfigUpdate)
+ {
+ LogSelectedLanguageReceivedNull();
+ }
+
+ MatroskaLanguageOption language = value ?? MatroskaLanguageOption.Undetermined;
+ UpdateBatchConfigTrackProperty(tc => tc.Language = language, ftv => ftv.Language = language);
}
}
}
@@ -197,7 +207,7 @@ public bool IsSelectedLanguageModificationEnabled
{
_isSelectedLanguageModificationEnabled = value;
OnPropertyChanged(nameof(IsSelectedLanguageModificationEnabled));
- UpdateBatchConfigTrackProperty(tc => tc.ShouldModifyLanguage = value);
+ UpdateGlobalModificationFlag(tc => tc.ShouldModifyLanguage = value);
}
}
}
@@ -209,12 +219,13 @@ public bool IsSelectedLanguageModificationEnabled
public bool ShowTrackAvailabilityText => _uiPreferences.ShowTrackAvailabilityText;
- protected TrackViewModelBase(ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
+ protected TrackViewModelBase(ILogger logger, ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
{
+ _logger = logger;
_batchConfiguration = batchConfiguration;
_uiPreferences = uiPreferences;
_languages = languageProvider.Languages;
-
+
// Subscribe to property changes on the service
_uiPreferences.PropertyChanged += OnUIPreferencesChanged;
}
@@ -270,10 +281,10 @@ public string GetTrackAvailabilityText(int trackIndex)
{
int available = GetTrackAvailabilityCount(trackIndex);
int total = TotalFileCount;
-
+
if (total == 0)
return "0/0";
-
+
return $"{available}/{total}";
}
@@ -358,18 +369,20 @@ protected virtual void OnTrackPropertyChanged(object? sender, PropertyChangedEve
/// Updates the properties of the currently selected track in the batch configuration using the provided update action.
///
///
- /// This method applies the provided update action to both:
+ /// This method applies the provided update actions to both:
///
/// - The global track collection (used for UI display) - triggers PropertyChanged which fires StateChanged
- /// - All per-file track configurations (used for command generation) - updated silently
+ /// - All per-file track value collections (used for command generation) - updated silently
///
/// The global track update will trigger the StateChanged event through its PropertyChanged handler,
/// ensuring the UI and command generation stay synchronized.
/// If updates are suppressed or no track is selected, the method performs no operation.
///
- /// An delegate that defines the update to apply to the selected track's
- /// configuration.
- protected virtual void UpdateBatchConfigTrackProperty(Action updateAction)
+ /// An delegate that defines the update to apply to the global track configuration.
+ /// An delegate that defines the update to apply to each per-file track value.
+ protected virtual void UpdateBatchConfigTrackProperty(
+ Action globalUpdateAction,
+ Action perFileUpdateAction)
{
// If suppressing updates, do nothing to avoid (potential) recursion.
if (_suppressBatchConfigUpdate)
@@ -382,24 +395,50 @@ protected virtual void UpdateBatchConfigTrackProperty(Action
if (index < 0 || index >= tracks.Count)
return;
- // First, update all per-file configurations silently (without triggering events)
- // This ensures command generation uses the updated values
+ // First, update all per-file value collections silently (without triggering events)
+ // This keeps per-file configurations in sync with the global track settings used for command generation
var trackType = SelectedTrack.Type;
foreach (var kvp in _batchConfiguration.FileConfigurations)
{
var fileConfig = kvp.Value;
var fileTracks = fileConfig.GetTrackListForType(trackType);
-
+
// Only update if this file actually has this track
if (index >= 0 && index < fileTracks.Count)
{
- updateAction(fileTracks[index]);
+ perFileUpdateAction(fileTracks[index]);
}
}
// Finally, apply the update action to the global track configuration
// This will trigger PropertyChanged -> TrackConfiguration_PropertyChanged -> StateChanged
// which updates CanProcessBatch and regenerates commands
+ globalUpdateAction(tracks[index]);
+ }
+
+ ///
+ /// Updates only the global for a modification flag (e.g. ShouldModify*).
+ /// Per-file track values do not carry modification flags, so only the global track is updated.
+ ///
+ ///
+ /// If updates are suppressed or no track is selected, the method performs no operation.
+ ///
+ /// An delegate that sets the modification flag on the global track configuration.
+ protected virtual void UpdateGlobalModificationFlag(Action updateAction)
+ {
+ // If suppressing updates, do nothing to avoid (potential) recursion.
+ if (_suppressBatchConfigUpdate)
+ return;
+ if (SelectedTrack == null || GetTracks() == null)
+ return;
+
+ int index = SelectedTrack.Index;
+ var tracks = GetTracks();
+ if (index < 0 || index >= tracks.Count)
+ return;
+
+ // Apply the update action to the global track configuration only
+ // This will trigger PropertyChanged -> TrackConfiguration_PropertyChanged -> StateChanged
updateAction(tracks[index]);
}
@@ -422,38 +461,43 @@ protected virtual void OnSelectedTrackChanged(TrackConfiguration? newSelectedTra
/// name="track"/> is , all track-related properties are reset to default values.
private void ApplyTrackProperties(TrackConfiguration? track)
{
- // If suppressing updates, do nothing to avoid (potential) recursion.
+ // Suppress batch config updates while synchronizing properties to avoid (potential) recursion.
_suppressBatchConfigUpdate = true;
- // TODO: Need a better way to reset properties when no track is provided.
- if (track is null)
+ try
{
- IsDefaultTrack = false;
- IsEnabledTrack = true;
- IsForcedTrack = false;
- TrackName = string.Empty;
- SelectedLanguage = null;
- IsDefaultFlagModificationEnabled = false;
- IsEnabledFlagModificationEnabled = false;
- IsForcedFlagModificationEnabled = false;
- IsTrackNameModificationEnabled = false;
- IsSelectedLanguageModificationEnabled = false;
+ // TODO: Need a better way to reset properties when no track is provided.
+ if (track is null)
+ {
+ IsDefaultTrack = false;
+ IsEnabledTrack = true;
+ IsForcedTrack = false;
+ TrackName = string.Empty;
+ SelectedLanguage = null;
+ IsDefaultFlagModificationEnabled = false;
+ IsEnabledFlagModificationEnabled = false;
+ IsForcedFlagModificationEnabled = false;
+ IsTrackNameModificationEnabled = false;
+ IsSelectedLanguageModificationEnabled = false;
+
+ return;
+ }
- return;
+ // Synchronize properties with the selected track.
+ IsDefaultTrack = track.Default;
+ IsEnabledTrack = track.Enabled;
+ IsForcedTrack = track.Forced;
+ TrackName = track.Name;
+ SelectedLanguage = track.Language;
+ IsDefaultFlagModificationEnabled = track.ShouldModifyDefaultFlag;
+ IsEnabledFlagModificationEnabled = track.ShouldModifyEnabledFlag;
+ IsForcedFlagModificationEnabled = track.ShouldModifyForcedFlag;
+ IsTrackNameModificationEnabled = track.ShouldModifyName;
+ IsSelectedLanguageModificationEnabled = track.ShouldModifyLanguage;
+ }
+ finally
+ {
+ _suppressBatchConfigUpdate = false;
}
-
- // Synchronize properties with the selected track.
- IsDefaultTrack = track.Default;
- IsEnabledTrack = track.Enabled;
- IsForcedTrack = track.Forced;
- TrackName = track.Name;
- SelectedLanguage = track.Language;
- IsDefaultFlagModificationEnabled = track.ShouldModifyDefaultFlag;
- IsEnabledFlagModificationEnabled = track.ShouldModifyEnabledFlag;
- IsForcedFlagModificationEnabled = track.ShouldModifyForcedFlag;
- IsTrackNameModificationEnabled = track.ShouldModifyName;
- IsSelectedLanguageModificationEnabled = track.ShouldModifyLanguage;
-
- _suppressBatchConfigUpdate = false;
}
}
diff --git a/src/MatroskaBatchFlow.Uno/Presentation/VideoViewModel.cs b/src/MatroskaBatchFlow.Uno/Presentation/VideoViewModel.cs
index 300a030..ebe4a16 100644
--- a/src/MatroskaBatchFlow.Uno/Presentation/VideoViewModel.cs
+++ b/src/MatroskaBatchFlow.Uno/Presentation/VideoViewModel.cs
@@ -28,8 +28,8 @@ public ObservableCollection VideoTracks
///
public bool IsFileListPopulated => _batchConfiguration.FileList.Count > 0;
- public VideoViewModel(ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
- : base(languageProvider, batchConfiguration, uiPreferences)
+ public VideoViewModel(ILogger logger, ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences)
+ : base(logger, languageProvider, batchConfiguration, uiPreferences)
{
VideoTracks = [.. _batchConfiguration.VideoTracks];
diff --git a/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchConfigurationTests.cs b/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchConfigurationTests.cs
index 164376a..956ee09 100644
--- a/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchConfigurationTests.cs
+++ b/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchConfigurationTests.cs
@@ -263,15 +263,39 @@ public void MigrateFileConfiguration_PreservesAllTrackTypes()
var newFileId = Guid.NewGuid();
// Create file configuration with all track types
- var audioTrack = new TrackConfiguration(new MediaInfoResultBuilder()
- .AddTrackOfType(TrackType.Audio)
- .Build().Media.Track.First(t => t.Type == TrackType.Audio));
- var videoTrack = new TrackConfiguration(new MediaInfoResultBuilder()
- .AddTrackOfType(TrackType.Video)
- .Build().Media.Track.First(t => t.Type == TrackType.Video));
- var subtitleTrack = new TrackConfiguration(new MediaInfoResultBuilder()
- .AddTrackOfType(TrackType.Text)
- .Build().Media.Track.First(t => t.Type == TrackType.Text));
+ var audioTrack = new FileTrackValues
+ {
+ ScannedTrackInfo = new MediaInfoResultBuilder()
+ .AddTrackOfType(TrackType.Audio)
+ .Build().Media.Track.First(t => t.Type == TrackType.Audio),
+ Type = TrackType.Audio,
+ Index = 0,
+ Default = false,
+ Forced = false,
+ Enabled = false
+ };
+ var videoTrack = new FileTrackValues
+ {
+ ScannedTrackInfo = new MediaInfoResultBuilder()
+ .AddTrackOfType(TrackType.Video)
+ .Build().Media.Track.First(t => t.Type == TrackType.Video),
+ Type = TrackType.Video,
+ Index = 0,
+ Default = false,
+ Forced = false,
+ Enabled = false
+ };
+ var subtitleTrack = new FileTrackValues
+ {
+ ScannedTrackInfo = new MediaInfoResultBuilder()
+ .AddTrackOfType(TrackType.Text)
+ .Build().Media.Track.First(t => t.Type == TrackType.Text),
+ Type = TrackType.Text,
+ Index = 0,
+ Default = false,
+ Forced = false,
+ Enabled = false
+ };
var fileConfig = new FileTrackConfiguration
{
@@ -303,15 +327,18 @@ public void MigrateFileConfiguration_PreservesUserModifications()
var oldFileId = Guid.NewGuid();
var newFileId = Guid.NewGuid();
- // Create audio track with user modifications
- var audioTrack = new TrackConfiguration(new MediaInfoResultBuilder()
- .AddTrackOfType(TrackType.Audio)
- .Build().Media.Track.First(t => t.Type == TrackType.Audio))
+ // Create audio track with user-modified values
+ var audioTrack = new FileTrackValues
{
+ ScannedTrackInfo = new MediaInfoResultBuilder()
+ .AddTrackOfType(TrackType.Audio)
+ .Build().Media.Track.First(t => t.Type == TrackType.Audio),
+ Type = TrackType.Audio,
+ Index = 0,
Name = "Modified Track Name",
Default = true,
- ShouldModifyName = true,
- ShouldModifyDefaultFlag = true
+ Forced = false,
+ Enabled = false
};
var fileConfig = new FileTrackConfiguration
@@ -324,13 +351,11 @@ public void MigrateFileConfiguration_PreservesUserModifications()
// Act
config.MigrateFileConfiguration(oldFileId, newFileId);
- // Assert - user modifications should be preserved
+ // Assert - user-modified values should be preserved after migration
var migratedConfig = config.FileConfigurations[newFileId];
var migratedTrack = migratedConfig.AudioTracks[0];
Assert.Equal("Modified Track Name", migratedTrack.Name);
Assert.True(migratedTrack.Default);
- Assert.True(migratedTrack.ShouldModifyName);
- Assert.True(migratedTrack.ShouldModifyDefaultFlag);
}
}
diff --git a/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchTrackConfigurationInitializerTests.cs b/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchTrackConfigurationInitializerTests.cs
index 61bd984..b81fbe7 100644
--- a/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchTrackConfigurationInitializerTests.cs
+++ b/tests/MatroskaBatchFlow.Core.UnitTests/Services/BatchTrackConfigurationInitializerTests.cs
@@ -19,6 +19,7 @@ private static ILanguageProvider CreateMockLanguageProvider()
{
var mockLanguageProvider = Substitute.For();
mockLanguageProvider.Languages.Returns(ImmutableList.Empty);
+ mockLanguageProvider.Resolve(Arg.Any()).Returns(MatroskaLanguageOption.Undetermined);
return mockLanguageProvider;
}
@@ -54,7 +55,7 @@ private static BatchTrackConfigurationInitializer CreateInitializer(
{
languageProvider ??= CreateMockLanguageProvider();
var trackConfigFactory = new TrackConfigurationFactory(languageProvider);
- return new BatchTrackConfigurationInitializer(batchConfig, trackConfigFactory);
+ return new BatchTrackConfigurationInitializer(batchConfig, trackConfigFactory, languageProvider);
}
[Fact]
@@ -233,4 +234,85 @@ public void Initialize_EmptyMediaInfo_CreatesConfigWithoutTracks()
Assert.True(fileConfigs.ContainsKey(scannedFile.Id));
Assert.Empty(fileConfigs[scannedFile.Id].AudioTracks);
}
+
+ [Fact]
+ public void Initialize_PerFileTracksAreFileTrackValuesWithScannedData()
+ {
+ // Arrange
+ var (mockConfig, fileConfigs, _, _, _) = CreateMockConfig();
+ var initializer = CreateInitializer(mockConfig);
+
+ var mediaInfoResult = new MediaInfoResultBuilder()
+ .WithCreatingLibrary()
+ .AddTrackOfType(TrackType.Audio)
+ .AddTrackOfType(TrackType.Audio)
+ .Build();
+ var scannedFile = new ScannedFileInfo(mediaInfoResult, "file.mkv");
+ mockConfig.FileList.Add(scannedFile);
+
+ // Act
+ initializer.Initialize(scannedFile, TrackType.Audio);
+
+ // Assert - per-file tracks are FileTrackValues initialized from the scan
+ var fileConfig = fileConfigs[scannedFile.Id];
+ Assert.Equal(2, fileConfig.AudioTracks.Count);
+
+ Assert.Equal(TrackType.Audio, fileConfig.AudioTracks[0].Type);
+ Assert.Equal(0, fileConfig.AudioTracks[0].Index);
+ Assert.NotNull(fileConfig.AudioTracks[0].ScannedTrackInfo);
+
+ Assert.Equal(TrackType.Audio, fileConfig.AudioTracks[1].Type);
+ Assert.Equal(1, fileConfig.AudioTracks[1].Index);
+ Assert.NotNull(fileConfig.AudioTracks[1].ScannedTrackInfo);
+ }
+
+ [Fact]
+ public void Initialize_FilesAddedAfterUserConfigure_ReceiveFileTrackValues()
+ {
+ // Regression test for issue #85: files added after processing were skipped because
+ // per-file TrackConfiguration objects always had ShouldModify* = false.
+ // Now per-file tracks are FileTrackValues with no ShouldModify* fields,
+ // so the argument generator reads ShouldModify* from global tracks for every file.
+ // Arrange
+ var (mockConfig, fileConfigs, audioTracks, _, _) = CreateMockConfig();
+ var initializer = CreateInitializer(mockConfig);
+
+ var firstFile = new ScannedFileInfo(
+ new MediaInfoResultBuilder()
+ .WithCreatingLibrary()
+ .AddTrackOfType(TrackType.Audio)
+ .Build(),
+ "file1.mkv"
+ );
+ mockConfig.FileList.Add(firstFile);
+ initializer.Initialize(firstFile, TrackType.Audio);
+
+ // Simulate user enabling modifications on the global track
+ audioTracks[0].ShouldModifyLanguage = true;
+ audioTracks[0].ShouldModifyName = true;
+
+ // Add a second file after modifications were configured (this was the bug scenario)
+ var secondFile = new ScannedFileInfo(
+ new MediaInfoResultBuilder()
+ .WithCreatingLibrary()
+ .AddTrackOfType(TrackType.Audio)
+ .Build(),
+ "file2.mkv"
+ );
+ mockConfig.FileList.Add(secondFile);
+
+ // Act
+ initializer.Initialize(secondFile, TrackType.Audio);
+
+ // Assert - second file receives FileTrackValues (not TrackConfiguration with ShouldModify* = false)
+ Assert.True(fileConfigs.ContainsKey(secondFile.Id));
+ var secondFileConfig = fileConfigs[secondFile.Id];
+ Assert.Single(secondFileConfig.AudioTracks);
+ Assert.Equal(TrackType.Audio, secondFileConfig.AudioTracks[0].Type);
+ Assert.Equal(0, secondFileConfig.AudioTracks[0].Index);
+
+ // Global tracks still hold the ShouldModify* flags set by the user
+ Assert.True(audioTracks[0].ShouldModifyLanguage);
+ Assert.True(audioTracks[0].ShouldModifyName);
+ }
}
diff --git a/tests/MatroskaBatchFlow.Core.UnitTests/Services/LanguageProviderTests.cs b/tests/MatroskaBatchFlow.Core.UnitTests/Services/LanguageProviderTests.cs
index 9a05872..06b924b 100644
--- a/tests/MatroskaBatchFlow.Core.UnitTests/Services/LanguageProviderTests.cs
+++ b/tests/MatroskaBatchFlow.Core.UnitTests/Services/LanguageProviderTests.cs
@@ -194,6 +194,150 @@ public void Languages_IsImmutable()
Assert.IsAssignableFrom>(provider.Languages);
}
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Resolve_ReturnsUndetermined_WhenLanguageCodeIsNullOrWhitespace(string? languageCode)
+ {
+ // Arrange
+ var provider = CreateProviderWithTestLanguages();
+
+ // Act
+ var result = provider.Resolve(languageCode);
+
+ // Assert
+ Assert.Equal(MatroskaLanguageOption.Undetermined, result);
+ }
+
+ [Fact]
+ public void Resolve_ReturnsUndetermined_WhenNoMatchFound()
+ {
+ // Arrange
+ var provider = CreateProviderWithTestLanguages();
+
+ // Act
+ var result = provider.Resolve("nonexistent");
+
+ // Assert
+ Assert.Equal(MatroskaLanguageOption.Undetermined, result);
+ }
+
+ [Fact]
+ public void Resolve_MatchesByIso639_2_b()
+ {
+ // Arrange — French has iso639_2_b "fre" (distinct from iso639_2_t "fra")
+ var provider = CreateProviderWithTestLanguages();
+
+ // Act
+ var result = provider.Resolve("fre");
+
+ // Assert
+ Assert.Equal("French", result.Name);
+ }
+
+ [Fact]
+ public void Resolve_MatchesByIso639_2_t()
+ {
+ // Arrange — French has iso639_2_t "fra" (distinct from iso639_2_b "fre")
+ var provider = CreateProviderWithTestLanguages();
+
+ // Act
+ var result = provider.Resolve("fra");
+
+ // Assert
+ Assert.Equal("French", result.Name);
+ }
+
+ [Fact]
+ public void Resolve_MatchesByIso639_1()
+ {
+ // Arrange
+ var provider = CreateProviderWithTestLanguages();
+
+ // Act
+ var result = provider.Resolve("en");
+
+ // Assert
+ Assert.Equal("English", result.Name);
+ }
+
+ [Fact]
+ public void Resolve_MatchesByIso639_3()
+ {
+ // Arrange — "cmn" only appears in the ISO 639-3 field, so this isolates that match path.
+ var provider = CreateProviderWithLanguageWithoutIso639_1();
+
+ // Act
+ var result = provider.Resolve("cmn");
+
+ // Assert
+ Assert.Equal("Chinese", result.Name);
+ }
+
+ [Fact]
+ public void Resolve_MatchesByName()
+ {
+ // Arrange
+ var provider = CreateProviderWithTestLanguages();
+
+ // Act
+ var result = provider.Resolve("French");
+
+ // Assert
+ Assert.Equal("French", result.Name);
+ }
+
+ [Fact]
+ public void Resolve_MatchesByCode()
+ {
+ // Arrange — Code is derived: iso639_1 ?? iso639_2_b, so "fr" for French.
+ // "fr" also matches iso639_1 directly. Use a language with null iso639_1
+ // to isolate the Code path.
+ var provider = CreateProviderWithLanguageWithoutIso639_1();
+
+ // Act — Code falls back to iso639_2_b "zho" when iso639_1 is null
+ var result = provider.Resolve("zho");
+
+ // Assert
+ Assert.Equal("Chinese", result.Name);
+ }
+
+ [Fact]
+ public void Resolve_IsCaseInsensitive()
+ {
+ // Arrange
+ var provider = CreateProviderWithTestLanguages();
+
+ // Act
+ var result = provider.Resolve("ENG");
+
+ // Assert
+ Assert.Equal("English", result.Name);
+ }
+
+ private LanguageProvider CreateProviderWithTestLanguages()
+ {
+ var testFile = CreateTestLanguageFile("resolve_test.json");
+ var options = CreateOptions(testFile);
+ return new LanguageProvider(options, _logger);
+ }
+
+ private LanguageProvider CreateProviderWithLanguageWithoutIso639_1()
+ {
+ var testFile = Path.Combine(_testFilesDirectory, "resolve_no_iso1.json");
+ var json = """
+ {
+ "languages": [
+ { "name": "Chinese", "iso639_1": null, "iso639_2_b": "zho", "iso639_2_t": "chi", "iso639_3": "cmn" }
+ ]
+ }
+ """;
+ File.WriteAllText(testFile, json);
+ var options = CreateOptions(testFile);
+ return new LanguageProvider(options, _logger);
+ }
+
private static IOptions CreateOptions(string filePath)
{
var options = Options.Create(new LanguageOptions { FilePath = filePath });
diff --git a/tests/MatroskaBatchFlow.Core.UnitTests/Services/TrackConfigurationFactoryTests.cs b/tests/MatroskaBatchFlow.Core.UnitTests/Services/TrackConfigurationFactoryTests.cs
index 9505027..c0274f3 100644
--- a/tests/MatroskaBatchFlow.Core.UnitTests/Services/TrackConfigurationFactoryTests.cs
+++ b/tests/MatroskaBatchFlow.Core.UnitTests/Services/TrackConfigurationFactoryTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Immutable;
using MatroskaBatchFlow.Core.Enums;
using MatroskaBatchFlow.Core.Models;
using MatroskaBatchFlow.Core.Services;
@@ -13,18 +12,19 @@ namespace MatroskaBatchFlow.Core.UnitTests.Services;
///
public class TrackConfigurationFactoryTests
{
- private static ILanguageProvider CreateMockLanguageProvider(IEnumerable? languages = null)
+ private readonly ILanguageProvider _mockLanguageProvider = Substitute.For();
+
+ public TrackConfigurationFactoryTests()
{
- var mockLanguageProvider = Substitute.For();
- mockLanguageProvider.Languages.Returns(languages?.ToImmutableList() ?? ImmutableList.Empty);
- return mockLanguageProvider;
+ // Default: any unmatched code resolves to Undetermined.
+ _mockLanguageProvider.Resolve(Arg.Any()).Returns(MatroskaLanguageOption.Undetermined);
}
[Fact]
public void Create_SetsBasicProperties()
{
// Arrange
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider());
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
var trackInfo = new TrackInfoBuilder()
.WithType(TrackType.Audio)
.WithTitle("English Commentary")
@@ -49,7 +49,7 @@ public void Create_SetsBasicProperties()
public void Create_SetsEmptyNameWhenTitleIsNull()
{
// Arrange
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider());
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
var trackInfo = new TrackInfoBuilder()
.WithType(TrackType.Video)
.WithStreamKindID(0)
@@ -63,7 +63,7 @@ public void Create_SetsEmptyNameWhenTitleIsNull()
}
[Fact]
- public void Create_ResolvesLanguageFromIso639_2_b()
+ public void Create_DelegatesToResolveAndUsesResult()
{
// Arrange
var englishLanguage = new MatroskaLanguageOption(
@@ -72,7 +72,8 @@ public void Create_ResolvesLanguageFromIso639_2_b()
iso639_2_b: "eng",
iso639_2_t: "eng",
iso639_3: "eng");
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider([englishLanguage]));
+ _mockLanguageProvider.Resolve("eng").Returns(englishLanguage);
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
var trackInfo = new TrackInfoBuilder()
.WithType(TrackType.Audio)
.WithLanguage("eng")
@@ -87,7 +88,7 @@ public void Create_ResolvesLanguageFromIso639_2_b()
}
[Fact]
- public void Create_ResolvesLanguageFromIso639_1()
+ public void Create_PassesLanguageCodeToResolve()
{
// Arrange
var japaneseLanguage = new MatroskaLanguageOption(
@@ -96,7 +97,8 @@ public void Create_ResolvesLanguageFromIso639_1()
iso639_2_b: "jpn",
iso639_2_t: "jpn",
iso639_3: "jpn");
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider([japaneseLanguage]));
+ _mockLanguageProvider.Resolve("ja").Returns(japaneseLanguage);
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
var trackInfo = new TrackInfoBuilder()
.WithType(TrackType.Audio)
.WithLanguage("ja")
@@ -114,7 +116,7 @@ public void Create_ResolvesLanguageFromIso639_1()
public void Create_ReturnsUndeterminedForUnknownLanguage()
{
// Arrange
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider());
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
var trackInfo = new TrackInfoBuilder()
.WithType(TrackType.Audio)
.WithLanguage("xyz")
@@ -132,7 +134,7 @@ public void Create_ReturnsUndeterminedForUnknownLanguage()
public void Create_ReturnsUndeterminedForEmptyLanguage()
{
// Arrange
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider());
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
var trackInfo = new TrackInfoBuilder()
.WithType(TrackType.Audio)
.WithLanguage(string.Empty)
@@ -150,7 +152,7 @@ public void Create_ReturnsUndeterminedForEmptyLanguage()
public void Create_ReturnsUndeterminedForWhitespaceLanguage()
{
// Arrange
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider());
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
var trackInfo = new TrackInfoBuilder()
.WithType(TrackType.Audio)
.WithLanguage(" ")
@@ -168,7 +170,7 @@ public void Create_ReturnsUndeterminedForWhitespaceLanguage()
public void Create_ThrowsForNullTrackInfo()
{
// Arrange
- var factory = new TrackConfigurationFactory(CreateMockLanguageProvider());
+ var factory = new TrackConfigurationFactory(_mockLanguageProvider);
// Act & Assert
Assert.Throws(() => factory.Create(null!, TrackType.Audio, 0));
diff --git a/tests/MatroskaBatchFlow.Uno.IntegrationTests/TrackModificationIntegrationTests.cs b/tests/MatroskaBatchFlow.Uno.IntegrationTests/TrackModificationIntegrationTests.cs
index b86e957..f180cf2 100644
--- a/tests/MatroskaBatchFlow.Uno.IntegrationTests/TrackModificationIntegrationTests.cs
+++ b/tests/MatroskaBatchFlow.Uno.IntegrationTests/TrackModificationIntegrationTests.cs
@@ -33,6 +33,7 @@ public async Task IntegrationTest_EditingTrackOnlyInSecondFile_TriggersStateChan
// Mock only external dependencies (language data, UI preferences)
var mockLanguageProvider = Substitute.For();
mockLanguageProvider.Languages.Returns(ImmutableList.Empty);
+ mockLanguageProvider.Resolve(Arg.Any()).Returns(MatroskaLanguageOption.Undetermined);
var mockUIPreferences = Substitute.For();
var file1Path = "file1.mkv";
@@ -61,7 +62,7 @@ public async Task IntegrationTest_EditingTrackOnlyInSecondFile_TriggersStateChan
// Initialize per-file configurations
var trackConfigFactory = new TrackConfigurationFactory(mockLanguageProvider);
- var initializer = new BatchTrackConfigurationInitializer(batchConfig, trackConfigFactory);
+ var initializer = new BatchTrackConfigurationInitializer(batchConfig, trackConfigFactory, mockLanguageProvider);
initializer.Initialize(file1, TrackType.Text);
initializer.Initialize(file2, TrackType.Text);
@@ -70,7 +71,7 @@ public async Task IntegrationTest_EditingTrackOnlyInSecondFile_TriggersStateChan
batchConfig.StateChanged += (_, __) => tcs.TrySetResult(true);
// Create SubtitleViewModel using real BatchConfiguration
- var subtitleViewModel = new SubtitleViewModel(mockLanguageProvider, batchConfig, mockUIPreferences);
+ var subtitleViewModel = new SubtitleViewModel(Substitute.For>(), mockLanguageProvider, batchConfig, mockUIPreferences);
// Select track 17 (index 16) - only exists in file2
Assert.Equal(17, batchConfig.SubtitleTracks.Count);
@@ -104,10 +105,9 @@ public async Task IntegrationTest_EditingTrackOnlyInSecondFile_TriggersStateChan
var file2Config = batchConfig.FileConfigurations[file2.Id];
Assert.Equal(17, file2Config.SubtitleTracks.Count);
Assert.Equal("Track17Modified", file2Config.SubtitleTracks[16].Name);
- Assert.True(file2Config.SubtitleTracks[16].ShouldModifyName);
// Verify command generation works correctly
- var argumentsGenerator = new MkvPropeditArgumentsGenerator();
+ var argumentsGenerator = new MkvPropeditArgumentsGenerator(Substitute.For>());
var commands = argumentsGenerator.BuildBatchArguments(batchConfig);
Assert.Single(commands);
diff --git a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/AudioViewModelTests.cs b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/AudioViewModelTests.cs
index 2b20719..0fc4f09 100644
--- a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/AudioViewModelTests.cs
+++ b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/AudioViewModelTests.cs
@@ -7,6 +7,7 @@
using MatroskaBatchFlow.Core.Utilities;
using MatroskaBatchFlow.Uno.Contracts.Services;
using MatroskaBatchFlow.Uno.Presentation;
+using Microsoft.Extensions.Logging;
using NSubstitute;
namespace MatroskaBatchFlow.Uno.UnitTests.Presentation;
@@ -16,12 +17,14 @@ public class AudioViewModelTests
private readonly ILanguageProvider _languageProvider;
private readonly IBatchConfiguration _batchConfiguration;
private readonly IUIPreferencesService _uiPreferences;
+ private readonly ILogger _logger;
public AudioViewModelTests()
{
_languageProvider = Substitute.For();
_batchConfiguration = Substitute.For();
_uiPreferences = Substitute.For();
+ _logger = Substitute.For>();
_languageProvider.Languages.Returns([]);
_batchConfiguration.AudioTracks.Returns(new ObservableCollection());
@@ -46,7 +49,7 @@ public void Constructor_InitializesAudioTracksFromBatchConfiguration()
_batchConfiguration.AudioTracks.Returns(audioTracks);
// Act
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.Single(viewModel.AudioTracks);
@@ -70,7 +73,7 @@ public void Constructor_SetsSelectedTrackToFirstTrack()
_batchConfiguration.AudioTracks.Returns(audioTracks);
// Act
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.NotNull(viewModel.SelectedTrack);
@@ -91,7 +94,7 @@ public void IsFileListPopulated_ReturnsTrueWhenFilesExist()
_batchConfiguration.FileList.Returns(fileList);
// Act
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.True(viewModel.IsFileListPopulated);
@@ -103,7 +106,7 @@ public void IsFileListPopulated_ReturnsFalseWhenNoFilesExist()
// Arrange - fileList is empty by default
// Act
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.False(viewModel.IsFileListPopulated);
@@ -116,7 +119,7 @@ public void OnFileListChanged_UpdatesIsFileListPopulated()
var fileList = new UniqueObservableCollection(Substitute.For());
_batchConfiguration.FileList.Returns(fileList);
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
Assert.False(viewModel.IsFileListPopulated);
var mediaInfoResult = new MediaInfoResultBuilder()
@@ -145,7 +148,7 @@ public void OnBatchConfigurationAudioTracksChanged_UpdatesAudioTracks()
var audioTracks = new ObservableCollection();
_batchConfiguration.AudioTracks.Returns(audioTracks);
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
Assert.Empty(viewModel.AudioTracks);
var mediaInfoResult = new MediaInfoResultBuilder()
@@ -169,7 +172,7 @@ public void OnBatchConfigurationChanged_UpdatesAudioTracksWhenAudioTracksPropert
var initialTracks = new ObservableCollection();
_batchConfiguration.AudioTracks.Returns(initialTracks);
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
var mediaInfoResult = new MediaInfoResultBuilder()
.WithCreatingLibrary()
@@ -199,7 +202,7 @@ public void OnBatchConfigurationChanged_DoesNotUpdateWhenOtherPropertiesChange()
var audioTracks = new ObservableCollection();
_batchConfiguration.AudioTracks.Returns(audioTracks);
- var viewModel = new AudioViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new AudioViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
int audioTracksChangedCount = 0;
viewModel.PropertyChanged += (s, e) =>
{
diff --git a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/SubtitleViewModelTests.cs b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/SubtitleViewModelTests.cs
index 54b0273..ec7effd 100644
--- a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/SubtitleViewModelTests.cs
+++ b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/SubtitleViewModelTests.cs
@@ -7,6 +7,7 @@
using MatroskaBatchFlow.Core.Utilities;
using MatroskaBatchFlow.Uno.Contracts.Services;
using MatroskaBatchFlow.Uno.Presentation;
+using Microsoft.Extensions.Logging;
using NSubstitute;
namespace MatroskaBatchFlow.Uno.UnitTests.Presentation;
@@ -16,12 +17,14 @@ public class SubtitleViewModelTests
private readonly ILanguageProvider _languageProvider;
private readonly IBatchConfiguration _batchConfiguration;
private readonly IUIPreferencesService _uiPreferences;
+ private readonly ILogger _logger;
public SubtitleViewModelTests()
{
_languageProvider = Substitute.For();
_batchConfiguration = Substitute.For();
_uiPreferences = Substitute.For();
+ _logger = Substitute.For>();
_languageProvider.Languages.Returns([]);
_batchConfiguration.SubtitleTracks.Returns(new ObservableCollection());
@@ -46,7 +49,7 @@ public void Constructor_InitializesSubtitleTracksFromBatchConfiguration()
_batchConfiguration.SubtitleTracks.Returns(subtitleTracks);
// Act
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.Single(viewModel.SubtitleTracks);
@@ -70,7 +73,7 @@ public void Constructor_SetsSelectedTrackToFirstTrack()
_batchConfiguration.SubtitleTracks.Returns(subtitleTracks);
// Act
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.NotNull(viewModel.SelectedTrack);
@@ -91,7 +94,7 @@ public void IsFileListPopulated_ReturnsTrueWhenFilesExist()
_batchConfiguration.FileList.Returns(fileList);
// Act
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.True(viewModel.IsFileListPopulated);
@@ -103,7 +106,7 @@ public void IsFileListPopulated_ReturnsFalseWhenNoFilesExist()
// Arrange - fileList is empty by default
// Act
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.False(viewModel.IsFileListPopulated);
@@ -116,7 +119,7 @@ public void OnFileListChanged_UpdatesIsFileListPopulated()
var fileList = new UniqueObservableCollection(Substitute.For());
_batchConfiguration.FileList.Returns(fileList);
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
Assert.False(viewModel.IsFileListPopulated);
var mediaInfoResult = new MediaInfoResultBuilder()
@@ -145,7 +148,7 @@ public void OnBatchConfigurationSubtitleTracksChanged_UpdatesSubtitleTracks()
var subtitleTracks = new ObservableCollection();
_batchConfiguration.SubtitleTracks.Returns(subtitleTracks);
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
Assert.Empty(viewModel.SubtitleTracks);
var mediaInfoResult = new MediaInfoResultBuilder()
@@ -169,7 +172,7 @@ public void OnBatchConfigurationChanged_UpdatesSubtitleTracksWhenSubtitleTracksP
var initialTracks = new ObservableCollection();
_batchConfiguration.SubtitleTracks.Returns(initialTracks);
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
var mediaInfoResult = new MediaInfoResultBuilder()
.WithCreatingLibrary()
@@ -199,7 +202,7 @@ public void OnBatchConfigurationChanged_DoesNotUpdateWhenOtherPropertiesChange()
var subtitleTracks = new ObservableCollection();
_batchConfiguration.SubtitleTracks.Returns(subtitleTracks);
- var viewModel = new SubtitleViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new SubtitleViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
int subtitleTracksChangedCount = 0;
viewModel.PropertyChanged += (s, e) =>
{
diff --git a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/TrackViewModelBaseTests.cs b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/TrackViewModelBaseTests.cs
index 7a91793..07b55a3 100644
--- a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/TrackViewModelBaseTests.cs
+++ b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/TrackViewModelBaseTests.cs
@@ -6,6 +6,7 @@
using MatroskaBatchFlow.Core.Utilities;
using MatroskaBatchFlow.Uno.Contracts.Services;
using MatroskaBatchFlow.Uno.Presentation;
+using Microsoft.Extensions.Logging;
using NSubstitute;
namespace MatroskaBatchFlow.Uno.UnitTests.Presentation;
@@ -19,6 +20,8 @@ namespace MatroskaBatchFlow.Uno.UnitTests.Presentation;
/// facilitate testing scenarios.
public class TrackViewModelBaseTests
{
+ private readonly ILogger _mockLogger = Substitute.For();
+
///
/// Test implementation of TrackViewModelBase for testing purposes
///
@@ -26,8 +29,8 @@ private class TestTrackViewModel : TrackViewModelBase
{
private readonly IList _testTracks;
- public TestTrackViewModel(ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences, IList tracks)
- : base(languageProvider, batchConfiguration, uiPreferences)
+ public TestTrackViewModel(ILogger logger, ILanguageProvider languageProvider, IBatchConfiguration batchConfiguration, IUIPreferencesService uiPreferences, IList tracks)
+ : base(logger, languageProvider, batchConfiguration, uiPreferences)
{
_testTracks = tracks;
SetupEventHandlers();
@@ -74,10 +77,10 @@ public void UpdateBatchConfigTrackProperty_UpdatesGlobalAndPerFileConfigurations
var file2 = new ScannedFileInfo(mediaInfoResult, "file2.mkv");
var file1Config = new FileTrackConfiguration { FilePath = file1.Path };
- file1Config.SubtitleTracks.Add(new TrackConfiguration(trackInfo) { Type = TrackType.Text, Index = 0, Name = "Original" });
+ file1Config.SubtitleTracks.Add(new FileTrackValues { ScannedTrackInfo = trackInfo, Type = TrackType.Text, Index = 0, Name = "Original", Default = false, Forced = false, Enabled = false });
var file2Config = new FileTrackConfiguration { FilePath = file2.Path };
- file2Config.SubtitleTracks.Add(new TrackConfiguration(trackInfo) { Type = TrackType.Text, Index = 0, Name = "Original" });
+ file2Config.SubtitleTracks.Add(new FileTrackValues { ScannedTrackInfo = trackInfo, Type = TrackType.Text, Index = 0, Name = "Original", Default = false, Forced = false, Enabled = false });
var fileConfigurations = new Dictionary
{
@@ -88,7 +91,7 @@ public void UpdateBatchConfigTrackProperty_UpdatesGlobalAndPerFileConfigurations
mockBatchConfig.FileConfigurations.Returns(fileConfigurations);
// Create view model
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
// Set the selected track
viewModel.SelectedTrack = globalTracks[0];
@@ -103,10 +106,8 @@ public void UpdateBatchConfigTrackProperty_UpdatesGlobalAndPerFileConfigurations
// Assert - verify per-file configurations were also updated
Assert.Equal("Updated Name", file1Config.SubtitleTracks[0].Name);
- Assert.True(file1Config.SubtitleTracks[0].ShouldModifyName);
Assert.Equal("Updated Name", file2Config.SubtitleTracks[0].Name);
- Assert.True(file2Config.SubtitleTracks[0].ShouldModifyName);
}
[Fact]
@@ -132,7 +133,7 @@ public void UpdateBatchConfigTrackProperty_SkipsFilesWithoutTrack()
// File1 has track 0
var file1 = new ScannedFileInfo(mediaInfoResult, "file1.mkv");
var file1Config = new FileTrackConfiguration { FilePath = file1.Path };
- file1Config.SubtitleTracks.Add(new TrackConfiguration(trackInfo) { Type = TrackType.Text, Index = 0, Name = "Original" });
+ file1Config.SubtitleTracks.Add(new FileTrackValues { ScannedTrackInfo = trackInfo, Type = TrackType.Text, Index = 0, Name = "Original", Default = false, Forced = false, Enabled = false });
// File2 has NO tracks (different track count)
var file2 = new ScannedFileInfo(mediaInfoResult, "file2.mkv");
@@ -147,7 +148,7 @@ public void UpdateBatchConfigTrackProperty_SkipsFilesWithoutTrack()
mockBatchConfig.FileConfigurations.Returns(fileConfigurations);
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
viewModel.SelectedTrack = globalTracks[0];
// Act - enable modification and update track name
@@ -182,7 +183,7 @@ public void SelectedTrack_WhenSetToNull_ResetsAllPropertiesToDefault()
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
viewModel.SelectedTrack = globalTracks[0];
// Act - Set selected track to null
@@ -222,7 +223,7 @@ public void IsTrackSelected_ReturnsTrueWhenTrackIsSelected()
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
// Act
viewModel.SelectedTrack = globalTracks[0];
@@ -247,7 +248,7 @@ public void IsTrackSelected_ReturnsFalseWhenNoTrackIsSelected()
var globalTracks = new List();
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
// Act & Assert
Assert.False(viewModel.IsTrackSelected);
@@ -274,7 +275,7 @@ public void IsDefaultTrack_UpdatesBatchConfiguration_WhenChanged()
var file1 = new ScannedFileInfo(mediaInfoResult, "file1.mkv");
var file1Config = new FileTrackConfiguration { FilePath = file1.Path };
- file1Config.SubtitleTracks.Add(new TrackConfiguration(trackInfo) { Type = TrackType.Text, Index = 0, Default = false });
+ file1Config.SubtitleTracks.Add(new FileTrackValues { ScannedTrackInfo = trackInfo, Type = TrackType.Text, Index = 0, Default = false, Forced = false, Enabled = false });
var fileConfigurations = new Dictionary
{
@@ -283,7 +284,7 @@ public void IsDefaultTrack_UpdatesBatchConfiguration_WhenChanged()
mockBatchConfig.FileConfigurations.Returns(fileConfigurations);
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
viewModel.SelectedTrack = globalTracks[0];
// Act
@@ -315,7 +316,7 @@ public void IsForcedTrack_UpdatesBatchConfiguration_WhenChanged()
var file1 = new ScannedFileInfo(mediaInfoResult, "file1.mkv");
var file1Config = new FileTrackConfiguration { FilePath = file1.Path };
- file1Config.SubtitleTracks.Add(new TrackConfiguration(trackInfo) { Type = TrackType.Text, Index = 0, Forced = false });
+ file1Config.SubtitleTracks.Add(new FileTrackValues { ScannedTrackInfo = trackInfo, Type = TrackType.Text, Index = 0, Default = false, Forced = false, Enabled = false });
var fileConfigurations = new Dictionary
{
@@ -324,7 +325,7 @@ public void IsForcedTrack_UpdatesBatchConfiguration_WhenChanged()
mockBatchConfig.FileConfigurations.Returns(fileConfigurations);
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
viewModel.SelectedTrack = globalTracks[0];
// Act
@@ -356,7 +357,7 @@ public void SelectedLanguage_UpdatesBatchConfiguration_WhenChanged()
var file1 = new ScannedFileInfo(mediaInfoResult, "file1.mkv");
var file1Config = new FileTrackConfiguration { FilePath = file1.Path };
- file1Config.SubtitleTracks.Add(new TrackConfiguration(trackInfo) { Type = TrackType.Text, Index = 0, Language = MatroskaLanguageOption.Undetermined });
+ file1Config.SubtitleTracks.Add(new FileTrackValues { ScannedTrackInfo = trackInfo, Type = TrackType.Text, Index = 0, Language = MatroskaLanguageOption.Undetermined, Default = false, Forced = false, Enabled = false });
var fileConfigurations = new Dictionary
{
@@ -365,7 +366,7 @@ public void SelectedLanguage_UpdatesBatchConfiguration_WhenChanged()
mockBatchConfig.FileConfigurations.Returns(fileConfigurations);
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, globalTracks);
viewModel.SelectedTrack = globalTracks[0];
var newLanguage = new MatroskaLanguageOption("English", "en", "eng", "eng", "eng");
@@ -407,7 +408,7 @@ public void GetTrackAvailabilityCount_ReturnsCorrectCount()
mockBatchConfig.FileList.Returns(fileList);
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
// Act
int count0 = viewModel.GetTrackAvailabilityCount(0);
@@ -439,7 +440,7 @@ public void GetTrackAvailabilityText_ReturnsFormattedString()
mockBatchConfig.FileList.Returns(fileList);
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
// Act
string result = viewModel.GetTrackAvailabilityText(0);
@@ -460,7 +461,7 @@ public void TotalFileCount_ReturnsCorrectCount()
mockBatchConfig.FileList.Returns(fileList);
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
// Act & Assert
Assert.Equal(0, viewModel.TotalFileCount);
@@ -478,7 +479,7 @@ public void ShowTrackAvailabilityText_ReflectsUIPreferencesValue()
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
// Act
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
// Assert
Assert.True(viewModel.ShowTrackAvailabilityText);
@@ -495,7 +496,7 @@ public void UIPreferences_PropertyChanged_UpdatesShowTrackAvailabilityText()
mockUIPreferences.ShowTrackAvailabilityText.Returns(false);
mockBatchConfig.FileConfigurations.Returns(new Dictionary());
- var viewModel = new TestTrackViewModel(mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
+ var viewModel = new TestTrackViewModel(_mockLogger, mockLanguageProvider, mockBatchConfig, mockUIPreferences, new List());
Assert.False(viewModel.ShowTrackAvailabilityText);
// Act
diff --git a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/VideoViewModelTests.cs b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/VideoViewModelTests.cs
index 9042330..70eba8f 100644
--- a/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/VideoViewModelTests.cs
+++ b/tests/MatroskaBatchFlow.Uno.UnitTests/Presentation/VideoViewModelTests.cs
@@ -7,6 +7,7 @@
using MatroskaBatchFlow.Core.Utilities;
using MatroskaBatchFlow.Uno.Contracts.Services;
using MatroskaBatchFlow.Uno.Presentation;
+using Microsoft.Extensions.Logging;
using NSubstitute;
namespace MatroskaBatchFlow.Uno.UnitTests.Presentation;
@@ -16,12 +17,14 @@ public class VideoViewModelTests
private readonly ILanguageProvider _languageProvider;
private readonly IBatchConfiguration _batchConfiguration;
private readonly IUIPreferencesService _uiPreferences;
+ private readonly ILogger _logger;
public VideoViewModelTests()
{
_languageProvider = Substitute.For();
_batchConfiguration = Substitute.For();
_uiPreferences = Substitute.For();
+ _logger = Substitute.For>();
_languageProvider.Languages.Returns([]);
_batchConfiguration.VideoTracks.Returns(new ObservableCollection());
@@ -46,7 +49,7 @@ public void Constructor_InitializesVideoTracksFromBatchConfiguration()
_batchConfiguration.VideoTracks.Returns(videoTracks);
// Act
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.Single(viewModel.VideoTracks);
@@ -70,7 +73,7 @@ public void Constructor_SetsSelectedTrackToFirstTrack()
_batchConfiguration.VideoTracks.Returns(videoTracks);
// Act
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.NotNull(viewModel.SelectedTrack);
@@ -91,7 +94,7 @@ public void IsFileListPopulated_ReturnsTrueWhenFilesExist()
_batchConfiguration.FileList.Returns(fileList);
// Act
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.True(viewModel.IsFileListPopulated);
@@ -103,7 +106,7 @@ public void IsFileListPopulated_ReturnsFalseWhenNoFilesExist()
// Arrange - fileList is empty by default
// Act
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
// Assert
Assert.False(viewModel.IsFileListPopulated);
@@ -116,7 +119,7 @@ public void OnFileListChanged_UpdatesIsFileListPopulated()
var fileList = new UniqueObservableCollection(Substitute.For());
_batchConfiguration.FileList.Returns(fileList);
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
Assert.False(viewModel.IsFileListPopulated);
var mediaInfoResult = new MediaInfoResultBuilder()
@@ -145,7 +148,7 @@ public void OnBatchConfigurationVideoTracksChanged_UpdatesVideoTracks()
var videoTracks = new ObservableCollection();
_batchConfiguration.VideoTracks.Returns(videoTracks);
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
Assert.Empty(viewModel.VideoTracks);
var mediaInfoResult = new MediaInfoResultBuilder()
@@ -169,7 +172,7 @@ public void OnBatchConfigurationChanged_UpdatesVideoTracksWhenVideoTracksPropert
var initialTracks = new ObservableCollection();
_batchConfiguration.VideoTracks.Returns(initialTracks);
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
var mediaInfoResult = new MediaInfoResultBuilder()
.WithCreatingLibrary()
@@ -199,7 +202,7 @@ public void OnBatchConfigurationChanged_DoesNotUpdateWhenOtherPropertiesChange()
var videoTracks = new ObservableCollection();
_batchConfiguration.VideoTracks.Returns(videoTracks);
- var viewModel = new VideoViewModel(_languageProvider, _batchConfiguration, _uiPreferences);
+ var viewModel = new VideoViewModel(_logger, _languageProvider, _batchConfiguration, _uiPreferences);
int videoTracksChangedCount = 0;
viewModel.PropertyChanged += (s, e) =>
{