2 from TrackListHandler
import TrackListHandler
3 from BuyAlbumHandler
import BuyAlbumHandler
, MagnatunePurchaseError
7 import gnomevfs
, gnome
, gconf
11 has_gnome_keyring
= False
14 has_gnome_keyring
= True
19 magnatune_partner_id
= "zimmerman"
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
):
30 'plugin': (rb
.Plugin
, 'plugin', 'plugin', gobject
.PARAM_WRITABLE|gobject
.PARAM_CONSTRUCT_ONLY
),
33 __client
= gconf
.client_get_default()
38 rb
.BrowserSource
.__init
__(self
, name
=_("Magnatune"))
45 self
.__activated
= False
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':
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
)
72 raise AttributeError, 'unknown property %s' % property.name
79 def do_impl_show_entry_popup(self
):
80 self
.show_source_popup ("/MagnatuneSourceViewPopup")
82 def do_impl_get_status(self
):
84 if self
.__load
_total
_size
> 0:
85 progress
= min (float(self
.__load
_current
_size
) / self
.__load
_total
_size
, 1.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
)
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
)
130 if self
.__notify
_id
!= 0:
131 gobject
.source_remove (self
.__notify
_id
)
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()
153 url
= self
.__home
_dict
[self
.__db
.entry_get(tr
, rhythmdb
.PROP_LOCATION
)]
155 gnomevfs
.url_show(url
)
158 def purchase_tracks(self
):
159 tracks
= self
.get_entry_view().get_selected_entries()
163 sku
= self
.__sku
_dict
[self
.__db
.entry_get(track
, rhythmdb
.PROP_LOCATION
)]
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
)
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)
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
)
211 self
.plugin
.clear_cc_details()
213 self
.__purchase
_album
(sku
, amount
, format
, ccnumber
, ccyear
, ccmonth
, name
, email
)
218 # internal catalogue downloading and loading
220 def __load_catalogue_read_cb (self
, handle
, data
, exc_type
, bytes_requested
, parser
):
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()
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"):
235 self
.__download
_album
(gnomevfs
.URI(uri
))
237 in_progress
= in_progress_dir
.next()
240 gtk
.gdk
.threads_leave()
246 handle
.close(lambda handle
, exc
: None) # FIXME: report it?
247 self
.__load
_handle
= None
248 self
.__updating
= False
249 self
.__notify
_status
_changed
()
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
):
259 self
.__load
_handle
= None
260 self
.__notify
_status
_changed
()
262 if gnomevfs
.exists(local_song_info_uri
):
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"))
284 gnomevfs
.unlink(local_song_info_temp_uri
)
285 self
.__updating
= False
286 self
.__load
_catalogue
()
293 def __download_progress_cb (self
, info
, data
):
294 #if info.status == gnomevfs.XFER_PROGRESS_STATUS_OK:
296 self
.__load
_current
_size
= info
.bytes_copied
297 self
.__load
_total
_size
= info
.bytes_total
298 self
.__notify
_status
_changed
()
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]
322 # error locating remote file
323 print "error locating remote catalogue", remote_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
()
330 # error locating local file
331 print "error locating local catalogue", local_exc
332 self
.__download
_catalogue
()
335 if remote_info
.mtime
> local_info
.mtime
:
336 # newer version available
337 self
.__download
_catalogue
()
341 except ValueError, e
:
342 # couldn't get the mtimes. download?
343 print "error checking times", e
344 self
.__download
_catalogue
()
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()
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
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)
385 error_dlg
.connect("response", lambda w
, r
: w
.destroy())
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
,
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
)
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
):
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
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
))
435 self
.__download
_album
(audio_dl_uri
)
436 self
.__wait
_dlg
.destroy()
437 gtk
.gdk
.threads_leave()
443 handle
.close(lambda handle
, exc
: None) # FIXME: report it?
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]
478 del self
.__downloads
[str(audio_dl_uri
)]
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
))
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
))
496 def __purchase_download_progress_cb(self
, info
, data
):
497 to_file_uri
= data
[0]
498 audio_dl_uri
= data
[1]
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
508 if self
.purchase_filesize
== 0:
509 self
.__downloading
= False
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
()
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
)
536 out
= gnomevfs
.open(uri
, open_mode
=mode
)
542 def __init__(self
, uri
):
543 self
.uri_list
= uri
.dirname
.split("/")[1:] # dirname starts with /
548 if self
.counter
== len(self
.uri_list
) + 1:
551 for i
in range(self
.counter
):
552 value
+= "/" + self
.uri_list
[i
]
554 return gnomevfs
.URI(value
)