FF-1516: Added more options to FFmpeg Builder Subtitle Track Merge

This commit is contained in:
John Andrews
2024-05-22 13:19:09 +12:00
parent 0484817ad1
commit 12ffe4bd56
7 changed files with 276 additions and 62 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -1,20 +1,31 @@
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
using System.IO;
using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
/// <summary>
/// Merges a subtitle into the FFmpeg Builder model
/// </summary>
public class FfmpegBuilderSubtitleTrackMerge : FfmpegBuilderNode
{
/// <inheritdoc />
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/subtitle-track-merge";
/// <inheritdoc />
public override string Icon => "fas fa-comment-medical";
/// <inheritdoc />
public override int Outputs => 2;
/// <summary>
/// Subtitles to include
/// </summary>
[Checklist(nameof(Options), 1)]
[Required]
public List<string> Subtitles { get; set; }
private static List<ListOption> _Options;
private static List<ListOption>? _Options;
/// <summary>
/// Options for the subtitles
/// </summary>
public static List<ListOption> Options
{
get
@@ -36,16 +47,57 @@ public class FfmpegBuilderSubtitleTrackMerge : FfmpegBuilderNode
}
}
/// <summary>
/// Gets or sets if the source directory should be used or the working directory
/// </summary>
[Boolean(2)]
[DefaultValue(true)]
public bool UseSourceDirectory { get; set; } = true;
/// <summary>
/// Gets or sets if the subtitle must match the filename
/// </summary>
[Boolean(3)]
public bool MatchFilename { get; set; }
/// <summary>
/// Gets or sets the pattern to use
/// </summary>
[TextVariable(4)]
[ConditionEquals(nameof(MatchFilename), false)]
public string Pattern { get; set; }
/// <summary>
/// Gets or sets the title of the subtitle
/// </summary>
[TextVariable(5)]
public string Title { get; set; }
/// <summary>
/// Gets or sets the language
/// </summary>
[TextVariable(6)]
public string Language { get; set; }
/// <summary>
/// Gets or sets if the subtitle is forced
/// </summary>
[Boolean(7)]
public bool Forced { get; set; }
/// <summary>
/// Gets or sets if the subtitle is to be marked as the default
/// </summary>
[Boolean(8)]
public bool Default { get; set; }
[Boolean(4)]
/// <summary>
/// Gets or sets if the subtitle should be deleted afterwards
/// </summary>
[Boolean(9)]
public bool DeleteAfterwards { get; set; }
/// <inheritdoc />
public override int Execute(NodeParameters args)
{
var dir = UseSourceDirectory ? FileHelper.GetDirectory(args.LibraryFileName) : args.TempPath;
@@ -66,6 +118,8 @@ public class FfmpegBuilderSubtitleTrackMerge : FfmpegBuilderNode
args.Logger?.ILog("Failed getting files: "+ files.Error);
return 2;
}
string? pattern = args.ReplaceVariables(Pattern ?? string.Empty, stripMissing: true).EmptyAsNull();
foreach (var file in files.ValueOrDefault ?? new string[] {})
{
@@ -93,19 +147,34 @@ public class FfmpegBuilderSubtitleTrackMerge : FfmpegBuilderNode
language = lang2;
forced = forced1 || forced2;
}
else if (pattern != null)
{
string filename = new FileInfo(file).Name;
if (Regex.IsMatch(filename, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) == false)
{
args.Logger?.ILog("Does not match pattern: " + filename);
continue;
}
args.Logger?.ILog("Matches pattern: " + filename);
}
string subTitle = language;
language = language?.EmptyAsNull() ?? args.ReplaceVariables(Language ?? string.Empty, stripMissing: true);
forced |= Forced;
string subTitle = args.ReplaceVariables(Title ?? string.Empty, stripMissing: true)?.EmptyAsNull() ?? language ?? string.Empty;
language = LanguageHelper.GetIso2Code(language.Split(' ').First()); // remove any SDH etc
args.Logger.ILog("Adding file: " + file + " [" + ext + "]" + (string.IsNullOrEmpty(language) == false ? " (Language: " + language + ")" : ""));
this.Model.InputFiles.Add(new InputFile(file) { DeleteAfterwards = this.DeleteAfterwards });
if (string.IsNullOrEmpty(subTitle))
subTitle = FileHelper.GetShortFileName(file).Replace("." + ext, "");
this.Model.SubtitleStreams.Add(new FfmpegSubtitleStream
{
Title = subTitle,
Language = string.IsNullOrEmpty(language) ? null : Regex.Replace(language, @" \([\w]+\)$", string.Empty).Trim(),
Language = string.IsNullOrEmpty(language) ? null : Regex.Replace(language, @" \([\w]+\)$", string.Empty).Trim(),
IsDefault = Default,
IsForced = forced,
Stream = new SubtitleStream()
{
InputFileIndex = this.Model.InputFiles.Count - 1,
@@ -113,6 +182,7 @@ public class FfmpegBuilderSubtitleTrackMerge : FfmpegBuilderNode
Language = language,
Forced = forced,
Title = subTitle,
Default = Default,
Codec = ext,
IndexString = (this.Model.InputFiles.Count - 1) + ":s:0"
},
@@ -126,6 +196,14 @@ public class FfmpegBuilderSubtitleTrackMerge : FfmpegBuilderNode
return count > 0 ? 1 : 2;
}
/// <summary>
/// Checks if the filename matches
/// </summary>
/// <param name="input">the input file</param>
/// <param name="other">the other file to check</param>
/// <param name="languageCode">the language code found in the subtitle</param>
/// <param name="forced">if the subtitle is detected as forced</param>
/// <returns>true if it matches, otherwise false</returns>
internal bool FilenameMatches(string input, string other, out string languageCode, out bool forced)
{
languageCode = string.Empty;

View File

@@ -0,0 +1,102 @@
using VideoNodes.Tests;
#if(DEBUG)
namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests;
using FileFlows.VideoNodes.FfmpegBuilderNodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
/// <summary>
/// Tests for FFmpeg Builder Subtitle Track Merge
/// </summary>
[TestClass]
public class FFmpegBuild_SubtitleTrackMergeTests : TestBase
{
/// <summary>
/// Tests a subtitle using a pattern
/// </summary>
[TestMethod]
public void PatternTest()
{
var args = new NodeParameters(TestFile_Subtitle, Logger, false, TestPath, new LocalFileService())
{
LibraryFileName = TestFile_Subtitle
};
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
var vf = new VideoFile();
vf.PreExecute(args);
Assert.AreEqual(1, vf.Execute(args));
var ffmpegBuilderStart = new FfmpegBuilderStart();
ffmpegBuilderStart.PreExecute(args);
Assert.AreEqual(1, ffmpegBuilderStart.Execute(args));
int currentSubs = ffmpegBuilderStart.GetModel().SubtitleStreams.Count;
var ele = new FfmpegBuilderSubtitleTrackMerge();
ele.Subtitles = ["srt", "sub", "sup", "ass"];
ele.Pattern = "^other";
ele.Title = "Other Subtitle";
ele.Language = "fre";
ele.Default = true;
ele.Forced = true;
ele.PreExecute(args);
Assert.AreEqual(1, ele.Execute(args));
int newSubs = ffmpegBuilderStart.GetModel().SubtitleStreams.Count;
Assert.AreEqual(currentSubs + 1, newSubs);
var newSub = ffmpegBuilderStart.GetModel().SubtitleStreams.Last();
Assert.AreEqual("Other Subtitle", newSub.Title);
Assert.AreEqual("fre", newSub.Language);
Assert.IsTrue(newSub.IsDefault);
Assert.IsTrue(newSub.IsForced);
}
/// <summary>
/// Tests a subtitle using file matches
/// </summary>
[TestMethod]
public void FileMatches()
{
var args = new NodeParameters(TestFile_Subtitle, Logger, false, TestPath, new LocalFileService())
{
LibraryFileName = TestFile_Subtitle
};
args.GetToolPathActual = (string tool) => FfmpegPath;
args.TempPath = TempPath;
var vf = new VideoFile();
vf.PreExecute(args);
Assert.AreEqual(1, vf.Execute(args));
var ffmpegBuilderStart = new FfmpegBuilderStart();
ffmpegBuilderStart.PreExecute(args);
Assert.AreEqual(1, ffmpegBuilderStart.Execute(args));
int currentSubs = ffmpegBuilderStart.GetModel().SubtitleStreams.Count;
var ele = new FfmpegBuilderSubtitleTrackMerge();
ele.Subtitles = ["srt", "sub", "sup", "ass"];
ele.MatchFilename = true;
ele.PreExecute(args);
Assert.AreEqual(1, ele.Execute(args));
int newSubs = ffmpegBuilderStart.GetModel().SubtitleStreams.Count;
Assert.AreEqual(currentSubs + 1, newSubs);
var newSub = ffmpegBuilderStart.GetModel().SubtitleStreams.Last();
Assert.AreEqual("English", newSub.Title);
Assert.AreEqual("eng", newSub.Language);
Assert.IsFalse(newSub.IsDefault);
Assert.IsFalse(newSub.IsForced);
}
}
#endif

View File

@@ -1,61 +1,82 @@
#if(DEBUG)
namespace VideoNodes.Tests
namespace VideoNodes.Tests;
/// <summary>
/// A logger for tests that stores the logs in memory
/// </summary>
public class TestLogger : ILogger
{
using FileFlows.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
private readonly List<string> Messages = new();
internal class TestLogger : ILogger
/// <summary>
/// Writes an information log message
/// </summary>
/// <param name="args">the log parameters</param>
public void ILog(params object[] args)
=> Log(LogType.Info, args);
/// <summary>
/// Writes an debug log message
/// </summary>
/// <param name="args">the log parameters</param>
public void DLog(params object[] args)
=> Log(LogType.Debug, args);
/// <summary>
/// Writes an warning log message
/// </summary>
/// <param name="args">the log parameters</param>
public void WLog(params object[] args)
=> Log(LogType.Warning, args);
/// <summary>
/// Writes an error log message
/// </summary>
/// <param name="args">the log parameters</param>
public void ELog(params object[] args)
=> Log(LogType.Error, args);
/// <summary>
/// Gets the tail of the log
/// </summary>
/// <param name="length">the number of messages to get</param>
public string GetTail(int length = 50)
{
private List<string> Messages = new List<string>();
public void DLog(params object[] args) => Log("DBUG", args);
public void ELog(params object[] args) => Log("ERRR", args);
public void ILog(params object[] args) => Log("INFO", args);
public void WLog(params object[] args) => Log("WARN", args);
private void Log(string type, object[] args)
{
if (args == null || args.Length == 0)
return;
#pragma warning disable IL2026
string message = type + " -> " +
string.Join(", ", args.Select(x =>
x == null ? "null" :
x.GetType().IsPrimitive || x is string ? x.ToString() :
System.Text.Json.JsonSerializer.Serialize(x)));
#pragma warning restore IL2026
Messages.Add(message);
}
public bool Contains(string message)
{
if (string.IsNullOrWhiteSpace(message))
return false;
string log = string.Join(Environment.NewLine, Messages);
return log.Contains(message);
}
public override string ToString()
=> string.Join(Environment.NewLine, this.Messages.ToArray());
public string GetTail(int length = 50)
{
if (length <= 0)
length = 50;
if (Messages.Count <= length)
return string.Join(Environment.NewLine, Messages);
return string.Join(Environment.NewLine, Messages.TakeLast(length));
}
if (Messages.Count <= length)
return string.Join(Environment.NewLine, Messages);
return string.Join(Environment.NewLine, Messages.TakeLast(50));
}
/// <summary>
/// Logs a message
/// </summary>
/// <param name="type">the type of log to record</param>
/// <param name="args">the arguments of the message</param>
private void Log(LogType type, params object[] args)
{
#pragma warning disable IL2026
string message = type + " -> " + string.Join(", ", args.Select(x =>
x == null ? "null" :
x.GetType().IsPrimitive ? x.ToString() :
x is string ? x.ToString() :
System.Text.Json.JsonSerializer.Serialize(x)));
#pragma warning restore IL2026
Writer?.Invoke(message);
Messages.Add(message);
}
/// <summary>
/// Gets or sets an optional writer
/// </summary>
public Action<string> Writer { get; set; }
/// <summary>
/// Returns the entire log as a string
/// </summary>
/// <returns>the entire log</returns>
public override string ToString()
=> string.Join(Environment.NewLine, Messages);
}
#endif

View File

@@ -37,6 +37,8 @@ public abstract class TestBase
[TestInitialize]
public void TestInitialize()
{
Logger.Writer = (msg) => TestContext.WriteLine(msg);
if (System.IO.File.Exists("../../../test.settings.dev.json"))
{
LoadSettings("../../../test.settings.dev.json");
@@ -45,7 +47,7 @@ public abstract class TestBase
{
LoadSettings("../../../test.settings.json");
}
this.TestPath = this.TestPath?.EmptyAsNull() ?? (IsLinux ? "~/src/ff-files/test-files" : @"d:\videos\testfiles");
this.TestPath = this.TestPath?.EmptyAsNull() ?? (IsLinux ? "~/src/ff-files/test-files/videos" : @"d:\videos\testfiles");
this.TempPath = this.TempPath?.EmptyAsNull() ?? (IsLinux ? "~/src/ff-files/temp" : @"d:\videos\temp");
this.FfmpegPath = this.FfmpegPath?.EmptyAsNull() ?? (IsLinux ? "/usr/local/bin/ffmpeg" : @"C:\utils\ffmpeg\ffmpeg.exe");
@@ -93,6 +95,7 @@ public abstract class TestBase
protected string TestFile_Tag => Path.Combine(TestPath, "tag.mp4");
protected string TestFile_Sitcom => Path.Combine(TestPath, "sitcom.mkv");
protected string TestFile_Pgs => Path.Combine(TestPath, "pgs.mkv");
protected string TestFile_Subtitle => Path.Combine(TestPath, "subtitle.mkv");
protected string TestFile_Font => Path.Combine(TestPath, "font.mkv");
protected string TestFile_DefaultSub => Path.Combine(TestPath, "default-sub.mkv");
protected string TestFile_ForcedDefaultSub => Path.Combine(TestPath, "sub-forced-default.mkv");

View File

@@ -484,6 +484,16 @@
"UseSourceDirectory-Help": "If checked the original source directory will be searched, otherwise the working directory will be used.",
"MatchFilename": "Match Filename",
"MatchFilename-Help": "When checked only subtitles with the same filename as the input file or the working file will be merged",
"Pattern": "Pattern",
"Pattern-Help": "Optional regular expression to match files against.",
"Title": "Title",
"Title-Help": "The title of the subtitle, if not set the language will be used as the title if found.",
"Language": "Language",
"Language-Help": "Optional [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code for the added subtitle.",
"Forced": "Forced",
"Forced-Help": "If the subtitle should be forced",
"Default": "Default",
"Default-Help": "If the subtitle should be the default subtitle",
"DeleteAfterwards": "Delete After Merging",
"DeleteAfterwards-Help": "If the subtitle file should be deleted after being merged"
}