Program.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net.Mime;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using TagLib;
  9. using File = System.IO.File;
  10. // ReSharper disable InconsistentNaming
  11. namespace BatchProcess
  12. {
  13. internal class Program
  14. {
  15. private static void Main(string[] args)
  16. {
  17. Console.WriteLine("Batch Processor");
  18. Console.WriteLine();
  19. var skipPause = false;
  20. if (args.Length > 0 && args[0].ToLower() == "--no-pause")
  21. {
  22. skipPause = true;
  23. args = args.Skip(1).ToArray();
  24. }
  25. try
  26. {
  27. RealMain(args);
  28. }
  29. catch (Exception e)
  30. {
  31. Console.WriteLine($"ERROR:{e}");
  32. }
  33. Console.WriteLine();
  34. if (skipPause) return;
  35. Console.Write("Press ENTER to exit...");
  36. Console.ReadLine();
  37. }
  38. private const string OP_DIR_EMB_COVER_MP3 = "embcover_mp3_jpg_dir";
  39. private const string OP_DIR_EMB_COVER_M4A = "embcover_m4a_jpg_dir";
  40. private const string OP_EMB_COVER = "embcover";
  41. private const string OP_COPY = "copy";
  42. private const string OP_EMB_COVER_IF_NOT_EMBEDDED = "embcover_if_not_embedded";
  43. private const string OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG = "embcover_if_not_embedded_cover_jpg";
  44. private const string OP_EMB_LRC = "emblrc";
  45. private const string OP_CONVERT_FLAC_TO_AAC = "convert_flac_to_aac";
  46. private const string OP_CONVERT_LIB_FLAC_TO_AAC = "convert_lib_flac_to_aac";
  47. private const string OP_CONVERT_LIB_FLAC_TO_AAC_IN_DIR = "convert_lib_flac_to_aac_in_dir";
  48. private const string OP_CONVERT_LIB_SUBSET_TO_MP4_IN_DIR = "op_convert_lib_subset_to_mp4_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($" * {OP_CONVERT_LIB_SUBSET_TO_MP4_IN_DIR} <libDir> [subset]");
  83. Console.WriteLine($" Convert to mp4(static cover) and copy tags to output, default subset is AAC_Q1.00");
  84. Console.WriteLine($" libDir/Album/subset/Track.m4a -> libDir/Album/subset_MP4/Track.mp4");
  85. Console.WriteLine($"");
  86. Console.WriteLine($" + More on demand");
  87. return;
  88. }
  89. var op = args[0];
  90. Console.WriteLine($"OP: {op}");
  91. var argsToOp = args.Skip(1).ToArray();
  92. switch (op.ToLower())
  93. {
  94. default: throw new ArgumentException($"No recognizable op:{op}");
  95. case OP_DIR_EMB_COVER_MP3: EmbJpegCoverDir(argsToOp, "*.mp3"); break;
  96. case OP_DIR_EMB_COVER_M4A: EmbJpegCoverDir(argsToOp, "*.m4a"); break;
  97. case OP_EMB_COVER: EmbCover(argsToOp, true); break;
  98. case OP_EMB_COVER_IF_NOT_EMBEDDED: EmbCover(argsToOp, false); break;
  99. case OP_EMB_LRC: EmbLrc(argsToOp); break;
  100. case OP_COPY: CopyTag(argsToOp); break;
  101. case OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG:
  102. if (argsToOp.Length != 1) throw new ArgumentException("args need 1");
  103. // ReSharper disable once AssignNullToNotNullAttribute
  104. var argsMod = new[] { argsToOp[0], Path.Combine(Path.GetDirectoryName(argsToOp[0]), "cover.jpg") };
  105. EmbCover(argsMod, false);
  106. break;
  107. case OP_CONVERT_FLAC_TO_AAC: ConvertFlacToAac(argsToOp); break;
  108. case OP_CONVERT_LIB_FLAC_TO_AAC: ConvertLibFlacToAac(argsToOp); break;
  109. case OP_CONVERT_LIB_FLAC_TO_AAC_IN_DIR: ConvertLibFlacToAacInDir(argsToOp); break;
  110. case OP_CONVERT_LIB_SUBSET_TO_MP4_IN_DIR: ConvertLibSubsetToMp4InDir(argsToOp); break;
  111. }
  112. }
  113. // impl
  114. private static void ConvertLibSubsetToMp4InDir(string[] args)
  115. {
  116. if (args.Length < 1) throw new ArgumentException("args least need 1");
  117. var libDir = args[0]; var subset = args.Length > 1 ? args[1] : "AAC_Q1.00";
  118. if (false == Directory.Exists(libDir)) throw new DirectoryNotFoundException($"lib dir `{libDir}' not found.");
  119. foreach (var discDir in Directory.GetDirectories(libDir))
  120. {
  121. var subsetDir = Path.Combine(discDir, subset); if (false == Directory.Exists(subsetDir)) continue;
  122. foreach (var track in Directory.GetFiles(subsetDir))
  123. {
  124. var outDir = Path.Combine(discDir, subset + "_MP4");
  125. var outFile = Path.Combine(outDir, Path.GetFileNameWithoutExtension(track) + ".MP4");
  126. if (File.Exists(outFile)) { Console.WriteLine($"SKIP: exist. `{outFile}'"); continue; }
  127. byte[] coverBytes;
  128. try
  129. {
  130. using var tag = TagLib.File.Create(track);
  131. coverBytes = tag.Tag.Pictures.Where(p => p.Type == PictureType.FrontCover).Select(p => p.Data.ToArray()).FirstOrDefault();
  132. }
  133. catch (Exception e) { Console.WriteLine($"ERR: {e.Message} in `{track}'"); continue; }
  134. if (coverBytes == null) { Console.WriteLine($"WARN: skip. no cover in `{track}'"); continue; }
  135. var tmp = Path.GetTempFileName();
  136. File.WriteAllBytes(tmp, coverBytes);
  137. if (Directory.Exists(outDir) == false) { Console.WriteLine($"Create dir `{outDir}'"); Directory.CreateDirectory(outDir); }
  138. Console.WriteLine($"PROCESS: `{track}'");
  139. var ffMpegArgs = new string[]
  140. {
  141. "-n","-hide_banner","-stats","-v","warning",
  142. "-framerate","60",
  143. "-loop","1","-i",tmp,
  144. "-i",track,
  145. "-map_metadata","-1","-map_chapters","-1",
  146. "-shortest",
  147. "-vf","scale=-1:480",
  148. "-vf","pad=ceil(iw/2)*2:ceil(ih/2)*2",
  149. "-c:v","h264","-pix_fmt","yuv420p",
  150. "-preset","placebo",
  151. "-crf","28","-bf","1250","-g","1250","-keyint_min","250",
  152. "-c:a","copy",
  153. outFile
  154. };
  155. var process = new Process
  156. {
  157. StartInfo =
  158. {
  159. FileName = "ffmpeg",
  160. CreateNoWindow = false,
  161. UseShellExecute = false,
  162. }
  163. };
  164. foreach (var item in ffMpegArgs) process.StartInfo.ArgumentList.Add(item);
  165. process.Start();
  166. process.PriorityClass = ProcessPriorityClass.BelowNormal;
  167. process.WaitForExit();
  168. File.Delete(tmp);//delete temp cover file
  169. if (process.ExitCode == 0) CopyTag(new[] { track, outFile }, false);
  170. }
  171. }
  172. }
  173. private static void ConvertLibFlacToAacInDir(string[] args)
  174. {
  175. if (args.Length < 1) throw new ArgumentException("args least need 1");
  176. var libDir = args[0];
  177. if (false == Directory.Exists(libDir)) throw new DirectoryNotFoundException($"lib dir `{libDir}' not found.");
  178. var q = args.Length > 1 && float.TryParse(args[1], out var tq) && tq is >= 0 and <= 1
  179. ? tq
  180. : 1.0;
  181. foreach (var albDir in Directory.GetDirectories(libDir))
  182. {
  183. var outAlbDir = Path.Combine(albDir, $"AAC_Q{q:N2}");
  184. foreach (var flacPath in Directory.GetFiles(albDir, "*.flac"))
  185. {
  186. if (false == Directory.Exists(outAlbDir))
  187. {
  188. Console.WriteLine($"Create dir:{outAlbDir}");
  189. Directory.CreateDirectory(outAlbDir);
  190. }
  191. var outM4APath = Path.Combine(outAlbDir, Path.ChangeExtension(Path.GetFileName(flacPath), ".m4a"));
  192. Console.WriteLine(
  193. ConvertFlacToAac(new[] { flacPath, outM4APath, $"{q}" })
  194. ? $"Convert OK. -> {outM4APath}"
  195. : $"Exist, Skipped. {outM4APath}"
  196. );
  197. }
  198. }
  199. }
  200. private static void ConvertLibFlacToAac(string[] args)
  201. {
  202. if (args.Length < 2) throw new ArgumentException("args least need 2");
  203. var inDir = args[0];
  204. var outDir = args[1];
  205. if (false == Directory.Exists(inDir)) throw new DirectoryNotFoundException($"input dir `{inDir}' not found.");
  206. if (false == Directory.Exists(outDir)) throw new DirectoryNotFoundException($"output dir `{inDir}' not found.");
  207. var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
  208. ? tq
  209. : 1.0;
  210. foreach (var albDir in Directory.GetDirectories(inDir))
  211. {
  212. var albDirName = Path.GetFileName(albDir);
  213. var outAlbDir = Path.Combine(outDir, albDirName);
  214. if (false == Directory.Exists(outAlbDir))
  215. {
  216. Console.WriteLine($"Create dir:{outAlbDir}");
  217. Directory.CreateDirectory(outAlbDir);
  218. }
  219. var coverPath = Path.Combine(albDir, "cover.jpg");
  220. var destCoverPath = Path.Combine(outAlbDir, "cover.jpg");
  221. if (File.Exists(coverPath) && false == File.Exists(destCoverPath))
  222. {
  223. Console.WriteLine($"Copy {coverPath}");
  224. Console.WriteLine($" -> {destCoverPath} ");
  225. File.Copy(coverPath, destCoverPath);
  226. }
  227. foreach (var flacPath in Directory.GetFiles(albDir, "*.flac"))
  228. {
  229. var outM4APath = Path.Combine(outAlbDir, Path.ChangeExtension(Path.GetFileName(flacPath), ".m4a"));
  230. if (ConvertFlacToAac(new[] { flacPath, outM4APath, $"{q}" }))
  231. {
  232. Console.WriteLine($"Convert OK. -> {outM4APath}");
  233. }
  234. else
  235. {
  236. Console.WriteLine($"Exist, Skipped. {outM4APath}");
  237. }
  238. }
  239. }
  240. }
  241. private static bool ConvertFlacToAac(string[] args)
  242. {
  243. if (args.Length < 2) throw new ArgumentException("args least need 2");
  244. var pathIn = args[0];
  245. var pathOutput = args[1];
  246. var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
  247. ? tq
  248. : 1.0;
  249. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
  250. if (File.Exists(pathOutput)) return false;
  251. if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
  252. Console.WriteLine($"Processing {pathIn}...");
  253. var flacProcess = new Process
  254. {
  255. StartInfo =
  256. {
  257. FileName = "FLAC",
  258. Arguments = $"-s -c -d \"{pathIn}\"",
  259. RedirectStandardOutput = true,
  260. UseShellExecute = false,
  261. }
  262. };
  263. var aacProcess = new Process
  264. {
  265. StartInfo =
  266. {
  267. FileName = "NeroAacEnc",
  268. Arguments = $"-if - -q {q} -of \"{pathOutput}\"",
  269. RedirectStandardInput = true,
  270. UseShellExecute = false,
  271. }
  272. };
  273. flacProcess.Start();
  274. aacProcess.Start();
  275. var pipe = Task.Factory.StartNew(() =>
  276. {
  277. try
  278. {
  279. flacProcess.StandardOutput.BaseStream.CopyTo(aacProcess.StandardInput.BaseStream, 1024 * 512);
  280. }
  281. catch
  282. {
  283. //EAT ERROR
  284. }
  285. try
  286. {
  287. aacProcess.StandardInput.BaseStream.Close();
  288. }
  289. catch
  290. {
  291. //EAT ERROR
  292. }
  293. });
  294. Task.WaitAll(pipe);
  295. flacProcess.WaitForExit();
  296. aacProcess.WaitForExit();
  297. CopyTag(args);
  298. return true;
  299. }
  300. private static void CopyTag(string[] args, bool cover = true)
  301. {
  302. if (args.Length < 2) throw new ArgumentException("args need 2");
  303. var pathIn = args[0];
  304. var pathOutput = args[1];
  305. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
  306. if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
  307. if (false == File.Exists(pathOutput)) throw new FileNotFoundException($"targetPath `{pathOutput}' not found.");
  308. using var src = TagLib.File.Create(pathIn);
  309. var dst = TagLib.File.Create(pathOutput);
  310. dst.Tag.Clear();
  311. dst.Save();
  312. dst.Dispose();
  313. dst = TagLib.File.Create(pathOutput);
  314. src.Tag.CopyTo(dst.Tag, true);
  315. if (cover) dst.Tag.Pictures = src.Tag.Pictures;
  316. src.Dispose();
  317. dst.Save();
  318. dst.Dispose();
  319. Console.WriteLine("Tag Copied.");
  320. }
  321. private static void EmbCover(string[] args, bool overwrite)
  322. {
  323. if (args.Length < 2) throw new ArgumentException("args least need 2");
  324. var pathIn = args[0];
  325. var pathCover = args[1];
  326. var pathOutput = args.Length > 2 ? args[2] : args[0];
  327. var mimeType = args.Length > 3 ? args[4] : MediaTypeNames.Image.Jpeg;
  328. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
  329. if (false == File.Exists(pathCover)) throw new FileNotFoundException($"coverPath `{pathCover}' not found.");
  330. Console.WriteLine($"Processing {pathIn}...");
  331. Console.WriteLine($"Comver image:{pathCover}");
  332. if (pathIn != pathOutput) File.Copy(pathIn, pathOutput, true);
  333. if (EmbedCoverIntoFile(pathOutput, pathCover, mimeType, overwrite))
  334. {
  335. // 12345678901234
  336. Console.WriteLine($"Saved to: {pathOutput}");
  337. }
  338. else
  339. {
  340. Console.WriteLine("Skipped.");
  341. }
  342. }
  343. private static void EmbLrc(string[] args)
  344. {
  345. if (args.Length < 3) throw new ArgumentException("args need 2");
  346. var pathIn = args[0];
  347. var pathLrc = args[1];
  348. var pathOutput = args[2];
  349. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
  350. if (false == File.Exists(pathLrc)) throw new FileNotFoundException($"coverPath `{pathLrc}' not found.");
  351. Console.WriteLine($"Processing {pathIn}...");
  352. Console.WriteLine($"Comver image:{pathLrc}");
  353. if (pathIn != pathOutput) File.Copy(pathIn, pathOutput);
  354. EmbedLrcIntoFile(pathOutput, pathLrc);
  355. // 12345678901234
  356. Console.WriteLine($"Saved to: {pathOutput}");
  357. }
  358. private static void EmbJpegCoverDir(string[] args, string pattern)
  359. {
  360. if (args.Length != 2) throw new ArgumentException("args need 2");
  361. var dirIn = args[0];
  362. var dirOut = args[1];
  363. if (false == Directory.Exists(dirIn)) throw new DirectoryNotFoundException($"inputDir `{dirIn}' not found.");
  364. if (false == Directory.Exists(dirOut)) throw new DirectoryNotFoundException($"outputDir `{dirOut} not found.'");
  365. Console.WriteLine($"Enter directory:{dirIn}");
  366. var files = Directory.GetFiles(dirIn, pattern);
  367. if (0 == files.Length)
  368. {
  369. Console.WriteLine($"Not result for {pattern}");
  370. }
  371. else
  372. {
  373. foreach (var file in files)
  374. {
  375. // 12345678901234
  376. Console.WriteLine($"Processing {file}...");
  377. var imgFilePath = Path.ChangeExtension(file, ".jpg");
  378. if (false == File.Exists(imgFilePath)) imgFilePath = Path.ChangeExtension(file, ".jpeg");
  379. if (false == File.Exists(imgFilePath))
  380. {
  381. Console.WriteLine("Cover image file not found, Skipped. ***");
  382. continue;
  383. }
  384. // 12345678901234
  385. Console.WriteLine($"Comver image:{imgFilePath}");
  386. var destFileName = Path.Combine(dirOut, Path.GetFileName(file));
  387. File.Copy(file, destFileName);
  388. EmbedCoverIntoFile(destFileName, imgFilePath, MediaTypeNames.Image.Jpeg);
  389. // 12345678901234
  390. Console.WriteLine($"Saved to: {destFileName}");
  391. }
  392. }
  393. Console.WriteLine($"Leave directory:{dirIn}");
  394. }
  395. private static bool EmbedCoverIntoFile(string filePath, string coverPath, string mimeType, bool overwrite = true)
  396. {
  397. using var mediaFile = TagLib.File.Create(filePath);
  398. if (!overwrite)
  399. {
  400. if (mediaFile.Tag.Pictures.Any(p => p.Type == PictureType.FrontCover)) return false;
  401. }
  402. var pic = new Picture //REF: stackoverflow.com/a/30285220/2430943
  403. {
  404. Type = PictureType.FrontCover,
  405. Description = "Cover",
  406. MimeType = mimeType,
  407. Data = File.ReadAllBytes(coverPath)
  408. };
  409. mediaFile.Tag.Pictures = new IPicture[] { pic };
  410. mediaFile.Save();
  411. return true;
  412. }
  413. private static void EmbedLrcIntoFile(string filePath, string lrcPath)
  414. {
  415. var mediaFile = TagLib.File.Create(filePath);
  416. mediaFile.Tag.Lyrics = File.ReadAllText(lrcPath, Encoding.UTF8);
  417. mediaFile.Save();
  418. }
  419. }
  420. }