#!/usr/local/bin/python
#
#	  FLAC Transcoder
#	  $Id: transcode.py,v 1.2 2005/11/29 01:56:34 matt Exp $
#
#	  Transodes a FLAC image with embeded TOC and metadata tags into whatever
#	  file formats are desired.	   The formats are set via an encoder list in
#	  the configuration file.	 For details, see CONFIG.

import os
import sys
import getopt
import re
import ConfigParser
import subprocess
import string
import MusicBrainzHelper
import FlacHelper
import logging
import pickle
import glob
import random

# Default values.
DEFAULT_FLAC='flac'
DEFAULT_METAFLAC='metaflac'
DEFAULT_CONFIG='transcode.cfg'

# summary status returned at the end
summaryTranscoded = []
summaryNoMusicBrainzId = []
summaryInvalidMusicBrainzId = []

# Print a usage statement
def usage ( progname ):
	print "usage: "+progname+" [-e <encoder list>] [-f <config file>]"
	print "				[-F <flac command>] [-M <metaflac command>] <input files>"
	print "	   -e : list of encoders to use"
	print "	   -f : alternate configuration file ("+DEFAULT_CONFIG+")"
	print "	   -F : flac command ("+DEFAULT_FLAC+")"
	print "	   -M : metaflac command ("+DEFAULT_METAFLAC+")"
	print
	print "	   input files: a list of flac files to encode"
	sys.exit( 1 )
	
# run <filename> with arguments <args> and wait for it to return.	 returns
# the return value from the other program
def invoke_command ( filename, args ):
	print filename
	for i in range(0, len(args)):
		if (type(args[i]) == str):
			args[i] = args[i].decode('cp1252')
		if (type(args[i]) == unicode):
			args[i] = args[i].encode('cp1252')
	args[0:0] = [ filename ]
	process = subprocess.Popen(args, executable=filename)
	return process.wait()

# Create a track number string.	   Returns a stringified version of number, with
# a '0' prepended if the number was less than 10.	 Don't use on numbers greater
# than 99.
def gen_trackstr ( number ):
	if number < 10:
		return '0'+str( number )
	else:
		return str( number )
		
# Extract track number 'track_num' from 'filename'.	   'flac_cmd' specifies the
# FLAC binary to use.		 
def extract_track ( filename, track_num, start_index, flac_cmd ):
	ext_file = str(track_num) + "-" + str(random.random()) + ".wav"

	args = [ 
		'-d', 
		'--cue='+str(track_num)+'.'+str(start_index)+'-'+str(track_num+1)+'.0',
		'--output-name='+ext_file,
		filename
	]

	retval = invoke_command(flac_cmd, args);
	if retval != 0:
		raise EnvironmentError("System call \'"+cmd+"\' returned: "+str(retval))
	
	return ext_file

# this maps illegal characters for windows filenames to legal replacements
def make_legal_for_windows(s):
	# remove trailing periods
	while s.endswith('.'):
		s = s[0:len(s) - 1]
	# convert to 8-bit
	if type(s) == unicode:
		s = s.encode('cp1252')
	# translate illegal characters to a safe replacement
	return string.translate(s, string.maketrans('\"*/:<>?\\|', '\'+-;[]_-;'))

# Substitute % keys from 'tag_dict' in the 'src_str'
def sub_str ( src_str, tag_dict):
	src_str = unicode(src_str)
	for key in tag_dict.keys():
		t = type(tag_dict[key])
		if (t == unicode):
			value = tag_dict[key]
			value = value.encode('cp1252', 'ignore')
			value = value.decode('cp1252', 'ignore')
		elif (t == str):
			value = tag_dict[key].decode('cp1252')
		else:
			value = str(tag_dict[key])
		src_str = src_str.replace(key, value)

	return src_str
	
# Process the FLAC image 'filename' using 'encoders'.	 
def process_file ( filename, encoders, flac_cmd, meta_cmd ):
	flacHelper = FlacHelper.FlacHelper(filename)
	cuesheet = flacHelper.readFlacCuesheet()

	# rescope these globals as locals
	global summaryTranscoded
	global summaryNoMusicBrainzId
	global summaryInvalidMusicBrainzId

	# load the FLAC cuesheet and get the minimum index number for each
	# track along with it's type
	# matt's old comment on why we do this:
	# INDEX_00 is typically inter-track gap that shows up as -0:01 and such
	# on your CD player.	The problem is that this index does not always
	# exist and if you tell flac to start at a non-existant start track, it'll
	# start with the previous index, in this case that could be the whole 
	# previous track.	 So, we test for the existance of index 0 and start
	# at INDEX_01 if it doesn't exist.
	startTrackRegex = re.compile(r'TRACK (?P<tracknum>\d+) (?P<type>\w+)')
	startIndexRegex = re.compile(r'INDEX (?P<index>\d+) (?P<min>\d+):(?P<sec>\d\d):(?P<frame>\d\d)')
	trackNumber = -1
	trackType = dict()
	trackFirstIndex = dict()
	for line in cuesheet:
		startTrackMatch = startTrackRegex.search(line)
		if startTrackMatch:
			trackNumber = int(startTrackMatch.group("tracknum")) - 1
			trackType[trackNumber] = int(startTrackMatch.group("tracknum"))
			trackFirstIndex[trackNumber] = 99
		startIndexMatch = startIndexRegex.search(line)
		if startIndexMatch:
			indexNumber = int(startIndexMatch.group("index"))
			if indexNumber < trackFirstIndex[trackNumber]:
				trackFirstIndex[trackNumber] = indexNumber

	# see if we have tags cached for this file
	cachedTagsFilename = filename + ".mb-tags"
	release = None
	if os.path.exists(cachedTagsFilename):
		try:
			cachedTagsFile = open(cachedTagsFilename, "rb")
			release = pickle.load(cachedTagsFile)
			cachedTagsFile.close()
		except:
			release = None

	# get this release from musicBrainz
	if release == None:
		# get the musicbrainz id for this file
		musicBrainzId = flacHelper.getMusicBrainzId()
		if musicBrainzId == None:
			print "File has no MusicBrainzId tag: %s" % filename
			summaryNoMusicBrainzId += filename
			return

		release = mb.getReleaseById(musicBrainzId)
		if release == None:
			print "MusicBrainzId tag not found for file: %s (%s)" % (filename, musicBrainzId)
			summaryInvalidMusicBrainzId += filename
			return

	if len(release.tracks) != len(trackFirstIndex.keys()):
		print "track count in FLAC not equal to track count in MusicBrainz"
		summaryInvalidMusicBrainzId += filename
		return

	print "Artist: %s" % release.artist.name.encode('cp1252', 'ignore')
	print "Title: %s" % release.title.encode('cp1252', 'ignore')

	# Create the tag dictionary.
	tags = dict()
	tags['%P'] = release.artist.name
	tags['%T'] = release.title
	tags['%G'] = "Rock"
	if len(release.releaseEvents) > 0:
		tags['%Y'] = release.releaseEvents[0].date[0:4]
	else:
		tags['%Y'] = "0"
	tags['%N'] = len(release.tracks)
	tags['$T'] = make_legal_for_windows(tags['%T'])
	tags['$P'] = make_legal_for_windows(tags['%P'])

	# we do stuff a little differently if this is a comp
	various = not release.isSingleArtistRelease()

	# Process each track.
	for trackNumber in xrange(0, tags['%N']):
		tags['%n'] = gen_trackstr(trackNumber + 1)
		track = release.tracks[trackNumber]

		# Add some tags to the dictionary on a per-track basis.
		tags['%t'] = track.title
		if various and track.artist:
			tags['%p'] = track.artist.name
		else:
			tags['%p'] = release.artist.name
		tags['$t'] = make_legal_for_windows(tags['%t'])
		tags['$p'] = make_legal_for_windows(tags['%p'])

		fExtracted = False

		try:
			for encoder in encoders:
				(enc_type, enc_std, enc_var, dir_str, filename_std, filename_var) = encoder

				dir = os.path.expanduser( sub_str( dir_str, tags ) )
				tags['%D'] = dir
				tags['$D'] = make_legal_for_windows( dir )
	
				# create the right encoder command based on album type
				if various:
					command_args = re.split('(?<!\\\)\s+', enc_var)
				else:
					command_args = re.split('(?<!\\\)\s+', enc_std)

				# create the right filename based on album type
				if various:
					filename_out = filename_var
				else:
					filename_out = filename_std

				filename_out = sub_str(filename_out, tags)
				tags['%F'] = filename_out

				if os.path.exists(filename_out):
					#print "output file exists, skipping: %s" % filename_out.decode('cp1252')
					None
				else:
					if not fExtracted:
						# Extract the track from the FLAC image.
						tags['%f'] = extract_track( 
							filename, 
							trackNumber + 1,
							trackFirstIndex[trackNumber], 
							flac_cmd )
						fExtracted = True
				
					cooked_args = []
					for arg in command_args:
						cooked_args.append( arg.replace('\ ',' ') )
			
					# the first entry is the command, the rest are the args
					encoder_command = cooked_args[0]
					cooked_args = cooked_args[1:]
		
					# do tag substitution on arguments
					for i in range(0, len(cooked_args)):
						cooked_args[i] = sub_str(cooked_args[i], tags )
		
					# create the destination directory and encode.
					try:
						os.makedirs( dir, 0755 )
					except OSError, err:
						import errno
						if err.errno != errno.EEXIST:
							raise err
						print "Directory \'"+dir+"\' already exists, proceeding."	 
					retval = invoke_command( encoder_command, cooked_args )
					if retval != 0:
						print cooked_args
						raise EnvironmentError("System call \'"+encoder_command+" "+' '.join( command_args) +"\' returned: "+str(retval))
		finally:
			if fExtracted:
				os.unlink( tags['%f'] )

	summaryTranscoded += [ [ filename, dir, len(release.tracks), [ encoder[0] for encoder in encoders ] ] ]

# Initialize variables.
config_file = DEFAULT_CONFIG
flac_cmd		= None
meta_cmd		= None
enc_list		= None
mb 				= MusicBrainzHelper.MusicBrainzHelper()

#log = logging.getLogger("musicbrainz2.webservice.WebService")
#logging.basicConfig(level=logging.DEBUG)
#log.setLevel(logging.DEBUG)

# Process the command lines.
try:
	(options, arguments) = getopt.getopt( sys.argv[1:], "e:f:F:M:" )
except getopt.GetoptError, error:
	print "Error:",str( error )
	sys.exit( 1 )

for option in options:
	(opt,val) = option
	if opt == '-e':
		enc_list = val.split(',')
	elif opt == '-f':
		config_file = val
	elif opt == '-F':
		flac_cmd = val
	elif opt == '-M':
		meta_cmd = val
	else:
		usage( argv[0] )

CONFIG = ConfigParser.ConfigParser( )
CONFIG.read( [os.path.expanduser( config_file )] )

# For items that were unchanged by command line arguments, set defaults.
if flac_cmd == None:
	try:
		flac_cmd = CONFIG.get('GeneralConfig','Flac')
	except ConfigParser.NoOptionError:
		flac_cmd = DEFAULT_FLAC

if meta_cmd == None:
	try:
		meta_cmd = CONFIG.get('GeneralConfig','MetaFlac')
	except ConfigParser.NoOptionError:
		meta_cmd = DEFAULT_METAFLAC
		
if enc_list == None:
	try:
		enc_list = CONFIG.get('GeneralConfig','Encoders').split(',')
	except ConfigParser.NoOptionError:
		print "FATAL: No encoder specified in config file or command line"
		usage ( sys.argv[0] )

# Get the encoders
encoders = []
for enc in enc_list:
	std_cmd = CONFIG.get(enc, 'Command')
	std_filename = CONFIG.get(enc, 'Filename')
	dir_str = CONFIG.get(enc, 'Directory')
	try:
		va_cmd= CONFIG.get(enc, 'CommandVA')
		va_filename = CONFIG.get(enc, 'FilenameVA')
	except ConfigParser.NoOptionError:
		va_cmd = std_cmd
	encoders += [(enc, std_cmd, va_cmd, dir_str, std_filename, va_filename)]

if len( arguments ) == 0:
	usage( sys.argv[0] )

# Process the rest of the arguments as filenames of FLAC images.
for arg in arguments:
	filenames = glob.glob(arg)
	for filename in filenames:
		process_file( filename, encoders, flac_cmd, meta_cmd )

# print summary status
print "Transcoded:"
for encoded in summaryTranscoded:
	print "  FLAC: %s" % encoded[0]
	print "    Output Directory: %s" % encoded[1]
	print "    Track Count: %s" % encoded[2]
	print "    Transcode Types: %s" % encoded[3]

print "Skipped -- No DISC_MUSICBRAINZ_ID tag:"
for skipped in summaryNoMusicBrainzId:
	print "  FLAC: %s" % skipped

print "Skipped -- Invalid DISC_MUSICBRAINZ_ID tag:"
for skipped in summaryInvalidMusicBrainzId:
	print "  FLAC: %s" % skipped


