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

View File

@@ -29,7 +29,7 @@ public class Apprise: Node
[Select(nameof(MessageTypeOptions), 2)]
public string MessageType { get; set; } = string.Empty;
private static List<ListOption> _MessageTypeOptions;
private static List<ListOption>? _MessageTypeOptions;
public static List<ListOption> MessageTypeOptions
{
get
@@ -54,9 +54,9 @@ public class Apprise: Node
/// </summary>
[Required]
[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
{
get

View File

@@ -165,7 +165,7 @@ public class FFprobeTimeSpanConverter : JsonConverter<TimeSpan>
{
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)
return default;

View File

@@ -111,7 +111,9 @@ public class AudioFileNormalization : AudioNode
args.Logger?.WLog("Failed to parse TwoPass json\"");
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}";
return (true, ar);
}

View File

@@ -314,10 +314,10 @@ namespace FileFlows.AudioNodes
}
var ffArgs = GetArguments(args, out string extension);
string actualExt = args.ReplaceVariables(CustomExtension, stripMissing: true)?.EmptyAsNull() ??
extension?.EmptyAsNull() ?? DefaultExtension;
string outputFile = FileHelper.Combine(args.TempPath, Guid.NewGuid() + "." + actualExt.TrimStart('.'));
var ffArgs = GetArguments(args, out var extension);
var actualExt = args.ReplaceVariables(CustomExtension, stripMissing: true)?.EmptyAsNull() ??
extension?.EmptyAsNull() ?? DefaultExtension;
var outputFile = FileHelper.Combine(args.TempPath, Guid.NewGuid() + "." + actualExt.TrimStart('.'));
ffArgs.Insert(0, "-hide_banner");
ffArgs.Insert(1, "-y"); // tells ffmpeg to replace the file if already exists, which it shouldnt but just incase

View File

@@ -23,23 +23,23 @@ public class Discord: Node
/// Gets or sets the title
/// </summary>
[TextVariable(1)]
public string Title { get; set; }
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the message type
/// </summary>
[DefaultValue("standard")]
[Select(nameof(MessageTypeOptions), 2)]
public string MessageType { get; set; }
public string MessageType { get; set; } = "standard";
/// <summary>
/// Gets or sets the message
/// </summary>
[Required]
[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
{
get
@@ -60,7 +60,7 @@ public class Discord: Node
}
}
private static List<ListOption> _MessageTemplates;
private static List<ListOption>? _MessageTemplates;
public static List<ListOption> MessageTemplates
{
get

View File

@@ -10,12 +10,12 @@ public class PluginSettings:IPluginSettings
/// </summary>
[Text(1)]
[Required]
public string WebhookId { get; set; }
public string WebhookId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the webhook token for this plugin
/// </summary>
[Text(2)]
[Required]
public string WebhookToken { get; set; }
public string WebhookToken { get; set; } = string.Empty;
}

View File

@@ -9,21 +9,21 @@
{
[Required]
[Text(1)]
public string SmtpServer { get; set; }
public string SmtpServer { get; set; } = string.Empty;
[Range(1, 6555)]
[NumberInt(1)]
public int SmtpPort { get; set; }
[Text(2)]
public string SmtpUsername { get; set; }
public string SmtpUsername { get; set; } = string.Empty;
[Password(3)]
public string SmtpPassword { get; set; }
public string SmtpPassword { get; set; } = string.Empty;
[Text(4)]
[Required]
[System.ComponentModel.DataAnnotations.RegularExpression(@"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
public string Sender { get; set; }
public string Sender { get; set; } = string.Empty;
}
}

View File

@@ -17,10 +17,10 @@ public class EmbyUpdater: Node
public override bool NoEditorOnAdd => true;
[Text(1)]
public string ServerUrl { get; set; }
public string ServerUrl { get; set; } = string.Empty;
[Text(2)]
public string AccessToken { get; set; }
public string AccessToken { get; set; } = string.Empty;
[KeyValue(3, null)]
public List<KeyValuePair<string, string>> Mapping { get; set; }

View File

@@ -8,13 +8,13 @@
{
[Text(1)]
[Required]
public string ServerUrl { get; set; }
public string ServerUrl { get; set; } = string.Empty;
[Text(2)]
[Required]
public string AccessToken { get; set; }
public string AccessToken { get; set; } = string.Empty;
[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.

View File

@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace FileFlows.Gotify.Communication;
@@ -42,7 +43,7 @@ public class Gotify: Node
/// Gets or sets the title of the message
/// </summary>
[TextVariable(1)]
public string Title { get; set; }
public string Title { get; set; } = string.Empty;
/// <summary>
@@ -58,9 +59,9 @@ public class Gotify: Node
/// </summary>
[Required]
[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
{
get
@@ -96,6 +97,7 @@ File shrunk in size by: {{ difference | file_size }} / {{ percent }}%
/// </summary>
/// <param name="args">the node parameters</param>
/// <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)
{
try

View File

@@ -10,12 +10,12 @@ public class PluginSettings : IPluginSettings
/// </summary>
[Text(1)]
[Required]
public string ServerUrl { get; set; }
public string ServerUrl { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the Access Token for the server
/// </summary>
[Text(2)]
[Required]
public string AccessToken { get; set; }
public string AccessToken { get; set; } = string.Empty;
}

View File

@@ -9,13 +9,13 @@
{
[Text(1)]
[Required]
public string ServerUrl { get; set; }
public string ServerUrl { get; set; } = string.Empty;
[Text(2)]
[Required]
public string AccessToken { get; set; }
public string AccessToken { get; set; } = string.Empty;
[KeyValue(3, null)]
public List<KeyValuePair<string, string>> Mapping { get; set; }
[KeyValue(3, null)]
public List<KeyValuePair<string, string>> Mapping { get; set; } = new();
}
}

View File

@@ -42,16 +42,16 @@ public class Pushbullet: Node
/// </summary>
[TextVariable(1)]
[Required]
public string Title { get; set; }
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the message
/// </summary>
[Required]
[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>
/// Gets a list of message templates
/// </summary>

View File

@@ -10,5 +10,5 @@ public class PluginSettings : IPluginSettings
/// </summary>
[Text(2)]
[Required]
public string ApiToken { get; set; }
public string ApiToken { get; set; } = string.Empty;
}

View File

@@ -62,7 +62,7 @@ public class Pushover: Node
[Range(30, 86400)]
public int Retry { get; set; } = 2;
private static List<ListOption> _Priorities;
private static List<ListOption>? _Priorities;
/// <summary>
/// Gets a list of message templates
/// </summary>
@@ -83,16 +83,16 @@ public class Pushover: Node
return _Priorities;
}
}
/// <summary>
/// Gets or sets the message
/// </summary>
[Required]
[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>
/// Gets a list of message templates
/// </summary>

View File

@@ -10,12 +10,12 @@ public class PluginSettings : IPluginSettings
/// </summary>
[Text(1)]
[Required]
public string UserKey { get; set; }
public string UserKey { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the API Token
/// </summary>
[Text(2)]
[Required]
public string ApiToken { get; set; }
public string ApiToken { get; set; } = string.Empty;
}

View File

@@ -39,9 +39,9 @@ public class Telegram: Node
/// </summary>
[Required]
[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
{
get

View File

@@ -10,12 +10,12 @@ public class PluginSettings : IPluginSettings
/// </summary>
[Text(1)]
[Required]
public string BotToken { get; set; }
public string BotToken { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the chat ID
/// </summary>
[Text(1)]
[Required]
public string ChatId { get; set; }
public string ChatId { get; set; } = string.Empty;
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.IO;
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
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
{
@@ -88,17 +89,19 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
var intervals = GetTimeIntervals(seconds);
if (intervals.Length == 0)
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())
{
process.StartInfo = new ProcessStartInfo();
process.StartInfo.FileName = ffplay;
process.StartInfo.FileName = ffmpeg;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = 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);
process.Start();
string output = process.StandardError.ReadToEnd();
@@ -106,7 +109,8 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
string error = process.StandardError.ReadToEnd();
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)
{
args.Logger?.WLog("Can't find dimensions for video");
@@ -117,12 +121,24 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
foreach (Match match in matches)
{
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]);
y = Math.Min(y, parts[3]);
width = Math.Max(width, parts[0]);
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)
@@ -161,6 +177,63 @@ public class FfmpegBuilderCropBlackBars : FfmpegBuilderNode
{
int diff = (vidWidth - detectedWidth) + (vidHeight - detectedHeight);
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;
}
}
}