|
@@ -0,0 +1,385 @@
|
|
|
+using System;
|
|
|
+using System.Diagnostics;
|
|
|
+using System.IO;
|
|
|
+using System.Linq;
|
|
|
+using System.Net.Mime;
|
|
|
+using System.Reflection;
|
|
|
+using System.Text;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using TagLib;
|
|
|
+using File = System.IO.File;
|
|
|
+
|
|
|
+// ReSharper disable InconsistentNaming
|
|
|
+
|
|
|
+namespace BatchProcess
|
|
|
+{
|
|
|
+ internal class Program
|
|
|
+ {
|
|
|
+ private static void Main(string[] args)
|
|
|
+ {
|
|
|
+ Console.WriteLine("Batch Processor");
|
|
|
+ Console.WriteLine();
|
|
|
+
|
|
|
+ var skipPause = false;
|
|
|
+ if (args.Length > 0 && args[0].ToLower() == "--no-pause")
|
|
|
+ {
|
|
|
+ skipPause = true;
|
|
|
+ args = args.Skip(1).ToArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ RealMain(args);
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ Console.WriteLine($"ERROR:{e.GetType().Name},{e.Message}");
|
|
|
+ }
|
|
|
+
|
|
|
+ Console.WriteLine();
|
|
|
+
|
|
|
+ if (skipPause) return;
|
|
|
+ Console.Write("Press ENTER to exit...");
|
|
|
+ Console.ReadLine();
|
|
|
+ }
|
|
|
+
|
|
|
+ private const string OP_DIR_EMB_COVER_MP3 = "embcover_mp3_jpg_dir";
|
|
|
+ private const string OP_DIR_EMB_COVER_M4A = "embcover_m4a_jpg_dir";
|
|
|
+ private const string OP_EMB_COVER = "embcover";
|
|
|
+ private const string OP_COPY = "copy";
|
|
|
+ private const string OP_EMB_COVER_IF_NOT_EMBEDDED = "embcover_if_not_embedded";
|
|
|
+ private const string OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG = "embcover_if_not_embedded_cover_jpg";
|
|
|
+ private const string OP_EMB_LRC = "emblrc";
|
|
|
+ private const string OP_CONVERT_FLAC_TO_AAC = "convert_flac_to_aac";
|
|
|
+ private const string OP_CONVERT_LIB_FLAC_TO_AAC = "convert_lib_flac_to_aac";
|
|
|
+
|
|
|
+ private static void RealMain(string[] args)
|
|
|
+ {
|
|
|
+ if (0 == args.Length)
|
|
|
+ {
|
|
|
+ Console.WriteLine($"Usage: {Path.GetFileName(Assembly.GetExecutingAssembly().CodeBase)} <op> [args...]");
|
|
|
+ Console.WriteLine($"Prefix ops:");
|
|
|
+ Console.WriteLine($" * --no-pause");
|
|
|
+ Console.WriteLine($" Disable enter key to exit");
|
|
|
+ Console.WriteLine($"Available ops:");
|
|
|
+ Console.WriteLine($" * {OP_COPY} <sourcePath> <targetPath>");
|
|
|
+ Console.WriteLine($" Copy tags from source to target (replace)");
|
|
|
+ Console.WriteLine($" * {OP_EMB_LRC} <inputPath> <lrcPath> <outputPath>");
|
|
|
+ Console.WriteLine($" Embed lyrics into file(utf8)");
|
|
|
+ Console.WriteLine($" * {OP_EMB_COVER}|{OP_EMB_COVER_IF_NOT_EMBEDDED} <inputPath> <coverPath> [outputPath] [mime-type]");
|
|
|
+ Console.WriteLine($" Embed cover into file, default mine-type is {MediaTypeNames.Image.Jpeg}");
|
|
|
+ Console.WriteLine($" * {OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG} <inputPath>");
|
|
|
+ Console.WriteLine($" Embed cover into file if not embedded, same folder cover.jpg");
|
|
|
+ Console.WriteLine($" * {OP_DIR_EMB_COVER_MP3} <inputDir> <outputDir> -- lookup dir, embed same file name jpg/jpeg into mp3");
|
|
|
+ Console.WriteLine($" Lookup dir, embed same file name jpg/jpeg into mp3");
|
|
|
+ Console.WriteLine($" Embed abc.jpg or abc.jpeg into abc.mp3 and save a copy to output dir.");
|
|
|
+ Console.WriteLine($" * {OP_DIR_EMB_COVER_M4A} <inputDir> <outputDir> -- lookup dir, embed same file name jpg/jpeg into m4a");
|
|
|
+ Console.WriteLine($" Lookup dir, embed same file name jpg/jpeg into m4a");
|
|
|
+ Console.WriteLine($" Embed abc.jpg or abc.jpeg into abc.m4a and save a copy to output dir.");
|
|
|
+ Console.WriteLine($" * {OP_CONVERT_FLAC_TO_AAC} <inputPath> <outputPath> [q value]");
|
|
|
+ Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
|
|
|
+ Console.WriteLine($" * {OP_CONVERT_LIB_FLAC_TO_AAC} <inputDir> <outputDir> [q value]");
|
|
|
+ Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
|
|
|
+ Console.WriteLine($" inputDir/Album/Track.flac -> outputDir/Album/Track.m4a");
|
|
|
+ Console.WriteLine($" Also copy inputDir/Album/cover.jpg to outDir/Album/cover.jpg if found");
|
|
|
+
|
|
|
+ Console.WriteLine($"");
|
|
|
+ Console.WriteLine($" + More on demand");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var op = args[0];
|
|
|
+
|
|
|
+ Console.WriteLine($"OP:{op}");
|
|
|
+
|
|
|
+ var argsToOp = args.Skip(1).ToArray();
|
|
|
+
|
|
|
+ switch (op.ToLower())
|
|
|
+ {
|
|
|
+ default: throw new ArgumentException($"No recognizable op:{op}");
|
|
|
+ case OP_DIR_EMB_COVER_MP3: EmbJpegCoverDir(argsToOp, "*.mp3"); break;
|
|
|
+ case OP_DIR_EMB_COVER_M4A: EmbJpegCoverDir(argsToOp, "*.m4a"); break;
|
|
|
+ case OP_EMB_COVER: EmbCover(argsToOp, true); break;
|
|
|
+ case OP_EMB_COVER_IF_NOT_EMBEDDED: EmbCover(argsToOp, false); break;
|
|
|
+ case OP_EMB_LRC: EmbLrc(argsToOp); break;
|
|
|
+ case OP_COPY: CopyTag(argsToOp); break;
|
|
|
+ case OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG:
|
|
|
+ if (argsToOp.Length != 1) throw new ArgumentException("args need 1");
|
|
|
+ // ReSharper disable once AssignNullToNotNullAttribute
|
|
|
+ var argsMod = new[] { argsToOp[0], Path.Combine(Path.GetDirectoryName(argsToOp[0]), "cover.jpg") };
|
|
|
+ EmbCover(argsMod, false);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case OP_CONVERT_FLAC_TO_AAC: ConvertFlacToAac(argsToOp); break;
|
|
|
+ case OP_CONVERT_LIB_FLAC_TO_AAC: ConvertLibFlacToAac(argsToOp); break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // impl
|
|
|
+
|
|
|
+ private static void ConvertLibFlacToAac(string[] args)
|
|
|
+ {
|
|
|
+ if (args.Length < 2) throw new ArgumentException("args least need 2");
|
|
|
+ var inDir = args[0];
|
|
|
+ var outDir = args[1];
|
|
|
+
|
|
|
+ if (false == Directory.Exists(inDir)) throw new DirectoryNotFoundException($"input dir `{inDir}' not found.");
|
|
|
+ if (false == Directory.Exists(outDir)) throw new DirectoryNotFoundException($"output dir `{inDir}' not found.");
|
|
|
+
|
|
|
+ var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
|
|
|
+ ? tq
|
|
|
+ : 1.0;
|
|
|
+
|
|
|
+ foreach (var albDir in Directory.GetDirectories(inDir))
|
|
|
+ {
|
|
|
+ var albDirName = Path.GetFileName(albDir);
|
|
|
+ var outAlbDir = Path.Combine(outDir, albDirName);
|
|
|
+ if (false == Directory.Exists(outAlbDir))
|
|
|
+ {
|
|
|
+ Console.WriteLine($"Create dir:{outAlbDir}");
|
|
|
+ Directory.CreateDirectory(outAlbDir);
|
|
|
+ }
|
|
|
+
|
|
|
+ var coverPath = Path.Combine(albDir, "cover.jpg");
|
|
|
+ var destCoverPath = Path.Combine(outAlbDir, "cover.jpg");
|
|
|
+ if (File.Exists(coverPath) && false == File.Exists(destCoverPath))
|
|
|
+ {
|
|
|
+ Console.WriteLine($"Copy {coverPath}");
|
|
|
+ Console.WriteLine($" -> {destCoverPath} ");
|
|
|
+ File.Copy(coverPath, destCoverPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var flacPath in Directory.GetFiles(albDir, "*.flac"))
|
|
|
+ {
|
|
|
+ var outM4APath = Path.Combine(outAlbDir, Path.ChangeExtension(Path.GetFileName(flacPath), ".m4a"));
|
|
|
+ if (ConvertFlacToAac(new[] { flacPath, outM4APath, $"{q}" }))
|
|
|
+ {
|
|
|
+ Console.WriteLine($"Convert OK. -> {outM4APath}");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Console.WriteLine($"Exist, Skipped. {outM4APath}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool ConvertFlacToAac(string[] args)
|
|
|
+ {
|
|
|
+ if (args.Length < 2) throw new ArgumentException("args least need 2");
|
|
|
+ var pathIn = args[0];
|
|
|
+ var pathOutput = args[1];
|
|
|
+ var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
|
|
|
+ ? tq
|
|
|
+ : 1.0;
|
|
|
+
|
|
|
+ if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
|
|
|
+ if (File.Exists(pathOutput)) return false;
|
|
|
+ if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
|
|
|
+
|
|
|
+ Console.WriteLine($"Processing {pathIn}...");
|
|
|
+
|
|
|
+ var flacProcess = new Process
|
|
|
+ {
|
|
|
+ StartInfo =
|
|
|
+ {
|
|
|
+ FileName = "FLAC",
|
|
|
+ Arguments = $"-s -c -d \"{pathIn}\"",
|
|
|
+ RedirectStandardOutput = true,
|
|
|
+ UseShellExecute = false,
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ var aacProcess = new Process
|
|
|
+ {
|
|
|
+ StartInfo =
|
|
|
+ {
|
|
|
+ FileName = "NeroAacEnc",
|
|
|
+ Arguments = $"-if - -q {q} -of \"{pathOutput}\"",
|
|
|
+ RedirectStandardInput = true,
|
|
|
+ UseShellExecute = false,
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ flacProcess.Start();
|
|
|
+ aacProcess.Start();
|
|
|
+
|
|
|
+ var pipe = Task.Factory.StartNew(() =>
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ flacProcess.StandardOutput.BaseStream.CopyTo(aacProcess.StandardInput.BaseStream, 1024 * 512);
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ //EAT ERROR
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ aacProcess.StandardInput.BaseStream.Close();
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ //EAT ERROR
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ Task.WaitAll(pipe);
|
|
|
+ flacProcess.WaitForExit();
|
|
|
+ aacProcess.WaitForExit();
|
|
|
+
|
|
|
+ CopyTag(args);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void CopyTag(string[] args)
|
|
|
+ {
|
|
|
+ if (args.Length < 2) throw new ArgumentException("args need 2");
|
|
|
+ var pathIn = args[0];
|
|
|
+ var pathOutput = args[1];
|
|
|
+
|
|
|
+ if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
|
|
|
+ if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
|
|
|
+ if (false == File.Exists(pathOutput)) throw new FileNotFoundException($"targetPath `{pathOutput}' not found.");
|
|
|
+
|
|
|
+ using var src = TagLib.File.Create(pathIn);
|
|
|
+ var dst = TagLib.File.Create(pathOutput);
|
|
|
+ dst.Tag.Clear();
|
|
|
+ dst.Save();
|
|
|
+ dst.Dispose();
|
|
|
+ dst = TagLib.File.Create(pathOutput);
|
|
|
+ src.Tag.CopyTo(dst.Tag, true);
|
|
|
+ dst.Tag.Pictures = src.Tag.Pictures;
|
|
|
+ src.Dispose();
|
|
|
+ dst.Save();
|
|
|
+ dst.Dispose();
|
|
|
+ Console.WriteLine("Tag Copied.");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void EmbCover(string[] args, bool overwrite)
|
|
|
+ {
|
|
|
+ if (args.Length < 2) throw new ArgumentException("args least need 2");
|
|
|
+ var pathIn = args[0];
|
|
|
+ var pathCover = args[1];
|
|
|
+ var pathOutput = args.Length > 2 ? args[2] : args[0];
|
|
|
+ var mimeType = args.Length > 3 ? args[4] : MediaTypeNames.Image.Jpeg;
|
|
|
+
|
|
|
+ if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
|
|
|
+ if (false == File.Exists(pathCover)) throw new FileNotFoundException($"coverPath `{pathCover}' not found.");
|
|
|
+
|
|
|
+ Console.WriteLine($"Processing {pathIn}...");
|
|
|
+ Console.WriteLine($"Comver image:{pathCover}");
|
|
|
+
|
|
|
+ if (pathIn != pathOutput) File.Copy(pathIn, pathOutput, true);
|
|
|
+
|
|
|
+ if (EmbedCoverIntoFile(pathOutput, pathCover, mimeType, overwrite))
|
|
|
+ {
|
|
|
+ // 12345678901234
|
|
|
+ Console.WriteLine($"Saved to: {pathOutput}");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Console.WriteLine("Skipped.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void EmbLrc(string[] args)
|
|
|
+ {
|
|
|
+ if (args.Length < 3) throw new ArgumentException("args need 2");
|
|
|
+ var pathIn = args[0];
|
|
|
+ var pathLrc = args[1];
|
|
|
+ var pathOutput = args[2];
|
|
|
+
|
|
|
+ if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
|
|
|
+ if (false == File.Exists(pathLrc)) throw new FileNotFoundException($"coverPath `{pathLrc}' not found.");
|
|
|
+
|
|
|
+ Console.WriteLine($"Processing {pathIn}...");
|
|
|
+ Console.WriteLine($"Comver image:{pathLrc}");
|
|
|
+
|
|
|
+ if (pathIn != pathOutput) File.Copy(pathIn, pathOutput);
|
|
|
+
|
|
|
+ EmbedLrcIntoFile(pathOutput, pathLrc);
|
|
|
+
|
|
|
+ // 12345678901234
|
|
|
+ Console.WriteLine($"Saved to: {pathOutput}");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void EmbJpegCoverDir(string[] args, string pattern)
|
|
|
+ {
|
|
|
+ if (args.Length != 2) throw new ArgumentException("args need 2");
|
|
|
+
|
|
|
+ var dirIn = args[0];
|
|
|
+ var dirOut = args[1];
|
|
|
+
|
|
|
+ if (false == Directory.Exists(dirIn)) throw new DirectoryNotFoundException($"inputDir `{dirIn}' not found.");
|
|
|
+ if (false == Directory.Exists(dirOut)) throw new DirectoryNotFoundException($"outputDir `{dirOut} not found.'");
|
|
|
+
|
|
|
+ Console.WriteLine($"Enter directory:{dirIn}");
|
|
|
+ var files = Directory.GetFiles(dirIn, pattern);
|
|
|
+ if (0 == files.Length)
|
|
|
+ {
|
|
|
+ Console.WriteLine($"Not result for {pattern}");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ foreach (var file in files)
|
|
|
+ {
|
|
|
+ // 12345678901234
|
|
|
+ Console.WriteLine($"Processing {file}...");
|
|
|
+
|
|
|
+ var imgFilePath = Path.ChangeExtension(file, ".jpg");
|
|
|
+ if (false == File.Exists(imgFilePath)) imgFilePath = Path.ChangeExtension(file, ".jpeg");
|
|
|
+ if (false == File.Exists(imgFilePath))
|
|
|
+ {
|
|
|
+ Console.WriteLine("Cover image file not found, Skipped. ***");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 12345678901234
|
|
|
+ Console.WriteLine($"Comver image:{imgFilePath}");
|
|
|
+
|
|
|
+ var destFileName = Path.Combine(dirOut, Path.GetFileName(file));
|
|
|
+ File.Copy(file, destFileName);
|
|
|
+
|
|
|
+ EmbedCoverIntoFile(destFileName, imgFilePath, MediaTypeNames.Image.Jpeg);
|
|
|
+
|
|
|
+ // 12345678901234
|
|
|
+ Console.WriteLine($"Saved to: {destFileName}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Console.WriteLine($"Leave directory:{dirIn}");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool EmbedCoverIntoFile(string filePath, string coverPath, string mimeType, bool overwrite = true)
|
|
|
+ {
|
|
|
+ using var mediaFile = TagLib.File.Create(filePath);
|
|
|
+
|
|
|
+ if (!overwrite)
|
|
|
+ {
|
|
|
+ if (mediaFile.Tag.Pictures.Any(p => p.Type == PictureType.FrontCover)) return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ var pic = new Picture //REF: stackoverflow.com/a/30285220/2430943
|
|
|
+ {
|
|
|
+ Type = PictureType.FrontCover,
|
|
|
+ Description = "Cover",
|
|
|
+ MimeType = mimeType,
|
|
|
+ Data = File.ReadAllBytes(coverPath)
|
|
|
+ };
|
|
|
+ mediaFile.Tag.Pictures = new IPicture[] { pic };
|
|
|
+
|
|
|
+ mediaFile.Save();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void EmbedLrcIntoFile(string filePath, string lrcPath)
|
|
|
+ {
|
|
|
+ var mediaFile = TagLib.File.Create(filePath);
|
|
|
+
|
|
|
+ mediaFile.Tag.Lyrics = File.ReadAllText(lrcPath, Encoding.UTF8);
|
|
|
+
|
|
|
+ mediaFile.Save();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|