Release v0.4.0

This commit is contained in:
Stein Magnus Jodal 2011-04-26 23:48:39 +02:00
commit 8df4505b97
109 changed files with 1486 additions and 1268 deletions

3
.gitignore vendored
View File

@ -2,11 +2,12 @@
*.swp
.coverage
.noseids
.tox
MANIFEST
build/
cover/
coverage.xml
dist/
docs/_build/
mopidy.log
mopidy.log*
nosetests.xml

View File

@ -1,5 +1,5 @@
#! /usr/bin/env python
if __name__ == '__main__':
from mopidy.__main__ import main
from mopidy.core import main
main()

View File

@ -5,6 +5,105 @@ Changes
This change log is used to track all major changes to Mopidy.
0.4.0 (2011-04-27)
==================
Mopidy 0.4.0 is another release without major feature additions. In 0.4.0 we've
fixed a bunch of issues and bugs, with the help of several new contributors
who are credited in the changelog below. The major change of 0.4.0 is an
internal refactoring which clears way for future features, and which also make
Mopidy work on Python 2.7. In other words, Mopidy 0.4.0 works on Ubuntu 11.04
and Arch Linux.
Please note that 0.4.0 requires some updated dependencies, as listed under
*Important changes* below. Also, the known bug in the Spotify playlist
loading from Mopidy 0.3.0 is still present.
.. warning:: Known bug in Spotify playlist loading
There is a known bug in the loading of Spotify playlists. To avoid the bug,
follow the simple workaround described at :issue:`59`.
**Important changes**
- Mopidy now depends on `Pykka <http://jodal.github.com/pykka>`_ >=0.12. If you
install from APT, Pykka will automatically be installed. If you are not
installing from APT, you may install Pykka from PyPI::
sudo pip install -U Pykka
- If you use the Spotify backend, you *should* upgrade to libspotify 0.0.7 and
the latest pyspotify from the Mopidy developers. 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/`.
**Changes**
- Mopidy now use Pykka actors for thread management and inter-thread
communication. The immediate advantage of this is that Mopidy now works on
Python 2.7, which is the default on e.g. Ubuntu 11.04. (Fixes: :issue:`66`)
- Spotify backend:
- Fixed multiple segmentation faults due to bugs in Pyspotify. Thanks to
Antoine Pierlot-Garcin and Jamie Kirkpatrick for patches to Pyspotify.
- Better error messages on wrong login or network problems. Thanks to Antoine
Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :issue:`77`)
- Reduce log level for trivial log messages from warning to info. (Fixes:
:issue:`71`)
- Pause playback on network connection errors. (Fixes: :issue:`65`)
- Local backend:
- Fix crash in :command:`mopidy-scan` if a track has no artist name. Thanks
to Martins Grunskis for test and patch and "octe" for patch.
- Fix crash in `tag_cache` parsing if a track has no total number of tracks
in the album. Thanks to Martins Grunskis for the patch.
- MPD frontend:
- Add support for "date" queries to both the ``find`` and ``search``
commands. This makes media library browsing in ncmpcpp work, though very
slow due to all the meta data requests to Spotify.
- Add support for ``play "-1"`` when in playing or paused state, which fixes
resume and addition of tracks to the current playlist while playing for the
MPoD client.
- Fix bug where ``status`` returned ``song: None``, which caused MPDroid to
crash. (Fixes: :issue:`69`)
- Gracefully fallback to IPv4 sockets on systems that supports IPv6, but has
turned it off. (Fixes: :issue:`75`)
- GStreamer output:
- Use ``uridecodebin`` for playing audio from both Spotify and the local
backend. This contributes to support for multiple backends simultaneously.
- Settings:
- Fix crash on ``--list-settings`` on clean installation. Thanks to Martins
Grunskis for the bug report and patch. (Fixes: :issue:`63`)
- Packaging:
- Replace test data symlinks with real files to avoid symlink issues when
installing with pip. (Fixes: :issue:`68`)
- Debugging:
- Include platform, architecture, Linux distribution, and Python version in
the debug log, to ease debugging of issues with attached debug logs.
0.3.1 (2010-01-22)
==================
@ -53,7 +152,7 @@ to this problem.
:doc:`/installation/libspotify/`.
- If you use the Last.fm frontend, you need to upgrade to pylast 0.5.7. Run
``sudp pip install --upgrade pylast`` or install Mopidy from APT.
``sudo pip install --upgrade pylast`` or install Mopidy from APT.
**Changes**

View File

@ -31,6 +31,9 @@ ncmpcpp
A console client that generally works well with Mopidy, and is regularly used
by Mopidy developers.
Search
^^^^^^
Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the
three search modes:
@ -42,6 +45,19 @@ three search modes:
If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp
from `Launchpad <https://launchpad.net/ubuntu/+source/ncmpcpp>`_.
Communication mode
^^^^^^^^^^^^^^^^^^
In newer versions of ncmpcpp, like 0.5.5 shipped with Ubuntu 11.04, ncmcpp
defaults to "notifications" mode for MPD communications, which Mopidy currently
does not support. To workaround this limitation in Mopidy, edit the ncmpcpp
configuration file at ``~/.ncmpcpp/config`` and add the following setting::
mpd_communication_mode = "polling"
You can track the development of "notifications" mode support in Mopidy in
:issue:`32`.
Graphical clients
=================

View File

@ -202,4 +202,4 @@ latex_documents = [
needs_sphinx = '1.0'
extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues#issue/%s', 'GH-')}
extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')}

View File

@ -74,11 +74,11 @@ Running tests
To run tests, you need a couple of dependencies. They can be installed through
Debian/Ubuntu package management::
sudo aptitude install python-coverage python-nose
sudo aptitude install python-coverage python-mock python-nose
Or, they can be installed using ``pip``::
sudo pip install -r requirements-tests.txt
sudo pip install -r requirements/tests.txt
Then, to run all tests, go to the project directory and run::
@ -107,14 +107,14 @@ For more documentation on testing, check out the `nose documentation
Continuous integration server
=============================
We run a continuous integration server called Hudson at
http://hudson.mopidy.com/ that runs all test on multiple platforms (Ubuntu, OS
X, etc.) for every commit we push to GitHub.
We run a continuous integration (CI) server at http://ci.mopidy.com/ that runs
all test on multiple platforms (Ubuntu, OS X, etc.) for every commit we push to
GitHub.
In addition to running tests, Hudson also does coverage statistics and uses
pylint to check for errors and possible improvements in our code. So, if you're
out of work, the code coverage and pylint data in Hudson should give you a
place to start.
In addition to running tests, the CI server also gathers coverage statistics
and uses pylint to check for errors and possible improvements in our code. So,
if you're out of work, the code coverage and pylint data at the CI server
should give you a place to start.
Writing documentation

View File

@ -25,6 +25,8 @@ Otherwise, make sure you got the required dependencies installed.
- Python >= 2.6, < 3
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
- Mixer dependencies: The default mixer does not require any additional

View File

@ -30,7 +30,7 @@ If you run a Debian based Linux distribution, like Ubuntu, see
http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source
on your installation. Then, simply run::
sudo apt-get install libspotify6
sudo apt-get install libspotify7
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
@ -39,14 +39,14 @@ When libspotify has been installed, continue with
On Linux from source
--------------------
Download and install libspotify 0.0.6 for your OS and CPU architecture from
Download and install libspotify 0.0.7 for your OS and CPU architecture from
https://developer.spotify.com/en/libspotify/.
For 64-bit Linux the process is as follows::
wget http://developer.spotify.com/download/libspotify/libspotify-0.0.6-linux6-x86_64.tar.gz
tar zxfv libspotify-0.0.6-linux6-x86_64.tar.gz
cd libspotify-0.0.6-linux6-x86_64/
wget http://developer.spotify.com/download/libspotify/libspotify-0.0.7-linux6-x86_64.tar.gz
tar zxfv libspotify-0.0.7-linux6-x86_64.tar.gz
cd libspotify-0.0.7-linux6-x86_64/
sudo make install prefix=/usr/local
sudo ldconfig
@ -113,4 +113,4 @@ Get the pyspotify code, and install it::
It is important that you install pyspotify from the ``mopidy`` branch of the
``mopidy/pyspotify`` repository, as the upstream repository at
``winjer/pyspotify`` is not updated with changes needed to support e.g.
libspotify 0.0.6 and high bitrate audio.
libspotify 0.0.7 and high bitrate audio.

View File

@ -1,9 +1,37 @@
import platform
import sys
if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
from subprocess import PIPE, Popen
VERSION = (0, 4, 0)
def get_version():
return u'0.3.1'
try:
return get_git_version()
except EnvironmentError:
return get_plain_version()
def get_git_version():
process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE)
if process.wait() != 0:
raise EnvironmentError('Execution of "git describe" failed')
version = process.stdout.read().strip()
if version.startswith('v'):
version = version[1:]
return version
def get_plain_version():
return '.'.join(map(str, VERSION))
def get_platform():
return platform.platform()
def get_python():
implementation = platform.python_implementation()
version = platform.python_version()
return u' '.join([implementation, version])
class MopidyException(Exception):
def __init__(self, message, *args, **kwargs):

View File

@ -1,17 +1,10 @@
import os
import sys
# Add ../ to the path so we can run Mopidy from a Git checkout without
# installing it on the system.
import os
import sys
sys.path.insert(0,
os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
from mopidy.core import CoreProcess
def main():
# Explictly call run() instead of start(), since we don't need to start
# another process.
CoreProcess().run()
if __name__ == '__main__':
from mopidy.core import main
main()

View File

@ -1,12 +1,4 @@
from copy import copy
import logging
import random
import time
from mopidy import settings
from mopidy.frontends.mpd import translator
from mopidy.models import Playlist
from mopidy.utils import get_class
from .current_playlist import CurrentPlaylistController
from .library import LibraryController, BaseLibraryProvider
@ -17,30 +9,6 @@ from .stored_playlists import (StoredPlaylistsController,
logger = logging.getLogger('mopidy.backends.base')
class Backend(object):
"""
:param core_queue: a queue for sending messages to
:class:`mopidy.process.CoreProcess`
:type core_queue: :class:`multiprocessing.Queue`
:param output: the audio output
:type output: :class:`mopidy.outputs.gstreamer.GStreamerOutput` or similar
:param mixer_class: either a mixer class, or :class:`None` to use the mixer
defined in settings
:type mixer_class: a subclass of :class:`mopidy.mixers.BaseMixer` or
:class:`None`
"""
def __init__(self, core_queue=None, output=None, mixer_class=None):
self.core_queue = core_queue
self.output = output
if mixer_class is None:
mixer_class = get_class(settings.MIXER)
self.mixer = mixer_class(self)
#: A :class:`multiprocessing.Queue` which can be used by e.g. library
#: callbacks executing in other threads to send messages to the core
#: thread, so that action may be taken in the correct thread.
core_queue = None
#: The current playlist controller. An instance of
#: :class:`mopidy.backends.base.CurrentPlaylistController`.
current_playlist = None
@ -49,9 +17,6 @@ class Backend(object):
# :class:`mopidy.backends.base.LibraryController`.
library = None
#: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`.
mixer = None
#: The playback controller. An instance of
#: :class:`mopidy.backends.base.PlaybackController`.
playback = None
@ -62,24 +27,3 @@ class Backend(object):
#: List of URI prefixes this backend can handle.
uri_handlers = []
def destroy(self):
"""
Call destroy on all sub-components in backend so that they can cleanup
after themselves.
"""
if self.current_playlist:
self.current_playlist.destroy()
if self.library:
self.library.destroy()
if self.mixer:
self.mixer.destroy()
if self.playback:
self.playback.destroy()
if self.stored_playlists:
self.stored_playlists.destroy()

View File

@ -2,8 +2,6 @@ from copy import copy
import logging
import random
from mopidy.frontends.mpd import translator
logger = logging.getLogger('mopidy.backends.base')
class CurrentPlaylistController(object):
@ -12,6 +10,8 @@ class CurrentPlaylistController(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
pykka_traversable = True
def __init__(self, backend):
self.backend = backend
self._cp_tracks = []
@ -197,8 +197,3 @@ class CurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
def mpd_format(self, *args, **kwargs):
"""Not a part of the generic backend API."""
kwargs['cpids'] = [ct[0] for ct in self._cp_tracks]
return translator.tracks_to_mpd_format(self.tracks, *args, **kwargs)

View File

@ -10,6 +10,8 @@ class LibraryController(object):
:type provider: instance of :class:`BaseLibraryProvider`
"""
pykka_traversable = True
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
@ -82,6 +84,8 @@ class BaseLibraryProvider(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
pykka_traversable = True
def __init__(self, backend):
self.backend = backend

View File

@ -2,6 +2,10 @@ import logging
import random
import time
from pykka.registry import ActorRegistry
from mopidy.frontends.base import BaseFrontend
logger = logging.getLogger('mopidy.backends.base')
class PlaybackController(object):
@ -15,6 +19,8 @@ class PlaybackController(object):
# pylint: disable = R0902
# Too many instance attributes
pykka_traversable = True
#: Constant representing the paused state.
PAUSED = u'paused'
@ -62,8 +68,8 @@ class PlaybackController(object):
self._state = self.STOPPED
self._shuffled = []
self._first_shuffle = True
self._play_time_accumulated = 0
self._play_time_started = None
self.play_time_accumulated = 0
self.play_time_started = None
def destroy(self):
"""
@ -240,7 +246,7 @@ class PlaybackController(object):
if self.repeat or self.consume or self.random:
return self.current_cp_track
if self.current_cp_track is None or self.current_playlist_position == 0:
if self.current_playlist_position in (None, 0):
return None
return self.backend.current_playlist.cp_tracks[
@ -269,7 +275,7 @@ class PlaybackController(object):
def state(self, new_state):
(old_state, self._state) = (self.state, new_state)
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
# FIXME _play_time stuff assumes backend does not have a better way of
# FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (self.PLAYING, self.STOPPED)
and new_state == self.PLAYING):
@ -284,23 +290,23 @@ class PlaybackController(object):
"""Time position in milliseconds."""
if self.state == self.PLAYING:
time_since_started = (self._current_wall_time -
self._play_time_started)
return self._play_time_accumulated + time_since_started
self.play_time_started)
return self.play_time_accumulated + time_since_started
elif self.state == self.PAUSED:
return self._play_time_accumulated
return self.play_time_accumulated
elif self.state == self.STOPPED:
return 0
def _play_time_start(self):
self._play_time_accumulated = 0
self._play_time_started = self._current_wall_time
self.play_time_accumulated = 0
self.play_time_started = self._current_wall_time
def _play_time_pause(self):
time_since_started = self._current_wall_time - self._play_time_started
self._play_time_accumulated += time_since_started
time_since_started = self._current_wall_time - self.play_time_started
self.play_time_accumulated += time_since_started
def _play_time_resume(self):
self._play_time_started = self._current_wall_time
self.play_time_started = self._current_wall_time
@property
def _current_wall_time(self):
@ -433,8 +439,8 @@ class PlaybackController(object):
self.next()
return True
self._play_time_started = self._current_wall_time
self._play_time_accumulated = time_position
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
return self.provider.seek(time_position)
@ -446,11 +452,10 @@ class PlaybackController(object):
stopping
:type clear_current_track: boolean
"""
if self.state == self.STOPPED:
return
self._trigger_stopped_playing_event()
if self.provider.stop():
self.state = self.STOPPED
if self.state != self.STOPPED:
self._trigger_stopped_playing_event()
if self.provider.stop():
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
@ -461,9 +466,11 @@ class PlaybackController(object):
For internal use only. Should be called by the backend directly after a
track has started playing.
"""
if self.current_track is not None:
self.backend.core_queue.put({
'to': 'frontend',
if self.current_track is None:
return
frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
for frontend_ref in frontend_refs:
frontend_ref.send_one_way({
'command': 'started_playing',
'track': self.current_track,
})
@ -476,9 +483,11 @@ class PlaybackController(object):
is stopped playing, e.g. at the next, previous, and stop actions and at
end-of-track.
"""
if self.current_track is not None:
self.backend.core_queue.put({
'to': 'frontend',
if self.current_track is None:
return
frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
for frontend_ref in frontend_refs:
frontend_ref.send_one_way({
'command': 'stopped_playing',
'track': self.current_track,
'stop_position': self.time_position,
@ -491,6 +500,8 @@ class BasePlaybackProvider(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
pykka_traversable = True
def __init__(self, backend):
self.backend = backend

View File

@ -11,6 +11,8 @@ class StoredPlaylistsController(object):
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
"""
pykka_traversable = True
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
@ -125,6 +127,8 @@ class BaseStoredPlaylistsProvider(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
pykka_traversable = True
def __init__(self, backend):
self.backend = backend
self._playlists = []

View File

@ -1,3 +1,5 @@
from pykka.actor import ThreadingActor
from mopidy.backends.base import (Backend, CurrentPlaylistController,
PlaybackController, BasePlaybackProvider, LibraryController,
BaseLibraryProvider, StoredPlaylistsController,
@ -5,15 +7,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController,
from mopidy.models import Playlist
class DummyQueue(object):
def __init__(self):
self.received_messages = []
def put(self, message):
self.received_messages.append(message)
class DummyBackend(Backend):
class DummyBackend(ThreadingActor, Backend):
"""
A backend which implements the backend API in the simplest way possible.
Used in tests of the frontends.
@ -24,8 +18,6 @@ class DummyBackend(Backend):
def __init__(self, *args, **kwargs):
super(DummyBackend, self).__init__(*args, **kwargs)
self.core_queue = DummyQueue()
self.current_playlist = CurrentPlaylistController(backend=self)
library_provider = DummyLibraryProvider(backend=self)
@ -46,13 +38,13 @@ class DummyBackend(Backend):
class DummyLibraryProvider(BaseLibraryProvider):
def __init__(self, *args, **kwargs):
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
self._library = []
self.dummy_library = []
def find_exact(self, **query):
return Playlist()
def lookup(self, uri):
matches = filter(lambda t: uri == t.uri, self._library)
matches = filter(lambda t: uri == t.uri, self.dummy_library)
if matches:
return matches[0]

View File

@ -1,22 +1,24 @@
import glob
import logging
import multiprocessing
import os
import shutil
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy.backends.base import (Backend, CurrentPlaylistController,
LibraryController, BaseLibraryProvider, PlaybackController,
BasePlaybackProvider, StoredPlaylistsController,
BaseStoredPlaylistsProvider)
from mopidy.models import Playlist, Track, Album
from mopidy.utils.process import pickle_connection
from mopidy.outputs.base import BaseOutput
from .translator import parse_m3u, parse_mpd_tag_cache
logger = logging.getLogger(u'mopidy.backends.local')
class LocalBackend(Backend):
class LocalBackend(ThreadingActor, Backend):
"""
A backend for playing music from a local music archive.
@ -48,6 +50,13 @@ class LocalBackend(Backend):
self.uri_handlers = [u'file://']
self.output = None
def on_start(self):
output_refs = ActorRegistry.get_by_class(BaseOutput)
assert len(output_refs) == 1, 'Expected exactly one running output.'
self.output = output_refs[0].proxy()
class LocalPlaybackController(PlaybackController):
def __init__(self, *args, **kwargs):
@ -58,24 +67,24 @@ class LocalPlaybackController(PlaybackController):
@property
def time_position(self):
return self.backend.output.get_position()
return self.backend.output.get_position().get()
class LocalPlaybackProvider(BasePlaybackProvider):
def pause(self):
return self.backend.output.set_state('PAUSED')
return self.backend.output.set_state('PAUSED').get()
def play(self, track):
return self.backend.output.play_uri(track.uri)
return self.backend.output.play_uri(track.uri).get()
def resume(self):
return self.backend.output.set_state('PLAYING')
return self.backend.output.set_state('PLAYING').get()
def seek(self, time_position):
return self.backend.output.set_position(time_position)
return self.backend.output.set_position(time_position).get()
def stop(self):
return self.backend.output.set_state('READY')
return self.backend.output.set_state('READY').get()
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):

View File

@ -100,8 +100,11 @@ def _convert_mpd_data(data, tracks, music_dir):
albumartist_kwargs = {}
if 'track' in data:
album_kwargs['num_tracks'] = int(data['track'].split('/')[1])
track_kwargs['track_no'] = int(data['track'].split('/')[0])
if '/' in data['track']:
album_kwargs['num_tracks'] = int(data['track'].split('/')[1])
track_kwargs['track_no'] = int(data['track'].split('/')[0])
else:
track_kwargs['track_no'] = int(data['track'])
if 'artist' in data:
artist_kwargs['name'] = data['artist']

View File

@ -1,14 +1,18 @@
import logging
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy.backends.base import (Backend, CurrentPlaylistController,
LibraryController, PlaybackController, StoredPlaylistsController)
from mopidy.outputs.base import BaseOutput
logger = logging.getLogger('mopidy.backends.spotify')
ENCODING = 'utf-8'
class SpotifyBackend(Backend):
class SpotifyBackend(ThreadingActor, Backend):
"""
A backend for playing music from the `Spotify <http://www.spotify.com/>`_
music streaming service. The backend uses the official `libspotify
@ -59,6 +63,14 @@ class SpotifyBackend(Backend):
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
self.output = None
self.spotify = None
def on_start(self):
output_refs = ActorRegistry.get_by_class(BaseOutput)
assert len(output_refs) == 1, 'Expected exactly one running output.'
self.output = output_refs[0].proxy()
self.spotify = self._connect()
def _connect(self):
@ -67,8 +79,6 @@ class SpotifyBackend(Backend):
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
logger.debug(u'Connecting to Spotify')
spotify = SpotifySessionManager(
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
core_queue=self.core_queue,
output=self.output)
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD)
spotify.start()
return spotify

View File

@ -1,5 +1,5 @@
import logging
import multiprocessing
import Queue
from spotify import Link, SpotifyError
@ -54,8 +54,9 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
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
queue = Queue.Queue()
self.backend.spotify.search(spotify_query.encode(ENCODING), queue)
try:
return queue.get(timeout=3) # XXX What is an reasonable timeout?
except Queue.Empty:
return Playlist(tracks=[])

View File

@ -20,10 +20,10 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
self.backend.output.set_state('PLAYING')
self.backend.output.play_uri('appsrc://')
return True
except SpotifyError as e:
logger.warning('Play %s failed: %s', track.uri, e)
logger.info('Playback of %s failed: %s', track.uri, e)
return False
def resume(self):

View File

@ -2,11 +2,15 @@ import logging
import os
import threading
import spotify.manager
from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from pykka.registry import ActorRegistry
from mopidy import get_version, settings
from mopidy.backends.base import Backend
from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist
from mopidy.outputs.base import BaseOutput
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
@ -14,26 +18,41 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager')
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
class SpotifySessionManager(BaseThread, PyspotifySessionManager):
cache_location = settings.SPOTIFY_CACHE_PATH
settings_location = settings.SPOTIFY_CACHE_PATH
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):
spotify.manager.SpotifySessionManager.__init__(
self, username, password)
BaseThread.__init__(self, core_queue)
def __init__(self, username, password):
PyspotifySessionManager.__init__(self, username, password)
BaseThread.__init__(self)
self.name = 'SpotifySMThread'
self.output = output
self.output = None
self.backend = None
self.connected = threading.Event()
self.session = None
def run_inside_try(self):
self.setup()
self.connect()
def setup(self):
output_refs = ActorRegistry.get_by_class(BaseOutput)
assert len(output_refs) == 1, 'Expected exactly one running output.'
self.output = output_refs[0].proxy()
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
self.backend = backend_refs[0].proxy()
def logged_in(self, session, error):
"""Callback used by pyspotify"""
if error:
logger.error(u'Spotify login error: %s', error)
return
logger.info(u'Connected to Spotify')
self.session = session
if settings.SPOTIFY_HIGH_BITRATE:
@ -55,7 +74,11 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
def connection_error(self, session, error):
"""Callback used by pyspotify"""
logger.error(u'Connection error: %s', error)
if error is None:
logger.info(u'Spotify connection error resolved')
else:
logger.error(u'Spotify connection error: %s', error)
self.backend.playback.pause()
def message_to_user(self, session, message):
"""Callback used by pyspotify"""
@ -88,7 +111,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
def play_token_lost(self, session):
"""Callback used by pyspotify"""
logger.debug(u'Play token lost')
self.core_queue.put({'command': 'stop_playback'})
self.backend.playback.pause()
def log_message(self, session, data):
"""Callback used by pyspotify"""
@ -107,19 +130,16 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
playlists.append(
SpotifyTranslator.to_mopidy_playlist(spotify_playlist))
playlists = filter(None, playlists)
self.core_queue.put({
'command': 'set_stored_playlists',
'playlists': playlists,
})
self.backend.stored_playlists.playlists = playlists
logger.debug(u'Refreshed %d stored playlist(s)', len(playlists))
def search(self, query, connection):
def search(self, query, queue):
"""Search method used by Mopidy backend"""
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
SpotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
connection.send(playlist)
queue.put(playlist)
self.connected.wait()
self.session.search(query, callback)

View File

@ -28,9 +28,9 @@ class SpotifyTranslator(object):
@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 not spotify_track.is_loaded():
return Track(uri=uri, name=u'[loading...]')
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
date = dt.date(spotify_track.album().year(), 1, 1)
else:
@ -57,8 +57,10 @@ class SpotifyTranslator(object):
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],
# 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))],
)
except SpotifyError, e:
logger.warning(u'Failed translating Spotify playlist '
logger.info(u'Failed translating Spotify playlist '
'(probably a playlist folder boundary): %s', e)

View File

@ -1,114 +1,76 @@
import logging
import multiprocessing
import optparse
import sys
import time
from pykka.registry import ActorRegistry
from mopidy import get_version, settings, OptionalDependencyError
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
from mopidy.utils.process import BaseThread, GObjectEventThread
from mopidy.utils.process import GObjectEventThread
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
class CoreProcess(BaseThread):
def __init__(self):
self.core_queue = multiprocessing.Queue()
super(CoreProcess, self).__init__(self.core_queue)
self.name = 'CoreProcess'
self.options = self.parse_options()
self.gobject_loop = None
self.output = None
self.backend = None
self.frontends = []
def main():
options = parse_options()
setup_logging(options.verbosity_level, options.save_debug_log)
setup_settings()
setup_gobject_loop()
setup_output()
setup_mixer()
setup_backend()
setup_frontends()
try:
while ActorRegistry.get_all():
time.sleep(1)
logger.info(u'No actors left. Exiting...')
except KeyboardInterrupt:
logger.info(u'User interrupt. Exiting...')
ActorRegistry.stop_all()
def parse_options(self):
parser = optparse.OptionParser(version='Mopidy %s' % get_version())
parser.add_option('-q', '--quiet',
action='store_const', const=0, dest='verbosity_level',
help='less output (warning level)')
parser.add_option('-v', '--verbose',
action='store_const', const=2, dest='verbosity_level',
help='more output (debug level)')
parser.add_option('--save-debug-log',
action='store_true', dest='save_debug_log',
help='save debug log to "./mopidy.log"')
parser.add_option('--list-settings',
action='callback', callback=list_settings_optparse_callback,
help='list current settings')
return parser.parse_args()[0]
def parse_options():
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
parser.add_option('-q', '--quiet',
action='store_const', const=0, dest='verbosity_level',
help='less output (warning level)')
parser.add_option('-v', '--verbose',
action='store_const', const=2, dest='verbosity_level',
help='more output (debug level)')
parser.add_option('--save-debug-log',
action='store_true', dest='save_debug_log',
help='save debug log to "./mopidy.log"')
parser.add_option('--list-settings',
action='callback', callback=list_settings_optparse_callback,
help='list current settings')
return parser.parse_args()[0]
def run_inside_try(self):
self.setup()
while True:
message = self.core_queue.get()
self.process_message(message)
def setup_settings():
get_or_create_folder('~/.mopidy/')
get_or_create_file('~/.mopidy/settings.py')
settings.validate()
def setup(self):
self.setup_logging()
self.setup_settings()
self.gobject_loop = self.setup_gobject_loop(self.core_queue)
self.output = self.setup_output(self.core_queue)
self.backend = self.setup_backend(self.core_queue, self.output)
self.frontends = self.setup_frontends(self.core_queue, self.backend)
def setup_gobject_loop():
gobject_loop = GObjectEventThread()
gobject_loop.start()
return gobject_loop
def setup_logging(self):
setup_logging(self.options.verbosity_level,
self.options.save_debug_log)
logger.info(u'-- Starting Mopidy --')
def setup_output():
return get_class(settings.OUTPUT).start().proxy()
def setup_settings(self):
get_or_create_folder('~/.mopidy/')
get_or_create_file('~/.mopidy/settings.py')
settings.validate()
def setup_mixer():
return get_class(settings.MIXER).start().proxy()
def setup_gobject_loop(self, core_queue):
gobject_loop = GObjectEventThread(core_queue)
gobject_loop.start()
return gobject_loop
def setup_backend():
return get_class(settings.BACKENDS[0]).start().proxy()
def setup_output(self, core_queue):
output = get_class(settings.OUTPUT)(core_queue)
output.start()
return output
def setup_backend(self, core_queue, output):
return get_class(settings.BACKENDS[0])(core_queue, output)
def setup_frontends(self, core_queue, backend):
frontends = []
for frontend_class_name in settings.FRONTENDS:
try:
frontend = get_class(frontend_class_name)(core_queue, backend)
frontend.start()
frontends.append(frontend)
except OptionalDependencyError as e:
logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
return frontends
def process_message(self, message):
if message.get('to') == 'core':
self.process_message_to_core(message)
elif message.get('to') == 'output':
self.output.process_message(message)
elif message.get('to') == 'frontend':
for frontend in self.frontends:
frontend.process_message(message)
elif message['command'] == 'end_of_track':
self.backend.playback.on_end_of_track()
elif message['command'] == 'stop_playback':
self.backend.playback.stop()
elif message['command'] == 'set_stored_playlists':
self.backend.stored_playlists.playlists = message['playlists']
else:
logger.warning(u'Cannot handle message: %s', message)
def process_message_to_core(self, message):
assert message['to'] == 'core', u'Message recipient must be "core".'
if message['command'] == 'exit':
if message['reason'] is not None:
logger.info(u'Exiting (%s)', message['reason'])
sys.exit(message['status'])
else:
logger.warning(u'Cannot handle message: %s', message)
def setup_frontends():
frontends = []
for frontend_class_name in settings.FRONTENDS:
try:
frontend = get_class(frontend_class_name).start().proxy()
frontends.append(frontend)
except OptionalDependencyError as e:
logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
return frontends

View File

@ -1,40 +1,5 @@
class BaseFrontend(object):
"""
Base class for frontends.
:param core_queue: queue for messaging the core
:type core_queue: :class:`multiprocessing.Queue`
:param backend: the backend
:type backend: :class:`mopidy.backends.base.Backend`
"""
def __init__(self, core_queue, backend):
self.core_queue = core_queue
self.backend = backend
def start(self):
"""
Start the frontend.
*MAY be implemented by subclass.*
"""
pass
def destroy(self):
"""
Destroy the frontend.
*MAY be implemented by subclass.*
"""
pass
def process_message(self, message):
"""
Process messages for the frontend.
*MUST be implemented by subclass.*
:param message: the message
:type message: dict
"""
raise NotImplementedError
pass

View File

@ -1,5 +1,4 @@
import logging
import multiprocessing
import time
try:
@ -8,16 +7,17 @@ except ImportError as import_error:
from mopidy import OptionalDependencyError
raise OptionalDependencyError(import_error)
from pykka.actor import ThreadingActor
from mopidy import settings, SettingsError
from mopidy.frontends.base import BaseFrontend
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.frontends.lastfm')
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
class LastfmFrontend(BaseFrontend):
class LastfmFrontend(ThreadingActor, BaseFrontend):
"""
Frontend which scrobbles the music you play to your `Last.fm
<http://www.last.fm>`_ profile.
@ -36,38 +36,11 @@ class LastfmFrontend(BaseFrontend):
- :attr:`mopidy.settings.LASTFM_PASSWORD`
"""
def __init__(self, *args, **kwargs):
super(LastfmFrontend, self).__init__(*args, **kwargs)
(self.connection, other_end) = multiprocessing.Pipe()
self.thread = LastfmFrontendThread(self.core_queue, other_end)
def start(self):
self.thread.start()
def destroy(self):
self.thread.destroy()
def process_message(self, message):
if self.thread.is_alive():
self.connection.send(message)
class LastfmFrontendThread(BaseThread):
def __init__(self, core_queue, connection):
super(LastfmFrontendThread, self).__init__(core_queue)
self.name = u'LastfmFrontendThread'
self.connection = connection
def __init__(self):
self.lastfm = None
self.last_start_time = None
def run_inside_try(self):
self.setup()
while self.lastfm is not None:
self.connection.poll(None)
message = self.connection.recv()
self.process_message(message)
def setup(self):
def on_start(self):
try:
username = settings.LASTFM_USERNAME
password_hash = pylast.md5(settings.LASTFM_PASSWORD)
@ -78,17 +51,19 @@ class LastfmFrontendThread(BaseThread):
except SettingsError as e:
logger.info(u'Last.fm scrobbler not started')
logger.debug(u'Last.fm settings error: %s', e)
self.stop()
except (pylast.NetworkError, pylast.MalformedResponseError,
pylast.WSError) as e:
logger.error(u'Error during Last.fm setup: %s', e)
self.stop()
def process_message(self, message):
if message['command'] == 'started_playing':
def on_receive(self, message):
if message.get('command') == 'started_playing':
self.started_playing(message['track'])
elif message['command'] == 'stopped_playing':
elif message.get('command') == 'stopped_playing':
self.stopped_playing(message['track'], message['stop_position'])
else:
pass # Ignore commands for other frontends
pass # Ignore any other messages
def started_playing(self, track):
artists = ', '.join([a.name for a in track.artists])

View File

@ -1,13 +1,15 @@
import asyncore
import logging
from pykka.actor import ThreadingActor
from mopidy.frontends.base import BaseFrontend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.thread import MpdThread
from mopidy.utils.process import unpickle_connection
from mopidy.frontends.mpd.server import MpdServer
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.frontends.mpd')
class MpdFrontend(BaseFrontend):
class MpdFrontend(ThreadingActor, BaseFrontend):
"""
The MPD frontend.
@ -18,32 +20,24 @@ class MpdFrontend(BaseFrontend):
- :attr:`mopidy.settings.MPD_SERVER_PORT`
"""
def __init__(self, *args, **kwargs):
super(MpdFrontend, self).__init__(*args, **kwargs)
self.thread = None
self.dispatcher = MpdDispatcher(self.backend)
def __init__(self):
self._thread = None
def start(self):
"""Starts the MPD server."""
self.thread = MpdThread(self.core_queue)
self.thread.start()
def on_start(self):
self._thread = MpdThread()
self._thread.start()
def destroy(self):
"""Destroys the MPD server."""
self.thread.destroy()
def on_receive(self, message):
pass # Ignore any messages
def process_message(self, message):
"""
Processes messages with the MPD frontend as destination.
:param message: the message
:type message: dict
"""
assert message['to'] == 'frontend', \
u'Message recipient must be "frontend".'
if message['command'] == 'mpd_request':
response = self.dispatcher.handle_request(message['request'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
else:
pass # Ignore messages for other frontends
class MpdThread(BaseThread):
def __init__(self):
super(MpdThread, self).__init__()
self.name = u'MpdThread'
def run_inside_try(self):
logger.debug(u'Starting MPD server thread')
server = MpdServer()
server.start()
asyncore.loop()

View File

@ -1,5 +1,8 @@
import re
from pykka.registry import ActorRegistry
from mopidy.backends.base import Backend
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
MpdUnknownCommand)
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
@ -10,15 +13,27 @@ from mopidy.frontends.mpd.protocol import (audio_output, command_list,
connection, current_playlist, empty, music_db, playback, reflection,
status, stickers, stored_playlists)
# pylint: enable = W0611
from mopidy.mixers.base import BaseMixer
from mopidy.utils import flatten
class MpdDispatcher(object):
"""
Dispatches MPD requests to the correct handler.
The MPD session feeds the MPD dispatcher with requests. The dispatcher
finds the correct handler, processes the request and sends the response
back to the MPD session.
"""
def __init__(self, backend=None):
self.backend = backend
# XXX Consider merging MpdDispatcher into MpdSession
def __init__(self):
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
self.backend = backend_refs[0].proxy()
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
self.mixer = mixer_refs[0].proxy()
self.command_list = False
self.command_list_ok = False

View File

@ -34,6 +34,6 @@ def outputs(frontend):
"""
return [
('outputid', 0),
('outputname', frontend.backend.__class__.__name__),
('outputname', None),
('outputenabled', 1),
]

View File

@ -1,6 +1,7 @@
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.translator import tracks_to_mpd_format
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')
def add(frontend, uri):
@ -18,9 +19,9 @@ def add(frontend, uri):
"""
if not uri:
return
for handler_prefix in frontend.backend.uri_handlers:
for handler_prefix in frontend.backend.uri_handlers.get():
if uri.startswith(handler_prefix):
track = frontend.backend.library.lookup(uri)
track = frontend.backend.library.lookup(uri).get()
if track is not None:
frontend.backend.current_playlist.add(track)
return
@ -50,13 +51,14 @@ def addid(frontend, uri, songpos=None):
raise MpdNoExistError(u'No such song', command=u'addid')
if songpos is not None:
songpos = int(songpos)
track = frontend.backend.library.lookup(uri)
track = frontend.backend.library.lookup(uri).get()
if track is None:
raise MpdNoExistError(u'No such song', command=u'addid')
if songpos and songpos > len(frontend.backend.current_playlist.tracks):
if songpos and songpos > len(
frontend.backend.current_playlist.tracks.get()):
raise MpdArgError(u'Bad song index', command=u'addid')
cp_track = frontend.backend.current_playlist.add(track,
at_position=songpos)
at_position=songpos).get()
return ('Id', cp_track[0])
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
@ -72,8 +74,8 @@ def delete_range(frontend, start, end=None):
if end is not None:
end = int(end)
else:
end = len(frontend.backend.current_playlist.tracks)
cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end]
end = len(frontend.backend.current_playlist.tracks.get())
cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end]
if not cp_tracks:
raise MpdArgError(u'Bad song index', command=u'delete')
for (cpid, _) in cp_tracks:
@ -84,7 +86,7 @@ def delete_songpos(frontend, songpos):
"""See :meth:`delete_range`"""
try:
songpos = int(songpos)
(cpid, _) = frontend.backend.current_playlist.cp_tracks[songpos]
(cpid, _) = frontend.backend.current_playlist.cp_tracks.get()[songpos]
frontend.backend.current_playlist.remove(cpid=cpid)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'delete')
@ -100,9 +102,9 @@ def deleteid(frontend, cpid):
"""
try:
cpid = int(cpid)
if frontend.backend.playback.current_cpid == cpid:
if frontend.backend.playback.current_cpid.get() == cpid:
frontend.backend.playback.next()
return frontend.backend.current_playlist.remove(cpid=cpid)
return frontend.backend.current_playlist.remove(cpid=cpid).get()
except LookupError:
raise MpdNoExistError(u'No such song', command=u'deleteid')
@ -128,7 +130,7 @@ def move_range(frontend, start, to, end=None):
``TO`` in the playlist.
"""
if end is None:
end = len(frontend.backend.current_playlist.tracks)
end = len(frontend.backend.current_playlist.tracks.get())
start = int(start)
end = int(end)
to = int(to)
@ -154,8 +156,9 @@ def moveid(frontend, cpid, to):
"""
cpid = int(cpid)
to = int(to)
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
position = frontend.backend.current_playlist.cp_tracks.index(cp_track)
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
position = frontend.backend.current_playlist.cp_tracks.get().index(
cp_track)
frontend.backend.current_playlist.move(position, position + 1, to)
@handle_pattern(r'^playlist$')
@ -189,9 +192,9 @@ def playlistfind(frontend, tag, needle):
"""
if tag == 'filename':
try:
cp_track = frontend.backend.current_playlist.get(uri=needle)
cp_track = frontend.backend.current_playlist.get(uri=needle).get()
(cpid, track) = cp_track
position = frontend.backend.current_playlist.cp_tracks.index(
position = frontend.backend.current_playlist.cp_tracks.get().index(
cp_track)
return track.mpd_format(cpid=cpid, position=position)
except LookupError:
@ -211,14 +214,17 @@ def playlistid(frontend, cpid=None):
if cpid is not None:
try:
cpid = int(cpid)
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
position = frontend.backend.current_playlist.cp_tracks.index(
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
position = frontend.backend.current_playlist.cp_tracks.get().index(
cp_track)
return cp_track[1].mpd_format(position=position, cpid=cpid)
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playlistid')
else:
return frontend.backend.current_playlist.mpd_format()
cpids = [ct[0] for ct in
frontend.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
frontend.backend.current_playlist.tracks.get(), cpids=cpids)
@handle_pattern(r'^playlistinfo$')
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$')
@ -248,18 +254,27 @@ def playlistinfo(frontend, songpos=None,
end = songpos + 1
if start == -1:
end = None
return frontend.backend.current_playlist.mpd_format(start, end)
cpids = [ct[0] for ct in
frontend.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
frontend.backend.current_playlist.tracks.get(),
start, end, cpids=cpids)
else:
if start is None:
start = 0
start = int(start)
if not (0 <= start <= len(frontend.backend.current_playlist.tracks)):
if not (0 <= start <= len(
frontend.backend.current_playlist.tracks.get())):
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
if end is not None:
end = int(end)
if end > len(frontend.backend.current_playlist.tracks):
if end > len(frontend.backend.current_playlist.tracks.get()):
end = None
return frontend.backend.current_playlist.mpd_format(start, end)
cpids = [ct[0] for ct in
frontend.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
frontend.backend.current_playlist.tracks.get(),
start, end, cpids=cpids)
@handle_pattern(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
@ -298,7 +313,10 @@ def plchanges(frontend, version):
"""
# XXX Naive implementation that returns all tracks as changed
if int(version) < frontend.backend.current_playlist.version:
return frontend.backend.current_playlist.mpd_format()
cpids = [ct[0] for ct in
frontend.backend.current_playlist.cp_tracks.get()]
return tracks_to_mpd_format(
frontend.backend.current_playlist.tracks.get(), cpids=cpids)
@handle_pattern(r'^plchangesposid "(?P<version>\d+)"$')
def plchangesposid(frontend, version):
@ -315,10 +333,10 @@ def plchangesposid(frontend, version):
``playlistlength`` returned by status command.
"""
# XXX Naive implementation that returns all tracks as changed
if int(version) != frontend.backend.current_playlist.version:
if int(version) != frontend.backend.current_playlist.version.get():
result = []
for (position, (cpid, _)) in enumerate(
frontend.backend.current_playlist.cp_tracks):
frontend.backend.current_playlist.cp_tracks.get()):
result.append((u'cpos', position))
result.append((u'Id', cpid))
return result
@ -351,7 +369,7 @@ def swap(frontend, songpos1, songpos2):
"""
songpos1 = int(songpos1)
songpos2 = int(songpos2)
tracks = frontend.backend.current_playlist.tracks
tracks = frontend.backend.current_playlist.tracks.get()
song1 = tracks[songpos1]
song2 = tracks[songpos2]
del tracks[songpos1]
@ -372,8 +390,9 @@ def swapid(frontend, cpid1, cpid2):
"""
cpid1 = int(cpid1)
cpid2 = int(cpid2)
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1)
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2)
position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1)
position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2)
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get()
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get()
cp_tracks = frontend.backend.current_playlist.cp_tracks.get()
position1 = cp_tracks.index(cp_track1)
position2 = cp_tracks.index(cp_track2)
swap(frontend, position1, position2)

View File

@ -41,8 +41,8 @@ def count(frontend, tag, needle):
return [('songs', 0), ('playtime', 0)] # TODO
@handle_pattern(r'^find '
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
' "[^"]+"\s?)+)$')
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
def find(frontend, mpd_query):
"""
*musicpd.org, music database section:*
@ -62,9 +62,13 @@ def find(frontend, mpd_query):
- does not add quotes around the field argument.
- capitalizes the type argument.
*ncmpcpp:*
- also uses the search type "date".
"""
query = _build_query(mpd_query)
return frontend.backend.library.find_exact(**query).mpd_format()
return frontend.backend.library.find_exact(**query).get().mpd_format()
@handle_pattern(r'^findadd '
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
@ -211,7 +215,7 @@ def _list_build_query(field, mpd_query):
def _list_artist(frontend, query):
artists = set()
playlist = frontend.backend.library.find_exact(**query)
playlist = frontend.backend.library.find_exact(**query).get()
for track in playlist.tracks:
for artist in track.artists:
artists.add((u'Artist', artist.name))
@ -219,7 +223,7 @@ def _list_artist(frontend, query):
def _list_album(frontend, query):
albums = set()
playlist = frontend.backend.library.find_exact(**query)
playlist = frontend.backend.library.find_exact(**query).get()
for track in playlist.tracks:
if track.album is not None:
albums.add((u'Album', track.album.name))
@ -227,7 +231,7 @@ def _list_album(frontend, query):
def _list_date(frontend, query):
dates = set()
playlist = frontend.backend.library.find_exact(**query)
playlist = frontend.backend.library.find_exact(**query).get()
for track in playlist.tracks:
if track.date is not None:
dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
@ -290,8 +294,8 @@ def rescan(frontend, uri=None):
return update(frontend, uri, rescan_unmodified_files=True)
@handle_pattern(r'^search '
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
' "[^"]+"\s?)+)$')
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
def search(frontend, mpd_query):
"""
*musicpd.org, music database section:*
@ -314,9 +318,13 @@ def search(frontend, mpd_query):
- does not add quotes around the field argument.
- capitalizes the field argument.
*ncmpcpp:*
- also uses the search type "date".
"""
query = _build_query(mpd_query)
return frontend.backend.library.search(**query).mpd_format()
return frontend.backend.library.search(**query).get().mpd_format()
@handle_pattern(r'^update( "(?P<uri>[^"]+)")*$')
def update(frontend, uri=None, rescan_unmodified_files=False):

View File

@ -1,3 +1,4 @@
from mopidy.backends.base import PlaybackController
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
@ -86,7 +87,7 @@ def next_(frontend):
order as the first time.
"""
return frontend.backend.playback.next()
return frontend.backend.playback.next().get()
@handle_pattern(r'^pause$')
@handle_pattern(r'^pause "(?P<state>[01])"$')
@ -103,11 +104,11 @@ def pause(frontend, state=None):
- Calls ``pause`` without any arguments to toogle pause.
"""
if state is None:
if (frontend.backend.playback.state ==
frontend.backend.playback.PLAYING):
if (frontend.backend.playback.state.get() ==
PlaybackController.PLAYING):
frontend.backend.playback.pause()
elif (frontend.backend.playback.state ==
frontend.backend.playback.PAUSED):
elif (frontend.backend.playback.state.get() ==
PlaybackController.PAUSED):
frontend.backend.playback.resume()
elif int(state):
frontend.backend.playback.pause()
@ -120,7 +121,7 @@ def play(frontend):
The original MPD server resumes from the paused state on ``play``
without arguments.
"""
return frontend.backend.playback.play()
return frontend.backend.playback.play().get()
@handle_pattern(r'^playid "(?P<cpid>\d+)"$')
@handle_pattern(r'^playid "(?P<cpid>-1)"$')
@ -132,22 +133,21 @@ def playid(frontend, cpid):
Begins playing the playlist at song ``SONGID``.
*GMPC:*
*Clarifications:*
- issues ``playid "-1"`` after playlist replacement to start playback
at the first track.
- ``playid "-1"`` when playing is ignored.
- ``playid "-1"`` when paused resumes playback.
- ``playid "-1"`` when stopped with a current track starts playback at the
current track.
- ``playid "-1"`` when stopped without a current track, e.g. after playlist
replacement, starts playback at the first track.
"""
cpid = int(cpid)
paused = (frontend.backend.playback.state ==
frontend.backend.playback.PAUSED)
if cpid == -1 and paused:
return frontend.backend.playback.resume()
if cpid == -1:
return _play_minus_one(frontend)
try:
if cpid == -1:
cp_track = _get_cp_track_for_play_minus_one(frontend)
else:
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
return frontend.backend.playback.play(cp_track)
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
return frontend.backend.playback.play(cp_track).get()
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playid')
@ -161,33 +161,41 @@ def playpos(frontend, songpos):
Begins playing the playlist at song number ``SONGPOS``.
*Many clients:*
*Clarifications:*
- issue ``play "-1"`` after playlist replacement to start the current
track. If the current track is not set, start playback at the first
track.
- ``playid "-1"`` when playing is ignored.
- ``playid "-1"`` when paused resumes playback.
- ``playid "-1"`` when stopped with a current track starts playback at the
current track.
- ``playid "-1"`` when stopped without a current track, e.g. after playlist
replacement, starts playback at the first track.
*BitMPC:*
- issues ``play 6`` without quotes around the argument.
"""
songpos = int(songpos)
if songpos == -1:
return _play_minus_one(frontend)
try:
if songpos == -1:
cp_track = _get_cp_track_for_play_minus_one(frontend)
else:
cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
return frontend.backend.playback.play(cp_track)
cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos]
return frontend.backend.playback.play(cp_track).get()
except IndexError:
raise MpdArgError(u'Bad song index', command=u'play')
def _get_cp_track_for_play_minus_one(frontend):
if not frontend.backend.current_playlist.cp_tracks:
def _play_minus_one(frontend):
if (frontend.backend.playback.state.get() == PlaybackController.PLAYING):
return # Nothing to do
elif (frontend.backend.playback.state.get() == PlaybackController.PAUSED):
return frontend.backend.playback.resume().get()
elif frontend.backend.playback.current_cp_track.get() is not None:
cp_track = frontend.backend.playback.current_cp_track.get()
return frontend.backend.playback.play(cp_track).get()
elif frontend.backend.current_playlist.cp_tracks.get():
cp_track = frontend.backend.current_playlist.cp_tracks.get()[0]
return frontend.backend.playback.play(cp_track).get()
else:
return # Fail silently
cp_track = frontend.backend.playback.current_cp_track
if cp_track is None:
cp_track = frontend.backend.current_playlist.cp_tracks[0]
return cp_track
@handle_pattern(r'^previous$')
def previous(frontend):
@ -233,7 +241,7 @@ def previous(frontend):
``previous`` should do a seek to time position 0.
"""
return frontend.backend.playback.previous()
return frontend.backend.playback.previous().get()
@handle_pattern(r'^random (?P<state>[01])$')
@handle_pattern(r'^random "(?P<state>[01])"$')
@ -344,7 +352,7 @@ def setvol(frontend, volume):
volume = 0
if volume > 100:
volume = 100
frontend.backend.mixer.volume = volume
frontend.mixer.volume = volume
@handle_pattern(r'^single (?P<state>[01])$')
@handle_pattern(r'^single "(?P<state>[01])"$')

View File

@ -81,4 +81,4 @@ def urlhandlers(frontend):
Gets a list of available URL handlers.
"""
return [(u'handler', uri) for uri in frontend.backend.uri_handlers]
return [(u'handler', uri) for uri in frontend.backend.uri_handlers.get()]

View File

@ -1,3 +1,4 @@
from mopidy.backends.base import PlaybackController
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@ -23,10 +24,11 @@ def currentsong(frontend):
Displays the song info of the current song (same song that is
identified in status).
"""
if frontend.backend.playback.current_track is not None:
return frontend.backend.playback.current_track.mpd_format(
position=frontend.backend.playback.current_playlist_position,
cpid=frontend.backend.playback.current_cpid)
current_cp_track = frontend.backend.playback.current_cp_track.get()
if current_cp_track is not None:
return current_cp_track[1].mpd_format(
position=frontend.backend.playback.current_playlist_position.get(),
cpid=current_cp_track[0])
@handle_pattern(r'^idle$')
@handle_pattern(r'^idle (?P<subsystems>.+)$')
@ -90,8 +92,7 @@ def stats(frontend):
'artists': 0, # TODO
'albums': 0, # TODO
'songs': 0, # TODO
# TODO Does not work after multiprocessing branch merge
'uptime': 0, # frontend.session.stats_uptime(),
'uptime': 0, # TODO
'db_playtime': 0, # TODO
'db_update': 0, # TODO
'playtime': 0, # TODO
@ -140,56 +141,59 @@ def status(frontend):
('xfade', _status_xfade(frontend)),
('state', _status_state(frontend)),
]
if frontend.backend.playback.current_track is not None:
if frontend.backend.playback.current_track.get() is not None:
result.append(('song', _status_songpos(frontend)))
result.append(('songid', _status_songid(frontend)))
if frontend.backend.playback.state in (frontend.backend.playback.PLAYING,
frontend.backend.playback.PAUSED):
if frontend.backend.playback.state.get() in (PlaybackController.PLAYING,
PlaybackController.PAUSED):
result.append(('time', _status_time(frontend)))
result.append(('elapsed', _status_time_elapsed(frontend)))
result.append(('bitrate', _status_bitrate(frontend)))
return result
def _status_bitrate(frontend):
if frontend.backend.playback.current_track is not None:
return frontend.backend.playback.current_track.bitrate
current_track = frontend.backend.playback.current_track.get()
if current_track is not None:
return current_track.bitrate
def _status_consume(frontend):
if frontend.backend.playback.consume:
if frontend.backend.playback.consume.get():
return 1
else:
return 0
def _status_playlist_length(frontend):
return len(frontend.backend.current_playlist.tracks)
return len(frontend.backend.current_playlist.tracks.get())
def _status_playlist_version(frontend):
return frontend.backend.current_playlist.version
return frontend.backend.current_playlist.version.get()
def _status_random(frontend):
return int(frontend.backend.playback.random)
return int(frontend.backend.playback.random.get())
def _status_repeat(frontend):
return int(frontend.backend.playback.repeat)
return int(frontend.backend.playback.repeat.get())
def _status_single(frontend):
return int(frontend.backend.playback.single)
return int(frontend.backend.playback.single.get())
def _status_songid(frontend):
if frontend.backend.playback.current_cpid is not None:
return frontend.backend.playback.current_cpid
current_cpid = frontend.backend.playback.current_cpid.get()
if current_cpid is not None:
return current_cpid
else:
return _status_songpos(frontend)
def _status_songpos(frontend):
return frontend.backend.playback.current_playlist_position
return frontend.backend.playback.current_playlist_position.get()
def _status_state(frontend):
if frontend.backend.playback.state == frontend.backend.playback.PLAYING:
state = frontend.backend.playback.state.get()
if state == PlaybackController.PLAYING:
return u'play'
elif frontend.backend.playback.state == frontend.backend.playback.STOPPED:
elif state == PlaybackController.STOPPED:
return u'stop'
elif frontend.backend.playback.state == frontend.backend.playback.PAUSED:
elif state == PlaybackController.PAUSED:
return u'pause'
def _status_time(frontend):
@ -197,19 +201,21 @@ def _status_time(frontend):
_status_time_total(frontend) // 1000)
def _status_time_elapsed(frontend):
return frontend.backend.playback.time_position
return frontend.backend.playback.time_position.get()
def _status_time_total(frontend):
if frontend.backend.playback.current_track is None:
current_track = frontend.backend.playback.current_track.get()
if current_track is None:
return 0
elif frontend.backend.playback.current_track.length is None:
elif current_track.length is None:
return 0
else:
return frontend.backend.playback.current_track.length
return current_track.length
def _status_volume(frontend):
if frontend.backend.mixer.volume is not None:
return frontend.backend.mixer.volume
volume = frontend.mixer.volume.get()
if volume is not None:
return volume
else:
return 0

View File

@ -19,8 +19,8 @@ def listplaylist(frontend, name):
file: relative/path/to/file3.mp3
"""
try:
return ['file: %s' % t.uri
for t in frontend.backend.stored_playlists.get(name=name).tracks]
playlist = frontend.backend.stored_playlists.get(name=name).get()
return ['file: %s' % t.uri for t in playlist.tracks]
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
@ -39,7 +39,8 @@ def listplaylistinfo(frontend, name):
Album, Artist, Track
"""
try:
return frontend.backend.stored_playlists.get(name=name).mpd_format()
playlist = frontend.backend.stored_playlists.get(name=name).get()
return playlist.mpd_format()
except LookupError:
raise MpdNoExistError(
u'No such playlist', command=u'listplaylistinfo')
@ -66,7 +67,7 @@ def listplaylists(frontend):
Last-Modified: 2010-02-06T02:11:08Z
"""
result = []
for playlist in frontend.backend.stored_playlists.playlists:
for playlist in frontend.backend.stored_playlists.playlists.get():
result.append((u'playlist', playlist.name))
last_modified = (playlist.last_modified or
dt.datetime.now()).isoformat()
@ -92,7 +93,7 @@ def load(frontend, name):
- ``load`` appends the given playlist to the current playlist.
"""
try:
playlist = frontend.backend.stored_playlists.get(name=name)
playlist = frontend.backend.stored_playlists.get(name=name).get()
frontend.backend.current_playlist.append(playlist.tracks)
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'load')

View File

@ -9,20 +9,33 @@ from .session import MpdSession
logger = logging.getLogger('mopidy.frontends.mpd.server')
def _try_ipv6_socket():
"""Determine if system really supports IPv6"""
if not socket.has_ipv6:
return False
try:
socket.socket(socket.AF_INET6).close()
return True
except IOError, e:
logger.debug(u'Platform supports IPv6, but socket '
'creation failed, disabling: %s', e)
return False
has_ipv6 = _try_ipv6_socket()
class MpdServer(asyncore.dispatcher):
"""
The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
for each client connection.
"""
def __init__(self, core_queue):
def __init__(self):
asyncore.dispatcher.__init__(self)
self.core_queue = core_queue
def start(self):
"""Start MPD server."""
try:
if socket.has_ipv6:
if has_ipv6:
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
# Explicitly configure socket to work for both IPv4 and IPv6
self.socket.setsockopt(
@ -47,15 +60,14 @@ class MpdServer(asyncore.dispatcher):
(client_socket, client_socket_address) = self.accept()
logger.info(u'MPD client connection from [%s]:%s',
client_socket_address[0], client_socket_address[1])
MpdSession(self, client_socket, client_socket_address,
self.core_queue).start()
MpdSession(self, client_socket, client_socket_address).start()
def handle_close(self):
"""Handle end of client connection."""
self.close()
def _format_hostname(self, hostname):
if (socket.has_ipv6
if (has_ipv6
and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
hostname = '::ffff:%s' % hostname
return hostname

View File

@ -1,30 +1,28 @@
import asynchat
import logging
import multiprocessing
from mopidy import settings
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
from mopidy.utils.log import indent
from mopidy.utils.process import pickle_connection
logger = logging.getLogger('mopidy.frontends.mpd.session')
class MpdSession(asynchat.async_chat):
"""
The MPD client session. Keeps track of a single client and passes its
MPD requests to the dispatcher.
The MPD client session. Keeps track of a single client session. Any
requests from the client is passed on to the MPD request dispatcher.
"""
def __init__(self, server, client_socket, client_socket_address,
core_queue):
def __init__(self, server, client_socket, client_socket_address):
asynchat.async_chat.__init__(self, sock=client_socket)
self.server = server
self.client_address = client_socket_address[0]
self.client_port = client_socket_address[1]
self.core_queue = core_queue
self.input_buffer = []
self.authenticated = False
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
self.dispatcher = MpdDispatcher()
def start(self):
"""Start a new client session."""
@ -53,15 +51,7 @@ class MpdSession(asynchat.async_chat):
if response is not None:
self.send_response(response)
return
my_end, other_end = multiprocessing.Pipe()
self.core_queue.put({
'to': 'frontend',
'command': 'mpd_request',
'request': request,
'reply_to': pickle_connection(other_end),
})
my_end.poll(None)
response = my_end.recv()
response = self.dispatcher.handle_request(request)
if response is not None:
self.handle_response(response)

View File

@ -1,18 +0,0 @@
import asyncore
import logging
from mopidy.frontends.mpd.server import MpdServer
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.frontends.mpd.thread')
class MpdThread(BaseThread):
def __init__(self, core_queue):
super(MpdThread, self).__init__(core_queue)
self.name = u'MpdThread'
def run_inside_try(self):
logger.debug(u'Starting MPD server thread')
server = MpdServer(self.core_queue)
server.start()
asyncore.loop()

View File

@ -84,8 +84,9 @@ def artists_to_mpd_format(artists):
:type track: array of :class:`mopidy.models.Artist`
:rtype: string
"""
artists = list(artists)
artists.sort(key=lambda a: a.name)
return u', '.join([a.name for a in artists])
return u', '.join([a.name for a in artists if a.name])
def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None):
"""

View File

@ -1,12 +1,14 @@
import alsaaudio
import logging
from pykka.actor import ThreadingActor
from mopidy import settings
from mopidy.mixers.base import BaseMixer
logger = logging.getLogger('mopidy.mixers.alsa')
class AlsaMixer(BaseMixer):
class AlsaMixer(ThreadingActor, BaseMixer):
"""
Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control
volume.
@ -20,8 +22,10 @@ class AlsaMixer(BaseMixer):
- :attr:`mopidy.settings.MIXER_ALSA_CONTROL`
"""
def __init__(self, *args, **kwargs):
super(AlsaMixer, self).__init__(*args, **kwargs)
def __init__(self):
self._mixer = None
def on_start(self):
self._mixer = alsaaudio.Mixer(self._get_mixer_control())
assert self._mixer is not None

View File

@ -2,17 +2,12 @@ from mopidy import settings
class BaseMixer(object):
"""
:param backend: a backend instance
:type backend: :class:`mopidy.backends.base.Backend`
**Settings:**
- :attr:`mopidy.settings.MIXER_MAX_VOLUME`
"""
def __init__(self, backend, *args, **kwargs):
self.backend = backend
self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
@property
def volume(self):
@ -35,9 +30,6 @@ class BaseMixer(object):
volume = 100
self._set_volume(volume)
def destroy(self):
pass
def _get_volume(self):
"""
Return volume as integer in range [0, 100]. :class:`None` if unknown.

View File

@ -1,12 +1,13 @@
import logging
from threading import Lock
from pykka.actor import ThreadingActor
from mopidy import settings
from mopidy.mixers.base import BaseMixer
logger = logging.getLogger(u'mopidy.mixers.denon')
class DenonMixer(BaseMixer):
class DenonMixer(ThreadingActor, BaseMixer):
"""
Mixer for controlling Denon amplifiers and receivers using the RS-232
protocol.
@ -25,27 +26,19 @@ class DenonMixer(BaseMixer):
"""
def __init__(self, *args, **kwargs):
"""
Connects using the serial specifications from Denon's RS-232 Protocol
specification: 9600bps 8N1.
"""
super(DenonMixer, self).__init__(*args, **kwargs)
device = kwargs.get('device', None)
if device:
self._device = device
else:
from serial import Serial
self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
self._device = kwargs.get('device', None)
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
self._volume = 0
self._lock = Lock()
def on_start(self):
if self._device is None:
from serial import Serial
self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
def _get_volume(self):
self._lock.acquire()
self.ensure_open_device()
self._ensure_open_device()
self._device.write('MV?\r')
vol = str(self._device.readline()[2:4])
self._lock.release()
logger.debug(u'_get_volume() = %s' % vol)
return self._levels.index(vol)
@ -53,14 +46,12 @@ class DenonMixer(BaseMixer):
# Clamp according to Denon-spec
if volume > 99:
volume = 99
self._lock.acquire()
self.ensure_open_device()
self._ensure_open_device()
self._device.write('MV%s\r'% self._levels[volume])
vol = self._device.readline()[2:4]
self._lock.release()
self._volume = self._levels.index(vol)
def ensure_open_device(self):
def _ensure_open_device(self):
if not self._device.isOpen():
logger.debug(u'(re)connecting to Denon device')
self._device.open()

View File

@ -1,10 +1,11 @@
from pykka.actor import ThreadingActor
from mopidy.mixers.base import BaseMixer
class DummyMixer(BaseMixer):
class DummyMixer(ThreadingActor, BaseMixer):
"""Mixer which just stores and reports the chosen volume."""
def __init__(self, *args, **kwargs):
super(DummyMixer, self).__init__(*args, **kwargs)
def __init__(self):
self._volume = None
def _get_volume(self):

View File

@ -1,13 +1,22 @@
from mopidy.mixers.base import BaseMixer
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
class GStreamerSoftwareMixer(BaseMixer):
from mopidy.mixers.base import BaseMixer
from mopidy.outputs.base import BaseOutput
class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
"""Mixer which uses GStreamer to control volume in software."""
def __init__(self, *args, **kwargs):
super(GStreamerSoftwareMixer, self).__init__(*args, **kwargs)
def __init__(self):
self.output = None
def on_start(self):
output_refs = ActorRegistry.get_by_class(BaseOutput)
assert len(output_refs) == 1, 'Expected exactly one running output.'
self.output = output_refs[0].proxy()
def _get_volume(self):
return self.backend.output.get_volume()
return self.output.get_volume().get()
def _set_volume(self, volume):
self.backend.output.set_volume(volume)
self.output.set_volume(volume).get()

View File

@ -1,14 +1,14 @@
import logging
from serial import Serial
from multiprocessing import Pipe
import serial
from pykka.actor import ThreadingActor
from mopidy import settings
from mopidy.mixers.base import BaseMixer
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.mixers.nad')
class NadMixer(BaseMixer):
class NadMixer(ThreadingActor, BaseMixer):
"""
Mixer for controlling NAD amplifiers and receivers using the NAD RS-232
protocol.
@ -36,21 +36,19 @@ class NadMixer(BaseMixer):
"""
def __init__(self, *args, **kwargs):
super(NadMixer, self).__init__(*args, **kwargs)
self._volume = None
self._pipe, other_end = Pipe()
NadTalker(self.backend.core_queue, pipe=other_end).start()
def __init__(self):
self._volume_cache = None
self._nad_talker = NadTalker.start().proxy()
def _get_volume(self):
return self._volume
return self._volume_cache
def _set_volume(self, volume):
self._volume = volume
self._pipe.send({'command': 'set_volume', 'volume': volume})
self._volume_cache = volume
self._nad_talker.set_volume(volume)
class NadTalker(BaseThread):
class NadTalker(ThreadingActor):
"""
Independent process which does the communication with the NAD device.
@ -72,29 +70,20 @@ class NadTalker(BaseThread):
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
_nad_volume = None
def __init__(self, core_queue, pipe=None):
super(NadTalker, self).__init__(core_queue)
self.name = u'NadTalker'
self.pipe = pipe
def __init__(self):
self._device = None
def run_inside_try(self):
def on_start(self):
self._open_connection()
self._set_device_to_known_state()
while self.pipe.poll(None):
message = self.pipe.recv()
if message['command'] == 'set_volume':
self._set_volume(message['volume'])
elif message['command'] == 'reset_device':
self._set_device_to_known_state()
def _open_connection(self):
# Opens serial connection to the device.
# Communication settings: 115200 bps 8N1
logger.info(u'Connecting to serial device "%s"',
settings.MIXER_EXT_PORT)
self._device = Serial(port=settings.MIXER_EXT_PORT, baudrate=115200,
timeout=self.TIMEOUT)
self._device = serial.Serial(port=settings.MIXER_EXT_PORT,
baudrate=115200, timeout=self.TIMEOUT)
self._get_device_model()
def _set_device_to_known_state(self):
@ -164,7 +153,7 @@ class NadTalker(BaseThread):
self._nad_volume = 0
logger.info(u'Done calibrating NAD amplifier')
def _set_volume(self, volume):
def set_volume(self, volume):
# Increase or decrease the amplifier volume until it matches the given
# target volume.
logger.debug(u'Setting volume to %d' % volume)

View File

@ -1,9 +1,11 @@
from subprocess import Popen, PIPE
import time
from pykka.actor import ThreadingActor
from mopidy.mixers.base import BaseMixer
class OsaMixer(BaseMixer):
class OsaMixer(ThreadingActor, BaseMixer):
"""
Mixer which uses ``osascript`` on OS X to control volume.
@ -14,7 +16,6 @@ class OsaMixer(BaseMixer):
**Settings:**
- None
"""
CACHE_TTL = 30

View File

@ -1,5 +1,3 @@
from copy import copy
from mopidy.frontends.mpd import translator
class ImmutableObject(object):
@ -23,6 +21,17 @@ class ImmutableObject(object):
return super(ImmutableObject, self).__setattr__(name, value)
raise AttributeError('Object is immutable.')
def __repr__(self):
kwarg_pairs = []
for (key, value) in sorted(self.__dict__.items()):
if isinstance(value, (frozenset, tuple)):
value = list(value)
kwarg_pairs.append('%s=%s' % (key, repr(value)))
return '%(classname)s(%(kwargs)s)' % {
'classname': self.__class__.__name__,
'kwargs': ', '.join(kwarg_pairs),
}
def __hash__(self):
hash_sum = 0
for key, value in self.__dict__.items():
@ -65,6 +74,7 @@ class ImmutableObject(object):
% key)
return self.__class__(**data)
class Artist(ImmutableObject):
"""
:param uri: artist URI
@ -105,6 +115,9 @@ class Album(ImmutableObject):
#: The album name. Read-only.
name = None
#: A set of album artists. Read-only.
artists = frozenset()
#: The number of tracks in the album. Read-only.
num_tracks = 0
@ -112,14 +125,9 @@ class Album(ImmutableObject):
musicbrainz_id = None
def __init__(self, *args, **kwargs):
self._artists = frozenset(kwargs.pop('artists', []))
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
super(Album, self).__init__(*args, **kwargs)
@property
def artists(self):
"""List of :class:`Artist` elements. Read-only."""
return list(self._artists)
class Track(ImmutableObject):
"""
@ -149,6 +157,9 @@ class Track(ImmutableObject):
#: The track name. Read-only.
name = None
#: A set of track artists. Read-only.
artists = frozenset()
#: The track :class:`Album`. Read-only.
album = None
@ -168,14 +179,9 @@ class Track(ImmutableObject):
musicbrainz_id = None
def __init__(self, *args, **kwargs):
self._artists = frozenset(kwargs.pop('artists', []))
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
super(Track, self).__init__(*args, **kwargs)
@property
def artists(self):
"""List of :class:`Artist`. Read-only."""
return list(self._artists)
def mpd_format(self, *args, **kwargs):
return translator.track_to_mpd_format(self, *args, **kwargs)
@ -198,24 +204,22 @@ class Playlist(ImmutableObject):
#: The playlist name. Read-only.
name = None
#: The playlist's tracks. Read-only.
tracks = tuple()
#: The playlist modification time. Read-only.
#:
#: :class:`datetime.datetime`, or :class:`None` if unknown.
last_modified = None
def __init__(self, *args, **kwargs):
self._tracks = kwargs.pop('tracks', [])
self.__dict__['tracks'] = tuple(kwargs.pop('tracks', []))
super(Playlist, self).__init__(*args, **kwargs)
@property
def tracks(self):
"""List of :class:`Track` elements. Read-only."""
return copy(self._tracks)
@property
def length(self):
"""The number of tracks in the playlist. Read-only."""
return len(self._tracks)
return len(self.tracks)
def mpd_format(self, *args, **kwargs):
return translator.playlist_to_mpd_format(self, *args, **kwargs)

View File

@ -3,33 +3,6 @@ class BaseOutput(object):
Base class for audio outputs.
"""
def __init__(self, core_queue):
self.core_queue = core_queue
def start(self):
"""
Start the output.
*MAY be implemented by subclasses.*
"""
pass
def destroy(self):
"""
Destroy the output.
*MAY be implemented by subclasses.*
"""
pass
def process_message(self, message):
"""
Process messages with the output as destination.
*MUST be implemented by subclass.*
"""
raise NotImplementedError
def play_uri(self, uri):
"""
Play URI.

View File

@ -1,6 +1,8 @@
from pykka.actor import ThreadingActor
from mopidy.outputs.base import BaseOutput
class DummyOutput(BaseOutput):
class DummyOutput(ThreadingActor, BaseOutput):
"""
Audio output used for testing.
"""
@ -8,15 +10,6 @@ class DummyOutput(BaseOutput):
# pylint: disable = R0902
# Too many instance attributes (9/7)
#: For testing. :class:`True` if :meth:`start` has been called.
start_called = False
#: For testing. :class:`True` if :meth:`destroy` has been called.
destroy_called = False
#: For testing. Contains all messages :meth:`process_message` has received.
messages = []
#: For testing. Contains the last URI passed to :meth:`play_uri`.
uri = None
@ -40,15 +33,6 @@ class DummyOutput(BaseOutput):
#: For testing. Contains the current volume.
volume = 100
def start(self):
self.start_called = True
def destroy(self):
self.destroy_called = True
def process_message(self, message):
self.messages.append(message)
def play_uri(self, uri):
self.uri = uri
return True

View File

@ -3,113 +3,39 @@ pygst.require('0.10')
import gst
import logging
import multiprocessing
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy.backends.base import Backend
from mopidy.outputs.base import BaseOutput
from mopidy.utils.process import (BaseThread, pickle_connection,
unpickle_connection)
logger = logging.getLogger('mopidy.outputs.gstreamer')
class GStreamerOutput(BaseOutput):
class GStreamerOutput(ThreadingActor, BaseOutput):
"""
Audio output through GStreamer.
Starts :class:`GStreamerMessagesThread` and :class:`GStreamerPlayerThread`.
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
**Settings:**
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
"""
def __init__(self, *args, **kwargs):
super(GStreamerOutput, self).__init__(*args, **kwargs)
self.output_queue = multiprocessing.Queue()
self.player_thread = GStreamerPlayerThread(self.core_queue,
self.output_queue)
def start(self):
self.player_thread.start()
def destroy(self):
self.player_thread.destroy()
def process_message(self, message):
assert message['to'] == 'output', \
u'Message recipient must be "output".'
self.output_queue.put(message)
def _send_recv(self, message):
(my_end, other_end) = multiprocessing.Pipe()
message['to'] = 'output'
message['reply_to'] = pickle_connection(other_end)
self.process_message(message)
my_end.poll(None)
return my_end.recv()
def _send(self, message):
message['to'] = 'output'
self.process_message(message)
def play_uri(self, uri):
return self._send_recv({'command': 'play_uri', 'uri': uri})
def deliver_data(self, capabilities, data):
return self._send({
'command': 'deliver_data',
'caps': capabilities,
'data': data,
})
def end_of_data_stream(self):
return self._send({'command': 'end_of_data_stream'})
def get_position(self):
return self._send_recv({'command': 'get_position'})
def set_position(self, position):
return self._send_recv({'command': 'set_position',
'position': position})
def set_state(self, state):
return self._send_recv({'command': 'set_state', 'state': state})
def get_volume(self):
return self._send_recv({'command': 'get_volume'})
def set_volume(self, volume):
return self._send_recv({'command': 'set_volume', 'volume': volume})
class GStreamerPlayerThread(BaseThread):
"""
A process for all work related to GStreamer.
The main loop processes events from both Mopidy and GStreamer.
This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be
running too. This is not enforced in any way by the code.
Make sure this subprocess is started by the MainThread in the top-most
parent process, and not some other thread. If not, we can get into the
problems described at
http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html.
"""
def __init__(self, core_queue, output_queue):
super(GStreamerPlayerThread, self).__init__(core_queue)
self.name = u'GStreamerPlayerThread'
self.output_queue = output_queue
def __init__(self):
self.gst_pipeline = None
def run_inside_try(self):
self.setup()
while True:
message = self.output_queue.get()
self.process_mopidy_message(message)
def on_start(self):
self._setup_gstreamer()
def _setup_gstreamer(self):
"""
**Warning:** :class:`GStreamerOutput` requires
:class:`mopidy.utils.process.GObjectEventThread` to be running. This is
not enforced by :class:`GStreamerOutput` itself.
"""
def setup(self):
logger.debug(u'Setting up GStreamer pipeline')
self.gst_pipeline = gst.parse_launch(' ! '.join([
@ -120,78 +46,36 @@ class GStreamerPlayerThread(BaseThread):
pad = self.gst_pipeline.get_by_name('convert').get_pad('sink')
if settings.BACKENDS[0] == 'mopidy.backends.local.LocalBackend':
uri_bin = gst.element_factory_make('uridecodebin', 'uri')
uri_bin.connect('pad-added', self.process_new_pad, pad)
self.gst_pipeline.add(uri_bin)
else:
app_src = gst.element_factory_make('appsrc', 'appsrc')
app_src_caps = gst.Caps("""
audio/x-raw-int,
endianness=(int)1234,
channels=(int)2,
width=(int)16,
depth=(int)16,
signed=(boolean)true,
rate=(int)44100""")
app_src.set_property('caps', app_src_caps)
self.gst_pipeline.add(app_src)
app_src.get_pad('src').link(pad)
uridecodebin = gst.element_factory_make('uridecodebin', 'uri')
uridecodebin.connect('pad-added', self._process_new_pad, pad)
self.gst_pipeline.add(uridecodebin)
# Setup bus and message processor
gst_bus = self.gst_pipeline.get_bus()
gst_bus.add_signal_watch()
gst_bus.connect('message', self.process_gst_message)
gst_bus.connect('message', self._process_gstreamer_message)
def process_new_pad(self, source, pad, target_pad):
def _process_new_pad(self, source, pad, target_pad):
pad.link(target_pad)
def process_mopidy_message(self, message):
"""Process messages from the rest of Mopidy."""
if message['command'] == 'play_uri':
response = self.play_uri(message['uri'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
elif message['command'] == 'deliver_data':
self.deliver_data(message['caps'], message['data'])
elif message['command'] == 'end_of_data_stream':
self.end_of_data_stream()
elif message['command'] == 'set_state':
response = self.set_state(message['state'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
elif message['command'] == 'get_volume':
volume = self.get_volume()
connection = unpickle_connection(message['reply_to'])
connection.send(volume)
elif message['command'] == 'set_volume':
response = self.set_volume(message['volume'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
elif message['command'] == 'set_position':
response = self.set_position(message['position'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
elif message['command'] == 'get_position':
response = self.get_position()
connection = unpickle_connection(message['reply_to'])
connection.send(response)
else:
logger.warning(u'Cannot handle message: %s', message)
def process_gst_message(self, bus, message):
def _process_gstreamer_message(self, bus, message):
"""Process messages from GStreamer."""
if message.type == gst.MESSAGE_EOS:
logger.debug(u'GStreamer signalled end-of-stream. '
'Sending end_of_track to core_queue ...')
self.core_queue.put({'command': 'end_of_track'})
'Telling backend ...')
self._get_backend().playback.on_end_of_track()
elif message.type == gst.MESSAGE_ERROR:
self.set_state('NULL')
error, debug = message.parse_error()
logger.error(u'%s %s', error, debug)
# FIXME Should we send 'stop_playback' to core here? Can we
# FIXME Should we send 'stop_playback' to the backend here? Can we
# differentiate on how serious the error is?
def _get_backend(self):
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
return backend_refs[0].proxy()
def play_uri(self, uri):
"""Play audio at URI"""
self.set_state('READY')
@ -200,12 +84,12 @@ class GStreamerPlayerThread(BaseThread):
def deliver_data(self, caps_string, data):
"""Deliver audio data to be played"""
app_src = self.gst_pipeline.get_by_name('appsrc')
source = self.gst_pipeline.get_by_name('source')
caps = gst.caps_from_string(caps_string)
buffer_ = gst.Buffer(buffer(data))
buffer_.set_caps(caps)
app_src.set_property('caps', caps)
app_src.emit('push-buffer', buffer_)
source.set_property('caps', caps)
source.emit('push-buffer', buffer_)
def end_of_data_stream(self):
"""
@ -214,7 +98,22 @@ class GStreamerPlayerThread(BaseThread):
We will get a GStreamer message when the stream playback reaches the
token, and can then do any end-of-stream related tasks.
"""
self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream')
self.gst_pipeline.get_by_name('source').emit('end-of-stream')
def get_position(self):
try:
position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0]
return position // gst.MSECOND
except gst.QueryError, e:
logger.error('time_position failed: %s', e)
return 0
def set_position(self, position):
self.gst_pipeline.get_state() # block until state changes are done
handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
self.gst_pipeline.get_state() # block until seek is done
return handeled
def set_state(self, state_name):
"""
@ -252,18 +151,3 @@ class GStreamerPlayerThread(BaseThread):
gst_volume = self.gst_pipeline.get_by_name('volume')
gst_volume.set_property('volume', volume / 100.0)
return True
def set_position(self, position):
self.gst_pipeline.get_state() # block until state changes are done
handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
self.gst_pipeline.get_state() # block until seek is done
return handeled
def get_position(self):
try:
position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0]
return position // gst.MSECOND
except gst.QueryError, e:
logger.error('time_position failed: %s', e)
return 0

View File

@ -1,13 +1,17 @@
import logging
import logging.handlers
import platform
from mopidy import settings
from mopidy import get_version, get_platform, get_python, settings
def setup_logging(verbosity_level, save_debug_log):
setup_root_logger()
setup_console_logging(verbosity_level)
if save_debug_log:
setup_debug_logging_to_file()
logger = logging.getLogger('mopidy.utils.log')
logger.info(u'Starting Mopidy %s on %s %s',
get_version(), get_platform(), get_python())
def setup_root_logger():
root = logging.getLogger('')

View File

@ -1,8 +1,5 @@
import logging
import multiprocessing
import multiprocessing.dummy
from multiprocessing.reduction import reduce_connection
import pickle
import threading
import gobject
gobject.threads_init()
@ -11,52 +8,10 @@ from mopidy import SettingsError
logger = logging.getLogger('mopidy.utils.process')
def pickle_connection(connection):
return pickle.dumps(reduce_connection(connection))
def unpickle_connection(pickled_connection):
# From http://stackoverflow.com/questions/1446004
(func, args) = pickle.loads(pickled_connection)
return func(*args)
class BaseProcess(multiprocessing.Process):
def __init__(self, core_queue):
super(BaseProcess, self).__init__()
self.core_queue = core_queue
def run(self):
logger.debug(u'%s: Starting process', self.name)
try:
self.run_inside_try()
except KeyboardInterrupt:
logger.info(u'Interrupted by user')
self.exit(0, u'Interrupted by user')
except SettingsError as e:
logger.error(e.message)
self.exit(1, u'Settings error')
except ImportError as e:
logger.error(e)
self.exit(2, u'Import error')
except Exception as e:
logger.exception(e)
self.exit(3, u'Unknown error')
def run_inside_try(self):
raise NotImplementedError
def destroy(self):
self.terminate()
def exit(self, status=0, reason=None):
self.core_queue.put({'to': 'core', 'command': 'exit',
'status': status, 'reason': reason})
self.destroy()
class BaseThread(multiprocessing.dummy.Process):
def __init__(self, core_queue):
class BaseThread(threading.Thread):
def __init__(self):
super(BaseThread, self).__init__()
self.core_queue = core_queue
# No thread should block process from exiting
self.daemon = True
@ -84,8 +39,6 @@ class BaseThread(multiprocessing.dummy.Process):
pass
def exit(self, status=0, reason=None):
self.core_queue.put({'to': 'core', 'command': 'exit',
'status': status, 'reason': reason})
self.destroy()
@ -98,8 +51,8 @@ class GObjectEventThread(BaseThread):
:mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
"""
def __init__(self, core_queue):
super(GObjectEventThread, self).__init__(core_queue)
def __init__(self):
super(GObjectEventThread, self).__init__()
self.name = u'GObjectEventThread'
self.loop = None

View File

@ -141,8 +141,7 @@ def list_settings_optparse_callback(*args):
lines = []
for (key, value) in sorted(settings.current.iteritems()):
default_value = settings.default.get(key)
if key.endswith('PASSWORD') and len(value):
value = u'********'
value = mask_value_if_secret(key, value)
lines.append(u'%s:' % key)
lines.append(u' Value: %s' % repr(value))
if value != default_value and default_value is not None:
@ -151,3 +150,9 @@ def list_settings_optparse_callback(*args):
lines.append(u' Error: %s' % errors[key])
print u'Settings: %s' % indent('\n'.join(lines), places=2)
sys.exit(0)
def mask_value_if_secret(key, value):
if key.endswith('PASSWORD') and value:
return u'********'
else:
return value

1
requirements/core.txt Normal file
View File

@ -0,0 +1 @@
Pykka >= 0.12

View File

@ -1,2 +1,4 @@
coverage
mock
nose
tox

View File

@ -1,7 +1,9 @@
import os
try: # 2.7
# pylint: disable = E0611,F0401
from unittest.case import SkipTest
# pylint: enable = E0611,F0401
except ImportError:
try: # Nose
from nose.plugins.skip import SkipTest
@ -14,9 +16,9 @@ from mopidy import settings
# Nuke any local settings to ensure same test env all over
settings.local.clear()
def data_folder(name):
folder = os.path.dirname(__file__)
folder = os.path.join(folder, 'data')
folder = os.path.abspath(folder)
return os.path.join(folder, name)
def path_to_data_dir(name):
path = os.path.dirname(__file__)
path = os.path.join(path, 'data')
path = os.path.abspath(path)
return os.path.join(path, name)

View File

@ -1,11 +1,9 @@
import mock
import multiprocessing
import random
from mopidy import settings
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.outputs.dummy import DummyOutput
from mopidy.utils import get_class
from mopidy.outputs.base import BaseOutput
from tests.backends.base import populate_playlist
@ -13,19 +11,13 @@ class CurrentPlaylistControllerTest(object):
tracks = []
def setUp(self):
self.core_queue = multiprocessing.Queue()
self.output = DummyOutput(self.core_queue)
self.backend = self.backend_class(
self.core_queue, self.output, DummyMixer)
self.backend = self.backend_class()
self.backend.output = mock.Mock(spec=BaseOutput)
self.controller = self.backend.current_playlist
self.playback = self.backend.playback
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
def tearDown(self):
self.backend.destroy()
self.output.destroy()
def test_add(self):
for track in self.tracks:
cp_track = self.controller.add(track)

View File

@ -1,7 +1,6 @@
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track, Album, Artist
from tests import SkipTest, data_folder
from tests import SkipTest, path_to_data_dir
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
@ -9,18 +8,15 @@ class LibraryControllerTest(object):
Album(name='album2', artists=artists[1:2]),
Album()]
tracks = [Track(name='track1', length=4000, artists=artists[:1],
album=albums[0], uri='file://' + data_folder('uri1')),
album=albums[0], uri='file://' + path_to_data_dir('uri1')),
Track(name='track2', length=4000, artists=artists[1:2],
album=albums[1], uri='file://' + data_folder('uri2')),
album=albums[1], uri='file://' + path_to_data_dir('uri2')),
Track()]
def setUp(self):
self.backend = self.backend_class(mixer_class=DummyMixer)
self.backend = self.backend_class()
self.library = self.backend.library
def tearDown(self):
self.backend.destroy()
def test_refresh(self):
self.library.refresh()

View File

@ -1,12 +1,10 @@
import mock
import multiprocessing
import random
import time
from mopidy import settings
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
from mopidy.outputs.dummy import DummyOutput
from mopidy.utils import get_class
from mopidy.outputs.base import BaseOutput
from tests import SkipTest
from tests.backends.base import populate_playlist
@ -17,10 +15,8 @@ class PlaybackControllerTest(object):
tracks = []
def setUp(self):
self.core_queue = multiprocessing.Queue()
self.output = DummyOutput(self.core_queue)
self.backend = self.backend_class(
self.core_queue, self.output, DummyMixer)
self.backend = self.backend_class()
self.backend.output = mock.Mock(spec=BaseOutput)
self.playback = self.backend.playback
self.current_playlist = self.backend.current_playlist
@ -29,10 +25,6 @@ class PlaybackControllerTest(object):
assert self.tracks[0].length >= 2000, \
'First song needs to be at least 2000 miliseconds'
def tearDown(self):
self.backend.destroy()
self.output.destroy()
def test_initial_state_is_stopped(self):
self.assertEqual(self.playback.state, self.playback.STOPPED)
@ -212,7 +204,7 @@ class PlaybackControllerTest(object):
def test_next_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
for track in self.tracks:
for _ in self.tracks:
self.playback.next()
self.assertEqual(self.playback.current_track, None)
@ -258,7 +250,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_next_track_at_end_of_playlist(self):
self.playback.play()
for track in self.current_playlist.cp_tracks[1:]:
for _ in self.current_playlist.cp_tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.track_at_next, None)
@ -266,7 +258,7 @@ class PlaybackControllerTest(object):
def test_next_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
for track in self.tracks[1:]:
for _ in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@ -348,7 +340,7 @@ class PlaybackControllerTest(object):
def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
for track in self.tracks:
for _ in self.tracks:
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, None)
@ -394,7 +386,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_end_of_track_track_at_end_of_playlist(self):
self.playback.play()
for track in self.current_playlist.cp_tracks[1:]:
for _ in self.current_playlist.cp_tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.track_at_next, None)
@ -402,7 +394,7 @@ class PlaybackControllerTest(object):
def test_end_of_track_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
for track in self.tracks[1:]:
for _ in self.tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@ -466,7 +458,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_previous_track_with_consume(self):
self.playback.consume = True
for track in self.tracks:
for _ in self.tracks:
self.playback.next()
self.assertEqual(self.playback.track_at_previous,
self.playback.current_track)
@ -474,7 +466,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_previous_track_with_random(self):
self.playback.random = True
for track in self.tracks:
for _ in self.tracks:
self.playback.next()
self.assertEqual(self.playback.track_at_previous,
self.playback.current_track)
@ -547,7 +539,6 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_on_current_playlist_change_when_stopped(self):
current_track = self.playback.current_track
self.backend.current_playlist.append([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@ -677,9 +668,10 @@ class PlaybackControllerTest(object):
self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.PLAYING)
@SkipTest
@populate_playlist
def test_seek_beyond_end_of_song(self):
raise SkipTest # FIXME need to decide return value
# FIXME need to decide return value
self.playback.play()
result = self.playback.seek(self.tracks[0].length*100)
self.assert_(not result, 'Seek return value was %s' % result)
@ -696,9 +688,10 @@ class PlaybackControllerTest(object):
self.playback.seek(self.current_playlist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, self.playback.STOPPED)
@SkipTest
@populate_playlist
def test_seek_beyond_start_of_song(self):
raise SkipTest # FIXME need to decide return value
# FIXME need to decide return value
self.playback.play()
result = self.playback.seek(-1000)
self.assert_(not result, 'Seek return value was %s' % result)
@ -734,10 +727,18 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.stop(), None)
def test_time_position_when_stopped(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
self.backend.output.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
@populate_playlist
def test_time_position_when_stopped_with_playlist(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
self.backend.output.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
@SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput
@ -770,7 +771,7 @@ class PlaybackControllerTest(object):
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
self.playback.consume = True
self.playback.play()
for i in range(len(self.backend.current_playlist.tracks)):
for _ in range(len(self.backend.current_playlist.tracks)):
self.playback.on_end_of_track()
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
@ -824,14 +825,14 @@ class PlaybackControllerTest(object):
def test_random_until_end_of_playlist(self):
self.playback.random = True
self.playback.play()
for track in self.tracks[1:]:
for _ in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.track_at_next, None)
@populate_playlist
def test_random_until_end_of_playlist_and_play_from_start(self):
self.playback.repeat = True
for track in self.tracks:
for _ in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.track_at_next, None)
self.assertEqual(self.playback.state, self.playback.STOPPED)
@ -843,7 +844,7 @@ class PlaybackControllerTest(object):
self.playback.repeat = True
self.playback.random = True
self.playback.play()
for track in self.tracks:
for _ in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.track_at_next, None)
@ -852,7 +853,7 @@ class PlaybackControllerTest(object):
self.playback.random = True
self.playback.play()
played = []
for track in self.tracks:
for _ in self.tracks:
self.assert_(self.playback.current_track not in played)
played.append(self.playback.current_track)
self.playback.next()

View File

@ -3,23 +3,20 @@ import shutil
import tempfile
from mopidy import settings
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist
from tests import SkipTest, data_folder
from tests import SkipTest, path_to_data_dir
class StoredPlaylistsControllerTest(object):
def setUp(self):
settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
settings.LOCAL_MUSIC_PATH = data_folder('')
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
self.backend = self.backend_class(mixer_class=DummyMixer)
self.backend = self.backend_class()
self.stored = self.backend.stored_playlists
def tearDown(self):
self.backend.destroy()
if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)

View File

@ -1,6 +1,6 @@
from mopidy.utils.path import path_to_uri
from tests import data_folder
from tests import path_to_data_dir
song = data_folder('song%s.wav')
song = path_to_data_dir('song%s.wav')
generate_song = lambda i: path_to_uri(song % i)

View File

@ -9,7 +9,7 @@ if sys.platform == 'win32':
from mopidy import settings
from mopidy.backends.local import LocalBackend
from tests import data_folder
from tests import path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
@ -17,8 +17,8 @@ class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
def setUp(self):
settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
settings.LOCAL_MUSIC_PATH = data_folder('')
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
super(LocalLibraryControllerTest, self).setUp()

View File

@ -11,7 +11,7 @@ from mopidy.backends.local import LocalBackend
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
from tests import data_folder
from tests import path_to_data_dir
from tests.backends.base.playback import PlaybackControllerTest
from tests.backends.local import generate_song
@ -32,7 +32,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
settings.runtime.clear()
def add_track(self, path):
uri = path_to_uri(data_folder(path))
uri = path_to_uri(path_to_data_dir(path))
track = Track(uri=uri, length=4464)
self.backend.current_playlist.add(track)

View File

@ -14,7 +14,7 @@ from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.utils.path import path_to_uri
from tests import data_folder
from tests import path_to_data_dir
from tests.backends.base.stored_playlists import \
StoredPlaylistsControllerTest
from tests.backends.local import generate_song
@ -65,13 +65,12 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
self.assertEqual(uri, contents.strip())
def test_playlists_are_loaded_at_startup(self):
track = Track(uri=path_to_uri(data_folder('uri2')))
track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
playlist = Playlist(tracks=[track], name='test')
self.stored.save(playlist)
self.backend.destroy()
self.backend = self.backend_class(mixer_class=DummyMixer)
self.backend = self.backend_class()
self.stored = self.backend.stored_playlists
self.assert_(self.stored.playlists)

View File

@ -8,26 +8,26 @@ from mopidy.utils.path import path_to_uri
from mopidy.backends.local.translator import parse_m3u, parse_mpd_tag_cache
from mopidy.models import Track, Artist, Album
from tests import SkipTest, data_folder
from tests import SkipTest, path_to_data_dir
song1_path = data_folder('song1.mp3')
song2_path = data_folder('song2.mp3')
encoded_path = data_folder(u'æøå.mp3')
song1_path = path_to_data_dir('song1.mp3')
song2_path = path_to_data_dir('song2.mp3')
encoded_path = path_to_data_dir(u'æøå.mp3')
song1_uri = path_to_uri(song1_path)
song2_uri = path_to_uri(song2_path)
encoded_uri = path_to_uri(encoded_path)
class M3UToUriTest(unittest.TestCase):
def test_empty_file(self):
uris = parse_m3u(data_folder('empty.m3u'))
uris = parse_m3u(path_to_data_dir('empty.m3u'))
self.assertEqual([], uris)
def test_basic_file(self):
uris = parse_m3u(data_folder('one.m3u'))
uris = parse_m3u(path_to_data_dir('one.m3u'))
self.assertEqual([song1_uri], uris)
def test_file_with_comment(self):
uris = parse_m3u(data_folder('comment.m3u'))
uris = parse_m3u(path_to_data_dir('comment.m3u'))
self.assertEqual([song1_uri], uris)
def test_file_with_absolute_files(self):
@ -64,11 +64,11 @@ class M3UToUriTest(unittest.TestCase):
os.remove(tmp.name)
def test_encoding_is_latin1(self):
uris = parse_m3u(data_folder('encoding.m3u'))
uris = parse_m3u(path_to_data_dir('encoding.m3u'))
self.assertEqual([encoded_uri], uris)
def test_open_missing_file(self):
uris = parse_m3u(data_folder('non-existant.m3u'))
uris = parse_m3u(path_to_data_dir('non-existant.m3u'))
self.assertEqual([], uris)
@ -81,7 +81,7 @@ expected_albums = [Album(name='albumname', artists=expected_artists,
expected_tracks = []
def generate_track(path, ident):
uri = path_to_uri(data_folder(path))
uri = path_to_uri(path_to_data_dir(path))
track = Track(name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], length=4000, uri=uri)
expected_tracks.append(track)
@ -98,28 +98,28 @@ generate_track('subdir1/subsubdir/song9.mp3', 1)
class MPDTagCacheToTracksTest(unittest.TestCase):
def test_emtpy_cache(self):
tracks = parse_mpd_tag_cache(data_folder('empty_tag_cache'),
data_folder(''))
tracks = parse_mpd_tag_cache(path_to_data_dir('empty_tag_cache'),
path_to_data_dir(''))
self.assertEqual(set(), tracks)
def test_simple_cache(self):
tracks = parse_mpd_tag_cache(data_folder('simple_tag_cache'),
data_folder(''))
uri = path_to_uri(data_folder('song1.mp3'))
tracks = parse_mpd_tag_cache(path_to_data_dir('simple_tag_cache'),
path_to_data_dir(''))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
track = Track(name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], length=4000, uri=uri)
self.assertEqual(set([track]), tracks)
def test_advanced_cache(self):
tracks = parse_mpd_tag_cache(data_folder('advanced_tag_cache'),
data_folder(''))
tracks = parse_mpd_tag_cache(path_to_data_dir('advanced_tag_cache'),
path_to_data_dir(''))
self.assertEqual(set(expected_tracks), tracks)
def test_unicode_cache(self):
tracks = parse_mpd_tag_cache(data_folder('utf8_tag_cache'),
data_folder(''))
tracks = parse_mpd_tag_cache(path_to_data_dir('utf8_tag_cache'),
path_to_data_dir(''))
uri = path_to_uri(data_folder('song1.mp3'))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
artists = [Artist(name=u'æøå')]
album = Album(name=u'æøå', artists=artists)
track = Track(uri=uri, name=u'æøå', artists=artists,
@ -132,14 +132,14 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
raise SkipTest
def test_cache_with_blank_track_info(self):
tracks = parse_mpd_tag_cache(data_folder('blank_tag_cache'),
data_folder(''))
uri = path_to_uri(data_folder('song1.mp3'))
tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'),
path_to_data_dir(''))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
self.assertEqual(set([Track(uri=uri, length=4000)]), tracks)
def test_musicbrainz_tagcache(self):
tracks = parse_mpd_tag_cache(data_folder('musicbrainz_tag_cache'),
data_folder(''))
tracks = parse_mpd_tag_cache(path_to_data_dir('musicbrainz_tag_cache'),
path_to_data_dir(''))
artist = list(expected_tracks[0].artists)[0].copy(
musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897')
albumartist = list(expected_tracks[0].artists)[0].copy(
@ -153,9 +153,9 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
self.assertEqual(track, list(tracks)[0])
def test_albumartist_tag_cache(self):
tracks = parse_mpd_tag_cache(data_folder('albumartist_tag_cache'),
data_folder(''))
uri = path_to_uri(data_folder('song1.mp3'))
tracks = parse_mpd_tag_cache(path_to_data_dir('albumartist_tag_cache'),
path_to_data_dir(''))
uri = path_to_uri(path_to_data_dir('song1.mp3'))
artist = Artist(name='albumartistname')
album = expected_albums[0].copy(artists=[artist])
track = Track(name='trackname', artists=expected_artists, track_no=1,

View File

@ -1 +0,0 @@
../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
../../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
../../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
../../../sample.mp3

View File

@ -1 +0,0 @@
../../../sample.mp3

View File

@ -1 +0,0 @@
../../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
../../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
../sample.mp3

Binary file not shown.

View File

@ -1 +0,0 @@
blank.flac

BIN
tests/data/song1.flac Normal file

Binary file not shown.

View File

@ -1 +0,0 @@
blank.mp3

BIN
tests/data/song1.mp3 Normal file

Binary file not shown.

View File

@ -1 +0,0 @@
blank.ogg

BIN
tests/data/song1.ogg Normal file

Binary file not shown.

View File

@ -1 +0,0 @@
blank.wav

BIN
tests/data/song1.wav Normal file

Binary file not shown.

View File

@ -1 +0,0 @@
blank.flac

BIN
tests/data/song2.flac Normal file

Binary file not shown.

View File

@ -1 +0,0 @@
blank.mp3

BIN
tests/data/song2.mp3 Normal file

Binary file not shown.

View File

@ -1 +0,0 @@
blank.ogg

BIN
tests/data/song2.ogg Normal file

Binary file not shown.

View File

@ -1 +0,0 @@
blank.wav

Some files were not shown because too many files have changed in this diff Show More