From 0f4771f5398be96d863c2a0c9e9d3b9fd85ab9ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Aug 2015 10:02:46 +0200 Subject: [PATCH 01/47] file: Add missing space to log message --- mopidy/file/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/file/library.py b/mopidy/file/library.py index 10586561..691020b4 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -108,7 +108,7 @@ class FileLibraryProvider(backend.LibraryProvider): media_dir_split[0].encode(FS_ENCODING)) if not local_path: - logger.warning('Failed expanding path (%s) from' + logger.warning('Failed expanding path (%s) from ' 'file/media_dirs config value.', media_dir_split[0]) continue From 326f8579ca489998f7ef26a2064b69e43c3bc986 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Aug 2015 10:03:33 +0200 Subject: [PATCH 02/47] core: Add quotes around URI scheme in log message --- mopidy/core/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index f20b0ba2..6d10a593 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -161,7 +161,7 @@ class Backends(list): for scheme in b.uri_schemes.get(): assert scheme not in backends_by_scheme, ( - 'Cannot add URI scheme %s for %s, ' + 'Cannot add URI scheme "%s" for %s, ' 'it is already handled by %s' ) % (scheme, name(b), name(backends_by_scheme[scheme])) backends_by_scheme[scheme] = b From 9f08bce6cd51ce8dbfa25cccab6b618cbb48ed1e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Aug 2015 10:06:58 +0200 Subject: [PATCH 03/47] core: Update test --- tests/core/test_actor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/core/test_actor.py b/tests/core/test_actor.py index 410933d2..8f062fa2 100644 --- a/tests/core/test_actor.py +++ b/tests/core/test_actor.py @@ -37,7 +37,8 @@ class CoreActorTest(unittest.TestCase): self.assertRaisesRegexp( AssertionError, - 'Cannot add URI scheme dummy1 for B2, it is already handled by B1', + 'Cannot add URI scheme "dummy1" for B2, ' + 'it is already handled by B1', Core, mixer=None, backends=[self.backend1, self.backend2]) def test_version(self): From 9a83a2d707bd430a0529b1ae889ec1e8b9604f04 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Aug 2015 22:34:59 +0200 Subject: [PATCH 04/47] stream: Ignore file protocol if Mopidy-File is enabled If Mopidy-File is enabled it handles playback of file:// URIs. Mopidy-Stream used to do this, but in Mopidy 1.1 we removed "file" from the default value of the stream/protocols config. However, many users upgrading to Mopidy 1.1 have set stream/protocols to include "file" in their existing config, and thus Mopidy fails to start because both backends tries to claim the "file" protocol. Fixes #1248 --- mopidy/stream/actor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index ae5be8e0..cc9632d5 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -36,6 +36,13 @@ class StreamBackend(pykka.ThreadingActor, backend.Backend): self.uri_schemes = audio_lib.supported_uri_schemes( config['stream']['protocols']) + if 'file' in self.uri_schemes and config['file']['enabled']: + logger.warning( + 'The stream/protocols config value includes the "file" ' + 'protocol. "file" playback is now handled by Mopidy-File. ' + 'Please remove it from the stream/protocols config.') + self.uri_schemes -= {'file'} + class StreamLibraryProvider(backend.LibraryProvider): From 3dfa39adb04b5e675e1ebe0c312bbfb3240f6a05 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Aug 2015 22:50:48 +0200 Subject: [PATCH 05/47] docs: Add #1248/#1254 to changelog --- docs/changelog.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 75fdc538..24bc7682 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,19 @@ Changelog This changelog is used to track all major changes to Mopidy. + +v1.1.1 (UNRELEASED) +=================== + +Bug fix release. + +- Stream: If "file" is present in the :confval:`stream/protocols` config value + and the :ref:`ext-file` extension is enabled, we exited with an error because + two extensions claimed the same URI scheme. We now log a warning recommending + to remove "file" from the :confval:`stream/protocols` config, and then + proceed startup. (Fixes: :issue:`1248`, PR: :issue:`1254`) + + v1.1.0 (2015-08-09) =================== From 9f24c331a4ea2237579dce9b6f06390ad2b10411 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Aug 2015 23:08:32 +0200 Subject: [PATCH 06/47] file: Adjust file/media_dirs failure logging Fixes #1249 --- mopidy/file/library.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mopidy/file/library.py b/mopidy/file/library.py index 691020b4..d477d109 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -108,12 +108,15 @@ class FileLibraryProvider(backend.LibraryProvider): media_dir_split[0].encode(FS_ENCODING)) if not local_path: - logger.warning('Failed expanding path (%s) from ' - 'file/media_dirs config value.', - media_dir_split[0]) + logger.debug( + 'Failed expanding path (%s) from file/media_dirs config ' + 'value.', + media_dir_split[0]) continue elif not os.path.isdir(local_path): - logger.warning('%s is not a directory', local_path) + logger.warning( + '%s is not a directory. Please create the directory or ' + 'update the file/media_dirs config value.', local_path) continue media_dir['path'] = local_path From 306b2f331fba29c073ce07691d1408e139c8914d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Aug 2015 23:11:47 +0200 Subject: [PATCH 07/47] docs: Add #1249/#1255 to changelog --- docs/changelog.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 75fdc538..7a604526 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. + +v1.1.1 (UNRELEASED) +=================== + +Bug fix release. + +- File: Adjust log levels when failing to expand ``$XDG_MUSIC_DIR`` into a real + path. This usually happens when running Mopidy as a system service, and thus + with a limited set of environment variables. (Fixes: :issue:`1249`, PR: + :issue:`1255`) + + v1.1.0 (2015-08-09) =================== From 087ee4288246b6586f5ab0fcd916e81bfeda9137 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 16 Aug 2015 12:06:14 +0200 Subject: [PATCH 08/47] audio: Fix scan timeout handling --- docs/changelog.rst | 3 +++ mopidy/audio/scan.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c7f1aeee..290806d3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,9 @@ Bug fix release. with a limited set of environment variables. (Fixes: :issue:`1249`, PR: :issue:`1255`) +- Audio: Fix timeout handling in scanner. This regression caused timeouts to + expire before it should, causing scans to fail. + v1.1.0 (2015-08-09) =================== diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index cf370052..13c76d52 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -139,7 +139,7 @@ def _process(pipeline, timeout_ms): types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) - start = clock.get_time() + previous = clock.get_time() while timeout > 0: message = bus.timed_pop_filtered(timeout, types) @@ -171,7 +171,9 @@ def _process(pipeline, timeout_ms): # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) - timeout -= clock.get_time() - start + now = clock.get_time() + timeout -= now - previous + previous = now raise exceptions.ScannerError('Timeout after %dms' % timeout_ms) From e77a4afaf46f2eff8a700d6eb73082bbb93be419 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 18 Aug 2015 23:44:38 +0200 Subject: [PATCH 09/47] audio: Make scanner report MIME for missing plugins --- docs/changelog.rst | 3 +++ mopidy/audio/scan.py | 13 ++++++------- tests/audio/test_scan.py | 11 +++++++++++ tests/data/scanner/plain.txt | 1 + tests/data/scanner/playlist.m3u | 1 + 5 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 tests/data/scanner/plain.txt create mode 100644 tests/data/scanner/playlist.m3u diff --git a/docs/changelog.rst b/docs/changelog.rst index 290806d3..c0328de5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,9 @@ Bug fix release. - Audio: Fix timeout handling in scanner. This regression caused timeouts to expire before it should, causing scans to fail. +- Audio: Update scanner to emit MIME type instead of an error when missing a + plugin. + v1.1.0 (2015-08-09) =================== diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 13c76d52..d1081788 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -12,8 +12,6 @@ from mopidy import exceptions from mopidy.audio import utils from mopidy.internal import encoding -_missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description - _Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) @@ -134,7 +132,7 @@ def _process(pipeline, timeout_ms): clock = pipeline.get_clock() bus = pipeline.get_bus() timeout = timeout_ms * gst.MSECOND - tags, mime, have_audio, missing_description = {}, None, False, None + tags, mime, have_audio, missing_message = {}, None, False, None types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) @@ -147,8 +145,7 @@ def _process(pipeline, timeout_ms): break elif message.type == gst.MESSAGE_ELEMENT: if gst.pbutils.is_missing_plugin_message(message): - missing_description = encoding.locale_decode( - _missing_plugin_desc(message)) + missing_message = message elif message.type == gst.MESSAGE_APPLICATION: if message.structure.get_name() == 'have-type': mime = message.structure['caps'].get_name() @@ -158,8 +155,10 @@ def _process(pipeline, timeout_ms): have_audio = True elif message.type == gst.MESSAGE_ERROR: error = encoding.locale_decode(message.parse_error()[0]) - if missing_description: - error = '%s (%s)' % (missing_description, error) + if missing_message and not mime: + caps = missing_message.structure['detail'] + mime = caps.get_structure(0).get_name() + return tags, mime, have_audio raise exceptions.ScannerError(error) elif message.type == gst.MESSAGE_EOS: return tags, mime, have_audio diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index c558835e..dff753d9 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -109,6 +109,17 @@ class ScannerTest(unittest.TestCase): wav = path_to_data_dir('scanner/empty.wav') self.assertEqual(self.result[wav].duration, 0) + def test_uri_list(self): + path = path_to_data_dir('scanner/playlist.m3u') + self.scan([path]) + self.assertEqual(self.result[path].mime, 'text/uri-list') + + def test_text_plain(self): + # GStreamer decode bin hardcodes bad handling of text plain :/ + path = path_to_data_dir('scanner/plain.txt') + self.scan([path]) + self.assertIn(path, self.errors) + @unittest.SkipTest def test_song_without_time_is_handeled(self): pass diff --git a/tests/data/scanner/plain.txt b/tests/data/scanner/plain.txt new file mode 100644 index 00000000..3180d4a9 --- /dev/null +++ b/tests/data/scanner/plain.txt @@ -0,0 +1 @@ +Some plain text file with nothing special in it. diff --git a/tests/data/scanner/playlist.m3u b/tests/data/scanner/playlist.m3u new file mode 100644 index 00000000..df0f0142 --- /dev/null +++ b/tests/data/scanner/playlist.m3u @@ -0,0 +1 @@ +http://example.com/ From 3e75d5cf06ccccc18077b9cce2072d4dd6c3267c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 19 Aug 2015 00:40:46 +0200 Subject: [PATCH 10/47] audio: Update missing plugins check in scanner tests --- tests/audio/test_scan.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index dff753d9..8c2b9af3 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -40,8 +40,11 @@ class ScannerTest(unittest.TestCase): self.assertEqual(self.result[name].tags[key], value) def check_if_missing_plugin(self): - if any(['missing a plug-in' in str(e) for e in self.errors.values()]): - raise unittest.SkipTest('Missing MP3 support?') + for path, result in self.result.items(): + if not path.endswith('.mp3'): + continue + if not result.playable and result.mime == 'audio/mpeg': + raise unittest.SkipTest('Missing MP3 support?') def test_tags_is_set(self): self.scan(self.find('scanner/simple')) From 74e4d3f9ab1c2dd869966ee975de75aeab984985 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Aug 2015 10:28:13 +0200 Subject: [PATCH 11/47] docs: pip-mopidy can now use apt-extensions --- docs/debian.rst | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/debian.rst b/docs/debian.rst index f939d9af..74bab30f 100644 --- a/docs/debian.rst +++ b/docs/debian.rst @@ -114,11 +114,17 @@ from a regular Mopidy setup you'll want to know about. sudo service mopidy status - Mopidy installed from a Debian package can use both Mopidy extensions - installed both from Debian packages and extensions installed with pip. + installed both from Debian packages and extensions installed with pip. This + has always been the case. - The other way around does not work: Mopidy installed with pip can use - extensions installed with pip, but not extensions installed from a Debian - package. This is because the Debian packages install extensions into + Mopidy installed with pip can use extensions installed with pip, but + not extensions installed from a Debian package released before August 2015. + This is because the Debian packages used to install extensions into :file:`/usr/share/mopidy` which is normally not on your ``PYTHONPATH``. - Thus, your pip-installed Mopidy will not find the Debian package-installed + Thus, your pip-installed Mopidy would not find the Debian package-installed extensions. + + In August 2015, all Mopidy extension Debian packages was modified to install + into :file:`/usr/lib/python2.7/dist-packages`, like any other Python Debian + package. Thus, Mopidy installed with pip can now use extensions installed + from Debian. From 7411c84ba7bb306f3fe06e4cbb9ed2223c0363d5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Aug 2015 10:50:35 +0200 Subject: [PATCH 12/47] docs: Avoid double use of 'both' in same sentence --- docs/debian.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/debian.rst b/docs/debian.rst index 74bab30f..f761c4b0 100644 --- a/docs/debian.rst +++ b/docs/debian.rst @@ -113,9 +113,8 @@ from a regular Mopidy setup you'll want to know about. sudo service mopidy status -- Mopidy installed from a Debian package can use both Mopidy extensions - installed both from Debian packages and extensions installed with pip. This - has always been the case. +- Mopidy installed from a Debian package can use Mopidy extensions installed + both from Debian packages and with pip. This has always been the case. Mopidy installed with pip can use extensions installed with pip, but not extensions installed from a Debian package released before August 2015. From 77e8436f3e2b09d85eaeb2d89d6ef1dd96eaf1bb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Aug 2015 19:57:39 +0200 Subject: [PATCH 13/47] docs: Elaborate on core/{cache,data}_dir usage Related to #1253 --- docs/config.rst | 12 ++++++++++++ mopidy/ext.py | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 26304176..7f0bda31 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -65,6 +65,13 @@ Core configuration Path to base directory for storing cached data. + Mopidy and extensions will use this path to cache data that can safely be + thrown away. + + If your system is running from an SD card, it can help avoid wear and + corruption of your SD card by pointing this config to another location. If + you have enough RAM, a tmpfs might be a good choice. + When running Mopidy as a regular user, this should usually be ``$XDG_CACHE_DIR/mopidy``, i.e. :file:`~/.cache/mopidy`. @@ -85,6 +92,11 @@ Core configuration Path to base directory for persistent data files. + Mopidy and extensions will use this path to store data that cannot be + be thrown away and reproduced without some effort. Examples include + Mopidy-Local's index of your media library and Mopidy-M3U's stored + playlists. + When running Mopidy as a regular user, this should usually be ``$XDG_DATA_DIR/mopidy``, i.e. :file:`~/.local/share/mopidy`. diff --git a/mopidy/ext.py b/mopidy/ext.py index 908b6d5d..199d7ab6 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -63,6 +63,8 @@ class Extension(object): def get_cache_dir(self, config): """Get or create cache directory for the extension. + Use this directory to cache data that can safely be thrown away. + :param config: the Mopidy config object :return: string """ @@ -87,6 +89,8 @@ class Extension(object): def get_data_dir(self, config): """Get or create data directory for the extension. + Use this directory to store data that should be persistent. + :param config: the Mopidy config object :returns: string """ From eee851f36beeee5b5072d1716bd560f7529099c8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Aug 2015 20:34:10 +0200 Subject: [PATCH 14/47] mpd: Fix missing punctuation in docstring --- mopidy/mpd/dispatcher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/mpd/dispatcher.py b/mopidy/mpd/dispatcher.py index a8e2c05c..099a2f18 100644 --- a/mopidy/mpd/dispatcher.py +++ b/mopidy/mpd/dispatcher.py @@ -271,9 +271,9 @@ class MpdContext(object): If ``lookup`` is true and the ``path`` is to a track, the returned ``data`` is a future which will contain the results from looking up - the URI with :meth:`mopidy.core.LibraryController.lookup` If ``lookup`` - is false and the ``path`` is to a track, the returned ``data`` will be - a :class:`mopidy.models.Ref` for the track. + the URI with :meth:`mopidy.core.LibraryController.lookup`. If + ``lookup`` is false and the ``path`` is to a track, the returned + ``data`` will be a :class:`mopidy.models.Ref` for the track. For all entries that are not tracks, the returned ``data`` will be :class:`None`. From 52b81bd858af62f3a01e781e068067d2af4ca900 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Aug 2015 20:40:18 +0200 Subject: [PATCH 15/47] file: Don't scan files on browsing Fixes #1260 --- docs/changelog.rst | 5 +++++ mopidy/file/library.py | 14 +------------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c0328de5..6ab7e732 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,11 @@ Bug fix release. with a limited set of environment variables. (Fixes: :issue:`1249`, PR: :issue:`1255`) +- File: When browsing files, we no longer scan the files to check if they're + playable. This makes browsing of the file hierarchy instant for HTTP clients, + which do no scanning of the files' metadata, and a bit faster for MPD + clients, which no longer scan the files twice. + - Audio: Fix timeout handling in scanner. This regression caused timeouts to expire before it should, causing scans to fail. diff --git a/mopidy/file/library.py b/mopidy/file/library.py index d477d109..b8531a6e 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -71,7 +71,7 @@ class FileLibraryProvider(backend.LibraryProvider): name = dir_entry.decode(FS_ENCODING, 'replace') if os.path.isdir(child_path): result.append(models.Ref.directory(name=name, uri=uri)) - elif os.path.isfile(child_path) and self._is_audio_file(uri): + elif os.path.isfile(child_path): result.append(models.Ref.track(name=name, uri=uri)) result.sort(key=operator.attrgetter('name')) @@ -134,18 +134,6 @@ class FileLibraryProvider(backend.LibraryProvider): name=media_dir['name'], uri=path.path_to_uri(media_dir['path'])) - def _is_audio_file(self, uri): - try: - result = self._scanner.scan(uri) - if result.playable: - logger.debug('Playable file: %s', result.uri) - else: - logger.debug('Unplayable file: %s (not audio)', result.uri) - return result.playable - except exceptions.ScannerError as e: - logger.debug('Unplayable file: %s (%s)', uri, e) - return False - def _is_in_basedir(self, local_path): return any( path.is_path_inside_base_dir(local_path, media_dir['path']) From baa2cc7ac845209aab5d2fb4b41fed179cf5c9ae Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 21 Aug 2015 20:44:25 +0200 Subject: [PATCH 16/47] docs: Add issue and PR links --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6ab7e732..55357087 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,7 +24,8 @@ Bug fix release. - File: When browsing files, we no longer scan the files to check if they're playable. This makes browsing of the file hierarchy instant for HTTP clients, which do no scanning of the files' metadata, and a bit faster for MPD - clients, which no longer scan the files twice. + clients, which no longer scan the files twice. (Fixes: :issue:`1260`, PR: + :issue:`1261`) - Audio: Fix timeout handling in scanner. This regression caused timeouts to expire before it should, causing scans to fail. From 78ffaeb8d29f92d9c107780bfe43017a85fff7a9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Aug 2015 23:51:07 +0200 Subject: [PATCH 17/47] playlists: Fix crash on urilist comment with non-ASCII chars Fixes #1265 --- docs/changelog.rst | 4 ++++ mopidy/internal/playlists.py | 2 +- tests/internal/test_playlists.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 55357087..a310e7e8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,10 @@ Bug fix release. to remove "file" from the :confval:`stream/protocols` config, and then proceed startup. (Fixes: :issue:`1248`, PR: :issue:`1254`) +- Stream: Fix bug in new playlist parser. A non-ASCII char in an urilist + comment would cause a crash while parsing due to comparision of a non-ASCII + bytestring with a Unicode string. (Fixes: :issue:`1265`) + - File: Adjust log levels when failing to expand ``$XDG_MUSIC_DIR`` into a real path. This usually happens when running Mopidy as a system service, and thus with a limited set of environment variables. (Fixes: :issue:`1249`, PR: diff --git a/mopidy/internal/playlists.py b/mopidy/internal/playlists.py index 219d3ec6..f8e654af 100644 --- a/mopidy/internal/playlists.py +++ b/mopidy/internal/playlists.py @@ -122,7 +122,7 @@ def parse_asx(data): def parse_urilist(data): result = [] for line in data.splitlines(): - if not line.strip() or line.startswith('#'): + if not line.strip() or line.startswith(b'#'): continue try: validation.check_uri(line) diff --git a/tests/internal/test_playlists.py b/tests/internal/test_playlists.py index 21537813..9a1c49d5 100644 --- a/tests/internal/test_playlists.py +++ b/tests/internal/test_playlists.py @@ -23,7 +23,7 @@ file:///tmp/baz URILIST = b""" file:///tmp/foo -# a comment +# a comment \xc5\xa7\xc5\x95 file:///tmp/bar file:///tmp/baz From c48b6515f99751ef80203cc7dc3fdfec4f16993a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Aug 2015 00:35:52 +0200 Subject: [PATCH 18/47] core: library.refresh() should check if backend has library ...and not playlists. Fixes #1257 --- docs/changelog.rst | 4 ++++ mopidy/core/library.py | 2 +- tests/core/test_library.py | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a310e7e8..c8007e7b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,10 @@ v1.1.1 (UNRELEASED) Bug fix release. +- Core: Make :meth:`mopidy.core.LibraryController.refresh` work for all + backends with a library provider. Previously, it wrongly worked for all + backends with a playlists provider. (Fixes: :issue:`1257`) + - Stream: If "file" is present in the :confval:`stream/protocols` config value and the :ref:`ext-file` extension is enabled, we exited with an error because two extensions claimed the same URI scheme. We now log a warning recommending diff --git a/mopidy/core/library.py b/mopidy/core/library.py index c300fbb9..ce420812 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -255,7 +255,7 @@ class LibraryController(object): backends = {} uri_scheme = urlparse.urlparse(uri).scheme if uri else None - for backend_scheme, backend in self.backends.with_playlists.items(): + for backend_scheme, backend in self.backends.with_library.items(): backends.setdefault(backend, set()).add(backend_scheme) for backend, backend_schemes in backends.items(): diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 941f1831..92b22bfb 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -20,6 +20,7 @@ class BaseCoreLibraryTest(unittest.TestCase): self.library1.get_images.return_value.get.return_value = {} self.library1.root_directory.get.return_value = dummy1_root self.backend1.library = self.library1 + self.backend1.has_playlists.return_value.get.return_value = False dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2') self.backend2 = mock.Mock() @@ -29,13 +30,14 @@ class BaseCoreLibraryTest(unittest.TestCase): self.library2.get_images.return_value.get.return_value = {} self.library2.root_directory.get.return_value = dummy2_root self.backend2.library = self.library2 + self.backend2.has_playlists.return_value.get.return_value = False # A backend without the optional library provider self.backend3 = mock.Mock() self.backend3.uri_schemes.get.return_value = ['dummy3'] self.backend3.actor_ref.actor_class.__name__ = 'DummyBackend3' - self.backend3.has_library().get.return_value = False - self.backend3.has_library_browse().get.return_value = False + self.backend3.has_library.return_value.get.return_value = False + self.backend3.has_library_browse.return_value.get.return_value = False self.core = core.Core(mixer=None, backends=[ self.backend1, self.backend2, self.backend3]) From b63642d873111a1dc41b8a61f407b7f7330a4f5d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Aug 2015 21:34:35 +0200 Subject: [PATCH 19/47] Use core/*_dir when creating dirs we need Partly fixes #1259 --- docs/changelog.rst | 9 +++++++++ mopidy/__main__.py | 20 +++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c8007e7b..5caa176a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,15 @@ Bug fix release. backends with a library provider. Previously, it wrongly worked for all backends with a playlists provider. (Fixes: :issue:`1257`) +- Core: Respect :confval:`core/cache_dir` and :confval:`core/data_dir` config + values added in 1.1.0 when creating the dirs Mopidy need to store data. This + should not change the behavior for desktop users running Mopidy. When running + Mopidy as a system service installed from a package which sets the core dir + configs properly (e.g. Debian and Arch packages), this fix avoids the + creation of a couple of directories that should not be used, typically + :file:`/var/lib/mopidy/.local` and :file:`/var/lib/mopidy/.cache`. (Fixes: + :issue:`1259`) + - Stream: If "file" is present in the :confval:`stream/protocols` config value and the :ref:`ext-file` extension is enabled, we exited with an error because two extensions claimed the same URI scheme. We now log a warning recommending diff --git a/mopidy/__main__.py b/mopidy/__main__.py index ee359268..208d2ff1 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -75,15 +75,16 @@ def main(): args = root_cmd.parse(mopidy_args) - create_file_structures_and_config(args, extensions_data) - check_old_locations() - config, config_errors = config_lib.load( args.config_files, [d.config_schema for d in extensions_data], [d.config_defaults for d in extensions_data], args.config_overrides) + create_core_dirs(config) + create_initial_config_file(args, extensions_data) + check_old_locations() + verbosity_level = args.base_verbosity_level if args.verbosity_level: verbosity_level += args.verbosity_level @@ -166,12 +167,17 @@ def main(): raise -def create_file_structures_and_config(args, extensions_data): - path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') - path.get_or_create_dir(b'$XDG_CONFIG_DIR/mopidy') +def create_core_dirs(config): + path.get_or_create_dir(config['core']['cache_dir']) + path.get_or_create_dir(config['core']['config_dir']) + path.get_or_create_dir(config['core']['data_dir']) + + +def create_initial_config_file(args, extensions_data): + """Initialize whatever the last config file is with defaults""" - # Initialize whatever the last config file is with defaults config_file = args.config_files[-1] + if os.path.exists(path.expand_path(config_file)): return From e0a028291a453ca49d879c7dfcaa67488394938d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Aug 2015 21:49:18 +0200 Subject: [PATCH 20/47] local: Replace local/data_dir with core/data_dir Partly fixes #1259 --- docs/changelog.rst | 6 ++++++ docs/ext/local.rst | 4 ++-- mopidy/local/__init__.py | 2 +- mopidy/local/ext.conf | 1 - mopidy/local/json.py | 4 ++-- mopidy/local/storage.py | 9 --------- tests/data/{ => local}/library.json.gz | Bin tests/local/test_json.py | 5 +++-- tests/local/test_library.py | 18 ++++++++++++------ tests/local/test_playback.py | 3 +-- tests/local/test_tracklist.py | 2 +- 11 files changed, 28 insertions(+), 26 deletions(-) rename tests/data/{ => local}/library.json.gz (100%) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5caa176a..9e1e5fec 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,12 @@ Bug fix release. :file:`/var/lib/mopidy/.local` and :file:`/var/lib/mopidy/.cache`. (Fixes: :issue:`1259`) +- Local: Deprecate :confval:`local/data_dir` and respect + :confval:`core/data_dir` instead. This does not change the defaults for + desktop users, only system services installed from packages that properly set + :confval:`core/data_dir`, like the Debian and Arch packages. (Fixes: + :issue:`1259`) + - Stream: If "file" is present in the :confval:`stream/protocols` config value and the :ref:`ext-file` extension is enabled, we exited with an error because two extensions claimed the same URI scheme. We now log a warning recommending diff --git a/docs/ext/local.rst b/docs/ext/local.rst index 18f66adc..d6e3b56a 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -47,8 +47,8 @@ active at a time. To create a new library provider you must create class that implements the :class:`mopidy.local.Library` interface and install it in the extension registry under ``local:library``. Any data that the library needs to store on -disc should be stored in :confval:`local/data_dir` using the library name as -part of the filename or directory to avoid any conflicts. +disc should be stored in the extension's data dir, as returned by +:meth:`~mopidy.ext.Extension.get_data_dir`. Configuration diff --git a/mopidy/local/__init__.py b/mopidy/local/__init__.py index ff61c17c..3ee2703e 100644 --- a/mopidy/local/__init__.py +++ b/mopidy/local/__init__.py @@ -23,7 +23,7 @@ class Extension(ext.Extension): schema = super(Extension, self).get_config_schema() schema['library'] = config.String() schema['media_dir'] = config.Path() - schema['data_dir'] = config.Path() + schema['data_dir'] = config.Deprecated() schema['playlists_dir'] = config.Deprecated() schema['tag_cache_file'] = config.Deprecated() schema['scan_timeout'] = config.Integer( diff --git a/mopidy/local/ext.conf b/mopidy/local/ext.conf index ebd7962f..6dd29147 100644 --- a/mopidy/local/ext.conf +++ b/mopidy/local/ext.conf @@ -2,7 +2,6 @@ enabled = true library = json media_dir = $XDG_MUSIC_DIR -data_dir = $XDG_DATA_DIR/mopidy/local scan_timeout = 1000 scan_flush_threshold = 1000 scan_follow_symlinks = false diff --git a/mopidy/local/json.py b/mopidy/local/json.py index 0be5e99e..501990ee 100644 --- a/mopidy/local/json.py +++ b/mopidy/local/json.py @@ -12,7 +12,7 @@ import tempfile import mopidy from mopidy import compat, local, models from mopidy.internal import encoding, timer -from mopidy.local import search, storage, translator +from mopidy.local import Extension, search, storage, translator logger = logging.getLogger(__name__) @@ -116,7 +116,7 @@ class JsonLibrary(local.Library): self._browse_cache = None self._media_dir = config['local']['media_dir'] self._json_file = os.path.join( - config['local']['data_dir'], b'library.json.gz') + Extension().get_data_dir(config), b'library.json.gz') storage.check_dirs_and_files(config) diff --git a/mopidy/local/storage.py b/mopidy/local/storage.py index 1808c4a2..aaa74fba 100644 --- a/mopidy/local/storage.py +++ b/mopidy/local/storage.py @@ -3,8 +3,6 @@ from __future__ import absolute_import, unicode_literals import logging import os -from mopidy.internal import encoding, path - logger = logging.getLogger(__name__) @@ -13,10 +11,3 @@ def check_dirs_and_files(config): logger.warning( 'Local media dir %s does not exist.' % config['local']['media_dir']) - - try: - path.get_or_create_dir(config['local']['data_dir']) - except EnvironmentError as error: - logger.warning( - 'Could not create local data dir: %s', - encoding.locale_decode(error)) diff --git a/tests/data/library.json.gz b/tests/data/local/library.json.gz similarity index 100% rename from tests/data/library.json.gz rename to tests/data/local/library.json.gz diff --git a/tests/local/test_json.py b/tests/local/test_json.py index 520287ad..169ba743 100644 --- a/tests/local/test_json.py +++ b/tests/local/test_json.py @@ -45,10 +45,11 @@ class BrowseCacheTest(unittest.TestCase): class JsonLibraryTest(unittest.TestCase): config = { + 'core': { + 'data_dir': path_to_data_dir(''), + }, 'local': { 'media_dir': path_to_data_dir(''), - 'data_dir': path_to_data_dir(''), - 'playlists_dir': b'', 'library': 'json', }, } diff --git a/tests/local/test_library.py b/tests/local/test_library.py index 7763057f..4142d6c3 100644 --- a/tests/local/test_library.py +++ b/tests/local/test_library.py @@ -65,10 +65,11 @@ class LocalLibraryProviderTest(unittest.TestCase): ] config = { + 'core': { + 'data_dir': path_to_data_dir(''), + }, 'local': { 'media_dir': path_to_data_dir(''), - 'data_dir': path_to_data_dir(''), - 'playlists_dir': b'', 'library': 'json', }, } @@ -105,11 +106,15 @@ class LocalLibraryProviderTest(unittest.TestCase): tmpdir = tempfile.mkdtemp() try: - tmplib = os.path.join(tmpdir, 'library.json.gz') - shutil.copy(path_to_data_dir('library.json.gz'), tmplib) + tmpdir_local = os.path.join(tmpdir, 'local') + shutil.copytree(path_to_data_dir('local'), tmpdir_local) - config = {'local': self.config['local'].copy()} - config['local']['data_dir'] = tmpdir + config = { + 'core': { + 'data_dir': tmpdir, + }, + 'local': self.config['local'], + } backend = actor.LocalBackend(config=config, audio=None) # Sanity check that value is in the library @@ -117,6 +122,7 @@ class LocalLibraryProviderTest(unittest.TestCase): self.assertEqual(result, self.tracks[0:1]) # Clear and refresh. + tmplib = os.path.join(tmpdir_local, 'library.json.gz') open(tmplib, 'w').close() backend.library.refresh() diff --git a/tests/local/test_playback.py b/tests/local/test_playback.py index bab70847..b99f8508 100644 --- a/tests/local/test_playback.py +++ b/tests/local/test_playback.py @@ -23,12 +23,11 @@ from tests.local import generate_song, populate_tracklist class LocalPlaybackProviderTest(unittest.TestCase): config = { 'core': { + 'data_dir': path_to_data_dir(''), 'max_tracklist_length': 10000, }, 'local': { 'media_dir': path_to_data_dir(''), - 'data_dir': path_to_data_dir(''), - 'playlists_dir': b'', 'library': 'json', } } diff --git a/tests/local/test_tracklist.py b/tests/local/test_tracklist.py index 72da3f13..b7ed7dcb 100644 --- a/tests/local/test_tracklist.py +++ b/tests/local/test_tracklist.py @@ -18,11 +18,11 @@ from tests.local import generate_song, populate_tracklist class LocalTracklistProviderTest(unittest.TestCase): config = { 'core': { + 'data_dir': path_to_data_dir(''), 'max_tracklist_length': 10000 }, 'local': { 'media_dir': path_to_data_dir(''), - 'data_dir': path_to_data_dir(''), 'playlists_dir': b'', 'library': 'json', } From e1f349a6d432ea814be3adfa58c192f6e0df9b71 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Aug 2015 00:06:36 +0200 Subject: [PATCH 21/47] m3u: Make empty playlists_dir default to ext's data dir Partly fixes #1259 --- docs/changelog.rst | 6 ++++++ docs/ext/m3u.rst | 3 ++- mopidy/m3u/__init__.py | 2 +- mopidy/m3u/actor.py | 18 +++++++++++------- mopidy/m3u/ext.conf | 2 +- mopidy/m3u/playlists.py | 2 +- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9e1e5fec..b750be67 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,12 @@ Bug fix release. :confval:`core/data_dir`, like the Debian and Arch packages. (Fixes: :issue:`1259`) +- M3U: Changed default for the :confval:`m3u/playlists_dir` from + ``$XDG_DATA_DIR/mopidy/m3u`` to unset, which now means the extension's data + dir. This does not change the defaults for desktop users, only system + services installed from packages that properly set :confval:`core/data_dir`, + like the Debian and Arch pakages. (Fixes: :issue:`1259`) + - Stream: If "file" is present in the :confval:`stream/protocols` config value and the :ref:`ext-file` extension is enabled, we exited with an error because two extensions claimed the same URI scheme. We now log a warning recommending diff --git a/docs/ext/m3u.rst b/docs/ext/m3u.rst index d05f88f1..2b86b73a 100644 --- a/docs/ext/m3u.rst +++ b/docs/ext/m3u.rst @@ -52,4 +52,5 @@ See :ref:`config` for general help on configuring Mopidy. .. confval:: m3u/playlists_dir - Path to directory with M3U files. + Path to directory with M3U files. Unset by default, in which case the + extension's data dir is used to store playlists. diff --git a/mopidy/m3u/__init__.py b/mopidy/m3u/__init__.py index e0fcf305..06825932 100644 --- a/mopidy/m3u/__init__.py +++ b/mopidy/m3u/__init__.py @@ -21,7 +21,7 @@ class Extension(ext.Extension): def get_config_schema(self): schema = super(Extension, self).get_config_schema() - schema['playlists_dir'] = config.Path() + schema['playlists_dir'] = config.Path(optional=True) return schema def setup(self, registry): diff --git a/mopidy/m3u/actor.py b/mopidy/m3u/actor.py index fe959d86..fc4734a2 100644 --- a/mopidy/m3u/actor.py +++ b/mopidy/m3u/actor.py @@ -4,7 +4,7 @@ import logging import pykka -from mopidy import backend +from mopidy import backend, m3u from mopidy.internal import encoding, path from mopidy.m3u.library import M3ULibraryProvider from mopidy.m3u.playlists import M3UPlaylistsProvider @@ -21,12 +21,16 @@ class M3UBackend(pykka.ThreadingActor, backend.Backend): self._config = config - try: - path.get_or_create_dir(config['m3u']['playlists_dir']) - except EnvironmentError as error: - logger.warning( - 'Could not create M3U playlists dir: %s', - encoding.locale_decode(error)) + if config['m3u']['playlists_dir'] is not None: + self._playlists_dir = config['m3u']['playlists_dir'] + try: + path.get_or_create_dir(self._playlists_dir) + except EnvironmentError as error: + logger.warning( + 'Could not create M3U playlists dir: %s', + encoding.locale_decode(error)) + else: + self._playlists_dir = m3u.Extension().get_data_dir(config) self.playlists = M3UPlaylistsProvider(backend=self) self.library = M3ULibraryProvider(backend=self) diff --git a/mopidy/m3u/ext.conf b/mopidy/m3u/ext.conf index 0e828b1b..adc0d00a 100644 --- a/mopidy/m3u/ext.conf +++ b/mopidy/m3u/ext.conf @@ -1,3 +1,3 @@ [m3u] enabled = true -playlists_dir = $XDG_DATA_DIR/mopidy/m3u +playlists_dir = diff --git a/mopidy/m3u/playlists.py b/mopidy/m3u/playlists.py index af92e062..3567f8aa 100644 --- a/mopidy/m3u/playlists.py +++ b/mopidy/m3u/playlists.py @@ -23,7 +23,7 @@ class M3UPlaylistsProvider(backend.PlaylistsProvider): def __init__(self, *args, **kwargs): super(M3UPlaylistsProvider, self).__init__(*args, **kwargs) - self._playlists_dir = self.backend._config['m3u']['playlists_dir'] + self._playlists_dir = self.backend._playlists_dir self._playlists = {} self.refresh() From 369f10b70676984f99a0f53926fe31ab5f329f10 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Aug 2015 00:24:49 +0200 Subject: [PATCH 22/47] docs: Link to PR #1266 --- docs/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b750be67..741956b9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,19 +21,19 @@ Bug fix release. configs properly (e.g. Debian and Arch packages), this fix avoids the creation of a couple of directories that should not be used, typically :file:`/var/lib/mopidy/.local` and :file:`/var/lib/mopidy/.cache`. (Fixes: - :issue:`1259`) + :issue:`1259`, PR: :issue:`1266`) - Local: Deprecate :confval:`local/data_dir` and respect :confval:`core/data_dir` instead. This does not change the defaults for desktop users, only system services installed from packages that properly set :confval:`core/data_dir`, like the Debian and Arch packages. (Fixes: - :issue:`1259`) + :issue:`1259`, PR: :issue:`1266`) - M3U: Changed default for the :confval:`m3u/playlists_dir` from ``$XDG_DATA_DIR/mopidy/m3u`` to unset, which now means the extension's data dir. This does not change the defaults for desktop users, only system services installed from packages that properly set :confval:`core/data_dir`, - like the Debian and Arch pakages. (Fixes: :issue:`1259`) + like the Debian and Arch pakages. (Fixes: :issue:`1259`, PR: :issue:`1266`) - Stream: If "file" is present in the :confval:`stream/protocols` config value and the :ref:`ext-file` extension is enabled, we exited with an error because From 4cae6ea64593a716097ef73e5765024e12648f56 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 27 Aug 2015 15:27:59 +0200 Subject: [PATCH 23/47] docs: ToC captions requires Sphinx 1.3 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index cc760720..cbb2f228 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -78,7 +78,7 @@ def setup(app): # -- General configuration ---------------------------------------------------- -needs_sphinx = '1.0' +needs_sphinx = '1.3' extensions = [ 'sphinx.ext.autodoc', From 0360936135ef8193cc2b508cecb766e943b1294e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 28 Aug 2015 22:03:58 +0200 Subject: [PATCH 24/47] docs: Add a section on updating the local library Fixes #1267 --- docs/ext/local.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/ext/local.rst b/docs/ext/local.rst index d6e3b56a..ef9df5d7 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -35,6 +35,23 @@ To make a local library for your music available for Mopidy: #. Start Mopidy, find the music library in a client, and play some local music! +Updating the local library +========================== + +When you've added or removed music in your collection and want to update +Mopidy's index of your local library, you need to rescan:: + + mopidy local scan + +Note that if you are using the default local library storage, ``json``, you +need to restart Mopidy after the scan completes for the updated index to be +used. + +If you want index updates to come into effect immediately, you can try out +`Mopidy-Local-SQLite `_, which +will probably become the default backend in the near future. + + Pluggable library support ========================= From fcfb1515b13de427f173f3b91fffb0e4e84ee943 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 28 Aug 2015 22:20:52 +0200 Subject: [PATCH 25/47] local: Lower the default scan_flush_threshold @tkem recommends that this is reduced from 1000 to maximum 100 to not block incoming requests to Mopidy-Local-SQLite for too long while scanning the local library. --- docs/changelog.rst | 4 ++++ mopidy/local/ext.conf | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 741956b9..a507002e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,10 @@ Bug fix release. :confval:`core/data_dir`, like the Debian and Arch packages. (Fixes: :issue:`1259`, PR: :issue:`1266`) +- Local: Change default value of :confval:`local/scan_flush_threshold` from + 1000 to 100 to shorten the time Mopidy-Local-SQLite blocks incoming requests + while scanning the local library. + - M3U: Changed default for the :confval:`m3u/playlists_dir` from ``$XDG_DATA_DIR/mopidy/m3u`` to unset, which now means the extension's data dir. This does not change the defaults for desktop users, only system diff --git a/mopidy/local/ext.conf b/mopidy/local/ext.conf index 6dd29147..b37a3a7a 100644 --- a/mopidy/local/ext.conf +++ b/mopidy/local/ext.conf @@ -3,7 +3,7 @@ enabled = true library = json media_dir = $XDG_MUSIC_DIR scan_timeout = 1000 -scan_flush_threshold = 1000 +scan_flush_threshold = 100 scan_follow_symlinks = false excluded_file_extensions = .directory From b480de77e1d7695ca74f345793376c3528c3b15e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 1 Sep 2015 15:32:21 +0200 Subject: [PATCH 26/47] core: Fix docstring error Fixes #1269 --- docs/changelog.rst | 5 ++++- mopidy/core/tracklist.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a507002e..183dbf37 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,9 @@ Bug fix release. :file:`/var/lib/mopidy/.local` and :file:`/var/lib/mopidy/.cache`. (Fixes: :issue:`1259`, PR: :issue:`1266`) +- Core: Fix error in :meth:`~mopidy.core.TracklistController.get_eot_tlid` + docstring. (Fixes: :issue:`1269`) + - Local: Deprecate :confval:`local/data_dir` and respect :confval:`core/data_dir` instead. This does not change the defaults for desktop users, only system services installed from packages that properly set @@ -126,7 +129,7 @@ Core API - Add ``tlid`` alternatives to methods that take ``tl_track`` and also add ``get_{eot,next,previous}_tlid`` methods as light weight alternatives to the - ``tl_track`` versions of the calls. (Fixes: :issue:`1131` PR: :issue:`1136`, + ``tl_track`` versions of the calls. (Fixes: :issue:`1131`, PR: :issue:`1136`, :issue:`1140`) - Add :meth:`mopidy.core.PlaybackController.get_current_tlid`. diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index db4e2a69..13efe322 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -236,7 +236,7 @@ class TracklistController(object): def get_eot_tlid(self): """ - The TLID of the track that will be played after the given track. + The TLID of the track that will be played after the current track. Not necessarily the same TLID as returned by :meth:`get_next_tlid`. @@ -332,7 +332,7 @@ class TracklistController(object): def get_previous_tlid(self): """ - Returns the TLID of the track that will be played if calling + Returns the TLID of the track that will be played if calling :meth:`mopidy.core.PlaybackController.previous()`. For normal playback this is the previous track in the tracklist. If From 715a989a5a07b4fd73d196a4748be32b6385fb01 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 1 Sep 2015 21:21:02 +0200 Subject: [PATCH 27/47] file: Allow lookup() of any file URI Fixes #1268 --- docs/changelog.rst | 10 ++++++++++ mopidy/file/library.py | 4 ---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 183dbf37..c66a738e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -63,6 +63,16 @@ Bug fix release. clients, which no longer scan the files twice. (Fixes: :issue:`1260`, PR: :issue:`1261`) +- File: Allow looking up metadata about any ``file://`` URI, just like we did + in Mopidy 1.0.x, where Mopidy-Stream handled ``file://`` URIs. In Mopidy + 1.1.0, Mopidy-File did not allow one to lookup files outside the directories + listed in :confval:`file/media_dir`. This broke Mopidy-Local-SQLite when the + :confval:`local/media_dir` directory was not within one of the + :confval:`file/media_dirs` directories. For browsing of files, we still limit + access to files inside the :confval:`file/media_dir` directories. For lookup, + you can now read metadata for any file you know the path of. (Fixes: + :issue:`1268`, PR: :issue:`1273`) + - Audio: Fix timeout handling in scanner. This regression caused timeouts to expire before it should, causing scans to fail. diff --git a/mopidy/file/library.py b/mopidy/file/library.py index b8531a6e..20ac0632 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -81,10 +81,6 @@ class FileLibraryProvider(backend.LibraryProvider): logger.debug('Looking up file URI: %s', uri) local_path = path.uri_to_path(uri) - if not self._is_in_basedir(local_path): - logger.warning('Ignoring URI outside base dir: %s', local_path) - return [] - try: result = self._scanner.scan(uri) track = utils.convert_tags_to_track(result.tags).copy( From 9957b3c2be965c7c5387230575eb7fe17efcdcf5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 1 Sep 2015 23:27:39 +0200 Subject: [PATCH 28/47] local: Reintroduce local/data_dir config for the 1.1.x series As to not break compat with mopidy-local-* in v1.1.1. --- mopidy/local/__init__.py | 2 +- mopidy/local/ext.conf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/local/__init__.py b/mopidy/local/__init__.py index 3ee2703e..552e5341 100644 --- a/mopidy/local/__init__.py +++ b/mopidy/local/__init__.py @@ -23,7 +23,7 @@ class Extension(ext.Extension): schema = super(Extension, self).get_config_schema() schema['library'] = config.String() schema['media_dir'] = config.Path() - schema['data_dir'] = config.Deprecated() + schema['data_dir'] = config.Path(optional=True) schema['playlists_dir'] = config.Deprecated() schema['tag_cache_file'] = config.Deprecated() schema['scan_timeout'] = config.Integer( diff --git a/mopidy/local/ext.conf b/mopidy/local/ext.conf index b37a3a7a..c8fe6b86 100644 --- a/mopidy/local/ext.conf +++ b/mopidy/local/ext.conf @@ -2,6 +2,7 @@ enabled = true library = json media_dir = $XDG_MUSIC_DIR +data_dir = $XDG_DATA_DIR/mopidy/local scan_timeout = 1000 scan_flush_threshold = 100 scan_follow_symlinks = false From 5ad76abc3df9a5b291720ae075ad2c87e6cc290e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Sep 2015 09:31:13 +0200 Subject: [PATCH 29/47] deps: Require Requests >= 2.0 --- docs/changelog.rst | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c66a738e..1e816468 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ v1.1.1 (UNRELEASED) Bug fix release. +- Dependencies: Specify that we need Requests >= 2.0, not just any version. + - Core: Make :meth:`mopidy.core.LibraryController.refresh` work for all backends with a library provider. Previously, it wrongly worked for all backends with a playlists provider. (Fixes: :issue:`1257`) diff --git a/setup.py b/setup.py index ba74179c..a353a932 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( include_package_data=True, install_requires=[ 'Pykka >= 1.1', - 'requests', + 'requests >= 2.0', 'setuptools', 'tornado >= 2.3', ], From f655fc700921710e6f0b468cff3a873ce3c285eb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Sep 2015 10:20:02 +0200 Subject: [PATCH 30/47] ext: Make get_{cache,config,data}_dir() class methods Fixes #1275 --- docs/changelog.rst | 6 ++++++ mopidy/ext.py | 21 ++++++++++++--------- mopidy/local/json.py | 4 ++-- mopidy/m3u/actor.py | 2 +- tests/test_ext.py | 6 +++--- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1e816468..f23b68c4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,12 @@ Bug fix release. - Core: Fix error in :meth:`~mopidy.core.TracklistController.get_eot_tlid` docstring. (Fixes: :issue:`1269`) +- Extension support: Make :meth:`~mopidy.ext.Extension.get_cache_dir`, + :meth:`~mopidy.ext.Extension.get_config_dir`, and + :meth:`~mopidy.ext.Extension.get_data_dir` class methods, so they can be used + without creating an instance of the :class:`~mopidy.ext.Extension` class. + (Fixes: :issue:`1275`) + - Local: Deprecate :confval:`local/data_dir` and respect :confval:`core/data_dir` instead. This does not change the defaults for desktop users, only system services installed from packages that properly set diff --git a/mopidy/ext.py b/mopidy/ext.py index 199d7ab6..7fd68f96 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -60,7 +60,8 @@ class Extension(object): schema['enabled'] = config_lib.Boolean() return schema - def get_cache_dir(self, config): + @classmethod + def get_cache_dir(cls, config): """Get or create cache directory for the extension. Use this directory to cache data that can safely be thrown away. @@ -68,25 +69,27 @@ class Extension(object): :param config: the Mopidy config object :return: string """ - assert self.ext_name is not None + assert cls.ext_name is not None cache_dir_path = bytes(os.path.join(config['core']['cache_dir'], - self.ext_name)) + cls.ext_name)) path.get_or_create_dir(cache_dir_path) return cache_dir_path - def get_config_dir(self, config): + @classmethod + def get_config_dir(cls, config): """Get or create configuration directory for the extension. :param config: the Mopidy config object :return: string """ - assert self.ext_name is not None + assert cls.ext_name is not None config_dir_path = bytes(os.path.join(config['core']['config_dir'], - self.ext_name)) + cls.ext_name)) path.get_or_create_dir(config_dir_path) return config_dir_path - def get_data_dir(self, config): + @classmethod + def get_data_dir(cls, config): """Get or create data directory for the extension. Use this directory to store data that should be persistent. @@ -94,9 +97,9 @@ class Extension(object): :param config: the Mopidy config object :returns: string """ - assert self.ext_name is not None + assert cls.ext_name is not None data_dir_path = bytes(os.path.join(config['core']['data_dir'], - self.ext_name)) + cls.ext_name)) path.get_or_create_dir(data_dir_path) return data_dir_path diff --git a/mopidy/local/json.py b/mopidy/local/json.py index 501990ee..8e8b5b1e 100644 --- a/mopidy/local/json.py +++ b/mopidy/local/json.py @@ -12,7 +12,7 @@ import tempfile import mopidy from mopidy import compat, local, models from mopidy.internal import encoding, timer -from mopidy.local import Extension, search, storage, translator +from mopidy.local import search, storage, translator logger = logging.getLogger(__name__) @@ -116,7 +116,7 @@ class JsonLibrary(local.Library): self._browse_cache = None self._media_dir = config['local']['media_dir'] self._json_file = os.path.join( - Extension().get_data_dir(config), b'library.json.gz') + local.Extension.get_data_dir(config), b'library.json.gz') storage.check_dirs_and_files(config) diff --git a/mopidy/m3u/actor.py b/mopidy/m3u/actor.py index fc4734a2..55257f87 100644 --- a/mopidy/m3u/actor.py +++ b/mopidy/m3u/actor.py @@ -30,7 +30,7 @@ class M3UBackend(pykka.ThreadingActor, backend.Backend): 'Could not create M3U playlists dir: %s', encoding.locale_decode(error)) else: - self._playlists_dir = m3u.Extension().get_data_dir(config) + self._playlists_dir = m3u.Extension.get_data_dir(config) self.playlists = M3UPlaylistsProvider(backend=self) self.library = M3ULibraryProvider(backend=self) diff --git a/tests/test_ext.py b/tests/test_ext.py index 1a6bd538..67c2e4ec 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -58,17 +58,17 @@ class TestExtension(object): def test_get_cache_dir_raises_assertion_error(self, extension): config = {'core': {'cache_dir': '/tmp'}} with pytest.raises(AssertionError): # ext_name not set - extension.get_cache_dir(config) + ext.Extension.get_cache_dir(config) def test_get_config_dir_raises_assertion_error(self, extension): config = {'core': {'config_dir': '/tmp'}} with pytest.raises(AssertionError): # ext_name not set - extension.get_config_dir(config) + ext.Extension.get_config_dir(config) def test_get_data_dir_raises_assertion_error(self, extension): config = {'core': {'data_dir': '/tmp'}} with pytest.raises(AssertionError): # ext_name not set - extension.get_data_dir(config) + ext.Extension.get_data_dir(config) class TestLoadExtensions(object): From 39c1f4f9be915226eb583b1196bbfcb057d19782 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Sep 2015 21:14:07 +0200 Subject: [PATCH 31/47] coverage: Remove nosetests workaround --- .coveragerc | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index e77617cb..00000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[report] -omit = - */pyshared/* - */python?.?/* - */site-packages/nose/* From f1315488a27f080f4e8ff581745faa42785dc07e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Sep 2015 21:14:21 +0200 Subject: [PATCH 32/47] travis: Use working coveralls client --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5f01f223..09497db6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ script: - "tox -e $TOX_ENV" after_success: - - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" + - "if [ $TOX_ENV == 'py27' ]; then pip install --pre coveralls; coveralls; fi" branches: except: From 42ffa72e00bd631c24eb27e06cb5d7019551a0e1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Sep 2015 21:14:29 +0200 Subject: [PATCH 33/47] tox: Remove xunit files previously used by Jenkins --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index e29a40f2..ecc358ac 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,6 @@ sitepackages = true commands = py.test \ --basetemp={envtmpdir} \ - --junit-xml=xunit-{envname}.xml \ --cov=mopidy --cov-report=term-missing \ -n 4 \ {posargs} From 74e29601351b5ea6c00e01ca7b54b038ccadeec8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Sep 2015 21:17:06 +0200 Subject: [PATCH 34/47] package: Remove .coveragerc from manifest --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5a99b8b8..b2b7f37c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include *.py include *.rst include *.txt -include .coveragerc include .mailmap include .travis.yml include AUTHORS From 6bfb250f2ae2ca4550c9719786f7bb25035e54d6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Sep 2015 21:55:49 +0200 Subject: [PATCH 35/47] tox: Test if coverage data is readable if not using pytest-xdist --- tox.ini | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index ecc358ac..511642b2 100644 --- a/tox.ini +++ b/tox.ini @@ -3,19 +3,17 @@ envlist = py27, py27-tornado23, py27-tornado31, docs, flake8 [testenv] sitepackages = true -commands = - py.test \ - --basetemp={envtmpdir} \ - --cov=mopidy --cov-report=term-missing \ - -n 4 \ - {posargs} deps = mock pytest pytest-capturelog pytest-cov - pytest-xdist responses +commands = + py.test \ + --basetemp={envtmpdir} \ + --cov=mopidy --cov-report=term-missing \ + {posargs} [testenv:py27-tornado23] commands = py.test tests/http From 354f7f87f126d6ea9a36929050f702510826c1eb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Sep 2015 23:02:47 +0200 Subject: [PATCH 36/47] Revert "tox: Test if coverage data is readable if not using pytest-xdist" This reverts commit 6bfb250f2ae2ca4550c9719786f7bb25035e54d6. --- tox.ini | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 511642b2..ecc358ac 100644 --- a/tox.ini +++ b/tox.ini @@ -3,17 +3,19 @@ envlist = py27, py27-tornado23, py27-tornado31, docs, flake8 [testenv] sitepackages = true +commands = + py.test \ + --basetemp={envtmpdir} \ + --cov=mopidy --cov-report=term-missing \ + -n 4 \ + {posargs} deps = mock pytest pytest-capturelog pytest-cov + pytest-xdist responses -commands = - py.test \ - --basetemp={envtmpdir} \ - --cov=mopidy --cov-report=term-missing \ - {posargs} [testenv:py27-tornado23] commands = py.test tests/http From 499c1d518a83acbf481dc62f62e607146e02c22d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Sep 2015 23:04:50 +0200 Subject: [PATCH 37/47] travis: Use beta coveralls, but stable coverage --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 09497db6..eb8aadfe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ script: - "tox -e $TOX_ENV" after_success: - - "if [ $TOX_ENV == 'py27' ]; then pip install --pre coveralls; coveralls; fi" + - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls==1.0b1; coveralls; fi" branches: except: From 0ebddbefa1e0fc4b4f7b4a4f479ffe9e12806b43 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 18 Aug 2015 22:43:10 +0200 Subject: [PATCH 38/47] http: Steal download helper from Stream extension --- mopidy/internal/http.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/mopidy/internal/http.py b/mopidy/internal/http.py index 6ff59590..e35b8561 100644 --- a/mopidy/internal/http.py +++ b/mopidy/internal/http.py @@ -1,9 +1,14 @@ from __future__ import absolute_import, unicode_literals +import logging +import time + import requests from mopidy import httpclient +logger = logging.getLogger(__name__) + def get_requests_session(proxy_config, user_agent): proxy = httpclient.format_proxy(proxy_config) @@ -14,3 +19,30 @@ def get_requests_session(proxy_config, user_agent): session.headers.update({'user-agent': full_user_agent}) return session + + +def download(session, uri, timeout=1.0, chunk_size=4096): + try: + response = session.get(uri, stream=True, timeout=timeout) + except requests.exceptions.Timeout: + logger.warning('Download of %r failed due to connection timeout after ' + '%.3fs', uri, timeout) + return None + except requests.exceptions.InvalidSchema: + logger.warning('%s has an unsupported schema.', uri) + return None + + content = [] + deadline = time.time() + timeout + for chunk in response.iter_content(chunk_size): + content.append(chunk) + if time.time() > deadline: + logger.warning('Download of %r failed due to download taking more ' + 'than %.3fs', uri, timeout) + return None + + if not response.ok: + logger.warning('Problem downloading %r: %s', uri, response.reason) + return None + + return b''.join(content) From 5c46b83f8193afdd29071f7c20a269e23c421738 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 18 Aug 2015 23:03:36 +0200 Subject: [PATCH 39/47] stream: Switch to using http download helper --- mopidy/stream/actor.py | 44 ++++------------------------------- tests/stream/test_playback.py | 16 ++++++------- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index cc9632d5..8066403b 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -3,13 +3,10 @@ from __future__ import absolute_import, unicode_literals import fnmatch import logging import re -import time import urlparse import pykka -import requests - from mopidy import audio as audio_lib, backend, exceptions, stream from mopidy.audio import scan, utils from mopidy.internal import http, playlists @@ -77,6 +74,10 @@ class StreamPlaybackProvider(backend.PlaybackProvider): super(StreamPlaybackProvider, self).__init__(audio, backend) self._config = config self._scanner = backend._scanner + self._session = http.get_requests_session( + proxy_config=config['proxy'], + user_agent='%s/%s' % ( + stream.Extension.dist_name, stream.Extension.version)) def translate_uri(self, uri): try: @@ -90,7 +91,7 @@ class StreamPlaybackProvider(backend.PlaybackProvider): scan_result.mime.startswith('application/')): return uri - content = self._download(uri) + content = http.download(self._session, uri) if content is None: return None @@ -98,38 +99,3 @@ class StreamPlaybackProvider(backend.PlaybackProvider): if tracks: # TODO Test streams and return first that seems to be playable return tracks[0] - - def _download(self, uri): - timeout = self._config['stream']['timeout'] / 1000.0 - - session = http.get_requests_session( - proxy_config=self._config['proxy'], - user_agent='%s/%s' % ( - stream.Extension.dist_name, stream.Extension.version)) - - try: - response = session.get( - uri, stream=True, timeout=timeout) - except requests.exceptions.Timeout: - logger.warning( - 'Download of stream playlist (%s) failed due to connection ' - 'timeout after %.3fs', uri, timeout) - return None - - deadline = time.time() + timeout - content = [] - for chunk in response.iter_content(4096): - content.append(chunk) - if time.time() > deadline: - logger.warning( - 'Download of stream playlist (%s) failed due to download ' - 'taking more than %.3fs', uri, timeout) - return None - - if not response.ok: - logger.warning( - 'Problem downloading stream playlist %s: %s', - uri, response.reason) - return None - - return b''.join(content) diff --git a/tests/stream/test_playback.py b/tests/stream/test_playback.py index 4da87ae0..0da13fa6 100644 --- a/tests/stream/test_playback.py +++ b/tests/stream/test_playback.py @@ -112,12 +112,12 @@ def test_translate_uri_when_playlist_download_fails(provider, caplog): result = provider.translate_uri(URI) assert result is None - assert 'Problem downloading stream playlist' in caplog.text() + assert 'Problem downloading' in caplog.text() def test_translate_uri_times_out_if_connection_times_out(provider, caplog): - with mock.patch.object(actor.requests, 'Session') as session_mock: - get_mock = session_mock.return_value.get + with mock.patch.object(provider, '_session') as session_mock: + get_mock = session_mock.get get_mock.side_effect = requests.exceptions.Timeout result = provider.translate_uri(URI) @@ -125,8 +125,8 @@ def test_translate_uri_times_out_if_connection_times_out(provider, caplog): get_mock.assert_called_once_with(URI, timeout=1.0, stream=True) assert result is None assert ( - 'Download of stream playlist (%s) failed due to connection ' - 'timeout after 1.000s' % URI in caplog.text()) + 'Download of %r failed due to connection timeout after 1.000s' % URI + in caplog.text()) @responses.activate @@ -134,12 +134,12 @@ def test_translate_uri_times_out_if_download_is_slow(provider, caplog): responses.add( responses.GET, URI, body=BODY, content_type='audio/x-mpegurl') - with mock.patch.object(actor, 'time') as time_mock: + with mock.patch('mopidy.internal.http.time') as time_mock: time_mock.time.side_effect = [0, TIMEOUT + 1] result = provider.translate_uri(URI) assert result is None assert ( - 'Download of stream playlist (%s) failed due to download taking ' - 'more than 1.000s' % URI in caplog.text()) + 'Download of %r failed due to download taking more than 1.000s' % + URI in caplog.text()) From 9de96a81f8a2f3e2c3d20f23e1afaf09b8f29fc3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 21:46:43 +0200 Subject: [PATCH 40/47] http: Move tests from stream to http util --- tests/internal/test_http.py | 63 +++++++++++++++++++++++++++++++++++ tests/stream/test_playback.py | 37 ++------------------ 2 files changed, 65 insertions(+), 35 deletions(-) create mode 100644 tests/internal/test_http.py diff --git a/tests/internal/test_http.py b/tests/internal/test_http.py new file mode 100644 index 00000000..6730777f --- /dev/null +++ b/tests/internal/test_http.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import, unicode_literals + +import mock + +import pytest + +import requests + +import responses + +from mopidy.internal import http + + +TIMEOUT = 1000 +URI = 'http://example.com/foo.txt' +BODY = "This is the contents of foo.txt." + + +@pytest.fixture +def session(): + return requests.Session() + + +@pytest.fixture +def session_mock(): + return mock.Mock(spec=requests.Session) + + +@responses.activate +def test_download_on_server_side_error(session, caplog): + responses.add(responses.GET, URI, body=BODY, status=500) + + result = http.download(session, URI) + + assert result is None + assert 'Problem downloading' in caplog.text() + + +def test_download_times_out_if_connection_times_out(session_mock, caplog): + session_mock.get.side_effect = requests.exceptions.Timeout + + result = http.download(session_mock, URI, timeout=1.0) + + session_mock.get.assert_called_once_with(URI, timeout=1.0, stream=True) + assert result is None + assert ( + 'Download of %r failed due to connection timeout after 1.000s' % URI + in caplog.text()) + + +@responses.activate +def test_download_times_out_if_download_is_slow(session, caplog): + responses.add(responses.GET, URI, body=BODY, content_type='text/plain') + + with mock.patch.object(http, 'time') as time_mock: + time_mock.time.side_effect = [0, TIMEOUT + 1] + + result = http.download(session, URI) + + assert result is None + assert ( + 'Download of %r failed due to download taking more than 1.000s' % URI + in caplog.text()) diff --git a/tests/stream/test_playback.py b/tests/stream/test_playback.py index 0da13fa6..c186ee88 100644 --- a/tests/stream/test_playback.py +++ b/tests/stream/test_playback.py @@ -4,8 +4,6 @@ import mock import pytest -import requests - import responses from mopidy import exceptions @@ -105,41 +103,10 @@ def test_translate_uri_when_scanner_fails(scanner, provider, caplog): assert 'Problem scanning URI bar: foo failed' in caplog.text() -@responses.activate def test_translate_uri_when_playlist_download_fails(provider, caplog): - responses.add(responses.GET, URI, body=BODY, status=500) - - result = provider.translate_uri(URI) - - assert result is None - assert 'Problem downloading' in caplog.text() - - -def test_translate_uri_times_out_if_connection_times_out(provider, caplog): - with mock.patch.object(provider, '_session') as session_mock: - get_mock = session_mock.get - get_mock.side_effect = requests.exceptions.Timeout - - result = provider.translate_uri(URI) - - get_mock.assert_called_once_with(URI, timeout=1.0, stream=True) - assert result is None - assert ( - 'Download of %r failed due to connection timeout after 1.000s' % URI - in caplog.text()) - - -@responses.activate -def test_translate_uri_times_out_if_download_is_slow(provider, caplog): - responses.add( - responses.GET, URI, body=BODY, content_type='audio/x-mpegurl') - - with mock.patch('mopidy.internal.http.time') as time_mock: - time_mock.time.side_effect = [0, TIMEOUT + 1] + with mock.patch.object(actor, 'http') as http_mock: + http_mock.download.return_value = None result = provider.translate_uri(URI) assert result is None - assert ( - 'Download of %r failed due to download taking more than 1.000s' % - URI in caplog.text()) From 92187f2c3f8688a3c4a94f257f779db8868d5aa9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Sep 2015 23:51:04 +0200 Subject: [PATCH 41/47] audio: Add timeout arg to scan() --- docs/changelog.rst | 3 +++ mopidy/audio/scan.py | 15 +++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f23b68c4..03016776 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,9 @@ Bug fix release. - Core: Fix error in :meth:`~mopidy.core.TracklistController.get_eot_tlid` docstring. (Fixes: :issue:`1269`) +- Audio: Add ``timeout`` parameter to :meth:`~mopidy.audio.scan.Scanner.scan`. + (Part of: :issue:`1250`) + - Extension support: Make :meth:`~mopidy.ext.Extension.get_cache_dir`, :meth:`~mopidy.ext.Extension.get_config_dir`, and :meth:`~mopidy.ext.Extension.get_data_dir` class methods, so they can be used diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index d1081788..ca2c308c 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -33,12 +33,15 @@ class Scanner(object): self._timeout_ms = int(timeout) self._proxy_config = proxy_config or {} - def scan(self, uri): + def scan(self, uri, timeout=None): """ Scan the given uri collecting relevant metadata. :param uri: URI of the resource to scan. - :type event: string + :type uri: string + :param timeout: timeout for scanning a URI in ms. Defaults to the + ``timeout`` value used when creating the scanner. + :type timeout: int :return: A named tuple containing ``(uri, tags, duration, seekable, mime)``. ``tags`` is a dictionary of lists for all the tags we found. @@ -46,12 +49,13 @@ class Scanner(object): :class:`None` if the URI has no duration. ``seekable`` is boolean. indicating if a seek would succeed. """ + timeout = int(timeout or self._timeout_ms) tags, duration, seekable, mime = None, None, None, None pipeline = _setup_pipeline(uri, self._proxy_config) try: _start_pipeline(pipeline) - tags, mime, have_audio = _process(pipeline, self._timeout_ms) + tags, mime, have_audio = _process(pipeline, timeout) duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: @@ -132,7 +136,10 @@ def _process(pipeline, timeout_ms): clock = pipeline.get_clock() bus = pipeline.get_bus() timeout = timeout_ms * gst.MSECOND - tags, mime, have_audio, missing_message = {}, None, False, None + tags = {} + mime = None + have_audio = False + missing_message = None types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) From 2d10eef0b17b53f486bb8336b9f67eb7d43dc4b0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Sep 2015 12:58:36 +0200 Subject: [PATCH 42/47] stream: Expand nested stream playlists Fixes #1250 --- docs/changelog.rst | 5 ++ mopidy/stream/actor.py | 73 +++++++++++++++--- tests/stream/test_playback.py | 134 ++++++++++++++++++++++++---------- 3 files changed, 161 insertions(+), 51 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 03016776..b9646ce8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -53,6 +53,11 @@ Bug fix release. services installed from packages that properly set :confval:`core/data_dir`, like the Debian and Arch pakages. (Fixes: :issue:`1259`, PR: :issue:`1266`) +- Stream: Expand nested playlists to find the stream URI. This used to work, + but regressed in 1.1.0 with the extraction of stream playlist parsing from + GStreamer to being handled by the Mopidy-Stream backend. (Fixes: + :issue:`1250`) + - Stream: If "file" is present in the :confval:`stream/protocols` config value and the :ref:`ext-file` extension is enabled, we exited with an error because two extensions claimed the same URI scheme. We now log a warning recommending diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index 8066403b..095fcb2f 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals import fnmatch import logging import re +import time import urlparse import pykka @@ -80,22 +81,70 @@ class StreamPlaybackProvider(backend.PlaybackProvider): stream.Extension.dist_name, stream.Extension.version)) def translate_uri(self, uri): - try: - scan_result = self._scanner.scan(uri) - except exceptions.ScannerError as e: - logger.warning( - 'Problem scanning URI %s: %s', uri, e) - return None + return unwrap_stream( + uri, + timeout=self._config['stream']['timeout'], + scanner=self._scanner, + requests_session=self._session) - if not (scan_result.mime.startswith('text/') or + +def unwrap_stream(uri, timeout, scanner, requests_session): + """ + Get a stream URI from a playlist URI, ``uri``. + + Unwraps nested playlists until something that's not a playlist is found or + the ``timeout`` is reached. + """ + + original_uri = uri + deadline = time.time() + timeout + + while time.time() < deadline: + logger.debug('Unwrapping stream from URI: %s', uri) + + try: + scan_timeout = deadline - time.time() + if scan_timeout < 0: + logger.info( + 'Unwrapping stream from URI (%s) failed: ' + 'timed out in %sms', + uri, timeout) + return None + scan_result = scanner.scan(uri, timeout=scan_timeout) + except exceptions.ScannerError as exc: + logger.debug('GStreamer failed scanning URI (%s): %s', uri, exc) + scan_result = None + + if scan_result is not None and not ( + scan_result.mime.startswith('text/') or scan_result.mime.startswith('application/')): + logger.debug( + 'Unwrapped potential %s stream: %s', scan_result.mime, uri) return uri - content = http.download(self._session, uri) + download_timeout = deadline - time.time() + if download_timeout < 0: + logger.info( + 'Unwrapping stream from URI (%s) failed: timed out in %sms', + uri, timeout) + return None + content = http.download( + requests_session, uri, timeout=download_timeout) + if content is None: + logger.info( + 'Unwrapping stream from URI (%s) failed: ' + 'error downloading URI %s', original_uri, uri) return None - tracks = list(playlists.parse(content)) - if tracks: - # TODO Test streams and return first that seems to be playable - return tracks[0] + uris = playlists.parse(content) + if not uris: + logger.debug( + 'Failed parsing URI (%s) as playlist; found potential stream.', + uri) + return uri + + # TODO Test streams and return first that seems to be playable + logger.debug( + 'Parsed playlist (%s) and found new URI: %s', uri, uris[0]) + uri = uris[0] diff --git a/tests/stream/test_playback.py b/tests/stream/test_playback.py index c186ee88..ba7c2c92 100644 --- a/tests/stream/test_playback.py +++ b/tests/stream/test_playback.py @@ -12,7 +12,8 @@ from mopidy.stream import actor TIMEOUT = 1000 -URI = 'http://example.com/listen.m3u' +PLAYLIST_URI = 'http://example.com/listen.m3u' +STREAM_URI = 'http://example.com/stream.mp3' BODY = """ #EXTM3U http://example.com/stream.mp3 @@ -37,9 +38,7 @@ def audio(): @pytest.fixture def scanner(): - scanner = mock.Mock(spec=scan.Scanner) - scanner.scan.return_value.mime = 'text/foo' - return scanner + return mock.Mock(spec=scan.Scanner) @pytest.fixture @@ -55,58 +54,115 @@ def provider(audio, backend, config): return actor.StreamPlaybackProvider(audio, backend, config) -@responses.activate -def test_translate_uri_of_audio_stream_returns_same_uri( - scanner, provider): +class TestTranslateURI(object): - scanner.scan.return_value.mime = 'audio/ogg' + @responses.activate + def test_audio_stream_returns_same_uri(self, scanner, provider): + scanner.scan.return_value.mime = 'audio/mpeg' - result = provider.translate_uri(URI) + result = provider.translate_uri(STREAM_URI) - scanner.scan.assert_called_once_with(URI) - assert result == URI + scanner.scan.assert_called_once_with(STREAM_URI, timeout=mock.ANY) + assert result == STREAM_URI + @responses.activate + def test_text_playlist_with_mpeg_stream( + self, scanner, provider, caplog): -@responses.activate -def test_translate_uri_of_playlist_returns_first_uri_in_list( - scanner, provider): + scanner.scan.side_effect = [ + mock.Mock(mime='text/foo'), # scanning playlist + mock.Mock(mime='audio/mpeg'), # scanning stream + ] + responses.add( + responses.GET, PLAYLIST_URI, + body=BODY, content_type='audio/x-mpegurl') - responses.add( - responses.GET, URI, body=BODY, content_type='audio/x-mpegurl') + result = provider.translate_uri(PLAYLIST_URI) - result = provider.translate_uri(URI) + assert scanner.scan.mock_calls == [ + mock.call(PLAYLIST_URI, timeout=mock.ANY), + mock.call(STREAM_URI, timeout=mock.ANY), + ] + assert result == STREAM_URI - scanner.scan.assert_called_once_with(URI) - assert result == 'http://example.com/stream.mp3' - assert responses.calls[0].request.headers['User-Agent'].startswith( - 'Mopidy-Stream/') + # Check logging to ensure debuggability + assert 'Unwrapping stream from URI: %s' % PLAYLIST_URI + assert 'Parsed playlist (%s)' % PLAYLIST_URI in caplog.text() + assert 'Unwrapping stream from URI: %s' % STREAM_URI + assert ( + 'Unwrapped potential audio/mpeg stream: %s' % STREAM_URI + in caplog.text()) + # Check proper Requests session setup + assert responses.calls[0].request.headers['User-Agent'].startswith( + 'Mopidy-Stream/') -@responses.activate -def test_translate_uri_of_playlist_with_xml_mimetype(scanner, provider): - scanner.scan.return_value.mime = 'application/xspf+xml' - responses.add( - responses.GET, URI, body=BODY, content_type='application/xspf+xml') + @responses.activate + def test_xml_playlist_with_mpeg_stream(self, scanner, provider): + scanner.scan.side_effect = [ + mock.Mock(mime='application/xspf+xml'), # scanning playlist + mock.Mock(mime='audio/mpeg'), # scanning stream + ] + responses.add( + responses.GET, PLAYLIST_URI, + body=BODY, content_type='application/xspf+xml') - result = provider.translate_uri(URI) + result = provider.translate_uri(PLAYLIST_URI) - scanner.scan.assert_called_once_with(URI) - assert result == 'http://example.com/stream.mp3' + assert scanner.scan.mock_calls == [ + mock.call(PLAYLIST_URI, timeout=mock.ANY), + mock.call(STREAM_URI, timeout=mock.ANY), + ] + assert result == STREAM_URI + @responses.activate + def test_scan_fails_but_playlist_parsing_succeeds( + self, scanner, provider, caplog): -def test_translate_uri_when_scanner_fails(scanner, provider, caplog): - scanner.scan.side_effect = exceptions.ScannerError('foo failed') + scanner.scan.side_effect = [ + exceptions.ScannerError('some failure'), # scanning playlist + mock.Mock(mime='audio/mpeg'), # scanning stream + ] + responses.add( + responses.GET, PLAYLIST_URI, + body=BODY, content_type='audio/x-mpegurl') - result = provider.translate_uri('bar') + result = provider.translate_uri(PLAYLIST_URI) - assert result is None - assert 'Problem scanning URI bar: foo failed' in caplog.text() + assert 'Unwrapping stream from URI: %s' % PLAYLIST_URI + assert ( + 'GStreamer failed scanning URI (%s)' % PLAYLIST_URI + in caplog.text()) + assert 'Parsed playlist (%s)' % PLAYLIST_URI in caplog.text() + assert ( + 'Unwrapped potential audio/mpeg stream: %s' % STREAM_URI + in caplog.text()) + assert result == STREAM_URI + @responses.activate + def test_scan_fails_and_playlist_parsing_fails( + self, scanner, provider, caplog): -def test_translate_uri_when_playlist_download_fails(provider, caplog): - with mock.patch.object(actor, 'http') as http_mock: - http_mock.download.return_value = None + scanner.scan.side_effect = exceptions.ScannerError('some failure') + responses.add( + responses.GET, STREAM_URI, + body=b'some audio data', content_type='audio/mpeg') - result = provider.translate_uri(URI) + result = provider.translate_uri(STREAM_URI) - assert result is None + assert 'Unwrapping stream from URI: %s' % STREAM_URI + assert ( + 'GStreamer failed scanning URI (%s)' % STREAM_URI + in caplog.text()) + assert ( + 'Failed parsing URI (%s) as playlist; found potential stream.' + % STREAM_URI in caplog.text()) + assert result == STREAM_URI + + def test_failed_download_returns_none(self, provider, caplog): + with mock.patch.object(actor, 'http') as http_mock: + http_mock.download.return_value = None + + result = provider.translate_uri(PLAYLIST_URI) + + assert result is None From 99b8e05cff8b252d384b92ea95c12930c3e44764 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 13 Sep 2015 23:29:42 +0200 Subject: [PATCH 43/47] docs: Add PR refs to changelog --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b9646ce8..36fa4f9f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,7 +29,7 @@ Bug fix release. docstring. (Fixes: :issue:`1269`) - Audio: Add ``timeout`` parameter to :meth:`~mopidy.audio.scan.Scanner.scan`. - (Part of: :issue:`1250`) + (Part of: :issue:`1250`, PR: :issue:`1281`) - Extension support: Make :meth:`~mopidy.ext.Extension.get_cache_dir`, :meth:`~mopidy.ext.Extension.get_config_dir`, and @@ -56,7 +56,7 @@ Bug fix release. - Stream: Expand nested playlists to find the stream URI. This used to work, but regressed in 1.1.0 with the extraction of stream playlist parsing from GStreamer to being handled by the Mopidy-Stream backend. (Fixes: - :issue:`1250`) + :issue:`1250`, PR: :issue:`1281`) - Stream: If "file" is present in the :confval:`stream/protocols` config value and the :ref:`ext-file` extension is enabled, we exited with an error because From e217bd6737e61ec672a710c4cdddfe6610d9bee2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Sep 2015 09:46:22 +0200 Subject: [PATCH 44/47] stream: Make playlist unwrap helper private --- mopidy/stream/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index 095fcb2f..4561bb63 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -81,14 +81,14 @@ class StreamPlaybackProvider(backend.PlaybackProvider): stream.Extension.dist_name, stream.Extension.version)) def translate_uri(self, uri): - return unwrap_stream( + return _unwrap_stream( uri, timeout=self._config['stream']['timeout'], scanner=self._scanner, requests_session=self._session) -def unwrap_stream(uri, timeout, scanner, requests_session): +def _unwrap_stream(uri, timeout, scanner, requests_session): """ Get a stream URI from a playlist URI, ``uri``. From 6e126ee8509ac40048c052e13280a1c31ddf99c3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Sep 2015 10:12:31 +0200 Subject: [PATCH 45/47] stream: Abort unwrap if visiting same URI twice --- mopidy/stream/actor.py | 12 ++++++++++-- tests/stream/test_playback.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index 4561bb63..b3bf0b30 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -97,9 +97,18 @@ def _unwrap_stream(uri, timeout, scanner, requests_session): """ original_uri = uri + seen_uris = set() deadline = time.time() + timeout while time.time() < deadline: + if uri in seen_uris: + logger.info( + 'Unwrapping stream from URI (%s) failed: ' + 'playlist referenced itself', uri) + return None + else: + seen_uris.add(uri) + logger.debug('Unwrapping stream from URI: %s', uri) try: @@ -107,8 +116,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session): if scan_timeout < 0: logger.info( 'Unwrapping stream from URI (%s) failed: ' - 'timed out in %sms', - uri, timeout) + 'timed out in %sms', uri, timeout) return None scan_result = scanner.scan(uri, timeout=scan_timeout) except exceptions.ScannerError as exc: diff --git a/tests/stream/test_playback.py b/tests/stream/test_playback.py index ba7c2c92..4c42b1cd 100644 --- a/tests/stream/test_playback.py +++ b/tests/stream/test_playback.py @@ -166,3 +166,22 @@ class TestTranslateURI(object): result = provider.translate_uri(PLAYLIST_URI) assert result is None + + @responses.activate + def test_playlist_references_itself(self, scanner, provider, caplog): + scanner.scan.return_value.mime = 'text/foo' + responses.add( + responses.GET, PLAYLIST_URI, + body=BODY.replace(STREAM_URI, PLAYLIST_URI), + content_type='audio/x-mpegurl') + + result = provider.translate_uri(PLAYLIST_URI) + + assert 'Unwrapping stream from URI: %s' % PLAYLIST_URI in caplog.text() + assert ( + 'Parsed playlist (%s) and found new URI: %s' + % (PLAYLIST_URI, PLAYLIST_URI)) in caplog.text() + assert ( + 'Unwrapping stream from URI (%s) failed: ' + 'playlist referenced itself' % PLAYLIST_URI) in caplog.text() + assert result is None From f6ebe6da7058a24b87413d6d83ed367b46f1ade4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Sep 2015 10:26:31 +0200 Subject: [PATCH 46/47] Bump version to 1.1.1 --- mopidy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 40308a53..df9aacc3 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -14,4 +14,4 @@ if not (2, 7) <= sys.version_info < (3,): warnings.filterwarnings('ignore', 'could not open display') -__version__ = '1.1.0' +__version__ = '1.1.1' From a2af001c8bf762228fed67a278df631b07a616f6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Sep 2015 10:30:39 +0200 Subject: [PATCH 47/47] docs: Update changelog for 1.1.1 --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 36fa4f9f..6932fb54 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,12 +5,13 @@ Changelog This changelog is used to track all major changes to Mopidy. -v1.1.1 (UNRELEASED) +v1.1.1 (2015-09-14) =================== Bug fix release. - Dependencies: Specify that we need Requests >= 2.0, not just any version. + This ensures that we fail earlier if Mopidy is used with a too old Requests. - Core: Make :meth:`mopidy.core.LibraryController.refresh` work for all backends with a library provider. Previously, it wrongly worked for all