From 30ea4860a7d1381e61e0de5aeebd80850c8d986a Mon Sep 17 00:00:00 2001 From: Janez Troha Date: Wed, 1 May 2013 14:12:49 +0300 Subject: [PATCH 001/111] Add mention of mopidy-convert-config --- docs/config.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 9b3291c6..856df13a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -37,6 +37,12 @@ below, together with their default values. In addition, all :ref:`extensions ` got additional config values. The extension's config values and config defaults are documented on the :ref:`extension pages `. +Migrating from pre 0.14 +----------------------- + +For those users upgrading from versions prior to 0.14 we made :option:`mopidy-convert-config` tool, +to ease proces of migrating settings to new config format. + Default core configuration ========================== From 9ad88f712d4872f152e1f55695dedea694a74417 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 1 May 2013 15:07:56 +0200 Subject: [PATCH 002/111] docs: Fix typo and level of header --- docs/config.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 856df13a..233ef26c 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -37,11 +37,13 @@ below, together with their default values. In addition, all :ref:`extensions ` got additional config values. The extension's config values and config defaults are documented on the :ref:`extension pages `. -Migrating from pre 0.14 ------------------------ -For those users upgrading from versions prior to 0.14 we made :option:`mopidy-convert-config` tool, -to ease proces of migrating settings to new config format. +Migrating from pre 0.14 +======================= + +For those users upgrading from versions prior to 0.14 we made +the :option:`mopidy-convert-config` tool, to ease the process of migrating +settings to the new config format. Default core configuration From db956ee1a98a19fe7136749b908aa232db5454d1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 1 May 2013 20:51:17 +0200 Subject: [PATCH 003/111] docs: Use 'config' more consistently --- docs/config.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 233ef26c..ff351b4a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -17,11 +17,11 @@ as simple as this: Mopidy primarily reads config from the file ``~/.config/mopidy/mopidy.conf``, where ``~`` means your *home directory*. If your username is ``alice`` and you -are running Linux, the settings file should probably be at +are running Linux, the config file should probably be at ``/home/alice/.config/mopidy/mopidy.conf``. You can either create the configuration file yourself, or run the ``mopidy`` command, and it will create -an empty settings file for you and print what config values must be set -to successfully start Mopidy. +an empty config file for you and print what config values must be set to +successfully start Mopidy. When you have created the configuration file, open it in a text editor, and add the config values you want to change. If you want to keep the default for a @@ -109,8 +109,8 @@ Core configuration values .. confval:: logging/config_file - Config file that overrides all logging settings, see `the Python logging - docs`_ for details. + Config file that overrides all logging config values, see `the Python + logging docs`_ for details. .. confval:: loglevels/* @@ -212,9 +212,9 @@ can use with the ``gst-launch-0.10`` command can be plugged into New configuration values ------------------------ -Mopidy's settings validator will stop you from defining any config values in -your settings file that Mopidy doesn't know about. This may sound obnoxious, -but it helps us detect typos in your settings, and deprecated settings that +Mopidy's config validator will stop you from defining any config values in +your config file that Mopidy doesn't know about. This may sound obnoxious, +but it helps us detect typos in your config, and deprecated config values that should be removed or updated. If you're extending Mopidy, and want to use Mopidy's configuration From 30ab6606278b639c5c7eecb74899d6e7cf6b9ca3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 1 May 2013 21:43:40 +0200 Subject: [PATCH 004/111] docs: Remove mixers' module docs --- docs/modules/audio/mixers/auto.rst | 6 ------ docs/modules/audio/mixers/fake.rst | 6 ------ mopidy/audio/mixers/auto.py | 10 ---------- mopidy/audio/mixers/fake.py | 9 --------- 4 files changed, 31 deletions(-) delete mode 100644 docs/modules/audio/mixers/auto.rst delete mode 100644 docs/modules/audio/mixers/fake.rst diff --git a/docs/modules/audio/mixers/auto.rst b/docs/modules/audio/mixers/auto.rst deleted file mode 100644 index caf6e3ab..00000000 --- a/docs/modules/audio/mixers/auto.rst +++ /dev/null @@ -1,6 +0,0 @@ -********************************************* -:mod:`mopidy.audio.mixers.auto` -- Auto mixer -********************************************* - -.. automodule:: mopidy.audio.mixers.auto - :synopsis: Mixer element which automatically selects the real mixer to use diff --git a/docs/modules/audio/mixers/fake.rst b/docs/modules/audio/mixers/fake.rst deleted file mode 100644 index dcab7767..00000000 --- a/docs/modules/audio/mixers/fake.rst +++ /dev/null @@ -1,6 +0,0 @@ -********************************************* -:mod:`mopidy.audio.mixers.fake` -- Fake mixer -********************************************* - -.. automodule:: mopidy.audio.mixers.fake - :synopsis: Fake mixer for use in tests diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py index 6b76e1a1..587994cb 100644 --- a/mopidy/audio/mixers/auto.py +++ b/mopidy/audio/mixers/auto.py @@ -2,16 +2,6 @@ This is Mopidy's default mixer. - -Dependencies -============ - -None - - -Configuration -============= - If this wasn't the default, you would set the :confval:`audio/mixer` config value to ``autoaudiomixer`` to use this mixer. """ diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py index 589610ce..7daea6a1 100644 --- a/mopidy/audio/mixers/fake.py +++ b/mopidy/audio/mixers/fake.py @@ -1,14 +1,5 @@ """Fake mixer for use in tests. -Dependencies -============ - -None - - -Configuration -============= - Set the :confval:`audio/mixer:` config value to ``fakemixer`` to use this mixer. """ From 7560df945eeae8e4feffac5256ab8f709e69869a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 1 May 2013 21:44:02 +0200 Subject: [PATCH 005/111] docs: Add section on extension config and disabling --- docs/config.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index ff351b4a..1710d42d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -139,6 +139,24 @@ Core configuration values http://docs.python.org/2/library/logging.config.html +Extension configuration +======================= + +Mopidy's extensions have their own config values that you may want to tweak. +For the available config values, please refer to the docs for each extension. +Most, if not all, can be found at :ref:`ext`. + +Mopidy extensions are enabled by default when they are installed. If you want +to disable an extension without uninstalling it, all extensions support the +``enabled`` config value even if it isn't explicitly documented by all +extensions. If the ``enabled`` config value is set to ``false`` the extension +will not be started. For example, to disable the Spotify extension, add the +following to your ``mopidy.conf``:: + + [spotify] + enabled = false + + Advanced configurations ======================= From 7ed7cc13eef8ee13ded10de035069194de0762ba Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 2 May 2013 23:13:08 +0200 Subject: [PATCH 006/111] docs: Add glossary (fixes #430) --- docs/ext/scrobbler.rst | 2 ++ docs/glossary.rst | 37 +++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 3 files changed, 40 insertions(+) create mode 100644 docs/glossary.rst diff --git a/docs/ext/scrobbler.rst b/docs/ext/scrobbler.rst index a0496b37..84188d02 100644 --- a/docs/ext/scrobbler.rst +++ b/docs/ext/scrobbler.rst @@ -1,3 +1,5 @@ +.. _ext-scrobbler: + **************** Mopidy-Scrobbler **************** diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 00000000..2aa63887 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,37 @@ +******** +Glossary +******** + +.. glossary:: + + backend + A part of Mopidy providing music library, playlist storage and/or + playback capability to the :term:`core`. Mopidy have a backend for each + music store or music service it supports. See :ref:`backend-api` for + details. + + core + The part of Mopidy that makes multiple frontends capable of using + multiple backends. The core module is also the owner of the + :term:`tracklist`. To use the core module, see :ref:`core-api`. + + extension + A Python package that can extend Mopidy with on or more + :term:`backends `, :term:`frontends `, or GStreamer + elements like :term:`mixers `. See :ref:`ext` for a list of + existing extensions and :ref:`extensiondev` for how to make a new + extension. + + frontend + A part of Mopidy *using* the :term:`core` API. Existing frontends + include the :ref:`MPD server `, the :ref:`MPRIS/D-Bus + integration `, the :ref:`Last.fm scrobbler `, + and the :ref:`HTTP server ` with JavaScript API. See + :ref:`frontend-api` for details. + + mixer + A GStreamer element that controls audio volume. + + tracklist + Mopidy's name for the play queue or current playlist. The name is + inspired by the MPRIS specification. diff --git a/docs/index.rst b/docs/index.rst index 199ba31c..fb91244d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -71,6 +71,7 @@ Reference .. toctree:: :maxdepth: 2 + glossary api/index modules/index From b2a0b58943707227509cfd87381c5ba0c0420082 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 11 May 2013 00:57:49 +0200 Subject: [PATCH 007/111] docs: Add how to get good sound on raspis (fixes: #299) --- docs/installation/raspberrypi.rst | 62 +++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index c5a4eae8..084dc977 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -57,7 +57,7 @@ How to for Debian 7 (Wheezy) #. Since I have a HDMI cable connected, but want the sound on the analog sound connector, I have to run:: - amixer cset numid=3 1 + sudo amixer cset numid=3 1 to force it to use analog output. ``1`` means analog, ``0`` means auto, and is the default, while ``2`` means HDMI. You can test sound output @@ -75,6 +75,60 @@ How to for Debian 7 (Wheezy) Fixing audio quality issues =========================== -As of January 2013, some reports also indicate that pushing the audio through -PulseAudio may help. We hope to, in the future, provide a complete set of -instructions here leading to acceptable analog audio quality. +As of about April 2013 the following steps should resolve any audio +issues for HDMI and analog without the use of an external USB sound +card. + +#. Ensure you system is up to date, for debian based systems run:: + + sudo apt-get update + sudo apt-get full-upgrade + +#. Ensure you have a new enough firmware, on debian based systems + `rpi-update `_ + can be used. + +#. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the + following:: + + pcm.!default { + type hw + card 0 + } + ctl.!default { + type hw + card 0 + } + + Note that if you have an ``asoundrc`` it will overide + any global settings. + +#. Update your ``~/.config/mopidy/mopidy.conf`` to contain:: + + [audio] + output = alsasink + + This is to tell GStreamer not to pick Jack which it seems to like + picking on RaspberryPis for some reason. + + +Following these steps you should be able to get crackle free sound on +either HDMI or analog. Note that you might need to ensure that pulse +is no longer running to get this working nicely. + +This recipe has been confirmed as working by a number of users on our +bug tracker and irc. As a reference, the following versions where used +for testing this, however all newer and some older version are likely +to work as we have not determined the exact revision that fixed this:: + + $ uname -a + Linux raspberrypi 3.6.11+ #408 PREEMPT Wed Apr 10 20:33:39 BST 2013 armv6l GNU/Linux + + $ /opt/vc/bin/vcgencmd version + Apr 25 2013 01:07:36 + Copyright (c) 2012 Broadcom + version 386589 (release) + +The only remaining known issues is a slight gap in playback at track +changes this is likely due to gapless playback not being implemented +and is being worked on irrespective of raspi related work. From 346ede73e502f3610128ac9d5821729ddf1820d9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 May 2013 18:40:08 +0200 Subject: [PATCH 008/111] docs: Review Raspi sound quality howto --- docs/installation/raspberrypi.rst | 45 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index 084dc977..acb37b36 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -79,12 +79,12 @@ As of about April 2013 the following steps should resolve any audio issues for HDMI and analog without the use of an external USB sound card. -#. Ensure you system is up to date, for debian based systems run:: +#. Ensure your system is up to date. On Debian based systems run:: sudo apt-get update sudo apt-get full-upgrade -#. Ensure you have a new enough firmware, on debian based systems +#. Ensure you have a new enough firmware. On Debian based systems `rpi-update `_ can be used. @@ -100,35 +100,34 @@ card. card 0 } - Note that if you have an ``asoundrc`` it will overide - any global settings. + Note that if you have an ``~-/.asoundrc`` it will overide any global + settings from ``/etc/asound.conf``. #. Update your ``~/.config/mopidy/mopidy.conf`` to contain:: [audio] output = alsasink - This is to tell GStreamer not to pick Jack which it seems to like - picking on RaspberryPis for some reason. + This is to tell GStreamer not to pick Jack which it seems to like picking on + Raspberry Pis for some reason. +Following these steps you should be able to get crackle free sound on either +HDMI or analog. Note that you might need to ensure that PulseAudio is no longer +running to get this working nicely. -Following these steps you should be able to get crackle free sound on -either HDMI or analog. Note that you might need to ensure that pulse -is no longer running to get this working nicely. +This recipe has been confirmed as working by a number of users on our issue +tracker and IRC. As a reference, the following versions where used for testing +this, however all newer and some older version are likely to work as we have +not determined the exact revision that fixed this:: -This recipe has been confirmed as working by a number of users on our -bug tracker and irc. As a reference, the following versions where used -for testing this, however all newer and some older version are likely -to work as we have not determined the exact revision that fixed this:: + $ uname -a + Linux raspberrypi 3.6.11+ #408 PREEMPT Wed Apr 10 20:33:39 BST 2013 armv6l GNU/Linux - $ uname -a - Linux raspberrypi 3.6.11+ #408 PREEMPT Wed Apr 10 20:33:39 BST 2013 armv6l GNU/Linux + $ /opt/vc/bin/vcgencmd version + Apr 25 2013 01:07:36 + Copyright (c) 2012 Broadcom + version 386589 (release) - $ /opt/vc/bin/vcgencmd version - Apr 25 2013 01:07:36 - Copyright (c) 2012 Broadcom - version 386589 (release) - -The only remaining known issues is a slight gap in playback at track -changes this is likely due to gapless playback not being implemented -and is being worked on irrespective of raspi related work. +The only remaining known issue is a slight gap in playback at track changes +this is likely due to gapless playback not being implemented and is being +worked on irrespective of Raspberry Pi related work. From ce111f6bcdb01f5a91b5fa3f4b5ee9db60644aa5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 12 May 2013 21:46:46 +0200 Subject: [PATCH 009/111] docs: Fix typo --- docs/installation/raspberrypi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index acb37b36..cc46c8b1 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -100,7 +100,7 @@ card. card 0 } - Note that if you have an ``~-/.asoundrc`` it will overide any global + Note that if you have an ``~/.asoundrc`` it will overide any global settings from ``/etc/asound.conf``. #. Update your ``~/.config/mopidy/mopidy.conf`` to contain:: From 31ef29e1216a856a3ce72551516ef61ea4af7701 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Jun 2013 14:59:37 +0200 Subject: [PATCH 010/111] docs: Fix dist-upgrade command (fixes #470) --- docs/installation/raspberrypi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index cc46c8b1..27aa5caf 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -82,7 +82,7 @@ card. #. Ensure your system is up to date. On Debian based systems run:: sudo apt-get update - sudo apt-get full-upgrade + sudo apt-get dist-upgrade #. Ensure you have a new enough firmware. On Debian based systems `rpi-update `_ From 2a43166b50c9b9c192b6f51c6876daaa239de515 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Jun 2013 14:59:37 +0200 Subject: [PATCH 011/111] docs: Fix dist-upgrade command (fixes #470) (cherry picked from commit 31ef29e1216a856a3ce72551516ef61ea4af7701) --- docs/installation/raspberrypi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index cc46c8b1..27aa5caf 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -82,7 +82,7 @@ card. #. Ensure your system is up to date. On Debian based systems run:: sudo apt-get update - sudo apt-get full-upgrade + sudo apt-get dist-upgrade #. Ensure you have a new enough firmware. On Debian based systems `rpi-update `_ From 3a8ca3a457b60425b17fc47272540461427fa388 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 19:43:33 +0200 Subject: [PATCH 012/111] reqs: Remove external mixers deps, as it was split out to its own package --- requirements/external_mixers.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 requirements/external_mixers.txt diff --git a/requirements/external_mixers.txt b/requirements/external_mixers.txt deleted file mode 100644 index 20cb7864..00000000 --- a/requirements/external_mixers.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyserial -# Available as python-serial in Debian/Ubuntu From 12f81773ec7bc04523460a2b8155113f1beee0e3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 19:43:50 +0200 Subject: [PATCH 013/111] reqs: python-ws4py is available in Ubuntu 13.04 and Debian testing --- requirements/http.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements/http.txt b/requirements/http.txt index aea7c1a8..f38bfa3c 100644 --- a/requirements/http.txt +++ b/requirements/http.txt @@ -2,4 +2,5 @@ cherrypy >= 3.2.2 # Available as python-cherrypy3 in Debian/Ubuntu ws4py >= 0.2.3 -# Available as python-ws4py from apt.mopidy.com +# Available as python-ws4py in newer Debian/Ubuntu and from apt.mopidy.com for +# older releases of Debian/Ubuntu From c00a2e0de8df800beb4d63c1b3cd685df5dab2f3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 19:45:59 +0200 Subject: [PATCH 014/111] reqs: unittest2 not required when on Python 2.7 --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 8e02d53d..dfc617a1 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ setup( tests_require=[ 'nose', 'mock >= 1.0', - 'unittest2', ], entry_points={ 'console_scripts': [ From 9a9eff3c62754db04e48601fa8825fcf3fbe0d79 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 19:46:47 +0200 Subject: [PATCH 015/111] setup: Remove Python 2.6 PyPI classifier --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index dfc617a1..6c2599cf 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,6 @@ setup( 'License :: OSI Approved :: Apache Software License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Multimedia :: Sound/Audio :: Players', ], From 04a9c69e48943180fcafc45dacd9fe628261ab00 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 20:02:08 +0200 Subject: [PATCH 016/111] fab: Add more targets, remove nose config file --- fabfile.py | 36 +++++++++++++++++++++++++++++++++--- setup.cfg | 6 ------ 2 files changed, 33 insertions(+), 9 deletions(-) delete mode 100644 setup.cfg diff --git a/fabfile.py b/fabfile.py index 3321cb16..de2cf7bb 100644 --- a/fabfile.py +++ b/fabfile.py @@ -1,21 +1,51 @@ -from fabric.api import local, settings +from fabric.api import execute, local, settings, task +@task +def docs(): + local('make -C docs/ html') + + +@task +def autodocs(): + auto(docs) + + +@task def test(path=None): path = path or 'tests/' local('nosetests ' + path) +@task def autotest(path=None): + auto(test, path=path) + + +@task +def coverage(path=None): + path = path or 'tests/' + local( + 'nosetests --with-coverage --cover-package=mopidy ' + '--cover-branches --cover-html ' + path) + + +@task +def autocoverage(path=None): + auto(coverage, path=path) + + +def auto(task, *args, **kwargs): while True: local('clear') with settings(warn_only=True): - test(path) + execute(task, *args, **kwargs) local( 'inotifywait -q -e create -e modify -e delete ' - '--exclude ".*\.(pyc|sw.)" -r mopidy/ tests/') + '--exclude ".*\.(pyc|sw.)" -r docs/ mopidy/ tests/') +@task def update_authors(): # Keep authors in the order of appearance and use awk to filter out dupes local( diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index bce0a6e2..00000000 --- a/setup.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[nosetests] -verbosity = 1 -#with-coverage = 1 -cover-package = mopidy -cover-inclusive = 1 -cover-html = 1 From 77b461d1e748cdfb9542e49e0d31349449d093e8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 20:02:31 +0200 Subject: [PATCH 017/111] docs: Update authors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 87925152..1d62b108 100644 --- a/AUTHORS +++ b/AUTHORS @@ -19,3 +19,4 @@ - Nick Steel - Zan Dobersek - Thomas Refis +- Janez Troha From ed196e05f6673ff7fc2c0289f82782ec8237e90c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 20:06:24 +0200 Subject: [PATCH 018/111] docs: Unbreak docs building by mocking dbus.String properly --- docs/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index a71c9a61..c0dba916 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,7 +33,10 @@ class Mock(object): if name in ('__file__', '__path__'): return '/dev/null' elif (name[0] == name[0].upper() - and not name.startswith('MIXER_TRACK_')): + # gst.interfaces.MIXER_TRACK_* + and not name.startswith('MIXER_TRACK_') + # dbus.String() + and not name == 'String'): return type(name, (), {}) else: return Mock() From 24cae011d6ce50ebb810c2833409805188265833 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Jun 2013 20:15:54 +0200 Subject: [PATCH 019/111] docs: Fix link on Travis badge --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f667b7db..c9db495e 100644 --- a/README.rst +++ b/README.rst @@ -25,4 +25,5 @@ To get started with Mopidy, check out `the docs `_. - Mailing list: `mopidy@googlegroups.com `_ - Twitter: `@mopidy `_ -.. image:: https://secure.travis-ci.org/mopidy/mopidy.png?branch=develop +.. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop + :target: https://travis-ci.org/mopidy/mopidy From a4543ca13aac1f0914e07153cc1fb7b152c366c2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 24 Jun 2013 23:12:17 +0200 Subject: [PATCH 020/111] docs: Fix link to rpi-update --- docs/installation/raspberrypi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index 27aa5caf..53674e7d 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -85,7 +85,7 @@ card. sudo apt-get dist-upgrade #. Ensure you have a new enough firmware. On Debian based systems - `rpi-update `_ + `rpi-update `_ can be used. #. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the From cd2f71fa3a01b993bc1012d2ab7d45b4f9448871 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 24 Jun 2013 23:12:17 +0200 Subject: [PATCH 021/111] docs: Fix link to rpi-update (cherry picked from commit a4543ca13aac1f0914e07153cc1fb7b152c366c2) --- docs/installation/raspberrypi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index 27aa5caf..53674e7d 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -85,7 +85,7 @@ card. sudo apt-get dist-upgrade #. Ensure you have a new enough firmware. On Debian based systems - `rpi-update `_ + `rpi-update `_ can be used. #. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the From 59e3b9aec35310a5a1f475fe474b86202f724503 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 27 Jun 2013 00:07:57 +0200 Subject: [PATCH 022/111] config: Formatting --- mopidy/config/types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/config/types.py b/mopidy/config/types.py index 3451992a..a2f759df 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -224,7 +224,8 @@ class Port(Integer): """ # TODO: consider probing if port is free or not? def __init__(self, choices=None): - super(Port, self).__init__(minimum=0, maximum=2**16-1, choices=choices) + super(Port, self).__init__( + minimum=0, maximum=2 ** 16 - 1, choices=choices) class Path(ConfigValue): From 2ad1bb8bb398d3acc05d91d33b3fcf1230632412 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 27 Jun 2013 00:08:05 +0200 Subject: [PATCH 023/111] config: Raise ValueError if Path is asked to serialize unicode If we accept unicode and try to encode using sys.getfilesystemencoding() then it may work most of the time, but will fail if we get non-ASCII chars in the unicode string and the file system encoding is e.g. ANSI-something because the locale is C. Thus, I figure it is better to always fail if we try to serialize Path from unicode strings. Paths should be maintained as bytes all the time. --- mopidy/config/types.py | 3 +-- tests/config/types_test.py | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/mopidy/config/types.py b/mopidy/config/types.py index a2f759df..ec0be1de 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import logging import re import socket -import sys from mopidy.utils import path from mopidy.config import validators @@ -257,7 +256,7 @@ class Path(ConfigValue): def serialize(self, value, display=False): if isinstance(value, unicode): - value = value.encode(sys.getfilesystemencoding()) + raise ValueError('paths should always be bytes') if isinstance(value, ExpandedPath): return value.original return value diff --git a/tests/config/types_test.py b/tests/config/types_test.py index 24f1265e..88c8f067 100644 --- a/tests/config/types_test.py +++ b/tests/config/types_test.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import logging import mock import socket -import sys import unittest from mopidy.config import types @@ -367,7 +366,4 @@ class PathTest(unittest.TestCase): def test_serialize_unicode_string(self): value = types.Path() - expected = 'æøå'.encode(sys.getfilesystemencoding()) - result = value.serialize('æøå') - self.assertEqual(expected, result) - self.assertIsInstance(result, bytes) + self.assertRaises(ValueError, value.serialize, 'æøå') From d6a1e13ed6fc1db1d2087ba56fc9130f9ab641f9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 27 Jun 2013 00:25:25 +0200 Subject: [PATCH 024/111] tests: Use utf-8 when encoding our test data paths to bytes --- tests/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index b358f32b..a384669e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,12 +1,11 @@ from __future__ import unicode_literals import os -import sys def path_to_data_dir(name): if not isinstance(name, bytes): - name = name.encode(sys.getfilesystemencoding()) + name = name.encode('utf-8') path = os.path.dirname(__file__) path = os.path.join(path, b'data') path = os.path.abspath(path) From 4a48f8f216900a74eead9efe61b1c1569bb40f3c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Jul 2013 11:43:05 +0200 Subject: [PATCH 025/111] Allow pyspotify 1.9 and higher, including 1.11 --- requirements/spotify.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/spotify.txt b/requirements/spotify.txt index 333e55c8..d11a5c04 100644 --- a/requirements/spotify.txt +++ b/requirements/spotify.txt @@ -1,4 +1,4 @@ -pyspotify >= 1.9, < 1.11 +pyspotify >= 1.9, < 2 # The libspotify Python wrapper # Available as the python-spotify package from apt.mopidy.com diff --git a/setup.py b/setup.py index 8e02d53d..1eaecdae 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( 'Pykka >= 1.1', ], extras_require={ - 'spotify': ['pyspotify >= 1.9, < 1.11'], + 'spotify': ['pyspotify >= 1.9, < 2'], 'scrobbler': ['pylast >= 0.5.7'], 'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'], }, From cb561519dbd7be3f71f20f69415e526de8cf6a3d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Jul 2013 11:46:39 +0200 Subject: [PATCH 026/111] Bump version number to 0.14.2 --- mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 14646df6..fbd1dcca 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -23,4 +23,4 @@ if (isinstance(pykka.__version__, basestring) warnings.filterwarnings('ignore', 'could not open display') -__version__ = '0.14.1' +__version__ = '0.14.2' diff --git a/tests/version_test.py b/tests/version_test.py index ce16d435..41e4f89b 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -38,5 +38,6 @@ class VersionTest(unittest.TestCase): self.assertLess(SV('0.11.1'), SV('0.12.0')) self.assertLess(SV('0.12.0'), SV('0.13.0')) self.assertLess(SV('0.13.0'), SV('0.14.0')) - self.assertLess(SV('0.14.0'), SV(__version__)) - self.assertLess(SV(__version__), SV('0.14.2')) + self.assertLess(SV('0.14.0'), SV('0.14.1')) + self.assertLess(SV('0.14.1'), SV(__version__)) + self.assertLess(SV(__version__), SV('0.14.3')) From bcf71430963ca0f1adfb29db7b84e52fa356d7ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Jul 2013 11:46:48 +0200 Subject: [PATCH 027/111] docs: Update changelog for 0.14.2 --- docs/changelog.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1dd14fb7..fbcab5de 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,18 @@ Changelog This changelog is used to track all major changes to Mopidy. + +v0.14.2 (2013-07-01) +==================== + +This is a maintenance release to make Mopidy 0.14 work with pyspotify 1.11. + +**Dependencies** + +- pyspotify >= 1.9, < 2 is now required for Spotify support. In other words, + you're free to upgrade to pyspotify 1.11, but it isn't a requirement. + + v0.14.1 (2013-04-28) ==================== From e206a6fdf7ba4cae372eb0728658b9c3415b66fb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Jul 2013 11:50:29 +0200 Subject: [PATCH 028/111] Release v0.14.2 From 99eed0e6b8da06de8c5259299b7f1fe87c6ceec0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Jul 2013 11:22:45 +0200 Subject: [PATCH 029/111] mpd: Allow 'count' without quotes around the 'tag' param Fix hechtus/mopidy-gmusic#1 --- mopidy/frontends/mpd/protocol/music_db.py | 6 +++++- tests/frontends/mpd/protocol/music_db_test.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index ff79c33a..4eb0e47d 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -39,7 +39,7 @@ def _artist_as_track(artist): artists=[artist]) -@handle_request(r'^count "(?P[^"]+)" "(?P[^"]*)"$') +@handle_request(r'^count "?(?P[^"]+)"? "(?P[^"]*)"$') def count(context, tag, needle): """ *musicpd.org, music database section:* @@ -48,6 +48,10 @@ def count(context, tag, needle): Counts the number of songs and their total playtime in the db matching ``TAG`` exactly. + + *GMPC:* + + - does not add quotes around the tag argument. """ return [('songs', 0), ('playtime', 0)] # TODO diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index d16a636b..a155072e 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -12,6 +12,12 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertInResponse('playtime: 0') self.assertInResponse('OK') + def test_count_without_quotes(self): + self.sendRequest('count tag "needle"') + self.assertInResponse('songs: 0') + self.assertInResponse('playtime: 0') + self.assertInResponse('OK') + def test_findadd(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(uri='dummy:a', name='A')]) From 579a93437f8eda28bd9eb126cc8f247406be8247 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Jul 2013 11:48:11 +0200 Subject: [PATCH 030/111] mpd: Allow full MPD queries to 'count' Fix hechtus/mopidy-gmusic#1 --- mopidy/frontends/mpd/protocol/music_db.py | 5 +++-- tests/frontends/mpd/protocol/music_db_test.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 4eb0e47d..8e31dbf8 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -39,8 +39,8 @@ def _artist_as_track(artist): artists=[artist]) -@handle_request(r'^count "?(?P[^"]+)"? "(?P[^"]*)"$') -def count(context, tag, needle): +@handle_request(r'^count ' + QUERY_RE) +def count(context, mpd_query): """ *musicpd.org, music database section:* @@ -52,6 +52,7 @@ def count(context, tag, needle): *GMPC:* - does not add quotes around the tag argument. + - use multiple tag-needle pairs to make more specific searches. """ return [('songs', 0), ('playtime', 0)] # TODO diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index a155072e..fa909bab 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -7,13 +7,19 @@ from tests.frontends.mpd import protocol class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_count(self): - self.sendRequest('count "tag" "needle"') + self.sendRequest('count "artist" "needle"') self.assertInResponse('songs: 0') self.assertInResponse('playtime: 0') self.assertInResponse('OK') def test_count_without_quotes(self): - self.sendRequest('count tag "needle"') + self.sendRequest('count artist "needle"') + self.assertInResponse('songs: 0') + self.assertInResponse('playtime: 0') + self.assertInResponse('OK') + + def test_count_with_multiple_pairs(self): + self.sendRequest('count "artist" "foo" "album" "bar"') self.assertInResponse('songs: 0') self.assertInResponse('playtime: 0') self.assertInResponse('OK') From 73f47a9fcedb5df8c7fb185f00ca9a6f2c7c8a6c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 11 Jul 2013 22:48:05 +0200 Subject: [PATCH 031/111] docs: raspi instructions work for both Raspbian and Debian --- docs/installation/raspberrypi.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index 53674e7d..e266dee2 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -16,12 +16,20 @@ distribution. .. _raspi-wheezy: -How to for Debian 7 (Wheezy) -============================ +How to for Raspbian "wheezy" and Debian "wheezy" +================================================ -#. Download the latest wheezy disk image from - http://downloads.raspberrypi.org/images/debian/7/. I used the one dated - 2012-08-08. +This guide applies for both: + +- Raspian "wheezy" for armhf (hard-float), and +- Debian "wheezy" for armel (soft-float) + +If you don't know which one to select, go for the armhf variant, as it'll give +you a lot better performance. + +#. Download the latest "wheezy" disk image from + http://www.raspberrypi.org/downloads/. This was last tested with the images + from 2013-05-25 for armhf and 2013-05-29 for armel. #. Flash the OS image to your SD card. See http://elinux.org/RPi_Easy_SD_Card_Setup for help. From a372df833379248cd7f077a6fcafe6bee646e98b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 26 Jul 2013 12:20:29 +0200 Subject: [PATCH 032/111] Scanner: Add disc number to album and track models (fixes #318) --- mopidy/scanner.py | 2 ++ tests/scanner_test.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 77085f90..9f13d454 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -139,6 +139,7 @@ def translator(data): _retrieve(gst.TAG_ALBUM, 'name', album_kwargs) _retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs) + _retrieve(gst.TAG_ALBUM_VOLUME_COUNT, 'num_discs', album_kwargs) _retrieve(gst.TAG_ARTIST, 'name', artist_kwargs) if gst.TAG_DATE in data and data[gst.TAG_DATE]: @@ -152,6 +153,7 @@ def translator(data): _retrieve(gst.TAG_TITLE, 'name', track_kwargs) _retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs) + _retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs) # Following keys don't seem to have TAG_* constant. _retrieve('album-artist', 'name', albumartist_kwargs) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index c9671523..224aa77f 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -26,6 +26,8 @@ class TranslatorTest(unittest.TestCase): 'album-artist': 'albumartistname', 'title': 'trackname', 'track-count': 2, + 'album-disc-number': 2, + 'album-disc-count': 3, 'date': FakeGstDate(2006, 1, 1,), 'container-format': 'ID3 tag', 'duration': 4531, @@ -39,6 +41,7 @@ class TranslatorTest(unittest.TestCase): self.album = { 'name': 'albumname', 'num_tracks': 2, + 'num_discs': 3, 'musicbrainz_id': 'mbalbumid', } @@ -56,7 +59,8 @@ class TranslatorTest(unittest.TestCase): 'uri': 'uri', 'name': 'trackname', 'date': '2006-01-01', - 'track_no': 1, + 'track_no': 1, + 'disc_no': 2, 'length': 4531, 'musicbrainz_id': 'mbtrackid', 'last_modified': 1234, From d1051e4f18298e963ba09daf1463ea78024546f1 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 26 Jul 2013 12:30:02 +0200 Subject: [PATCH 033/111] Removed unnecessary whitespaces. --- tests/scanner_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 224aa77f..dcf891c0 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -59,7 +59,7 @@ class TranslatorTest(unittest.TestCase): 'uri': 'uri', 'name': 'trackname', 'date': '2006-01-01', - 'track_no': 1, + 'track_no': 1, 'disc_no': 2, 'length': 4531, 'musicbrainz_id': 'mbtrackid', From 2f94b5d219091ed8b2e8796200b4d414934f624a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 22 Jul 2013 13:43:26 +0200 Subject: [PATCH 034/111] mpd: Remove remnants of Python 2.6 support --- mopidy/frontends/mpd/translator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 8e9d12e0..804f693a 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -156,14 +156,14 @@ def query_from_mpd_list_format(field, mpd_query): if field == 'album': if not tokens[0]: raise ValueError - return {'artist': [tokens[0]]} # See above NOTE + return {'artist': [tokens[0]]} else: raise MpdArgError( 'should be "Album" for 3 arguments', command='list') elif len(tokens) % 2 == 0: query = {} while tokens: - key = str(tokens[0].lower()) # See above NOTE + key = tokens[0].lower() value = tokens[1] tokens = tokens[2:] if key not in ('artist', 'album', 'date', 'genre'): From 6855fddbfbbdb4ae8567d1c99a5cd798e14855ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 26 Jul 2013 12:46:42 +0200 Subject: [PATCH 035/111] docs: Update changelog, authors --- AUTHORS | 1 + docs/changelog.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AUTHORS b/AUTHORS index 1d62b108..2d02e48f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,3 +20,4 @@ - Zan Dobersek - Thomas Refis - Janez Troha +- Tobias Sauerwein diff --git a/docs/changelog.rst b/docs/changelog.rst index 1ee0e26e..2d2ce873 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,11 @@ v0.15.0 (UNRELEASED) - :option:`mopidy --show-config` will now take into consideration any :option:`mopidy --option` arguments appearing later on the command line. +**Local backend** + +- An album's number of discs and a track's disc number are now extracted when + scanning your music collection. + v0.14.2 (2013-07-01) ==================== From c58de5aa7548bf8457e4123db18099f4207d3be2 Mon Sep 17 00:00:00 2001 From: alzeih Date: Sun, 28 Jul 2013 16:42:16 +1200 Subject: [PATCH 036/111] workaround for mopidy/mopidy#62 - appends folder names to playlist --- mopidy/backends/spotify/session_manager.py | 9 +++++++-- mopidy/backends/spotify/translator.py | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index c0592ea7..980ce9f8 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -173,12 +173,17 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): logger.debug('Still getting data; skipped refresh of playlists') return playlists = [] + folders = [] for spotify_playlist in self.session.playlist_container(): + if spotify_playlist.type() == 'folder_start': + folders.append(spotify_playlist) + if spotify_playlist.type() == 'folder_end': + folders.pop() playlists.append(translator.to_mopidy_playlist( - spotify_playlist, + spotify_playlist, folders, bitrate=self.bitrate, username=self.username)) playlists.append(translator.to_mopidy_playlist( - self.session.starred(), + self.session.starred(), None, bitrate=self.bitrate, username=self.username)) playlists = filter(None, playlists) self.backend.playlists.playlists = playlists diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 60961cf8..beec45a3 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -67,7 +67,7 @@ def to_mopidy_track(spotify_track, bitrate=None): return track_cache[uri] -def to_mopidy_playlist(spotify_playlist, bitrate=None, username=None): +def to_mopidy_playlist(spotify_playlist, spotify_folders, bitrate=None, username=None): if spotify_playlist is None or spotify_playlist.type() != 'playlist': return try: @@ -78,6 +78,9 @@ def to_mopidy_playlist(spotify_playlist, bitrate=None, username=None): if not spotify_playlist.is_loaded(): return Playlist(uri=uri, name='[loading...]') name = spotify_playlist.name() + if spotify_folders is not None: + folder_names = unicode.join('', ['[' + folder.name() + ']' for folder in spotify_folders]) + name = folder_names + ' ' + name tracks = [ to_mopidy_track(spotify_track, bitrate=bitrate) for spotify_track in spotify_playlist From c4dcef81b4b894446eb4f26d01d99bdfbeb21a43 Mon Sep 17 00:00:00 2001 From: alzeih Date: Sun, 28 Jul 2013 22:42:15 +1200 Subject: [PATCH 037/111] - use path separators instead of square brackets - use generator expression instead of list comprehension - fix bug for empty but not None list of folders --- mopidy/backends/spotify/translator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index beec45a3..f0c3df65 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -78,9 +78,9 @@ def to_mopidy_playlist(spotify_playlist, spotify_folders, bitrate=None, username if not spotify_playlist.is_loaded(): return Playlist(uri=uri, name='[loading...]') name = spotify_playlist.name() - if spotify_folders is not None: - folder_names = unicode.join('', ['[' + folder.name() + ']' for folder in spotify_folders]) - name = folder_names + ' ' + name + if spotify_folders: + folder_names = '/'.join(folder.name() for folder in spotify_folders) + name = folder_names + '/' + name tracks = [ to_mopidy_track(spotify_track, bitrate=bitrate) for spotify_track in spotify_playlist From 47c26acf573871a81e841dc261628c8e7d4cfb83 Mon Sep 17 00:00:00 2001 From: alzeih Date: Sun, 28 Jul 2013 23:53:47 +1200 Subject: [PATCH 038/111] spotify folders not necessary as a positional arg - rename to folders - remove confusing None for starred playlists --- mopidy/backends/spotify/session_manager.py | 4 ++-- mopidy/backends/spotify/translator.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 980ce9f8..8f520896 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -180,10 +180,10 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): if spotify_playlist.type() == 'folder_end': folders.pop() playlists.append(translator.to_mopidy_playlist( - spotify_playlist, folders, + spotify_playlist, folders=folders, bitrate=self.bitrate, username=self.username)) playlists.append(translator.to_mopidy_playlist( - self.session.starred(), None, + self.session.starred(), bitrate=self.bitrate, username=self.username)) playlists = filter(None, playlists) self.backend.playlists.playlists = playlists diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index f0c3df65..5813f880 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -67,7 +67,7 @@ def to_mopidy_track(spotify_track, bitrate=None): return track_cache[uri] -def to_mopidy_playlist(spotify_playlist, spotify_folders, bitrate=None, username=None): +def to_mopidy_playlist(spotify_playlist, folders=None, bitrate=None, username=None): if spotify_playlist is None or spotify_playlist.type() != 'playlist': return try: @@ -78,8 +78,8 @@ def to_mopidy_playlist(spotify_playlist, spotify_folders, bitrate=None, username if not spotify_playlist.is_loaded(): return Playlist(uri=uri, name='[loading...]') name = spotify_playlist.name() - if spotify_folders: - folder_names = '/'.join(folder.name() for folder in spotify_folders) + if folders: + folder_names = '/'.join(folder.name() for folder in folders) name = folder_names + '/' + name tracks = [ to_mopidy_track(spotify_track, bitrate=bitrate) From 608b9c9b6c4ccb5b29b49b2c59035eb651d4a7c0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 28 Jul 2013 23:25:42 +0200 Subject: [PATCH 039/111] docs: Update changelog, authors --- AUTHORS | 1 + docs/changelog.rst | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/AUTHORS b/AUTHORS index 2d02e48f..1e3faf0e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,3 +21,4 @@ - Thomas Refis - Janez Troha - Tobias Sauerwein +- alzeih diff --git a/docs/changelog.rst b/docs/changelog.rst index 2d2ce873..a8c9dee6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,12 @@ v0.15.0 (UNRELEASED) - An album's number of discs and a track's disc number are now extracted when scanning your music collection. +**Spotify backend** + +- Prepend playlist folder names to the playlist name, so that the playlist + hierarchy from your Spotify account is available in Mopidy. (Fixes: + :issue:`62`) + v0.14.2 (2013-07-01) ==================== From de80c33753bcd96804500748ae80865104ade8c4 Mon Sep 17 00:00:00 2001 From: Alexandre Petitjean Date: Mon, 29 Jul 2013 09:57:32 +0200 Subject: [PATCH 040/111] Add proxy/port to configuration --- mopidy/backends/spotify/session_manager.py | 7 ++++++- mopidy/config/__init__.py | 1 + mopidy/config/convert.py | 1 + mopidy/config/default.conf | 1 + mopidy/config/types.py | 8 ++++++-- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 8f520896..3e8f1b67 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -33,9 +33,14 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): self.cache_location = config['spotify']['cache_dir'] self.settings_location = config['spotify']['cache_dir'] + # Add proxy port only if available + full_proxy = config['proxy']['hostname'] + if 'port' in config['proxy']: + full_proxy = full_proxy + ':' + str(config['proxy']['port']) + PyspotifySessionManager.__init__( self, config['spotify']['username'], config['spotify']['password'], - proxy=config['proxy']['hostname'], + proxy=full_proxy, proxy_username=config['proxy']['username'], proxy_password=config['proxy']['password']) diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index e9ae7d86..7ccaa3f3 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -29,6 +29,7 @@ _proxy_schema = ConfigSchema('proxy') _proxy_schema['hostname'] = Hostname(optional=True) _proxy_schema['username'] = String(optional=True) _proxy_schema['password'] = Secret(optional=True) +_proxy_schema['port'] = Port(optional=True) # NOTE: if multiple outputs ever comes something like LogLevelConfigSchema #_outputs_schema = config.AudioOutputConfigSchema() diff --git a/mopidy/config/convert.py b/mopidy/config/convert.py index 6cb20fcd..3c3edb85 100644 --- a/mopidy/config/convert.py +++ b/mopidy/config/convert.py @@ -39,6 +39,7 @@ def convert(settings): helper('audio/output', 'OUTPUT') helper('proxy/hostname', 'SPOTIFY_PROXY_HOST') + helper('proxy/port', 'SPOTIFY_PROXY_PORT') helper('proxy/username', 'SPOTIFY_PROXY_USERNAME') helper('proxy/password', 'SPOTIFY_PROXY_PASSWORD') diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index b525ef47..204c789b 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -16,3 +16,4 @@ output = autoaudiosink hostname = username = password = +port = \ No newline at end of file diff --git a/mopidy/config/types.py b/mopidy/config/types.py index ec0be1de..3dff0908 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -117,7 +117,10 @@ class Integer(ConfigValue): self._choices = choices def deserialize(self, value): - value = int(value) + try: + value = int(value) + except: + value = 0 validators.validate_choice(value, self._choices) validators.validate_minimum(value, self._minimum) validators.validate_maximum(value, self._maximum) @@ -222,7 +225,8 @@ class Port(Integer): allocate a port for us. """ # TODO: consider probing if port is free or not? - def __init__(self, choices=None): + def __init__(self, choices=None, optional=False): + self._required = not optional super(Port, self).__init__( minimum=0, maximum=2 ** 16 - 1, choices=choices) From 05733cf844cb8f90eaf4174c05908eb6e6227daa Mon Sep 17 00:00:00 2001 From: Alexandre Petitjean Date: Mon, 29 Jul 2013 22:22:28 +0200 Subject: [PATCH 041/111] correct remarks from #481 --- mopidy/backends/spotify/session_manager.py | 10 ++++---- mopidy/config/__init__.py | 2 +- mopidy/config/default.conf | 2 +- mopidy/config/types.py | 27 +++++++++++++++++----- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 3e8f1b67..dff29fad 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -33,10 +33,12 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): self.cache_location = config['spotify']['cache_dir'] self.settings_location = config['spotify']['cache_dir'] - # Add proxy port only if available - full_proxy = config['proxy']['hostname'] - if 'port' in config['proxy']: - full_proxy = full_proxy + ':' + str(config['proxy']['port']) + full_proxy = '' + if config['proxy']['hostname']: + full_proxy = config['proxy']['hostname'] + # Add proxy port only if available + if config['proxy']['port']: + full_proxy = full_proxy + ':' + str(config['proxy']['port']) PyspotifySessionManager.__init__( self, config['spotify']['username'], config['spotify']['password'], diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 7ccaa3f3..35a1d8e0 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -29,7 +29,7 @@ _proxy_schema = ConfigSchema('proxy') _proxy_schema['hostname'] = Hostname(optional=True) _proxy_schema['username'] = String(optional=True) _proxy_schema['password'] = Secret(optional=True) -_proxy_schema['port'] = Port(optional=True) +_proxy_schema['port'] = Port() # NOTE: if multiple outputs ever comes something like LogLevelConfigSchema #_outputs_schema = config.AudioOutputConfigSchema() diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index 204c789b..4a259c49 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -16,4 +16,4 @@ output = autoaudiosink hostname = username = password = -port = \ No newline at end of file +port = diff --git a/mopidy/config/types.py b/mopidy/config/types.py index 3dff0908..8309a591 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -117,10 +117,7 @@ class Integer(ConfigValue): self._choices = choices def deserialize(self, value): - try: - value = int(value) - except: - value = 0 + value = int(value) validators.validate_choice(value, self._choices) validators.validate_minimum(value, self._minimum) validators.validate_maximum(value, self._maximum) @@ -225,11 +222,29 @@ class Port(Integer): allocate a port for us. """ # TODO: consider probing if port is free or not? - def __init__(self, choices=None, optional=False): - self._required = not optional + def __init__(self, choices=None): super(Port, self).__init__( minimum=0, maximum=2 ** 16 - 1, choices=choices) + def deserialize(self, value): + # in case of no value is given, just return nothing + if not len(value): + return value + # now we can try to convert + try: + value = int(value) + except ValueError: + try: + value = socket.getservbyname(value, 'tcp') + except socket.error: + raise ValueError('must be a valid port number') + else: + validators.validate_choice(value, self._choices) + validators.validate_minimum(value, self._minimum) + validators.validate_maximum(value, self._maximum) + return value + + class Path(ConfigValue): """File system path From dfcb49a8baa193a3dc413c2ff7387e2ee5222f7d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 5 Jul 2013 12:25:10 +0200 Subject: [PATCH 042/111] ext: Add library updaters to extensionss --- mopidy/backends/local/__init__.py | 4 ++++ mopidy/ext.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index f718eeb5..0f6a95bf 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -29,3 +29,7 @@ class Extension(ext.Extension): def get_backend_classes(self): from .actor import LocalBackend return [LocalBackend] + + def get_library_updaters(self): + from .library import LocalLibraryUpdateProvider + return [LocalLibraryUpdateProvider] diff --git a/mopidy/ext.py b/mopidy/ext.py index d7c5c96f..4b6e4502 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -79,6 +79,9 @@ class Extension(object): """ return [] + def get_library_updaters(self): + return [] + def register_gstreamer_elements(self): """Hook for registering custom GStreamer elements From c2cc9f027c60c084a762ca851d6c9a0ffe4f337a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 5 Jul 2013 12:28:11 +0200 Subject: [PATCH 043/111] local: Switch to using updater from extensions --- mopidy/backends/base.py | 10 +--------- mopidy/backends/local/actor.py | 3 +-- mopidy/backends/local/library.py | 11 +++++++---- mopidy/scanner.py | 23 +++++++++++++++++------ 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index f0561b4c..292fa4cb 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -96,15 +96,7 @@ class BaseLibraryProvider(object): class BaseLibraryUpdateProvider(object): - """ - :param backend: backend the controller is a part of - :type backend: :class:`mopidy.backends.base.Backend` - """ - - pykka_traversable = True - - def __init__(self, backend): - self.backend = backend + uri_schemes = [] def load(self): """Loads the library and returns all tracks in it. diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py index b73c53e2..8f53af4d 100644 --- a/mopidy/backends/local/actor.py +++ b/mopidy/backends/local/actor.py @@ -8,7 +8,7 @@ import pykka from mopidy.backends import base from mopidy.utils import encoding, path -from .library import LocalLibraryProvider, LocalLibraryUpdateProvider +from .library import LocalLibraryProvider from .playlists import LocalPlaylistsProvider logger = logging.getLogger('mopidy.backends.local') @@ -23,7 +23,6 @@ class LocalBackend(pykka.ThreadingActor, base.Backend): self.check_dirs_and_files() self.library = LocalLibraryProvider(backend=self) - self.updater = LocalLibraryUpdateProvider(backend=self) self.playback = base.BasePlaybackProvider(audio=audio, backend=self) self.playlists = LocalPlaylistsProvider(backend=self) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 43768cd4..c80e95fe 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -135,11 +135,12 @@ class LocalLibraryProvider(base.BaseLibraryProvider): # TODO: rename and move to tagcache extension. class LocalLibraryUpdateProvider(base.BaseLibraryProvider): - def __init__(self, *args, **kwargs): - super(LocalLibraryUpdateProvider, self).__init__(*args, **kwargs) + uri_schemes = ['file'] + + def __init__(self, config): self._tracks = {} - self._media_dir = self.backend.config['local']['media_dir'] - self._tag_cache_file = self.backend.config['local']['tag_cache_file'] + self._media_dir = config['local']['media_dir'] + self._tag_cache_file = config['local']['tag_cache_file'] def load(self): tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) @@ -156,6 +157,8 @@ class LocalLibraryUpdateProvider(base.BaseLibraryProvider): def commit(self): directory, basename = os.path.split(self._tag_cache_file) + + # TODO: cleanup directory/basename.* files. tmp = tempfile.NamedTemporaryFile( prefix=basename + '.', dir=directory, delete=False) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 9f13d454..2a901910 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -45,9 +45,9 @@ def main(): log.setup_root_logger() log.setup_console_logging(logging_config, args.verbosity_level) - extensions = dict((e.ext_name, e) for e in ext.load_extensions()) + extensions = ext.load_extensions() config, errors = config_lib.load( - config_files, extensions.values(), config_overrides) + config_files, extensions, config_overrides) log.setup_log_levels(config) if not config['local']['media_dir']: @@ -56,10 +56,21 @@ def main(): # TODO: missing config error checking and other default setup code. - audio = dummy_audio.DummyAudio() - local_backend_classes = extensions['local'].get_backend_classes() - local_backend = local_backend_classes[0](config, audio) - local_updater = local_backend.updater + updaters = {} + for e in extensions: + for updater_class in e.get_library_updaters(): + if updater_class and 'file' in updater_class.uri_schemes: + updaters[e.ext_name] = updater_class + + if not updaters: + logging.error('No usable updaters found.') + return + elif len(updaters) > 1: + names = ', '.join(updaters.keys()) + logging.error('More than one updater found. Provided by: %s', names) + return + + local_updater = updaters.values()[0](config) # TODO: switch to actor? media_dir = config['local']['media_dir'] From 3cfa6c3bc070bcdfd45abf8b0475a50669379b5b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 5 Jul 2013 12:28:52 +0200 Subject: [PATCH 044/111] local: Remove updater from backends --- mopidy/backends/base.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 292fa4cb..226ac75b 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -15,11 +15,6 @@ class Backend(object): #: the backend doesn't provide a library. library = None - #: The library update provider. An instance of - #: :class:`~mopidy.backends.base.BaseLibraryUpdateProvider`, or - #: :class:`None` if the backend doesn't provide a library. - updater = None - #: The playback provider. An instance of #: :class:`~mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if #: the backend doesn't provide playback. @@ -40,9 +35,6 @@ class Backend(object): def has_library(self): return self.library is not None - def has_updater(self): - return self.updater is not None - def has_playback(self): return self.playback is not None From bc4935bfcb5258e9254ba8641fceab764a1c82f6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 9 Jul 2013 21:26:23 +0200 Subject: [PATCH 045/111] backends: Add change track helper to playback provider --- mopidy/backends/base.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 226ac75b..207edb3a 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -155,10 +155,23 @@ class BasePlaybackProvider(object): :type track: :class:`mopidy.models.Track` :rtype: :class:`True` if successful, else :class:`False` """ - self.audio.prepare_change() - self.audio.set_uri(track.uri).get() + self.audio.prepare_change() # TODO: add .get() to this? + self.change_track(track) return self.audio.start_playback().get() + def change_track(self, track): + """ + Swith to provided track. + + *MAY be reimplemented by subclass.* + + :param track: the track to play + :type track: :class:`mopidy.models.Track` + :rtype: :class:`True` if successful, else :class:`False` + """ + self.audio.set_uri(track.uri).get() + return True + def resume(self): """ Resume playback at the same time position playback was paused. From af707dfdbb687d57fb03b068bab01704171eb0a9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 9 Jul 2013 22:57:34 +0200 Subject: [PATCH 046/111] utils: Switch to urlparse for file-uri/path handling --- mopidy/utils/path.py | 18 ++++----- tests/utils/path_test.py | 81 ++++++++++------------------------------ 2 files changed, 27 insertions(+), 72 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index e4d717d1..dc769119 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -6,8 +6,8 @@ import re # pylint: disable = W0402 import string # pylint: enable = W0402 -import sys import urllib +import urlparse import glib @@ -51,7 +51,7 @@ def get_or_create_file(file_path): return file_path -def path_to_uri(*paths): +def path_to_uri(*parts): """ Convert OS specific path to file:// URI. @@ -61,17 +61,16 @@ def path_to_uri(*paths): Returns a file:// URI as an unicode string. """ - path = os.path.join(*paths) + path = os.path.join(*parts) if isinstance(path, unicode): path = path.encode('utf-8') - if sys.platform == 'win32': - return 'file:' + urllib.quote(path) - return 'file://' + urllib.quote(path) + path = urllib.quote(path) + return urlparse.urlunsplit((b'file', b'', path, b'', b'')) def uri_to_path(uri): """ - Convert the file:// to a OS specific path. + Convert an URI to a OS specific path. Returns a bytestring, since the file path can contain chars with other encoding than UTF-8. @@ -82,10 +81,7 @@ def uri_to_path(uri): """ if isinstance(uri, unicode): uri = uri.encode('utf-8') - if sys.platform == 'win32': - return urllib.unquote(re.sub(b'^file:', b'', uri)) - else: - return urllib.unquote(re.sub(b'^file://', b'', uri)) + return urllib.unquote(urlparse.urlsplit(uri).path) def split_path(path): diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index a19e48f7..0bead5b7 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import os import shutil -import sys import tempfile import unittest @@ -117,86 +116,46 @@ class GetOrCreateFileTest(unittest.TestCase): class PathToFileURITest(unittest.TestCase): def test_simple_path(self): - if sys.platform == 'win32': - result = path.path_to_uri('C:/WINDOWS/clock.avi') - self.assertEqual(result, 'file:///C://WINDOWS/clock.avi') - else: - result = path.path_to_uri('/etc/fstab') - self.assertEqual(result, 'file:///etc/fstab') + result = path.path_to_uri('/etc/fstab') + self.assertEqual(result, 'file:///etc/fstab') def test_dir_and_path(self): - if sys.platform == 'win32': - result = path.path_to_uri('C:/WINDOWS/', 'clock.avi') - self.assertEqual(result, 'file:///C://WINDOWS/clock.avi') - else: - result = path.path_to_uri('/etc', 'fstab') - self.assertEqual(result, 'file:///etc/fstab') + result = path.path_to_uri('/etc', 'fstab') + self.assertEqual(result, 'file:///etc/fstab') def test_space_in_path(self): - if sys.platform == 'win32': - result = path.path_to_uri('C:/test this') - self.assertEqual(result, 'file:///C://test%20this') - else: - result = path.path_to_uri('/tmp/test this') - self.assertEqual(result, 'file:///tmp/test%20this') + result = path.path_to_uri('/tmp/test this') + self.assertEqual(result, 'file:///tmp/test%20this') def test_unicode_in_path(self): - if sys.platform == 'win32': - result = path.path_to_uri('C:/æøå') - self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5') - else: - result = path.path_to_uri('/tmp/æøå') - self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5') + result = path.path_to_uri('/tmp/æøå') + self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5') def test_utf8_in_path(self): - if sys.platform == 'win32': - result = path.path_to_uri('C:/æøå'.encode('utf-8')) - self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5') - else: - result = path.path_to_uri('/tmp/æøå'.encode('utf-8')) - self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5') + result = path.path_to_uri('/tmp/æøå'.encode('utf-8')) + self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5') def test_latin1_in_path(self): - if sys.platform == 'win32': - result = path.path_to_uri('C:/æøå'.encode('latin-1')) - self.assertEqual(result, 'file:///C://%E6%F8%E5') - else: - result = path.path_to_uri('/tmp/æøå'.encode('latin-1')) - self.assertEqual(result, 'file:///tmp/%E6%F8%E5') + result = path.path_to_uri('/tmp/æøå'.encode('latin-1')) + self.assertEqual(result, 'file:///tmp/%E6%F8%E5') class UriToPathTest(unittest.TestCase): def test_simple_uri(self): - if sys.platform == 'win32': - result = path.uri_to_path('file:///C://WINDOWS/clock.avi') - self.assertEqual(result, 'C:/WINDOWS/clock.avi'.encode('utf-8')) - else: - result = path.uri_to_path('file:///etc/fstab') - self.assertEqual(result, '/etc/fstab'.encode('utf-8')) + result = path.uri_to_path('file:///etc/fstab') + self.assertEqual(result, '/etc/fstab'.encode('utf-8')) def test_space_in_uri(self): - if sys.platform == 'win32': - result = path.uri_to_path('file:///C://test%20this') - self.assertEqual(result, 'C:/test this'.encode('utf-8')) - else: - result = path.uri_to_path('file:///tmp/test%20this') - self.assertEqual(result, '/tmp/test this'.encode('utf-8')) + result = path.uri_to_path('file:///tmp/test%20this') + self.assertEqual(result, '/tmp/test this'.encode('utf-8')) def test_unicode_in_uri(self): - if sys.platform == 'win32': - result = path.uri_to_path('file:///C://%C3%A6%C3%B8%C3%A5') - self.assertEqual(result, 'C:/æøå'.encode('utf-8')) - else: - result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5') - self.assertEqual(result, '/tmp/æøå'.encode('utf-8')) + result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5') + self.assertEqual(result, '/tmp/æøå'.encode('utf-8')) def test_latin1_in_uri(self): - if sys.platform == 'win32': - result = path.uri_to_path('file:///C://%E6%F8%E5') - self.assertEqual(result, 'C:/æøå'.encode('latin-1')) - else: - result = path.uri_to_path('file:///tmp/%E6%F8%E5') - self.assertEqual(result, '/tmp/æøå'.encode('latin-1')) + result = path.uri_to_path('file:///tmp/%E6%F8%E5') + self.assertEqual(result, '/tmp/æøå'.encode('latin-1')) class SplitPathTest(unittest.TestCase): From 6818e202181838765ac0cf0673bef11982c406f1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 9 Jul 2013 23:26:01 +0200 Subject: [PATCH 047/111] utils: Convert path_to_uri to single argument --- mopidy/backends/local/translator.py | 5 +++-- mopidy/utils/path.py | 3 +-- tests/utils/path_test.py | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 4ae10af2..344e8ad7 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import logging +import os import urllib from mopidy.models import Track, Artist, Album @@ -50,7 +51,7 @@ def parse_m3u(file_path, media_dir): if line.startswith('file://'): uris.append(line) else: - path = path_to_uri(media_dir, line) + path = path_to_uri(os.path.join(media_dir, line)) uris.append(path) return uris @@ -167,7 +168,7 @@ def _convert_mpd_data(data, tracks, music_dir): # Make sure we only pass bytestrings to path_to_uri to avoid implicit # decoding of bytestrings to unicode strings - track_kwargs['uri'] = path_to_uri(music_dir, path) + track_kwargs['uri'] = path_to_uri(os.path.join(music_dir, path)) track_kwargs['length'] = int(data.get('time', 0)) * 1000 diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index dc769119..af1f38b1 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -51,7 +51,7 @@ def get_or_create_file(file_path): return file_path -def path_to_uri(*parts): +def path_to_uri(path): """ Convert OS specific path to file:// URI. @@ -61,7 +61,6 @@ def path_to_uri(*parts): Returns a file:// URI as an unicode string. """ - path = os.path.join(*parts) if isinstance(path, unicode): path = path.encode('utf-8') path = urllib.quote(path) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 0bead5b7..ed9f8044 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -119,10 +119,6 @@ class PathToFileURITest(unittest.TestCase): result = path.path_to_uri('/etc/fstab') self.assertEqual(result, 'file:///etc/fstab') - def test_dir_and_path(self): - result = path.path_to_uri('/etc', 'fstab') - self.assertEqual(result, 'file:///etc/fstab') - def test_space_in_path(self): result = path.path_to_uri('/tmp/test this') self.assertEqual(result, 'file:///tmp/test%20this') From 18ed7c627951cbde418d02825e306fea138dc002 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 29 Jul 2013 00:08:32 +0200 Subject: [PATCH 048/111] local: Convert mopidy-local extension to local: uris. - Use local:playlist:, local:track: and local:search uris - Adds LocalPlaybackProvider which translates to file uris. - Switches to storing actual uris in playlists - so local: urls and not file:// or plain paths. - Moved file:// to streaming plugin - Cleaned up tests and imports for these changes. --- mopidy/backends/base.py | 3 +- mopidy/backends/local/actor.py | 5 ++-- mopidy/backends/local/library.py | 8 ++++-- mopidy/backends/local/playback.py | 19 +++++++++++++ mopidy/backends/local/playlists.py | 38 +++++++++++-------------- mopidy/backends/local/translator.py | 24 +++++++--------- mopidy/backends/stream/ext.conf | 1 + mopidy/core/playback.py | 1 + mopidy/frontends/mpd/dispatcher.py | 1 + mopidy/scanner.py | 3 +- mopidy/utils/path.py | 1 - tests/backends/base/library.py | 23 ++++++--------- tests/backends/local/__init__.py | 5 +--- tests/backends/local/playback_test.py | 15 +++++----- tests/backends/local/playlists_test.py | 12 ++++---- tests/backends/local/translator_test.py | 24 +++++++--------- tests/data/library_tag_cache | 12 ++++---- 17 files changed, 98 insertions(+), 97 deletions(-) create mode 100644 mopidy/backends/local/playback.py diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 207edb3a..3c1bbbf0 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -157,7 +157,8 @@ class BasePlaybackProvider(object): """ self.audio.prepare_change() # TODO: add .get() to this? self.change_track(track) - return self.audio.start_playback().get() + self.audio.start_playback().get() + return True def change_track(self, track): """ diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py index 8f53af4d..f3611891 100644 --- a/mopidy/backends/local/actor.py +++ b/mopidy/backends/local/actor.py @@ -10,6 +10,7 @@ from mopidy.utils import encoding, path from .library import LocalLibraryProvider from .playlists import LocalPlaylistsProvider +from .playback import LocalPlaybackProvider logger = logging.getLogger('mopidy.backends.local') @@ -23,10 +24,10 @@ class LocalBackend(pykka.ThreadingActor, base.Backend): self.check_dirs_and_files() self.library = LocalLibraryProvider(backend=self) - self.playback = base.BasePlaybackProvider(audio=audio, backend=self) + self.playback = LocalPlaybackProvider(audio=audio, backend=self) self.playlists = LocalPlaylistsProvider(backend=self) - self.uri_schemes = ['file'] + self.uri_schemes = ['local'] def check_dirs_and_files(self): if not os.path.isdir(self.config['local']['media_dir']): diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index c80e95fe..9dd112e9 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -81,7 +81,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider): result_tracks = filter(any_filter, result_tracks) else: raise LookupError('Invalid lookup field: %s' % field) - return SearchResult(uri='file:search', tracks=result_tracks) + # TODO: add local:search: + return SearchResult(uri='local:search', tracks=result_tracks) def search(self, query=None, uris=None): # TODO Only return results within URI roots given by ``uris`` @@ -122,7 +123,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider): result_tracks = filter(any_filter, result_tracks) else: raise LookupError('Invalid lookup field: %s' % field) - return SearchResult(uri='file:search', tracks=result_tracks) + # TODO: add local:search: + return SearchResult(uri='local:search', tracks=result_tracks) def _validate_query(self, query): for (_, values) in query.iteritems(): @@ -135,7 +137,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): # TODO: rename and move to tagcache extension. class LocalLibraryUpdateProvider(base.BaseLibraryProvider): - uri_schemes = ['file'] + uri_schemes = ['local'] def __init__(self, config): self._tracks = {} diff --git a/mopidy/backends/local/playback.py b/mopidy/backends/local/playback.py new file mode 100644 index 00000000..8c40cb9e --- /dev/null +++ b/mopidy/backends/local/playback.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals + +import logging +import os + +from mopidy.backends import base +from mopidy.utils import path + +logger = logging.getLogger('mopidy.backends.spotify') + + +class LocalPlaybackProvider(base.BasePlaybackProvider): + def change_track(self, track): + media_dir = self.backend.config['local']['media_dir'] + # TODO: check that type is correct. + file_path = path.uri_to_path(track.uri).split(':', 1)[1] + file_path = os.path.join(media_dir, file_path) + track = track.copy(uri=path.path_to_uri(file_path)) + return super(LocalPlaybackProvider, self).change_track(track) diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py index cd370eaa..af3814ae 100644 --- a/mopidy/backends/local/playlists.py +++ b/mopidy/backends/local/playlists.py @@ -24,7 +24,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): def create(self, name): name = formatting.slugify(name) - uri = path.path_to_uri(self._get_m3u_path(name)) + uri = 'local:playlist:%s.m3u' % name playlist = Playlist(uri=uri, name=name) return self.save(playlist) @@ -37,6 +37,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): self._delete_m3u(playlist.uri) def lookup(self, uri): + # TODO: store as {uri: playlist}? for playlist in self._playlists: if playlist.uri == uri: return playlist @@ -45,8 +46,8 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): playlists = [] for m3u in glob.glob(os.path.join(self._playlists_dir, '*.m3u')): - uri = path.path_to_uri(m3u) name = os.path.splitext(os.path.basename(m3u))[0] + uri = 'local:playlist:%s' % name tracks = [] for track_uri in parse_m3u(m3u, self._media_dir): @@ -61,6 +62,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): playlists.append(playlist) self.playlists = playlists + # TODO: send what scheme we loaded them for? listener.BackendListener.send('playlists_loaded') logger.info( @@ -86,38 +88,30 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): return playlist - def _get_m3u_path(self, name): - name = formatting.slugify(name) - file_path = os.path.join(self._playlists_dir, name + '.m3u') + def _m3u_uri_to_path(self, uri): + # TODO: create uri handling helpers for local uri types. + file_path = path.uri_to_path(uri).split(':', 1)[1] + file_path = os.path.join(self._playlists_dir, file_path) path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir) return file_path def _save_m3u(self, playlist): - file_path = path.uri_to_path(playlist.uri) - path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir) + file_path = self._m3u_uri_to_path(playlist.uri) with open(file_path, 'w') as file_handle: for track in playlist.tracks: - if track.uri.startswith('file://'): - uri = path.uri_to_path(track.uri) - else: - uri = track.uri - file_handle.write(uri + '\n') + file_handle.write(track.uri + '\n') def _delete_m3u(self, uri): - file_path = path.uri_to_path(uri) - path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir) + file_path = self._m3u_uri_to_path(uri) if os.path.exists(file_path): os.remove(file_path) def _rename_m3u(self, playlist): - src_file_path = path.uri_to_path(playlist.uri) - path.check_file_path_is_inside_base_dir( - src_file_path, self._playlists_dir) + dst_name = formatting.slugify(playlist.name) + dst_uri = 'local:playlist:%s.m3u' % dst_name - dst_file_path = self._get_m3u_path(playlist.name) - path.check_file_path_is_inside_base_dir( - dst_file_path, self._playlists_dir) + src_file_path = self._m3u_uri_to_path(playlist.uri) + dst_file_path = self._m3u_uri_to_path(dst_uri) shutil.move(src_file_path, dst_file_path) - - return playlist.copy(uri=path.path_to_uri(dst_file_path)) + return playlist.copy(uri=dst_uri) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 344e8ad7..b8e98dd3 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging import os import urllib +import urlparse from mopidy.models import Track, Artist, Album from mopidy.utils.encoding import locale_decode @@ -31,7 +32,6 @@ def parse_m3u(file_path, media_dir): - m3u files are latin-1. - This function does not bother with Extended M3U directives. """ - # TODO: uris as bytes uris = [] try: @@ -47,9 +47,11 @@ def parse_m3u(file_path, media_dir): if line.startswith('#'): continue - # FIXME what about other URI types? - if line.startswith('file://'): + if urlparse.urlsplit(line).scheme: uris.append(line) + elif os.path.normpath(line) == os.path.abspath(line): + path = path_to_uri(line) + uris.append(path) else: path = path_to_uri(os.path.join(media_dir, line)) uris.append(path) @@ -57,6 +59,7 @@ def parse_m3u(file_path, media_dir): return uris +# TODO: remove music_dir from API def parse_mpd_tag_cache(tag_cache, music_dir=''): """ Converts a MPD tag_cache into a lists of tracks, artists and albums. @@ -87,17 +90,17 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''): key, value = line.split(b': ', 1) if key == b'key': - _convert_mpd_data(current, tracks, music_dir) + _convert_mpd_data(current, tracks) current.clear() current[key.lower()] = value.decode('utf-8') - _convert_mpd_data(current, tracks, music_dir) + _convert_mpd_data(current, tracks) return tracks -def _convert_mpd_data(data, tracks, music_dir): +def _convert_mpd_data(data, tracks): if not data: return @@ -161,15 +164,8 @@ def _convert_mpd_data(data, tracks, music_dir): path = data['file'][1:] else: path = data['file'] - path = urllib.unquote(path.encode('utf-8')) - - if isinstance(music_dir, unicode): - music_dir = music_dir.encode('utf-8') - - # Make sure we only pass bytestrings to path_to_uri to avoid implicit - # decoding of bytestrings to unicode strings - track_kwargs['uri'] = path_to_uri(os.path.join(music_dir, path)) + track_kwargs['uri'] = 'local:track:%s' % path track_kwargs['length'] = int(data.get('time', 0)) * 1000 track = Track(**track_kwargs) diff --git a/mopidy/backends/stream/ext.conf b/mopidy/backends/stream/ext.conf index 9caafac1..dc0287da 100644 --- a/mopidy/backends/stream/ext.conf +++ b/mopidy/backends/stream/ext.conf @@ -1,6 +1,7 @@ [stream] enabled = true protocols = + file http https mms diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 2e79827a..2f296751 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -401,6 +401,7 @@ class PlaybackController(object): if self.random and self._shuffled: self._shuffled.remove(tl_track) if on_error_step == 1: + # TODO: can cause an endless loop for single track repeat. self.next() elif on_error_step == -1: self.previous() diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 6590897d..0e55271d 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -266,6 +266,7 @@ class MpdContext(object): for playlist in self.core.playlists.playlists.get(): if not playlist.name: continue + # TODO: add scheme to name perhaps 'foo (spotify)' etc. name = self.create_unique_name(playlist.name) self._playlist_uri_from_name[name] = playlist.uri self._playlist_name_from_uri[playlist.uri] = name diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 2a901910..1752ece9 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -27,7 +27,6 @@ pygst.require('0.10') import gst from mopidy import config as config_lib, ext -from mopidy.audio import dummy as dummy_audio from mopidy.models import Track, Artist, Album from mopidy.utils import log, path, versioning @@ -59,7 +58,7 @@ def main(): updaters = {} for e in extensions: for updater_class in e.get_library_updaters(): - if updater_class and 'file' in updater_class.uri_schemes: + if updater_class and 'local' in updater_class.uri_schemes: updaters[e.ext_name] = updater_class if not updaters: diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index af1f38b1..602b2569 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import logging import os -import re # pylint: disable = W0402 import string # pylint: enable = W0402 diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index 4bc525c8..23c76f38 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -7,8 +7,6 @@ import pykka from mopidy import core from mopidy.models import Track, Album, Artist -from tests import path_to_data_dir - class LibraryControllerTest(object): artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()] @@ -17,13 +15,10 @@ class LibraryControllerTest(object): Album(name='album2', artists=artists[1:2]), Album()] tracks = [ - Track( - uri='file://' + path_to_data_dir('uri1'), name='track1', - artists=artists[:1], album=albums[0], date='2001-02-03', - length=4000), - Track( - uri='file://' + path_to_data_dir('uri2'), name='track2', - artists=artists[1:2], album=albums[1], date='2002', length=4000), + Track(uri='local:track:path1', name='track1', artists=artists[:1], + album=albums[0], date='2001-02-03', length=4000), + Track(uri='local:track:path2', name='track2', artists=artists[1:2], + album=albums[1], date='2002', length=4000), Track()] config = {} @@ -66,11 +61,11 @@ class LibraryControllerTest(object): self.assertEqual(list(result[0].tracks), []) def test_find_exact_uri(self): - track_1_uri = 'file://' + path_to_data_dir('uri1') + track_1_uri = 'local:track:path1' result = self.library.find_exact(uri=track_1_uri) self.assertEqual(list(result[0].tracks), self.tracks[:1]) - track_2_uri = 'file://' + path_to_data_dir('uri2') + track_2_uri = 'local:track:path2' result = self.library.find_exact(uri=track_2_uri) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) @@ -136,10 +131,10 @@ class LibraryControllerTest(object): self.assertEqual(list(result[0].tracks), []) def test_search_uri(self): - result = self.library.search(uri=['RI1']) + result = self.library.search(uri=['TH1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) - result = self.library.search(uri=['RI2']) + result = self.library.search(uri=['TH2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_track(self): @@ -183,7 +178,7 @@ class LibraryControllerTest(object): self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(any=['Bum1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) - result = self.library.search(any=['RI1']) + result = self.library.search(any=['TH1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) def test_search_wrong_type(self): diff --git a/tests/backends/local/__init__.py b/tests/backends/local/__init__.py index 684e12d8..ca93cdc0 100644 --- a/tests/backends/local/__init__.py +++ b/tests/backends/local/__init__.py @@ -2,8 +2,5 @@ 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) +generate_song = lambda i: 'local:track:song%s.wav' % i diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 4c699699..b12464bd 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -5,11 +5,10 @@ import unittest from mopidy.backends.local import actor from mopidy.core import PlaybackState from mopidy.models import Track -from mopidy.utils.path import path_to_uri -from tests import path_to_data_dir from tests.backends.base.playback import PlaybackControllerTest from tests.backends.local import generate_song +from tests import path_to_data_dir class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): @@ -24,25 +23,25 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): tracks = [ Track(uri=generate_song(i), length=4464) for i in range(1, 4)] - def add_track(self, path): - uri = path_to_uri(path_to_data_dir(path)) + def add_track(self, uri): track = Track(uri=uri, length=4464) self.tracklist.add([track]) def test_uri_scheme(self): - self.assertIn('file', self.core.uri_schemes) + self.assertNotIn('file', self.core.uri_schemes) + self.assertIn('local', self.core.uri_schemes) def test_play_mp3(self): - self.add_track('blank.mp3') + self.add_track('local:track:blank.mp3') self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_play_ogg(self): - self.add_track('blank.ogg') + self.add_track('local:track:blank.ogg') self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_play_flac(self): - self.add_track('blank.flac') + self.add_track('local:track:blank.flac') self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index 2882e476..591a9d1d 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -7,7 +7,7 @@ import unittest from mopidy.backends.local import actor from mopidy.models import Track -from mopidy.utils.path import path_to_uri +from mopidy.utils.path import path_to_uri, uri_to_path from tests import path_to_data_dir from tests.backends.base.playlists import ( @@ -89,21 +89,20 @@ class LocalPlaylistsControllerTest( def test_playlist_contents_is_written_to_disk(self): track = Track(uri=generate_song(1)) - track_path = track.uri[len('file://'):] playlist = self.core.playlists.create('test') - playlist_path = playlist.uri[len('file://'):] + playlist_path = os.path.join(self.playlists_dir, 'test.m3u') playlist = playlist.copy(tracks=[track]) playlist = self.core.playlists.save(playlist) with open(playlist_path) as playlist_file: contents = playlist_file.read() - self.assertEqual(track_path, contents.strip()) + self.assertEqual(track.uri, contents.strip()) def test_playlists_are_loaded_at_startup(self): playlist_path = os.path.join(self.playlists_dir, 'test.m3u') - track = Track(uri=path_to_uri(path_to_data_dir('uri2'))) + track = Track(uri='local:track:path2') playlist = self.core.playlists.create('test') playlist = playlist.copy(tracks=[track]) playlist = self.core.playlists.save(playlist) @@ -112,8 +111,7 @@ class LocalPlaylistsControllerTest( self.assert_(backend.playlists.playlists) self.assertEqual( - path_to_uri(playlist_path), - backend.playlists.playlists[0].uri) + 'local:playlist:test', backend.playlists.playlists[0].uri) self.assertEqual( playlist.name, backend.playlists.playlists[0].name) self.assertEqual( diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 4f958232..5ed07fca 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -98,7 +98,7 @@ expected_tracks = [] def generate_track(path, ident): - uri = path_to_uri(path_to_data_dir(path)) + uri = 'local:track:%s' % path track = Track( uri=uri, name='trackname', artists=expected_artists, album=expected_albums[0], track_no=1, date='2006', length=4000, @@ -126,11 +126,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase): def test_simple_cache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('simple_tag_cache'), path_to_data_dir('')) - uri = path_to_uri(path_to_data_dir('song1.mp3')) track = Track( - uri=uri, name='trackname', artists=expected_artists, track_no=1, - album=expected_albums[0], date='2006', length=4000, - last_modified=1272319626) + uri='local:track:song1.mp3', name='trackname', + artists=expected_artists, track_no=1, album=expected_albums[0], + date='2006', length=4000, last_modified=1272319626) self.assertEqual(set([track]), tracks) def test_advanced_cache(self): @@ -142,12 +141,11 @@ class MPDTagCacheToTracksTest(unittest.TestCase): tracks = parse_mpd_tag_cache( path_to_data_dir('utf8_tag_cache'), path_to_data_dir('')) - uri = path_to_uri(path_to_data_dir('song1.mp3')) artists = [Artist(name='æøå')] album = Album(name='æøå', artists=artists) track = Track( - uri=uri, name='æøå', artists=artists, album=album, length=4000, - last_modified=1272319626) + uri='local:track:song1.mp3', name='æøå', artists=artists, + album=album, length=4000, last_modified=1272319626) self.assertEqual(track, list(tracks)[0]) @@ -159,8 +157,8 @@ class MPDTagCacheToTracksTest(unittest.TestCase): def test_cache_with_blank_track_info(self): tracks = parse_mpd_tag_cache( path_to_data_dir('blank_tag_cache'), path_to_data_dir('')) - uri = path_to_uri(path_to_data_dir('song1.mp3')) - expected = Track(uri=uri, length=4000, last_modified=1272319626) + expected = Track( + uri='local:track:song1.mp3', length=4000, last_modified=1272319626) self.assertEqual(set([expected]), tracks) def test_musicbrainz_tagcache(self): @@ -183,10 +181,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase): def test_albumartist_tag_cache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('albumartist_tag_cache'), path_to_data_dir('')) - uri = path_to_uri(path_to_data_dir('song1.mp3')) artist = Artist(name='albumartistname') album = expected_albums[0].copy(artists=[artist]) track = Track( - uri=uri, name='trackname', artists=expected_artists, track_no=1, - album=album, date='2006', length=4000, last_modified=1272319626) + uri='local:track:song1.mp3', name='trackname', + artists=expected_artists, track_no=1, album=album, date='2006', + length=4000, last_modified=1272319626) self.assertEqual(track, list(tracks)[0]) diff --git a/tests/data/library_tag_cache b/tests/data/library_tag_cache index 50771a0a..9dc11777 100644 --- a/tests/data/library_tag_cache +++ b/tests/data/library_tag_cache @@ -3,22 +3,22 @@ mpd_version: 0.14.2 fs_charset: UTF-8 info_end songList begin -key: uri1 -file: /uri1 +key: key1 +file: /path1 Artist: artist1 Title: track1 Album: album1 Date: 2001-02-03 Time: 4 -key: uri2 -file: /uri2 +key: key1 +file: /path2 Artist: artist2 Title: track2 Album: album2 Date: 2002 Time: 4 -key: uri3 -file: /uri3 +key: key3 +file: /path3 Artist: artist3 Title: track3 Album: album3 From 6c7566a2f3460ef6fcc24ab842dd0d0cf8c1ebe0 Mon Sep 17 00:00:00 2001 From: alzeih Date: Tue, 30 Jul 2013 13:57:29 +1200 Subject: [PATCH 049/111] Strip invalid characters from playlist names for MPD frontend Fixes mopidy/mopidy$480 and mopidy/mopidy#474 --- mopidy/frontends/mpd/dispatcher.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 6590897d..7a4ee7d7 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -236,6 +236,9 @@ class MpdContext(object): #: The subsytems that we want to be notified about in idle mode. subscriptions = None + #regex for invalid characters in playlist names + _invalid_playlist_chars = re.compile(r'[\n\t\\/]') + def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher self.session = session @@ -248,10 +251,11 @@ class MpdContext(object): self.refresh_playlists_mapping() def create_unique_name(self, playlist_name): - name = playlist_name + stripped_name = self._invalid_playlist_chars.sub(' ', playlist_name) + name = stripped_name i = 2 while name in self._playlist_uri_from_name: - name = '%s [%d]' % (playlist_name, i) + name = '%s [%d]' % (stripped_name, i) i += 1 return name From f9da3fe3e922a8a8df1f893769b8f645952d74c2 Mon Sep 17 00:00:00 2001 From: Alexandre Petitjean Date: Tue, 30 Jul 2013 08:17:10 +0200 Subject: [PATCH 050/111] implement remarks for #481, also add scheme as requested in #472 --- mopidy/backends/spotify/session_manager.py | 6 ++++-- mopidy/config/__init__.py | 4 +++- mopidy/config/default.conf | 3 ++- mopidy/config/types.py | 1 - 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index dff29fad..130164fb 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -36,9 +36,11 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): full_proxy = '' if config['proxy']['hostname']: full_proxy = config['proxy']['hostname'] - # Add proxy port only if available + # Add proxy port and scheme only if available if config['proxy']['port']: - full_proxy = full_proxy + ':' + str(config['proxy']['port']) + full_proxy += ':' + str(config['proxy']['port']) + if config['proxy']['scheme']: + full_proxy = config['proxy']['scheme'] + "://" + full_proxy PyspotifySessionManager.__init__( self, config['spotify']['username'], config['spotify']['password'], diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 35a1d8e0..582a1a03 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -26,10 +26,12 @@ _audio_schema['mixer_track'] = String(optional=True) _audio_schema['output'] = String() _proxy_schema = ConfigSchema('proxy') +_proxy_schema['scheme'] = String(optional=True, + choices=['', 'https', 'socks4', 'socks5']) _proxy_schema['hostname'] = Hostname(optional=True) +_proxy_schema['port'] = Port() _proxy_schema['username'] = String(optional=True) _proxy_schema['password'] = Secret(optional=True) -_proxy_schema['port'] = Port() # NOTE: if multiple outputs ever comes something like LogLevelConfigSchema #_outputs_schema = config.AudioOutputConfigSchema() diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index 4a259c49..8d2101fb 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -13,7 +13,8 @@ mixer_track = output = autoaudiosink [proxy] +scheme = hostname = +port = username = password = -port = diff --git a/mopidy/config/types.py b/mopidy/config/types.py index 8309a591..ba8b8f2a 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -245,7 +245,6 @@ class Port(Integer): return value - class Path(ConfigValue): """File system path From bb3242d15aef348ddc3df2605c80bb136cfcd854 Mon Sep 17 00:00:00 2001 From: Alexandre Petitjean Date: Tue, 30 Jul 2013 08:37:24 +0200 Subject: [PATCH 051/111] correct default port config for proxy and deserialize function --- mopidy/config/default.conf | 2 +- mopidy/config/types.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index 8d2101fb..d241323a 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -15,6 +15,6 @@ output = autoaudiosink [proxy] scheme = hostname = -port = +port = http username = password = diff --git a/mopidy/config/types.py b/mopidy/config/types.py index ba8b8f2a..8715c8da 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -227,10 +227,6 @@ class Port(Integer): minimum=0, maximum=2 ** 16 - 1, choices=choices) def deserialize(self, value): - # in case of no value is given, just return nothing - if not len(value): - return value - # now we can try to convert try: value = int(value) except ValueError: @@ -238,10 +234,10 @@ class Port(Integer): value = socket.getservbyname(value, 'tcp') except socket.error: raise ValueError('must be a valid port number') - else: - validators.validate_choice(value, self._choices) - validators.validate_minimum(value, self._minimum) - validators.validate_maximum(value, self._maximum) + + validators.validate_choice(value, self._choices) + validators.validate_minimum(value, self._minimum) + validators.validate_maximum(value, self._maximum) return value From 6ac62c6869a690fc3cfa32b3ce5837d0811eb3aa Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 30 Jul 2013 22:59:22 +0200 Subject: [PATCH 052/111] local: Review fixes. --- mopidy/backends/base.py | 5 ++--- mopidy/backends/local/playback.py | 2 +- mopidy/ext.py | 5 +++++ mopidy/scanner.py | 6 +++--- tests/backends/local/__init__.py | 2 -- tests/backends/local/playback_test.py | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 3c1bbbf0..6b980f06 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -155,10 +155,9 @@ class BasePlaybackProvider(object): :type track: :class:`mopidy.models.Track` :rtype: :class:`True` if successful, else :class:`False` """ - self.audio.prepare_change() # TODO: add .get() to this? + self.audio.prepare_change() self.change_track(track) - self.audio.start_playback().get() - return True + return self.audio.start_playback().get() def change_track(self, track): """ diff --git a/mopidy/backends/local/playback.py b/mopidy/backends/local/playback.py index 8c40cb9e..eda06ff7 100644 --- a/mopidy/backends/local/playback.py +++ b/mopidy/backends/local/playback.py @@ -6,7 +6,7 @@ import os from mopidy.backends import base from mopidy.utils import path -logger = logging.getLogger('mopidy.backends.spotify') +logger = logging.getLogger('mopidy.backends.local') class LocalPlaybackProvider(base.BasePlaybackProvider): diff --git a/mopidy/ext.py b/mopidy/ext.py index 4b6e4502..22daa3cb 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -80,6 +80,11 @@ class Extension(object): return [] def get_library_updaters(self): + """List of library updater classes + + :returns: list of :class:`~mopidy.backends.base.BaseLibraryUpdateProvider` + subclasses + """ return [] def register_gstreamer_elements(self): diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 1752ece9..f87407f8 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -62,11 +62,11 @@ def main(): updaters[e.ext_name] = updater_class if not updaters: - logging.error('No usable updaters found.') + logging.error('No usable library updaters found.') return elif len(updaters) > 1: - names = ', '.join(updaters.keys()) - logging.error('More than one updater found. Provided by: %s', names) + logging.error('More than one library updater found. ' + 'Provided by: %s', ', '.join(updaters.keys())) return local_updater = updaters.values()[0](config) # TODO: switch to actor? diff --git a/tests/backends/local/__init__.py b/tests/backends/local/__init__.py index ca93cdc0..1738722f 100644 --- a/tests/backends/local/__init__.py +++ b/tests/backends/local/__init__.py @@ -1,6 +1,4 @@ from __future__ import unicode_literals -from mopidy.utils.path import path_to_uri - generate_song = lambda i: 'local:track:song%s.wav' % i diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index b12464bd..530f09c8 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -6,9 +6,9 @@ from mopidy.backends.local import actor from mopidy.core import PlaybackState from mopidy.models import Track +from tests import path_to_data_dir from tests.backends.base.playback import PlaybackControllerTest from tests.backends.local import generate_song -from tests import path_to_data_dir class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): From 4f310f51fc7262317886921402a969de517293a2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 30 Jul 2013 23:21:08 +0200 Subject: [PATCH 053/111] Change docs license to Apache License, Version 2.0 I've looked through all contributions to the docs. Any contributions not by Thomas Adamcik or myself are trivial (e.g. typo fixes) or have mostly been replaced by later changes. Thus I'm comfortable with changing the license with the agreement of Thomas Adamcik. By changing the documentation license to be the same as the source code license, we avoid ambiguities around the license of e.g. docstrings which are written as part of the source code but are included in the generated docs. --- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/license.rst | 10 ++++++++++ docs/licenses.rst | 34 ---------------------------------- 4 files changed, 12 insertions(+), 36 deletions(-) create mode 100644 docs/license.rst delete mode 100644 docs/licenses.rst diff --git a/docs/conf.py b/docs/conf.py index c0dba916..f3e4166c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,7 +101,7 @@ master_doc = 'index' # General information about the project. project = 'Mopidy' -copyright = '2010-2013, Stein Magnus Jodal and contributors' +copyright = '2009-2013, 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 diff --git a/docs/index.rst b/docs/index.rst index fb91244d..ca40c96c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,7 +48,7 @@ About :maxdepth: 1 authors - licenses + license changelog versioning diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 00000000..98928f63 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,10 @@ +******* +License +******* + +Mopidy is copyright 2009-2013 Stein Magnus Jodal and contributors. For a list +of contributors, see :doc:`authors`. For details on who have contributed what, +please refer to our git repository. + +Mopidy is licensed under the `Apache License, Version 2.0 +`_. diff --git a/docs/licenses.rst b/docs/licenses.rst deleted file mode 100644 index fc2530e5..00000000 --- a/docs/licenses.rst +++ /dev/null @@ -1,34 +0,0 @@ -******** -Licenses -******** - -For a list of contributors, see :doc:`authors`. For details on who have -contributed what, please refer to our git repository. - -Source code license -=================== - -Copyright 2009-2013 Stein Magnus Jodal and contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - -Documentation license -===================== - -Copyright 2010-2013 Stein Magnus Jodal and contributors - -This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 -Unported License. To view a copy of this license, visit -http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative -Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. From 786d7c1ae0adcc5b0e0652ab767256777393706d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 30 Jul 2013 23:21:08 +0200 Subject: [PATCH 054/111] Change docs license to Apache License, Version 2.0 I've looked through all contributions to the docs. Any contributions not by Thomas Adamcik or myself are trivial (e.g. typo fixes) or have mostly been replaced by later changes. Thus I'm comfortable with changing the license with the agreement of Thomas Adamcik. By changing the documentation license to be the same as the source code license, we avoid ambiguities around the license of e.g. docstrings which are written as part of the source code but are included in the generated docs. --- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/license.rst | 10 ++++++++++ docs/licenses.rst | 34 ---------------------------------- 4 files changed, 12 insertions(+), 36 deletions(-) create mode 100644 docs/license.rst delete mode 100644 docs/licenses.rst diff --git a/docs/conf.py b/docs/conf.py index c0dba916..f3e4166c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,7 +101,7 @@ master_doc = 'index' # General information about the project. project = 'Mopidy' -copyright = '2010-2013, Stein Magnus Jodal and contributors' +copyright = '2009-2013, 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 diff --git a/docs/index.rst b/docs/index.rst index fb91244d..ca40c96c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,7 +48,7 @@ About :maxdepth: 1 authors - licenses + license changelog versioning diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 00000000..98928f63 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,10 @@ +******* +License +******* + +Mopidy is copyright 2009-2013 Stein Magnus Jodal and contributors. For a list +of contributors, see :doc:`authors`. For details on who have contributed what, +please refer to our git repository. + +Mopidy is licensed under the `Apache License, Version 2.0 +`_. diff --git a/docs/licenses.rst b/docs/licenses.rst deleted file mode 100644 index fc2530e5..00000000 --- a/docs/licenses.rst +++ /dev/null @@ -1,34 +0,0 @@ -******** -Licenses -******** - -For a list of contributors, see :doc:`authors`. For details on who have -contributed what, please refer to our git repository. - -Source code license -=================== - -Copyright 2009-2013 Stein Magnus Jodal and contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - -Documentation license -===================== - -Copyright 2010-2013 Stein Magnus Jodal and contributors - -This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 -Unported License. To view a copy of this license, visit -http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative -Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. From 97848bc1a2bf80896680ad45bac9933e3f782d83 Mon Sep 17 00:00:00 2001 From: Alexandre Petitjean Date: Wed, 31 Jul 2013 10:05:20 +0200 Subject: [PATCH 055/111] String config value can be optionnal AND have a choices list Integer value can be optionnal --- mopidy/backends/spotify/session_manager.py | 1 - mopidy/config/__init__.py | 4 ++-- mopidy/config/default.conf | 2 +- mopidy/config/types.py | 26 +++++++--------------- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 130164fb..125b6ada 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -36,7 +36,6 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): full_proxy = '' if config['proxy']['hostname']: full_proxy = config['proxy']['hostname'] - # Add proxy port and scheme only if available if config['proxy']['port']: full_proxy += ':' + str(config['proxy']['port']) if config['proxy']['scheme']: diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 582a1a03..dfdae6a7 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -27,9 +27,9 @@ _audio_schema['output'] = String() _proxy_schema = ConfigSchema('proxy') _proxy_schema['scheme'] = String(optional=True, - choices=['', 'https', 'socks4', 'socks5']) + choices=['http', 'https', 'socks4', 'socks5']) _proxy_schema['hostname'] = Hostname(optional=True) -_proxy_schema['port'] = Port() +_proxy_schema['port'] = Port(optional=True) _proxy_schema['username'] = String(optional=True) _proxy_schema['password'] = Secret(optional=True) diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index d241323a..3899c2c5 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -15,6 +15,6 @@ output = autoaudiosink [proxy] scheme = hostname = -port = http +port = username = password = diff --git a/mopidy/config/types.py b/mopidy/config/types.py index 8715c8da..29651940 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -71,9 +71,9 @@ class String(ConfigValue): def deserialize(self, value): value = decode(value).strip() validators.validate_required(value, self._required) - validators.validate_choice(value, self._choices) if not value: return None + validators.validate_choice(value, self._choices) return value def serialize(self, value, display=False): @@ -111,12 +111,16 @@ class Secret(ConfigValue): class Integer(ConfigValue): """Integer value.""" - def __init__(self, minimum=None, maximum=None, choices=None): + def __init__(self, minimum=None, maximum=None, choices=None, optional=False): + self._required = not optional self._minimum = minimum self._maximum = maximum self._choices = choices def deserialize(self, value): + validators.validate_required(value, self._required) + if not value: + return None value = int(value) validators.validate_choice(value, self._choices) validators.validate_minimum(value, self._minimum) @@ -222,23 +226,9 @@ class Port(Integer): allocate a port for us. """ # TODO: consider probing if port is free or not? - def __init__(self, choices=None): + def __init__(self, choices=None, optional=False): super(Port, self).__init__( - minimum=0, maximum=2 ** 16 - 1, choices=choices) - - def deserialize(self, value): - try: - value = int(value) - except ValueError: - try: - value = socket.getservbyname(value, 'tcp') - except socket.error: - raise ValueError('must be a valid port number') - - validators.validate_choice(value, self._choices) - validators.validate_minimum(value, self._minimum) - validators.validate_maximum(value, self._maximum) - return value + minimum=0, maximum=2 ** 16 - 1, choices=choices, optional=optional) class Path(ConfigValue): From c66e2a5b0fce548a653bab26245da38f84c52da0 Mon Sep 17 00:00:00 2001 From: Alexandre Petitjean Date: Wed, 31 Jul 2013 23:11:29 +0200 Subject: [PATCH 056/111] add some tests about optionnal integer and optionnal+choices string --- tests/config/types_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/config/types_test.py b/tests/config/types_test.py index 88c8f067..d915b4f2 100644 --- a/tests/config/types_test.py +++ b/tests/config/types_test.py @@ -98,6 +98,11 @@ class StringTest(unittest.TestCase): self.assertIsInstance(result, bytes) self.assertEqual(b'', result) + def test_deserialize_enforces_choices_optionnal(self): + value = types.String(optional=True, choices=['foo', 'bar', 'baz']) + self.assertEqual(None, value.deserialize(b'')) + self.assertRaises(ValueError, value.deserialize, b'foobar') + class SecretTest(unittest.TestCase): def test_deserialize_passes_through(self): @@ -163,6 +168,10 @@ class IntegerTest(unittest.TestCase): self.assertEqual(5, value.deserialize('5')) self.assertRaises(ValueError, value.deserialize, '15') + def test_deserialize_respects_optional(self): + value = types.Integer(optional=True) + self.assertEqual(None, value.deserialize('')) + class BooleanTest(unittest.TestCase): def test_deserialize_conversion_success(self): From 3c6a0543f51138119daf27e8674f24b2fd28b4c9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 31 Jul 2013 23:52:24 +0200 Subject: [PATCH 057/111] Scanner: Fix deadlock on incorrectly identified files --- mopidy/backends/local/__init__.py | 1 + mopidy/backends/local/ext.conf | 1 + mopidy/scanner.py | 22 ++++++++++++++++++++-- tests/data/scanner/example.log | Bin 0 -> 206 bytes tests/scanner_test.py | 4 ++++ 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tests/data/scanner/example.log diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index f718eeb5..cf982220 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -18,6 +18,7 @@ class Extension(ext.Extension): def get_config_schema(self): schema = super(Extension, self).get_config_schema() + schema['scan_timeout'] = config.Integer(minimum=0) schema['media_dir'] = config.Path() schema['playlists_dir'] = config.Path() schema['tag_cache_file'] = config.Path() diff --git a/mopidy/backends/local/ext.conf b/mopidy/backends/local/ext.conf index 54c3ab78..ae30438f 100644 --- a/mopidy/backends/local/ext.conf +++ b/mopidy/backends/local/ext.conf @@ -1,5 +1,6 @@ [local] enabled = true +scan_timeout = 1000 media_dir = $XDG_MUSIC_DIR playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 9f13d454..cdfdad96 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -97,9 +97,14 @@ def main(): logging.warning('Failed %s: %s', uri, error) logging.debug('Debug info for %s: %s', uri, debug) + if not config['local']['scan_timeout']: + scan_timeout = 1000 + else: + scan_timeout = config['local']['scan_timeout'] + logging.info('Scanning new and modified tracks.') # TODO: just pass the library in instead? - scanner = Scanner(uris_update, store, debug) + scanner = Scanner(uris_update, store, debug, scan_timeout) try: scanner.start() except KeyboardInterrupt: @@ -176,12 +181,14 @@ def translator(data): class Scanner(object): - def __init__(self, uris, data_callback, error_callback=None): + def __init__(self, uris, data_callback, error_callback=None, scan_timeout=1000): self.data = {} self.uris = iter(uris) self.data_callback = data_callback self.error_callback = error_callback + self.scan_timeout = scan_timeout self.loop = gobject.MainLoop() + self.timeout_id = None self.fakesink = gst.element_factory_make('fakesink') self.fakesink.set_property('signal-handoffs', True) @@ -252,6 +259,13 @@ class Scanner(object): self.error_callback(uri, error, debug) self.next_uri() + def process_timeout(self): + if self.error_callback: + uri = self.uribin.get_property('uri') + self.error_callback(uri, 'Processing timeout after %i seconds' % self.timeout, 'debug') + self.next_uri() + return True + def get_duration(self): self.pipe.get_state() # Block until state change is done. try: @@ -262,6 +276,9 @@ class Scanner(object): def next_uri(self): self.data = {} + if self.timeout_id: + gobject.source_remove(self.timeout_id) + self.timeout_id = None try: uri = next(self.uris) except StopIteration: @@ -269,6 +286,7 @@ class Scanner(object): return False self.pipe.set_state(gst.STATE_NULL) self.uribin.set_property('uri', uri) + self.timeout_id = gobject.timeout_add(self.scan_timeout, self.process_timeout) self.pipe.set_state(gst.STATE_PLAYING) return True diff --git a/tests/data/scanner/example.log b/tests/data/scanner/example.log new file mode 100644 index 0000000000000000000000000000000000000000..c49a044d01813d3ce57031f8fc41fe7e7abb2974 GIT binary patch literal 206 zcmYL@%L>9U5Jk^g@E`mD5z~sGJ1Jedbze1>f{&sV^yk&nbRl7qn_=#`^Zk^(s5sCu zVZ)On4L5b+-SSrL2+WwP_C)6@Rc@WQJ9ANGtehD9wTwriWqN{V%qUeUzi=v=*y$Zo j`?Rawvu`c4QAPiMdSH?0TOIvA12<=?4sNoxvrGv;5;`AM literal 0 HcmV?d00001 diff --git a/tests/scanner_test.py b/tests/scanner_test.py index dcf891c0..903d6510 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -210,6 +210,10 @@ class ScannerTest(unittest.TestCase): self.scan('scanner/image') self.assert_(self.errors) + def test_log_file_is_ignored(self): + self.scan('scanner/example.log') + self.assert_(self.errors) + @unittest.SkipTest def test_song_without_time_is_handeled(self): pass From 20729eabe309083c05e78074d75a802f6c4efe47 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 2 Jun 2013 15:56:25 +0200 Subject: [PATCH 058/111] audio: Add visualiser support and adjust playbin flags Sets up playbin to use just audio, soft_volume and downloading. --- mopidy/audio/actor.py | 34 +++++++++++++++++++++++++++++++--- mopidy/config/__init__.py | 1 + mopidy/config/default.conf | 1 + 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index af0a0c68..d78df9b7 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -22,6 +22,22 @@ mixers.register_mixers() MB = 1 << 20 +# GST_PLAY_FLAG_VIDEO (1<<0) +# GST_PLAY_FLAG_AUDIO (1<<1) +# GST_PLAY_FLAG_TEXT (1<<2) +# GST_PLAY_FLAG_VIS (1<<3) +# GST_PLAY_FLAG_SOFT_VOLUME (1<<4) +# GST_PLAY_FLAG_NATIVE_AUDIO (1<<5) +# GST_PLAY_FLAG_NATIVE_VIDEO (1<<6) +# GST_PLAY_FLAG_DOWNLOAD (1<<7) +# GST_PLAY_FLAG_BUFFERING (1<<8) +# GST_PLAY_FLAG_DEINTERLACE (1<<9) +# GST_PLAY_FLAG_SOFT_COLORBALANCE (1<<10) + +# Default flags to use for playbin: AUDIO, SOFT_VOLUME, DOWNLOAD +PLAYBIN_FLAGS = (1<<1) | (1<<4) | (1<<7) +PLAYBIN_VIS_FLAGS = PLAYBIN_FLAGS | (1<<3) + class Audio(pykka.ThreadingActor): """ @@ -55,6 +71,7 @@ class Audio(pykka.ThreadingActor): try: self._setup_playbin() self._setup_output() + self._setup_visualizer() self._setup_mixer() self._setup_message_processor() except gobject.GError as ex: @@ -78,9 +95,7 @@ class Audio(pykka.ThreadingActor): def _setup_playbin(self): playbin = gst.element_factory_make('playbin2') - - fakesink = gst.element_factory_make('fakesink') - playbin.set_property('video-sink', fakesink) + playbin.set_property('flags', PLAYBIN_FLAGS) self._connect(playbin, 'about-to-finish', self._on_about_to_finish) self._connect(playbin, 'notify::source', self._on_new_source) @@ -149,6 +164,19 @@ class Audio(pykka.ThreadingActor): 'Failed to create audio output "%s": %s', output_desc, ex) process.exit_process() + def _setup_visualizer(self): + visualizer_element = self._config['audio']['visualizer'] + if not visualizer_element: + return + try: + visualizer = gst.element_factory_make(visualizer_element) + self._playbin.set_property('vis-plugin', visualizer) + self._playbin.set_property('flags', PLAYBIN_VIS_FLAGS) + logger.info('Audio visualizer set to "%s"', visualizer_element) + except gobject.GError as ex: + logger.error( + 'Failed to create audio visualizer "%s": %s', visualizer_element, ex) + def _setup_mixer(self): mixer_desc = self._config['audio']['mixer'] track_desc = self._config['audio']['mixer_track'] diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index e9ae7d86..60e8cb49 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -24,6 +24,7 @@ _audio_schema = ConfigSchema('audio') _audio_schema['mixer'] = String() _audio_schema['mixer_track'] = String(optional=True) _audio_schema['output'] = String() +_audio_schema['visualizer'] = String(optional=True) _proxy_schema = ConfigSchema('proxy') _proxy_schema['hostname'] = Hostname(optional=True) diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index b525ef47..d4bc0daf 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -11,6 +11,7 @@ pykka = info mixer = autoaudiomixer mixer_track = output = autoaudiosink +visualizer = [proxy] hostname = From a4d2f28ade7adfb0883878d88dc4c8387840f8eb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 1 Aug 2013 01:09:01 +0200 Subject: [PATCH 059/111] audio: Fix tests with respect to visualizer support --- tests/audio/actor_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py index c311bdc3..617131cc 100644 --- a/tests/audio/actor_test.py +++ b/tests/audio/actor_test.py @@ -21,6 +21,7 @@ class AudioTest(unittest.TestCase): 'mixer': 'fakemixer track_max_volume=65536', 'mixer_track': None, 'output': 'fakesink', + 'visualizer': None, } } self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) @@ -70,6 +71,7 @@ class AudioTest(unittest.TestCase): 'mixer': 'fakemixer track_max_volume=40', 'mixer_track': None, 'output': 'fakesink', + 'visualizer': None, } } self.audio = audio.Audio.start(config=config).proxy() From c5c2d10455d924f7cb8130bb0f7c517e8df00dd1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 1 Aug 2013 01:53:07 +0200 Subject: [PATCH 060/111] audio: Update changelog and docs for visualizer. --- docs/changelog.rst | 5 +++++ docs/config.rst | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a8c9dee6..8c88e8c1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,6 +33,11 @@ v0.15.0 (UNRELEASED) hierarchy from your Spotify account is available in Mopidy. (Fixes: :issue:`62`) +**Audio** + +- Added support for viusalization. :confval:`audio/visualizer` can now be set + to GStreamer visualizers. + v0.14.2 (2013-07-01) ==================== diff --git a/docs/config.rst b/docs/config.rst index 9f997b23..6fd7579d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -90,6 +90,16 @@ Core configuration values ``gst-inspect-0.10`` to see what output properties can be set on the sink. For example: ``gst-inspect-0.10 shout2send`` +.. confval:: audio/visualizer + + Visualizer to use. + + Can be left blank if no visualizer is desired. Otherwise this expects a + GStreamer visualizer. Typical values are ``monoscope``, ``goom``, + ``goom2k1`` or one of the `libvisual`_ visualizers. + +.. _libvisual: http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-plugin-libvisual.html + .. confval:: logging/console_format The log format used for informational logging. From da86015a2b400e2be0fb27838527faeb4d202ad9 Mon Sep 17 00:00:00 2001 From: AlexandreP2101 Date: Thu, 1 Aug 2013 08:47:22 +0200 Subject: [PATCH 061/111] Correct typo --- tests/config/types_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/config/types_test.py b/tests/config/types_test.py index d915b4f2..74e9672d 100644 --- a/tests/config/types_test.py +++ b/tests/config/types_test.py @@ -98,7 +98,7 @@ class StringTest(unittest.TestCase): self.assertIsInstance(result, bytes) self.assertEqual(b'', result) - def test_deserialize_enforces_choices_optionnal(self): + def test_deserialize_enforces_choices_optional(self): value = types.String(optional=True, choices=['foo', 'bar', 'baz']) self.assertEqual(None, value.deserialize(b'')) self.assertRaises(ValueError, value.deserialize, b'foobar') From dee0d01e20db0be447ce0fa6ef714c11feaaea88 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 1 Aug 2013 09:31:44 +0200 Subject: [PATCH 062/111] Throw an error when scan_timeout config is not loaded. --- mopidy/scanner.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index cdfdad96..d280785c 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -98,9 +98,9 @@ def main(): logging.debug('Debug info for %s: %s', uri, debug) if not config['local']['scan_timeout']: - scan_timeout = 1000 - else: - scan_timeout = config['local']['scan_timeout'] + self.error_callback(uri, 'Config value local/scan_timeout is not set.', 'debug') + + scan_timeout = config['local']['scan_timeout'] logging.info('Scanning new and modified tracks.') # TODO: just pass the library in instead? @@ -262,7 +262,7 @@ class Scanner(object): def process_timeout(self): if self.error_callback: uri = self.uribin.get_property('uri') - self.error_callback(uri, 'Processing timeout after %i seconds' % self.timeout, 'debug') + self.error_callback(uri, 'Processing timeout after %i seconds' % self.scan_timeout, 'debug') self.next_uri() return True From ac9acaabf0efb630aa746873ccaf43b09d65fc53 Mon Sep 17 00:00:00 2001 From: alzeih Date: Fri, 2 Aug 2013 01:32:07 +1200 Subject: [PATCH 063/111] typo in regex --- mopidy/frontends/mpd/dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 7a4ee7d7..00e5747a 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -237,7 +237,7 @@ class MpdContext(object): subscriptions = None #regex for invalid characters in playlist names - _invalid_playlist_chars = re.compile(r'[\n\t\\/]') + _invalid_playlist_chars = re.compile(r'[\n\r\\/]') def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher From e637eb90416154a3bf43cb05a8db9d0ec84ff452 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 2 Aug 2013 00:01:45 +0200 Subject: [PATCH 064/111] docs: Update changelog, authors --- .mailmap | 2 ++ AUTHORS | 1 + docs/changelog.rst | 3 +++ 3 files changed, 6 insertions(+) diff --git a/.mailmap b/.mailmap index b38c3f66..7098b6d4 100644 --- a/.mailmap +++ b/.mailmap @@ -5,3 +5,5 @@ Kristian Klette Johannes Knutsen Johannes Knutsen John Bäckstrand +Alexandre Petitjean +Alexandre Petitjean diff --git a/AUTHORS b/AUTHORS index 1e3faf0e..ddb1602e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,3 +22,4 @@ - Janez Troha - Tobias Sauerwein - alzeih +- Alexandre Petitjean diff --git a/docs/changelog.rst b/docs/changelog.rst index a8c9dee6..c2c121d5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,6 +33,9 @@ v0.15.0 (UNRELEASED) hierarchy from your Spotify account is available in Mopidy. (Fixes: :issue:`62`) +- Fix proxy config values that was broken with the config system change in + 0.14. (Fixes: :issue:`472`) + v0.14.2 (2013-07-01) ==================== From 3f1192e95be03a226a65aa3a30a42ed34e155909 Mon Sep 17 00:00:00 2001 From: alzeih Date: Fri, 2 Aug 2013 13:38:52 +1200 Subject: [PATCH 065/111] Match MPD implementation and add tests --- mopidy/frontends/mpd/dispatcher.py | 2 +- tests/frontends/mpd/dispatcher_test.py | 30 +++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 00e5747a..52504440 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -237,7 +237,7 @@ class MpdContext(object): subscriptions = None #regex for invalid characters in playlist names - _invalid_playlist_chars = re.compile(r'[\n\r\\/]') + _invalid_playlist_chars = re.compile(r'[\n\r/]') def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 9ef88e44..1543b64f 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -6,7 +6,7 @@ import pykka from mopidy import core from mopidy.backends import dummy -from mopidy.frontends.mpd.dispatcher import MpdDispatcher +from mopidy.frontends.mpd.dispatcher import MpdDispatcher, MpdContext from mopidy.frontends.mpd.exceptions import MpdAckError from mopidy.frontends.mpd.protocol import request_handlers, handle_request @@ -63,3 +63,31 @@ class MpdDispatcherTest(unittest.TestCase): result = self.dispatcher.handle_request('known request') self.assertIn('OK', result) self.assertIn(expected, result) + + +class MpdContextTest(unittest.TestCase): + def setUp(self): + config = { + 'mpd': { + 'password': None, + } + } + self.backend = dummy.create_dummy_backend_proxy() + self.core = core.Core.start(backends=[self.backend]).proxy() + self.dispatcher = MpdDispatcher(config=config) + self.context = self.dispatcher.context + + def tearDown(self): + pykka.ActorRegistry.stop_all() + + def test_context_create_unique_name_replaces_newlines_with_space(self): + result = self.context.create_unique_name("playlist name\n") + self.assertEqual("playlist name ", result) + + def test_context_create_unique_name_replaces_carriage_returns_with_space(self): + result = self.context.create_unique_name("playlist name\r") + self.assertEqual("playlist name ", result) + + def test_context_create_unique_name_replaces_forward_slashes_with_space(self): + result = self.context.create_unique_name("playlist name/") + self.assertEqual("playlist name ", result) From dc7289ba8303a3c759053d5f72dda986d3ae88fc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 4 Aug 2013 11:22:36 +0200 Subject: [PATCH 066/111] docs: Fix formatting of code examples in lists --- js/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/README.md b/js/README.md index 0e5e17c9..793e9f9d 100644 --- a/js/README.md +++ b/js/README.md @@ -51,15 +51,15 @@ Building from source 1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're running Ubuntu: - sudo apt-get install python-software-properties - sudo add-apt-repository ppa:chris-lea/node.js - sudo apt-get update - sudo apt-get install nodejs npm + sudo apt-get install python-software-properties + sudo add-apt-repository ppa:chris-lea/node.js + sudo apt-get update + sudo apt-get install nodejs npm 2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies: - cd js/ - npm install + cd js/ + npm install That's it. From 95c8077135a9b80cbbb291fe3c8cbe062b1a9b73 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 4 Aug 2013 11:24:00 +0200 Subject: [PATCH 067/111] docs: nodejs deb now includes npm --- js/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/README.md b/js/README.md index 793e9f9d..eddfa99f 100644 --- a/js/README.md +++ b/js/README.md @@ -54,7 +54,7 @@ Building from source sudo apt-get install python-software-properties sudo add-apt-repository ppa:chris-lea/node.js sudo apt-get update - sudo apt-get install nodejs npm + sudo apt-get install nodejs 2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies: From 16a8886617f9a9df8109bc30aac2bb1a5720f91f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 5 Aug 2013 22:45:45 +0200 Subject: [PATCH 068/111] docs: Update changelog with respect to local uri scheme and plugable library updaters. --- docs/changelog.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index c3a80955..f5b6f6ab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -32,6 +32,14 @@ v0.15.0 (UNRELEASED) - An album's number of discs and a track's disc number are now extracted when scanning your music collection. +- Added support for plugable library updaters. This allows extension writers + to start providing their own custom libraries instead of being stuck with + just our tag cache as the only option. + +- Converted local backend to use new `local:playlist:path` and + `local:track:path` uri scheme. Also moves support of `file://` to streaming + backend. + **Spotify backend** - Prepend playlist folder names to the playlist name, so that the playlist From 66f367048d2a0e2249ad99eb8194393eebe3fffa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 5 Aug 2013 23:09:04 +0200 Subject: [PATCH 069/111] docs: Document local/scan_timeout config value --- docs/ext/local.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ext/local.rst b/docs/ext/local.rst index fc89e69a..1abebb1d 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -47,6 +47,11 @@ Configuration values Path to tag cache for local media. +.. confval:: local/scan_timeout + + Number of milliseconds before giving up scanning a file and moving on to + the next file. + Usage ===== From a5d8f02454cb938cdca25703c048b137d1011683 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 5 Aug 2013 23:18:07 +0200 Subject: [PATCH 070/111] scanner: Fix local/scan_timeout value check --- mopidy/scanner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index d280785c..53ded736 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -54,6 +54,10 @@ def main(): logging.warning('Config value local/media_dir is not set.') return + if not config['local']['scan_timeout']: + logging.warning('Config value local/scan_timeout is not set.') + return + # TODO: missing config error checking and other default setup code. audio = dummy_audio.DummyAudio() @@ -97,9 +101,6 @@ def main(): logging.warning('Failed %s: %s', uri, error) logging.debug('Debug info for %s: %s', uri, debug) - if not config['local']['scan_timeout']: - self.error_callback(uri, 'Config value local/scan_timeout is not set.', 'debug') - scan_timeout = config['local']['scan_timeout'] logging.info('Scanning new and modified tracks.') From 022bcd089fe9e0a227b3d308371968adf2ace998 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 5 Aug 2013 23:18:37 +0200 Subject: [PATCH 071/111] scanner: Fix unregistering of the timeout callback --- mopidy/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 53ded736..5aeb2768 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -265,7 +265,7 @@ class Scanner(object): uri = self.uribin.get_property('uri') self.error_callback(uri, 'Processing timeout after %i seconds' % self.scan_timeout, 'debug') self.next_uri() - return True + return False def get_duration(self): self.pipe.get_state() # Block until state change is done. From e2bec7922011cd78e512db16fb9b893519813ed0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 5 Aug 2013 23:19:39 +0200 Subject: [PATCH 072/111] scanner: Reorder config values --- mopidy/backends/local/__init__.py | 2 +- mopidy/backends/local/ext.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index cf982220..3e2561a1 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -18,10 +18,10 @@ class Extension(ext.Extension): def get_config_schema(self): schema = super(Extension, self).get_config_schema() - schema['scan_timeout'] = config.Integer(minimum=0) schema['media_dir'] = config.Path() schema['playlists_dir'] = config.Path() schema['tag_cache_file'] = config.Path() + schema['scan_timeout'] = config.Integer(minimum=0) return schema def validate_environment(self): diff --git a/mopidy/backends/local/ext.conf b/mopidy/backends/local/ext.conf index ae30438f..7e0f0f2b 100644 --- a/mopidy/backends/local/ext.conf +++ b/mopidy/backends/local/ext.conf @@ -1,6 +1,6 @@ [local] enabled = true -scan_timeout = 1000 media_dir = $XDG_MUSIC_DIR playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache +scan_timeout = 1000 From f35b45bf579da0721fbe07f0da42870e5c1e28b6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 5 Aug 2013 23:24:43 +0200 Subject: [PATCH 073/111] scanner: Fix time unit in log message. Formatting --- mopidy/scanner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 5aeb2768..669f8182 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -263,7 +263,8 @@ class Scanner(object): def process_timeout(self): if self.error_callback: uri = self.uribin.get_property('uri') - self.error_callback(uri, 'Processing timeout after %i seconds' % self.scan_timeout, 'debug') + self.error_callback( + uri, 'Scan timed out after %d ms' % self.scan_timeout, None) self.next_uri() return False From dc291186390f7ef30c577ab4c98b7d859cd04815 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 5 Aug 2013 23:32:11 +0200 Subject: [PATCH 074/111] docs: Update changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index c3a80955..73828c23 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -32,6 +32,10 @@ v0.15.0 (UNRELEASED) - An album's number of discs and a track's disc number are now extracted when scanning your music collection. +- The scanner now gives up scanning a file after a second, and continues with + the next file. This fixes some hangs on non-media files, like logs. (Fixes: + :issue:`476`, :issue:`483`) + **Spotify backend** - Prepend playlist folder names to the playlist name, so that the playlist From 7182f98843d76f6d98ed7a43882cc1f5b0f1d307 Mon Sep 17 00:00:00 2001 From: alzeih Date: Tue, 6 Aug 2013 21:14:23 +1200 Subject: [PATCH 075/111] cleanup tests and code - move tests to tests/frontends/mpd/stored_playlists_test.py stored_playlists_test.py: - rename test methods names to remove _config - remove unnecessary imports, setup, teardown, variables - sort remaining imports mopidy/frontends/mpd/dispatcher.py: - remove regex comment --- mopidy/frontends/mpd/dispatcher.py | 1 - tests/frontends/mpd/dispatcher_test.py | 30 +------------------- tests/frontends/mpd/stored_playlists_test.py | 28 ++++++++++++++++++ 3 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 tests/frontends/mpd/stored_playlists_test.py diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 52504440..a4cfb5ce 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -236,7 +236,6 @@ class MpdContext(object): #: The subsytems that we want to be notified about in idle mode. subscriptions = None - #regex for invalid characters in playlist names _invalid_playlist_chars = re.compile(r'[\n\r/]') def __init__(self, dispatcher, session=None, config=None, core=None): diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 1543b64f..9ef88e44 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -6,7 +6,7 @@ import pykka from mopidy import core from mopidy.backends import dummy -from mopidy.frontends.mpd.dispatcher import MpdDispatcher, MpdContext +from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.exceptions import MpdAckError from mopidy.frontends.mpd.protocol import request_handlers, handle_request @@ -63,31 +63,3 @@ class MpdDispatcherTest(unittest.TestCase): result = self.dispatcher.handle_request('known request') self.assertIn('OK', result) self.assertIn(expected, result) - - -class MpdContextTest(unittest.TestCase): - def setUp(self): - config = { - 'mpd': { - 'password': None, - } - } - self.backend = dummy.create_dummy_backend_proxy() - self.core = core.Core.start(backends=[self.backend]).proxy() - self.dispatcher = MpdDispatcher(config=config) - self.context = self.dispatcher.context - - def tearDown(self): - pykka.ActorRegistry.stop_all() - - def test_context_create_unique_name_replaces_newlines_with_space(self): - result = self.context.create_unique_name("playlist name\n") - self.assertEqual("playlist name ", result) - - def test_context_create_unique_name_replaces_carriage_returns_with_space(self): - result = self.context.create_unique_name("playlist name\r") - self.assertEqual("playlist name ", result) - - def test_context_create_unique_name_replaces_forward_slashes_with_space(self): - result = self.context.create_unique_name("playlist name/") - self.assertEqual("playlist name ", result) diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py new file mode 100644 index 00000000..2c647681 --- /dev/null +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals + +import unittest + +from mopidy.frontends.mpd.dispatcher import MpdContext, MpdDispatcher + + +class MpdContextTest(unittest.TestCase): + def setUp(self): + config = { + 'mpd': { + 'password': None, + } + } + dispatcher = MpdDispatcher(config=config) + self.context = dispatcher.context + + def test_create_unique_name_replaces_newlines_with_space(self): + result = self.context.create_unique_name("playlist name\n") + self.assertEqual("playlist name ", result) + + def test_create_unique_name_replaces_carriage_returns_with_space(self): + result = self.context.create_unique_name("playlist name\r") + self.assertEqual("playlist name ", result) + + def test_create_unique_name_replaces_forward_slashes_with_space(self): + result = self.context.create_unique_name("playlist name/") + self.assertEqual("playlist name ", result) From 333f1c0dbfe0f436b05e7209e78da2daa8e9c8aa Mon Sep 17 00:00:00 2001 From: alzeih Date: Tue, 6 Aug 2013 22:22:18 +1200 Subject: [PATCH 076/111] Move tests to *protocol*/stored_playlists_test.py --- .../mpd/protocol/stored_playlists_test.py | 24 ++++++++++++++++ tests/frontends/mpd/stored_playlists_test.py | 28 ------------------- 2 files changed, 24 insertions(+), 28 deletions(-) delete mode 100644 tests/frontends/mpd/stored_playlists_test.py diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py index 8199be2b..820096f4 100644 --- a/tests/frontends/mpd/protocol/stored_playlists_test.py +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -107,6 +107,30 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertNotInResponse('playlist: ') self.assertInResponse('OK') + def test_listplaylists_replaces_newline_with_space(self): + self.backend.playlists.playlists = [ + Playlist(name='a\n', uri='dummy:')] + self.sendRequest('listplaylists') + self.assertInResponse('playlist: a ') + self.assertNotInResponse('playlist: a\n') + self.assertInResponse('OK') + + def test_listplaylists_replaces_carriage_return_with_space(self): + self.backend.playlists.playlists = [ + Playlist(name='a\r', uri='dummy:')] + self.sendRequest('listplaylists') + self.assertInResponse('playlist: a ') + self.assertNotInResponse('playlist: a\r') + self.assertInResponse('OK') + + def test_listplaylists_replaces_forward_slash_with_space(self): + self.backend.playlists.playlists = [ + Playlist(name='a/', uri='dummy:')] + self.sendRequest('listplaylists') + self.assertInResponse('playlist: a ') + self.assertNotInResponse('playlist: a/') + self.assertInResponse('OK') + def test_load_appends_to_tracklist(self): self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py deleted file mode 100644 index 2c647681..00000000 --- a/tests/frontends/mpd/stored_playlists_test.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import unicode_literals - -import unittest - -from mopidy.frontends.mpd.dispatcher import MpdContext, MpdDispatcher - - -class MpdContextTest(unittest.TestCase): - def setUp(self): - config = { - 'mpd': { - 'password': None, - } - } - dispatcher = MpdDispatcher(config=config) - self.context = dispatcher.context - - def test_create_unique_name_replaces_newlines_with_space(self): - result = self.context.create_unique_name("playlist name\n") - self.assertEqual("playlist name ", result) - - def test_create_unique_name_replaces_carriage_returns_with_space(self): - result = self.context.create_unique_name("playlist name\r") - self.assertEqual("playlist name ", result) - - def test_create_unique_name_replaces_forward_slashes_with_space(self): - result = self.context.create_unique_name("playlist name/") - self.assertEqual("playlist name ", result) From 3be63461706864669032691a0539ea74c163e96d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 6 Aug 2013 12:55:24 +0200 Subject: [PATCH 077/111] docs: Update changelog --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 65cb3408..43e99350 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -53,6 +53,11 @@ v0.15.0 (UNRELEASED) - Fix proxy config values that was broken with the config system change in 0.14. (Fixes: :issue:`472`) +**MPD frontend** + +- Replace newline, carriage return and forward slash in playlist names. (Fixes: + :issue:`474`, :issue:`480`) + v0.14.2 (2013-07-01) ==================== From 4756c639b1a7a10554e2650ec109e87d1e7db2e8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 6 Aug 2013 13:46:55 +0200 Subject: [PATCH 078/111] docs: Update AUTHORS with alzeih' real name --- .mailmap | 1 + AUTHORS | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index 7098b6d4..2ff779fc 100644 --- a/.mailmap +++ b/.mailmap @@ -5,5 +5,6 @@ Kristian Klette Johannes Knutsen Johannes Knutsen John Bäckstrand +Alli Witheford Alexandre Petitjean Alexandre Petitjean diff --git a/AUTHORS b/AUTHORS index ddb1602e..903aca92 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,5 +21,5 @@ - Thomas Refis - Janez Troha - Tobias Sauerwein -- alzeih +- Alli Witheford - Alexandre Petitjean From 0a32d89bab080c0d01f76c13b9f9b3832b835679 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 6 Aug 2013 17:32:53 +0200 Subject: [PATCH 079/111] Add test case for scanning empty wav files. --- tests/data/scanner/empty.wav | Bin 0 -> 56 bytes tests/scanner_test.py | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 tests/data/scanner/empty.wav diff --git a/tests/data/scanner/empty.wav b/tests/data/scanner/empty.wav new file mode 100644 index 0000000000000000000000000000000000000000..efe516b38d828c5234aaa6ee783c6587e7ff489b GIT binary patch literal 56 zcmWIYbaUfiU| Date: Wed, 7 Aug 2013 13:09:32 +0200 Subject: [PATCH 080/111] mpris: Frontend setup failure should only yield a warning --- mopidy/frontends/mpris/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris/actor.py b/mopidy/frontends/mpris/actor.py index fae8618f..d44e9262 100644 --- a/mopidy/frontends/mpris/actor.py +++ b/mopidy/frontends/mpris/actor.py @@ -34,7 +34,7 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener): self.mpris_object = objects.MprisObject(self.config, self.core) self._send_startup_notification() except Exception as e: - logger.error('MPRIS frontend setup failed (%s)', e) + logger.warning('MPRIS frontend setup failed (%s)', e) self.stop() def on_stop(self): From 5c7a496dc2b3548ff66afce5662271ebc190f251 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 8 Aug 2013 21:07:04 +0200 Subject: [PATCH 081/111] Fix flake8 warnings --- mopidy/audio/actor.py | 7 +++--- mopidy/backends/local/translator.py | 1 - mopidy/backends/spotify/translator.py | 3 ++- mopidy/config/types.py | 3 ++- mopidy/core/library.py | 10 +++++---- mopidy/ext.py | 3 ++- mopidy/scanner.py | 6 +++-- tests/backends/local/playlists_test.py | 3 --- tests/config/schemas_test.py | 3 ++- .../mpd/protocol/stored_playlists_test.py | 22 +++++++++---------- 10 files changed, 33 insertions(+), 28 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index d78df9b7..c11d79b3 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -35,8 +35,8 @@ MB = 1 << 20 # GST_PLAY_FLAG_SOFT_COLORBALANCE (1<<10) # Default flags to use for playbin: AUDIO, SOFT_VOLUME, DOWNLOAD -PLAYBIN_FLAGS = (1<<1) | (1<<4) | (1<<7) -PLAYBIN_VIS_FLAGS = PLAYBIN_FLAGS | (1<<3) +PLAYBIN_FLAGS = (1 << 1) | (1 << 4) | (1 << 7) +PLAYBIN_VIS_FLAGS = PLAYBIN_FLAGS | (1 << 3) class Audio(pykka.ThreadingActor): @@ -175,7 +175,8 @@ class Audio(pykka.ThreadingActor): logger.info('Audio visualizer set to "%s"', visualizer_element) except gobject.GError as ex: logger.error( - 'Failed to create audio visualizer "%s": %s', visualizer_element, ex) + 'Failed to create audio visualizer "%s": %s', + visualizer_element, ex) def _setup_mixer(self): mixer_desc = self._config['audio']['mixer'] diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index b8e98dd3..7cd46fbb 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import logging import os -import urllib import urlparse from mopidy.models import Track, Artist, Album diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 5813f880..f35cad2e 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -67,7 +67,8 @@ def to_mopidy_track(spotify_track, bitrate=None): return track_cache[uri] -def to_mopidy_playlist(spotify_playlist, folders=None, bitrate=None, username=None): +def to_mopidy_playlist( + spotify_playlist, folders=None, bitrate=None, username=None): if spotify_playlist is None or spotify_playlist.type() != 'playlist': return try: diff --git a/mopidy/config/types.py b/mopidy/config/types.py index 29651940..d3cd2462 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -111,7 +111,8 @@ class Secret(ConfigValue): class Integer(ConfigValue): """Integer value.""" - def __init__(self, minimum=None, maximum=None, choices=None, optional=False): + def __init__( + self, minimum=None, maximum=None, choices=None, optional=False): self._required = not optional self._minimum = minimum self._maximum = maximum diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 50d75144..cdc3f53a 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -69,8 +69,9 @@ class LibraryController(object): """ query = query or kwargs futures = [ - backend.library.find_exact(query=query, uris=uris) - for (backend, uris) in self._get_backends_to_uris(uris).items()] + backend.library.find_exact(query=query, uris=backend_uris) + for (backend, backend_uris) + in self._get_backends_to_uris(uris).items()] return [result for result in pykka.get_all(futures) if result] def lookup(self, uri): @@ -145,6 +146,7 @@ class LibraryController(object): """ query = query or kwargs futures = [ - backend.library.search(query=query, uris=uris) - for (backend, uris) in self._get_backends_to_uris(uris).items()] + backend.library.search(query=query, uris=backend_uris) + for (backend, backend_uris) + in self._get_backends_to_uris(uris).items()] return [result for result in pykka.get_all(futures) if result] diff --git a/mopidy/ext.py b/mopidy/ext.py index 22daa3cb..5db7c093 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -82,7 +82,8 @@ class Extension(object): def get_library_updaters(self): """List of library updater classes - :returns: list of :class:`~mopidy.backends.base.BaseLibraryUpdateProvider` + :returns: list of + :class:`~mopidy.backends.base.BaseLibraryUpdateProvider` subclasses """ return [] diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4632cebe..81ac5c53 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -192,7 +192,8 @@ def translator(data): class Scanner(object): - def __init__(self, uris, data_callback, error_callback=None, scan_timeout=1000): + def __init__( + self, uris, data_callback, error_callback=None, scan_timeout=1000): self.data = {} self.uris = iter(uris) self.data_callback = data_callback @@ -298,7 +299,8 @@ class Scanner(object): return False self.pipe.set_state(gst.STATE_NULL) self.uribin.set_property('uri', uri) - self.timeout_id = gobject.timeout_add(self.scan_timeout, self.process_timeout) + self.timeout_id = gobject.timeout_add( + self.scan_timeout, self.process_timeout) self.pipe.set_state(gst.STATE_PLAYING) return True diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index 591a9d1d..d405e887 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -7,7 +7,6 @@ import unittest from mopidy.backends.local import actor from mopidy.models import Track -from mopidy.utils.path import path_to_uri, uri_to_path from tests import path_to_data_dir from tests.backends.base.playlists import ( @@ -100,8 +99,6 @@ class LocalPlaylistsControllerTest( self.assertEqual(track.uri, contents.strip()) def test_playlists_are_loaded_at_startup(self): - playlist_path = os.path.join(self.playlists_dir, 'test.m3u') - track = Track(uri='local:track:path2') playlist = self.core.playlists.create('test') playlist = playlist.copy(tracks=[track]) diff --git a/tests/config/schemas_test.py b/tests/config/schemas_test.py index 8bcfc651..9da8f667 100644 --- a/tests/config/schemas_test.py +++ b/tests/config/schemas_test.py @@ -81,7 +81,8 @@ class ConfigSchemaTest(unittest.TestCase): class LogLevelConfigSchemaTest(unittest.TestCase): def test_conversion(self): schema = schemas.LogLevelConfigSchema('test') - result, errors = schema.deserialize({'foo.bar': 'DEBUG', 'baz': 'INFO'}) + result, errors = schema.deserialize( + {'foo.bar': 'DEBUG', 'baz': 'INFO'}) self.assertEqual(logging.DEBUG, result['foo.bar']) self.assertEqual(logging.INFO, result['baz']) diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py index 820096f4..d75944c4 100644 --- a/tests/frontends/mpd/protocol/stored_playlists_test.py +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -10,8 +10,8 @@ from tests.frontends.mpd import protocol class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylist(self): self.backend.playlists.playlists = [ - Playlist(name='name', uri='dummy:name', - tracks=[Track(uri='dummy:a')])] + Playlist( + name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylist "name"') self.assertInResponse('file: dummy:a') @@ -19,8 +19,8 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylist_without_quotes(self): self.backend.playlists.playlists = [ - Playlist(name='name', uri='dummy:name', - tracks=[Track(uri='dummy:a')])] + Playlist( + name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylist name') self.assertInResponse('file: dummy:a') @@ -41,8 +41,8 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylistinfo(self): self.backend.playlists.playlists = [ - Playlist(name='name', uri='dummy:name', - tracks=[Track(uri='dummy:a')])] + Playlist( + name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylistinfo "name"') self.assertInResponse('file: dummy:a') @@ -52,8 +52,8 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylistinfo_without_quotes(self): self.backend.playlists.playlists = [ - Playlist(name='name', uri='dummy:name', - tracks=[Track(uri='dummy:a')])] + Playlist( + name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylistinfo name') self.assertInResponse('file: dummy:a') @@ -109,7 +109,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylists_replaces_newline_with_space(self): self.backend.playlists.playlists = [ - Playlist(name='a\n', uri='dummy:')] + Playlist(name='a\n', uri='dummy:')] self.sendRequest('listplaylists') self.assertInResponse('playlist: a ') self.assertNotInResponse('playlist: a\n') @@ -117,7 +117,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylists_replaces_carriage_return_with_space(self): self.backend.playlists.playlists = [ - Playlist(name='a\r', uri='dummy:')] + Playlist(name='a\r', uri='dummy:')] self.sendRequest('listplaylists') self.assertInResponse('playlist: a ') self.assertNotInResponse('playlist: a\r') @@ -125,7 +125,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylists_replaces_forward_slash_with_space(self): self.backend.playlists.playlists = [ - Playlist(name='a/', uri='dummy:')] + Playlist(name='a/', uri='dummy:')] self.sendRequest('listplaylists') self.assertInResponse('playlist: a ') self.assertNotInResponse('playlist: a/') From 404fb002355427b3fb0e3a17e6db86d27688a309 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 8 Aug 2013 21:12:42 +0200 Subject: [PATCH 082/111] Remove pylint remnants --- docs/devtools.rst | 4 ++-- mopidy/__init__.py | 2 -- mopidy/audio/mixers/auto.py | 2 -- mopidy/backends/spotify/session_manager.py | 6 ------ mopidy/core/playback.py | 6 ------ mopidy/frontends/mpd/protocol/__init__.py | 2 -- mopidy/utils/path.py | 2 -- mopidy/utils/versioning.py | 2 -- pylintrc | 21 --------------------- requirements/tests.txt | 1 - 10 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 pylintrc diff --git a/docs/devtools.rst b/docs/devtools.rst index bc066cd0..6b8e6e30 100644 --- a/docs/devtools.rst +++ b/docs/devtools.rst @@ -22,8 +22,8 @@ tested by Jenkins before it is merged into the ``develop`` branch, which is a bit late, but good enough to get broad testing before new code is released. In addition to running tests, the Jenkins CI server also gathers coverage -statistics and uses pylint to check for errors and possible improvements in our -code. So, if you're out of work, the code coverage and pylint data at the CI +statistics and uses flake8 to check for errors and possible improvements in our +code. So, if you're out of work, the code coverage and flake8 data at the CI server should give you a place to start. diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 17cab15b..6a7699e5 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -1,8 +1,6 @@ from __future__ import unicode_literals -# pylint: disable = E0611,F0401 from distutils.version import StrictVersion as SV -# pylint: enable = E0611,F0401 import sys import warnings diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py index 587994cb..6e240ebe 100644 --- a/mopidy/audio/mixers/auto.py +++ b/mopidy/audio/mixers/auto.py @@ -29,9 +29,7 @@ class AutoAudioMixer(gst.Bin): gst.Bin.__init__(self) mixer = self._find_mixer() if mixer: - # pylint: disable=E1101 self.add(mixer) - # pylint: enable=E1101 logger.debug('AutoAudioMixer chose: %s', mixer.get_name()) else: logger.debug('AutoAudioMixer did not find any usable mixers') diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 125b6ada..3ab4498b 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -18,9 +18,6 @@ logger = logging.getLogger('mopidy.backends.spotify') BITRATES = {96: 2, 160: 0, 320: 1} -# pylint: disable = R0901 -# SpotifySessionManager: Too many ancestors (9/7) - class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): cache_location = None @@ -116,9 +113,6 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): 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) - if not self.push_audio_data: return 0 diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 2f296751..ea849dbf 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -13,9 +13,6 @@ logger = logging.getLogger('mopidy.core') class PlaybackController(object): - # pylint: disable = R0902 - # Too many instance attributes - pykka_traversable = True def __init__(self, audio, backends, core): @@ -175,9 +172,6 @@ class PlaybackController(object): """ def get_tl_track_at_eot(self): - # pylint: disable = R0911 - # Too many return statements - tl_tracks = self.core.tracklist.tl_tracks if not tl_tracks: diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 1827624b..0eadea7d 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -72,9 +72,7 @@ def load_protocol_modules(): The protocol modules must be imported to get them registered in :attr:`request_handlers` and :attr:`mpd_commands`. """ - # pylint: disable = W0612 from . import ( # noqa audio_output, channels, command_list, connection, current_playlist, empty, music_db, playback, reflection, status, stickers, stored_playlists) - # pylint: enable = W0612 diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 602b2569..c5aa6e45 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -2,9 +2,7 @@ from __future__ import unicode_literals import logging import os -# pylint: disable = W0402 import string -# pylint: enable = W0402 import urllib import urlparse diff --git a/mopidy/utils/versioning.py b/mopidy/utils/versioning.py index 3ad72458..e8856473 100644 --- a/mopidy/utils/versioning.py +++ b/mopidy/utils/versioning.py @@ -14,11 +14,9 @@ def get_version(): def get_git_version(): process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) - # pylint: disable = E1101 if process.wait() != 0: raise EnvironmentError('Execution of "git describe" failed') version = process.stdout.read().strip() - # pylint: enable = E1101 if version.startswith('v'): version = version[1:] return version diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 41e1ab5d..00000000 --- a/pylintrc +++ /dev/null @@ -1,21 +0,0 @@ -[MESSAGES CONTROL] -# -# Disabled messages -# ----------------- -# -# C0103 - Invalid name "%s" (should match %s) -# C0111 - Missing docstring -# R0201 - Method could be a function -# R0801 - Similar lines in %s files -# R0902 - Too many instance attributes (%s/%s) -# R0903 - Too few public methods (%s/%s) -# R0904 - Too many public methods (%s/%s) -# R0912 - Too many branches (%s/%s) -# R0913 - Too many arguments (%s/%s) -# R0921 - Abstract class not referenced -# W0141 - Used builtin function '%s' -# W0142 - Used * or ** magic -# W0511 - TODO, FIXME and XXX in the code -# W0613 - Unused argument %r -# -disable = C0103,C0111,R0201,R0801,R0902,R0903,R0904,R0912,R0913,R0921,W0141,W0142,W0511,W0613 diff --git a/requirements/tests.txt b/requirements/tests.txt index c093682b..8aacebbc 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -2,4 +2,3 @@ coverage flake8 mock >= 1.0 nose -pylint From f433a77ec569512e23d71827036652dd60065b15 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 9 Aug 2013 14:51:18 +0200 Subject: [PATCH 083/111] fab: Add lint/autolint tasks for running flake8 on everything --- fabfile.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fabfile.py b/fabfile.py index de2cf7bb..f23da2b1 100644 --- a/fabfile.py +++ b/fabfile.py @@ -35,6 +35,17 @@ def autocoverage(path=None): auto(coverage, path=path) +@task +def lint(path=None): + path = path or '.' + local('flake8 $(find %s -iname "*.py")' % path) + + +@task +def autolint(path=None): + auto(lint, path=path) + + def auto(task, *args, **kwargs): while True: local('clear') From 7570b242e0f3bc8bb2f1888fade67a95f0722c47 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 9 Aug 2013 14:53:32 +0200 Subject: [PATCH 084/111] travis: Run flake8 as part of build --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index df08679b..02ea9234 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,14 @@ install: - "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list" - "sudo apt-get update || true" - "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy/ {print $2}')" + - "pip install flake8" before_script: - "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt" -script: nosetests +script: + - "flake8 $(find . -iname '*.py')" + - "nosetests" notifications: irc: From f0df024fd9f258b5c6d02e8664f302e79114977b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 9 Aug 2013 15:00:23 +0200 Subject: [PATCH 085/111] travis: Remove -q flags, since Travis collapses install command output --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 02ea9234..0b68eb8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: python install: - - "wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -" - - "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list" + - "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -" + - "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list" - "sudo apt-get update || true" - "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy/ {print $2}')" - "pip install flake8" From 0098e76dc7374079b4381904493f4ab44ec73ccf Mon Sep 17 00:00:00 2001 From: Pavol Babincak Date: Thu, 15 Aug 2013 20:54:10 +0200 Subject: [PATCH 086/111] Properly encode localised mixer names before log --- mopidy/audio/actor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index c11d79b3..6a1d7f6b 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -222,7 +222,8 @@ class Audio(pykka.ThreadingActor): self._mixer_track.min_volume, self._mixer_track.max_volume) logger.info( 'Audio mixer set to "%s" using track "%s"', - mixer.get_factory().get_name(), track.label) + str(mixer.get_factory().get_name()).decode('utf-8'), + str(track.label).decode('utf-8')) def _select_mixer_track(self, mixer, track_label): # Ignore tracks without volumes, then look for track with From 4352de920e8ea3d11d139f9cbdf124d1b2eea513 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 15 Aug 2013 23:18:43 +0200 Subject: [PATCH 087/111] docs: Update authors and changelog --- AUTHORS | 1 + docs/changelog.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AUTHORS b/AUTHORS index 903aca92..052865b7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,3 +23,4 @@ - Tobias Sauerwein - Alli Witheford - Alexandre Petitjean +- Pavol Babincak diff --git a/docs/changelog.rst b/docs/changelog.rst index 43e99350..03317d30 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,8 @@ v0.15.0 (UNRELEASED) - Added support for viusalization. :confval:`audio/visualizer` can now be set to GStreamer visualizers. +- Properly encode localised mixer names before logging. + **Local backend** - An album's number of discs and a track's disc number are now extracted when From c311ef98e6727637e9c3e0707802b7c3697a0aa6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 29 Aug 2013 12:32:27 +0200 Subject: [PATCH 088/111] docs: Fix typo --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 03317d30..e3b94b58 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,7 +24,7 @@ v0.15.0 (UNRELEASED) **Audio** -- Added support for viusalization. :confval:`audio/visualizer` can now be set +- Added support for audio visualization. :confval:`audio/visualizer` can now be set to GStreamer visualizers. - Properly encode localised mixer names before logging. From 9af4290f456893d145e1fb1a5a718d9ff6d829a1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 1 Sep 2013 23:14:30 +0200 Subject: [PATCH 089/111] mpd: Accept listall and listallinfo without the URI argument --- docs/changelog.rst | 10 +++++++--- mopidy/frontends/mpd/protocol/music_db.py | 10 ++++++---- tests/frontends/mpd/protocol/music_db_test.py | 12 ++++++++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e3b94b58..1b08a742 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -42,9 +42,9 @@ v0.15.0 (UNRELEASED) to start providing their own custom libraries instead of being stuck with just our tag cache as the only option. -- Converted local backend to use new `local:playlist:path` and - `local:track:path` uri scheme. Also moves support of `file://` to streaming - backend. +- Converted local backend to use new ``local:playlist:path`` and + ``local:track:path`` URI scheme. Also moves support of ``file://`` to + streaming backend. **Spotify backend** @@ -60,6 +60,10 @@ v0.15.0 (UNRELEASED) - Replace newline, carriage return and forward slash in playlist names. (Fixes: :issue:`474`, :issue:`480`) +- Accept ``listall`` and ``listallinfo`` commands without the URI parameter. + The methods are still not implemented, but now the commands are accepted as + valid. + v0.14.2 (2013-07-01) ==================== diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 8e31dbf8..f81d57ee 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -245,8 +245,9 @@ def _list_date(context, query): return dates -@handle_request(r'^listall "(?P[^"]+)"') -def listall(context, uri): +@handle_request(r'^listall$') +@handle_request(r'^listall "(?P[^"]+)"$') +def listall(context, uri=None): """ *musicpd.org, music database section:* @@ -257,8 +258,9 @@ def listall(context, uri): raise MpdNotImplemented # TODO -@handle_request(r'^listallinfo "(?P[^"]+)"') -def listallinfo(context, uri): +@handle_request(r'^listallinfo$') +@handle_request(r'^listallinfo "(?P[^"]+)"$') +def listallinfo(context, uri=None): """ *musicpd.org, music database section:* diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index fa909bab..21c6721f 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -82,11 +82,19 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqual(playlists[0].tracks[0].uri, 'dummy:a') self.assertInResponse('OK') - def test_listall(self): + def test_listall_without_uri(self): + self.sendRequest('listall') + self.assertEqualResponse('ACK [0@0] {} Not implemented') + + def test_listall_with_uri(self): self.sendRequest('listall "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') - def test_listallinfo(self): + def test_listallinfo_without_uri(self): + self.sendRequest('listallinfo') + self.assertEqualResponse('ACK [0@0] {} Not implemented') + + def test_listallinfo_with_uri(self): self.sendRequest('listallinfo "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') From c3066995e132f720fe31c2bc8e521105ce16241e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 1 Sep 2013 23:29:42 +0200 Subject: [PATCH 090/111] docs: Add Mopidy-Subsonic extension --- docs/ext/index.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/ext/index.rst b/docs/ext/index.rst index 562766ce..a9857012 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -75,3 +75,19 @@ GitHub: `dz0ny/mopidy-soundcloud `_ Issues: https://github.com/dz0ny/mopidy-soundcloud/issues + + +Mopidy-Subsonic +--------------- + +Provides a backend for playing music from a `Subsonic Music Streamer +`_ library. + +Author: + Bradon Kanyid +PyPI: + `Mopidy-Subsonic `_ +GitHub: + `rattboi/mopidy-subsonic `_ +Issues: + https://github.com/rattboi/mopidy-subsonic/issues From a443348d4b8ffc1f108513ad82c5d37f7ec70f29 Mon Sep 17 00:00:00 2001 From: Javier Domingo Date: Fri, 6 Sep 2013 16:54:36 +0200 Subject: [PATCH 091/111] mpd: Don't decide the remove action Those three lines shouldn't be there. That is tracklist/playback logic, not frontend logic. Indeed, care is already taken in core =) --- mopidy/frontends/mpd/protocol/current_playlist.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 055d39e6..20452203 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -101,9 +101,6 @@ def deleteid(context, tlid): Deletes the song ``SONGID`` from the playlist """ tlid = int(tlid) - tl_track = context.core.playback.current_tl_track.get() - if tl_track and tl_track.tlid == tlid: - context.core.playback.next() tl_tracks = context.core.tracklist.remove(tlid=tlid).get() if not tl_tracks: raise MpdNoExistError('No such song', command='deleteid') From 1dedb331d32d84f59e20979a9607ff24031ebaf8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Sep 2013 22:37:34 +0200 Subject: [PATCH 092/111] docs: Add Mopidy-GMusic (fix #355) --- docs/ext/index.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/ext/index.rst b/docs/ext/index.rst index a9857012..20270238 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -46,6 +46,22 @@ Issues: https://github.com/dz0ny/mopidy-beets/issues +Mopidy-GMusic +------------- + +Provides a backend for playing music from `Google Play Music +`_. + +Author: + Ronald Hecht +PyPI: + N/A +GitHub: + `hechtus/mopidy-gmusic `_ +Issues: + https://github.com/hechtus/mopidy-gmusic/issues + + Mopidy-NAD ---------- From 82077ace0239c0db1f11c9ee2067b66d098676d9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Sep 2013 22:41:21 +0200 Subject: [PATCH 093/111] docs: Add Mopidy-SomaFM (fix #509) --- docs/ext/index.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/ext/index.rst b/docs/ext/index.rst index 20270238..42df3f03 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -77,6 +77,22 @@ Issues: https://github.com/mopidy/mopidy/issues +Mopidy-SomaFM +------------- + +Provides a backend for playing music from the `SomaFM `_ +service. + +Author: + Alexandre Petitjean +PyPI: + `Mopidy-SomaFM `_ +GitHub: + `AlexandrePTJ/mopidy-somafm `_ +Issues: + https://github.com/AlexandrePTJ/mopidy-somafm/issues + + Mopidy-SoundCloud ----------------- From 05d4fa846fd2502695d58ec1fc47dd46c7b6f9a1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Sep 2013 23:49:15 +0200 Subject: [PATCH 094/111] config: Deserialize Secret to unicode (fix #473) MPD, Scrobbler, and Spotify extensions have been reviewed for need for changes due to this, without anything being found. --- docs/changelog.rst | 5 +++++ mopidy/config/types.py | 26 +++++++++----------------- tests/config/types_test.py | 8 ++++---- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b08a742..42a8f60b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -64,6 +64,11 @@ v0.15.0 (UNRELEASED) The methods are still not implemented, but now the commands are accepted as valid. +**Extension support** + +- :class:`~mopidy.config.Secret` is now deserialized to unicode strings instead + of bytestrings. This may require modifications to extensions. + v0.14.2 (2013-07-01) ==================== diff --git a/mopidy/config/types.py b/mopidy/config/types.py index d3cd2462..d264de30 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -82,30 +82,22 @@ class String(ConfigValue): return encode(value) -class Secret(ConfigValue): - """Secret value. +class Secret(String): + """Secret string value. - Should be used for passwords, auth tokens etc. Deserializing will not - convert to unicode. Will mask value when being displayed. + Is decoded as utf-8 and \\n \\t escapes should work and be preserved. + + Should be used for passwords, auth tokens etc. Will mask value when being + displayed. """ def __init__(self, optional=False, choices=None): self._required = not optional - - def deserialize(self, value): - value = value.strip() - validators.validate_required(value, self._required) - if not value: - return None - return value + self._choices = None # Choices doesn't make sense for secrets def serialize(self, value, display=False): - if isinstance(value, unicode): - value = value.encode('utf-8') - if value is None: - return b'' - elif display: + if value is not None and display: return b'********' - return value + return super(Secret, self).serialize(value, display) class Integer(ConfigValue): diff --git a/tests/config/types_test.py b/tests/config/types_test.py index 74e9672d..0df3dfb4 100644 --- a/tests/config/types_test.py +++ b/tests/config/types_test.py @@ -105,11 +105,11 @@ class StringTest(unittest.TestCase): class SecretTest(unittest.TestCase): - def test_deserialize_passes_through(self): + def test_deserialize_decodes_utf8(self): value = types.Secret() - result = value.deserialize(b'foo') - self.assertIsInstance(result, bytes) - self.assertEqual(b'foo', result) + result = value.deserialize('æøå'.encode('utf-8')) + self.assertIsInstance(result, unicode) + self.assertEqual('æøå', result) def test_deserialize_enforces_required(self): value = types.Secret() From 713cd598d065a53fe3e9286986e5fdfc24140ccd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Sep 2013 10:12:58 +0200 Subject: [PATCH 095/111] docs: Mopidy-GMusic is now on PyPI --- docs/ext/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ext/index.rst b/docs/ext/index.rst index 42df3f03..736f2fb6 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -55,7 +55,7 @@ Provides a backend for playing music from `Google Play Music Author: Ronald Hecht PyPI: - N/A + `Mopidy-GMusic `_ GitHub: `hechtus/mopidy-gmusic `_ Issues: From e830f314809f32b10be9a1831646ea380a9bbc37 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Sep 2013 21:48:56 +0200 Subject: [PATCH 096/111] Fix too broad truthness test (fix #501) This caused TlTracks with tlid=0 to be sent to HTTP clients without the tlid field. --- docs/changelog.rst | 6 ++++++ mopidy/models.py | 2 +- tests/models_test.py | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 42a8f60b..dea88921 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -64,6 +64,12 @@ v0.15.0 (UNRELEASED) The methods are still not implemented, but now the commands are accepted as valid. +**HTTP frontend** + +- Fix too broad truthness test that caused :class:`~mopidy.models.TlTrack` + objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the + ``tlid`` field. (Fixes: :issue:`501`) + **Extension support** - :class:`~mopidy.config.Secret` is now deserialized to unicode strings instead diff --git a/mopidy/models.py b/mopidy/models.py index fe390ddf..3fc92bb4 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -90,7 +90,7 @@ class ImmutableObject(object): for v in value] elif isinstance(value, ImmutableObject): value = value.serialize() - if value: + if not (isinstance(value, list) and len(value) == 0): data[public_key] = value return data diff --git a/tests/models_test.py b/tests/models_test.py index a0fe08c7..afd1858b 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -95,6 +95,11 @@ class ArtistTest(unittest.TestCase): {'__model__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) + def test_serialize_falsy_values(self): + self.assertDictEqual( + {'__model__': 'Artist', 'uri': '', 'name': None}, + Artist(uri='', name=None).serialize()) + def test_to_json_and_back(self): artist1 = Artist(uri='uri', name='name') serialized = json.dumps(artist1, cls=ModelJSONEncoder) From 1a02b4d17f072557cf7833970b77bc473e6b5494 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Sep 2013 22:13:36 +0200 Subject: [PATCH 097/111] Remove support for running Python on the mopidy/ dir It doesn't make sense to run Mopidy without extensions registered, thus you'll need to use setuptools and to run `python setup.py develop` anyway. Doing so makes running `mopidy` from anywhere in the development virtualenv work, removing any need for running `python mopidy/`. --- mopidy/__main__.py | 6 ------ tests/help_test.py | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 0118395c..a55059a4 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -17,12 +17,6 @@ mopidy_args = sys.argv[1:] sys.argv[1:] = [] -# Add ../ to the path so we can run Mopidy from a Git checkout without -# installing it on the system. -sys.path.insert( - 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) - - from mopidy import commands, ext from mopidy.audio import Audio from mopidy import config as config_lib diff --git a/tests/help_test.py b/tests/help_test.py index 4f210031..574e4fd7 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -12,7 +12,10 @@ class HelpTest(unittest.TestCase): def test_help_has_mopidy_options(self): mopidy_dir = os.path.dirname(mopidy.__file__) args = [sys.executable, mopidy_dir, '--help'] - process = subprocess.Popen(args, stdout=subprocess.PIPE) + process = subprocess.Popen( + args, + env={'PYTHONPATH': os.path.join(mopidy_dir, '..')}, + stdout=subprocess.PIPE) output = process.communicate()[0] self.assertIn('--version', output) self.assertIn('--help', output) From 7cd7216b94a06c9b7c74d7f8a7f57d290a587532 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Sep 2013 22:27:54 +0200 Subject: [PATCH 098/111] Create empty config file if none found (see #467) We already had code in place for this, but it was run after the config validation, so on a new installation Mopidy would fail because of missing config values before the config file was created. --- mopidy/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index a55059a4..2323768f 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -53,6 +53,8 @@ def main(): logging_config, args.verbosity_level, args.save_debug_log) logging_initialized = True + create_file_structures() + installed_extensions = ext.load_extensions() config, config_errors = config_lib.load( @@ -73,7 +75,6 @@ def main(): proxied_config = config_lib.Proxy(config) log.setup_log_levels(proxied_config) - create_file_structures() check_old_locations() ext.register_gstreamer_elements(enabled_extensions) From f244d94b52332dbad08e0df6d52251fb026951d0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Sep 2013 23:20:18 +0200 Subject: [PATCH 099/111] main: Split main() function in two By separating the teardown procedures for errors that happens before and after actors are started, we get a lot less output when failing on config errors. This makes the config errors the last lines printed by the `mopidy` command, making them easier to spot. Related to #467 --- mopidy/__main__.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 2323768f..c426266b 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -36,15 +36,12 @@ def main(): if args.show_deps: commands.show_deps() - loop = gobject.MainLoop() - enabled_extensions = [] # Make sure it is defined before the finally block - logging_initialized = False - # TODO: figure out a way to make the boilerplate in this file reusable in # scanner and other places we need it. try: # Initial config without extensions to bootstrap logging. + logging_initialized = False logging_config, _ = config_lib.load( args.config_files, [], args.config_overrides) @@ -61,6 +58,7 @@ def main(): args.config_files, installed_extensions, args.config_overrides) # Filter out disabled extensions and remove any config errors for them. + enabled_extensions = [] for extension in installed_extensions: enabled = config[extension.ext_name]['enabled'] if ext.validate_extension(extension) and enabled: @@ -77,21 +75,34 @@ def main(): log.setup_log_levels(proxied_config) check_old_locations() ext.register_gstreamer_elements(enabled_extensions) - - # Anything that wants to exit after this point must use - # mopidy.utils.process.exit_process as actors have been started. - audio = setup_audio(proxied_config) - backends = setup_backends(proxied_config, enabled_extensions, audio) - core = setup_core(audio, backends) - setup_frontends(proxied_config, enabled_extensions, core) - loop.run() except KeyboardInterrupt: if logging_initialized: logger.info('Interrupted. Exiting...') + return except Exception as ex: if logging_initialized: logger.exception(ex) raise + + # Anything that wants to exit after this point must use + # mopidy.utils.process.exit_process as actors have been started. + start(proxied_config, enabled_extensions) + + +def start(config, enabled_extensions): + loop = gobject.MainLoop() + try: + audio = setup_audio(config) + backends = setup_backends(config, enabled_extensions, audio) + core = setup_core(audio, backends) + setup_frontends(config, enabled_extensions, core) + loop.run() + except KeyboardInterrupt: + logger.info('Interrupted. Exiting...') + return + except Exception as ex: + logger.exception(ex) + raise finally: loop.quit() stop_frontends(enabled_extensions) From c4dc4985b51470a42255d8b50fc813b99562be53 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Sep 2013 23:27:41 +0200 Subject: [PATCH 100/111] main: Don't be verbose on CTRL+C before starting actors --- mopidy/__main__.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index c426266b..df5f99a4 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -75,19 +75,17 @@ def main(): log.setup_log_levels(proxied_config) check_old_locations() ext.register_gstreamer_elements(enabled_extensions) + + # Anything that wants to exit after this point must use + # mopidy.utils.process.exit_process as actors have been started. + start(proxied_config, enabled_extensions) except KeyboardInterrupt: - if logging_initialized: - logger.info('Interrupted. Exiting...') - return + pass except Exception as ex: if logging_initialized: logger.exception(ex) raise - # Anything that wants to exit after this point must use - # mopidy.utils.process.exit_process as actors have been started. - start(proxied_config, enabled_extensions) - def start(config, enabled_extensions): loop = gobject.MainLoop() @@ -100,9 +98,6 @@ def start(config, enabled_extensions): except KeyboardInterrupt: logger.info('Interrupted. Exiting...') return - except Exception as ex: - logger.exception(ex) - raise finally: loop.quit() stop_frontends(enabled_extensions) From ac8d4b7413e70524a1e327ce3ccef2d6948f27cc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Sep 2013 00:15:33 +0200 Subject: [PATCH 101/111] main: Reorder/rename functions --- mopidy/__main__.py | 84 +++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index df5f99a4..aa0c751e 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -51,6 +51,7 @@ def main(): logging_initialized = True create_file_structures() + check_old_locations() installed_extensions = ext.load_extensions() @@ -73,7 +74,6 @@ def main(): proxied_config = config_lib.Proxy(config) log.setup_log_levels(proxied_config) - check_old_locations() ext.register_gstreamer_elements(enabled_extensions) # Anything that wants to exit after this point must use @@ -87,24 +87,25 @@ def main(): raise -def start(config, enabled_extensions): - loop = gobject.MainLoop() - try: - audio = setup_audio(config) - backends = setup_backends(config, enabled_extensions, audio) - core = setup_core(audio, backends) - setup_frontends(config, enabled_extensions, core) - loop.run() - except KeyboardInterrupt: - logger.info('Interrupted. Exiting...') - return - finally: - loop.quit() - stop_frontends(enabled_extensions) - stop_core() - stop_backends(enabled_extensions) - stop_audio() - process.stop_remaining_actors() +def create_file_structures(): + path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') + path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf') + + +def check_old_locations(): + dot_mopidy_dir = path.expand_path(b'~/.mopidy') + if os.path.isdir(dot_mopidy_dir): + logger.warning( + 'Old Mopidy dot dir found at %s. Please migrate your config to ' + 'the ini-file based config format. See release notes for further ' + 'instructions.', dot_mopidy_dir) + + old_settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py') + if os.path.isfile(old_settings_file): + logger.warning( + 'Old Mopidy settings file found at %s. Please migrate your ' + 'config to the ini-file based config format. See release notes ' + 'for further instructions.', old_settings_file) def log_extension_info(all_extensions, enabled_extensions): @@ -126,28 +127,27 @@ def check_config_errors(errors): sys.exit(1) -def check_old_locations(): - dot_mopidy_dir = path.expand_path(b'~/.mopidy') - if os.path.isdir(dot_mopidy_dir): - logger.warning( - 'Old Mopidy dot dir found at %s. Please migrate your config to ' - 'the ini-file based config format. See release notes for further ' - 'instructions.', dot_mopidy_dir) - - old_settings_file = path.expand_path(b'$XDG_CONFIG_DIR/mopidy/settings.py') - if os.path.isfile(old_settings_file): - logger.warning( - 'Old Mopidy settings file found at %s. Please migrate your ' - 'config to the ini-file based config format. See release notes ' - 'for further instructions.', old_settings_file) +def start(config, extensions): + loop = gobject.MainLoop() + try: + audio = start_audio(config) + backends = start_backends(config, extensions, audio) + core = start_core(audio, backends) + start_frontends(config, extensions, core) + loop.run() + except KeyboardInterrupt: + logger.info('Interrupted. Exiting...') + return + finally: + loop.quit() + stop_frontends(extensions) + stop_core() + stop_backends(extensions) + stop_audio() + process.stop_remaining_actors() -def create_file_structures(): - path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') - path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf') - - -def setup_audio(config): +def start_audio(config): logger.info('Starting Mopidy audio') return Audio.start(config=config).proxy() @@ -157,7 +157,7 @@ def stop_audio(): process.stop_actors_by_class(Audio) -def setup_backends(config, extensions, audio): +def start_backends(config, extensions, audio): backend_classes = [] for extension in extensions: backend_classes.extend(extension.get_backend_classes()) @@ -181,7 +181,7 @@ def stop_backends(extensions): process.stop_actors_by_class(backend_class) -def setup_core(audio, backends): +def start_core(audio, backends): logger.info('Starting Mopidy core') return Core.start(audio=audio, backends=backends).proxy() @@ -191,7 +191,7 @@ def stop_core(): process.stop_actors_by_class(Core) -def setup_frontends(config, extensions, core): +def start_frontends(config, extensions, core): frontend_classes = [] for extension in extensions: frontend_classes.extend(extension.get_frontend_classes()) From 94079be1c6398d135c730f5cb9a81aa9fd9de4d7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Sep 2013 00:38:52 +0200 Subject: [PATCH 102/111] js: Upgrade Bane from 0.4.0 to 1.0.0 --- js/lib/{bane-0.4.0.js => bane-1.0.0.js} | 6 +++--- js/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename js/lib/{bane-0.4.0.js => bane-1.0.0.js} (98%) diff --git a/js/lib/bane-0.4.0.js b/js/lib/bane-1.0.0.js similarity index 98% rename from js/lib/bane-0.4.0.js rename to js/lib/bane-1.0.0.js index a1da6efa..8051764d 100644 --- a/js/lib/bane-0.4.0.js +++ b/js/lib/bane-1.0.0.js @@ -3,10 +3,10 @@ * * https://github.com/busterjs/bane * - * @version 0.4.0 + * @version 1.0.0 */ -((typeof define === "function" && define.amd && function (m) { define(m); }) || +((typeof define === "function" && define.amd && function (m) { define("bane", m); }) || (typeof module === "object" && function (m) { module.exports = m(); }) || function (m) { this.bane = m(); } )(function () { @@ -152,7 +152,7 @@ notifyListener(event, toNotify[i], args); } - toNotify = listeners(this, event).slice() + toNotify = listeners(this, event).slice(); args = slice.call(arguments, 1); for (i = 0, l = toNotify.length; i < l; ++i) { notifyListener(event, toNotify[i], args); diff --git a/js/package.json b/js/package.json index 1623e3f8..8a4e425d 100644 --- a/js/package.json +++ b/js/package.json @@ -14,7 +14,7 @@ }, "main": "src/mopidy.js", "dependencies": { - "bane": "~0.4.0", + "bane": "~1.0.0", "faye-websocket": "~0.4.4", "when": "~2.0.0" }, From ba64c7f15a31301f0303cf86fec0739f655dcf31 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Sep 2013 00:39:13 +0200 Subject: [PATCH 103/111] js: Upgrade When.js from 2.0.0 to 2.4.0 --- js/lib/{when-2.0.0.js => when-2.4.0.js} | 531 +++++++++++++++--------- js/package.json | 2 +- 2 files changed, 334 insertions(+), 199 deletions(-) rename js/lib/{when-2.0.0.js => when-2.4.0.js} (61%) diff --git a/js/lib/when-2.0.0.js b/js/lib/when-2.4.0.js similarity index 61% rename from js/lib/when-2.0.0.js rename to js/lib/when-2.4.0.js index 78249532..aa386275 100644 --- a/js/lib/when-2.0.0.js +++ b/js/lib/when-2.4.0.js @@ -9,27 +9,30 @@ * * @author Brian Cavalier * @author John Hann - * @version 2.0.0 + * @version 2.4.0 */ -(function(define) { 'use strict'; -define(function () { +(function(define, global) { 'use strict'; +define(function (require) { // Public API - when.defer = defer; // Create a deferred + when.promise = promise; // Create a pending promise when.resolve = resolve; // Create a resolved promise when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a {promise, resolver} pair when.join = join; // Join 2 or more promises when.all = all; // Resolve a list of promises when.map = map; // Array.map() for promises when.reduce = reduce; // Array.reduce() for promises + when.settle = settle; // Settle a list of promises when.any = any; // One-winner race when.some = some; // Multi-winner race - when.isPromise = isPromise; // Determine if a thing is a promise + when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike + when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable /** * Register an observer for a promise or immediate value. @@ -57,13 +60,35 @@ define(function () { * a trusted when.js promise. Any other duck-typed promise is considered * untrusted. * @constructor + * @param {function} sendMessage function to deliver messages to the promise's handler + * @param {function?} inspect function that reports the promise's state * @name Promise */ - function Promise(then) { - this.then = then; + function Promise(sendMessage, inspect) { + this._message = sendMessage; + this.inspect = inspect; } Promise.prototype = { + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + then: function(onFulfilled, onRejected, onProgress) { + /*jshint unused:false*/ + var args, sendMessage; + + args = arguments; + sendMessage = this._message; + + return _promise(function(resolve, reject, notify) { + sendMessage('when', args, resolve, notify); + }, this._status && this._status.observed()); + }, + /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected @@ -84,9 +109,7 @@ define(function () { * @returns {Promise} */ ensure: function(onFulfilledOrRejected) { - var self = this; - - return this.then(injectHandler, injectHandler).yield(self); + return this.then(injectHandler, injectHandler)['yield'](this); function injectHandler() { return resolve(onFulfilledOrRejected()); @@ -107,6 +130,16 @@ define(function () { }); }, + /** + * Runs a side effect when this promise fulfills, without changing the + * fulfillment value. + * @param {function} onFulfilledSideEffect + * @returns {Promise} + */ + tap: function(onFulfilledSideEffect) { + return this.then(onFulfilledSideEffect)['yield'](this); + }, + /** * Assumes that this promise will fulfill with an array, and arranges * for the onFulfilled to be called with the array as its argument list @@ -162,13 +195,16 @@ define(function () { } /** - * Creates a new Deferred with fully isolated resolver and promise parts, - * either or both of which may be given out safely to consumers. + * Creates a {promise, resolver} pair, either or both of which + * may be given out safely to consumers. * The resolver has resolve, reject, and progress. The promise - * only has then. + * has then plus extended promise API. * * @return {{ * promise: Promise, + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise * resolver: { * resolve: function:Promise, * reject: function:Promise, @@ -216,12 +252,26 @@ define(function () { /** * Creates a new promise whose fate is determined by resolver. - * @private (for now) * @param {function} resolver function(resolve, reject, notify) * @returns {Promise} promise whose fate is determine by resolver */ function promise(resolver) { - var value, handlers = []; + return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); + } + + /** + * Creates a new promise, linked to parent, whose fate is determined + * by resolver. + * @param {function} resolver function(resolve, reject, notify) + * @param {Promise?} status promise from which the new promise is begotten + * @returns {Promise} promise whose fate is determine by resolver + * @private + */ + function _promise(resolver, status) { + var self, value, consumers = []; + + self = new Promise(_message, inspect); + self._status = status; // Call the provider resolver to seal the promise's fate try { @@ -231,29 +281,34 @@ define(function () { } // Return the promise - return new Promise(then); + return self; /** - * Register handlers for this promise. - * @param [onFulfilled] {Function} fulfillment handler - * @param [onRejected] {Function} rejection handler - * @param [onProgress] {Function} progress handler - * @return {Promise} new Promise + * Private message delivery. Queues and delivers messages to + * the promise's ultimate fulfillment value or rejection reason. + * @private + * @param {String} type + * @param {Array} args + * @param {Function} resolve + * @param {Function} notify */ - function then(onFulfilled, onRejected, onProgress) { - return promise(function(resolve, reject, notify) { - handlers - // Call handlers later, after resolution - ? handlers.push(function(value) { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }) - // Call handlers soon, but not in the current stack - : enqueue(function() { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }); - }); + function _message(type, args, resolve, notify) { + consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); + + function deliver(p) { + p._message(type, args, resolve, notify); + } + } + + /** + * Returns a snapshot of the promise's state at the instant inspect() + * is called. The returned object is not live and will not update as + * the promise's state changes. + * @returns {{ state:String, value?:*, reason?:* }} status snapshot + * of the promise. + */ + function inspect() { + return value ? value.inspect() : toPendingState(); } /** @@ -262,14 +317,17 @@ define(function () { * @param {*|Promise} val resolution value */ function promiseResolve(val) { - if(!handlers) { + if(!consumers) { return; } value = coerce(val); - scheduleHandlers(handlers, value); + scheduleConsumers(consumers, value); + consumers = undef; - handlers = undef; + if(status) { + updateStatus(value, status); + } } /** @@ -285,27 +343,90 @@ define(function () { * @param {*} update progress event payload to pass to all listeners */ function promiseNotify(update) { - if(handlers) { - scheduleHandlers(handlers, progressing(update)); + if(consumers) { + scheduleConsumers(consumers, progressed(update)); } } } + /** + * Creates a fulfilled, local promise as a proxy for a value + * NOTE: must never be exposed + * @param {*} value fulfillment value + * @returns {Promise} + */ + function fulfilled(value) { + return near( + new NearFulfilledProxy(value), + function() { return toFulfilledState(value); } + ); + } + + /** + * Creates a rejected, local promise with the supplied reason + * NOTE: must never be exposed + * @param {*} reason rejection reason + * @returns {Promise} + */ + function rejected(reason) { + return near( + new NearRejectedProxy(reason), + function() { return toRejectedState(reason); } + ); + } + + /** + * Creates a near promise using the provided proxy + * NOTE: must never be exposed + * @param {object} proxy proxy for the promise's ultimate value or reason + * @param {function} inspect function that returns a snapshot of the + * returned near promise's state + * @returns {Promise} + */ + function near(proxy, inspect) { + return new Promise(function (type, args, resolve) { + try { + resolve(proxy[type].apply(proxy, args)); + } catch(e) { + resolve(rejected(e)); + } + }, inspect); + } + + /** + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise + */ + function progressed(update) { + return new Promise(function (type, args, _, notify) { + var onProgress = args[2]; + try { + notify(typeof onProgress === 'function' ? onProgress(update) : update); + } catch(e) { + notify(e); + } + }); + } + /** * Coerces x to a trusted Promise * * @private * @param {*} x thing to coerce - * @returns {Promise} Guaranteed to return a trusted Promise. If x + * @returns {*} Guaranteed to return a trusted Promise. If x * is trusted, returns x, otherwise, returns a new, trusted, already-resolved * Promise whose resolution value is: * * the resolution value of x if it's a foreign promise, or * * x if it's a value */ function coerce(x) { - if(x instanceof Promise) { + if (x instanceof Promise) { return x; - } else if (x !== Object(x)) { + } + + if (!(x === Object(x) && 'then' in x)) { return fulfilled(x); } @@ -332,61 +453,34 @@ define(function () { } /** - * Create an already-fulfilled promise for the supplied value - * @private + * Proxy for a near, fulfilled value * @param {*} value - * @return {Promise} fulfilled promise + * @constructor */ - function fulfilled(value) { - var self = new Promise(function (onFulfilled) { - try { - return typeof onFulfilled == 'function' - ? coerce(onFulfilled(value)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearFulfilledProxy(value) { + this.value = value; } + NearFulfilledProxy.prototype.when = function(onResult) { + return typeof onResult === 'function' ? onResult(this.value) : this.value; + }; + /** - * Create an already-rejected promise with the supplied rejection reason. - * @private + * Proxy for a near rejection * @param {*} reason - * @return {Promise} rejected promise + * @constructor */ - function rejected(reason) { - var self = new Promise(function (_, onRejected) { - try { - return typeof onRejected == 'function' - ? coerce(onRejected(reason)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearRejectedProxy(reason) { + this.reason = reason; } - /** - * Create a progress promise with the supplied update. - * @private - * @param {*} update - * @return {Promise} progress promise - */ - function progressing(update) { - var self = new Promise(function (_, __, onProgress) { - try { - return typeof onProgress == 'function' - ? progressing(onProgress(update)) : self; - } catch (e) { - return progressing(e); - } - }); - - return self; - } + NearRejectedProxy.prototype.when = function(_, onError) { + if(typeof onError === 'function') { + return onError(this.reason); + } else { + throw this.reason; + } + }; /** * Schedule a task that will process a list of handlers @@ -395,7 +489,7 @@ define(function () { * @param {Array} handlers queue of handlers to execute * @param {*} value passed as the only arg to each handler */ - function scheduleHandlers(handlers, value) { + function scheduleConsumers(handlers, value) { enqueue(function() { var handler, i = 0; while (handler = handlers[i++]) { @@ -404,14 +498,23 @@ define(function () { }); } + function updateStatus(value, status) { + value.then(statusFulfilled, statusRejected); + + function statusFulfilled() { status.fulfilled(); } + function statusRejected(r) { status.rejected(r); } + } + /** - * Determines if promiseOrValue is a promise or not - * - * @param {*} promiseOrValue anything - * @returns {boolean} true if promiseOrValue is a {@link Promise} + * Determines if x is promise-like, i.e. a thenable object + * NOTE: Will return true for *any thenable object*, and isn't truly + * safe, since it may attempt to access the `then` property of x (i.e. + * clever/malicious getters may do weird things) + * @param {*} x anything + * @returns {boolean} true if x is promise-like */ - function isPromise(promiseOrValue) { - return promiseOrValue && typeof promiseOrValue.then === 'function'; + function isPromiseLike(x) { + return x && typeof x.then === 'function'; } /** @@ -423,17 +526,15 @@ define(function () { * @param {Array} promisesOrValues array of anything, may contain a mix * of promises and values * @param howMany {number} number of promisesOrValues to resolve - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to an array of howMany values that * resolved first, or will reject with an array of * (promisesOrValues.length - howMany) + 1 rejection reasons. */ function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { - checkCallbacks(2, arguments); - return when(promisesOrValues, function(promisesOrValues) { return promise(resolveSome).then(onFulfilled, onRejected, onProgress); @@ -457,7 +558,7 @@ define(function () { rejectOne = function(reason) { reasons.push(reason); if(!--toReject) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; reject(reasons); } }; @@ -466,7 +567,7 @@ define(function () { // This orders the values based on promise resolution order values.push(val); if (!--toResolve) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; resolve(values); } }; @@ -496,9 +597,9 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to the value that resolved first, or * will reject with an array of all rejected inputs. */ @@ -519,14 +620,13 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} */ function all(promisesOrValues, onFulfilled, onRejected, onProgress) { - checkCallbacks(1, arguments); - return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); + return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); } /** @@ -535,28 +635,49 @@ define(function () { * have fulfilled, or will reject when *any one* of the input promises rejects. */ function join(/* ...promises */) { - return map(arguments, identity); + return _map(arguments, identity); } /** - * Traditional map function, similar to `Array.prototype.map()`, but allows - * input to contain {@link Promise}s and/or values, and mapFunc may return - * either a value or a {@link Promise} - * - * @param {Array|Promise} array array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function} mapFunc mapping function mapFunc(value) which may return - * either a {@link Promise} or value - * @returns {Promise} a {@link Promise} that will resolve to an array containing - * the mapped output values. + * Settles all input promises such that they are guaranteed not to + * be pending once the returned promise fulfills. The returned promise + * will always fulfill, except in the case where `array` is a promise + * that rejects. + * @param {Array|Promise} array or promise for array of promises to settle + * @returns {Promise} promise that always fulfills with an array of + * outcome snapshots for each input promise. + */ + function settle(array) { + return _map(array, toFulfilledState, toRejectedState); + } + + /** + * Promise-aware array map function, similar to `Array.prototype.map()`, + * but input array may contain promises or values. + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. */ function map(array, mapFunc) { + return _map(array, mapFunc); + } + + /** + * Internal map that allows a fallback to handle rejections + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @param {function?} fallback function to handle rejected promises + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. + */ + function _map(array, mapFunc, fallback) { return when(array, function(array) { - return promise(resolveMap); + return _promise(resolveMap); function resolveMap(resolve, reject, notify) { - var results, len, toResolve, resolveOne, i; + var results, len, toResolve, i; // Since we know the resulting length, we can preallocate the results // array to avoid array expansions. @@ -565,27 +686,28 @@ define(function () { if(!toResolve) { resolve(results); - } else { + return; + } - resolveOne = function(item, i) { - when(item, mapFunc).then(function(mapped) { - results[i] = mapped; - - if(!--toResolve) { - resolve(results); - } - }, reject, notify); - }; - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolveOne(array[i], i); - } else { - --toResolve; - } + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; } } + + function resolveOne(item, i) { + when(item, mapFunc, fallback).then(function(mapped) { + results[i] = mapped; + notify(mapped); + + if(!--toResolve) { + resolve(results); + } + }, reject); + } } }); } @@ -625,12 +747,46 @@ define(function () { }); } + // Snapshot states + + /** + * Creates a fulfilled state snapshot + * @private + * @param {*} x any value + * @returns {{state:'fulfilled',value:*}} + */ + function toFulfilledState(x) { + return { state: 'fulfilled', value: x }; + } + + /** + * Creates a rejected state snapshot + * @private + * @param {*} x any reason + * @returns {{state:'rejected',reason:*}} + */ + function toRejectedState(x) { + return { state: 'rejected', reason: x }; + } + + /** + * Creates a pending state snapshot + * @private + * @returns {{state:'pending'}} + */ + function toPendingState() { + return { state: 'pending' }; + } + // - // Utilities, etc. + // Internals, utilities, etc. // var reduceArray, slice, fcall, nextTick, handlerQueue, - timeout, funcProto, call, arrayProto, undef; + setTimeout, funcProto, call, arrayProto, monitorApi, + cjsRequire, undef; + + cjsRequire = require; // // Shared handler queue processing @@ -648,20 +804,13 @@ define(function () { */ function enqueue(task) { if(handlerQueue.push(task) === 1) { - scheduleDrainQueue(); + nextTick(drainQueue); } } /** - * Schedule the queue to be drained in the next tick. - */ - function scheduleDrainQueue() { - nextTick(drainQueue); - } - - /** - * Drain the handler queue entirely or partially, being careful to allow - * the queue to be extended while it is being processed, and to continue + * Drain the handler queue entirely, being careful to allow the + * queue to be extended while it is being processed, and to continue * processing until it is truly empty. */ function drainQueue() { @@ -674,20 +823,36 @@ define(function () { handlerQueue = []; } - // - // Capture function and array utils - // - /*global setImmediate:true*/ + // capture setTimeout to avoid being caught by fake timers + // used in time based tests + setTimeout = global.setTimeout; - // capture setTimeout to avoid being caught by fake timers used in time based tests - timeout = setTimeout; - nextTick = typeof setImmediate === 'function' - ? typeof window === 'undefined' - ? setImmediate - : setImmediate.bind(window) - : typeof process === 'object' - ? process.nextTick - : function(task) { timeout(task, 0); }; + // Allow attaching the monitor to when() if env has no console + monitorApi = typeof console != 'undefined' ? console : when; + + // Prefer setImmediate or MessageChannel, cascade to node, + // vertx and finally setTimeout + /*global setImmediate,MessageChannel,process*/ + if (typeof setImmediate === 'function') { + nextTick = setImmediate.bind(global); + } else if(typeof MessageChannel !== 'undefined') { + var channel = new MessageChannel(); + channel.port1.onmessage = drainQueue; + nextTick = function() { channel.port2.postMessage(0); }; + } else if (typeof process === 'object' && process.nextTick) { + nextTick = process.nextTick; + } else { + try { + // vert.x 1.x || 2.x + nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; + } catch(ignore) { + nextTick = function(t) { setTimeout(t, 0); }; + } + } + + // + // Capture/polyfill function and array utils + // // Safe function calls funcProto = Function.prototype; @@ -748,40 +913,10 @@ define(function () { return reduced; }; - // - // Utility functions - // - - /** - * Helper that checks arrayOfCallbacks to ensure that each element is either - * a function, or null or undefined. - * @private - * @param {number} start index at which to start checking items in arrayOfCallbacks - * @param {Array} arrayOfCallbacks array to check - * @throws {Error} if any element of arrayOfCallbacks is something other than - * a functions, null, or undefined. - */ - function checkCallbacks(start, arrayOfCallbacks) { - // TODO: Promises/A+ update type checking and docs - var arg, i = arrayOfCallbacks.length; - - while(i > start) { - arg = arrayOfCallbacks[--i]; - - if (arg != null && typeof arg != 'function') { - throw new Error('arg '+i+' must be a function'); - } - } - } - - function noop() {} - function identity(x) { return x; } return when; }); -})( - typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); } -); +})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this); diff --git a/js/package.json b/js/package.json index 8a4e425d..9134bbc0 100644 --- a/js/package.json +++ b/js/package.json @@ -16,7 +16,7 @@ "dependencies": { "bane": "~1.0.0", "faye-websocket": "~0.4.4", - "when": "~2.0.0" + "when": "~2.4.0" }, "devDependencies": { "buster": "~0.6.12", From 1082d41777bc01ce4a4fb2861196f5265b53a289 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Sep 2013 00:40:42 +0200 Subject: [PATCH 104/111] js: Upgrade faye-websocket from 0.4.4 to 0.7.0 --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 9134bbc0..d6060d32 100644 --- a/js/package.json +++ b/js/package.json @@ -15,7 +15,7 @@ "main": "src/mopidy.js", "dependencies": { "bane": "~1.0.0", - "faye-websocket": "~0.4.4", + "faye-websocket": "~0.7.0", "when": "~2.4.0" }, "devDependencies": { From 655915da9d948b0d35f0bb46bd6cc448f670ecf4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Sep 2013 00:41:05 +0200 Subject: [PATCH 105/111] js: Upgrade devDependencies --- js/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/package.json b/js/package.json index d6060d32..cfed0208 100644 --- a/js/package.json +++ b/js/package.json @@ -19,14 +19,14 @@ "when": "~2.4.0" }, "devDependencies": { - "buster": "~0.6.12", + "buster": "~0.6.13", "grunt": "~0.4.1", "grunt-buster": "~0.2.1", "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-jshint": "~0.4.3", - "grunt-contrib-uglify": "~0.2.0", - "grunt-contrib-watch": "~0.4.3", - "phantomjs": "~1.9.0" + "grunt-contrib-jshint": "~0.6.4", + "grunt-contrib-uglify": "~0.2.4", + "grunt-contrib-watch": "~0.5.3", + "phantomjs": "~1.9.2-0" }, "scripts": { "test": "grunt test", From 31d42f743b3f2edb2cc2bab515c41fd9b480edb8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Sep 2013 00:42:09 +0200 Subject: [PATCH 106/111] js: Bump npm package version --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index cfed0208..5b8e46d8 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "mopidy", - "version": "0.1.0", + "version": "0.1.1", "description": "Client lib for controlling a Mopidy music server over a WebSocket", "homepage": "http://www.mopidy.com/", "author": { From 0a2ef6322e583189d55352a5e10825e9a6b70056 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 17 Sep 2013 00:43:58 +0200 Subject: [PATCH 107/111] js: Build updated mopidy.js --- docs/changelog.rst | 2 + mopidy/frontends/http/data/mopidy.js | 537 ++++++++++++++--------- mopidy/frontends/http/data/mopidy.min.js | 4 +- 3 files changed, 340 insertions(+), 203 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index dea88921..888fbfb7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -70,6 +70,8 @@ v0.15.0 (UNRELEASED) objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the ``tlid`` field. (Fixes: :issue:`501`) +- Upgrade Mopidy.js dependencies. + **Extension support** - :class:`~mopidy.config.Secret` is now deserialized to unicode strings instead diff --git a/mopidy/frontends/http/data/mopidy.js b/mopidy/frontends/http/data/mopidy.js index 1669eaff..3e4e832e 100644 --- a/mopidy/frontends/http/data/mopidy.js +++ b/mopidy/frontends/http/data/mopidy.js @@ -1,8 +1,8 @@ -/*! Mopidy.js - built 2013-03-31 +/*! Mopidy.js - built 2013-09-17 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ -((typeof define === "function" && define.amd && function (m) { define(m); }) || +((typeof define === "function" && define.amd && function (m) { define("bane", m); }) || (typeof module === "object" && function (m) { module.exports = m(); }) || function (m) { this.bane = m(); } )(function () { @@ -148,7 +148,7 @@ notifyListener(event, toNotify[i], args); } - toNotify = listeners(this, event).slice() + toNotify = listeners(this, event).slice(); args = slice.call(arguments, 1); for (i = 0, l = toNotify.length; i < l; ++i) { notifyListener(event, toNotify[i], args); @@ -187,27 +187,30 @@ if (typeof window !== "undefined") { * * @author Brian Cavalier * @author John Hann - * @version 2.0.0 + * @version 2.4.0 */ -(function(define) { 'use strict'; -define(function () { +(function(define, global) { 'use strict'; +define(function (require) { // Public API - when.defer = defer; // Create a deferred + when.promise = promise; // Create a pending promise when.resolve = resolve; // Create a resolved promise when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a {promise, resolver} pair when.join = join; // Join 2 or more promises when.all = all; // Resolve a list of promises when.map = map; // Array.map() for promises when.reduce = reduce; // Array.reduce() for promises + when.settle = settle; // Settle a list of promises when.any = any; // One-winner race when.some = some; // Multi-winner race - when.isPromise = isPromise; // Determine if a thing is a promise + when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike + when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable /** * Register an observer for a promise or immediate value. @@ -235,13 +238,35 @@ define(function () { * a trusted when.js promise. Any other duck-typed promise is considered * untrusted. * @constructor + * @param {function} sendMessage function to deliver messages to the promise's handler + * @param {function?} inspect function that reports the promise's state * @name Promise */ - function Promise(then) { - this.then = then; + function Promise(sendMessage, inspect) { + this._message = sendMessage; + this.inspect = inspect; } Promise.prototype = { + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + then: function(onFulfilled, onRejected, onProgress) { + /*jshint unused:false*/ + var args, sendMessage; + + args = arguments; + sendMessage = this._message; + + return _promise(function(resolve, reject, notify) { + sendMessage('when', args, resolve, notify); + }, this._status && this._status.observed()); + }, + /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected @@ -262,9 +287,7 @@ define(function () { * @returns {Promise} */ ensure: function(onFulfilledOrRejected) { - var self = this; - - return this.then(injectHandler, injectHandler).yield(self); + return this.then(injectHandler, injectHandler)['yield'](this); function injectHandler() { return resolve(onFulfilledOrRejected()); @@ -285,6 +308,16 @@ define(function () { }); }, + /** + * Runs a side effect when this promise fulfills, without changing the + * fulfillment value. + * @param {function} onFulfilledSideEffect + * @returns {Promise} + */ + tap: function(onFulfilledSideEffect) { + return this.then(onFulfilledSideEffect)['yield'](this); + }, + /** * Assumes that this promise will fulfill with an array, and arranges * for the onFulfilled to be called with the array as its argument list @@ -340,13 +373,16 @@ define(function () { } /** - * Creates a new Deferred with fully isolated resolver and promise parts, - * either or both of which may be given out safely to consumers. + * Creates a {promise, resolver} pair, either or both of which + * may be given out safely to consumers. * The resolver has resolve, reject, and progress. The promise - * only has then. + * has then plus extended promise API. * * @return {{ * promise: Promise, + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise * resolver: { * resolve: function:Promise, * reject: function:Promise, @@ -394,12 +430,26 @@ define(function () { /** * Creates a new promise whose fate is determined by resolver. - * @private (for now) * @param {function} resolver function(resolve, reject, notify) * @returns {Promise} promise whose fate is determine by resolver */ function promise(resolver) { - var value, handlers = []; + return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); + } + + /** + * Creates a new promise, linked to parent, whose fate is determined + * by resolver. + * @param {function} resolver function(resolve, reject, notify) + * @param {Promise?} status promise from which the new promise is begotten + * @returns {Promise} promise whose fate is determine by resolver + * @private + */ + function _promise(resolver, status) { + var self, value, consumers = []; + + self = new Promise(_message, inspect); + self._status = status; // Call the provider resolver to seal the promise's fate try { @@ -409,29 +459,34 @@ define(function () { } // Return the promise - return new Promise(then); + return self; /** - * Register handlers for this promise. - * @param [onFulfilled] {Function} fulfillment handler - * @param [onRejected] {Function} rejection handler - * @param [onProgress] {Function} progress handler - * @return {Promise} new Promise + * Private message delivery. Queues and delivers messages to + * the promise's ultimate fulfillment value or rejection reason. + * @private + * @param {String} type + * @param {Array} args + * @param {Function} resolve + * @param {Function} notify */ - function then(onFulfilled, onRejected, onProgress) { - return promise(function(resolve, reject, notify) { - handlers - // Call handlers later, after resolution - ? handlers.push(function(value) { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }) - // Call handlers soon, but not in the current stack - : enqueue(function() { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }); - }); + function _message(type, args, resolve, notify) { + consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); + + function deliver(p) { + p._message(type, args, resolve, notify); + } + } + + /** + * Returns a snapshot of the promise's state at the instant inspect() + * is called. The returned object is not live and will not update as + * the promise's state changes. + * @returns {{ state:String, value?:*, reason?:* }} status snapshot + * of the promise. + */ + function inspect() { + return value ? value.inspect() : toPendingState(); } /** @@ -440,14 +495,17 @@ define(function () { * @param {*|Promise} val resolution value */ function promiseResolve(val) { - if(!handlers) { + if(!consumers) { return; } value = coerce(val); - scheduleHandlers(handlers, value); + scheduleConsumers(consumers, value); + consumers = undef; - handlers = undef; + if(status) { + updateStatus(value, status); + } } /** @@ -463,27 +521,90 @@ define(function () { * @param {*} update progress event payload to pass to all listeners */ function promiseNotify(update) { - if(handlers) { - scheduleHandlers(handlers, progressing(update)); + if(consumers) { + scheduleConsumers(consumers, progressed(update)); } } } + /** + * Creates a fulfilled, local promise as a proxy for a value + * NOTE: must never be exposed + * @param {*} value fulfillment value + * @returns {Promise} + */ + function fulfilled(value) { + return near( + new NearFulfilledProxy(value), + function() { return toFulfilledState(value); } + ); + } + + /** + * Creates a rejected, local promise with the supplied reason + * NOTE: must never be exposed + * @param {*} reason rejection reason + * @returns {Promise} + */ + function rejected(reason) { + return near( + new NearRejectedProxy(reason), + function() { return toRejectedState(reason); } + ); + } + + /** + * Creates a near promise using the provided proxy + * NOTE: must never be exposed + * @param {object} proxy proxy for the promise's ultimate value or reason + * @param {function} inspect function that returns a snapshot of the + * returned near promise's state + * @returns {Promise} + */ + function near(proxy, inspect) { + return new Promise(function (type, args, resolve) { + try { + resolve(proxy[type].apply(proxy, args)); + } catch(e) { + resolve(rejected(e)); + } + }, inspect); + } + + /** + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise + */ + function progressed(update) { + return new Promise(function (type, args, _, notify) { + var onProgress = args[2]; + try { + notify(typeof onProgress === 'function' ? onProgress(update) : update); + } catch(e) { + notify(e); + } + }); + } + /** * Coerces x to a trusted Promise * * @private * @param {*} x thing to coerce - * @returns {Promise} Guaranteed to return a trusted Promise. If x + * @returns {*} Guaranteed to return a trusted Promise. If x * is trusted, returns x, otherwise, returns a new, trusted, already-resolved * Promise whose resolution value is: * * the resolution value of x if it's a foreign promise, or * * x if it's a value */ function coerce(x) { - if(x instanceof Promise) { + if (x instanceof Promise) { return x; - } else if (x !== Object(x)) { + } + + if (!(x === Object(x) && 'then' in x)) { return fulfilled(x); } @@ -510,61 +631,34 @@ define(function () { } /** - * Create an already-fulfilled promise for the supplied value - * @private + * Proxy for a near, fulfilled value * @param {*} value - * @return {Promise} fulfilled promise + * @constructor */ - function fulfilled(value) { - var self = new Promise(function (onFulfilled) { - try { - return typeof onFulfilled == 'function' - ? coerce(onFulfilled(value)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearFulfilledProxy(value) { + this.value = value; } + NearFulfilledProxy.prototype.when = function(onResult) { + return typeof onResult === 'function' ? onResult(this.value) : this.value; + }; + /** - * Create an already-rejected promise with the supplied rejection reason. - * @private + * Proxy for a near rejection * @param {*} reason - * @return {Promise} rejected promise + * @constructor */ - function rejected(reason) { - var self = new Promise(function (_, onRejected) { - try { - return typeof onRejected == 'function' - ? coerce(onRejected(reason)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearRejectedProxy(reason) { + this.reason = reason; } - /** - * Create a progress promise with the supplied update. - * @private - * @param {*} update - * @return {Promise} progress promise - */ - function progressing(update) { - var self = new Promise(function (_, __, onProgress) { - try { - return typeof onProgress == 'function' - ? progressing(onProgress(update)) : self; - } catch (e) { - return progressing(e); - } - }); - - return self; - } + NearRejectedProxy.prototype.when = function(_, onError) { + if(typeof onError === 'function') { + return onError(this.reason); + } else { + throw this.reason; + } + }; /** * Schedule a task that will process a list of handlers @@ -573,7 +667,7 @@ define(function () { * @param {Array} handlers queue of handlers to execute * @param {*} value passed as the only arg to each handler */ - function scheduleHandlers(handlers, value) { + function scheduleConsumers(handlers, value) { enqueue(function() { var handler, i = 0; while (handler = handlers[i++]) { @@ -582,14 +676,23 @@ define(function () { }); } + function updateStatus(value, status) { + value.then(statusFulfilled, statusRejected); + + function statusFulfilled() { status.fulfilled(); } + function statusRejected(r) { status.rejected(r); } + } + /** - * Determines if promiseOrValue is a promise or not - * - * @param {*} promiseOrValue anything - * @returns {boolean} true if promiseOrValue is a {@link Promise} + * Determines if x is promise-like, i.e. a thenable object + * NOTE: Will return true for *any thenable object*, and isn't truly + * safe, since it may attempt to access the `then` property of x (i.e. + * clever/malicious getters may do weird things) + * @param {*} x anything + * @returns {boolean} true if x is promise-like */ - function isPromise(promiseOrValue) { - return promiseOrValue && typeof promiseOrValue.then === 'function'; + function isPromiseLike(x) { + return x && typeof x.then === 'function'; } /** @@ -601,17 +704,15 @@ define(function () { * @param {Array} promisesOrValues array of anything, may contain a mix * of promises and values * @param howMany {number} number of promisesOrValues to resolve - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to an array of howMany values that * resolved first, or will reject with an array of * (promisesOrValues.length - howMany) + 1 rejection reasons. */ function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { - checkCallbacks(2, arguments); - return when(promisesOrValues, function(promisesOrValues) { return promise(resolveSome).then(onFulfilled, onRejected, onProgress); @@ -635,7 +736,7 @@ define(function () { rejectOne = function(reason) { reasons.push(reason); if(!--toReject) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; reject(reasons); } }; @@ -644,7 +745,7 @@ define(function () { // This orders the values based on promise resolution order values.push(val); if (!--toResolve) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; resolve(values); } }; @@ -674,9 +775,9 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to the value that resolved first, or * will reject with an array of all rejected inputs. */ @@ -697,14 +798,13 @@ define(function () { * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} */ function all(promisesOrValues, onFulfilled, onRejected, onProgress) { - checkCallbacks(1, arguments); - return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); + return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); } /** @@ -713,28 +813,49 @@ define(function () { * have fulfilled, or will reject when *any one* of the input promises rejects. */ function join(/* ...promises */) { - return map(arguments, identity); + return _map(arguments, identity); } /** - * Traditional map function, similar to `Array.prototype.map()`, but allows - * input to contain {@link Promise}s and/or values, and mapFunc may return - * either a value or a {@link Promise} - * - * @param {Array|Promise} array array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function} mapFunc mapping function mapFunc(value) which may return - * either a {@link Promise} or value - * @returns {Promise} a {@link Promise} that will resolve to an array containing - * the mapped output values. + * Settles all input promises such that they are guaranteed not to + * be pending once the returned promise fulfills. The returned promise + * will always fulfill, except in the case where `array` is a promise + * that rejects. + * @param {Array|Promise} array or promise for array of promises to settle + * @returns {Promise} promise that always fulfills with an array of + * outcome snapshots for each input promise. + */ + function settle(array) { + return _map(array, toFulfilledState, toRejectedState); + } + + /** + * Promise-aware array map function, similar to `Array.prototype.map()`, + * but input array may contain promises or values. + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. */ function map(array, mapFunc) { + return _map(array, mapFunc); + } + + /** + * Internal map that allows a fallback to handle rejections + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @param {function?} fallback function to handle rejected promises + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. + */ + function _map(array, mapFunc, fallback) { return when(array, function(array) { - return promise(resolveMap); + return _promise(resolveMap); function resolveMap(resolve, reject, notify) { - var results, len, toResolve, resolveOne, i; + var results, len, toResolve, i; // Since we know the resulting length, we can preallocate the results // array to avoid array expansions. @@ -743,27 +864,28 @@ define(function () { if(!toResolve) { resolve(results); - } else { + return; + } - resolveOne = function(item, i) { - when(item, mapFunc).then(function(mapped) { - results[i] = mapped; - - if(!--toResolve) { - resolve(results); - } - }, reject, notify); - }; - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolveOne(array[i], i); - } else { - --toResolve; - } + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; } } + + function resolveOne(item, i) { + when(item, mapFunc, fallback).then(function(mapped) { + results[i] = mapped; + notify(mapped); + + if(!--toResolve) { + resolve(results); + } + }, reject); + } } }); } @@ -803,12 +925,46 @@ define(function () { }); } + // Snapshot states + + /** + * Creates a fulfilled state snapshot + * @private + * @param {*} x any value + * @returns {{state:'fulfilled',value:*}} + */ + function toFulfilledState(x) { + return { state: 'fulfilled', value: x }; + } + + /** + * Creates a rejected state snapshot + * @private + * @param {*} x any reason + * @returns {{state:'rejected',reason:*}} + */ + function toRejectedState(x) { + return { state: 'rejected', reason: x }; + } + + /** + * Creates a pending state snapshot + * @private + * @returns {{state:'pending'}} + */ + function toPendingState() { + return { state: 'pending' }; + } + // - // Utilities, etc. + // Internals, utilities, etc. // var reduceArray, slice, fcall, nextTick, handlerQueue, - timeout, funcProto, call, arrayProto, undef; + setTimeout, funcProto, call, arrayProto, monitorApi, + cjsRequire, undef; + + cjsRequire = require; // // Shared handler queue processing @@ -826,20 +982,13 @@ define(function () { */ function enqueue(task) { if(handlerQueue.push(task) === 1) { - scheduleDrainQueue(); + nextTick(drainQueue); } } /** - * Schedule the queue to be drained in the next tick. - */ - function scheduleDrainQueue() { - nextTick(drainQueue); - } - - /** - * Drain the handler queue entirely or partially, being careful to allow - * the queue to be extended while it is being processed, and to continue + * Drain the handler queue entirely, being careful to allow the + * queue to be extended while it is being processed, and to continue * processing until it is truly empty. */ function drainQueue() { @@ -852,20 +1001,36 @@ define(function () { handlerQueue = []; } - // - // Capture function and array utils - // - /*global setImmediate:true*/ + // capture setTimeout to avoid being caught by fake timers + // used in time based tests + setTimeout = global.setTimeout; - // capture setTimeout to avoid being caught by fake timers used in time based tests - timeout = setTimeout; - nextTick = typeof setImmediate === 'function' - ? typeof window === 'undefined' - ? setImmediate - : setImmediate.bind(window) - : typeof process === 'object' - ? process.nextTick - : function(task) { timeout(task, 0); }; + // Allow attaching the monitor to when() if env has no console + monitorApi = typeof console != 'undefined' ? console : when; + + // Prefer setImmediate or MessageChannel, cascade to node, + // vertx and finally setTimeout + /*global setImmediate,MessageChannel,process*/ + if (typeof setImmediate === 'function') { + nextTick = setImmediate.bind(global); + } else if(typeof MessageChannel !== 'undefined') { + var channel = new MessageChannel(); + channel.port1.onmessage = drainQueue; + nextTick = function() { channel.port2.postMessage(0); }; + } else if (typeof process === 'object' && process.nextTick) { + nextTick = process.nextTick; + } else { + try { + // vert.x 1.x || 2.x + nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; + } catch(ignore) { + nextTick = function(t) { setTimeout(t, 0); }; + } + } + + // + // Capture/polyfill function and array utils + // // Safe function calls funcProto = Function.prototype; @@ -926,43 +1091,13 @@ define(function () { return reduced; }; - // - // Utility functions - // - - /** - * Helper that checks arrayOfCallbacks to ensure that each element is either - * a function, or null or undefined. - * @private - * @param {number} start index at which to start checking items in arrayOfCallbacks - * @param {Array} arrayOfCallbacks array to check - * @throws {Error} if any element of arrayOfCallbacks is something other than - * a functions, null, or undefined. - */ - function checkCallbacks(start, arrayOfCallbacks) { - // TODO: Promises/A+ update type checking and docs - var arg, i = arrayOfCallbacks.length; - - while(i > start) { - arg = arrayOfCallbacks[--i]; - - if (arg != null && typeof arg != 'function') { - throw new Error('arg '+i+' must be a function'); - } - } - } - - function noop() {} - function identity(x) { return x; } return when; }); -})( - typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); } -); +})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this); if (typeof module === "object" && typeof require === "function") { var bane = require("bane"); diff --git a/mopidy/frontends/http/data/mopidy.min.js b/mopidy/frontends/http/data/mopidy.min.js index 08ee1dac..75d9fff1 100644 --- a/mopidy/frontends/http/data/mopidy.min.js +++ b/mopidy/frontends/http/data/mopidy.min.js @@ -1,5 +1,5 @@ -/*! Mopidy.js - built 2013-03-31 +/*! Mopidy.js - built 2013-09-17 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ -function Mopidy(e){return this instanceof Mopidy?(this._settings=this._configure(e||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(e)}if(("function"==typeof define&&define.amd&&function(e){define(e)}||"object"==typeof module&&function(e){module.exports=e()}||function(e){this.bane=e()})(function(){"use strict";function e(e,t,n){var o,i=n.length;if(i>0)for(o=0;i>o;++o)n[o](e,t);else setTimeout(function(){throw t.message=e+" listener threw error: "+t.message,t},0)}function t(e){if("function"!=typeof e)throw new TypeError("Listener is not function");return e}function n(e){return e.supervisors||(e.supervisors=[]),e.supervisors}function o(e,t){return e.listeners||(e.listeners={}),t&&!e.listeners[t]&&(e.listeners[t]=[]),t?e.listeners[t]:e.listeners}function i(e){return e.errbacks||(e.errbacks=[]),e.errbacks}function r(r){function c(t,n,o){try{n.listener.apply(n.thisp||r,o)}catch(s){e(t,s,i(r))}}return r=r||{},r.on=function(e,i,r){return"function"==typeof e?n(this).push({listener:e,thisp:i}):(o(this,e).push({listener:t(i),thisp:r}),void 0)},r.off=function(e,t){var r,s,c,f;if(!e){r=n(this),r.splice(0,r.length),s=o(this);for(c in s)s.hasOwnProperty(c)&&(r=o(this,c),r.splice(0,r.length));return r=i(this),r.splice(0,r.length),void 0}if("function"==typeof e?(r=n(this),t=e):r=o(this,e),!t)return r.splice(0,r.length),void 0;for(c=0,f=r.length;f>c;++c)if(r[c].listener===t)return r.splice(c,1),void 0},r.once=function(e,t,n){var o=function(){r.off(e,o),t.apply(this,arguments)};r.on(e,o,n)},r.bind=function(e,t){var n,o,i;if(t)for(o=0,i=t.length;i>o;++o){if("function"!=typeof e[t[o]])throw Error("No such method "+t[o]);this.on(t[o],e[t[o]],e)}else for(n in e)"function"==typeof e[n]&&this.on(n,e[n],e);return e},r.emit=function(e){var t,i,r=n(this),f=s.call(arguments);for(t=0,i=r.length;i>t;++t)c(e,r[t],f);for(r=o(this,e).slice(),f=s.call(arguments,1),t=0,i=r.length;i>t;++t)c(e,r[t],f)},r.errback=function(e){this.errbacks||(this.errbacks=[]),this.errbacks.push(t(e))},r}var s=Array.prototype.slice;return{createEventEmitter:r}}),"undefined"!=typeof window&&(window.define=function(e){try{delete window.define}catch(t){window.define=void 0}window.when=e()},window.define.amd={}),function(e){"use strict";e(function(){function e(e,t,o,i){return n(e).then(t,o,i)}function t(e){this.then=e}function n(e){return r(function(t){t(e)})}function o(t){return e(t,f)}function i(){function e(e,r,s){t.resolve=t.resolver.resolve=function(t){return i?n(t):(i=!0,e(t),o)},t.reject=t.resolver.reject=function(e){return i?n(f(e)):(i=!0,r(e),o)},t.notify=t.resolver.notify=function(e){return s(e),e}}var t,o,i;return t={promise:R,resolve:R,reject:R,notify:R,resolver:{resolve:R,reject:R,notify:R}},t.promise=o=r(e),t}function r(e){function n(e,t,n){return r(function(o,i,r){p?p.push(function(s){s.then(e,t,n).then(o,i,r)}):k(function(){h.then(e,t,n).then(o,i,r)})})}function o(e){p&&(h=s(e),a(p,h),p=R)}function i(e){o(f(e))}function c(e){p&&a(p,u(e))}var h,p=[];try{e(o,i,c)}catch(l){i(l)}return new t(n)}function s(e){return e instanceof t?e:e!==Object(e)?c(e):r(function(t,n,o){k(function(){try{var i=e.then;"function"==typeof i?j(i,e,t,n,o):t(c(e))}catch(r){n(r)}})})}function c(e){var n=new t(function(t){try{return"function"==typeof t?s(t(e)):n}catch(o){return f(o)}});return n}function f(e){var n=new t(function(t,o){try{return"function"==typeof o?s(o(e)):n}catch(i){return f(i)}});return n}function u(e){var n=new t(function(t,o,i){try{return"function"==typeof i?u(i(e)):n}catch(r){return u(r)}});return n}function a(e,t){k(function(){for(var n,o=0;n=e[o++];)n(t)})}function h(e){return e&&"function"==typeof e.then}function p(t,n,o,i,s){return m(2,arguments),e(t,function(t){function c(o,i,r){function s(e){l(e)}function c(e){p(e)}var f,u,a,h,p,l,d,y;if(d=t.length>>>0,f=Math.max(0,Math.min(n,d)),a=[],u=d-f+1,h=[],f)for(l=function(e){h.push(e),--u||(p=l=v,i(h))},p=function(e){a.push(e),--f||(p=l=v,o(a))},y=0;d>y;++y)y in t&&e(t[y],c,s,r);else o(a)}return r(c).then(o,i,s)})}function l(e,t,n,o){function i(e){return t?t(e[0]):e[0]}return p(e,1,i,n,o)}function d(e,t,n,o){return m(1,arguments),b(e,M).then(t,n,o)}function y(){return b(arguments,M)}function b(t,n){return e(t,function(t){function o(o,i,r){var s,c,f,u,a;if(f=c=t.length>>>0,s=[],f)for(u=function(t,c){e(t,n).then(function(e){s[c]=e,--f||o(s)},i,r)},a=0;c>a;a++)a in t?u(t[a],a):--f;else o(s)}return r(o)})}function w(t,n){var o=j(E,arguments,1);return e(t,function(t){var i;return i=t.length,o[0]=function(t,o,r){return e(t,function(t){return e(o,function(e){return n(t,e,r,i)})})},S.apply(t,o)})}function k(e){1===W.push(e)&&g()}function g(){D(_)}function _(){for(var e,t=0;e=W[t++];)e();W=[]}function m(e,t){for(var n,o=t.length;o>e;)if(n=t[--o],null!=n&&"function"!=typeof n)throw Error("arg "+o+" must be a function")}function v(){}function M(e){return e}e.defer=i,e.resolve=n,e.reject=o,e.join=y,e.all=d,e.map=b,e.reduce=w,e.any=l,e.some=p,e.isPromise=h,t.prototype={otherwise:function(e){return this.then(R,e)},ensure:function(e){function t(){return n(e())}var o=this;return this.then(t,t).yield(o)},yield:function(e){return this.then(function(){return e})},spread:function(e){return this.then(function(t){return d(t,function(t){return e.apply(R,t)})})},always:function(e,t){return this.then(e,e,t)}};var S,E,j,D,W,O,q,C,x,R;return W=[],O=setTimeout,D="function"==typeof setImmediate?"undefined"==typeof window?setImmediate:setImmediate.bind(window):"object"==typeof process?process.nextTick:function(e){O(e,0)},q=Function.prototype,C=q.call,j=q.bind?C.bind(C):function(e,t){return e.apply(t,E.call(arguments,2))},x=[],E=x.slice,S=x.reduce||function(e){var t,n,o,i,r;if(r=0,t=Object(this),i=t.length>>>0,n=arguments,1>=n.length)for(;;){if(r in t){o=t[r++];break}if(++r>=i)throw new TypeError}else o=n[1];for(;i>r;++r)r in t&&(o=e(o,t[r],r,t));return o},e})}("function"==typeof define&&define.amd?define:function(e){module.exports=e()}),"object"==typeof module&&"function"==typeof require)var bane=require("bane"),websocket=require("faye-websocket"),when=require("when");Mopidy.WebSocket="object"==typeof module&&"function"==typeof require?websocket.Client:window.WebSocket,Mopidy.prototype._configure=function(e){var t="undefined"!=typeof document&&document.location.host||"localhost";return e.webSocketUrl=e.webSocketUrl||"ws://"+t+"/mopidy/ws/",e.autoConnect!==!1&&(e.autoConnect=!0),e.backoffDelayMin=e.backoffDelayMin||1e3,e.backoffDelayMax=e.backoffDelayMax||64e3,e},Mopidy.prototype._getConsole=function(){var e=e!==void 0&&e||{};return e.log=e.log||function(){},e.warn=e.warn||function(){},e.error=e.error||function(){},e},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===Mopidy.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new Mopidy.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(e){this.emit("websocket:close",e)}.bind(this),this._webSocket.onerror=function(e){this.emit("websocket:error",e)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(e){this.emit("websocket:incomingMessage",e)}.bind(this)},Mopidy.prototype._cleanup=function(e){Object.keys(this._pendingRequests).forEach(function(t){var n=this._pendingRequests[t];delete this._pendingRequests[t],n.reject({message:"WebSocket closed",closeEvent:e})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(e){this._console.warn("WebSocket error:",e.stack||e)},Mopidy.prototype._send=function(e){var t=when.defer();switch(this._webSocket.readyState){case Mopidy.WebSocket.CONNECTING:t.resolver.reject({message:"WebSocket is still connecting"});break;case Mopidy.WebSocket.CLOSING:t.resolver.reject({message:"WebSocket is closing"});break;case Mopidy.WebSocket.CLOSED:t.resolver.reject({message:"WebSocket is closed"});break;default:e.jsonrpc="2.0",e.id=this._nextRequestId(),this._pendingRequests[e.id]=t.resolver,this._webSocket.send(JSON.stringify(e)),this.emit("websocket:outgoingMessage",e)}return t.promise},Mopidy.prototype._nextRequestId=function(){var e=-1;return function(){return e+=1}}(),Mopidy.prototype._handleMessage=function(e){try{var t=JSON.parse(e.data);t.hasOwnProperty("id")?this._handleResponse(t):t.hasOwnProperty("event")?this._handleEvent(t):this._console.warn("Unknown message type received. Message was: "+e.data)}catch(n){if(!(n instanceof SyntaxError))throw n;this._console.warn("WebSocket message parsing failed. Message was: "+e.data)}},Mopidy.prototype._handleResponse=function(e){if(!this._pendingRequests.hasOwnProperty(e.id))return this._console.warn("Unexpected response received. Message was:",e),void 0;var t=this._pendingRequests[e.id];delete this._pendingRequests[e.id],e.hasOwnProperty("result")?t.resolve(e.result):e.hasOwnProperty("error")?(t.reject(e.error),this._console.warn("Server returned error:",e.error)):(t.reject({message:"Response without 'result' or 'error' received",data:{response:e}}),this._console.warn("Response without 'result' or 'error' received. Message was:",e))},Mopidy.prototype._handleEvent=function(e){var t=e.event,n=e;delete n.event,this.emit("event:"+this._snakeToCamel(t),n)},Mopidy.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(e){var t=function(e){return function(){var t=Array.prototype.slice.call(arguments);return this._send({method:e,params:t})}.bind(this)}.bind(this),n=function(e){var t=e.split(".");return t.length>=1&&"core"===t[0]&&(t=t.slice(1)),t},o=function(e){var t=this;return e.forEach(function(e){e=this._snakeToCamel(e),t[e]=t[e]||{},t=t[e]}.bind(this)),t}.bind(this),i=function(i){var r=n(i),s=this._snakeToCamel(r.slice(-1)[0]),c=o(r.slice(0,-1));c[s]=t(i),c[s].description=e[i].description,c[s].params=e[i].params}.bind(this);Object.keys(e).forEach(i),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(e){return e.replace(/(_[a-z])/g,function(e){return e.toUpperCase().replace("_","")})},"object"==typeof exports&&(exports.Mopidy=Mopidy); \ No newline at end of file +function Mopidy(a){return this instanceof Mopidy?(this._settings=this._configure(a||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(a)}if(("function"==typeof define&&define.amd&&function(a){define("bane",a)}||"object"==typeof module&&function(a){module.exports=a()}||function(a){this.bane=a()})(function(){"use strict";function a(a,b,c){var d,e=c.length;if(e>0)for(d=0;e>d;++d)c[d](a,b);else setTimeout(function(){throw b.message=a+" listener threw error: "+b.message,b},0)}function b(a){if("function"!=typeof a)throw new TypeError("Listener is not function");return a}function c(a){return a.supervisors||(a.supervisors=[]),a.supervisors}function d(a,b){return a.listeners||(a.listeners={}),b&&!a.listeners[b]&&(a.listeners[b]=[]),b?a.listeners[b]:a.listeners}function e(a){return a.errbacks||(a.errbacks=[]),a.errbacks}function f(f){function h(b,c,d){try{c.listener.apply(c.thisp||f,d)}catch(g){a(b,g,e(f))}}return f=f||{},f.on=function(a,e,f){return"function"==typeof a?c(this).push({listener:a,thisp:e}):(d(this,a).push({listener:b(e),thisp:f}),void 0)},f.off=function(a,b){var f,g,h,i;if(!a){f=c(this),f.splice(0,f.length),g=d(this);for(h in g)g.hasOwnProperty(h)&&(f=d(this,h),f.splice(0,f.length));return f=e(this),f.splice(0,f.length),void 0}if("function"==typeof a?(f=c(this),b=a):f=d(this,a),!b)return f.splice(0,f.length),void 0;for(h=0,i=f.length;i>h;++h)if(f[h].listener===b)return f.splice(h,1),void 0},f.once=function(a,b,c){var d=function(){f.off(a,d),b.apply(this,arguments)};f.on(a,d,c)},f.bind=function(a,b){var c,d,e;if(b)for(d=0,e=b.length;e>d;++d){if("function"!=typeof a[b[d]])throw new Error("No such method "+b[d]);this.on(b[d],a[b[d]],a)}else for(c in a)"function"==typeof a[c]&&this.on(c,a[c],a);return a},f.emit=function(a){var b,e,f=c(this),i=g.call(arguments);for(b=0,e=f.length;e>b;++b)h(a,f[b],i);for(f=d(this,a).slice(),i=g.call(arguments,1),b=0,e=f.length;e>b;++b)h(a,f[b],i)},f.errback=function(a){this.errbacks||(this.errbacks=[]),this.errbacks.push(b(a))},f}var g=Array.prototype.slice;return{createEventEmitter:f}}),"undefined"!=typeof window&&(window.define=function(a){try{delete window.define}catch(b){window.define=void 0}window.when=a()},window.define.amd={}),function(a,b){"use strict";a(function(a){function c(a,b,c,d){return e(a).then(b,c,d)}function d(a,b){this._message=a,this.inspect=b}function e(a){return h(function(b){b(a)})}function f(a){return c(a,k)}function g(){function a(a,f,g){b.resolve=b.resolver.resolve=function(b){return d?e(b):(d=!0,a(b),c)},b.reject=b.resolver.reject=function(a){return d?e(k(a)):(d=!0,f(a),c)},b.notify=b.resolver.notify=function(a){return g(a),a}}var b,c,d;return b={promise:S,resolve:S,reject:S,notify:S,resolver:{resolve:S,reject:S,notify:S}},b.promise=c=h(a),b}function h(a){return i(a,Q.PromiseStatus&&Q.PromiseStatus())}function i(a,b){function c(a,b,c,d){function e(e){e._message(a,b,c,d)}l?l.push(e):E(function(){e(j)})}function e(){return j?j.inspect():D()}function f(a){l&&(j=n(a),q(l,j),l=S,b&&r(j,b))}function g(a){f(k(a))}function h(a){l&&q(l,m(a))}var i,j,l=[];i=new d(c,e),i._status=b;try{a(f,g,h)}catch(o){g(o)}return i}function j(a){return l(new o(a),function(){return B(a)})}function k(a){return l(new p(a),function(){return C(a)})}function l(a,b){return new d(function(b,c,d){try{d(a[b].apply(a,c))}catch(e){d(k(e))}},b)}function m(a){return new d(function(b,c,d,e){var f=c[2];try{e("function"==typeof f?f(a):a)}catch(g){e(g)}})}function n(a){return a instanceof d?a:a===Object(a)&&"then"in a?h(function(b,c,d){E(function(){try{var e=a.then;"function"==typeof e?J(e,a,b,c,d):b(j(a))}catch(f){c(f)}})}):j(a)}function o(a){this.value=a}function p(a){this.reason=a}function q(a,b){E(function(){for(var c,d=0;c=a[d++];)c(b)})}function r(a,b){function c(){b.fulfilled()}function d(a){b.rejected(a)}a.then(c,d)}function s(a){return a&&"function"==typeof a.then}function t(a,b,d,e,f){return c(a,function(a){function g(d,e,f){function g(a){n(a)}function h(a){m(a)}var i,j,k,l,m,n,o,p;if(o=a.length>>>0,i=Math.max(0,Math.min(b,o)),k=[],j=o-i+1,l=[],i)for(n=function(a){l.push(a),--j||(m=n=G,e(l))},m=function(a){k.push(a),--i||(m=n=G,d(k))},p=0;o>p;++p)p in a&&c(a[p],h,g,f);else d(k)}return h(g).then(d,e,f)})}function u(a,b,c,d){function e(a){return b?b(a[0]):a[0]}return t(a,1,e,c,d)}function v(a,b,c,d){return z(a,G).then(b,c,d)}function w(){return z(arguments,G)}function x(a){return z(a,B,C)}function y(a,b){return z(a,b)}function z(a,b,d){return c(a,function(a){function e(e,f,g){function h(a,h){c(a,b,d).then(function(a){i[h]=a,g(a),--k||e(i)},f)}var i,j,k,l;if(k=j=a.length>>>0,i=[],!k)return e(i),void 0;for(l=0;j>l;l++)l in a?h(a[l],l):--k}return i(e)})}function A(a,b){var d=J(I,arguments,1);return c(a,function(a){var e;return e=a.length,d[0]=function(a,d,f){return c(a,function(a){return c(d,function(c){return b(a,c,f,e)})})},H.apply(a,d)})}function B(a){return{state:"fulfilled",value:a}}function C(a){return{state:"rejected",reason:a}}function D(){return{state:"pending"}}function E(a){1===L.push(a)&&K(F)}function F(){for(var a,b=0;a=L[b++];)a();L=[]}function G(a){return a}c.promise=h,c.resolve=e,c.reject=f,c.defer=g,c.join=w,c.all=v,c.map=y,c.reduce=A,c.settle=x,c.any=u,c.some=t,c.isPromise=s,c.isPromiseLike=s,d.prototype={then:function(){var a,b;return a=arguments,b=this._message,i(function(c,d,e){b("when",a,c,e)},this._status&&this._status.observed())},otherwise:function(a){return this.then(S,a)},ensure:function(a){function b(){return e(a())}return this.then(b,b).yield(this)},yield:function(a){return this.then(function(){return a})},tap:function(a){return this.then(a).yield(this)},spread:function(a){return this.then(function(b){return v(b,function(b){return a.apply(S,b)})})},always:function(a,b){return this.then(a,a,b)}},o.prototype.when=function(a){return"function"==typeof a?a(this.value):this.value},p.prototype.when=function(a,b){if("function"==typeof b)return b(this.reason);throw this.reason};var H,I,J,K,L,M,N,O,P,Q,R,S;if(R=a,L=[],M=b.setTimeout,Q="undefined"!=typeof console?console:c,"function"==typeof setImmediate)K=setImmediate.bind(b);else if("undefined"!=typeof MessageChannel){var T=new MessageChannel;T.port1.onmessage=F,K=function(){T.port2.postMessage(0)}}else if("object"==typeof process&&process.nextTick)K=process.nextTick;else try{K=R("vertx").runOnLoop||R("vertx").runOnContext}catch(U){K=function(a){M(a,0)}}return N=Function.prototype,O=N.call,J=N.bind?O.bind(O):function(a,b){return a.apply(b,I.call(arguments,2))},P=[],I=P.slice,H=P.reduce||function(a){var b,c,d,e,f;if(f=0,b=Object(this),e=b.length>>>0,c=arguments,c.length<=1)for(;;){if(f in b){d=b[f++];break}if(++f>=e)throw new TypeError}else d=c[1];for(;e>f;++f)f in b&&(d=a(d,b[f],f,b));return d},c})}("function"==typeof define&&define.amd?define:function(a){module.exports=a(require)},this),"object"==typeof module&&"function"==typeof require)var bane=require("bane"),websocket=require("faye-websocket"),when=require("when");Mopidy.WebSocket="object"==typeof module&&"function"==typeof require?websocket.Client:window.WebSocket,Mopidy.prototype._configure=function(a){var b="undefined"!=typeof document&&document.location.host||"localhost";return a.webSocketUrl=a.webSocketUrl||"ws://"+b+"/mopidy/ws/",a.autoConnect!==!1&&(a.autoConnect=!0),a.backoffDelayMin=a.backoffDelayMin||1e3,a.backoffDelayMax=a.backoffDelayMax||64e3,a},Mopidy.prototype._getConsole=function(){var a="undefined"!=typeof a&&a||{};return a.log=a.log||function(){},a.warn=a.warn||function(){},a.error=a.error||function(){},a},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===Mopidy.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new Mopidy.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(a){this.emit("websocket:close",a)}.bind(this),this._webSocket.onerror=function(a){this.emit("websocket:error",a)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(a){this.emit("websocket:incomingMessage",a)}.bind(this)},Mopidy.prototype._cleanup=function(a){Object.keys(this._pendingRequests).forEach(function(b){var c=this._pendingRequests[b];delete this._pendingRequests[b],c.reject({message:"WebSocket closed",closeEvent:a})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(a){this._console.warn("WebSocket error:",a.stack||a)},Mopidy.prototype._send=function(a){var b=when.defer();switch(this._webSocket.readyState){case Mopidy.WebSocket.CONNECTING:b.resolver.reject({message:"WebSocket is still connecting"});break;case Mopidy.WebSocket.CLOSING:b.resolver.reject({message:"WebSocket is closing"});break;case Mopidy.WebSocket.CLOSED:b.resolver.reject({message:"WebSocket is closed"});break;default:a.jsonrpc="2.0",a.id=this._nextRequestId(),this._pendingRequests[a.id]=b.resolver,this._webSocket.send(JSON.stringify(a)),this.emit("websocket:outgoingMessage",a)}return b.promise},Mopidy.prototype._nextRequestId=function(){var a=-1;return function(){return a+=1}}(),Mopidy.prototype._handleMessage=function(a){try{var b=JSON.parse(a.data);b.hasOwnProperty("id")?this._handleResponse(b):b.hasOwnProperty("event")?this._handleEvent(b):this._console.warn("Unknown message type received. Message was: "+a.data)}catch(c){if(!(c instanceof SyntaxError))throw c;this._console.warn("WebSocket message parsing failed. Message was: "+a.data)}},Mopidy.prototype._handleResponse=function(a){if(!this._pendingRequests.hasOwnProperty(a.id))return this._console.warn("Unexpected response received. Message was:",a),void 0;var b=this._pendingRequests[a.id];delete this._pendingRequests[a.id],a.hasOwnProperty("result")?b.resolve(a.result):a.hasOwnProperty("error")?(b.reject(a.error),this._console.warn("Server returned error:",a.error)):(b.reject({message:"Response without 'result' or 'error' received",data:{response:a}}),this._console.warn("Response without 'result' or 'error' received. Message was:",a))},Mopidy.prototype._handleEvent=function(a){var b=a.event,c=a;delete c.event,this.emit("event:"+this._snakeToCamel(b),c)},Mopidy.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(a){var b=function(a){return function(){var b=Array.prototype.slice.call(arguments);return this._send({method:a,params:b})}.bind(this)}.bind(this),c=function(a){var b=a.split(".");return b.length>=1&&"core"===b[0]&&(b=b.slice(1)),b},d=function(a){var b=this;return a.forEach(function(a){a=this._snakeToCamel(a),b[a]=b[a]||{},b=b[a]}.bind(this)),b}.bind(this),e=function(e){var f=c(e),g=this._snakeToCamel(f.slice(-1)[0]),h=d(f.slice(0,-1));h[g]=b(e),h[g].description=a[e].description,h[g].params=a[e].params}.bind(this);Object.keys(a).forEach(e),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(a){return a.replace(/(_[a-z])/g,function(a){return a.toUpperCase().replace("_","")})},"object"==typeof exports&&(exports.Mopidy=Mopidy); \ No newline at end of file From 882f5a48d16946c0c416df36ee10ad5d931af35b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 19 Sep 2013 19:24:05 +0200 Subject: [PATCH 108/111] docs: Update changelog for v0.15.0 --- docs/changelog.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 888fbfb7..0fb05f8c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,15 +4,16 @@ Changelog This changelog is used to track all major changes to Mopidy. -v0.15.0 (UNRELEASED) +v0.15.0 (2013-09-19) ==================== -(no description yet) +A release with a number of small and medium fixes, with no specific focus. **Dependencies** - Mopidy no longer supports Python 2.6. Currently, the only Python version - supported by Mopidy is Python 2.7. (Fixes: :issue:`344`) + supported by Mopidy is Python 2.7. We're continuously working towards running + Mopidy on Python 3. (Fixes: :issue:`344`) **Command line options** @@ -20,14 +21,16 @@ v0.15.0 (UNRELEASED) options. - :option:`mopidy --show-config` will now take into consideration any - :option:`mopidy --option` arguments appearing later on the command line. + :option:`mopidy --option` arguments appearing later on the command line. This + helps you see the effective configuration for runs with the same + :option:`mopidy --options` arguments. **Audio** -- Added support for audio visualization. :confval:`audio/visualizer` can now be set - to GStreamer visualizers. +- Added support for audio visualization. :confval:`audio/visualizer` can now be + set to GStreamer visualizers. -- Properly encode localised mixer names before logging. +- Properly encode localized mixer names before logging. **Local backend** @@ -38,7 +41,7 @@ v0.15.0 (UNRELEASED) the next file. This fixes some hangs on non-media files, like logs. (Fixes: :issue:`476`, :issue:`483`) -- Added support for plugable library updaters. This allows extension writers +- Added support for pluggable library updaters. This allows extension writers to start providing their own custom libraries instead of being stuck with just our tag cache as the only option. @@ -66,16 +69,17 @@ v0.15.0 (UNRELEASED) **HTTP frontend** -- Fix too broad truthness test that caused :class:`~mopidy.models.TlTrack` +- Fix too broad truth test that caused :class:`mopidy.models.TlTrack` objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the ``tlid`` field. (Fixes: :issue:`501`) -- Upgrade Mopidy.js dependencies. +- Upgrade Mopidy.js dependencies. This version has been released to NPM as + Mopidy.js v0.1.1. **Extension support** -- :class:`~mopidy.config.Secret` is now deserialized to unicode strings instead - of bytestrings. This may require modifications to extensions. +- :class:`mopidy.config.Secret` is now deserialized to unicode instead of + bytes. This may require modifications to extensions. v0.14.2 (2013-07-01) From ba361add4cdd7c862223aaa97dd634c6dad05f42 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 19 Sep 2013 19:30:36 +0200 Subject: [PATCH 109/111] Bump version to 0.15.0 --- mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 6a7699e5..6ef80b0f 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring) warnings.filterwarnings('ignore', 'could not open display') -__version__ = '0.14.2' +__version__ = '0.15.0' diff --git a/tests/version_test.py b/tests/version_test.py index 27b246d7..6503ef39 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -38,5 +38,6 @@ class VersionTest(unittest.TestCase): self.assertLess(SV('0.12.0'), SV('0.13.0')) self.assertLess(SV('0.13.0'), SV('0.14.0')) self.assertLess(SV('0.14.0'), SV('0.14.1')) - self.assertLess(SV('0.14.1'), SV(__version__)) - self.assertLess(SV(__version__), SV('0.14.3')) + self.assertLess(SV('0.14.1'), SV('0.14.2')) + self.assertLess(SV('0.14.2'), SV(__version__)) + self.assertLess(SV(__version__), SV('0.15.1')) From 9661cb46502aa4675837dc29ec4757c9f5eefdbb Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sun, 22 Sep 2013 01:42:26 +0200 Subject: [PATCH 110/111] doc: Remove duplicate "Extension configuration" section in config.rst --- docs/config.rst | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 6fd7579d..5b8f5de1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -147,24 +147,6 @@ Core configuration values .. _the Python logging docs: http://docs.python.org/2/library/logging.config.html -Extension configuration -======================= - -Mopidy's extensions have their own config values that you may want to tweak. -For the available config values, please refer to the docs for each extension. -Most, if not all, can be found at :ref:`ext`. - -Mopidy extensions are enabled by default when they are installed. If you want -to disable an extension without uninstalling it, all extensions support the -``enabled`` config value even if it isn't explicitly documented by all -extensions. If the ``enabled`` config value is set to ``false`` the extension -will not be started. For example, to disable the Spotify extension, add the -following to your ``mopidy.conf``:: - - [spotify] - enabled = false - - Extension configuration ======================= From de86274cea97523126a84b92d1879bb3593a8993 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 6 Oct 2013 13:58:05 +0200 Subject: [PATCH 111/111] readme: Add crate.io sheilds to readme. --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index c9db495e..a34b1bb6 100644 --- a/README.rst +++ b/README.rst @@ -27,3 +27,11 @@ To get started with Mopidy, check out `the docs `_. .. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop :target: https://travis-ci.org/mopidy/mopidy + +.. image:: https://pypip.in/v/Mopidy/badge.png + :target: https://crate.io/packages/Mopidy/ + :alt: Latest PyPI version + +.. image:: https://pypip.in/d/Mopidy/badge.png + :target: https://crate.io/packages/Mopidy/ + :alt: Number of PyPI downloads