mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-20 01:29:35 -06:00
FF-256 - added option to normalize converted audio
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -75,7 +75,7 @@ public class Apprise: Node
|
||||
object data = new
|
||||
{
|
||||
body = message,
|
||||
tag= Tag?.Any() != true ? "all" : String.Join(";", this.Tag),
|
||||
tag = Tag?.Any() != true ? "all" : String.Join(";", this.Tag),
|
||||
type = this.MessageType?.EmptyAsNull() ?? "info"
|
||||
};
|
||||
|
||||
|
||||
21
AudioNodes/AudioInfo.cs
Normal file
21
AudioNodes/AudioInfo.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace FileFlows.AudioNodes
|
||||
{
|
||||
public class AudioInfo
|
||||
{
|
||||
public string Language { get; set; }
|
||||
public int Track { get; set; }
|
||||
public int Disc { get; set; }
|
||||
public int TotalDiscs { get; set; }
|
||||
public string Artist { 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; }
|
||||
}
|
||||
}
|
||||
328
AudioNodes/AudioInfoHelper.cs
Normal file
328
AudioNodes/AudioInfoHelper.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
namespace FileFlows.AudioNodes
|
||||
{
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using FileFlows.Plugin;
|
||||
|
||||
public class AudioInfoHelper
|
||||
{
|
||||
private string ffMpegExe;
|
||||
private ILogger Logger;
|
||||
|
||||
public AudioInfoHelper(string ffMpegExe, ILogger logger)
|
||||
{
|
||||
this.ffMpegExe = ffMpegExe;
|
||||
this.Logger = logger;
|
||||
}
|
||||
|
||||
public static string GetFFMpegPath(NodeParameters args) => args.GetToolPath("FFMpeg");
|
||||
public AudioInfo Read(string filename)
|
||||
{
|
||||
var mi = new AudioInfo();
|
||||
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;
|
||||
}
|
||||
|
||||
mi = ReadMetaData(filename);
|
||||
|
||||
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("Audio Information:" + Environment.NewLine + output);
|
||||
|
||||
if(output.IndexOf("Input #0") < 0)
|
||||
{
|
||||
Logger.ELog("Failed to read audio information for file");
|
||||
return mi;
|
||||
}
|
||||
|
||||
if (output.ToLower().Contains("mp3"))
|
||||
mi.Codec = "mp3";
|
||||
else if (output.ToLower().Contains("ogg"))
|
||||
mi.Codec = "ogg";
|
||||
else if (output.ToLower().Contains("flac"))
|
||||
mi.Codec = "flac";
|
||||
else if (output.ToLower().Contains("wav"))
|
||||
mi.Codec = "wav";
|
||||
else if (filename.ToLower().EndsWith(".mp3"))
|
||||
mi.Codec = "mp3";
|
||||
else if (filename.ToLower().EndsWith(".ogg"))
|
||||
mi.Codec = "ogg";
|
||||
else if (filename.ToLower().EndsWith(".flac"))
|
||||
mi.Codec = "flac";
|
||||
else if (filename.ToLower().EndsWith(".wav"))
|
||||
mi.Codec = "wav";
|
||||
|
||||
foreach (string line in output.Split('\n'))
|
||||
{
|
||||
int colonIndex = line.IndexOf(":");
|
||||
if(colonIndex < 1)
|
||||
continue;
|
||||
|
||||
string lowLine = line.ToLower().Trim();
|
||||
|
||||
if (lowLine.StartsWith("language"))
|
||||
mi.Language = line.Substring(colonIndex + 1).Trim();
|
||||
else if (lowLine.StartsWith("track") && lowLine.Contains("total") == false)
|
||||
{
|
||||
if (mi.Track < 1)
|
||||
{
|
||||
var trackMatch = Regex.Match(line.Substring(colonIndex + 1).Trim(), @"^[\d]+");
|
||||
if (trackMatch.Success && int.TryParse(trackMatch.Value, out int value))
|
||||
mi.Track = value;
|
||||
}
|
||||
}
|
||||
else if (lowLine.StartsWith("artist") || lowLine.StartsWith("album_artist"))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mi.Artist))
|
||||
mi.Artist = line.Substring(colonIndex + 1).Trim();
|
||||
}
|
||||
else if (lowLine.StartsWith("title") && lowLine.Contains(".jpg") == false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mi.Title))
|
||||
mi.Title = line.Substring(colonIndex + 1).Trim();
|
||||
}
|
||||
else if (lowLine.StartsWith("album"))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mi.Album))
|
||||
mi.Album = line.Substring(colonIndex + 1).Trim();
|
||||
}
|
||||
else if (lowLine.StartsWith("disc"))
|
||||
{
|
||||
if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value))
|
||||
mi.Disc = value;
|
||||
}
|
||||
else if (lowLine.StartsWith("disctotal") || lowLine.StartsWith("totaldiscs"))
|
||||
{
|
||||
if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value))
|
||||
mi.TotalDiscs = value;
|
||||
}
|
||||
else if (lowLine.StartsWith("date") || lowLine.StartsWith("retail date") || lowLine.StartsWith("retaildate") || lowLine.StartsWith("originaldate") || lowLine.StartsWith("original date"))
|
||||
{
|
||||
if (int.TryParse(line.Substring(colonIndex + 1).Trim(), out int value))
|
||||
{
|
||||
if(mi.Date < new DateTime(1900, 1, 1))
|
||||
mi.Date = new DateTime(value, 1, 1);
|
||||
}
|
||||
else if(DateTime.TryParse(line.Substring(colonIndex + 1).Trim(), out DateTime dtValue) && dtValue.Year > 1900)
|
||||
mi.Date = dtValue;
|
||||
}
|
||||
else if (lowLine.StartsWith("genre"))
|
||||
{
|
||||
if(mi.Genres?.Any() != true)
|
||||
mi.Genres = line.Substring(colonIndex + 1).Trim().Split(' ');
|
||||
}
|
||||
else if (lowLine.StartsWith("encoder"))
|
||||
mi.Encoder = line.Substring(colonIndex + 1).Trim();
|
||||
else if (lowLine.StartsWith("duration"))
|
||||
{
|
||||
if (mi.Duration < 1)
|
||||
{
|
||||
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.ToLower().IndexOf("bitrate:") > 0)
|
||||
{
|
||||
string br = line.Substring(line.ToLower().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());
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(mi.Artist) || string.IsNullOrEmpty(mi.Album) || mi.Track < 1 || string.IsNullOrEmpty(mi.Title))
|
||||
{
|
||||
// try parse the file
|
||||
ParseFileNameInfo(filename, mi);
|
||||
}
|
||||
|
||||
return mi;
|
||||
}
|
||||
|
||||
public AudioInfo ReadMetaData(string file)
|
||||
{
|
||||
using var tfile = TagLib.File.Create(file);
|
||||
AudioInfo info = new AudioInfo();
|
||||
try
|
||||
{
|
||||
info.Title = tfile.Tag.Title;
|
||||
info.Duration = (long)tfile.Properties.Duration.TotalSeconds;
|
||||
info.TotalDiscs = Convert.ToInt32(tfile.Tag.DiscCount);
|
||||
if (info.TotalDiscs < 1)
|
||||
info.TotalDiscs = 1;
|
||||
info.Disc = Convert.ToInt32(tfile.Tag.Disc);
|
||||
if (info.Disc < 1)
|
||||
info.Disc = 1;
|
||||
info.Artist = String.Join(", ", tfile.Tag.AlbumArtists);
|
||||
info.Album = tfile.Tag.Album;
|
||||
info.Track = Convert.ToInt32(tfile.Tag.Track);
|
||||
if(tfile.Tag.Year > 1900)
|
||||
{
|
||||
info.Date = new DateTime(Convert.ToInt32(tfile.Tag.Year), 1, 1);
|
||||
}
|
||||
info.Genres = tfile.Tag.Genres.SelectMany(x => x.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim())).ToArray();
|
||||
}
|
||||
catch (Exception) { }
|
||||
tfile.Dispose();
|
||||
return info;
|
||||
}
|
||||
|
||||
public void ParseFileNameInfo(string filename, AudioInfo mi)
|
||||
{
|
||||
using var tfile = TagLib.File.Create(filename);
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(filename);
|
||||
|
||||
bool dirty = false;
|
||||
|
||||
if (mi.Disc < 1)
|
||||
{
|
||||
var cdMatch = Regex.Match(filename.Replace("\\", "/"), @"(?<=(/(cd|disc)))[\s]*([\d]+)(?!=(/))", RegexOptions.IgnoreCase);
|
||||
if (cdMatch.Success && int.TryParse(cdMatch.Value.Trim(), out int disc))
|
||||
{
|
||||
dirty = true;
|
||||
mi.Disc = disc;
|
||||
tfile.Tag.Disc = Convert.ToUInt32(disc);
|
||||
}
|
||||
}
|
||||
|
||||
if (mi.Track < 1)
|
||||
{
|
||||
var trackMatch = Regex.Match(fileInfo.Name, @"[\-_\s\.]+([\d]{1,2})[\-_\s\.]+");
|
||||
if (trackMatch.Success)
|
||||
{
|
||||
string trackString = trackMatch.Value;
|
||||
if (int.TryParse(Regex.Match(trackString, @"[\d]+").Value, out int track))
|
||||
{
|
||||
mi.Track = track;
|
||||
tfile.Tag.Track = Convert.ToUInt32(track);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string album = fileInfo.Directory.Name;
|
||||
var yearMatch = Regex.Match(album, @"(?<=(\())[\d]{4}(?!=\))");
|
||||
if (yearMatch.Success)
|
||||
{
|
||||
album = album.Replace("(" + yearMatch.Value + ")", "").Trim();
|
||||
|
||||
if (mi.Date < new DateTime(1900, 1, 1))
|
||||
{
|
||||
if (int.TryParse(yearMatch.Value, out int year))
|
||||
{
|
||||
mi.Date = new DateTime(year, 1, 1);
|
||||
tfile.Tag.Year = Convert.ToUInt32(year);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(mi.Album))
|
||||
{
|
||||
mi.Album = album;
|
||||
if (string.IsNullOrEmpty(album) == false)
|
||||
{
|
||||
tfile.Tag.Album = mi.Album;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(mi.Artist))
|
||||
{
|
||||
mi.Artist = fileInfo.Directory.Parent.Name;
|
||||
if (string.IsNullOrEmpty(mi.Artist) == false)
|
||||
{
|
||||
tfile.Tag.AlbumArtists = new[] { mi.Artist };
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// the title
|
||||
if (string.IsNullOrEmpty(mi.Title))
|
||||
{
|
||||
int titleIndex = fileInfo.Name.LastIndexOf(" - ");
|
||||
if (titleIndex > 0)
|
||||
{
|
||||
mi.Title = fileInfo.Name.Substring(titleIndex + 3);
|
||||
if (string.IsNullOrEmpty(fileInfo.Extension) == false)
|
||||
{
|
||||
mi.Title = mi.Title.Replace(fileInfo.Extension, "");
|
||||
tfile.Tag.Title = mi.Title;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(dirty)
|
||||
tfile.Save();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.WLog("Failed parsing Audio info from filename: " + ex.Message + Environment.NewLine + ex.StackTrace);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tfile.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
53
AudioNodes/AudioNodes.csproj
Normal file
53
AudioNodes/AudioNodes.csproj
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-16"?><Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
<Product>Audio Nodes</Product>
|
||||
<PackageProjectUrl>https://fileflows.com/</PackageProjectUrl>
|
||||
<Description>Nodes for processing Audio files. This plugin contains nodes to convert Audio files to different formats. Node to parse the Audio information from a file.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;CS8618;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625;CS8765;CS8767;</NoWarn>
|
||||
<WarningLevel>0</WarningLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<NoWarn>1701;1702;CS8618;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625;CS8765;CS8767;</NoWarn>
|
||||
<WarningLevel>0</WarningLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="AudioNodes.en.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Plugin">
|
||||
<HintPath>..\FileFlows.Plugin.dll</HintPath>
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
85
AudioNodes/AudioNodes.en.json
Normal file
85
AudioNodes/AudioNodes.en.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"Flow":{
|
||||
"Parts": {
|
||||
"AudioFile": {
|
||||
"Description": "An input Audio file that has had its Audio Information read and can be processed",
|
||||
"Outputs": {
|
||||
"1": "Audio file from library"
|
||||
}
|
||||
},
|
||||
"AudioFileNormalization": {
|
||||
"Description": "Normalizes an audio file using two passes of FFMPEGs loudnorm filter",
|
||||
"Outputs": {
|
||||
"1": "Audio file normalized and saved to temporary file"
|
||||
}
|
||||
},
|
||||
"ConvertAudio": {
|
||||
"Description": "Convert a Audio file to the specified audio codec",
|
||||
"Outputs": {
|
||||
"1": "Audio converted and saved to temporary file",
|
||||
"2": "Audio already in codec, no conversion done"
|
||||
},
|
||||
"Fields": {
|
||||
"Bitrate": "Bitrate",
|
||||
"Bitrate-Help": "The bitrate for the new file, the higher the bitrate the better the quality but larger the file.",
|
||||
"Codec": "Codec",
|
||||
"Codec-Help": "The audio codec to convert the file into.",
|
||||
"Normalize": "Normalize",
|
||||
"Normalize-Help": "If the audio should be normalized with two pass audio normalization",
|
||||
"SkipIfCodecMatches": "Skip If Codec Matches",
|
||||
"SkipIfCodecMatches-Help": "If the existing audio codec matches, this file will not be processed regardless of the bitrate. Otherwise if off, the bitrate must be less than or equal to for it to skip."
|
||||
}
|
||||
},
|
||||
"ConvertToAAC": {
|
||||
"Description": "Convert a Audio file to AAC",
|
||||
"Outputs": {
|
||||
"1": "Audio converted and saved to temporary file"
|
||||
},
|
||||
"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 Audio file to FLAC",
|
||||
"Outputs": {
|
||||
"1": "Audio converted and saved to temporary file"
|
||||
},
|
||||
"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 Audio file to MP3",
|
||||
"Outputs": {
|
||||
"1": "Audio converted and saved to temporary file"
|
||||
},
|
||||
"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 Audio file to OGG",
|
||||
"Outputs": {
|
||||
"1": "Audio converted and saved to temporary file"
|
||||
},
|
||||
"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 Audio file to WAV",
|
||||
"Outputs": {
|
||||
"1": "Audio converted and saved to temporary file"
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
AudioNodes/ExtensionMethods.cs
Normal file
17
AudioNodes/ExtensionMethods.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace FileFlows.AudioNodes
|
||||
{
|
||||
internal static class ExtensionMethods
|
||||
{
|
||||
public static void AddOrUpdate(this Dictionary<string, object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
AudioNodes/InputNodes/AudioFile.cs
Normal file
65
AudioNodes/InputNodes/AudioFile.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
namespace FileFlows.AudioNodes
|
||||
{
|
||||
using System.ComponentModel;
|
||||
using FileFlows.Plugin;
|
||||
using FileFlows.Plugin.Attributes;
|
||||
|
||||
public class AudioFile : AudioNode
|
||||
{
|
||||
public override int Outputs => 1;
|
||||
public override FlowElementType Type => FlowElementType.Input;
|
||||
|
||||
private Dictionary<string, object> _Variables;
|
||||
public override Dictionary<string, object> Variables => _Variables;
|
||||
public AudioFile()
|
||||
{
|
||||
_Variables = new Dictionary<string, object>()
|
||||
{
|
||||
{ "mi.Album", "Album" },
|
||||
{ "mi.Artist", "Artist" },
|
||||
{ "mi.ArtistThe", "Artist, The" },
|
||||
{ "mi.BitRate", 845 },
|
||||
{ "mi.Channels", 2 },
|
||||
{ "mi.Codec", "flac" },
|
||||
{ "mi.Date", new DateTime(2020, 05, 23) },
|
||||
{ "mi.Year", 2020 },
|
||||
{ "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 },
|
||||
{ "mi.Disc", 2 },
|
||||
{ "mi.TotalDiscs", 2 }
|
||||
};
|
||||
}
|
||||
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
string ffmpegExe = GetFFMpegExe(args);
|
||||
if (string.IsNullOrEmpty(ffmpegExe))
|
||||
return -1;
|
||||
|
||||
try
|
||||
{
|
||||
if (ReadAudioFileInfo(args, ffmpegExe, args.WorkingFile))
|
||||
return 1;
|
||||
|
||||
var AudioInfo = GetAudioInfo(args);
|
||||
|
||||
if (string.IsNullOrEmpty(AudioInfo.Codec) == false)
|
||||
args.RecordStatistic("CODEC", AudioInfo.Codec);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
args.Logger.ELog("Failed processing AudioFile: " + ex.Message);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
115
AudioNodes/Nodes/AudioFileNormalization.cs
Normal file
115
AudioNodes/Nodes/AudioFileNormalization.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using FileFlows.Plugin;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FileFlows.AudioNodes;
|
||||
|
||||
public class AudioFileNormalization : AudioNode
|
||||
{
|
||||
public override int Inputs => 1;
|
||||
public override int Outputs => 1;
|
||||
public override FlowElementType Type => FlowElementType.Process;
|
||||
|
||||
public override string Icon => "fas fa-volume-up";
|
||||
|
||||
|
||||
const string LOUDNORM_TARGET = "I=-24:LRA=7:TP=-2.0";
|
||||
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
try
|
||||
{
|
||||
string ffmpegExe = GetFFMpegExe(args);
|
||||
if (string.IsNullOrEmpty(ffmpegExe))
|
||||
return -1;
|
||||
|
||||
AudioInfo AudioInfo = GetAudioInfo(args);
|
||||
if (AudioInfo == null)
|
||||
return -1;
|
||||
|
||||
List<string> ffArgs = new List<string>();
|
||||
|
||||
|
||||
long sampleRate = AudioInfo.Frequency > 0 ? AudioInfo.Frequency : 48_000;
|
||||
|
||||
string twoPass = DoTwoPass(args, ffmpegExe);
|
||||
ffArgs.AddRange(new[] { "-i", args.WorkingFile, "-c:a", AudioInfo.Codec, "-ar", sampleRate.ToString(), "-af", twoPass });
|
||||
|
||||
string extension = new FileInfo(args.WorkingFile).Extension;
|
||||
if (extension.StartsWith("."))
|
||||
extension = extension.Substring(1);
|
||||
|
||||
string outputFile = Path.Combine(args.TempPath, Guid.NewGuid().ToString() + "." + extension);
|
||||
ffArgs.Add(outputFile);
|
||||
|
||||
var result = args.Execute(new ExecuteArgs
|
||||
{
|
||||
Command = ffmpegExe,
|
||||
ArgumentList = ffArgs.ToArray()
|
||||
});
|
||||
|
||||
return result.ExitCode == 0 ? 1 : -1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
args.Logger?.ELog("Failed processing AudioFile: " + ex.Message);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
public static string DoTwoPass(NodeParameters args, string ffmpegExe)
|
||||
{
|
||||
//-af loudnorm=I=-24:LRA=7:TP=-2.0"
|
||||
var result = args.Execute(new ExecuteArgs
|
||||
{
|
||||
Command = ffmpegExe,
|
||||
ArgumentList = new[]
|
||||
{
|
||||
"-hide_banner",
|
||||
"-i", args.WorkingFile,
|
||||
"-af", "loudnorm=" + LOUDNORM_TARGET + ":print_format=json",
|
||||
"-f", "null",
|
||||
"-"
|
||||
}
|
||||
});
|
||||
if(result.ExitCode != 0)
|
||||
throw new Exception("Failed to prcoess audio track");
|
||||
|
||||
string output = result.StandardOutput;
|
||||
|
||||
int index = output.LastIndexOf("{");
|
||||
if (index == -1)
|
||||
throw new Exception("Failed to detected json in output");
|
||||
string json = output.Substring(index);
|
||||
json = json.Substring(0, json.IndexOf("}") + 1);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
throw new Exception("Failed to parse TwoPass json");
|
||||
LoudNormStats stats = JsonSerializer.Deserialize<LoudNormStats>(json);
|
||||
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 ar;
|
||||
}
|
||||
|
||||
private class LoudNormStats
|
||||
{
|
||||
/*
|
||||
{
|
||||
"input_i" : "-7.47",
|
||||
"input_tp" : "12.33",
|
||||
"input_lra" : "6.70",
|
||||
"input_thresh" : "-18.13",
|
||||
"output_i" : "-24.25",
|
||||
"output_tp" : "-3.60",
|
||||
"output_lra" : "5.90",
|
||||
"output_thresh" : "-34.74",
|
||||
"normalization_type" : "dynamic",
|
||||
"target_offset" : "0.25"
|
||||
}
|
||||
*/
|
||||
public string input_i { get; set; }
|
||||
public string input_tp { get; set; }
|
||||
public string input_lra { get; set; }
|
||||
public string input_thresh { get; set; }
|
||||
public string target_offset { get; set; }
|
||||
}
|
||||
}
|
||||
110
AudioNodes/Nodes/AudioNode.cs
Normal file
110
AudioNodes/Nodes/AudioNode.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
namespace FileFlows.AudioNodes
|
||||
{
|
||||
using FileFlows.Plugin;
|
||||
|
||||
public abstract class AudioNode : 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 Audio_INFO = "AudioInfo";
|
||||
protected void SetAudioInfo(NodeParameters args, AudioInfo AudioInfo, Dictionary<string, object> variables)
|
||||
{
|
||||
if (args.Parameters.ContainsKey(Audio_INFO))
|
||||
args.Parameters[Audio_INFO] = AudioInfo;
|
||||
else
|
||||
args.Parameters.Add(Audio_INFO, AudioInfo);
|
||||
|
||||
if(AudioInfo.Artist.EndsWith(", The"))
|
||||
variables.AddOrUpdate("audio.Artist", "The " + AudioInfo.Artist.Substring(0, AudioInfo.Artist.Length - ", The".Length).Trim());
|
||||
else
|
||||
variables.AddOrUpdate("audio.Artist", AudioInfo.Artist);
|
||||
|
||||
if(AudioInfo.Artist?.StartsWith("The ") == true)
|
||||
variables.AddOrUpdate("audio.ArtistThe", AudioInfo.Artist.Substring(4).Trim() + ", The");
|
||||
else
|
||||
variables.AddOrUpdate("audio.ArtistThe", AudioInfo.Artist);
|
||||
|
||||
variables.AddOrUpdate("audio.Album", AudioInfo.Album);
|
||||
variables.AddOrUpdate("audio.BitRate", AudioInfo.BitRate);
|
||||
variables.AddOrUpdate("audio.Channels", AudioInfo.Channels);
|
||||
variables.AddOrUpdate("audio.Codec", AudioInfo.Codec);
|
||||
variables.AddOrUpdate("audio.Date", AudioInfo.Date);
|
||||
variables.AddOrUpdate("audio.Year", AudioInfo.Date.Year);
|
||||
variables.AddOrUpdate("audio.Duration", AudioInfo.Duration);
|
||||
variables.AddOrUpdate("audio.Encoder", AudioInfo.Encoder);
|
||||
variables.AddOrUpdate("audio.Frequency", AudioInfo.Frequency);
|
||||
variables.AddOrUpdate("audio.Genres", AudioInfo.Genres);
|
||||
variables.AddOrUpdate("audio.Language", AudioInfo.Language);
|
||||
variables.AddOrUpdate("audio.Title", AudioInfo.Title);
|
||||
variables.AddOrUpdate("audio.Track", AudioInfo.Track);
|
||||
variables.AddOrUpdate("audio.Disc", AudioInfo.Disc < 1 ? 1 : AudioInfo.Disc);
|
||||
variables.AddOrUpdate("audio.TotalDiscs", AudioInfo.TotalDiscs < 1 ? 1 : AudioInfo.TotalDiscs);
|
||||
|
||||
args.UpdateVariables(variables);
|
||||
}
|
||||
|
||||
protected AudioInfo GetAudioInfo(NodeParameters args)
|
||||
{
|
||||
if (args.Parameters.ContainsKey(Audio_INFO) == false)
|
||||
{
|
||||
args.Logger.WLog("No codec information loaded, use a 'Audio File' node first");
|
||||
return null;
|
||||
}
|
||||
var result = args.Parameters[Audio_INFO] as AudioInfo;
|
||||
if (result == null)
|
||||
{
|
||||
args.Logger.WLog("AudioInfo not found for file");
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected bool ReadAudioFileInfo(NodeParameters args, string ffmpegExe, string filename)
|
||||
{
|
||||
|
||||
var AudioInfo = new AudioInfoHelper(ffmpegExe, args.Logger).Read(filename);
|
||||
if (AudioInfo.Duration == 0)
|
||||
{
|
||||
args.Logger?.ILog("Failed to load Audio information.");
|
||||
return false;
|
||||
}
|
||||
|
||||
SetAudioInfo(args, AudioInfo, Variables);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
332
AudioNodes/Nodes/ConvertNode.cs
Normal file
332
AudioNodes/Nodes/ConvertNode.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
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.AudioNodes
|
||||
{
|
||||
public class ConvertToMP3 : ConvertNode
|
||||
{
|
||||
protected override string Extension => "mp3";
|
||||
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
|
||||
protected override List<string> GetArguments()
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
"-c:a",
|
||||
"mp3",
|
||||
"-ab",
|
||||
Bitrate + "k"
|
||||
};
|
||||
}
|
||||
}
|
||||
public class ConvertToWAV : ConvertNode
|
||||
{
|
||||
protected override string Extension => "wav";
|
||||
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
|
||||
protected override List<string> GetArguments()
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
"-c:a",
|
||||
"pcm_s16le",
|
||||
"-ab",
|
||||
Bitrate + "k"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class ConvertToAAC : ConvertNode
|
||||
{
|
||||
protected override string Extension => "aac";
|
||||
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
|
||||
|
||||
protected override bool SetId3Tags => true;
|
||||
|
||||
protected override List<string> GetArguments()
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-ab",
|
||||
Bitrate + "k"
|
||||
};
|
||||
}
|
||||
}
|
||||
public class ConvertToOGG: ConvertNode
|
||||
{
|
||||
protected override string Extension => "ogg";
|
||||
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
|
||||
protected override List<string> GetArguments()
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-ab",
|
||||
Bitrate + "k"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//public class ConvertToFLAC : ConvertNode
|
||||
//{
|
||||
// protected override string Extension => "flac";
|
||||
// public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
|
||||
// protected override List<string> GetArguments()
|
||||
// {
|
||||
// return new List<string>
|
||||
// {
|
||||
// "-c:a",
|
||||
// "flac",
|
||||
// "-ab",
|
||||
// Bitrate + "k"
|
||||
// };
|
||||
// }
|
||||
//}
|
||||
|
||||
public class ConvertAudio : ConvertNode
|
||||
{
|
||||
protected override string Extension => Codec;
|
||||
|
||||
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
|
||||
|
||||
[Select(nameof(CodecOptions), 0)]
|
||||
public string Codec { get; set; }
|
||||
|
||||
[Boolean(4)]
|
||||
[ConditionEquals(nameof(Normalize), true, inverse: true)]
|
||||
public bool SkipIfCodecMatches { get; set; }
|
||||
|
||||
public override int Outputs => 2;
|
||||
|
||||
private static List<ListOption> _CodecOptions;
|
||||
public static List<ListOption> CodecOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_CodecOptions == null)
|
||||
{
|
||||
_CodecOptions = new List<ListOption>
|
||||
{
|
||||
new ListOption { Label = "AAC", Value = "aac"},
|
||||
new ListOption { Label = "MP3", Value = "MP3"},
|
||||
new ListOption { Label = "OGG", Value = "ogg"},
|
||||
new ListOption { Label = "WAV", Value = "wav"},
|
||||
};
|
||||
}
|
||||
return _CodecOptions;
|
||||
}
|
||||
}
|
||||
|
||||
protected override List<string> GetArguments()
|
||||
{
|
||||
string codec = Codec switch
|
||||
{
|
||||
"ogg" => "libvorbis",
|
||||
"wav" => "pcm_s16le",
|
||||
_ => Codec.ToLower()
|
||||
};
|
||||
|
||||
return new List<string>
|
||||
{
|
||||
"-c:a",
|
||||
codec,
|
||||
"-ab",
|
||||
Bitrate + "k"
|
||||
};
|
||||
}
|
||||
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
AudioInfo AudioInfo = GetAudioInfo(args);
|
||||
if (AudioInfo == null)
|
||||
return -1;
|
||||
|
||||
string ffmpegExe = GetFFMpegExe(args);
|
||||
if (string.IsNullOrEmpty(ffmpegExe))
|
||||
return -1;
|
||||
|
||||
if(Normalize == false && AudioInfo.Codec?.ToLower() == Codec?.ToLower())
|
||||
{
|
||||
if (SkipIfCodecMatches)
|
||||
{
|
||||
args.Logger?.ILog($"Audio file already '{Codec}' at bitrate '{AudioInfo.BitRate}', and set to skip if codec matches");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if(AudioInfo.BitRate <= Bitrate)
|
||||
{
|
||||
args.Logger?.ILog($"Audio file already '{Codec}' at bitrate '{AudioInfo.BitRate}'");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
return base.Execute(args);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ConvertNode:AudioNode
|
||||
{
|
||||
protected abstract string Extension { get; }
|
||||
|
||||
protected virtual bool SetId3Tags => false;
|
||||
|
||||
public override int Inputs => 1;
|
||||
public override int Outputs => 1;
|
||||
|
||||
protected virtual List<string> GetArguments()
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
"-map_metadata",
|
||||
"0:0",
|
||||
"-ab",
|
||||
Bitrate + "k"
|
||||
};
|
||||
}
|
||||
|
||||
public override FlowElementType Type => FlowElementType.Process;
|
||||
|
||||
[Select(nameof(BitrateOptions), 1)]
|
||||
public int Bitrate { get; set; }
|
||||
|
||||
[Boolean(3)]
|
||||
public bool Normalize { get; set; }
|
||||
|
||||
private static List<ListOption> _BitrateOptions;
|
||||
public static List<ListOption> BitrateOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_BitrateOptions == null)
|
||||
{
|
||||
_BitrateOptions = new List<ListOption>
|
||||
{
|
||||
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;
|
||||
|
||||
//AudioInfo AudioInfo = GetAudioInfo(args);
|
||||
//if (AudioInfo == 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.Insert(4, "-vn"); // disables video
|
||||
|
||||
|
||||
if (Normalize)
|
||||
{
|
||||
string twoPass = AudioFileNormalization.DoTwoPass(args, ffmpegExe);
|
||||
ffArgs.Add("-af");
|
||||
ffArgs.Add(twoPass);
|
||||
}
|
||||
|
||||
ffArgs.Add(outputFile);
|
||||
|
||||
args.Logger?.ILog("FFArgs: " + String.Join(" ", ffArgs.Select(x => x.IndexOf(" ") > 0 ? "\"" + x + "\"" : x).ToArray()));
|
||||
|
||||
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);
|
||||
|
||||
// update the Audio file info
|
||||
if (ReadAudioFileInfo(args, ffmpegExe, args.WorkingFile))
|
||||
return 1;
|
||||
|
||||
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();
|
||||
//}
|
||||
}
|
||||
}
|
||||
15
AudioNodes/Plugin.cs
Normal file
15
AudioNodes/Plugin.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace FileFlows.AudioNodes;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using FileFlows.Plugin.Attributes;
|
||||
|
||||
public class Plugin : FileFlows.Plugin.IPlugin
|
||||
{
|
||||
public Guid Uid => new Guid("d951a39e-4296-4801-ab41-4070b0789465");
|
||||
public string Name => "Audio Nodes";
|
||||
public string MinimumVersion => "0.9.0.1487";
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
}
|
||||
82
AudioNodes/Tests/AudioFileNormalizationTests.cs
Normal file
82
AudioNodes/Tests/AudioFileNormalizationTests.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
#if(DEBUG)
|
||||
|
||||
|
||||
namespace FileFlows.AudioNodes.Tests;
|
||||
|
||||
using FileFlows.AudioNodes;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
[TestClass]
|
||||
public class AudioFileNormalizationTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void AudioFileNormalization_Mp3()
|
||||
{
|
||||
|
||||
const string file = @"D:\music\unprocessed\01-billy_joel-movin_out.mp3";
|
||||
|
||||
AudioFileNormalization node = new ();
|
||||
var logger = new TestLogger();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
int output = node.Execute(args);
|
||||
|
||||
string log = logger.ToString();
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
[TestMethod]
|
||||
public void AudioFileNormalization_Bulk()
|
||||
{
|
||||
|
||||
foreach (var file in Directory.GetFiles(@"D:\music\unprocessed"))
|
||||
{
|
||||
|
||||
AudioFileNormalization node = new();
|
||||
var logger = new TestLogger();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
int output = node.Execute(args);
|
||||
|
||||
string log = logger.ToString();
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void AudioFileNormalization_ConvertFlacToMp3()
|
||||
{
|
||||
|
||||
const string file = @"D:\music\flacs\03-billy_joel-dont_ask_me_why.flac";
|
||||
var logger = new TestLogger();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
|
||||
new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
|
||||
ConvertToMP3 convertNode = new();
|
||||
int output = convertNode.Execute(args);
|
||||
|
||||
|
||||
AudioFileNormalization normalNode = new();
|
||||
output = normalNode.Execute(args);
|
||||
|
||||
string log = logger.ToString();
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
92
AudioNodes/Tests/AudioInfoTests.cs
Normal file
92
AudioNodes/Tests/AudioInfoTests.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
#if(DEBUG)
|
||||
|
||||
|
||||
namespace FileFlows.AudioNodes.Tests
|
||||
{
|
||||
using FileFlows.AudioNodes;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
[TestClass]
|
||||
public class AudioInfoTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void AudioInfo_SplitTrack()
|
||||
{
|
||||
|
||||
const string file = @"\\oracle\Audio\The Cranberries\No Need To Argue\The Cranberries - No Need To Argue - 00 - I Don't Need (Demo).mp3";
|
||||
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => ffmpegExe;
|
||||
args.TempPath = @"D:\music\temp";
|
||||
|
||||
var AudioInfo = new AudioInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile);
|
||||
|
||||
Assert.AreEqual(9, AudioInfo.Track);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AudioInfo_NormalTrack()
|
||||
{
|
||||
|
||||
const string file = @"\\oracle\Audio\Taylor Swift\Speak Now\Taylor Swift - Speak Now - 08 - Never Grow Up.mp3";
|
||||
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => ffmpegExe;
|
||||
args.TempPath = @"D:\music\temp";
|
||||
|
||||
var AudioInfo = new AudioInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile);
|
||||
|
||||
Assert.AreEqual(8, AudioInfo.Track);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AudioInfo_GetMetaData()
|
||||
{
|
||||
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
var logger = new TestLogger();
|
||||
foreach (string file in Directory.GetFiles(@"D:\videos\Audio"))
|
||||
{
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => ffmpegExe;
|
||||
|
||||
// laod the variables
|
||||
Assert.AreEqual(1, new AudioFile().Execute(args));
|
||||
|
||||
var audio = new AudioInfoHelper(ffmpegExe, args.Logger).Read(args.WorkingFile);
|
||||
|
||||
string folder = args.ReplaceVariables("{audio.ArtistThe} ({audio.Year})");
|
||||
Assert.AreEqual($"{audio.Artist} ({audio.Date.Year})", folder);
|
||||
|
||||
string fname = args.ReplaceVariables("{audio.Artist} - {audio.Album} - {audio.Track:##} - {audio.Title}");
|
||||
Assert.AreEqual($"{audio.Artist} - {audio.Track.ToString("00")} - {audio.Title}", fname);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AudioInfo_FileNameMetadata()
|
||||
{
|
||||
const string ffmpegExe = @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
var logger = new TestLogger();
|
||||
string file = @"\\jor-el\Audio\Meat Loaf\Bat out of Hell II- Back Into Hell… (1993)\Meat Loaf - Bat out of Hell II- Back Into Hell… - 03 - I’d Do Anything for Love (but I Won’t Do That).flac";
|
||||
|
||||
var audio = new AudioInfo();
|
||||
|
||||
new AudioInfoHelper(ffmpegExe, logger).ParseFileNameInfo(file, audio);
|
||||
|
||||
Assert.AreEqual("Meat Loaf", audio.Artist);
|
||||
Assert.AreEqual("Bat out of Hell II- Back Into Hell…", audio.Album);
|
||||
Assert.AreEqual(1993, audio.Date.Year);
|
||||
Assert.AreEqual("I’d Do Anything for Love (but I Won’t Do That)", audio.Title);
|
||||
Assert.AreEqual(3, audio.Track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
194
AudioNodes/Tests/ConvertTests.cs
Normal file
194
AudioNodes/Tests/ConvertTests.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
#if(DEBUG)
|
||||
|
||||
|
||||
namespace FileFlows.AudioNodes.Tests
|
||||
{
|
||||
using FileFlows.AudioNodes;
|
||||
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.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio 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.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio 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.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio 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.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio 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.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
int output = node.Execute(args);
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_Mp3_AlreadyMp3()
|
||||
{
|
||||
|
||||
const string file = @"D:\videos\Audio\13-the_cranberries-why.mp3";
|
||||
|
||||
ConvertAudio node = new();
|
||||
node.SkipIfCodecMatches = true;
|
||||
node.Codec = "mp3";
|
||||
|
||||
node.Bitrate = 192;
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
int output = node.Execute(args);
|
||||
|
||||
Assert.AreEqual(2, output);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_VideoToMp3()
|
||||
{
|
||||
|
||||
const string file = @"D:\videos\testfiles\basic.mkv";
|
||||
|
||||
ConvertToMP3 node = new();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
//new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
node.PreExecute(args);
|
||||
int output = node.Execute(args);
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_VideoToAac()
|
||||
{
|
||||
|
||||
const string file = @"D:\videos\testfiles\basic.mkv";
|
||||
|
||||
ConvertToAAC node = new();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
//new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
node.PreExecute(args);
|
||||
int output = node.Execute(args);
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_TwoPass()
|
||||
{
|
||||
|
||||
const string file = @"D:\music\flacs\01-billy_joel-you_may_be_right.flac";
|
||||
|
||||
ConvertToAAC node = new();
|
||||
var logger = new TestLogger();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
node.Normalize = true;
|
||||
int output = node.Execute(args);
|
||||
|
||||
string log = logger.ToString();
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_TwoPass_VideoFile()
|
||||
{
|
||||
|
||||
const string file = @"D:\videos\testfiles\basic.mkv";
|
||||
|
||||
ConvertToAAC node = new();
|
||||
var logger = new TestLogger();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, logger, false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
args.TempPath = @"D:\music\temp";
|
||||
new AudioFile().Execute(args); // need to read the Audio info and set it
|
||||
node.Normalize = true;
|
||||
int output = node.Execute(args);
|
||||
|
||||
string log = logger.ToString();
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
61
AudioNodes/Tests/TestLogger.cs
Normal file
61
AudioNodes/Tests/TestLogger.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
#if(DEBUG)
|
||||
|
||||
namespace FileFlows.AudioNodes.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<string> Messages = new List<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Join(Environment.NewLine, this.Messages.ToArray());
|
||||
}
|
||||
|
||||
public string GetTail(int length = 50)
|
||||
{
|
||||
if (length <= 0)
|
||||
length = 50;
|
||||
if (Messages.Count <= length)
|
||||
return string.Join(Environment.NewLine, Messages);
|
||||
return string.Join(Environment.NewLine, Messages.TakeLast(length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -4,8 +4,8 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -29,7 +29,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apprise", "Apprise\Apprise.
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageNodes", "ImageNodes\ImageNodes.csproj", "{3C6B9933-B6BC-4C00-9247-71F575AA276B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoLegacyNodes", "VideoLegacyNodes\VideoLegacyNodes.csproj", "{4339720B-5061-431F-9080-FC087ACFDB3B}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoLegacyNodes", "VideoLegacyNodes\VideoLegacyNodes.csproj", "{4339720B-5061-431F-9080-FC087ACFDB3B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudioNodes", "AudioNodes\AudioNodes.csproj", "{600204C7-94F1-4793-9D02-D836A1B15B60}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -93,6 +95,10 @@ Global
|
||||
{4339720B-5061-431F-9080-FC087ACFDB3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4339720B-5061-431F-9080-FC087ACFDB3B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{600204C7-94F1-4793-9D02-D836A1B15B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{600204C7-94F1-4793-9D02-D836A1B15B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{600204C7-94F1-4793-9D02-D836A1B15B60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{600204C7-94F1-4793-9D02-D836A1B15B60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace FileFlows.MusicNodes
|
||||
|
||||
public class MusicFile : MusicNode
|
||||
{
|
||||
public override bool Obsolete => true;
|
||||
public override int Outputs => 1;
|
||||
public override FlowElementType Type => FlowElementType.Input;
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
<Product>Music Nodes</Product>
|
||||
<PackageProjectUrl>https://fileflows.com/</PackageProjectUrl>
|
||||
<Description>Nodes for processing music files. This plugin contains nodes to convert music files to different formats. Node to parse the music information from a file.</Description>
|
||||
<Description>OBSOLETE. This plugin has been replaced by the Audio Nodes plugin.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace FileFlows.MusicNodes;
|
||||
|
||||
public class AudioFileNormalization : MusicNode
|
||||
{
|
||||
public override bool Obsolete => true;
|
||||
public override int Inputs => 1;
|
||||
public override int Outputs => 1;
|
||||
public override FlowElementType Type => FlowElementType.Process;
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace FileFlows.MusicNodes
|
||||
{
|
||||
public class ConvertToMP3 : ConvertNode
|
||||
{
|
||||
public override bool Obsolete => true;
|
||||
protected override string Extension => "mp3";
|
||||
public static List<ListOption> BitrateOptions => ConvertNode.BitrateOptions;
|
||||
protected override List<string> GetArguments()
|
||||
@@ -239,6 +240,7 @@ namespace FileFlows.MusicNodes
|
||||
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.Insert(4, "-vn"); // disables video
|
||||
ffArgs.Add(outputFile);
|
||||
|
||||
args.Logger?.ILog("FFArgs: " + String.Join(" ", ffArgs.Select(x => x.IndexOf(" ") > 0 ? "\"" + x + "\"" : x).ToArray()));
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace FileFlows.MusicNodes
|
||||
|
||||
public abstract class MusicNode : Node
|
||||
{
|
||||
public override bool Obsolete => true;
|
||||
public override string Icon => "fas fa-music";
|
||||
|
||||
protected string GetFFMpegExe(NodeParameters args)
|
||||
|
||||
@@ -6,7 +6,7 @@ using FileFlows.Plugin.Attributes;
|
||||
public class Plugin : FileFlows.Plugin.IPlugin
|
||||
{
|
||||
public Guid Uid => new Guid("d84fbd06-f0e3-4827-8de0-6b0ef20dd883");
|
||||
public string Name => "Music Nodes";
|
||||
public string Name => "Music Nodes (Obsolete)";
|
||||
public string MinimumVersion => "0.9.0.1487";
|
||||
|
||||
public void Init()
|
||||
|
||||
@@ -113,6 +113,40 @@ namespace FileFlows.MusicNodes.Tests
|
||||
|
||||
Assert.AreEqual(2, output);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_VideoToMp3()
|
||||
{
|
||||
|
||||
const string file = @"D:\videos\testfiles\basic.mkv";
|
||||
|
||||
ConvertToMP3 node = new();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
|
||||
args.GetToolPathActual = (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
|
||||
node.PreExecute(args);
|
||||
int output = node.Execute(args);
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_VideoToAac()
|
||||
{
|
||||
|
||||
const string file = @"D:\videos\testfiles\basic.mkv";
|
||||
|
||||
ConvertToAAC node = new();
|
||||
var args = new FileFlows.Plugin.NodeParameters(file, new TestLogger(), false, string.Empty);
|
||||
args.GetToolPathActual = (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
|
||||
node.PreExecute(args);
|
||||
int output = node.Execute(args);
|
||||
|
||||
Assert.AreEqual(1, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>FileFlows.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
|
||||
@@ -4,13 +4,17 @@ public class FfmpegBuilderScaler : FfmpegBuilderNode
|
||||
{
|
||||
public override string HelpUrl => "https://docs.fileflows.com/plugins/video-nodes/ffmpeg-builder/video-scaler";
|
||||
|
||||
[Boolean(2)]
|
||||
public bool Force { get; set; }
|
||||
|
||||
|
||||
[Select(nameof(ResolutionOptions), 1)]
|
||||
public string Resolution { get; set; }
|
||||
public override int Outputs => 2;
|
||||
|
||||
[Boolean(2)]
|
||||
public bool Force { get; set; }
|
||||
|
||||
[ConditionEquals(nameof(Force), true, inverse: true)]
|
||||
[Boolean(3)]
|
||||
public bool OnlyIfLarger { get; set; }
|
||||
|
||||
|
||||
private static List<ListOption> _ResolutionOptions;
|
||||
@@ -33,28 +37,50 @@ public class FfmpegBuilderScaler : FfmpegBuilderNode
|
||||
return _ResolutionOptions;
|
||||
}
|
||||
}
|
||||
public override int Outputs => 2;
|
||||
public override int Execute(NodeParameters args)
|
||||
{
|
||||
var videoInfo = GetVideoInfo(args);
|
||||
if (videoInfo == null || videoInfo.VideoStreams?.Any() != true)
|
||||
return -1;
|
||||
|
||||
bool scale1920 = Resolution.StartsWith("1920");
|
||||
bool scale4k= Resolution.StartsWith("3840");
|
||||
bool scale720 = Resolution.StartsWith("1280");
|
||||
bool scale480 = Resolution.StartsWith("640");
|
||||
int width = videoInfo.VideoStreams[0].Width;
|
||||
|
||||
if (Force == false)
|
||||
{
|
||||
var resolution = ResolutionHelper.GetResolution(videoInfo);
|
||||
if (resolution == ResolutionHelper.Resolution.r1080p && Resolution.StartsWith("1920"))
|
||||
|
||||
if (OnlyIfLarger)
|
||||
{
|
||||
if (scale4k && width > 3840)
|
||||
return Scale();
|
||||
if (scale1920 && width > 1920)
|
||||
return Scale();
|
||||
if (scale720 && width > 1280)
|
||||
return Scale();
|
||||
if (scale480 && width > 640)
|
||||
return Scale();
|
||||
return 2;
|
||||
else if (resolution == ResolutionHelper.Resolution.r4k && Resolution.StartsWith("3840"))
|
||||
}
|
||||
|
||||
if (resolution == ResolutionHelper.Resolution.r1080p && scale1920)
|
||||
return 2;
|
||||
else if (resolution == ResolutionHelper.Resolution.r720p && Resolution.StartsWith("1280"))
|
||||
else if (resolution == ResolutionHelper.Resolution.r4k && scale4k)
|
||||
return 2;
|
||||
else if (resolution == ResolutionHelper.Resolution.r480p && Resolution.StartsWith("640"))
|
||||
else if (resolution == ResolutionHelper.Resolution.r720p && scale720)
|
||||
return 2;
|
||||
else if (resolution == ResolutionHelper.Resolution.r480p && scale480)
|
||||
return 2;
|
||||
}
|
||||
return Scale();
|
||||
|
||||
Model.VideoStreams[0].Filter.AddRange(new[] { $"scale={Resolution}:flags=lanczos" });
|
||||
|
||||
return 1;
|
||||
int Scale()
|
||||
{
|
||||
Model.VideoStreams[0].Filter.AddRange(new[] { $"scale={Resolution}:flags=lanczos" });
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using VideoNodes.Tests;
|
||||
namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests;
|
||||
|
||||
[TestClass]
|
||||
public class FfmpegBuilder_BasicTests
|
||||
public class FfmpegBuilder_BasicTests : TestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void FfmpegBuilder_Basic_h265()
|
||||
@@ -1341,6 +1341,45 @@ public class FfmpegBuilder_BasicTests
|
||||
Assert.IsTrue(log.Contains($"-ss 240 -i \"{file}\" -hide_banner -vframes 25 -vf cropdetect -f null -"));
|
||||
Assert.IsTrue(log.Contains($"-ss 360 -i \"{file}\" -hide_banner -vframes 25 -vf cropdetect -f null -"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void FfmpegBuilder_Scale()
|
||||
{
|
||||
var logger = new TestLogger();
|
||||
const string ffmpeg = @"C:\utils\ffmpeg\ffmpeg.exe";
|
||||
var vi = new VideoInfoHelper(ffmpeg, logger);
|
||||
var vii = vi.Read(TestFile_120_mbps_4k_uhd_hevc_10bit);
|
||||
var args = new NodeParameters(TestFile_50_mbps_hd_h264, logger, false, string.Empty);
|
||||
args.GetToolPathActual = (string tool) => ffmpeg;
|
||||
args.TempPath = @"D:\videos\temp";
|
||||
args.Parameters.Add("VideoInfo", vii);
|
||||
|
||||
|
||||
FfmpegBuilderStart ffStart = new();
|
||||
ffStart.PreExecute(args);
|
||||
Assert.AreEqual(1, ffStart.Execute(args));
|
||||
|
||||
FfmpegBuilderVideoCodec ffCodec = new();
|
||||
ffCodec.VideoCodec = "h265";
|
||||
ffCodec.VideoCodecParameters = "h265";
|
||||
ffCodec.PreExecute(args);
|
||||
ffCodec.Execute(args);
|
||||
|
||||
FfmpegBuilderScaler ffScaler = new();
|
||||
ffScaler.Resolution = "640:-2";
|
||||
ffScaler.Force = true;
|
||||
ffScaler.PreExecute(args);
|
||||
ffScaler.Execute(args);
|
||||
|
||||
FfmpegBuilderExecutor ffExecutor = new();
|
||||
ffExecutor.PreExecute(args);
|
||||
int result = ffExecutor.Execute(args);
|
||||
string log = logger.ToString();
|
||||
Assert.AreEqual(1, result);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,8 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<FileVersion>0.9.1.159</FileVersion>
|
||||
<ProductVersion>0.9.1.159</ProductVersion>
|
||||
<FileVersion>0.9.4.168</FileVersion>
|
||||
<ProductVersion>0.9.4.168</ProductVersion>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<Company>FileFlows</Company>
|
||||
<Authors>John Andrews</Authors>
|
||||
|
||||
@@ -347,6 +347,8 @@
|
||||
"Fields": {
|
||||
"Force": "Force",
|
||||
"Force-Help": "When checked the video will be force scaled even if the working file is already in this resolution (or near this resolution).",
|
||||
"OnlyIfLarger": "Only If Larger",
|
||||
"OnlyIfLarger-Help": "The video will only be scaled to this resolution if the video is currently larger than this resolution",
|
||||
"Resolution": "Resolution"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,14 +69,14 @@ public class VideoExtractAudio : AudioSelectionEncodingNode
|
||||
var track = GetTrack(videoInfo);
|
||||
if (track == null)
|
||||
{
|
||||
args.Logger.WLog("Unable to find matchiung audio track to extract");
|
||||
args.Logger.WLog("Unable to find matching audio track to extract");
|
||||
return 2;
|
||||
}
|
||||
|
||||
string outputFile = GetOutputFile(args);
|
||||
var parameters = GetAudioTrackParameters(track);
|
||||
|
||||
var extracted = ExtractSubtitle(args, FFMPEG, parameters, outputFile);
|
||||
var extracted = ExtractAudio(args, FFMPEG, parameters, outputFile);
|
||||
if(extracted)
|
||||
{
|
||||
args.UpdateVariables(new Dictionary<string, object>
|
||||
@@ -142,7 +142,7 @@ public class VideoExtractAudio : AudioSelectionEncodingNode
|
||||
};
|
||||
}
|
||||
|
||||
internal bool ExtractSubtitle(NodeParameters args, string ffmpegExe, string[] parameters, string output)
|
||||
internal bool ExtractAudio(NodeParameters args, string ffmpegExe, string[] parameters, string output)
|
||||
{
|
||||
if (File.Exists(output))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user