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.

245 lines
6.5KB

  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. # TODO fill this out
  12. my %genreMap = (
  13. edm => 52,
  14. soundtrack => 24,
  15. );
  16. # this is a godsent page
  17. # https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping
  18. # a lot of this may not work
  19. # TODO escape potential 's
  20. my %idLookup = (
  21. album => 'TALB',
  22. albumsort => 'TSOA',
  23. discsubtitle => 'TSST',
  24. grouping => 'TIT1',
  25. title => 'TIT2',
  26. titlesort => 'TSOT',
  27. subtitle => 'TIT3',
  28. subtitle => 'TIT3',
  29. albumartist => 'TPE2',
  30. albumartistsort => 'TSO2', # Maybe?
  31. artist => 'TPE1',
  32. artistsort => 'TSOP',
  33. arranger => 'TIPL=arranger',
  34. author => 'TEXT',
  35. composer => 'TCOM',
  36. conductor => 'TPE3',
  37. engineer => 'TIPL=engineer',
  38. djmixer => 'TIPL=DJ-mix',
  39. mixer => 'TIPL=mix',
  40. #performer => 'TMCL', # This produces some really weird tags
  41. producer => 'TIPL=producer',
  42. publisher => 'TPUB',
  43. label => 'TPUB',
  44. remixer => 'TPE4',
  45. discnumber => ['TPOS', sub {
  46. my $t = shift;
  47. my $totalkey = exists($t->{disctotal}) ? 'disctotal' : 'totaldiscs';
  48. return "$t->{discnumber}" if !exists($t->{$totalkey});
  49. return "$t->{discnumber}/$t->{$totalkey}";
  50. }],
  51. totaldiscs => undef,
  52. disctotal => undef,
  53. tracknumber => ['TRCK', sub {
  54. my $t = shift;
  55. my $totalkey = exists($t->{tracktotal}) ? 'tracktotal' : 'totaltracks';
  56. return "$t->{tracknumber}" if !exists($t->{$totalkey});
  57. return "$t->{tracknumber}/$t->{$totalkey}";
  58. }],
  59. totaltracks => undef,
  60. tracktotal => undef,
  61. #date => 'TDRC', # This is for id3v2.4
  62. date => 'TYER',
  63. originaldate => 'TDOR', # Also for 2.4 only
  64. isrc => 'TSRC',
  65. barcode => 'TXXX=BARCODE',
  66. catalog => ['TXXX=CATALOGNUMBER', sub { return tagmap_catalogid(shift, 'catalog'); } ],
  67. catalognumber => ['TXXX=CATALOGNUMBER', sub { return tagmap_catalogid(shift, 'catalognumber'); } ],
  68. catalogid => ['TXXX=CATALOGNUMBER', sub { return tagmap_catalogid(shift, 'catalogid'); } ],
  69. 'encoded-by' => 'TENC',
  70. encoder => 'TSSE',
  71. encoding => 'TSSE',
  72. 'encoder settings' => 'TSSE',
  73. media => 'TMED',
  74. replaygain_album_gain => 'TXXX=REPLAYGAIN_ALBUM_GAIN',
  75. replaygain_album_peak => 'TXXX=REPLAYGAIN_ALBUM_PEAK',
  76. replaygain_track_gain => 'TXXX=REPLAYGAIN_TRACK_GAIN',
  77. replaygain_track_peak => 'TXXX=REPLAYGAIN_TRACK_PEAK',
  78. genre => ['TCON', sub {
  79. return undef if ($opt_no_genre);
  80. my $genreName = shift->{genre};
  81. if (!exists($genreMap{lc($genreName)})) {
  82. # If no genre number exists, use the name
  83. return $genreName;
  84. }
  85. return $genreMap{$genreName};
  86. }],
  87. #mood => ['TMOO', sub {
  88. #}],
  89. bpm => 'TBPM',
  90. comment => ['COMM=Comment', sub {
  91. return undef if (defined($opt_comment) && $opt_comment eq "");
  92. return shift->{comment};
  93. }],
  94. copyright => 'TCOP',
  95. language => 'TLAN',
  96. script => 'TXXX=SCRIPT',
  97. lyrics => 'USLT',
  98. circle => 'TXXX=CIRCLE',
  99. );
  100. sub tagmap_catalogid {
  101. my $t = shift;
  102. my $own_tag_name = shift;
  103. return undef if (defined($opt_catid) && $opt_catid eq "");
  104. return $t->{$own_tag_name};
  105. }
  106. my $opt_genre;
  107. my $opt_help;
  108. GetOptions(
  109. "genre|g=s" => \$opt_genre,
  110. "no-genre|G" => \$opt_no_genre,
  111. "help|h" => \$opt_help,
  112. "catid=s" => \$opt_catid,
  113. "comment=s" => \$opt_comment,
  114. ) or die("Error in command line option");
  115. if ($opt_help) {
  116. help();
  117. }
  118. if (scalar(@ARGV) != 2) {
  119. print("Bad arguments\n");
  120. usage();
  121. }
  122. my ($IDIR, $ODIR) = @ARGV;
  123. if (!-e $ODIR) {
  124. mkdir $ODIR;
  125. }
  126. find({ wanted => \&iterFlac, no_chdir => 1 }, $IDIR);
  127. sub iterFlac {
  128. # Return if file is not a file, or if it's not a flac
  129. return if (!-f || !/\.flac$/);
  130. my @required_tags = ("artist", "title", "album", "tracknumber");
  131. my $flac = $_;
  132. shellsan(\$flac);
  133. my $dest = "$ODIR/" . basename($flac);
  134. $dest =~ s/\.flac$/\.mp3/;
  135. my $tags = getFlacTags($flac);
  136. my $has_req_tags = 1;
  137. foreach (@required_tags) {
  138. if (!exists($tags->{lc($_)})) {
  139. $has_req_tags = 0;
  140. last;
  141. }
  142. }
  143. if (!$has_req_tags) {
  144. print("WARNING: File: '$flac' does not have all the required tags. Skipping\n");
  145. return;
  146. }
  147. argsToTags($tags);
  148. my $tagopts = tagsToOpts($tags);
  149. qx(flac -cd -- '$flac' | lame -V0 -S --vbr-new --add-id3v2 @$tagopts - '$dest');
  150. }
  151. sub argsToTags {
  152. my $argTags = shift;
  153. if (defined($opt_genre)) {
  154. $argTags->{genre} = $opt_genre;
  155. } elsif (defined($opt_comment) && $opt_comment ne "") {
  156. $argTags->{comment} = $opt_comment;
  157. } elsif (defined($opt_catid) && $opt_catid ne "") {
  158. $argTags->{catalognumber} = $opt_catid;
  159. }
  160. }
  161. sub tagsToOpts {
  162. my $tags = shift;
  163. my @tagopts;
  164. # TODO escape ' and =?
  165. foreach my $currKey (keys (%$tags)) {
  166. if (!exists($idLookup{$currKey})) {
  167. print("Tag: '$currKey' doesn't have a mapping, skipping\n");
  168. next;
  169. }
  170. my $tagName = $idLookup{$currKey};
  171. my $type = ref($tagName);
  172. if ($type eq "" && defined($tagName)) {
  173. my $tagCont = $tags->{$currKey};
  174. shellsan(\$tagCont);
  175. push(@tagopts, qq(--tv '$tagName=$tagCont'));
  176. } elsif ($type eq "ARRAY") {
  177. my $tagCont = $tagName->[1]->($tags);
  178. if (defined($tagCont)) {
  179. shellsan(\$tagCont);
  180. push(@tagopts, qq(--tv '$tagName->[0]=$tagCont'));
  181. }
  182. }
  183. }
  184. return \@tagopts;
  185. }
  186. sub getFlacTags {
  187. my $flac = shift;
  188. my %tags;
  189. my @tagtxt = qx(metaflac --list --block-type=VORBIS_COMMENT -- '$flac');
  190. foreach my $tagline (@tagtxt) {
  191. if ($tagline =~ /comment\[\d+\]:\s(.*?)=(.*)/) {
  192. $tags{lc($1)} = $2;
  193. }
  194. }
  195. return \%tags;
  196. }
  197. sub shellsan {
  198. ${$_[0]} =~ s/'/'\\''/g;
  199. }
  200. sub usage {
  201. print("Usage: flac2mp3.pl [-h | --help] [-g | --genre NUM] <input_dir> <output_dir>\n");
  202. exit 1;
  203. }
  204. sub help {
  205. my $h = <<EOF;
  206. Usage:
  207. flac2mp3.pl [options] <input_dir> <output_dir>
  208. -h, --help print this help text
  209. -g, --genre NUM force this genre as a tag (lame --genre-list)
  210. -G, --no-genre ignore genre in flac file
  211. --catid STRING the catalog id to set (or "")
  212. --comment STRING the comment to set (or "")
  213. EOF
  214. print($h);
  215. exit 0;
  216. }
  217. # vim: ts=4 sw=4 et sta