diff --git a/FileFlows.Plugin.dll b/FileFlows.Plugin.dll index 1b8cc7dc..9f8899e5 100644 Binary files a/FileFlows.Plugin.dll and b/FileFlows.Plugin.dll differ diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index e1b3b943..1cbdb60f 100644 Binary files a/FileFlows.Plugin.pdb and b/FileFlows.Plugin.pdb differ diff --git a/MetaNodes/TheMovieDb/MovieLookup.cs b/MetaNodes/TheMovieDb/MovieLookup.cs index ef315ca3..38697df7 100644 --- a/MetaNodes/TheMovieDb/MovieLookup.cs +++ b/MetaNodes/TheMovieDb/MovieLookup.cs @@ -147,7 +147,11 @@ public class MovieLookup : Node Variables["movie.Title"] = result.Title; Variables["movie.Year"] = result.ReleaseDate.Year; - Variables["VideoMetadata"] = GetVideoMetadata(movieApi, result.Id, args.TempPath); + var meta = GetVideoMetadata(movieApi, result.Id, args.TempPath); + Variables["VideoMetadata"] = meta; + if (string.IsNullOrWhiteSpace(meta.OriginalLanguage) == false) + Variables["OriginalLanguage"] = meta.OriginalLanguage; + Variables[Globals.MOVIE_INFO] = result; args.UpdateVariables(Variables); diff --git a/MetaNodes/TheMovieDb/TVEpisodeLookup.cs b/MetaNodes/TheMovieDb/TVEpisodeLookup.cs index 8df961c5..8a7fffa5 100644 --- a/MetaNodes/TheMovieDb/TVEpisodeLookup.cs +++ b/MetaNodes/TheMovieDb/TVEpisodeLookup.cs @@ -156,6 +156,10 @@ public class TVEpisodeLookup : Node Variables["tvepisode.Overview"] = epInfo.Overview; //Variables["VideoMetadata"] = GetVideoMetadata(movieApi, result.Id, args.TempPath); Variables[Globals.TV_SHOW_INFO] = result; + + if (string.IsNullOrWhiteSpace(result.OriginalLanguage) == false) + Variables["OriginalLanguage"] = result.OriginalLanguage; + args.UpdateVariables(Variables); return 1; diff --git a/MetaNodes/TheMovieDb/TVShowLookup.cs b/MetaNodes/TheMovieDb/TVShowLookup.cs index 5af82865..332e3e9b 100644 --- a/MetaNodes/TheMovieDb/TVShowLookup.cs +++ b/MetaNodes/TheMovieDb/TVShowLookup.cs @@ -107,6 +107,8 @@ public class TVShowLookup : Node Variables["tvshow.Year"] = result.FirstAirDate.Year; Variables["VideoMetadata"] = GetVideoMetadata(movieApi, result.Id, args.TempPath); Variables[Globals.TV_SHOW_INFO] = result; + if (string.IsNullOrWhiteSpace(result.OriginalLanguage) == false) + Variables["OriginalLanguage"] = result.OriginalLanguage; args.UpdateVariables(Variables); return 1; diff --git a/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderKeepOriginalLanguage.cs b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderKeepOriginalLanguage.cs new file mode 100644 index 00000000..2eada97c --- /dev/null +++ b/VideoNodes/FfmpegBuilderNodes/FfmpegBuilderKeepOriginalLanguage.cs @@ -0,0 +1,208 @@ +using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; + +namespace FileFlows.VideoNodes.FfmpegBuilderNodes; + +/// +/// FFmpeg Builder flow element to keep the original language track +/// +public class FfmpegBuilderKeepOriginalLanguage: FfmpegBuilderNode +{ + /// + /// Gets the help URL for the flow element + /// + public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/keep-original-language"; + + /// + /// Gets the number of outputs of the flow element + /// + public override int Outputs => 2; + + /// + /// Gets the icon of the flow element + /// + public override string Icon => "fas fa-globe"; + + /// + /// Gets or sets the stream type + /// + [Select(nameof(StreamTypeOptions), 1)] + public string StreamType { get; set; } + + private static List _StreamTypeOptions; + /// + /// Gets the stream options to show in the UI + /// + public static List StreamTypeOptions + { + get + { + if (_StreamTypeOptions == null) + { + _StreamTypeOptions = new List + { + new () { Label = "Audio", Value = "Audio" }, + new () { Label = "Subtitle", Value = "Subtitle" }, + new () { Label = "Both", Value = "Both" }, + }; + } + return _StreamTypeOptions; + } + } + + /// + /// Gets or sets the languages + /// + [StringArray(2)] + public List AdditionalLanguages { get; set; } + + /// + /// Gets or sets if only the first of each language should be kept + /// + [Boolean(3)] + public bool KeepOnlyFirst { get; set; } + + /// + /// Gets or sets if the first stream should be kept if no other streams match + /// + [Boolean(4)] + public bool FirstIfNone { get; set; } + + /// + /// Gets or sets if tracks with no language should be treated as the original langauge + /// + [Boolean(5)] + public bool TreatEmptyAsOriginal { get; set; } + + /// + /// Executes the flow element + /// + /// the flow parameters + /// the flow output to call next + public override int Execute(NodeParameters args) + { + string originalLanguage; + if (args.Variables.TryGetValue("OriginalLanguage", out object oValue) == false || + string.IsNullOrWhiteSpace(originalLanguage = oValue as string)) + { + args.Logger?.ILog("OriginalLanguage variable was not set."); + return 2; + } + args.Logger?.ILog("OriginalLanguage: " + originalLanguage); + + int changes = 0; + if(StreamType is "Audio" or "Both") + { + changes += ProcessStreams(args, Model.AudioStreams, originalLanguage); + } + if(StreamType is "Subtitle" or "Both") + { + changes += ProcessStreams(args, Model.SubtitleStreams, originalLanguage); + } + + return changes > 0 ? 1 : 2; + } + + private int ProcessStreams(NodeParameters args, List streams, string originalLanguage) where T : FfmpegStream + { + if (streams?.Any() != true) + return 0; + + int changed = 0; + bool firstStreamDeleted = streams[0].Deleted; + var foundLanguages = new List(); + foreach (var stream in streams) + { + bool deleted; + if (TreatEmptyAsOriginal && string.IsNullOrWhiteSpace(stream.Language)) + deleted = false; + else + deleted = KeepStream(originalLanguage, stream.Language) == false; + + if (deleted == false) + { + string lang = LanguageHelper.GetIso1Code(stream.Language?.EmptyAsNull() ?? originalLanguage); + if (foundLanguages.Contains(lang) == false) + foundLanguages.Add(lang); + else if (KeepOnlyFirst) + deleted = true; + } + + if (stream.Deleted == deleted) + continue; + stream.Deleted = deleted; + ++changed; + args.Logger?.ILog($"Stream '{stream.GetType().Name}' '{stream.Language}' " + (deleted ? "deleted" : "restored")); + } + + if (FirstIfNone && streams.Any(x => x.Deleted == false) == false) + { + if (firstStreamDeleted == false) + { + --changed; // remove the change + } + streams[0].Deleted = false; + args.Logger?.ILog($"Stream '{streams[0].GetType().Name}' '{streams[0].Language}' restored as only stream"); + } + + return changed; + } + + private bool KeepStream(string originalLanguage, string streamLanguage) + { + if (LanguageMatches(streamLanguage, originalLanguage)) + return true; + if (AdditionalLanguages?.Any() != true) + return false; + + foreach (var lang in this.AdditionalLanguages) + { + if (LanguageMatches(streamLanguage, lang)) + return true; + } + + return false; + } + + /// + /// Tests if a language matches + /// + /// the language of ths stream + /// the language to test + /// true if matches, otherwise false + private bool LanguageMatches(string streamLanguage, string testLanguage) + { + if (string.IsNullOrWhiteSpace(testLanguage)) + return false; + if (string.IsNullOrWhiteSpace(streamLanguage)) + return false; + if (testLanguage.ToLowerInvariant().Contains(streamLanguage.ToLowerInvariant())) + return true; + try + { + if (LanguageHelper.GetIso2Code(streamLanguage) == LanguageHelper.GetIso2Code(testLanguage)) + return true; + } + catch (Exception) + { + } + + try + { + if (LanguageHelper.GetIso1Code(streamLanguage) == LanguageHelper.GetIso1Code(testLanguage)) + return true; + } + catch (Exception) + { + } + + try + { + var rgx = new Regex(testLanguage, RegexOptions.IgnoreCase); + return rgx.IsMatch(streamLanguage); + } + catch (Exception) + { + return false; + } + } +} diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_KeepOriginalLanguageTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_KeepOriginalLanguageTests.cs new file mode 100644 index 00000000..35cf1e6c --- /dev/null +++ b/VideoNodes/Tests/FfmpegBuilderTests/FfmpegBuilder_KeepOriginalLanguageTests.cs @@ -0,0 +1,275 @@ +#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_KeepOriginalLanguageTests +{ + VideoInfo vii; + NodeParameters args; + TestLogger logger = new TestLogger(); + private void Prepare() + { + const string file = @"D:\videos\unprocessed\basic.mkv"; + const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe"; + var vi = new VideoInfoHelper(ffmpeg, logger); + vii = vi.Read(file); + vii.AudioStreams = new List + { + new AudioStream + { + Index = 2, + IndexString = "0:a:0", + Language = "en", + Codec = "AC3", + Channels = 5.1f + }, + new AudioStream + { + Index = 3, + IndexString = "0:a:1", + Language = "en", + Codec = "AAC", + Channels = 2 + }, + new AudioStream + { + Index = 4, + IndexString = "0:a:3", + Language = "fre", + Codec = "AAC", + Channels = 2 + }, + new AudioStream + { + Index = 5, + IndexString = "0:a:4", + Language = "deu", + Codec = "AAC", + Channels = 5.1f + } + }; + + vii.SubtitleStreams = new List + { + new() + { + Index = 2, + IndexString = "0:s:0", + Language = "en", + Codec = "AC3" + }, + new() + { + Index = 3, + IndexString = "0:s:1", + Language = "en", + Codec = "AAC" + }, + new() + { + Index = 4, + IndexString = "0:s:3", + Language = "fre", + Codec = "AAC", + }, + new() + { + Index = 5, + IndexString = "0:s:4", + Language = "deu", + Codec = "AAC" + } + }; + args = new NodeParameters(file, logger, false, string.Empty); + args.GetToolPathActual = (string tool) => ffmpeg; + args.TempPath = @"D:\videos\temp"; + args.Parameters.Add("VideoInfo", vii); + + + FfmpegBuilderStart ffStart = new(); + ffStart.PreExecute(args); + Assert.AreEqual(1, ffStart.Execute(args)); + } + + private FfmpegModel GetFFmpegModel() + { + return args.Variables["FfmpegBuilderModel"] as FfmpegModel; + } + + [TestMethod] + public void FfmpegBuilder_Audio_German() + { + Prepare(); + + FfmpegBuilderKeepOriginalLanguage ffElement = new(); + ffElement.StreamType = "Audio"; + args.Variables["OriginalLanguage"] = "German"; + ffElement.PreExecute(args); + var result = ffElement.Execute(args); + var log = logger.ToString(); + + Assert.AreEqual(1, result); + var model = GetFFmpegModel(); + var kept = model.AudioStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(1, kept.Count); + Assert.AreEqual("deu", kept[0].Language); + } + + [TestMethod] + public void FfmpegBuilder_Audio_None() + { + Prepare(); + + FfmpegBuilderKeepOriginalLanguage ffElement = new(); + ffElement.StreamType = "Audio"; + ffElement.FirstIfNone = true; + args.Variables["OriginalLanguage"] = "Maori"; + ffElement.PreExecute(args); + var result = ffElement.Execute(args); + var log = logger.ToString(); + + Assert.AreEqual(1, result); + var model = GetFFmpegModel(); + var kept = model.AudioStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(1, kept.Count); + Assert.AreEqual("en", kept[0].Language); + } + + + [TestMethod] + public void FfmpegBuilder_Audio_OriginalAndEnglish() + { + Prepare(); + + FfmpegBuilderKeepOriginalLanguage ffElement = new(); + ffElement.StreamType = "Audio"; + ffElement.AdditionalLanguages = new List{ "English" }; + args.Variables["OriginalLanguage"] = "French"; + ffElement.PreExecute(args); + var result = ffElement.Execute(args); + var log = logger.ToString(); + + Assert.AreEqual(1, result); + var model = GetFFmpegModel(); + var kept = model.AudioStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(3, kept.Count); + Assert.AreEqual("en", kept[0].Language); + Assert.AreEqual("en", kept[1].Language); + Assert.AreEqual("fre", kept[2].Language); + } + + + [TestMethod] + public void FfmpegBuilder_Both_German() + { + Prepare(); + + FfmpegBuilderKeepOriginalLanguage ffElement = new(); + ffElement.StreamType = "Both"; + args.Variables["OriginalLanguage"] = "German"; + ffElement.PreExecute(args); + var result = ffElement.Execute(args); + var log = logger.ToString(); + + Assert.AreEqual(1, result); + var model = GetFFmpegModel(); + var kept = model.AudioStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(1, kept.Count); + Assert.AreEqual("deu", kept[0].Language); + + var subKept = model.SubtitleStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(1, subKept.Count); + Assert.AreEqual("deu", subKept[0].Language); + } + + [TestMethod] + public void FfmpegBuilder_Both_None() + { + Prepare(); + + FfmpegBuilderKeepOriginalLanguage ffElement = new(); + ffElement.StreamType = "Both"; + ffElement.FirstIfNone = true; + args.Variables["OriginalLanguage"] = "Maori"; + ffElement.PreExecute(args); + var result = ffElement.Execute(args); + var log = logger.ToString(); + + Assert.AreEqual(1, result); + var model = GetFFmpegModel(); + var kept = model.AudioStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(1, kept.Count); + Assert.AreEqual("en", kept[0].Language); + + var subKept = model.SubtitleStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(1, subKept.Count); + Assert.AreEqual("en", subKept[0].Language); + } + + [TestMethod] + public void FfmpegBuilder_Both_OriginalAndEnglish() + { + Prepare(); + + FfmpegBuilderKeepOriginalLanguage ffElement = new(); + ffElement.StreamType = "Both"; + ffElement.AdditionalLanguages = new List{ "English" }; + args.Variables["OriginalLanguage"] = "French"; + ffElement.PreExecute(args); + var result = ffElement.Execute(args); + var log = logger.ToString(); + + Assert.AreEqual(1, result); + var model = GetFFmpegModel(); + var kept = model.AudioStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(3, kept.Count); + Assert.AreEqual("en", kept[0].Language); + Assert.AreEqual("en", kept[1].Language); + Assert.AreEqual("fre", kept[2].Language); + + + var subKept = model.SubtitleStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(3, subKept.Count); + Assert.AreEqual("en", subKept[0].Language); + Assert.AreEqual("en", subKept[1].Language); + Assert.AreEqual("fre", subKept[2].Language); + } + + [TestMethod] + public void FfmpegBuilder_Both_OriginalAndEnglish_OnlyFirst() + { + Prepare(); + + FfmpegBuilderKeepOriginalLanguage ffElement = new(); + ffElement.StreamType = "Both"; + ffElement.KeepOnlyFirst = true; + ffElement.AdditionalLanguages = new List{ "English" }; + args.Variables["OriginalLanguage"] = "French"; + ffElement.PreExecute(args); + var result = ffElement.Execute(args); + var log = logger.ToString(); + + Assert.AreEqual(1, result); + var model = GetFFmpegModel(); + var kept = model.AudioStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(2, kept.Count); + Assert.AreEqual("en", kept[0].Language); + Assert.AreEqual("0:a:0", kept[0].Stream.IndexString); + Assert.AreEqual("fre", kept[1].Language); + + + var subKept = model.SubtitleStreams.Where(x => x.Deleted == false).ToList(); + Assert.AreEqual(2, subKept.Count); + Assert.AreEqual("en", subKept[0].Language); + Assert.AreEqual("0:s:0", subKept[0].Stream.IndexString); + Assert.AreEqual("fre", subKept[1].Language); + } +} + +#endif \ No newline at end of file diff --git a/VideoNodes/VideoNodes.en.json b/VideoNodes/VideoNodes.en.json index 07b9d202..445b1f6f 100644 --- a/VideoNodes/VideoNodes.en.json +++ b/VideoNodes/VideoNodes.en.json @@ -308,6 +308,26 @@ "2": "No HDR stream found" } }, + "FfmpegBuilderKeepOriginalLanguage": { + "Label": "FFMPEG Builder: Keep Original Language", + "Outputs": { + "1": "Tracks have been modified", + "2": "No tracks have been changed" + }, + "Description": "This flow element that will keep only the original language and any additional languages the user defines.\n\nAll other language streams will be removed/marked for deletion.", + "Fields": { + "StreamType": "Type", + "StreamType-Help": "The type of tracks that should be updated", + "AdditionalLanguages": "Additional Languages", + "AdditionalLanguages-Help": "An optional list of additional language codes to set on the tracks with missing languages.\n\nIt is recommended that an [ISO 639-2 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) are used.", + "KeepOnlyFirst": "Keep Only First", + "KeepOnlyFirst-Help": "When enabled only the first track of each language would be kept.\n\nFor example if there were 2 English tracks, 3 Spanish tracks and 1 German track. The original language was Spanish, additional languages was set to `eng`, then the result would be 1 English track and 1 Spanish track, the rest would be removed.", + "FirstIfNone": "First If None", + "FirstIfNone-Help": "When enabled, this ensures at least one track is kept. If no tracks matching the original language and no tracks matching the additional languages are found, the first track will be kept regardless.\n\nThis avoids any issues of no audio left on the video.", + "TreatEmptyAsOriginal": "Treat Empty As Original", + "TreatEmptyAsOriginal-Help": "When enabled, any track that has no language set, will be treated as if it were the original language.\n\nFor example, original language is Maori, and a track has no language set on it, it will be treated as Maori." + } + }, "FfmpegBuilderMetadataRemover": { "Label": "FFMPEG Builder: Metadata Remover", "Description": "Removes metadata from the FFMPEG Builder so when the file is processed the selected metadata will be removed.\n\nNote: Only the metadata when this node is effected, if metadata is added after this node runs, that will not be effected.",