#!/usr/bin/env python

# iPodScrobbler version 1.2
# copyright January 2007 by Mike Bleyer <Michael dot Bleyer @ hoc dot net>
# please mail bug reports, comments, etc. to me.
#
# This program is released under the GNU GPL, version 2 or later.
# See http://www.gnu.org/licenses/gpl.html
#
# iPodScrobbler: audioscrobbler iPod batch upload script
# What it does: this script reads your Apple iPod iTunesDB
# and scans all the tracks for the last date you have listened to them.
# It then uploads this info to www.last.fm
# You need a last.fm account to use this script!
# For more info see www.audioscrobbler.net and www.last.fm
#
# Should work on MacOS, Windows, Unix, but I cannot test them all after every change.
#
# TODO:
# - handle FAILED messages more gracefully
# - still need to do more testing with different firmware versions and generation iPods
#
# KNOWN PROBLEMS:
# - iPod shuffles do not work, they do not have a clock builtin and cannot set timestamps
#
# CHANGELOG:
#
# - version 1.2:
#   fixed a bug in the iPodDateOffset global, it was skewed by one hour
#
# - version 1.1:
#   added an automatic repeat for the last.fm "post variable" error
#
# - version 1.0:
#   first version
#

import sys, struct, os, string, re, getopt, time, md5, urllib, urllib2

# globals
verbose=0

# init default Unicode string encoding (ascii)
uencoding='ascii'
# check if local python installation forces something else in site.py (default=ascii)
try:
	import site
	uencoding=site.encoding
except: pass
# check system locale for proper Unicode string encoding
import locale
if uencoding=='ascii' and locale.getdefaultlocale()[1]:
	uencoding = locale.getdefaultlocale()[1]
# if we are on MacOSX, we default to UTF8
# because apples python reports 'ISO8859-1' as locale, but MacOSX uses utf8
if sys.platform=='darwin':
	uencoding='utf8'

# iPod date timestamps are number of seconds since 1.1.1904
# set an offset of 2082844800 seconds for POSIX time (epoch)
iPodDateOffset = long(-2082844800.0)


# default behaviour is to upload only contents of Play Counts file
# e.g. everything that has been played since the last upload
upload_init = 0

# global variable defaults
verbose=1
dryRun=0
nameFilter=None
genreFilter=None
since=0
lastfmConnection = None

# stuff for strange last.fm errors
max_retries_request = 3
lfm_err_msg="Plugin bug: Not all request variables are set"


class ScrobbUploader(object):
	"""Object to handle communication with Audioscrobbler.com"""
	def __init__(self,username=u'',password=u'',client=u'tst',clientversion=u'1.0',baseurl='http://post.audioscrobbler.com/'):
		"""All strings should be unicode. Don't change the rest unless you know what you are doing."""
		# audioscrobbler protocol version
		self.scrobblerprotocolversion = u'1.1'
		self.baseurl = baseurl
		self.username = username
		self.password = password
		self.client = client
		self.clientversion = clientversion
		self.interval = 0
		# post data required for every submit request (user, md5), saved as regular string (already encoded)
		self.postbase = ''
		# url-encoded tracks, one string each
		self.trackstrings = {}

	def handshake(self):
		"""Do a handshake with Audioscrobbler. If successful, the md5challenge will be set."""
		url = self.baseurl+'?'+urllib.urlencode([
			('hs','true'),
			('p',urllib.quote_plus(self.scrobblerprotocolversion.encode('utf8'))),
			('c',urllib.quote_plus(self.client.encode('utf8'))),
			('v',urllib.quote_plus(self.clientversion.encode('utf8'))),
			('u',urllib.quote_plus(self.username.encode('utf8')))
			])
		result = ['']
		try:
			result = urllib2.urlopen(url).readlines()
		except IOError, expn:
			sys.stderr.write('Error: failed to get audioscrobbler handshake response with request:\"'+url+'\".\n')
			sys.stderr.write('Response: \"'+repr(result)+'\"\n')
			sys.stderr.write('Exception: \"'+str(expn)+'\"\n')
			sys.exit(1)

		if result[0].startswith('BADUSER'):
			sys.stderr.write('Error: username \"'+SafeDecode(self.username)+'\" incorrect or unknown by www.last.fm\n')
			sys.exit(1)
		elif result[0].startswith('UPTODATE'):
			md5challenge = string.strip(result[1])
			md5response = md5.md5(md5.md5(self.password).hexdigest()+md5challenge).hexdigest()
			self.postbase = 'u='+urllib.quote_plus(self.username.encode('utf8'))+'&s='+md5response
			self.submiturl = string.strip(result[2])
		elif result[0].startswith('UPDATE'):
			updateurl = string.strip(result[0][6:])
			sys.stdout.write('Notice: a new version of your audioscrobbler plugin is available at \"'+SafeDecode(updateurl)+'\"\n')
			md5challenge = string.strip(result[1])
			md5response = md5.md5(md5.md5(self.password).hexdigest()+md5challenge).hexdigest()
			self.postbase = 'u='+urllib.quote_plus(self.username.encode('utf8'))+'&s='+md5response
			self.submiturl = string.strip(result[2])
		elif result[0].startswith('FAILED'):
			reason = string.strip(result[0][6:])
			sys.stderr.write('Error: handshake with www.last.fm failed due to \"'+SafeDecode(reason)+'\"\n')
			sys.exit(1)
		else:
			sys.stderr.write('Error: audioscrobbler sent an unknown response, I don\'t know what to do, sorry.\n')
			sys.stderr.write('Request: \"'+repr(url)+'\"\n')
			sys.stderr.write('Response: \"'+repr(result)+'\"\n')
			sys.exit(1)
		# get the Interval if present
		iregex = re.match('INTERVAL (\d+)',result[-1])
		if iregex is not None:
			self.interval = int(iregex.group(1))

	def posttracks(self):
		"""Uploads a set of up to 10 tracks to last.fm with a single HTTP POST request."""
		# check if we have an md5, if not, the handshake did not succeed
		if len(self.postbase)<1:
			sys.stderr.write('Error: no successful handshake, md5 challenge is missing.\n')
			sys.exit(1)
		# check if we actually have some tracks to upload
		if len(self.trackstrings)>0:
			# check if we have an interval set, if yes, wait before posting
			if self.interval>0:
				time.sleep(self.interval)
			# assemble the post data
			postdata = self.postbase
			for i in self.trackstrings.keys():
				postdata += self.trackstrings[i]

			# send it
			result = ['']
			# maximum number of retries per request
			retry = max_retries_request
			while retry>0:
				try:
					result = urllib2.urlopen(self.submiturl,postdata).readlines()
					# get the Interval if present
					iregex = re.match('INTERVAL (\d+)',result[-1])
					if iregex is not None:
						self.interval = int(iregex.group(1))
					else:
						self.interval = 1
				except IOError, expn:
					sys.stderr.write('Error: failed to submit tracks to audioscrobbler with post request:\"'+postdata+'\".\n')
					sys.stderr.write('Response: \"'+repr(result)+'\"\n')
					sys.stderr.write('Exception: \"'+str(expn)+'\"\n')
					sys.exit(1)

				if result[0].startswith("OK"):
					retry=0
					sys.stdout.write('\tUploaded '+str(len(self.trackstrings))+' tracks ok!\n')
					# clear cache here
					for i in self.trackstrings.keys():
						del self.trackstrings[i]
				elif result[0].startswith("FAILED"):
					reason = string.strip(result[0][6:])
					# check if its the strange request variables bug - if yes retry
					if reason[0:len(lfm_err_msg)]==lfm_err_msg:
						retry=retry-1
						sys.stderr.write('Strange last.fm response, trying again...\n')
						time.sleep(self.interval)
						continue
					else:
						retry=0
						sys.stderr.write('Error: submit with www.last.fm failed due to \"'+SafeDecode(reason)+'\"\n')
						sys.stderr.write('Request: \"'+self.submiturl+'?'+postdata+'\"\n')
						sys.stderr.write('Response: \"'+repr(result)+'\"\n')
						sys.exit(1)
				elif result[0].startswith('BADAUTH'):
					retry=0
					sys.stderr.write('Error: password incorrect or unknown by www.last.fm\n')
					sys.exit(1)
				else:
					retry=0
					sys.stderr.write('Error: audioscrobbler sent an unknown response, I don\'t know what to do, sorry.\n')
					sys.stderr.write('Request: \"'+self.submiturl+'?'+postdata+'\"\n')
					sys.stderr.write('Response: \"'+repr(result)+'\"\n')
					sys.exit(1)

	def addtrack(self,artist=u'',track=u'',album=u'',musicbrainzid=u'',length=0,utctimestamp=0,immediately=0):
		"""Add a single track and encode it. If 10 are added or if immediately is set to true, they are submitted to last.fm."""
		i=len(self.trackstrings)
		trackstring = ''
		trackstring += '&a['+str(i)+']='+urllib.quote_plus(artist.encode('utf8'))
		trackstring += '&t['+str(i)+']='+urllib.quote_plus(track.encode('utf8'))
		trackstring += '&b['+str(i)+']='+urllib.quote_plus(album.encode('utf8'))
		trackstring += '&m['+str(i)+']='+urllib.quote_plus(musicbrainzid.encode('utf8'))
		trackstring += '&l['+str(i)+']='+str(length)
		utcstring = u'%s' % (time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(utctimestamp)))
		trackstring += '&i['+str(i)+']='+urllib.quote_plus(utcstring.encode('utf8'))
		self.trackstrings[i]=trackstring
		if immediately:
			# dont wait until 10 are full, add tracks right away
			self.posttracks()
		else:
			if len(self.trackstrings)>9:
				self.posttracks()

def SafeDecode(uText=u''):
	"""Safe and robust way to convert a unicode string without encoding errors."""
	# This function makes sure that a Unicode string is converted to a valid string that the local OS can handle
	# there must be a better way than this?? How can we pass UTF-16 unicode chars to a system function call??
	# try with locale setting first
	try:
		return uText.encode(uencoding)
	except UnicodeError: pass
	# if this doesn't work, try UTF8
	# commented out because it yields invalid output even though proper 8-bit
	#try:
	#	return uText.encode('utf8')
	#except UnicodeError: pass
	# as last resort, use ascii with unknown chars replaced by underscore '_'
	try:
		return uText.encode('ascii', 'replace')
	except:
		return repr(uText)



def UploadTrackLastPlayedInfo(songdict={},playdict={}):
	"""Upload and output Track last played info."""
	# require this because we need to sort by timestamp
	uploaddict = {}

	if len(songdict) != len(playdict):
		sys.stderr.write('Error: number of tracks in Song dictionary and Play Counts dictionary is different.')
		sys.exit(2)
	if verbose>1:
		sys.stdout.write('Uploading info to www.last.fm\n')
		if nameFilter:
			sys.stdout.write('\tUsing name filter pattern \"'+nameFilter.pattern+'\".\n')
		if genreFilter:
			sys.stdout.write('\tUsing genre filter pattern \"'+genreFilter.pattern+'\".\n')
		if since>0:
			sincestring='%s' % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(since)))
			sys.stdout.write('\tProcessing only tracks listened to since: '+sincestring+'\n')
	# iterate over items and join all the info from both dictionaries
	for key in songdict.keys():
		artist,album,title,genre,irating,itimestamp,iduration=songdict[key]
		artist=string.strip(artist)
		album=string.strip(album)
		title=string.strip(title)
		playcount,pctimestamp,pcrating=playdict[key]

		if verbose>2:
			sys.stdout.write('['+str(key)+'] Artist:\"'+SafeDecode(artist)+'\"  Title:\"'+SafeDecode(title)+'\"\n')

		# upload initially (from iTunesDB) or normal/incremental (from Play Counts)
		timestamp = 0
		rating = 0
		if upload_init:
			# from iTunesDB
			timestamp = itimestamp
			rating = irating
		elif itimestamp != pctimestamp:
			# from Play Counts file
			timestamp = pctimestamp
			rating = pcrating
		else:
			continue

		# only process items with a valid timestamp
		if timestamp<1:
			continue

		# apply date filter, skip older stuff
		if (since > 0) and ((timestamp+iPodDateOffset) < since):
			continue

		# UTC YYYY-MM-DD hh:mm:ss
		if verbose>2:
			utcTimeString = '%s' % (time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(timestamp+iPodDateOffset)))
			localTimeString = '%s' % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp+iPodDateOffset)))
			sys.stdout.write('\tUTC timestamp:   '+utcTimeString+'\n\tLocal timestamp: '+localTimeString+'\n\tRating:'+str(rating)+'\n')


		# Songs with a duration of less than 30 seconds should not be submitted to last.fm
		if iduration<30000:
			if verbose>2:
				sys.stdout.write('\tskipping, has a duration of: '+str(iduration)+' milliseconds.\n')
			continue
		if verbose>2:
			sys.stdout.write('\tDuration of: '+str(iduration/1000)+' seconds.\n')
		# check for name regular expression pattern filter, if yes apply
		if nameFilter and not nameFilter.search(artist) and not nameFilter.search(title) and not nameFilter.search(album):
			if verbose>2:
				sys.stdout.write('\tskipping, no match name pattern: \"'+SafeDecode(artist)+', '+SafeDecode(title)+'\"\n')
			continue
		# check for genre regular expression pattern filter, if yes apply
		if genreFilter and not genreFilter.search(genre):
			if verbose>2:
				sys.stdout.write('\tskipping, no match genre pattern: \"'+SafeDecode(genre)+'\"\n')
			continue
		# sanity check, skip track with Various artists, "Track xx" name and incomplete ID tags
		if (len(artist)<1) or (len(title)<1) or artist.lower().startswith('various artists') or title.lower().startswith('track'):
			continue
		# add it here
		uploaddict[timestamp+iPodDateOffset]=(artist,title,album,iduration/1000)
	sorter=[]
	# now upload the sorted
	for ts in uploaddict.keys():
		sorter.append(ts)
	sorter.sort()
	i=0
	max=len(sorter)
	for ts in sorter:
		i+=1
		art,tit,alb,dur=uploaddict[ts]
		localTimeString = '%s' % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)))
		# if dry run, no upload, just message
		if dryRun:
			if verbose>0:
				sys.stdout.write('['+str(i)+'/'+str(max)+']\t'+localTimeString+' Artist:\"'+SafeDecode(art)+'\"  Title:\"'+SafeDecode(tit)+'\" dry run, no upload...\n')
		else:
			if lastfmConnection:
				lastfmConnection.addtrack(art,tit,alb,u'',dur,ts,immediately=0)
				if verbose>0:
					sys.stdout.write('['+str(i)+'/'+str(max)+']\t'+localTimeString+' Artist:\"'+SafeDecode(art)+'\"  Title:\"'+SafeDecode(tit)+'\" submitted...\n')


# helper function
def ReadSongInfo(iTunesDBFilename='iTunesDB'):
	"""Read a dictionary of items from the db file. Return format is 'itemnumber':('Artist','Album','Song',Timestamp)"""
	# init empty dict
	songdict={}
	# make sure db file exists
	if not os.path.isfile(iTunesDBFilename):
		sys.stderr.write('Error: iTunesDB file \"'+iTunesDBFilename+'\" not found.\n')
		return songdict
	if verbose>1:
		sys.stdout.write('Reading iTunesDB file \"'+iTunesDBFilename+'\".\n')
	# read the song data
	bigstring=''
	try:
		dbFile=open(iTunesDBFilename,'rb')
		bigstring=dbFile.read()
		dbFile.close()
	except:
		sys.stderr.write('Error: cannot open iTunesDB file \"'+iTunesDBFilename+'\" for reading.\n')
		return songdict

	# parse the iTunes db file info, it's in UTF-16 Unicode (little endian)
	# this is still somewhat clumsy, it works for now
	# and has the advantage that we don't care about the exact iTunesDB record format
	if verbose>0:
		sys.stdout.write('Parsing iTunesDB file \"'+iTunesDBFilename+'\" for audio track info.\n')
	trackListHeader='mhlt'
	fileHeader='mhit'
	itemHeader='mhod'
	# the record structure is as follows:
	# a file record (mhit) contains info about a single song file
	# it has several string item child records (mhod) which contain info such as:
	#    song title, artist, album, genre, filetype, filename
	max=len(bigstring)-5
	i=0
	number_of_tracks=0
	track_number=0
	# do a length/size sanity check of the iTunesDB first
	(total_length,)=struct.unpack('<L',bigstring[i+8:i+12])
	if total_length==(max+5):
		while i<max:
			# we found a track list, should be only one per iPod
			if bigstring[i:i+4]==trackListHeader:
				# read number of songs to sanity check
				(number_of_tracks,)=struct.unpack('<L',bigstring[i+8:i+12])
				i=i+11
			# we found a filename item
			elif bigstring[i:i+4]==fileHeader:
				# seed with defaults, not every song must have all these set
				# but we require them
				mp3Filename=None
				mp3Artist=u'Artist'
				mp3Album=u'Album'
				mp3Title=u'Title'
				mp3Genre=u'Genre'
				# we have a song file item, get its header length
				(songHeadLen,)=struct.unpack('<L',bigstring[i+4:i+8])
				fieldformat = '<LLLLL2sccLLLLLLL2s2sLLLLLLLLLLLL8scc2s2s2sLL'
				field_bytes=struct.calcsize(fieldformat)
				# calculate the number of padbytes to be more robust, header is composite length
				pad_bytes=songHeadLen-field_bytes-8

				(songCompLen, songItems, songKey, visible, ft,
				 songFiletype,songCompilation_flag, songStars_rating,
				 songfile_date, songfile_size, songfile_duration, songPosition, songNumber, songYear, songBitrate,
				 songu12, songsample_rate1,
				 songvolume_adjust, songstartplayback, songstopplayback,songsoundcheck,songplay_count1,songplay_count2,
				 songlast_time_played,songdisc_number,songtotal_discs,songapplestore_userid,songlast_modified, songbookmark_time,
				 songdb_song_id, songchecked, songapplication_rating,songbpm,songartwork_count,u9,songartwork_size,u11,
				 padding) = struct.unpack(fieldformat+str(pad_bytes)+'s', bigstring[i+8:i+8+field_bytes+pad_bytes])


				# skip to start of first item
				i=i+songHeadLen
				# now read all the string item records
				j=0
				for j in range(songItems):
					# until the next filename item or end of list
					if (bigstring[i:i+4]==itemHeader):
						# we have a string item, get its full length
						(itemCompLen,)=struct.unpack('<L',bigstring[i+8:i+12])
						# get its type
						(itemType,)=struct.unpack('<L',bigstring[i+12:i+16])
						# get string length
						(itemLen,)=struct.unpack('<L',bigstring[i+28:i+32])
						# set i to the beginning of next item record
						i=i+itemCompLen
						# get the string
						itemArray=bigstring[i-itemLen:i]
						if itemType==1:
							mp3Title=unicode(itemArray,'utf-16-le')
						elif itemType==2:
							mp3Filename=unicode(itemArray,'utf-16-le')
						elif itemType==3:
							mp3Album=unicode(itemArray,'utf-16-le')
						elif itemType==4:
							mp3Artist=unicode(itemArray,'utf-16-le')
						elif itemType==5:
							mp3Genre=unicode(itemArray,'utf-16-le')
						elif itemType==12:
							mp3Composer=unicode(itemArray,'utf-16-le')
					else:
						sys.stderr.write('Error parsing iTunesDB string records (mhod)\n')
				# safety check number of mhod records
				if j!=(songItems-1):
					sys.stderr.write('Error parsing iTunesDB: number of mhod strings not correct\n')
				
				# add song file info to dict
				songdict[track_number]=(mp3Artist,mp3Album,mp3Title,mp3Genre,ord(songStars_rating)/20,songlast_time_played,songfile_duration)
				track_number=track_number+1
				if verbose>2:
					sys.stdout.write('Read track by \"'+SafeDecode(mp3Artist)+'\": '+SafeDecode(mp3Title)+'\n')
				# set counter back by one, so we don't skip the next song file
				i=i-1
			i=i+1
		if number_of_tracks != len(songdict):
			sys.stderr.write('Error: found only '+str(len(songdict))+' tracks, expected '+number_of_tracks+'.\n')			
	if verbose>0:
		sys.stdout.write('Read info for '+str(len(songdict))+' audio tracks from iTunesDB.\n')
	return songdict


# helper function
def ReadPlayCountsInfo(playCountsFilename='Play Counts'):
	"""Read a dictionary of items from the Play Counts db file. Return format is 'itemnumber':(playcount,lastplayed,rating)"""
	# init empty dict
	playcountsdict={}
	# make sure db file exists
	if not os.path.isfile(playCountsFilename):
		sys.stderr.write('Error: Play Counts file \"'+playCountsFilename+'\" not found.\n')
		return playcountsdict
	if verbose>1:
		sys.stdout.write('Reading Play Counts file \"'+playCountsFilename+'\".\n')
	# read the song data
	bigstring=''
	try:
		dbFile=open(playCountsFilename,'rb')
		bigstring=dbFile.read()
		dbFile.close()
	except:
		sys.stderr.write('Error: cannot open Play Counts file \"'+playCountsFilename+'\" for reading.\n')
		return playcountsdict

	# parse the Play Counts db file info, try to stay flexible with different versions
	if verbose>0:
		sys.stdout.write('Parsing Play Counts file \"'+playCountsFilename+'\" for play count info.\n')
	# the record structure is as follows:
	# a file header contains info about the list of items
	# it has n number of entries all of the same length
	# read the header
	if bigstring[:4]=='mhdp':
		headerformat = '<4sLLL'
		header_bytes=struct.calcsize(headerformat)
		(headID, headLen, entryLen, numEntries)=struct.unpack(headerformat,bigstring[:header_bytes])
	else:
		sys.stderr.write('Error: file \"'+playCountsFilename+'\" is not a valid Play Counts file.\n')
		return playcountsdict

	# do a length/size sanity check of the Play Counts file first
	if len(bigstring) != (headLen+(entryLen*numEntries)):
		sys.stderr.write('Error: Play Counts file \"'+playCountsFilename+'\" has corrupt length info.\n')
		return playcountsdict
	else:
		# entry format and size varies depending on iPod Firmware version
		fieldformat = ''
		pad_bytes = 0
		if entryLen == 16:
			fieldformat = '<LLLL'
		elif entryLen == 20:
			fieldformat = '<LLLL4s'
		elif entryLen>20:
			pad_bytes=entryLen-16
			fieldformat = '<LLLL'+str(pad_bytes)+'s'
		else:
			pad_bytes=entryLen-12
			fieldformat = '<LLL'+str(pad_bytes)+'s'

		playcount=0
		lastplayed=0
		for i in range(numEntries):
			offset = headLen + (i * entryLen)
			rating=0
			bookmark=0
			if entryLen == 16:
				(playcount,lastplayed,bookmark,rating) = struct.unpack(fieldformat, bigstring[offset:offset+entryLen])
			elif entryLen == 20:
				(playcount,lastplayed,bookmark,rating,unknown) = struct.unpack(fieldformat, bigstring[offset:offset+entryLen])
			elif entryLen>20:
				(playcount,lastplayed,bookmark,rating,unknown) = struct.unpack(fieldformat, bigstring[offset:offset+entryLen])
			else:
				(playcount,lastplayed,unknown) = struct.unpack(fieldformat, bigstring[offset:offset+entryLen])
			if verbose>2:
				sys.stdout.write(str(i)+': playcount:'+str(playcount)+' lastplayed:'+str(lastplayed+iPodDateOffset)+' rating:'+str(rating)+'\n')

			playcountsdict[i]=(playcount,lastplayed,rating/20)
	if verbose>2:
		sys.stdout.write('Play Counts file entry length is:'+str(entryLen)+' bytes.\n')
	if verbose>0:
		sys.stdout.write('Play counts file contains '+str(len(playcountsdict))+' entries.\n')
	return playcountsdict


def usage():
	"""Output usage message."""
	sys.stdout.write('Usage: '+sys.argv[0]+' [options] [ <iPod source volume/drive> | <iTunesDB file> <Play Counts file> ]\n')
	sys.stdout.write('  Options:\n')
	sys.stdout.write('	[ -h | --help ]  print this message\n')
	sys.stdout.write('	[ -i | --initialize ]  upload timestamps from iTunesDB and not Play Counts file\n')
	sys.stdout.write('	[ -d | --dry-run ]  only print messages but do not upload track timestamps\n')
	sys.stdout.write('	[ -v | --verbose=<level> ]  print messages about activity (0=none, 1=default, 3=all)\n')
	sys.stdout.write('	[ -u | --user=<username> ]  www.last.fm username\n')
	sys.stdout.write('	[ -p | --pass=<password> ]  www.last.fm password\n')
	sys.stdout.write('	[ -n | --name-filter=<regular expression pattern> ]  upload only filenames that match pattern\n')
	sys.stdout.write('	[ -g | --genre-filter=<regular expression pattern> ]  upload only matching genre pattern\n')
	sys.stdout.write('	[ -t | --timestamp=YYYYMMDD-HH:MM:SS ]  upload only track info listened to after/since timestamp (localtime)\n')
	sys.stdout.write('	[ -e | --encoding=<unicode encoding> ]  use this encoding instead of system setting\n')
	sys.stdout.write('\n')

def main():
	"""Main action."""
	global since, verbose, dryRun, uencoding, nameFilter, genreFilter, upload_init, lastfmConnection
	user = u''
	pwd = u''
	iPodSourceVolume=u''
	topLevelFolder = u'iPod_Control'
	iTunesDBFile=u'iTunesDB'
	playCountsFile=u'Play Counts'

	# look for command line options
	opts=[]
	args=[]
	try:
		opts, args = getopt.getopt(sys.argv[1:], "hidv:u:p:n:g:t:", ['help','initialize','dry-run','verbose=','user=','pass=','name-filter=','genre-filter=','timestamp='])
	except getopt.GetoptError:
		# print help information and exit:
		usage()
		sys.exit(2)
	output = None
	for o, a in opts:
		if o in ("-h", "--help"):
			usage()
			sys.exit(0)
		if o in ("-e", "--encoding"):
			try:
				import codecs
				ef,df,srf,swf = codecs.lookup(a)
				uencoding = a
			except LookupError:
				sys.stderr.write('Error: Unicode encoding \"'+a+'\" is unknown or unavailable.\n')
				sys.exit(1)
		if o in ("-v", "--verbose"):
			try:
				verbose = int(a)
			except:
				sys.stderr.write('Error: verbose level \"'+a+'\" must be positive integer.\n')
				sys.exit(1)
		if o in ("-d", "--dry-run"):
			dryRun = 1
		if o in ("-i", "--initialize"):
			upload_init = 1
		if o in ("-u", "--user"):
			try:
				ps = string.strip(a)
				# remove quotes from shell?
				if (ps[0] == '"' and ps[-1] == '"') or (ps[0] == "'" and ps[-1] == "'"):
					ps = ps[1:-1]
				user=unicode(ps,uencoding)
			except:
				sys.stderr.write('Error: username \"'+a+'\" is invalid.\n')
				sys.exit(1)
		if o in ("-p", "--pass"):
			try:
				ps = string.strip(a)
				# remove quotes from shell?
				if (ps[0] == '"' and ps[-1] == '"') or (ps[0] == "'" and ps[-1] == "'"):
					ps = ps[1:-1]
				pwd=unicode(ps,uencoding)
			except:
				sys.stderr.write('Error: password \"'+a+'\" is invalid.\n')
				sys.exit(1)
		if o in ("-n", "--name-filter"):
			try:
				ps = string.strip(a)
				# remove quotes from shell?
				if (ps[0] == '"' and ps[-1] == '"') or (ps[0] == "'" and ps[-1] == "'"):
					ps = ps[1:-1]
				nameFilter=re.compile(unicode(ps,uencoding),re.U)
			except:
				sys.stderr.write('Error: Name filter regular expression pattern \"'+a+'\" is invalid.\n')
				sys.exit(1)
		if o in ("-g", "--genre-filter"):
			try:
				ps = string.strip(a)
				# remove quotes from shell?
				if (ps[0] == '"' and ps[-1] == '"') or (ps[0] == "'" and ps[-1] == "'"):
					ps = ps[1:-1]
				genreFilter=re.compile(unicode(ps,uencoding),re.U|re.I)
			except:
				sys.stderr.write('Error: Genre filter regular expression pattern \"'+a+'\" is invalid.\n')
				sys.exit(1)
		if o in ("-t", "--timestamp"):
			try:
				ps = string.strip(a)
				# remove quotes from shell?
				if (ps[0] == '"' and ps[-1] == '"') or (ps[0] == "'" and ps[-1] == "'"):
					ps = ps[1:-1]
				since=time.mktime(time.strptime(ps,'%Y%m%d-%H:%M:%S'))
			except:
				sys.stderr.write('Error: timestamp pattern \"'+a+'\" is invalid, try \"YYYYMMDD-HH:MM:SS\".\n')
				sys.exit(1)

	if len(args)==1:
		# assume arg is a directory with iTunesDB and Play Counts files in it
		if os.path.exists(args[0]):
			if os.path.isdir(args[0]):
				if os.path.isfile(os.path.join(args[0],'Play Counts')):
					iPodSourceVolume=args[0]
					iTunesDBFile=os.path.join(iPodSourceVolume,'iTunesDB')
					playCountsFile=os.path.join(iPodSourceVolume,'Play Counts')
				elif os.path.isfile(os.path.join(args[0],topLevelFolder,'iTunes','Play Counts')):
					iPodSourceVolume=args[0]
					iTunesDBFile=os.path.join(iPodSourceVolume,topLevelFolder,'iTunes','iTunesDB')
					playCountsFile=os.path.join(iPodSourceVolume,topLevelFolder,'iTunes','Play Counts')
				else:
					sys.stderr.write('Error: Could not find iTunesDB and Play Counts file on \"'+args[0]+'\".\n')
					sys.stderr.write('       Maybe iTunes removed the Play Counts file. Please read the README.\n')
					sys.exit(1)					
			else:
				sys.stderr.write('Error: single argument \"'+args[0]+'\" must be a directory. See --help.\n')
				sys.exit(1)
		else:
			sys.stderr.write('Error: Source directory \"'+args[0]+'\" not found.\n')
			sys.exit(1)
	elif len(args)==2:
		# assume 1=iTunesDB path and 2=Play Counts path
		iTunesDBFile=args[0]
		playCountsFile=args[1]
		if not os.path.isfile(iTunesDBFile):
			sys.stderr.write('Error: Source file \"'+iTunesDBFile+'\" not found or no file.\n')
			sys.exit(1)
		if not os.path.isfile(playCountsFile):
			sys.stderr.write('Error: Source file \"'+playCountsFile+'\" not found or no file.\n')
			sys.exit(1)
	else:
		sys.stderr.write('Error: Please specify one directory or two files. See --help.\n')
		sys.exit(1)
	# check unicode and print warning
	if uencoding=='ascii':
		sys.stdout.write('Warning: your Python site encoding is set to \"ascii\".\n')
		sys.stdout.write('		 This may result in garbled file names as non-ascii characters cannot be converted.\n')
		sys.stdout.write('		 Try \"latin-1\", \"utf8\" or your platform encoding instead for better results.\n')
	else:
		if verbose>0:
			sys.stdout.write('Using local encoding \"'+uencoding+'\".\n')

	if len(user)<1:
		sys.stdout.write('No username given, switching to dry-run.\n')
		dryRun = 1
	if len(pwd)<1:
		sys.stdout.write('No password given, switching to dry-run.\n')
		dryRun = 1
	if verbose>1:
		sys.stdout.write('Reading track info from iPod \"'+iPodSourceVolume+'\".\n')
	mytracks=ReadSongInfo(iTunesDBFile)
	myplaycounts=ReadPlayCountsInfo(playCountsFile)
	lastfmConnection
	if not dryRun:
		lastfmConnection = ScrobbUploader(user,pwd)
		lastfmConnection.handshake()
	UploadTrackLastPlayedInfo(mytracks,myplaycounts)
	# upload remaining
	if not dryRun:
		lastfmConnection.posttracks()

if __name__=="__main__":
	main()
