Thursday, January 12, 2012

Script to maintain ID3 tags (fixalbums.pl)

This script was written to help maintain files that are already in the destination folder.  If I change a folder name, this script will update the tags that are generated based on the folder name.  Real quick, files are stored in a structure: /Artist/Album/Track. Title.  Tags are then generated for each file based on this structure.  The genre tag is hard coded to 'Rock'.  My goal is to eventually grab the genre from Amazon.  I'm sure they store that info somewhere.  This script can grab artwork from Amazon as well.

Update June 8, 2014 - Minor changes with the addition of CUE file creation.  See my updates to the syncaudio.pl for an explanation of media data structure.  For this script, a added the option to fix the media files (was always assumed and therefore the default).  Now, one can run the script to create cue files and/or fix media tags.

<<< --- fixalbums.pl --->>>

#!/usr/bin/perl
use strict;
use warnings;
use Cwd;
use Audio::FLAC::Header;
use MP3::Info;
use MP3::Tag;
use sort '_qsort';
use File::Glob qw(:globally :nocase);
use Encode qw(encode decode);
use RequestSignatureHelper;
use Data::Dumper;
#use utf8;
#use Encode;

require LWP::UserAgent;
use LWP::Simple;      # libwww-perl providing simple HTML get actions
#use LWP::Simple qw($ua get); # libwww-perl providing simple HTML get actions
use HTML::Entities;
use URI::Escape;
use XML::Simple;

use vars qw($opt_c $opt_d $opt_a $opt_h $opt_f $opt_k $opt_m $opt_p);
use Getopt::Std;

sub Display_Help {
        print "Usage:\n\n";
        print "-a Add artwork to the metadata\n";
        print "-c Grab a cover from Amazon if one doesn't exist\n";
        print "-d Debug mode will desplay more progress information\n";
        print "-f Force FLAC to WAV and back to FLAC (Gets rid of all metadata)\n";
        print "-k Keep the WAV as a backup (use with -f)\n";
        print "-m Update metatags\n";
        print "-p Create a CUEFILE playlist\n";
        print "-h This help screen\n";
}

sub System_Call {
        my @args = @_;
        system (@args);
        if ($? == -1) {
                print "\nFailed to execute: $!\n";
                exit 2;
        }
        elsif ($? & 127) {
                printf "\nChild died with signal %d, %s coredump\n",
                ($? & 127),  ($? & 128) ? 'with' : 'without';
                exit 2;
        }
}

sub Get_Artist_Album {
        return ( split m!/!, cwd() )[-2,-1];
}

sub Get_Album_Cover {
        my $am_artist = "";
        my $am_title = "";
        my $ama_uri = "";
        my $ama_genre = "";
        my $found = 0;
        my $uri = "";

        my $artist = $_[0] or die "\tI don't like the text: $_[0] - $!\n";
        my $album = $_[1] or die "\tI don't like the text: $_[1] - $!\n";
        $album =~ s/ Cd\d*//g;
        $album =~ s/ \(.*\)//g;
        $album =~ s/^\(.*\) //g;
#       my $Full_Path = $_[2] or die "\tI don't like the path text: $_[2] - $!\n";
        if (defined $opt_d) { print "# ", $artist, "\\", $album, "\n"; }
#       The following will replace a space with %20
#       $artist =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
#       $album =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
        if (defined $opt_d) { print "Processing: ", $album, "\n"; }

        if (defined $opt_d) {
                print "# starting to look for poster on Amazon.com\n";
        }

#       $ua->proxy('http','http://192.168.2.1:8080');
        use constant myAWSId        => '<From Amazon>';
        use constant myAWSSecret    => '<From Amazon>';
        use constant myEndPoint     => 'ecs.amazonaws.com';
        my $helper = new RequestSignatureHelper (
                +RequestSignatureHelper::kAWSAccessKeyId => myAWSId,
                +RequestSignatureHelper::kAWSSecretKey => myAWSSecret,
                +RequestSignatureHelper::kEndPoint => myEndPoint,
        );
        my $request = {
                Service => 'AWSECommerceService',
                AssociateTag => 'hous06b-20',
                Operation => 'ItemSearch',
                SearchIndex => 'Music',
                Keywords => $album." ".$artist,
#               Artist => $artist,
#               Title => $album,
                Sort => 'relevancerank',
#               ResponseGroup => 'Images,Small',
                ResponseGroup => 'Images,ItemAttributes,BrowseNodes',
        };
        my $signedRequest = $helper->sign($request);

        my $queryString = $helper->canonicalize($signedRequest);
        my $url = "http://" . myEndPoint . "/onca/xml?" . $queryString;
        if (defined $opt_d) { print "Sending request to URL: $url \n"; }
        if (defined $opt_d) { print "# Query URL: $url\n";}

        my $ua = new LWP::UserAgent();
        my $response = $ua->get($url);
        my $content = $response->content();

        my $xmlParser = new XML::Simple();
        my $xml_doc = $xmlParser->XMLin($content);

        if (defined $opt_d) {
                print "Parsed XML is: " . Dumper($xml_doc) . "\n";

                if ($response->is_success()) {
                        my $title=$xml_doc->{Items}->{Request}->{ItemSearchRequest}->{Title};
                } else {
                        my $error = findError($xml_doc);
                        if (defined $error) {
                                print "Error: ".$error->{Code}.": ".$error->{Message}."\n";
                        } else {
                                print "Unknown Error!\n";
                        }
                }
        }

        if (ref($xml_doc->{Items}->{Item}) ne 'ARRAY') {
                my @tmpArray = ($xml_doc->{Items}->{Item});
                $xml_doc->{Items}->{Item} = \@tmpArray;
        }

        my $k = 0;
        do {
                $am_artist = $xml_doc->{Items}->{Item}->[$k]->{ItemAttributes}->{Artist};
                $am_title  = $xml_doc->{Items}->{Item}->[$k]->{ItemAttributes}->{Title};
                $ama_uri = $xml_doc->{Items}->{Item}->[$k]->{LargeImage}->{URL};
                if (defined $ama_uri) {
                        $ama_genre=$xml_doc->{Items}->{Item}->[$k]->{BrowseNodes}->{BrowseNode}->[0]->{Name};
                        if (defined $opt_d) {
                                print "# Amazon: found cover for $am_artist/$am_title: ";
                                print $ama_uri, "\n";
                                print "# Amazon: Genre: $ama_genre \n";
                        }
                        $found = 1;
                }
                $k++;
        } until ($found || $k == 25);
        #only search through first 25 matches

        my $image = get $ama_uri if (defined($ama_uri) && $ama_uri ne "");
        if (!defined($image)) {
                if (defined $opt_d) { print("# No image found for $artist/$album\n"); }
                return;
        }
        if ($ama_uri ne "" && length($image) eq "807") {
                if (defined $opt_d) { printf("# this image is blank\n"); }
                $ama_uri = "";
        }

        if (!defined($ama_uri)) {
                $ama_uri = "";
        }

        if ($ama_uri ne "") {
                if (defined $opt_d) { printf("Found: ", $ama_uri, "\n"); }
                $uri = $ama_uri;
        }

        my $jpeg_image = get $uri;
        my $outfile = "folder.jpg";
        if (defined $opt_d) { printf("OutFile: ", $outfile, "\n"); }
        open(INFO, ">$outfile");

        binmode INFO;
        print INFO $jpeg_image;
        close INFO;
}

sub Convert_Image_Format {
#This function assumes only one image file in the source directory
        my @Src_IMG_Files = <*.{bmp,png}> ;
        my $Num_Src_IMG_Files = @Src_IMG_Files;
        if ($Num_Src_IMG_Files ge 1) {
                my $Src_IMG_File = $Src_IMG_Files[0];
                System_Call ("convert", "$Src_IMG_File", "folder.jpg");
                unlink $Src_IMG_File;
        }
}

sub Convert_To_Flac {
        print "Converting to Flac";
        my $results;
        my @WAVS = <*.wav>;
        foreach my $wav (@WAVS) {
                chomp $wav;
                print ".";
                $results = System_Call ("shntool", "conv", "-w", "-q", "-o", "flac", "$wav");
                if (!$results) {
                        if (! defined $opt_k){
                                unlink $wav;
                        }
                } else {
                        print "Results: ",$results,"\n";
                        print "There is a problem with: ", $wav, "\n";
                        exit 1;
                }
        }
        print "\n";
}

sub Convert_To_Wav {
        print "Converting to Wav";
        my $results;
        my @FILES = <*.{flac,ape}> ;
        foreach my $File (@FILES) {
                chomp $File;
                print ".";
                $results = System_Call("shntool", "conv", "-q", "-o", "wav", "$File");
                if (!$results) {
                        unlink $File;
                } else {
                        print "Results: ",$results,"\n";
                        print "There is a problem with: ", $File, "\n";
                        exit 1;
                }
        }
        print "\n";
}

sub Process_Flac_To_Wav
{
        my $flac_file = $_[0];
        my $results;
        print "$flac_file -> WAV\n";
        my $wav_file = $flac_file;
        $wav_file =~ s/(.).flac/$1.wav/;
        $wav_file =~ tr/ / /s; #Remove unnecessary spaces
        $results=System_Call("flac","--verify","-F","-f","-s","-d","$flac_file");
        if (!$results) {
                unlink $flac_file;
                print "WAV -> $flac_file\n";
                $results=System_Call("flac","-5","--verify","-f","-s","$wav_file");
                if (!$results) {
                        unlink $wav_file;
                }
        } else {
                print "There is a problem with: ", $flac_file, "\n";
#               exit 1;
        }
}

sub Process_Files {
        Convert_Image_Format;
        my @MP3_Files = <*.mp3> ;
        my @FLAC_Files = <*.flac> ;
        my @JPG_Files = <*.jpg> ;
        my $Num_MP3_Files = @MP3_Files;
        my $Num_FLAC_Files = @FLAC_Files;
        my $Num_JPG_Files = @JPG_Files;
        if ($Num_MP3_Files > 0) {
                my ($Artist, $Album) = Get_Artist_Album;
                if (defined $opt_c && $Num_JPG_Files eq 0) {
                        Get_Album_Cover($Artist, $Album);
                }
                $Artist = decode('UTF-8',$Artist);
                $Album = decode('UTF-8',$Album);
                foreach my $MP3_File (@MP3_Files) {
                        chomp $MP3_File;
                        if (defined $opt_d) {print "Fixing: ", $MP3_File, "\n"; }
                        if ($MP3_File =~ /^(\d+[\.\_\-\ ]+)(.*)(\.\w+)$/) {
                                my ($Track, $Title, $Ext) = ($1, $2, lc($3));
                                $Track =~ s/^(\d+)(.*)/$1/;
                                $Track = sprintf("%02d", $Track);
                                $Title = Format_Text ($Title);
                                $Title = decode('UTF-8',$Title);
                                my $New_File = "$Track. $Title$Ext";
                                if (defined $opt_d) { print "\tNew Name: $New_File\n"; }
                                rename ($MP3_File, $New_File) unless $MP3_File eq $New_File;
#                               remove_mp3tag($New_File, 'ALL');
                                my $mp3 = MP3::Tag->new($New_File);
                                $mp3->get_tags;
                                if (exists $mp3->{ID3v1}) {
                                        $mp3->{ID3v1}->remove_tag;
                                }
                                if (exists $mp3->{ID3v2}) {
                                        $mp3->{ID3v2}->remove_tag;
                                }
                                my $id3v1 = $mp3->new_tag("ID3v1");
                                $id3v1->all($Title,$Artist,$Album,"","",$Track,"Rock");
                                $id3v1->write_tag;
                                my $id3v2 = $mp3->new_tag("ID3v2");
                                $id3v2->add_frame('TRCK',$Track);
                                $id3v2->add_frame('TIT2',$Title);
                                $id3v2->add_frame('TPE1',$Artist);
                                $id3v2->add_frame('TALB',$Album);
                                $id3v2->add_frame('TCON',"17");
                                if ((open(PICFILE, "folder.jpg")) && (defined $opt_a)) {
                                        my $filesize = (-s PICFILE);
                                        binmode(PICFILE);
                                        read(PICFILE, my $imgdata, $filesize);
                                        close(PICFILE);
                                        $id3v2->add_frame("APIC",
                                                chr(0x0),
                                                "image/jpeg",
                                                chr(0x0),
                                                "Cover Image",
                                                $imgdata);
                                }
                                $id3v2->write_tag;
                        }
                }
        }
        if ($Num_FLAC_Files > 0) {
                if (defined $opt_f) {
                        Convert_To_Wav;
                        Convert_To_Flac;
                }
                my ($Artist, $Album) = Get_Artist_Album;
                if (defined $opt_c && $Num_JPG_Files eq 0) {
                        Get_Album_Cover($Artist, $Album);
                }
                foreach my $FLAC_File (@FLAC_Files) {
                        chomp $FLAC_File;
                        if (defined $opt_d) {print "Fixing: ", $FLAC_File, "\n"; }
#                       if (defined $opt_f) {
#                               Process_Flac_To_Wav $FLAC_File
#                       }
                        if ($FLAC_File =~ /^(\d+[\.\_\-\ ]+)(.*)(\.\w+)$/) {
                                my ($Track, $Title, $Ext) = ($1, $2, lc($3));
                                $Track =~ s/^(\d+)(.*)/$1/;
                                $Track = sprintf("%02d", $Track);
                                $Title = Format_Text ($Title);
                                my $New_File = "$Track. $Title$Ext";
                                if (defined $opt_d) { print "\t$New_File\n"; }
                                rename ($FLAC_File, $New_File) unless $FLAC_File eq $New_File;
                                my $flac = Audio::FLAC::Header->new($New_File);
#                               %$tags = ();
                                my $tags = $flac->tags();
                                $tags->{TRACKNUMBER} = $Track;
                                $tags->{TITLE} = $Title;
                                $tags->{ARTIST} = $Artist;
#                               $tags->{'ALBUMARTIST'} = $Artist;
                                $tags->{ALBUM} = $Album;
                                $tags->{GENRE} = "Rock";
                                $flac->write();
                                if ((-e "folder.jpg")&& (defined $opt_a)) {
                                        System_Call ("metaflac", "--import-picture-from=folder.jpg",$New_File);
                                }
                        }
                }
        }

}

sub Create_CUE {
        my $i = 1;
        my $Track = 0;
        my @files = sort <*.{mp3,flac}>;
        my ($Artist, $Album) = Get_Artist_Album;
        #open (CUEFILE, '>'.$Artist.'-'.$Album.'.cue');
        open (CUEFILE, '>cd.cue');
        print CUEFILE "PERFORMER \"".$Artist."\"\n";
        print CUEFILE "TITLE \"".$Album."\"\n";
        foreach my $file (@files) {
                print CUEFILE "FILE \"".$file."\" WAVE\n";
                $Track = sprintf("%02d", $i);
                print CUEFILE "  TRACK ".$Track." AUDIO\n";
                print CUEFILE "    INDEX 01 00:00:00\n";
                $i++;
        }
        close (CUEFILE);
}

sub Format_Text {
        my $Text = $_[0] or die "\tI don't like the text: $_[0] - $!\n";
        $Text = lc($Text); #Make everything lowercase
        $Text =~ tr/_/ /; #Remove underscores
        $Text =~ s/\.\.\./\.\.\.\ /g;
        $Text =~ s/(\d),(\d)/$1$2/g;
        $Text =~ s/,/ /g;
        $Text =~ tr/\`\´\’/\'/s;
        $Text =~ s/ and / \& /g;
        $Text =~ tr/\[\{/\(/;
        $Text =~ tr/\]\}/\)/;
        $Text =~ s/\( /\(/g;
        $Text =~ s/ \)/\)/g;
        $Text =~ tr/ / /s; #Remove unnecessary spaces
        $Text =~ s/\·/-/g;
        $Text =~ s/\~/-/g;
        $Text =~ s/\:/-/g;
        $Text =~ s/\s*-\s*/-/g;
#       $Text =~ s/\.$//; #Some titles have an extra period - bye
#       $Text =~ s/(\d)\./$1/g; #Do not need period after numbers here
        my @Words = split(/ /,$Text);
        foreach my $Word (@Words) {
                $Word = ucfirst($Word);
        }
        $Text = "@Words";
        $Text =~ s/([(-])([a-z])/$1\u$2/g;
        $Text =~ s/(\W'\S)/uc($1)/eg; #Some items following ' should be uc
        $Text =~ s/(\.)([a-z])/$1\u$2/g; #Letter.Letter.Letter... is uc
        $Text =~ s/Dis[ck]\ /Cd/;
        $Text =~ s/Dis[ck](\d)/Cd$1/;
        $Text =~ s/Cd\ (\d)/Cd$1/;
        $Text =~ s/\((Cd\d+)\)/$1/;
        $Text =~ s/-Cd/ Cd/;
        my $x = $Text =~ tr/(/(/; #Count open parens
        my $y = $Text =~ tr/)/)/; #Count closing parens
        if ($x > $y) {
                $Text = $Text.")";
        }
        return ($Text);
}

unless (-x "/usr/bin/flac") {
        print "I need the program flac to function\n";
        exit 1;
}

unless (-x "/usr/bin/metaflac") {
        print "I need the program metaflac to function\n";
        exit 1;
}

getopts('cdahfkmp');
if (defined $opt_h) {
        Display_Help;
        exit 0;
}

if (defined $opt_m) {
        Process_Files;
}
if (defined $opt_p) {
        Create_CUE;
}
my $Artist_Dir = cwd();
opendir (Artist_DH, $Artist_Dir) || die "can't opendir $Artist_Dir: $!";
my @Albums = grep { !/^\./ && -d "$_" } sort readdir(Artist_DH);
foreach my $Album (@Albums) {
        chomp $Album;
        print "Processing: ", $Album, "\n";
        my $NewAlbum = Format_Text ($Album);
        rename ($Album, $NewAlbum) unless $Album eq $NewAlbum;
        if (defined $opt_d) { print "$NewAlbum \n"; }
        chdir $NewAlbum or warn "Cannot change to $NewAlbum\n";
        if (defined $opt_m) {
                Process_Files;
        }
        if (defined $opt_p) {
                Create_CUE;
        }
        chdir "..";
}
closedir Artist_DH;

No comments:

Post a Comment