From 7b1058666c138126daab8b1f4e5edf302befd5bf Mon Sep 17 00:00:00 2001 From: reven Date: Wed, 12 Jan 2022 12:49:01 +1300 Subject: [PATCH] added MusicNodes plugin --- MusicNodes/ExtensionMethods.cs | 17 ++ MusicNodes/InputNodes/MusicFile.cs | 62 +++++++ MusicNodes/MusicInfo.cs | 18 ++ MusicNodes/MusicInfoHelper.cs | 139 ++++++++++++++ MusicNodes/MusicNodes.csproj | Bin 0 -> 3926 bytes MusicNodes/MusicNodes.en.json | 110 ++++------- MusicNodes/Nodes/ConvertNode.cs | 231 ++++++++++++++++++++++++ MusicNodes/Nodes/ID3Tagger.cs | 12 ++ MusicNodes/Nodes/MusicNode.cs | 82 +++++++++ MusicNodes/Plugin.cs | 15 ++ MusicNodes/Tests/ConvertTests.cs | 99 ++++++++++ MusicNodes/Tests/TestLogger.cs | 47 +++++ build/utils/spellcheck/ignoredwords.txt | 10 +- 13 files changed, 770 insertions(+), 72 deletions(-) create mode 100644 MusicNodes/ExtensionMethods.cs create mode 100644 MusicNodes/InputNodes/MusicFile.cs create mode 100644 MusicNodes/MusicInfo.cs create mode 100644 MusicNodes/MusicInfoHelper.cs create mode 100644 MusicNodes/MusicNodes.csproj create mode 100644 MusicNodes/Nodes/ConvertNode.cs create mode 100644 MusicNodes/Nodes/ID3Tagger.cs create mode 100644 MusicNodes/Nodes/MusicNode.cs create mode 100644 MusicNodes/Plugin.cs create mode 100644 MusicNodes/Tests/ConvertTests.cs create mode 100644 MusicNodes/Tests/TestLogger.cs diff --git a/MusicNodes/ExtensionMethods.cs b/MusicNodes/ExtensionMethods.cs new file mode 100644 index 00000000..264e1947 --- /dev/null +++ b/MusicNodes/ExtensionMethods.cs @@ -0,0 +1,17 @@ +namespace FileFlows.MusicNodes +{ + internal static class ExtensionMethods + { + public static void AddOrUpdate(this Dictionary dict, string key, object value) + { + if (dict.ContainsKey(key)) + dict[key] = value; + else + dict.Add(key, value); + } + public static string? EmptyAsNull(this string str) + { + return str == string.Empty ? null : str; + } + } +} diff --git a/MusicNodes/InputNodes/MusicFile.cs b/MusicNodes/InputNodes/MusicFile.cs new file mode 100644 index 00000000..b0cff6ec --- /dev/null +++ b/MusicNodes/InputNodes/MusicFile.cs @@ -0,0 +1,62 @@ +namespace FileFlows.MusicNodes +{ + using System.ComponentModel; + using FileFlows.Plugin; + using FileFlows.Plugin.Attributes; + + public class MusicFile : MusicNode + { + public override int Outputs => 1; + public override FlowElementType Type => FlowElementType.Input; + + private Dictionary _Variables; + public override Dictionary Variables => _Variables; + public MusicFile() + { + _Variables = new Dictionary() + { + { "mi.Album", "Album" }, + { "mi.BitRate", 845 }, + { "mi.Channels", 2 }, + { "mi.Codec", "flac" }, + { "mi.Date", new DateTime(2020, 05, 23) }, + { "mi.Duration", 256 }, + { "mi.Encoder", "FLAC 1.2.1" }, + { "mi.Frequency", 44100 }, + { "mi.Genres", new [] { "Pop", "Rock" } }, + { "mi.Language", "English" }, + { "mi.Title", "Song Title" }, + { "mi.Track", 2 } + }; + } + + public override int Execute(NodeParameters args) + { + string ffmpegExe = GetFFMpegExe(args); + if (string.IsNullOrEmpty(ffmpegExe)) + return -1; + + try + { + + var videoInfo = new MusicInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile); + if (videoInfo.Duration == 0) + { + args.Logger.ILog("Failed to load music information."); + return 0; + } + + SetMusicInfo(args, videoInfo, Variables); + + return 1; + } + catch (Exception ex) + { + args.Logger.ELog("Failed processing MusicFile: " + ex.Message); + return -1; + } + } + } + + +} \ No newline at end of file diff --git a/MusicNodes/MusicInfo.cs b/MusicNodes/MusicInfo.cs new file mode 100644 index 00000000..fc39530d --- /dev/null +++ b/MusicNodes/MusicInfo.cs @@ -0,0 +1,18 @@ +namespace FileFlows.MusicNodes +{ + public class MusicInfo + { + public string Language { get; set; } + public int Track { get; set; } + public string Title { get; set; } + public string Album { get; set; } + public DateTime Date { get; set; } + public string[] Genres { get; set; } + public string Encoder { get; set; } + public long Duration { get; set; } + public long BitRate { get; set; } + public string Codec { get; set; } + public long Channels { get; set; } + public long Frequency { get; set; } + } +} \ No newline at end of file diff --git a/MusicNodes/MusicInfoHelper.cs b/MusicNodes/MusicInfoHelper.cs new file mode 100644 index 00000000..3dc55da0 --- /dev/null +++ b/MusicNodes/MusicInfoHelper.cs @@ -0,0 +1,139 @@ +namespace FileFlows.MusicNodes +{ + using System.Diagnostics; + using System.IO; + using System.Text.RegularExpressions; + using FileFlows.Plugin; + + public class MusicInfoHelper + { + private string ffMpegExe; + private ILogger Logger; + + public MusicInfoHelper(string ffMpegExe, ILogger logger) + { + this.ffMpegExe = ffMpegExe; + this.Logger = logger; + } + + public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg"); + public MusicInfo Read(string filename) + { + var mi = new MusicInfo(); + if (File.Exists(filename) == false) + { + Logger.ELog("File not found: " + filename); + return mi; + } + if (string.IsNullOrEmpty(ffMpegExe) || File.Exists(ffMpegExe) == false) + { + Logger.ELog("FFMpeg not found: " + (ffMpegExe ?? "not passed in")); + return mi; + } + + try + { + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo(); + process.StartInfo.FileName = ffMpegExe; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.Arguments = $"-hide_banner -i \"{filename}\""; + process.Start(); + string output = process.StandardError.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (string.IsNullOrEmpty(error) == false && error != "At least one output file must be specified") + { + Logger.ELog("Failed reading ffmpeg info: " + error); + return mi; + } + + Logger.ILog("Music Information:" + Environment.NewLine + output); + + if(output.IndexOf("Input #0") < 0) + { + Logger.ELog("Failed to read audio information for file"); + return mi; + } + + foreach(string line in output.Split('\n')) + { + int colonIndex = line.IndexOf(":"); + if(colonIndex < 1) + continue; + if(line.Trim().StartsWith("Language")) + mi.Language = line.Substring(colonIndex + 1).Trim(); + else if (line.Trim().StartsWith("track")) + { + if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value)) + mi.Track = value; + } + else if (line.Trim().StartsWith("Title")) + mi.Title = line.Substring(colonIndex + 1).Trim(); + else if (line.Trim().StartsWith("Album")) + mi.Album = line.Substring(colonIndex + 1).Trim(); + else if (line.Trim().StartsWith("Date") && mi.Date < new DateTime(1900, 1, 1)) + { + if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value)) + mi.Date = new DateTime(value, 1, 1); + } + else if (line.Trim().StartsWith("Retail Date")) + { + if (DateTime.TryParse(line.Substring(colonIndex + 1).Trim(), out DateTime value)) + mi.Date = value; + } + else if (line.Trim().StartsWith("Genre")) + mi.Genres = line.Substring(colonIndex + 1).Trim().Split(' '); + else if (line.Trim().StartsWith("Encoder")) + mi.Encoder = line.Substring(colonIndex + 1).Trim(); + else if (line.Trim().StartsWith("Duration")) + { + string temp = line.Substring(colonIndex + 1).Trim(); + if(temp.IndexOf(",") > 0) + { + temp = temp.Substring(0, temp.IndexOf(",")); + if (TimeSpan.TryParse(temp, out TimeSpan value)) + mi.Duration = (long)value.TotalSeconds; + } + } + + + if (line.IndexOf("bitrate:") > 0) + { + string br = line.Substring(line.IndexOf("bitrate:") + "bitrate:".Length).Trim(); + if (br.IndexOf(" ") > 0) + { + br = br.Substring(0, br.IndexOf(" ")); + if (long.TryParse(br, out long value)) + mi.BitRate = value; + } + } + + var match = Regex.Match(line, @"([\d]+) Hz"); + if (match.Success) + { + mi.Frequency = int.Parse(match.Groups[1].Value); + } + + if (line.IndexOf(" stereo,") > 0) + mi.Channels = 2; + + } + + } + } + catch (Exception ex) + { + Logger.ELog(ex.Message, ex.StackTrace.ToString()); + } + + return mi; + } + + } +} \ No newline at end of file diff --git a/MusicNodes/MusicNodes.csproj b/MusicNodes/MusicNodes.csproj new file mode 100644 index 0000000000000000000000000000000000000000..b702c466478a172f586426486b1089ad55dc2e4b GIT binary patch literal 3926 zcmeH~Ur!TJ5XI*e6TicTm>>^s3nC>7B?Lh+f{he?@ntEcR7%^>B9idg)!(_dm)-3@ zK;+Fd?e2Ewo;!19?#%4(U#qrmKkdv~7TFK0+qw0uX&r0Zn$6mR#a84RA!%EU|BsNi zt>L5>*0*DOfz~oQ>sGc^tANn4@95W%Mz+t2Q>VSl%9@k*&^SgK+n#M%m8;j91>vbZ zao>ow0j=xo>f4pQbL%gxJJ6Jt*Q{ECtbu0Vwz*62scx6-bk6=|Y?fr%`Lk*TcAc&- zJ5ux=(7JFW51QZ{IF`gi4{ULh*dg*dHWzHVb{=`pE?C#PSwD(4s*y+7agk|(bHx4| z4s8>^+t?|~#D`ee=cxtnTg1EX7*d4F?*8J&az**?v1HlIAR-kk5+_YKI$?L8H{xiU zdtLK;-$g&~dBFJsR+_hEGFDj|yX;-DB^Q%Px_LyOy+>FsZtj9ReZ^!xs#x44tCXcj zQ<)gn%wj6e%FKi~Ux5<5+=kx{+23=%4{K!+Ht@Aie3T)|^wtb(=1~$>oZ^p8veyi?s>v>Wesz}0880IC6kic?RlAy7A#YU?B6d!F57za_ zN7a)m_UlAwtMxbs*S% z#x8wq@D_#Wgmua$ef=&BqCL6R zr@B5b#%V^TTvy(zN~e8n&Ys!xflQV)U|u~U=*-)-)2I*={Tuw7w>f&(8o%=3(8cT8 zos28cD)fOlI-l=aX}1ugUmc6en*}RTjTY?9;BJw-P3qwa-DrW|^vP=lS<$kZMSGdk zT*^GA#EV3^}sH8LCqY(s^xCBqL7HZD(_i8lh7t-Six$CvS$CL$hMG&MUrY z?Nf!0uqr#VXKDwa~haRV%C;gZ= z6upJCVJeU7E1y3 zj8{?SZAfnx59jS}CqtbuOefY%$tCwDlWv^w6plqA7vxW2g#=~E_r@N^=tnC1+8+L literal 0 HcmV?d00001 diff --git a/MusicNodes/MusicNodes.en.json b/MusicNodes/MusicNodes.en.json index 77114167..c90305a4 100644 --- a/MusicNodes/MusicNodes.en.json +++ b/MusicNodes/MusicNodes.en.json @@ -1,76 +1,44 @@ { - "Flow":{ - "Parts": { - "AudioTrackReorder": { - "Description": "Allows you to reorder audio tracks in the preferred order.\n\nEnter the audio codecs in the order you want. Any not listed will be ordered after the ones entered in their original order.\nIf there are multiple tracks with same codec, they will be ordered first by the order you entered, then in their original order.\n\nOutput 1: Audio tracks were reordered\nOutput 2: Audio tracks did not need reordering", - "Fields": { - "OrderedTracks": "Ordered Audio Codecs" - } - }, - "VideoFile": { - "Description": "An input video file that has had its VideoInformation read and can be processed" - }, - "DetectBlackBars": { - "Description": "Processes a video file and scans for black bars in the video.\n\nIf found a parameter \"VideoCrop\" will be added.\n\nOutput 1: Black bars detected\nOutput 2: Not detected", - "Fields": { - "CroppingThreshold": "Threshold", - "CroppingThreshold-Help": "The amount of pixels that must be greater than to crop. E.g. if there's only 5 pixels detected as black space, you may consider this too small to crop." - } - }, - "SubtitleRemover": { - "Description": "Removes subtitles from a video file if found..\n\nOutput 1: Subtitles were removed\nOutput 2: No subtitles found that needed to be removed", - "Fields": { - "SubtitlesToRemove": "Subtitles To Remove" - } - }, - "VideoCodec": { - "Description": "This node will check the codecs in the input file, and trigger when matched.\n\nOutput 1: Matches\nOutput 2: Does not match", - "Fields": { - "Codecs": "Codecs", - "Codecs-Help": "Enter a list of case insensitive video or audio codecs.\nEg hevc, h265, mpeg4, ac3" - } - }, - "VideoEncode": { - "Description": "A generic video encoding node, this lets you customize how to encode a video file using ffmpeg.\n\nOutput 1: Video was processed\nOutput 2: No processing required", - "Fields": { - "Extension": "Extension", - "Extension-Help": "The file extension to use on the newly created file", - "VideoCodec": "Video Codec", - "VideoCodec-Help": "The video codec the video should be in, for example hevc, h264", - "VideoCodecParameters": "Video Codec Parameters", - "VideoCodecParameters-Help": "The parameters to use to encode the video, eg. \"hevc_nvenc -preset hq -crf 23\" to encode into hevc using the HQ preset a constant rate factor of 23 and using NVIDIA hardware acceleration.", - "AudioCodec": "Audio Codec", - "AudioCodec-Help": "The audio codec to encode the video with", - "Language": "Language", - "Language-Help": "Optional ISO 639-2 language code to use. Will attempt to find an audio track with this language code if not the best audio track will be used.\nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes" - } - }, - "Video_H265_AC3": { - "Description": "This will ensure all videos are encoded in H265 (if not already encoded) and that AC3 audio is the first audio channel\n\nOutput 1: Video was processed\nOutput 2: No processing required", - "Fields": { - "Language": "Language", - "Language-Help": "Optional ISO 639-2 language code to use. Will attempt to find an audio track with this language code if not the best audio track will be used.\nhttps://en.wikipedia.org/wiki/List_of_ISO_639-2_codes", - "Crf": "Constant Rate Factor", - "Crf-Help": "Refer to ffmpeg for more details, the lower the value the bigger the file. A good value is around 19-23. Default is 21.", - "NvidiaEncoding": "NVIDIA Encoding", - "NvidiaEncoding-Help": "If NVIDIA hardware encoding should be used. If you do not have a supported NVIDIA card the encoding will fail.", - "Threads": "Threads", - "Threads-Help": "Only used if not using NVIDIA. If set to 0, the threads will use FFMpegs defaults.", - "NormalizeAudio": "Normalize Audio", - "NormalizeAudio-Help": "If the audio track should have its volume level normalized", - "ForceRencode": "Force Re-Encode", - "ForceRencode-Help": "If the video should always be re-encoded regardless if it already is in H265/AC3" - } - }, - "FFMPEG": { - "Description": "The node lets you run any FFMPEG command you like. Giving you full control over what it can do.\n\nFor more information refer to the FFMPEG documentation", - "Fields": { - "Extension": "Extension", - "Extension-Help": "The file extension to use on the newly created file", - "CommandLine": "Command Line", - "CommandLine-Help": "The command line to run with FFMPEG.\n'{WorkingFile}': the working file of the flow\n'{Output}': The output file that will be passed as the last parameter to FFMPEG including the extension defined above." - } + "Flow":{ + "Parts": { + "MusicFile": { + "Description": "An input music file that has had its MusicInformation read and can be processed" + }, + "ConvertToAAC": { + "Description": "Convert a music file to AAC", + "Fields": { + "Bitrate": "Bitrate", + "Bitrate-Help": "The bitrate for the new AAC file, the higher the bitrate the better the quality but larger the file. 192 Kbps is the recommended rate." + } + }, + "ConvertToFLAC": { + "Description": "Convert a music file to FLAC", + "Fields": { + "Bitrate": "Bitrate", + "Bitrate-Help": "The bitrate for the new FLAC file, the higher the bitrate the better the quality but larger the file. 128 Kbps is the recommended rate." + } + }, + "ConvertToMP3": { + "Description": "Convert a music file to MP3", + "Fields": { + "Bitrate": "Bitrate", + "Bitrate-Help": "The bitrate for the new MP3 file, the higher the bitrate the better the quality but larger the file. 192 Kbps is the recommended rate." + } + }, + "ConvertToOGG": { + "Description": "Convert a music file to OGG", + "Fields": { + "Bitrate": "Bitrate", + "Bitrate-Help": "The bitrate for the new OGG file, the higher the bitrate the better the quality but larger the file. 128 Kbps is the recommended rate." + } + }, + "ConvertToWAV": { + "Description": "Convert a music file to WAV", + "Fields": { + "Bitrate": "Bitrate", + "Bitrate-Help": "The bitrate for the new WAV file, the higher the bitrate the better the quality but larger the file. 128 Kbps is the recommended rate." } } } + } } \ No newline at end of file diff --git a/MusicNodes/Nodes/ConvertNode.cs b/MusicNodes/Nodes/ConvertNode.cs new file mode 100644 index 00000000..26af603f --- /dev/null +++ b/MusicNodes/Nodes/ConvertNode.cs @@ -0,0 +1,231 @@ +using FileFlows.Plugin; +using FileFlows.Plugin.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileFlows.MusicNodes +{ + public class ConvertToMP3 : ConvertNode + { + protected override string Extension => "mp3"; + public static List BitrateOptions => ConvertNode.BitrateOptions; + protected override List GetArguments() + { + return new List + { + "-c:a", + "mp3", + "-ab", + Bitrate + "k" + }; + } + } + public class ConvertToWAV : ConvertNode + { + protected override string Extension => "wav"; + public static List BitrateOptions => ConvertNode.BitrateOptions; + protected override List GetArguments() + { + return new List + { + "-c:a", + "pcm_s16le", + "-ab", + Bitrate + "k" + }; + } + } + + public class ConvertToAAC : ConvertNode + { + protected override string Extension => "aac"; + public static List BitrateOptions => ConvertNode.BitrateOptions; + + protected override bool SetId3Tags => true; + + protected override List GetArguments() + { + return new List + { + "-c:a", + "aac", + "-ab", + Bitrate + "k" + }; + } + } + public class ConvertToOGG: ConvertNode + { + protected override string Extension => "ogg"; + public static List BitrateOptions => ConvertNode.BitrateOptions; + protected override List GetArguments() + { + return new List + { + "-c:a", + "libvorbis", + "-ab", + Bitrate + "k" + }; + } + } + + //public class ConvertToFLAC : ConvertNode + //{ + // protected override string Extension => "flac"; + // public static List BitrateOptions => ConvertNode.BitrateOptions; + // protected override List GetArguments() + // { + // return new List + // { + // "-c:a", + // "flac", + // "-ab", + // Bitrate + "k" + // }; + // } + //} + + public abstract class ConvertNode:MusicNode + { + protected abstract string Extension { get; } + + protected virtual bool SetId3Tags => false; + + public override int Inputs => 1; + public override int Outputs => 1; + + protected virtual List GetArguments() + { + return new List + { + "-map_metadata", + "0:0", + "-ab", + Bitrate + "k" + }; + } + + public override FlowElementType Type => FlowElementType.Process; + + [Select(nameof(BitrateOptions), 1)] + public int Bitrate { get; set; } + + private static List _BitrateOptions; + public static List BitrateOptions + { + get + { + if (_BitrateOptions == null) + { + _BitrateOptions = new List + { + new ListOption { Label = "64 Kbps", Value = 64}, + new ListOption { Label = "96 Kbps", Value = 96}, + new ListOption { Label = "128 Kbps", Value = 128}, + new ListOption { Label = "160 Kbps", Value = 160}, + new ListOption { Label = "192 Kbps", Value = 192}, + new ListOption { Label = "224 Kbps", Value = 224}, + new ListOption { Label = "256 Kbps", Value = 256}, + new ListOption { Label = "288 Kbps", Value = 288}, + new ListOption { Label = "320 Kbps", Value = 320}, + }; + } + return _BitrateOptions; + } + } + + + public override int Execute(NodeParameters args) + { + string ffmpegExe = GetFFMpegExe(args); + if (string.IsNullOrEmpty(ffmpegExe)) + return -1; + + //MusicInfo musicInfo = GetMusicInfo(args); + //if (musicInfo == null) + // return -1; + + if (Bitrate < 64 || Bitrate > 320) + { + args.Logger?.ILog("Bitrate not set or invalid, setting to 192kbps"); + Bitrate = 192; + } + + string outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + Extension); + + var ffArgs = GetArguments(); + 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(2, "-i"); + ffArgs.Insert(3, args.WorkingFile); + ffArgs.Add(outputFile); + + args.Logger?.ILog("FFArgs: " + String.Join(", ", ffArgs)); + + var result = args.Execute(new ExecuteArgs + { + Command = ffmpegExe, + ArgumentList = ffArgs.ToArray() + }); + + if(result.ExitCode != 0) + { + args.Logger?.ELog("Invalid exit code detected: " + result.ExitCode); + return -1; + } + + //CopyMetaData(outputFile, args.FileName); + + args.SetWorkingFile(outputFile); + return 1; + } + + //private void CopyMetaData(string outputFile, string originalFile) + //{ + // Track original = new Track(originalFile); + // Track dest = new Track(outputFile); + + // dest.Album = original.Album; + // dest.AlbumArtist = original.AlbumArtist; + // dest.Artist = original.Artist; + // dest.Comment = original.Comment; + // dest.Composer= original.Composer; + // dest.Conductor = original.Conductor; + // dest.Copyright = original.Copyright; + // dest.Date = original.Date; + // dest.Description= original.Description; + // dest.DiscNumber= original.DiscNumber; + // dest.DiscTotal = original.DiscTotal; + // if (original.EmbeddedPictures?.Any() == true) + // { + // foreach (var pic in original.EmbeddedPictures) + // dest.EmbeddedPictures.Add(pic); + // } + // dest.Genre= original.Genre; + // dest.Lyrics= original.Lyrics; + // dest.OriginalAlbum= original.OriginalAlbum; + // dest.OriginalArtist = original.OriginalArtist; + // dest.Popularity= original.Popularity; + // dest.Publisher= original.Publisher; + // dest.PublishingDate= original.PublishingDate; + // dest.Title= original.Title; + // dest.TrackNumber= original.TrackNumber; + // dest.TrackTotal= original.TrackTotal; + // dest.Year= original.Year; + // foreach (var key in original.AdditionalFields.Keys) + // { + // if(dest.AdditionalFields.ContainsKey(key)) + // dest.AdditionalFields[key] = original.AdditionalFields[key]; + // else + // dest.AdditionalFields.Add(key, original.AdditionalFields[key]); + // } + + // dest.Save(); + //} + } +} diff --git a/MusicNodes/Nodes/ID3Tagger.cs b/MusicNodes/Nodes/ID3Tagger.cs new file mode 100644 index 00000000..31e18341 --- /dev/null +++ b/MusicNodes/Nodes/ID3Tagger.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileFlows.MusicNodes.Nodes +{ + internal class ID3Tagger + { + } +} diff --git a/MusicNodes/Nodes/MusicNode.cs b/MusicNodes/Nodes/MusicNode.cs new file mode 100644 index 00000000..61c48d4f --- /dev/null +++ b/MusicNodes/Nodes/MusicNode.cs @@ -0,0 +1,82 @@ +namespace FileFlows.MusicNodes +{ + using FileFlows.Plugin; + + public abstract class MusicNode : Node + { + public override string Icon => "fas fa-music"; + + protected string GetFFMpegExe(NodeParameters args) + { + string ffmpeg = args.GetToolPath("FFMpeg"); + if (string.IsNullOrEmpty(ffmpeg)) + { + args.Logger.ELog("FFMpeg tool not found."); + return ""; + } + var fileInfo = new FileInfo(ffmpeg); + if (fileInfo.Exists == false) + { + args.Logger.ELog("FFMpeg tool configured by ffmpeg file does not exist."); + return ""; + } + return fileInfo.FullName; + } + protected string GetFFMpegPath(NodeParameters args) + { + string ffmpeg = args.GetToolPath("FFMpeg"); + if (string.IsNullOrEmpty(ffmpeg)) + { + args.Logger.ELog("FFMpeg tool not found."); + return ""; + } + var fileInfo = new FileInfo(ffmpeg); + if (fileInfo.Exists == false) + { + args.Logger.ELog("FFMpeg tool configured by ffmpeg file does not exist."); + return ""; + } + return fileInfo.DirectoryName; + } + + private const string MUSIC_INFO = "MusicInfo"; + protected void SetMusicInfo(NodeParameters args, MusicInfo musicInfo, Dictionary variables) + { + if (args.Parameters.ContainsKey(MUSIC_INFO)) + args.Parameters[MUSIC_INFO] = musicInfo; + else + args.Parameters.Add(MUSIC_INFO, musicInfo); + + variables.AddOrUpdate("mi.Album", musicInfo.Album); + variables.AddOrUpdate("mi.BitRate", musicInfo.BitRate); + variables.AddOrUpdate("mi.Channels", musicInfo.Channels); + variables.AddOrUpdate("mi.Codec", musicInfo.Codec); + variables.AddOrUpdate("mi.Date", musicInfo.Date); + variables.AddOrUpdate("mi.Duration", musicInfo.Duration); + variables.AddOrUpdate("mi.Encoder", musicInfo.Encoder); + variables.AddOrUpdate("mi.Frequency", musicInfo.Frequency); + variables.AddOrUpdate("mi.Genres", musicInfo.Genres); + variables.AddOrUpdate("mi.Language", musicInfo.Language); + variables.AddOrUpdate("mi.Title", musicInfo.Title); + variables.AddOrUpdate("mi.Track", musicInfo.Track); + + args.UpdateVariables(variables); + } + + protected MusicInfo GetMusicInfo(NodeParameters args) + { + if (args.Parameters.ContainsKey(MUSIC_INFO) == false) + { + args.Logger.WLog("No codec information loaded, use a 'Music File' node first"); + return null; + } + var result = args.Parameters[MUSIC_INFO] as MusicInfo; + if (result == null) + { + args.Logger.WLog("MusicInfo not found for file"); + return null; + } + return result; + } + } +} \ No newline at end of file diff --git a/MusicNodes/Plugin.cs b/MusicNodes/Plugin.cs new file mode 100644 index 00000000..5297c5db --- /dev/null +++ b/MusicNodes/Plugin.cs @@ -0,0 +1,15 @@ +namespace FileFlows.MusicNodes +{ + using System.ComponentModel.DataAnnotations; + using FileFlows.Plugin.Attributes; + + public class Plugin : FileFlows.Plugin.IPlugin + { + public string Name => "Music Nodes"; + public string MinimumVersion => string.Empty; + + public void Init() + { + } + } +} \ No newline at end of file diff --git a/MusicNodes/Tests/ConvertTests.cs b/MusicNodes/Tests/ConvertTests.cs new file mode 100644 index 00000000..6ccb2540 --- /dev/null +++ b/MusicNodes/Tests/ConvertTests.cs @@ -0,0 +1,99 @@ +#if(DEBUG) + + +namespace FileFlows.MusicNodes.Tests +{ + using FileFlows.MusicNodes; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + [TestClass] + public class ConvertTests + { + [TestMethod] + public void Convert_FlacToAac() + { + + const string file = @"D:\music\unprocessed\01-billy_joel-you_may_be_right.flac"; + + ConvertToAAC node = new (); + var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty); + args.GetToolPath = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe"; + args.TempPath = @"D:\music\temp"; + new MusicFile().Execute(args); // need to read the music info and set it + int output = node.Execute(args); + + Assert.AreEqual(1, output); + } + + [TestMethod] + public void Convert_FlacToMp3() + { + + const string file = @"D:\music\unprocessed\01-billy_joel-you_may_be_right.flac"; + + ConvertToMP3 node = new(); + var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty); + args.GetToolPath = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe"; + args.TempPath = @"D:\music\temp"; + new MusicFile().Execute(args); // need to read the music info and set it + int output = node.Execute(args); + + Assert.AreEqual(1, output); + } + [TestMethod] + public void Convert_Mp3ToWAV() + { + + const string file = @"D:\music\unprocessed\04-billy_joel-scenes_from_an_italian_restaurant-b2125758.mp3"; + + ConvertToWAV node = new(); + var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty); + args.GetToolPath = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe"; + args.TempPath = @"D:\music\temp"; + new MusicFile().Execute(args); // need to read the music info and set it + int output = node.Execute(args); + + Assert.AreEqual(1, output); + } + + [TestMethod] + public void Convert_Mp3ToOgg() + { + + const string file = @"D:\music\unprocessed\04-billy_joel-scenes_from_an_italian_restaurant-b2125758.mp3"; + + ConvertToOGG node = new(); + var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty); + args.GetToolPath = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe"; + args.TempPath = @"D:\music\temp"; + new MusicFile().Execute(args); // need to read the music info and set it + int output = node.Execute(args); + + Assert.AreEqual(1, output); + } + + + [TestMethod] + public void Convert_AacToMp3() + { + + const string file = @"D:\music\temp\37f315a0-4afc-4a72-a0b4-eb7eb681b9b3.aac"; + + ConvertToMP3 node = new(); + var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty); + args.GetToolPath = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe"; + args.TempPath = @"D:\music\temp"; + new MusicFile().Execute(args); // need to read the music info and set it + int output = node.Execute(args); + + Assert.AreEqual(1, output); + } + } +} + +#endif \ No newline at end of file diff --git a/MusicNodes/Tests/TestLogger.cs b/MusicNodes/Tests/TestLogger.cs new file mode 100644 index 00000000..0cc435aa --- /dev/null +++ b/MusicNodes/Tests/TestLogger.cs @@ -0,0 +1,47 @@ +#if(DEBUG) + +namespace FileFlows.MusicNodes.Tests +{ + using FileFlows.Plugin; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + internal class TestLogger : ILogger + { + private List Messages = new List(); + + public void DLog(params object[] args) => Log("DBUG", args); + + public void ELog(params object[] args) => Log("ERRR", args); + + public void ILog(params object[] args) => Log("INFO", args); + + public void WLog(params object[] args) => Log("WARN", args); + + private void Log(string type, object[] args) + { + if (args == null || args.Length == 0) + return; + string message = type + " -> " + + string.Join(", ", args.Select(x => + x == null ? "null" : + x.GetType().IsPrimitive || x is string ? x.ToString() : + System.Text.Json.JsonSerializer.Serialize(x))); + Messages.Add(message); + } + + public bool Contains(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + string log = string.Join(Environment.NewLine, Messages); + return log.Contains(message); + } + } +} + +#endif \ No newline at end of file diff --git a/build/utils/spellcheck/ignoredwords.txt b/build/utils/spellcheck/ignoredwords.txt index 08459825..1cea7b76 100644 --- a/build/utils/spellcheck/ignoredwords.txt +++ b/build/utils/spellcheck/ignoredwords.txt @@ -46,4 +46,12 @@ Remuxes remuxed srt ssa -rescaled \ No newline at end of file +rescaled +Bitrate +bitrate +MusicInformation +Kbps +AAC +WAV +OGG +FLAC \ No newline at end of file