1
"""The Ubuntu One Music Store Rhythmbox plugin."""
2
# Copyright (C) 2009 Canonical, Ltd.
4
# This library is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU Lesser General Public License
6
# version 3.0 as published by the Free Software Foundation.
8
# This library is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU Lesser General Public License version 3.0 for more details.
13
# You should have received a copy of the GNU Lesser General Public
14
# License along with this library. If not, see
15
# <http://www.gnu.org/licenses/>.
17
# Authored by Stuart Langridge <stuart.langridge@canonical.com>
20
import gtk, gobject, os, urllib, gconf, stat, urlparse, gio
21
import gst, gst.pbutils
22
import aptdaemon.client
23
from aptdaemon.enums import *
24
from aptdaemon.gtkwidgets import AptErrorDialog, \
27
from ubuntuone.gtkwidgets import MusicStore as U1MusicStore
28
import xdg.BaseDirectory
29
import dbus.exceptions
32
from gettext import lgettext as _
33
gettext.bindtextdomain("rhythmbox-ubuntuone-music-store", "/usr/share/locale")
34
gettext.textdomain("rhythmbox-ubuntuone-music-store")
36
MUSIC_STORE_WIDGET = U1MusicStore() # keep this around for later
37
U1LIBRARYPATH = MUSIC_STORE_WIDGET.get_library_location()
38
RB_LIBRARY_LOCATIONS = "/apps/rhythmbox/library_locations"
40
class U1MusicStoreWidget(object):
41
"""The Ubuntu One Music Store."""
42
def __init__(self, plugin, find_file):
44
self.find_file = find_file
46
def activate(self, shell):
48
self.db = shell.get_property("db")
49
group = rb.rb_source_group_get_by_name ("stores")
51
group = rb.rb_source_group_register ("stores",
53
rb.SOURCE_GROUP_CATEGORY_FIXED)
55
icon_file_name = self.find_file("musicstore_icon.png")
56
icon = gtk.gdk.pixbuf_new_from_file_at_size(icon_file_name, 22, 22)
57
self.entry_type = self.db.entry_register_type("U1EntryType")
58
self.source = gobject.new (U1Source,
60
entry_type=self.entry_type,
64
shell.register_entry_type_for_source(self.source, self.entry_type)
66
self.source.connect("preview-mp3", self.play_preview_mp3)
67
self.source.connect("play-library", self.play_library)
68
self.source.connect("download-finished", self.download_finished)
69
self.source.connect("url-loaded", self.url_loaded)
72
shell.append_source(self.source, None) # Add the source to the list
75
def deactivate(self, shell):
76
"""Plugin shutdown."""
78
self.source.delete_thyself()
79
# remove the library, if it's empty
81
filecount = len(os.listdir(self.U1_LIBRARY_SYMLINK))
84
# so they never downloaded anything
87
client = gconf.client_get_default()
88
libraries = client.get_list(RB_LIBRARY_LOCATIONS, gconf.VALUE_STRING)
89
if self.u1_library_path_url in libraries:
90
client.notify_remove(self.library_adder)
91
libraries.remove(self.u1_library_path_url)
92
client.set_list(RB_LIBRARY_LOCATIONS, gconf.VALUE_STRING, libraries)
93
# delete held references
98
def url_loaded(self, source, url):
99
"""A URL is loaded in the plugin"""
100
print "URL loaded:", url
101
if urlparse.urlparse(url).scheme == "https":
106
def _udf_path_to_library_uri(self, path):
107
"""Calculate the path in the library.
108
Since the library is accessed via the created symlink, but the path
109
passed to us is to a file in the actual music store UDF, we need to
110
work out the path inside the library, i.e., what the path to that file
111
is via the symlink."""
113
if path.startswith(U1LIBRARYPATH):
114
subpath = path[len(U1LIBRARYPATH):]
117
if subpath.startswith("/"): subpath = subpath[1:]
118
library_path = os.path.join(self.U1_LIBRARY_SYMLINK, subpath)
119
# convert path to URI. Don't use urllib for this; Python and
120
# glib escape URLs differently. gio does it the glib way.
121
library_uri = gio.File(library_path).get_uri()
124
def download_finished(self, source, path):
125
"""A file is finished downloading"""
126
library_uri = self._udf_path_to_library_uri(path)
128
if not self.shell.props.db.entry_lookup_by_location(library_uri):
129
self.db.add_uri(library_uri)
131
def play_library(self, source, path):
132
"""Switch to and start playing a song from the library"""
133
uri = self._udf_path_to_library_uri(path)
134
entry = self.shell.props.db.entry_lookup_by_location(uri)
136
print "couldn't find entry", uri
138
libsrc = self.shell.props.library_source
139
genre_view, artist_view, album_view = libsrc.get_property_views()
140
song_view = libsrc.get_entry_view()
141
artist = self.shell.props.db.entry_get(entry, rhythmdb.PROP_ARTIST)
142
album = self.shell.props.db.entry_get(entry, rhythmdb.PROP_ALBUM)
143
self.shell.props.sourcelist.select(libsrc)
144
artist_view.set_selection([artist])
145
album_view.set_selection([album])
146
song_view.scroll_to_entry(entry)
147
player = self.shell.get_player()
150
player.play_entry(entry, libsrc)
152
def play_preview_mp3(self, source, url, title):
153
"""Play a passed mp3; signal handler for preview-mp3 signal."""
154
# create an entry, don't save it, and play it
155
entry = self.shell.props.db.entry_lookup_by_location(url)
157
entry = self.shell.props.db.entry_new(self.entry_type, url)
158
self.shell.props.db.set(entry, rhythmdb.PROP_TITLE, title)
159
player = self.shell.get_player()
162
player.play_entry(entry, self.source)
163
# FIXME delete this entry when it finishes playing. Don't know how yet.
165
def add_u1_library(self):
166
"""Add the U1 library if not listed in RB and re-add if changed."""
167
u1_library_path_folder = xdg.BaseDirectory.save_data_path("ubuntuone")
168
# Ensure that we can write to the folder, because syncdaemon creates it
169
# with no write permissions
170
os.chmod(u1_library_path_folder,
171
os.stat(u1_library_path_folder)[stat.ST_MODE] | stat.S_IWUSR)
172
# Translators: this is the name under Music for U1 music in Rhythmbox
173
u1_library_path = os.path.join(u1_library_path_folder, _("Purchased from Ubuntu One"))
174
if not os.path.islink(u1_library_path):
175
if not os.path.exists(u1_library_path):
176
print "Attempting to symlink %s to %s" % (U1LIBRARYPATH,
178
os.symlink(U1LIBRARYPATH, u1_library_path)
180
# something that isn't a symlink already exists. That's not
181
# supposed to happen.
182
# Write a warning and carry on.
183
print ("Warning: library location %s existed. It should have "
184
"been a symlink to %s, and it wasn't. This isn't supposed "
185
"to happen. Carrying on anyway, on the assumption that "
186
"you know what you're doing. If this is a problem, then "
187
"delete or rename %s.") % (u1_library_path, U1LIBRARYPATH,
189
self.u1_library_path_url = "file://%s" % urllib.quote(u1_library_path)
190
self.U1_LIBRARY_SYMLINK = u1_library_path
191
client = gconf.client_get_default()
192
self._add_u1_library_if_not_present(client)
193
self._remove_old_u1_library_if_present(client)
194
# Watch for changes to the gconf key and re-add the library
195
self.library_adder = client.notify_add(RB_LIBRARY_LOCATIONS,
196
self._add_u1_library_if_not_present)
198
def _add_u1_library_if_not_present(self, client, *args, **kwargs):
199
"""Check for the U1 library and add to libraries list."""
200
libraries = client.get_list(RB_LIBRARY_LOCATIONS, gconf.VALUE_STRING)
201
if self.u1_library_path_url not in libraries:
202
libraries.append(self.u1_library_path_url)
203
client.set_list(RB_LIBRARY_LOCATIONS, gconf.VALUE_STRING, libraries)
205
def _remove_old_u1_library_if_present(self, client, *args, **kwargs):
206
"""Check for the old U1 library and remove it from libraries list."""
207
libraries = client.get_list(RB_LIBRARY_LOCATIONS, gconf.VALUE_STRING)
209
# originally, this was the library path. Someone might have this.
210
old_path = "file://%s" % os.path.expanduser("~/.ubuntuone/musicstore")
211
if old_path in libraries:
212
libraries.remove(old_path)
214
# Then, this was the library path, which we put into gconf unescaped
215
actual_udf_path_unescaped = "file://%s" % U1LIBRARYPATH
216
if actual_udf_path_unescaped in libraries:
217
libraries.remove(actual_udf_path_unescaped)
219
# In theory, no-one should have the escaped path, but let's check
220
actual_udf_path_escaped = "file://%s" % U1LIBRARYPATH
221
if actual_udf_path_escaped in libraries:
222
libraries.remove(actual_udf_path_escaped)
224
# Also, remove any library which is in .local/share and *isn't*
225
# the current library. This caters for people who got the library
226
# created under one name (say, English) and then had it created
227
# under another (say, after a translation to Dutch)
228
u1_library_path_folder = xdg.BaseDirectory.save_data_path("ubuntuone")
229
u1_library_path_folder_url = "file://%s" % urllib.quote(u1_library_path_folder)
230
symlink_url = "file://" + urllib.quote(self.U1_LIBRARY_SYMLINK)
232
if l.startswith(u1_library_path_folder_url) and l != symlink_url:
234
# and delete the symlink itself
235
symlink_to_remove = urllib.unquote(l[7:])
236
os.unlink(symlink_to_remove)
239
client.set_list(RB_LIBRARY_LOCATIONS, gconf.VALUE_STRING, libraries)
241
class U1Source(rb.Source):
242
"""A Rhythmbox source widget for the U1 Music Store."""
243
# gproperties required so that rb.Source is instantiable
245
'plugin': (rb.Plugin, 'plugin', 'plugin',
246
gobject.PARAM_WRITABLE|gobject.PARAM_CONSTRUCT_ONLY),
248
# we have the preview-mp3 signal; we receive it from the widget, and re-emit
249
# it so that the plugin gets it, because the plugin actually plays the mp3
251
"preview-mp3": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str, str)),
252
"play-library": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str,)),
253
"download-finished": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str,)),
254
"url-loaded": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (str,)),
258
rb.Source.__init__(self, name=_("Ubuntu One"))
259
self.browser = MUSIC_STORE_WIDGET
260
self.__activated = False
262
def do_impl_activate(self):
263
"""Source startup."""
264
if self.__activated: return
265
self.__activated = True
266
self.test_can_play_mp3()
267
rb.Source.do_impl_activate (self)
269
def do_impl_want_uri(self, uri):
270
"""I want to handle u1ms URLs"""
271
if uri.startswith("u1ms://"):
273
rb.Source.do_impl_want_uri(self, uri)
276
def do_impl_add_uri(self, uri, title, genre):
277
"""Handle a u1ms URL"""
278
if not uri.startswith("u1ms://"):
280
uri_to_use = uri.replace("u1ms://", "http://")
281
print "Calling u1musicstore plugin with %s" % uri_to_use
282
self.browser.load_store_link(uri_to_use)
285
def test_can_play_mp3(self):
286
"""Can the user play mp3s? Start a GStreamer pipeline to check."""
287
mp3pth = os.path.realpath(os.path.join(
288
os.path.split(__file__)[0], "empty.mp3"))
289
uri = "file://%s" % urllib.quote("%s" % mp3pth)
290
self.install_pipeline = gst.parse_launch(
291
'uridecodebin uri=%s ! fakesink' % uri)
292
bus = self.install_pipeline.get_bus()
293
bus.add_signal_watch()
294
bus.connect("message::element", self._got_element_msg)
295
bus.connect("message::eos", self._got_end_of_stream)
296
self.install_pipeline.set_state(gst.STATE_PLAYING)
298
def _got_element_msg(self, bus, msg):
299
"""Handler for element messages from the check-mp3 pipeline.
300
GStreamer throws a "plugin-missing" element message if the
301
user does not have the right codecs to play a file."""
302
plugin_missing = gst.pbutils.is_missing_plugin_message(msg)
304
self.install_pipeline.set_state(gst.STATE_NULL)
305
self.install_mp3_playback()
307
def _got_end_of_stream(self, bus, msg):
308
"""Handler for end of stream from the check-mp3 pipeline.
309
If we reach the end of the stream, mp3 playback is enabled."""
310
self.install_pipeline.set_state(gst.STATE_NULL)
311
if os.environ.has_key("U1INSTALLMP3ANYWAY"):
312
# override the decision not to install the package
313
self.install_mp3_playback()
315
self.add_music_store_widget()
317
def install_mp3_playback(self):
318
"""Use aptdaemon to install the Fluendo mp3 playback codec package."""
319
self.install_box = gtk.Alignment(xscale=0.0, yscale=0.0, xalign=0.5,
321
self.install_vbox = gtk.VBox()
322
self.install_label_head = gtk.Label()
323
self.install_label_head.set_use_markup(True)
324
not_installed = _("MP3 plugins are not installed")
325
self.install_label_head.set_markup('<span weight="bold" size="larger">'
326
'%s</span>' % not_installed)
327
self.install_label_head.set_alignment(0.0, 0.5)
328
self.install_label_body = gtk.Label()
329
self.install_label_body.set_text(_('To listen to your purchased songs'
330
', you need to install MP3 plugins. Click below to install them.'))
331
self.install_label_body.set_alignment(0.0, 0.5)
332
self.install_label_eula = gtk.Label()
333
# EULA text copied from /var/lib/dpkg/info/gstreamer0.10-fluendo-plugins-mp3-partner.templates
334
# The partner package shows the EULA itself on installations, but
335
# aptdaemon installations work like DEBIAN_FRONTEND=noninteractive
336
# so we show the EULA here. (This also avoids a popup window.)
337
# EULA text is not translatable; do not wrap it with gettext!
338
self.install_label_eula.set_markup(
339
"<small>MPEG Layer-3 audio decoding technology notice\n"
340
"MPEG Layer-3 audio decoding technology licensed "
341
"from Fraunhofer IIS and Thomson\n"
342
"This product cannot be installed in product other than Personal "
343
"Computers sold for general purpose usage, and not for set-top "
344
"boxes, embedded PC, PC which are sold and customized for "
345
"mainly audio or multimedia playback and/or registration, "
346
"unless the seller has received a license by Fraunhofer IIS"
347
"and Thomson and pay the relevant royalties to them.</small>")
348
self.install_label_eula.set_alignment(0.0, 0.5)
349
self.install_label_eula.set_size_request(400,200)
350
self.install_label_eula.set_line_wrap(True)
351
self.install_hbtn = gtk.HButtonBox()
352
self.install_hbtn.set_layout(gtk.BUTTONBOX_END)
353
self.install_button = gtk.Button(label=_("Install MP3 plugins"))
354
self.install_button.connect("clicked", self._start_mp3_install)
355
self.install_hbtn.add(self.install_button)
356
self.install_vbox.pack_start(self.install_label_head, expand=False)
357
self.install_vbox.pack_start(self.install_label_body, expand=False,
359
self.install_vbox.pack_start(self.install_hbtn, expand=False)
360
self.install_vbox.pack_start(self.install_label_eula, expand=False,
362
self.install_box.add(self.install_vbox)
363
self.install_box.show_all()
364
self.add(self.install_box)
366
def _start_mp3_install(self, btn):
367
"""Add the 'partner' repository and update the package list from it."""
368
self.ac = aptdaemon.client.AptClient()
370
self.ac.add_repository("deb","http://archive.canonical.com/", "lucid", ["partner"])
371
except dbus.exceptions.DBusException, e:
372
if e.get_dbus_name() == "org.freedesktop.PolicyKit.Error.NotAuthorized":
373
# user cancelled, so exit from here so they can press the button again
375
self.ac.update_cache(reply_handler=self._finish_updating_packages,
376
error_handler=self._on_error)
378
def _finish_updating_packages(self, transaction):
379
"""Now that partner is added, install our mp3 codec package."""
380
self.update_progress = AptProgressBar(transaction)
381
self.update_progress.show()
382
self.install_label_head.set_text("")
383
self.install_label_body.set_text(_("Finding MP3 plugins"))
384
self.install_label_eula.hide()
385
self.install_hbtn.hide()
386
self.install_vbox.pack_start(self.update_progress, expand=False)
387
transaction.run(reply_handler=lambda: True,
388
error_handler=self._on_error)
389
self.ac.install_packages(["gstreamer0.10-fluendo-plugins-mp3-partner"],
390
reply_handler=self._run_transaction,
391
error_handler=self._on_error)
393
def _run_transaction(self, transaction):
394
"""Show progress of aptdaemon package installation."""
395
self.update_progress.hide()
396
self.install_progress = AptProgressBar(transaction)
397
self.install_progress.show()
398
self.install_label_head.set_text("")
399
self.install_label_body.set_text(_("Installing MP3 plugins"))
400
self.install_label_eula.hide()
401
self.install_hbtn.hide()
402
self.install_vbox.pack_start(self.install_progress, expand=False)
403
transaction.run(reply_handler=lambda: True,
404
error_handler=self._on_error)
405
transaction.connect("finished", self._finished)
407
def _finished(self, trans, exit_code):
408
"""Aptdaemon package installation finished; show music store."""
409
if exit_code == 0 or exit_code == 2: # 0: success, 2: already installed
410
self.remove(self.install_box)
411
gst.update_registry()
412
self.add_music_store_widget()
414
self._on_error("Could not find the "
415
"gstreamer0.10-fluendo-plugins-mp3-partner package.")
417
def _on_error(self, error):
418
"""Error handler for aptdaemon."""
420
problem_installing = _("There was a problem installing, sorry")
421
self.install_label_head.set_markup('<span weight="bold" size="larger">'
422
'%s</span>' % problem_installing)
423
self.install_label_body.set_text(_('Check your internet connection and '
425
if getattr(self, "install_progress"):
426
self.install_progress.hide()
427
self.install_hbtn.show()
429
def add_music_store_widget(self):
430
"""Display the music store widget in Rhythmbox."""
431
self.add(self.browser)
433
self.browser.set_no_show_all(True)
434
self.browser.set_property("visible", True)
435
self.browser.connect("preview-mp3", self.re_emit_preview)
436
self.browser.connect("play-library", self.re_emit_playlibrary)
437
self.browser.connect("download-finished", self.re_emit_downloadfinished)
438
self.browser.connect("url-loaded", self.re_emit_urlloaded)
440
def do_impl_can_pause(self):
441
"""Implementation can pause.
442
If we don't handle this, Rhythmbox segfaults."""
443
return True # so we can pause, else we segfault
445
def re_emit_preview(self, widget, url, title):
446
"""Handle the preview-mp3 signal and re-emit it to the rb.Plugin."""
447
self.emit("preview-mp3", url, title)
449
def re_emit_playlibrary(self, widget, path):
450
"""Handle the play-library signal and re-emit it to the rb.Plugin."""
451
self.emit("play-library", path)
453
def re_emit_downloadfinished(self, widget, path):
454
"""Handle the download-finished signal and re-emit it to the rb.Plugin."""
455
self.emit("download-finished", path)
457
def re_emit_urlloaded(self, widget, url):
458
"""Handle the url-loaded signal and re-emit it to the rb.Plugin."""
459
self.emit("url-loaded", url)
461
def do_set_property(self, property, value):
462
"""Allow property settings to handle the plugin call."""
463
if property.name == 'plugin':
464
self.__plugin = value
466
raise AttributeError, 'unknown property %s' % property.name