Release v0.4.0
This commit is contained in:
commit
8df4505b97
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,11 +2,12 @@
|
||||
*.swp
|
||||
.coverage
|
||||
.noseids
|
||||
.tox
|
||||
MANIFEST
|
||||
build/
|
||||
cover/
|
||||
coverage.xml
|
||||
dist/
|
||||
docs/_build/
|
||||
mopidy.log
|
||||
mopidy.log*
|
||||
nosetests.xml
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
if __name__ == '__main__':
|
||||
from mopidy.__main__ import main
|
||||
from mopidy.core import main
|
||||
main()
|
||||
|
||||
101
docs/changes.rst
101
docs/changes.rst
@ -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**
|
||||
|
||||
@ -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
|
||||
=================
|
||||
|
||||
@ -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-')}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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=[])
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
154
mopidy/core.py
154
mopidy/core.py
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -34,6 +34,6 @@ def outputs(frontend):
|
||||
"""
|
||||
return [
|
||||
('outputid', 0),
|
||||
('outputname', frontend.backend.__class__.__name__),
|
||||
('outputname', None),
|
||||
('outputenabled', 1),
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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])"$')
|
||||
|
||||
@ -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()]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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('')
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
1
requirements/core.txt
Normal file
@ -0,0 +1 @@
|
||||
Pykka >= 0.12
|
||||
@ -1,2 +1,4 @@
|
||||
coverage
|
||||
mock
|
||||
nose
|
||||
tox
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1 +0,0 @@
|
||||
../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/song1.mp3
Normal file
BIN
tests/data/scanner/advanced/song1.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/song2.mp3
Normal file
BIN
tests/data/scanner/advanced/song2.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/song3.mp3
Normal file
BIN
tests/data/scanner/advanced/song3.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/subdir1/song4.mp3
Normal file
BIN
tests/data/scanner/advanced/subdir1/song4.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/subdir1/song5.mp3
Normal file
BIN
tests/data/scanner/advanced/subdir1/song5.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../../../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
Normal file
BIN
tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../../../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
Normal file
BIN
tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/subdir2/song6.mp3
Normal file
BIN
tests/data/scanner/advanced/subdir2/song6.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../../sample.mp3
|
||||
BIN
tests/data/scanner/advanced/subdir2/song7.mp3
Normal file
BIN
tests/data/scanner/advanced/subdir2/song7.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
../sample.mp3
|
||||
BIN
tests/data/scanner/simple/song1.mp3
Normal file
BIN
tests/data/scanner/simple/song1.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.flac
|
||||
BIN
tests/data/song1.flac
Normal file
BIN
tests/data/song1.flac
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.mp3
|
||||
BIN
tests/data/song1.mp3
Normal file
BIN
tests/data/song1.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.ogg
|
||||
BIN
tests/data/song1.ogg
Normal file
BIN
tests/data/song1.ogg
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.wav
|
||||
BIN
tests/data/song1.wav
Normal file
BIN
tests/data/song1.wav
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.flac
|
||||
BIN
tests/data/song2.flac
Normal file
BIN
tests/data/song2.flac
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.mp3
|
||||
BIN
tests/data/song2.mp3
Normal file
BIN
tests/data/song2.mp3
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.ogg
|
||||
BIN
tests/data/song2.ogg
Normal file
BIN
tests/data/song2.ogg
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
blank.wav
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user