Release v0.4.0
This commit is contained in:
commit
8df4505b97
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,11 +2,12 @@
|
|||||||
*.swp
|
*.swp
|
||||||
.coverage
|
.coverage
|
||||||
.noseids
|
.noseids
|
||||||
|
.tox
|
||||||
MANIFEST
|
MANIFEST
|
||||||
build/
|
build/
|
||||||
cover/
|
cover/
|
||||||
coverage.xml
|
coverage.xml
|
||||||
dist/
|
dist/
|
||||||
docs/_build/
|
docs/_build/
|
||||||
mopidy.log
|
mopidy.log*
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from mopidy.__main__ import main
|
from mopidy.core import main
|
||||||
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.
|
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)
|
0.3.1 (2010-01-22)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
@ -53,7 +152,7 @@ to this problem.
|
|||||||
:doc:`/installation/libspotify/`.
|
:doc:`/installation/libspotify/`.
|
||||||
|
|
||||||
- If you use the Last.fm frontend, you need to upgrade to pylast 0.5.7. Run
|
- 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**
|
**Changes**
|
||||||
|
|||||||
@ -31,6 +31,9 @@ ncmpcpp
|
|||||||
A console client that generally works well with Mopidy, and is regularly used
|
A console client that generally works well with Mopidy, and is regularly used
|
||||||
by Mopidy developers.
|
by Mopidy developers.
|
||||||
|
|
||||||
|
Search
|
||||||
|
^^^^^^
|
||||||
|
|
||||||
Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the
|
Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the
|
||||||
three search modes:
|
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
|
If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp
|
||||||
from `Launchpad <https://launchpad.net/ubuntu/+source/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
|
Graphical clients
|
||||||
=================
|
=================
|
||||||
|
|||||||
@ -202,4 +202,4 @@ latex_documents = [
|
|||||||
|
|
||||||
needs_sphinx = '1.0'
|
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
|
To run tests, you need a couple of dependencies. They can be installed through
|
||||||
Debian/Ubuntu package management::
|
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``::
|
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::
|
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
|
Continuous integration server
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
We run a continuous integration server called Hudson at
|
We run a continuous integration (CI) server at http://ci.mopidy.com/ that runs
|
||||||
http://hudson.mopidy.com/ that runs all test on multiple platforms (Ubuntu, OS
|
all test on multiple platforms (Ubuntu, OS X, etc.) for every commit we push to
|
||||||
X, etc.) for every commit we push to GitHub.
|
GitHub.
|
||||||
|
|
||||||
In addition to running tests, Hudson also does coverage statistics and uses
|
In addition to running tests, the CI server also gathers coverage statistics
|
||||||
pylint to check for errors and possible improvements in our code. So, if you're
|
and uses pylint to check for errors and possible improvements in our code. So,
|
||||||
out of work, the code coverage and pylint data in Hudson should give you a
|
if you're out of work, the code coverage and pylint data at the CI server
|
||||||
place to start.
|
should give you a place to start.
|
||||||
|
|
||||||
|
|
||||||
Writing documentation
|
Writing documentation
|
||||||
|
|||||||
@ -25,6 +25,8 @@ Otherwise, make sure you got the required dependencies installed.
|
|||||||
|
|
||||||
- Python >= 2.6, < 3
|
- Python >= 2.6, < 3
|
||||||
|
|
||||||
|
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12
|
||||||
|
|
||||||
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
|
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
|
||||||
|
|
||||||
- Mixer dependencies: The default mixer does not require any additional
|
- 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
|
http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source
|
||||||
on your installation. Then, simply run::
|
on your installation. Then, simply run::
|
||||||
|
|
||||||
sudo apt-get install libspotify6
|
sudo apt-get install libspotify7
|
||||||
|
|
||||||
When libspotify has been installed, continue with
|
When libspotify has been installed, continue with
|
||||||
:ref:`pyspotify_installation`.
|
:ref:`pyspotify_installation`.
|
||||||
@ -39,14 +39,14 @@ When libspotify has been installed, continue with
|
|||||||
On Linux from source
|
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/.
|
https://developer.spotify.com/en/libspotify/.
|
||||||
|
|
||||||
For 64-bit Linux the process is as follows::
|
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
|
wget http://developer.spotify.com/download/libspotify/libspotify-0.0.7-linux6-x86_64.tar.gz
|
||||||
tar zxfv libspotify-0.0.6-linux6-x86_64.tar.gz
|
tar zxfv libspotify-0.0.7-linux6-x86_64.tar.gz
|
||||||
cd libspotify-0.0.6-linux6-x86_64/
|
cd libspotify-0.0.7-linux6-x86_64/
|
||||||
sudo make install prefix=/usr/local
|
sudo make install prefix=/usr/local
|
||||||
sudo ldconfig
|
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
|
It is important that you install pyspotify from the ``mopidy`` branch of the
|
||||||
``mopidy/pyspotify`` repository, as the upstream repository at
|
``mopidy/pyspotify`` repository, as the upstream repository at
|
||||||
``winjer/pyspotify`` is not updated with changes needed to support e.g.
|
``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
|
import sys
|
||||||
if not (2, 6) <= sys.version_info < (3,):
|
if not (2, 6) <= sys.version_info < (3,):
|
||||||
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
|
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
|
||||||
|
|
||||||
|
from subprocess import PIPE, Popen
|
||||||
|
|
||||||
|
VERSION = (0, 4, 0)
|
||||||
|
|
||||||
def get_version():
|
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):
|
class MopidyException(Exception):
|
||||||
def __init__(self, message, *args, **kwargs):
|
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
|
# Add ../ to the path so we can run Mopidy from a Git checkout without
|
||||||
# installing it on the system.
|
# installing it on the system.
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
sys.path.insert(0,
|
sys.path.insert(0,
|
||||||
os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
|
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__':
|
if __name__ == '__main__':
|
||||||
|
from mopidy.core import main
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -1,12 +1,4 @@
|
|||||||
from copy import copy
|
|
||||||
import logging
|
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 .current_playlist import CurrentPlaylistController
|
||||||
from .library import LibraryController, BaseLibraryProvider
|
from .library import LibraryController, BaseLibraryProvider
|
||||||
@ -17,30 +9,6 @@ from .stored_playlists import (StoredPlaylistsController,
|
|||||||
logger = logging.getLogger('mopidy.backends.base')
|
logger = logging.getLogger('mopidy.backends.base')
|
||||||
|
|
||||||
class Backend(object):
|
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
|
#: The current playlist controller. An instance of
|
||||||
#: :class:`mopidy.backends.base.CurrentPlaylistController`.
|
#: :class:`mopidy.backends.base.CurrentPlaylistController`.
|
||||||
current_playlist = None
|
current_playlist = None
|
||||||
@ -49,9 +17,6 @@ class Backend(object):
|
|||||||
# :class:`mopidy.backends.base.LibraryController`.
|
# :class:`mopidy.backends.base.LibraryController`.
|
||||||
library = None
|
library = None
|
||||||
|
|
||||||
#: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`.
|
|
||||||
mixer = None
|
|
||||||
|
|
||||||
#: The playback controller. An instance of
|
#: The playback controller. An instance of
|
||||||
#: :class:`mopidy.backends.base.PlaybackController`.
|
#: :class:`mopidy.backends.base.PlaybackController`.
|
||||||
playback = None
|
playback = None
|
||||||
@ -62,24 +27,3 @@ class Backend(object):
|
|||||||
|
|
||||||
#: List of URI prefixes this backend can handle.
|
#: List of URI prefixes this backend can handle.
|
||||||
uri_handlers = []
|
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 logging
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from mopidy.frontends.mpd import translator
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.base')
|
logger = logging.getLogger('mopidy.backends.base')
|
||||||
|
|
||||||
class CurrentPlaylistController(object):
|
class CurrentPlaylistController(object):
|
||||||
@ -12,6 +10,8 @@ class CurrentPlaylistController(object):
|
|||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
:type backend: :class:`mopidy.backends.base.Backend`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
def __init__(self, backend):
|
def __init__(self, backend):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
self._cp_tracks = []
|
self._cp_tracks = []
|
||||||
@ -197,8 +197,3 @@ class CurrentPlaylistController(object):
|
|||||||
random.shuffle(shuffled)
|
random.shuffle(shuffled)
|
||||||
self._cp_tracks = before + shuffled + after
|
self._cp_tracks = before + shuffled + after
|
||||||
self.version += 1
|
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`
|
:type provider: instance of :class:`BaseLibraryProvider`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
def __init__(self, backend, provider):
|
def __init__(self, backend, provider):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
@ -82,6 +84,8 @@ class BaseLibraryProvider(object):
|
|||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
:type backend: :class:`mopidy.backends.base.Backend`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
def __init__(self, backend):
|
def __init__(self, backend):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
|
from mopidy.frontends.base import BaseFrontend
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.base')
|
logger = logging.getLogger('mopidy.backends.base')
|
||||||
|
|
||||||
class PlaybackController(object):
|
class PlaybackController(object):
|
||||||
@ -15,6 +19,8 @@ class PlaybackController(object):
|
|||||||
# pylint: disable = R0902
|
# pylint: disable = R0902
|
||||||
# Too many instance attributes
|
# Too many instance attributes
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
#: Constant representing the paused state.
|
#: Constant representing the paused state.
|
||||||
PAUSED = u'paused'
|
PAUSED = u'paused'
|
||||||
|
|
||||||
@ -62,8 +68,8 @@ class PlaybackController(object):
|
|||||||
self._state = self.STOPPED
|
self._state = self.STOPPED
|
||||||
self._shuffled = []
|
self._shuffled = []
|
||||||
self._first_shuffle = True
|
self._first_shuffle = True
|
||||||
self._play_time_accumulated = 0
|
self.play_time_accumulated = 0
|
||||||
self._play_time_started = None
|
self.play_time_started = None
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
"""
|
"""
|
||||||
@ -240,7 +246,7 @@ class PlaybackController(object):
|
|||||||
if self.repeat or self.consume or self.random:
|
if self.repeat or self.consume or self.random:
|
||||||
return self.current_cp_track
|
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 None
|
||||||
|
|
||||||
return self.backend.current_playlist.cp_tracks[
|
return self.backend.current_playlist.cp_tracks[
|
||||||
@ -269,7 +275,7 @@ class PlaybackController(object):
|
|||||||
def state(self, new_state):
|
def state(self, new_state):
|
||||||
(old_state, self._state) = (self.state, new_state)
|
(old_state, self._state) = (self.state, new_state)
|
||||||
logger.debug(u'Changing state: %s -> %s', old_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 :/
|
# handeling this stuff :/
|
||||||
if (old_state in (self.PLAYING, self.STOPPED)
|
if (old_state in (self.PLAYING, self.STOPPED)
|
||||||
and new_state == self.PLAYING):
|
and new_state == self.PLAYING):
|
||||||
@ -284,23 +290,23 @@ class PlaybackController(object):
|
|||||||
"""Time position in milliseconds."""
|
"""Time position in milliseconds."""
|
||||||
if self.state == self.PLAYING:
|
if self.state == self.PLAYING:
|
||||||
time_since_started = (self._current_wall_time -
|
time_since_started = (self._current_wall_time -
|
||||||
self._play_time_started)
|
self.play_time_started)
|
||||||
return self._play_time_accumulated + time_since_started
|
return self.play_time_accumulated + time_since_started
|
||||||
elif self.state == self.PAUSED:
|
elif self.state == self.PAUSED:
|
||||||
return self._play_time_accumulated
|
return self.play_time_accumulated
|
||||||
elif self.state == self.STOPPED:
|
elif self.state == self.STOPPED:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _play_time_start(self):
|
def _play_time_start(self):
|
||||||
self._play_time_accumulated = 0
|
self.play_time_accumulated = 0
|
||||||
self._play_time_started = self._current_wall_time
|
self.play_time_started = self._current_wall_time
|
||||||
|
|
||||||
def _play_time_pause(self):
|
def _play_time_pause(self):
|
||||||
time_since_started = self._current_wall_time - self._play_time_started
|
time_since_started = self._current_wall_time - self.play_time_started
|
||||||
self._play_time_accumulated += time_since_started
|
self.play_time_accumulated += time_since_started
|
||||||
|
|
||||||
def _play_time_resume(self):
|
def _play_time_resume(self):
|
||||||
self._play_time_started = self._current_wall_time
|
self.play_time_started = self._current_wall_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _current_wall_time(self):
|
def _current_wall_time(self):
|
||||||
@ -433,8 +439,8 @@ class PlaybackController(object):
|
|||||||
self.next()
|
self.next()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self._play_time_started = self._current_wall_time
|
self.play_time_started = self._current_wall_time
|
||||||
self._play_time_accumulated = time_position
|
self.play_time_accumulated = time_position
|
||||||
|
|
||||||
return self.provider.seek(time_position)
|
return self.provider.seek(time_position)
|
||||||
|
|
||||||
@ -446,11 +452,10 @@ class PlaybackController(object):
|
|||||||
stopping
|
stopping
|
||||||
:type clear_current_track: boolean
|
:type clear_current_track: boolean
|
||||||
"""
|
"""
|
||||||
if self.state == self.STOPPED:
|
if self.state != self.STOPPED:
|
||||||
return
|
self._trigger_stopped_playing_event()
|
||||||
self._trigger_stopped_playing_event()
|
if self.provider.stop():
|
||||||
if self.provider.stop():
|
self.state = self.STOPPED
|
||||||
self.state = self.STOPPED
|
|
||||||
if clear_current_track:
|
if clear_current_track:
|
||||||
self.current_cp_track = None
|
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
|
For internal use only. Should be called by the backend directly after a
|
||||||
track has started playing.
|
track has started playing.
|
||||||
"""
|
"""
|
||||||
if self.current_track is not None:
|
if self.current_track is None:
|
||||||
self.backend.core_queue.put({
|
return
|
||||||
'to': 'frontend',
|
frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
|
||||||
|
for frontend_ref in frontend_refs:
|
||||||
|
frontend_ref.send_one_way({
|
||||||
'command': 'started_playing',
|
'command': 'started_playing',
|
||||||
'track': self.current_track,
|
'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
|
is stopped playing, e.g. at the next, previous, and stop actions and at
|
||||||
end-of-track.
|
end-of-track.
|
||||||
"""
|
"""
|
||||||
if self.current_track is not None:
|
if self.current_track is None:
|
||||||
self.backend.core_queue.put({
|
return
|
||||||
'to': 'frontend',
|
frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
|
||||||
|
for frontend_ref in frontend_refs:
|
||||||
|
frontend_ref.send_one_way({
|
||||||
'command': 'stopped_playing',
|
'command': 'stopped_playing',
|
||||||
'track': self.current_track,
|
'track': self.current_track,
|
||||||
'stop_position': self.time_position,
|
'stop_position': self.time_position,
|
||||||
@ -491,6 +500,8 @@ class BasePlaybackProvider(object):
|
|||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
:type backend: :class:`mopidy.backends.base.Backend`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
def __init__(self, backend):
|
def __init__(self, backend):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ class StoredPlaylistsController(object):
|
|||||||
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
|
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
def __init__(self, backend, provider):
|
def __init__(self, backend, provider):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
@ -125,6 +127,8 @@ class BaseStoredPlaylistsProvider(object):
|
|||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
:type backend: :class:`mopidy.backends.base.Backend`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
def __init__(self, backend):
|
def __init__(self, backend):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
self._playlists = []
|
self._playlists = []
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||||
PlaybackController, BasePlaybackProvider, LibraryController,
|
PlaybackController, BasePlaybackProvider, LibraryController,
|
||||||
BaseLibraryProvider, StoredPlaylistsController,
|
BaseLibraryProvider, StoredPlaylistsController,
|
||||||
@ -5,15 +7,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
|||||||
from mopidy.models import Playlist
|
from mopidy.models import Playlist
|
||||||
|
|
||||||
|
|
||||||
class DummyQueue(object):
|
class DummyBackend(ThreadingActor, Backend):
|
||||||
def __init__(self):
|
|
||||||
self.received_messages = []
|
|
||||||
|
|
||||||
def put(self, message):
|
|
||||||
self.received_messages.append(message)
|
|
||||||
|
|
||||||
|
|
||||||
class DummyBackend(Backend):
|
|
||||||
"""
|
"""
|
||||||
A backend which implements the backend API in the simplest way possible.
|
A backend which implements the backend API in the simplest way possible.
|
||||||
Used in tests of the frontends.
|
Used in tests of the frontends.
|
||||||
@ -24,8 +18,6 @@ class DummyBackend(Backend):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DummyBackend, self).__init__(*args, **kwargs)
|
super(DummyBackend, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.core_queue = DummyQueue()
|
|
||||||
|
|
||||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
self.current_playlist = CurrentPlaylistController(backend=self)
|
||||||
|
|
||||||
library_provider = DummyLibraryProvider(backend=self)
|
library_provider = DummyLibraryProvider(backend=self)
|
||||||
@ -46,13 +38,13 @@ class DummyBackend(Backend):
|
|||||||
class DummyLibraryProvider(BaseLibraryProvider):
|
class DummyLibraryProvider(BaseLibraryProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||||
self._library = []
|
self.dummy_library = []
|
||||||
|
|
||||||
def find_exact(self, **query):
|
def find_exact(self, **query):
|
||||||
return Playlist()
|
return Playlist()
|
||||||
|
|
||||||
def lookup(self, uri):
|
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:
|
if matches:
|
||||||
return matches[0]
|
return matches[0]
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,24 @@
|
|||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||||
LibraryController, BaseLibraryProvider, PlaybackController,
|
LibraryController, BaseLibraryProvider, PlaybackController,
|
||||||
BasePlaybackProvider, StoredPlaylistsController,
|
BasePlaybackProvider, StoredPlaylistsController,
|
||||||
BaseStoredPlaylistsProvider)
|
BaseStoredPlaylistsProvider)
|
||||||
from mopidy.models import Playlist, Track, Album
|
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
|
from .translator import parse_m3u, parse_mpd_tag_cache
|
||||||
|
|
||||||
logger = logging.getLogger(u'mopidy.backends.local')
|
logger = logging.getLogger(u'mopidy.backends.local')
|
||||||
|
|
||||||
class LocalBackend(Backend):
|
class LocalBackend(ThreadingActor, Backend):
|
||||||
"""
|
"""
|
||||||
A backend for playing music from a local music archive.
|
A backend for playing music from a local music archive.
|
||||||
|
|
||||||
@ -48,6 +50,13 @@ class LocalBackend(Backend):
|
|||||||
|
|
||||||
self.uri_handlers = [u'file://']
|
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):
|
class LocalPlaybackController(PlaybackController):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -58,24 +67,24 @@ class LocalPlaybackController(PlaybackController):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def time_position(self):
|
def time_position(self):
|
||||||
return self.backend.output.get_position()
|
return self.backend.output.get_position().get()
|
||||||
|
|
||||||
|
|
||||||
class LocalPlaybackProvider(BasePlaybackProvider):
|
class LocalPlaybackProvider(BasePlaybackProvider):
|
||||||
def pause(self):
|
def pause(self):
|
||||||
return self.backend.output.set_state('PAUSED')
|
return self.backend.output.set_state('PAUSED').get()
|
||||||
|
|
||||||
def play(self, track):
|
def play(self, track):
|
||||||
return self.backend.output.play_uri(track.uri)
|
return self.backend.output.play_uri(track.uri).get()
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
return self.backend.output.set_state('PLAYING')
|
return self.backend.output.set_state('PLAYING').get()
|
||||||
|
|
||||||
def seek(self, time_position):
|
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):
|
def stop(self):
|
||||||
return self.backend.output.set_state('READY')
|
return self.backend.output.set_state('READY').get()
|
||||||
|
|
||||||
|
|
||||||
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||||
|
|||||||
@ -100,8 +100,11 @@ def _convert_mpd_data(data, tracks, music_dir):
|
|||||||
albumartist_kwargs = {}
|
albumartist_kwargs = {}
|
||||||
|
|
||||||
if 'track' in data:
|
if 'track' in data:
|
||||||
album_kwargs['num_tracks'] = int(data['track'].split('/')[1])
|
if '/' in data['track']:
|
||||||
track_kwargs['track_no'] = int(data['track'].split('/')[0])
|
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:
|
if 'artist' in data:
|
||||||
artist_kwargs['name'] = data['artist']
|
artist_kwargs['name'] = data['artist']
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||||
LibraryController, PlaybackController, StoredPlaylistsController)
|
LibraryController, PlaybackController, StoredPlaylistsController)
|
||||||
|
from mopidy.outputs.base import BaseOutput
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.spotify')
|
logger = logging.getLogger('mopidy.backends.spotify')
|
||||||
|
|
||||||
ENCODING = 'utf-8'
|
ENCODING = 'utf-8'
|
||||||
|
|
||||||
class SpotifyBackend(Backend):
|
class SpotifyBackend(ThreadingActor, Backend):
|
||||||
"""
|
"""
|
||||||
A backend for playing music from the `Spotify <http://www.spotify.com/>`_
|
A backend for playing music from the `Spotify <http://www.spotify.com/>`_
|
||||||
music streaming service. The backend uses the official `libspotify
|
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.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()
|
self.spotify = self._connect()
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
@ -67,8 +79,6 @@ class SpotifyBackend(Backend):
|
|||||||
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
||||||
logger.debug(u'Connecting to Spotify')
|
logger.debug(u'Connecting to Spotify')
|
||||||
spotify = SpotifySessionManager(
|
spotify = SpotifySessionManager(
|
||||||
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
|
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD)
|
||||||
core_queue=self.core_queue,
|
|
||||||
output=self.output)
|
|
||||||
spotify.start()
|
spotify.start()
|
||||||
return spotify
|
return spotify
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import Queue
|
||||||
|
|
||||||
from spotify import Link, SpotifyError
|
from spotify import Link, SpotifyError
|
||||||
|
|
||||||
@ -54,8 +54,9 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
|
|||||||
spotify_query.append(u'%s:"%s"' % (field, value))
|
spotify_query.append(u'%s:"%s"' % (field, value))
|
||||||
spotify_query = u' '.join(spotify_query)
|
spotify_query = u' '.join(spotify_query)
|
||||||
logger.debug(u'Spotify search query: %s' % spotify_query)
|
logger.debug(u'Spotify search query: %s' % spotify_query)
|
||||||
my_end, other_end = multiprocessing.Pipe()
|
queue = Queue.Queue()
|
||||||
self.backend.spotify.search(spotify_query.encode(ENCODING), other_end)
|
self.backend.spotify.search(spotify_query.encode(ENCODING), queue)
|
||||||
my_end.poll(None)
|
try:
|
||||||
playlist = my_end.recv()
|
return queue.get(timeout=3) # XXX What is an reasonable timeout?
|
||||||
return playlist
|
except Queue.Empty:
|
||||||
|
return Playlist(tracks=[])
|
||||||
|
|||||||
@ -20,10 +20,10 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
|
|||||||
self.backend.spotify.session.load(
|
self.backend.spotify.session.load(
|
||||||
Link.from_string(track.uri).as_track())
|
Link.from_string(track.uri).as_track())
|
||||||
self.backend.spotify.session.play(1)
|
self.backend.spotify.session.play(1)
|
||||||
self.backend.output.set_state('PLAYING')
|
self.backend.output.play_uri('appsrc://')
|
||||||
return True
|
return True
|
||||||
except SpotifyError as e:
|
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
|
return False
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
|
|||||||
@ -2,11 +2,15 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
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 import get_version, settings
|
||||||
|
from mopidy.backends.base import Backend
|
||||||
from mopidy.backends.spotify.translator import SpotifyTranslator
|
from mopidy.backends.spotify.translator import SpotifyTranslator
|
||||||
from mopidy.models import Playlist
|
from mopidy.models import Playlist
|
||||||
|
from mopidy.outputs.base import BaseOutput
|
||||||
from mopidy.utils.process import BaseThread
|
from mopidy.utils.process import BaseThread
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
|
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
|
||||||
@ -14,26 +18,41 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager')
|
|||||||
# pylint: disable = R0901
|
# pylint: disable = R0901
|
||||||
# SpotifySessionManager: Too many ancestors (9/7)
|
# SpotifySessionManager: Too many ancestors (9/7)
|
||||||
|
|
||||||
class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
|
class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||||
cache_location = settings.SPOTIFY_CACHE_PATH
|
cache_location = settings.SPOTIFY_CACHE_PATH
|
||||||
settings_location = settings.SPOTIFY_CACHE_PATH
|
settings_location = settings.SPOTIFY_CACHE_PATH
|
||||||
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
|
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
|
||||||
user_agent = 'Mopidy %s' % get_version()
|
user_agent = 'Mopidy %s' % get_version()
|
||||||
|
|
||||||
def __init__(self, username, password, core_queue, output):
|
def __init__(self, username, password):
|
||||||
spotify.manager.SpotifySessionManager.__init__(
|
PyspotifySessionManager.__init__(self, username, password)
|
||||||
self, username, password)
|
BaseThread.__init__(self)
|
||||||
BaseThread.__init__(self, core_queue)
|
|
||||||
self.name = 'SpotifySMThread'
|
self.name = 'SpotifySMThread'
|
||||||
self.output = output
|
|
||||||
|
self.output = None
|
||||||
|
self.backend = None
|
||||||
|
|
||||||
self.connected = threading.Event()
|
self.connected = threading.Event()
|
||||||
self.session = None
|
self.session = None
|
||||||
|
|
||||||
def run_inside_try(self):
|
def run_inside_try(self):
|
||||||
|
self.setup()
|
||||||
self.connect()
|
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):
|
def logged_in(self, session, error):
|
||||||
"""Callback used by pyspotify"""
|
"""Callback used by pyspotify"""
|
||||||
|
if error:
|
||||||
|
logger.error(u'Spotify login error: %s', error)
|
||||||
|
return
|
||||||
logger.info(u'Connected to Spotify')
|
logger.info(u'Connected to Spotify')
|
||||||
self.session = session
|
self.session = session
|
||||||
if settings.SPOTIFY_HIGH_BITRATE:
|
if settings.SPOTIFY_HIGH_BITRATE:
|
||||||
@ -55,7 +74,11 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
|
|||||||
|
|
||||||
def connection_error(self, session, error):
|
def connection_error(self, session, error):
|
||||||
"""Callback used by pyspotify"""
|
"""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):
|
def message_to_user(self, session, message):
|
||||||
"""Callback used by pyspotify"""
|
"""Callback used by pyspotify"""
|
||||||
@ -88,7 +111,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
|
|||||||
def play_token_lost(self, session):
|
def play_token_lost(self, session):
|
||||||
"""Callback used by pyspotify"""
|
"""Callback used by pyspotify"""
|
||||||
logger.debug(u'Play token lost')
|
logger.debug(u'Play token lost')
|
||||||
self.core_queue.put({'command': 'stop_playback'})
|
self.backend.playback.pause()
|
||||||
|
|
||||||
def log_message(self, session, data):
|
def log_message(self, session, data):
|
||||||
"""Callback used by pyspotify"""
|
"""Callback used by pyspotify"""
|
||||||
@ -107,19 +130,16 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
|
|||||||
playlists.append(
|
playlists.append(
|
||||||
SpotifyTranslator.to_mopidy_playlist(spotify_playlist))
|
SpotifyTranslator.to_mopidy_playlist(spotify_playlist))
|
||||||
playlists = filter(None, playlists)
|
playlists = filter(None, playlists)
|
||||||
self.core_queue.put({
|
self.backend.stored_playlists.playlists = playlists
|
||||||
'command': 'set_stored_playlists',
|
|
||||||
'playlists': playlists,
|
|
||||||
})
|
|
||||||
logger.debug(u'Refreshed %d stored playlist(s)', len(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"""
|
"""Search method used by Mopidy backend"""
|
||||||
def callback(results, userdata=None):
|
def callback(results, userdata=None):
|
||||||
# TODO Include results from results.albums(), etc. too
|
# TODO Include results from results.albums(), etc. too
|
||||||
playlist = Playlist(tracks=[
|
playlist = Playlist(tracks=[
|
||||||
SpotifyTranslator.to_mopidy_track(t)
|
SpotifyTranslator.to_mopidy_track(t)
|
||||||
for t in results.tracks()])
|
for t in results.tracks()])
|
||||||
connection.send(playlist)
|
queue.put(playlist)
|
||||||
self.connected.wait()
|
self.connected.wait()
|
||||||
self.session.search(query, callback)
|
self.session.search(query, callback)
|
||||||
|
|||||||
@ -28,9 +28,9 @@ class SpotifyTranslator(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def to_mopidy_track(cls, spotify_track):
|
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))
|
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:
|
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
|
||||||
date = dt.date(spotify_track.album().year(), 1, 1)
|
date = dt.date(spotify_track.album().year(), 1, 1)
|
||||||
else:
|
else:
|
||||||
@ -57,8 +57,10 @@ class SpotifyTranslator(object):
|
|||||||
return Playlist(
|
return Playlist(
|
||||||
uri=str(Link.from_playlist(spotify_playlist)),
|
uri=str(Link.from_playlist(spotify_playlist)),
|
||||||
name=spotify_playlist.name().decode(ENCODING),
|
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:
|
except SpotifyError, e:
|
||||||
logger.warning(u'Failed translating Spotify playlist '
|
logger.info(u'Failed translating Spotify playlist '
|
||||||
'(probably a playlist folder boundary): %s', e)
|
'(probably a playlist folder boundary): %s', e)
|
||||||
|
|||||||
154
mopidy/core.py
154
mopidy/core.py
@ -1,114 +1,76 @@
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
import optparse
|
import optparse
|
||||||
import sys
|
import time
|
||||||
|
|
||||||
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import get_version, settings, OptionalDependencyError
|
from mopidy import get_version, settings, OptionalDependencyError
|
||||||
from mopidy.utils import get_class
|
from mopidy.utils import get_class
|
||||||
from mopidy.utils.log import setup_logging
|
from mopidy.utils.log import setup_logging
|
||||||
from mopidy.utils.path import get_or_create_folder, get_or_create_file
|
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
|
from mopidy.utils.settings import list_settings_optparse_callback
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.core')
|
logger = logging.getLogger('mopidy.core')
|
||||||
|
|
||||||
class CoreProcess(BaseThread):
|
def main():
|
||||||
def __init__(self):
|
options = parse_options()
|
||||||
self.core_queue = multiprocessing.Queue()
|
setup_logging(options.verbosity_level, options.save_debug_log)
|
||||||
super(CoreProcess, self).__init__(self.core_queue)
|
setup_settings()
|
||||||
self.name = 'CoreProcess'
|
setup_gobject_loop()
|
||||||
self.options = self.parse_options()
|
setup_output()
|
||||||
self.gobject_loop = None
|
setup_mixer()
|
||||||
self.output = None
|
setup_backend()
|
||||||
self.backend = None
|
setup_frontends()
|
||||||
self.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):
|
def parse_options():
|
||||||
parser = optparse.OptionParser(version='Mopidy %s' % get_version())
|
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
|
||||||
parser.add_option('-q', '--quiet',
|
parser.add_option('-q', '--quiet',
|
||||||
action='store_const', const=0, dest='verbosity_level',
|
action='store_const', const=0, dest='verbosity_level',
|
||||||
help='less output (warning level)')
|
help='less output (warning level)')
|
||||||
parser.add_option('-v', '--verbose',
|
parser.add_option('-v', '--verbose',
|
||||||
action='store_const', const=2, dest='verbosity_level',
|
action='store_const', const=2, dest='verbosity_level',
|
||||||
help='more output (debug level)')
|
help='more output (debug level)')
|
||||||
parser.add_option('--save-debug-log',
|
parser.add_option('--save-debug-log',
|
||||||
action='store_true', dest='save_debug_log',
|
action='store_true', dest='save_debug_log',
|
||||||
help='save debug log to "./mopidy.log"')
|
help='save debug log to "./mopidy.log"')
|
||||||
parser.add_option('--list-settings',
|
parser.add_option('--list-settings',
|
||||||
action='callback', callback=list_settings_optparse_callback,
|
action='callback', callback=list_settings_optparse_callback,
|
||||||
help='list current settings')
|
help='list current settings')
|
||||||
return parser.parse_args()[0]
|
return parser.parse_args()[0]
|
||||||
|
|
||||||
def run_inside_try(self):
|
def setup_settings():
|
||||||
self.setup()
|
get_or_create_folder('~/.mopidy/')
|
||||||
while True:
|
get_or_create_file('~/.mopidy/settings.py')
|
||||||
message = self.core_queue.get()
|
settings.validate()
|
||||||
self.process_message(message)
|
|
||||||
|
|
||||||
def setup(self):
|
def setup_gobject_loop():
|
||||||
self.setup_logging()
|
gobject_loop = GObjectEventThread()
|
||||||
self.setup_settings()
|
gobject_loop.start()
|
||||||
self.gobject_loop = self.setup_gobject_loop(self.core_queue)
|
return gobject_loop
|
||||||
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_logging(self):
|
def setup_output():
|
||||||
setup_logging(self.options.verbosity_level,
|
return get_class(settings.OUTPUT).start().proxy()
|
||||||
self.options.save_debug_log)
|
|
||||||
logger.info(u'-- Starting Mopidy --')
|
|
||||||
|
|
||||||
def setup_settings(self):
|
def setup_mixer():
|
||||||
get_or_create_folder('~/.mopidy/')
|
return get_class(settings.MIXER).start().proxy()
|
||||||
get_or_create_file('~/.mopidy/settings.py')
|
|
||||||
settings.validate()
|
|
||||||
|
|
||||||
def setup_gobject_loop(self, core_queue):
|
def setup_backend():
|
||||||
gobject_loop = GObjectEventThread(core_queue)
|
return get_class(settings.BACKENDS[0]).start().proxy()
|
||||||
gobject_loop.start()
|
|
||||||
return gobject_loop
|
|
||||||
|
|
||||||
def setup_output(self, core_queue):
|
def setup_frontends():
|
||||||
output = get_class(settings.OUTPUT)(core_queue)
|
frontends = []
|
||||||
output.start()
|
for frontend_class_name in settings.FRONTENDS:
|
||||||
return output
|
try:
|
||||||
|
frontend = get_class(frontend_class_name).start().proxy()
|
||||||
def setup_backend(self, core_queue, output):
|
frontends.append(frontend)
|
||||||
return get_class(settings.BACKENDS[0])(core_queue, output)
|
except OptionalDependencyError as e:
|
||||||
|
logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
|
||||||
def setup_frontends(self, core_queue, backend):
|
return frontends
|
||||||
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)
|
|
||||||
|
|||||||
@ -1,40 +1,5 @@
|
|||||||
class BaseFrontend(object):
|
class BaseFrontend(object):
|
||||||
"""
|
"""
|
||||||
Base class for frontends.
|
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`
|
|
||||||
"""
|
"""
|
||||||
|
pass
|
||||||
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
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -8,16 +7,17 @@ except ImportError as import_error:
|
|||||||
from mopidy import OptionalDependencyError
|
from mopidy import OptionalDependencyError
|
||||||
raise OptionalDependencyError(import_error)
|
raise OptionalDependencyError(import_error)
|
||||||
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy import settings, SettingsError
|
from mopidy import settings, SettingsError
|
||||||
from mopidy.frontends.base import BaseFrontend
|
from mopidy.frontends.base import BaseFrontend
|
||||||
from mopidy.utils.process import BaseThread
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.frontends.lastfm')
|
logger = logging.getLogger('mopidy.frontends.lastfm')
|
||||||
|
|
||||||
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
|
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
|
||||||
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
|
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
|
||||||
|
|
||||||
class LastfmFrontend(BaseFrontend):
|
class LastfmFrontend(ThreadingActor, BaseFrontend):
|
||||||
"""
|
"""
|
||||||
Frontend which scrobbles the music you play to your `Last.fm
|
Frontend which scrobbles the music you play to your `Last.fm
|
||||||
<http://www.last.fm>`_ profile.
|
<http://www.last.fm>`_ profile.
|
||||||
@ -36,38 +36,11 @@ class LastfmFrontend(BaseFrontend):
|
|||||||
- :attr:`mopidy.settings.LASTFM_PASSWORD`
|
- :attr:`mopidy.settings.LASTFM_PASSWORD`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self):
|
||||||
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
|
|
||||||
self.lastfm = None
|
self.lastfm = None
|
||||||
self.last_start_time = None
|
self.last_start_time = None
|
||||||
|
|
||||||
def run_inside_try(self):
|
def on_start(self):
|
||||||
self.setup()
|
|
||||||
while self.lastfm is not None:
|
|
||||||
self.connection.poll(None)
|
|
||||||
message = self.connection.recv()
|
|
||||||
self.process_message(message)
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
try:
|
try:
|
||||||
username = settings.LASTFM_USERNAME
|
username = settings.LASTFM_USERNAME
|
||||||
password_hash = pylast.md5(settings.LASTFM_PASSWORD)
|
password_hash = pylast.md5(settings.LASTFM_PASSWORD)
|
||||||
@ -78,17 +51,19 @@ class LastfmFrontendThread(BaseThread):
|
|||||||
except SettingsError as e:
|
except SettingsError as e:
|
||||||
logger.info(u'Last.fm scrobbler not started')
|
logger.info(u'Last.fm scrobbler not started')
|
||||||
logger.debug(u'Last.fm settings error: %s', e)
|
logger.debug(u'Last.fm settings error: %s', e)
|
||||||
|
self.stop()
|
||||||
except (pylast.NetworkError, pylast.MalformedResponseError,
|
except (pylast.NetworkError, pylast.MalformedResponseError,
|
||||||
pylast.WSError) as e:
|
pylast.WSError) as e:
|
||||||
logger.error(u'Error during Last.fm setup: %s', e)
|
logger.error(u'Error during Last.fm setup: %s', e)
|
||||||
|
self.stop()
|
||||||
|
|
||||||
def process_message(self, message):
|
def on_receive(self, message):
|
||||||
if message['command'] == 'started_playing':
|
if message.get('command') == 'started_playing':
|
||||||
self.started_playing(message['track'])
|
self.started_playing(message['track'])
|
||||||
elif message['command'] == 'stopped_playing':
|
elif message.get('command') == 'stopped_playing':
|
||||||
self.stopped_playing(message['track'], message['stop_position'])
|
self.stopped_playing(message['track'], message['stop_position'])
|
||||||
else:
|
else:
|
||||||
pass # Ignore commands for other frontends
|
pass # Ignore any other messages
|
||||||
|
|
||||||
def started_playing(self, track):
|
def started_playing(self, track):
|
||||||
artists = ', '.join([a.name for a in track.artists])
|
artists = ', '.join([a.name for a in track.artists])
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
|
import asyncore
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy.frontends.base import BaseFrontend
|
from mopidy.frontends.base import BaseFrontend
|
||||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
from mopidy.frontends.mpd.server import MpdServer
|
||||||
from mopidy.frontends.mpd.thread import MpdThread
|
from mopidy.utils.process import BaseThread
|
||||||
from mopidy.utils.process import unpickle_connection
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.frontends.mpd')
|
logger = logging.getLogger('mopidy.frontends.mpd')
|
||||||
|
|
||||||
class MpdFrontend(BaseFrontend):
|
class MpdFrontend(ThreadingActor, BaseFrontend):
|
||||||
"""
|
"""
|
||||||
The MPD frontend.
|
The MPD frontend.
|
||||||
|
|
||||||
@ -18,32 +20,24 @@ class MpdFrontend(BaseFrontend):
|
|||||||
- :attr:`mopidy.settings.MPD_SERVER_PORT`
|
- :attr:`mopidy.settings.MPD_SERVER_PORT`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self):
|
||||||
super(MpdFrontend, self).__init__(*args, **kwargs)
|
self._thread = None
|
||||||
self.thread = None
|
|
||||||
self.dispatcher = MpdDispatcher(self.backend)
|
|
||||||
|
|
||||||
def start(self):
|
def on_start(self):
|
||||||
"""Starts the MPD server."""
|
self._thread = MpdThread()
|
||||||
self.thread = MpdThread(self.core_queue)
|
self._thread.start()
|
||||||
self.thread.start()
|
|
||||||
|
|
||||||
def destroy(self):
|
def on_receive(self, message):
|
||||||
"""Destroys the MPD server."""
|
pass # Ignore any messages
|
||||||
self.thread.destroy()
|
|
||||||
|
|
||||||
def process_message(self, message):
|
|
||||||
"""
|
|
||||||
Processes messages with the MPD frontend as destination.
|
|
||||||
|
|
||||||
:param message: the message
|
class MpdThread(BaseThread):
|
||||||
:type message: dict
|
def __init__(self):
|
||||||
"""
|
super(MpdThread, self).__init__()
|
||||||
assert message['to'] == 'frontend', \
|
self.name = u'MpdThread'
|
||||||
u'Message recipient must be "frontend".'
|
|
||||||
if message['command'] == 'mpd_request':
|
def run_inside_try(self):
|
||||||
response = self.dispatcher.handle_request(message['request'])
|
logger.debug(u'Starting MPD server thread')
|
||||||
connection = unpickle_connection(message['reply_to'])
|
server = MpdServer()
|
||||||
connection.send(response)
|
server.start()
|
||||||
else:
|
asyncore.loop()
|
||||||
pass # Ignore messages for other frontends
|
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
|
from mopidy.backends.base import Backend
|
||||||
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
|
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
|
||||||
MpdUnknownCommand)
|
MpdUnknownCommand)
|
||||||
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
|
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,
|
connection, current_playlist, empty, music_db, playback, reflection,
|
||||||
status, stickers, stored_playlists)
|
status, stickers, stored_playlists)
|
||||||
# pylint: enable = W0611
|
# pylint: enable = W0611
|
||||||
|
from mopidy.mixers.base import BaseMixer
|
||||||
from mopidy.utils import flatten
|
from mopidy.utils import flatten
|
||||||
|
|
||||||
class MpdDispatcher(object):
|
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):
|
# XXX Consider merging MpdDispatcher into MpdSession
|
||||||
self.backend = backend
|
|
||||||
|
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 = False
|
||||||
self.command_list_ok = False
|
self.command_list_ok = False
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,6 @@ def outputs(frontend):
|
|||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
('outputid', 0),
|
('outputid', 0),
|
||||||
('outputname', frontend.backend.__class__.__name__),
|
('outputname', None),
|
||||||
('outputenabled', 1),
|
('outputenabled', 1),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
|
||||||
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
||||||
MpdNotImplemented)
|
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>[^"]*)"$')
|
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')
|
||||||
def add(frontend, uri):
|
def add(frontend, uri):
|
||||||
@ -18,9 +19,9 @@ def add(frontend, uri):
|
|||||||
"""
|
"""
|
||||||
if not uri:
|
if not uri:
|
||||||
return
|
return
|
||||||
for handler_prefix in frontend.backend.uri_handlers:
|
for handler_prefix in frontend.backend.uri_handlers.get():
|
||||||
if uri.startswith(handler_prefix):
|
if uri.startswith(handler_prefix):
|
||||||
track = frontend.backend.library.lookup(uri)
|
track = frontend.backend.library.lookup(uri).get()
|
||||||
if track is not None:
|
if track is not None:
|
||||||
frontend.backend.current_playlist.add(track)
|
frontend.backend.current_playlist.add(track)
|
||||||
return
|
return
|
||||||
@ -50,13 +51,14 @@ def addid(frontend, uri, songpos=None):
|
|||||||
raise MpdNoExistError(u'No such song', command=u'addid')
|
raise MpdNoExistError(u'No such song', command=u'addid')
|
||||||
if songpos is not None:
|
if songpos is not None:
|
||||||
songpos = int(songpos)
|
songpos = int(songpos)
|
||||||
track = frontend.backend.library.lookup(uri)
|
track = frontend.backend.library.lookup(uri).get()
|
||||||
if track is None:
|
if track is None:
|
||||||
raise MpdNoExistError(u'No such song', command=u'addid')
|
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')
|
raise MpdArgError(u'Bad song index', command=u'addid')
|
||||||
cp_track = frontend.backend.current_playlist.add(track,
|
cp_track = frontend.backend.current_playlist.add(track,
|
||||||
at_position=songpos)
|
at_position=songpos).get()
|
||||||
return ('Id', cp_track[0])
|
return ('Id', cp_track[0])
|
||||||
|
|
||||||
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
@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:
|
if end is not None:
|
||||||
end = int(end)
|
end = int(end)
|
||||||
else:
|
else:
|
||||||
end = len(frontend.backend.current_playlist.tracks)
|
end = len(frontend.backend.current_playlist.tracks.get())
|
||||||
cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end]
|
cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end]
|
||||||
if not cp_tracks:
|
if not cp_tracks:
|
||||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||||
for (cpid, _) in cp_tracks:
|
for (cpid, _) in cp_tracks:
|
||||||
@ -84,7 +86,7 @@ def delete_songpos(frontend, songpos):
|
|||||||
"""See :meth:`delete_range`"""
|
"""See :meth:`delete_range`"""
|
||||||
try:
|
try:
|
||||||
songpos = int(songpos)
|
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)
|
frontend.backend.current_playlist.remove(cpid=cpid)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||||
@ -100,9 +102,9 @@ def deleteid(frontend, cpid):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cpid = int(cpid)
|
cpid = int(cpid)
|
||||||
if frontend.backend.playback.current_cpid == cpid:
|
if frontend.backend.playback.current_cpid.get() == cpid:
|
||||||
frontend.backend.playback.next()
|
frontend.backend.playback.next()
|
||||||
return frontend.backend.current_playlist.remove(cpid=cpid)
|
return frontend.backend.current_playlist.remove(cpid=cpid).get()
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
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.
|
``TO`` in the playlist.
|
||||||
"""
|
"""
|
||||||
if end is None:
|
if end is None:
|
||||||
end = len(frontend.backend.current_playlist.tracks)
|
end = len(frontend.backend.current_playlist.tracks.get())
|
||||||
start = int(start)
|
start = int(start)
|
||||||
end = int(end)
|
end = int(end)
|
||||||
to = int(to)
|
to = int(to)
|
||||||
@ -154,8 +156,9 @@ def moveid(frontend, cpid, to):
|
|||||||
"""
|
"""
|
||||||
cpid = int(cpid)
|
cpid = int(cpid)
|
||||||
to = int(to)
|
to = int(to)
|
||||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
|
||||||
position = frontend.backend.current_playlist.cp_tracks.index(cp_track)
|
position = frontend.backend.current_playlist.cp_tracks.get().index(
|
||||||
|
cp_track)
|
||||||
frontend.backend.current_playlist.move(position, position + 1, to)
|
frontend.backend.current_playlist.move(position, position + 1, to)
|
||||||
|
|
||||||
@handle_pattern(r'^playlist$')
|
@handle_pattern(r'^playlist$')
|
||||||
@ -189,9 +192,9 @@ def playlistfind(frontend, tag, needle):
|
|||||||
"""
|
"""
|
||||||
if tag == 'filename':
|
if tag == 'filename':
|
||||||
try:
|
try:
|
||||||
cp_track = frontend.backend.current_playlist.get(uri=needle)
|
cp_track = frontend.backend.current_playlist.get(uri=needle).get()
|
||||||
(cpid, track) = cp_track
|
(cpid, track) = cp_track
|
||||||
position = frontend.backend.current_playlist.cp_tracks.index(
|
position = frontend.backend.current_playlist.cp_tracks.get().index(
|
||||||
cp_track)
|
cp_track)
|
||||||
return track.mpd_format(cpid=cpid, position=position)
|
return track.mpd_format(cpid=cpid, position=position)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
@ -211,14 +214,17 @@ def playlistid(frontend, cpid=None):
|
|||||||
if cpid is not None:
|
if cpid is not None:
|
||||||
try:
|
try:
|
||||||
cpid = int(cpid)
|
cpid = int(cpid)
|
||||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
|
||||||
position = frontend.backend.current_playlist.cp_tracks.index(
|
position = frontend.backend.current_playlist.cp_tracks.get().index(
|
||||||
cp_track)
|
cp_track)
|
||||||
return cp_track[1].mpd_format(position=position, cpid=cpid)
|
return cp_track[1].mpd_format(position=position, cpid=cpid)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise MpdNoExistError(u'No such song', command=u'playlistid')
|
raise MpdNoExistError(u'No such song', command=u'playlistid')
|
||||||
else:
|
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$')
|
||||||
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$')
|
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$')
|
||||||
@ -248,18 +254,27 @@ def playlistinfo(frontend, songpos=None,
|
|||||||
end = songpos + 1
|
end = songpos + 1
|
||||||
if start == -1:
|
if start == -1:
|
||||||
end = None
|
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:
|
else:
|
||||||
if start is None:
|
if start is None:
|
||||||
start = 0
|
start = 0
|
||||||
start = int(start)
|
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')
|
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
|
||||||
if end is not None:
|
if end is not None:
|
||||||
end = int(end)
|
end = int(end)
|
||||||
if end > len(frontend.backend.current_playlist.tracks):
|
if end > len(frontend.backend.current_playlist.tracks.get()):
|
||||||
end = None
|
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>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||||
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?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
|
# 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:
|
||||||
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+)"$')
|
@handle_pattern(r'^plchangesposid "(?P<version>\d+)"$')
|
||||||
def plchangesposid(frontend, version):
|
def plchangesposid(frontend, version):
|
||||||
@ -315,10 +333,10 @@ def plchangesposid(frontend, version):
|
|||||||
``playlistlength`` returned by status command.
|
``playlistlength`` returned by status command.
|
||||||
"""
|
"""
|
||||||
# XXX Naive implementation that returns all tracks as changed
|
# 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 = []
|
result = []
|
||||||
for (position, (cpid, _)) in enumerate(
|
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'cpos', position))
|
||||||
result.append((u'Id', cpid))
|
result.append((u'Id', cpid))
|
||||||
return result
|
return result
|
||||||
@ -351,7 +369,7 @@ def swap(frontend, songpos1, songpos2):
|
|||||||
"""
|
"""
|
||||||
songpos1 = int(songpos1)
|
songpos1 = int(songpos1)
|
||||||
songpos2 = int(songpos2)
|
songpos2 = int(songpos2)
|
||||||
tracks = frontend.backend.current_playlist.tracks
|
tracks = frontend.backend.current_playlist.tracks.get()
|
||||||
song1 = tracks[songpos1]
|
song1 = tracks[songpos1]
|
||||||
song2 = tracks[songpos2]
|
song2 = tracks[songpos2]
|
||||||
del tracks[songpos1]
|
del tracks[songpos1]
|
||||||
@ -372,8 +390,9 @@ def swapid(frontend, cpid1, cpid2):
|
|||||||
"""
|
"""
|
||||||
cpid1 = int(cpid1)
|
cpid1 = int(cpid1)
|
||||||
cpid2 = int(cpid2)
|
cpid2 = int(cpid2)
|
||||||
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1)
|
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get()
|
||||||
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2)
|
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get()
|
||||||
position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1)
|
cp_tracks = frontend.backend.current_playlist.cp_tracks.get()
|
||||||
position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2)
|
position1 = cp_tracks.index(cp_track1)
|
||||||
|
position2 = cp_tracks.index(cp_track2)
|
||||||
swap(frontend, position1, position2)
|
swap(frontend, position1, position2)
|
||||||
|
|||||||
@ -41,8 +41,8 @@ def count(frontend, tag, needle):
|
|||||||
return [('songs', 0), ('playtime', 0)] # TODO
|
return [('songs', 0), ('playtime', 0)] # TODO
|
||||||
|
|
||||||
@handle_pattern(r'^find '
|
@handle_pattern(r'^find '
|
||||||
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
|
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
|
||||||
' "[^"]+"\s?)+)$')
|
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
|
||||||
def find(frontend, mpd_query):
|
def find(frontend, mpd_query):
|
||||||
"""
|
"""
|
||||||
*musicpd.org, music database section:*
|
*musicpd.org, music database section:*
|
||||||
@ -62,9 +62,13 @@ def find(frontend, mpd_query):
|
|||||||
|
|
||||||
- does not add quotes around the field argument.
|
- does not add quotes around the field argument.
|
||||||
- capitalizes the type argument.
|
- capitalizes the type argument.
|
||||||
|
|
||||||
|
*ncmpcpp:*
|
||||||
|
|
||||||
|
- also uses the search type "date".
|
||||||
"""
|
"""
|
||||||
query = _build_query(mpd_query)
|
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 '
|
@handle_pattern(r'^findadd '
|
||||||
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
|
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):
|
def _list_artist(frontend, query):
|
||||||
artists = set()
|
artists = set()
|
||||||
playlist = frontend.backend.library.find_exact(**query)
|
playlist = frontend.backend.library.find_exact(**query).get()
|
||||||
for track in playlist.tracks:
|
for track in playlist.tracks:
|
||||||
for artist in track.artists:
|
for artist in track.artists:
|
||||||
artists.add((u'Artist', artist.name))
|
artists.add((u'Artist', artist.name))
|
||||||
@ -219,7 +223,7 @@ def _list_artist(frontend, query):
|
|||||||
|
|
||||||
def _list_album(frontend, query):
|
def _list_album(frontend, query):
|
||||||
albums = set()
|
albums = set()
|
||||||
playlist = frontend.backend.library.find_exact(**query)
|
playlist = frontend.backend.library.find_exact(**query).get()
|
||||||
for track in playlist.tracks:
|
for track in playlist.tracks:
|
||||||
if track.album is not None:
|
if track.album is not None:
|
||||||
albums.add((u'Album', track.album.name))
|
albums.add((u'Album', track.album.name))
|
||||||
@ -227,7 +231,7 @@ def _list_album(frontend, query):
|
|||||||
|
|
||||||
def _list_date(frontend, query):
|
def _list_date(frontend, query):
|
||||||
dates = set()
|
dates = set()
|
||||||
playlist = frontend.backend.library.find_exact(**query)
|
playlist = frontend.backend.library.find_exact(**query).get()
|
||||||
for track in playlist.tracks:
|
for track in playlist.tracks:
|
||||||
if track.date is not None:
|
if track.date is not None:
|
||||||
dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
|
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)
|
return update(frontend, uri, rescan_unmodified_files=True)
|
||||||
|
|
||||||
@handle_pattern(r'^search '
|
@handle_pattern(r'^search '
|
||||||
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
|
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
|
||||||
' "[^"]+"\s?)+)$')
|
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
|
||||||
def search(frontend, mpd_query):
|
def search(frontend, mpd_query):
|
||||||
"""
|
"""
|
||||||
*musicpd.org, music database section:*
|
*musicpd.org, music database section:*
|
||||||
@ -314,9 +318,13 @@ def search(frontend, mpd_query):
|
|||||||
|
|
||||||
- does not add quotes around the field argument.
|
- does not add quotes around the field argument.
|
||||||
- capitalizes the field argument.
|
- capitalizes the field argument.
|
||||||
|
|
||||||
|
*ncmpcpp:*
|
||||||
|
|
||||||
|
- also uses the search type "date".
|
||||||
"""
|
"""
|
||||||
query = _build_query(mpd_query)
|
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>[^"]+)")*$')
|
@handle_pattern(r'^update( "(?P<uri>[^"]+)")*$')
|
||||||
def update(frontend, uri=None, rescan_unmodified_files=False):
|
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.protocol import handle_pattern
|
||||||
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
||||||
MpdNotImplemented)
|
MpdNotImplemented)
|
||||||
@ -86,7 +87,7 @@ def next_(frontend):
|
|||||||
order as the first time.
|
order as the first time.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return frontend.backend.playback.next()
|
return frontend.backend.playback.next().get()
|
||||||
|
|
||||||
@handle_pattern(r'^pause$')
|
@handle_pattern(r'^pause$')
|
||||||
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
||||||
@ -103,11 +104,11 @@ def pause(frontend, state=None):
|
|||||||
- Calls ``pause`` without any arguments to toogle pause.
|
- Calls ``pause`` without any arguments to toogle pause.
|
||||||
"""
|
"""
|
||||||
if state is None:
|
if state is None:
|
||||||
if (frontend.backend.playback.state ==
|
if (frontend.backend.playback.state.get() ==
|
||||||
frontend.backend.playback.PLAYING):
|
PlaybackController.PLAYING):
|
||||||
frontend.backend.playback.pause()
|
frontend.backend.playback.pause()
|
||||||
elif (frontend.backend.playback.state ==
|
elif (frontend.backend.playback.state.get() ==
|
||||||
frontend.backend.playback.PAUSED):
|
PlaybackController.PAUSED):
|
||||||
frontend.backend.playback.resume()
|
frontend.backend.playback.resume()
|
||||||
elif int(state):
|
elif int(state):
|
||||||
frontend.backend.playback.pause()
|
frontend.backend.playback.pause()
|
||||||
@ -120,7 +121,7 @@ def play(frontend):
|
|||||||
The original MPD server resumes from the paused state on ``play``
|
The original MPD server resumes from the paused state on ``play``
|
||||||
without arguments.
|
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>\d+)"$')
|
||||||
@handle_pattern(r'^playid "(?P<cpid>-1)"$')
|
@handle_pattern(r'^playid "(?P<cpid>-1)"$')
|
||||||
@ -132,22 +133,21 @@ def playid(frontend, cpid):
|
|||||||
|
|
||||||
Begins playing the playlist at song ``SONGID``.
|
Begins playing the playlist at song ``SONGID``.
|
||||||
|
|
||||||
*GMPC:*
|
*Clarifications:*
|
||||||
|
|
||||||
- issues ``playid "-1"`` after playlist replacement to start playback
|
- ``playid "-1"`` when playing is ignored.
|
||||||
at the first track.
|
- ``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)
|
cpid = int(cpid)
|
||||||
paused = (frontend.backend.playback.state ==
|
if cpid == -1:
|
||||||
frontend.backend.playback.PAUSED)
|
return _play_minus_one(frontend)
|
||||||
if cpid == -1 and paused:
|
|
||||||
return frontend.backend.playback.resume()
|
|
||||||
try:
|
try:
|
||||||
if cpid == -1:
|
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
|
||||||
cp_track = _get_cp_track_for_play_minus_one(frontend)
|
return frontend.backend.playback.play(cp_track).get()
|
||||||
else:
|
|
||||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
|
||||||
return frontend.backend.playback.play(cp_track)
|
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise MpdNoExistError(u'No such song', command=u'playid')
|
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``.
|
Begins playing the playlist at song number ``SONGPOS``.
|
||||||
|
|
||||||
*Many clients:*
|
*Clarifications:*
|
||||||
|
|
||||||
- issue ``play "-1"`` after playlist replacement to start the current
|
- ``playid "-1"`` when playing is ignored.
|
||||||
track. If the current track is not set, start playback at the first
|
- ``playid "-1"`` when paused resumes playback.
|
||||||
track.
|
- ``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:*
|
*BitMPC:*
|
||||||
|
|
||||||
- issues ``play 6`` without quotes around the argument.
|
- issues ``play 6`` without quotes around the argument.
|
||||||
"""
|
"""
|
||||||
songpos = int(songpos)
|
songpos = int(songpos)
|
||||||
|
if songpos == -1:
|
||||||
|
return _play_minus_one(frontend)
|
||||||
try:
|
try:
|
||||||
if songpos == -1:
|
cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos]
|
||||||
cp_track = _get_cp_track_for_play_minus_one(frontend)
|
return frontend.backend.playback.play(cp_track).get()
|
||||||
else:
|
|
||||||
cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
|
|
||||||
return frontend.backend.playback.play(cp_track)
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise MpdArgError(u'Bad song index', command=u'play')
|
raise MpdArgError(u'Bad song index', command=u'play')
|
||||||
|
|
||||||
def _get_cp_track_for_play_minus_one(frontend):
|
def _play_minus_one(frontend):
|
||||||
if not frontend.backend.current_playlist.cp_tracks:
|
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
|
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$')
|
@handle_pattern(r'^previous$')
|
||||||
def previous(frontend):
|
def previous(frontend):
|
||||||
@ -233,7 +241,7 @@ def previous(frontend):
|
|||||||
``previous`` should do a seek to time position 0.
|
``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])$')
|
||||||
@handle_pattern(r'^random "(?P<state>[01])"$')
|
@handle_pattern(r'^random "(?P<state>[01])"$')
|
||||||
@ -344,7 +352,7 @@ def setvol(frontend, volume):
|
|||||||
volume = 0
|
volume = 0
|
||||||
if volume > 100:
|
if volume > 100:
|
||||||
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])$')
|
||||||
@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.
|
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.protocol import handle_pattern
|
||||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
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
|
Displays the song info of the current song (same song that is
|
||||||
identified in status).
|
identified in status).
|
||||||
"""
|
"""
|
||||||
if frontend.backend.playback.current_track is not None:
|
current_cp_track = frontend.backend.playback.current_cp_track.get()
|
||||||
return frontend.backend.playback.current_track.mpd_format(
|
if current_cp_track is not None:
|
||||||
position=frontend.backend.playback.current_playlist_position,
|
return current_cp_track[1].mpd_format(
|
||||||
cpid=frontend.backend.playback.current_cpid)
|
position=frontend.backend.playback.current_playlist_position.get(),
|
||||||
|
cpid=current_cp_track[0])
|
||||||
|
|
||||||
@handle_pattern(r'^idle$')
|
@handle_pattern(r'^idle$')
|
||||||
@handle_pattern(r'^idle (?P<subsystems>.+)$')
|
@handle_pattern(r'^idle (?P<subsystems>.+)$')
|
||||||
@ -90,8 +92,7 @@ def stats(frontend):
|
|||||||
'artists': 0, # TODO
|
'artists': 0, # TODO
|
||||||
'albums': 0, # TODO
|
'albums': 0, # TODO
|
||||||
'songs': 0, # TODO
|
'songs': 0, # TODO
|
||||||
# TODO Does not work after multiprocessing branch merge
|
'uptime': 0, # TODO
|
||||||
'uptime': 0, # frontend.session.stats_uptime(),
|
|
||||||
'db_playtime': 0, # TODO
|
'db_playtime': 0, # TODO
|
||||||
'db_update': 0, # TODO
|
'db_update': 0, # TODO
|
||||||
'playtime': 0, # TODO
|
'playtime': 0, # TODO
|
||||||
@ -140,56 +141,59 @@ def status(frontend):
|
|||||||
('xfade', _status_xfade(frontend)),
|
('xfade', _status_xfade(frontend)),
|
||||||
('state', _status_state(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(('song', _status_songpos(frontend)))
|
||||||
result.append(('songid', _status_songid(frontend)))
|
result.append(('songid', _status_songid(frontend)))
|
||||||
if frontend.backend.playback.state in (frontend.backend.playback.PLAYING,
|
if frontend.backend.playback.state.get() in (PlaybackController.PLAYING,
|
||||||
frontend.backend.playback.PAUSED):
|
PlaybackController.PAUSED):
|
||||||
result.append(('time', _status_time(frontend)))
|
result.append(('time', _status_time(frontend)))
|
||||||
result.append(('elapsed', _status_time_elapsed(frontend)))
|
result.append(('elapsed', _status_time_elapsed(frontend)))
|
||||||
result.append(('bitrate', _status_bitrate(frontend)))
|
result.append(('bitrate', _status_bitrate(frontend)))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _status_bitrate(frontend):
|
def _status_bitrate(frontend):
|
||||||
if frontend.backend.playback.current_track is not None:
|
current_track = frontend.backend.playback.current_track.get()
|
||||||
return frontend.backend.playback.current_track.bitrate
|
if current_track is not None:
|
||||||
|
return current_track.bitrate
|
||||||
|
|
||||||
def _status_consume(frontend):
|
def _status_consume(frontend):
|
||||||
if frontend.backend.playback.consume:
|
if frontend.backend.playback.consume.get():
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _status_playlist_length(frontend):
|
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):
|
def _status_playlist_version(frontend):
|
||||||
return frontend.backend.current_playlist.version
|
return frontend.backend.current_playlist.version.get()
|
||||||
|
|
||||||
def _status_random(frontend):
|
def _status_random(frontend):
|
||||||
return int(frontend.backend.playback.random)
|
return int(frontend.backend.playback.random.get())
|
||||||
|
|
||||||
def _status_repeat(frontend):
|
def _status_repeat(frontend):
|
||||||
return int(frontend.backend.playback.repeat)
|
return int(frontend.backend.playback.repeat.get())
|
||||||
|
|
||||||
def _status_single(frontend):
|
def _status_single(frontend):
|
||||||
return int(frontend.backend.playback.single)
|
return int(frontend.backend.playback.single.get())
|
||||||
|
|
||||||
def _status_songid(frontend):
|
def _status_songid(frontend):
|
||||||
if frontend.backend.playback.current_cpid is not None:
|
current_cpid = frontend.backend.playback.current_cpid.get()
|
||||||
return frontend.backend.playback.current_cpid
|
if current_cpid is not None:
|
||||||
|
return current_cpid
|
||||||
else:
|
else:
|
||||||
return _status_songpos(frontend)
|
return _status_songpos(frontend)
|
||||||
|
|
||||||
def _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):
|
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'
|
return u'play'
|
||||||
elif frontend.backend.playback.state == frontend.backend.playback.STOPPED:
|
elif state == PlaybackController.STOPPED:
|
||||||
return u'stop'
|
return u'stop'
|
||||||
elif frontend.backend.playback.state == frontend.backend.playback.PAUSED:
|
elif state == PlaybackController.PAUSED:
|
||||||
return u'pause'
|
return u'pause'
|
||||||
|
|
||||||
def _status_time(frontend):
|
def _status_time(frontend):
|
||||||
@ -197,19 +201,21 @@ def _status_time(frontend):
|
|||||||
_status_time_total(frontend) // 1000)
|
_status_time_total(frontend) // 1000)
|
||||||
|
|
||||||
def _status_time_elapsed(frontend):
|
def _status_time_elapsed(frontend):
|
||||||
return frontend.backend.playback.time_position
|
return frontend.backend.playback.time_position.get()
|
||||||
|
|
||||||
def _status_time_total(frontend):
|
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
|
return 0
|
||||||
elif frontend.backend.playback.current_track.length is None:
|
elif current_track.length is None:
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
return frontend.backend.playback.current_track.length
|
return current_track.length
|
||||||
|
|
||||||
def _status_volume(frontend):
|
def _status_volume(frontend):
|
||||||
if frontend.backend.mixer.volume is not None:
|
volume = frontend.mixer.volume.get()
|
||||||
return frontend.backend.mixer.volume
|
if volume is not None:
|
||||||
|
return volume
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@ -19,8 +19,8 @@ def listplaylist(frontend, name):
|
|||||||
file: relative/path/to/file3.mp3
|
file: relative/path/to/file3.mp3
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return ['file: %s' % t.uri
|
playlist = frontend.backend.stored_playlists.get(name=name).get()
|
||||||
for t in frontend.backend.stored_playlists.get(name=name).tracks]
|
return ['file: %s' % t.uri for t in playlist.tracks]
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
|
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
|
||||||
|
|
||||||
@ -39,7 +39,8 @@ def listplaylistinfo(frontend, name):
|
|||||||
Album, Artist, Track
|
Album, Artist, Track
|
||||||
"""
|
"""
|
||||||
try:
|
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:
|
except LookupError:
|
||||||
raise MpdNoExistError(
|
raise MpdNoExistError(
|
||||||
u'No such playlist', command=u'listplaylistinfo')
|
u'No such playlist', command=u'listplaylistinfo')
|
||||||
@ -66,7 +67,7 @@ def listplaylists(frontend):
|
|||||||
Last-Modified: 2010-02-06T02:11:08Z
|
Last-Modified: 2010-02-06T02:11:08Z
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
for playlist in frontend.backend.stored_playlists.playlists:
|
for playlist in frontend.backend.stored_playlists.playlists.get():
|
||||||
result.append((u'playlist', playlist.name))
|
result.append((u'playlist', playlist.name))
|
||||||
last_modified = (playlist.last_modified or
|
last_modified = (playlist.last_modified or
|
||||||
dt.datetime.now()).isoformat()
|
dt.datetime.now()).isoformat()
|
||||||
@ -92,7 +93,7 @@ def load(frontend, name):
|
|||||||
- ``load`` appends the given playlist to the current playlist.
|
- ``load`` appends the given playlist to the current playlist.
|
||||||
"""
|
"""
|
||||||
try:
|
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)
|
frontend.backend.current_playlist.append(playlist.tracks)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise MpdNoExistError(u'No such playlist', command=u'load')
|
raise MpdNoExistError(u'No such playlist', command=u'load')
|
||||||
|
|||||||
@ -9,20 +9,33 @@ from .session import MpdSession
|
|||||||
|
|
||||||
logger = logging.getLogger('mopidy.frontends.mpd.server')
|
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):
|
class MpdServer(asyncore.dispatcher):
|
||||||
"""
|
"""
|
||||||
The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
|
The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
|
||||||
for each client connection.
|
for each client connection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, core_queue):
|
def __init__(self):
|
||||||
asyncore.dispatcher.__init__(self)
|
asyncore.dispatcher.__init__(self)
|
||||||
self.core_queue = core_queue
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start MPD server."""
|
"""Start MPD server."""
|
||||||
try:
|
try:
|
||||||
if socket.has_ipv6:
|
if has_ipv6:
|
||||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||||
# Explicitly configure socket to work for both IPv4 and IPv6
|
# Explicitly configure socket to work for both IPv4 and IPv6
|
||||||
self.socket.setsockopt(
|
self.socket.setsockopt(
|
||||||
@ -47,15 +60,14 @@ class MpdServer(asyncore.dispatcher):
|
|||||||
(client_socket, client_socket_address) = self.accept()
|
(client_socket, client_socket_address) = self.accept()
|
||||||
logger.info(u'MPD client connection from [%s]:%s',
|
logger.info(u'MPD client connection from [%s]:%s',
|
||||||
client_socket_address[0], client_socket_address[1])
|
client_socket_address[0], client_socket_address[1])
|
||||||
MpdSession(self, client_socket, client_socket_address,
|
MpdSession(self, client_socket, client_socket_address).start()
|
||||||
self.core_queue).start()
|
|
||||||
|
|
||||||
def handle_close(self):
|
def handle_close(self):
|
||||||
"""Handle end of client connection."""
|
"""Handle end of client connection."""
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _format_hostname(self, hostname):
|
def _format_hostname(self, hostname):
|
||||||
if (socket.has_ipv6
|
if (has_ipv6
|
||||||
and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
|
and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
|
||||||
hostname = '::ffff:%s' % hostname
|
hostname = '::ffff:%s' % hostname
|
||||||
return hostname
|
return hostname
|
||||||
|
|||||||
@ -1,30 +1,28 @@
|
|||||||
import asynchat
|
import asynchat
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
|
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||||
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
|
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
|
||||||
from mopidy.utils.log import indent
|
from mopidy.utils.log import indent
|
||||||
from mopidy.utils.process import pickle_connection
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.frontends.mpd.session')
|
logger = logging.getLogger('mopidy.frontends.mpd.session')
|
||||||
|
|
||||||
class MpdSession(asynchat.async_chat):
|
class MpdSession(asynchat.async_chat):
|
||||||
"""
|
"""
|
||||||
The MPD client session. Keeps track of a single client and passes its
|
The MPD client session. Keeps track of a single client session. Any
|
||||||
MPD requests to the dispatcher.
|
requests from the client is passed on to the MPD request dispatcher.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server, client_socket, client_socket_address,
|
def __init__(self, server, client_socket, client_socket_address):
|
||||||
core_queue):
|
|
||||||
asynchat.async_chat.__init__(self, sock=client_socket)
|
asynchat.async_chat.__init__(self, sock=client_socket)
|
||||||
self.server = server
|
self.server = server
|
||||||
self.client_address = client_socket_address[0]
|
self.client_address = client_socket_address[0]
|
||||||
self.client_port = client_socket_address[1]
|
self.client_port = client_socket_address[1]
|
||||||
self.core_queue = core_queue
|
|
||||||
self.input_buffer = []
|
self.input_buffer = []
|
||||||
self.authenticated = False
|
self.authenticated = False
|
||||||
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
|
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
|
||||||
|
self.dispatcher = MpdDispatcher()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start a new client session."""
|
"""Start a new client session."""
|
||||||
@ -53,15 +51,7 @@ class MpdSession(asynchat.async_chat):
|
|||||||
if response is not None:
|
if response is not None:
|
||||||
self.send_response(response)
|
self.send_response(response)
|
||||||
return
|
return
|
||||||
my_end, other_end = multiprocessing.Pipe()
|
response = self.dispatcher.handle_request(request)
|
||||||
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()
|
|
||||||
if response is not None:
|
if response is not None:
|
||||||
self.handle_response(response)
|
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`
|
:type track: array of :class:`mopidy.models.Artist`
|
||||||
:rtype: string
|
:rtype: string
|
||||||
"""
|
"""
|
||||||
|
artists = list(artists)
|
||||||
artists.sort(key=lambda a: a.name)
|
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):
|
def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import alsaaudio
|
import alsaaudio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.mixers.base import BaseMixer
|
from mopidy.mixers.base import BaseMixer
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.mixers.alsa')
|
logger = logging.getLogger('mopidy.mixers.alsa')
|
||||||
|
|
||||||
class AlsaMixer(BaseMixer):
|
class AlsaMixer(ThreadingActor, BaseMixer):
|
||||||
"""
|
"""
|
||||||
Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control
|
Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control
|
||||||
volume.
|
volume.
|
||||||
@ -20,8 +22,10 @@ class AlsaMixer(BaseMixer):
|
|||||||
- :attr:`mopidy.settings.MIXER_ALSA_CONTROL`
|
- :attr:`mopidy.settings.MIXER_ALSA_CONTROL`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self):
|
||||||
super(AlsaMixer, self).__init__(*args, **kwargs)
|
self._mixer = None
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
self._mixer = alsaaudio.Mixer(self._get_mixer_control())
|
self._mixer = alsaaudio.Mixer(self._get_mixer_control())
|
||||||
assert self._mixer is not None
|
assert self._mixer is not None
|
||||||
|
|
||||||
|
|||||||
@ -2,17 +2,12 @@ from mopidy import settings
|
|||||||
|
|
||||||
class BaseMixer(object):
|
class BaseMixer(object):
|
||||||
"""
|
"""
|
||||||
:param backend: a backend instance
|
|
||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
|
||||||
|
|
||||||
**Settings:**
|
**Settings:**
|
||||||
|
|
||||||
- :attr:`mopidy.settings.MIXER_MAX_VOLUME`
|
- :attr:`mopidy.settings.MIXER_MAX_VOLUME`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, backend, *args, **kwargs):
|
amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
|
||||||
self.backend = backend
|
|
||||||
self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume(self):
|
def volume(self):
|
||||||
@ -35,9 +30,6 @@ class BaseMixer(object):
|
|||||||
volume = 100
|
volume = 100
|
||||||
self._set_volume(volume)
|
self._set_volume(volume)
|
||||||
|
|
||||||
def destroy(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _get_volume(self):
|
def _get_volume(self):
|
||||||
"""
|
"""
|
||||||
Return volume as integer in range [0, 100]. :class:`None` if unknown.
|
Return volume as integer in range [0, 100]. :class:`None` if unknown.
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
from threading import Lock
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.mixers.base import BaseMixer
|
from mopidy.mixers.base import BaseMixer
|
||||||
|
|
||||||
logger = logging.getLogger(u'mopidy.mixers.denon')
|
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
|
Mixer for controlling Denon amplifiers and receivers using the RS-232
|
||||||
protocol.
|
protocol.
|
||||||
@ -25,27 +26,19 @@ class DenonMixer(BaseMixer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
self._device = kwargs.get('device', None)
|
||||||
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._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
|
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
|
||||||
self._volume = 0
|
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):
|
def _get_volume(self):
|
||||||
self._lock.acquire()
|
self._ensure_open_device()
|
||||||
self.ensure_open_device()
|
|
||||||
self._device.write('MV?\r')
|
self._device.write('MV?\r')
|
||||||
vol = str(self._device.readline()[2:4])
|
vol = str(self._device.readline()[2:4])
|
||||||
self._lock.release()
|
|
||||||
logger.debug(u'_get_volume() = %s' % vol)
|
logger.debug(u'_get_volume() = %s' % vol)
|
||||||
return self._levels.index(vol)
|
return self._levels.index(vol)
|
||||||
|
|
||||||
@ -53,14 +46,12 @@ class DenonMixer(BaseMixer):
|
|||||||
# Clamp according to Denon-spec
|
# Clamp according to Denon-spec
|
||||||
if volume > 99:
|
if volume > 99:
|
||||||
volume = 99
|
volume = 99
|
||||||
self._lock.acquire()
|
self._ensure_open_device()
|
||||||
self.ensure_open_device()
|
|
||||||
self._device.write('MV%s\r'% self._levels[volume])
|
self._device.write('MV%s\r'% self._levels[volume])
|
||||||
vol = self._device.readline()[2:4]
|
vol = self._device.readline()[2:4]
|
||||||
self._lock.release()
|
|
||||||
self._volume = self._levels.index(vol)
|
self._volume = self._levels.index(vol)
|
||||||
|
|
||||||
def ensure_open_device(self):
|
def _ensure_open_device(self):
|
||||||
if not self._device.isOpen():
|
if not self._device.isOpen():
|
||||||
logger.debug(u'(re)connecting to Denon device')
|
logger.debug(u'(re)connecting to Denon device')
|
||||||
self._device.open()
|
self._device.open()
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy.mixers.base import BaseMixer
|
from mopidy.mixers.base import BaseMixer
|
||||||
|
|
||||||
class DummyMixer(BaseMixer):
|
class DummyMixer(ThreadingActor, BaseMixer):
|
||||||
"""Mixer which just stores and reports the chosen volume."""
|
"""Mixer which just stores and reports the chosen volume."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self):
|
||||||
super(DummyMixer, self).__init__(*args, **kwargs)
|
|
||||||
self._volume = None
|
self._volume = None
|
||||||
|
|
||||||
def _get_volume(self):
|
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."""
|
"""Mixer which uses GStreamer to control volume in software."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self):
|
||||||
super(GStreamerSoftwareMixer, self).__init__(*args, **kwargs)
|
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):
|
def _get_volume(self):
|
||||||
return self.backend.output.get_volume()
|
return self.output.get_volume().get()
|
||||||
|
|
||||||
def _set_volume(self, volume):
|
def _set_volume(self, volume):
|
||||||
self.backend.output.set_volume(volume)
|
self.output.set_volume(volume).get()
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from serial import Serial
|
import serial
|
||||||
from multiprocessing import Pipe
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.mixers.base import BaseMixer
|
from mopidy.mixers.base import BaseMixer
|
||||||
from mopidy.utils.process import BaseThread
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.mixers.nad')
|
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
|
Mixer for controlling NAD amplifiers and receivers using the NAD RS-232
|
||||||
protocol.
|
protocol.
|
||||||
@ -36,21 +36,19 @@ class NadMixer(BaseMixer):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self):
|
||||||
super(NadMixer, self).__init__(*args, **kwargs)
|
self._volume_cache = None
|
||||||
self._volume = None
|
self._nad_talker = NadTalker.start().proxy()
|
||||||
self._pipe, other_end = Pipe()
|
|
||||||
NadTalker(self.backend.core_queue, pipe=other_end).start()
|
|
||||||
|
|
||||||
def _get_volume(self):
|
def _get_volume(self):
|
||||||
return self._volume
|
return self._volume_cache
|
||||||
|
|
||||||
def _set_volume(self, volume):
|
def _set_volume(self, volume):
|
||||||
self._volume = volume
|
self._volume_cache = volume
|
||||||
self._pipe.send({'command': 'set_volume', 'volume': volume})
|
self._nad_talker.set_volume(volume)
|
||||||
|
|
||||||
|
|
||||||
class NadTalker(BaseThread):
|
class NadTalker(ThreadingActor):
|
||||||
"""
|
"""
|
||||||
Independent process which does the communication with the NAD device.
|
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.
|
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
|
||||||
_nad_volume = None
|
_nad_volume = None
|
||||||
|
|
||||||
def __init__(self, core_queue, pipe=None):
|
def __init__(self):
|
||||||
super(NadTalker, self).__init__(core_queue)
|
|
||||||
self.name = u'NadTalker'
|
|
||||||
self.pipe = pipe
|
|
||||||
self._device = None
|
self._device = None
|
||||||
|
|
||||||
def run_inside_try(self):
|
def on_start(self):
|
||||||
self._open_connection()
|
self._open_connection()
|
||||||
self._set_device_to_known_state()
|
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):
|
def _open_connection(self):
|
||||||
# Opens serial connection to the device.
|
# Opens serial connection to the device.
|
||||||
# Communication settings: 115200 bps 8N1
|
# Communication settings: 115200 bps 8N1
|
||||||
logger.info(u'Connecting to serial device "%s"',
|
logger.info(u'Connecting to serial device "%s"',
|
||||||
settings.MIXER_EXT_PORT)
|
settings.MIXER_EXT_PORT)
|
||||||
self._device = Serial(port=settings.MIXER_EXT_PORT, baudrate=115200,
|
self._device = serial.Serial(port=settings.MIXER_EXT_PORT,
|
||||||
timeout=self.TIMEOUT)
|
baudrate=115200, timeout=self.TIMEOUT)
|
||||||
self._get_device_model()
|
self._get_device_model()
|
||||||
|
|
||||||
def _set_device_to_known_state(self):
|
def _set_device_to_known_state(self):
|
||||||
@ -164,7 +153,7 @@ class NadTalker(BaseThread):
|
|||||||
self._nad_volume = 0
|
self._nad_volume = 0
|
||||||
logger.info(u'Done calibrating NAD amplifier')
|
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
|
# Increase or decrease the amplifier volume until it matches the given
|
||||||
# target volume.
|
# target volume.
|
||||||
logger.debug(u'Setting volume to %d' % volume)
|
logger.debug(u'Setting volume to %d' % volume)
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy.mixers.base import BaseMixer
|
from mopidy.mixers.base import BaseMixer
|
||||||
|
|
||||||
class OsaMixer(BaseMixer):
|
class OsaMixer(ThreadingActor, BaseMixer):
|
||||||
"""
|
"""
|
||||||
Mixer which uses ``osascript`` on OS X to control volume.
|
Mixer which uses ``osascript`` on OS X to control volume.
|
||||||
|
|
||||||
@ -14,7 +16,6 @@ class OsaMixer(BaseMixer):
|
|||||||
**Settings:**
|
**Settings:**
|
||||||
|
|
||||||
- None
|
- None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CACHE_TTL = 30
|
CACHE_TTL = 30
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from copy import copy
|
|
||||||
|
|
||||||
from mopidy.frontends.mpd import translator
|
from mopidy.frontends.mpd import translator
|
||||||
|
|
||||||
class ImmutableObject(object):
|
class ImmutableObject(object):
|
||||||
@ -23,6 +21,17 @@ class ImmutableObject(object):
|
|||||||
return super(ImmutableObject, self).__setattr__(name, value)
|
return super(ImmutableObject, self).__setattr__(name, value)
|
||||||
raise AttributeError('Object is immutable.')
|
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):
|
def __hash__(self):
|
||||||
hash_sum = 0
|
hash_sum = 0
|
||||||
for key, value in self.__dict__.items():
|
for key, value in self.__dict__.items():
|
||||||
@ -65,6 +74,7 @@ class ImmutableObject(object):
|
|||||||
% key)
|
% key)
|
||||||
return self.__class__(**data)
|
return self.__class__(**data)
|
||||||
|
|
||||||
|
|
||||||
class Artist(ImmutableObject):
|
class Artist(ImmutableObject):
|
||||||
"""
|
"""
|
||||||
:param uri: artist URI
|
:param uri: artist URI
|
||||||
@ -105,6 +115,9 @@ class Album(ImmutableObject):
|
|||||||
#: The album name. Read-only.
|
#: The album name. Read-only.
|
||||||
name = None
|
name = None
|
||||||
|
|
||||||
|
#: A set of album artists. Read-only.
|
||||||
|
artists = frozenset()
|
||||||
|
|
||||||
#: The number of tracks in the album. Read-only.
|
#: The number of tracks in the album. Read-only.
|
||||||
num_tracks = 0
|
num_tracks = 0
|
||||||
|
|
||||||
@ -112,14 +125,9 @@ class Album(ImmutableObject):
|
|||||||
musicbrainz_id = None
|
musicbrainz_id = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._artists = frozenset(kwargs.pop('artists', []))
|
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
|
||||||
super(Album, self).__init__(*args, **kwargs)
|
super(Album, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
|
||||||
def artists(self):
|
|
||||||
"""List of :class:`Artist` elements. Read-only."""
|
|
||||||
return list(self._artists)
|
|
||||||
|
|
||||||
|
|
||||||
class Track(ImmutableObject):
|
class Track(ImmutableObject):
|
||||||
"""
|
"""
|
||||||
@ -149,6 +157,9 @@ class Track(ImmutableObject):
|
|||||||
#: The track name. Read-only.
|
#: The track name. Read-only.
|
||||||
name = None
|
name = None
|
||||||
|
|
||||||
|
#: A set of track artists. Read-only.
|
||||||
|
artists = frozenset()
|
||||||
|
|
||||||
#: The track :class:`Album`. Read-only.
|
#: The track :class:`Album`. Read-only.
|
||||||
album = None
|
album = None
|
||||||
|
|
||||||
@ -168,14 +179,9 @@ class Track(ImmutableObject):
|
|||||||
musicbrainz_id = None
|
musicbrainz_id = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._artists = frozenset(kwargs.pop('artists', []))
|
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
|
||||||
super(Track, self).__init__(*args, **kwargs)
|
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):
|
def mpd_format(self, *args, **kwargs):
|
||||||
return translator.track_to_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.
|
#: The playlist name. Read-only.
|
||||||
name = None
|
name = None
|
||||||
|
|
||||||
|
#: The playlist's tracks. Read-only.
|
||||||
|
tracks = tuple()
|
||||||
|
|
||||||
#: The playlist modification time. Read-only.
|
#: The playlist modification time. Read-only.
|
||||||
#:
|
#:
|
||||||
#: :class:`datetime.datetime`, or :class:`None` if unknown.
|
#: :class:`datetime.datetime`, or :class:`None` if unknown.
|
||||||
last_modified = None
|
last_modified = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._tracks = kwargs.pop('tracks', [])
|
self.__dict__['tracks'] = tuple(kwargs.pop('tracks', []))
|
||||||
super(Playlist, self).__init__(*args, **kwargs)
|
super(Playlist, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
|
||||||
def tracks(self):
|
|
||||||
"""List of :class:`Track` elements. Read-only."""
|
|
||||||
return copy(self._tracks)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def length(self):
|
def length(self):
|
||||||
"""The number of tracks in the playlist. Read-only."""
|
"""The number of tracks in the playlist. Read-only."""
|
||||||
return len(self._tracks)
|
return len(self.tracks)
|
||||||
|
|
||||||
def mpd_format(self, *args, **kwargs):
|
def mpd_format(self, *args, **kwargs):
|
||||||
return translator.playlist_to_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.
|
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):
|
def play_uri(self, uri):
|
||||||
"""
|
"""
|
||||||
Play URI.
|
Play URI.
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy.outputs.base import BaseOutput
|
from mopidy.outputs.base import BaseOutput
|
||||||
|
|
||||||
class DummyOutput(BaseOutput):
|
class DummyOutput(ThreadingActor, BaseOutput):
|
||||||
"""
|
"""
|
||||||
Audio output used for testing.
|
Audio output used for testing.
|
||||||
"""
|
"""
|
||||||
@ -8,15 +10,6 @@ class DummyOutput(BaseOutput):
|
|||||||
# pylint: disable = R0902
|
# pylint: disable = R0902
|
||||||
# Too many instance attributes (9/7)
|
# 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`.
|
#: For testing. Contains the last URI passed to :meth:`play_uri`.
|
||||||
uri = None
|
uri = None
|
||||||
|
|
||||||
@ -40,15 +33,6 @@ class DummyOutput(BaseOutput):
|
|||||||
#: For testing. Contains the current volume.
|
#: For testing. Contains the current volume.
|
||||||
volume = 100
|
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):
|
def play_uri(self, uri):
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
return True
|
return True
|
||||||
|
|||||||
@ -3,113 +3,39 @@ pygst.require('0.10')
|
|||||||
import gst
|
import gst
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
|
from pykka.actor import ThreadingActor
|
||||||
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
|
from mopidy.backends.base import Backend
|
||||||
from mopidy.outputs.base import BaseOutput
|
from mopidy.outputs.base import BaseOutput
|
||||||
from mopidy.utils.process import (BaseThread, pickle_connection,
|
|
||||||
unpickle_connection)
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.outputs.gstreamer')
|
logger = logging.getLogger('mopidy.outputs.gstreamer')
|
||||||
|
|
||||||
class GStreamerOutput(BaseOutput):
|
class GStreamerOutput(ThreadingActor, BaseOutput):
|
||||||
"""
|
"""
|
||||||
Audio output through GStreamer.
|
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||||
|
|
||||||
Starts :class:`GStreamerMessagesThread` and :class:`GStreamerPlayerThread`.
|
|
||||||
|
|
||||||
**Settings:**
|
**Settings:**
|
||||||
|
|
||||||
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
|
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self):
|
||||||
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
|
|
||||||
self.gst_pipeline = None
|
self.gst_pipeline = None
|
||||||
|
|
||||||
def run_inside_try(self):
|
def on_start(self):
|
||||||
self.setup()
|
self._setup_gstreamer()
|
||||||
while True:
|
|
||||||
message = self.output_queue.get()
|
def _setup_gstreamer(self):
|
||||||
self.process_mopidy_message(message)
|
"""
|
||||||
|
**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')
|
logger.debug(u'Setting up GStreamer pipeline')
|
||||||
|
|
||||||
self.gst_pipeline = gst.parse_launch(' ! '.join([
|
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')
|
pad = self.gst_pipeline.get_by_name('convert').get_pad('sink')
|
||||||
|
|
||||||
if settings.BACKENDS[0] == 'mopidy.backends.local.LocalBackend':
|
uridecodebin = gst.element_factory_make('uridecodebin', 'uri')
|
||||||
uri_bin = gst.element_factory_make('uridecodebin', 'uri')
|
uridecodebin.connect('pad-added', self._process_new_pad, pad)
|
||||||
uri_bin.connect('pad-added', self.process_new_pad, pad)
|
self.gst_pipeline.add(uridecodebin)
|
||||||
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)
|
|
||||||
|
|
||||||
# Setup bus and message processor
|
# Setup bus and message processor
|
||||||
gst_bus = self.gst_pipeline.get_bus()
|
gst_bus = self.gst_pipeline.get_bus()
|
||||||
gst_bus.add_signal_watch()
|
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)
|
pad.link(target_pad)
|
||||||
|
|
||||||
def process_mopidy_message(self, message):
|
def _process_gstreamer_message(self, bus, 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):
|
|
||||||
"""Process messages from GStreamer."""
|
"""Process messages from GStreamer."""
|
||||||
if message.type == gst.MESSAGE_EOS:
|
if message.type == gst.MESSAGE_EOS:
|
||||||
logger.debug(u'GStreamer signalled end-of-stream. '
|
logger.debug(u'GStreamer signalled end-of-stream. '
|
||||||
'Sending end_of_track to core_queue ...')
|
'Telling backend ...')
|
||||||
self.core_queue.put({'command': 'end_of_track'})
|
self._get_backend().playback.on_end_of_track()
|
||||||
elif message.type == gst.MESSAGE_ERROR:
|
elif message.type == gst.MESSAGE_ERROR:
|
||||||
self.set_state('NULL')
|
self.set_state('NULL')
|
||||||
error, debug = message.parse_error()
|
error, debug = message.parse_error()
|
||||||
logger.error(u'%s %s', error, debug)
|
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?
|
# 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):
|
def play_uri(self, uri):
|
||||||
"""Play audio at URI"""
|
"""Play audio at URI"""
|
||||||
self.set_state('READY')
|
self.set_state('READY')
|
||||||
@ -200,12 +84,12 @@ class GStreamerPlayerThread(BaseThread):
|
|||||||
|
|
||||||
def deliver_data(self, caps_string, data):
|
def deliver_data(self, caps_string, data):
|
||||||
"""Deliver audio data to be played"""
|
"""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)
|
caps = gst.caps_from_string(caps_string)
|
||||||
buffer_ = gst.Buffer(buffer(data))
|
buffer_ = gst.Buffer(buffer(data))
|
||||||
buffer_.set_caps(caps)
|
buffer_.set_caps(caps)
|
||||||
app_src.set_property('caps', caps)
|
source.set_property('caps', caps)
|
||||||
app_src.emit('push-buffer', buffer_)
|
source.emit('push-buffer', buffer_)
|
||||||
|
|
||||||
def end_of_data_stream(self):
|
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
|
We will get a GStreamer message when the stream playback reaches the
|
||||||
token, and can then do any end-of-stream related tasks.
|
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):
|
def set_state(self, state_name):
|
||||||
"""
|
"""
|
||||||
@ -252,18 +151,3 @@ class GStreamerPlayerThread(BaseThread):
|
|||||||
gst_volume = self.gst_pipeline.get_by_name('volume')
|
gst_volume = self.gst_pipeline.get_by_name('volume')
|
||||||
gst_volume.set_property('volume', volume / 100.0)
|
gst_volume.set_property('volume', volume / 100.0)
|
||||||
return True
|
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
|
||||||
import logging.handlers
|
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):
|
def setup_logging(verbosity_level, save_debug_log):
|
||||||
setup_root_logger()
|
setup_root_logger()
|
||||||
setup_console_logging(verbosity_level)
|
setup_console_logging(verbosity_level)
|
||||||
if save_debug_log:
|
if save_debug_log:
|
||||||
setup_debug_logging_to_file()
|
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():
|
def setup_root_logger():
|
||||||
root = logging.getLogger('')
|
root = logging.getLogger('')
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import threading
|
||||||
import multiprocessing.dummy
|
|
||||||
from multiprocessing.reduction import reduce_connection
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
import gobject
|
import gobject
|
||||||
gobject.threads_init()
|
gobject.threads_init()
|
||||||
@ -11,52 +8,10 @@ from mopidy import SettingsError
|
|||||||
|
|
||||||
logger = logging.getLogger('mopidy.utils.process')
|
logger = logging.getLogger('mopidy.utils.process')
|
||||||
|
|
||||||
def pickle_connection(connection):
|
|
||||||
return pickle.dumps(reduce_connection(connection))
|
|
||||||
|
|
||||||
def unpickle_connection(pickled_connection):
|
class BaseThread(threading.Thread):
|
||||||
# From http://stackoverflow.com/questions/1446004
|
def __init__(self):
|
||||||
(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):
|
|
||||||
super(BaseThread, self).__init__()
|
super(BaseThread, self).__init__()
|
||||||
self.core_queue = core_queue
|
|
||||||
# No thread should block process from exiting
|
# No thread should block process from exiting
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
|
|
||||||
@ -84,8 +39,6 @@ class BaseThread(multiprocessing.dummy.Process):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def exit(self, status=0, reason=None):
|
def exit(self, status=0, reason=None):
|
||||||
self.core_queue.put({'to': 'core', 'command': 'exit',
|
|
||||||
'status': status, 'reason': reason})
|
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
@ -98,8 +51,8 @@ class GObjectEventThread(BaseThread):
|
|||||||
:mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
|
:mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, core_queue):
|
def __init__(self):
|
||||||
super(GObjectEventThread, self).__init__(core_queue)
|
super(GObjectEventThread, self).__init__()
|
||||||
self.name = u'GObjectEventThread'
|
self.name = u'GObjectEventThread'
|
||||||
self.loop = None
|
self.loop = None
|
||||||
|
|
||||||
|
|||||||
@ -141,8 +141,7 @@ def list_settings_optparse_callback(*args):
|
|||||||
lines = []
|
lines = []
|
||||||
for (key, value) in sorted(settings.current.iteritems()):
|
for (key, value) in sorted(settings.current.iteritems()):
|
||||||
default_value = settings.default.get(key)
|
default_value = settings.default.get(key)
|
||||||
if key.endswith('PASSWORD') and len(value):
|
value = mask_value_if_secret(key, value)
|
||||||
value = u'********'
|
|
||||||
lines.append(u'%s:' % key)
|
lines.append(u'%s:' % key)
|
||||||
lines.append(u' Value: %s' % repr(value))
|
lines.append(u' Value: %s' % repr(value))
|
||||||
if value != default_value and default_value is not None:
|
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])
|
lines.append(u' Error: %s' % errors[key])
|
||||||
print u'Settings: %s' % indent('\n'.join(lines), places=2)
|
print u'Settings: %s' % indent('\n'.join(lines), places=2)
|
||||||
sys.exit(0)
|
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
|
coverage
|
||||||
|
mock
|
||||||
nose
|
nose
|
||||||
|
tox
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
try: # 2.7
|
try: # 2.7
|
||||||
|
# pylint: disable = E0611,F0401
|
||||||
from unittest.case import SkipTest
|
from unittest.case import SkipTest
|
||||||
|
# pylint: enable = E0611,F0401
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try: # Nose
|
try: # Nose
|
||||||
from nose.plugins.skip import SkipTest
|
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
|
# Nuke any local settings to ensure same test env all over
|
||||||
settings.local.clear()
|
settings.local.clear()
|
||||||
|
|
||||||
def data_folder(name):
|
def path_to_data_dir(name):
|
||||||
folder = os.path.dirname(__file__)
|
path = os.path.dirname(__file__)
|
||||||
folder = os.path.join(folder, 'data')
|
path = os.path.join(path, 'data')
|
||||||
folder = os.path.abspath(folder)
|
path = os.path.abspath(path)
|
||||||
return os.path.join(folder, name)
|
return os.path.join(path, name)
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
|
import mock
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.mixers.dummy import DummyMixer
|
|
||||||
from mopidy.models import Playlist, Track
|
from mopidy.models import Playlist, Track
|
||||||
from mopidy.outputs.dummy import DummyOutput
|
from mopidy.outputs.base import BaseOutput
|
||||||
from mopidy.utils import get_class
|
|
||||||
|
|
||||||
from tests.backends.base import populate_playlist
|
from tests.backends.base import populate_playlist
|
||||||
|
|
||||||
@ -13,19 +11,13 @@ class CurrentPlaylistControllerTest(object):
|
|||||||
tracks = []
|
tracks = []
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.core_queue = multiprocessing.Queue()
|
self.backend = self.backend_class()
|
||||||
self.output = DummyOutput(self.core_queue)
|
self.backend.output = mock.Mock(spec=BaseOutput)
|
||||||
self.backend = self.backend_class(
|
|
||||||
self.core_queue, self.output, DummyMixer)
|
|
||||||
self.controller = self.backend.current_playlist
|
self.controller = self.backend.current_playlist
|
||||||
self.playback = self.backend.playback
|
self.playback = self.backend.playback
|
||||||
|
|
||||||
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
|
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.backend.destroy()
|
|
||||||
self.output.destroy()
|
|
||||||
|
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
for track in self.tracks:
|
for track in self.tracks:
|
||||||
cp_track = self.controller.add(track)
|
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 mopidy.models import Playlist, Track, Album, Artist
|
||||||
|
|
||||||
from tests import SkipTest, data_folder
|
from tests import SkipTest, path_to_data_dir
|
||||||
|
|
||||||
class LibraryControllerTest(object):
|
class LibraryControllerTest(object):
|
||||||
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
|
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
|
||||||
@ -9,18 +8,15 @@ class LibraryControllerTest(object):
|
|||||||
Album(name='album2', artists=artists[1:2]),
|
Album(name='album2', artists=artists[1:2]),
|
||||||
Album()]
|
Album()]
|
||||||
tracks = [Track(name='track1', length=4000, artists=artists[:1],
|
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],
|
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()]
|
Track()]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.backend = self.backend_class(mixer_class=DummyMixer)
|
self.backend = self.backend_class()
|
||||||
self.library = self.backend.library
|
self.library = self.backend.library
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.backend.destroy()
|
|
||||||
|
|
||||||
def test_refresh(self):
|
def test_refresh(self):
|
||||||
self.library.refresh()
|
self.library.refresh()
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
|
import mock
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from mopidy import settings
|
|
||||||
from mopidy.mixers.dummy import DummyMixer
|
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
from mopidy.outputs.dummy import DummyOutput
|
from mopidy.outputs.base import BaseOutput
|
||||||
from mopidy.utils import get_class
|
|
||||||
|
|
||||||
from tests import SkipTest
|
from tests import SkipTest
|
||||||
from tests.backends.base import populate_playlist
|
from tests.backends.base import populate_playlist
|
||||||
@ -17,10 +15,8 @@ class PlaybackControllerTest(object):
|
|||||||
tracks = []
|
tracks = []
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.core_queue = multiprocessing.Queue()
|
self.backend = self.backend_class()
|
||||||
self.output = DummyOutput(self.core_queue)
|
self.backend.output = mock.Mock(spec=BaseOutput)
|
||||||
self.backend = self.backend_class(
|
|
||||||
self.core_queue, self.output, DummyMixer)
|
|
||||||
self.playback = self.backend.playback
|
self.playback = self.backend.playback
|
||||||
self.current_playlist = self.backend.current_playlist
|
self.current_playlist = self.backend.current_playlist
|
||||||
|
|
||||||
@ -29,10 +25,6 @@ class PlaybackControllerTest(object):
|
|||||||
assert self.tracks[0].length >= 2000, \
|
assert self.tracks[0].length >= 2000, \
|
||||||
'First song needs to be at least 2000 miliseconds'
|
'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):
|
def test_initial_state_is_stopped(self):
|
||||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
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):
|
def test_next_until_end_of_playlist_and_play_from_start(self):
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
|
|
||||||
for track in self.tracks:
|
for _ in self.tracks:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
|
|
||||||
self.assertEqual(self.playback.current_track, None)
|
self.assertEqual(self.playback.current_track, None)
|
||||||
@ -258,7 +250,7 @@ class PlaybackControllerTest(object):
|
|||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_next_track_at_end_of_playlist(self):
|
def test_next_track_at_end_of_playlist(self):
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
for track in self.current_playlist.cp_tracks[1:]:
|
for _ in self.current_playlist.cp_tracks[1:]:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
self.assertEqual(self.playback.track_at_next, None)
|
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):
|
def test_next_track_at_end_of_playlist_with_repeat(self):
|
||||||
self.playback.repeat = True
|
self.playback.repeat = True
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
for track in self.tracks[1:]:
|
for _ in self.tracks[1:]:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
self.assertEqual(self.playback.track_at_next, self.tracks[0])
|
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):
|
def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
|
|
||||||
for track in self.tracks:
|
for _ in self.tracks:
|
||||||
self.playback.on_end_of_track()
|
self.playback.on_end_of_track()
|
||||||
|
|
||||||
self.assertEqual(self.playback.current_track, None)
|
self.assertEqual(self.playback.current_track, None)
|
||||||
@ -394,7 +386,7 @@ class PlaybackControllerTest(object):
|
|||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_end_of_track_track_at_end_of_playlist(self):
|
def test_end_of_track_track_at_end_of_playlist(self):
|
||||||
self.playback.play()
|
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.playback.on_end_of_track()
|
||||||
self.assertEqual(self.playback.track_at_next, None)
|
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):
|
def test_end_of_track_track_at_end_of_playlist_with_repeat(self):
|
||||||
self.playback.repeat = True
|
self.playback.repeat = True
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
for track in self.tracks[1:]:
|
for _ in self.tracks[1:]:
|
||||||
self.playback.on_end_of_track()
|
self.playback.on_end_of_track()
|
||||||
self.assertEqual(self.playback.track_at_next, self.tracks[0])
|
self.assertEqual(self.playback.track_at_next, self.tracks[0])
|
||||||
|
|
||||||
@ -466,7 +458,7 @@ class PlaybackControllerTest(object):
|
|||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_previous_track_with_consume(self):
|
def test_previous_track_with_consume(self):
|
||||||
self.playback.consume = True
|
self.playback.consume = True
|
||||||
for track in self.tracks:
|
for _ in self.tracks:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
self.assertEqual(self.playback.track_at_previous,
|
self.assertEqual(self.playback.track_at_previous,
|
||||||
self.playback.current_track)
|
self.playback.current_track)
|
||||||
@ -474,7 +466,7 @@ class PlaybackControllerTest(object):
|
|||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_previous_track_with_random(self):
|
def test_previous_track_with_random(self):
|
||||||
self.playback.random = True
|
self.playback.random = True
|
||||||
for track in self.tracks:
|
for _ in self.tracks:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
self.assertEqual(self.playback.track_at_previous,
|
self.assertEqual(self.playback.track_at_previous,
|
||||||
self.playback.current_track)
|
self.playback.current_track)
|
||||||
@ -547,7 +539,6 @@ class PlaybackControllerTest(object):
|
|||||||
|
|
||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_on_current_playlist_change_when_stopped(self):
|
def test_on_current_playlist_change_when_stopped(self):
|
||||||
current_track = self.playback.current_track
|
|
||||||
self.backend.current_playlist.append([self.tracks[2]])
|
self.backend.current_playlist.append([self.tracks[2]])
|
||||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||||
self.assertEqual(self.playback.current_track, None)
|
self.assertEqual(self.playback.current_track, None)
|
||||||
@ -677,9 +668,10 @@ class PlaybackControllerTest(object):
|
|||||||
self.playback.seek(0)
|
self.playback.seek(0)
|
||||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||||
|
|
||||||
|
@SkipTest
|
||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_seek_beyond_end_of_song(self):
|
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()
|
self.playback.play()
|
||||||
result = self.playback.seek(self.tracks[0].length*100)
|
result = self.playback.seek(self.tracks[0].length*100)
|
||||||
self.assert_(not result, 'Seek return value was %s' % result)
|
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.playback.seek(self.current_playlist.tracks[-1].length * 100)
|
||||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||||
|
|
||||||
|
@SkipTest
|
||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_seek_beyond_start_of_song(self):
|
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()
|
self.playback.play()
|
||||||
result = self.playback.seek(-1000)
|
result = self.playback.seek(-1000)
|
||||||
self.assert_(not result, 'Seek return value was %s' % result)
|
self.assert_(not result, 'Seek return value was %s' % result)
|
||||||
@ -734,10 +727,18 @@ class PlaybackControllerTest(object):
|
|||||||
self.assertEqual(self.playback.stop(), None)
|
self.assertEqual(self.playback.stop(), None)
|
||||||
|
|
||||||
def test_time_position_when_stopped(self):
|
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)
|
self.assertEqual(self.playback.time_position, 0)
|
||||||
|
|
||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_time_position_when_stopped_with_playlist(self):
|
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)
|
self.assertEqual(self.playback.time_position, 0)
|
||||||
|
|
||||||
@SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput
|
@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):
|
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
|
||||||
self.playback.consume = True
|
self.playback.consume = True
|
||||||
self.playback.play()
|
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.playback.on_end_of_track()
|
||||||
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
|
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
|
||||||
|
|
||||||
@ -824,14 +825,14 @@ class PlaybackControllerTest(object):
|
|||||||
def test_random_until_end_of_playlist(self):
|
def test_random_until_end_of_playlist(self):
|
||||||
self.playback.random = True
|
self.playback.random = True
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
for track in self.tracks[1:]:
|
for _ in self.tracks[1:]:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
self.assertEqual(self.playback.track_at_next, None)
|
self.assertEqual(self.playback.track_at_next, None)
|
||||||
|
|
||||||
@populate_playlist
|
@populate_playlist
|
||||||
def test_random_until_end_of_playlist_and_play_from_start(self):
|
def test_random_until_end_of_playlist_and_play_from_start(self):
|
||||||
self.playback.repeat = True
|
self.playback.repeat = True
|
||||||
for track in self.tracks:
|
for _ in self.tracks:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
self.assertNotEqual(self.playback.track_at_next, None)
|
self.assertNotEqual(self.playback.track_at_next, None)
|
||||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||||
@ -843,7 +844,7 @@ class PlaybackControllerTest(object):
|
|||||||
self.playback.repeat = True
|
self.playback.repeat = True
|
||||||
self.playback.random = True
|
self.playback.random = True
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
for track in self.tracks:
|
for _ in self.tracks:
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
self.assertNotEqual(self.playback.track_at_next, None)
|
self.assertNotEqual(self.playback.track_at_next, None)
|
||||||
|
|
||||||
@ -852,7 +853,7 @@ class PlaybackControllerTest(object):
|
|||||||
self.playback.random = True
|
self.playback.random = True
|
||||||
self.playback.play()
|
self.playback.play()
|
||||||
played = []
|
played = []
|
||||||
for track in self.tracks:
|
for _ in self.tracks:
|
||||||
self.assert_(self.playback.current_track not in played)
|
self.assert_(self.playback.current_track not in played)
|
||||||
played.append(self.playback.current_track)
|
played.append(self.playback.current_track)
|
||||||
self.playback.next()
|
self.playback.next()
|
||||||
|
|||||||
@ -3,23 +3,20 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.mixers.dummy import DummyMixer
|
|
||||||
from mopidy.models import Playlist
|
from mopidy.models import Playlist
|
||||||
|
|
||||||
from tests import SkipTest, data_folder
|
from tests import SkipTest, path_to_data_dir
|
||||||
|
|
||||||
class StoredPlaylistsControllerTest(object):
|
class StoredPlaylistsControllerTest(object):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
|
settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
|
||||||
settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
|
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
|
||||||
settings.LOCAL_MUSIC_PATH = data_folder('')
|
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
|
self.stored = self.backend.stored_playlists
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.backend.destroy()
|
|
||||||
|
|
||||||
if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
|
if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
|
||||||
shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)
|
shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from mopidy.utils.path import path_to_uri
|
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)
|
generate_song = lambda i: path_to_uri(song % i)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ if sys.platform == 'win32':
|
|||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.backends.local import LocalBackend
|
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
|
from tests.backends.base.library import LibraryControllerTest
|
||||||
|
|
||||||
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
|
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
|
||||||
@ -17,8 +17,8 @@ class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
|
|||||||
backend_class = LocalBackend
|
backend_class = LocalBackend
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
|
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
|
||||||
settings.LOCAL_MUSIC_PATH = data_folder('')
|
settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
|
||||||
|
|
||||||
super(LocalLibraryControllerTest, self).setUp()
|
super(LocalLibraryControllerTest, self).setUp()
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from mopidy.backends.local import LocalBackend
|
|||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
from mopidy.utils.path import path_to_uri
|
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.base.playback import PlaybackControllerTest
|
||||||
from tests.backends.local import generate_song
|
from tests.backends.local import generate_song
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
|
|||||||
settings.runtime.clear()
|
settings.runtime.clear()
|
||||||
|
|
||||||
def add_track(self, path):
|
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)
|
track = Track(uri=uri, length=4464)
|
||||||
self.backend.current_playlist.add(track)
|
self.backend.current_playlist.add(track)
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from mopidy.mixers.dummy import DummyMixer
|
|||||||
from mopidy.models import Playlist, Track
|
from mopidy.models import Playlist, Track
|
||||||
from mopidy.utils.path import path_to_uri
|
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 \
|
from tests.backends.base.stored_playlists import \
|
||||||
StoredPlaylistsControllerTest
|
StoredPlaylistsControllerTest
|
||||||
from tests.backends.local import generate_song
|
from tests.backends.local import generate_song
|
||||||
@ -65,13 +65,12 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
|
|||||||
self.assertEqual(uri, contents.strip())
|
self.assertEqual(uri, contents.strip())
|
||||||
|
|
||||||
def test_playlists_are_loaded_at_startup(self):
|
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')
|
playlist = Playlist(tracks=[track], name='test')
|
||||||
|
|
||||||
self.stored.save(playlist)
|
self.stored.save(playlist)
|
||||||
|
|
||||||
self.backend.destroy()
|
self.backend = self.backend_class()
|
||||||
self.backend = self.backend_class(mixer_class=DummyMixer)
|
|
||||||
self.stored = self.backend.stored_playlists
|
self.stored = self.backend.stored_playlists
|
||||||
|
|
||||||
self.assert_(self.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.backends.local.translator import parse_m3u, parse_mpd_tag_cache
|
||||||
from mopidy.models import Track, Artist, Album
|
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')
|
song1_path = path_to_data_dir('song1.mp3')
|
||||||
song2_path = data_folder('song2.mp3')
|
song2_path = path_to_data_dir('song2.mp3')
|
||||||
encoded_path = data_folder(u'æøå.mp3')
|
encoded_path = path_to_data_dir(u'æøå.mp3')
|
||||||
song1_uri = path_to_uri(song1_path)
|
song1_uri = path_to_uri(song1_path)
|
||||||
song2_uri = path_to_uri(song2_path)
|
song2_uri = path_to_uri(song2_path)
|
||||||
encoded_uri = path_to_uri(encoded_path)
|
encoded_uri = path_to_uri(encoded_path)
|
||||||
|
|
||||||
class M3UToUriTest(unittest.TestCase):
|
class M3UToUriTest(unittest.TestCase):
|
||||||
def test_empty_file(self):
|
def test_empty_file(self):
|
||||||
uris = parse_m3u(data_folder('empty.m3u'))
|
uris = parse_m3u(path_to_data_dir('empty.m3u'))
|
||||||
self.assertEqual([], uris)
|
self.assertEqual([], uris)
|
||||||
|
|
||||||
def test_basic_file(self):
|
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)
|
self.assertEqual([song1_uri], uris)
|
||||||
|
|
||||||
def test_file_with_comment(self):
|
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)
|
self.assertEqual([song1_uri], uris)
|
||||||
|
|
||||||
def test_file_with_absolute_files(self):
|
def test_file_with_absolute_files(self):
|
||||||
@ -64,11 +64,11 @@ class M3UToUriTest(unittest.TestCase):
|
|||||||
os.remove(tmp.name)
|
os.remove(tmp.name)
|
||||||
|
|
||||||
def test_encoding_is_latin1(self):
|
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)
|
self.assertEqual([encoded_uri], uris)
|
||||||
|
|
||||||
def test_open_missing_file(self):
|
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)
|
self.assertEqual([], uris)
|
||||||
|
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ expected_albums = [Album(name='albumname', artists=expected_artists,
|
|||||||
expected_tracks = []
|
expected_tracks = []
|
||||||
|
|
||||||
def generate_track(path, ident):
|
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,
|
track = Track(name='trackname', artists=expected_artists, track_no=1,
|
||||||
album=expected_albums[0], length=4000, uri=uri)
|
album=expected_albums[0], length=4000, uri=uri)
|
||||||
expected_tracks.append(track)
|
expected_tracks.append(track)
|
||||||
@ -98,28 +98,28 @@ generate_track('subdir1/subsubdir/song9.mp3', 1)
|
|||||||
|
|
||||||
class MPDTagCacheToTracksTest(unittest.TestCase):
|
class MPDTagCacheToTracksTest(unittest.TestCase):
|
||||||
def test_emtpy_cache(self):
|
def test_emtpy_cache(self):
|
||||||
tracks = parse_mpd_tag_cache(data_folder('empty_tag_cache'),
|
tracks = parse_mpd_tag_cache(path_to_data_dir('empty_tag_cache'),
|
||||||
data_folder(''))
|
path_to_data_dir(''))
|
||||||
self.assertEqual(set(), tracks)
|
self.assertEqual(set(), tracks)
|
||||||
|
|
||||||
def test_simple_cache(self):
|
def test_simple_cache(self):
|
||||||
tracks = parse_mpd_tag_cache(data_folder('simple_tag_cache'),
|
tracks = parse_mpd_tag_cache(path_to_data_dir('simple_tag_cache'),
|
||||||
data_folder(''))
|
path_to_data_dir(''))
|
||||||
uri = path_to_uri(data_folder('song1.mp3'))
|
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||||
track = Track(name='trackname', artists=expected_artists, track_no=1,
|
track = Track(name='trackname', artists=expected_artists, track_no=1,
|
||||||
album=expected_albums[0], length=4000, uri=uri)
|
album=expected_albums[0], length=4000, uri=uri)
|
||||||
self.assertEqual(set([track]), tracks)
|
self.assertEqual(set([track]), tracks)
|
||||||
|
|
||||||
def test_advanced_cache(self):
|
def test_advanced_cache(self):
|
||||||
tracks = parse_mpd_tag_cache(data_folder('advanced_tag_cache'),
|
tracks = parse_mpd_tag_cache(path_to_data_dir('advanced_tag_cache'),
|
||||||
data_folder(''))
|
path_to_data_dir(''))
|
||||||
self.assertEqual(set(expected_tracks), tracks)
|
self.assertEqual(set(expected_tracks), tracks)
|
||||||
|
|
||||||
def test_unicode_cache(self):
|
def test_unicode_cache(self):
|
||||||
tracks = parse_mpd_tag_cache(data_folder('utf8_tag_cache'),
|
tracks = parse_mpd_tag_cache(path_to_data_dir('utf8_tag_cache'),
|
||||||
data_folder(''))
|
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'æøå')]
|
artists = [Artist(name=u'æøå')]
|
||||||
album = Album(name=u'æøå', artists=artists)
|
album = Album(name=u'æøå', artists=artists)
|
||||||
track = Track(uri=uri, name=u'æøå', artists=artists,
|
track = Track(uri=uri, name=u'æøå', artists=artists,
|
||||||
@ -132,14 +132,14 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
|
|||||||
raise SkipTest
|
raise SkipTest
|
||||||
|
|
||||||
def test_cache_with_blank_track_info(self):
|
def test_cache_with_blank_track_info(self):
|
||||||
tracks = parse_mpd_tag_cache(data_folder('blank_tag_cache'),
|
tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'),
|
||||||
data_folder(''))
|
path_to_data_dir(''))
|
||||||
uri = path_to_uri(data_folder('song1.mp3'))
|
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||||
self.assertEqual(set([Track(uri=uri, length=4000)]), tracks)
|
self.assertEqual(set([Track(uri=uri, length=4000)]), tracks)
|
||||||
|
|
||||||
def test_musicbrainz_tagcache(self):
|
def test_musicbrainz_tagcache(self):
|
||||||
tracks = parse_mpd_tag_cache(data_folder('musicbrainz_tag_cache'),
|
tracks = parse_mpd_tag_cache(path_to_data_dir('musicbrainz_tag_cache'),
|
||||||
data_folder(''))
|
path_to_data_dir(''))
|
||||||
artist = list(expected_tracks[0].artists)[0].copy(
|
artist = list(expected_tracks[0].artists)[0].copy(
|
||||||
musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897')
|
musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897')
|
||||||
albumartist = list(expected_tracks[0].artists)[0].copy(
|
albumartist = list(expected_tracks[0].artists)[0].copy(
|
||||||
@ -153,9 +153,9 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
|
|||||||
self.assertEqual(track, list(tracks)[0])
|
self.assertEqual(track, list(tracks)[0])
|
||||||
|
|
||||||
def test_albumartist_tag_cache(self):
|
def test_albumartist_tag_cache(self):
|
||||||
tracks = parse_mpd_tag_cache(data_folder('albumartist_tag_cache'),
|
tracks = parse_mpd_tag_cache(path_to_data_dir('albumartist_tag_cache'),
|
||||||
data_folder(''))
|
path_to_data_dir(''))
|
||||||
uri = path_to_uri(data_folder('song1.mp3'))
|
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||||
artist = Artist(name='albumartistname')
|
artist = Artist(name='albumartistname')
|
||||||
album = expected_albums[0].copy(artists=[artist])
|
album = expected_albums[0].copy(artists=[artist])
|
||||||
track = Track(name='trackname', artists=expected_artists, track_no=1,
|
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