1
# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
3
# Copyright (C) 2006 Adam Zimmerman <adam_zimmerman@sfu.ca>
4
# Copyright (C) 2006 James Livingston <doclivingston@gmail.com>
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2, or (at your option)
11
# The Rhythmbox authors hereby grant permission for non-GPL compatible
12
# GStreamer plugins to be used and distributed together with GStreamer
13
# and Rhythmbox. This permission is above and beyond the permissions granted
14
# by the GPL license by which Rhythmbox is covered. If you modify this code
15
# you may extend this exception to your version of the code, but you are not
16
# obligated to do so. If you do not wish to do so, delete this exception
17
# statement from your version.
19
# This program is distributed in the hope that it will be useful,
20
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
# GNU General Public License for more details.
24
# You should have received a copy of the GNU General Public License
25
# along with this program; if not, write to the Free Software
26
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
35
import gnomekeyring as keyring
38
from gi.repository import RB
39
from gi.repository import Gtk, Gio
40
# XXX use GnomeKeyring when introspection is available
42
from TrackListHandler import TrackListHandler
43
from BuyAlbumHandler import BuyAlbumHandler, MagnatunePurchaseError
45
magnatune_partner_id = "rhythmbox"
48
magnatune_song_info_uri = Gio.file_new_for_uri("http://magnatune.com/info/song_info_xml.zip")
49
magnatune_buy_album_uri = "https://magnatune.com/buy/choose?"
50
magnatune_api_download_uri = "http://%s:%s@download.magnatune.com/buy/membership_free_dl_xml?"
52
magnatune_in_progress_dir = Gio.file_new_for_path(RB.user_data_dir()).resolve_relative_path('magnatune')
53
magnatune_cache_dir = Gio.file_new_for_path(RB.user_cache_dir()).resolve_relative_path('magnatune')
55
magnatune_song_info = os.path.join(magnatune_cache_dir.get_path(), 'song_info.xml')
56
magnatune_song_info_temp = os.path.join(magnatune_cache_dir.get_path(), 'song_info.zip.tmp')
59
class MagnatuneSource(RB.BrowserSource):
61
RB.BrowserSource.__init__(self)
64
self.__settings = Gio.Settings("org.gnome.rhythmbox.plugins.magnatune")
66
self.__activated = False
68
self.__notify_id = 0 # gobject.idle_add id for status notifications
69
self.__info_screen = None # the loading screen
77
self.__updating = True # whether we're loading the catalog right now
78
self.__has_loaded = False # whether the catalog has been loaded yet
79
self.__update_id = 0 # gobject.idle_add id for catalog updates
80
self.__catalogue_loader = None
81
self.__catalogue_check = None
82
self.__load_progress = (0, 0) # (complete, total)
84
# album download stuff
85
self.__downloads = {} # keeps track of download progress for each file
86
self.__cancellables = {} # keeps track of Gio.Cancellable objects so we can abort album downloads
92
def do_impl_show_entry_popup(self):
93
self.show_source_popup("/MagnatuneSourceViewPopup")
95
def do_get_status(self, status, progress_text, progress):
97
complete, total = self.__load_progress
99
progress = min(float(complete) / total, 1.0)
102
return (_("Loading Magnatune catalog"), None, progress)
103
elif len(self.__downloads) > 0:
104
complete, total = map(sum, zip(*self.__downloads.itervalues()))
106
progress = min(float(complete) / total, 1.0)
109
return (_("Downloading Magnatune Album(s)"), None, progress)
111
qm = self.props.query_model
112
return (qm.compute_status_normal("%d song", "%d songs"), None, 2.0)
114
def do_get_ui_actions(self):
115
return ["MagnatuneDownloadAlbum",
116
"MagnatuneArtistInfo",
117
"MagnatuneCancelDownload"]
119
def do_selected(self):
120
if not self.__activated:
121
shell = self.props.shell
122
self.__db = shell.props.db
123
self.__entry_type = self.props.entry_type
125
if not magnatune_in_progress_dir.query_exists(None):
126
magnatune_in_progress_path = magnatune_in_progress_dir.get_path()
127
os.mkdir(magnatune_in_progress_path, 0700)
129
if not magnatune_cache_dir.query_exists(None):
130
magnatune_cache_path = magnatune_cache_dir.get_path()
131
os.mkdir(magnatune_cache_path, 0700)
133
self.__activated = True
134
self.__show_loading_screen(True)
136
# start our catalogue updates
137
self.__update_id = gobject.timeout_add_seconds(6 * 60 * 60, self.__update_catalogue)
138
self.__update_catalogue()
140
def do_impl_can_delete(self):
143
def do_impl_pack_paned(self, paned):
144
self.__paned_box = Gtk.VBox(homogeneous=False, spacing=5)
145
self.pack_start(self.__paned_box, True, True, 0)
146
self.__paned_box.pack_start(paned, True, True, 0)
149
def do_delete_thyself(self):
150
if self.__update_id != 0:
151
gobject.source_remove(self.__update_id)
154
if self.__notify_id != 0:
155
gobject.source_remove(self.__notify_id)
158
if self.__catalogue_loader is not None:
159
self.__catalogue_loader.cancel()
160
self.__catalogue_loader = None
162
if self.__catalogue_check is not None:
163
self.__catalogue_check.cancel()
164
self.__catalogue_check = None
166
RB.BrowserSource.do_delete_thyself(self)
169
# methods for use by plugin and UI
172
def display_artist_info(self):
173
screen = self.props.shell.props.window.get_screen()
174
tracks = self.get_entry_view().get_selected_entries()
178
sku = self.__sku_dict[self.__db.entry_get_string(tr, RB.RhythmDBPropType.LOCATION)]
179
url = self.__home_dict[sku]
181
Gtk.show_uri(screen, url, Gdk.CURRENT_TIME)
184
def purchase_redirect(self):
185
screen = self.props.shell.props.window.get_screen()
186
tracks = self.get_entry_view().get_selected_entries()
190
sku = self.__sku_dict[self.__db.entry_get_string(tr, RB.RhythmDBPropType.LOCATION)]
191
url = magnatune_buy_album_uri + urllib.urlencode({ 'sku': sku, 'ref': magnatune_partner_id })
193
Gtk.show_uri(screen, url, Gdk.CURRENT_TIME)
196
def download_album(self):
197
if selt.__settings['account_type'] != 'download':
198
# The user doesn't have a download account, so redirect them to the purchase page.
199
self.purchase_redirect()
203
# Just use the first library location
204
library = Gio.Settings("org.gnome.rhythmbox.rhythmdb")
205
library_location = library['locations'][0]
206
except IndexError, e:
207
RB.error_dialog(title = _("Couldn't purchase album"),
208
message = _("You must have a library location set to purchase an album."))
211
tracks = self.get_entry_view().get_selected_entries()
215
sku = self.__sku_dict[self.__db.entry_get_string(track, RB.RhythmDBPropType.LOCATION)]
219
self.__auth_download(sku)
222
# internal catalogue downloading and loading
225
def __update_catalogue(self):
226
def update_cb(result):
227
self.__catalogue_check = None
230
elif self.__has_loaded is False:
233
def download_catalogue():
234
def find_song_info(catalogue):
235
for info in catalogue.infolist():
236
if info.filename.endswith("song_info.xml"):
237
return info.filename;
240
def download_progress(complete, total):
241
self.__load_progress = (complete, total)
242
self.__notify_status_changed()
244
def download_finished(uri, result):
246
success = uri.copy_finish(result)
253
# done downloading, unzip to real location
254
catalog_zip = zipfile.ZipFile(magnatune_song_info_temp)
255
catalog = open(magnatune_song_info, 'w')
256
filename = find_song_info(catalog_zip)
258
RB.error_dialog(title=_("Unable to load catalog"),
259
message=_("Rhythmbox could not understand the Magnatune catalog, please file a bug."))
261
catalog.write(catalog_zip.read(filename))
266
self.__updating = False
267
self.__catalogue_loader = None
268
self.__notify_status_changed()
273
self.__updating = True
275
dest = Gio.file_new_for_path(magnatune_song_info_temp)
276
self.__catalogue_loader = Gio.Cancellable()
278
# For some reason, Gio.FileCopyFlags.OVERWRITE doesn't work for copy_async
282
magnatune_song_info_uri.copy_async(dest,
284
progress_callback=download_progress,
285
flags=Gio.FileCopyFlags.OVERWRITE,
286
cancellable=self.__catalogue_loader)
288
def load_catalogue():
289
def got_items(result, items):
290
account_type = self.__settings['account_type']
293
if account_type == 'none':
295
elif result is not None or len(items) == 0:
296
RB.error_dialog(title = _("Couldn't get account details"),
297
message = str(result))
301
username, password = items[0].secret.split('\n')
302
except ValueError: # Couldn't parse secret, possibly because it's empty
304
parser = xml.sax.make_parser()
305
parser.setContentHandler(TrackListHandler(self.__db, self.__entry_type, self.__sku_dict, self.__home_dict, self.__art_dict, account_type, username, password))
307
self.__catalogue_loader = rb.ChunkLoader()
308
self.__catalogue_loader.get_url_chunks(magnatune_song_info, 64*1024, True, catalogue_chunk_cb, parser)
310
def catalogue_chunk_cb(result, total, parser):
311
if not result or isinstance(result, Exception):
313
# report error somehow?
314
print "error loading catalogue: %s" % result
318
except xml.sax.SAXParseException, e:
319
# there isn't much we can do here
320
print "error parsing catalogue: %s" % e
322
self.__show_loading_screen(False)
323
self.__updating = False
324
self.__catalogue_loader = None
326
# restart in-progress downloads
327
# (doesn't really belong here)
328
for f in magnatune_in_progress_dir.enumerate_children('standard::name'):
330
if not name.startswith("in_progress_"):
332
uri = magnatune_in_progress_dir.resolve_relative_path(name).load_contents()[0]
333
print "restarting download from %s" % uri
334
self.__download_album(Gio.file_new_for_uri(uri), name[12:])
336
# hack around some weird chars that show up in the catalogue for some reason
337
result = result.replace("\x19", "'")
338
result = result.replace("\x13", "-")
341
result = result.replace("Rock & Roll", "Rock & Roll")
345
except xml.sax.SAXParseException, e:
346
print "error parsing catalogue: %s" % e
348
load_size['size'] += len(result)
349
self.__load_progress = (load_size['size'], total)
351
self.__notify_status_changed()
354
self.__has_loaded = True
355
self.__updating = True
356
self.__load_progress = (0, 0) # (complete, total)
357
self.__notify_status_changed()
359
load_size = {'size': 0}
360
keyring.find_items(keyring.ITEM_GENERIC_SECRET, {'rhythmbox-plugin': 'magnatune'}, got_items)
363
self.__catalogue_check = rb.UpdateCheck()
364
self.__catalogue_check.check_for_update(magnatune_song_info, magnatune_song_info_uri.get_uri(), update_cb)
367
def __show_loading_screen(self, show):
368
if self.__info_screen is None:
369
# load the builder stuff
370
builder = Gtk.Builder()
371
builder.add_from_file(rb.find_plugin_file(self.props.plugin, "magnatune-loading.ui"))
372
self.__info_screen = builder.get_object("magnatune_loading_scrolledwindow")
373
self.pack_start(self.__info_screen, True, True, 0)
374
self.get_entry_view().set_no_show_all(True)
375
self.__info_screen.set_no_show_all(True)
377
self.__info_screen.set_property("visible", show)
378
self.__paned_box.set_property("visible", not show)
380
def __notify_status_changed(self):
381
def change_idle_cb():
382
self.notify_status_changed()
386
if self.__notify_id == 0:
387
self.__notify_id = gobject.idle_add(change_idle_cb)
390
# internal purchasing code
393
def __auth_download(self, sku): # http://magnatune.com/info/api
394
def got_items(result, items):
395
if result is not None or len(items) == 0:
396
RB.error_dialog(title = _("Couldn't get account details"),
397
message = str(result))
401
username, password = items[0].secret.split('\n')
402
except ValueError: # Couldn't parse secret, possibly because it's empty
405
print "downloading album: " + sku
407
'id': magnatune_partner_id,
410
url = magnatune_api_download_uri % (username, password)
411
url = url + urllib.urlencode(url_dict)
414
l.get_url(url, auth_data_cb, (username, password))
416
def auth_data_cb(data, (username, password)):
417
buy_album_handler = BuyAlbumHandler(self.__settings['format'])
418
auth_parser = xml.sax.make_parser()
419
auth_parser.setContentHandler(buy_album_handler)
426
data = data.replace("<br>", "") # get rid of any stray <br> tags that will mess up the parser
428
auth_parser.feed(data)
431
# process the URI: add authentication info, quote the filename component for some reason
432
parsed = urlparse.urlparse(buy_album_handler.url)
433
netloc = "%s:%s@%s" % (username, password, parsed.hostname)
435
spath = os.path.split(urllib.url2pathname(parsed.path))
437
path = urllib.pathname2url(os.path.join(spath[0], urllib.quote(basename)))
439
authed = (parsed[0], netloc, path) + parsed[3:]
440
audio_dl_uri = urlparse.urlunparse(authed)
442
self.__download_album(Gio.file_new_for_uri(audio_dl_uri), sku)
444
except MagnatunePurchaseError, e:
445
RB.error_dialog(title = _("Download Error"),
446
message = _("An error occurred while trying to authorize the download.\nThe Magnatune server returned:\n%s") % str(e))
448
RB.error_dialog(title = _("Error"),
449
message = _("An error occurred while trying to download the album.\nThe error text is:\n%s") % str(e))
452
keyring.find_items(keyring.ITEM_GENERIC_SECRET, {'rhythmbox-plugin': 'magnatune'}, got_items)
454
def __download_album(self, audio_dl_uri, sku):
455
def download_progress(current, total):
456
self.__downloads[str_uri] = (current, total)
457
self.__notify_status_changed()
459
def download_finished(uri, result):
460
del self.__cancellables[str_uri]
461
del self.__downloads[str_uri]
464
success = uri.copy_finish(result)
467
print "Download not completed: " + str(e)
470
threading.Thread(target=unzip_album).start()
472
remove_download_files()
474
if len(self.__downloads) == 0: # All downloads are complete
475
shell = self.props.shell
476
manager = shell.props.ui_manager
477
manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(False)
479
shell.notify_custom(4000, _("Finished Downloading"), _("All Magnatune downloads have been completed."))
481
self.__notify_status_changed()
484
# just use the first library location
485
library = Gio.Settings("org.gnome.rhythmbox.rhythmdb")
486
library_location = Gio.file_new_for_uri(library['locations'][0])
488
album = zipfile.ZipFile(dest.get_path())
489
for track in album.namelist():
490
track_uri = library_location.resolve_relative_path(track).get_uri()
492
track_uri = RB.sanitize_uri_for_filesystem(track_uri)
493
RB.uri_create_parent_dirs(track_uri)
495
track_out = Gio.file_new_for_uri(track_uri).create()
496
if track_out is not None:
497
track_out.write(album.read(track))
499
self.__db.add_uri(track_uri)
502
remove_download_files()
504
def remove_download_files():
509
in_progress = magnatune_in_progress_dir.resolve_relative_path("in_progress_" + sku)
510
dest = magnatune_in_progress_dir.resolve_relative_path(sku)
512
str_uri = audio_dl_uri.get_uri()
513
in_progress.replace_contents(str_uri, None, False, flags=Gio.FileCreateFlags.PRIVATE|Gio.FileCreateFlags.REPLACE_DESTINATION)
515
shell = self.props.shell
516
manager = shell.props.ui_manager
517
manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(True)
519
self.__downloads[str_uri] = (0, 0) # (current, total)
521
cancel = Gio.Cancellable()
522
self.__cancellables[str_uri] = cancel
524
# For some reason, Gio.FileCopyFlags.OVERWRITE doesn't work for copy_async
529
# no way to resume downloads, sadly
530
audio_dl_uri.copy_async(dest,
532
progress_callback=download_progress,
533
flags=Gio.FileCopyFlags.OVERWRITE,
537
def cancel_downloads(self):
538
for cancel in self.__cancellables.values():
541
shell = self.props.shell
542
manager = shell.props.ui_manager
543
manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(False)
545
def playing_entry_changed(self, entry):
546
if not self.__db or not entry:
548
if entry.get_entry_type() != self.__db.entry_type_get_by_name("MagnatuneEntryType"):
551
gobject.idle_add(self.emit_cover_art_uri, entry)
553
def emit_cover_art_uri(self, entry):
554
sku = self.__sku_dict[self.__db.entry_get_string(entry, RB.RhythmDBPropType.LOCATION)]
555
url = self.__art_dict[sku]
556
self.__db.emit_entry_extra_metadata_notify(entry, 'rb:coverArt-uri', url)
559
gobject.type_register(MagnatuneSource)