From c31815b0e65b2cac0fc357e9c37ae75090876e5e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Oct 2013 23:21:45 +0200 Subject: [PATCH 01/16] docs: Update changelog --- docs/changelog.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9863b6ab..374dc2cd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -69,6 +69,15 @@ of the following extensions as well: - Added :meth:`mopidy.core.CoreListener.mute_changed` event that is triggered when the mute state changes. +- In "random" mode, after a full playthrough of the tracklist, playback + continued from the last track played to the end of the playlist in non-random + order. It now stops when all tracks have been played once, unless "repeat" + mode is enabled. (Fixes: :issue:`453`) + +- In "single" mode, after a track ended, playback continued with the next track + in the tracklis. It now stops after playing a single track, unless "repeat" + mode is enabled. (Fixes: :issue:`496`) + **Audio** - Added support for parsing and playback of playlists in GStreamer. For end @@ -92,6 +101,11 @@ of the following extensions as well: - Media files with less than 100ms duration are now excluded from the library. +- Unknown URIs found in playlists are now made into track objects with the URI + set instead of being ignored. This makes it possible to have playlists with + e.g. HTTP radio streams and not just ``local:track:...`` URIs. This used to + work, but was broken in Mopidy 0.15.0. (Fixes: :issue:`527`) + **MPD frontend** - Made the formerly unused commands ``outputs``, ``enableoutput``, and From 010cb62756efd785724b297d85466684f9d86844 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 20 Oct 2013 23:45:33 +0200 Subject: [PATCH 02/16] docs: Add note about fixing #198 to changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 374dc2cd..e05a6c9a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -97,7 +97,7 @@ of the following extensions as well: - Replaced our custom media library scanner with GStreamer's builtin scanner. This should make scanning less error prone and faster as timeouts should be - infrequent. + infrequent. (Fixes: :issue:`198`) - Media files with less than 100ms duration are now excluded from the library. From 2117add55f94151452942de7399d6973338207f8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Oct 2013 23:48:17 +0200 Subject: [PATCH 03/16] docs: Include minimum GStreamer version --- docs/installation/index.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 238184f4..65c014b2 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -175,10 +175,10 @@ can install Mopidy from PyPI using Pip. #. Then you'll need to install all of Mopidy's hard non-Python dependencies: - - GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most - popular Linux distributions. Search for GStreamer in your package manager, - and make sure to install the Python bindings, and the "good" and "ugly" - plugin sets. + - GStreamer 0.10.31 or later, with Python bindings. GStreamer is packaged + for most popular Linux distributions. Search for GStreamer in your package + manager, and make sure to install the Python bindings, and the "good" and + "ugly" plugin sets. If you use Debian/Ubuntu you can install GStreamer like this:: From ef15c4f6fc66fefde84c30ec19129475738ba399 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Oct 2013 20:27:19 +0200 Subject: [PATCH 04/16] docs: Enable new RTD theme --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 56ddbf92..5a75b7d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -78,6 +78,9 @@ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../')) # the string True. on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +# Enable Read the Docs' new theme +RTD_NEW_THEME = True + # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be From 8097add7ed6aabbeca42682011930dbb794a6921 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Oct 2013 20:30:25 +0200 Subject: [PATCH 05/16] docs: More exact GStreamer version range --- docs/installation/index.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 65c014b2..cd4ad983 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -88,7 +88,7 @@ Mopidy Git repo, which always corresponds to the latest release. #. Optional: If you want to scrobble your played tracks to Last.fm, you need to install `python2-pylast`:: - + sudo pacman -S python2-pylast #. Finally, you need to set a couple of :doc:`config values `, and @@ -175,10 +175,10 @@ can install Mopidy from PyPI using Pip. #. Then you'll need to install all of Mopidy's hard non-Python dependencies: - - GStreamer 0.10.31 or later, with Python bindings. GStreamer is packaged - for most popular Linux distributions. Search for GStreamer in your package - manager, and make sure to install the Python bindings, and the "good" and - "ugly" plugin sets. + - GStreamer 0.10 (>= 0.10.31, < 0.11), with Python bindings. GStreamer is + packaged for most popular Linux distributions. Search for GStreamer in + your package manager, and make sure to install the Python bindings, and + the "good" and "ugly" plugin sets. If you use Debian/Ubuntu you can install GStreamer like this:: From 80b9329dcb7e5c49753b6e7e34745a436fc8e93d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Oct 2013 20:57:30 +0200 Subject: [PATCH 06/16] docs: Update authors --- .mailmap | 1 + AUTHORS | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mailmap b/.mailmap index 2ff779fc..242d4d91 100644 --- a/.mailmap +++ b/.mailmap @@ -8,3 +8,4 @@ John Bäckstrand Alli Witheford Alexandre Petitjean Alexandre Petitjean +Javier Domingo Cansino diff --git a/AUTHORS b/AUTHORS index fdfb82fb..e59b92e2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,7 +24,6 @@ - Alli Witheford - Alexandre Petitjean - Terje Larsen -- Pavol Babincak -- Javier Domingo - Javier Domingo Cansino +- Pavol Babincak - Lasse Bigum From 9d0b04e96f7dadacb33bc7d0e37864b7afedacac Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Oct 2013 21:20:00 +0200 Subject: [PATCH 07/16] docs: Fix link to Mopidy-Arcam --- docs/ext/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ext/index.rst b/docs/ext/index.rst index 940dd37a..27fe3b45 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -33,7 +33,7 @@ developers. Mopidy-Arcam ------------ -https://github.com/mopidy/mopidy-arcam +https://github.com/TooDizzy/mopidy-arcam Extension for controlling volume using an external Arcam amplifier. Developed and tested with an Arcam AVR-300. From 84612ca1ac9afb46150a9fd980a3ee52fd2e3fe4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Oct 2013 22:27:04 +0200 Subject: [PATCH 08/16] docs: Fix typo --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e05a6c9a..57e0a3c8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -75,7 +75,7 @@ of the following extensions as well: mode is enabled. (Fixes: :issue:`453`) - In "single" mode, after a track ended, playback continued with the next track - in the tracklis. It now stops after playing a single track, unless "repeat" + in the tracklist. It now stops after playing a single track, unless "repeat" mode is enabled. (Fixes: :issue:`496`) **Audio** From f4d0d5648f0a726ea5011ef7c3c6bed3d9f63dc4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Oct 2013 22:39:33 +0200 Subject: [PATCH 09/16] docs: Fix typo --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 57e0a3c8..df235d1d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -111,7 +111,7 @@ of the following extensions as well: - Made the formerly unused commands ``outputs``, ``enableoutput``, and ``disableoutput`` mute/unmute audio. (Related to: :issue:`186`) -- The MPD command ``list`` now works with ``"albumartist"`` as it's second +- The MPD command ``list`` now works with ``"albumartist"`` as its second argument, e.g. ``"album" "albumartist" "anartist"``. (Fixes: :issue:`468`) **Extension support** From 1e3191a0f9a45a7d640a5881ba26a1f9af918975 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 24 Oct 2013 09:51:09 +0200 Subject: [PATCH 10/16] docs: Search is on every page, don't need a search page --- docs/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 17a40c32..732c9f32 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,4 +81,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` From e448d77eb76a55ad08991fa05096cd926e69bfba Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 24 Oct 2013 22:19:59 +0200 Subject: [PATCH 11/16] mpd: Fix flipped mute logic --- mopidy/frontends/mpd/protocol/audio_output.py | 4 ++-- tests/frontends/mpd/protocol/audio_output_test.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py index 65e693ec..17cf4ac4 100644 --- a/mopidy/frontends/mpd/protocol/audio_output.py +++ b/mopidy/frontends/mpd/protocol/audio_output.py @@ -14,7 +14,7 @@ def disableoutput(context, outputid): Turns an output off. """ if int(outputid) == 0: - context.core.playback.set_mute(True) + context.core.playback.set_mute(False) else: raise MpdNoExistError('No such audio output', command='disableoutput') @@ -29,7 +29,7 @@ def enableoutput(context, outputid): Turns an output on. """ if int(outputid) == 0: - context.core.playback.set_mute(False) + context.core.playback.set_mute(True) else: raise MpdNoExistError('No such audio output', command='enableoutput') diff --git a/tests/frontends/mpd/protocol/audio_output_test.py b/tests/frontends/mpd/protocol/audio_output_test.py index cbfb5043..4871f169 100644 --- a/tests/frontends/mpd/protocol/audio_output_test.py +++ b/tests/frontends/mpd/protocol/audio_output_test.py @@ -5,12 +5,12 @@ from tests.frontends.mpd import protocol class AudioOutputHandlerTest(protocol.BaseTestCase): def test_enableoutput(self): - self.core.playback.mute = True + self.core.playback.mute = False self.sendRequest('enableoutput "0"') self.assertInResponse('OK') - self.assertEqual(self.core.playback.mute.get(), False) + self.assertEqual(self.core.playback.mute.get(), True) def test_enableoutput_unknown_outputid(self): self.sendRequest('enableoutput "7"') @@ -18,12 +18,12 @@ class AudioOutputHandlerTest(protocol.BaseTestCase): self.assertInResponse('ACK [50@0] {enableoutput} No such audio output') def test_disableoutput(self): - self.core.playback.mute = False + self.core.playback.mute = True self.sendRequest('disableoutput "0"') self.assertInResponse('OK') - self.assertEqual(self.core.playback.mute.get(), True) + self.assertEqual(self.core.playback.mute.get(), False) def test_disableoutput_unknown_outputid(self): self.sendRequest('disableoutput "7"') From b0ae7d3c6fbd002cfd202e6fd0ffa54dce7e57af Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 24 Oct 2013 23:22:01 +0200 Subject: [PATCH 12/16] local: Fix crash on non-ASCII chars in URIs --- docs/changelog.rst | 3 +++ mopidy/backends/local/playback.py | 2 +- tests/backends/local/playback_test.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index df235d1d..e77cd948 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -106,6 +106,9 @@ of the following extensions as well: e.g. HTTP radio streams and not just ``local:track:...`` URIs. This used to work, but was broken in Mopidy 0.15.0. (Fixes: :issue:`527`) +- Fixed crash when playing ``local:track:...`` URIs which contained non-ASCII + chars after uridecode. + **MPD frontend** - Made the formerly unused commands ``outputs``, ``enableoutput``, and diff --git a/mopidy/backends/local/playback.py b/mopidy/backends/local/playback.py index eda06ff7..98c92a85 100644 --- a/mopidy/backends/local/playback.py +++ b/mopidy/backends/local/playback.py @@ -13,7 +13,7 @@ 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 = path.uri_to_path(track.uri).split(b':', 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/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index ab135766..8fbc4415 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -72,6 +72,14 @@ class LocalPlaybackProviderTest(unittest.TestCase): self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) + def test_play_uri_with_non_ascii_bytes(self): + # Regression test: If trying to do .split(u':') on a bytestring, the + # string will be decoded from ASCII to Unicode, which will crash on + # non-ASCII strings, like the bytestring the following URI decodes to. + self.add_track('local:track:12%20Doin%E2%80%99%20It%20Right.flac') + self.playback.play() + self.assertEqual(self.playback.state, PlaybackState.PLAYING) + def test_initial_state_is_stopped(self): self.assertEqual(self.playback.state, PlaybackState.STOPPED) From add79d90dd4052837215d066624ebe676144ed58 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 25 Oct 2013 18:16:08 +0200 Subject: [PATCH 13/16] docs: Warn about EOT issue with Shoutcast (fixes #545) --- docs/config.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 5b8f5de1..c381ef70 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -209,6 +209,13 @@ this work first:: Streaming through SHOUTcast/Icecast ----------------------------------- +.. warning:: Known issue + + Currently, Mopidy does not handle end-of-track vs end-of-stream signalling + in GStreamer correctly. This causes the SHOUTcast stream to be disconnected + at the end of each track, rendering it quite useless. For further details, + see :issue:`492`. + If you want to play the audio on another computer than the one running Mopidy, you can stream the audio from Mopidy through an SHOUTcast or Icecast audio streaming server. Multiple media players can then be connected to the streaming From d5cb4282d97068845fda65940bf09a84c7b100f4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 27 Oct 2013 11:38:01 +0100 Subject: [PATCH 14/16] config: Add preprocessor for preserving comments when editing configs. Adds markers to configs files that ensures configparser won't mangle comments in the files. Will be combined with a postprocessor that undoes these changes. --- mopidy/config/__init__.py | 37 ++++++++++++++ tests/config/config_test.py | 96 +++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 0767b50c..8d68c7f3 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -2,8 +2,10 @@ from __future__ import unicode_literals import ConfigParser as configparser import io +import itertools import logging import os.path +import re from mopidy.config import keyring from mopidy.config.schemas import * # noqa @@ -145,6 +147,41 @@ def _format(config, comments, schemas, display): return b'\n'.join(output) +def _preprocess(string): + """Convert a raw config into a form that preserves comments etc.""" + results = ['[__COMMENTS__]'] + counter = itertools.count(0) + + section_re = re.compile(r'^(\[[^\]]+\])\s*(.+)$') + blank_line_re = re.compile(r'^\s*$') + comment_re = re.compile(r'^(#|;)') + inline_comment_re = re.compile(r' ;') + + def newlines(match): + return '__BLANK%d__ =' % next(counter) + + def comments(match): + if match.group(1) == '#': + return '__HASH%d__ =' % next(counter) + elif match.group(1) == ';': + return '__SEMICOLON%d__ =' % next(counter) + + def inlinecomments(match): + return '\n__INLINE%d__ =' % next(counter) + + def sections(match): + return '%s\n__SECTION%d__ = %s' % ( + match.group(1), next(counter), match.group(2)) + + for line in string.splitlines(): + line = blank_line_re.sub(newlines, line) + line = section_re.sub(sections, line) + line = comment_re.sub(comments, line) + line = inline_comment_re.sub(inlinecomments, line) + results.append(line) + return '\n'.join(results) + + class Proxy(collections.Mapping): def __init__(self, data): self._data = data diff --git a/tests/config/config_test.py b/tests/config/config_test.py index c40baa87..16769513 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -106,3 +106,99 @@ class ValidateTest(unittest.TestCase): self.assertEqual({'foo': {'bar': 'bad'}}, errors) # TODO: add more tests + + +INPUT_CONFIG = """# comments before first section should work + +[section] anything goes ; after the [] block it seems. +; this is a valid comment +this-should-equal-baz = baz ; as this is a comment +this-should-equal-everything = baz # as this is not a comment + +# this is also a comment ; and the next line should be a blank comment. +; +# foo # = should all be treated as a comment. +""" + +PROCESSED_CONFIG = """[__COMMENTS__] +__HASH0__ = comments before first section should work +__BLANK1__ = +[section] +__SECTION2__ = anything goes +__INLINE3__ = after the [] block it seems. +__SEMICOLON4__ = this is a valid comment +this-should-equal-baz = baz +__INLINE5__ = as this is a comment +this-should-equal-everything = baz # as this is not a comment +__BLANK6__ = +__HASH7__ = this is also a comment +__INLINE8__ = and the next line should be a blank comment. +__SEMICOLON9__ = +__HASH10__ = foo # = should all be treated as a comment.""" + + +class ProcessorTest(unittest.TestCase): + maxDiff = None # Show entire diff. + + def test_preprocessor_empty_config(self): + result = config._preprocess('') + self.assertEqual(result, '[__COMMENTS__]') + + def test_preprocessor_plain_section(self): + result = config._preprocess('[section]\nfoo = bar') + self.assertEqual(result, '[__COMMENTS__]\n' + '[section]\n' + 'foo = bar') + + def test_preprocessor_initial_comments(self): + result = config._preprocess('; foobar') + self.assertEqual(result, '[__COMMENTS__]\n' + '__SEMICOLON0__ = foobar') + + result = config._preprocess('# foobar') + self.assertEqual(result, '[__COMMENTS__]\n' + '__HASH0__ = foobar') + + result = config._preprocess('; foo\n# bar') + self.assertEqual(result, '[__COMMENTS__]\n' + '__SEMICOLON0__ = foo\n' + '__HASH1__ = bar') + + def test_preprocessor_initial_comment_inline_handling(self): + result = config._preprocess('; foo ; bar ; baz') + self.assertEqual(result, '[__COMMENTS__]\n' + '__SEMICOLON0__ = foo\n' + '__INLINE1__ = bar\n' + '__INLINE2__ = baz') + + def test_preprocessor_inline_semicolon_comment(self): + result = config._preprocess('[section]\nfoo = bar ; baz') + self.assertEqual(result, '[__COMMENTS__]\n' + '[section]\n' + 'foo = bar\n' + '__INLINE0__ = baz') + + def test_preprocessor_no_inline_hash_comment(self): + result = config._preprocess('[section]\nfoo = bar # baz') + self.assertEqual(result, '[__COMMENTS__]\n' + '[section]\n' + 'foo = bar # baz') + + def test_preprocessor_section_extra_text(self): + result = config._preprocess('[section] foobar') + self.assertEqual(result, '[__COMMENTS__]\n' + '[section]\n' + '__SECTION0__ = foobar') + + def test_preprocessor_section_extra_text_inline_semicolon(self): + result = config._preprocess('[section] foobar ; baz') + self.assertEqual(result, '[__COMMENTS__]\n' + '[section]\n' + '__SECTION0__ = foobar\n' + '__INLINE1__ = baz') + + def test_preprocessor_conversion(self): + """Tests all of the above cases at once.""" + result = config._preprocess(INPUT_CONFIG) + self.assertEqual(result, PROCESSED_CONFIG) + From 73f91710e14bc72c7de2f5ffc2b21042c43dc422 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 27 Oct 2013 12:30:02 +0100 Subject: [PATCH 15/16] config: Add postprocessor for converting config back. Idea forward from here is that once we have a config sub command that we expose a setting config values which will: 1. Run the preprocessor on the file to edit. 2. Load it into config parser. 3. Modify the value. 4. Write it to a io.ByteString 5. Run the postprocessor 6. Save the file with comments etc intact. --- mopidy/config/__init__.py | 16 ++++++- tests/config/config_test.py | 89 +++++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 8d68c7f3..6d66e253 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -147,7 +147,7 @@ def _format(config, comments, schemas, display): return b'\n'.join(output) -def _preprocess(string): +def _preprocess(config_string): """Convert a raw config into a form that preserves comments etc.""" results = ['[__COMMENTS__]'] counter = itertools.count(0) @@ -173,7 +173,7 @@ def _preprocess(string): return '%s\n__SECTION%d__ = %s' % ( match.group(1), next(counter), match.group(2)) - for line in string.splitlines(): + for line in config_string.splitlines(): line = blank_line_re.sub(newlines, line) line = section_re.sub(sections, line) line = comment_re.sub(comments, line) @@ -182,6 +182,18 @@ def _preprocess(string): return '\n'.join(results) +def _postprocess(config_string): + """Converts a preprocessed config back to original form.""" + flags = re.IGNORECASE | re.MULTILINE + result = re.sub(r'^\[__COMMENTS__\](\n|$)', '', config_string, flags=flags) + result = re.sub(r'\n__INLINE\d+__ =(.*)$', ' ;\g<1>', result, flags=flags) + result = re.sub(r'^__HASH\d+__ =(.*)$', '#\g<1>', result, flags=flags) + result = re.sub(r'^__SEMICOLON\d+__ =(.*)$', ';\g<1>', result, flags=flags) + result = re.sub(r'\n__SECTION\d+__ =(.*)$', '\g<1>', result, flags=flags) + result = re.sub(r'^__BLANK\d+__ =$', '', result, flags=flags) + return result + + class Proxy(collections.Mapping): def __init__(self, data): self._data = data diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 16769513..fceb293d 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -117,8 +117,7 @@ this-should-equal-everything = baz # as this is not a comment # this is also a comment ; and the next line should be a blank comment. ; -# foo # = should all be treated as a comment. -""" +# foo # = should all be treated as a comment.""" PROCESSED_CONFIG = """[__COMMENTS__] __HASH0__ = comments before first section should work @@ -137,20 +136,20 @@ __SEMICOLON9__ = __HASH10__ = foo # = should all be treated as a comment.""" -class ProcessorTest(unittest.TestCase): - maxDiff = None # Show entire diff. +class PreProcessorTest(unittest.TestCase): + maxDiff = None # Show entire diff. - def test_preprocessor_empty_config(self): + def test_empty_config(self): result = config._preprocess('') self.assertEqual(result, '[__COMMENTS__]') - def test_preprocessor_plain_section(self): + def test_plain_section(self): result = config._preprocess('[section]\nfoo = bar') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' 'foo = bar') - def test_preprocessor_initial_comments(self): + def test_initial_comments(self): result = config._preprocess('; foobar') self.assertEqual(result, '[__COMMENTS__]\n' '__SEMICOLON0__ = foobar') @@ -164,41 +163,105 @@ class ProcessorTest(unittest.TestCase): '__SEMICOLON0__ = foo\n' '__HASH1__ = bar') - def test_preprocessor_initial_comment_inline_handling(self): + def test_initial_comment_inline_handling(self): result = config._preprocess('; foo ; bar ; baz') self.assertEqual(result, '[__COMMENTS__]\n' '__SEMICOLON0__ = foo\n' '__INLINE1__ = bar\n' '__INLINE2__ = baz') - def test_preprocessor_inline_semicolon_comment(self): + def test_inline_semicolon_comment(self): result = config._preprocess('[section]\nfoo = bar ; baz') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' 'foo = bar\n' '__INLINE0__ = baz') - def test_preprocessor_no_inline_hash_comment(self): + def test_no_inline_hash_comment(self): result = config._preprocess('[section]\nfoo = bar # baz') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' 'foo = bar # baz') - def test_preprocessor_section_extra_text(self): + def test_section_extra_text(self): result = config._preprocess('[section] foobar') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' '__SECTION0__ = foobar') - def test_preprocessor_section_extra_text_inline_semicolon(self): + def test_section_extra_text_inline_semicolon(self): result = config._preprocess('[section] foobar ; baz') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' '__SECTION0__ = foobar\n' '__INLINE1__ = baz') - def test_preprocessor_conversion(self): + def test_conversion(self): """Tests all of the above cases at once.""" result = config._preprocess(INPUT_CONFIG) self.assertEqual(result, PROCESSED_CONFIG) + +class PostProcessorTest(unittest.TestCase): + maxDiff = None # Show entire diff. + + def test_empty_config(self): + result = config._postprocess('[__COMMENTS__]') + self.assertEqual(result, '') + + def test_plain_section(self): + result = config._postprocess('[__COMMENTS__]\n' + '[section]\n' + 'foo = bar') + self.assertEqual(result, '[section]\nfoo = bar') + + def test_initial_comments(self): + result = config._postprocess('[__COMMENTS__]\n' + '__SEMICOLON0__ = foobar') + self.assertEqual(result, '; foobar') + + result = config._postprocess('[__COMMENTS__]\n' + '__HASH0__ = foobar') + self.assertEqual(result, '# foobar') + + result = config._postprocess('[__COMMENTS__]\n' + '__SEMICOLON0__ = foo\n' + '__HASH1__ = bar') + self.assertEqual(result, '; foo\n# bar') + + def test_initial_comment_inline_handling(self): + result = config._postprocess('[__COMMENTS__]\n' + '__SEMICOLON0__ = foo\n' + '__INLINE1__ = bar\n' + '__INLINE2__ = baz') + self.assertEqual(result, '; foo ; bar ; baz') + + def test_inline_semicolon_comment(self): + result = config._postprocess('[__COMMENTS__]\n' + '[section]\n' + 'foo = bar\n' + '__INLINE0__ = baz') + self.assertEqual(result, '[section]\nfoo = bar ; baz') + + def test_no_inline_hash_comment(self): + result = config._preprocess('[section]\nfoo = bar # baz') + self.assertEqual(result, '[__COMMENTS__]\n' + '[section]\n' + 'foo = bar # baz') + + def test_section_extra_text(self): + result = config._postprocess('[__COMMENTS__]\n' + '[section]\n' + '__SECTION0__ = foobar') + self.assertEqual(result, '[section] foobar') + + def test_section_extra_text_inline_semicolon(self): + result = config._postprocess('[__COMMENTS__]\n' + '[section]\n' + '__SECTION0__ = foobar\n' + '__INLINE1__ = baz') + self.assertEqual(result, '[section] foobar ; baz') + + def test_conversion(self): + result = config._postprocess(PROCESSED_CONFIG) + self.assertEqual(result, INPUT_CONFIG) From ecc0bae3447aefb635533bcb9a3b861fbd39bb54 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 27 Oct 2013 14:10:56 +0100 Subject: [PATCH 16/16] local: Delete uris in library refresh (fixes #500) Makes sure we remove uri's that can no longer be found in the tag cache. --- mopidy/backends/local/library.py | 5 +++++ tests/backends/local/library_test.py | 30 +++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 0de63faf..f21ac81a 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -27,9 +27,14 @@ class LocalLibraryProvider(base.BaseLibraryProvider): self._media_dir, self._tag_cache_file) tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) + uris_to_remove = set(self._uri_mapping) for track in tracks: self._uri_mapping[track.uri] = track + uris_to_remove.discard(track.uri) + + for uri in uris_to_remove: + del self._uri_mapping[uri] logger.info( 'Loaded %d local tracks from %s using %s', diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 6b0cd6f6..09b3febb 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import tempfile import unittest import pykka @@ -11,6 +12,8 @@ from mopidy.models import Track, Album, Artist from tests import path_to_data_dir +# TODO: update tests to only use backend, not core. we need a seperate +# core test that does this integration test. class LocalLibraryProviderTest(unittest.TestCase): artists = [ Artist(name='artist1'), @@ -49,7 +52,6 @@ class LocalLibraryProviderTest(unittest.TestCase): } def setUp(self): - self.backend = actor.LocalBackend.start( config=self.config, audio=None).proxy() self.core = core.Core(backends=[self.backend]) @@ -65,9 +67,31 @@ class LocalLibraryProviderTest(unittest.TestCase): def test_refresh_uri(self): pass - @unittest.SkipTest def test_refresh_missing_uri(self): - pass + # Verifies that https://github.com/mopidy/mopidy/issues/500 + # has been fixed. + + tag_cache = tempfile.NamedTemporaryFile() + with open(self.config['local']['tag_cache_file']) as fh: + tag_cache.write(fh.read()) + tag_cache.flush() + + config = {'local': self.config['local'].copy()} + config['local']['tag_cache_file'] = tag_cache.name + backend = actor.LocalBackend(config=config, audio=None) + + # Sanity check that value is in tag cache + result = backend.library.lookup(self.tracks[0].uri) + self.assertEqual(result, self.tracks[0:1]) + + # Clear tag cache and refresh + tag_cache.seek(0) + tag_cache.truncate() + backend.library.refresh() + + # Now it should be gone. + result = backend.library.lookup(self.tracks[0].uri) + self.assertEqual(result, []) def test_lookup(self): tracks = self.library.lookup(self.tracks[0].uri)