Program.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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 = "convert_lib_subset_to_mp4_in_dir";
  49. private const string OP_SCAN_LIB_NO_COVER = "scan_lib_no_cover";
  50. private static void RealMain(string[] args)
  51. {
  52. if (0 == args.Length)
  53. {
  54. Console.WriteLine($"Usage: {Path.GetFileName(AppDomain.CurrentDomain.FriendlyName)} <op> [args...]");
  55. Console.WriteLine($"Prefix ops:");
  56. Console.WriteLine($" * --no-pause");
  57. Console.WriteLine($" Disable enter key to exit");
  58. Console.WriteLine($"Available ops:");
  59. Console.WriteLine($" * {OP_COPY} <sourcePath> <targetPath>");
  60. Console.WriteLine($" Copy tags from source to target (replace)");
  61. Console.WriteLine($" Note: Only standard tags and front cover");
  62. Console.WriteLine($" * {OP_EMB_LRC} <inputPath> <lrcPath> <outputPath>");
  63. Console.WriteLine($" Embed lyrics into file(utf8)");
  64. Console.WriteLine($" * {OP_EMB_COVER}|{OP_EMB_COVER_IF_NOT_EMBEDDED} <inputPath> <coverPath> [outputPath] [mime-type]");
  65. Console.WriteLine($" Embed cover into file, default mine-type is {MediaTypeNames.Image.Jpeg}");
  66. Console.WriteLine($" * {OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG} <inputPath>");
  67. Console.WriteLine($" Embed cover into file if not embedded, same folder cover.jpg");
  68. Console.WriteLine($" * {OP_DIR_EMB_COVER_MP3} <inputDir> <outputDir>");
  69. Console.WriteLine($" Lookup dir, embed same file name jpg/jpeg into mp3");
  70. Console.WriteLine($" Embed abc.jpg or abc.jpeg into abc.mp3 and save a copy to output dir.");
  71. Console.WriteLine($" * {OP_DIR_EMB_COVER_M4A} <inputDir> <outputDir>");
  72. Console.WriteLine($" Lookup dir, embed same file name jpg/jpeg into m4a");
  73. Console.WriteLine($" Embed abc.jpg or abc.jpeg into abc.m4a and save a copy to output dir.");
  74. Console.WriteLine($" * {OP_CONVERT_FLAC_TO_AAC} <inputPath> <outputPath> [q value]");
  75. Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
  76. Console.WriteLine($" * {OP_CONVERT_LIB_FLAC_TO_AAC} <inputDir> <outputDir> [q value]");
  77. Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
  78. Console.WriteLine($" inputDir/Album/Track.flac -> outputDir/Album/Track.m4a");
  79. Console.WriteLine($" Also copy inputDir/Album/cover.jpg to outDir/Album/cover.jpg if found");
  80. Console.WriteLine($" * {OP_CONVERT_LIB_FLAC_TO_AAC_IN_DIR} <libDir> [q value]");
  81. Console.WriteLine($" Convert to aac and copy tags to output, default q is 1.0");
  82. Console.WriteLine($" libDir/Album/Track.flac -> libDir/Album/AAC_Q1.00/Track.m4a");
  83. Console.WriteLine($" * {OP_CONVERT_LIB_SUBSET_TO_MP4_IN_DIR} <libDir> [subset]");
  84. Console.WriteLine($" Convert to mp4(static cover) and copy tags to output, default subset is AAC_Q1.00");
  85. Console.WriteLine($" libDir/Album/subset/Track.m4a -> libDir/Album/subset_MP4/Track.mp4");
  86. Console.WriteLine($" * {OP_SCAN_LIB_NO_COVER} <libDir> [pattern] [...]");
  87. Console.WriteLine($" Scan files in lib find no cover, pattern default *.flac");
  88. Console.WriteLine($"");
  89. Console.WriteLine($" + More on demand");
  90. return;
  91. }
  92. var op = args[0];
  93. Console.WriteLine($"OP: {op}");
  94. var argsToOp = args.Skip(1).ToArray();
  95. switch (op.ToLower())
  96. {
  97. default: throw new ArgumentException($"No recognizable op:{op}");
  98. case OP_DIR_EMB_COVER_MP3: EmbJpegCoverDir(argsToOp, "*.mp3"); break;
  99. case OP_DIR_EMB_COVER_M4A: EmbJpegCoverDir(argsToOp, "*.m4a"); break;
  100. case OP_EMB_COVER: EmbCover(argsToOp, true); break;
  101. case OP_EMB_COVER_IF_NOT_EMBEDDED: EmbCover(argsToOp, false); break;
  102. case OP_EMB_LRC: EmbLrc(argsToOp); break;
  103. case OP_COPY: CopyTag(argsToOp); break;
  104. case OP_EMB_COVER_IF_NOT_EMBEDDED_COVER_DOT_JPG:
  105. if (argsToOp.Length != 1) throw new ArgumentException("args need 1");
  106. // ReSharper disable once AssignNullToNotNullAttribute
  107. var argsMod = new[] { argsToOp[0], Path.Combine(Path.GetDirectoryName(argsToOp[0]), "cover.jpg") };
  108. EmbCover(argsMod, false);
  109. break;
  110. case OP_CONVERT_FLAC_TO_AAC: ConvertFlacToAac(argsToOp); break;
  111. case OP_CONVERT_LIB_FLAC_TO_AAC: ConvertLibFlacToAac(argsToOp); break;
  112. case OP_CONVERT_LIB_FLAC_TO_AAC_IN_DIR: ConvertLibFlacToAacInDir(argsToOp); break;
  113. case OP_CONVERT_LIB_SUBSET_TO_MP4_IN_DIR: ConvertLibSubsetToMp4InDir(argsToOp); break;
  114. case OP_SCAN_LIB_NO_COVER: ScanLibNoCover(argsToOp); break;
  115. }
  116. }
  117. // impl
  118. private static void ScanLibNoCover(string[] args)
  119. {
  120. if (args.Length < 1) throw new ArgumentException("args least need 1");
  121. var libDir = args[0];
  122. var patterns = args.Length > 1
  123. ? args.Skip(1).ToArray()
  124. : new[] { "*.flac" };
  125. if (false == Directory.Exists(libDir)) throw new DirectoryNotFoundException($"lib dir `{libDir}' not found.");
  126. foreach (var discDir in Directory.GetDirectories(libDir))
  127. {
  128. var trackFiles = Microsoft.VisualBasic.FileIO.FileSystem.GetFiles(discDir, Microsoft.VisualBasic.FileIO.SearchOption.SearchTopLevelOnly, patterns);
  129. foreach (var file in trackFiles)
  130. {
  131. using var t = TagLib.File.Create(file);
  132. var hasCover = t.Tag.Pictures.Any(p => p.Type == PictureType.FrontCover);
  133. if (!hasCover) Console.WriteLine(file);
  134. }
  135. }
  136. }
  137. private static void ConvertLibSubsetToMp4InDir(string[] args)
  138. {
  139. if (args.Length < 1) throw new ArgumentException("args least need 1");
  140. var libDir = args[0]; var subset = args.Length > 1 ? args[1] : "AAC_Q1.00";
  141. if (false == Directory.Exists(libDir)) throw new DirectoryNotFoundException($"lib dir `{libDir}' not found.");
  142. foreach (var discDir in Directory.GetDirectories(libDir))
  143. {
  144. var subsetDir = Path.Combine(discDir, subset); if (false == Directory.Exists(subsetDir)) continue;
  145. foreach (var track in Directory.GetFiles(subsetDir))
  146. {
  147. var outDir = Path.Combine(discDir, subset + "_MP4");
  148. var outFile = Path.Combine(outDir, Path.GetFileNameWithoutExtension(track) + ".MP4");
  149. if (File.Exists(outFile)) { Console.WriteLine($"SKIP: exist. `{outFile}'"); continue; }
  150. byte[] coverBytes;
  151. try
  152. {
  153. using var tag = TagLib.File.Create(track);
  154. coverBytes = tag.Tag.Pictures.Where(p => p.Type == PictureType.FrontCover).Select(p => p.Data.ToArray()).FirstOrDefault();
  155. }
  156. catch (Exception e) { Console.WriteLine($"ERR: {e.Message} in `{track}'"); continue; }
  157. if (coverBytes == null) { Console.WriteLine($"WARN: skip. no cover in `{track}'"); continue; }
  158. var tmp = Path.GetTempFileName();
  159. File.WriteAllBytes(tmp, coverBytes);
  160. if (Directory.Exists(outDir) == false) { Console.WriteLine($"Create dir `{outDir}'"); Directory.CreateDirectory(outDir); }
  161. Console.WriteLine($"PROCESS: `{track}'");
  162. var ffMpegArgs = new string[]
  163. {
  164. "-n","-hide_banner","-stats","-v","warning",
  165. "-framerate","60",
  166. "-loop","1","-i",tmp,
  167. "-i",track,
  168. "-map_metadata","-1","-map_chapters","-1",
  169. "-shortest",
  170. "-vf","scale=-1:480,pad=ceil(iw/2)*2:ceil(ih/2)*2",
  171. "-c:v","h264","-pix_fmt","yuv420p",
  172. "-preset","placebo",
  173. "-crf","28","-bf","1250","-g","1250","-keyint_min","250",
  174. "-c:a","copy",
  175. outFile
  176. };
  177. var process = new Process
  178. {
  179. StartInfo =
  180. {
  181. FileName = "ffmpeg",
  182. CreateNoWindow = false,
  183. UseShellExecute = false,
  184. }
  185. };
  186. foreach (var item in ffMpegArgs) process.StartInfo.ArgumentList.Add(item);
  187. process.Start();
  188. process.PriorityClass = ProcessPriorityClass.BelowNormal;
  189. process.WaitForExit();
  190. File.Delete(tmp);//delete temp cover file
  191. if (process.ExitCode == 0) CopyTag(new[] { track, outFile }, false);
  192. }
  193. }
  194. }
  195. private static void ConvertLibFlacToAacInDir(string[] args)
  196. {
  197. if (args.Length < 1) throw new ArgumentException("args least need 1");
  198. var libDir = args[0];
  199. if (false == Directory.Exists(libDir)) throw new DirectoryNotFoundException($"lib dir `{libDir}' not found.");
  200. var q = args.Length > 1 && float.TryParse(args[1], out var tq) && tq is >= 0 and <= 1
  201. ? tq
  202. : 1.0;
  203. foreach (var albDir in Directory.GetDirectories(libDir))
  204. {
  205. var outAlbDir = Path.Combine(albDir, $"AAC_Q{q:N2}");
  206. foreach (var flacPath in Directory.GetFiles(albDir, "*.flac"))
  207. {
  208. if (false == Directory.Exists(outAlbDir))
  209. {
  210. Console.WriteLine($"Create dir:{outAlbDir}");
  211. Directory.CreateDirectory(outAlbDir);
  212. }
  213. var outM4APath = Path.Combine(outAlbDir, Path.ChangeExtension(Path.GetFileName(flacPath), ".m4a"));
  214. Console.WriteLine(
  215. ConvertFlacToAac(new[] { flacPath, outM4APath, $"{q}" })
  216. ? $"Convert OK. -> {outM4APath}"
  217. : $"Exist, Skipped. {outM4APath}"
  218. );
  219. }
  220. }
  221. }
  222. private static void ConvertLibFlacToAac(string[] args)
  223. {
  224. if (args.Length < 2) throw new ArgumentException("args least need 2");
  225. var inDir = args[0];
  226. var outDir = args[1];
  227. if (false == Directory.Exists(inDir)) throw new DirectoryNotFoundException($"input dir `{inDir}' not found.");
  228. if (false == Directory.Exists(outDir)) throw new DirectoryNotFoundException($"output dir `{inDir}' not found.");
  229. var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
  230. ? tq
  231. : 1.0;
  232. foreach (var albDir in Directory.GetDirectories(inDir))
  233. {
  234. var albDirName = Path.GetFileName(albDir);
  235. var outAlbDir = Path.Combine(outDir, albDirName);
  236. if (false == Directory.Exists(outAlbDir))
  237. {
  238. Console.WriteLine($"Create dir:{outAlbDir}");
  239. Directory.CreateDirectory(outAlbDir);
  240. }
  241. var coverPath = Path.Combine(albDir, "cover.jpg");
  242. var destCoverPath = Path.Combine(outAlbDir, "cover.jpg");
  243. if (File.Exists(coverPath) && false == File.Exists(destCoverPath))
  244. {
  245. Console.WriteLine($"Copy {coverPath}");
  246. Console.WriteLine($" -> {destCoverPath} ");
  247. File.Copy(coverPath, destCoverPath);
  248. }
  249. foreach (var flacPath in Directory.GetFiles(albDir, "*.flac"))
  250. {
  251. var outM4APath = Path.Combine(outAlbDir, Path.ChangeExtension(Path.GetFileName(flacPath), ".m4a"));
  252. if (ConvertFlacToAac(new[] { flacPath, outM4APath, $"{q}" }))
  253. {
  254. Console.WriteLine($"Convert OK. -> {outM4APath}");
  255. }
  256. else
  257. {
  258. Console.WriteLine($"Exist, Skipped. {outM4APath}");
  259. }
  260. }
  261. }
  262. }
  263. private static bool ConvertFlacToAac(string[] args)
  264. {
  265. if (args.Length < 2) throw new ArgumentException("args least need 2");
  266. var pathIn = args[0];
  267. var pathOutput = args[1];
  268. var q = args.Length > 2 && float.TryParse(args[2], out var tq) && tq is >= 0 and <= 1
  269. ? tq
  270. : 1.0;
  271. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
  272. if (File.Exists(pathOutput)) return false;
  273. if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
  274. Console.WriteLine($"Processing {pathIn}...");
  275. var flacProcess = new Process
  276. {
  277. StartInfo =
  278. {
  279. FileName = "FLAC",
  280. Arguments = $"-s -c -d \"{pathIn}\"",
  281. RedirectStandardOutput = true,
  282. UseShellExecute = false,
  283. }
  284. };
  285. var aacProcess = new Process
  286. {
  287. StartInfo =
  288. {
  289. FileName = "NeroAacEnc",
  290. Arguments = $"-if - -q {q} -of \"{pathOutput}\"",
  291. RedirectStandardInput = true,
  292. UseShellExecute = false,
  293. }
  294. };
  295. flacProcess.Start();
  296. aacProcess.Start();
  297. var pipe = Task.Factory.StartNew(() =>
  298. {
  299. try
  300. {
  301. flacProcess.StandardOutput.BaseStream.CopyTo(aacProcess.StandardInput.BaseStream, 1024 * 512);
  302. }
  303. catch
  304. {
  305. //EAT ERROR
  306. }
  307. try
  308. {
  309. aacProcess.StandardInput.BaseStream.Close();
  310. }
  311. catch
  312. {
  313. //EAT ERROR
  314. }
  315. });
  316. Task.WaitAll(pipe);
  317. flacProcess.WaitForExit();
  318. aacProcess.WaitForExit();
  319. CopyTag(args);
  320. return true;
  321. }
  322. private static void CopyTag(string[] args, bool cover = true)
  323. {
  324. if (args.Length < 2) throw new ArgumentException("args need 2");
  325. var pathIn = args[0];
  326. var pathOutput = args[1];
  327. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"sourcePath `{pathIn}' not found.");
  328. if (false == Directory.Exists(Path.GetDirectoryName(pathOutput))) throw new DirectoryNotFoundException($"targetPath dir not found.");
  329. if (false == File.Exists(pathOutput)) throw new FileNotFoundException($"targetPath `{pathOutput}' not found.");
  330. using var src = TagLib.File.Create(pathIn);
  331. var dst = TagLib.File.Create(pathOutput);
  332. dst.Tag.Clear();
  333. dst.Save();
  334. dst.Dispose();
  335. dst = TagLib.File.Create(pathOutput);
  336. src.Tag.CopyTo(dst.Tag, true);
  337. if (cover) dst.Tag.Pictures = src.Tag.Pictures;
  338. src.Dispose();
  339. dst.Save();
  340. dst.Dispose();
  341. Console.WriteLine("Tag Copied.");
  342. }
  343. private static void EmbCover(string[] args, bool overwrite)
  344. {
  345. if (args.Length < 2) throw new ArgumentException("args least need 2");
  346. var pathIn = args[0];
  347. var pathCover = args[1];
  348. var pathOutput = args.Length > 2 ? args[2] : args[0];
  349. var mimeType = args.Length > 3 ? args[4] : MediaTypeNames.Image.Jpeg;
  350. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
  351. if (false == File.Exists(pathCover)) throw new FileNotFoundException($"coverPath `{pathCover}' not found.");
  352. Console.WriteLine($"Processing {pathIn}...");
  353. Console.WriteLine($"Comver image:{pathCover}");
  354. if (pathIn != pathOutput) File.Copy(pathIn, pathOutput, true);
  355. if (EmbedCoverIntoFile(pathOutput, pathCover, mimeType, overwrite))
  356. {
  357. // 12345678901234
  358. Console.WriteLine($"Saved to: {pathOutput}");
  359. }
  360. else
  361. {
  362. Console.WriteLine("Skipped.");
  363. }
  364. }
  365. private static void EmbLrc(string[] args)
  366. {
  367. if (args.Length < 3) throw new ArgumentException("args need 2");
  368. var pathIn = args[0];
  369. var pathLrc = args[1];
  370. var pathOutput = args[2];
  371. if (false == File.Exists(pathIn)) throw new FileNotFoundException($"inputPath `{pathIn}' not found.");
  372. if (false == File.Exists(pathLrc)) throw new FileNotFoundException($"coverPath `{pathLrc}' not found.");
  373. Console.WriteLine($"Processing {pathIn}...");
  374. Console.WriteLine($"Comver image:{pathLrc}");
  375. if (pathIn != pathOutput) File.Copy(pathIn, pathOutput);
  376. EmbedLrcIntoFile(pathOutput, pathLrc);
  377. // 12345678901234
  378. Console.WriteLine($"Saved to: {pathOutput}");
  379. }
  380. private static void EmbJpegCoverDir(string[] args, string pattern)
  381. {
  382. if (args.Length != 2) throw new ArgumentException("args need 2");
  383. var dirIn = args[0];
  384. var dirOut = args[1];
  385. if (false == Directory.Exists(dirIn)) throw new DirectoryNotFoundException($"inputDir `{dirIn}' not found.");
  386. if (false == Directory.Exists(dirOut)) throw new DirectoryNotFoundException($"outputDir `{dirOut} not found.'");
  387. Console.WriteLine($"Enter directory:{dirIn}");
  388. var files = Directory.GetFiles(dirIn, pattern);
  389. if (0 == files.Length)
  390. {
  391. Console.WriteLine($"Not result for {pattern}");
  392. }
  393. else
  394. {
  395. foreach (var file in files)
  396. {
  397. // 12345678901234
  398. Console.WriteLine($"Processing {file}...");
  399. var imgFilePath = Path.ChangeExtension(file, ".jpg");
  400. if (false == File.Exists(imgFilePath)) imgFilePath = Path.ChangeExtension(file, ".jpeg");
  401. if (false == File.Exists(imgFilePath))
  402. {
  403. Console.WriteLine("Cover image file not found, Skipped. ***");
  404. continue;
  405. }
  406. // 12345678901234
  407. Console.WriteLine($"Comver image:{imgFilePath}");
  408. var destFileName = Path.Combine(dirOut, Path.GetFileName(file));
  409. File.Copy(file, destFileName);
  410. EmbedCoverIntoFile(destFileName, imgFilePath, MediaTypeNames.Image.Jpeg);
  411. // 12345678901234
  412. Console.WriteLine($"Saved to: {destFileName}");
  413. }
  414. }
  415. Console.WriteLine($"Leave directory:{dirIn}");
  416. }
  417. private static bool EmbedCoverIntoFile(string filePath, string coverPath, string mimeType, bool overwrite = true)
  418. {
  419. using var mediaFile = TagLib.File.Create(filePath);
  420. if (!overwrite)
  421. {
  422. if (mediaFile.Tag.Pictures.Any(p => p.Type == PictureType.FrontCover)) return false;
  423. }
  424. var pic = new Picture //REF: stackoverflow.com/a/30285220/2430943
  425. {
  426. Type = PictureType.FrontCover,
  427. Description = "Cover",
  428. MimeType = mimeType,
  429. Data = File.ReadAllBytes(coverPath)
  430. };
  431. mediaFile.Tag.Pictures = new IPicture[] { pic };
  432. mediaFile.Save();
  433. return true;
  434. }
  435. private static void EmbedLrcIntoFile(string filePath, string lrcPath)
  436. {
  437. var mediaFile = TagLib.File.Create(filePath);
  438. mediaFile.Tag.Lyrics = File.ReadAllText(lrcPath, Encoding.UTF8);
  439. mediaFile.Save();
  440. }
  441. }
  442. }