diff --git a/MANIFEST.in b/MANIFEST.in index cb752f87..38819adb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ -include COPYING pylintrc *.rst *.txt +include LICENSE pylintrc *.rst *.txt +include mopidy/backends/libspotify/spotify_appkey.key recursive-include docs * prune docs/_build recursive-include tests *.py diff --git a/docs/api/settings.rst b/docs/api/settings.rst index 12d2833f..cfc270d6 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -13,7 +13,7 @@ there. A complete ``~/.mopidy/settings.py`` may look like this:: - MPD_SERVER_HOSTNAME = u'0.0.0.0' + MPD_SERVER_HOSTNAME = u'::' SPOTIFY_USERNAME = u'alice' SPOTIFY_PASSWORD = u'mysecret' diff --git a/docs/changes.rst b/docs/changes.rst index 7b154915..c20b2ad1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,6 +23,8 @@ Another great release. - Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained and the Libspotify backend is working much better. - :mod:`mopidy.backends.libspotify` is now the default backend. +- A Spotify application key is now bundled with the source. The + ``SPOTIFY_LIB_APPKEY`` setting is thus removed. - MPD frontend: - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 5544a005..243243ab 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -31,15 +31,13 @@ released when we reach the other goal. Stuff we really want to do, but just not right now ================================================== -- Replace libspotify with `openspotify - `_ for - :mod:`mopidy.backends.libspotify`. *Update:* Seems like openspotify - development has stalled. +- **[PENDING]** Create `Homebrew `_ recipies + for all our dependencies and Mopidy itself to make OS X installation a + breeze. See `Homebrew's issue #1612 + `_. - Create `Debian packages `_ of all our dependencies and Mopidy itself (hosted in our own Debian repo until we get stuff into the various distros) to make Debian/Ubuntu installation a breeze. -- **[WIP]** Create `Homebrew `_ recipies for - all our dependencies and Mopidy itself to make OS X installation a breeze. - Run frontend tests against a real MPD server to ensure we are in sync. - Start working with MPD client maintainers to get rid of weird assumptions like only searching for first two letters and doing the rest of the filtering diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 044f2155..d5e76cce 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -2,12 +2,10 @@ Installation ************ -Mopidy itself is a breeze to install, as it just requires a standard Python -installation and the GStreamer library. The libraries we depend on to connect -to the Spotify service is far more tricky to get working for the time being. -Until installation of these libraries are either well documented by their -developers, or the libraries are packaged for various Linux distributions, we -will supply our own installation guides, as linked to below. +To get a basic version of Mopidy running, you need Python and the GStreamer +library. To use Spotify with Mopidy, you also need :doc:`libspotify and +pyspotify `. Mopidy itself can either be installed from the Python +package index, PyPI, or from git. Install dependencies @@ -102,13 +100,8 @@ username and password into the file, like this:: SPOTIFY_PASSWORD = u'mysecret' Currently :mod:`mopidy.backends.libspotify` is the default -backend. - -If you want to use :mod:`mopidy.backends.libspotify`, copy the Spotify -application key to ``~/.mopidy/spotify_appkey.key``, and add the following -setting:: - - BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) +backend. Before you can use :mod:`mopidy.backends.libspotify`, you must copy +the Spotify application key to ``~/.mopidy/spotify_appkey.key``. If you want to use :mod:`mopidy.backends.local`, add the following setting:: diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 7c62033b..c92ce1ed 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -22,7 +22,10 @@ def main(): get_or_create_folder('~/.mopidy/') core_queue = multiprocessing.Queue() get_class(settings.SERVER)(core_queue).start() - core = CoreProcess(core_queue) + output_class = get_class(settings.OUTPUT) + backend_class = get_class(settings.BACKENDS[0]) + frontend_class = get_class(settings.FRONTEND) + core = CoreProcess(core_queue, output_class, backend_class, frontend_class) core.start() asyncore.loop() diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 04b5af66..7a971bc5 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -1,19 +1,7 @@ -import datetime as dt import logging -import os -import multiprocessing -import threading -from spotify import Link, SpotifyError -from spotify.manager import SpotifySessionManager -from spotify.alsahelper import AlsaController - -from mopidy import get_version, settings -from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, - BaseLibraryController, BasePlaybackController, - BaseStoredPlaylistsController) -from mopidy.models import Artist, Album, Track, Playlist -from mopidy.process import pickle_connection +from mopidy import settings +from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController logger = logging.getLogger('mopidy.backends.libspotify') @@ -28,15 +16,19 @@ class LibspotifyBackend(BaseBackend): for libspotify. It got no documentation, but multiple examples are available. Like libspotify, pyspotify's calls are mostly asynchronous. - This backend should also work with `openspotify - `_, but we haven't tested - that yet. - **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify """ + # Imports inside methods are to prevent loading of __init__.py to fail on + # missing spotify dependencies. + def __init__(self, *args, **kwargs): + from .library import LibspotifyLibraryController + from .playback import LibspotifyPlaybackController + from .stored_playlists import LibspotifyStoredPlaylistsController + super(LibspotifyBackend, self).__init__(*args, **kwargs) + self.current_playlist = BaseCurrentPlaylistController(backend=self) self.library = LibspotifyLibraryController(backend=self) self.playback = LibspotifyPlaybackController(backend=self) @@ -46,6 +38,8 @@ class LibspotifyBackend(BaseBackend): self.spotify = self._connect() def _connect(self): + from .session_manager import LibspotifySessionManager + logger.info(u'Connecting to Spotify') spotify = LibspotifySessionManager( settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, @@ -53,243 +47,3 @@ class LibspotifyBackend(BaseBackend): output_queue=self.output_queue) spotify.start() return spotify - - -class LibspotifyLibraryController(BaseLibraryController): - def find_exact(self, **query): - return self.search(**query) - - def lookup(self, uri): - spotify_track = Link.from_string(uri).as_track() - return LibspotifyTranslator.to_mopidy_track(spotify_track) - - def refresh(self, uri=None): - pass # TODO - - def search(self, **query): - spotify_query = [] - for (field, values) in query.iteritems(): - if not hasattr(values, '__iter__'): - values = [values] - for value in values: - if field == u'track': - field = u'title' - if field == u'any': - spotify_query.append(value) - else: - spotify_query.append(u'%s:"%s"' % (field, value)) - spotify_query = u' '.join(spotify_query) - logger.debug(u'Spotify search query: %s' % spotify_query) - my_end, other_end = multiprocessing.Pipe() - self.backend.spotify.search(spotify_query.encode(ENCODING), other_end) - my_end.poll(None) - playlist = my_end.recv() - return playlist - - -class LibspotifyPlaybackController(BasePlaybackController): - def _set_output_state(self, state_name): - logger.debug(u'Setting output state to %s ...', state_name) - (my_end, other_end) = multiprocessing.Pipe() - self.backend.output_queue.put({ - 'command': 'set_state', - 'state': state_name, - 'reply_to': pickle_connection(other_end), - }) - my_end.poll(None) - return my_end.recv() - - def _pause(self): - return self._set_output_state('PAUSED') - - def _play(self, track): - self._set_output_state('READY') - if self.state == self.PLAYING: - self.stop() - if track.uri is None: - return False - try: - self.backend.spotify.session.load( - Link.from_string(track.uri).as_track()) - self.backend.spotify.session.play(1) - self._set_output_state('PLAYING') - return True - except SpotifyError as e: - logger.warning('Play %s failed: %s', track.uri, e) - return False - - def _resume(self): - return self._set_output_state('PLAYING') - - def _seek(self, time_position): - self._set_output_state('READY') - result = self.backend.spotify.session.seek(time_position) - self._set_output_state('PLAYING') - - def _stop(self): - result = self._set_output_state('READY') - self.backend.spotify.session.play(0) - return result - - -class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController): - def create(self, name): - pass # TODO - - def delete(self, playlist): - pass # TODO - - def lookup(self, uri): - pass # TODO - - def refresh(self): - pass # TODO - - def rename(self, playlist, new_name): - pass # TODO - - def save(self, playlist): - pass # TODO - - -class LibspotifyTranslator(object): - @classmethod - def to_mopidy_artist(cls, spotify_artist): - if not spotify_artist.is_loaded(): - return Artist(name=u'[loading...]') - return Artist( - uri=str(Link.from_artist(spotify_artist)), - name=spotify_artist.name().decode(ENCODING), - ) - - @classmethod - def to_mopidy_album(cls, spotify_album): - if not spotify_album.is_loaded(): - return Album(name=u'[loading...]') - # TODO pyspotify got much more data on albums than this - return Album(name=spotify_album.name().decode(ENCODING)) - - @classmethod - def to_mopidy_track(cls, spotify_track): - if not spotify_track.is_loaded(): - return Track(name=u'[loading...]') - uri = str(Link.from_track(spotify_track, 0)) - if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR: - date = dt.date(spotify_track.album().year(), 1, 1) - else: - date = None - return Track( - uri=uri, - name=spotify_track.name().decode(ENCODING), - artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()], - album=cls.to_mopidy_album(spotify_track.album()), - track_no=spotify_track.index(), - date=date, - length=spotify_track.duration(), - bitrate=320, - ) - - @classmethod - def to_mopidy_playlist(cls, spotify_playlist): - if not spotify_playlist.is_loaded(): - return Playlist(name=u'[loading...]') - return Playlist( - uri=str(Link.from_playlist(spotify_playlist)), - name=spotify_playlist.name().decode(ENCODING), - tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], - ) - -class LibspotifySessionManager(SpotifySessionManager, threading.Thread): - cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) - settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) - appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY) - user_agent = 'Mopidy %s' % get_version() - - def __init__(self, username, password, core_queue, output_queue): - SpotifySessionManager.__init__(self, username, password) - threading.Thread.__init__(self) - self.core_queue = core_queue - self.output_queue = output_queue - self.connected = threading.Event() - self.session = None - - def run(self): - self.connect() - - def logged_in(self, session, error): - """Callback used by pyspotify""" - logger.info('Logged in') - self.session = session - self.connected.set() - - def logged_out(self, session): - """Callback used by pyspotify""" - logger.info('Logged out') - - def metadata_updated(self, session): - """Callback used by pyspotify""" - logger.debug('Metadata updated, refreshing stored playlists') - playlists = [] - for spotify_playlist in session.playlist_container(): - playlists.append( - LibspotifyTranslator.to_mopidy_playlist(spotify_playlist)) - self.core_queue.put({ - 'command': 'set_stored_playlists', - 'playlists': playlists, - }) - - def connection_error(self, session, error): - """Callback used by pyspotify""" - logger.error('Connection error: %s', error) - - def message_to_user(self, session, message): - """Callback used by pyspotify""" - logger.info(message) - - def notify_main_thread(self, session): - """Callback used by pyspotify""" - logger.debug('Notify main thread') - - def music_delivery(self, session, frames, frame_size, num_frames, - sample_type, sample_rate, channels): - """Callback used by pyspotify""" - # TODO Base caps_string on arguments - caps_string = """ - audio/x-raw-int, - endianness=(int)1234, - channels=(int)2, - width=(int)16, - depth=(int)16, - signed=True, - rate=(int)44100 - """ - self.output_queue.put({ - 'command': 'deliver_data', - 'caps': caps_string, - 'data': bytes(frames), - }) - - def play_token_lost(self, session): - """Callback used by pyspotify""" - logger.debug('Play token lost') - self.core_queue.put({'command': 'stop_playback'}) - - def log_message(self, session, data): - """Callback used by pyspotify""" - logger.debug(data) - - def end_of_track(self, session): - """Callback used by pyspotify""" - logger.debug('End of data stream.') - self.output_queue.put({'command': 'end_of_data_stream'}) - - def search(self, query, connection): - """Search method used by Mopidy backend""" - def callback(results, userdata): - # TODO Include results from results.albums(), etc. too - playlist = Playlist(tracks=[ - LibspotifyTranslator.to_mopidy_track(t) - for t in results.tracks()]) - connection.send(playlist) - self.connected.wait() - self.session.search(query, callback) diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/libspotify/library.py new file mode 100644 index 00000000..c2b70dca --- /dev/null +++ b/mopidy/backends/libspotify/library.py @@ -0,0 +1,41 @@ +import logging +import multiprocessing + +from spotify import Link + +from mopidy.backends.base import BaseLibraryController +from mopidy.backends.libspotify import ENCODING +from mopidy.backends.libspotify.translator import LibspotifyTranslator + +logger = logging.getLogger('mopidy.backends.libspotify.library') + +class LibspotifyLibraryController(BaseLibraryController): + def find_exact(self, **query): + return self.search(**query) + + def lookup(self, uri): + spotify_track = Link.from_string(uri).as_track() + return LibspotifyTranslator.to_mopidy_track(spotify_track) + + def refresh(self, uri=None): + pass # TODO + + def search(self, **query): + spotify_query = [] + for (field, values) in query.iteritems(): + if not hasattr(values, '__iter__'): + values = [values] + for value in values: + if field == u'track': + field = u'title' + if field == u'any': + spotify_query.append(value) + else: + spotify_query.append(u'%s:"%s"' % (field, value)) + spotify_query = u' '.join(spotify_query) + logger.debug(u'Spotify search query: %s' % spotify_query) + my_end, other_end = multiprocessing.Pipe() + self.backend.spotify.search(spotify_query.encode(ENCODING), other_end) + my_end.poll(None) + playlist = my_end.recv() + return playlist diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py new file mode 100644 index 00000000..3ba91d5f --- /dev/null +++ b/mopidy/backends/libspotify/playback.py @@ -0,0 +1,51 @@ +import logging +import multiprocessing + +from spotify import Link, SpotifyError + +from mopidy.backends.base import BasePlaybackController +from mopidy.process import pickle_connection + +logger = logging.getLogger('mopidy.backends.libspotify.playback') + +class LibspotifyPlaybackController(BasePlaybackController): + def _set_output_state(self, state_name): + logger.debug(u'Setting output state to %s ...', state_name) + (my_end, other_end) = multiprocessing.Pipe() + self.backend.output_queue.put({ + 'command': 'set_state', + 'state': state_name, + 'reply_to': pickle_connection(other_end), + }) + my_end.poll(None) + return my_end.recv() + + def _pause(self): + return self._set_output_state('PAUSED') + + def _play(self, track): + self._set_output_state('READY') + if self.state == self.PLAYING: + self.stop() + if track.uri is None: + return False + try: + self.backend.spotify.session.load( + Link.from_string(track.uri).as_track()) + self.backend.spotify.session.play(1) + self._set_output_state('PLAYING') + return True + except SpotifyError as e: + logger.warning('Play %s failed: %s', track.uri, e) + return False + + def _resume(self): + return self._set_output_state('PLAYING') + + def _seek(self, time_position): + pass # TODO + + def _stop(self): + result = self._set_output_state('READY') + self.backend.spotify.session.play(0) + return result diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py new file mode 100644 index 00000000..707423aa --- /dev/null +++ b/mopidy/backends/libspotify/session_manager.py @@ -0,0 +1,106 @@ +import logging +import os +import threading + +from spotify.manager import SpotifySessionManager + +from mopidy import get_version, settings +from mopidy.models import Playlist +from mopidy.backends.libspotify.translator import LibspotifyTranslator + +logger = logging.getLogger('mopidy.backends.libspotify.session_manager') + +class LibspotifySessionManager(SpotifySessionManager, threading.Thread): + cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) + settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) + appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') + user_agent = 'Mopidy %s' % get_version() + + def __init__(self, username, password, core_queue, output_queue): + SpotifySessionManager.__init__(self, username, password) + threading.Thread.__init__(self) + self.core_queue = core_queue + self.output_queue = output_queue + self.connected = threading.Event() + self.session = None + + def run(self): + self.connect() + + def logged_in(self, session, error): + """Callback used by pyspotify""" + logger.info('Logged in') + self.session = session + self.connected.set() + + def logged_out(self, session): + """Callback used by pyspotify""" + logger.info('Logged out') + + def metadata_updated(self, session): + """Callback used by pyspotify""" + logger.debug('Metadata updated, refreshing stored playlists') + playlists = [] + for spotify_playlist in session.playlist_container(): + playlists.append( + LibspotifyTranslator.to_mopidy_playlist(spotify_playlist)) + self.core_queue.put({ + 'command': 'set_stored_playlists', + 'playlists': playlists, + }) + + def connection_error(self, session, error): + """Callback used by pyspotify""" + logger.error('Connection error: %s', error) + + def message_to_user(self, session, message): + """Callback used by pyspotify""" + logger.info(message.strip()) + + def notify_main_thread(self, session): + """Callback used by pyspotify""" + logger.debug('Notify main thread') + + def music_delivery(self, session, frames, frame_size, num_frames, + sample_type, sample_rate, channels): + """Callback used by pyspotify""" + # TODO Base caps_string on arguments + caps_string = """ + audio/x-raw-int, + endianness=(int)1234, + channels=(int)2, + width=(int)16, + depth=(int)16, + signed=True, + rate=(int)44100 + """ + self.output_queue.put({ + 'command': 'deliver_data', + 'caps': caps_string, + 'data': bytes(frames), + }) + + def play_token_lost(self, session): + """Callback used by pyspotify""" + logger.debug('Play token lost') + self.core_queue.put({'command': 'stop_playback'}) + + def log_message(self, session, data): + """Callback used by pyspotify""" + logger.debug(data.strip()) + + def end_of_track(self, session): + """Callback used by pyspotify""" + logger.debug('End of data stream.') + self.output_queue.put({'command': 'end_of_data_stream'}) + + def search(self, query, connection): + """Search method used by Mopidy backend""" + def callback(results, userdata): + # TODO Include results from results.albums(), etc. too + playlist = Playlist(tracks=[ + LibspotifyTranslator.to_mopidy_track(t) + for t in results.tracks()]) + connection.send(playlist) + self.connected.wait() + self.session.search(query, callback) diff --git a/mopidy/backends/libspotify/spotify_appkey.key b/mopidy/backends/libspotify/spotify_appkey.key new file mode 100644 index 00000000..1f840b96 Binary files /dev/null and b/mopidy/backends/libspotify/spotify_appkey.key differ diff --git a/mopidy/backends/libspotify/stored_playlists.py b/mopidy/backends/libspotify/stored_playlists.py new file mode 100644 index 00000000..3339578c --- /dev/null +++ b/mopidy/backends/libspotify/stored_playlists.py @@ -0,0 +1,20 @@ +from mopidy.backends.base import BaseStoredPlaylistsController + +class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController): + def create(self, name): + pass # TODO + + def delete(self, playlist): + pass # TODO + + def lookup(self, uri): + pass # TODO + + def refresh(self): + pass # TODO + + def rename(self, playlist, new_name): + pass # TODO + + def save(self, playlist): + pass # TODO diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/libspotify/translator.py new file mode 100644 index 00000000..3a39aad5 --- /dev/null +++ b/mopidy/backends/libspotify/translator.py @@ -0,0 +1,53 @@ +import datetime as dt + +from spotify import Link + +from mopidy.models import Artist, Album, Track, Playlist +from mopidy.backends.libspotify import ENCODING + +class LibspotifyTranslator(object): + @classmethod + def to_mopidy_artist(cls, spotify_artist): + if not spotify_artist.is_loaded(): + return Artist(name=u'[loading...]') + return Artist( + uri=str(Link.from_artist(spotify_artist)), + name=spotify_artist.name().decode(ENCODING), + ) + + @classmethod + def to_mopidy_album(cls, spotify_album): + if not spotify_album.is_loaded(): + return Album(name=u'[loading...]') + # TODO pyspotify got much more data on albums than this + return Album(name=spotify_album.name().decode(ENCODING)) + + @classmethod + def to_mopidy_track(cls, spotify_track): + if not spotify_track.is_loaded(): + return Track(name=u'[loading...]') + uri = str(Link.from_track(spotify_track, 0)) + if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR: + date = dt.date(spotify_track.album().year(), 1, 1) + else: + date = None + return Track( + uri=uri, + name=spotify_track.name().decode(ENCODING), + artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()], + album=cls.to_mopidy_album(spotify_track.album()), + track_no=spotify_track.index(), + date=date, + length=spotify_track.duration(), + bitrate=320, + ) + + @classmethod + def to_mopidy_playlist(cls, spotify_playlist): + if not spotify_playlist.is_loaded(): + return Playlist(name=u'[loading...]') + return Playlist( + uri=str(Link.from_playlist(spotify_playlist)), + name=spotify_playlist.name().decode(ENCODING), + tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], + ) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 65b65504..b81fbd0f 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -39,6 +39,8 @@ class GStreamerProcess(BaseProcess): http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html. """ + pipeline_description = 'appsrc name=data ! volume name=volume ! autoaudiosink name=sink' + def __init__(self, core_queue, output_queue): super(GStreamerProcess, self).__init__() self.core_queue = core_queue @@ -65,8 +67,10 @@ class GStreamerProcess(BaseProcess): messages_thread.daemon = True messages_thread.start() - # A pipeline consisting of many elements - self.gst_pipeline = gst.Pipeline("pipeline") + self.gst_pipeline = gst.parse_launch(self.pipeline_description) + self.gst_data_src = self.gst_pipeline.get_by_name('data') + self.gst_volume = self.gst_pipeline.get_by_name('volume') + self.gst_sink = self.gst_pipeline.get_by_name('sink') # Setup bus and message processor self.gst_bus = self.gst_pipeline.get_bus() @@ -74,42 +78,6 @@ class GStreamerProcess(BaseProcess): self.gst_bus_id = self.gst_bus.connect('message', self.process_gst_message) - # Bin for playing audio URIs - #self.gst_uri_src = gst.element_factory_make('uridecodebin', 'uri_src') - #self.gst_pipeline.add(self.gst_uri_src) - - # Bin for playing audio data - self.gst_data_src = gst.element_factory_make('appsrc', 'data_src') - self.gst_pipeline.add(self.gst_data_src) - - # Volume filter - self.gst_volume = gst.element_factory_make('volume', 'volume') - self.gst_pipeline.add(self.gst_volume) - - # Audio output sink - self.gst_sink = gst.element_factory_make('autoaudiosink', 'sink') - self.gst_pipeline.add(self.gst_sink) - - # Add callback that will link uri_src output with volume filter input - # when the output pad is ready. - # See http://stackoverflow.com/questions/2993777 for details. - def on_new_decoded_pad(dbin, pad, is_last): - uri_src = pad.get_parent() - pipeline = uri_src.get_parent() - volume = pipeline.get_by_name('volume') - uri_src.link(volume) - logger.debug("Linked uri_src's new decoded pad to volume filter") - # FIXME uridecodebin got no new-decoded-pad signal, but it's - # subcomponent decodebin2 got that signal. Fixing this is postponed - # till after data_src is up and running perfectly - #self.gst_uri_src.connect('new-decoded-pad', on_new_decoded_pad) - - # Link data source output with volume filter input - self.gst_data_src.link(self.gst_volume) - - # Link volume filter output to audio sink input - self.gst_volume.link(self.gst_sink) - def process_mopidy_message(self, message): """Process messages from the rest of Mopidy.""" if message['command'] == 'play_uri': diff --git a/mopidy/process.py b/mopidy/process.py index 9759c4e6..b1cdc8af 100644 --- a/mopidy/process.py +++ b/mopidy/process.py @@ -28,16 +28,23 @@ class BaseProcess(multiprocessing.Process): except SettingsError as e: logger.error(e.message) sys.exit(1) + except ImportError as e: + logger.error(e) + sys.exit(1) def run_inside_try(self): raise NotImplementedError class CoreProcess(BaseProcess): - def __init__(self, core_queue): + def __init__(self, core_queue, output_class, backend_class, + frontend_class): super(CoreProcess, self).__init__() self.core_queue = core_queue self.output_queue = None + self.output_class = output_class + self.backend_class = backend_class + self.frontend_class = frontend_class self.output = None self.backend = None self.frontend = None @@ -50,11 +57,9 @@ class CoreProcess(BaseProcess): def setup(self): self.output_queue = multiprocessing.Queue() - self.output = get_class(settings.OUTPUT)(self.core_queue, - self.output_queue) - self.backend = get_class(settings.BACKENDS[0])(self.core_queue, - self.output_queue) - self.frontend = get_class(settings.FRONTEND)(self.backend) + self.output = self.output_class(self.core_queue, self.output_queue) + self.backend = self.backend_class(self.core_queue, self.output_queue) + self.frontend = self.frontend_class(self.backend) def process_message(self, message): if message.get('to') == 'output': diff --git a/mopidy/settings.py b/mopidy/settings.py index d4321685..b17af913 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -3,16 +3,19 @@ Available settings and their default values. .. warning:: - Do *not* change settings in ``mopidy/settings.py``. Instead, add a file - called ``~/.mopidy/settings.py`` and redefine settings there. + Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a + file called ``~/.mopidy/settings.py`` and redefine settings there. """ +# Absolute import needed to import ~/.mopidy/settings.py and not ourselves from __future__ import absolute_import import os import sys #: List of playback backends to use. See :mod:`mopidy.backends` for all -#: available backends. Default:: +#: available backends. +#: +#: Default:: #: #: BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) #: @@ -28,32 +31,51 @@ BACKENDS = ( CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \ ' [%(process)d:%(threadName)s] %(name)s\n %(message)s' -#: The log format used for dump logs. Default:: +#: The log format used for dump logs. +#: +#: Default:: #: #: DUMP_LOG_FILENAME = CONSOLE_LOG_FORMAT DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT -#: The file to dump debug log data to. Default:: +#: The file to dump debug log data to when Mopidy is run with the +#: :option:`--dump` option. +#: +#: Default:: #: #: DUMP_LOG_FILENAME = u'dump.log' DUMP_LOG_FILENAME = u'dump.log' -#: Protocol frontend to use. Default:: +#: Protocol frontend to use. +#: +#: Default:: #: #: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' -#: Path to folder with local music. Default:: +#: Path to folder with local music. +#: +#: Used by :mod:`mopidy.backends.local`. +#: +#: Default:: #: #: LOCAL_MUSIC_FOLDER = u'~/music' LOCAL_MUSIC_FOLDER = u'~/music' -#: Path to playlist folder with m3u files for local music. Default:: +#: Path to playlist folder with m3u files for local music. +#: +#: Used by :mod:`mopidy.backends.local`. +#: +#: Default:: #: #: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists' LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists' -#: Path to tag cache for local music. Default:: +#: Path to tag cache for local music. +#: +#: Used by :mod:`mopidy.backends.local`. +#: +#: Default:: #: #: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache' LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache' @@ -86,6 +108,7 @@ MIXER_ALSA_CONTROL = False #: External mixers only. Which port the mixer is connected to. #: #: This must point to the device port like ``/dev/ttyUSB0``. +#: #: Default: :class:`None` MIXER_EXT_PORT = None @@ -104,17 +127,23 @@ MIXER_EXT_SPEAKERS_A = None #: Default: :class:`None`. MIXER_EXT_SPEAKERS_B = None -#: Audio output handler to use. Default:: +#: Audio output handler to use. +#: +#: Default:: #: #: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' -#: Server to use. Default:: +#: Server to use. +#: +#: Default:: #: #: SERVER = u'mopidy.frontends.mpd.server.MpdServer' SERVER = u'mopidy.frontends.mpd.server.MpdServer' -#: Which address Mopidy should bind to. Examples: +#: Which address Mopidy's MPD server should bind to. +#: +#:Examples: #: #: ``127.0.0.1`` #: Listens only on the IPv4 loopback interface. Default. @@ -126,21 +155,26 @@ SERVER = u'mopidy.frontends.mpd.server.MpdServer' #: Listens on all interfaces, both IPv4 and IPv6. MPD_SERVER_HOSTNAME = u'127.0.0.1' -#: Which TCP port Mopidy should listen to. Default: 6600 +#: Which TCP port Mopidy's MPD server should listen to. +#: +#: Default: 6600 MPD_SERVER_PORT = 6600 -#: Your Spotify Premium username. Used by all Spotify backends. +#: Path to the libspotify cache. +#: +#: Used by :mod:`mopidy.backends.libspotify`. +SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache' + +#: Your Spotify Premium username. +#: +#: Used by :mod:`mopidy.backends.libspotify`. SPOTIFY_USERNAME = u'' -#: Your Spotify Premium password. Used by all Spotify backends. +#: Your Spotify Premium password. +#: +#: Used by :mod:`mopidy.backends.libspotify`. SPOTIFY_PASSWORD = u'' -#: Path to your libspotify application key. Used by LibspotifyBackend. -SPOTIFY_LIB_APPKEY = u'~/.mopidy/spotify_appkey.key' - -#: Path to the libspotify cache. Used by LibspotifyBackend. -SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache' - # Import user specific settings dotdir = os.path.expanduser(u'~/.mopidy/') settings_file = os.path.join(dotdir, u'settings.py') diff --git a/mopidy/utils.py b/mopidy/utils.py index ff032b4e..bdc0b632 100644 --- a/mopidy/utils.py +++ b/mopidy/utils.py @@ -24,8 +24,11 @@ def get_class(name): module_name = name[:name.rindex('.')] class_name = name[name.rindex('.') + 1:] logger.debug('Loading: %s', name) - module = import_module(module_name) - class_object = getattr(module, class_name) + try: + module = import_module(module_name) + class_object = getattr(module, class_name) + except (ImportError, AttributeError): + raise ImportError("Couldn't load: %s" % name) return class_object def get_or_create_folder(folder): diff --git a/setup.py b/setup.py index bbf300f7..fabc8353 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,34 @@ +""" +Most of this file is taken from the Django project, which is BSD licensed. +""" + from distutils.core import setup +from distutils.command.install_data import install_data from distutils.command.install import INSTALL_SCHEMES import os +import sys from mopidy import get_version +class osx_install_data(install_data): + # On MacOS, the platform-specific lib dir is + # /System/Library/Framework/Python/.../ which is wrong. Python 2.5 supplied + # with MacOS 10.5 has an Apple-specific fix for this in + # distutils.command.install_data#306. It fixes install_lib but not + # install_data, which is why we roll our own install_data class. + + def finalize_options(self): + # By the time finalize_options is called, install.install_lib is set to + # the fixed directory, so we set the installdir to install_lib. The + # install_data class uses ('install_data', 'install_dir') instead. + self.set_undefined_options('install', ('install_lib', 'install_dir')) + install_data.finalize_options(self) + +if sys.platform == "darwin": + cmdclasses = {'install_data': osx_install_data} +else: + cmdclasses = {'install_data': install_data} + def fullsplit(path, result=None): """ Split a pathname into components (the opposite of os.path.join) in a @@ -20,7 +45,8 @@ def fullsplit(path, result=None): # Tell distutils to put the data_files in platform-specific installation # locations. See here for an explanation: -# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb +# http://groups.google.com/group/comp.lang.python/browse_thread/ +# thread/35ec7b2fed36eaec/2105ee4d9e8042cb for scheme in INSTALL_SCHEMES.values(): scheme['data'] = scheme['purelib'] @@ -49,17 +75,19 @@ setup( author='Stein Magnus Jodal', author_email='stein.magnus@jodal.no', packages=packages, + package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']}, + cmdclass=cmdclasses, data_files=data_files, scripts=['bin/mopidy'], url='http://www.mopidy.com/', - license='GPLv2', + license='Apache License, Version 2.0', description='MPD server with Spotify support', long_description=open('README.rst').read(), classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: End Users/Desktop', - 'License :: OSI Approved :: GNU General Public License (GPL)', + 'License :: OSI Approved :: Apache Software License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 2.6', diff --git a/tests/utils_test.py b/tests/utils_test.py index d5c98d86..ca44de45 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -11,6 +11,25 @@ from mopidy.models import Track, Artist, Album from tests import SkipTest, data_folder +class GetClassTest(unittest.TestCase): + def test_loading_module_that_does_not_exist(self): + test = lambda: get_class('foo.bar.Baz') + self.assertRaises(ImportError, test) + + def test_loading_class_that_does_not_exist(self): + test = lambda: get_class('unittest.FooBarBaz') + self.assertRaises(ImportError, test) + + def test_import_error_message_contains_complete_class_path(self): + try: + get_class('foo.bar.Baz') + except ImportError as e: + self.assert_('foo.bar.Baz' in str(e)) + + def test_loading_existing_class(self): + cls = get_class('unittest.TestCase') + self.assertEqual(cls.__name__, 'TestCase') + class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): self.parent = tempfile.mkdtemp()