#!/usr/bin/perl -w

#
# $Id: mp3roaster,v 1.22 2005/10/09 09:27:57 eim Exp $
#
# TODO:
#
#   . Define and complete verbose/debug options mode.
#   . Fix the clear line code (x75, see perldoc perldop).
#   . Complete all the little TODO signed comments in the code.
#
# LICENSE:
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   For further details take a look at the COPYING file.
#

use strict;

use Getopt::Long;           # For the input options
use File::Basename;         # Get the basename of files
use File::Compare;          # Compare files
use File::Path;             # Get file path
use File::Copy;             # Copy files or filehandles
use File::MMagic;           # Guess file type
use Term::ReadKey;          # Module for simple terminal control
use MPEG::MP3Info;          # Allows to get MP3 infos
use Ogg::Vorbis::Header;    # Perl extension for Ogg Vorbis streams
use Audio::FLAC::Header;    # Perl extension for FLAC files
use Audio::Wav              # Perl extension for wav files

Getopt::Long::Configure("no_auto_abbrev", "no_ignorecase");

#
#   DECLARE GLOBAL VARS
#
my $config_file_dir     = "mp3roaster";
my $config_file_name    = "mp3roasterrc";
my %opthash;

#
#   GET OPTIONS
#
GetOptions (

    \%opthash,

    'opt_cdr_dev|dev|D=s',
    'opt_cdr_speed|speed|s=i',
    'opt_cdr_dummy|dummy|d',
    'opt_cdr_dao|dao|a',
    'opt_cdr_burnfree|burnfree|b',
    'opt_temp_dir|temp|t=s',
    'opt_mp3_decoder|mp3dec|m=s',
    'opt_check_lowercase|lowercase|l',
    'opt_check_spaces|spaces|S',
    'opt_check_brackets|brackets|B',
    'opt_check_quotes|quotes|q',
    'opt_check_questions|questions|Q',
    'opt_wav_normalize|normalize|n',

    'verbose|v',
    'help|h',
    'version|V'
);

# {{{ sub showHelp{}

#
# showHelp
#
# Print the help (usage) screen.
#
sub showHelp {

    my ($def_appname, $def_description, $def_bug_contact) = @_;

    print <<EOH
Usage: $def_appname [options] MP3/OGG/FLAC/WAV files.
$def_description.

Options:
  -D, --dev         CDR device to use.
  -s, --speed       Burn speed.
  -d, --dummy       Burn with laser off.
  -a, --dao         Burn in disk-at-once (DAO) mode for gapless recording.
  -b, --burnfree    Turn on support for Buffer Underrun Free writing.
  -t, --temp        Temporary directory.
  -m, --mp3dec      MP3 decoder to use.
  -l, --lowercase   Convert filenames to lowercase.
  -S, --spaces      Replace spaces with underscores.
  -B, --brackets    Replace brackets with underscores.
  -q, --quotes      Replace quotes with underscores.
  -Q, --questions   Replace questionmarks with underscores.
  -n, --normalize   Normalize WAV files before burning.
  -v, --verbose     Enable verbose output.
  -h, --help        Show this help screen.
  -V, --version     Show program version.

Examples:
  % $def_appname \"Root Dance.mp3\" Free\\ Software.flac bar.ogg Decoded.wav

Report bugs to $def_bug_contact.
EOH
};

# }}}
# {{{ sub showVersion{}

#
# showVersion
#
# Print the version screen.
#
sub showVersion {

    my ($def_appname, $def_version, $def_cvs_revision, $def_author,
        $def_author_email, $def_co_author, $def_co_author_email, $def_copyright_time) = @_;

    print <<EOH
$def_appname $def_version ($def_cvs_revision).

Written by $def_author <$def_author_email>.
with lots of help from $def_co_author <$def_co_author_email>.

Copyright (C) $def_copyright_time $def_author <$def_author_email>
This is free software; see the source for copying conditions. There is
NO warranty; the author is NOT RESPONSIBLE for any use of this program.
EOH
};

# }}}
# {{{ sub print_error{}

#
# print_error
#
# Compose and return an error message.
#
sub print_error {

    my ($in_sub, $in_r_message) = @_;

    if (defined $in_sub) {

        return " \! ERROR in sub $in_sub\n   $in_r_message\n\n";

    } else {

        return " \! ERROR: $in_r_message\n\n";
    }
}

# }}}
# {{{ sub which_config_file{}

#
# which_config_file
#
# This function checks for a configuration file.
# If this file exists and is not world writable
# the function returns it's location path as string.
#
# If the configuration file is world writable we
# print out an error message and die.
#
# If no configuration is available we return undef.
#
sub which_config_file {

    my ($in_file_dir, $in_file_name) = @_;      # directory and file
    my $sub = "WHICH_CONFIG_FILE";              # subroutine name

    my $location;
    my @location_array = (

        "$ENV{'HOME'}/.$in_file_name",                  # ~/   -> .<FILE>
        "$ENV{'HOME'}/.$in_file_dir/$in_file_name",     # ~/   -> /.<DIR> -> <FILE>
        "/etc/$in_file_name",                           # /etc -> <FILE>
        "/etc/$in_file_dir/$in_file_name"               # /etc -> /<DIR>  -> <FILE>
    );

    foreach $location (@location_array) {

        # if file available
        if (-r $location) {

            # if config file is world writable
                if ((stat($location))[2] & 02) {

                        # print error and die
                        die print_error ($sub, "$location should not be world-writable");
            }

            return $location;
        }
    }

    return undef;
}

# }}}
# {{{ sub read_config_file_and_options{}

#
# read_config_file_and_options
#
# This function reads the configuration file
# and returns a HASH with all configurations.
#
sub read_config_file_and_options {

    my ($in_config_file_location) = @_;         # config file path
    my $sub = "READ_CONFIG_FILE_AND_OPTIONS";   # subroutine name
    my $oldRS;                                  # file holder
    my $rc;                                     # file holder

    my $reference;
    my %confhash;
    my %refhash;

    # This variables are found in the configuration file and
    # need to be declared here in order to follow `use strict`
    my $config_cdr_dev;
    my $config_cdr_speed;
    my $config_cdr_dummy;
    my $config_cdr_dao;
    my $config_cdr_burnfree;
    my $config_temp_dir;
    my $config_mp3_decoder;
    my $config_check_lowercase;
    my $config_check_spaces;
    my $config_check_brackets;
    my $config_check_quotes;
    my $config_check_questions;
    my $config_wav_normalize;

    # This options MUST be defined in the configuration file
    # or MUST be passed to the program as a command line option
    %refhash = (

        "config_cdr_dev"            => \$config_cdr_dev,
        "config_cdr_speed"          => \$config_cdr_speed,
        "config_cdr_dummy"          => \$config_cdr_dummy,
        "config_cdr_dao"            => \$config_cdr_dao,
        "config_cdr_burnfree"       => \$config_cdr_burnfree,
        "config_temp_dir"           => \$config_temp_dir,
        "config_mp3_decoder"        => \$config_mp3_decoder,
        "config_check_lowercase"    => \$config_check_lowercase,
        "config_check_spaces"       => \$config_check_spaces,
        "config_check_brackets"     => \$config_check_brackets,
        "config_check_quotes"       => \$config_check_quotes,
        "config_check_questions"    => \$config_check_questions,
        "config_wav_normalize"      => \$config_wav_normalize
    );

    open('RC', $in_config_file_location);   # open file

        $oldRS = $/;
        undef $/;
        $rc = <RC>;

    close('RC');    # close file

    unless(defined eval $rc) {      # read data and check syntax

        die print_error($sub, "Found a syntax error in $in_config_file_location:\n $@");
    }

    $/ = $oldRS;

    #
    # Procedure...
    #
    #   1. Check if option was given
    #   2. Check if config var was given
    #   3. Error
    #

    # cycle all config variable names
    foreach $reference (keys %refhash) {

        # $$foo holds the values from the config file
        my $foo = $refhash{$reference};
        # print "FOO: $$foo\n";

        # create option variable names
        my $reference2 = $reference;
        $reference2 =~ s/config/opt/i;

        # if option variable for `variable name` available
        if (defined $opthash{$reference2}) {

            $confhash{$reference} = $opthash{$reference2};

            if ($opthash{verbose}) {
                print " OPTS: $reference2 => $opthash{$reference2}\n";
            }

        # if config variable for `variable name` available
        } elsif (defined $$foo) {

            #
            # Return error if only blank spaces where defined for an option
            # in the configuration file, e.g.: $config_cdr_dev = " ", this is
            # different from $config_cdr_dev = "" which will be stooped later
            #
            # TODO  Add a regexp function which checks for one or more spaces
            #   or remove this option an let the functions check the values
            #
            if ($$foo eq " ") {

                die print_error($sub, "Just a blank value is set for option \"\$$reference\" in the\n   configuration file, you need to fix your configuration file now\n   or provide a correct value via the respective command line option.");

            } else {

                $confhash{$reference} = $$foo;

                if ($opthash{verbose}) {
                    print " CONF: $reference => $confhash{$reference}\n";
                }
            }

        # if variable was not defined anywhere
        } else {
            $reference =~ s/config_//i;
            die print_error($sub, "Option \"$reference\" was neither defined in the configuration file\n   and neither as an option, please define this value somewhere.");
        }
    };

    return %confhash;
}

# }}}
# {{{ sub fetch_file_array{}

#
# fetch_file_array
#
# This function checks if the file specified via
# ARGV exists and is a valid MP3 or OGG file via
# the check_file_type function.
#
# If the file is valid it will be copied to the
# files_array which will be returned by this function.
#
# This function can only return a good value in all bad
# cases it calls the print_error function and dies.
#
sub fetch_file_array {

    my $sub = "FETCH_FILE_ARRAY";   # subroutine name
    my @file_array;                 # the file array
    my $i = 0;                      # a simple counter

    foreach my $file (@ARGV) {

        if (-r $file) {

            if (defined check_file_type($file)) {

                $file_array[$i] = $file;
                $i++;

            } else {
                die print_error ($sub, "The file \"$file\" is not a valid MP3, OGG or FLAC file.");
            }

        } else {
                die print_error ($sub, "The file \"$file\" does not exist.");
        }
    }

    return @file_array;
}

# }}}
# {{{ sub fetch_atip_time{}

#
# fetch_atip_time
#
# This function fetches the ATIP of the CD/RW Disc.
# We use the cdrecord application to get this infos.
#
# We return the ATIP time in seconds.
#
sub fetch_atip_time {

    my ($config_cdr_dev) = @_;
    my $sub = "FETCH_ATIP_TIME";
    my $atip_sec;
    my $atip_min;
    my $atip_time;

    if (!$config_cdr_dev) {

        my $r_message = "You must specify the CDR/W drive id in the configuration file or with\n   the option --drive x.y.z. Execute: % cdrecord \-scanbus for further details.";
        die print_error($sub, $r_message);
    }

    # open file and use "cdrecord -atip" to fetch ATIP info
    open(CDINFO,"cdrecord -atip dev=".$config_cdr_dev." 2>&1 |");

        while (<CDINFO>) {

            # fetch until timeout
            next unless (/out:.+\((\d+):(\d+)/);

            # get the time vars
            $atip_min = $1;
            $atip_sec = $2;
        }

    close CDINFO;

    # die if no CDR in Drive
    unless ($atip_sec && $atip_min) {

        my $r_message = "No CD-R/W in the CDR/W writer.";
        die print_error ($sub, $r_message);
    }

    #
    # Print out the ATIP time
    # TODO : This should be printed out in verbose mode.
    #
    # printf " * %d:%.2d minutes available on CDR/W.\n",$atip_min,$atip_sec;

    # compose ATIP time in secs
    $atip_time = ($atip_min * 60) + $atip_sec;

    return $atip_time;
}

# }}}
# {{{ sub check_file_type{}

#
# check_file_type
#
# This function returns the extension of a file
# or undef if the file is not valid.
#
sub check_file_type {

    my ($file) = @_;
    my $sub = "CHECK_FILE_TYPE";

    my $ext;

    my $mm = new File::MMagic;
    my $fh = new FileHandle "< $file";
    my $res = $mm->checktype_filehandle($fh);

    #
    # Secrets of File::MMagic, this works only on Debian...
    #
    # if ($res eq "application/octet-stream"), we need to add
    # if ($res eq "audio/mpeg"), for Red Hat and maybe also some other distro.
    #
    if ($res eq "application/octet-stream" || "audio/x-wav" || "audio/mpeg") {

        $ext = (split(/\./,$file))[-1];

        if ($ext eq "wav"||"mp3"||"ogg"||"flac") {

            return $ext;
        }
    }

    return undef;
}

# }}}
# {{{ sub check_file{}

#
# check_file
#
# This function checks the filename of each $file
# in the @file_array using a specified syntax command,
# this command is passed to the functions as parameter.
#
sub check_file {

    my ($config_check_lowercase, $config_check_spaces, $config_check_brackets, $config_check_quotes, $config_check_questions, @file_array) = @_;
    my $sub = "CHECK_FILE";         # subroutine name

    # Browse through the MP3 Array
    foreach my $file (@file_array) {

        # Make copy of actual file
        my $file_tmp = $file;

        if ($config_check_lowercase) {

            # Lowercase in $file_tmp
            $file_tmp =~ tr/A-Z/a-z/;
        }

        if ($config_check_spaces) {

            # Remove spaces in $file_tmp
            $file_tmp =~ tr/" "/"_"/;
        }

        if ($config_check_brackets) {

            # Remove brackets in $file_tmp
            $file_tmp =~ tr/"("/"_"/;
            $file_tmp =~ tr/")"/"_"/;
        }

        if ($config_check_quotes) {

            # Remove quotes in $file_tmp
            $file_tmp =~ tr/"'"/"_"/;
            $file_tmp =~ tr/'"'/"_"/;
        }

        if ($config_check_questions) {

            # Remove questionmarks in $file_tmp
            $file_tmp =~ tr/"?"/"_"/;
        }

        # If original MP3 name is different from lowercased copy
        if ($file_tmp ne $file) {

            # Check if there's already a file with the lowercased name
            if (-e $file_tmp) {

                # If there's already a file which
                # is the original, the same, rename it.
                if (compare ("$file", "$file_tmp") == 0) {

                    # Rename the file
                    rename ("$file" , "$file_tmp");

                    # Update the @file_array
                    $file = $file_tmp;

                # The File is not the same, big problem !
                # die with error message.
                } else {

                    #
                    # TODO
                    # Solve this extreme error case.
                    #

                    print "\n";
                    print "Can't Rename $file\n";
                    print "there is already a $file_tmp named file\n";
                    print "which is not equal\n";
                    print "\n";

                    exit 1;
                }

            # If there's no file with the lowercased name
            } else {

                # Rename the file
                rename ("$file" , "$file_tmp");

                # Update the @file_array
                $file = $file_tmp;
            }
        }
    }

    return @file_array;
}

# }}}
# {{{ sub fetch_file_time{}

#
# fetch_file_time
#
# This is a generic function which calculates the
# total time of all MP3/OGG/FLAC/WAV files in the @file_array.
#
# The function calculates also the total fudge factor time
# which is the sum of all the seconds between two audio tracks,
# I've set it's value to 4 seconds.
#
# We have four dependences for this function:
#
#   DEPENDENCY MODULE       DEBIAN PACKAGE
#   -------------------------------------------------
#   MPEG::MP3Info           libmp3-info-perl
#   Ogg::Vorbis::Header     libogg-vorbis-header-perl
#   Audio::FLAC::Header     libaudio-flac-header-perl
#   Audio::Wav              libaudio-wav-perl
#
sub fetch_file_time {

    my (@file_array) = @_;
    my $sub = "FETCH_FILE_TIME";    # subroutine name

    my $mp3info;                    # get_mp3info() return
    my $ogg;                        # holds the Ogg::Vorbis::Header object
    my $oggtime;                    # holds the ogg's length in seconds
    my $flac;                       # holds the Audio::FLAC object
    my $flactime;                   # holds the flac's length in seconds
    my $wav;                        # holds the wav object
    my $wavtime;                    # holds the wav's length in seconds

    my $tot_secs;                   # sum of all file plus fudge seconds

    my $tot_min;                    # calculated file length in minutes
    my $tot_sec;                    # calculated file length in seconds

    my $fudge = "4";                # fudge time value (preference)
    my $tot_fudge_secs;             # sum of all fudge seconds

    my $tot_fudge_min;              # calculated fudge length in minutes
    my $tot_fudge_sec;              # calculated fudge length in seconds

    # Cycle the file array
    foreach my $file (@file_array) {

        # if we treat a WAV file
        if (check_file_type($file) eq "wav") {

            #
            # We use Audio::Wav here to fetch information from the wav file
            #
            $wav = new Audio::Wav->read($file);
            $wavtime = $wav->length_seconds();

            $tot_secs += $wavtime + $fudge;

        # if we treat a MP3 file
        } elsif (check_file_type($file) eq "mp3") {

            # Fetch informations about the MP3 file via libmp3-info-perl.
            $mp3info = get_mp3info($file);

            # Sum total seconds plus fudge factor
            $tot_secs += ($mp3info->{MM}*60) + $mp3info->{SS} + $fudge;

        # if we treat a OGG file
        } elsif (check_file_type($file) eq "ogg") {

            #
            #   We use Ogg::Vorbis::Header here
            #   Example: http://search.cpan.org/src/FOOF/libvorbis-perl-0.02/test.pl
            #
            $ogg = Ogg::Vorbis::Header->load($file);
            $oggtime = $ogg->info("length");

            $tot_secs += $oggtime + $fudge;

        # if we treat a FLAC file
        } elsif (check_file_type($file) eq "flac") {

            #
            #   We use Audio::FLAC here
            #
            $flac = Audio::FLAC::Header->new($file);
            $flactime = $flac->{trackTotalLengthSeconds};

            $tot_secs += $flactime + $fudge;
        }

        # Calculating the total fudge factor time
        $tot_fudge_secs += ($fudge);
    }

    # Calculating $tot_min and $tot_sec
    $tot_min = int $tot_secs/60;
    $tot_sec = $tot_secs % 60;

    # Calculating $tot_fudge_min and $tot_fuge_sec
    $tot_fudge_min = int $tot_fudge_secs/60;
    $tot_fudge_sec = $tot_fudge_secs % 60;

    # Compose the MP3 time in secs
    # $file_time = $tot_secs;

    # TODO
    # This should become a verbose option
    # Print out the file time
    # printf " * %d:%.2d \(+ %d:%.2d fudge\) minutes of audio.\n", $tot_min, $tot_sec, $tot_fudge_min, $tot_fudge_sec;

    return $tot_secs;
}

# }}}
# {{{ sub compare_file_atip{}

#
# compare_file_atip
#
# This function compares the total time of the MP3/OGG/FLAC/WAV files
# with the ATIP time and warns you the songs won't fit on CDR/W.
#
sub compare_file_atip {

    my ($in_atip_time, $in_file_time) = @_;
    my $key;
    my @fifo;

    my $in_atip_min = int $in_atip_time/60;
    my $in_file_min = int $in_file_time/60;

    # if file time is shorter than ATIP time
    if ($in_file_time <= $in_atip_time) {

        print " * The audio files fit on CD\/RW\ (CD: $in_atip_min min / File: $in_file_min min\)\n";

    } else {

        print " * WARNING: The files won\'t fit on CD\/RW \(CD: $in_atip_min min / File: $in_file_min min\)\n";
    }

    print "   Do you want to continue? ([Any Key/n]) ";

    open(TTY,"</dev/tty");
    ReadMode "raw";
    $key = ReadKey 0, *TTY;
    ReadMode "normal";

    print "\n";
    (print "   Well then, bye bye human -- See ya.\n") && (exit 0) if ($key eq ('n'||'N'));
}

# }}}
# {{{ sub check_temp{}

#
# check_temp
#
# This function checks if the temp directory exists,
# else it will be created with appropriate permissions.
#
# If the temp directory already exists we will return
# 1 else we create it but return 0 in order to alert
# the code segment which recalls this function.
#
# If $config_temp_dir is a file we can't create the
# temp directory so we print an error and die.
#
sub check_temp {

    my ($def_appname, $config_temp_dir) = @_;
    my $sub = "CHECK_TEMP";

    if (-x "$config_temp_dir") {    # if it's a directory and exists: OK

        return 1;

    } else {

        if (-f "$config_temp_dir") {    # if it's a file: fatal error, die

            die print_error($sub, "$config_temp_dir is a file, please remove it first, then launch $def_appname again.");

        } else {    # if it's not available: create it

            mkpath($config_temp_dir, 0, 0755);
        }

        return 0;
    }
}

# }}}
# {{{ sub check_for_wav_file{}

#
# check_for_wav_file
#
# With this function we check if a wav file exists for a
# given MP3/OGG or whatever file we are going to decode.
#
# If true ask user what todo: Use the present decoded WAV
# file or re decode the compressed file (The last option is
# safe cause WAV file could eventually not be complete)
#
# Return 0 if user wants to decode again, else 1.
#
sub check_for_wav_file {

    my ($i, $file, $wavpath) = @_;
    my $key;

    if (-f $wavpath) {

        print "\r                                                                                ";
        print "\r   File $file already decoded, decode again? ([Any Key/n]) ";

        open(TTY,"</dev/tty");
        ReadMode "raw";
        $key = ReadKey 0, *TTY;
        ReadMode "normal";

        if ($key eq ('n'||'N')) {

            return 1;

                } else {

            print "\r                                                                                ";
            printf "\r   file %d: $file", $i+1;

            # Remove the file in order to be re decoded
            unlink($wavpath);
            return 0;
        }
    }
}

# }}}
# {{{ sub file_decode{}

#
# file_decode
#
# This function decodes all the MP3/OGG/FLAC/WAV files to wav files,
# we also create the wav array and return it if all decoding
# processes were executed clean, else we print out an error
# message and die.
#
sub file_decode {

    my ($config_wav_normalize, $config_temp_dir, $config_mp3_decoder, @file_array) = @_;
    my $sub = "FILE_DECODE";
    my $wavpath;
    my $tmpFileType;    # Stores the temporary file type from check_file_type().
    my $i = 0;
    my @wav_array;
    my $cmd;
    my $rc;


    foreach my $file (@file_array) {

        $wavpath = (basename $file);
        $tmpFileType = check_file_type($file);  # Determinate the file type.

        #
        # TODO
        # clear the line with 80 chars, this could be done much better (x75, see perldoc perldop)
        #
        print "\r                                                                                ";
        printf "\r   file %d: $wavpath", $i+1;

        if ($tmpFileType eq "wav") {

            if ($config_wav_normalize) {

                #
                # In this case we need to copy the WAV file from it's
                # current location to the temp dir in order to be able
                # to run normalize on it.
                #
                # TODO  Add some control routine to check if copy was successful.
                #
                printf "\r   file %d: $wavpath", $i+1;
                copy ($wavpath, $config_temp_dir."/".$wavpath);
                $wav_array[$i] = $config_temp_dir."/".$wavpath;
                $i++;

            } else {

                #
                # Leave WAV file where it is and just update the WAV array.
                #
                $wav_array[$i] = $wavpath;
                $i++;
            }

        } elsif ($tmpFileType eq "mp3") {

            $wavpath =~ s/mp3/wav/i;
            $wavpath = $config_temp_dir."/".$wavpath;

            if (!check_for_wav_file($i, $file, $wavpath)) {

                # unofficial Perl case
                for ($config_mp3_decoder) {

                    # same syntax
                    if (/mpg321/ || /mpg123/) {

                        $cmd = "$config_mp3_decoder --rate 44100 --stereo -w \"$wavpath\" \"$file\" 2>&-";

                        #
                        # TODO  Finish to implement toolame decoder support.
                        #       toolame is available on Debian GNU/Linux and is free.
                        #       lame is not free instead so toolame is the case (perhaps).
                        #
                        # } elsif (/toolame/) {
                        #
                        #   $cmd = "toolame --decode $file $wavpath";
                        #

                    } else {

                        die print_error($sub, "Configure a valid mp3 decoder.")
                    }
                }

                $rc = system($cmd);

                if (!$rc) {

                    $wav_array[$i] = $wavpath;
                    $i++;

                } else {

                    die print_error($sub, "$file was not correctly decoded,\n   $config_mp3_decoder error code $rc.");
                }

            } else {

                $wav_array[$i] = $wavpath;
                $i++;
            }

        } elsif ($tmpFileType eq "ogg") {

            $wavpath =~ s/ogg/wav/i;
            $wavpath = $config_temp_dir."/".$wavpath;

            if (!check_for_wav_file($i, $file, $wavpath)) {

                #
                #   Function system returns the exit code, exec not.
                #
                #   ogg123 exits with error code 256 if the destination
                #   file already exists, mpg321 does not.
                #
                #   Here we can also use oggdec which is available, as ogg123,
                #   in the Debian vorbis-tools package. Which one is better?
                #
                #       % oggdec foo.wav will decode to foo.wav
                #
                $cmd = "ogg123 -d wav -f \"$wavpath\" \"$file\" 2>&-";
                $rc = system($cmd);

                if (!$rc) {

                    $wav_array[$i] = $wavpath;
                    $i++;

                } else {

                    die print_error($sub, "$file was not correctly decoded to $wavpath\n   ogg123 error code $rc, try to remove $wavpath if exists.");
                }

            } else {

                $wav_array[$i] = $wavpath;
                $i++;
            }

        } elsif ($tmpFileType eq "flac") {

            $wavpath =~ s/flac/wav/i;
            $wavpath = $config_temp_dir."/".$wavpath;

            if (!check_for_wav_file($i, $file, $wavpath)) {

                $cmd = "flac -d  -s \"$file\" -o \"$wavpath\" 2>&-";
                $rc = system($cmd);

                if (!$rc) {

                    $wav_array[$i] = $wavpath;
                    $i++;

                } else {

                    die print_error($sub, "$file was not correctly decoded to $wavpath\n   flac error code $rc, try to remove $wavpath if exists.");
                }

            } else {

                $wav_array[$i] = $wavpath;
                $i++;
            }
        }
    }

    return @wav_array;
}

# }}}
# {{{ sub wav_normalize{}

#
# wav_normalize
#
# This function recalls normalize with the -m (mix)
# option in order to standardize the max volume level
# for all wav files in the temp directory, if execution
# is successfully we'll go back to main without any return
# value else we die with an error message.
#
sub wav_normalize {

    my ($config_temp_dir) = @_;
    my $sub = "WAV_NORMALIZE";
    my $cmd;
    my $rc;

    $cmd = "normalize-audio -m $config_temp_dir/*.wav 2>&-";
    $rc = system($cmd);

    if ($rc) {

        die print_error($sub, "the normalizing process was not successful,\n   normalize error code $rc.");
    }
}

# }}}
# {{{ sub burn{}

#
# burn
#
# This function burns all wav files to a CD/RW disc
# using the cdrecord application, if cdrecord returns
# an error code we print out an error message and die,
# else we go back to main without any return value.
#
sub burn {

    my ($config_cdr_dev, $config_cdr_speed, $config_cdr_dummy, $config_cdr_dao, $config_cdr_burnfree, @wav_array) = @_;
    my $sub = "BURN";
    my $burn_wav_list;
    my $cdrecord_options;
    my $cmd;
    my $rc;

    foreach my $wav (@wav_array) {

        $burn_wav_list = $burn_wav_list." \"$wav\"";
    }

    $cdrecord_options = "dev=$config_cdr_dev speed=$config_cdr_speed gracetime=2 -eject -pad -audio -silent";

    if ($config_cdr_dummy) {

        $cdrecord_options = $cdrecord_options." -dummy";
    }

    if ($config_cdr_dao) {

        $cdrecord_options = $cdrecord_options." -dao";

    } else {

        $cdrecord_options = $cdrecord_options." -tao";
    }

    if ($config_cdr_burnfree) {

        $cdrecord_options = $cdrecord_options." driveropts=burnfree";
    }

    # Let's burn folks -- Finally!
    $cmd = "cdrecord $cdrecord_options $burn_wav_list >/dev/null 2>&1";
    $rc = system($cmd);

    if ($rc) {

        die print_error($sub, "the burn process was not successful,\n   cdrecord error code $rc.");
    }
}

# }}}
# {{{ sub flush_temp{}

#
# flush_temp
#
# This Function removes all wav files of the WAV array
# from the temporary directory and returns the summarized
# number of all removed files. The temporary directory
# itself won't be remove because user could have set it
# to something like / which is anyway quite lame.
#
sub flush_temp {

    my (@wav_array) = @_;
    my $i = 0;

    foreach my $wav (@wav_array) {

        unlink($wav);
        $i++;
    }

    return $i;
}

# }}}
# {{{ main{}

#
# main
#
sub main {

    # {{{ %def

    my %def = (

        'appname'           => 'mp3roaster',
        'realname'          => 'MP3Roaster',
        'description'       => 'A Perl hack for burning audio CDs out of MP3/OGG/FLAC/WAVs',
        'cvs_revision'      => '$Revision: 1.22 $',
        'version'           => '0.3.0',
        'bug_contact'       => 'eim@users.sourceforge.net',
        'author'            => 'Ivo Marino',
        'author_email'      => 'eim@users.sourceforge.net',
        'co_author'         => 'Lorenzo Taylor',
        'co_author_email'   => 'lorenzo1@users.sourceforge.net',
        'copyright_time'    => '2002-2005'
    );

    # }}}

    my $config_file_location;
    my %confhash;
    my @file_array;
    my @wav_array;

    if (@ARGV) {

        print "$def{appname} $def{version} ($def{cvs_revision}).\n\n";

        #
        # 1: check for configuration file
        #
        if (defined ($config_file_location = which_config_file($config_file_dir, $config_file_name))) {

            #
            # 2: read configuration file and put all vars in a hash
            #
            print " * Options [ ";
            %confhash = read_config_file_and_options($config_file_location);

            # print "dev ($confhash{config_cdr_dev}) ";
            print "$confhash{config_cdr_speed}x ";

            print "dummy " if ($confhash{config_cdr_dummy});
            print "dao " if ($confhash{config_cdr_dao});
            print "burnfree " if ($confhash{config_cdr_burnfree});
            print "lowercase " if ($confhash{config_check_lowercase});
            print "spaces " if ($confhash{config_check_spaces});
            print "brackets " if ($confhash{config_check_brackets});
            print "quotes " if ($confhash{config_check_quotes});
            print "questionmarks " if ($confhash{config_check_questions});
            print "normalize " if ($confhash{config_wav_normalize});

            print "]\n";

            #
            # 3: create the file array
            #
            @file_array = fetch_file_array;

            print " * Processing ";

            if (($#file_array) > 0) {

                printf "%d files.\n", $#file_array+1;

            } else {

               print "1 file.\n";
            }

            #
            # 4: check files (optional)
            #
            if (($confhash{config_check_lowercase}) || ($confhash{config_check_spaces})  || ($confhash{config_check_brackets}) || ($confhash{config_check_quotes}) || ($confhash{config_check_questions})){

                print " * Checking file [ ";

                @file_array = check_file($confhash{config_check_lowercase}, $confhash{config_check_spaces}, $confhash{config_check_brackets}, $confhash{config_check_quotes}, $confhash{config_check_questions}, @file_array);

                print "]\n";
            }

            #
            # 5: check if files fit on CD/RW
            #
            compare_file_atip(fetch_atip_time($confhash{config_cdr_dev}), fetch_file_time(@file_array));

            #
            # 6: check the temp directory
            #
            if (check_temp($def{appname}, $confhash{config_temp_dir})) {

                print " * Temporary directory available.\n";

            } else {

                print " * Temporary directory created.\n";
            }

            #
            # 7: decode MP3/OGG/FLAC files and get the wave array
            #
            print " * Decoding MP3/OGG/FLAC files to wav...\n";

            @wav_array = file_decode($confhash{config_wav_normalize}, $confhash{config_temp_dir}, $confhash{config_mp3_decoder}, @file_array);

            #
            # TODO
            # clear the line with 80 chars, this could be done much better (x75, see perldoc perldop)
            #
            print "\r                                                                                ";

            if (($#wav_array) > 0) {

                printf "\r   done, %d files successfully decoded.\n", $#wav_array+1;

            } else {

                print  "\r   done, 1 file successfully decoded.\n";
            }

            #
            # 8: normalize all the WAV files
            #
            if ($confhash{config_wav_normalize}) {

                print " * Normalizing wav files, please wait... ";

                wav_normalize($confhash{config_temp_dir});

                print "done.\n";
            }

            #
            # 9: now let's BURN
            #
            # TODO  Debug this printf..
            #
            # printf " * Burning %d files, please wait... ", ($#wav_array+1);
            #
            print " * Burning files, please wait... ";
            burn($confhash{config_cdr_dev}, $confhash{config_cdr_speed}, $confhash{config_cdr_dummy}, $confhash{config_cdr_dao}, $confhash{config_cdr_burnfree}, @wav_array);
            print "done.\n";

            #
            # 10: flush all wav files in temporary directory
            #
            my $f = flush_temp(@wav_array);

            if ($f > 1) {

                printf " * Removed %d wav files.\n\n", $f;

            } else {

                print " * Removed 1 wav file.\n\n";
            }

            exit 0;

        } else {

            die print_error(undef, "MP3Roaster configuration file is not available.");
        }

    } elsif ($opthash{version}) {

        showVersion($def{appname}, $def{version}, $def{cvs_revision}, $def{author},
            $def{author_email}, $def{co_author}, $def{co_author_email}, $def{copyright_time});

    } elsif ($#ARGV < 1 || $opthash{help}) {

        showHelp($def{appname}, $def{description}, $def{bug_contact});
    }
}

# }}}

main();
exit 0;

__END__

# {{{ PerlDoc

=head1 NAME

mp3roaster - A Perl hack for burning audio CDs out of MP3/OGG/FLAC/WAVs

=head1 SYNOPSIS

B<mp3roaster> [OPTION]... C<MP3/OGG/FLAC/WAV files>

=head1 DESCRIPTION

B<mp3roaster>
A Perl hack for burning audio CDs out of MP3, OGG VORBIS and FLAC files.
The main highlights of this application are an easy to use command line syntax
and automatic volume leveling support for best audio CD quality.

=head1 ENVIRONMENT

MP3Roaster should run on all Unix like operating systems which have Perl and
cdrecord installed. It has been developed on Debian GNU/Linux.

=head1 OPTIONS

All options have been imported, now we should add specific descriptions
for each option.

=over 4

=item B<-d, --dev>

CDR device to use

=item B<-s, --speed>

Burn speed

=item B<-d, --dummy>

Burn with laser off

=item B<-a, --dao>

Burn in disk-at-once (DAO) mode for gapless recording

=item B<-b, --burnfree>

Turn on Buffer Underrun Free writing

=item B<-t, --temp>

Temporary directory

=item B<-m, --mp3dec>

MP3 decoder to use

=item B<-l, --lowercase>

Convert filenames to lowercase

=item B<-S, --spaces>

Replace spaces with underscores

=item B<-B, --brackets>

Replace brackets with underscores

=item B<-q, --quotes>

Replace quotes with underscores

=item B<-Q, --questions>

Replace questionmarks with underscores

=item B<-n, --normalize>

Normalize WAV files before burning

=item B<-v, --verbose>

Enable verbose output

=item B<-h, --help>

Show the help screen

=item B<-V, --version>

Show version and infos

=back

=head1 RETURN VALUE

B<mp3roaster> returns 0 on success, 1 on error.

=head1 DIAGNOSTICS

This has to be written yet.

=head1 EXAMPLES

Once correctly configured MP3Roaster is very easy to use, just launch it
from the command line with a bunch of compressed audio files, here is a
short example:

    % mp3roaster "Root Dance.mp3" Free\ Software.flac bar.ogg Decoded.wav

This will burn Root\ Dance.mp3, Free\ Software.flac bar.ogg and Decoded.wav
on your audio CD preserving the song order as specified on the command line.

Here is another example showing the usage of command line options like
the dummy option:

    % mp3roaster -d "Root Dance.mp3" Free\ Software.flac bar.ogg Decoded.wav

    This will do the same job as above but with the laser of your
    CD writer turned off, so no data will be really written.

You see MP3Roaster is really easy to use, this was one of my main goals
while I've written the code: Keep it simple ;)

=head1 FILES

MP3Roaster can be configured through a system wide and a personal configuration
file. When you run MP3Roaster it will first check for your personal
configuration file in your home directory, if no one is found it will fall-back
to the system wide configuration files placed in /etc.

By default MP3Roaster installs the configuration files in /etc/mp3roaster, but
there are other possible file locations for the configuration file:

    . System wide configuration in /etc

        . /etc/mp3roaster/mp3roasterrc      (DEFAULT)
        . /etc/mp3roasterrc

    . Personal configuration in your home

        . ~/.mp3roaster/mp3roasterrc
        . ~/.mp3roasterrc

So if you want to have personal MP3Roaster configuration file just copy
the system wide configuration file to your home directory and edit it.

=head1 CAVEHEATS

None actually.

=head1 BUGS

There may be some minor troubles regarding file names, feel free to report
any bugs you may encounter (In fact you shouldn't).

=head1 NOTES

There are currrently no special notes.

=head1 SEE ALSO

cdrecord.

=head1 AUTHOR

=over 4

=item Ivo Marino <eim@users.sourceforge.net>

=item Lorenzo Taylor <lorenzo1@users.sourceforge.net>

=back

=head1 HISTORY

Take a look at the ChangeLog for now.

=cut

# }}}

# vim: ts=8:sw=4:sts=4:et:foldmethod=marker:
