Plugins: albumart.4.py

File albumart.4.py, 20.2 kB (added by wxcover, 2 months ago)

New version (0.4) of albumart : uses pyAWS; searches on amazon, walmart, darktown and buy.com; search box available; few fixes.

Line 
1 #Copyright 2005-2008 Eduardo Gonzalez, Niklas Janlert, Christoph Reiter, Antonio Riva
2 #Amazon API code by Mark Pilgrim
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 2 as
6 # published by the Free Software Foundation
7
8 # Last Modified: Tue 2008-05-13 19:40:12 (+0200) by <wxcover@users.sourceforge.net>
9 # -Added walmart, darktown and buy.com cover searching.
10 # -Few fixes
11 # -Updated version number (0.25 -> 0.4)
12
13 # Mon 2008-05-05 14:54:27 (-0400)
14 # Updated for new Amazon API by Jeremy Cantrell <jmcantrell@gmail.com>
15
16 import os
17 import sys
18 import urllib
19 import re
20 import time
21 import threading
22 from cStringIO import StringIO
23 import gtk
24 import gobject
25 import pango
26 import util
27 import qltk
28 import config
29
30 from plugins.songsmenu import SongsMenuPlugin
31
32 if sys.version_info < (2, 4): from sets import Set as set
33
34 from pyaws import ecs
35
36 class AlbumArtWindow(gtk.Window):
37     def __init__(self, songs):
38         gtk.Window.__init__(self)
39         self.set_border_width(12)
40         self.set_title("AlbumArt")
41         self.set_default_size(850, 550)
42
43         #TreeView stuff
44         self.liststore = liststore = gtk.ListStore(object, str)
45         treeview = gtk.TreeView(liststore)
46         treeview.set_headers_visible(False)
47         selection = treeview.get_selection()
48         selection.set_mode(gtk.SELECTION_SINGLE)
49         selection.connect("changed", self.__preview)
50
51         self.url = url = gtk.Entry()
52         url.set_text(songs[0]("artist") + " " + songs[0]("album"))
53        
54         urlButton = gtk.Button("Search")
55         urlButton.connect("clicked", self.__start_search, url,liststore)
56         url.connect("key-release-event", self.key_start_search, url,liststore)
57      
58         urlBox= gtk.HBox()
59         urlBox.pack_start(url, expand=True, fill=True)
60         urlBox.pack_start(urlButton, expand=False, fill=False)
61
62         rend = gtk.CellRendererPixbuf()
63         def cell_data(column, cell, model, iter):
64             cell.set_property("pixbuf", model[iter][0]["thumb"])
65         tvcol1 = gtk.TreeViewColumn("Pixbuf", rend)
66         tvcol1.set_cell_data_func(rend, cell_data)
67         tvcol1.set_sizing(gtk.TREE_VIEW_COLUMN_GROW_ONLY)
68         rend.set_property('xpad', 2)
69         rend.set_property('ypad', 2)
70         rend.set_property('width', 56)
71         rend.set_property('height', 56)
72         treeview.append_column(tvcol1)
73
74         rend = gtk.CellRendererText()
75         rend.set_property("ellipsize", pango.ELLIPSIZE_END)
76         tvcol2 = gtk.TreeViewColumn("Info", rend, markup=1)
77         treeview.append_column(tvcol2)
78
79         sw = gtk.ScrolledWindow()
80         sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
81         sw.set_shadow_type(gtk.SHADOW_IN)
82         sw.add(treeview)
83
84         #Image frame and save button
85         self.image = image = gtk.Image()
86         frame = gtk.Frame()
87         frame.set_shadow_type(gtk.SHADOW_IN)
88         frame.add(image)
89         scrolled = gtk.ScrolledWindow()
90         scrolled.add_with_viewport(frame)
91         scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
92         vbox = gtk.VBox(spacing=5)
93         vbox.pack_start(scrolled)
94         self.button = button = gtk.Button(stock=gtk.STOCK_SAVE)
95         button.set_sensitive(False)
96         def save_cb(button, combo):
97             model, path = selection.get_selected()
98             data = model[path][0]["cover_data"]
99             fname = self.__get_fname(songs, combo)
100             self.__save_cover(data, fname)
101         combo = gtk.combo_box_new_text()
102         try: set_fn = config.get("plugins", "cover_fn")
103         except: set_fn = ".folder.jpg"
104         active = -1
105         for i, fn in enumerate([".folder.jpg", "folder.jpg", "cover.jpg"]):
106             combo.append_text(fn)
107             if fn == set_fn: active = i
108         if active == -1:
109             combo.append_text(set_fn)
110             combo.set_active(len(combo.get_model()) - 1)
111         else: combo.set_active(active)
112         button.connect("clicked", save_cb, combo)
113         bbox = gtk.HButtonBox()
114         bbox.pack_start(combo)
115         bbox.pack_start(button, expand=False, fill=False)
116         bbox.set_layout(gtk.BUTTONBOX_SPREAD)
117         vbox.pack_start(bbox, expand=False, fill=False)
118
119         hpaned = gtk.HPaned()
120         hpaned.pack1(sw)
121         hpaned.pack2(vbox)
122         hpaned.set_position(300)
123
124         vbox= gtk.VBox()
125         vbox.pack_start(urlBox, expand=False, fill=False)
126         vbox.pack_start(hpaned, expand=True, fill=True)
127         self.add(vbox)
128
129         #define search engines
130         self.engines = [self.__search_amazon, self.__search_walmart,
131                         self.__search_darktown, self.__search_buy]
132
133         #max covers per site
134         self.max_albums = 10
135        
136         #for progress calculation
137         self.progress = 0
138        
139         #use albumartist if available
140         if songs[0]("albumartist"):
141             artist = songs[0]("albumartist")
142         else:
143             artist = songs[0]("artist")
144         query = artist + " " + songs[0]("album")
145         query = query.encode("latin1", "replace")
146        
147         #show search status row
148         iter = self.liststore.insert(0)
149         self.liststore.set(iter, 0, {"bag":None,"thumb":None,"thumb_data":None})
150         self.liststore.set_value(iter, 1,
151                                  "<i><b>Searching progress: 0%</b></i>")
152
153         #start all search engines in seperate threads
154         for i in xrange(len(self.engines)):
155             thread = threading.Thread(target=self.__search,
156                                       args=(query,i,iter,))
157             thread.setDaemon(True)
158             thread.start()
159
160         self.show_all()
161
162     def key_start_search(self, widget,event, entry,liststore):
163         if(event.keyval==65293):
164             self.__start_search( widget, entry,liststore)
165
166
167     def __start_search(self, widget, entry,liststore):
168         liststore.clear()
169         self.progress = 0
170         entry_text = entry.get_text()
171         print "Search: %s\n" % entry_text
172        
173         iter = liststore.insert(0)
174         liststore.set(iter, 0, {"bag":None,"thumb":None,"thumb_data":None})
175         liststore.set_value(iter, 1,
176                                  "<i><b>Searching progress: 0%</b></i>")
177                                  
178         for i in xrange(len(self.engines)):
179             thread = threading.Thread(target=self.__search, args=(entry_text,i,iter,))
180             thread.setDaemon(True)
181             thread.start()
182
183     def __destroy_cb(self, widget, *args):
184         widget.destroy()
185         #self.destroy()
186
187     def __search(self, query, num, iter):
188
189         try:
190             bags = self.engines[num](query)
191             for bag in bags:
192                 gobject.idle_add(self.__add_bag, self.liststore, bag)
193         except:
194             pass
195        
196         self.progress += 1
197         self.liststore.set_value(iter, 1, "<i><b>Searching progress: "
198                                  +str(self.progress*100/len(self.engines))
199                                  +"%</b></i>")
200
201         if self.progress == len(self.engines):
202             time.sleep(2)
203             if self.liststore.iter_next(iter) == None:
204                 self.liststore.set_value(iter, 1,
205                                          "<i><b>No albumart found</b></i>")
206             else:
207                 self.liststore.remove(iter)
208
209     def __search_amazon(self, query):
210         ecs.setLicenseKey("0RKH4ZH1JCFZHMND91G2")
211         bags = []
212         try:
213             query = query.encode("latin1", 'replace')
214             bags = ecs.ItemSearch(query, SearchIndex='Music', ResponseGroup='Images,Small').cache
215             # -- replace the previous line by the 2 next if you want to display more than 10 covers from amazon (slower) --
216             #for item in ecs.ItemSearch(query, SearchIndex='Music', ResponseGroup='Images,Small'):
217             #    bags.append(item);
218         except ecs.AWSException, msg:
219             dialog = qltk.Message(gtk.MESSAGE_ERROR, None, "Search error", msg)
220             dialog.connect('response', self.__destroy_cb)
221             gobject.idle_add(dialog.show)
222         except UnicodeEncodeError, msg:
223             dialog = qltk.Message(gtk.MESSAGE_ERROR, None, "Encoding error",
224                                   msg)
225             dialog.connect('response', self.__destroy_cb)
226             gobject.idle_add(dialog.show)
227         else:
228             # Just keep the top x matches
229             return bags[:self.max_albums]
230         return []
231            
232     def __search_darktown(self, query):
233         class item: pass
234         bags = []
235        
236         #Artists @ Darkdown often miss the leading "The", better remove it
237         #for better search results - also ' and . should be removed
238         if query[:4] == "The ":
239             query = query[4:]
240         query = query.replace("'"," ")
241         query = query.replace(".","")
242        
243         mainUrl = urllib.urlopen('http://www.darktown.to/search.php?'
244                 'action=search&what='+urllib.quote(query)+'&category=audio')
245         mainData = mainUrl.read()
246         mainRe = re.findall('javascript:openCentered\(\'(.*?)Front\'', mainData)
247        
248         count = 0
249         for result in mainRe:
250             if count >= self.max_albums: break
251             count += 1
252            
253             resultUrl = urllib.urlopen('http://www.darktown.to'+result+'Front')
254             resultData = resultUrl.read()
255
256             resultImgBig = re.findall('href="(.*?)">DOWNLOAD</a>', resultData)
257             resultImgSmall = re.findall('src="http://img.darktown.to/thumbnail.php(.*?)"', resultData)
258             resultArtist = re.findall('<b><font size=4>(.*?)</font', resultData)
259             resultTitle = re.findall('</font></b><br><b>(.*?)</b><br><br><b>',
260                                      resultData)
261
262             cover = item()
263             cover.SmallImage = item()
264             cover.LargeImage = item()
265             cover.SmallImage.URL = 'http://img.darktown.to/thumbnail.php'+resultImgSmall[0]
266             cover.LargeImage.URL = resultImgBig[0]
267             cover.Artists = item()
268             cover.Artists.Artist = resultArtist[0].decode("latin1", "replace")
269             cover.ProductName = resultTitle[0].decode("latin1", "replace")
270            
271             bags.append(cover)
272  
273         return bags
274        
275     def __search_walmart(self, query):
276         class item: pass
277         bags = []
278
279         mainUrl = urllib.urlopen('http://www.walmart.com/search/search-ng.do?'
280                                  'search_constraint=4104&search_query='
281                                  +urllib.quote(query)+'&ic='
282                                  +str(self.max_albums)+'_0')
283
284         mainData = mainUrl.read()
285        
286         #abort if nothing was found
287         if mainData.find('0 results found for') != -1:
288             return bags
289        
290         countRe = re.findall('(\d*)(\s+)results found for', mainData)
291
292         #walmart will redirect to the specific album page
293         #if the query exactly matches the product title ... so 2 ways needed
294         if len(countRe):
295             #abort if it returns too much shit
296             if int(countRe[0][0]) > 50:
297                 return bags
298                
299             mainRe = re.findall('<div class="ItemPic"><a href=\'.*?\'><img src='
300                                 '\'(.*?)\' width="100" height="100" border="0" '
301                                 'alt=\'(.*?)\'', mainData)
302        
303             artistRe = re.findall('Artist: <span class="BodySLtgry">'
304                                   '(<a href=\'.*?\'>|)(.*?)(</a>|)</span>',
305                                   mainData)
306            
307             for num in xrange(len(mainRe)):
308                 cover = item()
309                 cover.SmallImage = item()
310                 cover.LargeImage = item()
311                 cover.SmallImage.URL = mainRe[num][0]
312                 cover.LargeImage.URL = mainRe[num][0][:-11]+"500X500.jpg"
313                 cover.Artists = item()
314                 cover.Artists.Artist = artistRe[num][1]
315                 cover.ProductName = mainRe[num][1]
316                
317                 bags.append(cover)
318            
319         else:
320             mainRe = re.findall('<img src=\'(.*?)\' width="150" height="150" '
321                                 'border="0" alt=\'(.*?)\'', mainData)
322             artistRe = re.findall('<span class="BodyXSLtgry">&gt;</span>&nbsp;'
323                                   '<a href="[^\"]*?">(.*?)</a>&nbsp;\s*', mainData)
324            
325             cover = item()
326             cover.SmallImage = item()
327             cover.LargeImage = item()
328             cover.SmallImage.URL = mainRe[0][0][:-11]+"100X100.jpg"
329             cover.LargeImage.URL = mainRe[0][0][:-11]+"500X500.jpg"
330             cover.Artists = item()
331             cover.Artists.Artist = artistRe[-1]
332             cover.ProductName = mainRe[0][1]
333            
334             bags.append(cover)
335            
336         return bags[:self.max_albums]
337        
338     def __search_buy(self, query):
339         class item: pass
340         bags = []
341
342         mainUrl = urllib.urlopen('http://www.buy.com/retail/usersearchresults.asp?qu='
343                                  +urllib.quote(query)+'&querytype=music&store=6&als=3&loc=109')
344
345         mainData = mainUrl.read()
346        
347         #abort if nothing was found
348         if mainData.find('We could not find an exact match for') != -1:
349             return bags
350        
351         mainRe = re.findall('src="([^\"]*?)" /></a></td><td width="98%" valign="top" class="list_middle">'
352                             '<table cellspacing="0" cellpadding="1" border="0" width="100%"><tr>'
353                             '<td colspan="2"><span class="productDescription"><b>[0-9]+.&nbsp;</b></span>'
354                             '<a title="([^\"]*?)" href="([^\"]*?)" class="medBlueText"><b>', mainData)
355    
356         artistRe = re.findall('<span class="body"><b>Artist:</b></span>&nbsp;<a href="[^\"]*?" class="bluetext" style="padding-right:4px;"><b>(.*?)</b></a>',
357                               mainData)
358        
359         for num in xrange(len(mainRe)):
360             cover = item()
361             cover.SmallImage = item()
362             cover.LargeImage = item()
363             cover.SmallImage.URL = mainRe[num][0]
364             cover.LargeImage.URL = mainRe[num][0].replace('prod_images','large_images')
365             cover.Artists = item()
366             cover.Artists.Artist = artistRe[num]
367             cover.ProductName = mainRe[num][1]
368            
369             bags.append(cover)
370            
371         return bags[:self.max_albums]
372
373     def __add_bag(self, model, bag):
374         # Don't show this bag if there's no large image
375         if getattr(bag, 'LargeImage', None):
376             # Text part
377             title = util.escape(getattr(bag, "Title", ""))
378             artist = (getattr(bag, "Artists", None) and
379                       getattr(bag.Artists, "Artist", None) or "")
380             if isinstance(artist, list):
381                 artist = ", ".join(artist)
382             artist = util.escape(artist)
383             if hasattr(bag, "ReleaseDate"):
384                 date = "(%s)" %util.escape(bag.ReleaseDate)
385             else:
386                 date = ""
387             markup = "<i><b>%s</b></i> %s\n%s" %(title, date, artist)
388
389             item = {"bag": bag, "thumb": None, "thumb_data": ""}
390             iter = model.append([item, markup])
391
392             # Image part
393             if getattr(bag, 'SmallImage', None):
394                 urlinfo = urllib.urlopen(bag.SmallImage.URL)
395                 sock = urlinfo.fp._sock
396                 sock.setblocking(0)
397                 data = StringIO()
398                
399                 loader = gtk.gdk.PixbufLoader()
400                 loader.connect("closed", self.__got_thumb_cb, data, item, model, iter)
401
402                 gobject.io_add_watch(
403                     sock, gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
404                     self.__copy_image, loader, data)
405
406     def __got_thumb_cb(self, loader, data, item, model, iter):
407         cover = loader.get_pixbuf()
408         if cover.get_width() > 1:
409             w = h = 48
410             cover = cover.scale_simple(w, h, gtk.gdk.INTERP_NEAREST)
411             thumb = gtk.gdk.Pixbuf(
412                 gtk.gdk.COLORSPACE_RGB, True, 8, w + 2, h + 2)
413             thumb.fill(0x000000ff)
414             cover.copy_area(0, 0, w, h, thumb, 1, 1)
415             item["thumb"] = thumb
416             item["thumb_data"] = data.getvalue()
417             model.row_changed(model.get_path(iter), iter)
418
419     def __preview(self, selection):
420         model, path = selection.get_selected()
421         item = model[path][0]
422         self.image.hide()
423         self.button.set_sensitive(False)
424
425         if item["thumb"]: # If there exists no thumbnail, then nothing bigger.
426             if "cover" not in item:
427                 self.__get_cover(item, item["bag"].LargeImage.URL)
428             else:
429                 self.image.set_from_pixbuf(item["cover"])
430                 self.image.show()
431                 self.button.set_sensitive(True)
432
433     def __get_cover(self, item, url):
434         data = StringIO()
435         urlinfo = urllib.urlopen(url)
436         sock = urlinfo.fp._sock
437         sock.setblocking(0)
438         loader = gtk.gdk.PixbufLoader()
439         gobject.io_add_watch(
440             sock, gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
441             self.__copy_image, loader, data)
442         loader.connect("closed", self.__got_cover_cb, data, item, url)
443         def update(loader, x, y, w, h, image):
444             if (w, h) > (1, 1):
445                 image.set_from_pixbuf(loader.get_pixbuf())
446                 image.show()
447         loader.connect("area-updated", update, self.image)
448
449     def __got_cover_cb(self, loader, data, item, url):
450         cover = loader.get_pixbuf()
451         # For some reason we get a 1x1 image if the given size didn't exist
452         if cover.get_width() > 1:
453             item["cover"] = cover
454             item["cover_data"] = data.getvalue()
455             self.image.set_from_pixbuf(item["cover"])
456             self.button.set_sensitive(True)
457         elif (url == item["bag"].LargeImage.URL) and getattr(item["bag"], 'MediumImage', None):
458             self.__get_cover(item, item["bag"].MediumImage.URL)
459         elif (url == item["bag"].LargeImage.URL) and getattr(item["bag"], 'SmallImage', None):
460             self.__get_cover(item, item["bag"].SmallImage.URL)
461         else:
462             item["cover"] = item["thumb"]
463             item["cover_data"] = item["thumb_data"]
464             self.image.set_from_pixbuf(item["cover"])
465             self.button.set_sensitive(True)
466
467     def __copy_image(self, src, condition, loader, data):
468         if condition in (gobject.IO_ERR, gobject.IO_HUP):
469             loader.close()
470             src.close()
471             return False
472         else: # Read
473             buf = src.recv(1024)
474             if buf:
475                 loader.write(buf)
476                 data.write(buf)
477                 return True # Run again
478             else:
479                 loader.close()
480                 src.close()
481                 return False
482
483     def __save_cover(self, data, fname):
484         if os.path.exists(fname) and not qltk.ConfirmAction(None,
485             "File exists", "The file <b>%s</b> already exists."
486             "\n\nOverwrite?" %util.escape(fname)).run():
487             return
488
489         f = open(fname, "w")
490         f.write(data)
491         f.close()
492         self.destroy()
493
494     def __get_fname(self, songs, combo):
495         append = combo.get_model()[(combo.get_active(),)][0]
496         dirname = songs[0]("~dirname")
497         fname = os.path.join(dirname, append)
498         #print "Will save to", fname
499         config.set("plugins", "cover_fn", append)
500         return fname
501
502 class DownloadAlbumArt(SongsMenuPlugin):
503     PLUGIN_ID = "Download Album art"
504     PLUGIN_NAME = _("Download Album Art")
505     PLUGIN_DESC = "Downloads album covers from Amazon.com, " \
506                   "Walmart.com, Darktown.to and Buy.com"
507     PLUGIN_ICON = gtk.STOCK_FIND
508     PLUGIN_VERSION = "0.4"
509
510     def PluginPreferences(parent):
511         vbox = gtk.VBox(spacing=5)
512         vbox.set_border_width(5)
513         bAM = gtk.Button("Visit Amazon.com")
514         bAM.connect('clicked', lambda s:util.website('http://www.amazon.com/'))
515         vbox.pack_start(bAM)
516         bWM = gtk.Button("Visit Walmart.com")
517         bWM.connect('clicked', lambda s:util.website('http://www.walmart.com/'))
518         vbox.pack_start(bWM)
519         bDT = gtk.Button("Visit Darktown.to")
520         bDT.connect('clicked', lambda s:util.website('http://www.darktown.to/'))
521         vbox.pack_start(bDT)
522         bBU = gtk.Button("Visit Buy.com")
523         bBU.connect('clicked', lambda s:util.website('http://www.buy.com/'))
524         vbox.pack_start(bBU)
525         return vbox
526        
527     PluginPreferences = staticmethod(PluginPreferences)
528
529     plugin_album = AlbumArtWindow