Sunday, January 29, 2012

DVD to MP4 with HandBrake (vidconv.sh)

Using Handbrake in a bash script.

This script processes my ISO file(s) and converts them to MP4.  The MP4 can then be used via UPNP or DLNA on my network devices.  I rip my DVDs to ISO and store them on my media server.  This is a living script and may change.  It takes a while to process each ISO at the moment.  I just want good quality when I view the video on a widescreen television.  The options for Handbrake I'm using:

-m                    Chapter markers
--main-feature   Find and process the main title.  By default, Title 1 is processed.
-e x264             Video encoder
-q 20                 Video quality
-r 29.97             Video frame rate
-b 2000             Video bitrate
-B 192               Audio bitrate
-i /mnt                This is the mount point for the ISO
-o                      Output file name
2> /dev/null        I don't want to see everything displayed to the screen - sending std error to nowhere land.

The script requires you to run as root since we are running mount and umount.

<<< --- vidconv.sh --->>>
#!/bin/bash

function ConvertIt()
{
    NewName=${1%%\.*}
    EXT=mp4
    if [ -f "$NewName.$EXT" ]; then
        echo "A file by the name of $NewName.$EXT already exists"
        return 1
    fi
    echo Processing:  $NewName
    mount -o loop "$1" /mnt
    HandBrakeCLI -m --main-feature -e x264 -q 20 -r 29.97 -b 2000 -B 192 -i /mnt -o "$NewName.$EXT" 2> /dev/null
    echo
    umount /mnt
}

if [[ $EUID -ne 0 ]]; then
   echo "This script must be run as root" 1>&2
   exit 1
fi
if [ "$1" = "" ]; then
    echo "You need to tell me which ISO to convert"
    echo "Use -a to process all ISO files in the currentl directory"
    exit 1
fi

if [ "$1" = "-a" ]; then
        for files in *.iso
        do
                ConvertIt "$files"
        done
else
    if [ ! -f "$1" ]; then
        echo "The input file $1 does not exist."
        exit 1
    fi
    ConvertIt "$1"
fi

exit 0




Thursday, January 12, 2012

listall.pl

Just a quick script to list all of my music files.  Similar to the 'tree' command from the DOS command line utilities.  Set the variable SrcDir to the root of your music data and give it a run.


<<< --- listall.pl --- >>>
#!/usr/bin/perl
use strict;
use warnings;
use Cwd;
use sort '_qsort';
use File::Glob qw(:globally :nocase);
our $DEBUG = 1;

my $SrcDir = "/var/lib/mythtv/music";
opendir (Source_Dir, $SrcDir) || die "can't opendir $SrcDir: $!";
chdir Source_Dir;
my @Artists = grep { !/^\./ && -d "$_" } sort readdir(Source_Dir);
foreach my $Artist (@Artists) {
        opendir (Artist_Dir, $Artist) || die "can't opendir $Artist: $!";
        if ($DEBUG) { print $Artist, "\n"; }
        chdir Artist_Dir;
        my @Albums = grep { !/^\./ && -d "$_" } sort readdir(Artist_Dir);
        foreach my $Album (@Albums) {
                if ($DEBUG) { print "\t", $Album, "\n"; }
                chdir $Album;
                my @Tracks = <*.{flac,mp3}>;
                foreach my $Track (@Tracks) {
                        if ($DEBUG) { print "\t\t", $Track, "\n"; }
                }
                chdir "..";
        }
        chdir "..";
        closedir Artist_Dir;
}
closedir Source_Dir;

Bash scripts to find and fix FLAC files

Three part process to test all FLAC files on my system, attempt to fix the files and lastly, update tag information.

Part 1 - Find the bad files
<<< --- flactest.sh --->>>

#!/bin/bash
#
# Part one of the process.  Looks for flac files with errors.
#
szDate=`/bin/date`
szMusicRoot='/var/lib/mythtv/music'
#szErrFile="$szMusicRoot/flac_errors.txt"
szErrFile="/tmp/flac_errors.txt"

echo Recursively testing flacs in $szMusicRoot
echo Flac decoding errors logged to $szErrFile
echo Flac test of $szMusicRoot started at $szDate >"$szErrFile"

/usr/bin/find "$szMusicRoot/" -name '*.flac' -type f -not -exec /usr/bin/flac -t --totally-silent '{}' \; -and -print >>"$szErrFile"

echo Done!

Part 2 - 'Fix' the bad files
<<< --- flacfix.sh --- >>>

#!/bin/bash
# This script is part two of the process.  This will 'repair' all files that
# have been written to flac_errors.txt
#
# Requires the file flac_errors.txt to already exist
#
szDate=`/bin/date`
szMusicRoot='/var/lib/mythtv/music'
#filelist="$szMusicRoot/flac_errors.txt"
filelist="/tmp/flac_errors.txt"
fixedfile="/tmp/nometa.txt"

flac=/usr/bin/flac

#find "$szMusicRoot" -type f -name "*.flac" > /tmp/flacfiles.list
echo > $szDate > $fixedfile

if [ -f /tmp/since ];
then
        mv /tmp/since /tmp/since."$szDate"
fi
touch /tmp/since

#Get rid of the first line of the file
file=$(head -1 $filelist)
sed -i 1d $filelist
echo "Files to process:  " $(wc -l $filelist)

while [ -s $filelist ] ; do
        file=$(head -1 $filelist)
        echo $file >> $fixedfile
        wav_pref=${file%%flac};
        wav_suff=wav
        wav_file="$wav_pref""$wav_suff"
        if [ -f "$file" ];
                then
                $flac -F -f -s -d --delete-input-file "$file"
        fi
        if [ -f "$wav_file" ];
                then
                $flac -5 -f -s --delete-input-file "$wav_file"
        fi
# remove current file from list
        sed -i 1d $filelist
done

Part 3 - Update the vorbis tag information
<<< ---  fixmeta.sh --- >>>

#!/bin/bash
# This script will read the input file, needmeta.txt which has directories
# that have a newer timestamp than the file /tmp/since.  These directories
# need to have the metadata updated for their audio files.  The script
# fixalbums.pl is executed in each of these directories.
#
# Requirements:
# touch the /tmp/since with your necessary time/date
# the previous script touches the /tmp/since file automatically.
# touch --date "12/25/2012 15:00:00" /tmp/since
#
szDate=`/bin/date`
szMusicRoot='/var/lib/mythtv/music'
filelist=$szMusicRoot/needmeta.txt
flac=/usr/bin/flac

echo Gathering directories to process ...
find $szMusicRoot -type d -newer /tmp/since > $filelist

while read Path
do
if [ "$szMusicRoot" != "$Path" ]
then
echo $Path
        ArtistAlbum=${Path##$szMusicRoot}
        NewArtist=${ArtistAlbum%/*}
        Artist=${NewArtist#/}
        Album=${ArtistAlbum##*/}
        AA=$szMusicRoot/$ArtistAlbum
        cd "$AA" && fixalbums.pl
fidone < "$filelist"

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;

Move audio from source to destination and ensure format is consistent.

Moves audio files from temporary area to permanent location, formatting along the way.

Background:  I had not used perl before this script so there are better methods than what I have here.  This works for me and is a living script.  That only means that I find myself making changes as I encounter situations that need addressed.

I use tagscanner on a Windows workstation that puts files into a holding area on my MythTV server (Amarok does a good job in Linux).  Tagscanner is really one of the best Windows' applications for audio files.  Extremely flexible and it's free.  The files are renamed into a format of "/Working/Directory/Artist/Album/Track. Title".  This script then starts in the source directory: "/Working/Directory" scanning all artists and albums.  From there this script formats all text to a consistent style and case (Linux bound - case matters).  This allows me to prevent some duplicates due to case and other formatting.  This script will grab album artwork from Amazon if I don't already have any in the source or destination directory.

Also, I prefer FLAC over MP3 but I also strive to have the better MP3 so this script will look for a higher bit rate in an MP3 and keep that over a lower bit rate.  The script gets rid of the MP3 if I have a FLAC of the same file.  For FLAC files, I convert them back to WAV to ensure that all Vorbis metatags have been removed and that the FLAC file's integrity has been checked.  I do the same to APE files.  Once in WAV format, I then convert those back to FLAC, adding only the tags that I am interested in having.

In the script, you will see the option (-a) to add artwork to each file.  I usually don't use that option.  If I convert the FLAC to MP3, I have another script that will add the artwork to the MP3.  This way, the MP3 works on my iPhone with artwork.  Just need Apple to start supporting FLAC (hey - we all have dreams).

Bottom line, put the raw files in "/some/base/directory/artist/album/track title" set the SRC_DIR and DEST_DIR variables in this script and let it run.  The location in the script for these two variables show how this script has evolved.  They push me to rewrite much of this thing.  Fortunately, the locations don't change too often.

Update - June 8 2014 - Just some minor changes to the script.  I started using foobar2000 for media playback in place of WinAMP.  With WinAMP, one can right-click a folder and play the content.  This isn't available in foobar.  Instead, foobar can use a cue file.  With that in mind, I created a sub routine to create a basic cue file when the media is being synced from the source to the destination.

I also modified the directory structure on the destination.  Before, I used /Working/Directory/Artist/Album/Track.  I now use /Working/Directory/Alpha/Artist/Album/Track.  An example is /Media/music/A/Adele/19/Track or Media/music/L/Led Zeppelin/II/Track.  I did this to make accessing the data more quickly when browsing, especially from a smartphone or HTPC remote.

<<< --- syncaudio.pl --->>>

#!/usr/bin/perl
use strict;
use warnings;
use Cwd;
#use Cwd qw(chdir);
use Audio::FLAC::Header;
use MP3::Info;
use MP3::Tag;
use sort '_qsort';
use File::Glob qw(:globally :nocase :glob);
use File::Path;
use File::Copy;
use Encode qw(encode decode);
use RequestSignatureHelper;
use Data::Dumper;
use lib '/home/jwhite/bin/lib';
use My::Stuff qw(Format_Text);
#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_a $opt_r $opt_d $opt_f $opt_n);
use Getopt::Std;

#our $DEBUG = 0;

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;
        }
        return $?;
}

sub findError {
        my $xml = shift;

        return undef unless ref($xml) eq 'HASH';

        if (exists $xml->{Error}) { return $xml->{Error}; };

        for (keys %$xml) {
        my $error = findError($xml->{$_});
        return $error if defined $error;
        }

        return undef;
}

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";
        my $Full_Path = $_[2] or die "\tI don't like the path text: $_[2] - $!\n";
        if (defined $opt_d) { print "# ", $artist, "\\", $album, "\n"; }
        $album =~ s/ Cd\d*//g;
        $album =~ s/ \(.*\)//g;
        $album =~ s/^\(.*\) //g;
        if (defined $opt_d) { print "# ", $artist, "\\", $album, "\n"; }
        if (defined $opt_d) { print "# starting to look for cover on Amazon.com\n"; }

#       $ua->proxy('http','http://192.168.2.1:8080');
        use constant myAWSId            => '<Get From Amazon>';
        use constant myAWSSecret        => '<Get 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 => '<Get From Amazon>',
                Operation => 'ItemSearch',
                SearchIndex => 'Music',
                Keywords => $album." ".$artist,
                Sort => 'relevancerank',
                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 "# 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: $ama_uri \n";
#                               print "# Amazon: Genre: $ama_genre \n";
#                       }
                        $found = 1;
                }
                $k++;
        } until ($found || $k == 10);
        #only search through first 10 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 = $Full_Path."/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", "-quiet", "-quality", "100", "folder.jpg");
                unlink $Src_IMG_File;
        }
}

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

sub Convert_Flac_To_Wav {
        print "Converting Flac to Wav";
        my $results;
        my @FILES = <*.{flac}> ;
        foreach my $File (@FILES) {
                chomp $File;
                print ".";
#               $results = System_Call("shntool", "conv", "-q", "-o", "wav", "$File");
# I had a condition where shntool could not interpret the WAV file correctly.
                $results = System_Call ("flac", "--verify", "-F", "-f", "-s", "-d", "$File");
                if (!$results) {
                        unlink $File;
                } else {
                        print "Results: ",$results,"\n";
                        print "There is a problem with: ", $File, "\n";
                        exit 1;
                }
        }
        print "\n";
}

sub Convert_Ape_To_Wav {
        print "Converting Ape to Wav";
        my $results;
        my @APEFILES = <*.{ape}> ;
        foreach my $ApeFile (@APEFILES) {
                chomp $ApeFile;
#               my $WavFile = $ApeFile;
#               $WavFile =~ s/\.ape$/\.wav/;
                print ".";
                $results = System_Call("shntool", "conv", "-q", "-o", "wav", "$ApeFile");
# mac does not provide a quiet mode. Still using shntool to convert to wav.
#               $results = System_Call ("mac", "$ApeFile", "$WavFile", "-d");
                if (!$results) {
                        unlink $ApeFile;
                } else {
                        print "Results: ",$results,"\n";
                        print "There is a problem with: ", $ApeFile, "\n";
                        exit 1;
                }
        }
        print "\n";
}

sub Create_CUE {
        my $CUEName = $_[0]."/00.cue";
        my $Artist = $_[1];
        my $Album = $_[2];
        my $i = 1;
        my $Track = 0;
        my @files = sort <$_[0]/*.{mp3,flac}>;
        open (CUEFILE, '>'.$CUEName);
        print CUEFILE "PERFORMER \"".$Artist."\"\n";
        print CUEFILE "TITLE \"".$Album."\"\n";
        foreach my $file (@files) {
                $file = ( split m!/!, $file )[-1];
                my $Title = $file;
                $Title =~ s{\.[^.]+$}{}; #Remove the extension
                $Title =~ s(^\d+[\.\_\-\ ]+)(); #Remove the leading track number
                print CUEFILE "FILE \"".$file."\" WAVE\n";
                $Track = sprintf("%02d", $i);
                print CUEFILE " TRACK ".$Track." AUDIO\n";
                print CUEFILE " TITLE \"".$Title."\"\n";
                print CUEFILE " INDEX 01 00:00:00\n";
                $i++;
        }
        close (CUEFILE);
}

sub Copy_Files {
        my $Artist = $_[0] or exit 1;
        my $Album = $_[1] or exit 1;
        my @Src_JPG_Files = {};
        my @Dest_JPG_Files = {};
        $Artist = decode('UTF-8',$Artist);
        $Album = decode('UTF-8',$Album);
        my $Category = substr($Artist,0,1);
        if ($Category =~ /\d/ ) { $Category = "0" };
        my $Dest_Dir = "/Media/music";
        my $Full_Path = $Dest_Dir."/".$Category."/".$Artist."/".$Album;
        my $Picture = $Full_Path."/folder.jpg";
        my @created = mkpath ($Full_Path);
        Convert_Image_Format;
#       If I have any JPG files in the source location, copy them to the dest.
        @Src_JPG_Files = <*.jpg>;
        my $Num_Src_JPG_Files = @Src_JPG_Files;
        if ($Num_Src_JPG_Files ge 1) {
                my $Src_JPG_File = $Src_JPG_Files[0];
                my $Dest_JPG_File = $Full_Path."/folder.jpg";
                print "Found an Image in the source and copying now\n";
                unless (copy($Src_JPG_File,$Dest_JPG_File)) {
                        print "Cannot copy JPG: ", $Src_JPG_File, "\n";
                }
        }
#       Don't bother getting cover from Amazon if I already have a JPG in the dest.
        @Dest_JPG_Files = bsd_glob( $Full_Path."/*.jpg" );
        my $Num_Dest_JPG_Files = @Dest_JPG_Files;
        if ($Num_Dest_JPG_Files eq 0) { Get_Album_Cover ($Artist, $Album, $Full_Path); }
#       Process MP3 and FLAC files from the source to the destination.
        my @Src_Files = sort <*.{mp3,flac}> ;
        my $Num_Files = @Src_Files;
        if ($Num_Files > 0) {
                my $CP_OK = 1;
                my $New_Track = 0;
                foreach my $Src_File (@Src_Files) {
                        my $Dest_File = $Src_File;
                        if ($Dest_File =~ /^(\d+[\.\_\-\ ]+)(.*)(\.\w+)$/) {
                                my ($Track, $Title, $Ext) = ($1, $2, lc($3));
                                $Track =~ s/^(\d+)(.*)/$1/;
                                if (defined $opt_n) {
                                        $Track = ++$New_Track;
                                }
                                $Track = sprintf("%02d", $Track);
                                $Title = Format_Text ($Title);
                                $Title = decode('UTF-8',$Title);
                                $CP_OK = 1;
                                $Dest_File = $Full_Path."/$Track. $Title$Ext";
                                if ( (-e $Dest_File) && ($Ext eq ".mp3") ) {
                                        my $srcinfo = get_mp3info($Src_File);
                                        my $destinfo = get_mp3info($Dest_File);
                                        if ($srcinfo->{BITRATE} <= $destinfo->{BITRATE}) {
                                                $CP_OK = 0;
                                        }
                                }
                                if ($Ext eq ".mp3") { #Don't copy mp3 if flac exists
                                        my $FLAC_Equiv = $Dest_File;
                                        $FLAC_Equiv =~ s/\.mp3$/\.flac/;
                                        if (-e $FLAC_Equiv) {
                                                $CP_OK = 0
                                        }
                                }
                                if ($CP_OK) {
                                        unless (copy($Src_File,$Dest_File)) {
                                                print "File Cannot be copied. Deleting zero-byte file.\n";
                                                unlink ($Dest_File);
                                        }
                                        if ($Ext eq ".flac") { #Replace mp3 with flac
                                                my $MP3_Equiv = $Dest_File;
                                                $MP3_Equiv =~ s/\.flac$/\.mp3/;
                                                if (-e $MP3_Equiv) {
                                                        print "\tReplacing $Track. $Title.mp3 with $Title.flac \n";
                                                        unlink ($MP3_Equiv);
                                                }
                                        }
                                        if ($Ext eq ".mp3") {
#                                               remove_mp3tag($Dest_File, 'ALL');
                                                my $mp3 = MP3::Tag->new($Dest_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, $Picture)) && (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;
                                        } else {
                                                my $flac = Audio::FLAC::Header->new($Dest_File);
                                                my $tags = $flac->tags();
                                                $tags->{TRACKNUMBER} = $Track;
                                                $tags->{TITLE} = $Title;
                                                $tags->{ARTIST} = $Artist;
                                                $tags->{ALBUM} = $Album;
#                                               $tags->{'ALBUMARTIST'} = $Artist;
                                                $tags->{GENRE} = "Rock";
                                                if ((-e $Picture) && (defined $opt_a)) {
                                                        System_Call ("metaflac", "--import-picture-from=$Picture",$Dest_File);
                                                }
                                                $flac->write();
                                        }
                                }
                        } else {
                                print "File format not recognized: $Src_File \n";
                                exit 1;
                        }
                }
        }
        Create_CUE $Full_Path,$Artist,$Album;
}

unless (-x "/usr/bin/shntool") {
        print "I need the program shntool to function\n";
        exit 1;
}
unless (-x "/usr/bin/mac") {
        print "I need the program mac to function\n";
        exit 1;
}
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('ardfn');
my $Src_Dir = "/Media/work";
opendir (Src_DH, $Src_Dir) || die "can't opendir $Src_Dir: $!";
chdir Src_DH;

my @Src_Artists = grep { !/^\./ && -d "$_" } sort readdir(Src_DH);
chomp @Src_Artists;
foreach my $Src_Artist (@Src_Artists) {
        chomp $Src_Artist;
        if (! ($Src_Artist eq 'Junk')) {
                my $Dest_Artist = Format_Text ($Src_Artist);
                opendir (Src_Artist_DH, $Src_Artist) || die "can't open $Src_Artist: $!";
                chdir($Src_Artist);
                my @Src_Albums = grep { !/^\./ && -d "$_" } sort readdir(Src_Artist_DH);
                foreach my $Src_Album (@Src_Albums) {
                        print "Processing $Src_Artist / $Src_Album \n";
                        my $Dest_Album = Format_Text ($Src_Album);
                        chdir($Src_Album);
                        if (defined $opt_f) {
                                Convert_Flac_To_Wav;
                                Convert_Ape_To_Wav;
                                Convert_Wav_To_Flac;
                        }
                        Copy_Files ($Dest_Artist, $Dest_Album);
                        chdir "..";
                }
                chdir "..";
                closedir Src_Artist_DH;
        }
}
closedir Src_DH;

Script to process single image/cue file (cuesplit.pl)


Script to process single image/cue file. Currently supports WAV, FLAC and APE. Known issue where extended characters in TITLE cause a problem (i.e. Mañana - had to manually replace ñ with n in the cue file).

<<<--- cuesplit.pl --->>>

#! /usr/bin/perl
use strict;
use File::Path;
use File::Glob qw(:globally :nocase);

sub Convert_Format {
    my $Image_File = $_[0];
    my $EXT = ($Image_File =~ m/([^.]+)$/)[0];
    # Try FLAC format
    $Image_File =~ s/$EXT/flac/;
    if (-e $Image_File) {
    return $Image_File;
    }
    # Try APE format
    $Image_File =~ s/flac/ape/;
    if (-e $Image_File) {
    return $Image_File;
    }
    # Try WAV format
    $Image_File =~ s/ape/wav/;
    if (-e $Image_File) {
    return $Image_File;
    }
    return $_[0];
}

my $DestPath;
my $Artist;
my $Album;
my $Line;
my $CUE;
my @CUES=<*.cue>;
foreach $CUE (@CUES) {
    open FILE, "<", $CUE or die $1;
    my @Lines = <FILE>;
    close FILE;
    foreach $Line (@Lines) {
        chomp $Line;
#           print $Line, "\n";
        if ($Line =~ /^PERFORMER/) {
            chomp $Line;
            $Line =~ s/^PERFORMER\s+\"//;
            $Line =~ s/(.*)\".*$/$1/;
            $Artist = $Line;
            $Artist =~ tr/\:\?/\-/s;
#               print $Artist, "\n";
        }
        if ($Line =~ /^TITLE/) {
            chomp $Line;
            $Line =~ s/^TITLE\s+\"//;
            $Line =~ s/(.*)\".*$/$1/;
            $Album = $Line;
            $Album =~ tr/\:\?/\-/s;
#               print $Album, "\n";
        }
    }
    $DestPath = $Artist."/".$Album;
    mkpath $DestPath;
    foreach $Line (@Lines) {
        if ($Line =~ /^FILE/) {
            chomp $Line;
            $Line =~ s/^FILE\s+\"//;
            $Line =~ s/(.*)\".*$/$1/;
            $Line = Convert_Format $Line;
            unless (-e $Line) {
                print "Cue needs: ", $Line, "\n";
                last;
            }
            print $Line, "\n";
            `shntool split "$Line" -f "$CUE" -m '?-:-' -t '%n. %t' -d "$DestPath" -o "flac ext=flac flac -s -o %f -"`
        }
    }
}