Handle no_results page in image online search
[mussorgsky] / src / album_art.py
1 #!/usr/bin/env python2.5
2 import urllib2, urllib
3 import os
4 from album_art_spec import getCoverArtFileName, getCoverArtThumbFileName, get_thumb_filename_for_path
5 import dbus, time
6 import string
7
8 try:
9     import libxml2
10     libxml_available = True
11 except ImportError:
12     libxml_available = False
13
14 try:
15     import PIL
16     import Image
17     pil_available = True
18 except ImportError:
19     pil_available = False
20
21
22 # Set socket timeout
23 import socket
24 import urllib2
25
26 timeout = 5
27 socket.setdefaulttimeout(timeout)
28
29 LASTFM_APIKEY = "1e1d53528c86406757a6887addef0ace"
30 BASE_LASTFM = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo"
31
32
33 BASE_MSN = "http://www.bing.com/images/search?q="
34 MSN_MEDIUM = "+filterui:imagesize-medium"
35 MSN_SMALL = "+filterui:imagesize-medium"
36 MSN_SQUARE = "+filterui:aspect-square"
37 MSN_PHOTO = "+filterui:photo-graphics"
38
39 CACHE_LOCATION = os.path.join (os.getenv ("HOME"), ".cache", "mussorgsky")
40 # LastFM:
41 # http://www.lastfm.es/api/show?service=290
42 #
43 class MussorgskyAlbumArt:
44
45     def __init__ (self):
46         bus = dbus.SessionBus ()
47         handle = time.time()
48
49         if (not os.path.exists (CACHE_LOCATION)):
50             os.makedirs (CACHE_LOCATION)
51             
52         if (pil_available):
53             self.thumbnailer = LocalThumbnailer ()
54         else:
55             try:
56                 self.thumbnailer = bus.get_object ('org.freedesktop.thumbnailer',
57                                                    '/org/freedesktop/thumbnailer/Generic')
58             except dbus.exceptions.DBusException:
59                 print "No thumbnailer available"
60                 self.thumbnailer = None
61
62     def get_album_art (self, artist, album, force=False):
63         """
64         Return a tuple (album_art, thumbnail_album_art)
65         """
66         filename = getCoverArtFileName (album)
67         thumbnail = getCoverArtThumbFileName (album)
68
69         album_art_available = False
70         if (os.path.exists (filename) and not force):
71             print "Album art already there " + filename
72             album_art_available = True
73         else:
74             results_page = self.__msn_images (artist, album)
75             for online_resource in self.__get_url_from_msn_results_page (results_page):
76                 print "Choosed:", online_resource
77                 content = self.__get_url (online_resource)
78                 if (content):
79                     print "Albumart: %s " % (filename)
80                     self.__save_content_into_file (content, filename)
81                     album_art_available = True
82                     break
83
84         if (not album_art_available):
85             return (None, None)
86
87         if (os.path.exists (thumbnail) and not force):
88             print "Thumbnail exists " + thumbnail
89         else:
90             if (not self.__request_thumbnail (filename)):
91                 print "Failed doing thumbnail. Probably album art is not an image!"
92                 os.remove (filename)
93                 return (None, None)
94             
95         return (filename, thumbnail)
96
97
98     def get_alternatives (self, artist, album, no_alternatives):
99         """
100         return a list of paths of possible album arts
101         """
102         results_page = self.__msn_images (artist, album)
103         valid_images = []
104         for image_url in self.__get_url_from_msn_results_page (results_page):
105             if (not image_url):
106                 # Some searches doesn't return anything at all!
107                 break
108             
109             image = self.__get_url (image_url)
110             if (image):
111                 image_path = os.path.join (CACHE_LOCATION, "alternative-" + str(len(valid_images)))
112                 self.__save_content_into_file (image, image_path)
113                 valid_images.append (image_path)
114                 if (len (valid_images) > no_alternatives):
115                     return valid_images
116         return valid_images
117
118     def save_alternative (self, artist, album, path):
119         if not os.path.exists (path):
120             print "**** CRITICAL **** image in path", path, "doesn't exist!"
121             return (None, None)
122         
123         filename = getCoverArtFileName (album)
124         thumbnail = getCoverArtThumbFileName (album)
125
126         os.rename (path, filename)
127         if (not self.__request_thumbnail (filename)):
128             print "Something wrong creating the thumbnail!"
129         return (filename, thumbnail)
130
131     def __last_fm (self, artist, album):
132
133         if (not libxml_available):
134             return None
135         
136         if (not album or len (album) < 1):
137             return None
138         
139         URL = BASE_LASTFM + "&api_key=" + LASTFM_APIKEY
140         if (artist and len(artist) > 1):
141             URL += "&artist=" + urllib.quote(artist)
142         if (album):
143             URL += "&album=" + urllib.quote(album)
144             
145         print "Retrieving: %s" % (URL)
146         result = self.__get_url (URL)
147         if (not result):
148             return None
149         doc = libxml2.parseDoc (result)
150         image_nodes = doc.xpathEval ("//image[@size='large']")
151         if len (image_nodes) < 1:
152             return None
153         else:
154             return image_nodes[0].content
155
156     def __msn_images (self, artist, album):
157
158         good_artist = self.__clean_string_for_search (artist)
159         good_album = self.__clean_string_for_search (album)
160
161         if (good_album and good_artist):
162             full_try = BASE_MSN + good_album + "+" + good_artist + MSN_MEDIUM + MSN_SQUARE
163             print "Searching (album + artist): %s" % (full_try)
164             result = self.__get_url (full_try)
165             if (result.find ("no_results") == -1):
166                 return result
167
168         if (album):
169             if (album.lower ().find ("greatest hit") != -1):
170                 print "Ignoring '%s': too generic" % (album)
171                 pass
172             else:
173                 album_try = BASE_MSN + good_album + MSN_MEDIUM + MSN_SQUARE
174                 print "Searching (album): %s" % (album_try)
175                 result = self.__get_url (album_try)
176                 if (result.find ("no_results") == -1):
177                     return result
178             
179         if (artist):
180             artist_try = BASE_MSN + good_artist + "+CD+music"  + MSN_SMALL + MSN_SQUARE + MSN_PHOTO
181             print "Searching (artist CD): %s" % (artist_try)
182             result = self.__get_url (artist_try)
183             if (result.find ("no_results") == -1):
184                 return result
185         
186         return None
187
188
189     def __get_url_from_msn_results_page (self, page):
190
191         if (not page):
192             return
193
194         current_option = None
195         starting_at = 0
196
197         # 500 is just a safe limit
198         for i in range (0, 500):
199             # Iterate until find a jpeg
200             start = page.find ("furl=", starting_at)
201             if (start == -1):
202                 yield None
203             end = page.find ("\"", start + len ("furl="))
204             current_option = page [start + len ("furl="): end].replace ("amp;", "")
205             if (current_option.lower().endswith (".jpg") or
206                 current_option.lower().endswith (".jpeg")):
207                 yield current_option
208             starting_at = end
209         yield None
210         
211
212     def __clean_string_for_search (self, text):
213         if (not text or len (text) < 1):
214             return None
215             
216         bad_stuff = "_:?\\-~"
217         clean = text
218         for c in bad_stuff:
219             clean = clean.replace (c, " ")
220
221         clean.replace ("/", "%2F")
222         clean = clean.replace (" CD1", "").replace(" CD2", "")
223         return urllib.quote(clean)
224
225     def __save_content_into_file (self, content, filename):
226         output = open (filename, 'w')
227         output.write (content)
228         output.close ()
229         
230     def __get_url (self, url):
231         request = urllib2.Request (url)
232         request.add_header ('User-Agent', 'Mussorgsky/0.1 Test')
233         opener = urllib2.build_opener ()
234         try:
235             return opener.open (request).read ()
236         except:
237             return None
238
239     def __request_thumbnail (self, filename):
240         if (not self.thumbnailer):
241             print "No thumbnailer available"
242             return
243         uri = "file://" + filename
244         handle = time.time ()
245         return self.thumbnailer.Queue ([uri], ["image/jpeg"], dbus.UInt32 (handle))
246             
247
248
249 class LocalThumbnailer:
250     def __init__ (self):
251         self.THUMBNAIL_SIZE = (124,124)
252
253     def Queue (self, uris, mimes, handle):
254         for i in range (0, len(uris)):
255             uri = uris[i]
256             fullCoverFileName = uri[7:]
257             if (os.path.exists (fullCoverFileName)):
258                 thumbFile = get_thumb_filename_for_path (fullCoverFileName)
259                 try:
260                     image = Image.open (fullCoverFileName)
261                     image = image.resize (self.THUMBNAIL_SIZE, Image.ANTIALIAS )
262                     image.save( thumbFile, "JPEG" )
263                     print "Thumbnail: " + thumbFile
264                 except IOError, e:
265                     print e
266                     return False
267         return True
268             
269
270
271 if __name__ == "__main__":
272     import sys
273     from optparse import OptionParser
274
275     parser = OptionParser()
276     parser.add_option ("-p", "--print", dest="print_paths",
277                        action="store_true", default=True,
278                        help="Print the destination paths")
279     parser.add_option ("-r", "--retrieve", dest="retrieve",
280                        action="store_true", default=False,
281                        help="Try to retrieve the online content")
282     parser.add_option ("-m", "--multiple", dest="multiple",
283                        action="store_true", default=False,
284                        help="Show more than one option")
285     parser.add_option ("-a", "--artist", dest="artist", type="string",
286                        help="ARTIST to look for", metavar="ARTIST")
287     parser.add_option ("-b", "--album", dest="album", type="string",
288                        help="ALBUM to look for", metavar="ALBUM")
289
290     (options, args) = parser.parse_args ()
291     print options
292     if (not options.artist and not options.album):
293         parser.print_help ()
294         sys.exit (-1)
295
296     if (options.multiple and options.retrieve):
297         print "Multiple and retrieve are incompatible"
298         parser.print_help ()
299         sys.exit (-1)
300         
301     if options.print_paths and not options.retrieve:
302         print "Album art:", getCoverArtFileName (options.album)
303         print "Thumbnail:", getCoverArtThumbFileName (options.album)
304
305     if options.retrieve:
306         maa = MussorgskyAlbumArt ()
307         maa.get_album_art (options.artist, options.album)
308
309     if options.multiple:
310         maa = MussorgskyAlbumArt ()
311         alt = maa.get_alternatives (options.artist, options.album, 5)
312         for a in alt:
313             print a