#!/usr/pkg/bin/perl

# mp32ogg
# Author: Nathan Walp <faceprint@faceprint.com>
# 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 <h.henkler@gmx.de> 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 <glynis@hjsoft.com> 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 <thomas.riemer@studserv.uni-stuttgart.de> 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 <squisher@web.de> 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/pkg/bin/oggenc";
$ogginfo = "/usr/pkg/bin/ogginfo";
$mpg123  = "/usr/pkg/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() {
	# force arg stringfication to get actual filename, not getopt::long
	# callback
	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";
	}
}
