Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions src/MatroskaBatchFlow.Core/Models/FileTrackConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections.ObjectModel;
using MatroskaBatchFlow.Core.Enums;
using MatroskaBatchFlow.Core.Services;

namespace MatroskaBatchFlow.Core.Models;

Expand All @@ -15,26 +14,26 @@ public sealed class FileTrackConfiguration
public string FilePath { get; set; } = string.Empty;

/// <summary>
/// Audio track configurations for this file.
/// Audio track values for this file.
/// </summary>
public ObservableCollection<TrackConfiguration> AudioTracks { get; set; } = [];
public ObservableCollection<FileTrackValues> AudioTracks { get; set; } = [];

/// <summary>
/// Video track configurations for this file.
/// Video track values for this file.
/// </summary>
public ObservableCollection<TrackConfiguration> VideoTracks { get; set; } = [];
public ObservableCollection<FileTrackValues> VideoTracks { get; set; } = [];

/// <summary>
/// Subtitle track configurations for this file.
/// Subtitle track values for this file.
/// </summary>
public ObservableCollection<TrackConfiguration> SubtitleTracks { get; set; } = [];
public ObservableCollection<FileTrackValues> SubtitleTracks { get; set; } = [];

/// <summary>
/// Gets the track list for a specific track type.
/// </summary>
/// <param name="trackType">The track type to retrieve.</param>
/// <returns>Observable collection of track configurations for the specified type.</returns>
public ObservableCollection<TrackConfiguration> GetTrackListForType(TrackType trackType)
/// <returns>Observable collection of track values for the specified type.</returns>
public ObservableCollection<FileTrackValues> GetTrackListForType(TrackType trackType)
{
return trackType switch
{
Expand Down
68 changes: 68 additions & 0 deletions src/MatroskaBatchFlow.Core/Models/FileTrackValues.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using MatroskaBatchFlow.Core.Enums;

namespace MatroskaBatchFlow.Core.Models;

/// <summary>
/// Stores per-file track values that are initially populated from the MediaInfo scan.
/// The original scanned data is always available via <see cref="ScannedTrackInfo"/>.
/// </summary>
/// <remarks>
/// The batch configuration uses a dual-model approach for tracks:
/// <list type="bullet">
/// <item>
/// <see cref="Services.TrackConfiguration"/> - one per track index in the global collection.
/// Carries modification intent (<c>ShouldModify*</c> flags) and the effective values
/// (Name, Language, flags) that should be written, and is shared across all files.
/// </item>
/// <item>
/// <see cref="FileTrackValues"/> - one per track per file in <see cref="FileTrackConfiguration"/>.
/// Represents the scanned/current per-file values and indicates whether a given file actually
/// has a track at a particular index.
/// </item>
/// </list>
/// During command generation, <see cref="Services.MkvPropeditArgumentsGenerator"/> reads both
/// <c>ShouldModify*</c> and the values to write from the global track configuration, and uses
/// <see cref="FileTrackValues"/> only to determine per-file track existence and indexing.
/// </remarks>
public sealed class FileTrackValues
{
/// <summary>
/// The raw track information as returned by MediaInfo.
/// </summary>
public required MediaInfoResult.MediaInfo.TrackInfo ScannedTrackInfo { get; init; }

/// <summary>
/// The type of this track (Audio, Video, Text).
/// </summary>
public TrackType Type { get; init; }

/// <summary>
/// Zero-based index of this track within its type (e.g. 0 = first audio track).
/// </summary>
public int Index { get; init; }

/// <summary>
/// The track name as scanned from the file.
/// </summary>
public string Name { get; set; } = string.Empty;

/// <summary>
/// The track language as scanned from the file.
/// </summary>
public MatroskaLanguageOption Language { get; set; } = MatroskaLanguageOption.Undetermined;

/// <summary>
/// Whether this track has the default flag set.
/// </summary>
public bool Default { get; set; }

/// <summary>
/// Whether this track has the forced flag set.
/// </summary>
public bool Forced { get; set; }

/// <summary>
/// Whether this track is enabled (corresponds to the Matroska FlagEnabled element).
/// </summary>
public bool Enabled { get; set; }
}
4 changes: 2 additions & 2 deletions src/MatroskaBatchFlow.Core/Services/BatchConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ public IList<TrackConfiguration> GetTrackListForType(TrackType trackType)

/// <inheritdoc />
/// <exception cref="InvalidOperationException">Thrown if no per-file track configuration exists for the specified file ID.</exception>
public IList<TrackConfiguration> GetTrackListForFile(Guid fileId, TrackType trackType)
public IList<FileTrackValues> 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))
Expand Down Expand Up @@ -656,7 +656,7 @@ public bool ShouldModifyEnabledFlag
}
}

protected void OnPropertyChanged(string propertyName)
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ namespace MatroskaBatchFlow.Core.Services;
/// Initializes per-file track configurations based on scanned file information.
/// </summary>
/// <remarks>
/// This class orchestrates the creation of individual track configurations for each file:
/// This class orchestrates track initialization for each file:
/// <list type="bullet">
/// <item>Delegates track configuration creation to <see cref="ITrackConfigurationFactory"/></item>
/// <item>Creates per-file <see cref="FileTrackValues"/> directly from scanned track metadata</item>
/// <item>Delegates global <see cref="TrackConfiguration"/> creation to <see cref="ITrackConfigurationFactory"/> when expanding to match maximum track counts</item>
/// <item>Populates file-specific track lists in <see cref="IBatchConfiguration.FileConfigurations"/></item>
/// <item>Updates global track collections to reflect maximum track counts for UI display</item>
/// </list>
/// </remarks>
/// <param name="batchConfig">The batch configuration to be modified.</param>
/// <param name="trackConfigFactory">The factory for creating track configurations.</param>
/// <param name="trackConfigFactory">The factory for creating global track configurations.</param>
/// <param name="languageProvider">The language provider for resolving track language codes.</param>
public class BatchTrackConfigurationInitializer(
IBatchConfiguration batchConfig,
ITrackConfigurationFactory trackConfigFactory) : IBatchTrackConfigurationInitializer
ITrackConfigurationFactory trackConfigFactory,
ILanguageProvider languageProvider) : IBatchTrackConfigurationInitializer
Comment thread
TimGels marked this conversation as resolved.
{
/// <inheritdoc/>
public void Initialize(ScannedFileInfo scannedFile, params TrackType[] trackTypes)
Expand All @@ -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
Expand All @@ -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,
});
}
}

Expand Down
14 changes: 8 additions & 6 deletions src/MatroskaBatchFlow.Core/Services/IBatchConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ namespace MatroskaBatchFlow.Core.Services;
/// <summary>
/// Defines the contract for batch configuration of media files.
/// <br />
/// 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 <see cref="FileTrackValues"/> which carry no modification intent.
/// </summary>
public interface IBatchConfiguration : INotifyPropertyChanged
{
Expand Down Expand Up @@ -51,13 +52,14 @@ public interface IBatchConfiguration : INotifyPropertyChanged
public IList<TrackConfiguration> GetTrackListForType(TrackType trackType);

/// <summary>
/// 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 (<c>ShouldModify*</c>) live on the global <see cref="TrackConfiguration"/> objects returned by <see cref="GetTrackListForType"/>.
/// </summary>
/// <param name="fileId">The ScannedFileInfo.Id (Guid) of the file.</param>
/// <param name="trackType">Type of track to retrieve.</param>
/// <returns>List of track configurations for the specified file and track type.</returns>
public IList<TrackConfiguration> GetTrackListForFile(Guid fileId, TrackType trackType);
/// <returns>List of per-file track values for the specified file and track type.</returns>
public IList<FileTrackValues> GetTrackListForFile(Guid fileId, TrackType trackType);

/// <summary>
/// Migrates file configuration from an old file ID to a new file ID.
Expand Down
7 changes: 7 additions & 0 deletions src/MatroskaBatchFlow.Core/Services/ILanguageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@ public interface ILanguageProvider
public ImmutableList<MatroskaLanguageOption> Languages { get; }

public void LoadLanguages();

/// <summary>
/// Resolves a raw language string from MediaInfo into a typed <see cref="MatroskaLanguageOption"/>.
/// </summary>
/// <param name="languageCode">The language code (e.g., "en", "eng", or "English").</param>
/// <returns>The matching language option, or <see cref="MatroskaLanguageOption.Undetermined"/> if no match found.</returns>
MatroskaLanguageOption Resolve(string? languageCode);
}
17 changes: 17 additions & 0 deletions src/MatroskaBatchFlow.Core/Services/LanguageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,21 @@ public void LoadLanguages()
Languages = [];
}
}

/// <inheritdoc/>
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using MatroskaBatchFlow.Core.Enums;
using Microsoft.Extensions.Logging;

namespace MatroskaBatchFlow.Core.Services;

/// <summary>
/// LoggerMessage definitions for <see cref="MkvPropeditArgumentsGenerator"/>.
/// </summary>
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);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides reusable logic for generating <c>mkvpropedit</c> command-line arguments from an <see cref="IBatchConfiguration"/>.
/// </summary>
public sealed class MkvPropeditArgumentsGenerator : IMkvPropeditArgumentsGenerator
public sealed partial class MkvPropeditArgumentsGenerator(ILogger<MkvPropeditArgumentsGenerator> logger) : IMkvPropeditArgumentsGenerator
{
/// <inheritdoc />
public string[] BuildBatchArguments(IBatchConfiguration batchConfiguration)
Expand Down Expand Up @@ -42,7 +43,7 @@ public string BuildFileArgumentString(ScannedFileInfo file, IBatchConfiguration
/// <param name="batchConfiguration">Contains global title settings and per-file track configurations.</param>
/// <returns> Token array suitable for joining. Returns an empty array if no modifications are requested
/// (to signal "no-op").</returns>
private static string[] BuildFileArgumentTokens(ScannedFileInfo file, IBatchConfiguration batchConfiguration)
private string[] BuildFileArgumentTokens(ScannedFileInfo file, IBatchConfiguration batchConfiguration)
{
var builder = new MkvPropeditArgumentsBuilder();

Expand Down Expand Up @@ -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.
/// </summary>
/// <remarks>
/// Both the modification intent (<c>ShouldModify*</c>) and the actual values to write (Name, Language,
/// flags) are read from the global <see cref="TrackConfiguration"/> at the matching index. Per-file
/// <see cref="FileTrackValues"/> are used only to determine which tracks exist in each file.
/// </remarks>
/// <param name="builder">The accumulating mkvpropedit argument builder.</param>
/// <param name="file">The file being processed.</param>
/// <param name="type">The track type (must map to a Matroska track element).</param>
/// <param name="batchConfig">The batch configuration containing track availability data.</param>
private static void AddTracksForFile(
private void AddTracksForFile(
MkvPropeditArgumentsBuilder builder,
ScannedFileInfo file,
TrackType type,
Expand All @@ -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;
}
Expand All @@ -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);
}
Comment thread
TimGels marked this conversation as resolved.

return tb;
Expand Down
Loading
Loading