#!/usr/bin/perl

# ogg2mp3
# Maintained by: James Ausmus <james.ausmus@gmail.com>
# Homepage: https://github.com/fithp/ogg2mp3
# Released under the GPLv2 license
#
# based on (most of):
#
# 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>
#
# and (the name and small bits of):
#
# ogg2mp3
# Author: David Ljung Madison <DaveSource.com>
# http://marginalhacks.com/
# License: http://MarginalHacks.com/License
#
#
# Inversion of mp32ogg was done by Mark Draheim <rickscafe.casablanca@gmx.net>
# as always with a helping hand from guidod
#
# 0.4 ogginfo fix thanks to Chris Dance
# 0.5 cli options thanks to Henry Gomersall 
# 0.5.1 Addition of endian-fix patch by Boris Peterson, modification headers to reflect new maintainership/homepage
# 0.6 Strictification done
# 0.6.1 Fix several bugs introduced in 0.6, add --debug option, fix lame --genre-list outputting to stderr bug - thanks to Peter Greenwood and Tobias Rehbein! Also add track number/total number of tracks to possible --rename options
#

my $version = "v0.6.1";

# TODO: use Ogg::Vorbis::Header::PurePerl when it becomes usable
# TODO: strictify the thing
use strict;
use File::Find ();
use File::stat;
use File::Basename;
use Getopt::Long;
use String::ShellQuote;

print "\n";
print " ------------------------------------------------------------------- \n";
print "  ogg2mp3 $version\n";
print "    based on mp32ogg (c) 2000-2002 Nathan Walp\n";
print "    inverted and adapted by Mark Draheim\n";
print "    Maintainership assumed by James Ausmus\n";
print "  This code is released under the General Public License v2.\n";
print " ------------------------------------------------------------------- \n\n";

my $MP3ENC  = "/usr/bin/lame";
#my $MP3INFO = "/usr/bin/mp3_check";
my $OGGINFO = "/usr/bin/ogginfo";
my $OGG123  = "/usr/bin/ogg123";

# check presence of executables
stat($MP3ENC) or die "Error: $MP3ENC not present!\n";
stat($OGGINFO) or die "Error: $OGGINFO not present!\n";
stat($OGG123) or die "Error: $OGG123 not present!\n";

# list of lame's allowed values
my $frequency_l = " 32 44.1 48 ";
my $bitrate_l   = " 32 40 48 56 64 80 96 112 128 160 192 224 256 320 ";
my $quality_l = " 0 1 2 3 4 5 6 7 8 9 ";

my $opt_delete;
my $opt_rename;
my $opt_lowercase;
my $opt_no_replace;
my $opt_bitrate;
my $opt_quality;
my $opt_verbose;
my $opt_debug;

# build genre hash
my %genres;
open(GENRES, "$MP3ENC 2>&1 --genre-list|") or die "Couldn't get genre list with $MP3ENC --genre-list\n";
while(<GENRES>) {
    chomp;
    next if /^\s*$/;
    # lowercase names are keys, ID number is value
    $genres{lc($2)} = $1 if /^\s*(\d*)\s(.*)$/;
}
close(GENRES);


# TODO; add overrides for lame settings, eg change sampling freq
# right now we inherit ogg settings or fallback to common settings
GetOptions("help|?",\&showhelp,
		"delete", \$opt_delete,
		"rename=s", \$opt_rename,
		"lowercase", \$opt_lowercase,
		"no-replace", \$opt_no_replace,
		"bitrate=s", \$opt_bitrate,
		"quality=s", \$opt_quality,
		"verbose", \$opt_verbose,
		"debug", \$opt_debug,
		"<>", \&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 .ogg with\n";
	print "                         .mp3 for the output file, produce output \n";
	print "                         filenames in this format, replacing %a, %t,\n";
	print "                         %l, %n, and %N with artist, title, album name,\n";
	print "                         track number, and total number of tracks for\n";
	print "                         the track/album\n";
	print "--bitrate=bitrate        Ask lame to use defined bitrate\n";
	print "--quality=quality        Ask lame to use defined quality (0-9)\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() {
	my $file = $_;
	my ($name,$dir,$ext) = fileparse($file,'\.ogg');
	if((/\.ogg/,$ext) && (-f $file)) {
		&checkfile($file);
	}
}

sub ConvertFile() {
	my $oggfile = shift(@_);
	my $delete = $opt_delete;
	my $filename = $opt_rename;
	my $bitrate = $opt_bitrate;
	my $quality = $opt_quality;
	my $lowercase = $opt_lowercase;
	my $noreplace = $opt_no_replace;
	my $verbose = $opt_verbose;

	if ($opt_debug) {
		print "Options:\n\toggfile: $oggfile\n\tdelete: $delete\n\tfilename: $filename\n\tbitrate: $bitrate\n\tquality: $quality\n\tlowercase: $lowercase\n\tnoreplace: $noreplace\n\tverbose: $verbose\n\n";
	}

	$_ = $filename;
	
	# two hashes; title tags use "=" delimiter, spec tags use ":" delimiter
	open(TAGS, "$OGGINFO \Q$oggfile\E |") or die "Error: Couldn't run ogginfo $oggfile\n";
	my %oggtags;
	my %oggrates;
	while(<TAGS>) {
	    chomp;
	    next if /^\s*$/;
	    # get title tags
	    if (/^\s*(\S+)=(.*)$/) {
            $oggtags{lc($1)} = $2;
        }
	    # grab channels, freq and bitrate
	    # right now we're only using the fixed values and we strip daft ,000000
	    if (/^\s*(Channels|Rate|Nominal bitrate):\s*(\d*).*$/i) {
            $oggrates{lc($1)} = $2;
        }
	}
	close(TAGS);

	if ($opt_debug) {
		my $dbg_idx=1;
		my $dbg_name;
		print "dumping tags list...\n";
		while (($dbg_idx,$dbg_name) = each(%oggtags)) {
			print "\t$dbg_idx: $dbg_name\n";
		}
		print "\n";
	}
	
	# default to joint stereo for encoding
	# change to stereo for bitrates >=160
	my $channels = "j";
	if ($oggrates{channels} == 1) {
		# mono
		$channels = "m";
	}
	
	# bitrate
	# Do bitrate stuff. Grab from command line if available.
	# Also, add stereo channels for higher bitrates
	if(($bitrate eq "") || !($bitrate_l =~ / $bitrate /)){
	    $bitrate = ($oggrates{"nominal bitrate"});
	    # nice fallback quality default
	    if($verbose) {
	        print "No bitrate set - Attempting to extract bitrate from ogg data...\n";
	    	print "Bitrate $bitrate found\n";
	    }
	}
	if(($quality eq "") || !($quality_l =~ / $quality /)){
	    # nice fallback quality default
	    $quality = 5;
	    if($verbose) {
	        print "No quality set - Falling back to quality 5\n";
	        print "Info: lower values mean higher quality.\n";
	    }
	}
	if($bitrate_l =~ / $bitrate /) {
	   if($bitrate >= 256) {
	      $channels = "s";
	   } elsif($bitrate >= 192) {
	      $channels = "s";
	   } elsif($bitrate >= 160) {
	      $channels = "s";
	   }
	} else {
	   # fallback defaults
	   $bitrate = 128;
	   print "Warning: Bitrate $bitrate is invalid\n";
	   print "Warning: Falling back to default: 128 kbps...\n";
	}
	
	# sampling frequency
	# ogg returns Hz, lame wants kHz	
	my $frequency = ($oggrates{rate})/1000;
	# check against allowed values
	if (!($frequency_l =~ / $frequency /)) {
	    print "Warning: Sampling frequency $frequency kHz is not a legal value for Lame MPEG1!\n";
	    print "Warning: Falling back to default 44.1 kHz...\n";
	    $frequency = "";
	}
	if (!$frequency) {
		# fallback default
		$frequency = "44.1";	
	}

	my $dirname = dirname($oggfile);
	my @trackNumInfo = split(/\//, $oggtags{tracknumber});
	
	if($filename eq "" ||
		((/\%a/) && $oggtags{artist} eq "") ||
		((/\%t/) && $oggtags{title} eq "") ||
		((/\%l/) && $oggtags{album} eq "") ||
		((/\%n/) && $trackNumInfo[0] eq "") ||
		((/\%N/) && $trackNumInfo[1] eq "")
	){

		if($filename ne "") {
			print "Warning: Not enough ID3 info to rename!\n";
			print "Warning: Reverting to old filename...\n";
		}
		$filename = fileparse($oggfile,'\.ogg');
	}
	else {
		$filename =~ s/\%a/$oggtags{artist}/g;
		$filename =~ s/\%t/$oggtags{title}/g;
		$filename =~ s/\%l/$oggtags{album}/g;
		$filename =~ s/\%n/$trackNumInfo[0]/g;
		$filename =~ s/\%N/$trackNumInfo[1]/g;
		if($lowercase) {
			$filename = lc($filename);
		}
		if(!$noreplace) {
			$filename =~ s/[\[\]\(\)\{\}!\@#\$\%^&\*\~ ]/_/g;
			$filename =~ s/[\'\"]//g;
		}
		my ($name, $dir, $ext) = fileparse($filename, '.mp3');
		$filename = "$dir$name";
	}

	my $mp3outputfile = "$filename.mp3";
	my $newdir = dirname($mp3outputfile);

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

	my $infostring = "";
	
	print "Converting $oggfile to MP3...\n";
	if (!$verbose) {
	    print "This may take some time depending on your hardware and encoding options,\n";
	    print "use --verbose to see what's going on,\n";
	    print "Please wait...\n\n";
	}
	# set verbose to quiet unless verbose is set
	my $decquiet = "-q";
	my $encquiet = "--quiet";
	if ($verbose) {
		$decquiet = "";
		$encquiet = "";
		print "\n   Freq: $frequency kHz\n";
		print "Bitrate: $bitrate kbps\tQuality Level: $quality\n";
		print " Artist: $oggtags{artist}\n";
		print "  Album: $oggtags{album}\n";
	        print "  Title: $oggtags{title}\n";
	        print "   Year: $oggtags{date}\n";
	        print "  Genre: $oggtags{genre}\n";
		print "  Track: $oggtags{tracknumber}\n\n";
	}

	if($oggtags{artist} ne "") {
		$infostring .= " --ta " . shell_quote($oggtags{artist});
	}
	if($oggtags{album} ne "") {
		$infostring .= " --tl " . shell_quote($oggtags{album});
	}
	if($oggtags{title} ne "") {
		$infostring .= " --tt " . shell_quote($oggtags{title});
	}
	if($oggtags{tracknumber} ne "") {
		$infostring .= " --tn " . shell_quote($oggtags{tracknumber});
	}
	if($oggtags{date} ne "") {
	   	$infostring .= " --ty " . shell_quote($oggtags{date});
	}
	if($oggtags{genre} ne "") {
		# need to lowecase ogg tag for match
		my $genretag = lc($oggtags{genre});
		# ogg has spaces in genres underscored, removing them
		$genretag =~ s/_/ /g;
		# lookup converted string in genre hash, get ID number
		$genretag = $genres{$genretag};
		# damn, those crazy lame guys use 0 as ID, thanks a bundle
		if ($genretag || ($genretag == 0)) {
	   	    $infostring .= " --tg " . shell_quote($genretag);
		}
	}
#	if($oggtags->{comment} ne "") {
#		$infostring .= " --comment " . shell_quote("COMMENT=$oggtags->{comment}");
#	}
	
	$infostring .= " --tc " . shell_quote("ogg had $oggrates{'nominal bitrate'} kbps $oggrates{rate} Hz");
		
	
	my $mp3outputfile_escaped = shell_quote($mp3outputfile);
	my $oggfile_escaped = shell_quote($oggfile);
	# this took me some time to figure
	# note that byte order is swapped by lame via -x option
	# TODO: somebody please tell me how to supress the "Assuming bla bla" output without devnull
	my $result = system("$OGG123 $decquiet -d raw -f - $oggfile_escaped | $MP3ENC $encquiet -r -q $quality -b $bitrate -s $frequency -m $channels $infostring - $mp3outputfile_escaped");

	# TODO: find some widely used mp3 checker
	# disabled the checking due to lack of checker
	if(!$result) {
#	   open(CHECK,"$MP3INFO $mp3outputfile_escaped |");
#	   while(<CHECK>)
#	   {
#	      if($_ eq "file_truncated=true\n")
#	      {
#		 warn "Conversion failed ($mp3outputfile truncated).\n";
#		 close CHECK;
#		 return;
#	      }
#	      elsif($_ eq "header_integrity=fail\n")
#	      {
#		 warn "Conversion failed ($mp3outputfile header integrity check failed).\n";
#		 close CHECK;
#		 return;
#	      }
#	      elsif($_ eq "stream_integrity=fail\n")
#	      {
#		 warn "Conversion failed ($mp3outputfile header integrity check failed).\n";
#		 close CHECK;
#		 return;
#	      }
#	   }
#	   close CHECK;
	   print "$mp3outputfile done!\n\n";
	   if($delete) {
	      unlink($oggfile);
	   }

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


