Program.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net.Mime;
  6. using System.Reflection;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using TagLib;
  10. using File = System.IO.File;
  11. // ReSharper disable InconsistentNaming
  12. namespace BatchProcess
  13. {
  14. internal class Program
  15. {
  16. private static void Main(string[] args)
  17. {
  18. Console.WriteLine("Batch Processor");
  19. Console.WriteLine();
  20. var skipPause = false;
  21. if (args.Length > 0 && args[0].ToLower() == "--no-pause")
  22. {
  23. skipPause = true;
  24. args = args.Skip(1).ToArray();
  25. }
  26. try
  27. {
  28. RealMain(args);
  29. }
  30. catch (Exception e)
  31. {
  32. Console.WriteLine($"ERROR:{e}");
  33. }
  34. Console.WriteLine();
  35. if (skipPause) return;
  36. Console.Write("Press ENTER to exit...");
  37. Console.ReadLine();
  38. }
  39. private const string OP_DIR_EMB_COVER_MP3 = "embcover_mp3_jpg_dir";
  40. private const string OP_DIR_EMB_COVER_M4A = "embcover_m4a_jpg_dir";
  41. private const string OP_EMB_COVER = "embcover";
  42. private const string OP_COPY = "copy";
  43. private const string OP_EMB_COVER_IF_NOT_EMBEDDED = "embcover_if_not_embedded";
  44. private const string OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG = "embcover_if_not_embedded_cover_jpg";
  45. private const string OP_EMB_LRC = "emblrc";
  46. private const string OP_CONVERT_FLAC_TO_AAC = "convert_flac_to_aac";
  47. private const string OP_CONVERT_LIB_FLAC_TO_AAC = "convert_lib_flac_to_aac";
  48. private const string OP_CONVERT_LIB_FLAC_TO_AAC_IN_DIR = "convert_lib_flac_to_aac_in_dir";
  49. private static void RealMain(string[] args)
  50. {
  51. if (0 == args.Length)
  52. {
  53. Console.WriteLine($"Usage: {Path.GetFileName(AppDomain.CurrentDomain.FriendlyName)} <op> [args...]");
  54. Console.WriteLine($"Prefix ops:");
  55. Console.WriteLine($" * --no-pause");
  56. Console.WriteLine($" Disable enter key to exit");
  57. Console.WriteLine($"Available ops:");
  58. Console.WriteLine($" * {OP_COPY} <sourcePath> <targetPath>");
  59. Console.WriteLine($" Copy tags from source to target (replace)");
  60. Console.WriteLine($" Note: Only standard tags and front cover");
  61. Console.WriteLine($" * {OP_EMB_LRC} <inputPath> <lrcPath> <outputPath>");
  62. Console.WriteLine($" Embed lyrics into file(utf8)");
  63. Console.WriteLine($" * {OP_EMB_COVER}|{OP_EMB_COVER_IF_NOT_EMBEDDED} <inputPath> <coverPath> [outputPath] [mime-type]");
  64. Console.WriteLine($" Embed cover into file, default mine-type is {MediaTypeNames.Image.Jpeg}");
  65. Console.WriteLine($" * {OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG} <inputPath>");
  66. Console.WriteLine($" Embed cover into file if not embedded, same folder cover.jpg");
  67. Console.WriteLine($" * {OP_DIR_EMB_COVER_MP3} <inputDir> <outputDir>");
  68. Console.WriteLine($" Lookup dir, embed same file name jpg/jpeg into mp3");
  69. Console.WriteLine($" Embed abc.jpg or abc.jpeg into abc.mp3 and save a copy to output dir.");
  70. Console.WriteLine($" * {OP_DIR_EMB_COVER_M4A} <inputDir> <outputDir>");
  71. Console.WriteLine($" Lookup dir, embed same file name jpg/jpeg into m4a");
  72. Console.WriteLine($" Embed abc.jpg or abc.jpeg into abc.m4a and save a copy to output dir.");
  73. Console.WriteLine($" * {OP_CONVERT_FLAC_TO_AAC} <inputPath> <outputPath> [q value]");
  74. Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
  75. Console.WriteLine($" * {OP_CONVERT_LIB_FLAC_TO_AAC} <inputDir> <outputDir> [q value]");
  76. Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
  77. Console.WriteLine($" inputDir/Album/Track.flac -> outputDir/Album/Track.m4a");
  78. Console.WriteLine($" Also copy inputDir/Album/cover.jpg to outDir/Album/cover.jpg if found");
  79. Console.WriteLine($" * {OP_CONVERT_LIB_FLAC_TO_AAC_IN_DIR} <libDir> [q value]");
  80. Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
  81. Console.WriteLine($" libDir/Album/Track.flac -> libDir/Album/AAC_Q1.00/Track.m4a");
  82. Console.WriteLine($"");
  83. Console.WriteLine($" + More on demand");
  84. return;
  85. }
  86. var op = args[0];
  87. Console.WriteLine($"OP:{op}");
  88. var argsToOp = args.Skip(1).ToArray();
  89. switch (op.ToLower())
  90. {
  91. default: throw new ArgumentException($"No recognizable op:{op}");
  92. case OP_DIR_EMB_COVER_MP3: EmbJpegCoverDir(argsToOp, "*.mp3"); break;
  93. case OP_DIR_EMB_COVER_M4A: EmbJpegCoverDir(argsToOp, "*.m4a"); break;
  94. case OP_EMB_COVER: EmbCover(argsToOp, true); break;
  95. case OP_EMB_COVER_IF_NOT_EMBEDDED: EmbCover(argsToOp, false); break;
  96. case OP_EMB_LRC: EmbLrc(argsToOp); break;
  97. case OP_COPY: CopyTag(argsToOp); break;
  98. case OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG:
  99. if (argsToOp.Length != 1) throw new ArgumentException("args need 1");
  100. // ReSharper disable once AssignNullToNotNullAttribute
  101. var argsMod = new[] { argsToOp[0], Path.Combine(Path.GetDirectoryName(argsToOp[0]), "cover.jpg") };
  102. EmbCover(argsMod, false);
  103. break;
  104. case OP_CONVERT_FLAC_TO_AAC: ConvertFlacToAac(argsToOp); break;
  105. case OP_CONVERT_LIB_FLAC_TO_AAC: ConvertLibFlacToAac(argsToOp); break;
  106. case OP_CONVERT_LIB_FLAC_TO_AAC_IN_DIR: ConvertLibFlacToAacInDir(argsToOp); break;
  107. }
  108. }
  109. // impl
  110. private static void ConvertLibFlacToAacInDir(string[] args)
  111. {
  112. if (args.Length < 1) throw new ArgumentException("args least need 1");
  113. var libDir = args[0];
  114. if (false == Directory.Exists(libDir)) throw new DirectoryNotFoundException($"lib dir `{libDir}' not found.");
  115. var q = args.Length > 1 && float.TryParse(args[1], out var tq) && tq is >= 0 and <= 1
  116. ? tq
  117. : 1.0;
  118. foreach (var albDir in Directory.GetDirectories(libDir))
  119. {
  120. var outAlbDir = Path.Combine(albDir, $"AAC_Q{q:N2}");
  121. if (false == Directory.Exists(outAlbDir))
  122. {
  123. Console.WriteLine($"Create dir:{outAlbDir}");
  124. Directory.CreateDirectory(outAlbDir);
  125. }
  126. foreach (var flacPath in Directory.GetFiles(albDir, "*.flac"))
  127. {
  128. var outM4APath = Path.Combine(outAlbDir, Path.ChangeExtension(Path.GetFileName(flacPath), ".m4a"));
  129. Console.WriteLine(
  130. ConvertFlacToAac(new[] {flacPath, outM4APath, $"{q}"})
  131. ? $"Convert OK. -> {outM4APath}"
  132. : $"Exist, Skipped. {outM4APath}"
  133. );
  134. }
  135. }
  136. }
  137. private static void ConvertLibFlacToAac(string[] args)
  138. {
  139. if (args.Length < 2) throw new ArgumentException("args least need 2");
  140. var inDir = args[0];
  141. var outDir = args[1];
  142. if (false == Directory.Exists(inDir)) throw new DirectoryNotFoundException($"input dir `{inDir}' not found.");
  143. if (false == Directory.Exists(outDir)) throw new DirectoryNotFoundException($"output dir `{inDir}' not found.");
  144. var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
  145. ? tq
  146. : 1.0;
  147. foreach (var albDir in Directory.GetDirectories(inDir))
  148. {
  149. var albDirName = Path.GetFileName(albDir);
  150. var outAlbDir = Path.Combine(outDir, albDirName);
  151. if (false == Directory.Exists(outAlbDir))
  152. {
  153. Console.WriteLine($"Create dir:{outAlbDir}");
  154. Directory.CreateDirectory(outAlbDir);
  155. }
  156. var coverPath = Path.Combine(albDir, "cover.jpg");
  157. var destCoverPath = Path.Combine(outAlbDir, "cover.jpg");
  158. if (File.Exists(coverPath) && false == File.Exists(destCoverPath))
  159. {
  160. Console.WriteLine($"Copy {coverPath}");
  161. Console.WriteLine($" -> {destCoverPath} ");
  162. File.Copy(coverPath, destCoverPath);
  163. }
  164. foreach (var flacPath in Directory.GetFiles(albDir, "*.flac"))
  165. {
  166. var outM4APath = Path.Combine(outAlbDir, Path.ChangeExtension(Path.GetFileName(flacPath), ".m4a"));
  167. if (ConvertFlacToAac(new[] { flacPath, outM4APath, $"{q}" }))
  168. {
  169. Console.WriteLine($"Convert OK. -> {outM4APath}");
  170. }
  171. else
  172. {
  173. Console.WriteLine($"Exist, Skipped. {outM4APath}");
  174. }
  175. }
  176. }
  177. }
  178. private static bool ConvertFlacToAac(string[] args)
  179. {
  180. if (args.Length < 2) throw new ArgumentException("args least need 2");
  181. var pathIn = args[0];
  182. var pathOutput = args[1];
  183. var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
  184. ? tq
  185. : 1.0;
  186. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
  187. if (File.Exists(pathOutput)) return false;
  188. if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
  189. Console.WriteLine($"Processing {pathIn}...");
  190. var flacProcess = new Process
  191. {
  192. StartInfo =
  193. {
  194. FileName = "FLAC",
  195. Arguments = $"-s -c -d \"{pathIn}\"",
  196. RedirectStandardOutput = true,
  197. UseShellExecute = false,
  198. }
  199. };
  200. var aacProcess = new Process
  201. {
  202. StartInfo =
  203. {
  204. FileName = "NeroAacEnc",
  205. Arguments = $"-if - -q {q} -of \"{pathOutput}\"",
  206. RedirectStandardInput = true,
  207. UseShellExecute = false,
  208. }
  209. };
  210. flacProcess.Start();
  211. aacProcess.Start();
  212. var pipe = Task.Factory.StartNew(() =>
  213. {
  214. try
  215. {
  216. flacProcess.StandardOutput.BaseStream.CopyTo(aacProcess.StandardInput.BaseStream, 1024 * 512);
  217. }
  218. catch
  219. {
  220. //EAT ERROR
  221. }
  222. try
  223. {
  224. aacProcess.StandardInput.BaseStream.Close();
  225. }
  226. catch
  227. {
  228. //EAT ERROR
  229. }
  230. });
  231. Task.WaitAll(pipe);
  232. flacProcess.WaitForExit();
  233. aacProcess.WaitForExit();
  234. CopyTag(args);
  235. return true;
  236. }
  237. private static void CopyTag(string[] args)
  238. {
  239. if (args.Length < 2) throw new ArgumentException("args need 2");
  240. var pathIn = args[0];
  241. var pathOutput = args[1];
  242. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
  243. if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
  244. if (false == File.Exists(pathOutput)) throw new FileNotFoundException($"targetPath `{pathOutput}' not found.");
  245. using var src = TagLib.File.Create(pathIn);
  246. var dst = TagLib.File.Create(pathOutput);
  247. dst.Tag.Clear();
  248. dst.Save();
  249. dst.Dispose();
  250. dst = TagLib.File.Create(pathOutput);
  251. src.Tag.CopyTo(dst.Tag, true);
  252. dst.Tag.Pictures = src.Tag.Pictures;
  253. src.Dispose();
  254. dst.Save();
  255. dst.Dispose();
  256. Console.WriteLine("Tag Copied.");
  257. }
  258. private static void EmbCover(string[] args, bool overwrite)
  259. {
  260. if (args.Length < 2) throw new ArgumentException("args least need 2");
  261. var pathIn = args[0];
  262. var pathCover = args[1];
  263. var pathOutput = args.Length > 2 ? args[2] : args[0];
  264. var mimeType = args.Length > 3 ? args[4] : MediaTypeNames.Image.Jpeg;
  265. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
  266. if (false == File.Exists(pathCover)) throw new FileNotFoundException($"coverPath `{pathCover}' not found.");
  267. Console.WriteLine($"Processing {pathIn}...");
  268. Console.WriteLine($"Comver image:{pathCover}");
  269. if (pathIn != pathOutput) File.Copy(pathIn, pathOutput, true);
  270. if (EmbedCoverIntoFile(pathOutput, pathCover, mimeType, overwrite))
  271. {
  272. // 12345678901234
  273. Console.WriteLine($"Saved to: {pathOutput}");
  274. }
  275. else
  276. {
  277. Console.WriteLine("Skipped.");
  278. }
  279. }
  280. private static void EmbLrc(string[] args)
  281. {
  282. if (args.Length < 3) throw new ArgumentException("args need 2");
  283. var pathIn = args[0];
  284. var pathLrc = args[1];
  285. var pathOutput = args[2];
  286. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
  287. if (false == File.Exists(pathLrc)) throw new FileNotFoundException($"coverPath `{pathLrc}' not found.");
  288. Console.WriteLine($"Processing {pathIn}...");
  289. Console.WriteLine($"Comver image:{pathLrc}");
  290. if (pathIn != pathOutput) File.Copy(pathIn, pathOutput);
  291. EmbedLrcIntoFile(pathOutput, pathLrc);
  292. // 12345678901234
  293. Console.WriteLine($"Saved to: {pathOutput}");
  294. }
  295. private static void EmbJpegCoverDir(string[] args, string pattern)
  296. {
  297. if (args.Length != 2) throw new ArgumentException("args need 2");
  298. var dirIn = args[0];
  299. var dirOut = args[1];
  300. if (false == Directory.Exists(dirIn)) throw new DirectoryNotFoundException($"inputDir `{dirIn}' not found.");
  301. if (false == Directory.Exists(dirOut)) throw new DirectoryNotFoundException($"outputDir `{dirOut} not found.'");
  302. Console.WriteLine($"Enter directory:{dirIn}");
  303. var files = Directory.GetFiles(dirIn, pattern);
  304. if (0 == files.Length)
  305. {
  306. Console.WriteLine($"Not result for {pattern}");
  307. }
  308. else
  309. {
  310. foreach (var file in files)
  311. {
  312. // 12345678901234
  313. Console.WriteLine($"Processing {file}...");
  314. var imgFilePath = Path.ChangeExtension(file, ".jpg");
  315. if (false == File.Exists(imgFilePath)) imgFilePath = Path.ChangeExtension(file, ".jpeg");
  316. if (false == File.Exists(imgFilePath))
  317. {
  318. Console.WriteLine("Cover image file not found, Skipped. ***");
  319. continue;
  320. }
  321. // 12345678901234
  322. Console.WriteLine($"Comver image:{imgFilePath}");
  323. var destFileName = Path.Combine(dirOut, Path.GetFileName(file));
  324. File.Copy(file, destFileName);
  325. EmbedCoverIntoFile(destFileName, imgFilePath, MediaTypeNames.Image.Jpeg);
  326. // 12345678901234
  327. Console.WriteLine($"Saved to: {destFileName}");
  328. }
  329. }
  330. Console.WriteLine($"Leave directory:{dirIn}");
  331. }
  332. private static bool EmbedCoverIntoFile(string filePath, string coverPath, string mimeType, bool overwrite = true)
  333. {
  334. using var mediaFile = TagLib.File.Create(filePath);
  335. if (!overwrite)
  336. {
  337. if (mediaFile.Tag.Pictures.Any(p => p.Type == PictureType.FrontCover)) return false;
  338. }
  339. var pic = new Picture //REF: stackoverflow.com/a/30285220/2430943
  340. {
  341. Type = PictureType.FrontCover,
  342. Description = "Cover",
  343. MimeType = mimeType,
  344. Data = File.ReadAllBytes(coverPath)
  345. };
  346. mediaFile.Tag.Pictures = new IPicture[] { pic };
  347. mediaFile.Save();
  348. return true;
  349. }
  350. private static void EmbedLrcIntoFile(string filePath, string lrcPath)
  351. {
  352. var mediaFile = TagLib.File.Create(filePath);
  353. mediaFile.Tag.Lyrics = File.ReadAllText(lrcPath, Encoding.UTF8);
  354. mediaFile.Save();
  355. }
  356. }
  357. }