FF-2049: Fixing TVEpisodeLookup

This commit is contained in:
John Andrews
2025-02-10 18:40:10 +13:00
parent ec1121ec79
commit bc32a80011
17 changed files with 227 additions and 20 deletions

View File

@@ -86,6 +86,12 @@ namespace FileFlows.AudioNodes
variables.AddOrUpdate("audio.Disc", AudioInfo.Disc < 1 ? 1 : AudioInfo.Disc);
variables.AddOrUpdate("audio.TotalDiscs", AudioInfo.TotalDiscs < 1 ? 1 : AudioInfo.TotalDiscs);
args.SetTraits(new string[]
{
AudioInfo.Codec,
FormatBitrate(AudioInfo.Bitrate),
FormatAudioChannels(AudioInfo.Channels)
}.Where(x => x != null).ToArray());
var metadata = new Dictionary<string, object>();
metadata.Add("Duration", AudioInfo.Duration);
@@ -192,5 +198,47 @@ namespace FileFlows.AudioNodes
SetAudioInfo(args, result.Value, Variables, filename);
return true;
}
/// <summary>
/// Converts an audio bitrate (in bits per second) to a human-readable format.
/// </summary>
/// <param name="bitrate">The bitrate in bits per second.</param>
/// <returns>
/// A human-readable string representation of the bitrate, such as "128 kbps" or "1.4 Mbps".
/// Returns <c>null</c> if the bitrate is 0.
/// </returns>
public static string? FormatBitrate(long bitrate)
{
if (bitrate < 1)
return null;
if (bitrate < 1000)
return $"{bitrate} bps";
if (bitrate < 1_000_000)
return $"{bitrate / 1000.0:0.#} kbps";
return $"{bitrate / 1_000_000.0:0.#} Mbps";
}/// <summary>
/// Converts the number of audio channels into a human-readable format.
/// </summary>
/// <param name="channels">The number of audio channels.</param>
/// <returns>
/// A human-readable string representation of the channels, such as "Mono", "Stereo", or "5.1".
/// Returns <c>null</c> if the number of channels is 0.
/// </returns>
public static string? FormatAudioChannels(long channels)
{
return channels switch
{
0 => null,
1 => "Mono",
2 => "Stereo",
3 => "2.1",
4 => "Quad",
5 => "4.1",
6 => "5.1",
7 => "6.1",
8 => "7.1",
_ => $"{channels} channels"
};
}
}
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.IO.Enumeration;
using FileFlows.Plugin.Helpers;
namespace FileFlows.ComicNodes.Comics;
@@ -174,6 +175,8 @@ public class ComicConverter: Node
return -1;
}
List<string> traits = [];
if (DeleteNonPageImages)
{
List<string> nonPages = new();
@@ -228,6 +231,11 @@ public class ComicConverter: Node
args.Logger?.ILog("Total Files: " + files.Length);
args.PartPercentageUpdate?.Invoke(0);
int count = 0;
if (files.Length == 1)
traits.Add("1 Page");
else
traits.Add($"{files.Length} Pages");
traits.Add(Codec.ToLowerInvariant() == "webp" ? "WebP" : "JPEG");
for (int i = 0; i < files.Length; i++)
{
@@ -296,6 +304,9 @@ public class ComicConverter: Node
args.Logger?.ELog(args.FailureReason);
return -1;
}
if(traits.Count > 0)
args.SetTraits(traits.ToArray());
args.SetWorkingFile(newFileResult.Value);
if(Format == "CBZ")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -77,6 +77,7 @@ public class FfmpegBuilderTrackRemover: TrackSelectorFlowElement<FfmpegBuilderT
{
bool removing = false;
int index = -1;
List<string> removed = [];
foreach (var track in tracks)
{
if (track.Deleted)
@@ -89,10 +90,21 @@ public class FfmpegBuilderTrackRemover: TrackSelectorFlowElement<FfmpegBuilderT
continue;
}
Args.Logger?.ILog($"Deleting Stream: {track}");
removed.Add(" - Removed: " + track.ToString());
track.Deleted = true;
removing = true;
}
return removing;
if (removing == false)
{
Args.Logger?.ILog("Nothing removed!");
return false;
}
Args.Logger?.ILog("\n------------------------------ Removed Summary ------------------------------ \n" +
string.Join("\n", removed) +
"\n-----------------------------------------------------------------------------");
return true;
}
}

View File

@@ -26,4 +26,33 @@ public class ChannelHelper
// Otherwise, round to the nearest integer
return (int)Math.Round(channels);
}
/// <summary>
/// Converts the number of audio channels (as a float) into a human-readable format,
/// handling floating-point precision issues.
/// </summary>
/// <param name="channels">The number of audio channels as a float.</param>
/// <returns>
/// A human-readable string representation of the channels, such as "Mono", "Stereo", or "5.1".
/// Returns <c>null</c> if the number of channels is 0.
/// </returns>
public static string? FormatAudioChannels(float channels)
{
if (channels == 0)
return null;
if (Approximately(channels, 1f)) return "Mono";
if (Approximately(channels, 2f)) return "Stereo";
if (Approximately(channels, 2.1f)) return "2.1";
if (Approximately(channels, 4f)) return "Quad";
if (Approximately(channels, 4.1f)) return "4.1";
if (channels is >= 5f and <= 6f) return "5.1"; // Covers 5, 5.1, 5.0999, 6
if (Approximately(channels, 6.1f)) return "6.1";
if (channels is >= 7f and <= 8f) return "7.1"; // Covers 7, 7.1, 8
return $"{channels} channels";
static bool Approximately(float a, float b, float epsilon = 0.05f)
=> Math.Abs(a - b) < epsilon;
}
}

View File

@@ -0,0 +1,55 @@
namespace FileFlows.VideoNodes.Helpers;
/// <summary>
/// Video Helper
/// </summary>
public class VideoHelper
{
/// <summary>
/// Determines the closest standard video resolution label based on width and height.
/// </summary>
/// <param name="width">The width of the video.</param>
/// <param name="height">The height of the video.</param>
/// <returns>
/// A resolution label such as "SD", "720p", "1080p", or "4K".
/// Returns <c>null</c> if the resolution does not match any standard.
/// </returns>
public static string FormatResolution(int width, int height)
{
if (width <= 0 || height <= 0)
return null;
if (Approximately(width, 7680) || Approximately(height, 4320))
return "8K";
if (Approximately(width, 3840) || Approximately(height, 2160))
return "4K";
if (Approximately(width, 1920) || Approximately(height, 1080))
return "1080p";
if (Approximately(width, 1280) || Approximately(height, 720))
return "720p";
if (Approximately(width, 640) || Approximately(height, 360))
return "360p";
if (Approximately(width, 480) || Approximately(height, 360))
return "480p";
if (Approximately(width, 720) || Approximately(height, 480))
return "SD";
if (Approximately(width, 426) || Approximately(height, 240))
return "240p";
return null;
static bool Approximately(int value, int target)
{
int tolerance = (int)(target * 0.1); // 10% tolerance
return Math.Abs(value - target) <= tolerance;
}
}
}

View File

@@ -80,7 +80,7 @@ public class VideoFile : VideoNode
return -1;
}
var videoInfoResult = new VideoInfoHelper(FFMPEG, args.Logger).Read(file);
var videoInfoResult = new VideoInfoHelper(FFMPEG, args.Logger, args).Read(file);
if (videoInfoResult.Failed(out string error))
{
args.FailureReason = error;

View File

@@ -13,10 +13,15 @@ public class FFmpegBuilder_TrackRemoverTests : VideoTestBase
VideoInfo vii;
NodeParameters args;
FfmpegModel Model;
protected override void TestStarting()
{
args = GetVideoNodeParameters();
InitializeModel();
}
private void InitializeModel(string filename = null)
{
args = GetVideoNodeParameters(filename);
VideoFile vf = new VideoFile();
vf.PreExecute(args);
vf.Execute(args);
@@ -94,6 +99,40 @@ public class FFmpegBuilder_TrackRemoverTests : VideoTestBase
Assert.AreEqual("fre", nonDeletedSubtitles[0].Language);
}
/// <summary>
/// Test German is matched and removed
/// </summary>
[TestMethod]
public void RemoveNonGerman()
{
InitializeModel(VideoEngGerAudio);
vii.AudioStreams[1].Language = "ger";
Model.AudioStreams[1].Language = "ger";
int originaluNonDeletedSubtitles = Model.AudioStreams.Count(x => x.Deleted == false);
FfmpegBuilderTrackRemover ffRemover = new();
ffRemover.CustomTrackSelection = true;
ffRemover.TrackSelectionOptions = new();
ffRemover.TrackSelectionOptions.Add(new ("Language", "!deu"));
ffRemover.StreamType = "Audio";
ffRemover.PreExecute(args);
Assert.AreEqual(1, ffRemover.Execute(args));
List<FfmpegAudioStream> nonDeletedAudio = new();
Logger.ILog(new string('-', 100));
foreach (var stream in Model.AudioStreams.Where(x => x.Deleted))
{
Logger.ILog("Deleted audio: " + stream);
}
Logger.ILog(new string('-', 100));
foreach (var stream in Model.AudioStreams.Where(x => x.Deleted == false))
{
Logger.ILog("Non audio subtitle: " + stream);
nonDeletedAudio.Add(stream);
}
Assert.AreNotEqual(originaluNonDeletedSubtitles, nonDeletedAudio.Count);
Assert.AreEqual(nonDeletedAudio.Count, 1);
Assert.IsTrue("deu" == nonDeletedAudio[0].Language || nonDeletedAudio[0].Language == "ger");
}
/// <summary>
/// Tests english subtitles are removed
/// </summary>

View File

@@ -168,9 +168,9 @@ public class FfmpegBuilder_AudioConverterTests: VideoTestBase
[TestCategory("Slow")]
public void FfmpegBuilder_AudioConverter_Opus_All()
{
var vi = new VideoInfoHelper(FFmpeg, Logger);
var vii = vi.Read(VideoMkv);
var args = GetVideoNodeParameters(VideoMkv);
var vi = new VideoInfoHelper(FFmpeg, Logger, args);
var vii = vi.Read(VideoMkv);
args.Parameters.Add("VideoInfo", vii);
FfmpegBuilderStart ffStart = new();

View File

@@ -50,6 +50,11 @@ public abstract class VideoTestBase : TestBase
/// Video with many subtitles file
/// </summary>
protected static readonly string VideoSubtitles = ResourcesTestFilesDir + "/subtitles.mkv";
/// <summary>
/// Video with english and german audio
/// </summary>
protected static readonly string VideoEngGerAudio = ResourcesTestFilesDir + "/eng_ger_audio.mp4";
/// <summary>
/// Audio MP3 file

View File

@@ -1,12 +1,12 @@
using System.Diagnostics;
using System.Globalization;
using FileFlows.VideoNodes.Helpers;
namespace FileFlows.VideoNodes;
public class VideoInfoHelper
public class VideoInfoHelper(string ffMpegExe, ILogger logger, NodeParameters args)
{
private string ffMpegExe;
private ILogger Logger;
private ILogger Logger = logger;
static Regex rgxTitle = new Regex(@"(?<=((^[\s]+title[\s]+:[\s])))(.*?)$", RegexOptions.Multiline);
static Regex rgxDuration = new Regex(@"(?<=((^[\s]+DURATION(\-[\w]+)?[\s]+:[\s])))([\d]+:?)+\.[\d]{1,7}", RegexOptions.Multiline);
@@ -45,17 +45,25 @@ public class VideoInfoHelper
set => _AnalyzeDuration = Math.Max(32, value);
}
public VideoInfoHelper(string ffMpegExe, ILogger logger)
{
this.ffMpegExe = ffMpegExe;
this.Logger = logger;
}
public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");
public Result<VideoInfo> Read(string filename)
=> ReadStatic(Logger, ffMpegExe, filename);
{
var result = ReadStatic(Logger, ffMpegExe, filename);
if (result.IsFailed == false)
{
var vi = result.Value;
args.SetTraits(new string[]
{
vi.VideoStreams?.FirstOrDefault()?.Codec,
vi.AudioStreams?.FirstOrDefault()?.Codec,
ChannelHelper.FormatAudioChannels(vi.AudioStreams?.FirstOrDefault()?.Channels ?? 0),
VideoHelper.FormatResolution(vi?.VideoStreams?.FirstOrDefault()?.Width ?? 0 , vi?.VideoStreams?.FirstOrDefault()?.Height ?? 0),
}.Where(x => string.IsNullOrWhiteSpace(x) == false).ToArray());
}
return result;
}
internal static Result<VideoInfo> ReadStatic(ILogger logger, string ffMpegExe, string filename)
{
#if(DEBUG) // UNIT TESTING

View File

@@ -87,7 +87,7 @@ namespace FileFlows.VideoNodes
args.SetWorkingFile(outputFile);
// get the new video info
var videoInfo = new VideoInfoHelper(ffmpegExe, args.Logger).Read(outputFile).ValueOrDefault;
var videoInfo = new VideoInfoHelper(ffmpegExe, args.Logger, args).Read(outputFile).ValueOrDefault;
SetVideoInfo(args, videoInfo, this.Variables ?? new Dictionary<string, object>());
}
else if (success.successs == false)

View File

@@ -66,7 +66,7 @@ public class ReadVideoInfo: EncodingNode
return -1;
}
var videoInfoResult = new VideoInfoHelper(FFMPEG, args.Logger).Read(localFileResult.Value);
var videoInfoResult = new VideoInfoHelper(FFMPEG, args.Logger, args).Read(localFileResult.Value);
if (videoInfoResult.Failed(out string error))
{
args.Logger.ELog(error);

View File

@@ -318,7 +318,7 @@ namespace FileFlows.VideoNodes
return null;
}
var viResult = new VideoInfoHelper(FFMPEG, args.Logger).Read(local);
var viResult = new VideoInfoHelper(FFMPEG, args.Logger, args).Read(local);
if (viResult.Failed(out string error))
{
args.Logger?.ELog(error);