mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-19 23:19:30 -06:00
FF-1493: Added ability to save images to the file log
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,5 +10,5 @@ public class PluginSettings : IPluginSettings
|
||||
/// </summary>
|
||||
[Text(2)]
|
||||
[Required]
|
||||
public string ApiToken { get; set; }
|
||||
public string ApiToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user