diff -u -p -r1.3 AmazonCoverArtSearch.py
--- plugins/artdisplay/artdisplay/AmazonCoverArtSearch.py	8 Jul 2006 09:08:17 -0000	1.3
+++ plugins/artdisplay/artdisplay/AmazonCoverArtSearch.py	10 Jul 2006 09:33:32 -0000
@@ -218,7 +218,7 @@ class AmazonCoverArtSearch (object):
 
 		return s
 
-	def get_best_match (self, search_results):
+	def get_best_match_urls (self, search_results):
 		# Default to "no match", our results must match our criteria
 		best_match = None
 
@@ -258,7 +258,10 @@ class AmazonCoverArtSearch (object):
 						if hit:
 							break
 
-			return best_match
+			if best_match: 
+				return [best_match.ImageUrlLarge, best_match.ImageUrlMedium]
+			else:
+				return []
 
 		except TypeError:
-			return None
+			return []
diff -u -p -r1.3 CoverArtDatabase.py
--- plugins/artdisplay/artdisplay/CoverArtDatabase.py	8 Jul 2006 09:08:17 -0000	1.3
+++ plugins/artdisplay/artdisplay/CoverArtDatabase.py	10 Jul 2006 09:33:32 -0000
@@ -1,6 +1,7 @@
 # -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- 
 #
-# Copyright (C) 2006 - Gareth Murphy, Martin Szulecki
+# Copyright (C) 2006 - Gareth Murphy, Martin Szulecki, 
+# Ed Catmur <ed@catmur.co.uk>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -21,16 +22,16 @@ import os
 import gtk
 
 from AmazonCoverArtSearch import AmazonCoverArtSearch
+from LocalCoverArtSearch import LocalCoverArtSearch
 
+ART_SEARCHES_LOCAL = [LocalCoverArtSearch]
+ART_SEARCHES_REMOTE = [AmazonCoverArtSearch]
 ART_FOLDER = '~/.gnome2/rhythmbox/covers'
 
 class CoverArtDatabase (object):
 	def __init__ (self):
 		self.loader = rb.Loader()
 
-	def create_search (self):
-		return AmazonCoverArtSearch (self.loader)
-
 	def build_art_cache_filename (self, album, artist, extension):
 		art_folder = os.path.expanduser (ART_FOLDER)
 		if not os.path.exists (art_folder):
@@ -41,20 +42,20 @@ class CoverArtDatabase (object):
 		# FIXME: the following block of code is messy and needs to be redone ASAP
 		return art_folder + '/%s - %s.%s' % (artist.replace ('/', '-'), album.replace ('/', '-'), extension)	
 
+	def engines (self, blist):
+		for Engine in ART_SEARCHES_LOCAL:
+			yield Engine (self.loader), Engine.__name__, False
+		for Engine in ART_SEARCHES_REMOTE:
+			if Engine.__name__ not in blist:
+				yield Engine (self.loader), Engine.__name__, True
+  
 	def get_pixbuf (self, db, entry, callback):
 		if entry is None:
 			callback (entry, None)
 			return
             
-		st_artist = db.entry_get (entry, rhythmdb.PROP_ARTIST)
-		st_album = db.entry_get (entry, rhythmdb.PROP_ALBUM)
-
-		# Handle special case
-		if st_album == "":
-			st_album = "Unknown"
-		if st_artist == "":
-			st_artist = "Unknown"
-
+		st_artist = db.entry_get (entry, rhythmdb.PROP_ARTIST) or "Unknown"
+		st_album = db.entry_get (entry, rhythmdb.PROP_ALBUM) or "Unknown"
 		# If unknown artist and album there is no point continuing
 		if st_album == "Unknown" and st_artist == "Unknown":
 			callback (entry, None)
@@ -66,6 +67,9 @@ class CoverArtDatabase (object):
 			st_artist = st_artist.replace (char, '')
 			st_album = st_album.replace (char, '')
 
+		rb.Coroutine (self.image_search, db, st_album, st_artist, entry, callback).begin ()
+
+	def image_search (self, plexer, db, st_album, st_artist, entry, callback):
 		art_location = self.build_art_cache_filename (st_album, st_artist, "jpg")
 		blist_location = self.build_art_cache_filename (st_album, st_artist, "rb-blist")
 
@@ -73,67 +77,54 @@ class CoverArtDatabase (object):
 		if os.path.exists (art_location):
 			pixbuf = gtk.gdk.pixbuf_new_from_file (art_location)	
 			callback (entry, pixbuf)
-		# Check for unsuccessful previous image download to prevent overhead search
-		elif os.path.exists (blist_location):
-			callback (entry, None)
-		else:
-			# Otherwise spawn (online) search-engine search
-			se = self.create_search ()
-			se.search (db, entry, self.on_search_engine_results, callback)
-
-	def on_search_engine_results (self, search_engine, entry, results, callback):
-		if results is None:
-			self._do_blacklist_and_callback (search_engine, callback)
 			return
 
-		# Get best match from results
-		best_match = search_engine.get_best_match (results)
-
-		if best_match is None:
-			self._do_blacklist_and_callback (search_engine, callback)
-			return
-
-		# Attempt to download image for best match
-		pic_url = str (best_match.ImageUrlLarge)
-		self.loader.get_url (pic_url, self.on_image_data_received, search_engine, "large", callback, best_match)
-
-	def _do_blacklist_and_callback (self, search_engine, callback):
-		self._create_blacklist (search_engine.st_artist, search_engine.st_album)
-		callback (search_engine.entry, None)
-
-	def _create_blacklist (self, artist, album):
-		location = self.build_art_cache_filename (album, artist, "rb-blist")
-		f = file (location, 'w')
-		f.close ()
-		return location
-
-	def _create_artwork (self, artist, album, image_data):
-		location = self.build_art_cache_filename (album, artist, "jpg")
-		f = file (location, 'wb')
-		f.write (image_data)
-		f.close ()
-		return location
-
-	def on_image_data_received (self, image_data, search_engine, image_version, callback, best_match):
-		if image_data is None:
-			res = search_engine.search_next ()
-			if not res:
-				self._do_blacklist_and_callback (search_engine, callback)
-			return
+		blist = self.read_blist (blist_location)
+		for engine, engine_name, engine_remote in self.engines (blist):
+			plexer.clear ()
+			engine.search (db, entry, plexer.send ())
+			while True:
+				yield None
+				_, (engine, entry, results) = plexer.receive ()
+				if not results:
+					break
+				for url in engine.get_best_match_urls (results):
+					yield self.loader.get_url (str (url), plexer.send ())
+					_, (data, ) = plexer.receive ()
+					pixbuf = self.image_data_load (data)
+					if pixbuf:
+						if engine_remote:
+							pixbuf.save (art_location, "jpeg", {"quality": "100"})
+						self.write_blist (blist_location, blist)
+						callback (entry, pixbuf)
+						return
+				if not engine.search_next ():
+					if engine_remote:
+						blist.append (engine_name)
+					break
+		self.write_blist (blist_location, blist)
+		callback (entry, None)
+
+	def read_blist (self, blist_location):
+		if os.path.exists (blist_location):
+			return [line.strip () for line in file (blist_location)]
+		else:
+			return []
 
-		if len (image_data) < 1000:
-			if image_version == "large" and best_match is not None:
-				# Fallback and try to load medium one
-				pic_url = str (best_match.ImageUrlMedium)
-				self.loader.get_url (pic_url, self.on_image_data_received, search_engine, "medium", callback, best_match)
-				return
-
-			res = search_engine.search_next ()
-			if not res:
-				# only write the blist if there are no more queries to try
-				self._do_blacklist_and_callback (search_engine, callback)
+	def write_blist (self, blist_location, blist):
+		if blist:
+			blist_file = file (blist_location, 'w')
+			blist_file.writelines ("%s\n" % b for b in blist)
+			blist_file.close ()
+		elif os.path.exists (blist_location):
+			os.path.unlink (blist_location)
 
-		else:
-			location = self._create_artwork (search_engine.st_artist, search_engine.st_album, image_data)
-			pixbuf = gtk.gdk.pixbuf_new_from_file (location)
-			callback (search_engine.entry, pixbuf)
+	def image_data_load (self, data):
+		if data and len (data) >= 1000:
+			pbl = gtk.gdk.PixbufLoader ()
+			try:
+				if pbl.write (data) and pbl.close ():
+					return pbl.get_pixbuf ()
+			except GError:
+				pass
+		return None
diff -u -p -r1.2 Makefile.am
--- plugins/artdisplay/artdisplay/Makefile.am	8 Jul 2006 09:08:17 -0000	1.2
+++ plugins/artdisplay/artdisplay/Makefile.am	10 Jul 2006 09:33:32 -0000
@@ -3,5 +3,6 @@
 plugindir = $(PLUGINDIR)/artdisplay
 plugin_PYTHON = 			\
 	AmazonCoverArtSearch.py		\
+	LocalCoverArtSearch.py		\
 	CoverArtDatabase.py		\
 	__init__.py
diff -u -p -r1.1 Makefile.am
--- plugins/rb/Makefile.am	8 Jul 2006 09:08:20 -0000	1.1
+++ plugins/rb/Makefile.am	10 Jul 2006 09:33:32 -0000
@@ -2,5 +2,6 @@
 plugindir = $(pkgdatadir)/plugins/rb/
 plugin_PYTHON = \
 		Loader.py		\
+		Coroutine.py		\
 		__init__.py
 
diff -u -p -r1.2 __init__.py
--- plugins/rb/__init__.py	9 Jul 2006 13:39:22 -0000	1.2
+++ plugins/rb/__init__.py	10 Jul 2006 09:33:32 -0000
@@ -2,6 +2,7 @@ import sys
 
 # rb classes
 from Loader import Loader
+from Coroutine import Coroutine
 
 def _excepthandler (exc_class, exc_inst, trace):
 	import sys
--- /dev/null	2006-05-23 00:25:23.000000000 +1000
+++ plugins/artdisplay/artdisplay/LocalCoverArtSearch.py	2006-07-10 17:54:26.000000000 +1000
@@ -0,0 +1,55 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- 
+#
+# Copyright (C) 2006 - Ed Catmur <ed@catmur.co.uk>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+
+import os
+import rhythmdb
+import gnomevfs
+
+IMAGE_NAMES = ["cover", "album", "albumart", ".folder", "folder"]
+LOAD_DIRECTORY_FLAGS = gnomevfs.FILE_INFO_GET_MIME_TYPE | gnomevfs.FILE_INFO_FORCE_FAST_MIME_TYPE
+
+def file_root (f_name):
+	return os.path.splitext (f_name)[0].lower ()
+
+class LocalCoverArtSearch:
+	def __init__ (self, loader):
+		self.loader = loader
+
+	def _load_dir_cb (self, handle, files, exception, (results, on_search_completed_cb, entry, args)):
+		for f in files:
+			if f.mime_type.startswith ("image/") and f.permissions & gnomevfs.PERM_USER_READ:
+				results.append (f.name)
+		if exception:
+			on_search_completed_cb (self, entry, results, *args)
+			if not issubclass (exception, gnomevfs.EOFError):
+				print "Error reading \"%s\": %s" % (self.uri.parent, exception)
+
+	def search (self, db, entry, on_search_completed_cb, *args):
+		self.uri = gnomevfs.URI (db.entry_get (entry,rhythmdb.PROP_LOCATION))
+		gnomevfs.async.load_directory (self.uri.parent, self._load_dir_cb, LOAD_DIRECTORY_FLAGS, data=([], on_search_completed_cb, entry, args))
+
+	def search_next (self):
+		return False
+
+	def get_best_match_urls (self, results):
+		# Compare lower case, without file extension
+		for name in [file_root (self.uri.short_name)] + IMAGE_NAMES:
+			for f_name in results:
+				if file_root (f_name) == name:
+					yield self.uri.parent.append_file_name (f_name).path
+
--- /dev/null	2006-05-23 00:25:23.000000000 +1000
+++ plugins/rb/Coroutine.py	2006-07-10 16:03:48.000000000 +1000
@@ -0,0 +1,57 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- 
+#
+# Copyright (C) 2006 - Ed Catmur <ed@catmur.co.uk>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+
+class Coroutine:
+	"""A simple message-passing coroutine implementation. 
+	Not thread- or signal-safe. 
+	Usage:
+		def my_iter (plexer, args):
+			some_async_task (..., callback=plexer.send (tokens))
+			yield None
+			tokens, (data, ) = plexer.receive ()
+			...
+		Coroutine (my_iter, args).begin ()
+	"""
+	def __init__ (self, iter, *args):
+		self._continuation = iter (self, *args)
+		self._executing = False
+	def _resume (self):
+		if not self._executing:
+			self._executing = True
+			try:
+				try:
+					self._continuation.next ()
+					while self._data:
+						self._continuation.next ()
+				except StopIteration:
+					pass
+			finally:
+				self._executing = False
+	def clear (self):
+		self._data = []
+	def begin (self):
+		self.clear ()
+		self._resume ()
+	def send (self, *tokens):
+		def callback (*args):
+			self._data.append ((tokens, args))
+			self._resume ()
+		return callback
+	def receive (self):
+		return self._data.pop (0)
+
