FF-1266 - added set titles flow element

This commit is contained in:
John Andrews
2024-02-04 11:16:06 +13:00
parent 273c02237c
commit 8a7a5420d1
3 changed files with 612 additions and 0 deletions

View File

@@ -0,0 +1,305 @@
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
/// <summary>
/// FFmpeg Builder flow element to set a tracks title
/// </summary>
public class FfmpegBuilderSetTrackTitles: FfmpegBuilderNode
{
/// <summary>
/// Gets the help URL for the flow element
/// </summary>
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/set-track-titles";
/// <summary>
/// Gets the number of outputs of the flow element
/// </summary>
public override int Outputs => 2;
/// <summary>
/// Gets the icon of the flow element
/// </summary>
public override string Icon => "fas fa-heading";
/// <summary>
/// Gets or sets the stream type
/// </summary>
[Select(nameof(StreamTypeOptions), 1)]
public string StreamType { get; set; }
private static List<ListOption> _StreamTypeOptions;
/// <summary>
/// Gets the stream options to show in the UI
/// </summary>
public static List<ListOption> StreamTypeOptions
{
get
{
if (_StreamTypeOptions == null)
{
_StreamTypeOptions = new List<ListOption>
{
new () { Label = "Audio", Value = "Audio" },
new () { Label = "Subtitle", Value = "Subtitle" },
new () { Label = "Both", Value = "Both" },
};
}
return _StreamTypeOptions;
}
}
/// <summary>
/// Gets or sets the format for the titles
/// </summary>
[Text(2)]
[DefaultValue("lang / codec / channels")]
public string Format { get; set; }
/// <summary>
/// Gets or sets the separator used between fields
/// </summary>
[Text(3)]
[DefaultValue(" / ")]
public string Separator { get; set; }
/// <summary>
/// Gets or sets if commentary should be left alone
/// </summary>
[Select(nameof(CommentaryFormatOptions), 4)]
[DefaultValue("Commentary")]
public string CommentaryFormat { get; set; }
private static List<ListOption> _CommentaryFormatOptions;
/// <summary>
/// Gets the commentary format options to show in the UI
/// </summary>
public static List<ListOption> CommentaryFormatOptions
{
get
{
if (_CommentaryFormatOptions == null)
{
_CommentaryFormatOptions = new List<ListOption>
{
new () { Label = "Same as format", Value = "" },
new () { Label = "Leave Alone", Value = "Leave" },
new () { Label = "Set to 'Commentary'", Value = "Commentary" },
};
}
return _CommentaryFormatOptions;
}
}
/// <summary>
/// Executes the flow element
/// </summary>
/// <param name="args">the flow parameters</param>
/// <returns>the flow output to call next</returns>
public override int Execute(NodeParameters args)
{
int changes = 0;
if (string.IsNullOrEmpty(StreamType) || StreamType is "Audio" or "Both")
{
foreach(var track in Model.AudioStreams)
{
bool isCommentary = IsCommentary(track.Title, track.Language)
|| IsCommentary(track.Stream?.Title, track.Stream?.Language);
string originalTitle = track.Title;
if (isCommentary && string.IsNullOrWhiteSpace(CommentaryFormat) == false)
{
if (CommentaryFormat == "Leave")
continue;
track.Title = "Commentary";
}
else
{
track.Title = FormatTitle(Format, Separator,
track.Language?.EmptyAsNull() ?? track.Stream?.Language,
track.Codec?.EmptyAsNull() ?? track.Stream?.Codec,
track.IsDefault,
track.Stream?.Bitrate ?? 0,
track.Channels > 0 ? track.Channels : track.Stream?.Channels ?? 0,
track.Stream?.SampleRate ?? 0
);
}
if (originalTitle == track.Title)
continue;
args.Logger?.ILog($"Title changed from '{(originalTitle ?? string.Empty)}' to '{track.Title}'");
track.ForcedChange = true;
++changes;
}
}
if (StreamType is "Subtitle" or "Both")
{
foreach(var track in Model.SubtitleStreams)
{
bool isCommentary = IsCommentary(track.Title, track.Language)
|| IsCommentary(track.Stream?.Title, track.Stream?.Language);
string originalTitle = track.Title;
if (isCommentary && string.IsNullOrWhiteSpace(CommentaryFormat) == false)
{
if (CommentaryFormat == "Leave")
continue;
track.Title = "Commentary";
}
else
{
track.Title = FormatTitle(Format, Separator,
track.Language?.EmptyAsNull() ?? track.Stream?.Language,
track.Codec?.EmptyAsNull() ?? track.Stream?.Codec,
track.IsDefault,
track.Stream?.Bitrate ?? 0,
0,
0,
track.IsForced
);
}
if (originalTitle == track.Title)
continue;
args.Logger?.ILog($"Title changed from '{(originalTitle ?? string.Empty)}' to '{track.Title}'");
track.ForcedChange = true;
++changes;
}
}
if (changes == 0)
{
args.Logger?.ILog("No changes made");
return 2;
}
args.Logger?.ILog("Changes were made");
return 1;
}
/// <summary>
/// Tests if a title is a commentary track
/// </summary>
/// <param name="title">the title</param>
/// <param name="title">the language</param>
/// <returns>true if commentary</returns>
private static bool IsCommentary(string title, string language)
=> title?.ToLowerInvariant()?.Contains("comment") == true || language?.ToLowerInvariant()?.Contains("comment") == true;
/// <summary>
/// Formats a string for the title
/// </summary>
/// <param name="formatter">the string formatting to use</param>
/// <param name="separator">the separator character that is used</param>
/// <param name="language">the language of the track</param>
/// <param name="codec">the codec of the track</param>
/// <param name="isDefault">if the track is the default or not</param>
/// <param name="bitrate">the bitrate</param>
/// <param name="channels">the number of channels</param>
/// <param name="sampleRate">the sample rate</param>
/// <param name="isForced">if the track is forced or not</param>
/// <returns>the formatted string</returns>
public static string FormatTitle(string formatter, string separator, string language, string codec, bool isDefault, float bitrate = 0,
float channels = 0, int sampleRate = 0, bool isForced = false)
{
if (string.IsNullOrWhiteSpace(formatter))
return string.Empty;
string english = LanguageHelper.GetEnglishFor(language) ?? language ?? string.Empty;
string iso2 = LanguageHelper.GetIso2Code(language) ?? language ?? string.Empty;
string iso1 = LanguageHelper.GetIso1Code(language) ?? language ?? string.Empty;
formatter = Replace(formatter, "lang!", english.ToUpperInvariant());
formatter = Replace(formatter, "lang-iso2!", iso2.ToUpperInvariant());
formatter = Replace(formatter, "lang-iso1!", iso1.ToUpperInvariant());
formatter = Replace(formatter, "!lang", english.ToLowerInvariant());
formatter = Replace(formatter, "!lang-iso2", iso2.ToLowerInvariant());
formatter = Replace(formatter, "!lang-iso1", iso1.ToLowerInvariant());
formatter = Replace(formatter, "lang", english);
formatter = Replace(formatter, "lang-iso2", iso2);
formatter = Replace(formatter, "lang-iso1", iso1);
formatter = Replace(formatter, "!codec", codec.ToLowerInvariant());
formatter = Replace(formatter, "!codec!", codec);
formatter = Replace(formatter, "codec", codec.ToUpperInvariant());
formatter = Replace(formatter, "default", isDefault ? "Default" : string.Empty);
formatter = Replace(formatter, "forced", isForced ? "Forced" : string.Empty);
formatter = Replace(formatter, "channels", Math.Abs(channels - 1) < 0.05f ? "Mono" :
Math.Abs(channels - 2) < 0.05f ? "Stereo" :
channels > 0 ? channels.ToString("0.0") : null);
formatter = Replace(formatter, "bitrate", bitrate < 1 ? null : ((bitrate / 1000f).ToString("0.0").Replace(".0", string.Empty) + "Kbps"));
formatter = Replace(formatter, "samplerate", sampleRate < 1 ? null : ((sampleRate / 1000f).ToString("0.0").Replace(".0", string.Empty) + "kHz"));
formatter = Replace(formatter, "sample-rate", sampleRate < 1 ? null : ((sampleRate / 1000f).ToString("0.0").Replace(".0", string.Empty) + "kHz"));
formatter = Replace(formatter, "sample", sampleRate < 1 ? null : ((sampleRate / 1000f).ToString("0.0").Replace(".0", string.Empty) + "kHz"));
// Remove standalone separators
if(string.IsNullOrWhiteSpace(separator.Trim()) == false)
{
int count = 0;
while (formatter.EndsWith(separator) && ++count < 100)
formatter = formatter[..^separator.Length];
count = 0;
while (formatter.StartsWith(separator) && ++count < 100)
formatter = formatter[separator.Length..];
count = 0;
while (formatter.Contains(separator + separator) && ++count < 100)
{
formatter = formatter.Replace((separator + separator), separator);
formatter = formatter.Replace(" ", " ");
}
count = 0;
while (formatter.EndsWith(separator.TrimEnd()) && ++count < 100)
formatter = formatter[..^separator.TrimEnd().Length];
count = 0;
while (formatter.StartsWith(separator.TrimStart()) && ++count < 100)
formatter = formatter[separator.TrimStart().Length..];
count = 0;
while (formatter.Contains((separator + separator).TrimStart()) && ++count < 100)
{
formatter = formatter.Replace((separator + separator).TrimStart(), separator);
formatter = formatter.Replace(" ", " ");
}
count = 0;
while (formatter.Contains((separator + separator).TrimEnd()) && ++count < 100)
{
formatter = formatter.Replace((separator + separator).TrimEnd(), separator);
formatter = formatter.Replace(" ", " ");
}
// Handle whitespace around two separators
string pattern = $@"{Regex.Escape(separator.Trim())}\s*{Regex.Escape(separator.Trim())}";
count = 0;
while (Regex.IsMatch(formatter, pattern) && ++count < 100)
{
formatter = Regex.Replace(formatter, pattern, separator);
}
count = 0;
while (formatter.Contains(" ") && ++count < 100)
{
formatter = formatter.Replace(" ", " ");
}
}
if (formatter.Trim() == separator.Trim())
return string.Empty;
return formatter.Trim();
string Replace(string input, string field, string value)
{
if (string.IsNullOrEmpty(value))
{
string pattern = $@"(?<=\b{Regex.Escape(separator)}|\s*|^){Regex.Escape(field)}(?=\s*\b|$)";
input = Regex.Replace(input, pattern, "", RegexOptions.IgnoreCase); // Remove the field
input = Regex.Replace(input, $@"\s*{Regex.Escape(separator)}\s*", separator); // Remove extra spaces around separators
}
else
{
string pattern = $@"(?<=\b{Regex.Escape(separator)}|\s*|^){Regex.Escape(field)}(?=\s*\b|$)";
input = Regex.Replace(input, pattern, value, RegexOptions.IgnoreCase);
}
return input;
}
}
}

View File

@@ -0,0 +1,289 @@
#if(DEBUG)
using FileFlows.VideoNodes.FfmpegBuilderNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests;
/// <summary>
/// Tests for track reorders
/// </summary>
[TestClass]
public class FFmpegBuilder_SetTrackTtitlesTests
{
[TestMethod]
public void FormatTitle_DefaultCase_Success()
{
// Arrange
string formatter = "Track: lang / codec / channels / default / bitrate / samplerate";
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 2.0f;
int sampleRate = 44100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English / AAC / Stereo / Default / 128Kbps / 44.1kHz", result);
}
[TestMethod]
public void FormatTitle_EmptyFormatter_ReturnsEmptyString()
{
// Arrange
string formatter = "";
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 2.0f;
int sampleRate = 44100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("", result);
}
[TestMethod]
public void FormatTitle_NullFormatter_ReturnsEmptyString()
{
// Arrange
string formatter = null;
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 2.0f;
int sampleRate = 44100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("", result);
}
[TestMethod]
public void FormatTitle_MonoChannel_Success()
{
// Arrange
string formatter = "Track: lang / channels / codec";
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 1.0f; // Mono
int sampleRate = 44_100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English / Mono / AAC", result);
}
[TestMethod]
public void FormatTitle_StereoChannel_Success()
{
// Arrange
string formatter = "Track: lang / channels / codec";
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 2.0f; // Stereo
int sampleRate = 44_100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English / Stereo / AAC", result);
}
[TestMethod]
public void FormatTitle_ZeroBitrateAndSampleRate_Success()
{
// Arrange
string formatter = "Track: lang / codec / bitrate / samplerate";
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 0;
float channels = 2.0f;
int sampleRate = 0;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English / AAC", result);
}
[TestMethod]
public void FormatTitle_EmptyValues_Success()
{
// Arrange
string formatter = "Track: lang / codec / default / forced / bitrate / samplerate";
string separator = " / ";
string language = string.Empty;
string codec = string.Empty;
bool isDefault = false;
float bitrate = 0;
float channels = 2.0f;
int sampleRate = 0;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track:", result);
}
[TestMethod]
public void FormatTitle_MissingSampleRateAndForced_Success()
{
// Arrange
string formatter = "Track: lang / codec / default / bitrate";
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 2.0f;
int sampleRate = 0; // Missing sample rate
bool isForced = false; // Missing forced
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English / AAC / Default / 128Kbps", result);
}
[TestMethod]
public void FormatTitle_AllValuesSet_Success()
{
// Arrange
string formatter = "Track: lang / codec / default / forced / bitrate / channels / sample-rate";
string separator = " / ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 2.0f;
int sampleRate = 44_100;
bool isForced = true;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English / AAC / Default / Forced / 128Kbps / Stereo / 44.1kHz", result);
}
[TestMethod]
public void FormatTitle_OnlyForcedSet_Success()
{
// Arrange
string formatter = "lang / codec / default / forced / bitrate / channels / sample-rate";
string separator = " / ";
string language = string.Empty; // Default value
string codec = string.Empty; // Default value
bool isDefault = false; // Default value
float bitrate = 0; // Default value
float channels = 0; // Default value
int sampleRate = 0; // Default value
bool isForced = true; // Only Forced is set to true
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Forced", result);
}
[TestMethod]
public void FormatTitle_SeparatorDash_Success()
{
// Arrange
string formatter = "Track: lang-codec-default-forced-bitrate-channels-sample-rate";
string separator = "-";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 2.0f;
int sampleRate = 44_100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English-AAC-Default-128Kbps-Stereo-44.1kHz", result);
}
[TestMethod]
public void FormatTitle_SeparatorBackslash_Success()
{
// Arrange
string formatter = "Track: lang\\codec\\default\\forced\\bitrate\\channels\\samplerate";
string separator = "\\";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 1.000445f;
int sampleRate = 44_100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English\\AAC\\Default\\128Kbps\\Mono\\44.1kHz", result);
}
[TestMethod]
public void FormatTitle_SeparatorSpace_Success()
{
// Arrange
string formatter = "Track: lang codec default forced bitrate channels samplerate";
string separator = " ";
string language = "English";
string codec = "AAC";
bool isDefault = true;
float bitrate = 128_000;
float channels = 5.0999996f;
int sampleRate = 44_100;
bool isForced = false;
// Act
string result = FfmpegBuilderSetTrackTitles.FormatTitle(formatter, separator, language, codec, isDefault, bitrate, channels, sampleRate, isForced);
// Assert
Assert.AreEqual("Track: English AAC Default 128Kbps 5.1 44.1kHz", result);
}
}
#endif

View File

@@ -546,6 +546,24 @@
"Index-Help": "The index of the track to set as default, only used if language is not specified."
}
},
"FfmpegBuilderSetTrackTitles": {
"Label": "FFMPEG Builder: Set Track Titles",
"Description": "Set track titles based on a formatter in the FFmpeg builder.",
"Outputs": {
"1": "Track titles updated",
"2": "No track titles were effected"
},
"Fields": {
"StreamType": "Type",
"StreamType-Help": "The type of track to set as default in the FFMPEG Builder",
"Format": "Format",
"Format-Help": "The formatter to use, see _Help_ for more information.",
"Separator": "Separator",
"Separator-Help": "The string separator used between format values, this is need to ensure no empty values are kept.",
"CommentaryFormat": "Commentary",
"CommentaryFormat-Help": "How commentary tracks should be treated, you can use the same formatter as other tracks or use a different method."
}
},
"FfmpegBuilderVideo10Bit": {
"Label": "FFMPEG Builder: Video 10 Bit",
"Description": "Sets FFMPEG Builder to encode the video streams in 10 Bit",