Merge branch 'develop' into feature/http-frontend

This commit is contained in:
Stein Magnus Jodal 2012-11-14 09:31:52 +01:00
commit e1ef73f517
147 changed files with 2636 additions and 2113 deletions

View File

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

View File

@ -19,10 +19,10 @@ Playback provider
:members:
Stored playlists provider
=========================
Playlists provider
==================
.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider
.. autoclass:: mopidy.backends.base.BasePlaylistsProvider
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
# flake8: noqa
from .actor import Audio
from .listener import AudioListener

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import pykka

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import logging
import time

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import pykka

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

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

View File

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

View File

@ -21,5 +21,7 @@ Make sure :attr:`mopidy.settings.FRONTENDS` includes
frontend.
"""
from __future__ import unicode_literals
# flake8: noqa
from .actor import MpdFrontend

View File

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

View File

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

View File

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

View File

@ -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' % (

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from mopidy.frontends.mpd.protocol import handle_request

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented

View File

@ -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>[^"]+)"$')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import os
import platform
import sys

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import locale

View File

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

View File

@ -1,6 +1,9 @@
from __future__ import unicode_literals
import logging
import sys
logger = logging.getLogger('mopidy.utils')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from subprocess import PIPE, Popen
from mopidy import __version__

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import os
import sys

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import nose
import yappi

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from mopidy import audio, settings
from mopidy.utils.path import path_to_uri

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from mopidy import settings
from mopidy.backends.local import LocalBackend

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

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