merged jodal gstreamer branch

This commit is contained in:
Johannes Knutsen 2010-08-14 02:24:24 +02:00
commit b24d9a70d0
19 changed files with 430 additions and 351 deletions

View File

@ -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

View File

@ -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'

View File

@ -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`.

View File

@ -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
<http://github.com/noahwilliamsson/openspotify>`_ for
:mod:`mopidy.backends.libspotify`. *Update:* Seems like openspotify
development has stalled.
- **[PENDING]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_ recipies
for all our dependencies and Mopidy itself to make OS X installation a
breeze. See `Homebrew's issue #1612
<http://github.com/mxcl/homebrew/issues/issue/1612>`_.
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ 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 <http://mxcl.github.com/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

View File

@ -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 <libspotify>`. 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::

View File

@ -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()

View File

@ -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
<http://github.com/noahwilliamsson/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)

View File

@ -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

View File

@ -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

View File

@ -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)

Binary file not shown.

View File

@ -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

View File

@ -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],
)

View File

@ -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':

View File

@ -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':

View File

@ -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')

View File

@ -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):

View File

@ -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',

View File

@ -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()