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