diff --git a/BasicNodes/File/PatternReplacer.cs b/BasicNodes/File/PatternReplacer.cs index efb10212..6f4c2d0f 100644 --- a/BasicNodes/File/PatternReplacer.cs +++ b/BasicNodes/File/PatternReplacer.cs @@ -20,7 +20,7 @@ public class PatternReplacer : Node internal bool UnitTest = false; - [KeyValue(1)] + [KeyValue(1, null)] [Required] public List> Replacements{ get; set; } diff --git a/BasicNodes/Tools/WebRequest.cs b/BasicNodes/Tools/WebRequest.cs index e41f05b8..d2add570 100644 --- a/BasicNodes/Tools/WebRequest.cs +++ b/BasicNodes/Tools/WebRequest.cs @@ -61,7 +61,7 @@ public class WebRequest : Node } - [KeyValue(4)] + [KeyValue(4, null)] public List> Headers { get; set; } [TextArea(5)] diff --git a/Emby/MediaManagement/EmbyUpdater.cs b/Emby/MediaManagement/EmbyUpdater.cs index b638bc98..b1a89f48 100644 --- a/Emby/MediaManagement/EmbyUpdater.cs +++ b/Emby/MediaManagement/EmbyUpdater.cs @@ -18,7 +18,7 @@ public class EmbyUpdater: Node [Text(2)] public string AccessToken { get; set; } - [KeyValue(3)] + [KeyValue(3, null)] public List> Mapping { get; set; } public override int Execute(NodeParameters args) diff --git a/Emby/PluginSettings.cs b/Emby/PluginSettings.cs index d9771f84..f46c0b45 100644 --- a/Emby/PluginSettings.cs +++ b/Emby/PluginSettings.cs @@ -14,7 +14,7 @@ [Required] public string AccessToken { get; set; } - [KeyValue(3)] + [KeyValue(3, null)] public List> Mapping { get; set; } } } diff --git a/FileFlows.Plugin.dll b/FileFlows.Plugin.dll index 0a484327..29d801f0 100644 Binary files a/FileFlows.Plugin.dll and b/FileFlows.Plugin.dll differ diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index 731ad06d..efd27326 100644 Binary files a/FileFlows.Plugin.pdb and b/FileFlows.Plugin.pdb differ diff --git a/Plex/MediaManagement/_PlexNode.cs b/Plex/MediaManagement/_PlexNode.cs index be3b6480..3f8e92eb 100644 --- a/Plex/MediaManagement/_PlexNode.cs +++ b/Plex/MediaManagement/_PlexNode.cs @@ -20,7 +20,7 @@ public abstract class PlexNode:Node [Text(2)] public string AccessToken { get; set; } - [KeyValue(3)] + [KeyValue(3, null)] public List> Mapping { get; set; } public override int Execute(NodeParameters args) diff --git a/Plex/PluginSettings.cs b/Plex/PluginSettings.cs index 94d3f6ef..f4ebec41 100644 --- a/Plex/PluginSettings.cs +++ b/Plex/PluginSettings.cs @@ -15,7 +15,7 @@ [Required] public string AccessToken { get; set; } - [KeyValue(3)] + [KeyValue(3, null)] public List> Mapping { get; set; } } } diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderKeepOriginalLanguage.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderKeepOriginalLanguage.cs index 2eada97c..32fa26d9 100644 --- a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderKeepOriginalLanguage.cs +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderKeepOriginalLanguage.cs @@ -102,6 +102,14 @@ public class FfmpegBuilderKeepOriginalLanguage: FfmpegBuilderNode return changes > 0 ? 1 : 2; } + /// + /// Processes the streams + /// + /// the node parameters + /// the streams to process for deletion + /// the original language of the source material + /// the stream type + /// the number of streams changed private int ProcessStreams(NodeParameters args, List streams, string originalLanguage) where T : FfmpegStream { if (streams?.Any() != true) diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderTrackSorter.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderTrackSorter.cs new file mode 100644 index 00000000..85a64d9d --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderTrackSorter.cs @@ -0,0 +1,357 @@ +using System.Globalization; +using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; +using FileFlows.VideoNodes.Helpers; + +namespace FileFlows.VideoNodes.FfmpegBuilderNodes; + +/// +/// FFmpeg Builder: Track Sorter +/// +public class FfmpegBuilderTrackSorter : FfmpegBuilderNode +{ + /// + /// Gets the number of output nodes + /// + public override int Outputs => 2; + + /// + /// Gets the icon + /// + public override string Icon => "fas fa-sort-alpha-down"; + + /// + /// Gets the help URL + /// + public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/track-sorter"; + + /// + /// Gets or sets the stream type + /// + [Select(nameof(StreamTypeOptions), 1)] + public string StreamType { get; set; } + + [KeyValue(1, nameof(SorterOptions))] + [Required] + public List> Sorters { get; set; } + + private static List _StreamTypeOptions; + + /// + /// Gets or sets the stream type options + /// + public static List StreamTypeOptions + { + get + { + if (_StreamTypeOptions == null) + { + _StreamTypeOptions = new List + { + new() { Label = "Audio", Value = "Audio" }, + new() { Label = "Subtitle", Value = "Subtitle" } + }; + } + + return _StreamTypeOptions; + } + } + + private static List _SorterOptions; + + /// + /// Gets or sets the sorter options + /// + public static List SorterOptions + { + get + { + if (_SorterOptions == null) + { + _SorterOptions = new List + { + new() { Label = "Bitrate", Value = "Bitrate" }, + new() { Label = "Bitrate Reversed", Value = "BitrateDesc" }, + new() { Label = "Channels", Value = "Channels" }, + new() { Label = "Channels Reversed", Value = "ChannelsDesc" }, + new() { Label = "Codec", Value = "Codec" }, + new() { Label = "Codec Reversed", Value = "CodecDesc" }, + new() { Label = "Language", Value = "Language" }, + new() { Label = "Language Reversed", Value = "LanguageDesc" }, + }; + } + + return _SorterOptions; + } + } + + /// + /// Executes the flow element + /// + /// the node parameters + /// the next output node + public override int Execute(NodeParameters args) + { + return 1; + } + + /// + /// Processes the streams + /// + /// the logger parameters + /// the streams to process for deletion + /// the stream type + /// if any changes were made + internal bool ProcessStreams(ILogger logger, List streams, int sortIndex = 0) where T : FfmpegStream + { + if (streams?.Any() != true || Sorters?.Any() != true || sortIndex >= Sorters.Count) + return false; + + var orderedStreams = SortStreams(streams); + + // Replace the unsorted items with the sorted ones + for (int i = 0; i < streams.Count; i++) + { + streams[i] = orderedStreams[i]; + } + + return true; + } + + internal List SortStreams(List streams) where T : FfmpegStream + { + if (streams?.Any() != true || Sorters?.Any() != true) + return streams; + + return streams.OrderBy(stream => GetSortKey(stream)) + .ToList(); + } + + private string GetSortKey(T stream) where T : FfmpegStream + { + string sortKey = ""; + + for (int i = 0; i < Sorters.Count; i++) + { + var sortValue = Math.Round(SortValue(stream, Sorters[i])).ToString(); + // Trim the sort value to 15 characters + string trimmedValue = sortValue[..Math.Min(sortValue.Length, 15)]; + + // Pad the trimmed value with left zeros if needed + string paddedValue = trimmedValue.PadLeft(15, '0'); + + // Concatenate the padded value to the sort key + sortKey += paddedValue; + } + + return sortKey; + } + + /// + /// Tests if two lists are the same + /// + /// the original list + /// the reordered list + /// the type of items + /// true if the lists are the same, otherwise false + public bool AreSame(List original, List reordered) where T : FfmpegStream + { + for (int i = 0; i < reordered.Count; i++) + { + if (reordered[i] != original[i]) + { + return false; + } + } + + return true; + } + + + + + + /// + /// Calculates the sort value for a stream property based on the specified sorter. + /// + /// Type of the stream. + /// The stream instance. + /// The key-value pair representing the sorter. + /// The calculated sort value for the specified property and sorter. + public static double SortValue(T stream, KeyValuePair sorter) where T : FfmpegStream + { + string property = sorter.Key; + bool invert = property.EndsWith("Desc"); + if (invert) + property = property[..^4]; // remove "Desc" + + string comparison = sorter.Value; + + var value = property switch + { + nameof(FfmpegStream.Codec) => stream.Codec, + nameof(AudioStream.Bitrate) => (stream is FfmpegAudioStream audioStream) ? audioStream?.Stream?.Bitrate : null, + _ => stream.GetType().GetProperty(property)?.GetValue(stream, null) + }; + + double result; + + if (value != null && value is string == false && string.IsNullOrWhiteSpace(comparison) && + double.TryParse(value.ToString(), out double dblValue)) + { + // invert the bits of dbl value and return that + result = dblValue; + } + else if (IsMathOperation(comparison)) + result = ApplyMathOperation(value.ToString(), comparison) ? 0 : 1; + else if (IsRegex(comparison)) + result = Regex.IsMatch(value.ToString(), comparison, RegexOptions.IgnoreCase) ? 0 : 1; + else if (value != null && double.TryParse(value.ToString(), out double dbl)) + result = dbl; + else + result = string.Equals(value?.ToString() ?? string.Empty, comparison ?? string.Empty, StringComparison.OrdinalIgnoreCase) ? 0 : 1; + + return invert ? InvertBits(result) : result; + } + + /// + /// Adjusts the comparison string by handling common mistakes in units and converting them into full numbers. + /// + /// The original comparison string to be adjusted. + /// The adjusted comparison string with corrected units or the original comparison if no adjustments are made. + private static string AdjustComparisonValue(string comparisonValue) + { + if (string.IsNullOrWhiteSpace(comparisonValue)) + return string.Empty; + + string adjustedComparison = comparisonValue.ToLower().Trim(); + + // Handle common mistakes in units + if (adjustedComparison.EndsWith("mbps")) + { + // Make an educated guess for Mbps to kbps conversion + return adjustedComparison[..^4] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000).ToString( + CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("kbps")) + { + // Make an educated guess for kbps to bps conversion + return adjustedComparison[..^4] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000).ToString( + CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("gb")) + { + // Make an educated guess for GB to bytes conversion + return adjustedComparison[..^2] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * Math.Pow(1024, 3)) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("tb")) + { + // Make an educated guess for TB to bytes conversion + return adjustedComparison[..^2] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * Math.Pow(1024, 4)) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + + return comparisonValue; + } + + /// + /// Inverts the bits of a double value. + /// + /// The double value to invert. + /// The inverted double value. + private static double InvertBits(double value) + { + // Convert the double to a string with 15 characters above the decimal point + string stringValue = Math.Round(value, 0).ToString("F0"); + + // Invert the digits and pad left with zeros + char[] charArray = stringValue.PadLeft(15, '0').ToCharArray(); + for (int i = 0; i < charArray.Length; i++) + { + charArray[i] = (char)('9' - (charArray[i] - '0')); + } + + // Parse the inverted string back to a double + double invertedDouble; + if (double.TryParse(new string(charArray), out invertedDouble)) + { + return invertedDouble; + } + else + { + // Handle parsing error + throw new InvalidOperationException("Failed to parse inverted double string."); + } + } + + /// + /// Checks if the comparison string represents a mathematical operation. + /// + /// The comparison string to check. + /// True if the comparison is a mathematical operation, otherwise false. + private static bool IsMathOperation(string comparison) + { + // Check if the comparison string starts with <=, <, >, >=, ==, or = + return new[] { "<=", "<", ">", ">=", "==", "=" }.Any(comparison.StartsWith); + } + + /// + /// Checks if the comparison string represents a regular expression. + /// + /// The comparison string to check. + /// True if the comparison is a regular expression, otherwise false. + private static bool IsRegex(string comparison) + { + return new[] { "?", "|", "^", "$" }.Any(ch => comparison.Contains(ch)); + } + + /// + /// Applies a mathematical operation to the value based on the specified operation string. + /// + /// The value to apply the operation to. + /// The operation string representing the mathematical operation. + /// True if the mathematical operation is successful, otherwise false. + private static bool ApplyMathOperation(string value, string operation) + { + // This is a basic example; you may need to handle different operators + switch (operation.Substring(0, 2)) + { + case "<=": + return Convert.ToDouble(value) <= Convert.ToDouble(AdjustComparisonValue(operation[2..])); + case ">=": + return Convert.ToDouble(value) >= Convert.ToDouble(AdjustComparisonValue(operation[2..])); + case "==": + return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[2..]))) < 0.05f; + case "!=": + return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[2..]))) > 0.05f; + } + + switch (operation.Substring(0, 1)) + { + case "<": + return Convert.ToDouble(value) < Convert.ToDouble(AdjustComparisonValue(operation[1..])); + case ">": + return Convert.ToDouble(value) > Convert.ToDouble(AdjustComparisonValue(operation[1..])); + case "=": + return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[1..]))) < 0.05f; + } + + return false; + } +} \ No newline at end of file diff --git a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs index 9facadfc..456a2df5 100644 --- a/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs +++ b/VideoNodes/FfmpegBuilderNodes/Models/FfmpegAudioStream.cs @@ -93,6 +93,18 @@ } public override string ToString() - => Stream.ToString(); + { + if (Stream != null) + return Stream.ToString(); + // can be null in unit tests + return string.Join(" / ", new string[] + { + Index.ToString(), + Language, + Codec, + Title, + Channels > 0 ? Channels.ToString("0.0") : null + }.Where(x => string.IsNullOrWhiteSpace(x) == false)); + } } } diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_TrackSorterTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_TrackSorterTests.cs new file mode 100644 index 00000000..1a5ee12c --- /dev/null +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_TrackSorterTests.cs @@ -0,0 +1,462 @@ +#if(DEBUG) + +using FileFlows.VideoNodes.FfmpegBuilderNodes; +using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VideoNodes.Tests; + +namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests; + +[TestClass] +public class FfmpegBuilder_TrackSorterTests +{ + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnSorters() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac" }, + new() { Index = 2, Channels = 2, Language = "fr", Codec = "aac" }, + new() { Index = 3, Channels = 5.1f, Language = "en", Codec = "ac3" }, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new("Language", "en"), + new("Channels", ">=5.1"), + }; + + // Act + var sorted = trackSorter.SortStreams(streams); + + // Assert + Assert.AreEqual(3, sorted[0].Index); + Assert.AreEqual(1, sorted[1].Index); + Assert.AreEqual(2, sorted[2].Index); + + // Additional assertions for logging + Assert.AreEqual("3 / en / ac3 / 5.1", sorted[0].ToString()); + Assert.AreEqual("1 / en / aac / 2.0", sorted[1].ToString()); + Assert.AreEqual("2 / fr / aac / 2.0", sorted[2].ToString()); + } + + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnChannels() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac" }, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "aac" }, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" }, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new("Channels", ">=5.1"), + }; + + // Act + var result = trackSorter.ProcessStreams(logger, streams); + + // Assert + Assert.AreEqual(2, streams[0].Index); + Assert.AreEqual(3, streams[1].Index); + Assert.AreEqual(1, streams[2].Index); + + // Additional assertions for logging + Assert.AreEqual("2 / fr / aac / 5.1", streams[0].ToString()); + Assert.AreEqual("3 / en / ac3 / 7.1", streams[1].ToString()); + Assert.AreEqual("1 / en / aac / 2.0", streams[2].ToString()); + } + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnLanguageThenCodec() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac" }, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "aac" }, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" }, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new("Language", "en"), + new("Codec", "ac3"), + }; + + // Act + var result = trackSorter.ProcessStreams(logger, streams); + + // Assert + Assert.AreEqual(3, streams[0].Index); + Assert.AreEqual(1, streams[1].Index); + Assert.AreEqual(2, streams[2].Index); + + // Additional assertions for logging + Assert.AreEqual("3 / en / ac3 / 7.1", streams[0].ToString()); + Assert.AreEqual("1 / en / aac / 2.0", streams[1].ToString()); + Assert.AreEqual("2 / fr / aac / 5.1", streams[2].ToString()); + } + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnCustomMathOperation() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac" }, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "aac" }, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" }, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new("Channels", ">4.0"), + }; + + // Act + var result = trackSorter.ProcessStreams(logger, streams); + + // Assert + Assert.AreEqual(2, streams[0].Index); + Assert.AreEqual(3, streams[1].Index); + Assert.AreEqual(1, streams[2].Index); + + // Additional assertions for logging + Assert.AreEqual("2 / fr / aac / 5.1", streams[0].ToString()); + Assert.AreEqual("3 / en / ac3 / 7.1", streams[1].ToString()); + Assert.AreEqual("1 / en / aac / 2.0", streams[2].ToString()); + } + + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnRegex() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac" }, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "aac" }, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" }, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new ("Language", "^en$"), + }; + + // Act + var result = trackSorter.ProcessStreams(logger, streams); + + // Assert + Assert.AreEqual(1, streams[0].Index); + Assert.AreEqual(3, streams[1].Index); + Assert.AreEqual(2, streams[2].Index); + + // Additional assertions for logging + Assert.AreEqual("1 / en / aac / 2.0", streams[0].ToString()); + Assert.AreEqual("3 / en / ac3 / 7.1", streams[1].ToString()); + Assert.AreEqual("2 / fr / aac / 5.1", streams[2].ToString()); + } + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnMultipleSorters() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac" }, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "aac" }, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" }, + new() { Index = 4, Channels = 2, Language = "fr", Codec = "ac3" }, + new() { Index = 5, Channels = 5.1f, Language = "en", Codec = "ac3" }, + new() { Index = 6, Channels = 7.1f, Language = "fr", Codec = "aac" }, + new() { Index = 7, Channels = 2, Language = "en", Codec = "ac3" }, + new() { Index = 8, Channels = 5.1f, Language = "fr", Codec = "ac3" }, + new() { Index = 9, Channels = 7.1f, Language = "en", Codec = "aac" }, + new() { Index = 10, Channels = 2, Language = "fr", Codec = "aac" }, + }; + + // Mock sorters by different properties and custom math operations + trackSorter.Sorters = new List> + { + new("Language", "en"), + new("Channels", ">4.0"), + new("Codec", "ac3") + }; + + // Act + var result = trackSorter.ProcessStreams(logger, streams); + + // Assert + + // en + Assert.AreEqual(3, streams[0].Index); + Assert.AreEqual(5, streams[1].Index); + Assert.AreEqual(9, streams[2].Index); + Assert.AreEqual(7, streams[3].Index); + Assert.AreEqual(1, streams[4].Index); + + // >5 non en, 2, 6, 8 , 8 first cause ac3 + Assert.AreEqual(8, streams[5].Index); + Assert.AreEqual(2, streams[6].Index); + Assert.AreEqual(6, streams[7].Index); + + + Assert.AreEqual(4, streams[8].Index); + Assert.AreEqual(10, streams[9].Index); + + + // Additional assertions for logging + // Add assertions for the log messages if needed + } + + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnBitrate() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac", Stream = new AudioStream() { Bitrate = 140_000_000 }}, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "dts" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 20_000_000 }}, + new() { Index = 4, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new ("Bitrate", "") + }; + + // Act + var sorted = trackSorter.SortStreams(streams); + + // Assert + Assert.AreEqual(3, sorted[0].Index); + Assert.AreEqual(1, sorted[1].Index); + Assert.AreEqual(2, sorted[2].Index); + Assert.AreEqual(4, sorted[3].Index); + + // Additional assertions for logging + Assert.AreEqual(20_000_000, sorted[0].Stream.Bitrate); + Assert.AreEqual(140_000_000, sorted[1].Stream.Bitrate); + Assert.AreEqual(200_000_000, sorted[2].Stream.Bitrate); + Assert.AreEqual(200_000_000, sorted[3].Stream.Bitrate); + } + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnChannelsAsc() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac", Stream = new AudioStream() { Bitrate = 140_000_000 }}, + new() { Index = 2, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 20_000_000 }}, + new() { Index = 3, Channels = 5.1f, Language = "fr", Codec = "dts" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + new() { Index = 4, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new ("Channels", "") + }; + + // Act + var sorted = trackSorter.SortStreams(streams); + + // Assert + Assert.AreEqual(1, sorted[0].Index); + Assert.AreEqual(3, sorted[1].Index); + Assert.AreEqual(2, sorted[2].Index); + Assert.AreEqual(4, sorted[3].Index); + + // Additional assertions for logging + Assert.AreEqual(2.0f, sorted[0].Channels); + Assert.AreEqual(5.1f, sorted[1].Channels); + Assert.AreEqual(7.1f, sorted[2].Channels); + Assert.AreEqual(7.1f, sorted[3].Channels); + } + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnChannelsDesc() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac", Stream = new AudioStream() { Bitrate = 140_000_000 }}, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "dts" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 20_000_000 }}, + new() { Index = 4, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new ("ChannelsDesc", "") + }; + + // Act + var sorted = trackSorter.SortStreams(streams); + + // Assert + Assert.AreEqual(3, sorted[0].Index); + Assert.AreEqual(4, sorted[1].Index); + Assert.AreEqual(2, sorted[2].Index); + Assert.AreEqual(1, sorted[3].Index); + + // Additional assertions for logging + Assert.AreEqual(7.1f, sorted[0].Channels); + Assert.AreEqual(7.1f, sorted[1].Channels); + Assert.AreEqual(5.1f, sorted[2].Channels); + Assert.AreEqual(2.0f, sorted[3].Channels); + } + + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnBitrateInvert() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac", Stream = new AudioStream() { Bitrate = 140_000_000 }}, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "dts" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 20_000_000 }}, + new() { Index = 4, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new ("BitrateDesc", "") + }; + + // Act + var sorted = trackSorter.SortStreams(streams); + + // Assert + Assert.AreEqual(2, sorted[0].Index); + Assert.AreEqual(4, sorted[1].Index); + Assert.AreEqual(1, sorted[2].Index); + Assert.AreEqual(3, sorted[3].Index); + + // Additional assertions for logging + Assert.AreEqual(200_000_000, sorted[0].Stream.Bitrate); + Assert.AreEqual(200_000_000, sorted[1].Stream.Bitrate); + Assert.AreEqual(140_000_000, sorted[2].Stream.Bitrate); + Assert.AreEqual(20_000_000, sorted[3].Stream.Bitrate); + } + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnBitrateAndCodec() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac", Stream = new AudioStream() { Bitrate = 140_000_000 }}, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "dts" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 20_000_000 }}, + new() { Index = 4, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new ("BitrateDesc", ""), + new ("Codec", "ac3"), + }; + + // Act + var sorted = trackSorter.SortStreams(streams); + + // Assert + Assert.AreEqual(4, sorted[0].Index); + Assert.AreEqual(2, sorted[1].Index); + Assert.AreEqual(1, sorted[2].Index); + Assert.AreEqual(3, sorted[3].Index); + + // Additional assertions for logging + Assert.AreEqual(200_000_000, sorted[0].Stream.Bitrate); + Assert.AreEqual(200_000_000, sorted[1].Stream.Bitrate); + Assert.AreEqual(140_000_000, sorted[2].Stream.Bitrate); + Assert.AreEqual(20_000_000, sorted[3].Stream.Bitrate); + + Assert.AreEqual("ac3", sorted[0].Codec); + Assert.AreEqual("dts", sorted[1].Codec); + Assert.AreEqual("aac", sorted[2].Codec); + Assert.AreEqual("ac3", sorted[3].Codec); + } + + [TestMethod] + public void ProcessStreams_SortsStreamsBasedOnBitrateUnit() + { + // Arrange + var logger = new TestLogger(); + var trackSorter = new FfmpegBuilderTrackSorter(); + List streams = new List + { + new() { Index = 1, Channels = 2, Language = "en", Codec = "aac", Stream = new AudioStream() { Bitrate = 140_000_000 }}, + new() { Index = 2, Channels = 5.1f, Language = "fr", Codec = "dts" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + new() { Index = 3, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 20_000_000 }}, + new() { Index = 4, Channels = 7.1f, Language = "en", Codec = "ac3" , Stream = new AudioStream() { Bitrate = 200_000_000 }}, + }; + + // Mock sorters by different properties + trackSorter.Sorters = new List> + { + new ("Bitrate", ">=150Mbps") + }; + + // Act + var sorted = trackSorter.SortStreams(streams); + + // Assert + Assert.AreEqual(2, sorted[0].Index); + Assert.AreEqual(4, sorted[1].Index); + Assert.AreEqual(1, sorted[2].Index); + Assert.AreEqual(3, sorted[3].Index); + + // Additional assertions for logging + Assert.AreEqual(200_000_000, sorted[0].Stream.Bitrate); + Assert.AreEqual(200_000_000, sorted[1].Stream.Bitrate); + Assert.AreEqual(140_000_000, sorted[2].Stream.Bitrate); + Assert.AreEqual(20_000_000, sorted[3].Stream.Bitrate); + } +} + +#endif \ No newline at end of file diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 4fd38a00..6b66d8f6 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -365,6 +365,22 @@ "MakeFirst-Help": "If the original language track should also become the first track." } }, + "FfmpegBuilderTrackSorter": { + "Label": "FFMPEG Builder: Track Sorter", + "Outputs": { + "1": "Tracks have been modified", + "2": "No tracks have been changed" + }, + "Description": "This flow element sorts tracks based on sorting options set by the user.", + "Fields": { + "StreamType": "Type", + "StreamType-Help": "The type of tracks that should be updated", + "Sorters": "Sorters", + "Sorters-Help": "Add one or more sorting options to sort the tracks by.", + "SortersKey": "Field", + "SortersValue": "Value" + } + }, "FfmpegBuilderSubtitleClearDefault": { "Label": "FFMPEG Builder: Subtitle Clear Default", "Description": "This node will clear the default flag from subtitles.",