You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

337 line
9.6KB

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