mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2026-02-17 07:08:42 -06:00
FF-1516: Added more options to FFmpeg Builder Subtitle Track Merge
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user