#!/usr/bin/perl -w
#
# Decrypt EMusic's EMP file format
#
# Thomas Themel <themel0r@wannabehacker.com> 20030615
#
# The purpose in writing this stuff was to get around the 
# need to use EMusic.com's horrid 'Download Manager' whose 
# Linux version was broken in rather many annoying ways.
#
# This version contains a configuration file named ~/.decryptemprc 
# that supports the following settings:
# 
# FileMask=...
#	This is the file name format for the downloaded MP3
#	files.	The following tokens are replaced:
#
#	%artist   - artist
#	%track	  - track title
#	%tracknum - track number
#	%album	  - album title
#
#	If not specified, the file name supplied by the EMusic
#	server is used.
#
# MP3Dir=...
#		The base directory to store files in. The file names
#		for MP3 files are relative to this directory. If not 
#		specified, this is set to the current directory.
#
# CleanNames
#		If this is set, blanks and special characters in file 
#		names are replaced with underscores.
#
# If you don't have a .decryptemprc, but an EMusic DLM configuration 
# file exists, a new .decryptemprc with settings from the DLM 
# configuration is created.
#
# CHANGES:
#
# junk <junk@styro.lib.muohio.edu> 20030917
#	-added $emusic_dir for download base
#	-modified $cleartext to remove bad chars
#	-extract ARTIST, ALBUM, and ALBUMART from emp
#	-add -P flag to wget to download mp3s to:
#		$emusic_dir/$artist/$album/
#	-download ALBUMART to same directory
#
# Thomas Themel <themel0r@wannabehacker.com> 20030918
#  - various usability fixes
#  - added config file 

use strict ;
use MIME::Base64 ;
use Getopt::Std ;

my %config ;

my $config_file = $ENV{HOME} . "/.decryptemprc" ;
my $dlm_config_file = $ENV{HOME} . "/.emusicdlm/dlm.conf" ;


my $downloadurl = "http://condir.mp3.com/emusic/" ;
my $idseparator = "?mob=" ;

# the key as extracted from the download manager's executable
# How this is generated from the string
# dmcjh3,m4hod87lkjh43iuydfkjhsdufyklmnsdfysd8of34kh
# is left as an exercise to the user (I was too lazy to figure
# it out and just grabbed this from memory).

my @constkey = ( 
	 0x6b, 0xd8, 0x44, 0x87, 0x52, 0x94, 0xfd, 0x6e, 
	 0x2c, 0x18, 0xe4, 0xc8, 0xde, 0x0b, 0xfa, 0x6d, 
	 0xb5, 0x06, 0x7b, 0xce, 0x77, 0xf4, 0x67, 0x3f, 
	 0x93, 0x09, 0x1c, 0x20, 0xf5, 0xbe, 0x27, 0xb1, 
	 0x02, 0xc9, 0x8f, 0x37, 0x68, 0x5e, 0xc1, 0x91, 
	 0xb4, 0x57, 0x8d, 0x90, 0x55, 0x8e, 0x45, 0x19, 
	 0xdb, 0x9c, 0xec, 0xa3, 0x9d, 0x32, 0xf7, 0x81, 
	 0xc5, 0x61, 0x8b, 0xab, 0x30, 0xa0, 0xbc, 0x31, 
	 0xdf, 0xf3, 0x4b, 0xa9, 0x2f, 0x3a, 0x4a, 0xbf, 
	 0x08, 0x66, 0xa7, 0xe2, 0x62, 0x3d, 0x36, 0xb2, 
	 0x4f, 0x73, 0x6c, 0x9a, 0x56, 0xcf, 0x33, 0xe5, 
	 0x43, 0x10, 0x17, 0xc2, 0x3e, 0x1e, 0x2b, 0x70, 
	 0x04, 0x7e, 0xc0, 0x9e, 0xc6, 0x4c, 0x92, 0x5c, 
	 0x0f, 0x23, 0x35, 0xd2, 0x7a, 0x3b, 0xaf, 0x80, 
	 0xd6, 0x9f, 0x0e, 0x78, 0x63, 0x76, 0x95, 0x58, 
	 0x1d, 0x83, 0x22, 0x4d, 0x96, 0xda, 0xc4, 0xae, 
	 0xca, 0xcb, 0xed, 0xd9, 0x86, 0x98, 0xea, 0xef, 
	 0xc3, 0xd0, 0x00, 0xba, 0x71, 0x46, 0xa8, 0x42, 
	 0x72, 0x2a, 0xd1, 0x49, 0xe8, 0xd3, 0xc7, 0xd5, 
	 0x50, 0xcc, 0x47, 0x21, 0xd7, 0x60, 0x38, 0x3c, 
	 0xe7, 0xd4, 0x89, 0xb6, 0x8a, 0x0c, 0xb8, 0xac, 
	 0x0d, 0x82, 0x29, 0x05, 0xe6, 0x5f, 0xfc, 0x5a, 
	 0x12, 0x74, 0x5d, 0x8c, 0x14, 0x03, 0x2d, 0x59, 
	 0x6f, 0xdc, 0x28, 0x7c, 0x15, 0xad, 0xa2, 0x26, 
	 0x11, 0x9b, 0x99, 0x24, 0xfb, 0xf8, 0xa4, 0x07, 
	 0x7d, 0x64, 0x75, 0x1b, 0xcd, 0xa5, 0x25, 0xfe, 
	 0xb7, 0xb9, 0xff, 0x5b, 0xb0, 0xe0, 0x13, 0x51, 
	 0x65, 0x4e, 0xbb, 0xf1, 0xeb, 0x48, 0x39, 0x53, 
	 0xf0, 0xe9, 0x85, 0xf2, 0x69, 0x0a, 0xaa, 0x34, 
	 0x84, 0x40, 0x41, 0x54, 0xdd, 0xf6, 0x1f, 0xbd, 
	 0xa1, 0xe1, 0x1a, 0xe3, 0x01, 0x97, 0x88, 0xa6, 
	 0xf9, 0x2e, 0x16, 0xb3, 0x6a, 0xee, 0x79, 0x7f) ; 

sub replace_tokens($%)
{
	my($string, %hash) = @_;
	my $key ;
	foreach $key (keys %hash)
	{
		$string =~ s/\%$key/$hash{$key}/g;
	}
	return $string ;
}

# equivalent of mkdir -p
sub ensure_path($)
{
	my @path = split/\//, $_[0] ; 
	my $complete ; 
	my $dir ;
	foreach $dir (@path)
	{
		$complete .= "$dir/" ; 
		mkdir $complete or return 0 unless -d $complete ;
	}
	return 1 ;
}

# Try and convert EMusic config file if there is no
# config file yet.
if(! -e $config_file && -r $dlm_config_file)
{
	open DLMCONFIG, "<$dlm_config_file" ; 
	open CONFIG, ">$config_file" ;

	while(<DLMCONFIG>)
	{
		chomp ;
		print CONFIG "$1=$2\n" if m@<(MP3Dir|FileMask)>(.*?)</\1>@ ;
	}

	close CONFIG;
	close DLMCONFIG;
}

# read config file
if(-r $config_file)
{
	open CONFIG, "<$config_file" ;
	while(<CONFIG>)
	{
		chomp ;
		s/#.*//;
		my ($key, $value) = split /\s*=\s*/, $_, 2 ;
		$config{$key} = $value ? $value : "1" ;
	}
	close CONFIG ;
}

my $emusic_dir = $config{MP3Dir} ;

# default to current directory
$emusic_dir = "." unless $emusic_dir ;

# this has no defaults, use what is in the EMP
# file if unspecified
my $path_format = $config{FileMask} ;

my %opts ;
getopts('wudh', \%opts) ;

if(defined $opts{h})
{
	print STDERR "decrypt-emp [-u|-d|-w] [file]...\n";
	print STDERR "	-w	Use wget to download the described MP3s (default)\n" ;
	print STDERR "	-d	Dump decrypted EMP file.\n";
	print STDERR "	-u	Dump URLs for MP3s described in EMP file.\n";
	exit (-1) ;
}


# wget as default
if(!defined $opts{u} and !defined $opts{d} and !defined $opts{w})
{
	$opts{w}=1 ;
}
else
{
	die "Conflicting options specified" if scalar keys(%opts) > 1 ;
}



my $isbatch = $#ARGV > 0 ;

print "<BATCH>\n" if $isbatch && defined $opts{d} ; 

while(<>)
{
	my $line = $_ ; 

	# make the eMusic mess correct base64
	$line =~ y@._-@+/=@;
	my $binary = decode_base64($line) ;

	my @ciphertext = unpack("C*", $binary) ; 
	my $carry = 0 ;

	# copy the key since it changes during decryption
	my @key = @constkey ;

	for(my $keyIdx = 1 ; $keyIdx <= $#ciphertext + 1; ++$keyIdx)
	{
		my $k1 = $key[$keyIdx & 0xFF] ;

		# update carryover
		$carry += $k1 ;
		$carry &= 0xFF ; 

		my $k2 = $key[$carry] ;

		# exchange key bytes
		$key[$keyIdx & 0xFF] = $k2 ;
		$key[$carry] = $k1 ;

		$ciphertext[$keyIdx - 1] ^= $key[($k1 + $k2) & 0xFF] ;
	}

	my $cleartext = pack("C*", @ciphertext) ;

	# unix text files are better 
	$cleartext =~ s/\r\n/\n/g;

	print $cleartext if ($opts{d});

	if($opts{u} || $opts{w})
	{
		my $dl_jpg = 0;

		my @tracks = split /<TRACK>/, $cleartext;

		foreach my $track (@tracks)
		{
			$track =~ m@<TRACKID>(.*?)</TRACKID>.*?<TRACKNUM>(.*?)</TRACKNUM>.*?<TITLE>(.*?)</TITLE>.*?<ALBUM>(.*?)</ALBUM>.*?<ARTIST>(.*?)</ARTIST>.*?<FILENAME>(.*?)</FILENAME>.*?<ALBUMART>(.*?)</ALBUMART>@s;
	   
			if(defined $1 and defined $2 and defined $4 and defined $3 and defined $5)
			{
				my $trackid = $1;
				my %track ;
				$track{tracknum} = $2 > 9 ? $2 : "0$2" ;
				$track{track} = $3;
				$track{album} = $4;
				$track{artist} = $5;
				$track{art} = $7;

				my $trackurl = "$downloadurl$6$idseparator$1" ;
				
				my $output_dir = "$emusic_dir/";
				my $output_file ;

				unless($config{FileMask})
				{
					# use file name from EMP file
					$output_file = $6;
				}
				else
				{
					# expand according to configuration
					my @path = split /\//, $config{FileMask} ;

					$output_file = pop @path ;
					$output_file = replace_tokens($output_file, %track) ; 
					
					$output_dir .= join '/', map { replace_tokens($_, %track) } @path ;
				}
				
				# damn, we should use a real XML parser...
				$output_file =~ s/&amp;/&/;
				$output_dir =~ s/&amp;/&/;
				$output_file =~ s/&apos;/'/;
				$output_dir =~ s/&apos;/'/;
				$output_file =~ s/&quot;/"/;
				$output_dir =~ s/&quot;/"/;

				# clean up weird characters in file names
				$output_file =~ s/[ \'\!\@\#\$\%\^\&\*\(\)]/_/g if $config{CleanNames};
				$output_dir =~ s/[ \'\!\@\#\$\%\^\&\*\(\)]/_/g if $config{CleanNames};
		   
								# make sure we can write to the directory
				ensure_path($output_dir) or die "Error creating output path";

				print "$trackurl\n" if $opts{u} ;
				
				if ($opts{w})
				{
					system("wget", "-c", "$trackurl", "-O", "$output_dir/$output_file") and
						die "wget invocation failed: $?\nCommand was: wget -c \"$trackurl\" -O \"$output_dir/$output_file\"" ;
				   
					if (defined $track{art} and !$dl_jpg)
					{
						$dl_jpg = 1;
						system("wget", "-c", "$track{art}", "-P", "$output_dir") and
							die "wget invocation failed: $?\nCommand was: wget -c $track{art} -P $output_dir" ;
					}

				}
			
			}

		}
		
	}
}
print "</BATCH>\n" if $isbatch && defined $opts{d} ;
