Amazon: Use batch requests
---
 .../artdisplay/artdisplay/AmazonCoverArtSearch.py  |  128 +++++++++++---------
 1 files changed, 68 insertions(+), 60 deletions(-)

diff --git a/plugins/artdisplay/artdisplay/AmazonCoverArtSearch.py b/plugins/artdisplay/artdisplay/AmazonCoverArtSearch.py
index 489d302..13ea35a 100644
--- a/plugins/artdisplay/artdisplay/AmazonCoverArtSearch.py
+++ b/plugins/artdisplay/artdisplay/AmazonCoverArtSearch.py
@@ -27,6 +27,10 @@ LICENSE_KEY = "18C3VZN9HCECM5G3HQG2"
 DEFAULT_LOCALE = "en_US"
 ASSOCIATE = "webservices-20"
 
+# We are not allowed to batch more than 2 requests at once
+# http://docs.amazonwebservices.com/AWSEcommerceService/4-0/PgCombiningOperations.html
+MAX_BATCH_JOBS = 2
+
 
 class Bag: pass
 
@@ -35,29 +39,31 @@ class AmazonCoverArtSearch (object):
 		self.searching = False
 		self.cancel = False
 		self.loader = loader
+		self.db = None
+		self.entry = None
+		(self.tld, self.encoding) = self.__get_locale ()
+
+	def __get_locale (self):
 		# "JP is the only locale that correctly takes UTF8 input. All other locales use LATIN1."
 		# http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1295&categoryID=117
-		self._supportedLocales = {
+		supported_locales = {
 			"en_US" : ("com", "latin1"),
 			"en_GB" : ("co.uk", "latin1"),
 			"de" : ("de", "latin1"),
 			"ja" : ("jp", "utf8")
 		}
-		self.db = None
-		self.entry = None
 
-	def __get_locale (self):
-		default = locale.getdefaultlocale ()
 		lc_id = DEFAULT_LOCALE
-		if default[0] is not None:
-			if self._supportedLocales.has_key (default[0]):
-				lc_id = default[0]
+		default = locale.getdefaultlocale ()[0]
+		if default:
+			if supported_locales.has_key (default):
+				lc_id = default
 			else:
-				lang = default[0].split("_")[0]
-				if self._supportedLocales.has_key (lang):
+				lang = default.split("_")[0]
+				if supported_locales.has_key (lang):
 					lc_id = lang
 
-		return self._supportedLocales[lc_id]
+		return supported_locales[lc_id]
 
 	def search (self, db, entry, on_search_completed_callback, *args):
 		self.searching = True
@@ -71,6 +77,10 @@ class AmazonCoverArtSearch (object):
 		st_artist = db.entry_get (entry, rhythmdb.PROP_ARTIST) or _("Unknown")
 		st_album = db.entry_get (entry, rhythmdb.PROP_ALBUM) or _("Unknown")
 
+		if st_artist == st_album == _("Unknown"):
+			self.on_search_completed (None)
+			return
+
 		# Tidy up
 
 		# Replace quote characters
@@ -118,60 +128,53 @@ class AmazonCoverArtSearch (object):
 				self.keywords.append ("%s %s" % (st_artist, st_album))
 				if st_album_no_vol != st_album:
 					self.keywords.append ("%s %s" % (st_artist, st_album_no_vol))
-				if (st_album != _("Unknown")):
-					self.keywords.append ("Various %s" % (st_album))
+				self.keywords.append ("Various %s" % (st_album))
 			self.keywords.append ("%s" % (st_artist))
 
 		# Initiate asynchronous search
-		self.search_next ();
+		self.search_next ()
 
-	def __build_url (self, keyword):
-		(tld, encoding) = self.__get_locale ()
+	def search_next (self):
+		if len (self.keywords) == 0:
+			# No keywords left to search -> no results
+			self.on_search_completed (None)
+			return False
 
-		url = "http://ecs.amazonaws." + tld + "/onca/xml" \
-		      "?Service=AWSECommerceService"              \
-		      "&AWSAccessKeyId=" + LICENSE_KEY +          \
-		      "&AssociateTag=" + ASSOCIATE +              \
-		      "&ResponseGroup=Images,ItemAttributes"      \
-		      "&Operation=ItemSearch"                     \
-		      "&SearchIndex=Music"                        \
-		      "&Keywords=" + urllib.quote (keyword.encode (encoding, 'replace'))
-		print "url: '%s'" % (url)
+		self.searching = True
 
-		return url
+		url = "http://ecs.amazonaws." + self.tld + "/onca/xml" \
+		      "?Service=AWSECommerceService"                   \
+		      "&AWSAccessKeyId=" + LICENSE_KEY +               \
+		      "&AssociateTag=" + ASSOCIATE +                   \
+		      "&ResponseGroup=Images,ItemAttributes"           \
+		      "&Operation=ItemSearch"                          \
+		      "&ItemSearch.Shared.SearchIndex=Music"
 
-	def search_next (self):
-		self.searching = True
-		
-		if len (self.keywords)==0:
-			keyword = None
-		else:
+		job = 1
+		while job <= MAX_BATCH_JOBS and len (self.keywords) > 0:
 			keyword = self.keywords.pop (0)
+			keyword = keyword.encode (self.encoding, "ignore")
+			keyword = keyword.strip ()
+			keyword = urllib.quote (keyword)
+			url += "&ItemSearch.%d.Keywords=%s" % (job, keyword)
+			job += 1
 
-		if keyword is None:
-			# No keywords left to search -> no results
-			self.on_search_completed (None)
-			ret = False
-		else:
-			# Retrieve search for keyword
-			url = self.__build_url (keyword.strip ())
-			self.loader.get_url (url, self.on_search_response)
-			ret = True
-
-		return ret
+		# Retrieve search for keyword
+		self.loader.get_url (url, self.on_search_response)
+		return True
 
 	def __unmarshal (self, element):
 		rc = Bag ()
-		childElements = [e for e in element.childNodes if isinstance (e, minidom.Element)]
-		if childElements:
-			for child in childElements:
+		child_elements = [e for e in element.childNodes if isinstance (e, minidom.Element)]
+		if child_elements:
+			for child in child_elements:
 				key = child.tagName
 				if hasattr (rc, key):
 					if not isinstance (getattr (rc, key), list):
 						setattr (rc, key, [getattr (rc, key)])
 					getattr (rc, key).append (self.__unmarshal (child))
 				# get_best_match_urls() wants a list, even if there is only one item/artist
-				elif (child.tagName == "Item") or (child.tagName == "Artist"):
+				elif child.tagName in ("Items", "Item", "Artist"):
 					setattr (rc, key, [self.__unmarshal(child)])
 				else:
 					setattr (rc, key, self.__unmarshal(child))
@@ -190,14 +193,14 @@ class AmazonCoverArtSearch (object):
 			self.search_next()
 			return
 		
-		data = self.__unmarshal (xmldoc).ItemSearchResponse.Items
-
-		if hasattr (data.Request, "Errors"):
-			# Search was unsuccessful, try next keyword
+		data = self.__unmarshal (xmldoc)
+		if not hasattr (data, "ItemSearchResponse") or \
+		   not hasattr (data.ItemSearchResponse, "Items"):
+			# Something went wrong ...
 			self.search_next ()
 		else:
 			# We got some search results
-			self.on_search_results (data.Item)
+			self.on_search_results (data.ItemSearchResponse.Items)
 
 	def on_search_results (self, results):
 		self.on_search_completed (results)
@@ -227,11 +230,15 @@ class AmazonCoverArtSearch (object):
 		# Default to "no match", our results must match our criteria
 		best_match = None
 
-		search_results = filter(self.__valid_match, search_results)
-		try:
+		for result in search_results:
+			if not hasattr (result, "Item"):
+				# Search was unsuccessful, try next batch job
+				continue
+
+			items = filter(self.__valid_match, result.Item)
 			if self.search_album != _("Unknown"):
 				album_check = self.__tidy_up_string (self.search_album)
-				for item in search_results:
+				for item in items:
 					if not hasattr (item.ItemAttributes, "Title"):
 						continue
 
@@ -255,7 +262,7 @@ class AmazonCoverArtSearch (object):
 				if best_match is None:
 					# Check if artist appears in the Artists list
 					hit = False
-					for item in search_results:
+					for item in items:
 						if not hasattr (item.ItemAttributes, "Artist"):
 							continue
 
@@ -268,9 +275,10 @@ class AmazonCoverArtSearch (object):
 						if hit:
 							break
 
-			return [getattr (best_match, size).URL for size in ("LargeImage", "MediumImage")
+			urls = [getattr (best_match, size).URL for size in ("LargeImage", "MediumImage")
 			        if hasattr (best_match, size)]
+			if urls:
+				return urls
 
-		except TypeError, e:
-			print "TypeError: %s" % (e)
-			return []
+		# No search was successful
+		return []
