fix a memory leak
[rhythmbox.git] / plugins / magnatune / magnatune / MagnatuneSource.py
blob7364afc40555940c6c9f37ca419bf5f98a705bf6
1 import rb, rhythmdb
2 from TrackListHandler import TrackListHandler
3 from BuyAlbumHandler import BuyAlbumHandler, MagnatunePurchaseError
5 import gobject
6 import gtk.glade
7 import gnomevfs, gnome, gconf
8 import xml
9 import urllib, zipfile
11 has_gnome_keyring = False
12 try:
13 import gnomekeyring
14 has_gnome_keyring = True
15 except:
16 pass
19 magnatune_partner_id = "zimmerman"
21 # URIs
22 magnatune_dir = gnome.user_dir_get() + "rhythmbox/magnatune/"
23 magnatune_song_info_uri = gnomevfs.URI("http://magnatune.com/info/song_info_xml.zip")
24 local_song_info_uri = gnomevfs.URI(magnatune_dir + "song_info.xml")
25 local_song_info_temp_uri = gnomevfs.URI(magnatune_dir + "song_info.xml.zip.tmp")
28 class MagnatuneSource(rb.BrowserSource):
29 __gproperties__ = {
30 'plugin': (rb.Plugin, 'plugin', 'plugin', gobject.PARAM_WRITABLE|gobject.PARAM_CONSTRUCT_ONLY),
33 __client = gconf.client_get_default()
36 def __init__(self):
38 rb.BrowserSource.__init__(self, name=_("Magnatune"))
40 # track data
41 self.__sku_dict = {}
42 self.__home_dict = {}
44 # catalogue stuff
45 self.__activated = False
46 self.__notify_id = 0
47 self.__update_id = 0
48 self.__xfer_handle = None
49 self.__info_screen = None
50 self.__updating = True
51 self.__has_loaded = False
52 self.__load_handle = None
53 self.__load_current_size = 0
54 self.__load_total_size = 1
56 self.__downloads = {} # keeps track of amount downloaded for each file
57 self.__downloading = False # keeps track of whether we are currently downloading an album
58 self.__download_progress = 0.0 # progress of current download(s)
59 self.purchase_filesize = 0 # total amount of bytes to download
61 def do_set_property(self, property, value):
62 if property.name == 'plugin':
63 self.__plugin = value
65 # we have to wait until we get the plugin to do this
66 circle_file_name = self.__plugin.find_file("magnatune_circle_small.png")
67 width, height = gtk.icon_size_lookup(gtk.ICON_SIZE_LARGE_TOOLBAR)
68 icon = gtk.gdk.pixbuf_new_from_file_at_size(circle_file_name, width, height)
69 self.set_property("icon", icon)
71 else:
72 raise AttributeError, 'unknown property %s' % property.name
76 # RBSource methods
79 def do_impl_show_entry_popup(self):
80 self.show_source_popup ("/MagnatuneSourceViewPopup")
82 def do_impl_get_status(self):
83 if self.__updating:
84 if self.__load_total_size > 0:
85 progress = min (float(self.__load_current_size) / self.__load_total_size, 1.0)
86 else:
87 progress = 0.0
88 return (_("Loading Magnatune catalogue"), None, progress)
89 elif self.__downloading:
90 progress = min (self.__download_progress, 1.0)
91 return (_("Downloading Magnatune Album(s)"), None, progress)
92 else:
93 qm = self.get_property("query-model")
94 return (qm.compute_status_normal("song", "songs"), None, 0.0)
96 def do_impl_get_ui_actions(self):
97 return ["MagnatunePurchaseAlbum",
98 "MagnatuneArtistInfo",
99 "MagnatuneCancelDownload"]
101 def do_impl_activate(self):
102 if not self.__activated:
103 shell = self.get_property('shell')
104 self.__db = shell.get_property('db')
105 self.__entry_type = self.get_property('entry-type')
107 self.__activated = True
108 self.__show_loading_screen (True)
109 self.__load_catalogue()
111 # start our catalogue updates
112 self.__update_id = gobject.timeout_add(6 * 60 * 60 * 1000, self.__update_catalogue)
113 self.__update_catalogue()
115 self.get_entry_view().set_sorting_type(self.__client.get_string("/apps/rhythmbox/plugins/magnatune/sorting"))
117 rb.BrowserSource.do_impl_activate (self)
119 # def do_impl_get_browser_key (self):
120 # return "/apps/rhythmbox/plugins/magnatune/show_browser"
122 # def do_impl_get_paned_key (self):
123 # return "/apps/rhythmbox/plugins/magnatune/paned_position"
125 def do_impl_delete_thyself(self):
126 if self.__update_id != 0:
127 gobject.source_remove (self.__update_id)
128 self.__update_id = 0
130 if self.__notify_id != 0:
131 gobject.source_remove (self.__notify_id)
132 self.__notify_id = 0
134 if self.__xfer_handle is not None:
135 self.__xfer_handle.close(lambda handle, exc: None) #FIXME: report it?
136 self.__xfer_handle = None
138 self.__client.set_string("/apps/rhythmbox/plugins/magnatune/sorting", self.get_entry_view().get_sorting_type())
140 rb.BrowserSource.do_impl_delete_thyself (self)
144 # methods for use by plugin and UI
148 def display_artist_info(self):
149 tracks = self.get_entry_view().get_selected_entries()
150 urls = set([])
152 for tr in tracks:
153 url = self.__home_dict[self.__db.entry_get(tr, rhythmdb.PROP_LOCATION)]
154 if url not in urls:
155 gnomevfs.url_show(url)
156 urls.add(url)
158 def purchase_tracks(self):
159 tracks = self.get_entry_view().get_selected_entries()
160 skus = []
162 for track in tracks:
163 sku = self.__sku_dict[self.__db.entry_get(track, rhythmdb.PROP_LOCATION)]
164 if sku in skus:
165 continue
166 skus.append(sku)
167 artist = self.__db.entry_get(track, rhythmdb.PROP_ARTIST)
168 album = self.__db.entry_get(track, rhythmdb.PROP_ALBUM)
170 gladexml = gtk.glade.XML(self.__plugin.find_file("magnatune-purchase.glade"))
172 gladexml.get_widget("pay_combobox").set_active(self.__client.get_int(self.__plugin.gconf_keys['pay']) - 5)
173 gladexml.get_widget("audio_combobox").set_active(self.__plugin.format_list.index(self.__client.get_string(self.__plugin.gconf_keys['format'])))
174 gladexml.get_widget("info_label").set_text(_("Would you like to purchase the album '%s' by '%s'.") % (album, artist))
175 gladexml.get_widget("remember_cc_details").set_sensitive(has_gnome_keyring)
177 try:
178 (ccnumber, ccyear, ccmonth, name, email) = self.plugin.get_cc_details()
179 gladexml.get_widget("cc_entry").set_text(ccnumber)
180 gladexml.get_widget("yy_entry").set_text(ccyear)
181 gladexml.get_widget("mm_entry").set_active(ccmonth-1)
182 gladexml.get_widget("name_entry").set_text(name)
183 gladexml.get_widget("email_entry").set_text(email)
185 gladexml.get_widget("remember_cc_details").set_active(True)
186 except Exception, e:
187 print e
189 gladexml.get_widget("cc_entry").set_text("")
190 gladexml.get_widget("yy_entry").set_text("")
191 gladexml.get_widget("mm_entry").set_active(0)
192 gladexml.get_widget("name_entry").set_text("")
193 gladexml.get_widget("email_entry").set_text("")
195 gladexml.get_widget("remember_cc_details").set_active(False)
198 window = gladexml.get_widget("purchase_dialog")
199 if window.run() == gtk.RESPONSE_ACCEPT:
200 amount = pay_combo.get_active() + 5
201 format = self.__plugin.format_list[format_combo.get_active()]
202 ccnumber = gladexml.get_widget("cc_entry").get_text()
203 ccyear = gladexml.get_widget("yy_entry").get_text()
204 ccmonth = gladexml.get_widget("mm_combobox").get_active_text()
205 name = gladexml.get_widget("name_entry").get_text()
206 email = self.__client.get_string(self.__plugin.gconf_keys['email'])
208 if gladexml.get_widget("remember_cc_details").props.active:
209 self.plugin.store_cc_details(ccnumber, ccyear, ccmonth, name, email)
210 else:
211 self.plugin.clear_cc_details()
213 self.__purchase_album (sku, amount, format, ccnumber, ccyear, ccmonth, name, email)
215 window.destroy()
218 # internal catalogue downloading and loading
220 def __load_catalogue_read_cb (self, handle, data, exc_type, bytes_requested, parser):
221 if exc_type:
222 if issubclass (exc_type, gnomevfs.EOFError):
223 # successfully loaded
224 gtk.gdk.threads_enter()
225 self.__show_loading_screen (False)
227 in_progress_dir = gnomevfs.DirectoryHandle(gnomevfs.URI(magnatune_dir))
228 in_progress = in_progress_dir.next()
229 while True:
230 if in_progress.name[0:12] == "in_progress_":
231 in_progress = gnomevfs.read_entire_file(magnatune_dir + in_progress.name)
232 for uri in in_progress.split("\n"):
233 if uri == '':
234 continue
235 self.__download_album(gnomevfs.URI(uri))
236 try:
237 in_progress = in_progress_dir.next()
238 except:
239 break
240 gtk.gdk.threads_leave()
241 else:
242 # error reading file
243 raise exc_type
245 parser.close()
246 handle.close(lambda handle, exc: None) # FIXME: report it?
247 self.__load_handle = None
248 self.__updating = False
249 self.__notify_status_changed()
250 else:
252 parser.feed(data)
253 handle.read(64 * 1024, self.__load_catalogue_read_cb, parser)
255 self.__notify_status_changed()
257 def __load_catalogue_open_cb (self, handle, exc_type):
258 if exc_type:
259 self.__load_handle = None
260 self.__notify_status_changed()
262 if gnomevfs.exists(local_song_info_uri):
263 raise exc_type
264 else:
265 return
267 parser = xml.sax.make_parser()
268 parser.setContentHandler(TrackListHandler(self.__db, self.__entry_type, self.__sku_dict, self.__home_dict))
269 handle.read (64 * 1024, self.__load_catalogue_read_cb, parser)
271 def __load_catalogue(self):
272 self.__notify_status_changed()
273 self.__load_handle = gnomevfs.async.open (local_song_info_uri, self.__load_catalogue_open_cb)
276 def __download_update_cb (self, _reserved, info, moving):
277 if info.phase == gnomevfs.XFER_PHASE_COMPLETED:
278 # done downloading, unzip to real location
279 catalog = zipfile.ZipFile(local_song_info_temp_uri.path)
280 out = create_if_needed(local_song_info_uri, gnomevfs.OPEN_WRITE)
281 out.write(catalog.read("opt/magnatune/info/song_info.xml"))
282 out.close()
283 catalog.close()
284 gnomevfs.unlink(local_song_info_temp_uri)
285 self.__updating = False
286 self.__load_catalogue()
287 else:
288 #print info
289 pass
291 return 1
293 def __download_progress_cb (self, info, data):
294 #if info.status == gnomevfs.XFER_PROGRESS_STATUS_OK:
295 if True:
296 self.__load_current_size = info.bytes_copied
297 self.__load_total_size = info.bytes_total
298 self.__notify_status_changed()
299 else:
300 print info
301 return 1
303 def __download_catalogue(self):
304 self.__updating = True
305 create_if_needed(local_song_info_temp_uri, gnomevfs.OPEN_WRITE).close()
306 self.__xfer_handle = gnomevfs.async.xfer (source_uri_list = [magnatune_song_info_uri],
307 target_uri_list = [local_song_info_temp_uri],
308 xfer_options = gnomevfs.XFER_FOLLOW_LINKS_RECURSIVE,
309 error_mode = gnomevfs.XFER_ERROR_MODE_ABORT,
310 overwrite_mode = gnomevfs.XFER_OVERWRITE_MODE_REPLACE,
311 progress_update_callback = self.__download_update_cb,
312 update_callback_data = False,
313 progress_sync_callback = self.__download_progress_cb,
314 sync_callback_data = None)
316 def __update_catalogue(self):
317 def info_cb (handle, results):
318 (remote_uri, remote_exc, remote_info) = results[0]
319 (local_uri, local_exc, local_info) = results[1]
321 if remote_exc:
322 # error locating remote file
323 print "error locating remote catalogue", remote_exc
324 elif local_exc:
325 if issubclass (local_exc, gnomevfs.NotFoundError):
326 # we haven't got it yet
327 print "no local copy of catalogue"
328 self.__download_catalogue()
329 else:
330 # error locating local file
331 print "error locating local catalogue", local_exc
332 self.__download_catalogue()
333 else:
334 try:
335 if remote_info.mtime > local_info.mtime:
336 # newer version available
337 self.__download_catalogue()
338 else:
339 # up to date
340 pass
341 except ValueError, e:
342 # couldn't get the mtimes. download?
343 print "error checking times", e
344 self.__download_catalogue()
345 return
347 gnomevfs.async.get_file_info ((magnatune_song_info_uri, local_song_info_uri), info_cb)
349 def __show_loading_screen(self, show):
350 if self.__info_screen is None:
351 # load the glade stuff
352 gladexml = gtk.glade.XML(self.__plugin.find_file("magnatune-loading.glade"), root="magnatune_loading_vbox")
353 self.__info_screen = gladexml.get_widget("magnatune_loading_vbox")
354 self.pack_start(self.__info_screen)
355 self.get_entry_view().set_no_show_all (True)
356 self.__info_screen.set_no_show_all (True)
358 self.__info_screen.set_property("visible", show)
359 self.get_entry_view().set_property("visible", not show)
361 def __notify_status_changed(self):
362 def change_idle_cb():
363 self.notify_status_changed()
364 self.__notify_id = 0
365 return False
367 if self.__notify_id == 0:
368 self.__notify_id = gobject.idle_add(change_idle_cb)
372 # internal purchasing code
374 def __purchase_album(self, sku, pay, format, ccnumber, ccyear, ccmonth, name, email):
375 print "purchasing tracks:", sku, pay, format, name, email
377 try:
378 self.__buy_track(sku, pay, format, name, email, ccnumber, ccyear, ccmonth)
379 except MagnatunePurchaseError, e:
380 error_dlg = gtk.Dialog(title="Error", flags=gtk.DIALOG_DESTROY_WITH_PARENT, buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK))
381 label = gtk.Label(_("An error occurred while trying to purchase the album.\nThe Magnatune server returned:\n%s") % str(e))
382 error_dlg.vbox.pack_start(label)
383 label.set_selectable(True)
384 label.show()
385 error_dlg.connect("response", lambda w, r: w.destroy())
386 error_dlg.show()
388 def __buy_track(self, sku, pay, format, name, email, ccnumber, ccyear, ccmonth): # http://magnatune.com/info/api#purchase
389 url = "https://magnatune.com/buy/buy_dl_cc_xml?"
390 url = url + urllib.urlencode({
391 'id': magnatune_partner_id,
392 'sku': sku,
393 'amount': pay,
394 'cc': ccnumber,
395 'yy': ccyear,
396 'mm': ccmonth,
397 'name': name,
398 'email':email
401 buy_album_handler = BuyAlbumHandler(format) # so we can get the url and auth info
402 auth_parser = xml.sax.make_parser()
403 auth_parser.setContentHandler(buy_album_handler)
405 self.__wait_dlg = gtk.Dialog(title="Authorizing Purchase", flags=gtk.DIALOG_NO_SEPARATOR|gtk.DIALOG_DESTROY_WITH_PARENT)
406 lbl = gtk.Label("Authorizing purchase with the Magnatune server. Please wait...")
407 self.__wait_dlg.vbox.pack_start(lbl)
408 lbl.show()
409 self.__wait_dlg.show()
410 gnomevfs.async.open(gnomevfs.URI(url), self.__auth_open_cb, data=(buy_album_handler, auth_parser))
412 def __auth_open_cb(self, handle, exc_type, data):
413 if exc_type:
414 raise exc_type
416 handle.read(64 * 1024, self.__auth_read_cb, data)
419 def __auth_read_cb (self, handle, data, exc_type, bytes_requested, parser):
420 buy_album_handler = parser[0]
421 auth_parser = parser[1]
422 data = data.replace("<br>", "") # get rid of any stray <br> tags that will mess up the parser
423 if exc_type:
424 if issubclass (exc_type, gnomevfs.EOFError):
425 # successfully loaded
426 gtk.gdk.threads_enter()
427 audio_dl_uri = gnomevfs.URI(buy_album_handler.url)
428 audio_dl_uri = gnomevfs.URI(buy_album_handler.url[0:buy_album_handler.url.rfind("/") + 1] + urllib.quote(audio_dl_uri.short_name))
429 audio_dl_uri.user_name = str(buy_album_handler.username) # URI objects don't like unicode strings
430 audio_dl_uri.password = str(buy_album_handler.password)
432 in_progress = create_if_needed(gnomevfs.URI(magnatune_dir + "in_progress_" + audio_dl_uri.short_name), gnomevfs.OPEN_WRITE)
433 in_progress.write(str(audio_dl_uri))
434 in_progress.close()
435 self.__download_album(audio_dl_uri)
436 self.__wait_dlg.destroy()
437 gtk.gdk.threads_leave()
438 else:
439 # error reading file
440 raise exc_type
442 auth_parser.close()
443 handle.close(lambda handle, exc: None) # FIXME: report it?
445 else:
447 auth_parser.feed(data)
448 handle.read(64 * 1024, self.__auth_read_cb, parser)
450 def __download_album(self, audio_dl_uri):
451 library_location = self.__client.get_list("/apps/rhythmbox/library_locations", gconf.VALUE_STRING)[0] # Just use the first library location
452 to_file_uri = gnomevfs.URI(magnatune_dir + audio_dl_uri.short_name)
454 shell = self.get_property('shell')
455 manager = shell.get_player().get_property('ui-manager')
456 manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(True)
457 self.__downloading = True
458 self.cancelled = False
459 self.purchase_filesize += gnomevfs.get_file_info(audio_dl_uri).size
460 create_if_needed(to_file_uri, gnomevfs.OPEN_WRITE).close()
461 gnomevfs.async.xfer (source_uri_list = [audio_dl_uri],
462 target_uri_list = [to_file_uri],
463 xfer_options = gnomevfs.XFER_FOLLOW_LINKS_RECURSIVE,
464 error_mode = gnomevfs.XFER_ERROR_MODE_ABORT,
465 overwrite_mode = gnomevfs.XFER_OVERWRITE_MODE_REPLACE,
466 progress_update_callback = self.__purchase_download_update_cb,
467 update_callback_data = (to_file_uri, library_location, audio_dl_uri),
468 progress_sync_callback = self.__purchase_download_progress_cb,
469 sync_callback_data = (to_file_uri, audio_dl_uri))
471 def __purchase_download_update_cb(self, _reserved, info, data):
472 if (info.phase == gnomevfs.XFER_PHASE_COMPLETED):
473 to_file_uri = data[0]
474 library_location = data[1]
475 audio_dl_uri = data[2]
477 try:
478 del self.__downloads[str(audio_dl_uri)]
479 except:
480 return 0
481 self.purchase_filesize -= gnomevfs.get_file_info(audio_dl_uri).size
482 album = zipfile.ZipFile(to_file_uri.path)
483 for track in album.namelist():
484 track_uri = gnomevfs.URI(library_location + "/" + track)
485 out = create_if_needed(track_uri, gnomevfs.OPEN_WRITE)
486 out.write(album.read(track))
487 out.close()
488 album.close()
489 gnomevfs.unlink(gnomevfs.URI(magnatune_dir + "in_progress_" + to_file_uri.short_name))
490 gnomevfs.unlink(to_file_uri)
491 if self.purchase_filesize == 0:
492 self.__downloading = False
493 self.__db.add_uri("file://" + urllib.quote(track_uri.dirname))
494 return 1
496 def __purchase_download_progress_cb(self, info, data):
497 to_file_uri = data[0]
498 audio_dl_uri = data[1]
500 if self.cancelled:
501 try:
502 del self.__downloads[str(audio_dl_uri)]
503 self.purchase_filesize -= gnomevfs.get_file_info(audio_dl_uri).size
504 gnomevfs.unlink(gnomevfs.URI(magnatune_dir + "in_progress_" + to_file_uri.short_name))
505 gnomevfs.unlink(to_file_uri)
506 except: # this may get run more than once
507 pass
508 if self.purchase_filesize == 0:
509 self.__downloading = False
510 return 0
512 self.__downloads[str(audio_dl_uri)] = info.bytes_copied
513 purchase_downloaded = 0
514 for i in self.__downloads.values():
515 purchase_downloaded += i
516 self.__download_progress = purchase_downloaded / float(self.purchase_filesize)
517 self.__notify_status_changed()
518 return 1
520 def cancel_downloads(self):
521 self.cancelled = True
522 shell = self.get_property('shell')
523 manager = shell.get_player().get_property('ui-manager')
524 manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(False)
526 gobject.type_register(MagnatuneSource)
529 def create_if_needed(uri, mode):
530 if not gnomevfs.exists(uri):
531 for directory in URIIterator(uri):
532 if not gnomevfs.exists(directory):
533 gnomevfs.make_directory(directory, 0755)
534 out = gnomevfs.create(uri, open_mode=mode)
535 else:
536 out = gnomevfs.open(uri, open_mode=mode)
537 return out
541 class URIIterator:
542 def __init__(self, uri):
543 self.uri_list = uri.dirname.split("/")[1:] # dirname starts with /
544 self.counter = 0
545 def __iter__(self):
546 return self
547 def next(self):
548 if self.counter == len(self.uri_list) + 1:
549 raise StopIteration
550 value = "file://"
551 for i in range(self.counter):
552 value += "/" + self.uri_list[i]
553 self.counter += 1
554 return gnomevfs.URI(value)