merged jodal gstreamer branch
This commit is contained in:
commit
b24d9a70d0
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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::
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
41
mopidy/backends/libspotify/library.py
Normal file
41
mopidy/backends/libspotify/library.py
Normal 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
|
||||
51
mopidy/backends/libspotify/playback.py
Normal file
51
mopidy/backends/libspotify/playback.py
Normal 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
|
||||
106
mopidy/backends/libspotify/session_manager.py
Normal file
106
mopidy/backends/libspotify/session_manager.py
Normal 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)
|
||||
BIN
mopidy/backends/libspotify/spotify_appkey.key
Normal file
BIN
mopidy/backends/libspotify/spotify_appkey.key
Normal file
Binary file not shown.
20
mopidy/backends/libspotify/stored_playlists.py
Normal file
20
mopidy/backends/libspotify/stored_playlists.py
Normal 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
|
||||
53
mopidy/backends/libspotify/translator.py
Normal file
53
mopidy/backends/libspotify/translator.py
Normal 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],
|
||||
)
|
||||
@ -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':
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
36
setup.py
36
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',
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user