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, GConf
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(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(path=RB.user_data_dir()).resolve_relative_path('magnatune')
53
magnatune_cache_dir = gio.File(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
'plugin': (RB.Plugin, 'plugin', 'plugin', gobject.PARAM_WRITABLE|gobject.PARAM_CONSTRUCT_ONLY),
64
__client = GConf.Client.get_default()
68
RB.BrowserSource.__init__(self, name=_("Magnatune"))
71
self.__activated = False
73
self.__notify_id = 0 # gobject.idle_add id for status notifications
74
self.__info_screen = None # the loading screen
82
self.__updating = True # whether we're loading the catalog right now
83
self.__has_loaded = False # whether the catalog has been loaded yet
84
self.__update_id = 0 # gobject.idle_add id for catalog updates
85
self.__catalogue_loader = None
86
self.__catalogue_check = None
87
self.__load_progress = (0, 0) # (complete, total)
89
# album download stuff
90
self.__downloads = {} # keeps track of download progress for each file
91
self.__cancellables = {} # keeps track of gio.Cancellable objects so we can abort album downloads
94
def do_set_property(self, property, value):
95
if property.name == 'plugin':
98
raise AttributeError, 'unknown property %s' % property.name
104
def do_impl_show_entry_popup(self):
105
self.show_source_popup("/MagnatuneSourceViewPopup")
107
def do_get_status(self, status, progress_text, progress):
109
complete, total = self.__load_progress
111
progress = min(float(complete) / total, 1.0)
114
return (_("Loading Magnatune catalog"), None, progress)
115
elif len(self.__downloads) > 0:
116
complete, total = map(sum, zip(*self.__downloads.itervalues()))
118
progress = min(float(complete) / total, 1.0)
121
return (_("Downloading Magnatune Album(s)"), None, progress)
123
qm = self.get_property("query-model")
124
return (qm.compute_status_normal("%d song", "%d songs"), None, 2.0)
126
def do_get_ui_actions(self):
127
return ["MagnatuneDownloadAlbum",
128
"MagnatuneArtistInfo",
129
"MagnatuneCancelDownload"]
131
def do_selected(self):
132
if not self.__activated:
133
shell = self.get_property('shell')
134
self.__db = shell.get_property('db')
135
self.__entry_type = self.get_property('entry-type')
137
# move files from old ~/.gnome2 paths
138
if not magnatune_in_progress_dir.query_exists():
139
self.__move_data_files()
141
self.__activated = True
142
self.__show_loading_screen(True)
144
# start our catalogue updates
145
self.__update_id = gobject.timeout_add_seconds(6 * 60 * 60, self.__update_catalogue)
146
self.__update_catalogue()
148
self.get_entry_view().set_sorting_type(self.__client.get_string("/apps/rhythmbox/plugins/magnatune/sorting"))
150
def do_impl_get_browser_key(self):
151
return "/apps/rhythmbox/plugins/magnatune/show_browser"
153
def do_impl_get_paned_key(self):
154
return "/apps/rhythmbox/plugins/magnatune/paned_position"
156
def do_impl_can_delete(self):
159
def do_impl_pack_paned(self, paned):
160
self.__paned_box = Gtk.VBox(homogeneous=False, spacing=5)
161
self.pack_start(self.__paned_box, True, True, 0)
162
self.__paned_box.pack_start(paned, True, True, 0)
165
def do_delete_thyself(self):
166
if self.__update_id != 0:
167
gobject.source_remove(self.__update_id)
170
if self.__notify_id != 0:
171
gobject.source_remove(self.__notify_id)
174
if self.__catalogue_loader is not None:
175
self.__catalogue_loader.cancel()
176
self.__catalogue_loader = None
178
if self.__catalogue_check is not None:
179
self.__catalogue_check.cancel()
180
self.__catalogue_check = None
182
self.__client.set_string("/apps/rhythmbox/plugins/magnatune/sorting", self.get_entry_view().get_sorting_type())
184
RB.BrowserSource.do_delete_thyself(self)
187
# methods for use by plugin and UI
190
def display_artist_info(self):
191
screen = self.props.shell.props.window.get_screen()
192
tracks = self.get_entry_view().get_selected_entries()
196
sku = self.__sku_dict[self.__db.entry_get_string(tr, RB.RhythmDBPropType.LOCATION)]
197
url = self.__home_dict[sku]
199
Gtk.show_uri(screen, url, Gdk.CURRENT_TIME)
202
def purchase_redirect(self):
203
screen = self.props.shell.props.window.get_screen()
204
tracks = self.get_entry_view().get_selected_entries()
208
sku = self.__sku_dict[self.__db.entry_get_string(tr, RB.RhythmDBPropType.LOCATION)]
209
url = magnatune_buy_album_uri + urllib.urlencode({ 'sku': sku, 'ref': magnatune_partner_id })
211
Gtk.show_uri(screen, url, Gdk.CURRENT_TIME)
214
def download_album(self):
215
if self.__client.get_string(self.__plugin.gconf_keys['account_type']) != 'download':
216
# The user doesn't have a download account, so redirect them to the purchase page.
217
self.purchase_redirect()
221
# Just use the first library location
222
library_location = rb.get_gconf_string_list("/apps/rhythmbox/library_locations")[0];
223
except IndexError, e:
224
RB.error_dialog(title = _("Couldn't purchase album"),
225
message = _("You must have a library location set to purchase an album."))
228
tracks = self.get_entry_view().get_selected_entries()
232
sku = self.__sku_dict[self.__db.entry_get_string(track, RB.RhythmDBPropType.LOCATION)]
236
self.__auth_download(sku)
239
# internal catalogue downloading and loading
242
def __update_catalogue(self):
243
def update_cb(result):
244
self.__catalogue_check = None
247
elif self.__has_loaded is False:
250
def download_catalogue():
251
def find_song_info(catalogue):
252
for info in catalogue.infolist():
253
if info.filename.endswith("song_info.xml"):
254
return info.filename;
257
def download_progress(complete, total):
258
self.__load_progress = (complete, total)
259
self.__notify_status_changed()
261
def download_finished(uri, result):
263
success = uri.copy_finish(result)
270
# done downloading, unzip to real location
271
catalog_zip = zipfile.ZipFile(magnatune_song_info_temp)
272
catalog = open(magnatune_song_info, 'w')
273
filename = find_song_info(catalog_zip)
275
RB.error_dialog(title=_("Unable to load catalog"),
276
message=_("Rhythmbox could not understand the Magnatune catalog, please file a bug."))
278
catalog.write(catalog_zip.read(filename))
283
self.__updating = False
284
self.__catalogue_loader = None
285
self.__notify_status_changed()
290
self.__updating = True
292
dest = gio.File(magnatune_song_info_temp)
293
self.__catalogue_loader = gio.Cancellable()
295
# For some reason, gio.FILE_COPY_OVERWRITE doesn't work for copy_async
299
magnatune_song_info_uri.copy_async(dest,
301
progress_callback=download_progress,
302
flags=gio.FILE_COPY_OVERWRITE,
303
cancellable=self.__catalogue_loader)
305
def load_catalogue():
306
def got_items(result, items):
307
account_type = self.__client.get_string(self.__plugin.gconf_keys['account_type'])
310
if account_type == 'none':
312
elif result is not None or len(items) == 0:
313
RB.error_dialog(title = _("Couldn't get account details"),
314
message = str(result))
318
username, password = items[0].secret.split('\n')
319
except ValueError: # Couldn't parse secret, possibly because it's empty
321
parser = xml.sax.make_parser()
322
parser.setContentHandler(TrackListHandler(self.__db, self.__entry_type, self.__sku_dict, self.__home_dict, self.__art_dict, account_type, username, password))
324
self.__catalogue_loader = rb.ChunkLoader()
325
self.__catalogue_loader.get_url_chunks(magnatune_song_info, 64*1024, True, catalogue_chunk_cb, parser)
327
def catalogue_chunk_cb(result, total, parser):
328
if not result or isinstance(result, Exception):
330
# report error somehow?
331
print "error loading catalogue: %s" % result
335
except xml.sax.SAXParseException, e:
336
# there isn't much we can do here
337
print "error parsing catalogue: %s" % e
339
self.__show_loading_screen(False)
340
self.__updating = False
341
self.__catalogue_loader = None
343
# restart in-progress downloads
344
# (doesn't really belong here)
345
for f in magnatune_in_progress_dir.enumerate_children('standard::name'):
347
if not name.startswith("in_progress_"):
349
uri = magnatune_in_progress_dir.resolve_relative_path(name).load_contents()[0]
350
print "restarting download from %s" % uri
351
self.__download_album(gio.File(uri=uri), name[12:])
353
# hack around some weird chars that show up in the catalogue for some reason
354
result = result.replace("\x19", "'")
355
result = result.replace("\x13", "-")
358
result = result.replace("Rock & Roll", "Rock & Roll")
362
except xml.sax.SAXParseException, e:
363
print "error parsing catalogue: %s" % e
365
load_size['size'] += len(result)
366
self.__load_progress = (load_size['size'], total)
368
self.__notify_status_changed()
371
self.__has_loaded = True
372
self.__updating = True
373
self.__load_progress = (0, 0) # (complete, total)
374
self.__notify_status_changed()
376
load_size = {'size': 0}
377
keyring.find_items(keyring.ITEM_GENERIC_SECRET, {'rhythmbox-plugin': 'magnatune'}, got_items)
380
self.__catalogue_check = rb.UpdateCheck()
381
self.__catalogue_check.check_for_update(magnatune_song_info, magnatune_song_info_uri.get_uri(), update_cb)
384
def __show_loading_screen(self, show):
385
if self.__info_screen is None:
386
# load the builder stuff
387
builder = Gtk.Builder()
388
builder.add_from_file(self.__plugin.find_file("magnatune-loading.ui"))
389
self.__info_screen = builder.get_object("magnatune_loading_scrolledwindow")
390
self.pack_start(self.__info_screen, True, True, 0)
391
self.get_entry_view().set_no_show_all(True)
392
self.__info_screen.set_no_show_all(True)
394
self.__info_screen.set_property("visible", show)
395
self.__paned_box.set_property("visible", not show)
397
def __notify_status_changed(self):
398
def change_idle_cb():
399
self.notify_status_changed()
403
if self.__notify_id == 0:
404
self.__notify_id = gobject.idle_add(change_idle_cb)
407
# internal purchasing code
410
def __auth_download(self, sku): # http://magnatune.com/info/api
411
def got_items(result, items):
412
if result is not None or len(items) == 0:
413
RB.error_dialog(title = _("Couldn't get account details"),
414
message = str(result))
418
username, password = items[0].secret.split('\n')
419
except ValueError: # Couldn't parse secret, possibly because it's empty
422
print "downloading album: " + sku
424
'id': magnatune_partner_id,
427
url = magnatune_api_download_uri % (username, password)
428
url = url + urllib.urlencode(url_dict)
431
l.get_url(url, auth_data_cb, (username, password))
433
def auth_data_cb(data, (username, password)):
434
buy_album_handler = BuyAlbumHandler(self.__client.get_string(self.__plugin.gconf_keys['format']))
435
auth_parser = xml.sax.make_parser()
436
auth_parser.setContentHandler(buy_album_handler)
443
data = data.replace("<br>", "") # get rid of any stray <br> tags that will mess up the parser
445
auth_parser.feed(data)
448
# process the URI: add authentication info, quote the filename component for some reason
449
parsed = urlparse.urlparse(buy_album_handler.url)
450
netloc = "%s:%s@%s" % (username, password, parsed.hostname)
452
spath = os.path.split(urllib.url2pathname(parsed.path))
454
path = urllib.pathname2url(os.path.join(spath[0], urllib.quote(basename)))
456
authed = (parsed[0], netloc, path) + parsed[3:]
457
audio_dl_uri = urlparse.urlunparse(authed)
459
self.__download_album(gio.File(audio_dl_uri), sku)
461
except MagnatunePurchaseError, e:
462
RB.error_dialog(title = _("Download Error"),
463
message = _("An error occurred while trying to authorize the download.\nThe Magnatune server returned:\n%s") % str(e))
465
RB.error_dialog(title = _("Error"),
466
message = _("An error occurred while trying to download the album.\nThe error text is:\n%s") % str(e))
469
keyring.find_items(keyring.ITEM_GENERIC_SECRET, {'rhythmbox-plugin': 'magnatune'}, got_items)
471
def __download_album(self, audio_dl_uri, sku):
472
def download_progress(current, total):
473
self.__downloads[str_uri] = (current, total)
474
self.__notify_status_changed()
476
def download_finished(uri, result):
477
del self.__cancellables[str_uri]
478
del self.__downloads[str_uri]
481
success = uri.copy_finish(result)
484
print "Download not completed: " + str(e)
487
threading.Thread(target=unzip_album).start()
489
remove_download_files()
491
if len(self.__downloads) == 0: # All downloads are complete
492
shell = self.get_property('shell')
493
manager = shell.get_player().get_property('ui-manager')
494
manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(False)
496
shell.notify_custom(4000, _("Finished Downloading"), _("All Magnatune downloads have been completed."))
498
self.__notify_status_changed()
501
# just use the first library location
502
library_location = gio.File(uri=rb.get_gconf_string_list("/apps/rhythmbox/library_locations")[0]);
504
album = zipfile.ZipFile(dest.get_path())
505
for track in album.namelist():
506
track_uri = library_location.resolve_relative_path(track).get_uri()
508
track_uri = RB.sanitize_uri_for_filesystem(track_uri)
509
RB.uri_create_parent_dirs(track_uri)
511
track_out = gio.File(uri=track_uri).create()
512
if track_out is not None:
513
track_out.write(album.read(track))
515
self.__db.add_uri(track_uri)
518
remove_download_files()
520
def remove_download_files():
525
in_progress = magnatune_in_progress_dir.resolve_relative_path("in_progress_" + sku)
526
dest = magnatune_in_progress_dir.resolve_relative_path(sku)
528
str_uri = audio_dl_uri.get_uri()
529
in_progress.replace_contents(str_uri, None, False, flags=gio.FILE_CREATE_PRIVATE|gio.FILE_CREATE_REPLACE_DESTINATION)
531
shell = self.get_property('shell')
532
manager = shell.get_player().get_property('ui-manager')
533
manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(True)
535
self.__downloads[str_uri] = (0, 0) # (current, total)
537
cancel = gio.Cancellable()
538
self.__cancellables[str_uri] = cancel
540
# For some reason, gio.FILE_COPY_OVERWRITE doesn't work for copy_async
545
# no way to resume downloads, sadly
546
audio_dl_uri.copy_async(dest,
548
progress_callback=download_progress,
549
flags=gio.FILE_COPY_OVERWRITE,
553
def cancel_downloads(self):
554
for cancel in self.__cancellables.values():
557
shell = self.get_property('shell')
558
manager = shell.get_player().get_property('ui-manager')
559
manager.get_action("/MagnatuneSourceViewPopup/MagnatuneCancelDownload").set_sensitive(False)
561
def playing_entry_changed(self, entry):
562
if not self.__db or not entry:
564
if entry.get_entry_type() != self.__db.entry_type_get_by_name("MagnatuneEntryType"):
567
gobject.idle_add(self.emit_cover_art_uri, entry)
569
def emit_cover_art_uri(self, entry):
570
sku = self.__sku_dict[self.__db.entry_get_string(entry, RB.RhythmDBPropType.LOCATION)]
571
url = self.__art_dict[sku]
572
self.__db.emit_entry_extra_metadata_notify(entry, 'rb:coverArt-uri', url)
575
def __move_data_files(self):
576
# create cache and data directories
577
magnatune_in_progress_path = magnatune_in_progress_dir.get_path()
578
magnatune_cache_path = magnatune_cache_dir.get_path()
580
# (we know they don't already exist, and we know the parent dirs do)
581
os.mkdir(magnatune_in_progress_path, 0700)
582
if os.path.exists(magnatune_cache_path) is False:
583
os.mkdir(magnatune_cache_path, 0700)
585
# move song info to cache dir
586
old_magnatune_dir = os.path.join(RB.dot_dir(), 'magnatune')
587
if os.path.exists(old_magnatune_dir) is False:
588
print "old magnatune directory does not exist"
591
old_song_info = os.path.join(old_magnatune_dir, 'song_info.xml')
592
if os.path.exists(old_song_info):
593
print "moving existing song_info.xml to cache dir"
594
os.rename(old_song_info, magnatune_song_info)
596
print "no song_info.xml found (%s)" % old_song_info
598
# move in progress downloads to data dir
599
otherfiles = os.listdir(old_magnatune_dir)
601
print "moving file %s to new in-progress dir" % f
602
os.rename(os.path.join(old_magnatune_dir, f),
603
os.path.join(magnatune_in_progress_path, f))
606
gobject.type_register(MagnatuneSource)