Merge branch 'develop' into feature/mpris-frontend

This commit is contained in:
Stein Magnus Jodal 2011-06-15 23:18:53 +02:00
commit efb3cb1102
16 changed files with 202 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ envlist = py26,py27,docs
[testenv]
deps = nose
commands = nosetests []
sitepackages = True
[testenv:docs]
basepython = python