#!/usr/bin/perl # Script (C) Stuart Hickinbottom 2004 (stuart@hickinbottom.demon.co.uk). # You are free to use and modify this script as you like, but please leave in # my credit. Thank you. # Pick up more errors use warnings; use strict; # Import some facilities use Getopt::Long; use Pod::Usage; use File::Find; use File::Glob ':glob'; use POSIX qw(strftime); use File::Path; use File::Temp qw/ tempfile /; use Cwd; # Exit more cleanly from signals (ensure destructors/atexits called) use sigtrap qw(die INT QUIT); use sigtrap qw(die untrapped normal-signals stack-trace any error-signals); # Global definitions use constant EXT_WAV => "wav"; use constant EXT_CUE => "cue"; use constant EXT_TAG => "tag"; use constant EXT_FLAC => "flac"; use constant CUE_WAV_FORMAT => "%s - %s.%s"; use constant FLAC_COMMAND => "flac \"%s\" -o \"%s\" -T artist=\"%s\" -T album=\"%s\" -T date=\"%s\" -T genre=\"%s\" -T comment=\"EAC-->WAV-->FLAC on %s\" %s"; # Prototype definitions sub find_dir(); sub process_directory($); sub is_restricted_filenames(); # Set the umask - we create files with full owner and group settings, but leave # everyone else out. umask(0007); # Process command-line arguments my $verbose = ""; my $input_dir = "/home/media/NewMusic"; my $output_dir = "/home/media/NEW-FLAC-CUE"; my $flac_options = "--replay-gain --best --silent"; GetOptions("verbose" => \$verbose, "input-directory=s" => \$input_dir, "output-directory=s" => \$output_dir, "flac-options=s" => \$flac_options, "version" => sub { versionMessage(); }, "help|?" => sub { pod2usage(1) }) or die "Failed to understand command options"; # Output options in use print "Input directory: $input_dir\n" if $verbose; print "Output directory: $output_dir\n" if $verbose; print "Flac options: $flac_options\n" if $verbose; # Discovery if filenames are restricted on this platform my $is_restricted_filenames = is_restricted_filenames(); print "Filenames restricted: " if $verbose; print "YES\n" if ($verbose && $is_restricted_filenames); print "NO\n" if ($verbose && !$is_restricted_filenames); # Search input directory for things to do. We add to the end of an array # recording the directories to process. print "Searching for music directories to process\n" if $verbose; my @music_dirs; find \&find_dir, $input_dir; # Process each found music directory. print "Found a total of " . scalar @music_dirs . " music job(s). Beginning processing...\n"; foreach my $music_dir (@music_dirs) { process_directory($music_dir); } print "Processing complete\n"; # We've finished, so exit. exit 0; ############################# # Check directory and remember it if it is a music job sub find_dir() { my $found_file = $_; # If this file is a CUESHEET, then we add it to the array of directories # that are to be processed. my $match_re = "\." . EXT_TAG; if ($found_file =~ m/$match_re$/) { my $found_dir = getcwd(); print "Found dir: $found_dir\n" if $verbose; push @music_dirs, $found_dir; } } # Flacify one music directory sub process_directory($) { my $music_dir = shift; print "Processing directory: $music_dir\n" if $verbose; # Check that all the required files are present in this directory. my $old_cwd = getcwd(); chdir $music_dir; my $tag_filename = $music_dir . "/" . bsd_glob('*.' . EXT_TAG, GLOB_NOCASE); my $wav_filename = $music_dir . "/" . bsd_glob('*.' . EXT_WAV, GLOB_NOCASE); my $cue_filename = $music_dir . "/" . bsd_glob('*.' . EXT_CUE, GLOB_NOCASE); chdir $old_cwd; unless ($tag_filename && $wav_filename && $cue_filename) { print STDERR "Incomplete fileset found in $music_dir\n"; print "TAG file missing\n" if not $tag_filename; print "CUE file missing\n" if not $cue_filename; print "WAV file missing\n" if not $wav_filename; return; } # Now read the pertinent information from the tags file. my %tags; unless (open(TAGFILE, "<$tag_filename")) { print STDERR "Unable to open tag file: $tag_filename ($!)\n"; return; } while () { next if /^\s*#/; next if /^$/; chomp; tr/ //d; my $tag; my $value; ($tag, $value) = /(.*?):\s*(.*?)$/; $value =~ s/\s*$//; $tags{$tag} = $value; } unless (close(TAGFILE)) { print STDERR "Unable to close tag file: $tag_filename ($!)\n"; return; } # Check for the required tags being defined. unless (defined($tags{artist}) && defined($tags{cdname}) && defined($tags{year}) && defined($tags{genre})) { print STDERR "One or more missing tags in: $tag_filename\n"; print STDERR "Required tags: artist, cdname, year, genre.\n"; return; } # Safely translate any dangerous characters (if required). my $safe_artist = $tags{artist}; my $safe_cdname = $tags{cdname}; if ($is_restricted_filenames) { $safe_artist =~ tr/\/\\:*?"<>|/_________/; $safe_cdname =~ tr/\/\\:*?"<>|/_________/; } # Build the target filenames my $new_cue_filename = sprintf CUE_WAV_FORMAT, $safe_artist, $safe_cdname, EXT_CUE; my $new_flac_filename = sprintf CUE_WAV_FORMAT, $safe_artist, $safe_cdname, EXT_FLAC; # Now read the cuesheet, modifying the WAVE file specification my @cuesheet; unless (open(CUEFILE, "<$cue_filename")) { print STDERR "Unable to open cuesheet: $cue_filename ($!)\n"; return; } while () { chomp; tr/ //d; my $cueline = $_; # Modify the WAVE filename, when we see that. $cueline =~ s/^(FILE ").*(" WAVE)/$1$new_flac_filename$2/; # Swap "Various" for "Various Artists", when we see that. $cueline =~ s/^PERFORMER "Various"/PERFORMER "Various Artists"/; # If we spot the performer going by, remember it as it's more # reliable than the one EAC passes as an argument for various # artists CDs. if ($cueline =~ /^PERFORMER \"(.*)\"$/) { $tags{artist} = $1 } push @cuesheet, $cueline; } # Just in case this spotted a different artist. $safe_artist = $tags{artist}; if ($is_restricted_filenames) { $safe_artist =~ tr/\/\\:*?"<>|/_________/; } unless (close(CUEFILE)) { print STDERR "Unable to close cuesheet: $cue_filename ($!)\n"; return; } # Build target names. my $target_dir = "$output_dir/$safe_artist/$safe_cdname"; my $new_cue_filename_full = $target_dir . '/' . $new_cue_filename; my $new_flac_filename_full = $target_dir . '/' . $new_flac_filename; # Create the target directory. rmtree($target_dir); unless(scalar mkpath($target_dir)) { print STDERR "Unable to create output directory: $target_dir ($!)\n"; return; } # Write the cuesheet. unless (open(CUEFILE, ">$new_cue_filename_full")) { print STDERR "Unable to open cuesheet: $new_cue_filename_full ($!)\n"; return; } foreach my $cueline (@cuesheet) { print CUEFILE "$cueline\n"; } unless (close(CUEFILE)) { print STDERR "Unable to close cuesheet: $new_cue_filename_full ($!)\n"; return; } # Give a processing message. print "Processing $tags{cdname} by $tags{artist}\n"; # Run flac. my $encode_date = strftime "%A %e %B %Y %I:%M:%S%P %Z", gmtime; my $flac_command = sprintf FLAC_COMMAND, $wav_filename, $new_flac_filename_full, $tags{artist}, $tags{cdname}, $tags{year}, $tags{genre}, $encode_date, $flac_options; print "Running flac: $flac_command\n" if $verbose; unless (system($flac_command) == 0) { print STDERR "Encoding failed ($!)\n"; return; }; # Remove the input directory now we have finished with it. unless (rmtree($music_dir)) { print STDERR "Unable to remove input directory: $music_dir ($!)\n"; return; } # Rename the converted files to prevent them being processed again. # (this will be replaced by deletions when everything is trustworthy). #unless (rename $wav_filename, "$wav_filename.old") { #print STDERR "Unable to rename $wav_filename ($!)\n"; #} # #unless (rename $tag_filename, "$tag_filename.old") { #print STDERR "Unable to rename $tag_filename ($!)\n"; #} # #unless (rename $cue_filename, "$cue_filename.old") { #print STDERR "Unable to rename $cue_filename ($!)\n"; #} } # Discovery if filenames cannot contain 'restricted' characters. This is used # to discover whether such characters are to be filtered out and replaced with # another safe character instead. sub is_restricted_filenames() { # This tries to create a temporary file with an unpleasant filename; # the rationale being that this will fail on platforms or filesystems that # won't allow such characters in filenames. This returns an error (available # through the $@ variable), if such characters are not allowed. eval { my ($fh, $filename) = tempfile(":*?\"<>|XXXX", UNLINK => 1); }; return $@; } # The help text __END__ =head1 SYNOPSIS flacify.pl [options] Options: -help Show this help description -verbose Output progress messages -input-directory Override default location of input directory -output-directory Override default location of output directory -flac-options Override default options fot flac encoder =cut