Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

371 lignes
10KB

  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. # TODO fill this out
  14. my %genreMap = (
  15. edm => 52,
  16. soundtrack => 24,
  17. );
  18. # this is a godsent page
  19. # https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping
  20. # https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
  21. # a lot of this may not work
  22. # TODO escape potential 's
  23. my %idLookup = (
  24. album => 'TALB',
  25. albumsort => 'TSOA',
  26. discsubtitle => 'TSST',
  27. grouping => 'TIT1',
  28. title => 'TIT2',
  29. titlesort => 'TSOT',
  30. subtitle => 'TIT3',
  31. subtitle => 'TIT3',
  32. albumartist => 'TPE2',
  33. albumartistsort => 'TSO2', # Maybe?
  34. artist => 'TPE1',
  35. artistsort => 'TSOP',
  36. arranger => 'TIPL=arranger',
  37. author => 'TEXT',
  38. composer => 'TCOM',
  39. conductor => 'TPE3',
  40. engineer => 'TIPL=engineer',
  41. djmixer => 'TIPL=DJ-mix',
  42. mixer => 'TIPL=mix',
  43. #performer => 'TMCL', # This produces some really weird tags
  44. producer => 'TIPL=producer',
  45. publisher => 'TPUB',
  46. organization => 'TPUB',
  47. label => 'TPUB',
  48. remixer => 'TPE4',
  49. discnumber => ['TPOS', sub {
  50. my $t = shift;
  51. my $totalkey = exists($t->{disctotal}) ? 'disctotal' : 'totaldiscs';
  52. return "$t->{discnumber}" if !exists($t->{$totalkey});
  53. return "$t->{discnumber}/$t->{$totalkey}";
  54. }],
  55. totaldiscs => undef,
  56. disctotal => undef,
  57. tracknumber => ['TRCK', sub {
  58. my $t = shift;
  59. my $totalkey = exists($t->{tracktotal}) ? 'tracktotal' : 'totaltracks';
  60. return "$t->{tracknumber}" if !exists($t->{$totalkey});
  61. return "$t->{tracknumber}/$t->{$totalkey}";
  62. }],
  63. totaltracks => undef,
  64. tracktotal => undef,
  65. #date => 'TDRC', # This is for id3v2.4
  66. #date => 'TYER',
  67. date => [undef, sub {
  68. my $t = shift;
  69. my $date = $t->{date};
  70. if (length($date) == 4) { # Only year
  71. return "TYER=$date";
  72. }
  73. if (!($date =~ m/^\d{4}\.\d{2}\.\d{2}$/)) {
  74. print("Date format unknown: $date\n");
  75. exit 1;
  76. }
  77. $date =~ s/\./-/g;
  78. return "TDRL=$date"; # Release date
  79. }],
  80. originaldate => 'TDOR', # Also for 2.4 only
  81. 'release date' => 'TDOR', # Also for 2.4 only
  82. isrc => 'TSRC',
  83. barcode => 'TXXX=BARCODE',
  84. catalog => ['TXXX=CATALOGNUMBER', sub { return tagmap_catalogid(shift, 'catalog'); } ],
  85. catalognumber => ['TXXX=CATALOGNUMBER', sub { return tagmap_catalogid(shift, 'catalognumber'); } ],
  86. catalogid => ['TXXX=CATALOGNUMBER', sub { return tagmap_catalogid(shift, 'catalogid'); } ],
  87. labelno => ['TXXX=CATALOGNUMBER', sub { return tagmap_catalogid(shift, 'labelno'); } ],
  88. 'encoded-by' => 'TENC',
  89. encoder => 'TSSE',
  90. encoding => 'TSSE',
  91. 'encoder settings' => 'TSSE',
  92. media => 'TMED',
  93. genre => ['TCON', sub {
  94. return undef if ($opt_no_genre);
  95. my $genreName = shift->{genre};
  96. if (!exists($genreMap{lc($genreName)})) {
  97. # If no genre number exists, use the name
  98. return $genreName;
  99. }
  100. return $genreMap{$genreName};
  101. }],
  102. #mood => ['TMOO', sub {
  103. #}],
  104. bpm => 'TBPM',
  105. comment => ['COMM=Comment', sub {
  106. return undef if (defined($opt_comment) && $opt_comment eq "");
  107. return shift->{comment};
  108. }],
  109. copyright => 'TCOP',
  110. language => 'TLAN',
  111. #replaygain_album_peak => 'TXXX=REPLAYGAIN_ALBUM_PEAK',
  112. #replaygain_album_gain => 'TXXX=REPLAYGAIN_ALBUM_GAIN',
  113. replaygain_track_gain => sub {
  114. return undef if (!$opt_rg);
  115. shift->{replaygain_track_gain} =~ /^(-?\d+\.\d+) dB$/;
  116. my $gain_db = $1;
  117. exit(1) if ($gain_db eq "");
  118. return "--replaygain-accurate --gain $gain_db";
  119. },
  120. #replaygain_album_gain => 'TXXX=REPLAYGAIN_ALBUM_GAIN',
  121. #replaygain_album_peak => 'TXXX=REPLAYGAIN_ALBUM_PEAK',
  122. #replaygain_track_gain => 'TXXX=REPLAYGAIN_TRACK_GAIN',
  123. #replaygain_track_peak => 'TXXX=REPLAYGAIN_TRACK_PEAK',
  124. script => 'TXXX=SCRIPT',
  125. lyrics => 'USLT',
  126. circle => 'TXXX=CIRCLE',
  127. event => 'TXXX=EVENT',
  128. discid => 'TXXX=DISCID',
  129. originaltitle => 'TXXX=ORIGINALTITLE',
  130. );
  131. sub tagmap_catalogid {
  132. my $t = shift;
  133. my $own_tag_name = shift;
  134. return undef if (defined($opt_catid) && $opt_catid eq "");
  135. return $t->{$own_tag_name};
  136. }
  137. my $opt_genre;
  138. my $opt_help;
  139. my @opt_tagreplace;
  140. my $opt_cbr = 0;
  141. GetOptions(
  142. "genre|g=s" => \$opt_genre,
  143. "no-genre|G" => \$opt_no_genre,
  144. "replay-gain|r" => \$opt_rg,
  145. "help|h" => \$opt_help,
  146. "catid=s" => \$opt_catid,
  147. "comment=s" => \$opt_comment,
  148. "tagreplace|t=s" => \@opt_tagreplace,
  149. "320|3" => \$opt_cbr,
  150. ) or die("Error in command line option");
  151. if ($opt_help) {
  152. help();
  153. }
  154. if (scalar(@ARGV) != 2) {
  155. print("Bad arguments\n");
  156. usage();
  157. }
  158. my ($IDIR, $ODIR) = @ARGV;
  159. if (!-e $ODIR) {
  160. mkdir $ODIR;
  161. }
  162. find({ wanted => \&iterFlac, no_chdir => 1 }, $IDIR);
  163. sub iterFlac {
  164. # Return if file is not a file, or if it's not a flac
  165. return if (!-f || !/\.flac$/);
  166. my @required_tags = ("artist", "title", "album", "tracknumber");
  167. my $flacDir = substr($File::Find::name, length($IDIR));
  168. my $flac = $_;
  169. my $flac_o = $flac;
  170. shellsan(\$flac);
  171. my $dest = "$ODIR/" . $flacDir;
  172. #print("DEBUG: $dest\n");
  173. $dest =~ s/\.flac$/\.mp3/;
  174. my $tags = getFlacTags($flac);
  175. my $has_req_tags = 1;
  176. foreach (@required_tags) {
  177. if (!exists($tags->{lc($_)})) {
  178. $has_req_tags = 0;
  179. last;
  180. }
  181. }
  182. if (!$has_req_tags) {
  183. print("WARNING: File: '$flac' does not have all the required tags. Skipping\n");
  184. exit(1);
  185. return;
  186. }
  187. argsToTags($tags, $flac_o);
  188. my $tagopts = tagsToOpts($tags);
  189. #print("Debug: @$tagopts\n");
  190. shellsan(\$dest);
  191. my $cmd;
  192. if ($opt_cbr) {
  193. $cmd = "flac -cd -- '$flac' | lame -S -b 320 -q 0 --add-id3v2 @$tagopts - '$dest'";
  194. } else {
  195. $cmd = "flac -cd -- '$flac' | lame -S -V0 --vbr-new -q 0 --add-id3v2 @$tagopts - '$dest'";
  196. }
  197. #print("Debug - CMD: [$cmd]\n");
  198. qx($cmd);
  199. if ($? != 0) {
  200. exit(1);
  201. }
  202. embedImageFromFlac($flac, $dest);
  203. }
  204. sub embedImageFromFlac {
  205. my $flac = shift;
  206. my $mp3 = shift;
  207. # I can't get the automatic deletion working :c
  208. my (undef, $fname) = tempfile();
  209. # Export image from flac
  210. qx(metaflac --export-picture-to='$fname' -- '$flac');
  211. if ($? != 0) {
  212. # Probably no image
  213. unlink($fname);
  214. return;
  215. }
  216. # Extract mime type too
  217. my $pinfo = qx(metaflac --list --block-type=PICTURE -- '$flac');
  218. $pinfo =~ m/MIME type: (.*)/;
  219. my $mimeType = $1;
  220. # Add image to mp3
  221. qx(mid3v2 -p '${fname}:cover:3:$mimeType' -- '$mp3');
  222. unlink($fname);
  223. }
  224. sub argsToTags {
  225. my $argTags = shift;
  226. my $fname = shift;
  227. $fname =~ s!^.*/!!;
  228. if (defined($opt_genre)) {
  229. $argTags->{genre} = $opt_genre;
  230. }
  231. if (defined($opt_comment) && $opt_comment ne "") {
  232. $argTags->{comment} = $opt_comment;
  233. }
  234. if (defined($opt_catid) && $opt_catid ne "") {
  235. $argTags->{catalognumber} = $opt_catid;
  236. }
  237. if (scalar @opt_tagreplace > 0) {
  238. foreach my $trepl (@opt_tagreplace) {
  239. $trepl =~ m!(.*?)/(.*?)=(.*)!;
  240. my ($freg, $tag, $tagval) = ($1, $2, $3);
  241. if ($fname =~ m!$freg!) {
  242. $argTags->{lc($tag)} = $tagval;
  243. }
  244. }
  245. }
  246. }
  247. sub tagsToOpts {
  248. my $tags = shift;
  249. my @tagopts;
  250. # TODO escape ' and =?
  251. foreach my $currKey (keys (%$tags)) {
  252. if (!exists($idLookup{$currKey})) {
  253. print("Tag: '$currKey' doesn't have a mapping, skipping\n");
  254. next;
  255. }
  256. my $tagName = $idLookup{$currKey};
  257. my $type = ref($tagName);
  258. if ($type eq "" && defined($tagName)) {
  259. # If tag name is defined and tag contents exists
  260. my $tagCont = $tags->{$currKey};
  261. shellsan(\$tagCont);
  262. push(@tagopts, qq(--tv '$tagName=$tagCont'));
  263. } elsif ($type eq "ARRAY") {
  264. my $tagCont = $tagName->[1]->($tags);
  265. my $tagKey = $tagName->[0];
  266. if (defined($tagCont)) {
  267. if (defined($tagKey)) {
  268. shellsan(\$tagCont);
  269. push(@tagopts, qq(--tv '$tagName->[0]=$tagCont'));
  270. } else {
  271. if (ref($tagCont) eq 'ARRAY') {
  272. # If we have an array of tags
  273. foreach my $tC (@$tagCont) {
  274. shellsan(\$tC);
  275. push(@tagopts, qq(--tv '$tC'));
  276. }
  277. } else {
  278. # If we have only one
  279. shellsan(\$tagCont);
  280. push(@tagopts, qq(--tv '$tagCont'));
  281. }
  282. }
  283. }
  284. } elsif ($type eq 'CODE') {
  285. # If we have just a code reference
  286. # do not assume, that this is a tag, rather a general cmd opt
  287. my $opt = $tagName->($tags);
  288. if (defined($opt)) {
  289. shellsan(\$opt);
  290. push(@tagopts, qq($opt));
  291. }
  292. }
  293. }
  294. return \@tagopts;
  295. }
  296. sub getFlacTags {
  297. my $flac = shift;
  298. my %tags;
  299. my @tagtxt = qx(metaflac --list --block-type=VORBIS_COMMENT -- '$flac');
  300. if ($? != 0) {
  301. exit(1);
  302. }
  303. foreach my $tagline (@tagtxt) {
  304. if ($tagline =~ /comment\[\d+\]:\s(.*?)=(.*)/) {
  305. if ($2 eq '') {
  306. print("Empty tag: $1\n");
  307. next;
  308. }
  309. $tags{lc($1)} = $2;
  310. }
  311. }
  312. return \%tags;
  313. }
  314. sub shellsan {
  315. ${$_[0]} =~ s/'/'\\''/g;
  316. }
  317. sub usage {
  318. print("Usage: flac2mp3.pl [-h | --help] [-r] [-3] [-g | --genre NUM] <input_dir> <output_dir>\n");
  319. exit 1;
  320. }
  321. sub help {
  322. my $h = <<EOF;
  323. Usage:
  324. flac2mp3.pl [options] <input_dir> <output_dir>
  325. -h, --help print this help text
  326. -g, --genre NUM force this genre as a tag (lame --genre-list)
  327. -G, --no-genre ignore genre in flac file
  328. -r, --replay-gain use replay gain values
  329. --catid STRING the catalog id to set (or "")
  330. --comment STRING the comment to set (or "")
  331. -t --tagreplace STR Replace flac tags for a specific file only
  332. Like -t '02*flac/TITLE=Some other title'
  333. -3, --320 Convert into CBR 320 instead into the default V0
  334. EOF
  335. print($h);
  336. exit 0;
  337. }
  338. # vim: ts=4 sw=4 et sta