| 1 | #!/usr/bin/perl -w | 
|---|
| 2 | use strict; | 
|---|
| 3 |  | 
|---|
| 4 | package Tracks; | 
|---|
| 5 |  | 
|---|
| 6 | use WebService::MusicBrainz; | 
|---|
| 7 | use Text::Unidecode; | 
|---|
| 8 | use YAML; | 
|---|
| 9 | use String::Format; | 
|---|
| 10 | use Text::Sprintf::Named; | 
|---|
| 11 |  | 
|---|
| 12 | my %CODES = ( | 
|---|
| 13 |     a => 'ARTIST', | 
|---|
| 14 |     t => 'TITLE', | 
|---|
| 15 |     b => 'ALBUM', | 
|---|
| 16 |     n => 'TRACKNUM', | 
|---|
| 17 |     y => 'YEAR', | 
|---|
| 18 | ); | 
|---|
| 19 |  | 
|---|
| 20 | sub new { | 
|---|
| 21 |     my ($invocant, $discid) = @_; | 
|---|
| 22 |     die "Need a DiscID" unless $discid; | 
|---|
| 23 |  | 
|---|
| 24 |     my $class = ref $invocant || $invocant; | 
|---|
| 25 |     my $self = { | 
|---|
| 26 |         discid => $discid, | 
|---|
| 27 |         tracks_info => [], | 
|---|
| 28 |         album_info => {}, | 
|---|
| 29 |     }; | 
|---|
| 30 |  | 
|---|
| 31 |     return bless $self, $class; | 
|---|
| 32 | } | 
|---|
| 33 |  | 
|---|
| 34 | sub get_tracks_info { $_[0]->{tracks_info} } | 
|---|
| 35 | sub get_album_info { $_[0]->{album_info} } | 
|---|
| 36 | sub get_track_count { scalar @{ $_[0]->{tracks_info} } } | 
|---|
| 37 |  | 
|---|
| 38 | sub query_musicbrainz { | 
|---|
| 39 |     my $self = shift; | 
|---|
| 40 |  | 
|---|
| 41 |     my $discid = $self->{discid}; | 
|---|
| 42 |  | 
|---|
| 43 |     # check for cached info | 
|---|
| 44 |     my $filename = "$discid.yml"; | 
|---|
| 45 |     if (-r $filename) { | 
|---|
| 46 |         my $info = YAML::LoadFile($filename); | 
|---|
| 47 |         @{ $self }{qw{tracks_info album_info}} = @{ $info }{qw{tracks_info album_info}}; | 
|---|
| 48 |         return $self; | 
|---|
| 49 |     } | 
|---|
| 50 |  | 
|---|
| 51 |     my $ws_artists = WebService::MusicBrainz->new_artist; | 
|---|
| 52 |     my $ws_releases = WebService::MusicBrainz->new_release; | 
|---|
| 53 |     my $ws_tracks = WebService::MusicBrainz->new_track; | 
|---|
| 54 |  | 
|---|
| 55 |     # search on the discid | 
|---|
| 56 |     my $response = $ws_releases->search({ DISCID => $discid }); | 
|---|
| 57 |  | 
|---|
| 58 |     # save this object, since WS::MBZ deletes it when you fetch it | 
|---|
| 59 |     # TODO: bug report to WS::MBZ? | 
|---|
| 60 |     my $release = $response->release; | 
|---|
| 61 |  | 
|---|
| 62 |     # return undef if there is no matching release for this DiscID | 
|---|
| 63 |     #return unless defined $release; | 
|---|
| 64 |     die "No matches for $discid" unless defined $release; | 
|---|
| 65 |  | 
|---|
| 66 |     # search again, using the MBID of the first release found | 
|---|
| 67 |     # TODO: deal with multiple releases found? | 
|---|
| 68 |     # include tracks and artist info | 
|---|
| 69 |     $response = $ws_releases->search({ | 
|---|
| 70 |         MBID => $release->id, | 
|---|
| 71 |         INC => 'discs tracks artist release-events counts', | 
|---|
| 72 |     }); | 
|---|
| 73 |  | 
|---|
| 74 |     # get the fully filled out Release object (that represents the disc) | 
|---|
| 75 |     $release = $response->release; | 
|---|
| 76 |  | 
|---|
| 77 |     # this is ID3v2:TDRL = Release Date | 
|---|
| 78 |     # (for now we just take the first date) | 
|---|
| 79 |     my $release_date = eval { @{ $release->release_event_list->events }[0]->date }; | 
|---|
| 80 |     my $release_events = $release->release_event_list->events; | 
|---|
| 81 |     #warn map { $_->date } @{ $release_events }; | 
|---|
| 82 |     $release_date = '' if $@; | 
|---|
| 83 |     my ($release_year) = ($release_date =~ /(\d{4})/); | 
|---|
| 84 |  | 
|---|
| 85 |     $self->{album_info} = { | 
|---|
| 86 |         ARTIST => $release->artist->name, | 
|---|
| 87 |         ALBUM  => $release->title, | 
|---|
| 88 |         YEAR   => $release_year, | 
|---|
| 89 |     };         | 
|---|
| 90 |  | 
|---|
| 91 |     # get full info on each of the tracks | 
|---|
| 92 |     my @tracks; | 
|---|
| 93 |     my $track_num = 1; | 
|---|
| 94 |     for my $track_id (map { $_->id } @{ $release->track_list->tracks }) { | 
|---|
| 95 |         my $response = $ws_tracks->search({ | 
|---|
| 96 |             MBID => $track_id, | 
|---|
| 97 |             INC => 'artist track-rels', | 
|---|
| 98 |         }); | 
|---|
| 99 |         my $track = $response->track; | 
|---|
| 100 |  | 
|---|
| 101 |         # get the ID3v1 level stuff | 
|---|
| 102 |         # (will worry about the fancy v2 later) | 
|---|
| 103 |         push @tracks, { | 
|---|
| 104 |             TRACKNUM => $track_num, | 
|---|
| 105 |             TITLE    => $track->title, | 
|---|
| 106 |             ALBUM    => $release->title, | 
|---|
| 107 |             ARTIST   => $track->artist->name, | 
|---|
| 108 |             YEAR     => $release_year, | 
|---|
| 109 |         }; | 
|---|
| 110 |  | 
|---|
| 111 |         $track_num++; | 
|---|
| 112 |     } | 
|---|
| 113 |  | 
|---|
| 114 |     $self->{tracks_info} = \@tracks; | 
|---|
| 115 |  | 
|---|
| 116 |     $self->cache_info; | 
|---|
| 117 |  | 
|---|
| 118 |     return $self; | 
|---|
| 119 | } | 
|---|
| 120 |  | 
|---|
| 121 | sub cache_info { | 
|---|
| 122 |     my $self = shift; | 
|---|
| 123 |  | 
|---|
| 124 |     #TODO: warn | 
|---|
| 125 |     # return without saving if the info is empty | 
|---|
| 126 |     return unless %{ $self->{album_info} } && @{ $self->{tracks_info} }; | 
|---|
| 127 |  | 
|---|
| 128 |     my $filename = $self->{discid} . '.yml'; | 
|---|
| 129 |     YAML::DumpFile($filename, { | 
|---|
| 130 |         discid      => $self->{discid}, | 
|---|
| 131 |         album_info  => $self->{album_info}, | 
|---|
| 132 |         tracks_info => $self->{tracks_info}, | 
|---|
| 133 |     }); | 
|---|
| 134 | } | 
|---|
| 135 |      | 
|---|
| 136 |  | 
|---|
| 137 | # utility to make filenames match [A-Za-z0-9_]+ | 
|---|
| 138 | sub filename_escape { | 
|---|
| 139 |     my @strings = map {  | 
|---|
| 140 |         unidecode($_);  # unicode to ascii | 
|---|
| 141 |         $_ = lc $_;     # all lowercase | 
|---|
| 142 |          | 
|---|
| 143 |         # special substituations | 
|---|
| 144 |         s/&/ and /g; | 
|---|
| 145 |         s/(\w)'(\w)/$1$2/g; | 
|---|
| 146 |  | 
|---|
| 147 |         s/[^A-Za-z0-9]/_/g;  # ascii alphanumerics only | 
|---|
| 148 |         s/_+/_/g;            # compress underscores | 
|---|
| 149 |         s/^_|_$//g;         | 
|---|
| 150 |  | 
|---|
| 151 |         $_;  # return | 
|---|
| 152 |     } @_; | 
|---|
| 153 |     return wantarray ? @strings : $strings[-1]; | 
|---|
| 154 | } | 
|---|
| 155 |  | 
|---|
| 156 | sub get_mp3_filename { | 
|---|
| 157 |     my ($self, $args) = @_; | 
|---|
| 158 |  | 
|---|
| 159 |     if (my $track_num = $args->{track}) { | 
|---|
| 160 |         my $track = $self->{tracks_info}[$track_num - 1]; | 
|---|
| 161 |  | 
|---|
| 162 |         #my $format = $args->{format} || '%02n-%a-%b-%t.mp3'; | 
|---|
| 163 |         #my %codes = map { $_ => filename_escape($track->{$CODES{$_}}) } keys %CODES; | 
|---|
| 164 |         #return stringf($format, %codes); | 
|---|
| 165 |          | 
|---|
| 166 |         my $format = $args->{format} || '%(TRACKNUM)02d-%(ARTIST)s-%(ALBUM)s-%(TITLE)s.mp3'; | 
|---|
| 167 |         my $formatter = Text::Sprintf::Named->new({fmt => $format}); | 
|---|
| 168 |         return $formatter->format({ | 
|---|
| 169 |             args => { map { $_ => filename_escape($track->{$_}) } keys %$track }, | 
|---|
| 170 |         }); | 
|---|
| 171 |  | 
|---|
| 172 |         #return sprintf( | 
|---|
| 173 |         #    '%02d-%s-%s-%s.mp3', | 
|---|
| 174 |         #    $track->{TRACKNUM}, | 
|---|
| 175 |         #    filename_escape(@{ $track }{qw{ARTIST ALBUM TITLE}}), | 
|---|
| 176 |         #); | 
|---|
| 177 |     } | 
|---|
| 178 |  | 
|---|
| 179 |     return; | 
|---|
| 180 | } | 
|---|
| 181 |  | 
|---|
| 182 | sub get_m3u_filename { | 
|---|
| 183 |     my ($self, $args) = @_; | 
|---|
| 184 |  | 
|---|
| 185 |     my $album = $self->{album_info}; | 
|---|
| 186 |  | 
|---|
| 187 |     my $format = $args->{format} || '%(ARTIST)s-%(ALBUM)s.m3u'; | 
|---|
| 188 |     my $formatter = Text::Sprintf::Named->new({fmt => $format}); | 
|---|
| 189 |     return $formatter->format({ | 
|---|
| 190 |         args => { map { $_ => filename_escape($album->{$_}) } keys %$album }, | 
|---|
| 191 |     }); | 
|---|
| 192 |  | 
|---|
| 193 |     #return sprintf('%s-%s.m3u', filename_escape(@{ $self->{album_info} }{qw{ARTIST ALBUM}})); | 
|---|
| 194 | } | 
|---|
| 195 |  | 
|---|
| 196 | package main; | 
|---|
| 197 |  | 
|---|
| 198 | use YAML; | 
|---|
| 199 |  | 
|---|
| 200 | my $flac_file = shift; | 
|---|
| 201 |  | 
|---|
| 202 | my $discid = `metaflac --show-tag MBZ_DISCID $flac_file | cut -d = -f 2`; | 
|---|
| 203 | chomp $discid; | 
|---|
| 204 |  | 
|---|
| 205 | my $tracks = Tracks->new($discid); | 
|---|
| 206 | my $info = $tracks->query_musicbrainz->get_tracks_info; | 
|---|
| 207 |  | 
|---|
| 208 | print Dump($info); | 
|---|
| 209 |  | 
|---|
| 210 | # split into individual mp3 tracks | 
|---|
| 211 |  | 
|---|
| 212 | my $m3u_filename = $tracks->get_m3u_filename; | 
|---|
| 213 | print $m3u_filename, "\n"; | 
|---|
| 214 | #open my $M3U, '>', $m3u_filename; | 
|---|
| 215 | #print $M3U "#EXTM3U\n"; | 
|---|
| 216 |  | 
|---|
| 217 | for my $i (1 .. $tracks->get_track_count) { | 
|---|
| 218 |     my $track = $info->[$i - 1]; | 
|---|
| 219 |      | 
|---|
| 220 |     my $mp3_filename = $tracks->get_mp3_filename({  | 
|---|
| 221 |         track => $i, | 
|---|
| 222 |         format => '%(ARTIST)s/%(ALBUM)s/%(TRACKNUM)02d-%(TITLE)s.mp3', | 
|---|
| 223 |     }); | 
|---|
| 224 |  | 
|---|
| 225 |     print '  ', $mp3_filename, "\n"; | 
|---|
| 226 |     next; | 
|---|
| 227 |  | 
|---|
| 228 |     # MP3 encoder | 
|---|
| 229 |     open my $LAME, '|-', 'lame', | 
|---|
| 230 |         '--tt', $track->{TITLE}, | 
|---|
| 231 |         '--ta', $track->{ARTIST}, | 
|---|
| 232 |         '--tl', $track->{ALBUM}, | 
|---|
| 233 |         '--tn', $track->{TRACKNUM}, | 
|---|
| 234 |         '--add-id3v2',             | 
|---|
| 235 |         '-', $mp3_filename; | 
|---|
| 236 |  | 
|---|
| 237 |     my $start = $i; | 
|---|
| 238 |     my $end = $start + 1; | 
|---|
| 239 |      | 
|---|
| 240 |     # decode track | 
|---|
| 241 |     open my $FLAC, '-|', "flac -d --cue $start.1-$end.1 -o - $flac_file"; | 
|---|
| 242 |  | 
|---|
| 243 |     # pipe decoded FLAC audio to MP3 encoder | 
|---|
| 244 |     while (<$FLAC>) { | 
|---|
| 245 |         print $LAME $_; | 
|---|
| 246 |     } | 
|---|
| 247 |  | 
|---|
| 248 |     close $LAME; | 
|---|
| 249 |     close $FLAC; | 
|---|
| 250 |  | 
|---|
| 251 |     #print $M3U $mp3_filename, "\n"; | 
|---|
| 252 | } | 
|---|
| 253 |  | 
|---|
| 254 | #close $M3U; | 
|---|