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