Plugins: albumart2.1.py

File albumart2.1.py, 18.6 kB (added by christoph.reiter@gmx.at, 10 months ago)

now supports amazon, walmart, darktown and itunes + some cleanups

Line 
1 #Copyright 2005 Eduardo Gonzalez, Niklas Janlert, Christoph Reiter
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 import os
9 import sys
10 import urllib
11 import urllib2
12 import re
13 import time
14 import threading
15 from cStringIO import StringIO
16 import gtk
17 import gobject
18 import pango
19 import util
20 import qltk
21 import config
22
23 from plugins.songsmenu import SongsMenuPlugin
24
25 if sys.version_info < (2, 4): from sets import Set as set
26
27 try:
28     import amazon
29 except ImportError:
30     import _amazon as amazon
31
32 class AlbumArtWindow(gtk.Window):
33     def __init__(self, songs):
34         gtk.Window.__init__(self)
35         self.set_border_width(12)
36         self.set_title("AlbumArt")
37         self.set_default_size(720, 450)
38
39         #TreeView stuff
40         self.liststore = liststore = gtk.ListStore(object, str)
41         treeview = gtk.TreeView(liststore)
42         treeview.set_headers_visible(False)
43         selection = treeview.get_selection()
44         selection.set_mode(gtk.SELECTION_SINGLE)
45         selection.connect("changed", self.__preview)
46
47         rend = gtk.CellRendererPixbuf()
48         def cell_data(column, cell, model, iter):
49             cell.set_property("pixbuf", model[iter][0]["thumb"])
50         tvcol1 = gtk.TreeViewColumn("Pixbuf", rend)
51         tvcol1.set_cell_data_func(rend, cell_data)
52         tvcol1.set_sizing(gtk.TREE_VIEW_COLUMN_GROW_ONLY)
53         rend.set_property('xpad', 2)
54         rend.set_property('ypad', 2)
55         rend.set_property('width', 56)
56         rend.set_property('height', 56)
57         treeview.append_column(tvcol1)
58
59         rend = gtk.CellRendererText()
60         rend.set_property("ellipsize", pango.ELLIPSIZE_END)
61         tvcol2 = gtk.TreeViewColumn("Info", rend, markup=1)
62         treeview.append_column(tvcol2)
63
64         sw = gtk.ScrolledWindow()
65         sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
66         sw.set_shadow_type(gtk.SHADOW_IN)
67         sw.add(treeview)
68
69         #Image frame and save button
70         self.image = image = gtk.Image()
71         frame = gtk.Frame()
72         frame.set_shadow_type(gtk.SHADOW_IN)
73         frame.add(image)
74         scrolled = gtk.ScrolledWindow()
75         scrolled.add_with_viewport(frame)
76         scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
77         vbox = gtk.VBox(spacing=5)
78         vbox.pack_start(scrolled)
79         self.button = button = gtk.Button(stock=gtk.STOCK_SAVE)
80         button.set_sensitive(False)
81         def save_cb(button, combo):
82             model, path = selection.get_selected()
83             data = model[path][0]["cover_data"]
84             fname = self.__get_fname(songs, combo)
85             self.__save_cover(data, fname)
86         combo = gtk.combo_box_new_text()
87         try: set_fn = config.get("plugins", "cover_fn")
88         except: set_fn = ".folder.jpg"
89         active = -1
90         for i, fn in enumerate([".folder.jpg", "folder.jpg", "cover.jpg"]):
91             combo.append_text(fn)
92             if fn == set_fn: active = i
93         if active == -1:
94             combo.append_text(set_fn)
95             combo.set_active(len(combo.get_model()) - 1)
96         else: combo.set_active(active)
97         button.connect("clicked", save_cb, combo)
98         bbox = gtk.HButtonBox()
99         bbox.pack_start(combo)
100         bbox.pack_start(button, expand=False, fill=False)
101         bbox.set_layout(gtk.BUTTONBOX_SPREAD)
102         vbox.pack_start(bbox, expand=False, fill=False)
103
104         hpaned = gtk.HPaned()
105         hpaned.pack1(sw)
106         hpaned.pack2(vbox)
107         hpaned.set_position(300)
108         self.add(hpaned)
109
110         #define search engnines
111         self.engines = [self.__search_amazon, self.__search_walmart,
112                         self.__search_darktown, self.__search_itunes]
113
114         #max covers per site
115         self.max_albums = 4
116        
117         #for progress calculation
118         self.progress = 0
119        
120         #use albumartist if available
121         if songs[0]("albumartist"):
122             artist = songs[0]("albumartist")
123         else:
124             artist = songs[0]("artist")
125         query = artist + " " + songs[0]("album")
126         query = query.encode("latin1", "replace")
127        
128         #show search status row
129         iter = self.liststore.insert(0)
130         self.liststore.set(iter, 0, {"bag":None,"thumb":None,"thumb_data":None})
131         self.liststore.set_value(iter, 1,
132                                  "<i><b>Searching progress: 0%</b></i>")
133
134         #start all search engines in seperate threads
135         for i in xrange(len(self.engines)):
136             thread = threading.Thread(target=self.__search,
137                                       args=(query,i,iter,))
138             thread.setDaemon(True)
139             thread.start()
140
141         self.show_all()
142
143     def __search(self, query, num, iter):
144
145         try:
146             bags = self.engines[num](query)
147             for bag in bags:
148                 gobject.idle_add(self.__add_bag, self.liststore, bag)
149         except:
150             pass
151        
152         self.progress += 1
153         self.liststore.set_value(iter, 1, "<i><b>Searching progress: "
154                                  +str(self.progress*100/len(self.engines))
155                                  +"%</b></i>")
156
157         if self.progress == len(self.engines):
158             time.sleep(2)
159             if self.liststore.iter_next(iter) == None:
160                 self.liststore.set_value(iter, 1,
161                                          "<i><b>No albumart found</b></i>")
162             else:
163                 self.liststore.remove(iter)
164            
165                
166
167     def __search_amazon(self, query):
168         amazon.setLicense("0RKH4ZH1JCFZHMND91G2")
169         try:
170             bags = amazon.searchByKeyword(
171                 query, type="lite", product_line="music")
172         except:
173             pass
174         else:
175             #delete products with no images and return the rest
176             count=0
177             while count < self.max_albums and count < len(bags):
178                 if len(bags[count].ImageUrlLarge) and \
179                    len(bags[count].ImageUrlSmall):
180                     count+=1
181                 else:
182                     del bags[count]
183             return bags[:self.max_albums]
184         return []
185            
186     def __search_darktown(self, query):
187         class item: pass
188         bags = []
189        
190         #Artists @ Darkdown often miss the leading "The", better remove it
191         #for better search results - also ' and . should be removed
192         if query[:4] == "The ":
193             query = query[4:]
194         query = query.replace("'"," ")
195         query = query.replace(".","")
196        
197         mainUrl = urllib.urlopen('http://www.darktown.to/search.php?'
198                 'action=search&what='+urllib.quote(query)+'&category=audio')
199         mainData = mainUrl.read()
200         mainRe = re.findall('javascript:openCentered\(\'(.*?)Front\'', mainData)
201        
202         count = 0
203         for result in mainRe:
204             if count >= self.max_albums: break
205             count += 1
206            
207             resultUrl = urllib.urlopen('http://www.darktown.to'+result+'Front')
208             resultData = resultUrl.read()
209
210             resultImgBig = re.findall('href="(.*?)">DOWNLOAD</a>', resultData)
211             resultImgSmall = re.findall('src="(.*?)"', resultData)
212             resultArtist = re.findall('<b><font size=4>(.*?)</font', resultData)
213             resultTitle = re.findall('</font></b><br><b>(.*?)</b><br><br><b>',
214                                      resultData)
215
216             cover = item()
217             cover.ImageUrlSmall = resultImgSmall[0]
218             cover.ImageUrlLarge = resultImgBig[0]
219             cover.Artists = item()
220             cover.Artists.Artist = resultArtist[0].decode("latin1", "replace")
221             cover.ProductName = resultTitle[0].decode("latin1", "replace")
222            
223             bags.append(cover)
224  
225         return bags
226    
227     def __search_itunes(self, query):
228         class item: pass
229         bags = []
230        
231         #search for music product in itunes store
232         mainUrl = urllib.urlopen('http://ax.phobos.apple.com.edgesuite.net/'
233                              'WebObjects/MZStoreServices.woa/wa/itmsSearch?'
234                              'media=music&term='+query.replace(' ','+'))
235         mainData = mainUrl.read()
236         p = re.compile('<TD class="searchResults" WIDTH="232" HEIGHT="20">'
237                        '(.*?)</FONT></TD>', re.DOTALL)
238         filterRe = re.findall(p, mainData)
239         if not len(filterRe):
240             return bags
241         linkRe = re.findall('<a class="searchResults" href="(.*?)">',
242                             filterRe[0])
243
244         #follow the link and extract the shop url and the product title
245         subUrl = urllib.urlopen('http://ax.phobos.apple.com.edgesuite.net'
246                              +linkRe[0].replace('&amp;','&'))
247         subData = subUrl.read()
248         subRe = re.findall('<a href="(.*?)">',subData)
249
250         name = re.findall('<img height="15" width="61" alt="(.*?)"',subData)
251         name = name[0].split(' - ')
252
253         #now use the joshers or tpembles php-script to get the actual image
254         #FIXME: code from http://tpemble.urfbownd.net/itunes/
255         #should be ported to python and included here...
256         
257         try:
258             #joshers:
259             params = urllib.urlencode({'url': subRe[0]})
260             url = urllib.urlopen('http://www.thejosher.net/iTunes/methodtwo.php'
261                                  , params)
262             data = url.read()
263             links = re.findall('<a href="http://a1.phobos.(.*?).jpg">'
264                                ,data)
265         except:   
266             #tpembles: (slower server, but just in case the other one goes off)
267             params = urllib.urlencode({'url': subRe[0]})
268             url = urllib.urlopen('http://tpemble.urfbownd.net/itunes/'
269                                  'itunes-art.php', params)
270             data = url.read()
271             links = re.findall('<a href="http://a1.phobos.(.*?).170x170-75'
272                                '.jpg">', data)
273
274         image_large = 'http://a1.phobos.'+links[0]+'.jpg'
275         image_small = 'http://a1.phobos.'+links[0]+'.60x60-50.jpg'
276
277         #sometimes the image is a tif and no jpg.. 404 will be raised
278         try:
279             urllib2.urlopen(image_large)
280             urllib2.urlopen(image_small)
281         except:
282             return bags
283
284         #add the albumart to the list
285         cover = item()
286         cover.ImageUrlSmall = image_small
287         cover.ImageUrlLarge = image_large
288         cover.Artists = item()
289         cover.Artists.Artist = name[0]
290         cover.ProductName = name[1]
291
292         bags.append(cover)
293
294         return bags
295        
296     def __search_walmart(self, query):
297         class item: pass
298         bags = []
299
300         mainUrl = urllib.urlopen('http://www.walmart.com/search/search-ng.do?'
301                                  'search_constraint=4104&search_query='
302                                  +urllib.quote(query)+'&ic='
303                                  +str(self.max_albums)+'_0')
304
305         mainData = mainUrl.read()
306        
307         #abort if nothing was found
308         if mainData.find('no matches found') != -1 or \
309             mainData.find('not find any matches') != -1:
310             return bags
311        
312         countRe = re.findall('<h1>(\d*) items found for', mainData)
313
314         #walmart will redirect to the specific album page
315         #if the query exactly matches the product title ... so 2 ways needed
316         if len(countRe):
317        
318             #abort if it returns too much shit
319             if int(countRe[0]) > 50:
320                 return bags
321                
322             mainRe = re.findall('<div class="ItemPic"><a href=\'.*?\'><img src='
323                                 '\'(.*?)\' width="60" height="60" border="0" '
324                                 'alt=\'(.*?)\'', mainData)
325        
326             artistRe = re.findall('Artist: <span class="BodySLtgry">'
327                                   '(<a href=\'.*?\'>|)(.*?)(</a>|)</span>',
328                                   mainData)
329            
330             for num in xrange(len(mainRe)): 
331                 cover = item()
332                 cover.ImageUrlSmall = mainRe[num][0]
333                 cover.ImageUrlLarge = mainRe[num][0][:-9]+"500X500.jpg"
334                 cover.Artists = item()
335                 cover.Artists.Artist = artistRe[num][1]
336                 cover.ProductName = mainRe[num][1]
337                
338                 bags.append(cover)
339            
340         else:
341             mainRe = re.findall('<img src=\'(.*?)\' width="150" height="150" '
342                                 'border="0" alt=\'(.*?)\'', mainData)
343             artistRe = re.findall('<span class="BodyXSLtgry">&gt;</span>&nbsp;'
344                                   '<a href=".*?">(.*?)</a>&nbsp;\s*', mainData)
345          
346             cover = item()
347             cover.ImageUrlSmall = mainRe[0][0][:-11]+"60X60.gif"
348             cover.ImageUrlLarge = mainRe[0][0][:-11]+"500X500.jpg"
349             cover.Artists = item()
350             cover.Artists.Artist = artistRe[-1]
351             cover.ProductName = mainRe[0][1]
352            
353             bags.append(cover)
354            
355         return bags
356
357     def __add_bag(self, model, bag):
358         # Text part
359         title = util.escape(getattr(bag, "ProductName", ""))
360         artist = (getattr(bag, "Artists", None) and
361                   getattr(bag.Artists, "Artist", None) or "")
362         if isinstance(artist, list):
363             artist = ", ".join(artist)
364         artist = util.escape(artist)
365         if hasattr(bag, "ReleaseDate"):
366             date = "(%s)" %util.escape(bag.ReleaseDate)
367         else:
368             date = ""
369         markup = "<i><b>%s</b></i> %s\n%s" %(title, date, artist)
370
371         item = {"bag": bag, "thumb": None, "thumb_data": ""}
372         iter = model.append([item, markup])
373
374         # Image part
375         urlinfo = urllib.urlopen(bag.ImageUrlSmall)
376         sock = urlinfo.fp._sock
377         sock.setblocking(0)
378         data = StringIO()
379
380         loader = gtk.gdk.PixbufLoader()
381         loader.connect("closed", self.__got_thumb_cb, data, item, model, iter)
382
383         gobject.io_add_watch(
384             sock, gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
385             self.__copy_image, loader, data)
386
387     def __got_thumb_cb(self, loader, data, item, model, iter):
388         cover = loader.get_pixbuf()
389         if cover.get_width() > 1:
390             w = h = 48
391             cover = cover.scale_simple(w, h, gtk.gdk.INTERP_NEAREST)
392             thumb = gtk.gdk.Pixbuf(
393                 gtk.gdk.COLORSPACE_RGB, True, 8, w + 2, h + 2)
394             thumb.fill(0x000000ff)
395             cover.copy_area(0, 0, w, h, thumb, 1, 1)
396             item["thumb"] = thumb
397             item["thumb_data"] = data.getvalue()
398             model.row_changed(model.get_path(iter), iter)
399        
400     def __preview(self, selection):
401         model, path = selection.get_selected()
402         try:
403             item = model[path][0]
404         except:
405             return
406         self.image.hide()
407         self.button.set_sensitive(False)
408        
409         if item["thumb"]: # If there exists no thumbnail, then nothing bigger.
410             if "cover" not in item:
411                 self.__get_cover(item, item["bag"].ImageUrlLarge)
412             else:
413                 self.image.set_from_pixbuf(item["cover"])
414                 self.image.show()
415                 self.button.set_sensitive(True)
416
417     def __get_cover(self, item, url):
418         data = StringIO()
419         urlinfo = urllib.urlopen(url)
420         sock = urlinfo.fp._sock
421         sock.setblocking(0)
422         loader = gtk.gdk.PixbufLoader()
423         gobject.io_add_watch(
424             sock, gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
425             self.__copy_image, loader, data)
426         loader.connect("closed", self.__got_cover_cb, data, item, url)
427         def update(loader, x, y, w, h, image):
428             if (w, h) > (1, 1):
429                 image.set_from_pixbuf(loader.get_pixbuf())
430                 image.show()
431         loader.connect("area-updated", update, self.image)
432
433     def __got_cover_cb(self, loader, data, item, url):
434         cover = loader.get_pixbuf()
435         # For some reason we get a 1x1 image if the given size didn't exist
436         if cover.get_width() > 1:
437             item["cover"] = cover
438             item["cover_data"] = data.getvalue()
439             self.image.set_from_pixbuf(item["cover"])
440             self.button.set_sensitive(True)
441         elif url == item["bag"].ImageUrlLarge:
442             self.__get_cover(item, item["bag"].ImageUrlMedium)
443         else:
444             item["cover"] = item["thumb"]
445             item["cover_data"] = item["thumb_data"]
446             self.image.set_from_pixbuf(item["cover"])
447             self.button.set_sensitive(True)
448
449     def __copy_image(self, src, condition, loader, data):
450         if condition in (gobject.IO_ERR, gobject.IO_HUP):
451             loader.close()
452             src.close()
453             return False
454         else: # Read
455             buf = src.recv(1024)
456             if buf:
457                 loader.write(buf)
458                 data.write(buf)
459                 return True # Run again
460             else:
461                 loader.close()
462                 src.close()
463                 return False
464
465     def __save_cover(self, data, fname):
466         if os.path.exists(fname) and not qltk.ConfirmAction(None,
467             "File exists", "The file <b>%s</b> already exists."
468             "\n\nOverwrite?" %util.escape(fname)).run():
469             return
470
471         f = open(fname, "w")
472         f.write(data)
473         f.close()
474         self.destroy()
475
476     def __get_fname(self, songs, combo):
477         append = combo.get_model()[(combo.get_active(),)][0]
478         dirname = songs[0]("~dirname")
479         fname = os.path.join(dirname, append)
480         #print "Will save to", fname
481         config.set("plugins", "cover_fn", append)
482         return fname
483
484 class DownloadAlbumArt(SongsMenuPlugin):
485     PLUGIN_ID = "Download Album art"
486     PLUGIN_NAME = _("Download Album Art")
487     PLUGIN_DESC = "Downloads album covers from Amazon.com, " \
488                   "Walmart.com, iTunes.com and Darktown.to"
489     PLUGIN_ICON = gtk.STOCK_FIND
490     PLUGIN_VERSION = "0.25"
491
492     def PluginPreferences(parent):
493         vbox = gtk.VBox(spacing=5)
494         vbox.set_border_width(5)
495         bAM = gtk.Button("Visit Amazon.com")
496         bAM.connect('clicked', lambda s:util.website('http://www.amazon.com/'))
497         vbox.pack_start(bAM)
498         bWM = gtk.Button("Visit Walmart.com")
499         bWM.connect('clicked', lambda s:util.website('http://www.walmart.com/'))
500         vbox.pack_start(bWM)
501         bIT = gtk.Button("Visit iTunes.com")
502         bIT.connect('clicked', lambda s:util.website('http://www.itunes.com/'))
503         vbox.pack_start(bIT)
504         bDT = gtk.Button("Visit Darktown.to")
505         bDT.connect('clicked', lambda s:util.website('http://www.darktown.to/'))
506         vbox.pack_start(bDT)
507         return vbox
508        
509     PluginPreferences = staticmethod(PluginPreferences)
510
511     plugin_album = AlbumArtWindow