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