FF-1493: Added ability to save images to the file log

This commit is contained in:
John Andrews
2024-04-24 17:57:38 +12:00
parent 478cf768c6
commit 583f2a725e
21 changed files with 131 additions and 54 deletions
+3 -3
View File
@@ -29,7 +29,7 @@ public class Apprise: Node
[Select(nameof(MessageTypeOptions), 2)] [Select(nameof(MessageTypeOptions), 2)]
public string MessageType { get; set; } = string.Empty; public string MessageType { get; set; } = string.Empty;
private static List<ListOption> _MessageTypeOptions; private static List<ListOption>? _MessageTypeOptions;
public static List<ListOption> MessageTypeOptions public static List<ListOption> MessageTypeOptions
{ {
get get
@@ -54,9 +54,9 @@ public class Apprise: Node
/// </summary> /// </summary>
[Required] [Required]
[Template(3, nameof(MessageTemplates))] [Template(3, nameof(MessageTemplates))]
public string Message { get; set; } public string Message { get; set; } = string.Empty;
private static List<ListOption> _MessageTemplates; private static List<ListOption>? _MessageTemplates;
public static List<ListOption> MessageTemplates public static List<ListOption> MessageTemplates
{ {
get get
+1 -1
View File
@@ -165,7 +165,7 @@ public class FFprobeTimeSpanConverter : JsonConverter<TimeSpan>
{ {
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
string stringValue = reader.GetString(); var stringValue = reader.GetString() ?? string.Empty;
if (double.TryParse(stringValue, out double seconds) == false) if (double.TryParse(stringValue, out double seconds) == false)
return default; return default;
+3 -1
View File
@@ -111,7 +111,9 @@ public class AudioFileNormalization : AudioNode
args.Logger?.WLog("Failed to parse TwoPass json\""); args.Logger?.WLog("Failed to parse TwoPass json\"");
return (false, string.Empty); return (false, string.Empty);
} }
LoudNormStats stats = JsonSerializer.Deserialize<LoudNormStats>(json); var stats = JsonSerializer.Deserialize<LoudNormStats>(json);
if (stats == null)
return (false, string.Empty);
string ar = $"loudnorm=print_format=summary:linear=true:{LOUDNORM_TARGET}:measured_I={stats.input_i}:measured_LRA={stats.input_lra}:measured_tp={stats.input_tp}:measured_thresh={stats.input_thresh}:offset={stats.target_offset}"; string ar = $"loudnorm=print_format=summary:linear=true:{LOUDNORM_TARGET}:measured_I={stats.input_i}:measured_LRA={stats.input_lra}:measured_tp={stats.input_tp}:measured_thresh={stats.input_thresh}:offset={stats.target_offset}";
return (true, ar); return (true, ar);
} }
@@ -314,10 +314,10 @@ namespace FileFlows.AudioNodes
} }
var ffArgs = GetArguments(args, out string extension); var ffArgs = GetArguments(args, out var extension);
string actualExt = args.ReplaceVariables(CustomExtension, stripMissing: true)?.EmptyAsNull() ?? var actualExt = args.ReplaceVariables(CustomExtension, stripMissing: true)?.EmptyAsNull() ??
extension?.EmptyAsNull() ?? DefaultExtension; extension?.EmptyAsNull() ?? DefaultExtension;
string outputFile = FileHelper.Combine(args.TempPath, Guid.NewGuid() + "." + actualExt.TrimStart('.')); var outputFile = FileHelper.Combine(args.TempPath, Guid.NewGuid() + "." + actualExt.TrimStart('.'));
ffArgs.Insert(0, "-hide_banner"); ffArgs.Insert(0, "-hide_banner");
ffArgs.Insert(1, "-y"); // tells ffmpeg to replace the file if already exists, which it shouldnt but just incase ffArgs.Insert(1, "-y"); // tells ffmpeg to replace the file if already exists, which it shouldnt but just incase
+5 -5
View File
@@ -23,23 +23,23 @@ public class Discord: Node
/// Gets or sets the title /// Gets or sets the title
/// </summary> /// </summary>
[TextVariable(1)] [TextVariable(1)]
public string Title { get; set; } public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets the message type /// Gets or sets the message type
/// </summary> /// </summary>
[DefaultValue("standard")] [DefaultValue("standard")]
[Select(nameof(MessageTypeOptions), 2)] [Select(nameof(MessageTypeOptions), 2)]
public string MessageType { get; set; } public string MessageType { get; set; } = "standard";
/// <summary> /// <summary>
/// Gets or sets the message /// Gets or sets the message
/// </summary> /// </summary>
[Required] [Required]
[Template(3, nameof(MessageTemplates))] [Template(3, nameof(MessageTemplates))]
public string Message { get; set; } public string Message { get; set; } = string.Empty;
private static List<ListOption> _MessageTypeOptions; private static List<ListOption>? _MessageTypeOptions;
public static List<ListOption> MessageTypeOptions public static List<ListOption> MessageTypeOptions
{ {
get get
@@ -60,7 +60,7 @@ public class Discord: Node
} }
} }
private static List<ListOption> _MessageTemplates; private static List<ListOption>? _MessageTemplates;
public static List<ListOption> MessageTemplates public static List<ListOption> MessageTemplates
{ {
get get
+2 -2
View File
@@ -10,12 +10,12 @@ public class PluginSettings:IPluginSettings
/// </summary> /// </summary>
[Text(1)] [Text(1)]
[Required] [Required]
public string WebhookId { get; set; } public string WebhookId { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets the webhook token for this plugin /// Gets or sets the webhook token for this plugin
/// </summary> /// </summary>
[Text(2)] [Text(2)]
[Required] [Required]
public string WebhookToken { get; set; } public string WebhookToken { get; set; } = string.Empty;
} }
+4 -4
View File
@@ -9,21 +9,21 @@
{ {
[Required] [Required]
[Text(1)] [Text(1)]
public string SmtpServer { get; set; } public string SmtpServer { get; set; } = string.Empty;
[Range(1, 6555)] [Range(1, 6555)]
[NumberInt(1)] [NumberInt(1)]
public int SmtpPort { get; set; } public int SmtpPort { get; set; }
[Text(2)] [Text(2)]
public string SmtpUsername { get; set; } public string SmtpUsername { get; set; } = string.Empty;
[Password(3)] [Password(3)]
public string SmtpPassword { get; set; } public string SmtpPassword { get; set; } = string.Empty;
[Text(4)] [Text(4)]
[Required] [Required]
[System.ComponentModel.DataAnnotations.RegularExpression(@"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")] [System.ComponentModel.DataAnnotations.RegularExpression(@"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
public string Sender { get; set; } public string Sender { get; set; } = string.Empty;
} }
} }
+2 -2
View File
@@ -17,10 +17,10 @@ public class EmbyUpdater: Node
public override bool NoEditorOnAdd => true; public override bool NoEditorOnAdd => true;
[Text(1)] [Text(1)]
public string ServerUrl { get; set; } public string ServerUrl { get; set; } = string.Empty;
[Text(2)] [Text(2)]
public string AccessToken { get; set; } public string AccessToken { get; set; } = string.Empty;
[KeyValue(3, null)] [KeyValue(3, null)]
public List<KeyValuePair<string, string>> Mapping { get; set; } public List<KeyValuePair<string, string>> Mapping { get; set; }
+3 -3
View File
@@ -8,13 +8,13 @@
{ {
[Text(1)] [Text(1)]
[Required] [Required]
public string ServerUrl { get; set; } public string ServerUrl { get; set; } = string.Empty;
[Text(2)] [Text(2)]
[Required] [Required]
public string AccessToken { get; set; } public string AccessToken { get; set; } = string.Empty;
[KeyValue(3, null)] [KeyValue(3, null)]
public List<KeyValuePair<string, string>> Mapping { get; set; } public List<KeyValuePair<string, string>>? Mapping { get; set; }
} }
} }
Binary file not shown.
Binary file not shown.
+5 -3
View File
@@ -1,4 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
namespace FileFlows.Gotify.Communication; namespace FileFlows.Gotify.Communication;
@@ -42,7 +43,7 @@ public class Gotify: Node
/// Gets or sets the title of the message /// Gets or sets the title of the message
/// </summary> /// </summary>
[TextVariable(1)] [TextVariable(1)]
public string Title { get; set; } public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -58,9 +59,9 @@ public class Gotify: Node
/// </summary> /// </summary>
[Required] [Required]
[Template(3, nameof(MessageTemplates))] [Template(3, nameof(MessageTemplates))]
public string Message { get; set; } public string Message { get; set; } = string.Empty;
private static List<ListOption> _MessageTemplates; private static List<ListOption>? _MessageTemplates;
public static List<ListOption> MessageTemplates public static List<ListOption> MessageTemplates
{ {
get get
@@ -96,6 +97,7 @@ File shrunk in size by: {{ difference | file_size }} / {{ percent }}%
/// </summary> /// </summary>
/// <param name="args">the node parameters</param> /// <param name="args">the node parameters</param>
/// <returns>the output to call next</returns> /// <returns>the output to call next</returns>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public override int Execute(NodeParameters args) public override int Execute(NodeParameters args)
{ {
try try
+2 -2
View File
@@ -10,12 +10,12 @@ public class PluginSettings : IPluginSettings
/// </summary> /// </summary>
[Text(1)] [Text(1)]
[Required] [Required]
public string ServerUrl { get; set; } public string ServerUrl { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets the Access Token for the server /// Gets or sets the Access Token for the server
/// </summary> /// </summary>
[Text(2)] [Text(2)]
[Required] [Required]
public string AccessToken { get; set; } public string AccessToken { get; set; } = string.Empty;
} }
+4 -4
View File
@@ -9,13 +9,13 @@
{ {
[Text(1)] [Text(1)]
[Required] [Required]
public string ServerUrl { get; set; } public string ServerUrl { get; set; } = string.Empty;
[Text(2)] [Text(2)]
[Required] [Required]
public string AccessToken { get; set; } public string AccessToken { get; set; } = string.Empty;
[KeyValue(3, null)] [KeyValue(3, null)]
public List<KeyValuePair<string, string>> Mapping { get; set; } public List<KeyValuePair<string, string>> Mapping { get; set; } = new();
} }
} }
+3 -3
View File
@@ -42,16 +42,16 @@ public class Pushbullet: Node
/// </summary> /// </summary>
[TextVariable(1)] [TextVariable(1)]
[Required] [Required]
public string Title { get; set; } public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets the message /// Gets or sets the message
/// </summary> /// </summary>
[Required] [Required]
[Template(2, nameof(MessageTemplates))] [Template(2, nameof(MessageTemplates))]
public string Message { get; set; } public string Message { get; set; } = string.Empty;
private static List<ListOption> _MessageTemplates; private static List<ListOption>? _MessageTemplates;
/// <summary> /// <summary>
/// Gets a list of message templates /// Gets a list of message templates
/// </summary> /// </summary>
+1 -1
View File
@@ -10,5 +10,5 @@ public class PluginSettings : IPluginSettings
/// </summary> /// </summary>
[Text(2)] [Text(2)]
[Required] [Required]
public string ApiToken { get; set; } public string ApiToken { get; set; } = string.Empty;
} }
+5 -5
View File
@@ -62,7 +62,7 @@ public class Pushover: Node
[Range(30, 86400)] [Range(30, 86400)]
public int Retry { get; set; } = 2; public int Retry { get; set; } = 2;
private static List<ListOption> _Priorities; private static List<ListOption>? _Priorities;
/// <summary> /// <summary>
/// Gets a list of message templates /// Gets a list of message templates
/// </summary> /// </summary>
@@ -83,16 +83,16 @@ public class Pushover: Node
return _Priorities; return _Priorities;
} }
} }
/// <summary> /// <summary>
/// Gets or sets the message /// Gets or sets the message
/// </summary> /// </summary>
[Required] [Required]
[Template(3, nameof(MessageTemplates))] [Template(3, nameof(MessageTemplates))]
public string Message { get; set; } public string Message { get; set; } = string.Empty;
private static List<ListOption> _MessageTemplates; private static List<ListOption>? _MessageTemplates;
/// <summary> /// <summary>
/// Gets a list of message templates /// Gets a list of message templates
/// </summary> /// </summary>
+2 -2
View File
@@ -10,12 +10,12 @@ public class PluginSettings : IPluginSettings
/// </summary> /// </summary>
[Text(1)] [Text(1)]
[Required] [Required]
public string UserKey { get; set; } public string UserKey { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets the API Token /// Gets or sets the API Token
/// </summary> /// </summary>
[Text(2)] [Text(2)]
[Required] [Required]
public string ApiToken { get; set; } public string ApiToken { get; set; } = string.Empty;
} }
+2 -2
View File
@@ -39,9 +39,9 @@ public class Telegram: Node
/// </summary> /// </summary>
[Required] [Required]
[Template(1, nameof(MessageTemplates))] [Template(1, nameof(MessageTemplates))]
public string Message { get; set; } public string Message { get; set; } = string.Empty;
private static List<ListOption> _MessageTemplates; private static List<ListOption>? _MessageTemplates;
public static List<ListOption> MessageTemplates public static List<ListOption> MessageTemplates
{ {
get get
+2 -2
View File
@@ -10,12 +10,12 @@ public class PluginSettings : IPluginSettings
/// </summary> /// </summary>
[Text(1)] [Text(1)]
[Required] [Required]
public string BotToken { get; set; } public string BotToken { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets the chat ID /// Gets or sets the chat ID
/// </summary> /// </summary>
[Text(1)] [Text(1)]
[Required] [Required]
public string ChatId { get; set; } public string ChatId { get; set; } = string.Empty;
} }
@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes; namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
@@ -77,7 +78,7 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
}; };
} }
string Execute(string ffplay, string file, NodeParameters args, int vidWidth, int vidHeight, int threshold, int seconds) string Execute(string ffmpeg, string file, NodeParameters args, int vidWidth, int vidHeight, int threshold, int seconds)
{ {
try try
{ {
@@ -88,17 +89,19 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
var intervals = GetTimeIntervals(seconds); var intervals = GetTimeIntervals(seconds);
if (intervals.Length == 0) if (intervals.Length == 0)
return string.Empty; return string.Empty;
foreach (int ss in intervals) // check at multiple times foreach (int ss in intervals) // check at multiple times
{ {
int intervalX = 0, intervalY = 0, intervalWidth = 0, intervalHeight = 0;
using (var process = new Process()) using (var process = new Process())
{ {
process.StartInfo = new ProcessStartInfo(); process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffplay; process.StartInfo.FileName = ffmpeg;
process.StartInfo.UseShellExecute = false; process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true; process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true; process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = $" -ss {ss} -i \"{file}\" -hide_banner -vframes 25 -vf cropdetect -f null -"; process.StartInfo.Arguments =
$" -ss {ss} -i \"{file}\" -hide_banner -vframes 25 -vf cropdetect -f null -";
args.Logger?.DLog("Executing ffmpeg " + process.StartInfo.Arguments); args.Logger?.DLog("Executing ffmpeg " + process.StartInfo.Arguments);
process.Start(); process.Start();
string output = process.StandardError.ReadToEnd(); string output = process.StandardError.ReadToEnd();
@@ -106,7 +109,8 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
string error = process.StandardError.ReadToEnd(); string error = process.StandardError.ReadToEnd();
process.WaitForExit(); process.WaitForExit();
var dimMatch = Regex.Match(output, @"Stream #[\d]+:[\d]+(.*?)Video:(.*?)([\d]+)x([\d]+)", RegexOptions.Multiline); var dimMatch = Regex.Match(output, @"Stream #[\d]+:[\d]+(.*?)Video:(.*?)([\d]+)x([\d]+)",
RegexOptions.Multiline);
if (dimMatch.Success == false) if (dimMatch.Success == false)
{ {
args.Logger?.WLog("Can't find dimensions for video"); args.Logger?.WLog("Can't find dimensions for video");
@@ -117,12 +121,24 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
foreach (Match match in matches) foreach (Match match in matches)
{ {
int[] parts = match.Value.Split(':').Select(x => int.Parse(x)).ToArray(); int[] parts = match.Value.Split(':').Select(x => int.Parse(x)).ToArray();
intervalX = parts[2];
intervalY = parts[3];
intervalWidth = parts[0];
intervalHeight = parts[1];
x = Math.Min(x, parts[2]); x = Math.Min(x, parts[2]);
y = Math.Min(y, parts[3]); y = Math.Min(y, parts[3]);
width = Math.Max(width, parts[0]); width = Math.Max(width, parts[0]);
height = Math.Max(height, parts[1]); height = Math.Max(height, parts[1]);
} }
} }
string imgFile = Path.Combine(args.TempPath, Guid.NewGuid() + ".jpg");
if (ExtractFrameWithFFmpeg(args, ffmpeg, file, ss, imgFile) && File.Exists(imgFile))
{
// draw rectangle on frame
args.ImageHelper?.DrawRectangleOnImage(imgFile, intervalX, intervalY, intervalWidth, intervalHeight);
args.LogImage(imgFile);
}
} }
if (width == 0 || height == 0) if (width == 0 || height == 0)
@@ -161,6 +177,63 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
{ {
int diff = (vidWidth - detectedWidth) + (vidHeight - detectedHeight); int diff = (vidWidth - detectedWidth) + (vidHeight - detectedHeight);
return (diff > threshold, diff); return (diff > threshold, diff);
}
/// <summary>
/// Extracts a frame from a video at a specified time, scales it to 640x480 resolution, and saves it as a JPEG image using FFmpeg.
/// </summary>
/// <param name="args">the node parameters</param>
/// <param name="ffmpeg">The path to the FFmpeg executable.</param>
/// <param name="inputFile">The input video file.</param>
/// <param name="ss">The time offset in seconds.</param>
/// <param name="destination">The output image file path.</param>
/// <returns>True if the frame was extracted and saved successfully, otherwise false.</returns>
static bool ExtractFrameWithFFmpeg(NodeParameters args, string ffmpeg, string inputFile, int ss, string destination)
{ try
{
// Create process start info
// Create process start info
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = ffmpeg,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};
// Specify arguments using ArgumentList to avoid escaping issues
psi.ArgumentList.Add("-ss");
psi.ArgumentList.Add(ss.ToString());
psi.ArgumentList.Add("-i");
psi.ArgumentList.Add(inputFile);
// psi.ArgumentList.Add("-vf");
// psi.ArgumentList.Add("select=eq(n\\,0),scale=640:480");
psi.ArgumentList.Add("-vframes");
psi.ArgumentList.Add("1");
psi.ArgumentList.Add(destination);
// Start the process
using (Process process = Process.Start(psi))
{
// Capture and display the output
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
// Wait for the process to exit
process.WaitForExit();
if (process.ExitCode == 0)
return true;
args.Logger?.WLog($"Error extracting frame: {error}");
return false;
}
}
catch (Exception ex)
{
args.Logger?.ELog($"An error occurred: {ex.Message}");
return false;
}
} }
} }