From 30ea4860a7d1381e61e0de5aeebd80850c8d986a Mon Sep 17 00:00:00 2001 From: Janez Troha Date: Wed, 1 May 2013 14:12:49 +0300 Subject: [PATCH 01/80] 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 02/80] 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 03/80] 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 04/80] 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 05/80] 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 06/80] 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 07/80] 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 08/80] 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 09/80] 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 10/80] 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 11/80] 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 12/80] 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 13/80] 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 14/80] 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 15/80] 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 16/80] 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 17/80] 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 18/80] 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 19/80] 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 20/80] 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 21/80] 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 22/80] 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 23/80] 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 24/80] 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 25/80] 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 26/80] 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 27/80] 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 28/80] 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 29/80] 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 30/80] 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 31/80] 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 32/80] 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 33/80] 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 34/80] 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 35/80] 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 36/80] 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 37/80] - 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 38/80] 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 39/80] 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 40/80] 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 41/80] 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 42/80] 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 43/80] 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 44/80] 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 45/80] 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 46/80] 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 47/80] 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 48/80] 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 49/80] 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 50/80] 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 51/80] 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 52/80] 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 53/80] 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 54/80] 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 55/80] 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 56/80] 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 57/80] 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 58/80] 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 59/80] 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 60/80] 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 61/80] 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 62/80] 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 63/80] 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 64/80] 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 65/80] 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 66/80] 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 67/80] 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 68/80] 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 69/80] 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 70/80] 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 71/80] 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 72/80] 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 73/80] 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 74/80] 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 75/80] 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 76/80] 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 77/80] 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 78/80] 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 79/80] 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 80/80] 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):