#!/usr/bin/perl

# mp32ogg
# Author: Nathan Walp <[email protected]>
# This software released under the terms of the Artistic License
# <http://www.opensource.org/licenses/artistic-license.html>

# ChangeLog
#
# 0.11
#     * Have output reflect change in license (doh)
#     * Check ogg after conversion using ogginfo, see if the file was truncated
#       (which would indicate failure)
#     * Fixed comment tagging "COMMENT=" gets prepended now
#     * Moved from bitrate setting to quality setting, and removed the bitrate
#       option
#     * Allow for the WinAMP genres
#
# 0.10
#     * Now properly escapes the commands so files with special characters are
#       handled correctly.
# 0.9
#     * Big changes.  For starters, a new license.  mp32ogg is now released
#       under the terms of the Artistic License.  If you're happy about the
#       change, feel free to praise me.  If you're pissed, /dev/null is a good
#       place to send the flames.
#     * Next is the addition of the extra tag requested by the OGG guys
#       themselves.  transcoded=mp3;<bitrate> will be a new tag on all converted
#       files.  Hopefully this will allow for conversion, while not hurting the
#       quality reputation of the OGG format.
#     * Finally, genre and year are now grabbed from the mp3 id3 tag, and
#       added to the ogg.
# 0.8.1
#     How do I always manage to get hit with the stupidest of typo-bugs?
#     Anything encoded with 0.8 is gonna have an incorrect artist.   Sorry :-/
#     Oh, and thanks to Horst Henkler <[email protected]> for being the first
#     to speak up about it.
# 0.8
#     * change -q to --quiet, since oggenc 1.0RC2 decided to break backwords
#       compatibility.  Changed the new raw options as well.  Basically, it
#       now works with the newer oggenc, as well as the old version.
# 0.7.1
#     * I HATE it when I forget to increment the version number ;-)
# 0.7
#     * John Flinchbaugh <[email protected]> sent me a handy patch to detect and
#       use the frequency of the mp3 files, as well as to detect stereo vs. mono
#       and encode the ogg appropriately.
# 0.6.1
#     * there's a reason you don't play with code without sleep and after an
#       exam. Thomas Riemer <[email protected]> was nice
#       enough to send me a patch fixing a bug introduced with --verbose.  If
#       you didn't specify --verbose in 0.6.0, no tag info got saved.  Thanks
#       Thomas.
# 0.6.0
#     thanks to David Mohr <[email protected]> for the patch adding:
#     * extract the bitrate from the mp3 and use it to encode the ogg file
#     * added the verbose option so that the user can decide whether he
#       wants output or not
# 0.5.1
#     * fixed another minor --rename bug, it now strips .ogg off of the rename
#       file like it's supposed to
#     * fixed relative directory bug
# 0.5
#     * fixed --rename to leave filenames the same when there's not enough ID3
#       info to base a new name on.
#     * changed errors from print to warn, so they get written to stderr
#     * added creation of directories for --rename when necessary
# 0.4
#     * conversion of special characters to _ in --rename filenames
#     * added --no-replace to stop that
#     * added --lowercase to make all characters in --rename filnames lowercase
# 0.3
#     * Massive code cleanup
#     * Support to recurse directories
#     * --delete option added
#     * --rename option added
# 0.2
#     * Made mpg123 and oggenc quiet, and added writing of ID3 Tags to output
# 0.1
#     First Release

$version = "v0.11";

use MP3::Info;
use File::Find ();
use File::Basename;
use Getopt::Long;
use String::ShellQuote;

use_winamp_genres();

$oggenc  = "/usr/bin/oggenc";
$ogginfo = "/usr/bin/ogginfo";
$mpg123  = "/usr/bin/mpg123";

print "mp32ogg $version\n";
print "(c) 2000-2002 Nathan Walp\n";
print "Released without warranty under the terms of the Artistic License\n\n";


GetOptions("help|?",\&showhelp,
               "delete",
               "rename=s",
               "lowercase",
               "no-replace",
               "verbose",
               "<>", \&checkfile);

sub showhelp() {
       print "Usage: $0 [options] dir1 dir2 file1 file2 ...\n\n";
       print "Options:\n";
       print "--delete                 Delete files after converting\n";
       print "--rename=format          Instead of simply replacing the .mp3 with\n";
       print "                         .ogg for the output file, produce output \n";
       print "                         filenames in this format, replacing %a, %t\n";
       print "                         and %l with artist, title, and album name\n";
       print "                         for the track\n";
       print "--lowercase              Force lowercase filenames when using --rename\n";
       print "--verbose                Verbose output\n";
       print "--help                   Display this help message\n";
       exit;

}




sub checkfile() {
       my $file = shift(@_);
       if(-d $file) {
               File::Find::find(\&findfunc, $file);
       }
       elsif (-f $file) {
               &ConvertFile($file);
       }
}

sub findfunc() {
       $file = $_;
       ($name,$dir,$ext) = fileparse($file,'\.mp\d');
       if((/\.mp\d/,$ext) && -f $file) {
               &checkfile($file);
       }
}

sub ConvertFile() {
       my $mp3file = shift(@_);
       my $delete = $opt_delete;
       my $filename = $opt_rename;
       my $lowercase = $opt_lowercase;
       my $noreplace = $opt_no_replace;
       my $verbose = $opt_verbose;

       $info = get_mp3tag($mp3file);
       $fileinfo = get_mp3info($mp3file);

       $_ = $filename;

       my $channels = 2;       # default to stereo
       if ($fileinfo->{MODE} == 3) {
               $channels = 1;  # set to mono if single channel mode
       }

       my $frequency = ($fileinfo->{FREQUENCY}*1000);
       if ($frequency == 0) {
               $frequency = 44100;             # default to 44100
       }

       $mp3bitrate = $fileinfo->{BITRATE};
       if($mp3bitrate ne "") {
          if($mp3bitrate > 256) {
             $quality = 8;
          } elsif($mp3bitrate > 192) {
             $quality = 7;
          } elsif($mp3bitrate > 128) {
             $quality = 6;
          } else {
             $quality = 5;
          }
       } else {
          $quality = 5;
          print "MP3::Info didn't report the bitrate... weird. Corrupt MP3 file? Bug?\n";
       }
       if($filename eq "" ||
               ((/\%a/) && $info->{ARTIST} eq "") ||
               ((/\%t/) && $info->{TITLE} eq "") ||
               ((/\%l/) && $info->{ALBUM} eq "") ){

               if($filename ne "") {
                       warn "not enough ID3 info to rename, reverting to old filename.\n";
               }

               ($filename,$dirname,$ext) = fileparse($mp3file,'\.mp\d');
       }
       else {
               $filename =~ s/\%a/$info->{ARTIST}/g;
               $filename =~ s/\%t/$info->{TITLE}/g;
               $filename =~ s/\%l/$info->{ALBUM}/g;
               if($lowercase) {
                       $filename = lc($filename);
               }
               if(!$noreplace) {
                       $filename =~ s/[\[\]\(\)\{\}!\@#\$\%^&\*\~ ]/_/g;
                       $filename =~ s/[\'\"]//g;
               }
               ($name, $dir, $ext) = fileparse($filename, '.ogg');
               $filename = "$dir$name";
               $dirname = dirname($mp3file);

       }

       $oggoutputfile = "$filename.ogg";
       $newdir = dirname($oggoutputfile);

       # until i find a way to make perl's mkdir work like mkdir -p...
       system("mkdir -p $newdir");


       $infostring = "";

       print "Converting $mp3file to OGG...\n";
       if ($verbose) {
               print "Length: $fileinfo->{TIME}\t\tFreq: $fileinfo->{FREQUENCY} kHz\n";
               print "MP3 Bitrate: $mp3bitrate\tOGG Quality Level: $quality\n";

               print " Artist: $info->{ARTIST}\n";
               print "  Album: $info->{ALBUM}\n";
               print "  Title: $info->{TITLE}\n";
               print "   Year: $info->{YEAR}\n";
               print "  Genre: $info->{GENRE}\n";
               print "Track #: $info->{TRACKNUM}\n";
               print "Comment: $info->{COMMENT}\n";
       }

       if($info->{ARTIST} ne "") {
               $infostring .= " --artist " . shell_quote($info->{ARTIST});
       }
       if($info->{ALBUM} ne "") {
               $infostring .= " --album " . shell_quote($info->{ALBUM});
       }
       if($info->{TITLE} ne "") {
               $infostring .= " --title " . shell_quote($info->{TITLE});
       }
       if($info->{TRACKNUM} ne "") {
               $infostring .= " --tracknum " . shell_quote($info->{TRACKNUM});
       }
       if($info->{YEAR} ne "") {
               $infostring .= " --date " . shell_quote($info->{YEAR});
       }
       if($info->{GENRE} ne "") {
               $infostring .= " --comment " . shell_quote("genre=$info->{GENRE}");
       }
       if($info->{COMMENT} ne "") {
               $infostring .= " --comment " . shell_quote("COMMENT=$info->{COMMENT}");
       }

       $infostring .= " --comment " . shell_quote("transcoded=mp3;$fileinfo->{BITRATE}");


       $oggoutputfile_escaped = shell_quote($oggoutputfile);
       $mp3file_escaped = shell_quote($mp3file);
       $result = system("$mpg123 -q -s $mp3file_escaped 2>/dev/null | $oggenc -q $quality --quiet --raw --raw-rate=$frequency --raw-chan=$channels -o $oggoutputfile_escaped $infostring -");

       if(!$result) {
          open(CHECK,"$ogginfo $oggoutputfile_escaped |");
          while(<CHECK>)
          {
             if($_ eq "file_truncated=true\n")
             {
                warn "Conversion failed ($oggoutputfile truncated).\n";
                close CHECK;
                return;
             }
             elsif($_ eq "header_integrity=fail\n")
             {
                warn "Conversion failed ($oggoutputfile header integrity check failed).\n";
                close CHECK;
                return;
             }
             elsif($_ eq "stream_integrity=fail\n")
             {
                warn "Conversion failed ($oggoutputfile header integrity check failed).\n";
                close CHECK;
                return;
             }
          }
          close CHECK;
          print "$oggoutputfile done!\n";
          if($delete) {
             unlink($mp3file);
          }

       }
       else {
          warn "Conversion failed ($oggenc returned $result).\n";
       }
}