
# https://github.com/albertz/itunes-scripts

# Copyright (c) 2012, Albert Zeyer, www.az2000.de
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met: 
# 
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer. 
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution. 
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


import sys
import codecs # utf8
import os

try:
	libraryXmlFile = codecs.open(os.path.expanduser("~/Music/iTunes/iTunes Music Library.xml"), "r", "utf-8")
except IOError: # file not found or so
	libraryXmlFile = None

def parse_xml(stream):
	state = 0
	spaces = " \t\n"
	data = ""
	node = ""
	nodeprefix = ""
	nodepostfix = ""
	nodeargs = []
	while True:
		c = stream.read(1)
		if c == "": break

		oldstate = state
		if state == 0:
			if c == "<": state = 1
			else: data += c
		elif state == 1: # in node
			if c in spaces:
				if node == "": pass
				else: state = 2
			elif c in "!/?":
				if node == "": nodeprefix += c
				else: nodepostfix += c
			elif c == ">": state = 0
			elif c == "\"": state = 3
			else: node += c
		elif state == 2: # in nodeargs
			if c in spaces: pass
			elif c == ">": state = 0
			elif c == "\"": state = 3
			elif c == "/": nodepostfix += c
			else:
				nodeargs.append(c)
				state = 4
		elif state == 3: # in nodearg str
			if c == "\\": state = 5
			elif c == "\"": state = 2
			else: pass # we dont store it right now
		elif state == 4: # in nodearg
			if c in spaces: state = 2
			elif c == ">": state = 0
			elif c == "\"": state = 3
			elif c == "/": nodepostfix += c
			else: nodeargs[-1] += c
		elif state == 5: # in escaped nodearg str
			# we dont store it right now
			state = 3

		if oldstate > 0 and state == 0:
			yield nodeprefix + node + nodepostfix, nodeargs, data
			nodeprefix, node, nodepostfix = "", "", ""
			nodeargs = []
			data = ""

import base64
def _plistDataConv(data):
	data = data.replace(" ", "")
	data = data.replace("\t", "")
	data = data.replace("\n", "")
	return base64.b64decode(data)

# code from here: http://wiki.python.org/moin/EscapingXml
import xml.parsers.expat
def xmlUnescape(s):
	want_unicode = False
	if isinstance(s, unicode):
		s = s.encode("utf-8")
		want_unicode = True

	# the rest of this assumes that `s` is UTF-8
	list = []

	# create and initialize a parser object
	p = xml.parsers.expat.ParserCreate("utf-8")
	p.buffer_text = True
	p.returns_unicode = want_unicode
	p.CharacterDataHandler = list.append

	# parse the data wrapped in a dummy element
	# (needed so the "document" is well-formed)
	p.Parse("<e>", 0)
	p.Parse(s, 0)
	p.Parse("</e>", 1)

	# join the extracted strings and return
	es = ""
	if want_unicode:
		es = u""
	return es.join(list)

plistPrimitiveTypes = {"integer": int, "real": float, "string": xmlUnescape, "date": str, "data": _plistDataConv}

def parse_plist_content(xmlIter, prefix, nodeExceptions = {}):
	for node, nodeargs, data in xmlIter:
		if node in nodeExceptions:
			raise nodeExceptions[node]
		elif node == "array":
			for entry in parse_plist_arrayContent(xmlIter, prefix): yield entry
		elif node == "dict":
			for entry in parse_plist_dictContent(xmlIter, prefix): yield entry
		elif node in plistPrimitiveTypes:
			for entry in parse_plist_primitiveContent(xmlIter, prefix, node): yield entry
		elif node == "true/":
			yield prefix, True
		elif node == "false/":
			yield prefix, False
		else:
			print >>sys.stderr, "didnt expected node", repr(node), "in content in prefix", repr(prefix)
		break
		
def parse_plist_primitiveContent(xmlIter, prefix, contentType):
	for node, nodeargs, data in xmlIter:
		if node == "/" + contentType:
			yield prefix, plistPrimitiveTypes[contentType](data)
		else:
			print >>sys.stderr, "didnt expected node", repr(node), "in primitive content", repr(contentType), "in prefix", repr(prefix)
		break

class PlistMarkerArrayBegin: pass
class PlistMarkerArrayEnd: pass

def parse_plist_arrayContent(xmlIter, prefix):
	yield prefix, PlistMarkerArrayBegin
	index = 0
	while True:
		try:
			for entry in parse_plist_content(xmlIter, prefix + [index], {"/array": PlistMarkerArrayEnd()}):
				yield entry
		except PlistMarkerArrayEnd:
			break
		index += 1
	yield prefix, PlistMarkerArrayEnd

class PlistMarkerDictBegin: pass
class PlistMarkerDictEnd: pass

# dict in plist is a list of key/value pairs
def parse_plist_dictContent(xmlIter, prefix):
	lastkey = None
	yield prefix, PlistMarkerDictBegin
	for node, nodeargs, data in xmlIter:
		if node == "key": pass
		elif node == "/key":
			if lastkey is not None: print >>sys.stderr, "expected value after key in dict content in prefix", repr(prefix)
			lastkey = data
			for entry in parse_plist_content(xmlIter, prefix + [lastkey]):
				yield entry
			lastkey = None
		elif node == "/dict": break
		else:
			print >>sys.stderr, "didn't expected node", repr(node), "in dict content in prefix", repr(prefix)
	yield prefix, PlistMarkerDictEnd

def parse_plist(xmlIter):
	for node, nodeargs, data in xmlIter:
		if node == "plist":
			for entry in parse_plist_content(xmlIter, []): yield entry

if libraryXmlFile:
	libraryPlistIter = parse_plist(parse_xml(libraryXmlFile))
else:
	libraryPlistIter = []

def songsIter(plistIter):
	for prefix, value in plistIter:
		if len(prefix) == 2 and prefix[0] == "Tracks" and value is PlistMarkerDictBegin:
			song = {}
			for prefix2, value2 in plistIter:
				if prefix2 == prefix and value2 is PlistMarkerDictEnd: break
				song[prefix2[2]] = value2
			if "Rating" not in song: song["Rating"] = None
			yield song

librarySongsIter = songsIter(libraryPlistIter)

def ratingsIter():
	import urllib
	import re
	import utils
	for song in librarySongsIter:
		rating = song["Rating"]
		if rating is None: continue # print only songs with any rating set
		rating /= 100.0 # the maximum is 100
		fn = song["Location"]
		fn = urllib.unquote(fn)
		fn = re.sub("^file://(localhost)?", "", fn)
		fn = utils.convertToUnicode(fn)
		yield (fn, rating)


if __name__ == "__main__":
	for fn, rating in ratingsIter():
		print rating, repr(fn)
	sys.exit()

def loadRatings():
	def doCalc(queue):
		for fn, rating in ratingsIter():
			queue.put((fn,rating))
		queue.put((None,None))
		
	from utils import AsyncTask
	queue = AsyncTask(func=doCalc, name="iTunes load ratings")
	
	while True:
		fn, rating = queue.get()
		if fn is None: return
		ratings[fn] = rating

# do some extra check in case we are reloading this module. don't reload the ratings. takes too long
try:
	loadRatingsThread
except NameError:
	ratings = {}
	from utils import daemonThreadCall
	daemonThreadCall(loadRatings, name = "iTunes ratings loader")
