Merge branch 'develop' into feature/mpris-frontend
This commit is contained in:
commit
efb3cb1102
@ -1,5 +1,10 @@
|
||||
include LICENSE pylintrc *.rst *.ini data/mopidy.desktop
|
||||
include *.ini
|
||||
include *.rst
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include data/mopidy.desktop
|
||||
include mopidy/backends/spotify/spotify_appkey.key
|
||||
include pylintrc
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
||||
recursive-include requirements *
|
||||
|
||||
37
docs/_static/thread_communication.txt
vendored
37
docs/_static/thread_communication.txt
vendored
@ -1,37 +0,0 @@
|
||||
Script for use with www.websequencediagrams.com
|
||||
===============================================
|
||||
|
||||
Main -> Core: create
|
||||
activate Core
|
||||
note over Core: create NadMixer
|
||||
Core -> NadTalker: create
|
||||
activate NadTalker
|
||||
note over NadTalker: calibrate device
|
||||
note over Core: create DespotifyBackend
|
||||
Core -> despotify: connect to Spotify
|
||||
activate despotify
|
||||
note over Core: create MpdFrontend
|
||||
Main -> Server: create
|
||||
activate Server
|
||||
note over Server: open port
|
||||
Client -> Server: connect
|
||||
note over Server: open session
|
||||
Client -> Server: play 1
|
||||
Server -> Core: play 1
|
||||
Core -> despotify: play first track
|
||||
Client -> Server: setvol 50
|
||||
Server -> Core: setvol 50
|
||||
Core -> NadTalker: volume = 50
|
||||
Client -> Server: status
|
||||
Server -> Core: status
|
||||
Core -> NadTalker: volume?
|
||||
NadTalker -> Core: volume = 50
|
||||
Core -> Server: status response
|
||||
Server -> Client: status response
|
||||
despotify -> Core: end of track callback
|
||||
Core -> despotify: play second track
|
||||
Client -> Server: stop
|
||||
Server -> Core: stop
|
||||
Core -> despotify: stop
|
||||
Client -> Server: disconnect
|
||||
note over Server: close session
|
||||
@ -5,12 +5,20 @@ Changes
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
|
||||
|
||||
v0.5.0 (in development)
|
||||
v0.6.0 (in development)
|
||||
=======================
|
||||
|
||||
**Changes**
|
||||
|
||||
- None yet
|
||||
|
||||
|
||||
v0.5.0 (2011-06-15)
|
||||
===================
|
||||
|
||||
Since last time we've added support for audio streaming to SHOUTcast servers
|
||||
and fixed the longstanding playlist loading issue in the Spotify backend. As
|
||||
always the release has a bunch of bug fixes.
|
||||
always the release has a bunch of bug fixes and minor improvements.
|
||||
|
||||
Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
*Important changes* below.
|
||||
@ -18,7 +26,7 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
**Important changes**
|
||||
|
||||
- If you use the Spotify backend, you *must* upgrade to libspotify 0.0.8 and
|
||||
pyspotify 1.2. If you install from APT, libspotify and pyspotify will
|
||||
pyspotify 1.3. If you install from APT, libspotify and pyspotify will
|
||||
automatically be upgraded. If you are not installing from APT, follow the
|
||||
instructions at :doc:`/installation/libspotify/`.
|
||||
|
||||
@ -45,8 +53,9 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
workaround of searching and reconnecting to make the playlists appear are
|
||||
no longer necessary. (Fixes: :issue:`59`)
|
||||
|
||||
- Replace not decodable characters returned from Spotify instead of throwing
|
||||
an exception, as we won't try to figure out the encoding of non-UTF-8-data.
|
||||
- Track's that are no longer available in Spotify's archives are now
|
||||
"autolinked" to corresponding tracks in other albums, just like the
|
||||
official Spotify clients do. (Fixes: :issue:`34`)
|
||||
|
||||
- MPD frontend:
|
||||
|
||||
@ -73,6 +82,9 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
- Added :option:`--interactive` for reading missing local settings from
|
||||
``stdin``. (Fixes: :issue:`96`)
|
||||
|
||||
- Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``,
|
||||
which initiates the same shutdown procedure as CTRL+C does.
|
||||
|
||||
- Tag cache generator:
|
||||
|
||||
- Made it possible to abort :command:`mopidy-scan` with CTRL+C.
|
||||
|
||||
@ -5,7 +5,7 @@ if not (2, 6) <= sys.version_info < (3,):
|
||||
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
VERSION = (0, 5, 0)
|
||||
VERSION = (0, 6, 0)
|
||||
|
||||
def get_version():
|
||||
try:
|
||||
|
||||
@ -33,7 +33,7 @@ class SpotifyBackend(ThreadingActor, Backend):
|
||||
**Dependencies:**
|
||||
|
||||
- libspotify == 0.0.8 (libspotify8 package from apt.mopidy.com)
|
||||
- pyspotify == 1.2 (python-spotify package from apt.mopidy.com)
|
||||
- pyspotify == 1.3 (python-spotify package from apt.mopidy.com)
|
||||
|
||||
**Settings:**
|
||||
|
||||
@ -72,19 +72,22 @@ class SpotifyBackend(ThreadingActor, Backend):
|
||||
self.gstreamer = None
|
||||
self.spotify = None
|
||||
|
||||
# Fail early if settings are not present
|
||||
self.username = settings.SPOTIFY_USERNAME
|
||||
self.password = settings.SPOTIFY_PASSWORD
|
||||
|
||||
def on_start(self):
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
|
||||
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
||||
self.spotify = self._connect()
|
||||
|
||||
def _connect(self):
|
||||
from .session_manager import SpotifySessionManager
|
||||
|
||||
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
||||
logger.debug(u'Connecting to Spotify')
|
||||
spotify = SpotifySessionManager(
|
||||
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD)
|
||||
spotify = SpotifySessionManager(self.username, self.password)
|
||||
spotify.start()
|
||||
return spotify
|
||||
|
||||
@ -1,16 +1,46 @@
|
||||
import logging
|
||||
|
||||
from spotify.manager import SpotifyContainerManager as PyspotifyContainerManager
|
||||
from spotify.manager import SpotifyContainerManager as \
|
||||
PyspotifyContainerManager
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.container_manager')
|
||||
|
||||
class SpotifyContainerManager(PyspotifyContainerManager):
|
||||
|
||||
def __init__(self, session_manager):
|
||||
PyspotifyContainerManager.__init__(self)
|
||||
self.session_manager = session_manager
|
||||
|
||||
def container_loaded(self, container, userdata):
|
||||
"""Callback used by pyspotify."""
|
||||
logger.debug(u'Container loaded')
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: playlist container loaded')
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
playlist_container = self.session_manager.session.playlist_container()
|
||||
for playlist in playlist_container:
|
||||
self.session_manager.playlist_manager.watch(playlist)
|
||||
logger.debug(u'Watching %d playlist(s) for changes',
|
||||
len(playlist_container))
|
||||
|
||||
def playlist_added(self, container, playlist, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: playlist added at position %d',
|
||||
position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
|
||||
def playlist_moved(self, container, playlist, old_position, new_position,
|
||||
userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: playlist "%s" moved from position %d to %d',
|
||||
playlist.name(), old_position, new_position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
|
||||
def playlist_removed(self, container, playlist, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: playlist "%s" removed from position %d',
|
||||
playlist.name(), position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
|
||||
93
mopidy/backends/spotify/playlist_manager.py
Normal file
93
mopidy/backends/spotify/playlist_manager.py
Normal file
@ -0,0 +1,93 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from spotify.manager import SpotifyPlaylistManager as PyspotifyPlaylistManager
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.playlist_manager')
|
||||
|
||||
class SpotifyPlaylistManager(PyspotifyPlaylistManager):
|
||||
def __init__(self, session_manager):
|
||||
PyspotifyPlaylistManager.__init__(self)
|
||||
self.session_manager = session_manager
|
||||
|
||||
def tracks_added(self, playlist, tracks, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: '
|
||||
u'%d track(s) added to position %d in playlist "%s"',
|
||||
len(tracks), position, playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def tracks_moved(self, playlist, tracks, new_position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: '
|
||||
u'%d track(s) moved to position %d in playlist "%s"',
|
||||
len(tracks), new_position, playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def tracks_removed(self, playlist, tracks, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: '
|
||||
u'%d track(s) removed from playlist "%s"', len(tracks), playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def playlist_renamed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: Playlist renamed to "%s"',
|
||||
playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def playlist_state_changed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: The state of playlist "%s" changed',
|
||||
playlist.name())
|
||||
|
||||
def playlist_update_in_progress(self, playlist, done, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
if done:
|
||||
logger.debug(u'Callback called: '
|
||||
u'Update of playlist "%s" done', playlist.name())
|
||||
else:
|
||||
logger.debug(u'Callback called: '
|
||||
u'Update of playlist "%s" in progress', playlist.name())
|
||||
|
||||
def playlist_metadata_updated(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: Metadata updated for playlist "%s"',
|
||||
playlist.name())
|
||||
|
||||
def track_created_changed(self, playlist, position, user, when, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
when = datetime.datetime.fromtimestamp(when)
|
||||
logger.debug(
|
||||
u'Callback called: Created by/when for track %d in playlist '
|
||||
u'"%s" changed to user "N/A" and time "%s"',
|
||||
position, playlist.name(), when)
|
||||
|
||||
def track_message_changed(self, playlist, position, message, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Message for track %d in playlist '
|
||||
u'"%s" changed to "%s"', position, playlist.name(), message)
|
||||
|
||||
def track_seen_changed(self, playlist, position, seen, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Seen attribute for track %d in playlist '
|
||||
u'"%s" changed to "%s"', position, playlist.name(), seen)
|
||||
|
||||
def description_changed(self, playlist, description, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Description changed for playlist "%s" to "%s"',
|
||||
playlist.name(), description)
|
||||
|
||||
def subscribers_changed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Subscribers changed for playlist "%s"',
|
||||
playlist.name())
|
||||
|
||||
def image_changed(self, playlist, image, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: Image changed for playlist "%s"',
|
||||
playlist.name())
|
||||
@ -9,11 +9,12 @@ from pykka.registry import ActorRegistry
|
||||
from mopidy import get_version, settings
|
||||
from mopidy.backends.base import Backend
|
||||
from mopidy.backends.spotify import BITRATES
|
||||
from mopidy.backends.spotify.container_manager import SpotifyContainerManager
|
||||
from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager
|
||||
from mopidy.backends.spotify.translator import SpotifyTranslator
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy.utils.process import BaseThread
|
||||
from mopidy.backends.spotify.container_manager import SpotifyContainerManager
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
|
||||
|
||||
@ -29,7 +30,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
def __init__(self, username, password):
|
||||
PyspotifySessionManager.__init__(self, username, password)
|
||||
BaseThread.__init__(self)
|
||||
self.name = 'SpotifySMThread'
|
||||
self.name = 'SpotifyThread'
|
||||
|
||||
self.gstreamer = None
|
||||
self.backend = None
|
||||
@ -38,6 +39,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
self.session = None
|
||||
|
||||
self.container_manager = None
|
||||
self.playlist_manager = None
|
||||
|
||||
def run_inside_try(self):
|
||||
self.setup()
|
||||
@ -45,7 +47,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
|
||||
def setup(self):
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
|
||||
assert len(gstreamer_refs) == 1, \
|
||||
'Expected exactly one running gstreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
@ -57,14 +60,19 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
if error:
|
||||
logger.error(u'Spotify login error: %s', error)
|
||||
return
|
||||
|
||||
logger.info(u'Connected to Spotify')
|
||||
self.session = session
|
||||
|
||||
logger.debug(u'Preferred Spotify bitrate is %s kbps.', settings.SPOTIFY_BITRATE)
|
||||
logger.debug(u'Preferred Spotify bitrate is %s kbps',
|
||||
settings.SPOTIFY_BITRATE)
|
||||
self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE])
|
||||
|
||||
self.container_manager = SpotifyContainerManager(self)
|
||||
self.playlist_manager = SpotifyPlaylistManager(self)
|
||||
|
||||
self.container_manager.watch(self.session.playlist_container())
|
||||
|
||||
self.connected.set()
|
||||
|
||||
def logged_out(self, session):
|
||||
@ -73,13 +81,12 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
|
||||
def metadata_updated(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Metadata updated')
|
||||
self.refresh_stored_playlists()
|
||||
logger.debug(u'Callback called: Metadata updated')
|
||||
|
||||
def connection_error(self, session, error):
|
||||
"""Callback used by pyspotify"""
|
||||
if error is None:
|
||||
logger.info(u'Spotify connection error resolved')
|
||||
logger.info(u'Spotify connection OK')
|
||||
else:
|
||||
logger.error(u'Spotify connection error: %s', error)
|
||||
self.backend.playback.pause()
|
||||
|
||||
@ -16,7 +16,7 @@ class SpotifyTranslator(object):
|
||||
return Artist(name=u'[loading...]')
|
||||
return Artist(
|
||||
uri=str(Link.from_artist(spotify_artist)),
|
||||
name=spotify_artist.name().decode(ENCODING, 'replace'),
|
||||
name=spotify_artist.name()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -24,7 +24,7 @@ class SpotifyTranslator(object):
|
||||
if spotify_album is None or 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, 'replace'))
|
||||
return Album(name=spotify_album.name())
|
||||
|
||||
@classmethod
|
||||
def to_mopidy_track(cls, spotify_track):
|
||||
@ -38,7 +38,7 @@ class SpotifyTranslator(object):
|
||||
date = None
|
||||
return Track(
|
||||
uri=uri,
|
||||
name=spotify_track.name().decode(ENCODING, 'replace'),
|
||||
name=spotify_track.name(),
|
||||
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(),
|
||||
@ -57,7 +57,7 @@ class SpotifyTranslator(object):
|
||||
try:
|
||||
return Playlist(
|
||||
uri=str(Link.from_playlist(spotify_playlist)),
|
||||
name=spotify_playlist.name().decode(ENCODING, 'replace'),
|
||||
name=spotify_playlist.name(),
|
||||
# FIXME if check on link is a hackish workaround for is_local
|
||||
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist
|
||||
if str(Link.from_track(t, 0))],
|
||||
|
||||
@ -40,11 +40,15 @@ def main():
|
||||
setup_mixer()
|
||||
setup_backend()
|
||||
setup_frontends()
|
||||
while ActorRegistry.get_all():
|
||||
while True:
|
||||
time.sleep(1)
|
||||
logger.info(u'No actors left. Exiting...')
|
||||
except SettingsError as e:
|
||||
logger.error(e.message)
|
||||
except KeyboardInterrupt:
|
||||
logger.info(u'User interrupt. Exiting...')
|
||||
logger.info(u'Interrupted. Exiting...')
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
finally:
|
||||
stop_all_actors()
|
||||
|
||||
def parse_options():
|
||||
|
||||
@ -49,7 +49,7 @@ class MpdSession(asynchat.async_chat):
|
||||
Format a response from the MPD command handlers and send it to the
|
||||
client.
|
||||
"""
|
||||
if response is not None:
|
||||
if response:
|
||||
response = LINE_TERMINATOR.join(response)
|
||||
logger.debug(u'Response to [%s]:%s: %s', self.client_address,
|
||||
self.client_port, indent(response))
|
||||
|
||||
@ -298,7 +298,7 @@ class GStreamer(ThreadingActor):
|
||||
output.sync_state_with_parent() # Required to add to running pipe
|
||||
gst.element_link_many(self._tee, output)
|
||||
self._outputs.append(output)
|
||||
logger.info('Added %s', output.get_name())
|
||||
logger.debug('GStreamer added %s', output.get_name())
|
||||
|
||||
def list_outputs(self):
|
||||
"""
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import signal
|
||||
import thread
|
||||
import threading
|
||||
|
||||
import gobject
|
||||
@ -12,19 +13,28 @@ from mopidy import SettingsError
|
||||
|
||||
logger = logging.getLogger('mopidy.utils.process')
|
||||
|
||||
def exit_process():
|
||||
logger.debug(u'Interrupting main...')
|
||||
thread.interrupt_main()
|
||||
logger.debug(u'Interrupted main')
|
||||
|
||||
def exit_handler(signum, frame):
|
||||
"""A :mod:`signal` handler which will exit the program on signal."""
|
||||
signals = dict((k, v) for v, k in signal.__dict__.iteritems()
|
||||
if v.startswith('SIG') and not v.startswith('SIG_'))
|
||||
logger.info(u'Got %s. Exiting...', signals[signum])
|
||||
stop_all_actors()
|
||||
logger.info(u'Got %s signal', signals[signum])
|
||||
exit_process()
|
||||
|
||||
def stop_all_actors():
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
while num_actors:
|
||||
logger.debug(u'Seeing %d actor and %d non-actor thread(s): %s',
|
||||
num_actors, threading.active_count() - num_actors,
|
||||
', '.join([t.name for t in threading.enumerate()]))
|
||||
logger.debug(u'Stopping %d actor(s)...', num_actors)
|
||||
ActorRegistry.stop_all()
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
logger.debug(u'All actors stopped.')
|
||||
|
||||
class BaseThread(threading.Thread):
|
||||
def __init__(self):
|
||||
|
||||
@ -73,7 +73,7 @@ class SettingsProxy(object):
|
||||
raise SettingsError(u'Settings validation failed.')
|
||||
|
||||
def _read_missing_settings_from_stdin(self, current, runtime):
|
||||
for setting, value in current.iteritems():
|
||||
for setting, value in sorted(current.iteritems()):
|
||||
if isinstance(value, basestring) and len(value) == 0:
|
||||
runtime[setting] = self._read_from_stdin(setting + u': ')
|
||||
|
||||
|
||||
@ -19,8 +19,9 @@ class VersionTest(unittest.TestCase):
|
||||
self.assert_(SV('0.3.0') < SV('0.3.1'))
|
||||
self.assert_(SV('0.3.1') < SV('0.4.0'))
|
||||
self.assert_(SV('0.4.0') < SV('0.4.1'))
|
||||
self.assert_(SV('0.4.1') < SV(get_plain_version()))
|
||||
self.assert_(SV(get_plain_version()) < SV('0.5.1'))
|
||||
self.assert_(SV('0.4.1') < SV('0.5.0'))
|
||||
self.assert_(SV('0.5.0') < SV(get_plain_version()))
|
||||
self.assert_(SV(get_plain_version()) < SV('0.6.1'))
|
||||
|
||||
def test_get_platform_contains_platform(self):
|
||||
self.assert_(platform.platform() in get_platform())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user