Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

489 wiersze
15KB

  1. #!/usr/bin/env perl
  2. use strict;
  3. use warnings;
  4. use Getopt::Long;
  5. use File::Find;
  6. use Data::Dumper;
  7. use File::Basename;
  8. use File::Temp qw/ tempfile /;
  9. my $opt_no_genre;
  10. my $opt_comment;
  11. my $opt_catid;
  12. my $opt_rg;
  13. my $opt_embedcover;
  14. # TODO fill this out
  15. my %genreMap = (
  16. edm => 52,
  17. soundtrack => 24,
  18. );
  19. # this is a godsent page
  20. # https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping
  21. # https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
  22. # a lot of this may not work
  23. # TODO escape potential 's
  24. #
  25. # Format is:
  26. # Vorbis tag string => Mp3 tag value
  27. # where mp3 tag value may be:
  28. # undef -> Skip this tag
  29. # A string -> Use this as the mp3 tag, and use the vorbis tag value as value (tags are escaped)
  30. # code -> Execute this function. This should return an array, where [0] is the tag, [1] is the value. (tags are not escaped)
  31. # An array (str, str) -> [0] is the mp3 tag to use, [1] is the value prefix (tags are escaped)
  32. # An array (str, code) -> [0] is the mp3 tag to use, [1] is a function that is executed, and the result is the tag value (tags are not escaped)
  33. # The code-s here will be called with the flac tags hashmap
  34. my %idLookup = (
  35. album => 'TALB',
  36. albumsort => 'TSOA',
  37. discsubtitle => 'TSST',
  38. grouping => 'TIT1',
  39. title => 'TIT2',
  40. titlesort => 'TSOT',
  41. subtitle => 'TIT3',
  42. subtitle => 'TIT3',
  43. albumartist => 'TPE2',
  44. albumartistsort => 'TSO2', # Maybe?
  45. artist => 'TPE1',
  46. artistsort => 'TSOP',
  47. # arranger => ['TIPL', 'arranger:'],
  48. arranger => ['TXXX', 'ARRANGER:'],
  49. author => 'TEXT',
  50. composer => 'TCOM',
  51. conductor => 'TPE3',
  52. engineer => ['TIPL', 'engineer:'],
  53. djmixer => ['TIPL', 'DJ-mix:'],
  54. mixer => ['TIPL', 'mix:'],
  55. # performer => ['TMCL', "instrument:"], # Should be like this, but mid3v2 says it doesn't have this tag.
  56. performer => ['TXXX', "PERFORMER:"],
  57. producer => ['TIPL', 'producer:'],
  58. publisher => 'TPUB',
  59. organization => 'TPUB',
  60. label => 'TPUB',
  61. remixer => 'TPE4',
  62. discnumber => ['TPOS', sub {
  63. my $t = shift;
  64. my $totalkey = exists($t->{disctotal}) ? 'disctotal' : 'totaldiscs';
  65. return "$t->{discnumber}[0]" if !exists($t->{$totalkey});
  66. return "$t->{discnumber}[0]/$t->{$totalkey}[0]";
  67. }],
  68. totaldiscs => undef,
  69. disctotal => undef,
  70. tracknumber => ['TRCK', sub {
  71. my $t = shift;
  72. my $totalkey = exists($t->{tracktotal}) ? 'tracktotal' : 'totaltracks';
  73. return "$t->{tracknumber}[0]" if !exists($t->{$totalkey});
  74. return "$t->{tracknumber}[0]/$t->{$totalkey}[0]";
  75. }],
  76. totaltracks => undef,
  77. tracktotal => undef,
  78. #date => 'TDRC', # This is for id3v2.4
  79. #date => 'TYER',
  80. date => sub {
  81. my $t = shift;
  82. my $date = $t->{date}[0];
  83. if (length($date) == 4 && $date =~ m/\d{4}/) { # Only year
  84. return ["TYER", "$date"];
  85. }
  86. if (!($date =~ m/^\d{4}[\.-]\d{2}[\.-]\d{2}$/)) {
  87. print("Date format unknown: $date\n");
  88. exit 1;
  89. }
  90. $date =~ s/[\.-]/-/g;
  91. return ["TDRL", "$date"]; # Release date
  92. },
  93. originaldate => 'TDOR', # Also for 2.4 only
  94. 'release date' => 'TDOR', # Also for 2.4 only
  95. isrc => 'TSRC',
  96. barcode => ['TXXX', 'BARCODE:'],
  97. catalog => ['TXXX', sub { return "CATALOGNUMBER:" . mp3TagEscapeOwn(tagmap_catalogid(shift, 'catalog')); } ],
  98. catalognumber => ['TXXX', sub { return "CATALOGNUMBER:" . mp3TagEscapeOwn(tagmap_catalogid(shift, 'catalognumber')); } ],
  99. catalogid => ['TXXX', sub { return "CATALOGNUMBER:" . mp3TagEscapeOwn(tagmap_catalogid(shift, 'catalogid')); } ],
  100. labelno => ['TXXX', sub { return "CATALOGNUMBER:" . mp3TagEscapeOwn(tagmap_catalogid(shift, 'labelno')); } ],
  101. #'encoded-by' => 'TENC',
  102. #encoder => 'TSSE',
  103. #encoding => 'TSSE',
  104. #'encoder settings' => 'TSSE',
  105. media => 'TMED',
  106. genre => ['TCON', sub {
  107. return undef if ($opt_no_genre);
  108. my $genreName = shift->{genre}[0];
  109. if (!exists($genreMap{lc($genreName)})) {
  110. # If no genre number exists, use the name
  111. return mp3TagEscapeOwn($genreName);
  112. }
  113. return mp3TagEscapeOwn($genreMap{$genreName});
  114. }],
  115. #mood => ['TMOO', sub {
  116. #}],
  117. bpm => 'TBPM',
  118. comment => ['COMM', sub {
  119. return undef if (defined($opt_comment) && $opt_comment eq "");
  120. # Use the default empty description and default english language
  121. return mp3TagEscapeOwn(shift->{comment}[0]);
  122. }],
  123. copyright => 'TCOP',
  124. language => 'TLAN',
  125. #replaygain_album_peak => 'TXXX=REPLAYGAIN_ALBUM_PEAK',
  126. #replaygain_album_gain => 'TXXX=REPLAYGAIN_ALBUM_GAIN',
  127. replaygain_track_gain => sub {
  128. print("EEEEEEERRRRRRRRROOOOOOOOOORRRRRRRRRRRE FIXXXXXXXXXXXX THIIIIIIIIISSSSSSSS\n");
  129. exit(1);
  130. return undef if (!$opt_rg);
  131. shift->{replaygain_track_gain}[0] =~ /^(-?\d+\.\d+) dB$/;
  132. my $gain_db = $1;
  133. exit(1) if ($gain_db eq "");
  134. return "--replaygain-accurate --gain $gain_db";
  135. # TODO this lulw
  136. },
  137. #replaygain_album_gain => 'TXXX=REPLAYGAIN_ALBUM_GAIN',
  138. #replaygain_album_peak => 'TXXX=REPLAYGAIN_ALBUM_PEAK',
  139. #replaygain_track_gain => 'TXXX=REPLAYGAIN_TRACK_GAIN',
  140. #replaygain_track_peak => 'TXXX=REPLAYGAIN_TRACK_PEAK',
  141. script => ['TXXX', 'SCRIPT:'],
  142. lyrics => 'USLT',
  143. lyricist => 'TEXT',
  144. circle => ['TXXX', 'CIRCLE:'],
  145. event => ['TXXX', 'EVENT:'],
  146. discid => ['TXXX', 'DISCID:'],
  147. originaltitle => ['TXXX', 'ORIGINALTITLE:'],
  148. origin => ['TXXX', 'ORIGIN:'],
  149. origintype => ['TXXX', 'ORIGINTYPE:'],
  150. );
  151. sub tagmap_catalogid {
  152. my $t = shift;
  153. my $own_tag_name = shift;
  154. return undef if (defined($opt_catid) && $opt_catid eq "");
  155. return $t->{$own_tag_name}[0];
  156. }
  157. my $opt_genre;
  158. my $opt_help;
  159. my @opt_tagreplace;
  160. my $opt_cbr = 0;
  161. GetOptions(
  162. "genre|g=s" => \$opt_genre,
  163. "no-genre|G" => \$opt_no_genre,
  164. "replay-gain|r" => \$opt_rg,
  165. "help|h" => \$opt_help,
  166. "catid=s" => \$opt_catid,
  167. "comment=s" => \$opt_comment,
  168. "cover=s" => \$opt_embedcover,
  169. "tagreplace|t=s" => \@opt_tagreplace,
  170. "320|3" => \$opt_cbr,
  171. ) or die("Error in command line option");
  172. if ($opt_help) {
  173. help();
  174. }
  175. if (scalar(@ARGV) != 2) {
  176. print("Bad arguments\n");
  177. usage();
  178. }
  179. my ($IDIR, $ODIR) = @ARGV;
  180. if (!-e $ODIR) {
  181. mkdir $ODIR;
  182. }
  183. find({ wanted => \&iterFlac, no_chdir => 1 }, $IDIR);
  184. sub iterFlac {
  185. # Return if file is not a file, or if it's not a flac
  186. return if (!-f || !/\.flac$/);
  187. my @required_tags = ("artist", "title", "album", "tracknumber");
  188. my $flacDir = substr($File::Find::name, length($IDIR));
  189. my $flac = $_;
  190. my $flac_o = $flac;
  191. shellsan(\$flac);
  192. my $dest = "$ODIR/" . $flacDir;
  193. #print("DEBUG: $dest\n");
  194. $dest =~ s/\.flac$/\.mp3/;
  195. my $tags = getFlacTags($flac);
  196. my $has_req_tags = 1;
  197. foreach (@required_tags) {
  198. if (!exists($tags->{lc($_)})) {
  199. $has_req_tags = 0;
  200. last;
  201. }
  202. }
  203. if (!$has_req_tags) {
  204. print("WARNING: File: '$flac' does not have all the required tags. Skipping\n");
  205. exit(1);
  206. return;
  207. }
  208. argsToTags($tags, $flac_o);
  209. #foreach (%$tags) {
  210. #print("Copying tag '$_->[0]=$_->[1]'\n");
  211. #}
  212. my $tagopts = tagsToOpts($tags);
  213. #print("Debug: @$tagopts\n");
  214. shellsan(\$dest);
  215. my $cmd;
  216. if ($opt_cbr) {
  217. $cmd = "flac -cd -- '$flac' | lame -S -b 320 -q 0 --add-id3v2 - '$dest'";
  218. } else {
  219. $cmd = "flac -cd -- '$flac' | lame -S -V0 --vbr-new -q 0 --add-id3v2 - '$dest'";
  220. }
  221. #print("Debug - CMD: [$cmd]\n");
  222. qx($cmd);
  223. if ($? != 0) {
  224. exit(1);
  225. }
  226. my $mid3v2TagLine = "";
  227. #print(Dumper(\@$tagopts));
  228. # Add tags with mid3v2 instead of lame to better support multiple tag values
  229. foreach my $tagItem (@$tagopts) {
  230. $mid3v2TagLine = $mid3v2TagLine . $tagItem . " ";
  231. }
  232. #print(Dumper(\$mid3v2TagLine));
  233. my $mid3v2TagCmd = "mid3v2 -e $mid3v2TagLine -- '$dest'";
  234. #print("Mid3V2 Debug - CMD: [$mid3v2TagCmd]\n");
  235. qx($mid3v2TagCmd);
  236. if ($? != 0) {
  237. print("ERROR: At mid3v2 tag set\n");
  238. exit(1);
  239. }
  240. embedImageFromFlac($flac, $dest);
  241. }
  242. sub getMimeType {
  243. my $file = shift;
  244. shellsan(\$file);
  245. my $mime = qx(file -b --mime-type '$file');
  246. chomp($mime);
  247. return $mime;
  248. }
  249. sub embedImageFromFlac {
  250. my $flac = shift;
  251. my $mp3 = shift;
  252. if (defined($opt_embedcover)) {
  253. return if ($opt_embedcover eq "");
  254. my $cmime = getMimeType($opt_embedcover);
  255. qx(mid3v2 -p '${opt_embedcover}:cover:3:$cmime' -- '$mp3');
  256. return;
  257. }
  258. # I can't get the automatic deletion working :c
  259. my (undef, $fname) = tempfile();
  260. # Export image from flac
  261. qx(metaflac --export-picture-to='$fname' -- '$flac');
  262. if ($? != 0) {
  263. # Probably no image
  264. unlink($fname);
  265. return;
  266. }
  267. # Extract mime type too
  268. my $pinfo = qx(metaflac --list --block-type=PICTURE -- '$flac');
  269. $pinfo =~ m/MIME type: (.*)/;
  270. my $mimeType = $1;
  271. # Add image to mp3
  272. qx(mid3v2 -p '${fname}:cover:3:$mimeType' -- '$mp3');
  273. unlink($fname);
  274. }
  275. sub argsToTags {
  276. my $argTags = shift;
  277. my $fname = shift;
  278. $fname =~ s!^.*/!!;
  279. if (defined($opt_genre)) {
  280. $argTags->{genre} = [$opt_genre];
  281. }
  282. if (defined($opt_comment)) {
  283. if ($opt_comment eq "") {
  284. delete($argTags->{comment});
  285. } else {
  286. $argTags->{comment} = [$opt_comment];
  287. }
  288. }
  289. if (defined($opt_catid) && $opt_catid ne "") {
  290. $argTags->{catalognumber} = [$opt_catid];
  291. }
  292. if (scalar @opt_tagreplace > 0) {
  293. foreach my $trepl (@opt_tagreplace) {
  294. $trepl =~ m!(.*?)/(.*?)=(.*)!;
  295. my ($freg, $tag, $tagval) = ($1, $2, $3);
  296. if ($fname =~ m!$freg!) {
  297. $argTags->{lc($tag)} = ($tagval);
  298. }
  299. }
  300. }
  301. }
  302. sub mergeDupeTxxx {
  303. # Merge tags together, that we can't have multiples of
  304. # Like TXXX with same key
  305. my $tagsArr = shift;
  306. for (my $i = 0; $i < scalar @$tagsArr; $i++) {
  307. if (lc($tagsArr->[$i]->[0]) ne "txxx") {
  308. next;
  309. }
  310. $tagsArr->[$i]->[1] =~ m/^(.*?):(.*)$/;
  311. my $txkeyFirst = $1;
  312. my $txvalFirst = $2;
  313. for (my $j = $i + 1; $j < scalar @$tagsArr; $j++) {
  314. next if (lc($tagsArr->[$j]->[0]) ne "txxx");
  315. $tagsArr->[$j]->[1] =~ m/^(.*?):(.*)$/;
  316. my $txkeySecond = $1;
  317. my $txvalSecond = $2;
  318. next if ($txkeyFirst ne $txkeySecond);
  319. # TXXX keys are equal, append the second to the first, and delete this entry
  320. $tagsArr->[$i]->[1] .= ';' . $txvalSecond;
  321. #print("DDDDDDDDDDDDD: Deleted $j index $txkeySecond:$txvalSecond\n");
  322. splice(@$tagsArr, $j, 1);
  323. }
  324. }
  325. }
  326. sub tagsToOpts {
  327. my $tags = shift;
  328. my @tagopts;
  329. foreach my $currKey (keys (%$tags)) {
  330. if (!exists($idLookup{$currKey})) {
  331. print("Tag: '$currKey' doesn't have a mapping, skipping\n");
  332. next;
  333. }
  334. my $tagMapping = $idLookup{$currKey};
  335. my $type = ref($tagMapping);
  336. if ($type eq "" && defined($tagMapping)) {
  337. # If tag name is defined and tag contents exists (aka not silenced)
  338. foreach my $tagCont (@{$tags->{$currKey}}) {
  339. mp3TagEscape(\$tagCont);
  340. shellsan(\$tagCont);
  341. push(@tagopts, ["$tagMapping", "$tagCont"]);
  342. }
  343. } elsif ($type eq "ARRAY") {
  344. my $mapKey = $tagMapping->[0];
  345. my $mapCont = $tagMapping->[1];
  346. my $mapContType = ref($mapCont);
  347. if (not defined($mapCont)) {
  348. print("WHUT???\n");
  349. exit(1);
  350. }
  351. if ($mapContType eq "") {
  352. foreach my $tagValue (@{$tags->{$currKey}}) {
  353. mp3TagEscape(\$tagValue);
  354. shellsan(\$tagValue);
  355. push(@tagopts, ["$mapKey", "$mapCont$tagValue"]);
  356. }
  357. } elsif ($mapContType eq "CODE") {
  358. my $tagValue = $mapCont->($tags);
  359. shellsan(\$tagValue);
  360. push(@tagopts, ["$mapKey", "$tagValue"]);
  361. }
  362. } elsif ($type eq 'CODE') {
  363. # If we have just a code reference
  364. # do not assume, that this is a tag, rather a general cmd opt
  365. #my $opt = $tagName->($tags);
  366. #if (defined($opt)) {
  367. #shellsan(\$opt);
  368. #push(@tagopts, qq($opt));
  369. #}
  370. my $codeRet = $tagMapping->($tags);
  371. my $mapKey = $codeRet->[0];
  372. my $mapCont = $codeRet->[1];
  373. shellsan(\$mapCont);
  374. push(@tagopts, ["$mapKey", "$mapCont"]);
  375. }
  376. }
  377. mergeDupeTxxx(\@tagopts);
  378. # Convert the tag array into an array of string to use with mid3v2
  379. my @tagoptsStr;
  380. foreach (@tagopts) {
  381. push(@tagoptsStr, qq('--$_->[0]' '$_->[1]'));
  382. }
  383. return \@tagoptsStr;
  384. }
  385. sub getFlacTags {
  386. my $flac = shift;
  387. my %tags;
  388. my @tagtxt = qx(metaflac --list --block-type=VORBIS_COMMENT -- '$flac');
  389. if ($? != 0) {
  390. exit(1);
  391. }
  392. foreach my $tagline (@tagtxt) {
  393. if ($tagline =~ /comment\[\d+\]:\s(.*?)=(.*)/) {
  394. if ($2 eq '') {
  395. print("Empty tag: $1\n");
  396. next;
  397. }
  398. if (not exists($tags{lc($1)})) {
  399. @{$tags{lc($1)}} = ($2);
  400. } else {
  401. push(@{$tags{lc($1)}}, $2);
  402. }
  403. }
  404. }
  405. return \%tags;
  406. }
  407. sub shellsan {
  408. ${$_[0]} =~ s/'/'\\''/g;
  409. }
  410. # Escape ':', and '\' characters in mp3 tag values
  411. sub mp3TagEscape {
  412. ${$_[0]} =~ s!([:\\])!\\$1!g;
  413. }
  414. sub mp3TagEscapeOwn {
  415. my $s = shift;
  416. mp3TagEscape(\$s);
  417. return $s;
  418. }
  419. sub usage {
  420. print("Usage: flac2mp3.pl [-h | --help] [-r] [-3] [-g | --genre NUM] <input_dir> <output_dir>\n");
  421. exit 1;
  422. }
  423. sub help {
  424. my $h = <<EOF;
  425. Usage:
  426. flac2mp3.pl [options] <input_dir> <output_dir>
  427. -h, --help print this help text
  428. -g, --genre NUM force this genre as a tag (lame --genre-list)
  429. -G, --no-genre ignore genre in flac file
  430. -r, --replay-gain use replay gain values
  431. --catid STRING the catalog id to set (or "")
  432. --comment STRING the comment to set (or "")
  433. --cover STRING Use this image as cover (or "" to not copy from flac)
  434. -t --tagreplace STR Replace flac tags for a specific file only
  435. Like -t '02*flac/TITLE=Some other title'
  436. -3, --320 Convert into CBR 320 instead into the default V0
  437. EOF
  438. print($h);
  439. exit 0;
  440. }
  441. # vim: ts=4 sw=4 et sta