Program.cs 16 KB

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