using FileFlows.VideoNodes.FfmpegBuilderNodes; using FileFlows.VideoNodes.FfmpegBuilderNodes.Models; using FileFlows.VideoNodes.Helpers; namespace FileFlows.VideoNodes; /// /// Flow element to test if a video has a stream /// public class VideoHasStream : VideoNode { /// /// Gets the number of inputs /// public override int Inputs => 1; /// /// Gets the number of outputs /// public override int Outputs => 2; /// /// Gets the type of flow element /// public override FlowElementType Type => FlowElementType.Logic; /// /// Gets the help URL /// public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/logical-nodes/video-has-stream"; /// /// Gets or sets the type of stream to check for /// [Select(nameof(StreamTypeOptions), 1)] public string Stream { get; set; } private static List? _StreamTypeOptions; /// /// Gets the types of streams available to check for /// public static List StreamTypeOptions { get { if (_StreamTypeOptions == null) { _StreamTypeOptions = new List { new () { Label = "Video", Value = "Video" }, new () { Label = "Audio", Value = "Audio" }, new () { Label = "Subtitle", Value = "Subtitle" } }; } return _StreamTypeOptions; } } /// /// Gets or sets the title to look for /// [TextVariable(2)] public string? Title { get; set; } /// /// Gets or sets the codec to look for /// [TextVariable(3)] public string? Codec { get; set; } /// /// Gets or sets the language to look for /// [ConditionEquals(nameof(Stream), "Video", inverse: true)] [TextVariable(4)] public string? Language { get; set; } /// /// Gets or sets the number of channels to look for /// This is a string so math operations can be done /// [ConditionEquals(nameof(Stream), "Audio")] [MathValue(5)] public string Channels { get; set; } /// /// Gets or sets if deleted tracks should also be checked /// [Boolean(6)] public bool CheckDeleted { get; set; } /// /// Gets or sets the forced status to check for /// [Select(nameof(ForcedOptions), 7)] [ConditionEquals(nameof(Stream), "Subtitle")] public string Forced { get; set; } private static List? _ForcedOptions; /// /// Gets the forced options /// public static List ForcedOptions { get { if (_ForcedOptions == null) { _ForcedOptions = new List { new () { Label = "Any", Value = "" }, new () { Label = "Forced", Value = "Forced" }, new () { Label = "Not Forced", Value = "Not Forced" } }; } return _ForcedOptions; } } /// /// Gets or sets the default status to check for /// [Select(nameof(DefaultOptions), 8)] [ConditionEquals(nameof(Stream), "Video", inverse: true)] public string Default { get; set; } private static List? _DefaultOptions; /// /// Gets the default options /// public static List DefaultOptions { get { if (_DefaultOptions == null) { _DefaultOptions = new List { new () { Label = "Any", Value = "" }, new () { Label = "Default", Value = "Default" }, new () { Label = "Not Default", Value = "Not Default" } }; } return _DefaultOptions; } } /// /// Gets or sets if result should be inverted /// [Boolean(10)] public bool Invert { get; set; } /// /// Tries to get the FFmpegModel if loaded into variables /// /// The node parameters /// the FFmpeg model if exists protected FfmpegModel GetFfmpegModel(NodeParameters args) { if (args.Variables.TryGetValue(FfmpegBuilderNode.MODEL_KEY, out var variable) && variable is FfmpegModel ffmpegModel) { args.Logger?.ILog("FFmpeg Model found and will be used."); return ffmpegModel; } args.Logger?.ILog("FFmpeg Model not found, using VideoInfo."); return null; } /// /// Executes the flow element /// /// the arguments /// the output to call next public override int Execute(NodeParameters args) { var videoInfo = GetVideoInfo(args); if (videoInfo == null) { args.FailureReason = "Failed to retrieve video info"; args.Logger?.ELog(args.FailureReason); return -1; } bool found = false; string title = args.ReplaceVariables(Title, stripMissing: true); string codec = args.ReplaceVariables(Codec, stripMissing: true); string lang = args.ReplaceVariables(Language, stripMissing: true); var ffmpegModel = GetFfmpegModel(args); if (ffmpegModel != null && CheckDeleted) { args.Logger?.ILog("Checking deleted, ignoring FFmpeg model"); ffmpegModel = null; } if (Channels?.Trim() is "=" or ">" or "<" or "!=" or "<=" or ">=" or "==") Channels = string.Empty; // incase the user selected one bu didnt enter anything else args.Logger?.ILog("Title to match: " + title); args.Logger?.ILog("Codec to match: " + codec); args.Logger?.ILog("Lang to match: " + lang); if (this.Stream == "Video") { var streams = ffmpegModel == null ? videoInfo.VideoStreams : ffmpegModel.VideoStreams.Where(x => x.Deleted == false).Select(x => x.Stream).ToList(); found = streams.Where(x => { if (ValueMatch(title, x.Title) == MatchResult.NoMatch) { args.Logger.ILog("Title does not match: " + x.Title); return false; } if (string.IsNullOrWhiteSpace(x.CodecTag) == false && ValueMatch(codec, x.CodecTag) == MatchResult.Matched) { args.Logger.ILog("Codec Tag does not match: " + x.CodecTag); return true; } if (ValueMatch(codec, x.Codec) == MatchResult.NoMatch) { args.Logger.ILog("Codec does not match: " + x.Codec); return false; } return true; }).Any(); } else if (this.Stream == "Audio") { args.Logger?.ILog("Channels to match: " + Channels); args.Logger?.ILog("Default to match: " + (Default ?? string.Empty)); var streams = ffmpegModel == null ? videoInfo.AudioStreams : ffmpegModel.AudioStreams.Where(x => x.Deleted == false).Select(x => x.Stream).ToList(); found = streams.Where(x => { if (ValueMatch(title, x.Title) == MatchResult.NoMatch) { args.Logger.ILog("Title does not match: " + x.Title); return false; } if (ValueMatch(codec, x.Codec) == MatchResult.NoMatch) { args.Logger.ILog("Codec does not match: " + x.Codec); return false; } if (string.IsNullOrEmpty(lang) == false && LanguageHelper.Matches(args, lang, x.Language) == false) return false; double xChannels = Math.Round(x.Channels, 1); if (string.IsNullOrWhiteSpace(Channels) == false && args.MathHelper.IsFalse(Channels, xChannels)) { args.Logger.ILog("Channels does not match: " + xChannels + " vs " + Channels); return false; } if (string.IsNullOrEmpty(Default) == false) { bool wantDefault = Default == "Default"; if (x.Default != wantDefault) { args.Logger.ILog("Defaults does not match: " + x.Default + " vs " + wantDefault); return false; } } args.Logger.ILog("Matching audio found: " + x); return true; }).Any(); } else if (this.Stream == "Subtitle") { var streams = ffmpegModel == null ? videoInfo.SubtitleStreams : ffmpegModel.SubtitleStreams.Where(x => x.Deleted == false).Select(x => x.Stream).ToList(); found = streams.Where(x => { if (ValueMatch(title, x.Title) == MatchResult.NoMatch) { args.Logger.ILog("Title does not match: " + x.Title); return false; } if (ValueMatch(codec, x.Codec) == MatchResult.NoMatch) { args.Logger.ILog("Codec does not match: " + x.Codec); return false; } if (string.IsNullOrEmpty(lang) == false && LanguageHelper.Matches(args, lang, x.Language) == false) return false; if (string.IsNullOrEmpty(Default) == false) { bool wantDefault = Default == "Default"; if (x.Default != wantDefault) { args.Logger.ILog("Defaults does not match: " + x.Default + " vs " + wantDefault); return false; } } if (string.IsNullOrEmpty(Forced) == false) { bool wantForced = Forced == "Forced"; if (x.Forced != wantForced) { args.Logger.ILog("Forced does not match: " + x.Forced + " vs " + Forced); return false; } } return true; }).Any(); } args.Logger?.ILog("Found stream: " + found); if (Invert) { args.Logger?.ILog("Invert result"); found = !found; } return found ? 1 : 2; } /// /// Tests if a value matches the pattern /// /// the pattern /// the value /// the result private MatchResult ValueMatch(string pattern, string value) { if (string.IsNullOrWhiteSpace(pattern)) return MatchResult.Skipped; if (string.IsNullOrEmpty(value)) return MatchResult.NoMatch; if (value.ToLowerInvariant() == "hevc" && (pattern.ToLowerInvariant() is "h265" or "265" or "h.265")) return MatchResult.Matched; // special case bool matches = Args.StringHelper.Matches(pattern, value); return matches ? MatchResult.Matched : MatchResult.NoMatch; // // try // { // // if (string.IsNullOrEmpty(value)) // return MatchResult.NoMatch; // // if (GeneralHelper.IsRegex(pattern)) // { // var rgx = new Regex(pattern, RegexOptions.IgnoreCase); // if (rgx.IsMatch(value)) // return MatchResult.Matched; // } // // if (value.ToLowerInvariant() == "hevc" && (pattern.ToLowerInvariant() is "h265" or "265" or "h.265")) // return MatchResult.Matched; // special case // // return pattern.ToLowerInvariant().Trim() == value.ToLowerInvariant().Trim() // ? MatchResult.Matched // : MatchResult.NoMatch; // } // catch (Exception) // { // return MatchResult.NoMatch; // } } /// /// Match results /// private enum MatchResult { /// /// No Match /// NoMatch = 0, /// /// Matched /// Matched = 1, /// /// Skipped /// Skipped = 2 } }