Merge branch 'develop' into feature/http-frontend
This commit is contained in:
commit
e1ef73f517
@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
@ -8,20 +10,26 @@ from mopidy.utils.log import setup_console_logging, setup_root_logger
|
||||
from mopidy.scanner import Scanner, translator
|
||||
from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
|
||||
|
||||
|
||||
setup_root_logger()
|
||||
setup_console_logging(2)
|
||||
|
||||
|
||||
tracks = []
|
||||
|
||||
|
||||
def store(data):
|
||||
track = translator(data)
|
||||
tracks.append(track)
|
||||
logging.debug(u'Added %s', track.uri)
|
||||
logging.debug('Added %s', track.uri)
|
||||
|
||||
|
||||
def debug(uri, error, debug):
|
||||
logging.error(u'Failed %s: %s - %s', uri, error, debug)
|
||||
logging.error('Failed %s: %s - %s', uri, error, debug)
|
||||
|
||||
|
||||
logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH)
|
||||
|
||||
logging.info(u'Scanning %s', settings.LOCAL_MUSIC_PATH)
|
||||
|
||||
scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
|
||||
try:
|
||||
@ -29,10 +37,12 @@ try:
|
||||
except KeyboardInterrupt:
|
||||
scanner.stop()
|
||||
|
||||
logging.info(u'Done')
|
||||
|
||||
logging.info('Done')
|
||||
|
||||
|
||||
for a in tracks_to_tag_cache_format(tracks):
|
||||
if len(a) == 1:
|
||||
print (u'%s' % a).encode('utf-8')
|
||||
print ('%s' % a).encode('utf-8')
|
||||
else:
|
||||
print (u'%s: %s' % a).encode('utf-8')
|
||||
print ('%s: %s' % a).encode('utf-8')
|
||||
|
||||
@ -19,10 +19,10 @@ Playback provider
|
||||
:members:
|
||||
|
||||
|
||||
Stored playlists provider
|
||||
=========================
|
||||
Playlists provider
|
||||
==================
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider
|
||||
.. autoclass:: mopidy.backends.base.BasePlaylistsProvider
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
@ -43,17 +43,17 @@ every request from a frontend it calls out to one or more backends which does
|
||||
the real work, and when the backends respond, the core actor is responsible for
|
||||
combining the responses into a single response to the requesting frontend.
|
||||
|
||||
The core actor also keeps track of the current playlist, since it doesn't
|
||||
belong to a specific backend.
|
||||
The core actor also keeps track of the tracklist, since it doesn't belong to a
|
||||
specific backend.
|
||||
|
||||
See :ref:`core-api` for more details.
|
||||
|
||||
.. digraph:: core_architecture
|
||||
|
||||
Core -> "Current\nplaylist\ncontroller"
|
||||
Core -> "Tracklist\ncontroller"
|
||||
Core -> "Library\ncontroller"
|
||||
Core -> "Playback\ncontroller"
|
||||
Core -> "Stored\nplaylists\ncontroller"
|
||||
Core -> "Playlists\ncontroller"
|
||||
|
||||
"Library\ncontroller" -> "Local backend"
|
||||
"Library\ncontroller" -> "Spotify backend"
|
||||
@ -62,8 +62,8 @@ See :ref:`core-api` for more details.
|
||||
"Playback\ncontroller" -> "Spotify backend"
|
||||
"Playback\ncontroller" -> Audio
|
||||
|
||||
"Stored\nplaylists\ncontroller" -> "Local backend"
|
||||
"Stored\nplaylists\ncontroller" -> "Spotify backend"
|
||||
"Playlists\ncontroller" -> "Local backend"
|
||||
"Playlists\ncontroller" -> "Spotify backend"
|
||||
|
||||
|
||||
Backends
|
||||
@ -80,12 +80,12 @@ See :ref:`backend-api` for more details.
|
||||
|
||||
"Local backend" -> "Local\nlibrary\nprovider" -> "Local disk"
|
||||
"Local backend" -> "Local\nplayback\nprovider" -> "Local disk"
|
||||
"Local backend" -> "Local\nstored\nplaylists\nprovider" -> "Local disk"
|
||||
"Local backend" -> "Local\nplaylists\nprovider" -> "Local disk"
|
||||
"Local\nplayback\nprovider" -> Audio
|
||||
|
||||
"Spotify backend" -> "Spotify\nlibrary\nprovider" -> "Spotify service"
|
||||
"Spotify backend" -> "Spotify\nplayback\nprovider" -> "Spotify service"
|
||||
"Spotify backend" -> "Spotify\nstored\nplaylists\nprovider" -> "Spotify service"
|
||||
"Spotify backend" -> "Spotify\nplaylists\nprovider" -> "Spotify service"
|
||||
"Spotify\nplayback\nprovider" -> Audio
|
||||
|
||||
|
||||
|
||||
@ -26,21 +26,21 @@ seek, and volume control.
|
||||
:members:
|
||||
|
||||
|
||||
Current playlist controller
|
||||
===========================
|
||||
Tracklist controller
|
||||
====================
|
||||
|
||||
Manages everything related to the currently loaded playlist.
|
||||
Manages everything related to the tracks we are currently playing.
|
||||
|
||||
.. autoclass:: mopidy.core.CurrentPlaylistController
|
||||
.. autoclass:: mopidy.core.TracklistController
|
||||
:members:
|
||||
|
||||
|
||||
Stored playlists controller
|
||||
===========================
|
||||
Playlists controller
|
||||
====================
|
||||
|
||||
Manages stored playlist.
|
||||
Manages persistence of playlists.
|
||||
|
||||
.. autoclass:: mopidy.core.StoredPlaylistsController
|
||||
.. autoclass:: mopidy.core.PlaylistsController
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
@ -93,6 +93,24 @@ backends:
|
||||
:attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump
|
||||
the traceback for all running threads.
|
||||
|
||||
- Make the entire code base use unicode strings by default, and only fall back
|
||||
to bytestrings where it is required. Another step closer to Python 3.
|
||||
|
||||
- The settings validator will now allow any setting prefixed with ``CUSTOM_``
|
||||
to exist in the settings file.
|
||||
|
||||
- The MPD commands ``search`` and ``find`` now allows the key ``file``, which
|
||||
is used by ncmpcpp instead of ``filename``.
|
||||
|
||||
- The Spotify backend now returns the track if you search for the Spotify track
|
||||
URI. (Fixes: :issue:`233`)
|
||||
|
||||
- Renamed "current playlist" to "tracklist" everywhere, including the core API
|
||||
used by frontends.
|
||||
|
||||
- Renamed "stored playlists" to "playlists" everywhere, including the core API
|
||||
used by frontends.
|
||||
|
||||
**Bug fixes**
|
||||
|
||||
- :issue:`218`: The MPD commands ``listplaylist`` and ``listplaylistinfo`` now
|
||||
@ -101,6 +119,9 @@ backends:
|
||||
- The MPD command ``plchanges`` always returned the entire playlist. It now
|
||||
returns an empty response when the client has seen the latest version.
|
||||
|
||||
- MPD no longer lowercases search queries. This broke e.g. search by URI, where
|
||||
casing may be essential.
|
||||
|
||||
|
||||
v0.8.1 (2012-10-30)
|
||||
===================
|
||||
|
||||
10
docs/conf.py
10
docs/conf.py
@ -12,6 +12,8 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@ -95,8 +97,8 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Mopidy'
|
||||
copyright = u'2010-2012, Stein Magnus Jodal and contributors'
|
||||
project = 'Mopidy'
|
||||
copyright = '2010-2012, Stein Magnus Jodal and contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
@ -237,8 +239,8 @@ latex_documents = [
|
||||
(
|
||||
'index',
|
||||
'Mopidy.tex',
|
||||
u'Mopidy Documentation',
|
||||
u'Stein Magnus Jodal',
|
||||
'Mopidy Documentation',
|
||||
'Stein Magnus Jodal',
|
||||
'manual'
|
||||
),
|
||||
]
|
||||
|
||||
@ -84,6 +84,22 @@ contributing.
|
||||
Code style
|
||||
==========
|
||||
|
||||
- Always import ``unicode_literals`` and use unicode literals for everything
|
||||
except where you're explicitly working with bytes, which are marked with the
|
||||
``b`` prefix.
|
||||
|
||||
Do this::
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
foo = 'I am a unicode string, which is a sane default'
|
||||
bar = b'I am a bytestring'
|
||||
|
||||
Not this::
|
||||
|
||||
foo = u'I am a unicode string'
|
||||
bar = 'I am a bytestring, but was it intentional?'
|
||||
|
||||
- Follow :pep:`8` unless otherwise noted. `pep8.py
|
||||
<http://pypi.python.org/pypi/pep8/>`_ or `flake8
|
||||
<http://pypi.python.org/pypi/flake8>`_ can be used to check your code
|
||||
|
||||
@ -153,7 +153,7 @@ plugins, ending in a summary line::
|
||||
|
||||
Next, you should be able to produce a audible tone by running::
|
||||
|
||||
gst-launch-0.10 audiotestsrc ! sudioresample ! autoaudiosink
|
||||
gst-launch-0.10 audiotestsrc ! audioresample ! autoaudiosink
|
||||
|
||||
If you cannot hear any sound when running this command, you won't hear any
|
||||
sound from Mopidy either, as Mopidy by default uses GStreamer's
|
||||
@ -200,6 +200,21 @@ can use with the ``gst-launch-0.10`` command can be plugged into
|
||||
:attr:`mopidy.settings.OUTPUT`.
|
||||
|
||||
|
||||
Custom settings
|
||||
===============
|
||||
|
||||
Mopidy's settings validator will stop you from defining any settings in your
|
||||
settings file that Mopidy doesn't know about. This may sound obnoxious, but it
|
||||
helps you detect typos in your settings, and deprecated settings that should be
|
||||
removed or updated.
|
||||
|
||||
If you're extending Mopidy in some way, and want to use Mopidy's settings
|
||||
system, you can prefix your settings with ``CUSTOM_`` to get around the
|
||||
settings validator. We recommend that you choose names like
|
||||
``CUSTOM_MYAPP_MYSETTING`` so that multiple custom extensions to Mopidy can be
|
||||
used at the same time without any danger of naming collisions.
|
||||
|
||||
|
||||
Available settings
|
||||
==================
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# pylint: disable = E0611,F0401
|
||||
from distutils.version import StrictVersion as SV
|
||||
# pylint: enable = E0611,F0401
|
||||
@ -9,13 +11,13 @@ import pykka
|
||||
|
||||
if not (2, 6) <= sys.version_info < (3,):
|
||||
sys.exit(
|
||||
u'Mopidy requires Python >= 2.6, < 3, but found %s' %
|
||||
'Mopidy requires Python >= 2.6, < 3, but found %s' %
|
||||
'.'.join(map(str, sys.version_info[:3])))
|
||||
|
||||
if (isinstance(pykka.__version__, basestring)
|
||||
and not SV('1.0') <= SV(pykka.__version__) < SV('2.0')):
|
||||
sys.exit(
|
||||
u'Mopidy requires Pykka >= 1.0, < 2, but found %s' % pykka.__version__)
|
||||
'Mopidy requires Pykka >= 1.0, < 2, but found %s' % pykka.__version__)
|
||||
|
||||
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import optparse
|
||||
import os
|
||||
@ -62,7 +64,7 @@ def main():
|
||||
except exceptions.SettingsError as ex:
|
||||
logger.error(ex.message)
|
||||
except KeyboardInterrupt:
|
||||
logger.info(u'Interrupted. Exiting...')
|
||||
logger.info('Interrupted. Exiting...')
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
finally:
|
||||
@ -76,7 +78,7 @@ def main():
|
||||
|
||||
def parse_options():
|
||||
parser = optparse.OptionParser(
|
||||
version=u'Mopidy %s' % versioning.get_version())
|
||||
version='Mopidy %s' % versioning.get_version())
|
||||
parser.add_option(
|
||||
'--help-gst',
|
||||
action='store_true', dest='help_gst',
|
||||
@ -114,15 +116,15 @@ def parse_options():
|
||||
|
||||
|
||||
def check_old_folders():
|
||||
old_settings_folder = os.path.expanduser(u'~/.mopidy')
|
||||
old_settings_folder = os.path.expanduser('~/.mopidy')
|
||||
|
||||
if not os.path.isdir(old_settings_folder):
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
u'Old settings folder found at %s, settings.py should be moved '
|
||||
u'to %s, any cache data should be deleted. See release notes for '
|
||||
u'further instructions.', old_settings_folder, path.SETTINGS_PATH)
|
||||
'Old settings folder found at %s, settings.py should be moved '
|
||||
'to %s, any cache data should be deleted. See release notes for '
|
||||
'further instructions.', old_settings_folder, path.SETTINGS_PATH)
|
||||
|
||||
|
||||
def setup_settings(interactive):
|
||||
@ -171,7 +173,7 @@ def setup_frontends(core):
|
||||
try:
|
||||
importing.get_class(frontend_class_name).start(core=core)
|
||||
except exceptions.OptionalDependencyError as ex:
|
||||
logger.info(u'Disabled: %s (%s)', frontend_class_name, ex)
|
||||
logger.info('Disabled: %s (%s)', frontend_class_name, ex)
|
||||
|
||||
|
||||
def stop_frontends():
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import Audio
|
||||
from .listener import AudioListener
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
@ -70,9 +72,9 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
# These caps matches the audio data provided by libspotify
|
||||
default_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')
|
||||
b'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
|
||||
b'width=(int)16, depth=(int)16, signed=(boolean)true, '
|
||||
b'rate=(int)44100')
|
||||
source = element.get_property('source')
|
||||
source.set_property('caps', default_caps)
|
||||
|
||||
@ -109,7 +111,7 @@ class Audio(pykka.ThreadingActor):
|
||||
return
|
||||
|
||||
# We assume that the bin will contain a single mixer.
|
||||
mixer = mixerbin.get_by_interface('GstMixer')
|
||||
mixer = mixerbin.get_by_interface(b'GstMixer')
|
||||
if not mixer:
|
||||
logger.warning(
|
||||
'Did not find any audio mixers in "%s"', settings.MIXER)
|
||||
@ -162,14 +164,14 @@ class Audio(pykka.ThreadingActor):
|
||||
self._trigger_reached_end_of_stream_event()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
error, debug = message.parse_error()
|
||||
logger.error(u'%s %s', error, debug)
|
||||
logger.error('%s %s', error, debug)
|
||||
self.stop_playback()
|
||||
elif message.type == gst.MESSAGE_WARNING:
|
||||
error, debug = message.parse_warning()
|
||||
logger.warning(u'%s %s', error, debug)
|
||||
logger.warning('%s %s', error, debug)
|
||||
|
||||
def _trigger_reached_end_of_stream_event(self):
|
||||
logger.debug(u'Triggering reached end of stream event')
|
||||
logger.debug('Triggering reached end of stream event')
|
||||
AudioListener.send('reached_end_of_stream')
|
||||
|
||||
def set_uri(self, uri):
|
||||
@ -327,7 +329,7 @@ class Audio(pykka.ThreadingActor):
|
||||
:rtype: int in range [0..100] or :class:`None`
|
||||
"""
|
||||
if self._software_mixing:
|
||||
return round(self._playbin.get_property('volume') * 100)
|
||||
return int(round(self._playbin.get_property('volume') * 100))
|
||||
|
||||
if self._mixer is None:
|
||||
return None
|
||||
@ -389,12 +391,12 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
# Default to blank data to trick shoutcast into clearing any previous
|
||||
# values it might have.
|
||||
taglist[gst.TAG_ARTIST] = u' '
|
||||
taglist[gst.TAG_TITLE] = u' '
|
||||
taglist[gst.TAG_ALBUM] = u' '
|
||||
taglist[gst.TAG_ARTIST] = ' '
|
||||
taglist[gst.TAG_TITLE] = ' '
|
||||
taglist[gst.TAG_ALBUM] = ' '
|
||||
|
||||
if artists:
|
||||
taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists])
|
||||
taglist[gst.TAG_ARTIST] = ', '.join([a.name for a in artists])
|
||||
|
||||
if track.name:
|
||||
taglist[gst.TAG_TITLE] = track.name
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
@ -12,6 +12,8 @@ This is Mopidy's default mixer.
|
||||
to ``autoaudiomixer`` to use this mixer.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
- Set :attr:`mopidy.settings.MIXER` to ``fakemixer`` to use this mixer.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gobject
|
||||
|
||||
@ -45,6 +45,8 @@ Configuration examples::
|
||||
u'source=aux speakers-a=on speakers-b=off')
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import pygst
|
||||
@ -107,7 +109,7 @@ class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
|
||||
def do_change_state(self, transition):
|
||||
if transition == gst.STATE_CHANGE_NULL_TO_READY:
|
||||
if serial is None:
|
||||
logger.warning(u'nadmixer dependency python-serial not found')
|
||||
logger.warning('nadmixer dependency python-serial not found')
|
||||
return gst.STATE_CHANGE_FAILURE
|
||||
self._start_nad_talker()
|
||||
return gst.STATE_CHANGE_SUCCESS
|
||||
@ -164,7 +166,7 @@ class NadTalker(pykka.ThreadingActor):
|
||||
self._set_device_to_known_state()
|
||||
|
||||
def _open_connection(self):
|
||||
logger.info(u'NAD amplifier: Connecting through "%s"', self.port)
|
||||
logger.info('NAD amplifier: Connecting through "%s"', self.port)
|
||||
self._device = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.BAUDRATE,
|
||||
@ -183,7 +185,7 @@ class NadTalker(pykka.ThreadingActor):
|
||||
|
||||
def _get_device_model(self):
|
||||
model = self._ask_device('Main.Model')
|
||||
logger.info(u'NAD amplifier: Connected to model "%s"', model)
|
||||
logger.info('NAD amplifier: Connected to model "%s"', model)
|
||||
return model
|
||||
|
||||
def _power_device_on(self):
|
||||
@ -212,19 +214,19 @@ class NadTalker(pykka.ThreadingActor):
|
||||
if current_nad_volume is None:
|
||||
current_nad_volume = self.VOLUME_LEVELS
|
||||
if current_nad_volume == self.VOLUME_LEVELS:
|
||||
logger.info(u'NAD amplifier: Calibrating by setting volume to 0')
|
||||
logger.info('NAD amplifier: Calibrating by setting volume to 0')
|
||||
self._nad_volume = current_nad_volume
|
||||
if self._decrease_volume():
|
||||
current_nad_volume -= 1
|
||||
if current_nad_volume == 0:
|
||||
logger.info(u'NAD amplifier: Done calibrating')
|
||||
logger.info('NAD amplifier: Done calibrating')
|
||||
else:
|
||||
self.actor_ref.proxy().calibrate_volume(current_nad_volume)
|
||||
|
||||
def set_volume(self, volume):
|
||||
# Increase or decrease the amplifier volume until it matches the given
|
||||
# target volume.
|
||||
logger.debug(u'Setting volume to %d' % volume)
|
||||
logger.debug('Setting volume to %d' % volume)
|
||||
target_nad_volume = int(round(volume * self.VOLUME_LEVELS / 100.0))
|
||||
if self._nad_volume is None:
|
||||
return # Calibration needed
|
||||
@ -250,12 +252,12 @@ class NadTalker(pykka.ThreadingActor):
|
||||
if self._ask_device(key) == value:
|
||||
return
|
||||
logger.info(
|
||||
u'NAD amplifier: Setting "%s" to "%s" (attempt %d/3)',
|
||||
'NAD amplifier: Setting "%s" to "%s" (attempt %d/3)',
|
||||
key, value, attempt)
|
||||
self._command_device(key, value)
|
||||
if self._ask_device(key) != value:
|
||||
logger.info(
|
||||
u'NAD amplifier: Gave up on setting "%s" to "%s"',
|
||||
'NAD amplifier: Gave up on setting "%s" to "%s"',
|
||||
key, value)
|
||||
|
||||
def _ask_device(self, key):
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
@ -0,0 +1 @@
|
||||
from __future__ import unicode_literals
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
|
||||
|
||||
@ -9,20 +11,36 @@ class Backend(object):
|
||||
audio = None
|
||||
|
||||
#: The library provider. An instance of
|
||||
# :class:`mopidy.backends.base.BaseLibraryProvider`.
|
||||
#: :class:`mopidy.backends.base.BaseLibraryProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide a library.
|
||||
library = None
|
||||
|
||||
#: The playback provider. An instance of
|
||||
#: :class:`mopidy.backends.base.BasePlaybackProvider`.
|
||||
#: :class:`mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if
|
||||
#: the backend doesn't provide playback.
|
||||
playback = None
|
||||
|
||||
#: The stored playlists provider. An instance of
|
||||
#: :class:`mopidy.backends.base.BaseStoredPlaylistsProvider`.
|
||||
stored_playlists = None
|
||||
#: The playlists provider. An instance of
|
||||
#: :class:`mopidy.backends.base.BasePlaylistsProvider`, or class:`None` if
|
||||
#: the backend doesn't provide playlists.
|
||||
playlists = None
|
||||
|
||||
#: List of URI schemes this backend can handle.
|
||||
uri_schemes = []
|
||||
|
||||
# Because the providers is marked as pykka_traversible, we can't get() them
|
||||
# from another actor, and need helper methods to check if the providers are
|
||||
# set or None.
|
||||
|
||||
def has_library(self):
|
||||
return self.library is not None
|
||||
|
||||
def has_playback(self):
|
||||
return self.playback is not None
|
||||
|
||||
def has_playlists(self):
|
||||
return self.playlists is not None
|
||||
|
||||
|
||||
class BaseLibraryProvider(object):
|
||||
"""
|
||||
@ -149,7 +167,7 @@ class BasePlaybackProvider(object):
|
||||
return self.audio.get_position().get()
|
||||
|
||||
|
||||
class BaseStoredPlaylistsProvider(object):
|
||||
class BasePlaylistsProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
@ -164,7 +182,7 @@ class BaseStoredPlaylistsProvider(object):
|
||||
@property
|
||||
def playlists(self):
|
||||
"""
|
||||
Currently stored playlists.
|
||||
Currently available playlists.
|
||||
|
||||
Read/write. List of :class:`mopidy.models.Playlist`.
|
||||
"""
|
||||
@ -176,7 +194,7 @@ class BaseStoredPlaylistsProvider(object):
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
See :meth:`mopidy.core.StoredPlaylistsController.create`.
|
||||
See :meth:`mopidy.core.PlaylistsController.create`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
@ -184,7 +202,7 @@ class BaseStoredPlaylistsProvider(object):
|
||||
|
||||
def delete(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.StoredPlaylistsController.delete`.
|
||||
See :meth:`mopidy.core.PlaylistsController.delete`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
@ -192,7 +210,7 @@ class BaseStoredPlaylistsProvider(object):
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.core.StoredPlaylistsController.lookup`.
|
||||
See :meth:`mopidy.core.PlaylistsController.lookup`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
@ -200,7 +218,7 @@ class BaseStoredPlaylistsProvider(object):
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
See :meth:`mopidy.core.StoredPlaylistsController.refresh`.
|
||||
See :meth:`mopidy.core.PlaylistsController.refresh`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
@ -208,7 +226,7 @@ class BaseStoredPlaylistsProvider(object):
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
See :meth:`mopidy.core.StoredPlaylistsController.save`.
|
||||
See :meth:`mopidy.core.PlaylistsController.save`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
|
||||
@ -14,6 +14,8 @@ The backend handles URIs starting with ``dummy:``.
|
||||
- None
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.backends import base
|
||||
@ -26,9 +28,9 @@ class DummyBackend(pykka.ThreadingActor, base.Backend):
|
||||
|
||||
self.library = DummyLibraryProvider(backend=self)
|
||||
self.playback = DummyPlaybackProvider(audio=audio, backend=self)
|
||||
self.stored_playlists = DummyStoredPlaylistsProvider(backend=self)
|
||||
self.playlists = DummyPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = [u'dummy']
|
||||
self.uri_schemes = ['dummy']
|
||||
|
||||
|
||||
class DummyLibraryProvider(base.BaseLibraryProvider):
|
||||
@ -78,7 +80,7 @@ class DummyPlaybackProvider(base.BasePlaybackProvider):
|
||||
return self._time_position
|
||||
|
||||
|
||||
class DummyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||
class DummyPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
def create(self, name):
|
||||
playlist = Playlist(name=name)
|
||||
self._playlists.append(playlist)
|
||||
|
||||
@ -20,5 +20,7 @@ https://github.com/mopidy/mopidy/issues?labels=Local+backend
|
||||
- :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import LocalBackend
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import pykka
|
||||
@ -5,9 +7,9 @@ import pykka
|
||||
from mopidy.backends import base
|
||||
|
||||
from .library import LocalLibraryProvider
|
||||
from .stored_playlists import LocalStoredPlaylistsProvider
|
||||
from .playlists import LocalPlaylistsProvider
|
||||
|
||||
logger = logging.getLogger(u'mopidy.backends.local')
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
class LocalBackend(pykka.ThreadingActor, base.Backend):
|
||||
@ -16,6 +18,6 @@ class LocalBackend(pykka.ThreadingActor, base.Backend):
|
||||
|
||||
self.library = LocalLibraryProvider(backend=self)
|
||||
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
|
||||
self.stored_playlists = LocalStoredPlaylistsProvider(backend=self)
|
||||
self.playlists = LocalPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = [u'file']
|
||||
self.uri_schemes = ['file']
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
@ -6,7 +8,7 @@ from mopidy.models import Playlist, Album
|
||||
|
||||
from .translator import parse_mpd_tag_cache
|
||||
|
||||
logger = logging.getLogger(u'mopidy.backends.local')
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
@ -30,7 +32,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
try:
|
||||
return self._uri_mapping[uri]
|
||||
except KeyError:
|
||||
logger.debug(u'Failed to lookup %r', uri)
|
||||
logger.debug('Failed to lookup %r', uri)
|
||||
return None
|
||||
|
||||
def find_exact(self, **query):
|
||||
@ -59,7 +61,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
result_tracks = filter(album_filter, result_tracks)
|
||||
elif field == 'artist':
|
||||
result_tracks = filter(artist_filter, result_tracks)
|
||||
elif field in ('uri', 'filename'):
|
||||
elif field == 'uri':
|
||||
result_tracks = filter(uri_filter, result_tracks)
|
||||
elif field == 'any':
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
@ -93,7 +95,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
result_tracks = filter(album_filter, result_tracks)
|
||||
elif field == 'artist':
|
||||
result_tracks = filter(artist_filter, result_tracks)
|
||||
elif field in ('uri', 'filename'):
|
||||
elif field == 'uri':
|
||||
result_tracks = filter(uri_filter, result_tracks)
|
||||
elif field == 'any':
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
@ -11,12 +13,12 @@ from mopidy.utils import formatting, path
|
||||
from .translator import parse_m3u
|
||||
|
||||
|
||||
logger = logging.getLogger(u'mopidy.backends.local')
|
||||
logger = logging.getLogger('mopidy.backends.local')
|
||||
|
||||
|
||||
class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||
class LocalPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||
super(LocalPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||
self._path = settings.LOCAL_PLAYLIST_PATH
|
||||
self.refresh()
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy.models import Track, Artist, Album
|
||||
@ -68,19 +70,19 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
|
||||
current = {}
|
||||
state = None
|
||||
|
||||
for line in contents.split('\n'):
|
||||
if line == 'songList begin':
|
||||
for line in contents.split(b'\n'):
|
||||
if line == b'songList begin':
|
||||
state = 'songs'
|
||||
continue
|
||||
elif line == 'songList end':
|
||||
elif line == b'songList end':
|
||||
state = None
|
||||
continue
|
||||
elif not state:
|
||||
continue
|
||||
|
||||
key, value = line.split(': ', 1)
|
||||
key, value = line.split(b': ', 1)
|
||||
|
||||
if key == 'key':
|
||||
if key == b'key':
|
||||
_convert_mpd_data(current, tracks, music_dir)
|
||||
current.clear()
|
||||
|
||||
|
||||
@ -30,5 +30,7 @@ https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
|
||||
- :attr:`mopidy.settings.SPOTIFY_PASSWORD`
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import SpotifyBackend
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import pykka
|
||||
@ -18,13 +20,13 @@ class SpotifyBackend(pykka.ThreadingActor, base.Backend):
|
||||
from .library import SpotifyLibraryProvider
|
||||
from .playback import SpotifyPlaybackProvider
|
||||
from .session_manager import SpotifySessionManager
|
||||
from .stored_playlists import SpotifyStoredPlaylistsProvider
|
||||
from .playlists import SpotifyPlaylistsProvider
|
||||
|
||||
self.library = SpotifyLibraryProvider(backend=self)
|
||||
self.playback = SpotifyPlaybackProvider(audio=audio, backend=self)
|
||||
self.stored_playlists = SpotifyStoredPlaylistsProvider(backend=self)
|
||||
self.playlists = SpotifyPlaylistsProvider(backend=self)
|
||||
|
||||
self.uri_schemes = [u'spotify']
|
||||
self.uri_schemes = ['spotify']
|
||||
|
||||
# Fail early if settings are not present
|
||||
username = settings.SPOTIFY_USERNAME
|
||||
@ -34,8 +36,8 @@ class SpotifyBackend(pykka.ThreadingActor, base.Backend):
|
||||
username, password, audio=audio, backend_ref=self.actor_ref)
|
||||
|
||||
def on_start(self):
|
||||
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
||||
logger.debug(u'Connecting to Spotify')
|
||||
logger.info('Mopidy uses SPOTIFY(R) CORE')
|
||||
logger.debug('Connecting to Spotify')
|
||||
self.spotify.start()
|
||||
|
||||
def on_stop(self):
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from spotify.manager import SpotifyContainerManager as \
|
||||
@ -13,21 +15,21 @@ class SpotifyContainerManager(PyspotifyContainerManager):
|
||||
|
||||
def container_loaded(self, container, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: playlist container loaded')
|
||||
logger.debug('Callback called: playlist container loaded')
|
||||
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
self.session_manager.refresh_playlists()
|
||||
|
||||
count = 0
|
||||
for playlist in self.session_manager.session.playlist_container():
|
||||
if playlist.type() == 'playlist':
|
||||
self.session_manager.playlist_manager.watch(playlist)
|
||||
count += 1
|
||||
logger.debug(u'Watching %d playlist(s) for changes', count)
|
||||
logger.debug('Watching %d playlist(s) for changes', count)
|
||||
|
||||
def playlist_added(self, container, playlist, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: playlist added at position %d', position)
|
||||
'Callback called: playlist added at position %d', position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
|
||||
@ -35,7 +37,7 @@ class SpotifyContainerManager(PyspotifyContainerManager):
|
||||
userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: playlist "%s" moved from position %d to %d',
|
||||
'Callback called: playlist "%s" moved from position %d to %d',
|
||||
playlist.name(), old_position, new_position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
@ -43,7 +45,7 @@ class SpotifyContainerManager(PyspotifyContainerManager):
|
||||
def playlist_removed(self, container, playlist, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: playlist "%s" removed from position %d',
|
||||
'Callback called: playlist "%s" removed from position %d',
|
||||
playlist.name(), position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import Queue
|
||||
|
||||
@ -16,7 +18,7 @@ class SpotifyTrack(Track):
|
||||
def __init__(self, uri):
|
||||
super(SpotifyTrack, self).__init__()
|
||||
self._spotify_track = Link.from_string(uri).as_track()
|
||||
self._unloaded_track = Track(uri=uri, name=u'[loading...]')
|
||||
self._unloaded_track = Track(uri=uri, name='[loading...]')
|
||||
self._track = None
|
||||
|
||||
@property
|
||||
@ -57,7 +59,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
try:
|
||||
return SpotifyTrack(uri)
|
||||
except SpotifyError as e:
|
||||
logger.debug(u'Failed to lookup "%s": %s', uri, e)
|
||||
logger.debug('Failed to lookup "%s": %s', uri, e)
|
||||
return None
|
||||
|
||||
def refresh(self, uri=None):
|
||||
@ -66,29 +68,36 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
def search(self, **query):
|
||||
if not query:
|
||||
# Since we can't search for the entire Spotify library, we return
|
||||
# all tracks in the stored playlists when the query is empty.
|
||||
# all tracks in the playlists when the query is empty.
|
||||
tracks = []
|
||||
for playlist in self.backend.stored_playlists.playlists:
|
||||
for playlist in self.backend.playlists.playlists:
|
||||
tracks += playlist.tracks
|
||||
return Playlist(tracks=tracks)
|
||||
spotify_query = []
|
||||
for (field, values) in query.iteritems():
|
||||
if field == u'track':
|
||||
field = u'title'
|
||||
if field == u'date':
|
||||
field = u'year'
|
||||
if field == 'uri':
|
||||
tracks = []
|
||||
for value in values:
|
||||
track = self.lookup(value)
|
||||
if track:
|
||||
tracks.append(track)
|
||||
return Playlist(tracks=tracks)
|
||||
elif field == 'track':
|
||||
field = 'title'
|
||||
elif field == 'date':
|
||||
field = 'year'
|
||||
if not hasattr(values, '__iter__'):
|
||||
values = [values]
|
||||
for value in values:
|
||||
if field == u'any':
|
||||
if field == 'any':
|
||||
spotify_query.append(value)
|
||||
elif field == u'year':
|
||||
elif field == 'year':
|
||||
value = int(value.split('-')[0]) # Extract year
|
||||
spotify_query.append(u'%s:%d' % (field, value))
|
||||
spotify_query.append('%s:%d' % (field, value))
|
||||
else:
|
||||
spotify_query.append(u'%s:"%s"' % (field, value))
|
||||
spotify_query = u' '.join(spotify_query)
|
||||
logger.debug(u'Spotify search query: %s' % spotify_query)
|
||||
spotify_query.append('%s:"%s"' % (field, value))
|
||||
spotify_query = ' '.join(spotify_query)
|
||||
logger.debug('Spotify search query: %s' % spotify_query)
|
||||
queue = Queue.Queue()
|
||||
self.backend.spotify.search(spotify_query, queue)
|
||||
try:
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
@ -14,90 +16,90 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager):
|
||||
def tracks_added(self, playlist, tracks, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: '
|
||||
u'%d track(s) added to position %d in playlist "%s"',
|
||||
'Callback called: '
|
||||
'%d track(s) added to position %d in playlist "%s"',
|
||||
len(tracks), position, playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
self.session_manager.refresh_playlists()
|
||||
|
||||
def tracks_moved(self, playlist, tracks, new_position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: '
|
||||
u'%d track(s) moved to position %d in playlist "%s"',
|
||||
'Callback called: '
|
||||
'%d track(s) moved to position %d in playlist "%s"',
|
||||
len(tracks), new_position, playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
self.session_manager.refresh_playlists()
|
||||
|
||||
def tracks_removed(self, playlist, tracks, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: '
|
||||
u'%d track(s) removed from playlist "%s"',
|
||||
'Callback called: '
|
||||
'%d track(s) removed from playlist "%s"',
|
||||
len(tracks), playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
self.session_manager.refresh_playlists()
|
||||
|
||||
def playlist_renamed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Playlist renamed to "%s"', playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
'Callback called: Playlist renamed to "%s"', playlist.name())
|
||||
self.session_manager.refresh_playlists()
|
||||
|
||||
def playlist_state_changed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: The state of playlist "%s" changed',
|
||||
'Callback called: The state of playlist "%s" changed',
|
||||
playlist.name())
|
||||
|
||||
def playlist_update_in_progress(self, playlist, done, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
if done:
|
||||
logger.debug(
|
||||
u'Callback called: Update of playlist "%s" done',
|
||||
'Callback called: Update of playlist "%s" done',
|
||||
playlist.name())
|
||||
else:
|
||||
logger.debug(
|
||||
u'Callback called: Update of playlist "%s" in progress',
|
||||
'Callback called: Update of playlist "%s" in progress',
|
||||
playlist.name())
|
||||
|
||||
def playlist_metadata_updated(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Metadata updated for playlist "%s"',
|
||||
'Callback called: Metadata updated for playlist "%s"',
|
||||
playlist.name())
|
||||
|
||||
def track_created_changed(self, playlist, position, user, when, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
when = datetime.datetime.fromtimestamp(when)
|
||||
logger.debug(
|
||||
u'Callback called: Created by/when for track %d in playlist '
|
||||
u'"%s" changed to user "N/A" and time "%s"',
|
||||
'Callback called: Created by/when for track %d in playlist '
|
||||
'"%s" changed to user "N/A" and time "%s"',
|
||||
position, playlist.name(), when)
|
||||
|
||||
def track_message_changed(self, playlist, position, message, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Message for track %d in playlist '
|
||||
u'"%s" changed to "%s"', position, playlist.name(), message)
|
||||
'Callback called: Message for track %d in playlist '
|
||||
'"%s" changed to "%s"', position, playlist.name(), message)
|
||||
|
||||
def track_seen_changed(self, playlist, position, seen, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Seen attribute for track %d in playlist '
|
||||
u'"%s" changed to "%s"', position, playlist.name(), seen)
|
||||
'Callback called: Seen attribute for track %d in playlist '
|
||||
'"%s" changed to "%s"', position, playlist.name(), seen)
|
||||
|
||||
def description_changed(self, playlist, description, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Description changed for playlist "%s" to "%s"',
|
||||
'Callback called: Description changed for playlist "%s" to "%s"',
|
||||
playlist.name(), description)
|
||||
|
||||
def subscribers_changed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Subscribers changed for playlist "%s"',
|
||||
'Callback called: Subscribers changed for playlist "%s"',
|
||||
playlist.name())
|
||||
|
||||
def image_changed(self, playlist, image, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Image changed for playlist "%s"',
|
||||
'Callback called: Image changed for playlist "%s"',
|
||||
playlist.name())
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.backends import base
|
||||
|
||||
|
||||
class SpotifyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||
class SpotifyPlaylistsProvider(base.BasePlaylistsProvider):
|
||||
def create(self, name):
|
||||
pass # TODO
|
||||
|
||||
def delete(self, playlist):
|
||||
def delete(self, uri):
|
||||
pass # TODO
|
||||
|
||||
def lookup(self, uri):
|
||||
@ -14,8 +16,5 @@ class SpotifyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||
def refresh(self):
|
||||
pass # TODO
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
pass # TODO
|
||||
|
||||
def save(self, playlist):
|
||||
pass # TODO
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
@ -50,14 +52,14 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
def logged_in(self, session, error):
|
||||
"""Callback used by pyspotify"""
|
||||
if error:
|
||||
logger.error(u'Spotify login error: %s', error)
|
||||
logger.error('Spotify login error: %s', error)
|
||||
return
|
||||
|
||||
logger.info(u'Connected to Spotify')
|
||||
logger.info('Connected to Spotify')
|
||||
self.session = session
|
||||
|
||||
logger.debug(
|
||||
u'Preferred Spotify bitrate is %s kbps',
|
||||
'Preferred Spotify bitrate is %s kbps',
|
||||
settings.SPOTIFY_BITRATE)
|
||||
self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE])
|
||||
|
||||
@ -70,30 +72,30 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
|
||||
def logged_out(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.info(u'Disconnected from Spotify')
|
||||
logger.info('Disconnected from Spotify')
|
||||
|
||||
def metadata_updated(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: Metadata updated')
|
||||
logger.debug('Callback called: Metadata updated')
|
||||
|
||||
def connection_error(self, session, error):
|
||||
"""Callback used by pyspotify"""
|
||||
if error is None:
|
||||
logger.info(u'Spotify connection OK')
|
||||
logger.info('Spotify connection OK')
|
||||
else:
|
||||
logger.error(u'Spotify connection error: %s', error)
|
||||
logger.error('Spotify connection error: %s', error)
|
||||
self.backend.playback.pause()
|
||||
|
||||
def message_to_user(self, session, message):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'User message: %s', message.strip())
|
||||
logger.debug('User message: %s', message.strip())
|
||||
|
||||
def music_delivery(self, session, frames, frame_size, num_frames,
|
||||
sample_type, sample_rate, channels):
|
||||
"""Callback used by pyspotify"""
|
||||
# pylint: disable = R0913
|
||||
# Too many arguments (8/5)
|
||||
assert sample_type == 0, u'Expects 16-bit signed integer samples'
|
||||
assert sample_type == 0, 'Expects 16-bit signed integer samples'
|
||||
capabilites = """
|
||||
audio/x-raw-int,
|
||||
endianness=(int)1234,
|
||||
@ -111,40 +113,39 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
|
||||
def play_token_lost(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Play token lost')
|
||||
logger.debug('Play token lost')
|
||||
self.backend.playback.pause()
|
||||
|
||||
def log_message(self, session, data):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'System message: %s' % data.strip())
|
||||
logger.debug('System message: %s' % data.strip())
|
||||
if 'offline-mgr' in data and 'files unlocked' in data:
|
||||
# XXX This is a very very fragile and ugly hack, but we get no
|
||||
# proper event when libspotify is done with initial data loading.
|
||||
# We delay the expensive refresh of Mopidy's stored playlists until
|
||||
# this message arrives. This way, we avoid doing the refresh once
|
||||
# for every playlist or other change. This reduces the time from
|
||||
# We delay the expensive refresh of Mopidy's playlists until this
|
||||
# message arrives. This way, we avoid doing the refresh once for
|
||||
# every playlist or other change. This reduces the time from
|
||||
# startup until the Spotify backend is ready from 35s to 12s in one
|
||||
# test with clean Spotify cache. In cases with an outdated cache
|
||||
# the time improvements should be a lot better.
|
||||
# the time improvements should be a lot greater.
|
||||
self._initial_data_receive_completed = True
|
||||
self.refresh_stored_playlists()
|
||||
self.refresh_playlists()
|
||||
|
||||
def end_of_track(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'End of data stream reached')
|
||||
logger.debug('End of data stream reached')
|
||||
self.audio.emit_end_of_stream()
|
||||
|
||||
def refresh_stored_playlists(self):
|
||||
"""Refresh the stored playlists in the backend with fresh meta data
|
||||
from Spotify"""
|
||||
def refresh_playlists(self):
|
||||
"""Refresh the playlists in the backend with data from Spotify"""
|
||||
if not self._initial_data_receive_completed:
|
||||
logger.debug(u'Still getting data; skipped refresh of playlists')
|
||||
logger.debug('Still getting data; skipped refresh of playlists')
|
||||
return
|
||||
playlists = map(
|
||||
translator.to_mopidy_playlist, self.session.playlist_container())
|
||||
playlists = filter(None, playlists)
|
||||
self.backend.stored_playlists.playlists = playlists
|
||||
logger.info(u'Loaded %d Spotify playlist(s)', len(playlists))
|
||||
self.backend.playlists.playlists = playlists
|
||||
logger.info('Loaded %d Spotify playlist(s)', len(playlists))
|
||||
|
||||
def search(self, query, queue):
|
||||
"""Search method used by Mopidy backend"""
|
||||
@ -161,6 +162,6 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
|
||||
def logout(self):
|
||||
"""Log out from spotify"""
|
||||
logger.debug(u'Logging out from Spotify')
|
||||
logger.debug('Logging out from Spotify')
|
||||
if self.session:
|
||||
self.session.logout()
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from spotify import Link
|
||||
|
||||
from mopidy import settings
|
||||
@ -9,7 +11,7 @@ def to_mopidy_artist(spotify_artist):
|
||||
return
|
||||
uri = str(Link.from_artist(spotify_artist))
|
||||
if not spotify_artist.is_loaded():
|
||||
return Artist(uri=uri, name=u'[loading...]')
|
||||
return Artist(uri=uri, name='[loading...]')
|
||||
return Artist(uri=uri, name=spotify_artist.name())
|
||||
|
||||
|
||||
@ -18,7 +20,7 @@ def to_mopidy_album(spotify_album):
|
||||
return
|
||||
uri = str(Link.from_album(spotify_album))
|
||||
if not spotify_album.is_loaded():
|
||||
return Album(uri=uri, name=u'[loading...]')
|
||||
return Album(uri=uri, name='[loading...]')
|
||||
return Album(
|
||||
uri=uri,
|
||||
name=spotify_album.name(),
|
||||
@ -31,7 +33,7 @@ def to_mopidy_track(spotify_track):
|
||||
return
|
||||
uri = str(Link.from_track(spotify_track, 0))
|
||||
if not spotify_track.is_loaded():
|
||||
return Track(uri=uri, name=u'[loading...]')
|
||||
return Track(uri=uri, name='[loading...]')
|
||||
spotify_album = spotify_track.album()
|
||||
if spotify_album is not None and spotify_album.is_loaded():
|
||||
date = spotify_album.year()
|
||||
@ -53,7 +55,7 @@ def to_mopidy_playlist(spotify_playlist):
|
||||
return
|
||||
uri = str(Link.from_playlist(spotify_playlist))
|
||||
if not spotify_playlist.is_loaded():
|
||||
return Playlist(uri=uri, name=u'[loading...]')
|
||||
return Playlist(uri=uri, name='[loading...]')
|
||||
return Playlist(
|
||||
uri=uri,
|
||||
name=spotify_playlist.name(),
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import Core
|
||||
from .current_playlist import CurrentPlaylistController
|
||||
from .library import LibraryController
|
||||
from .listener import CoreListener
|
||||
from .playback import PlaybackController, PlaybackState
|
||||
from .stored_playlists import StoredPlaylistsController
|
||||
from .playlists import PlaylistsController
|
||||
from .tracklist import TracklistController
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.audio import AudioListener
|
||||
|
||||
from .current_playlist import CurrentPlaylistController
|
||||
from .library import LibraryController
|
||||
from .playback import PlaybackController
|
||||
from .stored_playlists import StoredPlaylistsController
|
||||
from .playlists import PlaylistsController
|
||||
from .tracklist import TracklistController
|
||||
|
||||
|
||||
class Core(pykka.ThreadingActor, AudioListener):
|
||||
#: The current playlist controller. An instance of
|
||||
#: :class:`mopidy.core.CurrentPlaylistController`.
|
||||
current_playlist = None
|
||||
|
||||
#: The library controller. An instance of
|
||||
# :class:`mopidy.core.LibraryController`.
|
||||
library = None
|
||||
@ -23,25 +21,29 @@ class Core(pykka.ThreadingActor, AudioListener):
|
||||
#: :class:`mopidy.core.PlaybackController`.
|
||||
playback = None
|
||||
|
||||
#: The stored playlists controller. An instance of
|
||||
#: :class:`mopidy.core.StoredPlaylistsController`.
|
||||
stored_playlists = None
|
||||
#: The playlists controller. An instance of
|
||||
#: :class:`mopidy.core.PlaylistsController`.
|
||||
playlists = None
|
||||
|
||||
#: The tracklist controller. An instance of
|
||||
#: :class:`mopidy.core.TracklistController`.
|
||||
tracklist = None
|
||||
|
||||
def __init__(self, audio=None, backends=None):
|
||||
super(Core, self).__init__()
|
||||
|
||||
self.backends = Backends(backends)
|
||||
|
||||
self.current_playlist = CurrentPlaylistController(core=self)
|
||||
|
||||
self.library = LibraryController(backends=self.backends, core=self)
|
||||
|
||||
self.playback = PlaybackController(
|
||||
audio=audio, backends=self.backends, core=self)
|
||||
|
||||
self.stored_playlists = StoredPlaylistsController(
|
||||
self.playlists = PlaylistsController(
|
||||
backends=self.backends, core=self)
|
||||
|
||||
self.tracklist = TracklistController(core=self)
|
||||
|
||||
@property
|
||||
def uri_schemes(self):
|
||||
"""List of URI schemes we can handle"""
|
||||
@ -58,10 +60,18 @@ class Backends(list):
|
||||
def __init__(self, backends):
|
||||
super(Backends, self).__init__(backends)
|
||||
|
||||
# These lists keeps the backends in the original order, but only
|
||||
# includes those which implements the required backend provider. Since
|
||||
# it is important to keep the order, we can't simply use .values() on
|
||||
# the X_by_uri_scheme dicts below.
|
||||
self.with_library = [b for b in backends if b.has_library().get()]
|
||||
self.with_playback = [b for b in backends if b.has_playback().get()]
|
||||
self.with_playlists = [b for b in backends
|
||||
if b.has_playlists().get()]
|
||||
|
||||
self.by_uri_scheme = {}
|
||||
for backend in backends:
|
||||
uri_schemes = backend.uri_schemes.get()
|
||||
for uri_scheme in uri_schemes:
|
||||
for uri_scheme in backend.uri_schemes.get():
|
||||
assert uri_scheme not in self.by_uri_scheme, (
|
||||
'Cannot add URI scheme %s for %s, '
|
||||
'it is already handled by %s'
|
||||
@ -69,3 +79,15 @@ class Backends(list):
|
||||
uri_scheme, backend.__class__.__name__,
|
||||
self.by_uri_scheme[uri_scheme].__class__.__name__)
|
||||
self.by_uri_scheme[uri_scheme] = backend
|
||||
|
||||
self.with_library_by_uri_scheme = {}
|
||||
self.with_playback_by_uri_scheme = {}
|
||||
self.with_playlists_by_uri_scheme = {}
|
||||
|
||||
for uri_scheme, backend in self.by_uri_scheme.items():
|
||||
if backend.has_library().get():
|
||||
self.with_library_by_uri_scheme[uri_scheme] = backend
|
||||
if backend.has_playback().get():
|
||||
self.with_playback_by_uri_scheme[uri_scheme] = backend
|
||||
if backend.has_playlists().get():
|
||||
self.with_playlists_by_uri_scheme[uri_scheme] = backend
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import urlparse
|
||||
|
||||
@ -15,7 +17,7 @@ class LibraryController(object):
|
||||
|
||||
def _get_backend(self, uri):
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
return self.backends.by_uri_scheme.get(uri_scheme, None)
|
||||
return self.backends.with_library_by_uri_scheme.get(uri_scheme, None)
|
||||
|
||||
def find_exact(self, **query):
|
||||
"""
|
||||
@ -34,7 +36,8 @@ class LibraryController(object):
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
futures = [b.library.find_exact(**query) for b in self.backends]
|
||||
futures = [b.library.find_exact(**query)
|
||||
for b in self.backends.with_library]
|
||||
results = pykka.get_all(futures)
|
||||
return Playlist(tracks=[
|
||||
track for playlist in results for track in playlist.tracks])
|
||||
@ -65,7 +68,8 @@ class LibraryController(object):
|
||||
if backend:
|
||||
backend.library.refresh(uri).get()
|
||||
else:
|
||||
futures = [b.library.refresh(uri) for b in self.backends]
|
||||
futures = [b.library.refresh(uri)
|
||||
for b in self.backends.with_library]
|
||||
pykka.get_all(futures)
|
||||
|
||||
def search(self, **query):
|
||||
@ -85,7 +89,8 @@ class LibraryController(object):
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
futures = [b.library.search(**query) for b in self.backends]
|
||||
futures = [b.library.search(**query)
|
||||
for b in self.backends.with_library]
|
||||
results = pykka.get_all(futures)
|
||||
track_lists = [playlist.tracks for playlist in results]
|
||||
tracks = list(itertools.chain(*track_lists))
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import random
|
||||
import urlparse
|
||||
@ -28,13 +30,13 @@ class PlaybackState(object):
|
||||
"""
|
||||
|
||||
#: Constant representing the paused state.
|
||||
PAUSED = u'paused'
|
||||
PAUSED = 'paused'
|
||||
|
||||
#: Constant representing the playing state.
|
||||
PLAYING = u'playing'
|
||||
PLAYING = 'playing'
|
||||
|
||||
#: Constant representing the stopped state.
|
||||
STOPPED = u'stopped'
|
||||
STOPPED = 'stopped'
|
||||
|
||||
|
||||
class PlaybackController(object):
|
||||
@ -51,9 +53,9 @@ class PlaybackController(object):
|
||||
|
||||
#: The currently playing or selected track.
|
||||
#:
|
||||
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
|
||||
#: A two-tuple of (TLID integer, :class:`mopidy.models.Track`) or
|
||||
#: :class:`None`.
|
||||
current_cp_track = None
|
||||
current_tl_track = None
|
||||
|
||||
#: :class:`True`
|
||||
#: Tracks are selected at random from the playlist.
|
||||
@ -86,53 +88,52 @@ class PlaybackController(object):
|
||||
self._volume = None
|
||||
|
||||
def _get_backend(self):
|
||||
if self.current_cp_track is None:
|
||||
if self.current_tl_track is None:
|
||||
return None
|
||||
uri = self.current_cp_track.track.uri
|
||||
uri = self.current_tl_track.track.uri
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
return self.backends.by_uri_scheme[uri_scheme]
|
||||
return self.backends.with_playback_by_uri_scheme.get(uri_scheme, None)
|
||||
|
||||
def _get_cpid(self, cp_track):
|
||||
if cp_track is None:
|
||||
def _get_tlid(self, tl_track):
|
||||
if tl_track is None:
|
||||
return None
|
||||
return cp_track.cpid
|
||||
return tl_track.tlid
|
||||
|
||||
def _get_track(self, cp_track):
|
||||
if cp_track is None:
|
||||
def _get_track(self, tl_track):
|
||||
if tl_track is None:
|
||||
return None
|
||||
return cp_track.track
|
||||
return tl_track.track
|
||||
|
||||
@property
|
||||
def current_cpid(self):
|
||||
def current_tlid(self):
|
||||
"""
|
||||
The CPID (current playlist ID) of the currently playing or selected
|
||||
The TLID (tracklist ID) of the currently playing or selected
|
||||
track.
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
Read-only. Extracted from :attr:`current_tl_track` for convenience.
|
||||
"""
|
||||
return self._get_cpid(self.current_cp_track)
|
||||
return self._get_tlid(self.current_tl_track)
|
||||
|
||||
@property
|
||||
def current_track(self):
|
||||
"""
|
||||
The currently playing or selected :class:`mopidy.models.Track`.
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
Read-only. Extracted from :attr:`current_tl_track` for convenience.
|
||||
"""
|
||||
return self._get_track(self.current_cp_track)
|
||||
return self._get_track(self.current_tl_track)
|
||||
|
||||
@property
|
||||
def current_playlist_position(self):
|
||||
def tracklist_position(self):
|
||||
"""
|
||||
The position of the current track in the current playlist.
|
||||
The position of the current track in the tracklist.
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
if self.current_tl_track is None:
|
||||
return None
|
||||
try:
|
||||
return self.core.current_playlist.cp_tracks.index(
|
||||
self.current_cp_track)
|
||||
return self.core.tracklist.tl_tracks.index(self.current_tl_track)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@ -142,49 +143,48 @@ class PlaybackController(object):
|
||||
The track that will be played at the end of the current track.
|
||||
|
||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||
:attr:`cp_track_at_eot` for convenience.
|
||||
:attr:`tl_track_at_eot` for convenience.
|
||||
"""
|
||||
return self._get_track(self.cp_track_at_eot)
|
||||
return self._get_track(self.tl_track_at_eot)
|
||||
|
||||
@property
|
||||
def cp_track_at_eot(self):
|
||||
def tl_track_at_eot(self):
|
||||
"""
|
||||
The track that will be played at the end of the current track.
|
||||
|
||||
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
Read-only. A two-tuple of (TLID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
Not necessarily the same track as :attr:`cp_track_at_next`.
|
||||
Not necessarily the same track as :attr:`tl_track_at_next`.
|
||||
"""
|
||||
# pylint: disable = R0911
|
||||
# Too many return statements
|
||||
|
||||
cp_tracks = self.core.current_playlist.cp_tracks
|
||||
tl_tracks = self.core.tracklist.tl_tracks
|
||||
|
||||
if not cp_tracks:
|
||||
if not tl_tracks:
|
||||
return None
|
||||
|
||||
if self.random and not self._shuffled:
|
||||
if self.repeat or self._first_shuffle:
|
||||
logger.debug('Shuffling tracks')
|
||||
self._shuffled = cp_tracks
|
||||
self._shuffled = tl_tracks
|
||||
random.shuffle(self._shuffled)
|
||||
self._first_shuffle = False
|
||||
|
||||
if self.random and self._shuffled:
|
||||
return self._shuffled[0]
|
||||
|
||||
if self.current_cp_track is None:
|
||||
return cp_tracks[0]
|
||||
if self.current_tl_track is None:
|
||||
return tl_tracks[0]
|
||||
|
||||
if self.repeat and self.single:
|
||||
return cp_tracks[self.current_playlist_position]
|
||||
return tl_tracks[self.tracklist_position]
|
||||
|
||||
if self.repeat and not self.single:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||
return tl_tracks[(self.tracklist_position + 1) % len(tl_tracks)]
|
||||
|
||||
try:
|
||||
return cp_tracks[self.current_playlist_position + 1]
|
||||
return tl_tracks[self.tracklist_position + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@ -194,46 +194,45 @@ class PlaybackController(object):
|
||||
The track that will be played if calling :meth:`next()`.
|
||||
|
||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||
:attr:`cp_track_at_next` for convenience.
|
||||
:attr:`tl_track_at_next` for convenience.
|
||||
"""
|
||||
return self._get_track(self.cp_track_at_next)
|
||||
return self._get_track(self.tl_track_at_next)
|
||||
|
||||
@property
|
||||
def cp_track_at_next(self):
|
||||
def tl_track_at_next(self):
|
||||
"""
|
||||
The track that will be played if calling :meth:`next()`.
|
||||
|
||||
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
Read-only. A two-tuple of (TLID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
For normal playback this is the next track in the playlist. If repeat
|
||||
is enabled the next track can loop around the playlist. When random is
|
||||
enabled this should be a random track, all tracks should be played once
|
||||
before the list repeats.
|
||||
"""
|
||||
cp_tracks = self.core.current_playlist.cp_tracks
|
||||
tl_tracks = self.core.tracklist.tl_tracks
|
||||
|
||||
if not cp_tracks:
|
||||
if not tl_tracks:
|
||||
return None
|
||||
|
||||
if self.random and not self._shuffled:
|
||||
if self.repeat or self._first_shuffle:
|
||||
logger.debug('Shuffling tracks')
|
||||
self._shuffled = cp_tracks
|
||||
self._shuffled = tl_tracks
|
||||
random.shuffle(self._shuffled)
|
||||
self._first_shuffle = False
|
||||
|
||||
if self.random and self._shuffled:
|
||||
return self._shuffled[0]
|
||||
|
||||
if self.current_cp_track is None:
|
||||
return cp_tracks[0]
|
||||
if self.current_tl_track is None:
|
||||
return tl_tracks[0]
|
||||
|
||||
if self.repeat:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||
return tl_tracks[(self.tracklist_position + 1) % len(tl_tracks)]
|
||||
|
||||
try:
|
||||
return cp_tracks[self.current_playlist_position + 1]
|
||||
return tl_tracks[self.tracklist_position + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@ -243,29 +242,28 @@ class PlaybackController(object):
|
||||
The track that will be played if calling :meth:`previous()`.
|
||||
|
||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||
:attr:`cp_track_at_previous` for convenience.
|
||||
:attr:`tl_track_at_previous` for convenience.
|
||||
"""
|
||||
return self._get_track(self.cp_track_at_previous)
|
||||
return self._get_track(self.tl_track_at_previous)
|
||||
|
||||
@property
|
||||
def cp_track_at_previous(self):
|
||||
def tl_track_at_previous(self):
|
||||
"""
|
||||
The track that will be played if calling :meth:`previous()`.
|
||||
|
||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
A two-tuple of (TLID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
For normal playback this is the previous track in the playlist. If
|
||||
random and/or consume is enabled it should return the current track
|
||||
instead.
|
||||
"""
|
||||
if self.repeat or self.consume or self.random:
|
||||
return self.current_cp_track
|
||||
return self.current_tl_track
|
||||
|
||||
if self.current_playlist_position in (None, 0):
|
||||
if self.tracklist_position in (None, 0):
|
||||
return None
|
||||
|
||||
return self.core.current_playlist.cp_tracks[
|
||||
self.current_playlist_position - 1]
|
||||
return self.core.tracklist.tl_tracks[self.tracklist_position - 1]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@ -290,7 +288,7 @@ class PlaybackController(object):
|
||||
@state.setter # noqa
|
||||
def state(self, new_state):
|
||||
(old_state, self._state) = (self.state, new_state)
|
||||
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
|
||||
logger.debug('Changing state: %s -> %s', old_state, new_state)
|
||||
|
||||
self._trigger_playback_state_changed(old_state, new_state)
|
||||
|
||||
@ -298,9 +296,10 @@ class PlaybackController(object):
|
||||
def time_position(self):
|
||||
"""Time position in milliseconds."""
|
||||
backend = self._get_backend()
|
||||
if backend is None:
|
||||
if backend:
|
||||
return backend.playback.get_time_position().get()
|
||||
else:
|
||||
return 0
|
||||
return backend.playback.get_time_position().get()
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
@ -319,12 +318,12 @@ class PlaybackController(object):
|
||||
# For testing
|
||||
self._volume = volume
|
||||
|
||||
def change_track(self, cp_track, on_error_step=1):
|
||||
def change_track(self, tl_track, on_error_step=1):
|
||||
"""
|
||||
Change to the given track, keeping the current playback state.
|
||||
|
||||
:param cp_track: track to change to
|
||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
:param tl_track: track to change to
|
||||
:type tl_track: two-tuple (TLID integer, :class:`mopidy.models.Track`)
|
||||
or :class:`None`
|
||||
:param on_error_step: direction to step at play error, 1 for next
|
||||
track (default), -1 for previous track
|
||||
@ -333,7 +332,7 @@ class PlaybackController(object):
|
||||
"""
|
||||
old_state = self.state
|
||||
self.stop()
|
||||
self.current_cp_track = cp_track
|
||||
self.current_tl_track = tl_track
|
||||
if old_state == PlaybackState.PLAYING:
|
||||
self.play(on_error_step=on_error_step)
|
||||
elif old_state == PlaybackState.PAUSED:
|
||||
@ -346,18 +345,18 @@ class PlaybackController(object):
|
||||
if self.state == PlaybackState.STOPPED:
|
||||
return
|
||||
|
||||
original_cp_track = self.current_cp_track
|
||||
original_tl_track = self.current_tl_track
|
||||
|
||||
if self.cp_track_at_eot:
|
||||
if self.tl_track_at_eot:
|
||||
self._trigger_track_playback_ended()
|
||||
self.play(self.cp_track_at_eot)
|
||||
self.play(self.tl_track_at_eot)
|
||||
else:
|
||||
self.stop(clear_current_track=True)
|
||||
|
||||
if self.consume:
|
||||
self.core.current_playlist.remove(cpid=original_cp_track.cpid)
|
||||
self.core.tracklist.remove(tlid=original_tl_track.tlid)
|
||||
|
||||
def on_current_playlist_change(self):
|
||||
def on_tracklist_change(self):
|
||||
"""
|
||||
Tell the playback controller that the current playlist has changed.
|
||||
|
||||
@ -366,9 +365,9 @@ class PlaybackController(object):
|
||||
self._first_shuffle = True
|
||||
self._shuffled = []
|
||||
|
||||
if (not self.core.current_playlist.cp_tracks or
|
||||
self.current_cp_track not in
|
||||
self.core.current_playlist.cp_tracks):
|
||||
if (not self.core.tracklist.tl_tracks or
|
||||
self.current_tl_track not in
|
||||
self.core.tracklist.tl_tracks):
|
||||
self.stop(clear_current_track=True)
|
||||
|
||||
def next(self):
|
||||
@ -378,58 +377,59 @@ class PlaybackController(object):
|
||||
The current playback state will be kept. If it was playing, playing
|
||||
will continue. If it was paused, it will still be paused, etc.
|
||||
"""
|
||||
if self.cp_track_at_next:
|
||||
if self.tl_track_at_next:
|
||||
self._trigger_track_playback_ended()
|
||||
self.change_track(self.cp_track_at_next)
|
||||
self.change_track(self.tl_track_at_next)
|
||||
else:
|
||||
self.stop(clear_current_track=True)
|
||||
|
||||
def pause(self):
|
||||
"""Pause playback."""
|
||||
backend = self._get_backend()
|
||||
if backend is None or backend.playback.pause().get():
|
||||
if not backend or backend.playback.pause().get():
|
||||
self.state = PlaybackState.PAUSED
|
||||
self._trigger_track_playback_paused()
|
||||
|
||||
def play(self, cp_track=None, on_error_step=1):
|
||||
def play(self, tl_track=None, on_error_step=1):
|
||||
"""
|
||||
Play the given track, or if the given track is :class:`None`, play the
|
||||
currently active track.
|
||||
|
||||
:param cp_track: track to play
|
||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
:param tl_track: track to play
|
||||
:type tl_track: two-tuple (TLID integer, :class:`mopidy.models.Track`)
|
||||
or :class:`None`
|
||||
:param on_error_step: direction to step at play error, 1 for next
|
||||
track (default), -1 for previous track
|
||||
:type on_error_step: int, -1 or 1
|
||||
"""
|
||||
|
||||
if cp_track is not None:
|
||||
assert cp_track in self.core.current_playlist.cp_tracks
|
||||
elif cp_track is None:
|
||||
if tl_track is not None:
|
||||
assert tl_track in self.core.tracklist.tl_tracks
|
||||
elif tl_track is None:
|
||||
if self.state == PlaybackState.PAUSED:
|
||||
return self.resume()
|
||||
elif self.current_cp_track is not None:
|
||||
cp_track = self.current_cp_track
|
||||
elif self.current_cp_track is None and on_error_step == 1:
|
||||
cp_track = self.cp_track_at_next
|
||||
elif self.current_cp_track is None and on_error_step == -1:
|
||||
cp_track = self.cp_track_at_previous
|
||||
elif self.current_tl_track is not None:
|
||||
tl_track = self.current_tl_track
|
||||
elif self.current_tl_track is None and on_error_step == 1:
|
||||
tl_track = self.tl_track_at_next
|
||||
elif self.current_tl_track is None and on_error_step == -1:
|
||||
tl_track = self.tl_track_at_previous
|
||||
|
||||
if cp_track is not None:
|
||||
self.current_cp_track = cp_track
|
||||
if tl_track is not None:
|
||||
self.current_tl_track = tl_track
|
||||
self.state = PlaybackState.PLAYING
|
||||
if not self._get_backend().playback.play(cp_track.track).get():
|
||||
backend = self._get_backend()
|
||||
if not backend or not backend.playback.play(tl_track.track).get():
|
||||
# Track is not playable
|
||||
if self.random and self._shuffled:
|
||||
self._shuffled.remove(cp_track)
|
||||
self._shuffled.remove(tl_track)
|
||||
if on_error_step == 1:
|
||||
self.next()
|
||||
elif on_error_step == -1:
|
||||
self.previous()
|
||||
|
||||
if self.random and self.current_cp_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_cp_track)
|
||||
if self.random and self.current_tl_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_tl_track)
|
||||
|
||||
self._trigger_track_playback_started()
|
||||
|
||||
@ -441,12 +441,14 @@ class PlaybackController(object):
|
||||
will continue. If it was paused, it will still be paused, etc.
|
||||
"""
|
||||
self._trigger_track_playback_ended()
|
||||
self.change_track(self.cp_track_at_previous, on_error_step=-1)
|
||||
self.change_track(self.tl_track_at_previous, on_error_step=-1)
|
||||
|
||||
def resume(self):
|
||||
"""If paused, resume playing the current track."""
|
||||
if (self.state == PlaybackState.PAUSED and
|
||||
self._get_backend().playback.resume().get()):
|
||||
if self.state != PlaybackState.PAUSED:
|
||||
return
|
||||
backend = self._get_backend()
|
||||
if backend and backend.playback.resume().get():
|
||||
self.state = PlaybackState.PLAYING
|
||||
self._trigger_track_playback_resumed()
|
||||
|
||||
@ -458,7 +460,7 @@ class PlaybackController(object):
|
||||
:type time_position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
if not self.core.current_playlist.tracks:
|
||||
if not self.core.tracklist.tracks:
|
||||
return False
|
||||
|
||||
if self.state == PlaybackState.STOPPED:
|
||||
@ -472,7 +474,11 @@ class PlaybackController(object):
|
||||
self.next()
|
||||
return True
|
||||
|
||||
success = self._get_backend().playback.seek(time_position).get()
|
||||
backend = self._get_backend()
|
||||
if not backend:
|
||||
return False
|
||||
|
||||
success = backend.playback.seek(time_position).get()
|
||||
if success:
|
||||
self._trigger_seeked(time_position)
|
||||
return success
|
||||
@ -486,14 +492,15 @@ class PlaybackController(object):
|
||||
:type clear_current_track: boolean
|
||||
"""
|
||||
if self.state != PlaybackState.STOPPED:
|
||||
if self._get_backend().playback.stop().get():
|
||||
backend = self._get_backend()
|
||||
if not backend or backend.playback.stop().get():
|
||||
self._trigger_track_playback_ended()
|
||||
self.state = PlaybackState.STOPPED
|
||||
if clear_current_track:
|
||||
self.current_cp_track = None
|
||||
self.current_tl_track = None
|
||||
|
||||
def _trigger_track_playback_paused(self):
|
||||
logger.debug(u'Triggering track playback paused event')
|
||||
logger.debug('Triggering track playback paused event')
|
||||
if self.current_track is None:
|
||||
return
|
||||
listener.CoreListener.send(
|
||||
@ -501,7 +508,7 @@ class PlaybackController(object):
|
||||
track=self.current_track, time_position=self.time_position)
|
||||
|
||||
def _trigger_track_playback_resumed(self):
|
||||
logger.debug(u'Triggering track playback resumed event')
|
||||
logger.debug('Triggering track playback resumed event')
|
||||
if self.current_track is None:
|
||||
return
|
||||
listener.CoreListener.send(
|
||||
@ -509,14 +516,14 @@ class PlaybackController(object):
|
||||
track=self.current_track, time_position=self.time_position)
|
||||
|
||||
def _trigger_track_playback_started(self):
|
||||
logger.debug(u'Triggering track playback started event')
|
||||
logger.debug('Triggering track playback started event')
|
||||
if self.current_track is None:
|
||||
return
|
||||
listener.CoreListener.send(
|
||||
'track_playback_started', track=self.current_track)
|
||||
|
||||
def _trigger_track_playback_ended(self):
|
||||
logger.debug(u'Triggering track playback ended event')
|
||||
logger.debug('Triggering track playback ended event')
|
||||
if self.current_track is None:
|
||||
return
|
||||
listener.CoreListener.send(
|
||||
@ -524,15 +531,15 @@ class PlaybackController(object):
|
||||
track=self.current_track, time_position=self.time_position)
|
||||
|
||||
def _trigger_playback_state_changed(self, old_state, new_state):
|
||||
logger.debug(u'Triggering playback state change event')
|
||||
logger.debug('Triggering playback state change event')
|
||||
listener.CoreListener.send(
|
||||
'playback_state_changed',
|
||||
old_state=old_state, new_state=new_state)
|
||||
|
||||
def _trigger_options_changed(self):
|
||||
logger.debug(u'Triggering options changed event')
|
||||
logger.debug('Triggering options changed event')
|
||||
listener.CoreListener.send('options_changed')
|
||||
|
||||
def _trigger_seeked(self, time_position):
|
||||
logger.debug(u'Triggering seeked event')
|
||||
logger.debug('Triggering seeked event')
|
||||
listener.CoreListener.send('seeked', time_position=time_position)
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
|
||||
|
||||
class StoredPlaylistsController(object):
|
||||
class PlaylistsController(object):
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, backends, core):
|
||||
@ -14,11 +16,12 @@ class StoredPlaylistsController(object):
|
||||
@property
|
||||
def playlists(self):
|
||||
"""
|
||||
Currently stored playlists.
|
||||
The available playlists.
|
||||
|
||||
Read-only. List of :class:`mopidy.models.Playlist`.
|
||||
"""
|
||||
futures = [b.stored_playlists.playlists for b in self.backends]
|
||||
futures = [b.playlists.playlists
|
||||
for b in self.backends.with_playlists]
|
||||
results = pykka.get_all(futures)
|
||||
return list(itertools.chain(*results))
|
||||
|
||||
@ -40,11 +43,11 @@ class StoredPlaylistsController(object):
|
||||
:type uri_scheme: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
if uri_scheme in self.backends.by_uri_scheme:
|
||||
if uri_scheme in self.backends.with_playlists_by_uri_scheme:
|
||||
backend = self.backends.by_uri_scheme[uri_scheme]
|
||||
else:
|
||||
backend = self.backends[0]
|
||||
return backend.stored_playlists.create(name).get()
|
||||
backend = self.backends.with_playlists[0]
|
||||
return backend.playlists.create(name).get()
|
||||
|
||||
def delete(self, uri):
|
||||
"""
|
||||
@ -57,13 +60,14 @@ class StoredPlaylistsController(object):
|
||||
:type uri: string
|
||||
"""
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
if uri_scheme in self.backends.by_uri_scheme:
|
||||
backend = self.backends.by_uri_scheme[uri_scheme]
|
||||
backend.stored_playlists.delete(uri).get()
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
if backend:
|
||||
backend.playlists.delete(uri).get()
|
||||
|
||||
def get(self, **criteria):
|
||||
"""
|
||||
Get playlist by given criterias from the set of stored playlists.
|
||||
Get playlist by given criterias from the set of playlists.
|
||||
|
||||
Raises :exc:`LookupError` if a unique match is not found.
|
||||
|
||||
@ -93,23 +97,24 @@ class StoredPlaylistsController(object):
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup playlist with given URI in both the set of stored playlists and
|
||||
in any other playlist sources. Returns :class:`None` if not found.
|
||||
Lookup playlist with given URI in both the set of playlists and in any
|
||||
other playlist sources. Returns :class:`None` if not found.
|
||||
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Playlist` or :class:`None`
|
||||
"""
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.by_uri_scheme.get(uri_scheme, None)
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
if backend:
|
||||
return backend.stored_playlists.lookup(uri).get()
|
||||
return backend.playlists.lookup(uri).get()
|
||||
else:
|
||||
return None
|
||||
|
||||
def refresh(self, uri_scheme=None):
|
||||
"""
|
||||
Refresh the stored playlists in :attr:`playlists`.
|
||||
Refresh the playlists in :attr:`playlists`.
|
||||
|
||||
If ``uri_scheme`` is :class:`None`, all backends are asked to refresh.
|
||||
If ``uri_scheme`` is an URI scheme handled by a backend, only that
|
||||
@ -120,16 +125,18 @@ class StoredPlaylistsController(object):
|
||||
:type uri_scheme: string
|
||||
"""
|
||||
if uri_scheme is None:
|
||||
futures = [b.stored_playlists.refresh() for b in self.backends]
|
||||
futures = [b.playlists.refresh()
|
||||
for b in self.backends.with_playlists]
|
||||
pykka.get_all(futures)
|
||||
else:
|
||||
if uri_scheme in self.backends.by_uri_scheme:
|
||||
backend = self.backends.by_uri_scheme[uri_scheme]
|
||||
backend.stored_playlists.refresh().get()
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
if backend:
|
||||
backend.playlists.refresh().get()
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
Save the playlist to the set of stored playlists.
|
||||
Save the playlist.
|
||||
|
||||
For a playlist to be saveable, it must have the ``uri`` attribute set.
|
||||
You should not set the ``uri`` atribute yourself, but use playlist
|
||||
@ -152,7 +159,7 @@ class StoredPlaylistsController(object):
|
||||
if playlist.uri is None:
|
||||
return
|
||||
uri_scheme = urlparse.urlparse(playlist.uri).scheme
|
||||
if uri_scheme not in self.backends.by_uri_scheme:
|
||||
return
|
||||
backend = self.backends.by_uri_scheme[uri_scheme]
|
||||
return backend.stored_playlists.save(playlist).get()
|
||||
backend = self.backends.with_playlists_by_uri_scheme.get(
|
||||
uri_scheme, None)
|
||||
if backend:
|
||||
return backend.playlists.save(playlist).get()
|
||||
@ -1,8 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from copy import copy
|
||||
import logging
|
||||
import random
|
||||
|
||||
from mopidy.models import CpTrack
|
||||
from mopidy.models import TlTrack
|
||||
|
||||
from . import listener
|
||||
|
||||
@ -10,23 +12,23 @@ from . import listener
|
||||
logger = logging.getLogger('mopidy.core')
|
||||
|
||||
|
||||
class CurrentPlaylistController(object):
|
||||
class TracklistController(object):
|
||||
pykka_traversable = True
|
||||
|
||||
def __init__(self, core):
|
||||
self.core = core
|
||||
self.cp_id = 0
|
||||
self._cp_tracks = []
|
||||
self.tlid = 0
|
||||
self._tl_tracks = []
|
||||
self._version = 0
|
||||
|
||||
@property
|
||||
def cp_tracks(self):
|
||||
def tl_tracks(self):
|
||||
"""
|
||||
List of two-tuples of (CPID integer, :class:`mopidy.models.Track`).
|
||||
List of two-tuples of (TLID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [copy(cp_track) for cp_track in self._cp_tracks]
|
||||
return [copy(tl_track) for tl_track in self._tl_tracks]
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
@ -35,14 +37,14 @@ class CurrentPlaylistController(object):
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [cp_track.track for cp_track in self._cp_tracks]
|
||||
return [tl_track.track for tl_track in self._tl_tracks]
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""
|
||||
Length of the current playlist.
|
||||
"""
|
||||
return len(self._cp_tracks)
|
||||
return len(self._tl_tracks)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
@ -55,7 +57,7 @@ class CurrentPlaylistController(object):
|
||||
@version.setter # noqa
|
||||
def version(self, version):
|
||||
self._version = version
|
||||
self.core.playback.on_current_playlist_change()
|
||||
self.core.playback.on_tracklist_change()
|
||||
self._trigger_playlist_changed()
|
||||
|
||||
def add(self, track, at_position=None, increase_version=True):
|
||||
@ -69,20 +71,20 @@ class CurrentPlaylistController(object):
|
||||
:type at_position: int or :class:`None`
|
||||
:param increase_version: if the playlist version should be increased
|
||||
:type increase_version: :class:`True` or :class:`False`
|
||||
:rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`) that
|
||||
:rtype: two-tuple of (TLID integer, :class:`mopidy.models.Track`) that
|
||||
was added to the current playlist playlist
|
||||
"""
|
||||
assert at_position <= len(self._cp_tracks), \
|
||||
u'at_position can not be greater than playlist length'
|
||||
cp_track = CpTrack(self.cp_id, track)
|
||||
assert at_position <= len(self._tl_tracks), \
|
||||
'at_position can not be greater than playlist length'
|
||||
tl_track = TlTrack(self.tlid, track)
|
||||
if at_position is not None:
|
||||
self._cp_tracks.insert(at_position, cp_track)
|
||||
self._tl_tracks.insert(at_position, tl_track)
|
||||
else:
|
||||
self._cp_tracks.append(cp_track)
|
||||
self._tl_tracks.append(tl_track)
|
||||
if increase_version:
|
||||
self.version += 1
|
||||
self.cp_id += 1
|
||||
return cp_track
|
||||
self.tlid += 1
|
||||
return tl_track
|
||||
|
||||
def append(self, tracks):
|
||||
"""
|
||||
@ -99,7 +101,7 @@ class CurrentPlaylistController(object):
|
||||
|
||||
def clear(self):
|
||||
"""Clear the current playlist."""
|
||||
self._cp_tracks = []
|
||||
self._tl_tracks = []
|
||||
self.version += 1
|
||||
|
||||
def get(self, **criteria):
|
||||
@ -110,7 +112,7 @@ class CurrentPlaylistController(object):
|
||||
|
||||
Examples::
|
||||
|
||||
get(cpid=7) # Returns track with CPID 7
|
||||
get(tlid=7) # Returns track with TLID 7
|
||||
# (current playlist ID)
|
||||
get(id=1) # Returns track with ID 1
|
||||
get(uri='xyz') # Returns track with URI 'xyz'
|
||||
@ -118,12 +120,12 @@ class CurrentPlaylistController(object):
|
||||
|
||||
:param criteria: on or more criteria to match by
|
||||
:type criteria: dict
|
||||
:rtype: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
:rtype: two-tuple (TLID integer, :class:`mopidy.models.Track`)
|
||||
"""
|
||||
matches = self._cp_tracks
|
||||
matches = self._tl_tracks
|
||||
for (key, value) in criteria.iteritems():
|
||||
if key == 'cpid':
|
||||
matches = filter(lambda ct: ct.cpid == value, matches)
|
||||
if key == 'tlid':
|
||||
matches = filter(lambda ct: ct.tlid == value, matches)
|
||||
else:
|
||||
matches = filter(
|
||||
lambda ct: getattr(ct.track, key) == value, matches)
|
||||
@ -132,22 +134,22 @@ class CurrentPlaylistController(object):
|
||||
criteria_string = ', '.join(
|
||||
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
|
||||
if len(matches) == 0:
|
||||
raise LookupError(u'"%s" match no tracks' % criteria_string)
|
||||
raise LookupError('"%s" match no tracks' % criteria_string)
|
||||
else:
|
||||
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
|
||||
raise LookupError('"%s" match multiple tracks' % criteria_string)
|
||||
|
||||
def index(self, cp_track):
|
||||
def index(self, tl_track):
|
||||
"""
|
||||
Get index of the given (CPID integer, :class:`mopidy.models.Track`)
|
||||
Get index of the given (TLID integer, :class:`mopidy.models.Track`)
|
||||
two-tuple in the current playlist.
|
||||
|
||||
Raises :exc:`ValueError` if not found.
|
||||
|
||||
:param cp_track: track to find the index of
|
||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
:param tl_track: track to find the index of
|
||||
:type tl_track: two-tuple (TLID integer, :class:`mopidy.models.Track`)
|
||||
:rtype: int
|
||||
"""
|
||||
return self._cp_tracks.index(cp_track)
|
||||
return self._tl_tracks.index(tl_track)
|
||||
|
||||
def move(self, start, end, to_position):
|
||||
"""
|
||||
@ -163,21 +165,21 @@ class CurrentPlaylistController(object):
|
||||
if start == end:
|
||||
end += 1
|
||||
|
||||
cp_tracks = self._cp_tracks
|
||||
tl_tracks = self._tl_tracks
|
||||
|
||||
assert start < end, 'start must be smaller than end'
|
||||
assert start >= 0, 'start must be at least zero'
|
||||
assert end <= len(cp_tracks), \
|
||||
assert end <= len(tl_tracks), \
|
||||
'end can not be larger than playlist length'
|
||||
assert to_position >= 0, 'to_position must be at least zero'
|
||||
assert to_position <= len(cp_tracks), \
|
||||
assert to_position <= len(tl_tracks), \
|
||||
'to_position can not be larger than playlist length'
|
||||
|
||||
new_cp_tracks = cp_tracks[:start] + cp_tracks[end:]
|
||||
for cp_track in cp_tracks[start:end]:
|
||||
new_cp_tracks.insert(to_position, cp_track)
|
||||
new_tl_tracks = tl_tracks[:start] + tl_tracks[end:]
|
||||
for tl_track in tl_tracks[start:end]:
|
||||
new_tl_tracks.insert(to_position, tl_track)
|
||||
to_position += 1
|
||||
self._cp_tracks = new_cp_tracks
|
||||
self._tl_tracks = new_tl_tracks
|
||||
self.version += 1
|
||||
|
||||
def remove(self, **criteria):
|
||||
@ -189,9 +191,9 @@ class CurrentPlaylistController(object):
|
||||
:param criteria: on or more criteria to match by
|
||||
:type criteria: dict
|
||||
"""
|
||||
cp_track = self.get(**criteria)
|
||||
position = self._cp_tracks.index(cp_track)
|
||||
del self._cp_tracks[position]
|
||||
tl_track = self.get(**criteria)
|
||||
position = self._tl_tracks.index(tl_track)
|
||||
del self._tl_tracks[position]
|
||||
self.version += 1
|
||||
|
||||
def shuffle(self, start=None, end=None):
|
||||
@ -204,7 +206,7 @@ class CurrentPlaylistController(object):
|
||||
:param end: position after last track to shuffle
|
||||
:type end: int or :class:`None`
|
||||
"""
|
||||
cp_tracks = self._cp_tracks
|
||||
tl_tracks = self._tl_tracks
|
||||
|
||||
if start is not None and end is not None:
|
||||
assert start < end, 'start must be smaller than end'
|
||||
@ -213,14 +215,14 @@ class CurrentPlaylistController(object):
|
||||
assert start >= 0, 'start must be at least zero'
|
||||
|
||||
if end is not None:
|
||||
assert end <= len(cp_tracks), 'end can not be larger than ' + \
|
||||
assert end <= len(tl_tracks), 'end can not be larger than ' + \
|
||||
'playlist length'
|
||||
|
||||
before = cp_tracks[:start or 0]
|
||||
shuffled = cp_tracks[start:end]
|
||||
after = cp_tracks[end or len(cp_tracks):]
|
||||
before = tl_tracks[:start or 0]
|
||||
shuffled = tl_tracks[start:end]
|
||||
after = tl_tracks[end or len(tl_tracks):]
|
||||
random.shuffle(shuffled)
|
||||
self._cp_tracks = before + shuffled + after
|
||||
self._tl_tracks = before + shuffled + after
|
||||
self.version += 1
|
||||
|
||||
def slice(self, start, end):
|
||||
@ -232,10 +234,10 @@ class CurrentPlaylistController(object):
|
||||
:type start: int
|
||||
:param end: position after last track to include in slice
|
||||
:type end: int
|
||||
:rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`)
|
||||
:rtype: two-tuple of (TLID integer, :class:`mopidy.models.Track`)
|
||||
"""
|
||||
return [copy(cp_track) for cp_track in self._cp_tracks[start:end]]
|
||||
return [copy(tl_track) for tl_track in self._tl_tracks[start:end]]
|
||||
|
||||
def _trigger_playlist_changed(self):
|
||||
logger.debug(u'Triggering playlist changed event')
|
||||
logger.debug('Triggering playlist changed event')
|
||||
listener.CoreListener.send('playlist_changed')
|
||||
@ -1,3 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class MopidyException(Exception):
|
||||
def __init__(self, message, *args, **kwargs):
|
||||
super(MopidyException, self).__init__(message, *args, **kwargs)
|
||||
|
||||
@ -0,0 +1 @@
|
||||
from __future__ import unicode_literals
|
||||
@ -65,16 +65,16 @@ class TrackListResource(object):
|
||||
|
||||
@cherrypy.tools.json_out()
|
||||
def GET(self):
|
||||
cp_tracks_future = self.core.current_playlist.cp_tracks
|
||||
current_cp_track_future = self.core.playback.current_cp_track
|
||||
tl_tracks_future = self.core.tracklist.tl_tracks
|
||||
current_tl_track_future = self.core.playback.current_tl_track
|
||||
tracks = []
|
||||
for cp_track in cp_tracks_future.get():
|
||||
track = cp_track.track.serialize()
|
||||
track['cpid'] = cp_track.cpid
|
||||
for tl_track in tl_tracks_future.get():
|
||||
track = tl_track.track.serialize()
|
||||
track['tlid'] = tl_track.tlid
|
||||
tracks.append(track)
|
||||
current_cp_track = current_cp_track_future.get()
|
||||
current_tl_track = current_tl_track_future.get()
|
||||
return {
|
||||
'currentTrackCpid': current_cp_track and current_cp_track.cpid,
|
||||
'currentTrackTlid': current_tl_track and current_tl_track.tlid,
|
||||
'tracks': tracks,
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ class PlaylistsResource(object):
|
||||
|
||||
@cherrypy.tools.json_out()
|
||||
def GET(self):
|
||||
playlists = self.core.stored_playlists.playlists.get()
|
||||
playlists = self.core.playlists.playlists.get()
|
||||
return {
|
||||
'playlists': [p.serialize() for p in playlists],
|
||||
}
|
||||
|
||||
@ -22,6 +22,8 @@ Make sure :attr:`mopidy.settings.FRONTENDS` includes
|
||||
the Last.fm frontend.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
@ -54,21 +56,21 @@ class LastfmFrontend(pykka.ThreadingActor, CoreListener):
|
||||
self.lastfm = pylast.LastFMNetwork(
|
||||
api_key=API_KEY, api_secret=API_SECRET,
|
||||
username=username, password_hash=password_hash)
|
||||
logger.info(u'Connected to Last.fm')
|
||||
logger.info('Connected to Last.fm')
|
||||
except exceptions.SettingsError as e:
|
||||
logger.info(u'Last.fm scrobbler not started')
|
||||
logger.debug(u'Last.fm settings error: %s', e)
|
||||
logger.info('Last.fm scrobbler not started')
|
||||
logger.debug('Last.fm settings error: %s', e)
|
||||
self.stop()
|
||||
except (pylast.NetworkError, pylast.MalformedResponseError,
|
||||
pylast.WSError) as e:
|
||||
logger.error(u'Error during Last.fm setup: %s', e)
|
||||
logger.error('Error during Last.fm setup: %s', e)
|
||||
self.stop()
|
||||
|
||||
def track_playback_started(self, track):
|
||||
artists = ', '.join([a.name for a in track.artists])
|
||||
duration = track.length and track.length // 1000 or 0
|
||||
self.last_start_time = int(time.time())
|
||||
logger.debug(u'Now playing track: %s - %s', artists, track.name)
|
||||
logger.debug('Now playing track: %s - %s', artists, track.name)
|
||||
try:
|
||||
self.lastfm.update_now_playing(
|
||||
artists,
|
||||
@ -79,22 +81,22 @@ class LastfmFrontend(pykka.ThreadingActor, CoreListener):
|
||||
mbid=(track.musicbrainz_id or ''))
|
||||
except (pylast.ScrobblingError, pylast.NetworkError,
|
||||
pylast.MalformedResponseError, pylast.WSError) as e:
|
||||
logger.warning(u'Error submitting playing track to Last.fm: %s', e)
|
||||
logger.warning('Error submitting playing track to Last.fm: %s', e)
|
||||
|
||||
def track_playback_ended(self, track, time_position):
|
||||
artists = ', '.join([a.name for a in track.artists])
|
||||
duration = track.length and track.length // 1000 or 0
|
||||
time_position = time_position // 1000
|
||||
if duration < 30:
|
||||
logger.debug(u'Track too short to scrobble. (30s)')
|
||||
logger.debug('Track too short to scrobble. (30s)')
|
||||
return
|
||||
if time_position < duration // 2 and time_position < 240:
|
||||
logger.debug(
|
||||
u'Track not played long enough to scrobble. (50% or 240s)')
|
||||
'Track not played long enough to scrobble. (50% or 240s)')
|
||||
return
|
||||
if self.last_start_time is None:
|
||||
self.last_start_time = int(time.time()) - duration
|
||||
logger.debug(u'Scrobbling track: %s - %s', artists, track.name)
|
||||
logger.debug('Scrobbling track: %s - %s', artists, track.name)
|
||||
try:
|
||||
self.lastfm.scrobble(
|
||||
artists,
|
||||
@ -106,4 +108,4 @@ class LastfmFrontend(pykka.ThreadingActor, CoreListener):
|
||||
mbid=(track.musicbrainz_id or ''))
|
||||
except (pylast.ScrobblingError, pylast.NetworkError,
|
||||
pylast.MalformedResponseError, pylast.WSError) as e:
|
||||
logger.warning(u'Error submitting played track to Last.fm: %s', e)
|
||||
logger.warning('Error submitting played track to Last.fm: %s', e)
|
||||
|
||||
@ -21,5 +21,7 @@ Make sure :attr:`mopidy.settings.FRONTENDS` includes
|
||||
frontend.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import MpdFrontend
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
@ -24,11 +26,11 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
||||
max_connections=settings.MPD_SERVER_MAX_CONNECTIONS)
|
||||
except IOError as error:
|
||||
logger.error(
|
||||
u'MPD server startup failed: %s',
|
||||
'MPD server startup failed: %s',
|
||||
encoding.locale_decode(error))
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(u'MPD server running at [%s]:%s', hostname, port)
|
||||
logger.info('MPD server running at [%s]:%s', hostname, port)
|
||||
|
||||
def on_stop(self):
|
||||
process.stop_actors_by_class(session.MpdSession)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
@ -52,8 +54,8 @@ class MpdDispatcher(object):
|
||||
|
||||
response = []
|
||||
for subsystem in subsystems:
|
||||
response.append(u'changed: %s' % subsystem)
|
||||
response.append(u'OK')
|
||||
response.append('changed: %s' % subsystem)
|
||||
response.append('OK')
|
||||
self.context.subscriptions = set()
|
||||
self.context.events = set()
|
||||
self.context.session.send_lines(response)
|
||||
@ -103,26 +105,26 @@ class MpdDispatcher(object):
|
||||
response = self._call_next_filter(request, response, filter_chain)
|
||||
if (self._is_receiving_command_list(request) or
|
||||
self._is_processing_command_list(request)):
|
||||
if response and response[-1] == u'OK':
|
||||
if response and response[-1] == 'OK':
|
||||
response = response[:-1]
|
||||
return response
|
||||
|
||||
def _is_receiving_command_list(self, request):
|
||||
return (
|
||||
self.command_list_receiving and request != u'command_list_end')
|
||||
self.command_list_receiving and request != 'command_list_end')
|
||||
|
||||
def _is_processing_command_list(self, request):
|
||||
return (
|
||||
self.command_list_index is not None and
|
||||
request != u'command_list_end')
|
||||
request != 'command_list_end')
|
||||
|
||||
### Filter: idle
|
||||
|
||||
def _idle_filter(self, request, response, filter_chain):
|
||||
if self._is_currently_idle() and not self._noidle.match(request):
|
||||
logger.debug(
|
||||
u'Client sent us %s, only %s is allowed while in '
|
||||
u'the idle state', repr(request), repr(u'noidle'))
|
||||
'Client sent us %s, only %s is allowed while in '
|
||||
'the idle state', repr(request), repr('noidle'))
|
||||
self.context.session.close()
|
||||
return []
|
||||
|
||||
@ -144,11 +146,11 @@ class MpdDispatcher(object):
|
||||
def _add_ok_filter(self, request, response, filter_chain):
|
||||
response = self._call_next_filter(request, response, filter_chain)
|
||||
if not self._has_error(response):
|
||||
response.append(u'OK')
|
||||
response.append('OK')
|
||||
return response
|
||||
|
||||
def _has_error(self, response):
|
||||
return response and response[-1].startswith(u'ACK')
|
||||
return response and response[-1].startswith('ACK')
|
||||
|
||||
### Filter: call handler
|
||||
|
||||
@ -157,7 +159,7 @@ class MpdDispatcher(object):
|
||||
response = self._format_response(self._call_handler(request))
|
||||
return self._call_next_filter(request, response, filter_chain)
|
||||
except pykka.ActorDeadError as e:
|
||||
logger.warning(u'Tried to communicate with dead actor.')
|
||||
logger.warning('Tried to communicate with dead actor.')
|
||||
raise exceptions.MpdSystemError(e)
|
||||
|
||||
def _call_handler(self, request):
|
||||
@ -173,7 +175,7 @@ class MpdDispatcher(object):
|
||||
command_name = request.split(' ')[0]
|
||||
if command_name in [command.name for command in protocol.mpd_commands]:
|
||||
raise exceptions.MpdArgError(
|
||||
u'incorrect arguments', command=command_name)
|
||||
'incorrect arguments', command=command_name)
|
||||
raise exceptions.MpdUnknownCommand(command=command_name)
|
||||
|
||||
def _format_response(self, response):
|
||||
@ -202,10 +204,10 @@ class MpdDispatcher(object):
|
||||
|
||||
def _format_lines(self, line):
|
||||
if isinstance(line, dict):
|
||||
return [u'%s: %s' % (key, value) for (key, value) in line.items()]
|
||||
return ['%s: %s' % (key, value) for (key, value) in line.items()]
|
||||
if isinstance(line, tuple):
|
||||
(key, value) = line
|
||||
return [u'%s: %s' % (key, value)]
|
||||
return ['%s: %s' % (key, value)]
|
||||
return [line]
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.exceptions import MopidyException
|
||||
|
||||
|
||||
@ -19,7 +21,7 @@ class MpdAckError(MopidyException):
|
||||
|
||||
error_code = 0
|
||||
|
||||
def __init__(self, message=u'', index=0, command=u''):
|
||||
def __init__(self, message='', index=0, command=''):
|
||||
super(MpdAckError, self).__init__(message, index, command)
|
||||
self.message = message
|
||||
self.index = index
|
||||
@ -31,7 +33,7 @@ class MpdAckError(MopidyException):
|
||||
|
||||
ACK [%(error_code)i@%(index)i] {%(command)s} description
|
||||
"""
|
||||
return u'ACK [%i@%i] {%s} %s' % (
|
||||
return 'ACK [%i@%i] {%s} %s' % (
|
||||
self.__class__.error_code, self.index, self.command, self.message)
|
||||
|
||||
|
||||
@ -48,7 +50,7 @@ class MpdPermissionError(MpdAckError):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdPermissionError, self).__init__(*args, **kwargs)
|
||||
self.message = u'you don\'t have permission for "%s"' % self.command
|
||||
self.message = 'you don\'t have permission for "%s"' % self.command
|
||||
|
||||
|
||||
class MpdUnknownCommand(MpdAckError):
|
||||
@ -56,8 +58,8 @@ class MpdUnknownCommand(MpdAckError):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdUnknownCommand, self).__init__(*args, **kwargs)
|
||||
self.message = u'unknown command "%s"' % self.command
|
||||
self.command = u''
|
||||
self.message = 'unknown command "%s"' % self.command
|
||||
self.command = ''
|
||||
|
||||
|
||||
class MpdNoExistError(MpdAckError):
|
||||
@ -73,4 +75,4 @@ class MpdNotImplemented(MpdAckError):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdNotImplemented, self).__init__(*args, **kwargs)
|
||||
self.message = u'Not implemented'
|
||||
self.message = 'Not implemented'
|
||||
|
||||
@ -10,17 +10,19 @@ implement our own MPD server which is compatible with the numerous existing
|
||||
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import namedtuple
|
||||
import re
|
||||
|
||||
#: The MPD protocol uses UTF-8 for encoding all data.
|
||||
ENCODING = u'UTF-8'
|
||||
ENCODING = 'UTF-8'
|
||||
|
||||
#: The MPD protocol uses ``\n`` as line terminator.
|
||||
LINE_TERMINATOR = u'\n'
|
||||
LINE_TERMINATOR = '\n'
|
||||
|
||||
#: The MPD protocol version is 0.16.0.
|
||||
VERSION = u'0.16.0'
|
||||
VERSION = '0.16.0'
|
||||
|
||||
MpdCommand = namedtuple('MpdCommand', ['name', 'auth_required'])
|
||||
|
||||
@ -55,7 +57,7 @@ def handle_request(pattern, auth_required=True):
|
||||
mpd_commands.add(
|
||||
MpdCommand(name=match.group(), auth_required=auth_required))
|
||||
if pattern in request_handlers:
|
||||
raise ValueError(u'Tried to redefine handler for %s with %s' % (
|
||||
raise ValueError('Tried to redefine handler for %s with %s' % (
|
||||
pattern, func))
|
||||
request_handlers[pattern] = func
|
||||
func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % (
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdUnknownCommand
|
||||
|
||||
@ -40,10 +42,10 @@ def command_list_end(context):
|
||||
command, current_command_list_index=index)
|
||||
command_list_response.extend(response)
|
||||
if (command_list_response and
|
||||
command_list_response[-1].startswith(u'ACK')):
|
||||
command_list_response[-1].startswith('ACK')):
|
||||
return command_list_response
|
||||
if command_list_ok:
|
||||
command_list_response.append(u'list_OK')
|
||||
command_list_response.append('list_OK')
|
||||
return command_list_response
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import (
|
||||
@ -25,7 +27,7 @@ def kill(context):
|
||||
|
||||
Kills MPD.
|
||||
"""
|
||||
raise MpdPermissionError(command=u'kill')
|
||||
raise MpdPermissionError(command='kill')
|
||||
|
||||
|
||||
@handle_request(r'^password "(?P<password>[^"]+)"$', auth_required=False)
|
||||
@ -41,7 +43,7 @@ def password_(context, password):
|
||||
if password == settings.MPD_SERVER_PASSWORD:
|
||||
context.dispatcher.authenticated = True
|
||||
else:
|
||||
raise MpdPasswordError(u'incorrect password', command=u'password')
|
||||
raise MpdPasswordError('incorrect password', command='password')
|
||||
|
||||
|
||||
@handle_request(r'^ping$', auth_required=False)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.frontends.mpd import translator
|
||||
from mopidy.frontends.mpd.exceptions import (
|
||||
MpdArgError, MpdNoExistError, MpdNotImplemented)
|
||||
@ -20,14 +22,11 @@ def add(context, uri):
|
||||
"""
|
||||
if not uri:
|
||||
return
|
||||
for uri_scheme in context.core.uri_schemes.get():
|
||||
if uri.startswith(uri_scheme):
|
||||
track = context.core.library.lookup(uri).get()
|
||||
if track is not None:
|
||||
context.core.current_playlist.add(track)
|
||||
return
|
||||
raise MpdNoExistError(
|
||||
u'directory or file not found', command=u'add')
|
||||
track = context.core.library.lookup(uri).get()
|
||||
if track:
|
||||
context.core.tracklist.add(track)
|
||||
return
|
||||
raise MpdNoExistError('directory or file not found', command='add')
|
||||
|
||||
|
||||
@handle_request(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
||||
@ -50,17 +49,16 @@ def addid(context, uri, songpos=None):
|
||||
- ``addid ""`` should return an error.
|
||||
"""
|
||||
if not uri:
|
||||
raise MpdNoExistError(u'No such song', command=u'addid')
|
||||
raise MpdNoExistError('No such song', command='addid')
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
track = context.core.library.lookup(uri).get()
|
||||
if track is None:
|
||||
raise MpdNoExistError(u'No such song', command=u'addid')
|
||||
if songpos and songpos > context.core.current_playlist.length.get():
|
||||
raise MpdArgError(u'Bad song index', command=u'addid')
|
||||
cp_track = context.core.current_playlist.add(
|
||||
track, at_position=songpos).get()
|
||||
return ('Id', cp_track.cpid)
|
||||
raise MpdNoExistError('No such song', command='addid')
|
||||
if songpos and songpos > context.core.tracklist.length.get():
|
||||
raise MpdArgError('Bad song index', command='addid')
|
||||
tl_track = context.core.tracklist.add(track, at_position=songpos).get()
|
||||
return ('Id', tl_track.tlid)
|
||||
|
||||
|
||||
@handle_request(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
@ -76,12 +74,12 @@ def delete_range(context, start, end=None):
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
else:
|
||||
end = context.core.current_playlist.length.get()
|
||||
cp_tracks = context.core.current_playlist.slice(start, end).get()
|
||||
if not cp_tracks:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
for (cpid, _) in cp_tracks:
|
||||
context.core.current_playlist.remove(cpid=cpid)
|
||||
end = context.core.tracklist.length.get()
|
||||
tl_tracks = context.core.tracklist.slice(start, end).get()
|
||||
if not tl_tracks:
|
||||
raise MpdArgError('Bad song index', command='delete')
|
||||
for (tlid, _) in tl_tracks:
|
||||
context.core.tracklist.remove(tlid=tlid)
|
||||
|
||||
|
||||
@handle_request(r'^delete "(?P<songpos>\d+)"$')
|
||||
@ -89,15 +87,15 @@ def delete_songpos(context, songpos):
|
||||
"""See :meth:`delete_range`"""
|
||||
try:
|
||||
songpos = int(songpos)
|
||||
(cpid, _) = context.core.current_playlist.slice(
|
||||
(tlid, _) = context.core.tracklist.slice(
|
||||
songpos, songpos + 1).get()[0]
|
||||
context.core.current_playlist.remove(cpid=cpid)
|
||||
context.core.tracklist.remove(tlid=tlid)
|
||||
except IndexError:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
raise MpdArgError('Bad song index', command='delete')
|
||||
|
||||
|
||||
@handle_request(r'^deleteid "(?P<cpid>\d+)"$')
|
||||
def deleteid(context, cpid):
|
||||
@handle_request(r'^deleteid "(?P<tlid>\d+)"$')
|
||||
def deleteid(context, tlid):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -106,12 +104,12 @@ def deleteid(context, cpid):
|
||||
Deletes the song ``SONGID`` from the playlist
|
||||
"""
|
||||
try:
|
||||
cpid = int(cpid)
|
||||
if context.core.playback.current_cpid.get() == cpid:
|
||||
tlid = int(tlid)
|
||||
if context.core.playback.current_tlid.get() == tlid:
|
||||
context.core.playback.next()
|
||||
return context.core.current_playlist.remove(cpid=cpid).get()
|
||||
return context.core.tracklist.remove(tlid=tlid).get()
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
||||
raise MpdNoExistError('No such song', command='deleteid')
|
||||
|
||||
|
||||
@handle_request(r'^clear$')
|
||||
@ -123,7 +121,7 @@ def clear(context):
|
||||
|
||||
Clears the current playlist.
|
||||
"""
|
||||
context.core.current_playlist.clear()
|
||||
context.core.tracklist.clear()
|
||||
|
||||
|
||||
@handle_request(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
|
||||
@ -137,11 +135,11 @@ def move_range(context, start, to, end=None):
|
||||
``TO`` in the playlist.
|
||||
"""
|
||||
if end is None:
|
||||
end = context.core.current_playlist.length.get()
|
||||
end = context.core.tracklist.length.get()
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
to = int(to)
|
||||
context.core.current_playlist.move(start, end, to)
|
||||
context.core.tracklist.move(start, end, to)
|
||||
|
||||
|
||||
@handle_request(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
|
||||
@ -149,11 +147,11 @@ def move_songpos(context, songpos, to):
|
||||
"""See :meth:`move_range`."""
|
||||
songpos = int(songpos)
|
||||
to = int(to)
|
||||
context.core.current_playlist.move(songpos, songpos + 1, to)
|
||||
context.core.tracklist.move(songpos, songpos + 1, to)
|
||||
|
||||
|
||||
@handle_request(r'^moveid "(?P<cpid>\d+)" "(?P<to>\d+)"$')
|
||||
def moveid(context, cpid, to):
|
||||
@handle_request(r'^moveid "(?P<tlid>\d+)" "(?P<to>\d+)"$')
|
||||
def moveid(context, tlid, to):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -163,11 +161,11 @@ def moveid(context, cpid, to):
|
||||
the playlist. If ``TO`` is negative, it is relative to the current
|
||||
song in the playlist (if there is one).
|
||||
"""
|
||||
cpid = int(cpid)
|
||||
tlid = int(tlid)
|
||||
to = int(to)
|
||||
cp_track = context.core.current_playlist.get(cpid=cpid).get()
|
||||
position = context.core.current_playlist.index(cp_track).get()
|
||||
context.core.current_playlist.move(position, position + 1, to)
|
||||
tl_track = context.core.tracklist.get(tlid=tlid).get()
|
||||
position = context.core.tracklist.index(tl_track).get()
|
||||
context.core.tracklist.move(position, position + 1, to)
|
||||
|
||||
|
||||
@handle_request(r'^playlist$')
|
||||
@ -202,16 +200,16 @@ def playlistfind(context, tag, needle):
|
||||
"""
|
||||
if tag == 'filename':
|
||||
try:
|
||||
cp_track = context.core.current_playlist.get(uri=needle).get()
|
||||
position = context.core.current_playlist.index(cp_track).get()
|
||||
return translator.track_to_mpd_format(cp_track, position=position)
|
||||
tl_track = context.core.tracklist.get(uri=needle).get()
|
||||
position = context.core.tracklist.index(tl_track).get()
|
||||
return translator.track_to_mpd_format(tl_track, position=position)
|
||||
except LookupError:
|
||||
return None
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
|
||||
@handle_request(r'^playlistid( "(?P<cpid>\d+)")*$')
|
||||
def playlistid(context, cpid=None):
|
||||
@handle_request(r'^playlistid( "(?P<tlid>\d+)")*$')
|
||||
def playlistid(context, tlid=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -220,17 +218,17 @@ def playlistid(context, cpid=None):
|
||||
Displays a list of songs in the playlist. ``SONGID`` is optional
|
||||
and specifies a single song to display info for.
|
||||
"""
|
||||
if cpid is not None:
|
||||
if tlid is not None:
|
||||
try:
|
||||
cpid = int(cpid)
|
||||
cp_track = context.core.current_playlist.get(cpid=cpid).get()
|
||||
position = context.core.current_playlist.index(cp_track).get()
|
||||
return translator.track_to_mpd_format(cp_track, position=position)
|
||||
tlid = int(tlid)
|
||||
tl_track = context.core.tracklist.get(tlid=tlid).get()
|
||||
position = context.core.tracklist.index(tl_track).get()
|
||||
return translator.track_to_mpd_format(tl_track, position=position)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playlistid')
|
||||
raise MpdNoExistError('No such song', command='playlistid')
|
||||
else:
|
||||
return translator.tracks_to_mpd_format(
|
||||
context.core.current_playlist.cp_tracks.get())
|
||||
context.core.tracklist.tl_tracks.get())
|
||||
|
||||
|
||||
@handle_request(r'^playlistinfo$')
|
||||
@ -254,20 +252,20 @@ def playlistinfo(context, songpos=None, start=None, end=None):
|
||||
"""
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
cp_track = context.core.current_playlist.cp_tracks.get()[songpos]
|
||||
return translator.track_to_mpd_format(cp_track, position=songpos)
|
||||
tl_track = context.core.tracklist.tl_tracks.get()[songpos]
|
||||
return translator.track_to_mpd_format(tl_track, position=songpos)
|
||||
else:
|
||||
if start is None:
|
||||
start = 0
|
||||
start = int(start)
|
||||
if not (0 <= start <= context.core.current_playlist.length.get()):
|
||||
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
|
||||
if not (0 <= start <= context.core.tracklist.length.get()):
|
||||
raise MpdArgError('Bad song index', command='playlistinfo')
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
if end > context.core.current_playlist.length.get():
|
||||
if end > context.core.tracklist.length.get():
|
||||
end = None
|
||||
cp_tracks = context.core.current_playlist.cp_tracks.get()
|
||||
return translator.tracks_to_mpd_format(cp_tracks, start, end)
|
||||
tl_tracks = context.core.tracklist.tl_tracks.get()
|
||||
return translator.tracks_to_mpd_format(tl_tracks, start, end)
|
||||
|
||||
|
||||
@handle_request(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
@ -307,9 +305,9 @@ def plchanges(context, version):
|
||||
- Calls ``plchanges "-1"`` two times per second to get the entire playlist.
|
||||
"""
|
||||
# XXX Naive implementation that returns all tracks as changed
|
||||
if int(version) < context.core.current_playlist.version.get():
|
||||
if int(version) < context.core.tracklist.version.get():
|
||||
return translator.tracks_to_mpd_format(
|
||||
context.core.current_playlist.cp_tracks.get())
|
||||
context.core.tracklist.tl_tracks.get())
|
||||
|
||||
|
||||
@handle_request(r'^plchangesposid "(?P<version>\d+)"$')
|
||||
@ -327,12 +325,12 @@ def plchangesposid(context, version):
|
||||
``playlistlength`` returned by status command.
|
||||
"""
|
||||
# XXX Naive implementation that returns all tracks as changed
|
||||
if int(version) != context.core.current_playlist.version.get():
|
||||
if int(version) != context.core.tracklist.version.get():
|
||||
result = []
|
||||
for (position, (cpid, _)) in enumerate(
|
||||
context.core.current_playlist.cp_tracks.get()):
|
||||
result.append((u'cpos', position))
|
||||
result.append((u'Id', cpid))
|
||||
for (position, (tlid, _)) in enumerate(
|
||||
context.core.tracklist.tl_tracks.get()):
|
||||
result.append(('cpos', position))
|
||||
result.append(('Id', tlid))
|
||||
return result
|
||||
|
||||
|
||||
@ -351,7 +349,7 @@ def shuffle(context, start=None, end=None):
|
||||
start = int(start)
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
context.core.current_playlist.shuffle(start, end)
|
||||
context.core.tracklist.shuffle(start, end)
|
||||
|
||||
|
||||
@handle_request(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
|
||||
@ -365,19 +363,19 @@ def swap(context, songpos1, songpos2):
|
||||
"""
|
||||
songpos1 = int(songpos1)
|
||||
songpos2 = int(songpos2)
|
||||
tracks = context.core.current_playlist.tracks.get()
|
||||
tracks = context.core.tracklist.tracks.get()
|
||||
song1 = tracks[songpos1]
|
||||
song2 = tracks[songpos2]
|
||||
del tracks[songpos1]
|
||||
tracks.insert(songpos1, song2)
|
||||
del tracks[songpos2]
|
||||
tracks.insert(songpos2, song1)
|
||||
context.core.current_playlist.clear()
|
||||
context.core.current_playlist.append(tracks)
|
||||
context.core.tracklist.clear()
|
||||
context.core.tracklist.append(tracks)
|
||||
|
||||
|
||||
@handle_request(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
|
||||
def swapid(context, cpid1, cpid2):
|
||||
@handle_request(r'^swapid "(?P<tlid1>\d+)" "(?P<tlid2>\d+)"$')
|
||||
def swapid(context, tlid1, tlid2):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -385,10 +383,10 @@ def swapid(context, cpid1, cpid2):
|
||||
|
||||
Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids).
|
||||
"""
|
||||
cpid1 = int(cpid1)
|
||||
cpid2 = int(cpid2)
|
||||
cp_track1 = context.core.current_playlist.get(cpid=cpid1).get()
|
||||
cp_track2 = context.core.current_playlist.get(cpid=cpid2).get()
|
||||
position1 = context.core.current_playlist.index(cp_track1).get()
|
||||
position2 = context.core.current_playlist.index(cp_track2).get()
|
||||
tlid1 = int(tlid1)
|
||||
tlid2 = int(tlid2)
|
||||
tl_track1 = context.core.tracklist.get(tlid=tlid1).get()
|
||||
tl_track2 = context.core.tracklist.get(tlid=tlid2).get()
|
||||
position1 = context.core.tracklist.index(tl_track1).get()
|
||||
position2 = context.core.tracklist.index(tl_track2).get()
|
||||
swap(context, position1, position2)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import shlex
|
||||
|
||||
@ -11,19 +13,21 @@ def _build_query(mpd_query):
|
||||
Parses a MPD query string and converts it to the Mopidy query format.
|
||||
"""
|
||||
query_pattern = (
|
||||
r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"')
|
||||
r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny)"? "[^"]+"')
|
||||
query_parts = re.findall(query_pattern, mpd_query)
|
||||
query_part_pattern = (
|
||||
r'"?(?P<field>([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny))"? '
|
||||
r'"?(?P<field>([Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny))"? '
|
||||
r'"(?P<what>[^"]+)"')
|
||||
query = {}
|
||||
for query_part in query_parts:
|
||||
m = re.match(query_part_pattern, query_part)
|
||||
field = m.groupdict()['field'].lower()
|
||||
if field == u'title':
|
||||
field = u'track'
|
||||
if field == 'title':
|
||||
field = 'track'
|
||||
elif field in ('file', 'filename'):
|
||||
field = 'uri'
|
||||
field = str(field) # Needed for kwargs keys on OS X and Windows
|
||||
what = m.groupdict()['what'].lower()
|
||||
what = m.groupdict()['what']
|
||||
if field in query:
|
||||
query[field].append(what)
|
||||
else:
|
||||
@ -45,7 +49,7 @@ def count(context, tag, needle):
|
||||
|
||||
|
||||
@handle_request(
|
||||
r'^find (?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
|
||||
r'^find (?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|'
|
||||
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
|
||||
def find(context, mpd_query):
|
||||
"""
|
||||
@ -70,6 +74,7 @@ def find(context, mpd_query):
|
||||
*ncmpcpp:*
|
||||
|
||||
- also uses the search type "date".
|
||||
- uses "file" instead of "filename".
|
||||
"""
|
||||
query = _build_query(mpd_query)
|
||||
return playlist_to_mpd_format(
|
||||
@ -183,13 +188,13 @@ def list_(context, field, mpd_query=None):
|
||||
"""
|
||||
field = field.lower()
|
||||
query = _list_build_query(field, mpd_query)
|
||||
if field == u'artist':
|
||||
if field == 'artist':
|
||||
return _list_artist(context, query)
|
||||
elif field == u'album':
|
||||
elif field == 'album':
|
||||
return _list_album(context, query)
|
||||
elif field == u'date':
|
||||
elif field == 'date':
|
||||
return _list_date(context, query)
|
||||
elif field == u'genre':
|
||||
elif field == 'genre':
|
||||
pass # TODO We don't have genre in our internal data structures yet
|
||||
|
||||
|
||||
@ -202,16 +207,16 @@ def _list_build_query(field, mpd_query):
|
||||
tokens = shlex.split(mpd_query.encode('utf-8'))
|
||||
except ValueError as error:
|
||||
if str(error) == 'No closing quotation':
|
||||
raise MpdArgError(u'Invalid unquoted character', command=u'list')
|
||||
raise MpdArgError('Invalid unquoted character', command='list')
|
||||
else:
|
||||
raise
|
||||
tokens = [t.decode('utf-8') for t in tokens]
|
||||
if len(tokens) == 1:
|
||||
if field == u'album':
|
||||
if field == 'album':
|
||||
return {'artist': [tokens[0]]}
|
||||
else:
|
||||
raise MpdArgError(
|
||||
u'should be "Album" for 3 arguments', command=u'list')
|
||||
'should be "Album" for 3 arguments', command='list')
|
||||
elif len(tokens) % 2 == 0:
|
||||
query = {}
|
||||
while tokens:
|
||||
@ -219,15 +224,15 @@ def _list_build_query(field, mpd_query):
|
||||
key = str(key) # Needed for kwargs keys on OS X and Windows
|
||||
value = tokens[1]
|
||||
tokens = tokens[2:]
|
||||
if key not in (u'artist', u'album', u'date', u'genre'):
|
||||
raise MpdArgError(u'not able to parse args', command=u'list')
|
||||
if key not in ('artist', 'album', 'date', 'genre'):
|
||||
raise MpdArgError('not able to parse args', command='list')
|
||||
if key in query:
|
||||
query[key].append(value)
|
||||
else:
|
||||
query[key] = [value]
|
||||
return query
|
||||
else:
|
||||
raise MpdArgError(u'not able to parse args', command=u'list')
|
||||
raise MpdArgError('not able to parse args', command='list')
|
||||
|
||||
|
||||
def _list_artist(context, query):
|
||||
@ -235,7 +240,7 @@ def _list_artist(context, query):
|
||||
playlist = context.core.library.find_exact(**query).get()
|
||||
for track in playlist.tracks:
|
||||
for artist in track.artists:
|
||||
artists.add((u'Artist', artist.name))
|
||||
artists.add(('Artist', artist.name))
|
||||
return artists
|
||||
|
||||
|
||||
@ -244,7 +249,7 @@ def _list_album(context, query):
|
||||
playlist = context.core.library.find_exact(**query).get()
|
||||
for track in playlist.tracks:
|
||||
if track.album is not None:
|
||||
albums.add((u'Album', track.album.name))
|
||||
albums.add(('Album', track.album.name))
|
||||
return albums
|
||||
|
||||
|
||||
@ -253,7 +258,7 @@ def _list_date(context, query):
|
||||
playlist = context.core.library.find_exact(**query).get()
|
||||
for track in playlist.tracks:
|
||||
if track.date is not None:
|
||||
dates.add((u'Date', track.date))
|
||||
dates.add(('Date', track.date))
|
||||
return dates
|
||||
|
||||
|
||||
@ -300,7 +305,7 @@ def lsinfo(context, uri=None):
|
||||
directories located at the root level, for both ``lsinfo``, ``lsinfo
|
||||
""``, and ``lsinfo "/"``.
|
||||
"""
|
||||
if uri is None or uri == u'/' or uri == u'':
|
||||
if uri is None or uri == '/' or uri == '':
|
||||
return stored_playlists.listplaylists(context)
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@ -318,7 +323,7 @@ def rescan(context, uri=None):
|
||||
|
||||
|
||||
@handle_request(
|
||||
r'^search (?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
|
||||
r'^search (?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|'
|
||||
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
|
||||
def search(context, mpd_query):
|
||||
"""
|
||||
@ -346,6 +351,7 @@ def search(context, mpd_query):
|
||||
*ncmpcpp:*
|
||||
|
||||
- also uses the search type "date".
|
||||
- uses "file" instead of "filename".
|
||||
"""
|
||||
query = _build_query(mpd_query)
|
||||
return playlist_to_mpd_format(
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import (
|
||||
@ -127,9 +129,9 @@ def play(context):
|
||||
return context.core.playback.play().get()
|
||||
|
||||
|
||||
@handle_request(r'^playid (?P<cpid>-?\d+)$')
|
||||
@handle_request(r'^playid "(?P<cpid>-?\d+)"$')
|
||||
def playid(context, cpid):
|
||||
@handle_request(r'^playid (?P<tlid>-?\d+)$')
|
||||
@handle_request(r'^playid "(?P<tlid>-?\d+)"$')
|
||||
def playid(context, tlid):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -146,14 +148,14 @@ def playid(context, cpid):
|
||||
- ``playid "-1"`` when stopped without a current track, e.g. after playlist
|
||||
replacement, starts playback at the first track.
|
||||
"""
|
||||
cpid = int(cpid)
|
||||
if cpid == -1:
|
||||
tlid = int(tlid)
|
||||
if tlid == -1:
|
||||
return _play_minus_one(context)
|
||||
try:
|
||||
cp_track = context.core.current_playlist.get(cpid=cpid).get()
|
||||
return context.core.playback.play(cp_track).get()
|
||||
tl_track = context.core.tracklist.get(tlid=tlid).get()
|
||||
return context.core.playback.play(tl_track).get()
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playid')
|
||||
raise MpdNoExistError('No such song', command='playid')
|
||||
|
||||
|
||||
@handle_request(r'^play (?P<songpos>-?\d+)$')
|
||||
@ -183,11 +185,10 @@ def playpos(context, songpos):
|
||||
if songpos == -1:
|
||||
return _play_minus_one(context)
|
||||
try:
|
||||
cp_track = context.core.current_playlist.slice(
|
||||
songpos, songpos + 1).get()[0]
|
||||
return context.core.playback.play(cp_track).get()
|
||||
tl_track = context.core.tracklist.slice(songpos, songpos + 1).get()[0]
|
||||
return context.core.playback.play(tl_track).get()
|
||||
except IndexError:
|
||||
raise MpdArgError(u'Bad song index', command=u'play')
|
||||
raise MpdArgError('Bad song index', command='play')
|
||||
|
||||
|
||||
def _play_minus_one(context):
|
||||
@ -195,12 +196,12 @@ def _play_minus_one(context):
|
||||
return # Nothing to do
|
||||
elif (context.core.playback.state.get() == PlaybackState.PAUSED):
|
||||
return context.core.playback.resume().get()
|
||||
elif context.core.playback.current_cp_track.get() is not None:
|
||||
cp_track = context.core.playback.current_cp_track.get()
|
||||
return context.core.playback.play(cp_track).get()
|
||||
elif context.core.current_playlist.slice(0, 1).get():
|
||||
cp_track = context.core.current_playlist.slice(0, 1).get()[0]
|
||||
return context.core.playback.play(cp_track).get()
|
||||
elif context.core.playback.current_tl_track.get() is not None:
|
||||
tl_track = context.core.playback.current_tl_track.get()
|
||||
return context.core.playback.play(tl_track).get()
|
||||
elif context.core.tracklist.slice(0, 1).get():
|
||||
tl_track = context.core.tracklist.slice(0, 1).get()[0]
|
||||
return context.core.playback.play(tl_track).get()
|
||||
else:
|
||||
return # Fail silently
|
||||
|
||||
@ -311,7 +312,7 @@ def replay_gain_status(context):
|
||||
Prints replay gain options. Currently, only the variable
|
||||
``replay_gain_mode`` is returned.
|
||||
"""
|
||||
return u'off' # TODO
|
||||
return 'off' # TODO
|
||||
|
||||
|
||||
@handle_request(r'^seek (?P<songpos>\d+) (?P<seconds>\d+)$')
|
||||
@ -329,13 +330,13 @@ def seek(context, songpos, seconds):
|
||||
|
||||
- issues ``seek 1 120`` without quotes around the arguments.
|
||||
"""
|
||||
if context.core.playback.current_playlist_position != songpos:
|
||||
if context.core.playback.tracklist_position != songpos:
|
||||
playpos(context, songpos)
|
||||
context.core.playback.seek(int(seconds) * 1000)
|
||||
|
||||
|
||||
@handle_request(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seekid(context, cpid, seconds):
|
||||
@handle_request(r'^seekid "(?P<tlid>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seekid(context, tlid, seconds):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -343,8 +344,8 @@ def seekid(context, cpid, seconds):
|
||||
|
||||
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
|
||||
"""
|
||||
if context.core.playback.current_cpid != cpid:
|
||||
playid(context, cpid)
|
||||
if context.core.playback.current_tlid != tlid:
|
||||
playid(context, tlid)
|
||||
context.core.playback.seek(int(seconds) * 1000)
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_request, mpd_commands
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
|
||||
@ -93,5 +95,5 @@ def urlhandlers(context):
|
||||
Gets a list of available URL handlers.
|
||||
"""
|
||||
return [
|
||||
(u'handler', uri_scheme)
|
||||
('handler', uri_scheme)
|
||||
for uri_scheme in context.core.uri_schemes.get()]
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.core import PlaybackState
|
||||
@ -34,10 +36,10 @@ def currentsong(context):
|
||||
Displays the song info of the current song (same song that is
|
||||
identified in status).
|
||||
"""
|
||||
current_cp_track = context.core.playback.current_cp_track.get()
|
||||
if current_cp_track is not None:
|
||||
position = context.core.playback.current_playlist_position.get()
|
||||
return track_to_mpd_format(current_cp_track, position=position)
|
||||
current_tl_track = context.core.playback.current_tl_track.get()
|
||||
if current_tl_track is not None:
|
||||
position = context.core.playback.tracklist_position.get()
|
||||
return track_to_mpd_format(current_tl_track, position=position)
|
||||
|
||||
|
||||
@handle_request(r'^idle$')
|
||||
@ -94,7 +96,7 @@ def idle(context, subsystems=None):
|
||||
context.subscriptions = set()
|
||||
|
||||
for subsystem in active:
|
||||
response.append(u'changed: %s' % subsystem)
|
||||
response.append('changed: %s' % subsystem)
|
||||
return response
|
||||
|
||||
|
||||
@ -173,17 +175,17 @@ def status(context):
|
||||
decimal places for millisecond precision.
|
||||
"""
|
||||
futures = {
|
||||
'current_playlist.length': context.core.current_playlist.length,
|
||||
'current_playlist.version': context.core.current_playlist.version,
|
||||
'tracklist.length': context.core.tracklist.length,
|
||||
'tracklist.version': context.core.tracklist.version,
|
||||
'playback.volume': context.core.playback.volume,
|
||||
'playback.consume': context.core.playback.consume,
|
||||
'playback.random': context.core.playback.random,
|
||||
'playback.repeat': context.core.playback.repeat,
|
||||
'playback.single': context.core.playback.single,
|
||||
'playback.state': context.core.playback.state,
|
||||
'playback.current_cp_track': context.core.playback.current_cp_track,
|
||||
'playback.current_playlist_position': (
|
||||
context.core.playback.current_playlist_position),
|
||||
'playback.current_tl_track': context.core.playback.current_tl_track,
|
||||
'playback.tracklist_position': (
|
||||
context.core.playback.tracklist_position),
|
||||
'playback.time_position': context.core.playback.time_position,
|
||||
}
|
||||
pykka.get_all(futures.values())
|
||||
@ -198,7 +200,7 @@ def status(context):
|
||||
('xfade', _status_xfade(futures)),
|
||||
('state', _status_state(futures)),
|
||||
]
|
||||
if futures['playback.current_cp_track'].get() is not None:
|
||||
if futures['playback.current_tl_track'].get() is not None:
|
||||
result.append(('song', _status_songpos(futures)))
|
||||
result.append(('songid', _status_songid(futures)))
|
||||
if futures['playback.state'].get() in (
|
||||
@ -210,9 +212,9 @@ def status(context):
|
||||
|
||||
|
||||
def _status_bitrate(futures):
|
||||
current_cp_track = futures['playback.current_cp_track'].get()
|
||||
if current_cp_track is not None:
|
||||
return current_cp_track.track.bitrate
|
||||
current_tl_track = futures['playback.current_tl_track'].get()
|
||||
if current_tl_track is not None:
|
||||
return current_tl_track.track.bitrate
|
||||
|
||||
|
||||
def _status_consume(futures):
|
||||
@ -223,11 +225,11 @@ def _status_consume(futures):
|
||||
|
||||
|
||||
def _status_playlist_length(futures):
|
||||
return futures['current_playlist.length'].get()
|
||||
return futures['tracklist.length'].get()
|
||||
|
||||
|
||||
def _status_playlist_version(futures):
|
||||
return futures['current_playlist.version'].get()
|
||||
return futures['tracklist.version'].get()
|
||||
|
||||
|
||||
def _status_random(futures):
|
||||
@ -243,45 +245,45 @@ def _status_single(futures):
|
||||
|
||||
|
||||
def _status_songid(futures):
|
||||
current_cp_track = futures['playback.current_cp_track'].get()
|
||||
if current_cp_track is not None:
|
||||
return current_cp_track.cpid
|
||||
current_tl_track = futures['playback.current_tl_track'].get()
|
||||
if current_tl_track is not None:
|
||||
return current_tl_track.tlid
|
||||
else:
|
||||
return _status_songpos(futures)
|
||||
|
||||
|
||||
def _status_songpos(futures):
|
||||
return futures['playback.current_playlist_position'].get()
|
||||
return futures['playback.tracklist_position'].get()
|
||||
|
||||
|
||||
def _status_state(futures):
|
||||
state = futures['playback.state'].get()
|
||||
if state == PlaybackState.PLAYING:
|
||||
return u'play'
|
||||
return 'play'
|
||||
elif state == PlaybackState.STOPPED:
|
||||
return u'stop'
|
||||
return 'stop'
|
||||
elif state == PlaybackState.PAUSED:
|
||||
return u'pause'
|
||||
return 'pause'
|
||||
|
||||
|
||||
def _status_time(futures):
|
||||
return u'%d:%d' % (
|
||||
return '%d:%d' % (
|
||||
futures['playback.time_position'].get() // 1000,
|
||||
_status_time_total(futures) // 1000)
|
||||
|
||||
|
||||
def _status_time_elapsed(futures):
|
||||
return u'%.3f' % (futures['playback.time_position'].get() / 1000.0)
|
||||
return '%.3f' % (futures['playback.time_position'].get() / 1000.0)
|
||||
|
||||
|
||||
def _status_time_total(futures):
|
||||
current_cp_track = futures['playback.current_cp_track'].get()
|
||||
if current_cp_track is None:
|
||||
current_tl_track = futures['playback.current_tl_track'].get()
|
||||
if current_tl_track is None:
|
||||
return 0
|
||||
elif current_cp_track.track.length is None:
|
||||
elif current_tl_track.track.length is None:
|
||||
return 0
|
||||
else:
|
||||
return current_cp_track.track.length
|
||||
return current_tl_track.track.length
|
||||
|
||||
|
||||
def _status_volume(futures):
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime as dt
|
||||
|
||||
from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
|
||||
@ -22,10 +24,10 @@ def listplaylist(context, name):
|
||||
file: relative/path/to/file3.mp3
|
||||
"""
|
||||
try:
|
||||
playlist = context.core.stored_playlists.get(name=name).get()
|
||||
playlist = context.core.playlists.get(name=name).get()
|
||||
return ['file: %s' % t.uri for t in playlist.tracks]
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
|
||||
raise MpdNoExistError('No such playlist', command='listplaylist')
|
||||
|
||||
|
||||
@handle_request(r'^listplaylistinfo (?P<name>\S+)$')
|
||||
@ -44,11 +46,10 @@ def listplaylistinfo(context, name):
|
||||
Album, Artist, Track
|
||||
"""
|
||||
try:
|
||||
playlist = context.core.stored_playlists.get(name=name).get()
|
||||
playlist = context.core.playlists.get(name=name).get()
|
||||
return playlist_to_mpd_format(playlist)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(
|
||||
u'No such playlist', command=u'listplaylistinfo')
|
||||
raise MpdNoExistError('No such playlist', command='listplaylistinfo')
|
||||
|
||||
|
||||
@handle_request(r'^listplaylists$')
|
||||
@ -73,8 +74,8 @@ def listplaylists(context):
|
||||
Last-Modified: 2010-02-06T02:11:08Z
|
||||
"""
|
||||
result = []
|
||||
for playlist in context.core.stored_playlists.playlists.get():
|
||||
result.append((u'playlist', playlist.name))
|
||||
for playlist in context.core.playlists.playlists.get():
|
||||
result.append(('playlist', playlist.name))
|
||||
last_modified = (
|
||||
playlist.last_modified or dt.datetime.now()).isoformat()
|
||||
# Remove microseconds
|
||||
@ -82,7 +83,7 @@ def listplaylists(context):
|
||||
# Add time zone information
|
||||
# TODO Convert to UTC before adding Z
|
||||
last_modified = last_modified + 'Z'
|
||||
result.append((u'Last-Modified', last_modified))
|
||||
result.append(('Last-Modified', last_modified))
|
||||
return result
|
||||
|
||||
|
||||
@ -100,10 +101,10 @@ def load(context, name):
|
||||
- ``load`` appends the given playlist to the current playlist.
|
||||
"""
|
||||
try:
|
||||
playlist = context.core.stored_playlists.get(name=name).get()
|
||||
context.core.current_playlist.append(playlist.tracks)
|
||||
playlist = context.core.playlists.get(name=name).get()
|
||||
context.core.tracklist.append(playlist.tracks)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such playlist', command=u'load')
|
||||
raise MpdNoExistError('No such playlist', command='load')
|
||||
|
||||
|
||||
@handle_request(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy.frontends.mpd import dispatcher, protocol
|
||||
@ -21,18 +23,18 @@ class MpdSession(network.LineProtocol):
|
||||
self.dispatcher = dispatcher.MpdDispatcher(session=self, core=core)
|
||||
|
||||
def on_start(self):
|
||||
logger.info(u'New MPD connection from [%s]:%s', self.host, self.port)
|
||||
self.send_lines([u'OK MPD %s' % protocol.VERSION])
|
||||
logger.info('New MPD connection from [%s]:%s', self.host, self.port)
|
||||
self.send_lines(['OK MPD %s' % protocol.VERSION])
|
||||
|
||||
def on_line_received(self, line):
|
||||
logger.debug(u'Request from [%s]:%s: %s', self.host, self.port, line)
|
||||
logger.debug('Request from [%s]:%s: %s', self.host, self.port, line)
|
||||
|
||||
response = self.dispatcher.handle_request(line)
|
||||
if not response:
|
||||
return
|
||||
|
||||
logger.debug(
|
||||
u'Response to [%s]:%s: %s', self.host, self.port,
|
||||
'Response to [%s]:%s: %s', self.host, self.port,
|
||||
formatting.indent(self.terminator.join(response)))
|
||||
|
||||
self.send_lines(response)
|
||||
@ -45,8 +47,8 @@ class MpdSession(network.LineProtocol):
|
||||
return super(MpdSession, self).decode(line.decode('string_escape'))
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
u'Stopping actor due to unescaping error, data '
|
||||
u'supplied by client was not valid.')
|
||||
'Stopping actor due to unescaping error, data '
|
||||
'supplied by client was not valid.')
|
||||
self.stop()
|
||||
|
||||
def close(self):
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd import protocol
|
||||
from mopidy.models import CpTrack
|
||||
from mopidy.models import TlTrack
|
||||
from mopidy.utils.path import mtime as get_mtime, uri_to_path, split_path
|
||||
|
||||
|
||||
@ -12,7 +14,7 @@ def track_to_mpd_format(track, position=None):
|
||||
Format track for output to MPD client.
|
||||
|
||||
:param track: the track
|
||||
:type track: :class:`mopidy.models.Track` or :class:`mopidy.models.CpTrack`
|
||||
:type track: :class:`mopidy.models.Track` or :class:`mopidy.models.TlTrack`
|
||||
:param position: track's position in playlist
|
||||
:type position: integer
|
||||
:param key: if we should set key
|
||||
@ -21,10 +23,10 @@ def track_to_mpd_format(track, position=None):
|
||||
:type mtime: boolean
|
||||
:rtype: list of two-tuples
|
||||
"""
|
||||
if isinstance(track, CpTrack):
|
||||
(cpid, track) = track
|
||||
if isinstance(track, TlTrack):
|
||||
(tlid, track) = track
|
||||
else:
|
||||
(cpid, track) = (None, track)
|
||||
(tlid, track) = (None, track)
|
||||
result = [
|
||||
('file', track.uri or ''),
|
||||
('Time', track.length and (track.length // 1000) or 0),
|
||||
@ -41,9 +43,9 @@ def track_to_mpd_format(track, position=None):
|
||||
if track.album is not None and track.album.artists:
|
||||
artists = artists_to_mpd_format(track.album.artists)
|
||||
result.append(('AlbumArtist', artists))
|
||||
if position is not None and cpid is not None:
|
||||
if position is not None and tlid is not None:
|
||||
result.append(('Pos', position))
|
||||
result.append(('Id', cpid))
|
||||
result.append(('Id', tlid))
|
||||
if track.album is not None and track.album.musicbrainz_id is not None:
|
||||
result.append(('MUSICBRAINZ_ALBUMID', track.album.musicbrainz_id))
|
||||
# FIXME don't use first and best artist?
|
||||
@ -93,7 +95,7 @@ def artists_to_mpd_format(artists):
|
||||
"""
|
||||
artists = list(artists)
|
||||
artists.sort(key=lambda a: a.name)
|
||||
return u', '.join([a.name for a in artists if a.name])
|
||||
return ', '.join([a.name for a in artists if a.name])
|
||||
|
||||
|
||||
def tracks_to_mpd_format(tracks, start=0, end=None):
|
||||
@ -104,7 +106,7 @@ def tracks_to_mpd_format(tracks, start=0, end=None):
|
||||
|
||||
:param tracks: the tracks
|
||||
:type tracks: list of :class:`mopidy.models.Track` or
|
||||
:class:`mopidy.models.CpTrack`
|
||||
:class:`mopidy.models.TlTrack`
|
||||
:param start: position of first track to include in output
|
||||
:type start: int (positive or negative)
|
||||
:param end: position after last track to include in output
|
||||
@ -178,7 +180,7 @@ def _add_to_tag_cache(result, folders, files):
|
||||
def tracks_to_directory_tree(tracks):
|
||||
directories = ({}, [])
|
||||
for track in tracks:
|
||||
path = u''
|
||||
path = ''
|
||||
current = directories
|
||||
|
||||
local_folder = settings.LOCAL_MUSIC_PATH
|
||||
|
||||
@ -50,5 +50,7 @@ Now you can control Mopidy through the player object. Examples:
|
||||
player.Quit(dbus_interface='org.mpris.MediaPlayer2')
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import MprisFrontend
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import pykka
|
||||
@ -12,7 +14,7 @@ try:
|
||||
import indicate
|
||||
except ImportError as import_error:
|
||||
indicate = None # noqa
|
||||
logger.debug(u'Startup notification will not be sent (%s)', import_error)
|
||||
logger.debug('Startup notification will not be sent (%s)', import_error)
|
||||
|
||||
|
||||
class MprisFrontend(pykka.ThreadingActor, CoreListener):
|
||||
@ -27,20 +29,20 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener):
|
||||
self.mpris_object = objects.MprisObject(self.core)
|
||||
self._send_startup_notification()
|
||||
except Exception as e:
|
||||
logger.error(u'MPRIS frontend setup failed (%s)', e)
|
||||
logger.error('MPRIS frontend setup failed (%s)', e)
|
||||
self.stop()
|
||||
|
||||
def on_stop(self):
|
||||
logger.debug(u'Removing MPRIS object from D-Bus connection...')
|
||||
logger.debug('Removing MPRIS object from D-Bus connection...')
|
||||
if self.mpris_object:
|
||||
self.mpris_object.remove_from_connection()
|
||||
self.mpris_object = None
|
||||
logger.debug(u'Removed MPRIS object from D-Bus connection')
|
||||
logger.debug('Removed MPRIS object from D-Bus connection')
|
||||
|
||||
def _send_startup_notification(self):
|
||||
"""
|
||||
Send startup notification using libindicate to make Mopidy appear in
|
||||
e.g. `Ubuntu's sound menu <https://wiki.ubuntu.com/SoundMenu>`_.
|
||||
e.g. `Ubunt's sound menu <https://wiki.ubuntu.com/SoundMenu>`_.
|
||||
|
||||
A reference to the libindicate server is kept for as long as Mopidy is
|
||||
running. When Mopidy exits, the server will be unreferenced and Mopidy
|
||||
@ -48,12 +50,12 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener):
|
||||
"""
|
||||
if not indicate:
|
||||
return
|
||||
logger.debug(u'Sending startup notification...')
|
||||
logger.debug('Sending startup notification...')
|
||||
self.indicate_server = indicate.Server()
|
||||
self.indicate_server.set_type('music.mopidy')
|
||||
self.indicate_server.set_desktop_file(settings.DESKTOP_FILE)
|
||||
self.indicate_server.show()
|
||||
logger.debug(u'Startup notification sent')
|
||||
logger.debug('Startup notification sent')
|
||||
|
||||
def _emit_properties_changed(self, *changed_properties):
|
||||
if self.mpris_object is None:
|
||||
@ -65,25 +67,25 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener):
|
||||
objects.PLAYER_IFACE, dict(props_with_new_values), [])
|
||||
|
||||
def track_playback_paused(self, track, time_position):
|
||||
logger.debug(u'Received track playback paused event')
|
||||
logger.debug('Received track playback paused event')
|
||||
self._emit_properties_changed('PlaybackStatus')
|
||||
|
||||
def track_playback_resumed(self, track, time_position):
|
||||
logger.debug(u'Received track playback resumed event')
|
||||
logger.debug('Received track playback resumed event')
|
||||
self._emit_properties_changed('PlaybackStatus')
|
||||
|
||||
def track_playback_started(self, track):
|
||||
logger.debug(u'Received track playback started event')
|
||||
logger.debug('Received track playback started event')
|
||||
self._emit_properties_changed('PlaybackStatus', 'Metadata')
|
||||
|
||||
def track_playback_ended(self, track, time_position):
|
||||
logger.debug(u'Received track playback ended event')
|
||||
logger.debug('Received track playback ended event')
|
||||
self._emit_properties_changed('PlaybackStatus', 'Metadata')
|
||||
|
||||
def volume_changed(self):
|
||||
logger.debug(u'Received volume changed event')
|
||||
logger.debug('Received volume changed event')
|
||||
self._emit_properties_changed('Volume')
|
||||
|
||||
def seeked(self, time_position_in_ms):
|
||||
logger.debug(u'Received seeked event')
|
||||
logger.debug('Received seeked event')
|
||||
self.mpris_object.Seeked(time_position_in_ms * 1000)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
@ -75,17 +77,17 @@ class MprisObject(dbus.service.Object):
|
||||
}
|
||||
|
||||
def _connect_to_dbus(self):
|
||||
logger.debug(u'Connecting to D-Bus...')
|
||||
logger.debug('Connecting to D-Bus...')
|
||||
mainloop = dbus.mainloop.glib.DBusGMainLoop()
|
||||
bus_name = dbus.service.BusName(
|
||||
BUS_NAME, dbus.SessionBus(mainloop=mainloop))
|
||||
logger.info(u'Connected to D-Bus')
|
||||
logger.info('Connected to D-Bus')
|
||||
return bus_name
|
||||
|
||||
def _get_track_id(self, cp_track):
|
||||
return '/com/mopidy/track/%d' % cp_track.cpid
|
||||
def _get_track_id(self, tl_track):
|
||||
return '/com/mopidy/track/%d' % tl_track.tlid
|
||||
|
||||
def _get_cpid(self, track_id):
|
||||
def _get_tlid(self, track_id):
|
||||
assert track_id.startswith('/com/mopidy/track/')
|
||||
return track_id.split('/')[-1]
|
||||
|
||||
@ -95,7 +97,7 @@ class MprisObject(dbus.service.Object):
|
||||
in_signature='ss', out_signature='v')
|
||||
def Get(self, interface, prop):
|
||||
logger.debug(
|
||||
u'%s.Get(%s, %s) called',
|
||||
'%s.Get(%s, %s) called',
|
||||
dbus.PROPERTIES_IFACE, repr(interface), repr(prop))
|
||||
(getter, _) = self.properties[interface][prop]
|
||||
if callable(getter):
|
||||
@ -107,7 +109,7 @@ class MprisObject(dbus.service.Object):
|
||||
in_signature='s', out_signature='a{sv}')
|
||||
def GetAll(self, interface):
|
||||
logger.debug(
|
||||
u'%s.GetAll(%s) called', dbus.PROPERTIES_IFACE, repr(interface))
|
||||
'%s.GetAll(%s) called', dbus.PROPERTIES_IFACE, repr(interface))
|
||||
getters = {}
|
||||
for key, (getter, _) in self.properties[interface].iteritems():
|
||||
getters[key] = getter() if callable(getter) else getter
|
||||
@ -117,7 +119,7 @@ class MprisObject(dbus.service.Object):
|
||||
in_signature='ssv', out_signature='')
|
||||
def Set(self, interface, prop, value):
|
||||
logger.debug(
|
||||
u'%s.Set(%s, %s, %s) called',
|
||||
'%s.Set(%s, %s, %s) called',
|
||||
dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value))
|
||||
_, setter = self.properties[interface][prop]
|
||||
if setter is not None:
|
||||
@ -130,7 +132,7 @@ class MprisObject(dbus.service.Object):
|
||||
def PropertiesChanged(self, interface, changed_properties,
|
||||
invalidated_properties):
|
||||
logger.debug(
|
||||
u'%s.PropertiesChanged(%s, %s, %s) signaled',
|
||||
'%s.PropertiesChanged(%s, %s, %s) signaled',
|
||||
dbus.PROPERTIES_IFACE, interface, changed_properties,
|
||||
invalidated_properties)
|
||||
|
||||
@ -138,12 +140,12 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
@dbus.service.method(dbus_interface=ROOT_IFACE)
|
||||
def Raise(self):
|
||||
logger.debug(u'%s.Raise called', ROOT_IFACE)
|
||||
logger.debug('%s.Raise called', ROOT_IFACE)
|
||||
# Do nothing, as we do not have a GUI
|
||||
|
||||
@dbus.service.method(dbus_interface=ROOT_IFACE)
|
||||
def Quit(self):
|
||||
logger.debug(u'%s.Quit called', ROOT_IFACE)
|
||||
logger.debug('%s.Quit called', ROOT_IFACE)
|
||||
exit_process()
|
||||
|
||||
### Root interface properties
|
||||
@ -158,33 +160,33 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def Next(self):
|
||||
logger.debug(u'%s.Next called', PLAYER_IFACE)
|
||||
logger.debug('%s.Next called', PLAYER_IFACE)
|
||||
if not self.get_CanGoNext():
|
||||
logger.debug(u'%s.Next not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.Next not allowed', PLAYER_IFACE)
|
||||
return
|
||||
self.core.playback.next().get()
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def Previous(self):
|
||||
logger.debug(u'%s.Previous called', PLAYER_IFACE)
|
||||
logger.debug('%s.Previous called', PLAYER_IFACE)
|
||||
if not self.get_CanGoPrevious():
|
||||
logger.debug(u'%s.Previous not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.Previous not allowed', PLAYER_IFACE)
|
||||
return
|
||||
self.core.playback.previous().get()
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def Pause(self):
|
||||
logger.debug(u'%s.Pause called', PLAYER_IFACE)
|
||||
logger.debug('%s.Pause called', PLAYER_IFACE)
|
||||
if not self.get_CanPause():
|
||||
logger.debug(u'%s.Pause not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.Pause not allowed', PLAYER_IFACE)
|
||||
return
|
||||
self.core.playback.pause().get()
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def PlayPause(self):
|
||||
logger.debug(u'%s.PlayPause called', PLAYER_IFACE)
|
||||
logger.debug('%s.PlayPause called', PLAYER_IFACE)
|
||||
if not self.get_CanPause():
|
||||
logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.PlayPause not allowed', PLAYER_IFACE)
|
||||
return
|
||||
state = self.core.playback.state.get()
|
||||
if state == PlaybackState.PLAYING:
|
||||
@ -196,17 +198,17 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def Stop(self):
|
||||
logger.debug(u'%s.Stop called', PLAYER_IFACE)
|
||||
logger.debug('%s.Stop called', PLAYER_IFACE)
|
||||
if not self.get_CanControl():
|
||||
logger.debug(u'%s.Stop not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.Stop not allowed', PLAYER_IFACE)
|
||||
return
|
||||
self.core.playback.stop().get()
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def Play(self):
|
||||
logger.debug(u'%s.Play called', PLAYER_IFACE)
|
||||
logger.debug('%s.Play called', PLAYER_IFACE)
|
||||
if not self.get_CanPlay():
|
||||
logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.Play not allowed', PLAYER_IFACE)
|
||||
return
|
||||
state = self.core.playback.state.get()
|
||||
if state == PlaybackState.PAUSED:
|
||||
@ -216,9 +218,9 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def Seek(self, offset):
|
||||
logger.debug(u'%s.Seek called', PLAYER_IFACE)
|
||||
logger.debug('%s.Seek called', PLAYER_IFACE)
|
||||
if not self.get_CanSeek():
|
||||
logger.debug(u'%s.Seek not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.Seek not allowed', PLAYER_IFACE)
|
||||
return
|
||||
offset_in_milliseconds = offset // 1000
|
||||
current_position = self.core.playback.time_position.get()
|
||||
@ -227,29 +229,29 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def SetPosition(self, track_id, position):
|
||||
logger.debug(u'%s.SetPosition called', PLAYER_IFACE)
|
||||
logger.debug('%s.SetPosition called', PLAYER_IFACE)
|
||||
if not self.get_CanSeek():
|
||||
logger.debug(u'%s.SetPosition not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.SetPosition not allowed', PLAYER_IFACE)
|
||||
return
|
||||
position = position // 1000
|
||||
current_cp_track = self.core.playback.current_cp_track.get()
|
||||
if current_cp_track is None:
|
||||
current_tl_track = self.core.playback.current_tl_track.get()
|
||||
if current_tl_track is None:
|
||||
return
|
||||
if track_id != self._get_track_id(current_cp_track):
|
||||
if track_id != self._get_track_id(current_tl_track):
|
||||
return
|
||||
if position < 0:
|
||||
return
|
||||
if current_cp_track.track.length < position:
|
||||
if current_tl_track.track.length < position:
|
||||
return
|
||||
self.core.playback.seek(position)
|
||||
|
||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||
def OpenUri(self, uri):
|
||||
logger.debug(u'%s.OpenUri called', PLAYER_IFACE)
|
||||
logger.debug('%s.OpenUri called', PLAYER_IFACE)
|
||||
if not self.get_CanPlay():
|
||||
# NOTE The spec does not explictly require this check, but guarding
|
||||
# the other methods doesn't help much if OpenUri is open for use.
|
||||
logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
|
||||
logger.debug('%s.Play not allowed', PLAYER_IFACE)
|
||||
return
|
||||
# NOTE Check if URI has MIME type known to the backend, if MIME support
|
||||
# is added to the backend.
|
||||
@ -258,16 +260,16 @@ class MprisObject(dbus.service.Object):
|
||||
return
|
||||
track = self.core.library.lookup(uri).get()
|
||||
if track is not None:
|
||||
cp_track = self.core.current_playlist.add(track).get()
|
||||
self.core.playback.play(cp_track)
|
||||
tl_track = self.core.tracklist.add(track).get()
|
||||
self.core.playback.play(tl_track)
|
||||
else:
|
||||
logger.debug(u'Track with URI "%s" not found in library.', uri)
|
||||
logger.debug('Track with URI "%s" not found in library.', uri)
|
||||
|
||||
### Player interface signals
|
||||
|
||||
@dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x')
|
||||
def Seeked(self, position):
|
||||
logger.debug(u'%s.Seeked signaled', PLAYER_IFACE)
|
||||
logger.debug('%s.Seeked signaled', PLAYER_IFACE)
|
||||
# Do nothing, as just calling the method is enough to emit the signal.
|
||||
|
||||
### Player interface properties
|
||||
@ -294,7 +296,7 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
def set_LoopStatus(self, value):
|
||||
if not self.get_CanControl():
|
||||
logger.debug(u'Setting %s.LoopStatus not allowed', PLAYER_IFACE)
|
||||
logger.debug('Setting %s.LoopStatus not allowed', PLAYER_IFACE)
|
||||
return
|
||||
if value == 'None':
|
||||
self.core.playback.repeat = False
|
||||
@ -310,7 +312,7 @@ class MprisObject(dbus.service.Object):
|
||||
if not self.get_CanControl():
|
||||
# NOTE The spec does not explictly require this check, but it was
|
||||
# added to be consistent with all the other property setters.
|
||||
logger.debug(u'Setting %s.Rate not allowed', PLAYER_IFACE)
|
||||
logger.debug('Setting %s.Rate not allowed', PLAYER_IFACE)
|
||||
return
|
||||
if value == 0:
|
||||
self.Pause()
|
||||
@ -320,7 +322,7 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
def set_Shuffle(self, value):
|
||||
if not self.get_CanControl():
|
||||
logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE)
|
||||
logger.debug('Setting %s.Shuffle not allowed', PLAYER_IFACE)
|
||||
return
|
||||
if value:
|
||||
self.core.playback.random = True
|
||||
@ -328,12 +330,12 @@ class MprisObject(dbus.service.Object):
|
||||
self.core.playback.random = False
|
||||
|
||||
def get_Metadata(self):
|
||||
current_cp_track = self.core.playback.current_cp_track.get()
|
||||
if current_cp_track is None:
|
||||
current_tl_track = self.core.playback.current_tl_track.get()
|
||||
if current_tl_track is None:
|
||||
return {'mpris:trackid': ''}
|
||||
else:
|
||||
(_, track) = current_cp_track
|
||||
metadata = {'mpris:trackid': self._get_track_id(current_cp_track)}
|
||||
(_, track) = current_tl_track
|
||||
metadata = {'mpris:trackid': self._get_track_id(current_tl_track)}
|
||||
if track.length:
|
||||
metadata['mpris:length'] = track.length * 1000
|
||||
if track.uri:
|
||||
@ -364,7 +366,7 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
def set_Volume(self, value):
|
||||
if not self.get_CanControl():
|
||||
logger.debug(u'Setting %s.Volume not allowed', PLAYER_IFACE)
|
||||
logger.debug('Setting %s.Volume not allowed', PLAYER_IFACE)
|
||||
return
|
||||
if value is None:
|
||||
return
|
||||
@ -382,15 +384,15 @@ class MprisObject(dbus.service.Object):
|
||||
if not self.get_CanControl():
|
||||
return False
|
||||
return (
|
||||
self.core.playback.cp_track_at_next.get() !=
|
||||
self.core.playback.current_cp_track.get())
|
||||
self.core.playback.tl_track_at_next.get() !=
|
||||
self.core.playback.current_tl_track.get())
|
||||
|
||||
def get_CanGoPrevious(self):
|
||||
if not self.get_CanControl():
|
||||
return False
|
||||
return (
|
||||
self.core.playback.cp_track_at_previous.get() !=
|
||||
self.core.playback.current_cp_track.get())
|
||||
self.core.playback.tl_track_at_previous.get() !=
|
||||
self.core.playback.current_tl_track.get())
|
||||
|
||||
def get_CanPlay(self):
|
||||
if not self.get_CanControl():
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
@ -14,7 +16,7 @@ class ImmutableObject(object):
|
||||
for key, value in kwargs.items():
|
||||
if not hasattr(self, key):
|
||||
raise TypeError(
|
||||
u"__init__() got an unexpected keyword argument '%s'" %
|
||||
'__init__() got an unexpected keyword argument "%s"' %
|
||||
key)
|
||||
self.__dict__[key] = value
|
||||
|
||||
@ -73,7 +75,7 @@ class ImmutableObject(object):
|
||||
data[key] = values.pop(key)
|
||||
if values:
|
||||
raise TypeError(
|
||||
u"copy() got an unexpected keyword argument '%s'" % key)
|
||||
'copy() got an unexpected keyword argument "%s"' % key)
|
||||
return self.__class__(**data)
|
||||
|
||||
def serialize(self):
|
||||
@ -149,7 +151,7 @@ class Album(ImmutableObject):
|
||||
super(Album, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
CpTrack = namedtuple('CpTrack', ['cpid', 'track'])
|
||||
TlTrack = namedtuple('TlTrack', ['tlid', 'track'])
|
||||
|
||||
|
||||
class Track(ImmutableObject):
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
@ -62,7 +64,7 @@ class Scanner(object):
|
||||
fakesink = gst.element_factory_make('fakesink')
|
||||
|
||||
self.uribin = gst.element_factory_make('uridecodebin')
|
||||
self.uribin.set_property('caps', gst.Caps('audio/x-raw-int'))
|
||||
self.uribin.set_property('caps', gst.Caps(b'audio/x-raw-int'))
|
||||
self.uribin.connect(
|
||||
'pad-added', self.process_new_pad, fakesink.get_pad('sink'))
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ All available settings and their default values.
|
||||
file called ``~/.config/mopidy/settings.py`` and redefine settings there.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
#: List of playback backends to use. See :ref:`backend-implementations` for all
|
||||
#: available backends.
|
||||
#:
|
||||
@ -20,21 +22,21 @@ All available settings and their default values.
|
||||
#: u'mopidy.backends.spotify.SpotifyBackend',
|
||||
#: )
|
||||
BACKENDS = (
|
||||
u'mopidy.backends.local.LocalBackend',
|
||||
u'mopidy.backends.spotify.SpotifyBackend',
|
||||
'mopidy.backends.local.LocalBackend',
|
||||
'mopidy.backends.spotify.SpotifyBackend',
|
||||
)
|
||||
|
||||
#: The log format used for informational logging.
|
||||
#:
|
||||
#: See http://docs.python.org/2/library/logging.html#formatter-objects for
|
||||
#: details on the format.
|
||||
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
|
||||
CONSOLE_LOG_FORMAT = '%(levelname)-8s %(message)s'
|
||||
|
||||
#: The log format used for debug logging.
|
||||
#:
|
||||
#: See http://docs.python.org/library/logging.html#formatter-objects for
|
||||
#: details on the format.
|
||||
DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
|
||||
DEBUG_LOG_FORMAT = '%(levelname)-8s %(asctime)s' + \
|
||||
' [%(process)d:%(threadName)s] %(name)s\n %(message)s'
|
||||
|
||||
#: The file to dump debug log data to when Mopidy is run with the
|
||||
@ -43,7 +45,7 @@ DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
|
||||
#: Default::
|
||||
#:
|
||||
#: DEBUG_LOG_FILENAME = u'mopidy.log'
|
||||
DEBUG_LOG_FILENAME = u'mopidy.log'
|
||||
DEBUG_LOG_FILENAME = 'mopidy.log'
|
||||
|
||||
#: If we should start a background thread that dumps thread's traceback when we
|
||||
#: get a SIGUSR1. Mainly a debug tool for figuring out deadlocks.
|
||||
@ -60,7 +62,7 @@ DEBUG_THREAD = False
|
||||
#: Default::
|
||||
#:
|
||||
#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
|
||||
DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
|
||||
DESKTOP_FILE = '/usr/share/applications/mopidy.desktop'
|
||||
|
||||
#: List of server frontends to use. See :ref:`frontend-implementations` for
|
||||
#: available frontends.
|
||||
@ -73,9 +75,9 @@ DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
|
||||
#: u'mopidy.frontends.mpris.MprisFrontend',
|
||||
#: )
|
||||
FRONTENDS = (
|
||||
u'mopidy.frontends.mpd.MpdFrontend',
|
||||
u'mopidy.frontends.lastfm.LastfmFrontend',
|
||||
u'mopidy.frontends.mpris.MprisFrontend',
|
||||
'mopidy.frontends.mpd.MpdFrontend',
|
||||
'mopidy.frontends.lastfm.LastfmFrontend',
|
||||
'mopidy.frontends.mpris.MprisFrontend',
|
||||
)
|
||||
|
||||
#: Which address Mopidy's HTTP server should bind to.
|
||||
@ -114,12 +116,12 @@ HTTP_SERVER_STATIC_DIR = None
|
||||
#: Your `Last.fm <http://www.last.fm/>`_ username.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.lastfm`.
|
||||
LASTFM_USERNAME = u''
|
||||
LASTFM_USERNAME = ''
|
||||
|
||||
#: Your `Last.fm <http://www.last.fm/>`_ password.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.lastfm`.
|
||||
LASTFM_PASSWORD = u''
|
||||
LASTFM_PASSWORD = ''
|
||||
|
||||
#: Path to folder with local music.
|
||||
#:
|
||||
@ -128,7 +130,7 @@ LASTFM_PASSWORD = u''
|
||||
#: Default::
|
||||
#:
|
||||
#: LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR'
|
||||
LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR'
|
||||
LOCAL_MUSIC_PATH = '$XDG_MUSIC_DIR'
|
||||
|
||||
#: Path to playlist folder with m3u files for local music.
|
||||
#:
|
||||
@ -137,7 +139,7 @@ LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR'
|
||||
#: Default::
|
||||
#:
|
||||
#: LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists'
|
||||
LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists'
|
||||
LOCAL_PLAYLIST_PATH = '$XDG_DATA_DIR/mopidy/playlists'
|
||||
|
||||
#: Path to tag cache for local music.
|
||||
#:
|
||||
@ -146,7 +148,7 @@ LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists'
|
||||
#: Default::
|
||||
#:
|
||||
#: LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
|
||||
LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
|
||||
LOCAL_TAG_CACHE_FILE = '$XDG_DATA_DIR/mopidy/tag_cache'
|
||||
|
||||
#: Audio mixer to use.
|
||||
#:
|
||||
@ -159,7 +161,7 @@ LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
|
||||
#: Default::
|
||||
#:
|
||||
#: MIXER = u'autoaudiomixer'
|
||||
MIXER = u'autoaudiomixer'
|
||||
MIXER = 'autoaudiomixer'
|
||||
|
||||
#: Audio mixer track to use.
|
||||
#:
|
||||
@ -186,7 +188,7 @@ MIXER_TRACK = None
|
||||
#: Listens on all IPv4 interfaces.
|
||||
#: ``::``
|
||||
#: Listens on all interfaces, both IPv4 and IPv6.
|
||||
MPD_SERVER_HOSTNAME = u'127.0.0.1'
|
||||
MPD_SERVER_HOSTNAME = '127.0.0.1'
|
||||
|
||||
#: Which TCP port Mopidy's MPD server should listen to.
|
||||
#:
|
||||
@ -218,7 +220,7 @@ MPD_SERVER_MAX_CONNECTIONS = 20
|
||||
#: Default::
|
||||
#:
|
||||
#: OUTPUT = u'autoaudiosink'
|
||||
OUTPUT = u'autoaudiosink'
|
||||
OUTPUT = 'autoaudiosink'
|
||||
|
||||
#: Path to the Spotify cache.
|
||||
#:
|
||||
@ -227,17 +229,17 @@ OUTPUT = u'autoaudiosink'
|
||||
#: Default::
|
||||
#:
|
||||
#: SPOTIFY_CACHE_PATH = u'$XDG_CACHE_DIR/mopidy/spotify'
|
||||
SPOTIFY_CACHE_PATH = u'$XDG_CACHE_DIR/mopidy/spotify'
|
||||
SPOTIFY_CACHE_PATH = '$XDG_CACHE_DIR/mopidy/spotify'
|
||||
|
||||
#: Your Spotify Premium username.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
SPOTIFY_USERNAME = u''
|
||||
SPOTIFY_USERNAME = ''
|
||||
|
||||
#: Your Spotify Premium password.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
SPOTIFY_PASSWORD = u''
|
||||
SPOTIFY_PASSWORD = ''
|
||||
|
||||
#: Spotify preferred bitrate.
|
||||
#:
|
||||
|
||||
@ -0,0 +1 @@
|
||||
from __future__ import unicode_literals
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import locale
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
@ -6,7 +8,7 @@ def indent(string, places=4, linebreak='\n'):
|
||||
lines = string.split(linebreak)
|
||||
if len(lines) == 1:
|
||||
return string
|
||||
result = u''
|
||||
result = ''
|
||||
for line in lines:
|
||||
result += linebreak + ' ' * places + line
|
||||
return result
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.utils')
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
||||
@ -14,9 +16,9 @@ def setup_logging(verbosity_level, save_debug_log):
|
||||
# New in Python 2.7
|
||||
logging.captureWarnings(True)
|
||||
logger = logging.getLogger('mopidy.utils.log')
|
||||
logger.info(u'Starting Mopidy %s', versioning.get_version())
|
||||
logger.info(u'%(name)s: %(version)s', deps.platform_info())
|
||||
logger.info(u'%(name)s: %(version)s', deps.python_info())
|
||||
logger.info('Starting Mopidy %s', versioning.get_version())
|
||||
logger.info('%(name)s: %(version)s', deps.platform_info())
|
||||
logger.info('%(name)s: %(version)s', deps.python_info())
|
||||
|
||||
|
||||
def setup_root_logger():
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import errno
|
||||
import gobject
|
||||
import logging
|
||||
@ -26,8 +28,8 @@ def try_ipv6_socket():
|
||||
return True
|
||||
except IOError as error:
|
||||
logger.debug(
|
||||
u'Platform supports IPv6, but socket creation failed, '
|
||||
u'disabling: %s',
|
||||
'Platform supports IPv6, but socket creation failed, '
|
||||
'disabling: %s',
|
||||
encoding.locale_decode(error))
|
||||
return False
|
||||
|
||||
@ -107,7 +109,7 @@ class Server(object):
|
||||
|
||||
def reject_connection(self, sock, addr):
|
||||
# FIXME provide more context in logging?
|
||||
logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1])
|
||||
logger.warning('Rejected connection from [%s]:%s', addr[0], addr[1])
|
||||
try:
|
||||
sock.close()
|
||||
except socket.error:
|
||||
@ -190,7 +192,7 @@ class Connection(object):
|
||||
except socket.error as e:
|
||||
if e.errno in (errno.EWOULDBLOCK, errno.EINTR):
|
||||
return data
|
||||
self.stop(u'Unexpected client error: %s' % e)
|
||||
self.stop('Unexpected client error: %s' % e)
|
||||
return ''
|
||||
|
||||
def enable_timeout(self):
|
||||
@ -219,7 +221,7 @@ class Connection(object):
|
||||
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
|
||||
self.recv_callback)
|
||||
except socket.error as e:
|
||||
self.stop(u'Problem with connection: %s' % e)
|
||||
self.stop('Problem with connection: %s' % e)
|
||||
|
||||
def disable_recv(self):
|
||||
if self.recv_id is None:
|
||||
@ -237,7 +239,7 @@ class Connection(object):
|
||||
gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
|
||||
self.send_callback)
|
||||
except socket.error as e:
|
||||
self.stop(u'Problem with connection: %s' % e)
|
||||
self.stop('Problem with connection: %s' % e)
|
||||
|
||||
def disable_send(self):
|
||||
if self.send_id is None:
|
||||
@ -248,30 +250,30 @@ class Connection(object):
|
||||
|
||||
def recv_callback(self, fd, flags):
|
||||
if flags & (gobject.IO_ERR | gobject.IO_HUP):
|
||||
self.stop(u'Bad client flags: %s' % flags)
|
||||
self.stop('Bad client flags: %s' % flags)
|
||||
return True
|
||||
|
||||
try:
|
||||
data = self.sock.recv(4096)
|
||||
except socket.error as e:
|
||||
if e.errno not in (errno.EWOULDBLOCK, errno.EINTR):
|
||||
self.stop(u'Unexpected client error: %s' % e)
|
||||
self.stop('Unexpected client error: %s' % e)
|
||||
return True
|
||||
|
||||
if not data:
|
||||
self.stop(u'Client most likely disconnected.')
|
||||
self.stop('Client most likely disconnected.')
|
||||
return True
|
||||
|
||||
try:
|
||||
self.actor_ref.tell({'received': data})
|
||||
except pykka.ActorDeadError:
|
||||
self.stop(u'Actor is dead.')
|
||||
self.stop('Actor is dead.')
|
||||
|
||||
return True
|
||||
|
||||
def send_callback(self, fd, flags):
|
||||
if flags & (gobject.IO_ERR | gobject.IO_HUP):
|
||||
self.stop(u'Bad client flags: %s' % flags)
|
||||
self.stop('Bad client flags: %s' % flags)
|
||||
return True
|
||||
|
||||
# If with can't get the lock, simply try again next time socket is
|
||||
@ -289,7 +291,7 @@ class Connection(object):
|
||||
return True
|
||||
|
||||
def timeout_callback(self):
|
||||
self.stop(u'Client timeout out after %s seconds' % self.timeout)
|
||||
self.stop('Client timeout out after %s seconds' % self.timeout)
|
||||
return False
|
||||
|
||||
|
||||
@ -356,7 +358,7 @@ class LineProtocol(pykka.ThreadingActor):
|
||||
|
||||
def on_stop(self):
|
||||
"""Ensure that cleanup when actor stops."""
|
||||
self.connection.stop(u'Actor is shutting down.')
|
||||
self.connection.stop('Actor is shutting down.')
|
||||
|
||||
def parse_lines(self):
|
||||
"""Consume new data and yield any lines found."""
|
||||
@ -375,8 +377,8 @@ class LineProtocol(pykka.ThreadingActor):
|
||||
return line.encode(self.encoding)
|
||||
except UnicodeError:
|
||||
logger.warning(
|
||||
u'Stopping actor due to encode problem, data '
|
||||
u'supplied by client was not valid %s',
|
||||
'Stopping actor due to encode problem, data '
|
||||
'supplied by client was not valid %s',
|
||||
self.encoding)
|
||||
self.stop()
|
||||
|
||||
@ -390,14 +392,14 @@ class LineProtocol(pykka.ThreadingActor):
|
||||
return line.decode(self.encoding)
|
||||
except UnicodeError:
|
||||
logger.warning(
|
||||
u'Stopping actor due to decode problem, data '
|
||||
u'supplied by client was not valid %s',
|
||||
'Stopping actor due to decode problem, data '
|
||||
'supplied by client was not valid %s',
|
||||
self.encoding)
|
||||
self.stop()
|
||||
|
||||
def join_lines(self, lines):
|
||||
if not lines:
|
||||
return u''
|
||||
return ''
|
||||
return self.terminator.join(lines) + self.terminator
|
||||
|
||||
def send_lines(self, lines):
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -25,10 +27,10 @@ def get_or_create_folder(folder):
|
||||
folder = os.path.expanduser(folder)
|
||||
if os.path.isfile(folder):
|
||||
raise OSError(
|
||||
u'A file with the same name as the desired dir, '
|
||||
u'"%s", already exists.' % folder)
|
||||
'A file with the same name as the desired dir, '
|
||||
'"%s", already exists.' % folder)
|
||||
elif not os.path.isdir(folder):
|
||||
logger.info(u'Creating dir %s', folder)
|
||||
logger.info('Creating dir %s', folder)
|
||||
os.makedirs(folder, 0755)
|
||||
return folder
|
||||
|
||||
@ -36,7 +38,7 @@ def get_or_create_folder(folder):
|
||||
def get_or_create_file(filename):
|
||||
filename = os.path.expanduser(filename)
|
||||
if not os.path.isfile(filename):
|
||||
logger.info(u'Creating file %s', filename)
|
||||
logger.info('Creating file %s', filename)
|
||||
open(filename, 'w')
|
||||
return filename
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
@ -10,26 +12,29 @@ from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import exceptions
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.utils.process')
|
||||
|
||||
|
||||
SIGNALS = dict((k, v) for v, k in signal.__dict__.iteritems()
|
||||
if v.startswith('SIG') and not v.startswith('SIG_'))
|
||||
|
||||
|
||||
def exit_process():
|
||||
logger.debug(u'Interrupting main...')
|
||||
logger.debug('Interrupting main...')
|
||||
thread.interrupt_main()
|
||||
logger.debug(u'Interrupted main')
|
||||
logger.debug('Interrupted main')
|
||||
|
||||
|
||||
def exit_handler(signum, frame):
|
||||
"""A :mod:`signal` handler which will exit the program on signal."""
|
||||
logger.info(u'Got %s signal', SIGNALS[signum])
|
||||
logger.info('Got %s signal', SIGNALS[signum])
|
||||
exit_process()
|
||||
|
||||
|
||||
def stop_actors_by_class(klass):
|
||||
actors = ActorRegistry.get_by_class(klass)
|
||||
logger.debug(u'Stopping %d instance(s) of %s', len(actors), klass.__name__)
|
||||
logger.debug('Stopping %d instance(s) of %s', len(actors), klass.__name__)
|
||||
for actor in actors:
|
||||
actor.stop()
|
||||
|
||||
@ -38,15 +43,15 @@ def stop_remaining_actors():
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
while num_actors:
|
||||
logger.error(
|
||||
u'There are actor threads still running, this is probably a bug')
|
||||
'There are actor threads still running, this is probably a bug')
|
||||
logger.debug(
|
||||
u'Seeing %d actor and %d non-actor thread(s): %s',
|
||||
'Seeing %d actor and %d non-actor thread(s): %s',
|
||||
num_actors, threading.active_count() - num_actors,
|
||||
', '.join([t.name for t in threading.enumerate()]))
|
||||
logger.debug(u'Stopping %d actor(s)...', num_actors)
|
||||
logger.debug('Stopping %d actor(s)...', num_actors)
|
||||
ActorRegistry.stop_all()
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
logger.debug(u'All actors stopped.')
|
||||
logger.debug('All actors stopped.')
|
||||
|
||||
|
||||
class BaseThread(threading.Thread):
|
||||
@ -56,11 +61,11 @@ class BaseThread(threading.Thread):
|
||||
self.daemon = True
|
||||
|
||||
def run(self):
|
||||
logger.debug(u'%s: Starting thread', self.name)
|
||||
logger.debug('%s: Starting thread', self.name)
|
||||
try:
|
||||
self.run_inside_try()
|
||||
except KeyboardInterrupt:
|
||||
logger.info(u'Interrupted by user')
|
||||
logger.info('Interrupted by user')
|
||||
except exceptions.SettingsError as e:
|
||||
logger.error(e.message)
|
||||
except ImportError as e:
|
||||
@ -69,11 +74,12 @@ class BaseThread(threading.Thread):
|
||||
logger.warning(e)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.debug(u'%s: Exiting thread', self.name)
|
||||
logger.debug('%s: Exiting thread', self.name)
|
||||
|
||||
def run_inside_try(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DebugThread(threading.Thread):
|
||||
daemon = True
|
||||
name = 'DebugThread'
|
||||
@ -81,7 +87,7 @@ class DebugThread(threading.Thread):
|
||||
event = threading.Event()
|
||||
|
||||
def handler(self, signum, frame):
|
||||
logger.info(u'Got %s signal', SIGNALS[signum])
|
||||
logger.info('Got %s signal', SIGNALS[signum])
|
||||
self.event.set()
|
||||
|
||||
def run(self):
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Absolute import needed to import ~/.config/mopidy/settings.py and not
|
||||
# ourselves
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import copy
|
||||
import getpass
|
||||
@ -53,11 +53,11 @@ class SettingsProxy(object):
|
||||
|
||||
current = self.current # bind locally to avoid copying+updates
|
||||
if attr not in current:
|
||||
raise exceptions.SettingsError(u'Setting "%s" is not set.' % attr)
|
||||
raise exceptions.SettingsError('Setting "%s" is not set.' % attr)
|
||||
|
||||
value = current[attr]
|
||||
if isinstance(value, basestring) and len(value) == 0:
|
||||
raise exceptions.SettingsError(u'Setting "%s" is empty.' % attr)
|
||||
raise exceptions.SettingsError('Setting "%s" is empty.' % attr)
|
||||
if not value:
|
||||
return value
|
||||
if attr.endswith('_PATH') or attr.endswith('_FILE'):
|
||||
@ -75,17 +75,17 @@ class SettingsProxy(object):
|
||||
self._read_missing_settings_from_stdin(self.current, self.runtime)
|
||||
if self.get_errors():
|
||||
logger.error(
|
||||
u'Settings validation errors: %s',
|
||||
'Settings validation errors: %s',
|
||||
formatting.indent(self.get_errors_as_string()))
|
||||
raise exceptions.SettingsError(u'Settings validation failed.')
|
||||
raise exceptions.SettingsError('Settings validation failed.')
|
||||
|
||||
def _read_missing_settings_from_stdin(self, current, runtime):
|
||||
for setting, value in sorted(current.iteritems()):
|
||||
if isinstance(value, basestring) and len(value) == 0:
|
||||
runtime[setting] = self._read_from_stdin(setting + u': ')
|
||||
runtime[setting] = self._read_from_stdin(setting + ': ')
|
||||
|
||||
def _read_from_stdin(self, prompt):
|
||||
if u'_PASSWORD' in prompt:
|
||||
if '_PASSWORD' in prompt:
|
||||
return (
|
||||
getpass.getpass(prompt)
|
||||
.decode(sys.stdin.encoding, 'ignore'))
|
||||
@ -101,7 +101,7 @@ class SettingsProxy(object):
|
||||
def get_errors_as_string(self):
|
||||
lines = []
|
||||
for (setting, error) in self.get_errors().iteritems():
|
||||
lines.append(u'%s: %s' % (setting, error))
|
||||
lines.append('%s: %s' % (setting, error))
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
@ -121,7 +121,6 @@ def validate_settings(defaults, settings):
|
||||
errors = {}
|
||||
|
||||
changed = {
|
||||
'CUSTOM_OUTPUT': 'OUTPUT',
|
||||
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
|
||||
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
|
||||
'FRONTEND': 'FRONTENDS',
|
||||
@ -151,37 +150,37 @@ def validate_settings(defaults, settings):
|
||||
for setting, value in settings.iteritems():
|
||||
if setting in changed:
|
||||
if changed[setting] is None:
|
||||
errors[setting] = u'Deprecated setting. It may be removed.'
|
||||
errors[setting] = 'Deprecated setting. It may be removed.'
|
||||
else:
|
||||
errors[setting] = u'Deprecated setting. Use %s.' % (
|
||||
errors[setting] = 'Deprecated setting. Use %s.' % (
|
||||
changed[setting],)
|
||||
|
||||
elif setting == 'OUTPUTS':
|
||||
errors[setting] = (
|
||||
u'Deprecated setting, please change to OUTPUT. OUTPUT expects '
|
||||
u'a GStreamer bin description string for your desired output.')
|
||||
'Deprecated setting, please change to OUTPUT. OUTPUT expects '
|
||||
'a GStreamer bin description string for your desired output.')
|
||||
|
||||
elif setting == 'SPOTIFY_BITRATE':
|
||||
if value not in (96, 160, 320):
|
||||
errors[setting] = (
|
||||
u'Unavailable Spotify bitrate. Available bitrates are 96, '
|
||||
u'160, and 320.')
|
||||
'Unavailable Spotify bitrate. Available bitrates are 96, '
|
||||
'160, and 320.')
|
||||
|
||||
elif setting.startswith('SHOUTCAST_OUTPUT_'):
|
||||
errors[setting] = (
|
||||
u'Deprecated setting, please set the value via the GStreamer '
|
||||
u'bin in OUTPUT.')
|
||||
'Deprecated setting, please set the value via the GStreamer '
|
||||
'bin in OUTPUT.')
|
||||
|
||||
elif setting in list_of_one_or_more:
|
||||
if not value:
|
||||
errors[setting] = u'Must contain at least one value.'
|
||||
errors[setting] = 'Must contain at least one value.'
|
||||
|
||||
elif setting not in defaults:
|
||||
errors[setting] = u'Unknown setting.'
|
||||
elif setting not in defaults and not setting.startswith('CUSTOM_'):
|
||||
errors[setting] = 'Unknown setting.'
|
||||
suggestion = did_you_mean(setting, defaults)
|
||||
|
||||
if suggestion:
|
||||
errors[setting] += u' Did you mean %s?' % suggestion
|
||||
errors[setting] += ' Did you mean %s?' % suggestion
|
||||
|
||||
return errors
|
||||
|
||||
@ -204,20 +203,20 @@ def format_settings_list(settings):
|
||||
for (key, value) in sorted(settings.current.iteritems()):
|
||||
default_value = settings.default.get(key)
|
||||
masked_value = mask_value_if_secret(key, value)
|
||||
lines.append(u'%s: %s' % (
|
||||
lines.append('%s: %s' % (
|
||||
key, formatting.indent(pprint.pformat(masked_value), places=2)))
|
||||
if value != default_value and default_value is not None:
|
||||
lines.append(
|
||||
u' Default: %s' %
|
||||
' Default: %s' %
|
||||
formatting.indent(pprint.pformat(default_value), places=4))
|
||||
if errors.get(key) is not None:
|
||||
lines.append(u' Error: %s' % errors[key])
|
||||
lines.append(' Error: %s' % errors[key])
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def mask_value_if_secret(key, value):
|
||||
if key.endswith('PASSWORD') and value:
|
||||
return u'********'
|
||||
return '********'
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
from mopidy import __version__
|
||||
|
||||
15
setup.py
15
setup.py
@ -2,6 +2,8 @@
|
||||
Most of this file is taken from the Django project, which is BSD licensed.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from distutils.core import setup
|
||||
from distutils.command.install_data import install_data
|
||||
from distutils.command.install import INSTALL_SCHEMES
|
||||
@ -64,18 +66,17 @@ for scheme in INSTALL_SCHEMES.values():
|
||||
# an easy way to do this.
|
||||
packages, data_files = [], []
|
||||
root_dir = os.path.dirname(__file__)
|
||||
if root_dir != '':
|
||||
if root_dir != b'':
|
||||
os.chdir(root_dir)
|
||||
project_dir = 'mopidy'
|
||||
|
||||
project_dir = b'mopidy'
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(project_dir):
|
||||
# Ignore dirnames that start with '.'
|
||||
for i, dirname in enumerate(dirnames):
|
||||
if dirname.startswith('.'):
|
||||
if dirname.startswith(b'.'):
|
||||
del dirnames[i]
|
||||
if '__init__.py' in filenames:
|
||||
packages.append('.'.join(fullsplit(dirpath)))
|
||||
if b'__init__.py' in filenames:
|
||||
packages.append(b'.'.join(fullsplit(dirpath)))
|
||||
elif filenames:
|
||||
data_files.append([
|
||||
dirpath, [os.path.join(dirpath, f) for f in filenames]])
|
||||
@ -87,7 +88,7 @@ setup(
|
||||
author='Stein Magnus Jodal',
|
||||
author_email='stein.magnus@jodal.no',
|
||||
packages=packages,
|
||||
package_data={'mopidy': ['backends/spotify/spotify_appkey.key']},
|
||||
package_data={b'mopidy': ['backends/spotify/spotify_appkey.key']},
|
||||
cmdclass=cmdclasses,
|
||||
data_files=data_files,
|
||||
scripts=['bin/mopidy', 'bin/mopidy-scan'],
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import nose
|
||||
import yappi
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy import audio, settings
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
from __future__ import unicode_literals
|
||||
@ -1,7 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
def populate_playlist(func):
|
||||
def wrapper(self):
|
||||
for track in self.tracks:
|
||||
self.core.current_playlist.add(track)
|
||||
self.core.tracklist.add(track)
|
||||
return func(self)
|
||||
|
||||
wrapper.__name__ = func.__name__
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import core
|
||||
@ -79,13 +81,13 @@ class LibraryControllerTest(object):
|
||||
result = self.library.find_exact(album=['album2'])
|
||||
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
|
||||
|
||||
def test_find_exact_filename(self):
|
||||
track_1_filename = 'file://' + path_to_data_dir('uri1')
|
||||
result = self.library.find_exact(filename=track_1_filename)
|
||||
def test_find_exact_uri(self):
|
||||
track_1_uri = 'file://' + path_to_data_dir('uri1')
|
||||
result = self.library.find_exact(uri=track_1_uri)
|
||||
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
|
||||
|
||||
track_2_filename = 'file://' + path_to_data_dir('uri2')
|
||||
result = self.library.find_exact(filename=track_2_filename)
|
||||
track_2_uri = 'file://' + path_to_data_dir('uri2')
|
||||
result = self.library.find_exact(uri=track_2_uri)
|
||||
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
|
||||
|
||||
def test_find_exact_wrong_type(self):
|
||||
@ -146,13 +148,6 @@ class LibraryControllerTest(object):
|
||||
result = self.library.search(uri=['RI2'])
|
||||
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
|
||||
|
||||
def test_search_filename(self):
|
||||
result = self.library.search(filename=['RI1'])
|
||||
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
|
||||
|
||||
result = self.library.search(filename=['RI2'])
|
||||
self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
|
||||
|
||||
def test_search_any(self):
|
||||
result = self.library.search(any=['Tist1'])
|
||||
self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
import random
|
||||
import time
|
||||
@ -20,7 +22,7 @@ class PlaybackControllerTest(object):
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core(backends=[self.backend])
|
||||
self.playback = self.core.playback
|
||||
self.current_playlist = self.core.current_playlist
|
||||
self.tracklist = self.core.tracklist
|
||||
|
||||
assert len(self.tracks) >= 3, \
|
||||
'Need at least three tracks to run tests.'
|
||||
@ -51,13 +53,13 @@ class PlaybackControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_play_track_state(self):
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
self.playback.play(self.tracklist.tl_tracks[-1])
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_play_track_return_value(self):
|
||||
self.assertEqual(self.playback.play(
|
||||
self.current_playlist.cp_tracks[-1]), None)
|
||||
self.tracklist.tl_tracks[-1]), None)
|
||||
|
||||
@populate_playlist
|
||||
def test_play_when_playing(self):
|
||||
@ -93,7 +95,7 @@ class PlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_play_track_sets_current_track(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
self.playback.play(self.tracklist.tl_tracks[-1])
|
||||
self.assertEqual(self.playback.current_track, self.tracks[-1])
|
||||
|
||||
@populate_playlist
|
||||
@ -106,12 +108,12 @@ class PlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_current_track_after_completed_playlist(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
self.playback.play(self.tracklist.tl_tracks[-1])
|
||||
self.playback.on_end_of_track()
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
self.assertEqual(self.playback.current_track, None)
|
||||
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
self.playback.play(self.tracklist.tl_tracks[-1])
|
||||
self.playback.next()
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
self.assertEqual(self.playback.current_track, None)
|
||||
@ -160,7 +162,7 @@ class PlaybackControllerTest(object):
|
||||
def test_previous_skips_to_previous_track_on_failure(self):
|
||||
# If backend's play() returns False, it is a failure.
|
||||
self.backend.playback.play = lambda track: track != self.tracks[1]
|
||||
self.playback.play(self.current_playlist.cp_tracks[2])
|
||||
self.playback.play(self.tracklist.tl_tracks[2])
|
||||
self.assertEqual(self.playback.current_track, self.tracks[2])
|
||||
self.playback.previous()
|
||||
self.assertNotEqual(self.playback.current_track, self.tracks[1])
|
||||
@ -170,13 +172,13 @@ class PlaybackControllerTest(object):
|
||||
def test_next(self):
|
||||
self.playback.play()
|
||||
|
||||
old_position = self.playback.current_playlist_position
|
||||
old_position = self.playback.tracklist_position
|
||||
old_uri = self.playback.current_track.uri
|
||||
|
||||
self.playback.next()
|
||||
|
||||
self.assertEqual(
|
||||
self.playback.current_playlist_position, old_position + 1)
|
||||
self.playback.tracklist_position, old_position + 1)
|
||||
self.assertNotEqual(self.playback.current_track.uri, old_uri)
|
||||
|
||||
@populate_playlist
|
||||
@ -196,7 +198,7 @@ class PlaybackControllerTest(object):
|
||||
for i, track in enumerate(self.tracks):
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
self.assertEqual(self.playback.current_track, track)
|
||||
self.assertEqual(self.playback.current_playlist_position, i)
|
||||
self.assertEqual(self.playback.tracklist_position, i)
|
||||
|
||||
self.playback.next()
|
||||
|
||||
@ -252,7 +254,7 @@ class PlaybackControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_next_track_at_end_of_playlist(self):
|
||||
self.playback.play()
|
||||
for _ in self.current_playlist.cp_tracks[1:]:
|
||||
for _ in self.tracklist.tl_tracks[1:]:
|
||||
self.playback.next()
|
||||
self.assertEqual(self.playback.track_at_next, None)
|
||||
|
||||
@ -275,7 +277,7 @@ class PlaybackControllerTest(object):
|
||||
self.playback.consume = True
|
||||
self.playback.play()
|
||||
self.playback.next()
|
||||
self.assertIn(self.tracks[0], self.current_playlist.tracks)
|
||||
self.assertIn(self.tracks[0], self.tracklist.tracks)
|
||||
|
||||
@populate_playlist
|
||||
def test_next_with_single_and_repeat(self):
|
||||
@ -299,20 +301,20 @@ class PlaybackControllerTest(object):
|
||||
random.seed(1)
|
||||
self.playback.random = True
|
||||
self.assertEqual(self.playback.track_at_next, self.tracks[2])
|
||||
self.current_playlist.append(self.tracks[:1])
|
||||
self.tracklist.append(self.tracks[:1])
|
||||
self.assertEqual(self.playback.track_at_next, self.tracks[1])
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_track(self):
|
||||
self.playback.play()
|
||||
|
||||
old_position = self.playback.current_playlist_position
|
||||
old_position = self.playback.tracklist_position
|
||||
old_uri = self.playback.current_track.uri
|
||||
|
||||
self.playback.on_end_of_track()
|
||||
|
||||
self.assertEqual(
|
||||
self.playback.current_playlist_position, old_position + 1)
|
||||
self.playback.tracklist_position, old_position + 1)
|
||||
self.assertNotEqual(self.playback.current_track.uri, old_uri)
|
||||
|
||||
@populate_playlist
|
||||
@ -332,7 +334,7 @@ class PlaybackControllerTest(object):
|
||||
for i, track in enumerate(self.tracks):
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
self.assertEqual(self.playback.current_track, track)
|
||||
self.assertEqual(self.playback.current_playlist_position, i)
|
||||
self.assertEqual(self.playback.tracklist_position, i)
|
||||
|
||||
self.playback.on_end_of_track()
|
||||
|
||||
@ -388,7 +390,7 @@ class PlaybackControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_end_of_track_track_at_end_of_playlist(self):
|
||||
self.playback.play()
|
||||
for _ in self.current_playlist.cp_tracks[1:]:
|
||||
for _ in self.tracklist.tl_tracks[1:]:
|
||||
self.playback.on_end_of_track()
|
||||
self.assertEqual(self.playback.track_at_next, None)
|
||||
|
||||
@ -411,7 +413,7 @@ class PlaybackControllerTest(object):
|
||||
self.playback.consume = True
|
||||
self.playback.play()
|
||||
self.playback.on_end_of_track()
|
||||
self.assertNotIn(self.tracks[0], self.current_playlist.tracks)
|
||||
self.assertNotIn(self.tracks[0], self.tracklist.tracks)
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_track_with_random(self):
|
||||
@ -427,7 +429,7 @@ class PlaybackControllerTest(object):
|
||||
random.seed(1)
|
||||
self.playback.random = True
|
||||
self.assertEqual(self.playback.track_at_next, self.tracks[2])
|
||||
self.current_playlist.append(self.tracks[:1])
|
||||
self.tracklist.append(self.tracks[:1])
|
||||
self.assertEqual(self.playback.track_at_next, self.tracks[1])
|
||||
|
||||
@populate_playlist
|
||||
@ -488,36 +490,36 @@ class PlaybackControllerTest(object):
|
||||
self.assertEqual(self.playback.current_track, self.tracks[1])
|
||||
|
||||
@populate_playlist
|
||||
def test_initial_current_playlist_position(self):
|
||||
self.assertEqual(self.playback.current_playlist_position, None)
|
||||
def test_initial_tracklist_position(self):
|
||||
self.assertEqual(self.playback.tracklist_position, None)
|
||||
|
||||
@populate_playlist
|
||||
def test_current_playlist_position_during_play(self):
|
||||
def test_tracklist_position_during_play(self):
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.current_playlist_position, 0)
|
||||
self.assertEqual(self.playback.tracklist_position, 0)
|
||||
|
||||
@populate_playlist
|
||||
def test_current_playlist_position_after_next(self):
|
||||
def test_tracklist_position_after_next(self):
|
||||
self.playback.play()
|
||||
self.playback.next()
|
||||
self.assertEqual(self.playback.current_playlist_position, 1)
|
||||
self.assertEqual(self.playback.tracklist_position, 1)
|
||||
|
||||
@populate_playlist
|
||||
def test_current_playlist_position_at_end_of_playlist(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
def test_tracklist_position_at_end_of_playlist(self):
|
||||
self.playback.play(self.tracklist.tl_tracks[-1])
|
||||
self.playback.on_end_of_track()
|
||||
self.assertEqual(self.playback.current_playlist_position, None)
|
||||
self.assertEqual(self.playback.tracklist_position, None)
|
||||
|
||||
def test_on_current_playlist_change_gets_called(self):
|
||||
callback = self.playback.on_current_playlist_change
|
||||
def test_on_tracklist_change_gets_called(self):
|
||||
callback = self.playback.on_tracklist_change
|
||||
|
||||
def wrapper():
|
||||
wrapper.called = True
|
||||
return callback()
|
||||
wrapper.called = False
|
||||
|
||||
self.playback.on_current_playlist_change = wrapper
|
||||
self.current_playlist.append([Track()])
|
||||
self.playback.on_tracklist_change = wrapper
|
||||
self.tracklist.append([Track()])
|
||||
|
||||
self.assert_(wrapper.called)
|
||||
|
||||
@ -531,25 +533,25 @@ class PlaybackControllerTest(object):
|
||||
self.assertEqual('end_of_track', message['command'])
|
||||
|
||||
@populate_playlist
|
||||
def test_on_current_playlist_change_when_playing(self):
|
||||
def test_on_tracklist_change_when_playing(self):
|
||||
self.playback.play()
|
||||
current_track = self.playback.current_track
|
||||
self.current_playlist.append([self.tracks[2]])
|
||||
self.tracklist.append([self.tracks[2]])
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
self.assertEqual(self.playback.current_track, current_track)
|
||||
|
||||
@populate_playlist
|
||||
def test_on_current_playlist_change_when_stopped(self):
|
||||
self.current_playlist.append([self.tracks[2]])
|
||||
def test_on_tracklist_change_when_stopped(self):
|
||||
self.tracklist.append([self.tracks[2]])
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
self.assertEqual(self.playback.current_track, None)
|
||||
|
||||
@populate_playlist
|
||||
def test_on_current_playlist_change_when_paused(self):
|
||||
def test_on_tracklist_change_when_paused(self):
|
||||
self.playback.play()
|
||||
self.playback.pause()
|
||||
current_track = self.playback.current_track
|
||||
self.current_playlist.append([self.tracks[2]])
|
||||
self.tracklist.append([self.tracks[2]])
|
||||
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
|
||||
self.assertEqual(self.playback.current_track, current_track)
|
||||
|
||||
@ -640,7 +642,7 @@ class PlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_seek_when_playing_updates_position(self):
|
||||
length = self.current_playlist.tracks[0].length
|
||||
length = self.tracklist.tracks[0].length
|
||||
self.playback.play()
|
||||
self.playback.seek(length - 1000)
|
||||
position = self.playback.time_position
|
||||
@ -655,7 +657,7 @@ class PlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_seek_when_paused_updates_position(self):
|
||||
length = self.current_playlist.tracks[0].length
|
||||
length = self.tracklist.tracks[0].length
|
||||
self.playback.play()
|
||||
self.playback.pause()
|
||||
self.playback.seek(length - 1000)
|
||||
@ -685,8 +687,8 @@ class PlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_seek_beyond_end_of_song_for_last_track(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
self.playback.seek(self.current_playlist.tracks[-1].length * 100)
|
||||
self.playback.play(self.tracklist.tl_tracks[-1])
|
||||
self.playback.seek(self.tracklist.tracks[-1].length * 100)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@unittest.SkipTest
|
||||
@ -772,9 +774,9 @@ class PlaybackControllerTest(object):
|
||||
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
|
||||
self.playback.consume = True
|
||||
self.playback.play()
|
||||
for _ in range(len(self.current_playlist.tracks)):
|
||||
for _ in range(len(self.tracklist.tracks)):
|
||||
self.playback.on_end_of_track()
|
||||
self.assertEqual(len(self.current_playlist.tracks), 0)
|
||||
self.assertEqual(len(self.tracklist.tracks), 0)
|
||||
|
||||
@populate_playlist
|
||||
def test_play_with_random(self):
|
||||
@ -809,7 +811,7 @@ class PlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_playlist_stops(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
self.playback.play(self.tracklist.tl_tracks[-1])
|
||||
self.playback.on_end_of_track()
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
@ -11,7 +13,7 @@ from mopidy.models import Playlist
|
||||
from tests import unittest, path_to_data_dir
|
||||
|
||||
|
||||
class StoredPlaylistsControllerTest(object):
|
||||
class PlaylistsControllerTest(object):
|
||||
def setUp(self):
|
||||
settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
|
||||
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
|
||||
@ -20,7 +22,6 @@ class StoredPlaylistsControllerTest(object):
|
||||
self.audio = mock.Mock(spec=audio.Audio)
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core(backends=[self.backend])
|
||||
self.stored = self.core.stored_playlists
|
||||
|
||||
def tearDown(self):
|
||||
pykka.ActorRegistry.stop_all()
|
||||
@ -31,74 +32,74 @@ class StoredPlaylistsControllerTest(object):
|
||||
settings.runtime.clear()
|
||||
|
||||
def test_create_returns_playlist_with_name_set(self):
|
||||
playlist = self.stored.create(u'test')
|
||||
playlist = self.core.playlists.create('test')
|
||||
self.assertEqual(playlist.name, 'test')
|
||||
|
||||
def test_create_returns_playlist_with_uri_set(self):
|
||||
playlist = self.stored.create(u'test')
|
||||
playlist = self.core.playlists.create('test')
|
||||
self.assert_(playlist.uri)
|
||||
|
||||
def test_create_adds_playlist_to_playlists_collection(self):
|
||||
playlist = self.stored.create(u'test')
|
||||
self.assert_(self.stored.playlists)
|
||||
self.assertIn(playlist, self.stored.playlists)
|
||||
playlist = self.core.playlists.create('test')
|
||||
self.assert_(self.core.playlists.playlists)
|
||||
self.assertIn(playlist, self.core.playlists.playlists)
|
||||
|
||||
def test_playlists_empty_to_start_with(self):
|
||||
self.assert_(not self.stored.playlists)
|
||||
self.assert_(not self.core.playlists.playlists)
|
||||
|
||||
def test_delete_non_existant_playlist(self):
|
||||
self.stored.delete('file:///unknown/playlist')
|
||||
self.core.playlists.delete('file:///unknown/playlist')
|
||||
|
||||
def test_delete_playlist_removes_it_from_the_collection(self):
|
||||
playlist = self.stored.create(u'test')
|
||||
self.assertIn(playlist, self.stored.playlists)
|
||||
playlist = self.core.playlists.create('test')
|
||||
self.assertIn(playlist, self.core.playlists.playlists)
|
||||
|
||||
self.stored.delete(playlist.uri)
|
||||
self.core.playlists.delete(playlist.uri)
|
||||
|
||||
self.assertNotIn(playlist, self.stored.playlists)
|
||||
self.assertNotIn(playlist, self.core.playlists.playlists)
|
||||
|
||||
def test_get_without_criteria(self):
|
||||
test = self.stored.get
|
||||
test = self.core.playlists.get
|
||||
self.assertRaises(LookupError, test)
|
||||
|
||||
def test_get_with_wrong_cirteria(self):
|
||||
test = lambda: self.stored.get(name='foo')
|
||||
test = lambda: self.core.playlists.get(name='foo')
|
||||
self.assertRaises(LookupError, test)
|
||||
|
||||
def test_get_with_right_criteria(self):
|
||||
playlist1 = self.stored.create(u'test')
|
||||
playlist2 = self.stored.get(name='test')
|
||||
playlist1 = self.core.playlists.create('test')
|
||||
playlist2 = self.core.playlists.get(name='test')
|
||||
self.assertEqual(playlist1, playlist2)
|
||||
|
||||
def test_get_by_name_returns_unique_match(self):
|
||||
playlist = Playlist(name='b')
|
||||
self.backend.stored_playlists.playlists = [
|
||||
self.backend.playlists.playlists = [
|
||||
Playlist(name='a'), playlist]
|
||||
self.assertEqual(playlist, self.stored.get(name='b'))
|
||||
self.assertEqual(playlist, self.core.playlists.get(name='b'))
|
||||
|
||||
def test_get_by_name_returns_first_of_multiple_matches(self):
|
||||
playlist = Playlist(name='b')
|
||||
self.backend.stored_playlists.playlists = [
|
||||
self.backend.playlists.playlists = [
|
||||
playlist, Playlist(name='a'), Playlist(name='b')]
|
||||
try:
|
||||
self.stored.get(name='b')
|
||||
self.fail(u'Should raise LookupError if multiple matches')
|
||||
self.core.playlists.get(name='b')
|
||||
self.fail('Should raise LookupError if multiple matches')
|
||||
except LookupError as e:
|
||||
self.assertEqual(u'"name=b" match multiple playlists', e[0])
|
||||
self.assertEqual('"name=b" match multiple playlists', e[0])
|
||||
|
||||
def test_get_by_name_raises_keyerror_if_no_match(self):
|
||||
self.backend.stored_playlists.playlists = [
|
||||
self.backend.playlists.playlists = [
|
||||
Playlist(name='a'), Playlist(name='b')]
|
||||
try:
|
||||
self.stored.get(name='c')
|
||||
self.fail(u'Should raise LookupError if no match')
|
||||
self.core.playlists.get(name='c')
|
||||
self.fail('Should raise LookupError if no match')
|
||||
except LookupError as e:
|
||||
self.assertEqual(u'"name=c" match no playlists', e[0])
|
||||
self.assertEqual('"name=c" match no playlists', e[0])
|
||||
|
||||
def test_lookup_finds_playlist_by_uri(self):
|
||||
original_playlist = self.stored.create(u'test')
|
||||
original_playlist = self.core.playlists.create('test')
|
||||
|
||||
looked_up_playlist = self.stored.lookup(original_playlist.uri)
|
||||
looked_up_playlist = self.core.playlists.lookup(original_playlist.uri)
|
||||
|
||||
self.assertEqual(original_playlist, looked_up_playlist)
|
||||
|
||||
@ -106,14 +107,14 @@ class StoredPlaylistsControllerTest(object):
|
||||
def test_refresh(self):
|
||||
pass
|
||||
|
||||
def test_save_replaces_stored_playlist_with_updated_playlist(self):
|
||||
playlist1 = self.stored.create(u'test1')
|
||||
self.assertIn(playlist1, self.stored.playlists)
|
||||
def test_save_replaces_existing_playlist_with_updated_playlist(self):
|
||||
playlist1 = self.core.playlists.create('test1')
|
||||
self.assertIn(playlist1, self.core.playlists.playlists)
|
||||
|
||||
playlist2 = playlist1.copy(name=u'test2')
|
||||
playlist2 = self.stored.save(playlist2)
|
||||
self.assertNotIn(playlist1, self.stored.playlists)
|
||||
self.assertIn(playlist2, self.stored.playlists)
|
||||
playlist2 = playlist1.copy(name='test2')
|
||||
playlist2 = self.core.playlists.save(playlist2)
|
||||
self.assertNotIn(playlist1, self.core.playlists.playlists)
|
||||
self.assertIn(playlist2, self.core.playlists.playlists)
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_playlist_with_unknown_track(self):
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
import random
|
||||
|
||||
@ -5,19 +7,19 @@ import pykka
|
||||
|
||||
from mopidy import audio, core
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.models import CpTrack, Playlist, Track
|
||||
from mopidy.models import TlTrack, Playlist, Track
|
||||
|
||||
from tests.backends.base import populate_playlist
|
||||
|
||||
|
||||
class CurrentPlaylistControllerTest(object):
|
||||
class TracklistControllerTest(object):
|
||||
tracks = []
|
||||
|
||||
def setUp(self):
|
||||
self.audio = mock.Mock(spec=audio.Audio)
|
||||
self.backend = self.backend_class.start(audio=self.audio).proxy()
|
||||
self.core = core.Core(audio=audio, backends=[self.backend])
|
||||
self.controller = self.core.current_playlist
|
||||
self.controller = self.core.tracklist
|
||||
self.playback = self.core.playback
|
||||
|
||||
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
|
||||
@ -26,25 +28,25 @@ class CurrentPlaylistControllerTest(object):
|
||||
pykka.ActorRegistry.stop_all()
|
||||
|
||||
def test_length(self):
|
||||
self.assertEqual(0, len(self.controller.cp_tracks))
|
||||
self.assertEqual(0, len(self.controller.tl_tracks))
|
||||
self.assertEqual(0, self.controller.length)
|
||||
self.controller.append(self.tracks)
|
||||
self.assertEqual(3, len(self.controller.cp_tracks))
|
||||
self.assertEqual(3, len(self.controller.tl_tracks))
|
||||
self.assertEqual(3, self.controller.length)
|
||||
|
||||
def test_add(self):
|
||||
for track in self.tracks:
|
||||
cp_track = self.controller.add(track)
|
||||
tl_track = self.controller.add(track)
|
||||
self.assertEqual(track, self.controller.tracks[-1])
|
||||
self.assertEqual(cp_track, self.controller.cp_tracks[-1])
|
||||
self.assertEqual(track, cp_track.track)
|
||||
self.assertEqual(tl_track, self.controller.tl_tracks[-1])
|
||||
self.assertEqual(track, tl_track.track)
|
||||
|
||||
def test_add_at_position(self):
|
||||
for track in self.tracks[:-1]:
|
||||
cp_track = self.controller.add(track, 0)
|
||||
tl_track = self.controller.add(track, 0)
|
||||
self.assertEqual(track, self.controller.tracks[0])
|
||||
self.assertEqual(cp_track, self.controller.cp_tracks[0])
|
||||
self.assertEqual(track, cp_track.track)
|
||||
self.assertEqual(tl_track, self.controller.tl_tracks[0])
|
||||
self.assertEqual(track, tl_track.track)
|
||||
|
||||
@populate_playlist
|
||||
def test_add_at_position_outside_of_playlist(self):
|
||||
@ -53,14 +55,14 @@ class CurrentPlaylistControllerTest(object):
|
||||
self.assertRaises(AssertionError, test)
|
||||
|
||||
@populate_playlist
|
||||
def test_get_by_cpid(self):
|
||||
cp_track = self.controller.cp_tracks[1]
|
||||
self.assertEqual(cp_track, self.controller.get(cpid=cp_track.cpid))
|
||||
def test_get_by_tlid(self):
|
||||
tl_track = self.controller.tl_tracks[1]
|
||||
self.assertEqual(tl_track, self.controller.get(tlid=tl_track.tlid))
|
||||
|
||||
@populate_playlist
|
||||
def test_get_by_uri(self):
|
||||
cp_track = self.controller.cp_tracks[1]
|
||||
self.assertEqual(cp_track, self.controller.get(uri=cp_track.track.uri))
|
||||
tl_track = self.controller.tl_tracks[1]
|
||||
self.assertEqual(tl_track, self.controller.get(uri=tl_track.track.uri))
|
||||
|
||||
@populate_playlist
|
||||
def test_get_by_uri_raises_error_for_invalid_uri(self):
|
||||
@ -93,18 +95,18 @@ class CurrentPlaylistControllerTest(object):
|
||||
self.controller.append([Track(uri='z'), track, track])
|
||||
try:
|
||||
self.controller.get(uri='a')
|
||||
self.fail(u'Should raise LookupError if multiple matches')
|
||||
self.fail('Should raise LookupError if multiple matches')
|
||||
except LookupError as e:
|
||||
self.assertEqual(u'"uri=a" match multiple tracks', e[0])
|
||||
self.assertEqual('"uri=a" match multiple tracks', e[0])
|
||||
|
||||
def test_get_by_uri_raises_error_if_no_match(self):
|
||||
self.controller.playlist = Playlist(
|
||||
tracks=[Track(uri='z'), Track(uri='y')])
|
||||
try:
|
||||
self.controller.get(uri='a')
|
||||
self.fail(u'Should raise LookupError if no match')
|
||||
self.fail('Should raise LookupError if no match')
|
||||
except LookupError as e:
|
||||
self.assertEqual(u'"uri=a" match no tracks', e[0])
|
||||
self.assertEqual('"uri=a" match no tracks', e[0])
|
||||
|
||||
def test_get_by_multiple_criteria_returns_elements_matching_all(self):
|
||||
track1 = Track(uri='a', name='x')
|
||||
@ -122,7 +124,7 @@ class CurrentPlaylistControllerTest(object):
|
||||
self.controller.append([track1, track2, track3])
|
||||
self.assertEqual(track2, self.controller.get(uri='b')[1])
|
||||
|
||||
def test_append_appends_to_the_current_playlist(self):
|
||||
def test_append_appends_to_the_tracklist(self):
|
||||
self.controller.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(len(self.controller.tracks), 2)
|
||||
self.controller.append([Track(uri='c'), Track(uri='d')])
|
||||
@ -152,15 +154,15 @@ class CurrentPlaylistControllerTest(object):
|
||||
self.assertEqual(self.playback.current_track, None)
|
||||
|
||||
def test_index_returns_index_of_track(self):
|
||||
cp_tracks = []
|
||||
tl_tracks = []
|
||||
for track in self.tracks:
|
||||
cp_tracks.append(self.controller.add(track))
|
||||
self.assertEquals(0, self.controller.index(cp_tracks[0]))
|
||||
self.assertEquals(1, self.controller.index(cp_tracks[1]))
|
||||
self.assertEquals(2, self.controller.index(cp_tracks[2]))
|
||||
tl_tracks.append(self.controller.add(track))
|
||||
self.assertEquals(0, self.controller.index(tl_tracks[0]))
|
||||
self.assertEquals(1, self.controller.index(tl_tracks[1]))
|
||||
self.assertEquals(2, self.controller.index(tl_tracks[2]))
|
||||
|
||||
def test_index_raises_value_error_if_item_not_found(self):
|
||||
test = lambda: self.controller.index(CpTrack(0, Track()))
|
||||
test = lambda: self.controller.index(TlTrack(0, Track()))
|
||||
self.assertRaises(ValueError, test)
|
||||
|
||||
@populate_playlist
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
import pykka
|
||||
|
||||
@ -19,14 +21,14 @@ class BackendEventsTest(unittest.TestCase):
|
||||
pykka.ActorRegistry.stop_all()
|
||||
|
||||
def test_pause_sends_track_playback_paused_event(self, send):
|
||||
self.core.current_playlist.add(Track(uri='dummy:a'))
|
||||
self.core.tracklist.add(Track(uri='dummy:a'))
|
||||
self.core.playback.play().get()
|
||||
send.reset_mock()
|
||||
self.core.playback.pause().get()
|
||||
self.assertEqual(send.call_args[0][0], 'track_playback_paused')
|
||||
|
||||
def test_resume_sends_track_playback_resumed(self, send):
|
||||
self.core.current_playlist.add(Track(uri='dummy:a'))
|
||||
self.core.tracklist.add(Track(uri='dummy:a'))
|
||||
self.core.playback.play()
|
||||
self.core.playback.pause().get()
|
||||
send.reset_mock()
|
||||
@ -34,20 +36,20 @@ class BackendEventsTest(unittest.TestCase):
|
||||
self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
|
||||
|
||||
def test_play_sends_track_playback_started_event(self, send):
|
||||
self.core.current_playlist.add(Track(uri='dummy:a'))
|
||||
self.core.tracklist.add(Track(uri='dummy:a'))
|
||||
send.reset_mock()
|
||||
self.core.playback.play().get()
|
||||
self.assertEqual(send.call_args[0][0], 'track_playback_started')
|
||||
|
||||
def test_stop_sends_track_playback_ended_event(self, send):
|
||||
self.core.current_playlist.add(Track(uri='dummy:a'))
|
||||
self.core.tracklist.add(Track(uri='dummy:a'))
|
||||
self.core.playback.play().get()
|
||||
send.reset_mock()
|
||||
self.core.playback.stop().get()
|
||||
self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
|
||||
|
||||
def test_seek_sends_seeked_event(self, send):
|
||||
self.core.current_playlist.add(Track(uri='dummy:a', length=40000))
|
||||
self.core.tracklist.add(Track(uri='dummy:a', length=40000))
|
||||
self.core.playback.play().get()
|
||||
send.reset_mock()
|
||||
self.core.playback.seek(1000).get()
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
|
||||
song = path_to_data_dir('song%s.wav')
|
||||
generate_song = lambda i: path_to_uri(song % i)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.local import LocalBackend
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.local import LocalBackend
|
||||
from mopidy.core import PlaybackState
|
||||
@ -25,7 +27,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
|
||||
def add_track(self, path):
|
||||
uri = path_to_uri(path_to_data_dir(path))
|
||||
track = Track(uri=uri, length=4464)
|
||||
self.current_playlist.add(track)
|
||||
self.tracklist.add(track)
|
||||
|
||||
def test_uri_scheme(self):
|
||||
self.assertIn('file', self.core.uri_schemes)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from mopidy import settings
|
||||
@ -6,13 +8,13 @@ from mopidy.models import Track
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import unittest, path_to_data_dir
|
||||
from tests.backends.base.stored_playlists import (
|
||||
StoredPlaylistsControllerTest)
|
||||
from tests.backends.base.playlists import (
|
||||
PlaylistsControllerTest)
|
||||
from tests.backends.local import generate_song
|
||||
|
||||
|
||||
class LocalStoredPlaylistsControllerTest(
|
||||
StoredPlaylistsControllerTest, unittest.TestCase):
|
||||
class LocalPlaylistsControllerTest(
|
||||
PlaylistsControllerTest, unittest.TestCase):
|
||||
|
||||
backend_class = LocalBackend
|
||||
|
||||
@ -20,38 +22,38 @@ class LocalStoredPlaylistsControllerTest(
|
||||
path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
self.stored.create(u'test')
|
||||
self.core.playlists.create('test')
|
||||
self.assertTrue(os.path.exists(path))
|
||||
|
||||
def test_create_slugifies_playlist_name(self):
|
||||
path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u')
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
playlist = self.stored.create(u'test FOO baR')
|
||||
self.assertEqual(u'test-foo-bar', playlist.name)
|
||||
playlist = self.core.playlists.create('test FOO baR')
|
||||
self.assertEqual('test-foo-bar', playlist.name)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
|
||||
def test_create_slugifies_names_which_tries_to_change_directory(self):
|
||||
path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u')
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
playlist = self.stored.create(u'../../test FOO baR')
|
||||
self.assertEqual(u'test-foo-bar', playlist.name)
|
||||
playlist = self.core.playlists.create('../../test FOO baR')
|
||||
self.assertEqual('test-foo-bar', playlist.name)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
|
||||
def test_saved_playlist_is_persisted(self):
|
||||
path1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test1.m3u')
|
||||
path2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2-foo-bar.m3u')
|
||||
|
||||
playlist = self.stored.create(u'test1')
|
||||
playlist = self.core.playlists.create('test1')
|
||||
|
||||
self.assertTrue(os.path.exists(path1))
|
||||
self.assertFalse(os.path.exists(path2))
|
||||
|
||||
playlist = playlist.copy(name=u'test2 FOO baR')
|
||||
playlist = self.stored.save(playlist)
|
||||
playlist = playlist.copy(name='test2 FOO baR')
|
||||
playlist = self.core.playlists.save(playlist)
|
||||
|
||||
self.assertEqual(u'test2-foo-bar', playlist.name)
|
||||
self.assertEqual('test2-foo-bar', playlist.name)
|
||||
self.assertFalse(os.path.exists(path1))
|
||||
self.assertTrue(os.path.exists(path2))
|
||||
|
||||
@ -59,19 +61,19 @@ class LocalStoredPlaylistsControllerTest(
|
||||
path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
playlist = self.stored.create(u'test')
|
||||
playlist = self.core.playlists.create('test')
|
||||
self.assertTrue(os.path.exists(path))
|
||||
|
||||
self.stored.delete(playlist.uri)
|
||||
self.core.playlists.delete(playlist.uri)
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
def test_playlist_contents_is_written_to_disk(self):
|
||||
track = Track(uri=generate_song(1))
|
||||
track_path = track.uri[len('file://'):]
|
||||
playlist = self.stored.create(u'test')
|
||||
playlist = self.core.playlists.create('test')
|
||||
playlist_path = playlist.uri[len('file://'):]
|
||||
playlist = playlist.copy(tracks=[track])
|
||||
playlist = self.stored.save(playlist)
|
||||
playlist = self.core.playlists.save(playlist)
|
||||
|
||||
with open(playlist_path) as playlist_file:
|
||||
contents = playlist_file.read()
|
||||
@ -82,20 +84,20 @@ class LocalStoredPlaylistsControllerTest(
|
||||
playlist_path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
|
||||
|
||||
track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
|
||||
playlist = self.stored.create(u'test')
|
||||
playlist = self.core.playlists.create('test')
|
||||
playlist = playlist.copy(tracks=[track])
|
||||
playlist = self.stored.save(playlist)
|
||||
playlist = self.core.playlists.save(playlist)
|
||||
|
||||
backend = self.backend_class(audio=self.audio)
|
||||
|
||||
self.assert_(backend.stored_playlists.playlists)
|
||||
self.assert_(backend.playlists.playlists)
|
||||
self.assertEqual(
|
||||
path_to_uri(playlist_path),
|
||||
backend.stored_playlists.playlists[0].uri)
|
||||
backend.playlists.playlists[0].uri)
|
||||
self.assertEqual(
|
||||
playlist.name, backend.stored_playlists.playlists[0].name)
|
||||
playlist.name, backend.playlists.playlists[0].name)
|
||||
self.assertEqual(
|
||||
track.uri, backend.stored_playlists.playlists[0].tracks[0].uri)
|
||||
track.uri, backend.playlists.playlists[0].tracks[0].uri)
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_santitising_of_playlist_filenames(self):
|
||||
@ -1,23 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.local import LocalBackend
|
||||
from mopidy.models import Track
|
||||
|
||||
from tests import unittest
|
||||
from tests.backends.base.current_playlist import CurrentPlaylistControllerTest
|
||||
from tests.backends.base.tracklist import TracklistControllerTest
|
||||
from tests.backends.local import generate_song
|
||||
|
||||
|
||||
class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest,
|
||||
unittest.TestCase):
|
||||
|
||||
class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase):
|
||||
backend_class = LocalBackend
|
||||
tracks = [
|
||||
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
|
||||
|
||||
def setUp(self):
|
||||
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
|
||||
super(LocalCurrentPlaylistControllerTest, self).setUp()
|
||||
super(LocalTracklistControllerTest, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super(LocalCurrentPlaylistControllerTest, self).tearDown()
|
||||
super(LocalTracklistControllerTest, self).tearDown()
|
||||
settings.runtime.clear()
|
||||
@ -1,5 +1,7 @@
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
@ -12,7 +14,7 @@ from tests import unittest, path_to_data_dir
|
||||
data_dir = path_to_data_dir('')
|
||||
song1_path = path_to_data_dir('song1.mp3')
|
||||
song2_path = path_to_data_dir('song2.mp3')
|
||||
encoded_path = path_to_data_dir(u'æøå.mp3')
|
||||
encoded_path = path_to_data_dir('æøå.mp3')
|
||||
song1_uri = path_to_uri(song1_path)
|
||||
song2_uri = path_to_uri(song2_path)
|
||||
encoded_uri = path_to_uri(encoded_path)
|
||||
@ -138,10 +140,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
|
||||
path_to_data_dir('utf8_tag_cache'), path_to_data_dir(''))
|
||||
|
||||
uri = path_to_uri(path_to_data_dir('song1.mp3'))
|
||||
artists = [Artist(name=u'æøå')]
|
||||
album = Album(name=u'æøå', artists=artists)
|
||||
artists = [Artist(name='æøå')]
|
||||
album = Album(name='æøå', artists=artists)
|
||||
track = Track(
|
||||
uri=uri, name=u'æøå', artists=artists, album=album, length=4000)
|
||||
uri=uri, name='æøå', artists=artists, album=album, length=4000)
|
||||
|
||||
self.assertEqual(track, list(tracks)[0])
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
from __future__ import unicode_literals
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
import pykka
|
||||
|
||||
@ -26,8 +28,8 @@ class CoreActorTest(unittest.TestCase):
|
||||
self.assertIn('dummy2', result)
|
||||
|
||||
def test_backends_with_colliding_uri_schemes_fails(self):
|
||||
self.backend1.__class__.__name__ = 'B1'
|
||||
self.backend2.__class__.__name__ = 'B2'
|
||||
self.backend1.__class__.__name__ = b'B1'
|
||||
self.backend2.__class__.__name__ = b'B2'
|
||||
self.backend2.uri_schemes.get.return_value = ['dummy1', 'dummy2']
|
||||
self.assertRaisesRegexp(
|
||||
AssertionError,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user