#!/usr/bin/perl -w use strict; package Tracks; use WebService::MusicBrainz; use Text::Unidecode; use YAML; use String::Format; use Text::Sprintf::Named; my %CODES = ( a => 'ARTIST', t => 'TITLE', b => 'ALBUM', n => 'TRACKNUM', y => 'YEAR', ); sub new { my ($invocant, $discid) = @_; die "Need a DiscID" unless $discid; my $class = ref $invocant || $invocant; my $self = { discid => $discid, tracks_info => [], album_info => {}, }; return bless $self, $class; } sub get_tracks_info { $_[0]->{tracks_info} } sub get_album_info { $_[0]->{album_info} } sub get_track_count { scalar @{ $_[0]->{tracks_info} } } sub query_musicbrainz { my $self = shift; my $discid = $self->{discid}; # check for cached info my $filename = "$discid.yml"; if (-r $filename) { my $info = YAML::LoadFile($filename); @{ $self }{qw{tracks_info album_info}} = @{ $info }{qw{tracks_info album_info}}; return $self; } my $ws_artists = WebService::MusicBrainz->new_artist; my $ws_releases = WebService::MusicBrainz->new_release; my $ws_tracks = WebService::MusicBrainz->new_track; # search on the discid my $response = $ws_releases->search({ DISCID => $discid }); # save this object, since WS::MBZ deletes it when you fetch it # TODO: bug report to WS::MBZ? my $release = $response->release; # return undef if there is no matching release for this DiscID #return unless defined $release; die "No matches for $discid" unless defined $release; # search again, using the MBID of the first release found # TODO: deal with multiple releases found? # include tracks and artist info $response = $ws_releases->search({ MBID => $release->id, INC => 'discs tracks artist release-events counts', }); # get the fully filled out Release object (that represents the disc) $release = $response->release; # this is ID3v2:TDRL = Release Date # (for now we just take the first date) my $release_date = eval { @{ $release->release_event_list->events }[0]->date }; my $release_events = $release->release_event_list->events; #warn map { $_->date } @{ $release_events }; $release_date = '' if $@; my ($release_year) = ($release_date =~ /(\d{4})/); $self->{album_info} = { ARTIST => $release->artist->name, ALBUM => $release->title, YEAR => $release_year, }; # get full info on each of the tracks my @tracks; my $track_num = 1; for my $track_id (map { $_->id } @{ $release->track_list->tracks }) { my $response = $ws_tracks->search({ MBID => $track_id, INC => 'artist track-rels', }); my $track = $response->track; # get the ID3v1 level stuff # (will worry about the fancy v2 later) push @tracks, { TRACKNUM => $track_num, TITLE => $track->title, ALBUM => $release->title, ARTIST => $track->artist->name, YEAR => $release_year, }; $track_num++; } $self->{tracks_info} = \@tracks; $self->cache_info; return $self; } sub cache_info { my $self = shift; #TODO: warn # return without saving if the info is empty return unless %{ $self->{album_info} } && @{ $self->{tracks_info} }; my $filename = $self->{discid} . '.yml'; YAML::DumpFile($filename, { discid => $self->{discid}, album_info => $self->{album_info}, tracks_info => $self->{tracks_info}, }); } # utility to make filenames match [A-Za-z0-9_]+ sub filename_escape { my @strings = map { unidecode($_); # unicode to ascii $_ = lc $_; # all lowercase # special substituations s/&/ and /g; s/(\w)'(\w)/$1$2/g; s/[^A-Za-z0-9]/_/g; # ascii alphanumerics only s/_+/_/g; # compress underscores s/^_|_$//g; $_; # return } @_; return wantarray ? @strings : $strings[-1]; } sub get_mp3_filename { my ($self, $args) = @_; if (my $track_num = $args->{track}) { my $track = $self->{tracks_info}[$track_num - 1]; #my $format = $args->{format} || '%02n-%a-%b-%t.mp3'; #my %codes = map { $_ => filename_escape($track->{$CODES{$_}}) } keys %CODES; #return stringf($format, %codes); my $format = $args->{format} || '%(TRACKNUM)02d-%(ARTIST)s-%(ALBUM)s-%(TITLE)s.mp3'; my $formatter = Text::Sprintf::Named->new({fmt => $format}); return $formatter->format({ args => { map { $_ => filename_escape($track->{$_}) } keys %$track }, }); #return sprintf( # '%02d-%s-%s-%s.mp3', # $track->{TRACKNUM}, # filename_escape(@{ $track }{qw{ARTIST ALBUM TITLE}}), #); } return; } sub get_m3u_filename { my ($self, $args) = @_; my $album = $self->{album_info}; my $format = $args->{format} || '%(ARTIST)s-%(ALBUM)s.m3u'; my $formatter = Text::Sprintf::Named->new({fmt => $format}); return $formatter->format({ args => { map { $_ => filename_escape($album->{$_}) } keys %$album }, }); #return sprintf('%s-%s.m3u', filename_escape(@{ $self->{album_info} }{qw{ARTIST ALBUM}})); } package main; use YAML; my $flac_file = shift; my $discid = `metaflac --show-tag MBZ_DISCID $flac_file | cut -d = -f 2`; chomp $discid; my $tracks = Tracks->new($discid); my $info = $tracks->query_musicbrainz->get_tracks_info; print Dump($info); # split into individual mp3 tracks my $m3u_filename = $tracks->get_m3u_filename; print $m3u_filename, "\n"; #open my $M3U, '>', $m3u_filename; #print $M3U "#EXTM3U\n"; for my $i (1 .. $tracks->get_track_count) { my $track = $info->[$i - 1]; my $mp3_filename = $tracks->get_mp3_filename({ track => $i, format => '%(ARTIST)s/%(ALBUM)s/%(TRACKNUM)02d-%(TITLE)s.mp3', }); print ' ', $mp3_filename, "\n"; next; # MP3 encoder open my $LAME, '|-', 'lame', '--tt', $track->{TITLE}, '--ta', $track->{ARTIST}, '--tl', $track->{ALBUM}, '--tn', $track->{TRACKNUM}, '--add-id3v2', '-', $mp3_filename; my $start = $i; my $end = $start + 1; # decode track open my $FLAC, '-|', "flac -d --cue $start.1-$end.1 -o - $flac_file"; # pipe decoded FLAC audio to MP3 encoder while (<$FLAC>) { print $LAME $_; } close $LAME; close $FLAC; #print $M3U $mp3_filename, "\n"; } #close $M3U;