From 22ec9d906032c6341dd290f8d400c4234c9ee0dd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 4 Dec 2015 22:11:55 +0100 Subject: [PATCH 001/142] mpd: Add missing seeked event handling for idle Fixes #1331 (cherry picked from commit 19daa89e15efa64cc128e742f6f8d3426f1adb0b) --- docs/changelog.rst | 2 ++ mopidy/mpd/actor.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index bc596d87..7637de58 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,8 @@ Bug fix release. - Local: If the scan or clear commands are used on a library that does not exist, exit with an error. (Fixes: :issue:`1298`) +- MPD: Notify idling clients when a seek is performed. (Fixes: :issue:`1331`) + v1.1.1 (2015-09-14) =================== diff --git a/mopidy/mpd/actor.py b/mopidy/mpd/actor.py index 8eb59c1f..58c758e4 100644 --- a/mopidy/mpd/actor.py +++ b/mopidy/mpd/actor.py @@ -77,3 +77,6 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): def stream_title_changed(self, title): self.send_idle('playlist') + + def seeked(self, time_position): + self.send_idle('player') From 2b00e831791cbab26c9aa19e28a42be6f7abdfa4 Mon Sep 17 00:00:00 2001 From: jcass Date: Sun, 6 Dec 2015 16:01:26 +0200 Subject: [PATCH 002/142] Mark track as playing and add to history if changing track while paused. --- docs/changelog.rst | 3 +++ mopidy/core/playback.py | 12 +++++++---- tests/core/test_playback.py | 43 ++++++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7637de58..29f7ef82 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,6 +19,9 @@ Bug fix release. - MPD: Notify idling clients when a seek is performed. (Fixes: :issue:`1331`) +- Core: Fix error in :meth:`~mopidy.core.PlaybackController._change_track` + docstring. (Fixes: :issue:`1352`) + v1.1.1 (2015-09-14) =================== diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 9a11066b..e3a4f68d 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -207,12 +207,16 @@ class PlaybackController(object): if old_state == PlaybackState.PLAYING: self._play(on_error_step=on_error_step) elif old_state == PlaybackState.PAUSED: - # NOTE: this is just a quick hack to fix #1177 as this code has - # already been killed in the gapless branch. + # NOTE: this is just a quick hack to fix #1177 and #1352 as this + # code has already been killed in the gapless branch. backend = self._get_backend() if backend: - backend.playback.prepare_change() - backend.playback.change_track(tl_track.track).get() + success = ( + backend.playback.prepare_change().get and + backend.playback.change_track(tl_track.track).get()) + if success: + self.core.tracklist._mark_playing(tl_track) + self.core.history._add_track(tl_track.track) self.pause() # TODO: this is not really end of track, this is on_need_next_track diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 5a8c9649..6bacb3ed 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -8,7 +8,7 @@ import pykka from mopidy import backend, core from mopidy.internal import deprecation -from mopidy.models import Track +from mopidy.models import TlTrack, Track from tests import dummy_audio as audio @@ -789,3 +789,44 @@ class Bug1177RegressionTest(unittest.TestCase): c.playback.pause() c.playback.next() b.playback.change_track.assert_called_once_with(track2) + + +class Bug1352RegressionTest(unittest.TestCase): + def test(self): + config = { + 'core': { + 'max_tracklist_length': 10000, + } + } + + b = mock.Mock() + b.uri_schemes.get.return_value = ['dummy'] + b.playback = mock.Mock(spec=backend.PlaybackProvider) + b.playback.change_track.return_value.get.return_value = True + b.playback.play.return_value.get.return_value = True + + track1 = Track(uri='dummy:a', length=40000) + track2 = Track(uri='dummy:b', length=40000) + + tl_track2 = TlTrack(1, track2) + + c = core.Core(config, mixer=None, backends=[b]) + c.tracklist.add([track1, track2]) + + d = mock.Mock() + c.history._add_track = d + + e = mock.Mock() + c.tracklist._mark_playing = e + + c.playback.play() + b.playback.change_track.reset_mock() + d.reset_mock() + e.reset_mock() + + c.playback.pause() + c.playback.next() + b.playback.change_track.assert_called_once_with(track2) + + d.assert_called_once_with(track2) + e.assert_called_once_with(tl_track2) From 139634b93bd0886a4245b4286923c406ccefc595 Mon Sep 17 00:00:00 2001 From: jcass Date: Sun, 6 Dec 2015 19:27:02 +0200 Subject: [PATCH 003/142] Update changelog. --- docs/changelog.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 29f7ef82..7d901aef 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,8 +19,9 @@ Bug fix release. - MPD: Notify idling clients when a seek is performed. (Fixes: :issue:`1331`) -- Core: Fix error in :meth:`~mopidy.core.PlaybackController._change_track` - docstring. (Fixes: :issue:`1352`) +- Core: Fix :meth:`~mopidy.core.PlaybackController._change_track` to mark + track as playing and add it to the history if changing track while paused. + (Fixes: :issue:`1352`) v1.1.1 (2015-09-14) From fb7b466bee23674f6d6bd97391ee5222f1daf580 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 7 Dec 2015 07:53:34 +0200 Subject: [PATCH 004/142] Result of prepare_change no longer affects whether a track is added to the history. Update changelog and test cases. --- docs/changelog.rst | 6 +++--- mopidy/core/playback.py | 5 ++--- tests/core/test_playback.py | 15 ++++++--------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7d901aef..41d5cccc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,9 +19,9 @@ Bug fix release. - MPD: Notify idling clients when a seek is performed. (Fixes: :issue:`1331`) -- Core: Fix :meth:`~mopidy.core.PlaybackController._change_track` to mark - track as playing and add it to the history if changing track while paused. - (Fixes: :issue:`1352`) +- Core: Fix bug in playback controller. If changing to another track while + the player is paused, the new track would not be added to the history or + marked as currently playing. (Fixes: :issue:`1352`) v1.1.1 (2015-09-14) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index e3a4f68d..eeba5106 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -211,9 +211,8 @@ class PlaybackController(object): # code has already been killed in the gapless branch. backend = self._get_backend() if backend: - success = ( - backend.playback.prepare_change().get and - backend.playback.change_track(tl_track.track).get()) + backend.playback.prepare_change() + success = backend.playback.change_track(tl_track.track).get() if success: self.core.tracklist._mark_playing(tl_track) self.core.history._add_track(tl_track.track) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 6bacb3ed..f7b6dfda 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -813,20 +813,17 @@ class Bug1352RegressionTest(unittest.TestCase): c = core.Core(config, mixer=None, backends=[b]) c.tracklist.add([track1, track2]) - d = mock.Mock() - c.history._add_track = d + c.history._add_track = mock.PropertyMock() + c.tracklist._mark_playing = mock.PropertyMock() - e = mock.Mock() - c.tracklist._mark_playing = e c.playback.play() b.playback.change_track.reset_mock() - d.reset_mock() - e.reset_mock() + c.history._add_track.reset_mock() + c.tracklist._mark_playing.reset_mock() c.playback.pause() c.playback.next() b.playback.change_track.assert_called_once_with(track2) - - d.assert_called_once_with(track2) - e.assert_called_once_with(tl_track2) + c.history._add_track.assert_called_once_with(track2) + c.tracklist._mark_playing.assert_called_once_with(tl_track2) \ No newline at end of file From 3cd3b45512d8ff06dafaecd3b7ef2934e60495d5 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 7 Dec 2015 07:56:35 +0200 Subject: [PATCH 005/142] Fix flake8 errors. --- tests/core/test_playback.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index f7b6dfda..61dfbb15 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -816,7 +816,6 @@ class Bug1352RegressionTest(unittest.TestCase): c.history._add_track = mock.PropertyMock() c.tracklist._mark_playing = mock.PropertyMock() - c.playback.play() b.playback.change_track.reset_mock() c.history._add_track.reset_mock() @@ -826,4 +825,4 @@ class Bug1352RegressionTest(unittest.TestCase): c.playback.next() b.playback.change_track.assert_called_once_with(track2) c.history._add_track.assert_called_once_with(track2) - c.tracklist._mark_playing.assert_called_once_with(tl_track2) \ No newline at end of file + c.tracklist._mark_playing.assert_called_once_with(tl_track2) From 6f8b3ab1a226aed2ccf3a3828a5dae323f9d3aac Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Dec 2015 22:26:55 +0100 Subject: [PATCH 006/142] docs: Add Mopidy-Spotmop --- docs/ext/spotmop.jpg | Bin 0 -> 83172 bytes docs/ext/web.rst | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 docs/ext/spotmop.jpg diff --git a/docs/ext/spotmop.jpg b/docs/ext/spotmop.jpg new file mode 100644 index 0000000000000000000000000000000000000000..88393f0ba78acecfe5e4cfc950e283f585849749 GIT binary patch literal 83172 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%0AMqTb0SPdxGsp@rmd~iaj~K0tizvf(w*Ok_ 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/ext/web.rst b/docs/ext/web.rst index 77ec7b91..04f9a026 100644 --- a/docs/ext/web.rst +++ b/docs/ext/web.rst @@ -164,6 +164,22 @@ To install, run:: pip install Mopidy-Simple-Webclient +Mopidy-Spotmop +============== + +https://github.com/jaedb/spotmop + +A client targeted at Spotify users. Made by James Barnsley. + +.. image:: /ext/spotmop.jpg + :width: 720 + :height: 455 + +To install, run:: + + pip install Mopidy-Spotmop + + Mopidy-WebSettings ================== From e9eb9d61b4fbbb388d12ed8b4028f79a7fea5c1e Mon Sep 17 00:00:00 2001 From: jcass Date: Fri, 11 Dec 2015 07:47:07 +0200 Subject: [PATCH 007/142] Test cases for issue #1358 --- tests/core/test_playback.py | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 5a8c9649..48e30ad4 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -388,6 +388,25 @@ class CorePlaybackTest(unittest.TestCase): self.assertNotIn(tl_track, self.core.tracklist.tl_tracks) + def test_next_in_consume_mode_removes_unplayable_track(self): + self.backend1.playback.change_track = mock.PropertyMock() + self.backend1.playback.change_track.return_value.get.return_value = \ + False + + self.backend2.playback.change_track = mock.PropertyMock() + self.backend2.playback.change_track.return_value.get.return_value = \ + False + self.core.tracklist.consume = True + + self.core.playback.play(self.tl_tracks[0]) + self.core.playback.next() + + self.assertNotIn(self.tl_tracks[1], self.core.tracklist.tl_tracks) + self.assertNotIn(self.tl_tracks[2], self.core.tracklist.tl_tracks) + + self.backend1.playback.change_track.reset_mock() + self.backend2.playback.change_track.reset_mock() + @mock.patch( 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) def test_next_emits_events(self, listener_mock): @@ -789,3 +808,65 @@ class Bug1177RegressionTest(unittest.TestCase): c.playback.pause() c.playback.next() b.playback.change_track.assert_called_once_with(track2) + + +class Bug1358RegressionTest(unittest.TestCase): + + def setUp(self): # noqa: N802 + config = { + 'core': { + 'max_tracklist_length': 10000, + } + } + + self.backend1 = mock.Mock() + self.backend1.uri_schemes.get.return_value = ['dummy1'] + self.playback1 = mock.Mock(spec=backend.PlaybackProvider) + self.backend1.playback.change_track.return_value.get.return_value = \ + False + self.backend1.playback = self.playback1 + + self.backend2 = mock.Mock() + self.backend2.uri_schemes.get.return_value = ['dummy2'] + self.playback2 = mock.Mock(spec=backend.PlaybackProvider) + self.backend1.playback.change_track.return_value.get.return_value = \ + False + self.backend2.playback = self.playback2 + + self.tracks = [ + Track(uri='dummy1:a', length=40000), + Track(uri='dummy2:a', length=40000), + ] + + self.uris = [ + 'dummy1:a', 'dummy2:a'] + + self.core = core.Core(config, mixer=None, + backends=[self.backend1, self.backend2]) + + def lookup(uris): + result = {uri: [] for uri in uris} + for track in self.tracks: + if track.uri in result: + result[track.uri].append(track) + return result + + self.lookup_patcher = mock.patch.object(self.core.library, 'lookup') + self.lookup_mock = self.lookup_patcher.start() + self.lookup_mock.side_effect = lookup + + self.core.tracklist.add(uris=self.uris) + + self.tl_tracks = self.core.tracklist.tl_tracks + + def tearDown(self): # noqa: N802 + self.lookup_patcher.stop() + + def test_next_in_consume_mode_removes_unplayable_track(self): + self.core.tracklist.consume = True + + self.core.playback.play(self.tl_tracks[0]) + self.core.playback.next() + + self.assertNotIn(self.tl_tracks[0], self.core.tracklist.tl_tracks) + self.assertNotIn(self.tl_tracks[1], self.core.tracklist.tl_tracks) From be5bdb8a85ebb546a0932d0de4f4797cad1d4b92 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Dec 2015 11:01:52 +0100 Subject: [PATCH 008/142] Fix review comments on PR#1359 --- tests/core/test_playback.py | 42 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 48e30ad4..5fab9f6b 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -390,22 +390,20 @@ class CorePlaybackTest(unittest.TestCase): def test_next_in_consume_mode_removes_unplayable_track(self): self.backend1.playback.change_track = mock.PropertyMock() - self.backend1.playback.change_track.return_value.get.return_value = \ - False + self.backend1.playback.change_track.return_value.get.return_value = ( + False) self.backend2.playback.change_track = mock.PropertyMock() - self.backend2.playback.change_track.return_value.get.return_value = \ - False - self.core.tracklist.consume = True + self.backend2.playback.change_track.return_value.get.return_value = ( + False) + self.core.tracklist.set_consume(True) self.core.playback.play(self.tl_tracks[0]) self.core.playback.next() - self.assertNotIn(self.tl_tracks[1], self.core.tracklist.tl_tracks) - self.assertNotIn(self.tl_tracks[2], self.core.tracklist.tl_tracks) - - self.backend1.playback.change_track.reset_mock() - self.backend2.playback.change_track.reset_mock() + tl_tracks = self.core.tracklist.get_tl_tracks() + self.assertNotIn(self.tl_tracks[1], tl_tracks) + self.assertNotIn(self.tl_tracks[2], tl_tracks) @mock.patch( 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) @@ -822,15 +820,15 @@ class Bug1358RegressionTest(unittest.TestCase): self.backend1 = mock.Mock() self.backend1.uri_schemes.get.return_value = ['dummy1'] self.playback1 = mock.Mock(spec=backend.PlaybackProvider) - self.backend1.playback.change_track.return_value.get.return_value = \ - False + self.backend1.playback.change_track.return_value.get.return_value = ( + False) self.backend1.playback = self.playback1 self.backend2 = mock.Mock() self.backend2.uri_schemes.get.return_value = ['dummy2'] self.playback2 = mock.Mock(spec=backend.PlaybackProvider) - self.backend1.playback.change_track.return_value.get.return_value = \ - False + self.backend1.playback.change_track.return_value.get.return_value = ( + False) self.backend2.playback = self.playback2 self.tracks = [ @@ -838,11 +836,10 @@ class Bug1358RegressionTest(unittest.TestCase): Track(uri='dummy2:a', length=40000), ] - self.uris = [ - 'dummy1:a', 'dummy2:a'] + self.uris = [t.uri for t in self.tracks] - self.core = core.Core(config, mixer=None, - backends=[self.backend1, self.backend2]) + self.core = core.Core( + config, mixer=None, backends=[self.backend1, self.backend2]) def lookup(uris): result = {uri: [] for uri in uris} @@ -857,16 +854,17 @@ class Bug1358RegressionTest(unittest.TestCase): self.core.tracklist.add(uris=self.uris) - self.tl_tracks = self.core.tracklist.tl_tracks + self.tl_tracks = self.core.tracklist.get_tl_tracks() def tearDown(self): # noqa: N802 self.lookup_patcher.stop() def test_next_in_consume_mode_removes_unplayable_track(self): - self.core.tracklist.consume = True + self.core.tracklist.set_consume(True) self.core.playback.play(self.tl_tracks[0]) self.core.playback.next() - self.assertNotIn(self.tl_tracks[0], self.core.tracklist.tl_tracks) - self.assertNotIn(self.tl_tracks[1], self.core.tracklist.tl_tracks) + tl_tracks = self.core.tracklist.get_tl_tracks() + self.assertNotIn(self.tl_tracks[0], tl_tracks) + self.assertNotIn(self.tl_tracks[1], tl_tracks) From 8c83ff763145b41a190073f899062dec06b2e960 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 26 Dec 2015 18:50:58 +0200 Subject: [PATCH 009/142] Fix documentation typos and inconsistencies. (cherry picked from commit 33a668c6c781284daa803901be6a720470d0252a) --- docs/clients/mpd.rst | 4 ++-- docs/ext/frontends.rst | 2 +- docs/ext/mixers.rst | 2 +- docs/ext/web.rst | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index fe7ef21d..5d5c3057 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -171,5 +171,5 @@ projects are a real match made in heaven." Partify ------- -`Partify `_ is a web based MPD client focusing on -making music playing collaborative and social. +`Partify `_ is a web based MPD client +focussing on making music playing collaborative and social. diff --git a/docs/ext/frontends.rst b/docs/ext/frontends.rst index 50dc348f..1e2ad3f4 100644 --- a/docs/ext/frontends.rst +++ b/docs/ext/frontends.rst @@ -81,4 +81,4 @@ Mopidy-Webhooks https://github.com/paddycarey/mopidy-webhooks Extension for sending HTTP POST requests with JSON payloads to a remote server -on when Mopidy core triggers an event and on regular intervals. +when Mopidy core triggers an event and on regular intervals. diff --git a/docs/ext/mixers.rst b/docs/ext/mixers.rst index 88fd27dd..5023f285 100644 --- a/docs/ext/mixers.rst +++ b/docs/ext/mixers.rst @@ -17,7 +17,7 @@ Mopidy-ALSAMixer https://github.com/mopidy/mopidy-alsamixer -Extension for controlling volume one a Linux system using ALSA. +Extension for controlling volume on a Linux system using ALSA. Mopidy-Arcam diff --git a/docs/ext/web.rst b/docs/ext/web.rst index 04f9a026..48017ef0 100644 --- a/docs/ext/web.rst +++ b/docs/ext/web.rst @@ -35,8 +35,8 @@ Mopidy-Local-Images https://github.com/tkem/mopidy-local-images -Not a full-featured Web client, but rather a local library and Web -extension which allows other Web clients access to album art embedded +Not a full-featured web client, but rather a local library and web +extension which allows other web clients access to album art embedded in local media files. .. image:: /ext/local_images.jpg @@ -69,7 +69,7 @@ Mopidy-Mobile https://github.com/tkem/mopidy-mobile -A Mopidy Web client extension and hybrid mobile app, made with Ionic, +A Mopidy web client extension and hybrid mobile app, made with Ionic, AngularJS and Apache Cordova by Thomas Kemmer. .. image:: /ext/mobile.png From 25bd13453129ea789287b554c456ed62839d3ecb Mon Sep 17 00:00:00 2001 From: jcass Date: Sun, 27 Dec 2015 08:04:32 +0200 Subject: [PATCH 010/142] docs: Provide details on procedure for submitting bug fixes for a minor release of Mopidy. (cherry picked from commit 8ca871cad9b4e22476f46842c59d29fb56d394ce) --- docs/contributing.rst | 8 ++++++++ docs/releasing.rst | 1 + 2 files changed, 9 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index b5230b18..199c6b2a 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -126,3 +126,11 @@ Pull request guidelines #. Send a pull request to the ``develop`` branch. See the `GitHub pull request docs `_ for help. + +.. note:: + + If you are contributing a bug fix for a specific minor version of Mopidy + you should create the branch based on ``release-x.y`` instead of + ``develop``. When the release is done the changes will be merged back into + ``develop`` automatically as part of the normal release process. See + :ref:`creating-releases`. diff --git a/docs/releasing.rst b/docs/releasing.rst index 4c2d8373..8d489146 100644 --- a/docs/releasing.rst +++ b/docs/releasing.rst @@ -6,6 +6,7 @@ Here we try to keep an up to date record of how Mopidy releases are made. This documentation serves both as a checklist, to reduce the project's dependency on key individuals, and as a stepping stone to more automation. +.. _creating-releases: Creating releases ================= From 92bb9b9b77e206d9dac1df3bd005afc289cdf8a5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 5 Dec 2015 22:44:39 +0100 Subject: [PATCH 011/142] logging: Catch errors when loading logging/config_file (cherry picked from commit ede5b8abff6ff269f7180288f98ff1f68c3de1fc) --- docs/changelog.rst | 3 +++ mopidy/internal/log.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 41d5cccc..26a115e5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,9 @@ Bug fix release. the player is paused, the new track would not be added to the history or marked as currently playing. (Fixes: :issue:`1352`) +- Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: + :issue:`1320`) + v1.1.1 (2015-09-14) =================== diff --git a/mopidy/internal/log.py b/mopidy/internal/log.py index 9c40da4f..011a70d2 100644 --- a/mopidy/internal/log.py +++ b/mopidy/internal/log.py @@ -19,6 +19,8 @@ LOG_LEVELS = { TRACE_LOG_LEVEL = 5 logging.addLevelName(TRACE_LOG_LEVEL, 'TRACE') +logger = logging.getLogger(__name__) + class DelayedHandler(logging.Handler): @@ -54,8 +56,12 @@ def setup_logging(config, verbosity_level, save_debug_log): if config['logging']['config_file']: # Logging config from file must be read before other handlers are # added. If not, the other handlers will have no effect. - logging.config.fileConfig(config['logging']['config_file'], - disable_existing_loggers=False) + try: + path = config['logging']['config_file'] + logging.config.fileConfig(path, disable_existing_loggers=False) + except Exception as e: + # Catch everything as logging does not specify what can go wrong. + logger.error('Loading logging config %r failed. %s', path, e) setup_console_logging(config, verbosity_level) if save_debug_log: From 0cfa6927bbc8b9d2c80db1b50364b0cca66c75e1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 29 Nov 2015 05:03:31 +0100 Subject: [PATCH 012/142] Skip tracks with empty uri in `track_to_mpd_format` Ref: https://github.com/mopidy/mopidy/issues/1340 --- mopidy/mpd/translator.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mopidy/mpd/translator.py b/mopidy/mpd/translator.py index 4aa4bdb9..b601fd4b 100644 --- a/mopidy/mpd/translator.py +++ b/mopidy/mpd/translator.py @@ -1,11 +1,15 @@ from __future__ import absolute_import, unicode_literals import datetime +import logging import re from mopidy.models import TlTrack from mopidy.mpd.protocol import tagtype_list + +logger = logging.getLogger(__name__) + # TODO: special handling of local:// uri scheme normalize_path_re = re.compile(r'[^/]+') @@ -34,8 +38,12 @@ def track_to_mpd_format(track, position=None, stream_title=None): else: (tlid, track) = (None, track) + if not track.uri: + logger.warning('Ignoring track without uri') + return [] + result = [ - ('file', track.uri or ''), + ('file', track.uri), ('Time', track.length and (track.length // 1000) or 0), ('Artist', concat_multi_values(track.artists, 'name')), ('Album', track.album and track.album.name or ''), From 6afd5b0c701a1af314bf222fe8568513059a804e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 1 Jan 2016 23:11:56 +0100 Subject: [PATCH 013/142] Fix tests and update changelog --- docs/changelog.rst | 2 ++ mopidy/mpd/translator.py | 4 +++- tests/mpd/test_translator.py | 10 +++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 26a115e5..dafbb794 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,6 +26,8 @@ Bug fix release. - Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: :issue:`1320`) +- MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`) + v1.1.1 (2015-09-14) =================== diff --git a/mopidy/mpd/translator.py b/mopidy/mpd/translator.py index b601fd4b..a76d6d59 100644 --- a/mopidy/mpd/translator.py +++ b/mopidy/mpd/translator.py @@ -172,7 +172,9 @@ def tracks_to_mpd_format(tracks, start=0, end=None): assert len(tracks) == len(positions) result = [] for track, position in zip(tracks, positions): - result.append(track_to_mpd_format(track, position)) + formatted_track = track_to_mpd_format(track, position) + if formatted_track: + result.append(formatted_track) return result diff --git a/tests/mpd/test_translator.py b/tests/mpd/test_translator.py index 65c80bbb..e1ef703d 100644 --- a/tests/mpd/test_translator.py +++ b/tests/mpd/test_translator.py @@ -56,7 +56,7 @@ class TrackMpdFormatTest(unittest.TestCase): def test_track_to_mpd_format_with_position_and_tlid(self): result = translator.track_to_mpd_format( - TlTrack(2, Track()), position=1) + TlTrack(2, Track(uri='a uri')), position=1) self.assertIn(('Pos', 1), result) self.assertIn(('Id', 2), result) @@ -153,13 +153,17 @@ class PlaylistMpdFormatTest(unittest.TestCase): def test_mpd_format(self): playlist = Playlist(tracks=[ - Track(track_no=1), Track(track_no=2), Track(track_no=3)]) + Track(uri='foo', track_no=1), + Track(uri='bar', track_no=2), + Track(uri='baz', track_no=3)]) result = translator.playlist_to_mpd_format(playlist) self.assertEqual(len(result), 3) def test_mpd_format_with_range(self): playlist = Playlist(tracks=[ - Track(track_no=1), Track(track_no=2), Track(track_no=3)]) + Track(uri='foo', track_no=1), + Track(uri='bar', track_no=2), + Track(uri='baz', track_no=3)]) result = translator.playlist_to_mpd_format(playlist, 1, 2) self.assertEqual(len(result), 1) self.assertEqual(dict(result[0])['Track'], 2) From 19726832e7db5ca7b46efdd81975ef3c04307321 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 1 Jan 2016 23:41:53 +0100 Subject: [PATCH 014/142] docs: Add PR ref to changelog --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index dafbb794..bea003f5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,7 +26,8 @@ Bug fix release. - Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: :issue:`1320`) -- MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`) +- MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: + :issue:`1343`) v1.1.1 (2015-09-14) From 094a874055aba9ccb6da2ea9f9d96c54e8cf371b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 1 Jan 2016 23:42:32 +0100 Subject: [PATCH 015/142] core: Make lookup() ignore tracks without URI Fixes #1340 --- docs/changelog.rst | 3 +++ mopidy/core/library.py | 2 +- tests/core/test_library.py | 13 +++++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bea003f5..a7404cc4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,9 @@ Bug fix release. - MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1343`) +- Core: Make :meth:`~mopidy.core.LibraryController.lookup` ignore tracks with + empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1381`) + v1.1.1 (2015-09-14) =================== diff --git a/mopidy/core/library.py b/mopidy/core/library.py index ce420812..2a4ee87d 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -236,7 +236,7 @@ class LibraryController(object): result = future.get() if result is not None: validation.check_instances(result, models.Track) - results[u] = result + results[u] = [r for r in result if r.uri] if uri: return results[uri] diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 92b22bfb..750f371f 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -153,8 +153,8 @@ class CoreLibraryTest(BaseCoreLibraryTest): self.core.library.lookup('dummy1:a', ['dummy2:a']) def test_lookup_can_handle_uris(self): - track1 = Track(name='abc') - track2 = Track(name='def') + track1 = Track(uri='dummy1:a', name='abc') + track2 = Track(uri='dummy2:a', name='def') self.library1.lookup().get.return_value = [track1] self.library2.lookup().get.return_value = [track2] @@ -169,6 +169,15 @@ class CoreLibraryTest(BaseCoreLibraryTest): self.assertFalse(self.library1.lookup.called) self.assertFalse(self.library2.lookup.called) + def test_lookup_ignores_tracks_without_uri_set(self): + track1 = Track(uri='dummy1:a', name='abc') + track2 = Track() + + self.library1.lookup().get.return_value = [track1, track2] + + result = self.core.library.lookup(uris=['dummy1:a']) + self.assertEqual(result, {'dummy1:a': [track1]}) + def test_refresh_with_uri_selects_dummy1_backend(self): self.core.library.refresh('dummy1:a') From f2194e9d5eac09d524454c643f54d8af855f932f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Dec 2015 11:01:52 +0100 Subject: [PATCH 016/142] fix: skip uplayable tracks when the next track is selected while in a paused state. --- docs/changelog.rst | 10 ++++++- mopidy/core/playback.py | 15 ++++++++-- tests/core/test_playback.py | 55 +++++++++++++++++++++++++++++++------ 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bea003f5..eb389166 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,7 +21,15 @@ Bug fix release. - Core: Fix bug in playback controller. If changing to another track while the player is paused, the new track would not be added to the history or - marked as currently playing. (Fixes: :issue:`1352`) + marked as currently playing. (Fixes: :issue:`1352`) Also skips over + unplayable tracks if the user attempts to change tracks while paused. + (Fixes :issue:`1378`). + +- Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: + :issue:`1320`) + +- MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: + :issue:`1343`) - Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: :issue:`1320`) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index eeba5106..a11b646d 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -207,8 +207,8 @@ class PlaybackController(object): if old_state == PlaybackState.PLAYING: self._play(on_error_step=on_error_step) elif old_state == PlaybackState.PAUSED: - # NOTE: this is just a quick hack to fix #1177 and #1352 as this - # code has already been killed in the gapless branch. + # NOTE: this is just a quick hack to fix #1177, #1352, and #1378 + # as this code has already been killed in the gapless branch. backend = self._get_backend() if backend: backend.playback.prepare_change() @@ -216,7 +216,16 @@ class PlaybackController(object): if success: self.core.tracklist._mark_playing(tl_track) self.core.history._add_track(tl_track.track) - self.pause() + else: + self.core.tracklist._mark_unplayable( + self.get_current_tl_track()) + if on_error_step == 1: + # TODO: can cause an endless loop for single track + # repeat. + self.next() + elif on_error_step == -1: + self.previous() + self.pause() # TODO: this is not really end of track, this is on_need_next_track def _on_end_of_track(self): diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index d176db0b..d46bdd52 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -41,19 +41,32 @@ class CorePlaybackTest(unittest.TestCase): self.backend3.uri_schemes.get.return_value = ['dummy3'] self.backend3.has_playback().get.return_value = False + # A backend for which 'change_track' fails + self.backend4 = mock.Mock() + self.backend4.uri_schemes.get.return_value = ['dummy4'] + self.playback4 = mock.Mock(spec=backend.PlaybackProvider) + self.playback4.get_time_position.return_value.get.return_value = 1000 + future_mock = mock.Mock(spec=pykka.future.Future) + future_mock.get.return_value = False + self.playback4.change_track.return_value = future_mock + self.backend4.playback = self.playback4 + self.tracks = [ Track(uri='dummy1:a', length=40000), Track(uri='dummy2:a', length=40000), - Track(uri='dummy3:a', length=40000), # Unplayable + Track(uri='dummy3:a', length=40000), # No playback provider Track(uri='dummy1:b', length=40000), Track(uri='dummy1:c', length=None), # No duration + Track(uri='dummy4:a', length=40000), # Unplayable + Track(uri='dummy1:d', length=40000), ] self.uris = [ - 'dummy1:a', 'dummy2:a', 'dummy3:a', 'dummy1:b', 'dummy1:c'] + 'dummy1:a', 'dummy2:a', 'dummy3:a', 'dummy1:b', 'dummy1:c', + 'dummy4:a', 'dummy1:d'] self.core = core.Core(config, mixer=None, backends=[ - self.backend1, self.backend2, self.backend3]) + self.backend1, self.backend2, self.backend3, self.backend4]) def lookup(uris): result = {uri: [] for uri in uris} @@ -172,16 +185,40 @@ class CorePlaybackTest(unittest.TestCase): self.playback2.change_track.return_value.get.return_value = False self.core.tracklist.clear() - self.core.tracklist.add(uris=self.uris[:2]) + self.core.tracklist.add(uris=self.uris[-2:]) tl_tracks = self.core.tracklist.tl_tracks self.core.playback.play(tl_tracks[0]) - self.core.playback.play(tl_tracks[1]) - # TODO: we really want to check that the track was marked unplayable - # and that next was called. This is just an indirect way of checking - # this :( - self.assertEqual(self.core.playback.state, core.PlaybackState.STOPPED) + assert self.core.playback.get_current_tl_track() == tl_tracks[1] + + def test_pause_play_skips_to_next_on_unplayable_track(self): + """Checks that we handle backend.change_track failing.""" + self.playback2.change_track.return_value.get.return_value = False + + self.core.tracklist.clear() + self.core.tracklist.add(uris=self.uris[-3:]) + tl_tracks = self.core.tracklist.tl_tracks + + self.core.playback.pause() + self.core.playback._set_current_tl_track(tl_tracks[0]) + self.core.playback.next() + self.core.playback.play(self.core.playback.get_current_tl_track()) + assert self.core.playback.get_current_tl_track() == tl_tracks[2] + + def test_pause_resume_skips_to_next_on_unplayable_track(self): + """Checks that we handle backend.change_track failing.""" + self.playback2.change_track.return_value.get.return_value = False + + self.core.tracklist.clear() + self.core.tracklist.add(uris=self.uris[-3:]) + tl_tracks = self.core.tracklist.tl_tracks + + self.core.playback.pause() + self.core.playback._set_current_tl_track(tl_tracks[0]) + self.core.playback.next() + self.core.playback.resume() + assert self.core.playback.get_current_tl_track() == tl_tracks[2] @mock.patch( 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) From aa140beda44a638b0fc1c72ed06af847609e4ef9 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 2 Jan 2016 15:56:04 +0200 Subject: [PATCH 017/142] Fix changelog merge conflict. --- docs/changelog.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index eb389166..3957ce83 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,12 +31,6 @@ Bug fix release. - MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1343`) -- Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: - :issue:`1320`) - -- MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: - :issue:`1343`) - v1.1.1 (2015-09-14) =================== From 75656b9b3dbc4307f2e09687bf60a16636a3e17f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 3 Jan 2016 21:07:10 +0100 Subject: [PATCH 018/142] core: Address review comments --- mopidy/core/library.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 2a4ee87d..240de619 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -236,6 +236,8 @@ class LibraryController(object): result = future.get() if result is not None: validation.check_instances(result, models.Track) + # TODO Consider making Track.uri field mandatory, and + # then remove this filtering of tracks without URIs. results[u] = [r for r in result if r.uri] if uri: From 31ec804d7e8a8ba22420b13e99f27a766fffad18 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 4 Jan 2016 00:05:10 +0200 Subject: [PATCH 019/142] core: Address review comments: remove unnecessary call to get_current_tl_track() --- mopidy/core/playback.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index a11b646d..dd484ff8 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -217,8 +217,7 @@ class PlaybackController(object): self.core.tracklist._mark_playing(tl_track) self.core.history._add_track(tl_track.track) else: - self.core.tracklist._mark_unplayable( - self.get_current_tl_track()) + self.core.tracklist._mark_unplayable(tl_track) if on_error_step == 1: # TODO: can cause an endless loop for single track # repeat. From 456dbe6c3a42344195e9526182c7a6fde86cee16 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 3 Jan 2016 23:41:34 +0100 Subject: [PATCH 020/142] tests: Fix mock usage --- tests/core/test_playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index d46bdd52..46564860 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -39,7 +39,7 @@ class CorePlaybackTest(unittest.TestCase): # A backend without the optional playback provider self.backend3 = mock.Mock() self.backend3.uri_schemes.get.return_value = ['dummy3'] - self.backend3.has_playback().get.return_value = False + self.backend3.has_playback.return_value.get.return_value = False # A backend for which 'change_track' fails self.backend4 = mock.Mock() From 7589d37f15822340fb526eb8388f954bc97c6c61 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 3 Jan 2016 23:41:46 +0100 Subject: [PATCH 021/142] docs: Cleanup changelog --- docs/changelog.rst | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6c878c4b..fbbb6f69 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,20 @@ v1.1.2 (UNRELEASED) Bug fix release. +- Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: + :issue:`1320`) + +- Core: If changing to another track while + the player is paused, the new track would not be added to the history or + marked as currently playing. (Fixes: :issue:`1352`, PR: :issue:`1356`) + +- Core: Skips over unplayable tracks if the user attempts to change tracks + while paused, like we already did if in playing state. (Fixes :issue:`1378`, + PR: :issue:`1379`) + +- Core: Make :meth:`~mopidy.core.LibraryController.lookup` ignore tracks with + empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1381`) + - Stream: If an URI is considered playable, don't consider it as a candidate for playlist parsing. Just looking at MIME type prefixes isn't enough, as for example Ogg Vorbis has the MIME type ``application/ogg``. (Fixes: @@ -19,21 +33,9 @@ Bug fix release. - MPD: Notify idling clients when a seek is performed. (Fixes: :issue:`1331`) -- Core: Fix bug in playback controller. If changing to another track while - the player is paused, the new track would not be added to the history or - marked as currently playing. (Fixes: :issue:`1352`) Also skips over - unplayable tracks if the user attempts to change tracks while paused. - (Fixes :issue:`1378`). - -- Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: - :issue:`1320`) - - MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1343`) -- Core: Make :meth:`~mopidy.core.LibraryController.lookup` ignore tracks with - empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1381`) - v1.1.1 (2015-09-14) =================== From c2c2aa032254481e8ebf1693bb08e5f7a4e94421 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 26 Dec 2015 15:28:07 +0200 Subject: [PATCH 022/142] Handle missing or empty 'port' configuration parameter. (cherry picked from commit 3488e6442de65254e47961edb64d3b28f7212b51) --- mopidy/httpclient.py | 4 ++-- tests/test_httpclient.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mopidy/httpclient.py b/mopidy/httpclient.py index 682a78bd..6be127ca 100644 --- a/mopidy/httpclient.py +++ b/mopidy/httpclient.py @@ -21,8 +21,8 @@ def format_proxy(proxy_config, auth=True): if not proxy_config.get('hostname'): return None - port = proxy_config.get('port', 80) - if port < 0: + port = proxy_config.get('port') + if not port or port < 0: port = 80 if proxy_config.get('username') and proxy_config.get('password') and auth: diff --git a/tests/test_httpclient.py b/tests/test_httpclient.py index 63591f80..30f03d8d 100644 --- a/tests/test_httpclient.py +++ b/tests/test_httpclient.py @@ -9,6 +9,7 @@ from mopidy import httpclient @pytest.mark.parametrize("config,expected", [ ({}, None), + ({'hostname': ''}, None), ({'hostname': 'proxy.lan'}, 'http://proxy.lan:80'), ({'scheme': None, 'hostname': 'proxy.lan'}, 'http://proxy.lan:80'), ({'scheme': 'https', 'hostname': 'proxy.lan'}, 'https://proxy.lan:80'), @@ -16,6 +17,8 @@ from mopidy import httpclient ({'password': 'pass', 'hostname': 'proxy.lan'}, 'http://proxy.lan:80'), ({'hostname': 'proxy.lan', 'port': 8080}, 'http://proxy.lan:8080'), ({'hostname': 'proxy.lan', 'port': -1}, 'http://proxy.lan:80'), + ({'hostname': 'proxy.lan', 'port': None}, 'http://proxy.lan:80'), + ({'hostname': 'proxy.lan', 'port': ''}, 'http://proxy.lan:80'), ({'username': 'user', 'password': 'pass', 'hostname': 'proxy.lan'}, 'http://user:pass@proxy.lan:80'), ]) From 38307a571756146debadd9f130893e8769400c13 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 5 Jan 2016 21:18:57 +0100 Subject: [PATCH 023/142] docs: Add PR#1371 to changelog --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index fbbb6f69..a67e6a15 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -36,6 +36,9 @@ Bug fix release. - MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1343`) +- Proxy: Handle case where :confval:`proxy/port` is either missing from config + or set to an empty string. (PR: :issue:`1371`) + v1.1.1 (2015-09-14) =================== From 3d6270ad3772d464aac334faa27209ef690da38c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 10 Jan 2016 07:41:50 +0100 Subject: [PATCH 024/142] docs: Document model's attributes Fixes #1385 --- docs/api/models.rst | 8 ++++++++ mopidy/models/__init__.py | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/api/models.rst b/docs/api/models.rst index 27c7647f..cc8518ba 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -55,20 +55,28 @@ Data model API :synopsis: Data model API .. autoclass:: mopidy.models.Ref + :members: .. autoclass:: mopidy.models.Track + :members: .. autoclass:: mopidy.models.Album + :members: .. autoclass:: mopidy.models.Artist + :members: .. autoclass:: mopidy.models.Playlist + :members: .. autoclass:: mopidy.models.Image + :members: .. autoclass:: mopidy.models.TlTrack + :members: .. autoclass:: mopidy.models.SearchResult + :members: Data model helpers diff --git a/mopidy/models/__init__.py b/mopidy/models/__init__.py index 7afa2db8..1ea23be0 100644 --- a/mopidy/models/__init__.py +++ b/mopidy/models/__init__.py @@ -348,14 +348,14 @@ class SearchResult(ValidatedImmutableObject): :type albums: list of :class:`Album` elements """ - # The search result URI. Read-only. + #: The search result URI. Read-only. uri = fields.URI() - # The tracks matching the search query. Read-only. + #: The tracks matching the search query. Read-only. tracks = fields.Collection(type=Track, container=tuple) - # The artists matching the search query. Read-only. + #: The artists matching the search query. Read-only. artists = fields.Collection(type=Artist, container=tuple) - # The albums matching the search query. Read-only. + #: The albums matching the search query. Read-only. albums = fields.Collection(type=Album, container=tuple) From 3ff120a0e60f941e16a51c48aea1a1a59e72f121 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 4 Dec 2015 23:38:55 +0100 Subject: [PATCH 025/142] listener: Try and protect actors against "bad" events (cherry picked from commit aa010e03e995d68913a071793f1847a1ac8cd356) --- mopidy/core/listener.py | 3 ++- mopidy/listener.py | 6 +++++- mopidy/mpd/dispatcher.py | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mopidy/core/listener.py b/mopidy/core/listener.py index d95bd491..5b7ea221 100644 --- a/mopidy/core/listener.py +++ b/mopidy/core/listener.py @@ -31,7 +31,8 @@ class CoreListener(listener.Listener): :type event: string :param kwargs: any other arguments to the specific event handlers """ - getattr(self, event)(**kwargs) + # Just delegate to parent, entry mostly for docs. + super(CoreListener, self).on_event(event, **kwargs) def track_playback_paused(self, tl_track, time_position): """ diff --git a/mopidy/listener.py b/mopidy/listener.py index 35bd8b73..7b129955 100644 --- a/mopidy/listener.py +++ b/mopidy/listener.py @@ -51,4 +51,8 @@ class Listener(object): :type event: string :param kwargs: any other arguments to the specific event handlers """ - getattr(self, event)(**kwargs) + try: + getattr(self, event)(**kwargs) + except Exception: + # Ensure we don't crash the actor due to "bad" events. + logger.exception('Triggering event failed: %s', event) diff --git a/mopidy/mpd/dispatcher.py b/mopidy/mpd/dispatcher.py index 099a2f18..175d8b32 100644 --- a/mopidy/mpd/dispatcher.py +++ b/mopidy/mpd/dispatcher.py @@ -47,6 +47,7 @@ class MpdDispatcher(object): return self._call_next_filter(request, response, filter_chain) def handle_idle(self, subsystem): + # TODO: validate against mopidy/mpd/protocol/status.SUBSYSTEMS self.context.events.add(subsystem) subsystems = self.context.subscriptions.intersection( From 70b4851a2991c79481108f362cac22df404fe6d7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 10 Jan 2016 07:51:56 +0100 Subject: [PATCH 026/142] docs: Add fix for bad event crash to changelog Fixes #1383 --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a67e6a15..e8135398 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,9 @@ Bug fix release. - Core: Make :meth:`~mopidy.core.LibraryController.lookup` ignore tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1381`) +- Core: Fix crash if backends emits events with wrong names or arguments. + (Fixes: :issue:`1383`) + - Stream: If an URI is considered playable, don't consider it as a candidate for playlist parsing. Just looking at MIME type prefixes isn't enough, as for example Ogg Vorbis has the MIME type ``application/ogg``. (Fixes: From 2a01760af37fac43cf14a0c29ffd559a19ca5789 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 16 Jan 2016 20:26:56 +0100 Subject: [PATCH 027/142] docs: Update Raspberry Pi howto Fixes #1326 --- .../raspberry-pi-by-jwrodgers.jpg | Bin 52681 -> 0 bytes docs/installation/raspberrypi.rst | 189 +++++++----------- docs/installation/raspberrypi2.jpg | Bin 0 -> 97346 bytes 3 files changed, 73 insertions(+), 116 deletions(-) delete mode 100644 docs/installation/raspberry-pi-by-jwrodgers.jpg create mode 100644 docs/installation/raspberrypi2.jpg diff --git a/docs/installation/raspberry-pi-by-jwrodgers.jpg b/docs/installation/raspberry-pi-by-jwrodgers.jpg deleted file mode 100644 index d093bb8836a30f2d166c202bdf37ec3c06a910d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52681 zcmbTd1ymi)7AD#UcXubayE_CqxVyW%1Pc~OaEIXT7TiN{3GM-c2AAN$c_;s!b?<#| zt(iA7b=K+i^{!pgRl90SSHCX5ZUPwcGGG}11OfqakRR~+0!Yi)Ia*P8xOll+SP4?t zdV0DFvawmXI61jEd$9W0IoLT_S=yPgy13i0{k>!R%Py#H=HY5?2?u-)C2%?EOM$86#t?BZy`(zS9ebU0I5Q_xh$#^lWd*^|iVz&(ZD-{J!SfJI?dauX2f=p`jN@cw<^cdOD1YgmRu;Ap%nrdQ?waaS z5G)J;2q-rHh0Xs9ds@AN^b-K2TwHzK?QCp4Dd;U2C^!TJ_$lP9d>pMjJy}%EEF8?- zEh(g2oLtSEeF5NK&-}L(fc1B5DIkN)%OSwa%fiVD8UBB#|7GUCx&GJS@7(^2<4WzH zIRoL1|A+RUvj3sE6aoPM4P0MI`GK+DvBJrCYrdHEl* z>f__XYG-A^`gcJ8o&J9r{>}M62mjR`>)-bNJ$4jtt*p(w96c%i4yuKVBV_GRc(|Hb zSW&S2e}(w}y5N6x>%aQJtZrp(Uskm+`|wDYucai*|y{-4e8|7EfN z>ce07FS&*Q?BW*y$AlHYn8E>|PsafmY-9l1Fc*>o`uDskB5DJFSDp^}{=eiNf+6|; zT>oDesCdXNl!u)S#b2_Nx+aB%m%I003|SL@FR%a-fDYgQga8>p1<(U504Kl)2m#`N zG$0SC0GfavU<_CQwty4h4)_28KnM^4L<0#xDv$}}0mVQCPzy8wtw0yh3k(6@foWg? zSOqqLUEmlv2X28!$bJY9LIq)i2tni^8W0nR6T}Y^1xbSxLFynqkSWLp*! za)a`N`T!LN^%<%Vsv4>Vsu$`z)B@Bd)G^czGyshZjR#E$%>vC2Ed{Lttq*Mt?GF7O zItn@sx)8b+x&wL`dJcLM`ULtO1`Y-rh608KMhHd@MhnIQ#tr5@ObkpWOgT&o%mBfdvL={9!L|?>s#A3t_#3{sG#787dBswG!BuylH zq!6UfNVQ0VNNY$}$jHcVkOh#{knNCzkv}7UK^{TgLcT}ALSaOaLNP?~LWx5uL-~gC z1LX=86_plM9919H6EzOC0<|A?9rX?k8;unWjAn@zgqDTYf;NkGhK`6%gD!z?jP8e? zhTe!ig?@s8h(U`Xg<*#A9wQ5*9b*aO1``{T15*Xl2{Q(>5_1&u01F$#E)E724@oI92X0h8&@0m9c~tGH|{1L zG#)J;7|#(e9J|z1)(&d17RXz zE8$Nf7$Qa@H6kCPT%tju6Ji`%JTh@Id$JU=uVjbh*yN(*cH}AKJ>*9exD*l;juf9M1}M%cNhswgy(kMPCn+D_ z(7n-m6Z)p^%}**MDt;MH6L8blg?8e5ulnqithv~;vOv>$0( zX%FZK=)iP-bX9a~^r-Y=^ltQp^z#gG4EziZ3^@$bj8Kd`jJAwfj8jZdOuS5XOgT(5 z%&^P?%+Ab(%u6gNED|iW0Z2D}8Y{P6X>^$s_?8WS>99SIk z9HAT?9G9GooEDr}obz0$TryliTy0zz+)Uh7+_~J#JlH%+JRf=bc%FIrc-?txc@O!h z`Aqq;`Ih)`_*MC1_(ud_1tbN61iA$71$hNM1seo^3$X|}2vrIl2-68$2^S0Rh`bRo z6Dbhc6r~U~70nmj6r&U~6Dt(k7N-)o6fYIumtc^vm#CIFlVq24mu!~&BPA#mDD~|v z^jq1tQEw-uv81)6Go{yM-pJUf{RV_--Tb(4GORD@~uwRLly^Zp^{v z+2-dKQWohJ$Cl!jDVB#;B34OO`_>}XN!AB8A~wl3hqhw2X|^YJQg)ek7xr@YdG>!C zR2<42ULAED>zojrOr6@Dv78;82VBTp-nq=UGPy>$Zn_D&rMjKFE4Y_>fIN&m+B|U} z#?-hMy;r!`mba*PmiL{Hwol_b%y-W3#(f!mBYpS%Wc*6}Vf-!p`vNEfLISn|B?1fH zgWj9J?+c;|`Vh1mEE`-Nf)wHqG7-uiniP5+rW4ljf%HT0hn;ZQ@Tv&32=|Dkk3t{w zKEZsl`!pHJ6`2wF5@i`R7R?@=7X1`s5i=Ic5&JpzHO@M2GM+a+HvuleIbktTEU`QZ zGs!P$Cs`@EC50j-D&;!WBy}{6J1s9ADcvi5^RwdTwhZcwgp8+5+suV5$*j6;lI+Or zKRK2;v$^8AwRxm@(RmN~cKOQ%vIQ-Lw1sI!a7Erld&Szt!zKJB6{SR_(WTF2&SjhB z>g59!d=(XyB$e@1P*q-4ht-DF(=~5v+G<&93+nLdqQ3xNyuKXOo7OKjC^Ymn@;BBt z(KKZ@V>d^&fLeT8&RcC;H{0~uX4>W3`#OX=nmbuL%epALvbu4*R0S0b4SEAWVS=DBL9YV<3B;y>{p(Ks zCxM_K-GzonfaEG+08k)kC>R(RXgJ6P1_~Jj1r5Mp!cxLvaY$glQ8UBgbPIu(OvI&9 zFR1V3GIyV$4&6G(ct|EE zCIAIVkmg{ap<(}F0YPCxQ(|$zypd4DHsf?lEI5ay3W2Mi!QqnZ-7W z1jK*`?I=`S5B9<+A=QM!D0*=`m{Jo*Ig)J^A z9t6A#W7Pi1N1`thQia7)hx{P~^?wmlno?4zHNX=>)j5tmTk>Wg1n3VP*Qkt^pO|U*HP6{ZKzVHJqEtf)+&@>$6 z$rJPf8KiNv#`~h^%R$mKEQm<-g~1Ry$c}yj2c!U25U0UWlZ1+rryqGm5T}R1dyJh`C3oqjuWuWA#OU{)*YYI&$Soe` z^=Rj~Q&YFmM7ExBR|uUac2{m%8=%yCj$BgT?{yi(i4i;R>9jO;De)ju4408(d6R}I zdYjyfbAJh>m4JuA2plArKM(qrH<0LMSG9WlX9!hrl7D4tTdz1V(s~~>dFDsrJ>f4C zU-`pux7B+QEs{7+DfS6mivoJaq$EtcZ2iEAW&PxZrjauR zXKiq>)nNep!c)Ro+}rG%!$u0IK$P%}Z^DrEwF z$uGI~`9{iHnoZ}mxxx2p_=73&>icH5(8u~p;%ncNxf(Z3ZTQs=IHt7?H0QZHeT5O7 z$CqqMYAWw$^U8_Yo`xEoZ}o_X1MQ(599gPaXi3^K4N|a$Yc@P)OLG`kjq3etSh+AG z$K4G^0`h|q&)1&1{YdI&8+aFWtR*-LzI+HpLH(H87{etd-akiBp@Xg5(t}L=xkhH4 zh+|iDf9c4+ay8&Kx+TfbpSh`9{kN=neb0a%L$Aht(CU5Es$FJHZ4sHN+r&?FHDePe z7hl4QnjtES&=YZW(K7oc1EIF$Z5XZ}9dR!BeF>6*?PI5ZR!TWj1Xm~r6|O`mX003i zY_Pd4C8}gBsK~#PvkGCKp7GWQyBB{Vli3bkUb+`E zjW$6;YYP$22^K(2hQ0tKjiU++7bTydH}jw-tvk+ON1u-u#Xt}HcaelcmNhbD2|(68 z;Xe}10Lg|(zCOVHublyOT^Pj(s2Mufnl;A=p9T-7iW4#i0>7UtqoudAoD}rhXZJ(5 z`&C2YSXcopxxAq=*Z!~gEjv7DcHZ*Q{xVTfzT%@P{MP~#?kH03BkVupEv^fbEYY~t z)zo2&3lpZ{Y$s^#5c4@-flp5wufT^pdxa5S{TJ_Z7R*+XUzYWxeX%cXxR<%5S5sar z6Y~uUZd~cUB)>YzigSN0l?B~}FIhO#M64WXxLB3u$A5-8rB0%E$3YDlf_q8b9*(M| zY2&aJbW7XqxhavL&a)y$DsF*@1jr4?+D;|Jx!c%~ez^9=pvEmQlr5YfP@;D^AQAc% zIMlSfAPoDF4niMds;fPB-ww7RaD_*!lX#h*#^9Ro5dWanNd z9z_?&iA1`Jm>09tdHN)z|xa4HRKH8h_O|DshWGU z&#r$&?0SbXe(f3;O>5bSrQv`GR2>jtC_Q_3JC=6B3+meRdwk~5cQ9ykF3_vl#gg%3 zfB85)FJo_nNmyZrb)}HGN0iRvK1ka>*dh9{M%T`-WMy@2L&oyMXafl!E3zj{_Ks+( z17egQBWaxvyc{F1`ez+WX4lFn=}1Fmcv9n$D*lVtWBtnAQXyj((Ud}1(O7j zy8CGdDbDY<1+Vv8`8bu0?R@i3{gy&vmLZ!doLqn4ztDy6wpIm7_})kr&za}8ajbTa zy=3D6u5sRbceLfdHZ0xZjQ?$O|DF?yaypmwHUDruB5{|dCV9HxkF?CpRhi6Gw zvh^V*1i2B17zK`3Abr|(J9I1W7!-O`UjkBg&l8Q@UYXPmzb|!L&$WpB9 zkwDA@oe+BdiRkAVmX=QMq$f|EhLLdiNid4&$Lp1Hiq5#x3{MBRPKvGCv-v@!SD-}H z=J6%frR4m1ZHac1drooCC};W3EO7=x?I~SE$x!Hu^hut^s2KO`QG1-JYppo z_I5NY!`#GfXaj~^674az&92zIMYGXf2lL1~ZoE?KWbS%d#o4{~FlD&XZx8yW-0c%! z=!0TX+kBiEa3*2_NW7>Shq@86E$)fi=0hxw|FKajfb;bAv1o9J=%Lb*;^v`dA#_!U zz3_KOh3vI|t&5U!kYbv26m8-pu8c~JXyv;&hc-tkU5T9$0y&88s43hk$5qBMD!|Fun_%P9G(7b>0w~^;X2zbBoxp>qAiw7yG2twujrrwM}_0=*} zW;WG!zpctwGfqj8*nKS{dm<65;Jj6&o6Vp{9DyK=4Ii%3JtnF9>J2$(I=@r=X`eN|w4NM|m4 zZlg}03VISDxKpd?PPeL8v|g#oZ@2xX$2oH=vRHC;g%XM&f*MF3zQ27%`JH&9hir6$T5>&RIXvlY^;sT_peGY*7nsD<^pc*C|0vKw>n@xk zIktK33)Jb7iz%!%;!39wC(TE+wpFsBgo@GMnGv-k>xP&IN;H!tq$S1aoOcoYL%MJ9 zs7@FWXi^PxEloHtvtum%tP5+(Bllo`z1vkKqz111^E6@`pB?yWF31Y7S5-<4q&>>y zW$0qkt;N2H-=iu`Q!ZGf4`kZ-BR0H{Zix!Z&P~V9en{887Tw$@$xHR7tzkyj=}ul^ZZU3#qzVV17Fo~ zmqhW=AkRCe{rUtPT|8mwH$f>kQ;WfE(Gl0*y$x1>MGqTt$HbU_%(@G`eRexFf*9&E zJOn3iyzsFwA4mITvGxn-2*sSbkHy>uR=k=R9%*}jC7=`;b9jCHp_x3}*zvW1x~MFre4nqN`(l8Q7sFYwoF!5-12& z@c)gpIj+e8@Q(njAPSe{V-uklqPHp}c1}s5c)kjZ@Qk=xE4k`z)Ola+pe_Y}PZxVUR_1N8ui0|>31T{QcXFnUJI{@NcODaqj5`qt#%0oqP!gQDLEKbQ zed9!!as`@q54&<&Ues_?ak2PNTH7Il|8|>zhXx5DZl}xg<`n?{ezrdoXq>C@70BJt zoEbceb;fn-OS80+?D<6{#y%8ODs6N=Oh7e(BbaqB5PZ>D_wX^OPQQt>$KCKHOWvi& zWB(NxMxOD%s~B*HC#j!gnSXd^TlE}knzB#IalBa57WxX@#$z4n>`(2iD~Rq0e;bi| z(efufOCL}jcBx|~vbyW*uQHItqa#h*<{@{%lnIomYFBS`;*%yvJ=Tv4sVKQk^rETx zwwi%9E*Ab#QY1DI_s;(=&!SEyO)#q8rd&=y5sSoNjrY zqHlhWcjEq;vcEusev&hq1Qaf8Ad%T?^m|?=&XJK|Qzo*LgCBZrzr?2Q?vn6F{#G=3tf{NB^|i6oO485MhX%~4{^4`x}nkz9&elb>U zd0M}k=^T(z`q5{y_JR&ISK`V%z38uN-Vki>YGlaYW9;NZL#(@Q(J65~>iv1TEY)@q z9oM}4wdaEtC2GZwI+2tP$|PS@qu8P7HQ^UJ-245xD%oF@+p7lL;=2F~E=E%G>^ zTcxyPy<5`3mXsEh6bR<3!W>7-2SUGfUx7h`x;qCZZc%IIN55?PSKzE93}QyXR!`JH z^>U$$ti^IXc)v7v(pJ8+U+{b(opaJOm4=FMC#=F+=r|ZfpCDNn&s4QDJtAJrMs1C|Dw)YZ2ke#pPW3aZE}Noi3)abZ+2gqV-?F|a9emFjVRY~KwdRX};TTr|aW z^yqXu8#i?$ILm7X5`EQ}yT?e|jjfPmuV{#UNAOj!kRUy;YJ zxVn;JXO)|a7KaI%Y?_=HcO+u-zsx(o$2cW%ulM}q&+4kuzPg;*6350TJUijR@N^}{ zcOZ!V1)?D>3=ue-`BVkiKC){B64i=@%WIcawb zbwkT|geQsCTr{=?%eL@sh3E|053RD2-VkZ?*EY(k2_&tt)7BqM)<43Cd?QqGFcPSF z%d;hH(|>usUd*zaE3lQQm?(#aXt*;jMD&<8L)4!9$`_`E8zsU1!H=6KpL2>b#g|Y-Bt3DHn&w%ZGD=!AX0^2Yp=5X~eQA6W zOH;p(3^Sj(gCFN9W%6DDd|`GfyIVEccL%}_?eURReC}MUFBh5lsjTf&BoGIcD@VhoOB{>=L!cj3fmQHRJeia1#StrqZlNbr7k7=8tiJwJKT zH~%Qkc$0=uaIW{c%Ht~SG6C(*_>S!6=(Z;C5tpJR}APag7VYvl#oOi4X z{+`ysz=PNNgGzkijIE|@mOoeu#*X;Ph&y0rtWCHL*r#*c3A`=u5~iE0*>8%G4YD8I zeA`&SKxAn{2;}CVAyl~Of<&5IV_jgb+G6fz37#JIMTfq+O)J;FLNLY zuhvfY;azm?yIqWfP9+3sCNEV?)tD--7EbIOQGvgPth2tAeB>h-Si6Jr0(V}iIhrA z`|*$EIj0)tlVrQ7J37}kJq~ov)sn4j!A*XwKSDT=I71t1bohx?b~4sV(j(uqg~qhN z@4OWohUlbw_1%u)dF}I=P?aC&=KR6+BP$bYhrLwkaR`xyEgH+&y#!sK*n>-Oz(+_I zXB&_S)859QTqP8pxk)z|x5O7V*A2p*t8aWX!oX!N%?6%1q+=!yX?W>c?^^wS}V(7|RI^ zXEOOPqAzs3e6E7LPD8+9M2ji{0>edQINcVD(VIe<-i=8RNs}8=45|EiSju&9ds%#X zJ9iSiN5*XzZ?-44(oGou216OZje6q!a1``u?udn$r@J+hGC7?QkzXy(kcH|8*1;c* z`fmrkZmA}gPWL#466mNN`x}Cgjq%#($nGDvEJ_|^3klzH zDL~W`SQ-~5!nVjB694)e-8}_MLIPAV^GTe#X>~+}OAQF4GSS1>B=*R_e8! zVkKHt2-SPv`D*R_Zpsa%y~sY6t{v%6PIPx%ji_2){<7Rzt?>?hw;xFz878yInYf0T z;IJuH`7Y_=DU!j9rS5Xd08W~p_M=?Rp!=K7E59W^3D8b=}SHSPWn7Y$s0J*b*yGET!2$grbBJGH8 zaDF!$VJQWRQz)m(tk*+55tCEjO5q@-Rni%&dO%5f zdfTaq^fTS?gPslVnVMZ-w|-E4|E`h_(E{HCA+dDQRzo$8MZjFyMB}I3ekR|Rs;{7} zRZ{a+z0L);-%jd_6ch$>3oGC(;jXm-GuZ$ay|gF`gq&V_-7>2_sd&=Xz(HN{`r328_Mq#j>t)TBO?>$3wQ z(jxHmzq7q-M+tp)p*_B==h;_mcPfxag~*3b!Uft{>b9tja$gLhSpD)wsw{F>@_V0k zXXg}HX4jzN;b9YC@d`w@kkoDSDhm)_W}9O6G@ITTL#^EAa`y8faH8K=ZMY)}Y|vJo zex9QHc1*iT-oH-T+{T;E$9FYeFP~iDf)Wf)2>+2nf;#m+r^@i5X1x)&DREuK@n?|x z`A%ZU_0anIPbT8J^Q~d^b7rQt2%cZ+~O^3Se*x<&YTtv{!L}=a4ds;6tT=9uq%4T`9T1OCHqk%%S_( z6zwFW+}46Ba&sAJY;1N(zlN@2Qld^$cx{Xkm@9qf(}#V*{RgpJ+K>AY3s1FL^X-Z9 zFjeg;((1(&+7H{74yTfq-1F85eK>PjX5TAbbYqKnzI*!~xu)$Lq}>`ShY8*zo?nT- zo>wn1znDYfAf#?98P$>1#yGip`?+r~h`^CFA^ZfBPn|?ve;N`eM%!jGKI52kv{lt$ zn_w>2rq_sjp%OTMNgGJ}^TE!05V@_QRdz|iZ5XG#aBtI>pmdpcf26W5S?14A$TN=BJe5OMuTbS5M^PS5gO z(TdTe-sM3h%ZODQhrlRnE<_ag1rc^`CkQ#Y?t-)KdcrWX$e~Y-;QF0MrvS6c<;z0D zoP-D@GCXE$iOl3lDv9!}{0gi$r@J?n&x zixXB5myPuxvP4%UI1M?DYMgKKcalG1IJ?iFQdT42N4>eJDF34O4Ub38>;MPt>o+gj z;y1c6$=%*^8J`j68`1ho>~2-wFij8-<@LR2i)MLO@kCk}z6lnA1#6FBF2lnZl2i~d ziikGzE+RP9Bq7_*5QTTTj`}k_-0TeG)gUC?2jtI4OHhIpHZ1^}T< zpolvp45b3bFD!G+-tXooWHb0!#6;I?A|b@j$TeOmzj=E&w_f=xRV+!pQQMA7_PR7# zhmqBGf<0M@MHM0Q7lT*Xr~~Bn!X%Ph94BEQs`c;P?#3v#&=49-Qvy+AOK1X(<+zO! zV>1a*1;=Zdn@`p3t4sSqM<}m=!FOk@C&9KaI5f!vJ;7G zs>Lif_j-$*V3-}+LPG-rORm)~I%4c6kbNM?FSf-+oALG`U=8e7u!r6Vb*Kf+U2jFtTf*xg|n6RdNL%gs$al4ljJxLtaQv;FRn-tY<} z;|AHb{7Gl0WPIbSDCF{|e9qu9wLdHdJ6`|VrGF>_jd8(CsMCRIsWDVmx6Tx6a8iYy zxvW;qsEVrmM_0$fxT<)`x-}whmkmtv4376TbPH1@p=R;cxfPR|`aNS&>>3Hw0`J(* zw^1loqswd+*MAaTtG;&wK({4jnmvB&y7xEpa9I?6nJYiZu<%P&Et&U6QrOkJ)xy`9ux_^Alw z>@xua4?`Dy5iaF0?P)I|QqeP8eZp}rlAm<+6z&+hqj^lc_%I~N*`FJT6dV^_A=;_B z#d}E%?VAGEFgFUmfEJk}c4Br|a!WtiY;&OKuzr1F9o7$VnM7+Wt6jSHvQj;wLF_k2 z($;tS(qHG6df5$%@t#f^sF>vM*C7)tx-}T6Ocl}e~8OV znFr|)r8NWTX==-c>xcE#N&ODn2;)|6<7lE5r(6JnxhJqJ-vb%c z!vtHpk)z4K)9(M|=&ZG{quJ4zE8bu*F_U0ZF|~Unax@!zCW4KUURG*d>p4-b9`6LchY7x`k=b_TCsNC->FNk9^z@qqbTd&Rdc^WAPMr?dyL2XoX~b&Sw{ zeMJ&WZ8L|gEi+HR&cKS0f7&a+XPf;HPx<@%Vxv-#6DCiNGi0ZW!<&C1Jabw~tyMHt zV!YjVnTXfu)|z_k7NV<8+9^A)csXIJS!f;%|CM0WOVd2gGIlXiqNmbUY9IVW({RIF zvg$NyBeue!ZDP7H8dP8(q9=7Gh;+@uyG0Xgq~~@xTc3@8=T+-qE|eAioL-hqPPV1O zg?=eJ|Evt_Z-aK;l9B(maDh74ZE<_^O5g!4`nLKg!?s0d*^bZ0l&dZT-PjIp|3DC; zvAbI4YHA2L>9{vdLE<9=D@nUC}E<8NUX|{{n#a7}tGMD9*Bkx0IbfIjesYd%J7z>Kn-qL)h%VPSC ztY)FZo^GWC@1~9<6nc`2AZXIz2bEhMCzzSZ@IuEObF@K~wo0;U6?=CMZM5o<9nIHgCCYZ6|bA?U4p(rkI_*(eDkO1ewsh(Z*^we$g;WNUQfsD zST2{Z&e2xxs;kk+ETpY0Vd~ovwIrH(HxTj&g*YLzvJ=ETJ`4IS)J)!9TGaAZ<8S-1 zFTE4h!n~5LpxPbUWTGM=8wj>P*4#$UQs%Fp&RoZ9`FbH>wMS8klfbc-guc??%aoLU z`Tb(nw&;nXOTnanhvuYLo#}AC@iZo3^_EnnnM=L1W&Z9J2=y`&)yiE|W226q4n@k_ zPt-CAUxNF6z7u5%lju@z)K^ps#Fz##P9Bg*rV#~g6*qQwY0s^%NpQ#YW5On2?$M30 z``=b)C8@>lzU(d-1Wm?E&E5D9K52H&?6#$ztSUQvh>cHSH+2_N7fo$oF;=pwSyj2) zb)BL{#KBOqr^Sksk-+CS_p=Gw)i-j8J7Lk9Q;w~(f)?Iq!g#!K`SQ5cHE;h)ogV$$usLYX*(>3m-n#Sip%&tJ!V!D{v%Q_S}e)lx@eq@RWOBY`@W5ZT6M1+{XC57Iny1 zcEktG<$Z1Uj4QN-y)}*j-dkbOgr7<{EKY=$iCKtm!ji+Dr1+VZzV7%{EWHBQEO>I3 zGmCo8z95?ULWb)b`=_hXbK&ZB&3W^mt&gq0XMS5u#qk&JvRN!|pcIouRPne)Bf=Nv z9cl$&54ISpmE9$nGmU)1Ud8SpS34?Ox~W2=a?jNIUeT+&9QHfviqkxoq02*aW(JXA zE?U@r{m}fYZ61M_RNj02S=P(|j(5a~GYW_t3E~^@^IBFBiDV^{q)~DO0w%=q^eHXF>~9eN5;hKQrc9Z*R9@BYNj6FyGXSRv=&y~^n zeIOX;a;=(X-aXCytF_sa?3jYZd;Q^FFFHZyhRY#%W#f-6&u-cp18+W6YkaX20I9aa zSY}5YwrtdR{u&;|DAr%$nenv*Q?_ZxJ{h!eJ$?iAwNW^u@ljxj*nbb_kM*ot%AJG@ zQ`cMSO~M|lMw4oa?Zlfz*hsHk&?zj`Uq0gFlox)N#uN9Zot0oS`I`KBVO62E`R-TR zx*8!%7gxYc&ZNhEvQxbAfDiv9gS+BjyL2UYrXwG|;K~s>;iJAnXVRaR1WB^f22SGi zE9jP=v1&#eCWlly)5TFcDqlL<;I=2X*@O*#pq?%OFxd&fER$bk2Ra;^jKJ%)~-CgOxY0y<}^~ zzv!^f!vi!;wkct&_zlOJDTACLH9jlliwZmckl;cw9$rh@oUxIVb)}1MEnVvu)-i(3 zcS?+Hi3%2D$n0yOekkc!t}hiuM^q~FCrOJ({EI4xv^oUNAKa6;TJ9=%iJBLa&Cbv5 z(=o+~0T?YxFvmgjFmH}3j2q%EF~t=qet;?!CqU!vtJ}B_Q~w?N*+Cjx zD>`jG+P7Jc+EJfU|!3r^hxFH%#4hX*t73ibcfihvfRv7p=j$W z{PM(ZNg|YSSNVr&)ohvK+@UlhTWRtOBk_UdyEC=+B-;%XkxM?2qmC6N-c@zq8$_C( zo(l=d&5>I4y7y+;zw=BPJCJ`k%@3z>kbWW+zb)q!1apB=SnCU(_Or_29&5zG^p2h8Tyo||ter6J@`+2Cto0ho24E5^@-hl2RZQ9iR zvQ@^oD48b3)8)I7^G%J~^+D6Ui?su+dKFSjFL;V|HFav24p&7aNg+3g;Q&>eEsBmc zsA872S>b?g?)r1?_iyu-WGI>08Xza{9Ndv_lo7ohn6ZOu$AZ%989^esMULl2Z(T=T zfqAkIoq+BgqAFT=Aw5+++nj8!Bn3^>-XD&{l1Yukx zyGG^vy7Mx^aei-k45teuQrA`%UaG)d!_jVKUZ!RiDa5+bX*q8-6hI>ZuB1jQP*ie_b$4{$#1UP!u7S zktw#0e_KUhr6;kghhYOfU~Krz_4Xv9e0y*{qWQ$r%l94_5hJ^f@* z1rN)|%Tq&3X&Y^t)L|KoQ1}uh5J=nFSX&)V{!1?~rw7M3B*GLHX^|;Gkb)B99+M$j zv(mwwVI{dODm{#*|0^21PZ9Nv2%~sk6#Qa)!-I~a zQ#TdsPJRVE>K_ykd_AeXQuW+7Tq-!-j(3)4J%589qz0-DvlMz>#3A^iGeaOd8qrT- z-aOJYw^h}-a=zEGA|00r&X!~+yejz{>rtB5D#UzvK52iZ-z_Fp*LyTs#*LSul&xV% z`%Gvbcqi)GsY*C!CkE?+&io!T^D=)1j*&uZgy}HPhD9ZzZO+Xv!SXC9>0ubFf!EV{ zDiR>4#34tcW%YpxoRsc^e?sI8^n;M|;B%yl+^qY6Pay-A?yOvnF(09}MuO~O`U$-E zZLLRRG1|=KRY)tJ;^U%hWOJaD(#|A)a|_xc->k3>seqO%KzAm?Wa`?RZZ5;gHFvD3 z-f04xZJz<|4Uj^|u~n!rD0_1^D=EwFG=H-?kJ)V5gTLz_q7qhzaf(-m&;JTAPzCL3 z^?cBADRMQzl|?aIRa&q~OpMps9UU(Ft{Q~RAL-T87YiM!1_N7X?p_H-k`heJZCkxx zRQ5so!&-FIzf2sHJzccD5%DEXx$LD>L9l0F&;ppMvRl~Of zkEQSWXFCXG9h{**B;)^S$r|CF3g!e1sv6*Vwoz}HE}qf1cobFWiIE$bq=pA7jyksxndq*3af~YQugXIcx?iVesc^5TwsU);5YyfV$ z&B|Due>r3L#HhkRXzYlc0M#NA?lHtj9*#Sjq{qkm6O%vc+d*dRL?;}{Z>xPr1fg$ukm>qhTg^cYUQbN-W!ksGqm6&gwbV&VRIuUZ*B7Mnfs za8h4V5-V!su4Jd$;|Nm={zSNMS&Vh zD(2R_XIjf=hb&d>1+BBadA_(6Z`%|s5xS!?@c2nRn_=8^3)=qB4c`E;;5 zNUpT8(zDld=VduXV7`-h*5fhS^d}d&ZMVe-yyI-?$!{7c?RRYw)gT%|+PxdiTbe~E z0Yyhv>;*sXSuK;(pY&1MKj#4uln?qI!+U6^e zCbIDUX?Xo>-q;1ZuhvbGr!Vqonn>?VZSu~$_>ecP?YfRm#@>gZB`Q_c$t6w> z*U?s`tHiGZB&RVtXk1C(^%p;Y?O?k;K}Ax+aEGM>ym!>5WIx?9kA@L;VkK;Mzo$;P zyVxYJ+a*faP)T!jjcvY@5spyV);JV^^>9Cp zJv{$;B&zQI(j@5cc{6#3%5mu6hpf7fdRZwSMSKA%swYE0;>VG0IWQK}r^G_G3=D?k zJeO=s(CwkN279CBJDlM+Rk&2{2-Trt;l3y#pxrBA)w)SAuE43hnhb=<#UL3YXGIrkh@-z;e@!OVCP3FxdM`@TDZ1h)_AAo0z{s$C0F;uLftgU3+AxJILcjp} z;qq^#`CVP7n)@{FVLk={0ZOi=QyrVWuUpHB7I6wKk;Fq1p?rkHGec20j4m+A8Pw{*P7AA%bcyc~o;WlV$O@`}=ckW6&&32xvqimEH zI$LEI0ldd!n;~O&0=+l4JS87i?9<$)V8}>2WpF46gvScNe@0<`ua}>Cwl%Zx?yWtj-;#L;DJ-b4@lF-nG;R2;ug-n|cI_uay26 zPv-l`aQ^cZ5Ocg0GuhR8)C(Da;&={G#JM$9?VSn1Z!u$Uv9a9ba>rVNAD?VV=Mi@*X& zZ>cPDrdsFj=N*%HF5|2?>nJIbzWw4`DgSM)OD%~f=_5eO71E@e(zibWg4%<}Li0qpM%P>I4%-@~Ar9FeI zWP%&GJ?dQCFL9yi_se~&BWlu=Ni9JqDy(w8Un=18I+93qa7G9sv^F-HM7QlNye%+{ zj9bfvhDGqgo>SMv50=B>Tcu$nlQ}YCPF!MSrnd1Q6Bwisb8_<(A$h$6bj?^#Z+F8` zTQ$|~x?D_&b#G%T7d(ff>_JBTIfln0YPz}BrqX^Q%^rKlX#5Exxr9XoC*&ZF^q&E?cbnUR=?iVOiXxya>Fo?H!z;Pt4#ffyv|?U5}A2pFm( zH<*M_tG}X+htO4oEEYLpc;lMdVaqHsuQEtI2>yqt?NROG%7901wIHcarDr41$I$%_ z{{U9t*&gGD{9Q*xC!Xg=nTyE??$MsCzTt73re)MYs|u{{R*SIFb|S z!~68FDYkNqu7s;8$Y5|+=xRlcjjHh_hO-UD&Y5=<>en*a1p?(iFm*d*_am=yo$9fc z=+&!MdpHLp)`CFmT&*vy?vY(3i9?(#^8l<*LBB)nYD-4SaN`wp$}@thcx;gY5hLb7 zf?1E0!h)r%pe?lQA;whmeO0)P)qy(&kP zfXaE783a@*L5y;|R?(m*kh>Q@Y||mRkl?dO_}C(-3MwwN#S__-f}~`0BBGdi6?r+i z5Pntj{WIxZQH+WSQl}e(*1D+dDrgbnA|w0YA&VJN*PH5neXFwhkT3;uIxxYmf+Xhg zSSTt!X6Og@`qeeoyspmf4VvO$JR>1nyYnwr<>LGP0jr4Z129&t_i^OQP2vN7RzZBb~-#ZR}Xb@JlB`<3`uox+|oC&><6&GtE(mF=r?15Q2zkL zakd3q>9OW?^j+&jlIVuZmGXe2pcoXnu77%3u|uY5%lcCkX1b`UNP)mp!NoLDRv^&P zNkFZPg`$#|wE&b*DQW;xQi@Lapb9IMz04OX?QbedsKpzU^dqsVZZ@9s+lMY@(;TJZ z74V41%vb@}ZvE=Al}C~#Ezx~c%ft|W;;-0G8;h2Or$z(HK<)MG`c%yh?ez^7!ty}A zB=+eeh1>?mn;7>7v0Ok!pW^=j3{9{XQ9LK#<{$o$s?JR%CH5sMbj`l5k0zOPksLXR zfFrTxI5p?LgnU5_kKtvN#zrSTxX;>|~g=Y>*hcm&~r z>M}A9*n^*~d1itV>+qwG^r%?0ntkMqKo}@-l0W(lXF1#DAGu_!PovU!&h7{wNz{4b zFp?Q1Vljitz=C~hqh7q9PyB4*>saI~Ykd^rMajc4Rv%8wSS~u&?Db#3jvqGKjkoPP zRJLY?W&%|U%^yy_l>_l-ize2%!-wzqT2Sw05RhC#4>A{0TbmO5C5>nsu+u z8vY8sg3DI?S+%`W9lgAQb}9=JrDO!-?kmY;<4bGL!Pnt8wUpgMc+y2OFe))8F$H># zrx?!ul~rxTI<5AruHH|3a4v7HNV#++JZu0kz+e%9T)4Z3qR?&Zj8pj5`h+WZq~sK5 z1DSpMAH5q&m*z!2{{SOJ$NX7!#J75V+uO7PD@Y=76pY}Ewp*s%Mt*~O$>_+&O-KMa zOa*W|<25pEMReGsmNH{blS_FghYZc(7EnnU2cR9QQhb>n z#CyJLnXV)$?FBazrz6=0*42>$>v1cUsIRh#>JerX;>d8P{LvG>Nn zpF#fsTA+sdX>M-A&g%=YZcc20lh}6qRpSj1*?+9+he$pQD-4Y>WtBOO%0|a-hN|v0 zTc6%RYly=hV&9vmuWG24CYn_cC(L%wdfMq639RY{OUWXV%GD$1XJsuT05{}7a>wg| z?^mRXIT)K^n`sU@IK_IO;f0Ia>35H-Ky6`p7HB4)DkUI)hspC0-^4Z^v}iEsHd+nr z`enMwtHm^FY`3kKb-^z#0OVY30)w6UnwO>Di4EPw&eFRnl(8qoZls)@yKFjRYRYoG z7*8tWt+s9r{&fh(n+@d6biy{YbWZ@L=fNnyILX+YU=LmCzq8s%3u>B*M+{8M9jtkB zr=bCJfCoy*{@G{<5k@8m!MTYjK7jT6_NHq3^p4=?oSrq5ucCZTzxhzWYB$ z`t4XwF0^Tf8kDauMElEEg<4Kf(1ztCbze-5z|=V*j#bNN?OO|RWALJAylc6g6!M_N zXc%l5_dE2g_5PbBjl{6rME0|xB1>6vN}TLfv4%s~`w(l2N^yLoPm_WX+#ZTXLb7zr z9>ckcDrv*l$!bCPoH!-)gRq&u?;)MI^D!8)alh zA(Z-pHx;KCW~DXJ!;d8JOOzcKyj={sz_e{ymrS;e4Q8^YSWBZ$w(D^$XTW{I9-WP8 zamDFNM%=Rb64u}I{1od1mogiPOtQ@(^E;Bv$K0LGSu6lXJXYnw=D@~!W35lqUM)Eq zOPHcIWqDtm@`3IN?^B?*_|x|&N%mysCWvay;_S0*@<${9vH*%{8=D$h`k-YBQ|qAnJA*hO`@oHDnU zDo#@vAdF*v^}ahu=HVVYJlj&Yg7;mL69{HLP(j8{H_!Gz$kuND*WqsDg+s7bzyx8j zTZ=0R2B_ClGO+U%Bw>j?@!M*R1-oi|O6wvk+N{18G>iMRVm1Y_f!ywU8r2vj$n)E~ zdugohB3np|>_fvod1OP}5;kQcvFLXPu5&CN8;G-&mNv)-u@&pQZNjak+;gO8u{IT2 zERd~&c^!JGz{n@4-m&wu8#(Nxjwe|Le|Vsjeu32W2e+XeL951fqpBGtjq#Zd^IVGd z$~fgBD}bd4Ykek>V{Iac*9ssQ2cc34{{X+|UTbx2ExUs(ZlPNRfbxUe zdb=D-&8Mt4{vnbxm6k9(+Y#m;dbE|N_d651Jr_&Y(Re)=4-}=ejTbru2*aUVc0JVk zXT3_{#COwNTt-#=QW?+Yd_NCoDhSv^*!SbKA?5GC?iV ztMi{LDA;6m2OwaB-m!4wB}Hc*Nv~qc`qDXVnj3{zgOn~xhQQwd4?s&Wk0Dv`TR5y; z_Lp;@&Ei>uHk{ndW9H{*g5a-Kjmac&`wa}~5}%F%$N5`3q` z1Y@TB*}))url{{=jyV}4oo8IGa|e}~dAUhe`MGjgd4>aE;Nq^CNY8>wPSJ&e^4;JQ ziB)38?8g99DD^A7PE&2ewo=|ro>4C+kQ^4k^$(Pgk8U&p#IIjLnsTcyV+hNI40Stqt=;bI%Ha4X-8?Pj? z&ndbbr>aJd4gqW(`G~**xI+!pGo{|5>Lar zFG1eBmB__Gif{nMO}B1rR!r`L(88Rg;2P@`m^ILW+PSi4dWy4Ep>OT2wLLmp3-1`) zNJC~RIb9uhS5xXLcMkEstHKElpB0zX^cGGimq1WI60{?NI4`% zRFWwNB$2xFE001uii>e%(rS-nv$?g@ETX*A=ZfamQIwJ<$UpV=q%GyX=ef~ma@^jgBj4%s4|?@p3gZ1EjI3mrOpwQQ1_jNepW;B?oAf{*U#h!yUbav-n~-AyLgEvt#be zf4xdYsAbAr>PMSalz3f5oF@^F^&YMLDVNhcYZa}ZEOW>RkCb_0QPuJ7+tlOUqa8x) zPylFpb04{fQ1Pf@Vx?^8rrw(;O)hV^l$F3$e|VO6Q+ z03W%ln?>ZeLEMa*u~v2z^%T_`*JN6hh#aDy+JGr)#Xt&BDL|zaBp0*_C>5!gQAc<7iHgV&kVjEf91pD?AmPYwm=NMeh4MEgPgC^-RXKXNlUlM?v#iIZTw1Nd zj|OK6&&`4fI2ax3)rJWNw$*_AXGBgIg6;Y7ZFL*-Ajw{%Khm@^aV!hOHac{%u=x0d zkje*?lDxnluJulQWYoUIrO9Z{aD?q;_<_aNv&_B=D7jS`%HWLTdxxw3CVPErg0+EX zDol-SDrHV^%DKV!=~+o6ip%llD|cY0RMXj2Pv;>HfBdmb{4u__w@xtA^((u0B8K~v zwDK%!1BOgwpYn>N8;_&gWXo#($acD|_x}K@-2(ecnmDc@k)XyrgO!+!dK_SV>QCby zqx&}h0D|?1qgF<~jUBV{t8zX^*c$U)4~Sv3{42iDWVlw;Q(0wI=RC;~T=NdZb0Hsk z%W=0C>-wF9dUmIJE-bB?4YX>CIe8b95J1mE>%C^Z*V%r>)2SoZ_|J*$xJU6{P}M|D zbsToy1kob}A(=)2C;n6SuQG#-&yU8@Y4)y`dhF3$;op`>u^~9yVh=F{dR1-aqj7w< zvtL|Ib8{g_X(nRqe1L9GU4>_C5n}l5@ZJeilhBRvQnuRrm5!|)HJfF%jwnQnAoB%e zJ7Ar*?0XPvTf!Iiwi<~ViG?AbPBHkCKPsWY0RI3%k9x}M6IKMq_E(p& zV{xvu(@kt*M~)t4iPVr-d;XPEt!h^n>I~Ql50w;P3N)JwyZuDPX$+Fc`3jM#WOR$G)J*4*N48|dQcm{k8Z?qffmIY`iSz(( zTD=#5?oGYL#-?U~Ak3Eb@tNmUKg6SC{{WnirD<)%c9C*vmZ@PAuPZccOpZUzA@dRJ zFg>bFRu`Ig58Ynrm(DDl6}f^YAjXZA%!8rMdT&&DQ|RUY0H||y1ajev-W$^3BTmv~ z8nT}`ykP7Xvj)ZhBY!9utBd2P-pLzY-6z7yg6g9Pih3{^%kAE=8s(gFB5K+!kII2O zP|wWpwg3T-bU5<1Kg!rR$!)}Xr17nml?%1Yp=no`DkeITPemT&3ZC|LLRO2tS5CKr z;wFyn2xDi#W+0Ur8?XoT_4cZMB-Bow#D*J)QDA6;D#kFN4DOrsIIeK;zNM(?S2~8E zx0BpP3quqn1ORWG;OD-;4_x-GZRZWy-$xDH?Ire?c0pG{t1=Ocft{8>I)mk5_9Bg| zvK?c0@YC5dfI+WAgNRkhUf*Az2ejYiDh`Bt%qOZzMNyZBD_c^L7yT^1$;fmVE6fDEx5XN%D1m`4> zM%d0V+PSh?exi;~{{S%J2sEffZKuvwX(S*((g(y~3_eDAa&kFX^9)ramg%>P&~K1> zX1hxounPYGFI8`P<#IXIxy}!;{{X+zveoG3oSWI!*u`xk8Ld`ZV0_UDIShli3^&g8 z>6+#4i6;aurJ~#3w!a4P2{?a5FFS7c7-YW9k!(%7L-wS7iFC#U^ zIZAI06)RB(r^AYd#z7FyB1nk?xJ*XBQE@>m7!6e zFyGCA^%c(Q_6;#v@2>vwJNbJtV-M40Al798k2vN&CUsc?d8mPFnW7HvauvfH31*4GpoDKKKZ)&lz;uzmq zc`RkVxHqy8_R}PBTa@K&BPx&`vPKy4;15$+MvSj~k6Mvoc_o~LsOF8fkv&JZp{uz3 z7y4;0Nx$PBB)s8Un`N2cb7!7S}LB3zPK39)n{kJGsATo-EzkgfftWE z=l8fAk+59yGY!FQw%)R;c2g{zQ)@$OD$V z;ch0izwzza?WICMKp`bTy=8(axtVGN;i;X|GQWn6$VOPYzeZD%f;6$N={h zqTx%z+(D#i@=4*@NqHZRTk`_Iwn**QrBT@M{ngf~99n#4;1AvfTr={n?%AgfTs(w9NupU1ak`pEHBMFdahm9&PvPM^5^?%copLG*&Q& zl(#dhqsc3#&Cv3x$r%G4nW~qzcw>TRnI<<1WM)7Y1e38`l0Nl}A;hL^oYVUg?5!m5 z=V_MyEi^)AmRoqsD8TucbRx4_9=E4)RF`%aaEm(|Sfp6*f}a*w$B=T3m}GrMcQv56 z)HR!34ohokC62=I2>eMREWLAtCvEZcs{IQ@7O?|lE$*h~4K=jQ%2@_*a!ATO%|xPM#WceA{nFZe5zLM20#A%=Naj*LI{HL{mY)b4nG!&2}H+svH) z7e6uO=o|TG9dK*Z;EgWym}0$#2@+@!Ne}^}K2fms9+e{A-Wz*3?O};l-ZT-cU~H;7 z0a@q9@-23EVwWEJJX^-OwNWGzG;BvN?#US*=cxNtCDws$CEMIyJodH>k0g&0c#fg-k2ZVn>t3_O95e}%eLQ6KXpefuNG@&UWrVO$e9)bU`g&I+a&dfGr~Lg*r;Sg2 zqs(+|Lgv~?C$%YUsM{7$LjM4YyNr>NGC{${HvMYaO-gNM_2#&WI94Q7L{%PD!1D8B zB}O*RNCz1;bE#=IcXv@;MH4O`nrM`SGJ1gDmA{wMtzvk-?p-S6lCj&`$%P&g6jP4G z9G-7bNj>sKZoOR`yd?>5qU~i~amv{w;YC?n&c7g>MHWe z=rmw9(GA%D01>HZ(k#@)Fkl$$r=UISsXx!PQ$u%5X;WjHeokz+Rz8(lHBu!q zgXvuS%R4o;K;~HBje+0Sj31XI!{6wRv=H-n))Zf@-{VR&ttgsw4l>RN;84ISc9lE9rrue29PgPisc3DGe~0& zImbXxT3H16SDr;t+<{kM4xOs2OpTgzAqT@yKt86e&~H*hI!3+gvWjB+S7jAsinah# zfTaSTinS0h3{v8qicmjFk_&-LNkv?sw4Yi96a!oWjJ=I{F9_-r=^Q7;aa-U?rQfue z$sSX-fBI|J04#YxB%Z>&XH5bx#E-;126>fNQ?e6VM$890^7CPRPnnO{))|%2czc?T zJi0o!;RVj5sE+MumqkIyC0G_2`x?jUTFi0i--cX8DPSXxF*ZER0$?`7ugKMeoADoA zi-zsEV%{qy)BFdwk={OMm0z16l5jDdv5u9T(eXLX%2MvbGn<&VWucEK6t$Y$y`+OYgdFYP$w zzm?q2EUhb-!6zj3KD$*Eix|FzBt~e%gUY7|KU$kD(iD41bCT{C_0RXImEDKZBk0od z86|aAkH(F;Po{8xts2=s7NB9W*vj-BxAds7T!?KN8=2K)amJ(IVQAFqOF=1*I8i^+r|-`DKbg9`&NG4i{8u5SeDN* z?oD2496PG(K|T`Dv~eyZneiM^kMlXe&)+7rdOrC5 zQDy;Mkl<&r=j)Jn+Oo~8-0N1crN!*80_?FfoDKV)=iJg-qM~xf00(t!Z-f5;eQ94O zY+)GmaJcavMLFy*XQG%#w<&30=+lB}J_Ne#wP+5->=0%JS&U^RU zzH2+E@l!~?7M~9yE`ND0{{W!v_p2!U5g5igilWjjD_4qDy3ycil`|XcyrdDjuwnx# z9YNZyI7-_@vf>rK)e=vG!GdcvkxSalNGjXs#EWBsHysaRP95P+CDU!;*R`!ZZ?r8! z5|WmgNl;XjU^^Z5#z#m- zdyYKQ5*e+yQqcIJc}?Zm0x${PmM}*CHU3 z2W)NTJx^`1Q&eNk<+nHb8M7gddwCi8CP1!|t6`DHLVNYA3;rS0+;hb+fx!LbP@7HI(3_h)t$* zZeC37%iMZgFE~*p$PUUS;P5p(~t>fJn&Rw7O-^odukic5c>pCLwP+ zc-goloQYs;V06PN0As%OT`r?2M$+_qPGy=;5WMk;rg!D#XAOc7VB@EhXMN3cs8?r0 zjB4946Dva8l8$93Ao~3(K-3c2T(c-c`4D672d!0KUQHa5+68FUAC-fESP*-5{VR-? z=akPMi1y^+WRPKx)Nk~zwT}BD9xpA+qR#hPc}ZJU@K*=-jxy){s;FeUGm^~Pl1@pc z4afUeTx4UtYZxf>c0v(`qKC$X*dhjII{+$D+XyavEADu<=m6{PdsK@E?e32#Eewm2 zik*%(Zf>Wi_O7XxRkyq#C{k1d&CTvN`MT~;+N@lScW%(gK||r^%Ed?>I(>brB#KAU ztmW}+>tZSz)PL$Z);gMvGipaZ$sBy9yy`w=K zGFY{wcGoX{!qD8vVTMfEWw#7B1b%~j^{AT7x?No@!XFYT#CW&>l*t?Uy+?7@rs$fr z%+l%+MR66Wy|-v)MHumv<81cmD&FT=zPy?ZA_j@{TT-Zn;_z2x$2kKzBXgf!@mbXv zWsGLMT4pYP8XK}CJdT6_W80^2NA<1O0P1%(6JAPBj|AVmTbXhv@j~EaljlL5(Xzez z({)WU8-}(ibEk$}g$JDEciWSCw|y_+qEU4)ScGAeM7-|F*D8Yq41X3eQnqo%6O641 z9;0;Dky**%2qRLXmM|EC+}JJG89AkE?LG&$NNlInwFVhB?Yw1Lv z00WGD`(zrfNl7b-*@GH$tc+WYdiP9 zH2H5PcbX`U3{h_QBW5J*tJ|A>>idp(-FTNuO+MF3xQGc>Q!T>+RE+GPbk27<=~*jz zB(o984230(95Va$9^+x{JpF65PeZ2M-u zlGLoN^(YKiOP}F!)oahVk3wre(OyI)w0)KFkbl_M6qND0m+ExJQI^s3MeVKDjWL?c z2`q-@F`qB}`u??NW2%d|yl)g0Q-Ylc`ij}{2M$eRD@$k1Bt3aLupXn-e>%Y4=n_vi z#AvET*HZ%YfAstNewEP-idvDBp<9Wlv($Iz2hY(#?N>I}8C8rn3G+ziyyHgI)J@59 zr6x>*jkZtw`&OFH@Xn6%xI{g7{#9uzGl{Q?;1FEx8tkLir6jw6eskN>vhZpO%pH%B zK1HiIE=~dF4K%&I1y!R(BJo?${c1&;39g|`b#eV_swsEy$L0BrUGU%&>T%Go2Jjw?_-nS||w%F;{r8zz8i6DB=(=2P2AP=ob zB^Lv|M7z6=6(yJqRW7ZsLu`%b0~O{xO~&setp$;3W$EQTKBl7YNTjBsY1_nBJ8n`;wPXU=C>LygK22M zDn+VZuV7EY#gBcAhxA{qQf@I`qmp@C^%K*0H;CSC5*e>K{vAM7k0g_SQ2^ z3I~xFDi2KhpK6BGRj^?H03Kx2qu^2dF5*ZPu0q_jZpy|!R`TuC6V!WGf^dembp9~m z?LuNCO+S?#nBcTwh~*#hv9DL-4M$VHisi4aOGj+JE5tSdat2iZ->&Dd&3O*Jq#Iey z&8mZICQAT+!za_)xt2wpS|bK)vNsporH-e61LVaN@*pG7G37s|Dze^YhSE1gCmh*R z(1JGpbefIp-$`|Mad6DV01S5LAO+vIYR${7OY!C|Smm|^1YBbn9f+i1aidzT7Z+}^ zsJ)$c@XKcMs%z`(i==TRm|T2{Fu$cmvr^E=2qS!r zt5qM8E{E#ZZFf35fS`P;Ju^>XuwmEhwO?ucA$M}AKBmd0+7Xc@+2v>d074HleTF^7 zX#^TJg>P?jX=;};NJ__Zd;=0kQyd$N4^}OYN^(fXSyFViWV9|7zP*gxGOfL-81e28 zBxCM+{RT~GEOfmmOJ!pOnytKTON$r2z1D5xdG6X* zM7N3&vZ`eK$8SNtF;lGW;l0sP-&~sZb8i~Qr68C^4XEEN$;;+$pI-f|H#44jB>Qxk zQLb2O)7d7Z1eY2uvmzv_P(rNO+Bnys9S+$CtzYWD3$Ae0jm-B69=!ytvZo`7d2@v; zCU6EZuwkYx8}lg z%7fGC>-DVLzKG={pA}yuy%SQmg2viW4czxJ!{AFCw+2WgJqr-oJM2n~ZaURgwwAKT z66z64C8Ho&Rx^hqrboCqIPZ*YQ}nYM+Du$x6~kws7^us2X{tPYvLY!)D;GcSNe6AZ ze>~SbvLrF$%avSLN1p0-Q|KC;K^#wGa22ANepwWpWPJ{H>N*|jvipc`-DOQWCXOh{ z=ai0Oqd3o^kF9!D-KDpNVth-b&U{ux-b;5s`mv7x0OnrcdVodW;Dw#lm6o@sT&AC> z>9Wbm<2v95Y=R1~$VN~OSamqAwUISvPAr_C8EKv?!M56Vv&HsSn(X&&YdoGDv%+#v zo}9$~T=mBFvHVEU}oo5pJ!85lRP{8MiDufw3dL)uF!OS@>^^+SP7t zj24HU)m1}AV{ObudUFA>2YrFBLc7;Ay;DPu*HO5-TNGYzgozi$NdVgtW90`JJ7jE0 zsyVjwUClJ((?lP`?0Qyy_fMNkxe!e8 zw50@!=;5*tKylH%SM;tH^W0mLByRAYQl?dYUjC!hV47Q=AdPK43dYLVGXN&Ib?13o z*O|8iJ(nL-QB38@nlp_{v}knO{S!;Gj_Mm$7c4wERZs6DBw?H$VU=J=9K59t`6jCW z0BW0QVSOSwASnyN*G}ss@~e0 zpZBh9C9rN|%#H2{s||Vtw$=40B9AdeG>Wm4&;~fis2wV`V@V+887I)T)fA3-TG7y` zX$cZABy{ciRGlX7D=VoT+rpAa%13sq%+}0*FkXZZNImd!H?A=GbIuj{4hDNuWQ`

ytC&(q{Ad_*pIi*~01sd5TBD?ng^|M{D#LXnsHrku%Pb-{<-k1I z3NxRtYK$0#fZJSQn2;2a^KYCJ>)#~QYrA6sW?kaWGJh_5 z1MBsqhEX#3Qg~>j0gQ65*8{2Vxy4L*x`RCtdQr zE3+z>W>Lt5?77Yd)|oTM6h=uIV?ouJ;0mz^A22-1a6#CEf(>427mImu6}8-|mkqQm z-5rMgJ#YZg<`%!?@pom+ljsiG#bH(v=XymuPXu_gep_L4g>Cv-`d6OA3D-r?UB&U>Z(x4^1 z)!pRxIxU(@Z7L$oZU7Oqg>un_$j;%2<=l3uFukSAPbRf1UB__VEKaOrj(%11E;0g6 zf6ceoY`QC%W%I!rLf2|z7c-!?x1KP$~L!}E$hAGTSlHx4>NcBSEF59#Un{}ZZ0i##R0kG`J|T^WGT^<>=%$W z-!|2T;%zY_@b2x@THi4~J(Oo6Kl6ek(8JAF2#ocVFItqirlduT4L;)2LUxw zBwXznI0UKALB?v=;d2^V==z1#rw(b5t_m=&OLKlbmnZv z{#h}x139f13u#x{?bMoeoP^uIESE7o$%;2Xe>OV(hH8wrkCm#AXQ1%U8^?cV6q?*) zO^Rj-XLEHRUyI)v!NDih@DP*RrD~;eq=T`nFBJYC8SmxPZKJc*3frVs_7_h63@o?+ zklFDRC^%DrleooE==@)O!;{0Nac$I^h2`9f;lpVGk*yWFDPRYQ+scZ0eS1~8do$3Z z)6Cn~Leb3=MLG#0VTM*22CCj*0A*!mJqnzI_ou^h4?|GuN|`Fw(VpTy8Te@0Yejji zx|AEVUgNnR%vYK52MxtMo-8b}$lHel%KrdgL+&fzAe$ZQGsN61E@P7CNKjpyo?Aw2 zEBcftWp8eW+pTg>6UVjJ`y0Z1yX^U^h&L&>}q|E zsqspI7=>P`>0K_Qu00&-H1=-oZh3%;Ba(M!HMrnaSDNBP9#jEy>)yPvFQZ7rQ5-QI zW;N=+hcS`E=_cIsImZ6@{?!p%ve)ctUA8@OT<4i%8EgSibqo0P=5uv2W0bHUUjJ%Y|kr3j&KnU=kHhgKL{n7qiX_7c3=uw!~wXI>JHJ5cHht%)9AWtS}t^m z-5VZlM&JP{9sDuUG4H#3j+J+(*~baRqGm($&mhY&JxI^>HB~o1PBWS~=RL@YWu%!c z(%w`{D+A^hcIQ0)?#FjN?YE#6v(YrWSV9pBV02ES?t7o*?NRjoKL;chW7Lz9YfE8i z$_J%yj|z_Ewr--LsTP*GU@CGgakoxTHmPVUV#rk#@F@j2P>`j?6tn_jieB_k7I~o1 zX{N4FUbJoK53K~#NudTKi+Fey65241P{hF@I|6?rdV{@q?zLy|aojXAR#?dL+{52KH)5kiya$FHk>_fBEEM7-dd7bQ*g=7xH?!!eR|(P_K0pit;#D!ql*YHA-2+Ck566y0OM|o$tgJ7 zsN_tYh6cp^)x%QK?=<__ZM6$&;Jl3f`Z<>jr0xmz$T`hT7V(RCIgv@u7=hPntfXNq zvYRK^@b%UHt!<}qFNvhded12HcM>G7V?mxqVtkL^uKC6~RNfAb??%)v?IDgk*C{M< zqx`Fl{8c1>3ZJMn%gs`IwYP@)SGZWwRx<6LKmY@8U;y>nr|1$X*CccvRI#{X2#ShK~)gfM7gulAf78JqOrRz@bYru5#b%Oti3$t_Z(eSs{V`d)?OAzPMmd+R{{S!P)YZRnzP5srr)SEyBqCx=mlm-Lj^Jr zNCfm%QPq!SA5($7U-IzwjUl&SZ|~P;MJoJ_gYy&R3=YLWf2}T)e`k579W~`^DI~Y} zav{e)Gm=vz1`mPExeQJ)NZOFw@jjjs9X9ZoK;>VAl_71HCn`d)!0+qpRE+> z(eBuIwy3iY%GUBJ4n0+K&GlSlRf*Q-nLI1BEgS5nDW!$JpD`KwgH{*MEV0^2VSOSq z#1$iygo;p}TwwW9L5vadw!{&fQ=%MUkr?TWhF(XuWOVYfvXFD=L8&W{rKM7Xs`EDa ztv%Ac){^aaZSdEzB)B}eDq~f1%1`BGCvCtg2*4y(NX~*M{qc7v07#k0J7%4n`5>T8 z4m>6amR25O_^RpUv-oDQx{2q?jjM0nVC}E6l4*eWAyi@ zS~ANkJaF=!$8G44PmYnF)NPWP1h!14;B_8Cd@-Mq!QYf;bvvQlWNvDvwz$&g zWRX-p26{FD=*oMk?8Exj-bkQ(S!0qRac?_fSc%6u9T*?V8?_XvA)%fBU28N11(UQ{r}F%koh!dy(}tpyzC4b;WknEZW9(joLO6FFa;4 zStL*5M(#hD=xdrb-2kmqy`!X5j1%WLuJSNq&Jht+a@>I605@Zw)S9hrL&kTG!Y_z* z`JX(oNB*ZfR6`?{=aN%-A(xQT4r@m<5_>&xD8n0GANmzW)HFYP60HyuLBq>z5B_2p&k9Fk~bAk!%!u zwg=X>8f}%noe_@4+BMv=y{XYev91w9k3eboIK~Y@hbP&~ zlramO0qIC%RX;cb+*d$wcJm)$u%LmCW*8pgmR79kX**j7Ttzez?JO>Qt~-4Q>_+0P zyn9a$Ic9rFks|P-Y1F;KE-)h~2v_C&tacyDsbN?iL=v|oWq@{3mQkI^9nZCDZEWrA zSH!b{3@tM9yy~|f7=#hZm;jCUWCK3prp+(oZK)p>fJR&p5|Csr`Q{?3*?3`;->ahuq<$Mq@nq`vP{RNqvfDq1@_PbC7t=e} zpxEi!cAF57PP2;LRe3=JEst_{HQT<_*LDPQp=|dZst%}Qj5-~S^HjV`pr)^9a6R8VO{R`pi_6RD zgm9dkIbHc-!8-xW&T*R2X}m>asX}hFi>0}@R^?(gWj>6(N8Y#k{*7g>>C##1FeI@! z$Cz~V{{W|@c&8Wewx`0Hh`zAAi%;R(LfC ziuQFrd87lGS!i@b|2lD2uJj;GGtvmRGJo3XE3)ir%T zPvQFwI!1=%2!nh%oG&{QmCoaByVsWLT6E1Tc<~74O{6@yVtr3+{{UK@rSR4EqkAlN zwjv^n%_}mPxB%YgZfs^PfdW^PnN>3Z6wsF@rYyKddHRA+Z- zTqIVIG_yC#in%}gR>IqZ>_(p(bKN}15@wnmHd&|L+`&DD(x$b)V7mg&FZ)~f# zv65n@TwruiFa=-O>6pgVXJ=p+83L`LfG8NQxWz?MH0+6KWT*z!WfWmWJrio`2GwFg z1$J6s70^%)0+xYJpc0mfN&uyzihx;WlQfy6tCSR)=Ej}cnA9NUA^OxisFrl)ZHV1l z_)SZG^%?qgsigGCXPR-x3oPq3)vRmF{{XKL{GaC@)sTZ$8dieZTz2Ul=;njP37F@F zvzBlEYU}|y80Y~RuWh`9$hlm2gXPad?0u`uc-M#42R9bzei}k>4_KS{O?kOtEE2WT zII_d(KdBC%W)A-VQMuGKxNNSBaxI`LlCtJj87Bt%V8GPD6URKU z%D!Wr>I<(>Gq<|Z)Db0WRE!CzolDn`jwkhkb*fh$?!?_dH6PDSQKcTtlb;h9MQkTL1nqUzI1lD+M$ z6T}V?+ES;=M#^G2AxYmYl1)T;@TK2 zz9&tLJMyDvEhml}AfX*(40;f%1`hRFeH-peMG(~Hk5Gp0TZvkAbv*3(J%6QoM;LJ3 zrxR(dZ$B(|G347^Vdb&4t6~^$3s|oK0I>4#j8y2!J1H#`8RrntXlnw-do17 z-ykaJPUHj7{{Um}SWCx9ncRD0e`Bx7@t2nmtBdh2*U9Fy;JFRH zM{fTB^{(-1SN4z;g7S1+VN^Ld`UBJLTwXUx9V85;4=ZF~0n47g$THE?H+ zZ`rnKXLEBD(RkKoE$4xGh;%)jRfBcL$2l3QIiR(Qc6qf;IyA@riD?S13Y$7q*35}$=IdzFZXv47V?9wspTW`uLOm|O_HeD(ex?wh;j2K|sgA%)bvfvu- zSN{Mr@peWiG|O@D&`;(p@;Cs=&@m+Qe+j@HD>D_$a&rg5#xOws3exzpTUgoH-&)*U znbt&-D3r93z{MIO2Vxu-3VRdNG%E(3(xaY>somac_Lj3;H1d`S43b6zTe;bA2E_?H ztcPL=#wwd#X0yIiZIp_5l6cc*F@_taai1#&0Qvz}b{5gu>a9P;%PjDtH#=Jxmdyak zk;7n?8*-=^EJjaiqqz^MT^oRg8*r+Tp*aZ}j3_6$$5ZM}NfNgjeU7ghe!Gz$RVb-MEt-3!fU%X+tVmzXx%3jJ%KUA99E4xn+;yDgua=lTp(D%Xi zJBppNB@`uuC1P0OCUP=IhJ@|(^re;sksHdABd;sO`S9c&gJaXEBXD;WcdF{mIk0Uu zX@qg9l`bTFTq7I;jer0Pjey8Kd*Z8RyL-F2qO9Ekzsgp6mI&r|dj>lYCQ z%cw?d6)Nk@vMK6}JE{Ihak!}ZewkxzyzyENW^WZ%FGlypzFPrXYq zc(Sz87kQlJq+^yogD3h|UVv}+H7OKwZHS06Nf~3iA78C>$Ed4|-7?bDExKIKE#t2= zY_b0CQ_KKV?s5)8XSN6x#@;(?29>oq<&y3f7WLGRsQ2yAZmk zBr`EZk(h2rUAmE+WPwvHZS3L`>Gzs-{JN|2My4XK$lbXYzt?|kR7v8VEixBJ&6U#< zvA0!m=nm&>d4|XNS4~p##?M5xmUfhB@XAz37K>@rovoHC9Cq1$-Vt&|? zeHJ+mkR!D5Ryd@cfP^pv2kcnmIV6?N^}FGIAK~+&Tii<#i9OulBx998Aa`~o6a9x;+;G;4njM0#hXY>{dF~Z(#2&z& zpK7#EBx@Q4jx`uq&|y@104to+h=4K@GqBpLX7R?Ozj4GI<%uWWkeXx?JgUXxWm0^s zdJ#ixHr|$838tQ#&~xr6IQmcsE1dT*TlhlacwsWXhaZUHdE59#dYa{^3%0Ph8f(Cp zyobSQ)RgA!%sne96~>*%I&I$z@kNxHcC?AjJ&|lUBc%54`IkHY03){aB`LM>+X(pQU{kKxHbx6*~|{b6#oqf%th1(%owOJ7;b+enEzDlDN)F z1LY2XjDmLi)g=or84T04*0+TCqa+Y8PCM4B(&p1w(}86YK_cgs$8T|8E)Fc#^n{yF z)otEMVq!mhO3I#riTQ`UduEHOuAih!btCgz+FCTsLD;IMf1cm16LHfTmq(tv*^O@U z=F;E(6kL%UNKQ;@-zfUv{ouJxyBJ>AgI=Rqd^kpc@UV zwiu%hixu{k7{*0gLu?gB)h0MV?OoPAv04L^X^pBvEh?ov3W7iic%TY!6(CVh6woQ) zPzov3(M}WriYaKt0F+YFR04K)pqe^S)~--OX^qmK?MyTWHW{e$4CbV_O61~%5Z`f% zhjncX%OcGfc;pGQOfzF+?0xIcCB**t#T0&m;V*pq6i-6{XQtv}>+?PE?5`Qj*wX#F|t+ zRH)fI5J|`&5G$>nuAwc9HKcHG_MbAc!7R>JTgM-fWIlH%r<4U1s^RTFOt})aou)+{ zjncKGkVW%|VnY$o0lxhzE{~`cd11e`lIvH5 zFf+u_Jc}7zC~1MjOrE>-Beg<%$gJU(2&a_8Aq>`blQ)H&W6dGmkFPEP^`X&e7S`7m zvkQ@RX)9|CuCs^}nU#spGO;E!UR;B=LCF;f^)H2o;eIYjBLJ$~+sU#aIpk^a%*&t3 zyL;6vx3bKh50Wc>&_X#=^zMDmO>!GIL6fx%-cN#6)$EeQV?2=Cjr=H=l{W5slkQ2Y z83*k2)3uo-xsvkK2w+%p8h6Jw3xIi%L#lF&leoymX7xhLr_RxqS5?R=cLY_Z3hT{( zG&dIqRn#@uxz@_w7YPy;QVWc9BP5eiBG{)MRW8q6;hksp#fml531zCH2Dz~^D?B+k z1b|5Z`W?qZRX>Pa8wV8F_>hE)RfTaBhhZT90O7y4q3u{jw$wBYLJ!zcS+2g8%RTMg z2I~QY|SE+E%5$T*wXDs?v)N`~A;m)WSazC(O4=LXSjp-SDooyZ& zrQFFBr%%+mp}c^YnI#99pF({{`&IR=#L*Nnw=BHFCp)+wm+R?Uu07!&+h{cnGnOrC z0p&Oxw+cTacF&?0be7_bMpyMU!XGf2n!dAO3 zxPwK9h;0s|r6w;Fm61AlqVE}P`nJ>{#OXEICd9cE)`wnoTZJ$Clz3-wX*L=D8g%q~TB5SR)(hC6vw2P%7CqoWpEhuPHBsRBCcY7+Sw`KD@&T+D!&njc7I3WI&OLGjCw^Lfqra9qmS5iY` z-y>ouV>n7IYJj~44K94xAk#-*e^2}EP9FaNtyrQIY}g)@8WbFzw=|9R%@Jfz(l1`d zVhb4Urov9q#->hA;R)Oyp{e@0(~Y}LX3hTq3lfC}9Wr-q00qoPF06!@7uMgHW$XY41Yq?^M)KB6^brfNYE*F$5 zo}~yo`W@=KO4K#J7uR9IR24+N@xB*6+7Jt_5oCt`g=vrp!KNJ*uhuGgh@|dwpqyZ?LDD zuGcJ%rKL}3my_7MfO1?y#4<4GdW?@zkyk1~;%*|a;j2h)^=}z0{17y^NMVZczw+9G`V;wN_S=6$*DGy# zefUYP$E9k|ae2X4vLA!GAhX%I>W%d7JB`Nq6$Pr#Str`7HQf@+T!JONM_J%*duym$ zl51!FBz~jm(D$nZk~t!dSY%mXkwc`ho@He{2tJk2ZZa@IsUxEc#a7Fx>1Rjd%{}jJ z)HIgb-H$4AupLF!;9r-}U3E#I&R@f4N zw>K#6bBgnRKKxtKICI9fus^i5McBs^d1F84ZiDDDew0X_(dbvVmfC%@&wV^_LW3;M z*Z^0H@n_>JUuqEO{2LEDTuQJT$w!nHV7^uAdi6LS#=N76xSNQ$u5XULxp`ni%(n3Y zBai-G=iKjCzlAmsYMfPNs5*EqsckLo zy^XX&2T$r7ArAJg8o7FJj*@aWMmcl^!%)njL-<%7wVe_Fn?vA`Q`TjTN4^-pX@ zDnEp+u+u_;u>C6X(!>lNTAOcb7aLUQ;sdpG6Gtm}6-P>zk<*cDx-KY4)k=62;J%dZ zQ9v{;I0{fdN>D05n8i2>UescMF-j=L^zO6)OG!&WEiNelrNuxJlAVf50&&UfNlV_4 z;8n^9cczg~#W!rw9M4MT!?r82rV&6#akkaMM5$~C-np{+3L*;I$b3tAqa9rc9>*Jh zI-s(c>(=wzT*e~RticV=zDX4Af;|sHK8Ce2udnM_{eMMk$)UWlo8h$x7&LO=;N!8! z{r0HIjbv5sQKi%9%tIS!Z!5#5++4az%!i#rLODvbPb2>Tgrhvc(+07xsoB~|GQ@M^ zC`Yxhnn>-N9Xv@1=9J{_Mn1Lb+UAq{b4?OU9BZf|0VqS|bLcbI+doS2y=zN*D}up% zx%B93jW-9vM;=+r8&UT5WXHRn>H!vGhS^V&+64(3f66-jPEWl$6WbU*nW#3hfP@?l zU9q12m2no6dnK2~BqzY-=Ux2lxyLYGyuSS_K(1Rj5Hn5(w|{B|<2`TUYT}!zJaSluV}W~ovc$u6yrV|%2kx-7~iPdW*ZvF{v_~3di*V@>Cq|G5q$CV z_>~>KoOV6`07}T(YM1;st3j!i)b)NK)Z$!eVVEk$PQmgEJK$t=Rn2=HjodfZ!h3g9 zk1U8CS)zPoqZK4`4DY>OGWKUfUg+^1CiL7uBapC_Jd7Xb1Z;EfRDIVB zzY`qb)Ro1ZUOss#D(utoMuq!DWWLf>R{SVw;RF0%asF@X^{h%Q%&J*}#DijF9JPPL z98o*yZ#1XGxQcX**bT}P^JCa&^s6o{v9rIE;%t~L!eGb?3@{^N0sPhG_&!cg8zS+t zyNmoUm)Uh_O?G@jA3v|TGn zHgepjg?Sk=JoDTF(J=X0VISfH)L>&B^IYxthV7@*%tds_rnv*+?gEf;vnT*;r7_r6^ZeZFa@gQk1snMk@Ov_yf^I~v)SK69rd|mA}cXgjmW_&!+rM1+#CvuH2R*% zy3{Wuhgj5P({#z(;e$ze9-^XL0>Ex_kPbixB=51Oe%jJ)A5n>IWWI*`;p86?J7GCj zCKf+4qOtWE;yA zKESH1hj`9Zje+LQ4k}6~Zjf7Q5b9IMZG9}#3EU(Rb2o<(Kf*V_9f#7Z^qJ?;xRqd@ zTc6qL#5;UHoT2U!y$@=#)->Q_O-gl>PQ197yIEmWr*P;7>bdOO=W$%4x@l);o>`@p z`m?t=W89PLRc#`Y%k=f{@=DSr7WY>OmltVeIpmlks!1egs14h%Y*fpEXC9W8R{5Bt z^Q^`pA0Xc@xZ^9ATgKEG;< zY@(c>e&h>XCi3QZ;*KJwMP`ZQJc{@k!jrKl(xJMvZqdk$j--ZG7$Af3yK8HBc5M%8~z{BtaVEngiS5UgcN*9fTx=n z=zhIx&-H#FaRIu9JBzOo`C55H6b?pkSoLpevPVpDsjf`)O*dJB?U`C9Spo7Uc476a zeKyv8XH>SZj!_ECcjZFCR1B8D+~5kw{u6NH@hlpKsWemF>Y8Mc%WrSv1c<2zAjSqs zJK*F2wrgP&s|DP58g!FF(U8$dP8|<%@7#OQrDZZkF{ZjOn*M`xrfT!tGyFxY(iTZV zk<7WL6^~6wNF5oTFdKT!z(ff2h)#GN^rAYeL36^s4uqirx z$bYx3HdI#ySwS7a73RDL#}H~!rIxWHLw6f^DKl)*xa%8^$NKiodQ@_=IErz}HZl)q z9-^w7T4s%2(GOG8Z1qbiEcFN$;?hBfiT+dX>_=iZ6`9m@?Q$Cl^qKAL_>VxEB7!@h z8sdUk4$Y89dfBp`m^Bvu^4m_elIHq&;DRyC?;`*ddXjoosD#X);Xe%O{4c{M z$6CEf?-kqe{<6~ z)A46l;%8`W5?wb-e1M8iJcHfG^dEfI6xj#mcuooae_FZLo?W2?0{`u^h?}U>pwQ zk^mja#ctNh3tL3ftm0TLSIsJ*-dE~L_Wrofr89^0RMITW7NtzGFli?PCq96m>IeJQ zj>k>kB}vayQ7lgmdg7}7Lip*AqV9h{=T()XQze6z=wee)FMqj2w5^FgxVnXBB_N zdJt4M5UDnveJjoN4LVe~Bpg0+r>Fgz;&PQW?o$p$9EuqgBU2XuYLeRtCe-7K z?S_04%s?lxVYlcjGh;Nu?psT(@N$v{!aq@jC9FB4;3u;Ev$dzVlC? zOux69;vQMb2_q%4uB2pi*#7`yOT~7PY8oAt?2m0EvX9=POj>aJgO%(7A8=|L-4pcN zPFGD6Y_%5DH0!DKd38-wi8UA&37DV}!5=v0Utn+s$A9HhG-+qyJ|eiY;j0Lyv5G_* z&GqcQ5^OQIA{_3Uvt&8%|^>rv5xynx{m77O!7+^^f(}nppQdd zb@-{k>w6U1&Xor}!1G;ODE|Nzd%FJsm~}qAeQLjkHTm=r`$=K3(s&?qq;`oJX>J%C z7eGABz~C2SgSf9()9-Ed3mbdwK6IYa;RKONof%DCJ5CnWMXx|W1JekN|*sd9PvW)H>e>*zXbvXLFEagb-JW(`}`0C(A%skG7e^JxktLK!v zWGN^3iL5=0dakv79qfggD5M3XV}%E<2dS;25Z^NI>z41Y~ko^O#PPBLBCbp-+mZ`xr28*_YG=p899|P7J%7Y2<{y02%X?|=5IjkhZ;+A~pV@QsAEi?k zB@Lsy$nu|)N|uPzB!?W2kFNQrByRC?xDq#fYyqyk)~#nc8D(4b1fmU;j+^%%_nPzG zDB?@L9O4P>Y_4t|!r~~MjM0U7q~*he^ck**M$GZzmsWaxy4v4z zn4t#_j-!3>OZGi8fs>Bnwxo4DQEHMc=H;H+Yio$ods&7hf=`vlZ*WIu8-w~(jWbHn zhXr16{pr7WgvRpG)bk$@;{lYOP{VLoY*(-~n&Dk>HM@I;kC^xlBk>5wK1O<0f;@v& zqaIEg7frDi#w$ArV~OVd*hPL)4%>~Y zeWal2S9a+bX=VjN$%2y{WmFULNDk_K@N3KtuYYlGeJrZe>K4$+EdKxzFC&2;&CQ+p zhw%5N#w<~NoNt++pAv>r4#aFlHqOpF^<=)de~OTS1k%b{Gyed~Vt!oqJpt?MS_8w+ zBpTCQMR9g4Wr9g13eisuw2=ei2y@C*Il(#H1Gvbl$#mz_<%wl_&r4)NEG;r@wOy^zl{%L z?a=qGs|deqdt22H;rSz&w=1fFv}}Je_5|)en^mT(!v{~(?k0{hjcErp)C&CG5Zfd- za0>xa+H`4$lTEKVqwpsk$KWN7 zxLe`I$tH4OKF6*r!?ZnLRpHpCu)dQzexQYcBb_pP`w^e0_Z6YiAdckO#R8cncL+{S z2qziHs!9@zrgd^lX`a*gk@%xw!7wGPyqEfJ3rp+I;A&L&+GG zd0A12=N^O(wRIXsoVF&J!1MiTG=~cXP!X3-{~D&>??7@S{=NT2_lMUmLwUu zVam=)>PF>%<=oP6mkHk7Op`n-95E4tF5!N@pFzL1dWDvjk$_HSm%OgH1N{roeR};W zeNyO(QCA~1XTo;o}&4o>Ga zZ*OW80gBc*(kSNMh*p*$VYtTnnOS>9q8>v0XB}5ZADz5o#`pW3QT<{1oo!)r&CV!15a8*(w|MJ z2PPHGmu{6Hjq_aEfP*5RwQzBedsOKZ9G_Z@@tysu$&k?$09B)uDGRgd~X$J-hv@2u}MsllFa`6ammxt(Kt*>8)dM*HEdG#1CnX zX#p50<0B)`_TOVwkwYxKM~1%t05SV^sc}S+3@*%ioaCRqOPcm2Pmf1txspq1Z6hBK zV$zuw6Nh!_w=wC^ed~a=fHyFWLGF8>sjj&SF+ug|RBf`7e7hiP8f-V#+@uAOenLN` zW%S#_q~G{A@U*sKK+{`ul@b7ZF@DUh)8!t` z1g6sV!8QFx*Gq#zvbS+%rt{~blZ5t zEt(OK<+ciWdZLb=^?>19`83U2ntKRt)-cK=xE@s}p$FEq{7<1?UEj6e6G>}rb8

z2MnqdZMP^R&~9^6(cK>*N*t2e>snRx7ms+>H&I+e&I-Iv7 z@^TWZ%$e8{2{^6i2sz;maxFqYmpXik*77~G2LVnA{{Wa^9Caf&tk>h!#5cTM66x|S z%F6_Di6UUjj1jpV4^67jn)fdps&TiXF9T`DZW1h@w>0uFLN_WfIhW~;>f+MII5`WB zc{_7%1vi0iE~nD1ZG>tiw8BJTTruyGJ!YoS;JdhK;Po_V<5-AH{9iV zRk$Koh`rf>gH4Z4dx>pXp@Jkr*Yhtwi61cLF%U3~)L`YiRZgb@YMP|eX{{u%ypHnD zR7C_}oq*2x$K0OP>3ZJ{*lKu@p;0=XU_l{V{{WZ{zh1TG+(D^X=u-=QO+GsLSND2+ zXkxX1b}V)-eU}5&nw}z99KAlrT|ZB^NM@4U!Eqc-`^)RW;oC**0r}6i=i0OS=B>|o zeg^h?ov>~sj3!TFi`78){#8?|>et%LsU`IJR2YsF0V0TcA`OK^4~*jnm#??ltfTU4 z98rp-i84(oxi<|nc+A-{G4nA#rl@%88Fi@^Ym6=5D2^$Mc_jn?08RZH_*?9KYTuEI z0>lz}`X6v9!6KFhjlhyN1z0fH`&F89Jh53Ommo*TQ? zZ=j!2lw88NXM!L?4@J)2yW{n)`qvG`e8pOE6w17?+&S5R=^UriuzuYR+mA`%o-Whv z_+&&jN~k+9Z#_bwIsJUcazCI z^zSRN=vd$mzz%>8!mNVH`j!{^i(1KP9g|N9(@2ia_CluOD6xit$R%V!&=Z}#Gm6FB zX{PG+EUiNaa(opH<>ozny@~d%gt}zoM;DQyl@3gisKG_`J*zu;r9Po&f2v0ywTjfF zF~AXa&Tv?cq>SK!l6^5$XqS#zAFK2;H;T6R^VrD6+6NIlf;LbaA1>e@{jw_DaYS)& z{q~)!+Y7G}F9JEq9Lh2X4g5oITCvWdu5kQ%Tz06bX($rvH#Z!Cxwef7V~%Zz9Z4Tb zEVNsj0MDn0E-xYQ5la;g>D}~*Dc*9YpV|tT*RrD*ynEDzV)%Z(`@c_JIJhgZtUi` zmUI!v;m2%P?co!?F^*w?5A&(^T3xlPp^Pl^U~^-jKUzHQeweE-52&Yo>_L$|cqR{$#Ps-p_OK~{Fwe$_4uUB=aA1-Ka6wxXmst#CFK zGAJ?HrbiY!*HI81s18v_QV}svM6RloBx0>}QBMjfaZm)tDM0#)P$@vDBr%#=0HvT3 zic)h)(ts&w#T}>x6ttP5pa<(gqoAYhKqq=$-t?WfrJw=56qK|8Xrhkv0Y^$GpeE7Y zl7qEepc+7WQ9=H60ZI0!4|*sD<7#QeJc?*Js0TI_+*e||Y$>14fa2n~xY+GdvF%)$ ze@c)+k#o>uqe!{jgSA*X15xBm9-CCARiXv$tXhq;L2z52s=x5vAp*L8{$zOHE$Y%Bc8+&IUSU5wj4o@Ge-i!AI#L}2DFs^u z5#E&JIZy>hy0?=}xOH_a32ZXs{4@U5+#HRMUrOgr&RENm69=0l4ZG921Ky3#wIRst zO$!Lwz~7gB_N6M>~`0ng-})?V4O6?I&jS`qgj6+&sP=?26H>7kQHE2ASE2RxG@`Okv_-VMYg?yBPZHX27Ffx^U^)swPw$R ztYw!^iaqagRC&A6y)Oed|jjG_KN0#iVo0&U&#u>graF;gT|iQG3B!Y*Okk zaLO|h;d9uU+uq*`X%YlN^Ewt$gM-@x9Z$F3vOEbe7qdckGAJwVMQ)@+Boam)<1PyG zDZ>JLk9xd%Jq#7WQA<1tX*_eKzlo@bIiP_-kfV|2W@2)EGu!W7B8&H^_tCw@?o6p| zkW6ZOk@AMyrf_?2y>(cA4VE}zMP-PyUP!#4b~}J~@4b2d01$E9dVDh2aOqc>$!m+{ z2MpcH0#EDH(xlkcYg|ts4y3Z`vaGkVs`6>@Hbi81E!#cE>6*a)B$w3qZ$j7ZV~sB7 zX(MqM+mj@(>58Jz^qbv(RtwC`tb9rCBs(hlv8xZq4P;tqnq|J57#6m&g;eMma-aH| zyh^>8#eWd;sUf=zbrm5UKmdHhZMO^c_NwhB`VB%47To0y%)JVqLsl`N1Ri2>x72zX zng?8$05A*5)Pi?D-ju&0vCcEOCvUw~YP!v?nt5fAJl&#Gzu(fS_@={&+lLOTd#2pm z>EXa&JfRf)tn1eV^fe?FI;OQU+FczU%1hRPOHDGNF~q*CeLt0Mbi{@mdltQjbE-yV ziZRMxVUg`wufRSpOGnf+JLOJG>==tKM65?Cjfc4Y; z071@kx$RJ%=*_7(*lAi|zm!KM)OPYTq7+6#&RC4(j>-p8YP^Cmk$`sx7z{l}wOn88 zOqV_~u)5S?auZMh0U~1;jLx4w3UQh#$u}rj1t_Va-6daXHDx&_*Vh^tQviU6mI z0RW}NI}}s_n5R3Py-upMlni76a#TfN79M_qqQv^ssM+2I#G&TPzmox zy)7qt0Q%8I6avvmX=ntZiVXoUnm?rmlj~O~6jF*P0*X%5cAyc7`%-kLZQ7F*0vc%> zQ}IcPfHAt)GhIl}QA{HnP#joS2@<6m=HqIRSIC=?Yk?SSjfw6mbcvkged;`mlfU(; zOsho~6KNw-wrhw&o0SKdN9J1dhEr(%8P9}H7eAExSFfuWpClZ2#?_YM?G+}4uWis3 zV0>u6{{ZT)EZDd^T^xQ!D*ohu$&g0!(}^ePP(f&1CUU3cKEB^te0T9OsT{>g$2X^@ zYbCP7 z^apcNjDiJKY_y61Bc?vJb#JU`dRCP+@S0m|AbcY{(FbdX1Rhn!Kr8(7S;+4nTGXVt zvx;8^>R;TXjvhiz2It)R^sOTW?sRZEBTdgh#;8M<#8HgT$e>(Lm`9~oc&3S*~EVogO zKI$n7K=&rom>tRF&OX@{R;f}YyF7DE)F;#L1oq?yA3SH}aqnJ-!%Ka}+QMl%JL;7gst*MiEsW1bkwm+=~0W@nSuS**d(ayx2XcE z?8g>WZZjaMX1Q3L7+4seK!X^x9#7(a4l)X|e0ZJmmV)-~>iLy#oh}_%w<3TL6V#jy&-+uLvw0azrjOx zROwPl$zv7MlAp_tl<>IiKywRE3+qn{N(BIlxD=pLid<9x0+xYJ6aXls#`NkECvR#t zrTWln0F+Xmw3wg@D58KV4HTpGpb99WpalksC<2No`cMTWDWZTVqKW`LDYWWJ0%v+d zNA#tuloJ$Uj8gUMKn99K?Mc#rbvBfArJxd;cc<-0dT&q$p0v}_pZ%$(gq(=$O?}5o z>PFouqlA8%TnNmDA( zgyP)`;T^6ZL-4X3?fDP2WBe6IP*es}kbOsL^tp8jAlE0;^>&5y`ISwn5tZc5%oz9I zy=6F`Lv1?cius@?$m8<+R|ZHta<600&x`5$Qj1WL1)s~N8J^6}QEz;3M zT#?H*<|RKeE`DG;6VMY=xPyik>p{I+S?<0&Vzb95D-b(-3~jYtMC-?aM{+RSaq$)n zYZdLZ@XaO`D~CJF9zIt805YAvn9XCZmQu)L-9hs%ahmjQN%ig)y3@7EB^sTK(X zMn@>d2Qlro$LxF8oJk`?cO|syQbjoRA7k8m)tA8;u~WZhB-6_!q&AmPGub4*D2q8z z*vB?KakgsTL@JkUau)*L86_W3xL@YNs@1{R9Piqv>Gw-+?z1O}Z!&mtpxBkjIR~)> z0zQ>$Gf>(o^bKa)P5ec)lJu#7I@?MZ?aJ$o{f=vK#5yg`hjz~jytWb^XkB;0`X8lv zKjIe-O%|i^(c$2N-CL;TNlNo%4uQQJ+*Z(L z>ZKZ8qV5ewHc{sp1ANr4+7W}Eakss7ywpDrb%^{(HCC~ew<##bKmPz>{pyx!xF=5` zfq%iP)N8hCZwYH%i> z0JvptY%tldHv`jb?^m~66K$*5$E|1zO{|3Y@G%NuVbp)$)NRu68e4FzmKG5{p`;`P zO{d&4nPX$sK|4C1PTi|5f1^XC+(02nAQDH-@UX~S+wYE7KBui&y_plp(MY;%pkn43 zUy#NRb?!m*8;a)~@miiuDG!O>?o}NB0P-*8`tE*}PPWA6r5ceKSqMOT=4e6OLPamk zw`8X%5uADL%9T+LD%v0$Ja!0*~uJ zK9zEU(gR0&Q$Qi>OG)&ifLu{VX$hbmwKj?cKmrXn9<=k)nnu+iX~i`3r=P7i8}HJf z9NM{+uH;h~bn8+{71up#JFY#da}Dc^iU^`1lse{>9;awxkn%{rXynJrf7th`Z8lZ9 ziftcQBwQfmj1jwi{+&HVY#e%32NGz%wIW7>cD1@fIc8PK#&e39#bk{#je9(^r;#arkZSq33-N>ES!WBlVd60AO8Stx`W#_$HR}M_KV|;S4Lk$&~-g~ zQvI*2-%koga|)+6!2VRuzz=HlSmtEeNx<7R()K5gQW=Y9qZ6EphH@&@SrTM%G&4gt zGet6#a1IFsW3R1wZx--#MG{LClTR(QT(F`+&hxJ@#^8>FW7i!=TJ*IiYKz2IH&%Qz zs9agwL31i7#8Z|49J`mfqqqhF!7`-l1saoLpHV;`-e* z`7JN`p2lVYxi@H=w2SULj^k?c&MMF*(_}7^BP(IjL_SFr^mRUm^#@~GQ8ZJ@;NG7O zAal>lmLG3gw$!xPWV1~^Nm^Ypc^}g7?uGFuM%nC#kE=Z$X~M?t4q*FH`sxmo$EIo4CR^0B<{?|B>R$UyfO2J@DX_z zca`=<{{VW^oEKVs<5z9%KIit0L|P`A2HAwXwnGK4N;7+?uR$PcG-AYO$v7TVxn5UR|-_#D609 z`kwV|X=JLPDu4w}x3jJ!xnrfJ4%hQVyA=r~+d&9fdo!Hlz(BB-3gA=_p9v)YG`9noMGV z&DQHrhBrK+B!rwK z@n>~DOCtCjT*g}?kxOE(0F+wTX+4bbtnuYfZ;-)Q|s+p-6rKV z9V#28iQ~7qh&fAyAK3_X%LjuB0_*hd1FD`kB!VtT)>-KkHuQgVxpdmeeHNRp#M${JK7%m&Oo zYQhagWzlV{8p_pVx6Up?ks}P`4EuEYZCxjT-AOFcstMVdvl8;I=Qsn>xWW=Rnlg+e zJ9OHuN1Z&5I*Hb8Leo*VOMW1=n&lzh2_cXqsp}bH4p9E1sH**5_U20@wTjf~`e8YW z+9YL-{n?K4A97FHwAyBw3ut2c3kkJ5@i&IhqmV87ax*9yVBe=|yVNInG`sCTi6n~h z{p4v92+@gxQU@*@JgTZTU1`5D=D0Y<@r|_Rr(5>ZDtez7XW%ymA$@kxALycUOaK_?VU*^sSzNx7#kY0 zx3=GUmv3q|+N>kC0DU*D%Mq>ZeX7D6a64Ccq5uu5N=J^=2RPybu&&CO=rK-Iu7GVq zMupm(2YOt3)3HD%DRI3o(v#kx1r)Smgo%nNaY@pIi9pRLJ5YL12}|0Jze-vF#s<_1 zTv0$BDGd}<0Hp6hqJUaX^rn)4DJf{65Ya^?0KF)pj8p+BXr%X`7J<@}>r3>Y5K++iT0=k`X%AXjClKC}XwJ?KALKz(Qh&vQppM@mkV z0-eF5Y6rCy01XGFB?qNRB6RCO6njuMpbS$;#X6AG1DPAs2(E^j-6#%BE0Zq%_1%VR zn+=T!BDz#(Utv>RZSPzPu$$YW#Q%E$7b_N>ghHS8Qirhdw~Ye=J)#F8Ft zB8|%&dL8;!oz|=M8+k4Oc;qW`h*mi|;1P^{FC ztaX-HUfRak*;wq3kv=4lcQQyuTOY%)u2@Y!%;lb0Tjkl9n?$+O(V<;IB%b5Ta}*L5 zP&$C`(T;lz^sNsI=+cftRk>t!hs0n)1}9)oYAx-pZQ=(kYHw_%)U7Ad?_{*PURhA>gOJ0q2e_oQw`X;C zQOXW*!yx@?)K4__HugMi60L2)^DWj;M5YkhrJJ1M4CO=wrA)pe9U8tx6Z{Cbi-j4JEiaJnf4FIN+l9qre zDQPGIgGCeq8YrZoCK?X(QPZthpin=ZB`BZ@QAlZM1Fa<}=}2e-jL>N)1?fjxDJVz} zQA_VgdQ#AUrkYdjLF+&`fbM09Z_nQsmIbgx@=_WdfG zRnx{!yON;a`_nA{^mZ+=Z$1k`#iGSNaY~OyB^(ZL1CqrWl)Fx1F8Q2YSQW2 zF)SpJd@SxWyC3$es4bEe3lfDMz5eyjhX!2!tiF}|m5vC*w{P|+t@M!!0m>Krt4{bJ zqY5xR1!z1Vdr})*_OC`5)cBwJjg~baZEH?PQHp>d z%@njykfiUu0+xVE4JoASKp+?xrJ|OAOn0QE=qNsw08Z3UI?#P6NKrtdfGFunOF$G+ zMF4$iDQGkTQAH=#fKVw7FRdqf0Sz5#NZOFp0YxS^pi!Ctq@|*OZ4?ojS^+4fG?W4w zDBh0r1itiQmVv!kpn{jZ2c;nBXabIu4z#_f1fqdO0KT-C$66^{pr8_rQkqV}fHO@d zmXOd3+L~ykGy`d*@3krILD4Uj1r8?M)z5rb9$>nW1C$uHX#PfsU1HD?<`LO6sv!L<5(lbpU3f!N^7JT^2E# zc-s}w3W7!|;-88KT5zEv0Z#&eDQXD;Nsnr9?Mf&>f{v8oN397OGuoUwQ9$Zw0MSJh z0(7ADr&CW_0PjJdQl8W#PPAfy(u#m36u8=hKorwUMF5l?=}%fi(u9U-X$2^t3Mit0 zN(kD6)`|e4iYNmVl%|k$pb*iTQ%O(-q@@&xpr8`_. As of -January 2013, Mopidy will run with Spotify support on both the armel -(soft-float) and armhf (hard-float) architectures, which includes the Raspbian -distribution. +Mopidy runs on all versions of `Raspberry Pi `_. +However, note that Raspberry Pi 2 B's CPU is approximately six times as +powerful as Raspberry Pi 1 and Raspberry Pi Zero, so Mopidy will be more joyful +to use on a Raspberry Pi 2. -.. image:: raspberry-pi-by-jwrodgers.jpg +.. image:: raspberrypi2.jpg :width: 640 - :height: 427 + :height: 363 .. _raspi-wheezy: -How to for Raspbian "wheezy" and Debian "wheezy" -================================================ +How to for Raspbian Jessie +========================== -This guide applies for both: +#. Download the latest Jessie or Jessie Lite disk image from + http://www.raspberrypi.org/downloads/raspbian/. -- Raspbian "wheezy" for armhf (hard-float), and -- Debian "wheezy" for armel (soft-float) + If you're only using your Pi for Mopidy, go with Jessie Lite as you won't + need the full graphical desktop included in the Jessie image. -If you don't know which one to select, go for the armhf variant, as it'll give -you a lot better performance. +#. Flash the Raspbian image you downloaded to your SD card. -#. Download the latest "wheezy" disk image from - http://www.raspberrypi.org/downloads/. This was last tested with the images - from 2013-05-25 for armhf and 2013-05-29 for armel. + See the `Raspberry Pi installation docs + `_ + for instructions. -#. Flash the OS image to your SD card. See - http://elinux.org/RPi_Easy_SD_Card_Setup for help. +#. If you connect a monitor and a keyboard, you'll see that the Pi boots right + into the ``raspi-config`` tool. -#. If you have an SD card that's >2 GB, you don't have to resize the file - systems on another computer. Just boot up your Raspberry Pi with the - unaltered partions, and it will boot right into the ``raspi-config`` tool, - which will let you grow the root file system to fill the SD card. This tool - will also allow you do other useful stuff, like turning on the SSH server. + If you boot with only a network cable connected, you'll have to find the IP + address of the Pi yourself, e.g. by looking in the client list on your + router/DHCP server. When you have found the Pi's IP address, you can SSH to + the IP address and login with the user ``pi`` and password ``raspberry``. + Once logged in, run ``sudo raspi-config`` to start the config tool as the + ``root`` user. -#. You can login to the default user using username ``pi`` and password - ``raspberry``. To become root, just enter ``sudo -i``. +#. Use the ``raspi-config`` tool to setup the basics of your Pi. You might want + to do one or more of the following: -#. To avoid a couple of potential problems with Mopidy, turn on IPv6 support: + - Expand the file system to fill the SD card. + - Change the password of the ``pi`` user. + - Change the time zone. - - Load the IPv6 kernel module now:: + Under "Advanced Options": - sudo modprobe ipv6 + - Set a hostname. + - Enable SSH if not already enabled. + - If your will use HDMI for display and 3.5mm jack for audio, force the + audio output to the 3.5mm jack. By default it will use HDMI for audio + output if an HDMI cable is connected and the 3.5mm jack if not. - - Add ``ipv6`` to ``/etc/modules`` to ensure the IPv6 kernel module is - loaded on boot:: + Once done, select "Finish" and restart your Pi. - echo ipv6 | sudo tee -a /etc/modules + If you want to change any settings later, you can simply rerun ``sudo + raspi-config``. -#. Since I have a HDMI cable connected, but want the sound on the analog sound - connector, I have to run:: - - sudo amixer cset numid=3 1 - - to force it to use analog output. ``1`` means analog, ``0`` means auto, and - is the default, while ``2`` means HDMI. You can test sound output - independent of Mopidy by running:: - - aplay /usr/share/sounds/alsa/Front_Center.wav - - If you hear a voice saying "Front Center", then your sound is working. - - To make the change to analog output stick, you can add the ``amixer`` - command to e.g. ``/etc/rc.local``, which will be executed when the system is - booting. +#. Once you've rebooted and has logged in as the ``pi`` user, you can enter + ``sudo -i`` to become ``root``. #. Install Mopidy and its dependencies as described in :ref:`debian-install`. @@ -79,76 +72,35 @@ you a lot better performance. starting at boot. -Appendix A: Fixing audio quality issues -======================================= +Testing sound output +==================== -As of about April 2013 the following steps should resolve any audio -issues for HDMI and analog without the use of an external USB sound -card. +You can test sound output independent of Mopidy by running:: -#. Ensure your system is up to date. On Debian based systems run:: + aplay /usr/share/sounds/alsa/Front_Center.wav - sudo apt-get update - sudo apt-get dist-upgrade +If you hear a voice saying "Front Center", then your sound is working. -#. Ensure you have a new enough firmware. On Debian based systems - `rpi-update `_ - can be used. +If you want to change your audio output setting, simply rerun ``sudo +raspi-config``. Alternatively, you can change the audio output setting +directly by running: -#. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the - following:: - - pcm.!default { - type hw - card 0 - } - ctl.!default { - type hw - card 0 - } - - Note that if you have an ``~/.asoundrc`` it will overide any global - settings from ``/etc/asound.conf``. - -#. For Mopidy to output audio directly to ALSA, instead of Jack which - GStreamer usually defaults to on Raspberry Pi, install the - ``gstreamer0.10-alsa`` package:: - - sudo apt-get install gstreamer0.10-alsa - - Then update your ``~/.config/mopidy/mopidy.conf`` to contain:: - - [audio] - output = alsasink - -Following these steps you should be able to get crackle free sound on either -HDMI or analog. Note that you might need to ensure that PulseAudio is no longer -running to get this working nicely. - -This recipe has been confirmed as working by a number of users on our issue -tracker and IRC. As a reference, the following versions where used for testing -this, however all newer and some older version are likely to work as we have -not determined the exact revision that fixed this:: - - $ uname -a - Linux raspberrypi 3.6.11+ #408 PREEMPT Wed Apr 10 20:33:39 BST 2013 armv6l GNU/Linux - - $ /opt/vc/bin/vcgencmd version - Apr 25 2013 01:07:36 - Copyright (c) 2012 Broadcom - version 386589 (release) - -The only remaining known issue is a slight gap in playback at track changes -this is likely due to gapless playback not being implemented and is being -worked on irrespective of Raspberry Pi related work. +- Auto (HDMI if connected, else 3.5mm jack): ``sudo amixer cset numid=3 0`` +- Use 3.5mm jack: ``sudo amixer cset numid=3 1`` +- Use HDMI: ``sudo amixer cset numid=3 2`` -Appendix B: Raspbmc not booting -=============================== +Workarounds for Raspbmc +======================= -Due to a dependency version problem where XBMC uses another version of libtag -than what Debian originally ships with, you might have to make some minor -changes for Raspbmc to start properly after installing Mopidy. +.. note:: + + This section is probably outdated. Get in contact if you can confirm that + this is no longer an issue or if this section needs other updates. + +Due to a dependency version problem where XBMC uses another version of +``libtag`` than what Debian originally ships with, you might have to make some +minor changes for Raspbmc to start properly after installing Mopidy. If you notice that XBMC is not starting but gets stuck in a loop, you need to make the following changes:: @@ -175,11 +127,16 @@ Please note that if you're running Xbian or another XBMC distribution these instructions might vary for your system. -Appendix C: Installation on XBian -================================= +Workarounds for XBian +===================== -Similar to the Raspbmc issue outlined in Appendix B, it's not possible to -install Mopidy on XBian without first resolving a dependency problem between +.. note:: + + This section is probably outdated. Get in contact if you can confirm that + this is no longer an issue or if this section needs other updates. + +Similar to the Raspbmc issue outlined above, it's not possible to install +Mopidy on XBian without first resolving a dependency problem between ``gstreamer0.10-plugins-good`` and ``libtag1c2a``. More information can be found in `this post `_. diff --git a/docs/installation/raspberrypi2.jpg b/docs/installation/raspberrypi2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8af91864176bcbeb0662e4624cf56d14342345ac GIT binary patch literal 97346 zcmeFZby!u;_b+~E0SO7ETT$t5P*Oxd>4pOw>Hvp0bQ(y5w6sdMbc50%AT3?eB^?rX z6Qb|hiv zv@{6B2vl}f->7Zn>TKPxLgCl81W6!YRjGcrJO z{3T5X&@?{3wWq(N4*|)bH)tSa;Ks_%&W7~nLQ6&fk9f!M^r9{#KrZ?vjSJ8iml!yJc@v5VIx7bn0t)$q4*i1$y}Q(njg=MQ z-G$84Q$UAz7Z&scRuYCcd6U=&mk(xo4r`Qa~L8wS+>8#dY_tQVe!C4Vdcsc?S16@N!MMFiohK7cQj(!c}Isw*oOw8-Vc=$L3u1sU*X1UH}@gnR=9kDBcvD*iJt8jUpp`}2_Z*JwraKNBi+|Dfa0w|Rk% zaq|`tG0E*a^mp&w=j7t%;pO8O6PJ*bdL;c=QAt@vRZU&Pzz||&Y+`C=3%9d(aCCBh z`O53Hw~w!1XjpheWYpW}m=8(GDXD4c8JPuzMa3nhW#tuh^$m?p%`L5MJ-vPX1B2g& zhNq@yzR%9hFDx!?Y;JAu?C$L!9G{?i zH5)44!)ItY zF1osgqGu9pC)w`;IEiy@4 zJM(zbg){1@M5Svc#t!lPX11GdBR7dc88aQ6O88nG_V+~dApR{~1`X)&!23KgJEqx& zp5g`=wGl6`o}`POYN~d#*kB@80q#wQc}jW3=4S(8ayqN4HuTKOa0d$d zjgKFH^dAq)ATQ6ZTr1-nd)~WXJ6P;wl(<17Cdd(6WUuM77R^k+tS;G^be#BPJ10`5 z+>zP9@w?aOM>+j+8$sCp$exaC9w~au6gUMkb%QFZ+$FL0zOGfX!8S@8!E&~%YEvIW z-`^tc=qPomZSnRGC@v047;IDt{q~kVaOREamL|jd(UkbomrH!p6|ol1=rE&%o*g(H!6{B zaQ$YEHgi)*7O{C((yCqM+u0X{pRu?`F<*WfMnV-%qYB<9GANOV4e1z)MT!(CwH_T29;4nV$g$ux(Gihygq_vnk5qWPLd|BIl>3iXj7$1R7=rH` zpcIG;FEaZf87#;zYO|Z!N;r31e?B9tsE^dr@cKb>P<02{aic-ShjJGzcNq6Dam3J* zJR}~4j-OSO;H|{AVtBPS7tyM{r-qu~0PQ=lrI*++Sgce?t}UGDJql7-g*EJu3Qlv4w1p2s5eak#=!K_}2=2 zPs6%UH;zZl4g{)PkQo6@Pn3?7{(P$VFR^cCa*3=wFeXYWUT>nrpiPj%%?&saxPCdf zfunyzWCC%?i=`vh(ikxI4)Jhv8l{(1=6;@RFCA`l#2h`5&I93-lZmz}m!`v*eP3(g zLWk>9k<;VUmZEhCr1HG2eDs3Hvh)u5f@4AQ%_jm;jWNr|fom#_!s4$bTj=b}v7?VX z-nI1}ad0i%;_3SS07RD$n(3Q=wu;8Hi0aMhT^RC2sDXGB59JiierN>sz&%#UXXInXvG_S}k9|N%{)FOl z-RU!JZ=AFB(C4W!{y9S5S^Mx2^jiUB+pPjV#Job+kVG_^f1=EL=MWc3!zfVSCV!R2 zwI$(37X`m)ssj!Q2U%NQ*~)R7staZNzm|UbOx2>orXJPAW;+p1%j8DrXQypi;RwGN z`0grsPcxm>qfZ=3xY-iD8O(Qx4b|^ys4#cQpu6ApY}V%h+R}U-6Tg$& zQ-n;ZfLg%~hqtt1zyfmZHvX#7Xo1y)FE8IN^1yiF3?E~Fw4mZJdtUVByya9Jc@=)& z4*gE<$tg(EPsT{{QSQ95(e=%AJHhF!bY8x88jkJcNv3|sqr+ICqIf9m;#BGih<2@rQ z`b=W@iGEdG{giffw={FEy0W`^50T%Eug|+IohMyXQ-&VGAjX8n1^2~>>eBgA+Hx(^ z>|AbD8K;lxIJ!x`p@W977fOBPRz(ved)dj*vF>VCq1Xs@(LV)28A7w(Zo0q16}i97 z7}S(8?gOJ{JC3&uUsJFO!rfjNTQD)*;}f9tA;J2D$=Nj4W%~7bV&->u$DDR$?Z@|+ zb70dd-|UOz=NeYOv91Vvb9viNcF>TEc524scj3i14B-d7z#gKdTkZNO|l zPt^<%LfJ824$Eu)_XD}bSsT`z3#%BfBOXtB@gX?hC7#5X=S)Of(l`bA=0#2`QUqHt zhA?=cb#{swSpA$4aM=BfG*B(@FiZwrC?UM9Tb=cJTb$=7q<}qMT25`Qch0e#&m^AF z*|vRru=(-!GZ|b|dBS$Hs3?sPwGS08CPUJgxC**RX(BEQ3|S$_Aq5lxtk8fB?C1Ca zoC?yn=x=Z`+CJPN}sWvDoae zuDD>!_md6j^ zwOp(Z2LqvnJmGhC=HGm4Z9%Jcri*gf-m56sT>DraY?uU1lpSNB+0(SZ`=q-c^)zUl z8|BsG@=&qkZ-cpLd%?P9S>M{=2G7(J1U#ljd{mF`G?|FTxo^@wUqN3SNl_~9HNN#M z*`a9+(`~Ok^Ux(Y7g_|g<+;l=ma@22N{|;i<5y07mDm!)R^lexplI?94SwTSMjJuz zciW-2d*F%F;driH+McH%;g_TR8Fz9!VFoIAv#)BlqPwd&P}XV~^?%Cls=+ovqxF?H z1wlgc6cGy*M>+)E8ce7icTT>-=f=lcZ#yz}4SqZYVMMMsIP<)svcRd^-RSaoLuWID zFAb#t(nSgmN^pb6U^4mnEmHgev zJTgonKLwf=(DjdA5VO5#pzubUh}-hZ^MDX(X*w0Y4Z);(azArQSraX9ymfszU6L!2 zE&HUIb6mG=V6#CBP$=cEQpCSzavds*moonweU1D$y+Vo2AxuHx!9Rk@z| zVva9+8PNBA?;_`emJTU>wyUM{@W!VcSYiA_1^A_;&N!cx1z;48LEVO-cDJk~Kg(z} z_Z`BB%r`~{OYmbl`do#QmzE?}U*WUOSZnbdZ}VkXub367@RG9lkys4-f_eRu1JNi*NZh%9fBsD0@O)pLTnwn+x3nt3eh& z+4B*12|EHU?YsG~MZa0bBbM|99j+cGJE@>Y%!Z7~fepFW==*TYw`6pZq~QWWQVqeT zV8)xwgY4I|XUmj_H{UInQd*;l9{;o_?9$RSGySo)yRKLy<59Y#(nJvOA_Z~M;aQ*Q_GqRplsaLK^(w&)hn)7iD(hq_hkZAfG+#_o z+gN|IJ#khDuO3;6I0aF4i!7HLG*Yk|zFKrv;CS+MP3b^GY$9Y&QAo2ErZKv2ZD40H zwa5(9d&JBmAme2taBAgqbt#)^b(4apCtkU{L649a3H8R}4?mg}@yKcx4FKCK>A zi-X>QSkcTJEC+@#!6qO4-lNdo1-}LbJ8RrU^y>6a8_U2SZkjyw`&9Aci8TXvv;$N5 zDJa2Wi~0!5u1~3rEwm`&(`MExSu&pb(k>2D7Gln>&eMST+X7@;A&Kvi{icd_y(0%d zRyw*L@5$hHD)ZovJ^oHqmBx6(%EplBI{zNHQFQ=ZQM;k?&JI1LrjLEy^3^)2av8-b zh~)vpw{=&p%x2`swssdNRsClhjxvubK9L}3tbhHXWmJ0gaJ^m1ctM1SiE#|t4B6z% zeTuu`-)n9kyMtZK{Yy$Iao{kt<4mYW5vfJ!!h!oJ#K(19P6p7R8#7AN^ zq0NtF+*qW+iL509-*|`9%6XgHc#omCG=F&eG1J!E-QC;e0h7r1yK)mtQ%yOY+##aq ze+kPPGEp5k6bxyleicxOl^oW|zcF|U%2P!bP3rcQUhUf+q4bh5P1S18f#PS}?^Kj0 zy~#G1ffAcGv#z5`kvdLP{>Btlyi6T4xd37Ajx&rl2-R_KzAU%0K0#zqQKOGZVPiI5 z#_3Lg{8rUNNp}UO#RM!X=Z@-=lxczK-SyBD_)4bC{Wv_$-W(pT4nyIX1m7R8r)Lxg zn2$zEp&7RwoU}I&Mn2P)wajnM& zdQ!uaLwTaf@=>kqDc_bgq3sBvxLDN_6L>zPr8>T^R9;M;tuOz+t(>(bSBUhbxJI+_pN7k&v#8PH1rglkh31QJB)=W{+A?OVJRW zl8xji9X%Uq!LWoK4pVw7xdF)fEyuYT?Mi;hko(DL$4{i2C}k8hQhFXl|?Vk`|f zb2_`Nyp}c!A04e(ZFuAeO4PDzwlSN3RYhy?p(O{3*Y3Q}A&0F(9bA!ck_Ie#&lpVz z9}p#}54B5t;~XM{k8v{zgM9eb%ytNrLC+)z{*sh9h9BB^8To2aK2v=5fUJ1$mHCWO z`Nv32zS?U18%J3kg3>?cG>?^*{FZNb|NKJ0yfzwOz?)}Fb>pS-c+X*hU}m%Fj+)&JWGpbyZ<}e4ctAu?h99@H zn~ups=!22N|GGP%!1LC>iv_j*U8W$(^7SFQO8~cOQV>FM6vLsQY~Fwnr9Er zi$(vedt2bAOPU|4^nj?LUq}7XyBm=hR#xbOgAPfK^AG#xWK8U0q5e@4ldfLDR7Yv= zXHslP*1dByq}-$=?F@j?ep8nL$3|fAmfhstygOT*Zjz+F4_o}qn|VcaFey6&$lUYf zLsO1*vUYm=V#2~AR%)cahOxxiWFr}n^%<<*$fujap=9AD?u0h3R#{AUU6&!RKCT_s ze^wN2TOK?+xL~b49=O3G)kmyX#R9f*+?*pK-bSmoXOOj(@a7+iZ?LhHFrTN*WoYm) zF^^>;J*kvA1-(mg5xn(ul*-XinqW^cW^2Na`>B}&2_#J1C_&P(hqav65%h5n#MWhQ zl*3s{JSB|WJMdErOXz*Y3&PRv=Vg)f7;Jr;QiR*Lyaid@8L9k^?ow59_r||ZHd5|# zd!OG-Zmq71Eon+ckHW@(`oLtRwp*Lj2NF^Ev#@4n~V z4}PHklng_p?ythH3pMJQxnuWok>z_SA3w@Boc8Vm6a?)CD-j^tXJwa8; z{F?3^qBZ!~W5_Fy_Hxmd@zDgdo@$b|PHoE#*ji4KSA#ycU8lNje7O)R)D8|s=F_wz zw7X&OG!umMF-DCAzkehi{7{O-O(yi7Q!L#I=OcTC`L zD}ELhOIv2JAxs~_Yyh)haRghjuraf;fCNPxt-uCQ2%JhEVr*(DM895HOHXBLC`7Ny zCCe&nB?d7uedKHdQFNA5GH`|(@EOvJ2xAC3@;h2sSwP@mDn|=*OIvtqQBJ2F|? z(gG4NsLo}GLu?IfOs(LiFw09>U^}3~e`^;6`0nMrVF2Px2P!H-fZ|t#*qDON>O;|tQ_1Ne1a_hDE!6o1Y&6`ZD|A(X6IyKqvBx&K3G{;&(a0cRr#L`!V;3~ zoJ>5d78X=SEWo!aPpFg?Wq;$Z)H!Ds7iQ=9Q<(Twv9tx)7;>rXCF0MlUo}3ova*3W znp%M25J{MUovpa3EgWoV01@V7X92$8xTyEP1pSgCX%4nE5rbJ+0SyM9nOGQT>jm#W zi>{bVZ7dwXHV|QUW;SN_OZ2b2U!ebI^ndisFVpJN@x=){e7BS(u@zk&`&kPT{lfu$b7`nK)RL*x2|vdHK1x zG%k4l0R7?56myN|Evoz?HGdL;D1Q{n>K&KehEIW3udDPgP99I zJewq`o|?emFk2It6_uD6)mhu9?#hAV$FuOP_=RKq{}nDRCic_@W@Ks(xwM*slql6*U`FQ!O!Qmo zAF#jjpLNwmHyi%Ne=hxi88G{wa{h$>RnY(8Hu>-P4lwLwW&ho8ehpp=i_4|&9Lq0n zV+yS7U~_R`k+6VR!llK9ABsvyvPrPB^Kf#D@vyRUi%N3vh;xXub8<diRu0o-U3FPdNUlt672%n!1 z5oGzDac2IDCt&28UzGpt8Lj=x;8Toqk` z&t@GZQ;YKz?yu)#Sb^E;yaiW~E2gu6$_8R|8+AXNwC!pQFwnE@79|{EM`U8Bzk6MO7d+=i~Z}rn-Cp&d{*~ zZ%yZJ^C#v?zh8a-YcW2Lcm8(f>yrN;(0>R40zXr@sWapvYWmMoeq*o1D4N*8;Sig1 zd#QlU?I6NTtc)DIR1zvc@OIvfK=^jvlz$?xggmr@*ua6#zYxI7$j5%pf6=$U`2PyJ z;{DadqNZTm3w~BcR#t%j%7Pb;^}GCvRTk{{SD_pioK$y#niu7A)C6^RLk7FKXwZh!xnz7NX>21;ip}%gR|)aVxrl5zg}pa^U;L4d^>us#rrQbBBN zU^Y~D?JS{|Fb7L2`!hGB;^yMw;QG~te{fw%0;EIC)x?3-*z$Y`a&!Kw{O=`yWl{gl z0_^)->U+-eN68h-dFMSf2U|k^T#h+-Ir$jb+1Pl0HTSHG|AG1!A%8B)9DJ<2jBK30 zR`%aD{R#RP8Gnmp{)lmYi})MnN{X1BEgWW{0I@WL*Z@<89L&Z7=m22v$N&gXE*tYV z%oXpik$OH_E@omX$mO(u$@&+_6{D(&DI6jS28;;w`a?T7jLP9Ghf4n<>yq~`s4HGY z1BfL=3~Y5ay_}6WVMSnH%Mfg1c**sT@_&E(zL@2K4M<=slI3c{5_q`U@&rsGL~nDp zp?Q8@6$ugX><_q*fj^XMXD2^VFwoFYuU*HuejO776B7#?4;Ksj1~w)p?oHeq_ymN6 zgx7Iy5#1yp!XqFgI6tq71n?lEprfFm6JTLt5&XBW(>mb1DgqSI6A1x0s`=}@Dj9I7 z7EtZ#uqtr069p9+4LG`a4LJ2l3Y=m+KlXWX;`8kD@-*w|<!eR1n!2do9 zVO}-G^FN437)U?jR3`HkN)mXyX-oBABas=Z1Qg3xUnW1#I`aX7vmt_BZ3`1iWd2*i($tzCtVg`AImZsTJ~ zNA)tjB#|EE7rxapD9|@fFJJ(1p;u9ejLVXWu;9xPniEj_S4p0}vw;>Hqbl+(!oB!e zS-NhC5lOW=8`R`k8th#YSmrSjYo(u4?(M`TN;)a`_MYt&@QnAB%eQOt+YY36*1frhA;tEROrii`kG?~6a%&Y{TN;6Og$(|kg4_LGJ4 z3k@xhUJG2>5!0>N<6R4W=aAJp>*GPD_Mzwl0^D*D2S*O)> zQ2pD13I#L!(5I#Q|YdK(_OU*$j+><^x!_UfZyg#&W{|~=iN`KEwrJzj&=zfLLr^1KeBh< z?o|lW<3-jL-e=NF?vT;fh`#B0u9lvG8?81ckEHU5B0-RMmFx^TIZ=ylzC!Nn zIbj;#OA%hH8etei$>TwImo$T;%b{|C54VX$V`iyk~|-$st-$pdO|A{>9vj{ zKCxw)`wb^Lw{ESv#LU@GWjWJQHm)2xa#Z}>rzs>=?XnoFp z!Y`0`4RaMTxAbM9Go?1(z3XfLu#>fKPIFpRVutaia7p~~QH>b(Fxap|F)bus%a!Ka zGq;F537z7P9Sb2*kc{k&TT}f3Beb&6XlOTgf8n%wQBk}>yY%ok1pygz@*l0)KcAR5 zH#pEMGY-w8X4{FB*fzJn|5zd=Xhe#&C9tgUrgGEMZKp+E`-wIIDvpIle|dUgrtLent`j(% zRPxz^9UeE+6(3_672{7EGU;WnplfoJ24^(BbbS83)SFWGtNMuk?&cO1U`d(->t3y+ za2)*@mZtn3~H`j`o7X2yKB;DderPyfMBipin z4w%KO-=WT8gV!wz3*#r&xY+qqztLi0;qoS#EL@* zu;$>~ROCEhoktGV>Yx|ou$}+XA$UKRBSLo5ncvo72RMpe@5_$h zgs*dk6^7*xYX?z({_+H_K@!!GQaP5%)@wLi>c-yP?WQ}sYbkHr?!8ehlJ?WdX0I(@ zb7b`?2xRvmr@cN$U@l`Ko5CJ&*z5|fHv_TjUER&y?;Yw4zScR1H*L7dMlEb8TLjjO z?wll$xN(%Pm(wJENkTCF582q++#WDurpuV3D zitt{%{EzhBMO&hkhG`#~HMFW~zU3r6+qH!;%dTn1jhT6k_;>aVzTJ9OGP$|%eIrd$ z5bCX+bZ^nASvO}rf^BW8qcjYx>N2xxD4XFh6I5T=f!Dv7X^~*?;--_Kfy>0LN{Q7U zCAW9iqvT5JlStz5>}wL8@i-^O^=C}V@AR`Rb2i1)w->XRcFC$%3(Q;OsFIH!j%Y%2 zWsaGixSMWPvlJZ5F%Qot9cJD1-|KXaRUDiGVaIJL9rZp+e7@)ECKr&{FUZ)Hzq!l{ z%{e$&7D~{4P2Oi2w75#!%phvBSm7{V264eIWZ`V{$9+4%x3R1~<+Hl!Je&~X9-^hO z?$bLS?=S)j4ix0)D-os?tIqEc5imVT%yl*|*?J;-w6EsX(hbP;?M!(X9~{)2s0%@7WE`bmSBShm z1@YKPamUwpzuui1PIC&X_=(TsIzR0ykDga;5o|V{B~gmvd{i83Rj-qv(*D}symE?T z=XTbz8&~+uya@H;EuD$jg!&}ZO1Wj-$BXDOib+9tO7nXQ^%N_Igs49^RJ>7+cau>x zOH$cVNYh}uv3fJyH~j68$u~C6&Z8=>_Pnru-PmKK4s&^f&aXOXY5Bs5Z39O?HID?e zO&2Wl^zfv&sDDAdyJz8BKv&pNzAviG&kB?&7d|+``GRp zIguhcgx2f_jiZ!lP|_lCjJI8z4mVpZTXDS|AL)vFtmPVtpp_r~3hqMemMXpi)^bgHN92TF29 zvbvOGRRzk9%b~u5uS%4BY*kq%z~5H}-S&@}{e>(dWSoYp+QXq$MSc~Qg&&QI;?_5} z^Mq>bhkH8)w+C=24Hk~LYH11sPppS4HVN8QT~}v5^(J-*f!9+kEaOY%6{I~lcYKza zsXzIW(Z90i<&W2Qy~mlU6HrXd#h_?nwVv9a!Md%O`ux z)dEOgXQPUM*-;!~R>PCg_@UyU!l}Y(pEGiKYmuV#dUH;SymxPrl!SchMn3EDXr)#m zm?u*++HcLiMZ>1#AfYg0RefdMeQEmBMC4Tak{h*JTk?{huIP@jeNSPSwo~$k9F#}o zHuzPAh26?f%$_a&*CjjLX$Z)heH9y9uKC^+g|)C$kcL?`gXY#D>{#9MFtx&ZAYN^O zg{RaG?zF>#Q}A;zJ29sfl9G}ItTALn4cZ?_2s7`)p3F#~CT4Q1x0yT+gTu21-`I>bDkLC3MTO#yLQJ{NzNfKl;%+3yXbeOIc%cmu& z?iwhhWDNHr>gy&k)pmDf3+l>=<)M!jjk>vx>@o58%XP`e?4;aO5D>Rl8Nc7E0)M3` zP)QV*8XdkOV{?odtn^c6d$8#al~0kBP6l-wf4dKU>o@|PV6R@=u7Jv&zJrkuZ^n5% zdgJ4Uw|eV!Vi>v>M-uM*l=?auv29`PuMS>S2b2S@CA}v~V9`u&+vv%Gj`(-^iv!08 zb#McvdZ(IkJ`s@*FUR8RifSCJR!?+>o9U%mfA-uHI3}IN8-c9F_--ygPSL02+uahU<4k1l-eT~dkCA^4?A9OHtx|pCg+9DarrQ(Lwnu$UWe^{gQJ9f5!_LABkhK%BxxV<)O58O03go;l=D$l~SgS(1~No!q8vBrr<=z*v; z{@s)pgLol4lkh~_EQHOwR!0G)Qosl*ALHVTzeK?Xf zI@!L^Xn1@>F7pD*^ZfTc#5%-ALf6xTKhCa{Om%O=WbMc1 z(HM*^p0%Ra=Smj0_mq=-dSJ`e`drgBex&`qld~h5fr}Mf7Cfsf*f|7Cnd=xBRPRxs z|J^xf#sE}kFqAbVXi-&)n~xVu9bVQLEzYJ;S$C|l+B|eRX?FUti}v{#jlu!$kl!A% zTVu4mlQd9Bo8!p$ivDKx%5If`g6&>f{%VR-DA5fOd^jYFy|+9je);7ez`_YT3OkY(L8PD<2LVL$~%5N;G9Qf-|-@YJ#7-4c=@& zNC=oH*;q?7m~UHTuQdK5Pai>#hPkgWPAq2`lGvRf*+2RHv+sUXl8|;pOGi#kLSC@^ z?2ZHM{i`EWXspqI)0Hb;)QeEZTI6#tm*C?Rij0D(P<~EjOONe5efxLgQm=(Rx1-Ej z%?`Dn9yGL-(ki8xgU$v;O&VJq3-;c3aJUn2qn6uWZGy$76XJ^9`Ryc|Q!5oid0-NE z`0w3!`A1bCH0b~V+PN}?@0;0u1;qmQe$4j_e$Hv0Uq_N-(q6La5gJ!>|Jlq_ADFh2 z_`#GEl`|&LrD4+sy9ZgH{(JnQbaMX(Se3wWNGgY0^iQ#BbG~1u`*ok3B9q2e?7v|b zv!>!RI~b6sQ!lqa!}a-*Yq?#pgV3$9xgQe+GB^f#Qp{b8lfz0qvk3>*;32`JqWlh| zR5*Ev4ovE6cf@>6*p9QxsOK-Szwg-CZ$A6x8JGn!JrRdbS$&W0LhawL^IjN;SXSZ4 zEZ`|llOC~#HfhQ>C4>m~-e@@}ZLfpsimvu|STE*8>g@cj%P$4nNix1qWgt}4H?w;* z8&TOf$kV2H$d-3Z8Sk~Ak(Vf6JqX#O6yB2_E;Zl53GB^x8yDcz`Q+TN`CG9+D1?6U z+vFzC83-THB#C!1steni>j+X;13#0&8JJH|89Pd-eRt@xBAhKcUP@H;U5MuFrRLu* zE4#PN;f4eG7#2oJ*Rdak9V|a_t)!0;Jc$y~%~o^CbBm7mUH?6W{9OdER9w<3u9po> zr3ycXQF&yv+4yvHy2IIz`2YPvs1r=_bEM6dPcFD#X8&K~{+;`eTq~Qa2M=Jgk{fuP z0Dejj{0JQp0|EJLOA`qh5%@m~s5kKN(QpX}si<$#(6X|zb3Aye$4z&K>)~xqD^Fk_ z^BS&7#gn z8mImI7T^X; z%3L~4cY7zExkyLqWq7KPT4zby$_t&H=Iws0>($B#MME&`PXjkgmj(}N%lxgpr5QBI zPqOU_)&)xGa#R1LvOe}t z@=~-az~mW&8kpHVzgr%pV$C(ERN6PR?XqJ{7VuqWVdQcATB3WLT!TC;P;f}JZE!*3 z6Dv3P=K$lN+6xmY;2&z#8>qSAG$1f!QF!hu5*8xrPPMy_dVas(EFfLU%^)sIt-DPl zFvor0FT~&f=9e#hfxc#`?4v#W$vj)q^#OK+?sb_u5+6EQ1H4v!G(u$Vk0+Q=Vv(nrPEEg^DDVl!&YJ;yKC(?~#rt zWihOrSTujJ;x>PY^tD}$A;SL+opj!sky<_TPr|Gjmv6%uDuZt+j z4%JpZYnf_RkZRLGDv%57OD1`KK7Q&7DcMq4ytAQcoBG!^5t}KlQ&2O*2UE-r#|0Cn zSu0_Quk;f3t<>NMm5;^2li-j{0aAUTTH+kns`GMr>O53xH@7c5?6U@ ziA^1=P1Y+z`AUZF%`R`m4ko@4HHeMYAN|eIn`Hj@0o0 z1PvLOF%YkNMxG?vjv8IqHXOf{?=G&$75J>zE}YU3@z?doW2aA$2Vhe?PmfQa)OnN1a#t6oQ8DG6|NfoKYCQJw&A4MXg*C46im~x`?ZndlQ2B()XLm{Ae`BcvwgGuS?(d&`d9ZPbNO-o8w!1cJev( z-Md5VUBs8Wa|L%&R;E(sXRobyIPiZCB7aUe^R(amW-MbwMJ2*~V6(?m$qZy9t1(Xvoe zK2E&zttYdgcsFd!#cHs6A+nTsK23;tbv}uaB!>3!kb?o;AWS-)e?~2T@2+zCk{R!& zR!o(#q_6M4HlyM$tuC1#x(uBs8YSrIa@ znE^BVkd;u-$8;}McLp0yRE`! z|JM5pGWS6Ip1CA)Iguo=4+VL0RZp}n*9z=VDl`>LnFNmvthVmtK6}0PxNW1)zhID+ zjQm4Sc}EXcNj{vb<0qz z6FH7dsQJhJoEi>2v=M^x%CWfBYaGXgFK1_M*EZmW;n$=)pmOsNs$rP(y%#K9=#dqj zNG3EXefN|Z`{bL&5o=VnLEd&aK zBcoOO9e(_8N7%zAbiRsmPd4EWw=`=unqQB$>x;5t&Ws;YaW2NNMR)%8R9QVF!F&-( zk4kK3Yn+{cIDD~{?U*f`gY>B3C&){-s-aPqQx40q&)qqJgjJuLOct7tQ(vD9z3=d} zOJ&C^pa?`$&-!ephmF!FTk8H{+lZ}*y^K?cmy;fcD%jwej9lUa3<^`T>x1j0A2FPl z5{iFd%;3srlyMfII_OD1ZMt~}mGN~)WW*cfDI5VAAGe6NA1tm*ey&&veg@5sk~Wep zSd0ya%sss+(;u4f*sArFz|hEETeZ(#0c%x>g&AFUDLLol;HOL|8PnTux?_XETCUKp zM1d_L8RuKVw%OH5ntm_TR?0iF!Q+u_b`1)F@1c=q25(*UwDkA|RfV5R?IGF z93D}yj>Cnf@N+_3<1H4sosd(Il)@f5 z=>|no^3(F}{F3;Ay_mj~sPuA;r(-8FH8kdOPc*`3*fH=1j-A%_frlQ z1IZASf4?84o-#ahfLEd1?@7(KbR(+*rMuR;dcSFH2CHTUi(q_NZ7j2IIkg&+LbCcv zg5fL0-N$~M=1y+o^2zBBxlJDGu!at^Ky*jHlel}8D8A5;&tLeG!3r={xmO1*#EmGasaknZ9c%Kp~F5ed-kc|yyHTyjFL6Af+P;iQOsihdF+$+jS8A%KFFHkvI?`b3o;EuHdkVvh9t&g_n89G6z|*5KII~)+hWN=G3>dOd&y<) zx)EB!IdBQQ=x$b(84j$gCEY(5Tv1-^R|C~)r{6Xz?bB*&pdNo(joJHC;n^Hhbf7cM z_Vt9fkK|c%Z*L%W2&$H5xF8|N_YGLMnVeXMsfpKre7o^DWv#K7ri((UvY41$!vpSB zs+6J=At#(0oy6ue0PAhB?%vs!IVs`%8j3lNp!1na`q)gAW7<=x)}ux#Jh8pEPDz1R|Dd9MVUT08gGw| z4HvoEQdTYI$DM5SV?n)=__of1wDG{l9##`?r$$JUvJ0@v9;$f@QORt%)5k9GkB{4UcHSdu#lahbBt7b(9#G zt(45=hH;-~&nuDmD@p7seTQ?@u@MOAzU&lw8vmN74pj~Kb*El7>WfVQ(+?5WJO}HJ zb}e{8?-yHLD$PV`H&|o)?RJC$NEhB#HpM7LX~v||D@d$hTLyVYW1y_o8n?Rgyx!}- zso`2OCb{YpXu{Bw^*%5fC1;89RmBz(V4qFLKFgDa}-F^ zqV){5W&0?-4Sm+K)Z4a{BUq_AvZ;{2==r>Zea>*UUrkrRHK*0P7pw8C>I~Ukp$Cpo8`g4!Zc}e-yn~m*~8KA6oy?m8i$~NqF|6i`;x>R*C3MY+8h& zWH;+~-OesDw-030qP9JcPC-ltdL5MfIS=e|q_q2va}A~vU%&@~^OpimCe(o$}~0Iw*Q9si+8CUfjGUeQKDk;6M^{QGf| z#vlh~mSC&LMRC=}u`}WG&DHz;fuAA>yVUv@ak_Yz)<{P~aWn7PYYpA5K1_L`_dbSf zBsi$1ipz7fuso)a_!zVQQdMwooukS3`zRVWH3ar1ojrD zpC?iF@jdq|Q=|MTqjxg>?dySdRU!oq0AiVxLTAgbBIl+e%u>~kkt}n+^ z--mkcs%;}))1yTMW6!>xGZ&{p)sGq?*=67(hEWr}CY|W&YVUOF5)l#c@3d5}cs@FB zS+-#OV~PKmK?q4mdFc_B9^@1xQ-+ISAvZWpe`M<|diPdbwmM3i6GzchS@WK-;P=6R zA3ora@eMz$Fp>{cHrTugh(GA+?&iEs=?su9Ho>^J`$53(IAD0l3uOikNB4Fm-m{bt^=d5dHMd(1 z1@d-3WQ40^Omh>}>>m#;hMMTlQa(y{Yb}>zHFhNN>0I5ILEun zrnQba=uT7`HIDqQj^I?ptBv93_+;}|7V7aIE`zUN*L?6R(eP*QFVWQBNQyQe)ne5b@C?VEmrY|E~&`iV`^MibV_mAp%jR;Bko zXE{sATRp6o{{UxyCKU@~-9Z=4r)d7vP$-AxVM2wji_0>urdN{1+6#IL^I~f&6}7#@ zQSH7Wdb5~3zT&b7I|VsfV>HkvGeNlEn0~hv>tQ`S2CCKfF#p|&kGqO9Lxcj2A<^(}Ar4Ads%mQzXLjx|Hu zd_bM9_b)hwi7U%r@i4397k;J7KM>8W^#1@3SDY>>Ibyie&wVZ`3Cyd*#DBireB^;gQl><@BX4+KSrck5@5lbMEqp_x5Ungqgro% zbJL#QXj|U+bxEu4j*52Ol0$_+QLr!~qVpt|TTs@u{t{5su(j|eHAfj^eExkKC{1pm zu;)uq7tA`>E6TI1J&muj7AWa(InaF@CwvoLlA-3=k21EUI%Rc)=N6uGb#g1f2p$1T87U*Y_c@Itjy>*BIug=lluPv5x?bm9r(e|*PQ1tCBxEKFgx2)L`?~K z6KW&pU|&Plwf-VujPZ*2?-{2Yv5G$!)j1)7uGhW#JzVS=;@RGn<@Z&V)-aOPo{zR$ zo@%cyo<8Ms1YiPaYCQE))~!1&3uKZ8@gcWVL*hrd3hO{vhtK7yeEfc2PwCV)av$bj z>wHE0`mauZF8Jj{{gip>^!IBdB^C-%j-o9`{AT0lCLM1krfq3w({DcA?ql@w!&UEF z(!^d!-ncyapq36$t!Eec4{vQ%G_DcWwXJ~$vUJT3JhkuIUasq4eJO0aIj!sE=kXKz z-FkAd>v}qvS3m4$IY`m;s9~T)dXBZP@%}T@o}BTUhNXtC5pjw<`!@BHOoGw2QAk(Q z2H@pwj-KU0Y_a4gtR-71klQo66{{TdQ|ZVV)OP#uk5;=l76y3dob&Ua^{@R&*ce|? zSw}-FL!g`z3UQx}Yy7_9t>G2av5L<)-8ZWnsU$ECJ6#}lF)pJD+ih6#I^G(k#Uj$O zk&Gx$aQe#mE7$Eq*7ctB7P(!Vi8U>G&10teVf7YL)n7Nxk=j(1fCQp>2zqt?XT}kD z&kb9tV^q}~Aa_cZ8?(;Z`DqORRA%Eo)b0 zj?w6Q8Cp%7MYQqkmimL%R#~ii&Xl8pZj>ZCjodw)@{PK~|jYx=gbeyygbx2} z_Z)(LG6`bn?pswY4_c3p^p6>_>v<&MEZuxJnyntQ=wXmwP|qL!asR{sLlFQ00|EjE z0}2EM0|NyB000310ucit5-|i2A~Hcy@DpKiATUCKk)g4{(cu&$Gf)LULtybFvch6= zfE81MlA{0G00;pC0RcY&{{X<~kL>ri0(d#2hY04H++W$Xcq*0{Q%i8mlSc4GxLtJj zVk!n1;(#YF?3$a&*&rsFK<17}y2YqrF^O)>BGNNXBOrrhPxc+%W{EJ2mt;=NCV+t8jE65r*0XXyrlV>XqMM$h!A!OjMCOjeten%DBt?^3 zi}zs8Y6QZmQwi+VaapYxEhy;(GzJob1xTEeG?&u0E_tv0O1-SV8v3NQ=Ez@MqC_As zDDIUvmB6kc42tC|D|a9h_mhmZQvz}rx^h>1D>J?&-U=rR?AU#BncM^qm4R& z@xc6%eaz2yU3!)V4W4SHQ-I;_S9^&UQyD5Qf*e*L_Xhe)8|DSqZqRVL#Rmsvs!%AI zSU?I1h0QeVwpXPK(L_s;$fjn!BuuqZIRb&&Pi_{>qD!GNCJv&~GxJ&Nl|Rk_D+q4# zFX7~A4uBiLrqj?{N@jZC6k>6~Ze0(OZZPi6mr&7}PJnhw#90l435|<3$<@0sY17Q0 z%vX6(21q@_ifw*j2Zv>^65^c^WEy5xO)72{c$PpE2HY=2HpQrBkLiU>!3n#`z+sS} zxCF}>I*VWf&&idakqR|Vq9I4}!IqMX<8@Dv=Y|WS4wCQ%hhEQ*f^YDgV-YJ#q4;}E zlTH_6YOPoageri*X=U$Nj_))n7{9q10wukB6-GQ<*n3c_;EigBxiGrr*?eQ7gE2=1 ziOevDk{X6D388GYU>MXeiqWKy5CVCUCd6)KIxgp~3fwAUi%l4l~Dqd1H}qTa~u1x}?rQs)$0<03Lg#Rnit zH5f&#g>)-50uEZkp=MMFvNGsE!<6j8m@Pb$cgXQxfK>BhAF1s`?1Vo}y7E|I7KMQM zR^p3nq7ZQi-LMmmF&^~#m%znApOA4}`TRDO39R<2q!(Mrjw#gx8hNU-c3TS&X~I?#L>1YAaGL14I4T)7LX)|Lf(iyMn^=S^jz~B=STLqd z(TK8wP_vZFNDC|x!8CqtkPyqE`fb+il%^T7z!7S}+^}+3NJ17KMPZBNg+Yr8$E{!o zBBD2wo?T|A7SUv_92V%rJ3D#$S0$w#hk?)hr#{d!*9c|;ewH2jVrIvbr@4=7ON^CP zsgQG&0HgqMhaRCkI-Q!BYK@|7i>Vr)mg98y3c{On4&tizEqMS>udXStby0$tw?$FW zLjc8^YY9}pg0uRMT17ERp<_(P6@zN{g@#Z#N_@bC=nVl%l7V#6o7duEZ!>p6u8L>=XuVQ0JVGg=uLo|FNgn9O9P3-`uweb7; zy{Nlwo+DgZN~@#_BNroZs#mF3XMztUz2l0M=aAOaZfz)bH5l69TjrcQckG{1Xl)Wh zuRIepdL-h*5#F)j1WeDdQ_*o8L?%*e1T|d;N;O{)O_zdn<>ZzhM5nt8u%mtm0^a1> zgmP|^c(P16q$o1`$H;Ow2PSVEMM@+>AfIlF9X_P}7u!|Z`ZBsIzP`M8_=LU&!vUO^YIB5tR z{>w$CDV}8Jp5XI@iv?bjMJQ;LY19Xp>z~MkTIR@zR4HI@B2*qI02M}xpoHJy?S1O6 z)8eXBGY+!TNpT8eh#sULV{`0UFl%Yyo2b+bDb?eub>(uZS2GP&1NbJ>4{U;tkA=1p zgf@HAW~Iu)%3=-ATvN>^vkw(WKDmaQD>22qP^XUhr_n9R5L=o8=&&I{q)aRVY^V+} zqB0(4%VeP0M8@J6Y6~nD#4=c<_EhuhH7%n_xbv`DH|n)h*r;2cEBIIh0H!4SI^dk* zOq57ylage*pJ~s{RMLdtrLA;H>oz(|5<7_KdwKA%a zPdw8OA;>MWQh0Tm6)yo)Bso@mVROB!NN_E~5)@K#jEZ=`V>B=bzh1n(s(F2#4#ojZo2lWbpM13SpGO zr=Ydt^K(?G($UFl?wfDlA_7{$7Y>fl{QIEksup&>_apSu9-|dxvnsp z+#)4iKv+2}BzYjLGm?TeX0L`O0&~l@8DKr%bwQ!EFombv{{XQ60B}?{I4uTskz;VHw64^&LZtBVDC)MNIjkXJC`T0m zSPQI2h_WpiqTCzExW6T4LwKze9@<@%Sh5d8zKiavxK$~sBmCEboRabEH+qO ziws>-phM0s8l`xlIfPt6Djf0%;E2Acl?o+4N1TeQLD)-3R9fO3&TD>!xGIj?WnO8` z{{T}CLZhX@3gZBK_$O&jr053S_xqd@U-ww z(ZSs+iw!Xc%)mvZagwomXM(*`B{}$P(REIg98S$rAD9-Mm>=~w{lQZ7yb5#RH@*;W zXZe*g>#p^I)m=3*>!?)POMq41iO@0`c~oG#3F5RIS_?Z0!owrMJ4|gcv3XiedZwWj zdh5YC!oC-08iPxW1kyvT!C}PgM>X2DyE&}H1)&Od#^hW{!Yr)IDJL`u z3Uy)CgmO;M>EUo?r61wWbso-c7Mnrz9LkPP*sDC#TOst-UAhf=p-`3p4b?3j&p4_e z(9nsL)J&?)Yo6>`&$&=@X*{`t>CbI=FABUgVxX!q>}ESJ#5V>O+?7s?8VjA5xdDjs zRP=r)%auC3Rk9k$GXX&h$5V@%((XzR6zZ42%=*nWwDfj%kGU#_H|CuDEx_ON_F74k zPsA#Fukcjd8%dR&cQ_`nlZvW+M+$&5A@4i@=ik9eq;Yy*54tx#Q%(~pmDu|&BSWI| ziXijItS-VEJ44^s6lpX#k`bp-xm`A|WZ}YA%`k>4ndG!)x3d2LP;5|eB;YUyG&F)U zO?PQXRj42+EjrL?7=>Dk=)1Jws5I2sV2^^KqP3e* zb(UIyQ^Qt~`mWWD!4lKF5H%z6-h*JvjFifKp_e983=!tRAqR6-IU))M9Vik_^|7)J zE=V}Nrw>1ZrA@H|HQ9lBnGUR5s_^!J{M9h$ zxvbCM_fFB8+ZCdKG&Z%o4_^dnH!)(GYHbHzJ>g<$?os3hb5n~Yl7$hPC238>AvBr| z!pBT=LX-5c9a71Zn@m7csv|T?4V<-AAi3O7Y5_upPKkZWV;ao7_#`0iYQ;vXc{{;1 z;)?@wB%sjUx7j`A#!vXwOY^GWZbLt(imm#T{C-6=#W$@N8?BRLFQ0Jtr@R4$xpV8U zg2f^{&?_446PO8+WY=Lbss>fFf^LTDK#C5r;BG$2*y%V5jDg+BQm8O9pTF*%q%oAk zMe|w}GBk5c=QHmmf7*`haYqo$N)SfDf{UEp=E&~r>Va$bN`;7<9YL!swE(LDP!JZ6 zp^AG)HDRa;Rv98xjwqfe!Oc5IY28nt=H!TlfU0^w7a#zz{!Ciyv@UUKX6w8a7B#IE z9hvLcp7P^5uU5^=n$Zh!5Zs5+X$Bm_-|{KVETTl*r_|W)_^B?r{zf5UITcq#p;u}k z`xCS}z-*jBV=Q(tXLXvPRB04NR@m`Dh=@*P<`g-*r4kS7w*3C-p#K0(@;m;&Swg1J zV}uj$SYtFype0WwIM-){NkptAVdkYzBvfj$pJL5v4B3{Pl^R8@lM6KpKaq&d5~=>H zyvf!ly-zb~+qa zXa@L&TAfG9rezlwG)O>5mEfI8q|X&l7T`RO+vckXs7HD>Pc1iUW3f)Sgy}p`P*ihR zz{z%vIfP@;QO{j?v57nrF%6@daN;u8WgdGiK+D4hWi$0%;67=)kL!p|BMVP(_#iu5 zoz(1AwrSXN`X*Bh7(y=WsguN^{Kjju9*PwzHpb{WnagCLUU!Bdh6iL|s-m?xb6Y<& z>X@=snBu@d;yx;?u~a&nBmvx-=LehyLF+UsUK}A>Ev%SbqmeUnsiINORBAEBHb}Y$ zfyox(@+DbEnayREr&KO>3R&^)`}9_gOjB?Jy)*S6>P*~!TtXk1(D!bDJ_=n}+jMnh zQO&1H7%#)_cm+fpIO-d7=isZ=#|<9O=H);t3vSZ8Q~g0Hjvvx;b|YssR1X{zPCccG zgLtOt45gP3X+`DryG*Cq_VucOkRcq-te0mSrWC!oiSR+~b+t5>qkHO|SaByXG-~v9 zcy-sQ9h~)mU6xDo@?DhfnMI^$)0$eVoLOjqZ%xlHM!scD6qHdj%olIypDy1Vf$xE_2hUwM` z87-5;*szW!d%mf}ob`&X7E?{xh+UNK8AYYJpeIov2`Ubeotg3be0(!)k#m=7;ct?q zchvF!0EkT8e_T~6#f6mV8G(L55-IHweo55{cYhQ=VVzh)52(zSX-)QcUxjHumQ_yL zmTi&U^i9&~k!4Dud(6KOmWl1Kd>@r%9$ZquKXQc_s#Y(?ovEKr0Mn zg=dQ!s+9D7CwMAa7iR2Cb-ljyb!1~uRjM`S$Wp2$&TLAlR5V+!Qm0yS7wb7qsR&g7 zscz`cP{{cc0YE5-^9^Pv_K)G0ci-jYut9M=l?FPapTJIPtwx66Zc7ue;R&KYYAp^! z6E{l5i58a&6|^klI_T8w+$wTT{wBx}(n#=4qU|=Jf4p1NAyLp6(anAlgMz7{vD^{S zyBxAnX+tCAUxF8uE)W6`X()m`jtQB04|{1X81~f+o^n~BKWvpM%Ou*g&72`% zY27PG8dWnPpsID;{h7ahe{XvJ6@l6-3we9^sQ^kkP?!N?ktJzwYZ-E>)M*ezp~uLQ zu`3Lfg2)sNqsZWym$&1rX0*@L5^`DYV=0)kJ&~7#b##j|{z;AywmePlwR`eMyy=^8nx9>?)v? z*EtG+`JM`&9}=e#I+)2L7D}tlSIPa{6dZvCKz3-g%re4_P8AxCi$A4uM@PSGn<7^pgY zQN^S0G`E=-C%Ab30M++QsMqbVddJ${CDD5Th9d#OPyYa~r?A-v45s{@_5T1WOEa=# z@oIFD0TA66z5f6OcN|Xs#ULEe#LKX+#DDID{R>6X4mNLl549Z&NYUrAGvn33<;3QV zV##%UO&p^?d7ohQ1y4vE4j$ouchOVrD_pia^7kVx8OrSl$pOXL{{Uo*WU;s|&~Zkf z;IR{^e`ehT2mS>A0RO}QLlFQ00|5jE1_%QO1Oov9000310uciQ5+N}VB0*7M@C6ex zago6wBQt@avC-iaFhT}EP+)Sx@k3H#vK1sWaDe~X00;pB0RcY&{{Z+n5OV%bDwR&O z848P=+1i@-8qRLOp76~70F!K)JG}hLvtBPRcX!QH59=HlVBycfRCHW-A0*nB>5>%| zhPDE{;`odaaBd|7Kgkv$6hfpS$@o_A^HqBd=VGUfszVU|a!+%b<8?n>ttXrl9@lnC zrFi1vocp8=8UFwv6Bw0h>FWiT2R?g~zicdk>adk#{i3kjUMaMYDo5uS&c${!y)(YL>t=ZS;=9j2Cn^!_BI=DfAsl;(GQhTy1~k}B2z## zl?Lk`Gx7Q^rkkD%!f*}_ycMVRe#2;!at{PQWV$`4Sf7Hi zbUl{Bdddyf$z`K3h5{2Cb;_XDm0KKB8#JL(Z>%b)g;6uINV3P4+677u8F` zOH=y^l^XMODq;y$99t2fI6E)SgP-shJT+Z&yGgAL5D@{{s1BrBdBCboYZw{?CcjiZ zH%~tV8>|82JbCX}HJ!>QH1Hs_Avn3ckgz$RBw(P7^bn3B6EF~%BBZLx?FE3UOqQ5L z8`7#KV^5vxmu*#%DgMn<_Jvg#Iu%~XRH|Qn*vV3W3aVTHyP!NckT!l)T}K@duKxI3 z0+&?FE__p18;JB8+roDsZDr8xIo___W3VcuGAfY0tOA| zo?%Ypca?z(3?ZQ6vurA2?5B3sVK}BY)us(~TXHzlRQ;h;X||zly92ug=u`Ecoq&(j8UAa7PO7_i1NqL-554g$JPiD)#kw6^tvc4M>4ND#bE?JGVmJ8 zAlIrys26&?t}6`{tZv5r6T`2KV1GMjHaz)z`G z*8-bifP*J&8N=`3sx_owsS|WeY}%mI!KOhH9lM>M6wny{w{O#eah(+I$!qdB z6&*v%Hu)h;wX>fT2y;M1&S=2movi~wa)3^0%mCw(Yh$vBC$yP#dJvr15W2Y9hKYqJ zLc}TFggF)~t{EU*wGh&-Zcs*Os7DmmV|&wpoY^83(liW78rahChKBV)Lc_b zV-=gDKqVWeFpb@XUD|&n3~^B5lD|2lO|K6h?)&{!m|SVUnsg4*hYX6g7K=N}n>ZO8 z(_~YpR;DD6va4yl?^v5xs&C;e<1F|NKTjR%ogm3Mlee9S<@t3gvX^M4(XVL|bzAZK zDvWI*b&Y|~#Z#x%v=*IWtkoR)gw(@7l5+uV=+@_VocE)N)=hMASK2ZO)d@h{#HyVj z+GZ%B7NaGqg&J*T_Tp6m6QU%-bBkTU3V^H%ECoRU72Kg+jV0Rx4e?w`x>TL3H5-&B zhc(iwIx|fG3(K{MSQ8B>l1?b(y2oULs<_~phqO<4ehnxApXBda$hjHWrmc#%^q-P=zeGB2h6wM(pEUOx+LZI(yY8veZ8qI# z!<^6s#sa8AY8pF6C*$<|l0!t4DparRMjOwPv)O6V*UYTU>{V(uE(}$nrHmCB`b2k} z`QfosEX}J4g=*^oiYF4J*`?J-dZkdi9gA_=oljSJ=Pn8Ma7E_?(A$7DS;KXy?cAG# z3rx;TU7cS-5D>+pfNrl$k zkeK3dIi@t;DeiE@C(FhNFyv8g^y9Hpq*!IETXKaO$?h$St566bWrzy}C12iyf%IYm zy&0fE(UK=#^hs1~;iT56WS@`FD^rQ1e`4spW-$K%$NN`G39(de@U2dLXr$8(1CSFK z7$7FJyueKYo~to-{{VM5_$ns#rl)g!{{SMjzLg_!xRpircbnF?gYq8ly8KNt*aRL4 z#q>3s*xBdq;+s^|Y=eTqjl6P4)*LWy+5J<)VE+IK)Y?JVf6hCXR9a$r+?*O+c;=r# z)G;_KL;A2985RvMEojLZYoMxN7bwOR z+|(A0Qng9McwzSVE1Qp(*%IbkcPqpxmE5GMbn2^sj1DLh zk`E6Wik&9Y?epG@yqBDnz2ora?ofUI0A7Uvvp2~(3&ObzL73${YWNosug*qbf zT^G=+2ptCW&S|d1Cq@NiV62J29E-Ko04RUqY} z_Q0sqbj+@<>NMO$qfMyPj3HeSo@vy~;GAOw*2Zc;Q=nmzcD8EZQ5s{?l7SN|Ar0ED zVb!w>+3zWgZJa>QoVm1egi;EXzJYYnE5BuzV^M_5vWP(*n?WpeBz+L60*rS2Pn~aEDDKI=|-$N@}*Ed zS`fvk>?mO3p66bb8%lHb4Gsk0ngdXQ7^myD=_>8OnB)4gLChsNhV|tH;I76=)9Kxn zab%}*0OoXpB1!}tw1$VhtjbWhJ0UfI4zcRBYSzlVMH~R<4m?f@)@jk&n@rVst58H_ zH+S**DTNst4jg_#G+hztT4;++CsKCRXy_YVjNXgGc=|4lwc0G$H2pB=VzX9cUX4SS4ddPN zLb194I3ZG#^VL--3aljJ2Qr-VP|A#k(ydt3{{Yt+W~wptfF~a1c%TbjY172*_o;)d zpd3&U=@z#=CCHUYmzw8o3SP|dlpYSMDb9rBdqbTV1R7@X&`>dwzr6@K7)6nqk~Tsl zfgJcEe=M{VSbs{AGYAlJPH8N=wtUe5Hw`u4a_MGr5e=l_dw0!Otogj`zDd@&)uJWt z*~59^$V{USzT+s;Y1&s^)dK360E=B*`pg@0q+)Dzh2RZ{Gl0LQow@x?x?kE*4K=RR>%Xo^DvB<6SMt?DQm z&-V@g0A)bxRPO%Zts2$r{{Yl~>g8{=yYU*MX_N8;{l}gvohQ_At5{>PySw=D_YI|7 zgL`k~Ig+}Nx`KsA0S?E#S}9bh2nPjHm1-vQSN>kJh!w<=a0s=x9N+s#{@9#V4CZ%y|DvL`4}neKdrJjF0HQYJ03j8 z$q?#&pn;WeG{{^EE>StDSED6NxseIa-iwLQ=UqS-c9UD+jKnY|F$Ts+jijm3ip2it z>~I;bt|^Dgv|?G_tyQWzr{U>rj^T&I@YtzRZ8qI+`Owx1TGcb%_4;`(lMm{p*^rv+ zlxiLy+!dOy;wv_zvTCJ9{ZjnM%W&s^$Vvb=FV4+fL<50UEL5s+Rk|*tOt%$It*XrE z1z7r62gPg?YtX7XvSp{?+?{@npwOv~)}7%OAa7)wRJXEMT-SR*nl@2)yX>CaePbZ- z`h6ifmDZPoAS=b$AR!Zi6DzTdAz6;>Xui;|&t)}{iOmMaE&-?`?UQ_#tI|2i8s6X}F-h7pA)j#U`uhikV=nWJxb}rqy5!H^i zu4kTiu8?_qZ<4KTzh^9%pYp7>K+rINhOEZl+@V5!CRFW(SEE?&!C(0LjS>oNdX(AK zTET(0a%gkg>YN-tJoqXVsImoIIoMMfb;lXpi4%0@vduEk_T*LJRh0k{Ek7_-k^N?J0Np6u?XB0si8E6o0Gb z&%tWY7ph`+eN{@O?!4t}ny+m=nfWSIOSP(LKe`sEAs1flDwaAq9qdP)dymRtYooB8b+wr@HcvPv~8riB97#>h)(++0cc?oPWvW7<68 zs#JC2`k4&=!)f5DRFV!0tijBK9Th< zkBC(sNB;m#EVhn*N|kCO7S)lst2MsORF=HrsyU4p8nHcmf_p>CX(Lpev_^;3>V&3MH4cobcj%by z4teiQA^5i*cJBA-acw@@s-v*;MU#aGvSBC_$IVBEhf!QZvh8}3)izFd%q8e`Y**b zzSLd$c;(`oQLhrxi=lVsn%OWjSXsEg<@fn_ZzCmwC^B(XCbp3sZeD)VT};oi$C9GPKZUi94{)C z8Wnp@RXQ%COwx3w+U-_h4<0I)O4B%0!BeT%4OGzIdNmMma_b4f*`tc6>#QzTI{yHQ zOu@uyv{N;Vl}gW!C^f9Xx*gkZ<~q$tnF0?C6*_KaZj{Cso2>}Fv<2FDyJ`A;d@#*I zihGq)PMdFD+6G(jR-7r*JU_Uuxs_{EYjz(Ztu(s(O+d^WCSA0R%{j*N?il4tlPRU7%Ot{z2n$4DAoU7nsUk20(@t5Y+3SgLIeGVD~B89%|NL}`5r zbvr^+L!%TN7;!D^z)chfCLX zGOXP(R36f@^ntYy9h$r*6Z=i{2x)N+PHo$vRMm4_pHaf587I_jF^XkI)XUJ?s%-Qc z%lN-8bNYDhR+^D#E@l~-PO}GAyaq5nN}VxGK>it;x}c!)Fh#~NPyACJTnzjZx<&Nc z6tW0a&X1Ifn!~oG@xpt9`_6R1$FOfaN2>s_7aOV!-t`@RN396UCB#S6_X*}v02o8~VABY-cc(BpSLoVRafmy@tI^h)XVjk}t@JBpTugh9 z-3u`W1H4t6pzYPfvn@{iY<6oQ>@hLcH1fa7SHrd9zRun*<ulV<>T`a@xF!+T+)YofLa&y}R zhU3qwC^KplxY5cQhJXWdt>84YNc9ESe@pc_wyDvb`T znw1Q^^8<&^YMQg?Y{zvGAve#rcYq!VkDVF;! za}`7$DxJaD$pX*m?)o1h6Cc)gNOyLAA4x*t!KFEbB0U0Q60C&Ptm{MY{{RpbD@AKd z%WVCDw|@b{dGJ(dic;cb$1~-?ZXS6503W&(5Qqmf>OsxAGF7@Ni{!=L?7R`OT5Pz` zH-`-T6&{o=b=y;L@G70Yg{;G!JDPBgJR|-%{E2I_ya@jQl!)=x?gC6=PW$4RC9Z+L zHu&H`?`|qYONYF3^jAZMp_Fw@9T*aCWT`RM{YqN2hG+fV+&JgEfVoYgogn~!AWoQ2 zi$Z;wl^b>9@Zzl<6`xi}@5k@)O|{#)ICXUnuZTY7NA`l1Xf}c2<;UbQ0W#BMd^XPo z$B7a0h~s*5GF4kmouupDX+QV60Xe$6~YD1sq+8{z#oLp=Pc$ zZ2nD^I&l90^RNHJ08S790s#X80tE;L1_%ZL0tWyA0s{a95fUL2F$E$)QDGD#GD1>u zfsqhma-qS|AS5s|L$N?mQ)7a%;qVn;bCROM@dhPCRAg{~(&GQx00;pA00ut-{{Z+M z)j9tFljQ|;vNn<^!UzrQ*OG@CM0(2Ov;CjCl)ie+qyhf`CiH^$N6og$s+_o+Gr8W{z&O% zCvhM<{Arb&R`LO#v})X({^)Iv=E$Wu)#w~j1{z#50x`(s)K-kyP=)ipeJoR4FkL3u zS0r|p{{TwjmM|Si+i!|169c1S2W4J7r_i0F3+}6XVl_!Na6R8f+y4N_I2n}vL7LhI z_^_g71{ECHxmEqeu9-NYky!Qs+feBTIb$y6xo@}r#;j!r&ZItfE4E1+ROTrOcw&oD z1GgfiSoP>xSatdHsUJb%29hm)I*G+AuPzGb{`!|a$ba$`d&FYwvohrGwRtRB$CO>m zmOcS~?dziDlbBhsf5W|APZrpT{{YKYhqT5jdmIczf#}IAXhGU*BbW8$j=5z|TaurR z8)gjzc*s+>y4Y59oO`O>^y>4#5%4siL5)WRnYmVvDOn?88H=#*A1Y>x&M99^a;A?_ z(8oG+;%XI>R}Thl0OMYDZ}*DwxfiHzB$STrT1P*R-CY;gRaqg`CChhI5dMx)32+gy$ZaP2hm zTOE21mhd$og5%2NgVIR75uYAai*?n3s!a#1qOI4;uI%FVfI7DKem`|AP+IA^Rn#9* zF2Hwu2W@d0WYS>bN5o%Evx2$rrEAFikQFbqMX)hda+K# z5sqV1et&gDxDm%9s!B-DqDKD!BllI!&k>@BjAvYqz&EajmM8Oa%JJ&=?N<>^`{h|f zP?`(dlknADCl;MtOE~Zc{XtBAq-XCH(|VtYrcS1PT$8nQj*RAhkrCy5byq|& z0gskPP9o1x<0Nydhm&^lh9AnTpujD1`gi&@M#0y-e%nLI*6^=4BDcr-ft}bYj%1yv zqz8V+@AnE~?0Viinvw*W)+s$T2hyv!nh`R(ja_{tZ`t2cB#_*#%RX@=hX8N!?f(E; zyLh2Sn%IKqsf|gU$iUjIE`&j4@_{N|>eQM&m#d#ATU)DEA=n1c7d@jSf&A&lSz=?I z0f`jS$9xuGgeH2YQ6COf1*Lc7R4cCWRQ6g;CMA?l0AQ4JJUn8u*P+<@u1G?0L~qs-B(J=2B_N% z-(>Rm;;n?vbVPd(F8o!@PS0@{2SO_xV>tYcT05%|940<|&RG66eZrAj^#ug0a$zvq ziO=w(&&*d#Hgm0%*GMyCsnRjF{rGHEfjyUT<9t_11v`0D{3_Zc%87bK*PcC^=nwkG z@T;R5;a4l1)w8<*%t*@~ePdl4Y?}*$+4y7VXM~cV<0O8^Ct*fAZ;YRnWbS^Aq$GIn zUUD#eMS5Bf)~u@r_aEt7(*x)Gc3YVGN7(9)E+-X=p5Ft{9{P=%QF9{= zT9I|&=a!CV-bO#2Yqg$P9i%HGF0i@m2T<#u0JP3xiN_C%ouN8&jP-LE-m@LY1p70o z$IO4F7BT9K92p}alzc^eK8~88 zH8DCmQPgMQT?`n1A7k8htJVJftDqxR#C%zG9%I5s4>Y zzBSQF8yyl!*3;d$=k!d`sS6y1H~kXwHj!FKQZHxp`m90lrjl7a+H==IwqScu*3tlb zn`+zoCiR&g2eC%v)5ihQp2{A6^IDzx_6}c0)#E}Kqi2-zP&cC5#J75O5x-SE>^b~C zbk_#xW=+nKfkG)Ro@jyUsWe_74z8;sj;jwEXV9^NeFWsS7_i{Aqb>{~nI#S5iZ-#y zJi-3}g{+ZTc%J#2`w0MzJ?-B@OGyV086r*b{vow2k}e}}MbL%^;wX9ECOL6B#Cwl1 z@UFSigMA84KQr;G)x1W~$|s2w;onZ#8RRg>dO-^Gmq*OY)RRvYKkiTGPaVjzx+}46 zA}K!{gY&Hl@K@nmIrA^ZTi_3~$>?LC3nVCO-b(ooJa~SsTbV z%@|+>TN%|azN1#uL?f9rbw|e;!8ySB)vdd`Xp2sS&cprYi7}(8^BcnR{>5(lsD*O(vz`kT#cU&@6g6K@A^0TIF1;M=MwU^t6m&%igY`5{BS|f+~QnLwQBxeK>j|vD7PE8F%OLmXwC7)nBO%=CnXOx)d z?i9?@V?6SAG;438+0T7wHt(;R$0SVA%kFjm0Dd&K(ah4vbsHnvnXs_QpRT)97iLI| za%>E81MuuvLNP87)T(;u1aFs#x9nlRl;xz$S&nBHG}v^fl_o;YZ54IS#}PQ7HKG-nCud-bU}{ z_zu}qsAHJ-_)}Yuu%vB{6nhMh=kcze*)f9qK>HM#!e>I2|^Iv!0tbLZ5~h95t}HKJJBHtu1MqfWNd-I_&`5c)vD z2NcOBEn^ti9)B7wB(W;z3)bwX2sA|9LL1iFqmo%Sj~#sS#Y$29pvxX{*c(>$ZmSXV z7Y%TC?5=?;B5LC5WzQf$tI!DAbFiySZ=<9eHPDB;;<`&q7W3yh>^;?XKmAMnGR%2K zrKzNb{{T#^azvGt3viy&9M&XKhsL%)qO2j3ZTj3Xog%Xvs~*wXJ{8x6B>PK3TX0a0 z&0Bn`RhlrxmkXLmH2}vaeoNArB~ztB>30YREfN zJ86#GdQwZuu1%iy#k!kg>BoI97Z7S4Lo%HY?jpF-ltUbfv)Z(YF0u7N{{VaQq)06V zy^xT}6ro0;p2EZVS31_POqy_ zwfNK08n<{N)LDFSwrj0%G&tqQwjqAvXNI0$@v%}7K zZPr}-j@6lMK<_ZNgZPiSvklH0*qJo_<39k#KsmpKYIypC_>0zGnA)4@!OZ;a z7F3o%IRT34S5uX1fB-^!1^hM5~3+PPMlbj#r?=CsO^S)Xb%scSY@ z(A@n(bhUZ-8GLbE>3CLea-Ok90~Lw5mZ$sVDn50sRb9I#gyDzAqhmMxM$Dbb^wZvJ z0^}j~W*QIWUSxwG_JeUo7;yJTv-7DsN2FPqhg|C46@4feRs@fq?d~2kqC~?69h=q7 zwZu`)!$xHzs!lhd$CoHDRyEmI=-!n{5tch~HWeXdF+6Bw(%~hFMesSP7o)(2%6NhD ziFL&iNH;h+=iQpOxblme%iLtjCd7bv`a|LT=y20AO4wMCRi{VW^pe|La++9U%er9aHiZz)+A|1D+H^0Q>vsBx*}|QtFS=<%62@vN6xGO&Xpc&H){@ z#$?i-CbeWswwy{eZCd7wTj|q{T~BxfZS$;Y81A7QDXvE3t4EB6J@oE7XlH+`-%|)> zmJT7vPzVtY;1uBd%|tETEikG$vJkl=*7y6VB!Wlj3rUW5{xR`f<-RoGU?$~`Ow2T6 zz#{EOhk8xY^_hs%7#+O#KksU2ZKHdGo~{Px;aygEB^zvTYZAc}-X(OOn6Ea{TX!s- zaq}OQ6A}xB?}X~6$j8x+CZIWL8O1Z-=24uk2ctxpUQPC0G>&~8eOTABk>q26um_o| znII+!i5Sm3=9`B%1~!+7(w=1gGzoLJD(9=;Q$vj|=u{KRwQzW=-xperv7~{>(=hna z0Ck0ChX(3RKE#Jm`|B6o9UC9*AUX{W2S^)M_AUqGTH{#mliGxaqRSAsl=C`qnvw@O zNqsyW+Lp;@f8e* zu96lLSftqq-S}nO`S{cYh-Qn>^K19*y#-Mnw1DT0Zn_-!ZQ1lic7QWR-ihrUzs{{X zNg=zM8GPhtAfh&Td&NsNt@=)VEfeHAik{l$XTDiFrWm~e@i?h2&7rr)?!2;WM#ipU z^22#?IMdV^iXq`%HR*5MDP$1FN3^Stg=DlRZo6eii9dj;{GQ;Yi;?MhqW<7_`BQQF zBaducYq|(Y`}XeIm|9&l6Fzi7F2O$2eqZZPGZ@+++Ezf{y!$=$vuJco;7O0hh;>_q z{`h44`PT8vX4v(5>++o=`H-8TU(hrsu1N#_|b;XF>jzM?SbR@))28@!&B$@sSdSrK=;ZMT^C_8u8n_ZXFcG*+jX^~^{$qM6$#e(X<2ozkKsT#1MKc=t0pUpBnOH; zXx|jd`s3H@vDHOqi5btX8)NaKq=GyvNEoboPu*O3oIz?1IIYXvqqNn$Q9!ZB6609| zTYKx3W45{0F^_cx3#j)2QcrVouoTAAXJO-xUg}k8IMFD=!2XYiSCvgKNXp2=t)MRN z@$91Zcbr8BeP=3zyFLCDZ*O@Jwu9A;Mx6XBhLF5+B83~_H+sA%+}+@;1cx z4)gSB5^7Ij`pfb(ylWmG*MODJz~om7%3dS(M^(?xG0#r@;vrOYPijoN1KsV?s?G_$b@3#O$4 zpth9}?UF&(WvP6k$*w?LM#N=F`1Qd502ji9VRXj6gfXvFd@CUU8S5)9)vL&HFEgdK ztv1W#RK>JA8V zp8Z1vR$0Vw!Z{yB2lG6ujM6Of;4-@Y3(w<8cP{aIA~?t`kF4D3#iVYk4lyf@Onf`& zo2HP;4xc;E=p#|n&lFxLbB@;78j&tYidftCZOi5XtFI;yL!gMjDt>t7O~6nY&Rx4( z{r7&y^-~a&Kg9(te*=16h`P1B5>$mAXXx8UWPr|&8^3rLi zR>zxMN%Er=w-bmC>dTCN6$rSMTIyq^GJoC$Utixt6afixRr9E4om!~fOv{g*d2>87 zyvz}88FwQb@;B#RQ7CI$UQ89UufS3&pp6h=BOI2AViiW@nv~o1p^3DKJ)`?09kLsV zIb?bx-f6YIyml=vhtj>HmOb=ich={91DzB|Zbi~{^~N>xGUvVsFlo2aW)hT$$`n`UFn=dn+j%}H)zitcrD z=c5`({{Rp>pM80|hJmr@TrNlb!}6_oFAuhdh@UZDaXu?Uhc{d0Sv%H9+}9Awy1Ns$ zw>fWQQIXg~Y@Fn8M0j|g3TW)rqEax^x9=bS0O~viD=$3NxQ^~wTS$y?y(Snt4C4c{ zW~lCC;Edr>p&>y}WG^%Cr@33#F{EMck_|D)tT_yw>eAtflaC-PdBM}9c7{H;`clDW z&jqIoDo`|8J@m%%Nz9Tq1c)=I#k=Vt1Imo6@*f~w3Bs%U=fb)t#Bu>;k;awZJ&APQd=r5LJOg3nTTVvDr(LL|^e!%NGo7rA*j(egEAle;}yTTYU; z1}M*34SP1-^pNyLt6QE$bFvEJOx#)XtDeBt%C76W>iiF7CB#zsT+6wFRPF5`_tMLK z5~%BWxK6et_Lk;P!i38g4%xA%G6p=pjX-Zp+-qDe(LXTAa76)iYaHKQI*;=ojUE-j zhkcUZf9yXh)rM$?u7`-9F%iqKdA$?e5!zRZ)K{{RXO{{Z7Xp6-6?R+QmaWQj%< z2Y{tWA!o_JAX2^I;?K_tdZY8M6}+iu-FzUrFi%ea!w z9;OmPvYc|s>r`>y$kLqZw^mXk)9ql@vb=FmWpgK5INJq#j(HEdktNb>OoKyf)f(-c zB#vghkyJ~d>4$`JG5qOK*(4*JYy30ZX}U`0I4;McBQ5B7kxyM2UleE?WpwUXn?lp-RPMys_U0WMx_M@!XU!#3jIIgs0j`6tq z8KRB}pgfmRRunk)(?ovGq4OE6NFtu!wR!&l@j~OF~V6x#mA@ifUXco{Ei{jhScd_mojUI3`CyO*eJA# zpz6Y(Yu#F8O+RL^jI2+M71~*Ul?%l)Kn}szDh+uwvS+iXxN}FO@3<<8xCAZ?9 z7V2;q3Z5hz7_5@8+IOa!xY05 zvfH~v-&EsjSgtIR@sCs(#)%KabNJ@1RK#s#b{u=2F1aEe&cmzt)7ixyt`l`P53+gGolOYbmYUw(^aJ22gLg zrpCWkzTr9bC9P@}b?=Dkqq|0V8W2l|gnGQcl}8M#9Cm5v3<$VZBfh+cqc&ee$i2Sm zYs-+*I2j6~C*k?lseIf(;UV^F4t&N=IaTM`U-JsG`$YZJ_csrvIFF9@>m7x8UD+^n ze0!vQ{{Zj4ypGNWhTD-miwZ^1a!vrKVMWr|%WvD;`c5l`{D&s5PpUUKsV&zw%%=p* zy#6%ZBN~Rdk#WQaBOV_^ysQ-zY9_e0;iOJkDY2t+TYxd>>p1yStX9gPE(Gj4)q~dY zph3kH=?uvr8lz6b@1(U%TUo0E%esUXcFir@j026^<3{(UostzjTV7u}5L*7FXuBD4 zx%(sWtuZe?YeXdN9oMN>2!mT5;MB^Q4!gZOz2+ou$>TH~N`g zRd0K13UO8V0ye`i`xPzR5ZlXj21t<`Lm@wVcJr%cw3+T_l=mhXsy`wUTkXk4&Q}po>3%x!mn?>f}({{$Pd!<_UPx@bXvbAPs@2l3)dS~Hcm_XpTwipA->Tcl%8_yuVb#I2&H|^T zM~~%3fB29Qj%7x)@EAPJTi#vSq&jzwC0!;%?KgD*+n>@SzP%k|y=jUQ8xev~=`=1a zqRKb#(HE_&{c2URLj;U>X7q&wzl2NQBzqe25insW{o9SrCAE}#w2Us1)fFW78joV< zzp|mTXq)~bMr6S29QI<3+VF&V^d3Q~x~B%L?ys&;U(A~0SL%Q(IMki#T4jx79n33< z*Ddv&k@EcoAwG!MWg}g@bITMXQfAZWw18ztaQ&Vqg(dq!i$?NWj^*4|ZOUCQ z)i`gFtrNoVw{xy4%KW5;%_CT6bzJH9(nV)-9f9SfiVCu}gX*_gPr|Wz(XqGpmh57G z%kHSs&O;n$BhPiRxZE1Qh6Lj}ohYY}_tP}8A1ZGC-*r8bNa<_{GaX$){b zqU2*&=St~)F}xhnx%|f}j^0_jH+>m6@y6z~#jCqh@^2Ac2|8c{7M-b9(s>_$-FiU$ zDQD?u7AuWmT_N;<@5KkQyGgU^6Nav`k@^NWy#3Fxy_7mnrexH(){%Okc+iT|HnxA- zW32xGWK!{%kfW0y=S>Z!CZEHQty8&U-9+&MyLCiB_02~Bxm)Cne&y3np6U%NW4SZc z6p3XS=ZPJ;Ee6LP=eAbt_g3`GTp|u^X{~xVG;wyx+J7o`v`H_bk7trN&J@-$oSaa$ zyh!A_$59kvqd!=W<@~E`*DCC-QOILG>Tqc##|joclm1o1i?`Ba%>LY@v8<`=xX7+O zlgrW5j!gq?@~_q`8%!X!C*SBGmLoFZc*ic1Cs|cR>*+azK|xmfHItSWAUpiZja({;$0|&Zcb~X zAa1i-{`H3XXwMcb?Y(Qcx%??$=-9lWfy|dOb81GBTGCO;)3DZHxQnEqJNe$R`PPf* zCFtortZGW)TFo8Ieetz*h7XM`*9lVPcM_(}M9DO=LVF~}pOB*SI0x@EVz$f{=ewD$ z_hZ{dl$Dit$9)Thx-rbH)r(I*jcRu%QPOwGrJDCP)`7JrwBm#|(Fl-tcGg(Y*4m}@ zU{k-wjm_H_?324-J=9U|qUg?zPe_>p$uGM+sX9BM^jO)AxV)ME&*xrcmK_Hxf%n#s zy5uKq1D$JYe%xR%C*&xrsZ*R_*1aQFY+394sbsu57oQAs_|QneRUCl+hoWdwNXM+> zm0L#=`DMI{MvJboTzf6dR3%eF>1+)0+OA}|o?jrhm6xx}pC$b2IMy(cC!(K0vFQH* zId*m|IVD@6b0h0Q?0f1l%QLSpajEYNpk^%?Bi&M5tBGfm_tzzHj{#a^%Eh0Hc$(k# zn^!8x6_JrxZ#chNu`Xh79#z&=Z>%XK61t!}>GY}Qwnui83S(khQ15Kjwat?|*iJC8 zuc+PBM7>t_KRPk8b@R1lb-?4gv3CMpC&l6TZs&gd+4kys|r}}L1f1fh|;<1P`Qo_1szPCI_fKE?B!dc-icj^@UJMo z#l^uow0fw28o_ET55)2mZTc~|)03x*jQ;@IJ85pZRo;7vcUw`rG4V8}{zZykQrbV^ ztq}wOj`uWWnYB5kmT8-YM^-L-QCs{&cI1VdJzYKC9{Tc2F{x1JBbh&iUf!M@sSce{ z%joZsu%wdhdV*CAsrB%|2M>~KAeH;;AZi{JxZnB?r|QQ+K+_+0eOV6VKhC22E&Qm; zMlkLIf!xG>2FIgLTa(N?J=7pCuIc?I1DC#`elfY%DtW`-P)8?Y>-&%AS`c98<|&pl-Fwjs<2S%kHf+Cb8d5s$mR+n10%kI2>z9H)TWjiZXP4*9uSUXQjKovX<*a znzeI`ic@P33J^UO8|I^)SR6q6hQ`LR7;XD25lqSTdn(0biBbB%vijXqS23PzD-XVv z?hx=JAis#Kk0tX%Rxl4|F;MiK=j$`MEUNs6kB`xlO(fEYqwhS;Ka#^3jPf!Jn&8hj zndhMfDmg9UGG59*c;nIcR~FGFc|&hDa2MlTUXzsE$bW{13-jh{^=FdNzwQ+xi>Ra! z;hEPxO!CguExJF^jA?6^0u8MK+bb@oq)`Zoak>#LOe>tU>TR{sFC5=+alR{XoF1eyHg&JhrE zHEKV){Amx=YSc~#5<{Qynrj}9VzGMj4%#-hWn{f0bKOkx<(;=IYU=7;1Vo`mdFP&f z@4mZq{Tu%PMCD$MWh)6Qlpd>YD^2aDlkLY@v%M2X{gl++TVibH{@N~8#OTYUyN?^k zoEk*D=(&{w_1aCFJ>sSos=iO^lD%VA7d}lV17GhnZS$^>xVj{$naafW{|)pn~mUXpiorAH2sum2eV5lk?%oF<4w&PIgoWVehUr#D&jb>=Ycv z{4=(!QeAYSv`}+Pe_6f#)QK&s7~>d38am@k>0`g&Mm=0Pl=Y61Pg{$`i{n^c_}Yz#KAyKxx$-E7y9*%8F`NgbT8!H@UX z2hh)OqiqUcHzVsg@A^0Xk+`jfD^z(0yjGXQ$=x8XuxlB;v^!Rrql2X_{6_S%G!ohs zIcRsoQH~pTwXt5RXi>_rtYzYY+J}FQEO!?RXBp(p%RV%!tk-v(kuAE-wOmf|!XD!! zhhIZ~QMrFAwa+F5AC(ZimSz+x4e^D=0UoK~AKIO$CFCs&duzJZZRSZm$9me8ub@d% zJ;uJu0@r(X>nIC=4{kgwbj=?%zrL;C9DyHsrdvmnCewo=@*etFrI^fPhfbm~+w^fl z;zvvq--_7jJ{7zo;d|mZZ;|}#g~ZV>_EHTE@;^&j^mIb%?0QZ7IKI*RD~O#jA-Lwk zNW(@?yRyGs=<_-c<5?YF_?_!aGpUesjka%*-}FFHlUXBFzskGKJ*P+c+pbS2+(-Vj z(n`ghj!Pce=v53gd0ugv0#uOsfCEJ)p8o)ycloBP`)gGrT!&ls^QK9`Mn18jH0{H( z^nvBX9u$zn5JVn?L-DR8on}ANIacdkQCgsOjE?HlcM8tEyKh=nQv;t?N+*Oq+;x~( zNa3cX-<_qvr$8LnN+Y-o$;R9_F){8zu#8O-I|8q3hF0Y#r`hQuqj#KSpf1Q8!`}YI;8?QLOK4a%!uGlzH z{A+JGzb=1uW*HVtbHUtu`>RS1jbS%_*Q34w+>=jZ(T}$&E(dRd(wv@q(Bs-cG4`4v zQQSdc6jKAU4$3jH0PvR?t;g`1xY5qB%Of*7=O8b6q0%>ti$UlhAH4DRR3Io4A?ijs zD6LJq9^dK>#d58y&C1|+9#x|H+do|^%ubD;QK~}{a9y_62P!PSciqm68%GLm@=35k z<4*;od`u3t%!91n?%`Y*an3)O}FN2hj4hIcIQVEi*UQPy{TZcgd|BDW=vqY+vtP*U!l+T0i}UVgOr12 z9~v>Nj#uk$ess?xEODsGCy=MJv0|2-nfqk_08gV@1#br#pd_)0zQbv&YkwuUhFOnS zAQk!4$X8Ip5Bx-(B(%-N#bqsnwEGqCf~a?Ed^1QH6$`;Tzi6f(bZ5M=KN~XG&qfQ zaSwMYvPhtr6<-5b=e2QWaTU4|u2%F@JgPks$>ofjs+~yN{0OZ>{SB+4mQ-Xh2P817*2kM& zJO2QLR+!1j7{`50ftQok?yR0?xB8!zdNr`EaWf$u)u!e(xKAE+#ka~YVxG?m7by=F zBRiZNdyO=cK^3wMmN=Il{{R}ojGAWk@y@Xa>lYtzt9fVROLrL$*a2#LnC5F;XDnkl z_)(HzZYFKnBSG-2nAbiP0|zXG5x=^;nYKJWCUaJPGJooSLO}Hp2T;%ADl^|% z2dXwd@8>{Zl54y44PJkT{G;g$jm<}QafXwc{a^K~J7r@WT~@;FoYcApoNjfKzx7`) zqm>+PE2?qeX?&%~i?pE4eyMW0y_qPk=&jm1)JTCZwj&wRXvO3zl5Lh`#*YQ?_vWqm ze#hTNK%}VWiqaNS?poC7$(~xq4O_`01sc0*E5(`Pofk^79kEs)^@9HZ;(tSNT}JKB z^{PSDP3XU>dDLY0esru3JI8;hZsAhM zHwCt?U)B4H+iL3l)wG4wZKe01w~+9g+fiO_M4Wpkoq3F)KBI0c&cxpJ-muKtyR@3c zye!@?wL8r(RxzCm=^q6hrkvYJEw29nxMPv`)G?G;=8jP(5POg3K`zrl{?RuStqiFk z$Etrzs&YZdr6gzrpXeIA{{Rp9N76Xa@igN|?5$8Xo{ohyw7rG0bEm)GLekARa1L$l zS1;=xRBO?$R2d0F-YZOv#b2#mbkX9p*i@IqbECn0eO9BcBkvWWU@jsXXqalpySw3D zy_^34-k+HJ>s%PPkG>PD<~fgzF=ydwTyM&U6LIYd>c}f^*c!WvG1|E}<<%6hK#hfr z7CiU*97LJo;A6u(pLHZLD2f#r(!bWb*DC7QD%jRWLGY|i8qAacegvJ*%8nE_){dKn z*2iT7hK+|JIf_`W6w&!b%{Kbc+iFsw`n}uxBj~2n%ipSUTQ`I**l8U zeQFQZj=aq3S-nXsP*T5GZY!w8leikQb_C6z;KIFAa$I z@;^G!aI)FIa}B5+Ybtw6YiGr6^v^Ccp><-fB$M~I9Z`=O;`E1@x8VSh<8piHVu^5r z=6p?L{{UWc`ZrqY)^{E?q)J^qm!dBq;JNjXZAI z(HFlRrk-SyNNw|t4yx+Mz-JlxVua}ow#A6hVBbC6Oy;qO`auV5dq2zlC@7G`ds1Df z4MRR3M@YJE!WJXtL!`Q8i?8tEZr_Csw8lLZ9iQ?SqnhQ4Ib7CaW{mfW2|A0IhhtGw z`lEI!{hzwfh6A1AeWA?2pytbTmQ z&X!9)Eusy|yJS+W>|u|$9^Z90IYWB7Yg2OU9pCaND%!VNIIdOZ-G_w^BK$>THRD}T zSwHeuD)eh%TZ-jezvSLuM=It20PWZR!~iW20RRF50R#aA0s#d90RaF20RRypF+ovb zae?Z3U{BqH&iO30FXnx5F*Kl0a z7XeM}7k%HUEWJ^=R{sE|P(5JZ{{R+F{{T8DCwNvW$G}nMZ4snn4cDK#EP+b}&-W7+ ziwAxw)P65%zIDsRMKngsyET0`_nTRS4Mn2I{{Vz{{X-^h#a^~PkM{+y9f%|bnEP5$fpvBioVf8;ksEis;)!B z2xRKTnrDlB#I&;oaD9pDYS_(f$;W$yOBcfp@IKPAU#6^n>~T~odK0F@uWCBJkOL+3 zAkoEWeG?kZQNet#q`rI-^(`#av1z^_NRl+$b)KBR5SHzC977bfr~c;U{{R>&S$Q3n z{J<_g5P2r1tppi0w&+CJ7Z$&IjR8*CTP>i9_%1^U_G(`$xsGNBomM7884M_*{$H8X zswHI-Y<`5`Y7oic+OV^;n5VF##o?Ttr4%j)Q$GxTBMDtva{YcIDF83PSK)yR8F;0k z_knbh=q9_=9R?b28!Xm4Ih#HPmt=#7?o7E|YQXx8K!bsfn%ieNI%*g*wj8AbbJhX6 zb-(*C7PLXuhPHSEGgL4*#dO7fqjMwdb(Q>Y$0QoOa&_?l2Owyno2D-Q5RPcrBbh~8 z+0cO6Vzvv6J{QMWv-Hq`fT3?b-4i#4qAEE#s#`JK>bg8ZaOn$Es9f8K<^qzzUp-Hl z?M^2Kg}i@I$Q$Z*w>;@9GxRvk^u@|arRz#i^tbSW8=^&p6~g}j2#N!U(%QQV&l3`X z)hd^LW&_otx4Ass!I&@{ULR4_LCT2OTFri#l9 zv-?usnn~G+?K-J&qu+1o3UgLwRIvqYiM_w?)qDd;|Iaz<~^zf{LXo`u0St5MgqK;_mS8#>N4}Ids4p< zO%yAW;9KJ{F}kllP2r95O2o>Cj2bglZfnAVw6>O3KAQ^<>0w(bZ02&8-B>lxPp#_P`OgLSR|s-i~DZnB|F!0?d#T0_2jrskAo#Yp%*kSwlLFw%b7Z~RPP zcB&j;RjY>befd4Z>nWJ3kQgh%*7S@#a61vgFp4eIZ|EcvDJ+p;|1IdcRam zXfl--t7@In_oyWV-RZz`gmH_egBnl_^Q#YmE;xu|)fQk)jxPO==tDwosfjk*A8_QN zG!bjc@Tz97We6I2T(<)(TRp!{x|d?AN0k@eUTl8GTF;E?7~*(capa41bb{;eDw@^S z(By|SSzWk-+w#2C z58fh(Zg_52#I|O@Gdrqb-pBZZFbcxz9%dKV0Z-CZjTjrR_$n1EZ~^9ZL4}_y?#GM2Bg+0G zy0>LqqG4O8<8frN;+I424d6B-i#zQcFd_{y0J`Sz+fx4kQIPjd2g_iyH#DXjO9B}` zq}h+GY#KMg@A@KG037h}aWQNsb%r8n847~9ygc`n}^fy_`EPpm7-c8v@AMw@#Qn*1L#we-#__UG>A zp#_^$*nA3~wHp3Ib!r`piy!fa)>W9{LVA}18HI!vfxTv4TNc!J6?Xb1e60`3vF>8E z*kbHmQvIQtOv|jm;2Y&nM*W`c`d-Nxt64O zhctAxuF{Q%Xur#g?8ZP5nd1C4~Th6P} z<*K20@fdycb0vc5%(maLI%kSyOswlQ*EzBe{FIXQ6AViDGW#_-77Q?yX>C~Eei-vL zzTRVHbgt8Okf&i_bu*vDuPr9QR@eenJ2M>C^*TC3UgL%jdHLh%1x`cH)-`11+EH=N z?hy5$fzuJG1zZiZO5K@Crq>m6XXc@26114TXJy$)85Gl=7FHZ}JF1_e4Pj-G@caOn z=^cdlq4O8=meGE1s!`CCw*j8g)yEYbl9L z=7Z>AP}vnX3@9mn=YF2Sm=uKGdj9}L2V9si9|g(qpJtcWhJKaa3_tqP4ACF2=TeT3 zxq)+P-U8-7MNIv4E9(&q(e0(L=4)!!yIsx}{Ys|l{Db>M#BHa*^C+!5sq6hFGrSBv z&#@~UD!5Pk;Z=`jpt;6!JT6s@4;)9A`eFiJ=1LYpt7&DPT?C>5dV3a$!%4}pjp9X% znIPENi>*s#B+XS8r4r6ZIZ7R-aCrUise`s}b(ZV~oo-NY=7nXj<-ii)8&q8Q4@i1f zD;`UuUPxt9+5?jK;Nuq^XWWwOk05{{R73-ORV11--~?+EIbqjq({igaAZrX56CpK&+QKrh=C^mOVQyXKMp9 zFzAYN-JREHe;r}=yl+nH)K6N#x>*a3D!&hSm}b6{RhEIX)-ATp-(jDmqpcMbl9X=N z+;)V927ASaVsbRg>T>!9I!Z6mI*$b1Iq{xD4G&qEm$?`_sBPpQ<{2TX*`sC%WTYMD@+LfnprQxSFwg%y?k%Pq6<(c z4$&RUgD7zsP<2NDKjDR2=#!2hYgZY^)+hV$EPZ13wb9J8&gYw}hLsM0bjdlH>etzr zLiIs<6w7?z`#L%yyw)2I%Ux>yla^+(0gopq-wd`|UECK5ymk7=+|VnI#JUdn0R%%MnD#9N1m3-d4OyZeC=qHL#-)9z@(^+(VmzUqE) z5_PXHmN675n0s-ptN#EIt6M?Ae6O-7SS?ob&~7*wThqY1EBA=4t-4F*wEzMl=xo+2 z4mgM%3UFU$yfD+5MnKjXCrZxVzO%?dAPto@{LIDGJd=N-#i1w&%?I8GuATQB8+6nX zucBnuxU0C^wR#bRvIVB$3|k_;s@A@C&uNcvYgZNqRa!*t7p~ct@^JQtuaX(6GNm-U z%drdzuPwfn1urnW-)gPObYP9uBe3fD$d z{;H2FZm(>-d-ShM{F3w(I3K;9Mi3Wj&`cU*v_ag0w-o$MI;`1xZxt|6LZcl3E!~8J zGTS=8iIj)?Ob+=|0STpFVupE2L*>PXNG<|xR~V+{$}%?>n&No$lTy{f{2GOZ{yOfP zFLhS3%)2eJetsENSY3;`WspW}w`YzXrAr9a9fH&2yiD5~cU28*buQWVhn#)b!=SDp zjaB*Tbf|j08s}^3_hNqm9@tiTsL$wwt1B%0)7n{2Bm6L z51_Cy!EMkBdv+&G7jD3ud$mvc5&XS3>8uX1bG$Ob`yx_)Fpt$?&L^!^e5d zOEQS7ZF`G;CE)WWa+t<3d-D?h*e<8JeU@+@OAEwsi?m6eoL-!M1SQmL51m^p@?;91x$kF02* zU)-q zybUEGhmYihsvZvH{Gh(AYm2_QgiwIs{^4v33InXX{)H;ac8B`_EoeH!22t%@a7;H{ z35UlK(5@D>zUbK*bhQ2tKUnlDp6UTrnxh=bf4!&7u&1Vg`k3RGD;_YQ@e`qxnAIR= zzdMROaD$GgFg&L6Xr{beD;3C_J2f!nd{j3^t9}$uh`Z#Y3p>-}=`-p)c#2-dY9~dB z$6E~D6nsu9wJ!52Zu|A>9xCm4MK`JhqTZ*?zOv_#B$L+c8K7=V}Yx>EJzu2<) z@D3Wfml`mvffK7|v&j{4ZiQ`Og7jO&!Lu}JPDZtza?0S~1yfnJZtImtRzjb*YTo8N zzz7OsD{jxHcr?V@L3bNBm|Xdi+EV))`SlPFI@Tzr(O%{3V!33Yw-+f9wb1pmU2_O( zyp9n2^>>KO!qBSEPv%k66(ID!BVA(wy6D#387?dpkr!25UDF0i!=ZJQ<8q19%KoTa z9jbo@X^rJE^Erz10O|KNgrrv}ncw4usdTf<*egMK8ZP!grMOvqox4E;sOCF#GG01I zAihKMLf>sn!@{dyl=O<{n7?igH?Gm?B4K5lIF{285L@2&5Dm+71=qpz4;3`@JqT_? zN(AK_vmapem}1@|^*|?!C)~!-c&oKR+`W z;)FTi(eLXj1{gL^M)m45+a=VCJLB^;QS;?668GBsYd}zW#^sT#f{)0%kg9eDQM(-Q z7TT40)IjT{jNU8}^y-C+EY^De0CMVMIn1Jfa<4O9>i+wR#&XLYE6faMa$nxQ3{5|= zr;7UZ5r0j;)dD^XyZ-=)>TT%T_bGrQM zT)5tI*$Zftv-9{z<}YP*`!(}ZJ6keU{JyZ9RI%*(_1rg_fy?u=>mI5UP@dOM(sYbw z#nSm;aTYX^^5y0(6fOfyksHNQpILkUfe*l z8sXWj^^UJ7_%HoPUX8I8MgE3r3vF_+s?$KWVXRgBAL13)C9QP+!I<%T6vv7e>ZZi< zwL076Tn1#OpDP7TJkBB@*b+cgL2UP?j>M!L2K$l|3Zsfmyze$F;hwI!z zH^_Rp;>dUQnBYrY^=e&o+8p+BzKF%foc&O1zOKBsOCIeKmv}Fm)(adhk*%c<65wDp z!B%v6Xzv>cIRY@amZtm?fv4~5sNW@ zmR{J>r8i(cch*@`N3!Nnec;rjxnqz0L(t1#K!gy8vp?H{J!IWy4+tmHp@-9h$~|!8 z`(4-rGidLl)oS zQ(8uLh%uYO^x(-i-wb@vIvr$BJ=`*y05{jW_=1Mby01cE(eh>#d;?YOk=5n(0IFMS zp76(~6E+Dn`x{j)cf~>$+2hGdva)TKu4HQ0KomB(Tib1G8X#QZT?bJOAcg5>?)MPh zd2Od>t|_eSNAY^Nc^-#AOfeSqpW&NKw?&VprZVWre^}4afy?qQw|_}P=#`&wCLw94 znumdR`0p}!aJm;cQ421s8vE3Dy>O$KQYrWV)d6fy!k-=L5cyk{!cv1(Wu{$G>~P? z3eIot7E@E)J)C-4fFiyxoaC|rK>TRIx@5`K#@BF68k`(#@?k&RVbPR)NBn zRP4b+D%>gop3O%;=wsMLWa)dVQUf|F`lf&{LSozV_$4tTZ^sJB7uKr@%sRT>`~Lup z>5UZ_u|gairAB?;SBOc`t`Ym`0BlguBZB)AA~G{NOuFU`T?YKZs^GINB*9_qm0&kJ691!xYzVZ@d@;(zaP*wSeDR&^1)(^D`v7Sm;Y!3A%LSn9vMc58n zuFT5|Tb=La8jOZo+ScNUMJ7}9`d3&gvi_Zq_0XO+Ejz+z=2c`6=hRWoMMpVlDr6@J z_+^PWDPc|diPU-qS~tU(c4(;8c=LmCs3I!^f}4wB&fEcZ#tP;i4_mn}piuN>W9*!u zmbEA^OZ*`fDORG#Yk~sIpnBiZFJ1SElpCB}=lKq2dCWp>Z(Ijp#;>p)*qjP7J*izt zsn@q^_&q|QL$Y34hhp(Hr}wkf9|<&5!xtqUg6ndDGN9%3K)F%XpZPac6o65%eOHIp zBL1`a1OEW9{kQ_Q%Ows;n^kw(*agGbo8j*pZUYeERUG7u^Nhf!Q;{fIxhpSt6}jd7 zGX4wR7|zK{o&NxerURL0@k*Ct_?SVH%hB@uF}nk5hqd}8u}P}kn)ckl$nQ-eXMbBn z<|UD^4=K}9<+Mdpu&O@h{&-vYPVgw8#OJ)8OG~#D zhVNF=I3DTE2N331OckBuE7Qg2(Dr*@EauS~U@Qf{AN5`k$w#UO+Tyq7*}DA&WGDO5 zKfTJAVIJWK%gG`wN`9FA1uPW{RS|bJ#Yw2Thf9P~LP-huZ#&XinCv4x(mOuSY|I&V zpx*Lygk!M2X`*~mMBE&RgOzjl^9f`mG zd9u|*qL-G;5b~mFuKowHors^o;+(g)|Ln`8wr=T2@sY><;5L z4Cm*ZkoJaCYOdCJ43_(`j1c#};4-HQ#lW{h>lIQ?O6}8_SkrTpXwgz8g!Q`dNhcIO zzORoENxz7a2tO9lyU1Hi*{$x-zXVTVExL5hn4=&EoOxrfZerBvuc&EOh$#4Ir`0gK ztd%9be&6KUf9%(tsFgGb_4LB}u=aMn)jk%^NI_$Uz||rCL~T_K`g%U_^Wfm3KfWV( z(DyK$6B!Ak%Bu%?pjoh8^e)gPDDxAXaYzGEgnXvYZ!wY<@uNtA_pJkUCJAMZ-H&Uu z!06u_$kz&^UBT*UJ^UpzFpuF|q%w}Et-Q> zH>{M_?+f*y(-_c?gQO;9< zDvD{#TLa^UIvuaV(;LE$ofvY?JLyNc3a@ci@lNWslzOpAx6;|9YbD#6C|4Xd^u@o2 zM5G_}G|wkl0LU1ZL2gLEKLSu$clkMgejv=djxrYv;Fmq<+dl+(FAOM@AjyL)*anQ$05Ge2CpMANnS~BK2u&<%4`- zjRkUR;L+O@Ss(|WchHU$bKY_R0`j#WWjki69y6@VwvQh*1bxJoCHiy=P(;AuQTT z0;U}?;;i;yWos>NYe;FP)(kOWaA*3aiOOdT>Zric7Uk#ywGvkIO3aoRVKx~bk5JY)FP-_Q%J?*iU|%$DZjss;uZ=%H z>k=b*Y2nO+_{+dI=10ZIm0{m>yaMa-yxjbV(h~B@U|-`y!$8*tu?cR3twHL&3h4V3 z|0fX@xyoaFz4Cm#=I24SVaXJfOtS<~W-R2ovlv@Y>e;v2=M2qz;B4=mR!%c`I#fhb zV}wPwoykNkQhU+6{A;FK?4}B<`A7Z~z3lZvEV28#5T`#z_00T}wbW@m1Rc0a4MSO@ z)7}%gAecDd+c`&lZF+1X0AAhrwn=ik;zXf+TkM;=w1*4U;p*Z4iXS|xjc|9XyV0F-t{S+G*fzL#H8%G>!GfuMjz_o1sb z20yzSH{*`D`>TAu23gPI6uciyi2lK$(?|RZ$VoB_j_pZ+K(S``B{I>~H5(Py@Vrl- z-kSOolKtfEf?%NdC)zV|8JxiLAFRnE3TD1gLX3`NRPv4m#o-Siec@}AhH8&_M2$KI~NL|GF90V+9w@0nVHtgKwfr= zZ1?|(Aaf~~`F8Wh{exg$;up%jlhF_}8+gcGoHEJ(foKr$knw2KQPE0n5?5t{ImqGX zCts_&k{-qi_zNhl;*!f3fOeMVwY^7r!_zNcRj?%>U~hIBjS3Mf&bebVw9pl}+oQhZ z+k$H-6z_WAa+&cFD>^bXd@m)75H_3Bcd6opJ|x-?c#SP%lUeEdG=j!KHB~Jx~V#3dnI z)3{#z4o@ybC*Vbng49QA+pf~K+$S#9(jebuzH7>BKJ5qlVSWEt7uI0!h3hhebCnk` zJ^fuSQUQrn#H8&|OK)sj|3qiwI?BEzLx7#%c?2ih%&^B2V_0ysP??D;d7kHi6m4L0 zoja2AJ~G+Vms}rv7}xbg<}OT&Y$ngqNJB?pR_KH%4T1=W_+Z~DxS#Eisa>`Z#0oih zu48LPfAnb16rL5ZNMXnBD_RUE%FoJm>S%Oq#l6CDLu#MVSUctm?e(W(Ic5b8j}#p_ z9`&Q_47JDsX==KmUo6{9Oho-dB{KIdx`okY)Yh22fX2w#gf0?&UcNQv5>5LJPtvit z8xB#@hIh4wfV=IuM;3lavy=%*NJA7n;?+BMBpR`e*7yt1RI!T&WpX06BDJ*QG*~I; zV&&^wRDBcQm(Sg!R%PSvrN`ImkX#YI!c+=sAl`aFACleh9$2oFzn{zx7^2Xex$ln- z)c&-*Wwr~e<2}zR(mrb>7Zi#zJo(!`OD)4za@>TdF`cwaQG^744fOO~VVZ;DY+-!>@nl>m=zU5|XpB>Ld!OCa2Xl1 zxLn+J`I~ycZ#Y;!KC6Ti_s8x&L}{ZcGg>5gSyQDC4#;7{WHZx@@0KHP;2Prl41TvnYwS>|f{KDdOAvaBVR;LiRdrPQ zk!G#nf*AO8)A^wV7E4Bk0uuuPPyhe`Ek%a%|Nn;ke>#PHa_aw~58^^1m}UQSa|Y)G zxsd;%d33b$ZmkFf&3|`+!f0OC+X5H!l)uP8|D}1dfKxc#y;jIe9c|6O_APVjKZ2xc z3q`H!u7qD6ecYBrf%j99-1q%^4-=Mz@Gd4A3s-y3!AwfuOjQ}O+xel$e?5ovm+4CW zu5-B*;;X-a`|IQY;Z>P9gAd%ooqq{0OmCTjvvvJR>g;JthK@o z>D78{mq?A=KCO+JWV-Lik+ac!3ib);^odz*g&8iC2F z(2toNe*r>o-A%oNlqoV~@sGH5-{1T8cAozN_QY=Ly?uKx3oqYHK8&ovaC2WWdO+Io z>ErC>07>I4-A3o*<4=;3*nLK2$`l2)%*S`72eDhLn_jO$3xvVLLLn~W1~dSg?GnaMz>o9px4!^=0bhOBdCKI;`k%cv@6FGhV+Wv$!pFdg!AqAX zyh7h@JN|Ua)JZc$+@qSkzH@9VNtz$|V)kp-c0z8vlJ2w1dK~JdcJ2L zf(kCP9t0cgpJN&ub9&#ry+Nm<{opS{2KFVO$!zdUa6bWRchE|pKew>L!-B-v1XT17 zlro7b_oa-~e{S3y-T#R^N3;6TMs<7C4T$kCXbNik2ca1%%2y*Jg3R202yL$GRtq(8 zA8^BeY|=!Bvv#&!4<}%opTO|j2)zwiUT3I5>3@S|$WXPW~wy&D|vG ze7>$MZJ)*Xci`XN@IJq+pZhT+vk05ZJ*>An1 zif4Dw{~bz3tTXV4Ie%gNpL_hz=1H=HSC5fS!~Z>+BF)#+ia7Jnga2!M)Y!*^f1qUl z75cy0^ABb=Is^a$3JMBrM+^Wv5`(GP5a@t^V`l&U2Q?ea%)TLGQ5OCuGaG!65ZYhB z8&NHW(3@WgelEyl+oK4P>3!nMnym!v-Um1N@QpN-E6sMI*h&dRQeoo`q;cnlqBwet z#n*OfOwTcJX^AGxIeDbe2at=zaMwZJ;!uPnv+8h6(F-wJa?b*YUim6~PW52?AW&7L zEEe$aT-$Q}&Q-fU%NdI7Q5`lGz-k4iRZC!gwy5*#BEho8PhQDvNq1J>ogWokL693u zE&ks%r3 z#aA)1n~aThd{bE6vGyh-HYw1~C6Jex)G5OZk_ghv)kXCH5eO%f4vkXLS;5jzh!CkC zxD@(iiR9B|N2CE^irJ@M{sKPhJ2(k=me94{a57v3+G)}wbFNqRFc+(LN+y;(;gq~1`#UiddFE1V2PW2`;e#U4tl`%tK@($2RLPBX*grb*E__>i6528J|* zTfOvl*~WI#$K1IvLUv9uQ|D@1%;UnR#R&&7B#y0(k67_S>HsNT;?!qC*?a#5{?wF%WIOr?)AaPEsZ=q0~wrY|J6)%~cKFKArBvP&zo%6(Ju zjc1Xk*|9Jv1Z!g&)kI~QT~^X66efZ8SQHgh*>8uWF6$yq&uX zXU9alwB|W;OqNx0j{P;Zb?xt_3BWtWu;}moN~mlv-!&z;cyVT3LR(ve?HSj=bZRM@ zP1ah{ywd+P$;cvtvKXEx|J%K?HKWa>)%l@_F%`4&>al3_u3w-+Bz=$j~)(c z@X(kVbbS8T!bgRw7@m`3%WP>MZ<2ejEPsL1NbDHys?BylHlk@rE6$lJ8V)c-uFOxl zsxvhaShO*!QekXkRkchb{c6W8bx@d5M7OjG)^1xivU-#^@qCu=IN0?K96`db_;p+n zr`ZluRUL=T)NY4_dF)+V!KBhs8gX*l&NIB-eJN~=8Rb3UGbLG!1vyFs&Mwvv4#^D^ zu5Gm&YCaVnhf}vXYGGyMV4+e?CtR&}^MZmgzSEIUtWJ62N73Jic2dSlw8u_(BE~_l zi7}U-+iIi&FZLV|EQfZ3Q8+UwYS+a7&<#NobcDFF5Y<&@S2Je0xjL=QaAGA*fl#(v z$7GG^zp}N`WyO-05_&L7vtr_!`6n>S$2hW13^CkUCJ-1Py?$qIhl!a0E|J(>iiA-= z4RWBEZRVFs?S)$Fj3_aPD1X(uueif!9@H{*P;+!LW#QH2?y1K<)UvTLL7dV zm<>jbvv+)`cbAYse9qtq|7MFZg`K-lLGM{zO-swUzJ)0p)^+Kc>0+CQBe`tuWf0h- z4#{X2+|h4ki-gUFnb2qNW4r3h0P$b3DHX7z}Mr^Jgb;Cx1L2~ zCx3SN#3<5^SLgojZL*c3mo!Bc4bsE-Cc)|SCT z_th>sc=uYtAhV36h$~DaNJlulmcrj$~^u@-?G&D6Bxz_Yd8XA$~-ag^t!bl$no9&p**;+Tuj5LF$CN%D= zk}{5F-P>kP zJDcSashvJyP(*6k25y>=%t9PvBP<6fboO1)_v7UFa`(cm;JIvgTH0Epts2gmG_7Fe z&Sz$q7_rQH-f-zs)5ZGxjzD+Wp|=+fE~H8&X2(}`fz)7|uNSy+WfgQ174LY9k^)ry z^0SfB6{Uldm$xwz3>LG$jWnG1Ct;${nQ+v>lvg@%wPO~f;K*BAgDWLS68!?*yH`3X zKH|9XvEAw>tb>1n+3`D&hvjUPC&ie$NF=x`1VdJzj10bO+cfJ7jwIT4^s2T_U;L>n z3k2@My;rm~4Os>?E|hgr{?)bAG)1~a7F=i%$)&^?w-uUI_NKt^fsjZ7){;_?k2-<^ zS&Qxk7quiupR5EG5Q`x~0*DYsj*{K~1%M7{-={w|S!3mn@$0g=I3Ht5Wu%3vEbr-c z3yG6sM)TNfb*lMP@y+a>nvpU=Zpk@=9tM`@c1EQMGOLh(d8m4g>%8?+Q?gI*?k|82 z(2z~N-sa=8Og=mzU`r1)8656iv&+EXX#^m6OzQrm=neR!#9+&RUp{F!> zF)}!FaNxPs+HJ6mz1EU!W7!SXEYE2tk0He5nENK6L_lSd=$alTBzSk=uo_hxvFYP9 z%Mzl1x9YT;X(I=X3Dc$xelp1*MikV~b`ZT-iU`viGp@dHyD>nKwYfU6K(rBg?v?h@ zre?t%EQ3`>5QilY&v3=J__|tsM5top6vC9WPYvDEruY}fp%I{Lq6M5taxYayoVm$z z6J7GZ^+PIjD@?`9btDq_^M^r@SpwlbC=}_|ls&x^IWO{OMdrR7P}WdXl9it!V|4GPNyra%FbB%#k9-9HCfF-sE+WE~4WQ3C^UbD?&oCI; z#(v8OO&Zxn8n4A{@~sTldbV}#S=b`rqecdgT&&P#-!W-k-a; z=t%ba916g6&0y*3J1rI^@m%X$c@TM5E5V%rZ4WLbjmXr=coj$aN#H8;;GYPF0FGd2)&w?W~{g&I&a7+Z0*F35K= zCfUT6+EkZp=J+KrfU`VXza34}aals#FnVN%bOo=%?@}^_?J{r$C9;G5-dJzuRq9tA z8uvk*_|&C*2I^4_Ek1hy(_SUXU6xLUGs=MgHQoqDo~bH(Xa?Um_Ms|+T6jlegH}46 zG(Z?Og?bjJB&ZY0^>sPv2OL9AUu(#M8d#)>_G-w$UGSz?4g~WfY8O{^dnb}(odI6X zfYEs@b4H{LBwLrg)Tm3ZUR7lAZjLRSawMIEOCfiDaBLhZ1)G7l_sP}dT&Y)8^Q^aV zki*!D9Yc357Hqa_ZJs^qBv2K55B|mTlB6nhLaEsn43DUlAI#WPwDlHL5m~p#_YBF_ zJbbbhVSo}moPldt7S(WUCE0!Usx4zbDY*Be{v2G-{C;aI5kZ$B{HRA4S&Dz-H20Sep01D_jiY_Nm zoEo)f4aUeXtmvf-kyQ}M%-H=y@cqU)=M#-;=1a>I^c3bCUp0!wPRezhuV4Ncd@cE0 zulGBEU2=t$u38#}0C6FBPs#B@$D>Gyi_})L@&iwz`O6uZbnyoB=Oh$Vm_^X7p}z zfLt#hV#~htE591Dsob^~)9YtF&;C*_W?W{J;$u_wI70>mBVN!7s0fI>uFeJgIv_TV z7DHg&f26%8%%`dt2UpQaXR7hk+3~jgGc$1QDg(N?JgIs#z46w?>}%FwUbd)7UffChL{7^Zn~UNS{0Xz|9f$YSPJ z-MgCK{fyA`eQ?$y9CJY@{4a~qMN=jz^JTRGKj1Yu;;wJ&*;0_%86Ruqno_o5jn4Yfs`NWs$GU%g#KD6rlSAeo z?Kn|Hd|irh4lYDDxyfhovUM-!37Lu1gTf2r66=;BF-ZjIqFCGO<|q5@gefyRCXE7) zj5psgS%U5#J3Y)?kma0QFB4XL>vaLT%G#<`?7F zgy`JINx9v`P+HdfUuUd2#*|D>2=IG@sG&?HG{Xhga{8c>DQE#!)-Ci-A3i&SX&v?r zCa)q|O7lTIt0tjd^XQ~`H=1(VaoXCbIy+<~{y>j%GK>7%i$_gX;JNS3j)8TA+i;+J zap!~}ygzEaBqkBMn+_%OgymhxPMpOvjlW>^l$fgmN2g(=ZN4`31p6j%^?2E@Z-okF z)@e;@iHAmYytBP+up9~<33l`hxpQ7u(S&1$DC-i1b(rgkS@aah&gOnn(F;;lhT=-v z>A$pTKPWav`O~~gf$|R2RTTxZ{W8vEAkg{s5dpx6IwLJ)?_6?qYNi?eU!s z53NQ{g0O7Z%w{@|41ICQ^KR82Uo2!d)=usvr{4FEOW;CI9qexSyO1zFW~lMF@pZZ= zoBbFg>IDV&o*}zAPKzm$<^em|Aa)rU#mlvtoySThC_i;bkxa?0oZ~x^@hBOOAsC8ZA(~sZ3@VfCB}4cMNi~SVlX6&*Hpi`Ax?7GAX^(+-{Efc=T5)J>+?*^RrAELU|tW*;e4MLIv01$)Er=CnPm77~3c=$U4F9Qb&Z zn7-kw`}IeUBbYy94r{`DOquO6b^WT75XHCkC3tc6(;({sNX++g3(^Ip9$aowD2CAS zMz(c!E!G|}h~eDxBNrITeSKp6J!y_B_Yw&D;gxo!w#^7ozY{yyzBB}aPewHczm-;J zVDscWrtWLfRMH}|HqNefk?j1~z(}9w{?i=Cxw>)5 zvCs9mI0RBkKI?XVYrp7^oLw7$c-bum>S&Xrc{Oe3LakU-W?|k(x_-|>5;?W1=}iGqQl`-GrQJo;MACV_F{>EfoD}!v zlTXunX3$bI{_JDfamxJKxF`#Js$i<%TE4Yy-RaDu+sAIAL|ePDjWU(2Ee|b$rG-ZX zLZl{X_K!NY?*#1p{>?IxPfT;rHt95W*fKI6(Gvvg&%#5bmeGP zZC!Q^Umvn#%&9yxa(6it?}?B}V%OIQlrY8Fri$?|gpHfjwXkFMezW6Vh>S=nU0=0E zo(C<;u@K@1RM~h~!&5=+zk?*Cy#RN?n9z3w!GJS#*LtB1dz zh#7w8myeyx$N(;pq5A)viu(=o!$@i~I78}lZXdx?cTelOQ6sy~mVXQj8E}0()FFXN zZ?Ov7999@)#q`T^yQ08E#hk&?lQw9l6*^Rsdb8YP-z?*9OzD|76$O(L2k1b2bItKf zmPMIFdXV9IK}uRwyMxSfEn(tinG841|JI}59G%1dL5_9)YZ^l>YJI8d;n)e0s;J_Q z*w6fEQ(Oj}a=0VP-5!kZ^z|;6B{sy$TiRlo&I512%=#;h z{sM;nG6sf1>Z1ZABv+-++V6HgjkU;R{p^O%8s#{{qLjefSj;bEdV{*T!qcbYC2o~` zs>*Uehrr&`3)z?m=NF2DcNw`6qTQMp0h4Ibe{bjJ&Vas(U1l z%bx=`NU6bM_BQ5GtzE<3C*5HmC_N32^QX2`I_N}%ouy4W4JKQ5gnVJJ@Q%&s@8Vcl z;ZrnuCy$=#`XHR}>B~W}z!lt}x?g$2+0SyrpX1D?!AnWE<07_PzkCYP-j}xK4Kpg( zS?D5O1IzKh{n|_e;EO)M3yziJTbDU4c$Uh;(KUO`F9zHxn;W;dfDX@he~4nILXV)< znrEpqmZ>Uy%S}S>*M@n{l2Ig^myxI zVOnC%t+Ksd*7bF*nfp?q;UJ~r0;Vo!3X|WEUtcdCwN;rdOR zo0Zq|e6>w&oWtn1mQYlkiqucUaiqw#j?dHcJQb|Sx&D_oj7F-T8F%i*d&}Ek^u?Pj zCVUo#hlaRaf*%()9d!3uthsLc+NYJeO2zHG_*d->3!Pp!fKQC_2y-Tm(g#_(GlG^ewjvmPDoL*Xpbov zj&@`l6V+!EmRB%KfmcJCradKh?u>&B8K;!esoKw7V^n2R9rVH8yVw zR-Ch@fdm1%l*6~jFTN^}K~RqBFFD!F(TGf|a<)6e6|;A239J4IpM)H6HBkzeE`J%J zA)@meuztT#yG1K*l$4}&Sf0*v>4&EN{#L&mU!R~;P8GWO<)&X!=#nu8-xj)3c;ur01z0(5Y|?9Ef11T7v`2>Gk{HH9*YTE`ynRHKceSik z;#ZCFCj5X(Q$@GH$TGC-{LK{UQ&(S|;=G;^B->tanPUhl_A8 z`(12?L&$GQTVhb6g~xFfe@hkch%|Cci;{uG+p}6T21pKnm6hs*~ESq$*4I6Ap@hjHP(EHFQ#=Q;c1;<{Va>krLPLerqNd@*qap%aXgMzmpI)(;P=@!rLz-> z{R=?y<`505d!CTBK;n1byTSHBx9yo_9~GPx)hWG=3xIMO=EAa8J->WE&J@FX#hB_2 z?+)L;qRd^Tqq5MRp2vfSB!ihtw5SvSMr{s3|3Wa(Q=HH+jpFCz^!mZ;i^+vfwT=VJ z9>i34I=)}$!q4_IiVuOP1#AMSmiHZ3Ts(?{Cf*^uGDVWI(W7MNJx5+ zTNx`mbUJaVkcL@aTe?#NlJOB-W_sseHNgu zC}C`q#SfFKJw<5FE!&OTVnw)QU#O?Py;UG&^gJFYt}K5CTu1}q;+QvY6htDkNhNPC zrGFF~+usGasF0JpO?RUoUW;H`rn5WL*oT-D@>VN~VJkZD$VtcD#Gj;3i8*@ zS7-h5D+S>&iyqU11%>adDk@kD3-O>vCePbftw1$he)2EBURBh1x86zGREKP8+_PFY zX+3biqJE3TtRhvzfMXh(7=;PTxDovpOZ~`4QGMoIiK?e`wRHmIm=xV8llR_$~KiMh3?J%={yY{6i~gO=GRu8K3u4maeXZoVnEtO^ES6! zzC|i;_j@U($mQ|FO)ySQzHPoTjEPB-GlOKQq6%&q-1ALEC00IQTts^!PmI@F1i zQn@}JdBKF3R5zC|iZR$JdJ{6xBF44t)D#BmZyyRNY<)v$1DiA5xON*Y#c5(Qy zNht#N5DWfYtzo6l?}uA1=Z}!ke=FOJbMKK_dx#$AVLg2pa&NdrG>U9{EJHP^Mp&M6 zfcUy*^U@?ul?CCLmM%+J!zN*`N)(RA z+Uk4jxIWr^fno0C z6=N{XquMaqF}i3Mh9a@6m4E|Rk*KRHNFB`C4yEeO3DWYIT)aOPk*)o-+n{)q{U($6 zTi#BfR;(Yl9Xb{Tpqs|t_NGp*|9TLn4rDG^5CuW7h!@o{=+U(Os9ki$vUdQmWk$K$ zAc(tyJ$W!9BQ&??&KZl<$+y@}niU93uR|^ErDLW(d#8-A zn{(W1TuF}h?npCeU5ejppb$&gcP0u8%YDBZaV&pxl2xTrxesPlUF*z3(7jeKL7Lj) zmhoQKc{q9`*aVGQy5{PkQ-51DkE`N>2A=!^SMee1t5^;X?vsNLxFpz@pq{|@R#cdCdB zZ3E>Ujmt+Acsx{s>0H6`-)-O8XXST8lxbzOJzUPB2D&T7NHSOG^lpNdb`x5<(qxw7JRO> z{_zgx&miGF6e-M781105r7vwIX3A>7g#CU2r9g;MDsBf3m0vebJ<$#2pWh2(;yN)w zX+hvFajY(;)`&xL%TNy0U5K7qU7AK6$H0UCLv3iVQ3Ej+2a2Q})jcGjwK<$_?^TAs zWF!#tbJ)=KUAf|U6jY?i0_?2L9*Uu-(KPR6@1dneth$QhnM}1EU?XBZHeEz1s2aOl zcZ0ub%LQbddCJOHd5)fW-w&+Nf6itlR^Ts;=z6orh*OA4{BoR$oqqOy&C2ygu$-o< z#r>c+`ddBrP|3Q#tx$Q@w0ym4A2!wX;t^Le2OV-q8>O%JQdd3jSi+++TmlPz95zia zh5^G`i=n{1c+;%v;OQ}WAQ0^0PO5sB<3x^L7g06FSz#h!($6c+{2(OLL3!;+j?)1H;8;$$wt8Nv(4t9Elp zSinGpfv-q&J#4CK7zDMmkpXj1-^2(gGQY>4$baz(%<{>{hC8NJjRPtfz$^8wICLXQdBv zY(DOR?d})%LYmdn=+!Z7lCr?o7V=gVh#j1$=i$Z;ka&Of;(zHU|y}K%;Ly-%whvr2t&bVc4 z;IDC|1JxyES5;IKdN!WxdrdLS-dBFETJpm&PbMGLpkFch$>{MGai^+t2$|c+Q;tbo z=4e&9a%}6=h!Mtc0NSn@G-%`w%^`W1)URnFhz#Ptje9m)H*&Iw=S?RK@4hOW(ZW%{ zG*&6QVVD_x)!!WU-+(S_G;q6U_*jVzyq1vqaZu@#$!P(##z8Vwt&O#fe$RgP4(rU4 zgxRF%LENYrq%TY)OlylynUZlJjdG}AUnEa9!XjcS1|=#|Lob$>Kv~M$&$N72n3p@Yo;F(`7Qkg?3bi;S6An9T815oO4(k75x>2r0b4Y6FfC~WKX7gn|6k~Q zkpHCfF@U8?Z@$AYtI?86btZ#_D{&yV*3NIlBcUH@1g{^gRaR^X6;Ss+=*HbxP%$e~oxunav;BMI}Ph}XN zPjId#2AM{&(Jalc=;;(X2@XOc2)zNYf&8kSC=KsQsVNbqE?GS z8t4!dt7(ytR{6}8H~STMTlXjZkZAfP5Mtm5OrQBqylP?_aZIJig)Oye%ea8VAU~h82#nDmC z(W97T4XsNpR{}?$8|CucYw>v3My@aKp2bPZQ=Bv(X`t}Us5JMVo{o*PHGY8pJBs@o zpALj>8d#1of`n!>U40gEhAs06CT6CzJZWorc_~w;x)fc@PZZ*$u6FR&pSR z7)FNkRQVLh#yyIaB_9Fyr978k*#c3uWfa+dHqZJ-y2?R;3r-ej;p_jdvA=ft4hq6Z zzP<4QK+%z^lYcIpU%N_9dGI$$bld5t49oa9yAd7BVXpr;i^VG}iGleGP*2%Ue@yf>rHzziPKUFKlcf*rUjl7x;VZd|+=~f@h2@b!@}olTGMLPNf?@N)qr<|b z<8&ls!Szy1zR4*iQktIh(4x0=48AuC{xjK$v^%998w}AO`^D34ht)?2Bn4QN^PWTtza%0 zao!Zd;9%EITVD^L2@+wxN&11(7BB0m?npyB6dTlamLH+ZX2)vh z>-_{q?27N|FjxCF8FZvf&<{f(^>9y678>06gi(;VycLygZ%et6U33n`ivfFtuL` zR|IH36-!4|1)cW22 z0>0rgl0!E>Tm+po`>JqLHM8_Q`Vb_`XZr~eftJCg)RJ}eLF_EB?EzE{td3iIY@(D< z9AO>YW5-Gq?Yts5e0zIq1P;nG42Or4dE3?vbhVAFTc8LTLh8hXRnNMG=8Pe?%Ga2L zjU9(mTfbi%FqxInqPm&4Wy?po&x0K{5m;Hbc!i!mF{WSzK?jC8!>Y}Y5vhgS{&RVR z{#ZYdHeSL>^`}-jbFroSw>oAu6ilCJq6IvnMJf09<-_=sM->=Q>$Nn|5dvQn$B?^D({Nmujf&+k?O#m=MRb(jgNo zm~!QkZB%q6%Wast2-68;@<)cH{ghSl#U{(pT))RIKvz z1R!N`p*Q+E!i7a5yj5`C{mWfRREV>;fE6dJt&?^&D4RT3`I{5|&dFvyIXMm*HDRi^ z10;*=LY_1wo7lc0SB@hN?m(okdQnm2&zY?TI<%AP&MgSdZ`{7mo0HO2#etX~m$x|p z=%7iY$t&%?Vl#+u(ihyk+1ulCDFMYO)Ujti1zRH%ozHK(0zp zDDIt!BTAX^b*IK?MaTr3x}&uBmy`j>kp>CmdM|KFp^-)`i2BGVW*+yU*?P|nf zC$6aEv6!D$AwNjgu0@3vq5~@O-yfF}zijm*HHeYKza+ezaMdFKoVe5CoPS=KX3v~H zx@*Ok64O)S!|AQBIqqgvXSg_F#Kk%Zyr_p2?%l@V4KSXBqfP`l%6ngI^8TTh6l))}DwfwY+34(ks}N=Ec3W+!JT2b@hLlntM)efpHDAj-UK&RJPZZF&qbFf3f? zL36RChG8j5dfVDc5NP*0+hhE`KxhL)7AFQ^1x&l%SK`APM9bN~wFH0NDWc<%+sGm9 z#YCY8NJQdh%P=@8(l-fi>0?0P0@~In%`;%XbM6oA11Uy8R$#iq0xZ;^v=GH?JS>=0 zLaLHT!bDM?rojoc5?av~6=D$HT@24aoXfT89GDmjC}lZj>|WT?t*HTE4K(Yke0HKY zg&+*e$<9wmXk!pg{cRr9POot`ji++vS=P#iRi+qDVb^4NDqyK$I%KhclyOCLgP%Ob z2%zJw28!Fc{Z7=v6x|$^0kXbys4BrOy86*FmYbft=8J;fbfisr{o;btZ)!jhldWMB zJ-&=kirKH6>lBUK!SmDmwJIS!ZA~nXts%ujE+l zNn;nGtkuYz(oBu$g~c8x;}trfb7$IuEd#F5jUV>S&{f%4f z(U1FC7&Tb)ZfJ=h0uVMVlNq}u*Q6Ej1C(RaY~m=)?^$A+1Z7dO_BGbhPC7Cz1w^IZ z4IzL;GK3TeBonerqblQT+SVXI$b#I=qpr)pF+moJLg02Y3o2hGLlb8$N@ihZGKi8l z8_8)f)Y!n?mNhaq=;=bnK^fXMzVSDQ8y5l+7*H;lr;dJ@8#C51J zN$pC53BG6N;)S;?rcSy>mcKTsa>Xv>n|giQIioC;(2(@i@B6DpRc|RCnp*NwMpoHl@YEeSx;O0=Ah=!x6?~<9`XIDs1vW@1{AE4z#Xb$t!U@3A1;-!5ERo(ABOfx z>w0sfbhRwSjqSAWOo=|vYAFC@3*BSk_M|3K*Kh9aNd+P4Ko+}fp8o*479eYLdk|wb zj&`M}%S7cxmy&Ks*h)#MkrOZ(cqex_7&dHAICtPKQZDxtXsTkCWpdGY4D2ilLW%G) zrGP|+BoMoKxdRkq{>aP5SSNL{-_ngW#ts={Mm28Ul>*pfs?fuzS=CEQ!CrMSP_Kby zch6O{z%8s&AR`(k*I>DQW>kz^q(r_ZFAhQ3i<-s7qK5z?)-8xM;NpqJAQ#z|)6mB6 zps@==$vbQQ^{9Y?=-KzH36nvD-o}+?3%DPmg2D}8DrCa`X-XzZc%~Tn_u<7u19!Ii zcxIroA$I#}Q+KXCz32oa`RVmnHVUy5o^2;u09eMBKWD9JkYv=FN+Et~Xi5h22LAw~ z!-M==p2lDPyqtz zIqjLE?5>7~UVb#C*Ymwc<1~{&1W07_ojXkun~d7er}lwp7g@LW*0E9m>Ot9_O(7V& zu4TLP7K)P=EJW|7%&wZ&1QDW5{%zKwH8cQ5FN8deLZwx1UT|*~dCq%Mozb&An$xg_3PImo_y*U7}o5TkYlY0j5^I!wtGaz$Lmnpo_2yLF(|7pi?x5J5J}t-ZIkK9LkftlLLjTWJlO84G#l z^dkZ%npQ&1SO;El_e?CFn$QfHQ6d)m`RPE#t5Sv9n{&)j3owadY(?4WsaJiSpeE61 zt_`fty(;q9M_BLMmYX6X4SH$)t5mYgOXT;o(qdbAv%grS7e<@U4xRB!c((_;Y41Ri zU!}5h#gnZtupV~pCtT3LOiiSp-59_9zf1elU~fQd^^ZBELSXr%+Mzj&`R0M*2Jv}= zb~|45MhvdMM;@csj8rihfq9P3vTc2F6f#zOO1IL^nc|hah-?JOVS}9{$|jmf zU+?RiT7(3^x7$o-tqW3?FgMmnx6DT6+bjYg25oKc8ZD(@s8mbEq0QtNj5=#ENT33M zy?`LOtJ2eUWvfY83$8dCXm2j@Wb7I5)<1QKCdRz){hpL08yWRZHqTf~Q8qLglCyNr z5j|O@kbyU6SkmS9CLx=}Ol1u{9kG0K#p-6DE^lDfUi%Q1TS+jsGM3sc5U#9Dkvi4F z3A1K4l6tX;;;`g2+3#`ew5Wu6=JhrPX*4My6)Q!z)B8ympt`Ucxa@1>#Xt~Hb=C84 znkE5=^}V#4^tCqu6vwsDCSvyzVrdYvCbK4&nI~^zgaYw-4`zJ3CiW5$q=Z|g=nTD^ z{{22H3}1_NsAU2-iTz@cW?eDwko!M*P3+C%37gYU026y<#F*Gfo%1#*B)20H3v z0{s`^_40q{a;7w!9bGzSij&tsObIs774H`-j_{WK^2xj0LajE zZdJ-I#9~5c&~KJ0knlBn*xxp_NrxjSLj)p>jNTMf1uP`WGW-psi5QNUYj=iSJ1zZJwH}HA7(j#8 zFAc|dy&)kkPiy$;O3p;X)n?jDH5r)MhC1o+?9_=D%v})eYt8K58^qL#Fm%Jx8*0Va z+Da}LhT3$uNRPTyIu`TlnFhee62kugy-n&VEQhB~(a$tbu(sjOO-3-<^pYXd+#rtV zmwH11uFc$D@ZD3TVURYMk?7ugO)4b&tD&pQP$b;uC*|?!Th=f~#*_~Bc=O^4Q7k)t zsO%rSaI$uqj%#dQdrJ3>B8kW&xW;sebLO;Q1jVeczWKc~B`20+tou-4LyOvTdGh+u zXOPjm+17=H4C|Nd=G`dh2nJ%_cPIfbI&;#R3W4+Xb<=8A#2t_Fn%t>5s3;6RwPz=#TrRCaNf242OXJu4q%u^HQU2}m zN|OffO&3nuTJ<7GreVLPgwz4zWzdZGK7ANht~Ght=K=++B_Xh4Lk56m##T?2LldUnvz`i$Ia zV|gz@Tm+1q5T1py#Av8VGTk-z6${-!nL(ti#MdBn1`X_P+MaNLGQoL^y0FLUS|G%Q zSrY^aI$ucrO7m*MsfxB-`%q^jEN&4jYFyJ=iv&fiS+vWD?Hwl~&{Gky0LLg^H;Jpl z*wdk{1d?MEn;Hh%)rm_f5?Se43!CVFJ?O@4&c5{WG2`(~mBK#+{Cm};(&umRXw{7}@4`%Nh*MC+}yBCK!gLb8iL!&`MaP-0XL z-xV+t64NAeb{;I$p+tJW*|TlAq0AsT%VW*Og8}mTql98a5m7`OydV-D!72t^pl_zrg z5i~qVU@`kX$1VxR0{7GQx(2*TL<}7_cU^j58(!&(0TZl`yGr8E?Ui9V{NKSrI4iCI zw6APsz)h)b^&pi!OK4o=%Bo3UO&qHF3tehA!CBBZ;EAGXGo0ZXSU0uhm;Or~;P=q; zP@ribN`WBLayFWpavNC;up=>(i7euV!~&R0xyo!p#=&ZUN|_}x!vd2NcpSS#2!wOkPohutVgT>0LhV$**6`}z6G+mzS@+Du=z)F?29F>M`vZPm3!11l~uqZtx2Gj3`a z43?Win49Ak2+i*$(1y0N(;Ya_rYf>}``R7mc%mluGx}XUw_=iDW1l?QfGkwh$K|AD zZ)#D7eBP1Bk9w1p1XQ~aPB(4auN1})#4A9qU6lDM6jL~f`Y3}GuAtLJQxCV;T1qV` z2JpwdiERa}TBc2~Mpt8<+#B2&DXcczq{s7G6usURV&S!Zi(ViMx#ab)d0R5C?aa0w8z0uxE+E8zykqqRgr?FPD& z-Y-JRF^rehvQsGX-z)I?!raX*Hn1LT2quA^{1c=d3M(@N@)czu|TK*hV)K$SRR+0_v@d*^z+T!Q8>a*daL9u(MJdTbk z7LMC`Q4JzQF&=A5kD`_#)z)nw8DO$VcDuVwiEyr+x1FM1TT^(U ziNgldp}4sXQ#{mx#nc%F<3JXah`X?T%)*Jvv$ZqM_Quoq^ zjGG5>LeUJQ%!j&r&`>}GHZPRh6olZ=681EH8>}|f>s^Xfm~T_I%_vo7%d|wSUv26_h_+RBKWD8HI6S>f>~q)2sEU{+ zrCEmbLKiN+eAFm)G5R~PQemKCLShzKn9ZZ)rE5ttmB~>;iSr&_g<(E=`g>6V7J9!L zRCD->KP~N6q>7t<56&%p@cPqnCk_42?PUm^OP+`8y=qJDU)%kHhRl_t<0D95*U3># z&500Q;Q*lC^-CPWEwN?{SW%#t36+k4v_N3B(1HpEV4t|`9d*H~B<)#3)8gtumL$dn zW*j(dSzU@-Xx0)|lv&6{rfiE9Y@j;|og!i}_x}J$=&73*=}rLrf##0s%K(^F1Xl(!yfGj#(q|0Ew zY0)8KAd7FLgP6!=iU__An*RWeK!U;}ep*RcjzeZ$sA&fD1vkII{g*IbMp- zVC$u`8}?cZ8KKKET*#W*=Xe?v1enFDDkz-I&eZ`n zkHCp@OU;IrF=7T^^*r>bL;=h5)4x)cgU|2A)Q|u}Z;M=yc8>8MTfD3O(z2HH@0s@N zQ5hnk5)y7WH;oQ)&6*2^jrHxdD(3OM{{Ro+C-F3&oYYkR0KvURl@Ux(PmBKmHK;I_ zW7=vk_WjZW07={{JGt1pf=-Guu2!qYEwmOO`~(!Nd?E3ZmNEi)H0u(ebO0^d)g$O> zHU#S2TqDuB>zV~o$V@y;N{!Sm3xrfsxp%Zd#iJOp1Qgi+0Iq6jE~S{ON+&fsgr4Ku zt?k+=mbq`l5KwH~&ZYk7sk4$zU7V*S4LfBl!Xbk4St3G;gdPX+lR6=hfi7Mu zFseFiYEOj)NoMj{=FOheH?wlE^-?`A(uGGcWNx@oy*isGS@|S4#Yxb7&|yd>7B;@I zK?M>$$fUkTau@Fw(wqki2rb6_TjiO*8lY(IF&;G@W(VSVGfI$b_cpiQt%m(KowL<)FDD zg4|}NYp>z|0D?_pH4#ngKa74GQmg8DpQ^DQJo425ALLZYZ}g!F2oUP-`R`m67!qUb zH{%AkwOrODTfd<`Y8nSg z`@H}+BQ2tRX(*-P=DwAk7ozuDb_#DL;V0btD!?S#W=ZSQOK(y^$_4a1^3-w?R%9)H zp4wJy&}0B6;wrhpJp5n*-VW6Mwv;t+j7o{YgL!w;3vJQ+hMw7O2Z3rK<-mf(}w6XsH$T34pwOwYNMO-3C z(UV6cTd!JNr1YOqbni=-q0h^|WK;H(?i(1ts?kClu9{*_vkyv>)&e6DIwrKssV^Jw zEw6x}i4H~r_jr45=B9>VNrI7N5I2(ES~{?HCPes-v^U)4?fAK!zA1B+s6*`&JG1Lc zbG9F0^b}4O z0AMZV`~D&X(9bZQ^>ZG1#SR8crOv64={cfh?8(udfIo?+KuTEU<$cFGW@ww}<_)%q zzOCF*o4JWe^dt_>lz0KeU(WTLlrvFSo6s3)l|6^sgdRhbAVLHNxQv?KgHVkLfkr?85X>14>P6;LehZvH~p$@}0-kgWb z&>x0iLAw*|&rbNO02avoFnV4nNKt!RXfx^fwfG)8eqvdo@X+$iG5fwd4K zI-gN9MQ(a|{3;bP_8xg^K)Aci)2ANC@5>yhOo~;!>o;a|IcNed4H5~Gi(9Q_1@3`T8R3k!U>S!~M5YGdlA;PMLlX?^-Ge ziy?63v9Gpn`cw=6_5MX#8!w%^`%nn1_YEK4&7s`1$7C4#rJDc|J^N>_J*vpkeV@d| z%f(0tjAP=_&h&Fp1)@XF)b_6Bj*<8J{8`B-x8S9Uis|_4S^T()RVuorPcakGnnn_M zke-$a^qMN=ajNU)l*y!?4j;3VF{$`LMJK#`R*H^9eUgg=nF?hYaFkmyogod3+F3ORXhpGO7!TM>a$}V)jV%8F_TrDh3Q?pusTifvqA4;qz5Ml} zkO&Xi8+yWqld1qmZ*JuKALV(9+>pQ^b=-IL~+0!T}Bq5&vW z!eVbMgK7c*SqWh7h&WejAix9?S9}N^W>{z~XVz-VOVakf3*v_X3T)Z)eYMDbJ@fF_ zG~XYHTmnEj?VUO4K%*yZx<~7+N!%x|+4J~c$$!Hzq7sB%t1ATP2#~tWs1f+j;XoO7 zbQwFq4>cQQ$GI-VIj0U%F2v|b@C@H*nG6WOeDF+LOGzzEWPJ2A&_4@5_?)T+lF}v1 z(QBpXkN|e?JZWk9t7XYv@JO#R@^NKn|ir_T+(@wgl~C+s!FyDC_>wtEBJgPLE`j8Q&2X_YYT$(Gu=t za(UnW4is+hJaA8@ATnY8YQvdIg}0 zS^ogY9qKTojjc#tugjU)o&Nx~r9gwu4!m@u8TpNyYB8Y)F102j7E|2Z&TdqABe;&T zt3=G4(Dv#c36ND=E?PtY($;-(?din`KwL~7?Wkv|zQOlUQjDlhf@hsH%_xGz&!t+1 zA`p&m7H0d#;}q92KTSBS)@txC?@j`QX8dW>wIv9-I()l)^q>kN#--e zJ43FIBdN@^j%a4H_y8ca3RTpd9E+^>qDY2jS??Fv3Cz^gQ%%Q8u_n)n{J^Yg4RA#0 zUhxkk)J5Llu?%fJ*HQ+OB-y%F`Pwuv8(vy~rHWbq0LvjmN8nwlHPaO70T(@ee^l6_ zHwhoU_h|MBgfK*FG3^0jW~X3RgeR&yMC&yfE+S*>SvohQ1i{QB(=>vEZ#c6SA~;uZ zN>vO;=`X~P)`*n@IbDlp{iaQ8ShILrez8C?FuT-`3P`M8hw+cazYFZBp#_Duv95$` zf@4ydYE+eNtAi&3%w`q>R1=v9$w;sPRLX<`QD&hyfpc{dm1S0CdSa%R{{TE+!djZ0 z>8O}9O<=1IY9gTvn78{-@c`QXxt?U2wv`+?4grq-%tX7$X6dmx1@sCC1=^Q8_27yJ zQENJDI`uuNL`Yf$xqF6gtWND+AaglWp_AdJi^C+(`1Ynmw>Lcg)V3O4RSj`XX-hQx zC}coD%mJCfYg;UZHJWD=1@OGDcTBB~LTE@Zfmc;E&}Wg89-3Fs!(du$Nt$B1}RKaQo<&u zcS1?YyX$Qtn-s6}Zr@`K?@L&OrpGHY)p`|tQIJmHjkhc~T+l+nhQUNhv<3hqg3&{f z3`|%VSU?E0x(AY$1XAfqj6zX@Miy9_xP}ekPN|`^=TTW28R-sQl_&^kW`M;IX7Ltf zohtP*vJ5m{_Mx#G=mdS($#ipGU3GQ_2|@VWs8~I zUo_TgX`^V8dPRq**4j&|p@wm_N=#uQCRi#$U}otjDisGYAeKQ=BJ?2yWg*DQsSE)} zjE&Y@H4-`^?vqn(&?3#rG~MZqW!hNhBK&;J0E`dVJ-H=Y6bR=a(*BL zyF-NMtN=s+?@!(ZMuxxmu5m*f9r;h{IEe;oxplBf;d$70TEaVox&h#VOju1$=T zBD8J#(b{U^W|K)r@BaXRKve+m8(HXu;d1?3o6nw)z`wp(siBGG3HBHQ(EwQaK-fvR zJW0f<1&E>wO%>LJdl)^uu5SvOXG`@^ZKkIckP*0b!l_U$q787KL|J*M zcA^B=tKK}>@x$)`1Mo7M`~}P4$;U({!n(otMF00iFmpo5Z1MG%UqL_y)0&fgb? zY2+3&vF8=(s6_#%de*u5aS(1J76+vX$!~gQIu{^-Cn~4`=MDg9c7M$xh2AI?;{wa% zQY|GSaHqW30m%<(yxlaCTeQUh(kH-TF25?DGuetnqT5)B!dD~ln!ChBfw2K)JJ*7E z$%Lp4)D<$_tRV(tSXpgZvq0v&CeWU`Vm&GL!Ev}Tk?0Rrbf(6whsa3e6}Td7X?zy> z^#D?g<;J&u8p6Hr$X#~!WAFY+WL*B4FzZwD6$qjnn%%9_)(%70_+mRC`cE!L}=25 zC~SPj(u8H<5bJ|*RG}+Iy=D)vQ^oG#B(^jT&gNSfG1EW0#xzUgl~Em{MNPhNS`{dT zMxBn7XofA<=@UeW2!t#IILJz2fLx@Y=nZiUZcf6ehy)57oUI`GqPE9}SEG@N<>a*eA4N5p)mm~51 z{hvlJ0RS(Q)*ST*UM;c1CbOM7$vf*qNj2&z065mNf0FSS3A7N6>z<0wqMYbGh%TCy z&MyA|7m*0crL0)MI}}UOU~L%TtZGuY41lA&tRc7uYoWNsC4mAum=bhADB)lNmt2!u zU(Fzl@RFVfZv(_AzK%jx zpLsOA0EyAP9}^OC^;W0Asma%d?4^}flAwX^zLg{`G(@b4RM~s@9RzHjFlZ4C4{bsH z`Uzf;+XYRg@;~sjH=>&&y9*sYQ({2~(llbxW2|BwaFSHU7Ox&0R?)(7R_6&|MyN;H zzZs*LAf}QkHQtjVBJmqZ)fFmlO0ov%-3h1_Lb}p1Iz^2Hjp~3>W`V$oQPMHmve{Qc zxFZuG+crcLy)Srf#o;Kclpt@64uf2UU|fdna?Ucs@M1u9ixWe-5iI`zM1qYd4%Nt6 zKm8x}{{W{QS(GZTD*JG1%+xXYxUe0C^mqsOnJ7BDks)!$_`<5{p zb>MA&vKuHNnbX?nRT`#1MalzPvJTe%!WimVT}KM4xV7ok<-!#}Om_5SavGahh!7$2 zL%v`5X$UePpn){(CNxM%Xr)n6HYPaCx$98IW{%X+O*sr-eGnTIdNZp8fyA-_968a( z3wLRP&7-c-H>83~MhjZS2DB0g2w=m=F$6)t3r>Zi=kagDn-Qubpu4^cZA$sSZSqWE=&5l5&*(5M#pU) z40bekV<7TgXhWKX`>d3c{m)LGG@)=pg1mQqU<5_JHAWPv9YZAg5e(yyIFc0X8a3Og zhUQk+Py_VDQs_!jQj}=j6lx1tAStYa_3!o88o&Jz3hztuyc`B_bgJY4t18a1DTgJ2 zgH1;D&#VY}_k)+4isq@(-a7vP*Mc|?!VcUFQ?Yx#{y0756pDFy$2$l6yiJad@dVor z+F}aqIsUjeB2O3$yM-^#AIN(}VkX@6;kjqD0KUOb(Q;DfvHI!!;nt%`PxgFbP#13* z326gYj~(T7c9@rNfOi4<8>ewyV~J&3A%U(z{8r$>a$X4$%jQ8Jubi+HkS8`k-#=Kd zMdwFA9xA+Zb%s%Fl&CpurZ{i4s4j_KlY&i7Z80@~00VGQ2yo8H*?yL?`AktaM0S^G zfw-W%p-P*Dc+|65^}kF(jiO8k7J4V7v8{$yDr(c@fhjVnv0MWUj(j zS6@{R1GBQ;M;HmKWjIm?@tho!9fCjzEr;!pXw5{_MDwoj{Cb#pM^3LeCL(NfyHwip zzqSc72EA_7bOW9axO9o^fbbBk!JIyE7PBB~D4ItDF3DW5)d+bKwGG3wEGIy!fTj&u zQDPPb2gXqW0;gu0E-Y-<)@g!h8wx<_w?`qS4UjjpMDmzY6S1!6-VhxUgb+UHPyT-vL;Ydz>V}QF-Gw3@X7EB>LSONqfGzr(5 zyyG}MsH&{o0`&^P{{Ycjm!BE9ogc0@C)WXfgZy&h#z8ZFiPz2}8E1~N$Oh@d=|4CW z1$n&Wg+aK{ivZW_mw@ZFDhvaPr^B0aWDG90x7^u=O)hLss|$ zhaB^WSL--VNAav70F(oJHNJez3R;GrIIAEz?b^}FwZ~7i78Ir)BL^B?BZW?@q@)`p zf}*)YVG!s9K`KESV-I)(sdRL5{5vpn_SghiB@B=p2!+bVF02QHM|yiU)xrkh2JOfo z80znpcN3E1qA?H^B)4+vj$WVgOwp<266}<~SNi9oc$R*{S1&{-_gtI~?k)Fz^aV!-C<2OZq zQ{S5F-@Mu7>3~6^lu|tKE+OoEa7Y>)@*bdctU`b`9=lgYqPj;5CQ=vc-yaTd0u`fC z5>E?z!#Q*NOb5GWA2nT9t{eJ(xO(VB+b#RQ3UBF0FVj6j!kG=_$|OwmF&ryZjl0BAa?#6HRWM_ zbS>Hba1bNKD1Styzhfub2q-DA_?kM`HyncMPf5Sj!(u)Gq5``hy#qMGD2Z?D4gE}LTcpPPPvZi8Q?u)wgxmAZZiprev-V_-A5Rzr z8ES}q$U7#(?{JgU#QBSs!^n`8_|Mm8Z|l@ zha8>^Obk3Q5QS>UdR!tXhy^AIDxg)qF-_(Eu7O z{{T%UWfq{?tfpXWIYD4Mg-*9_YP!Wy<3-bEChd&HLitCXM?1QC`er#znBPf;=Hw7i#>sqe2JINhrdwN6A{CGQO1Q1e}%HF-4vxFSFf!x>7E)BqaagF)7@^%-76F>K-O-tDOugF_+?q0R`BQB{WxB7&se z@cj~YQYG~=Mja4iN$dPS`E%YH`4~IV5EG4a`0*=asPNH(4&r;K6-BjGC2K))V4!ox0tsYSQ3E5%7{RY;C=Sm&-fnX3q=?bk zNc+|tsnf#7vti%6Im1M32&kwZCKA@(jp$%dH*~Py=`&I68YcHib(2YEpBNDD8W4gm z^gJuhPOOp9R2XcW4;YcBNRg-@*e9NKhS?J85Kk#hq3aG2&4*wEYMMNl>;@8ujdp8h zr=6?GRPcgwC@t0H#xQ~jwAz5ItM3*E;Vb#)>gDa=p#(G(H`Q`rHKuQuRGvrcm2!zl zye5%3-;Ae*(XglkTV?y#d0dY!{{Ri^p7L_6qRYfd-@dRMe!Y%}JovbP)wEh7plY{_ zf*M4N*evU_qTnEshkSwXa(!Wz)M0pXyE`0ru(F^u4Wo(rh8ft$SPT#fo)!oMu^oE5 z>GL`_%VJcVXEq-DQ%94WWElm-6#{`(Hu1rabjWl84v@l1Yj`L6qosB*`ElxqE{zE9 z?-#FB2U$XoXVcTVA-j0~7_uI3K$`8JzgTJ=8B_`g1PM?|q<+Vwrj;Z}LYh^sLnze? zg{Hf@Tuz#a0}69B1mMMWSGxC&ESdtAydWvYvfbvP1$`*Qaq=3o*Kwn{AzV{Mm^57{ zij@&GhaQN{#W^C80R@0WyyEIVl?W87CYx2W3nkE1QDoOu3FXTa?U4l#G?3u$T?!Hf zG>8@{Q)6#;fgsgglt$E1Nw}^sS&!PZ(#l>&g*IOZXp$)RGzn8x*3B>MpgVygEO^GN zhX7Iq?0*@is|n8ZT3(MImja2vdt6=`(X1{(yo4eKgf+$``ZPTvP$&at%D6emQMGsz ztMdKfOKU)AzS`f@onl1WJI|L|^N;I{pdGj^1D~917IZ)d!~qdhgMaTe-@GQ6wJiriiIUNTF z5P)$ygMj2}-vT!ae$p7CM*{u=)7H+p$nR^gosi;-Yg#&(l#?S}9D}M}L*d1q)(5Lv zJ4wfbHHw6SyavX}Uc_t#j7z2gpws}Kv7QIt?}M(j>n4=Hi~auq7{sJa(TRyAC!+Wi zryPzFmE?=k-OGYQz}6LFt=8Yit5Tw+w?J$9Sq4?n547)(>5dfEvqfK|`}m>t1&?H3 zl?aF3!%gve!@X+R(a)15s-ZfOh-h-x?;20%wG%`tR8Nxu#gFQ$t!@zkFfi&I2R+!~ zCg`yJa6{R~k^>248d{|7${?b~veF36;FU4>qziyZ5ER=)eIM#@oL|$vk8TtnM+?AO zm8z4hSBk`BDiu{WBmh7$-4YPHaNV6^rilRnKo?S~pb0UWt9npP#7zy{8sdamA|9#$ ziD9+|LRQFn-hs7wySp+w*oy*h0~OlNDUV<*FZtu6@tqVf9!P-E5Ovn^0xtw}vyhxS zPmJXPtgjw0;v9HD2Tslw6+S+BF{T#uNz8zG;XL6vRDKj_2K@Ew6m7>u@C4sgp3`x6 z)@VQtHOplzZFFKIFriO?QD*>`R>HLc%g2H=cJ0?Y#Pa*3Z)1_tD_cVb{{W1o&=Fup zE#C}WHGruqav>T=9AYj35tfyLuJn4B8!>vumjR}LFAnZYfYhRR?cXi`00m&GR4VJA zS5wA2IWpMID}a-ceN2Ln+1b7i?6o=znxk@0j;SE$?FOZK_L*008GaV2!&%bvDTFv zQ-;k+s%$$r-!CReUufZ(qCh3nv{sTPGIGSEfm<#MXcw~`r-Nt+bDk$C5{1>#rnwBb zDg~+=g3(Za-PwMIKesXicA?@E%yRiXjbg5Z7VupdypL1xOl;9nd=6gl*z1Lzzk9_= zQa0iNC0 zcGe`60nAZK5a=(iaoyq};4(lIZMO)Kc{eQ`wL_dxDzs%PjEvnA<2MQg$|#~8zln(# zV%dOqg&P-L+~3JsnnOdgU2B!gdeH+~1P=rU#MdAYAwUVG`q&jMW4a+WbNq+L1LuH= z;cYtC{pSD$g3yzNSB!iXGsA zF=_|eI5i%$GE!AjwLtTSjq1(-O^^bh1SE=Q0Ic{e`~mp#*Si45k|!#2v2sND*pA(j z?_)dwX$tM)4ff#*$ZWn_fZFQ;&oPF;=n`z&^cEg~P3`+lK-4MuXI9VkQz4h)9w^RRFuLT;UR(LC+%dt>cB8 zltC*Yr*}#Dv<0seYOE($<^KS(CL{;m{9cdS1^3aVpk=EiFC1po1~KZug(xQMO0m{U zb;N)yf}drJ+f))}m1tDsinym3c|ZUgNOF@7kT458Cp<_00Mmz{Aa*mB5P%&x(~ete zg^*BOCcwOC^2X+3Y*;}WBP%HvsI@{OhOP&Jo6gW_RSPr-=pgSH;X`9U+JrwE2MSuy z%)C$qG>EkvFAMd<79x7lJ0auj|fSXawgLLg7d56fkE!boW*ZMWI-54K zW4e-{G+wBSenXTZ;3ZKs0n~V(3Ju8ckVw3Auj``|zAO(=WYX7k!m~b)Z7VLs z*x9aIKcKswp*D`^n)oOf$fEKTc8?u?55=OfjX-~)Lt@?Y*B0T2Z) zjSdNJEfNEkF$16=Pj8!$tTvI3B=AZI9BQd?$irYlEHHO5!LA|%M8cLY93^Z*a4!QvdU@T z4m(PzUdCV4?=>BB7S%?oIPBpa%4@+oDGWZy8+U}$SR+%SV44y?a$y@#6>^!6R}n&N zP8Vk}YK*gzM5V)J(Y}|8i%@XENwzfW$e5vj6~evjPI+r!Kz(x{NUbZvx+G(awutr{ zV^R}kbk_4Sw}YTfS74cGc$)}VYB60YB4b=oMNvi+N-!=p%d~v_{Ip-KxNa5r1wi@oVjx;-Y~cr&r|$?As8bEk#VN^+oJnZ6Jd1j` zOjCXb&A?WMm~OsvaU|mS!zUP^fp7geJNE49*n zVxUSOqy6`Y7vG_LPuG(P$+O1jgjN*cO;DMelZcCIZ0*n(V=1^0LI7AI8bIq(zazy0 zgzvnBT#=JiTXUp50Wv9s)9AM7kkN~A9Xqx9-VXHW(tjMFi3#KU0C{2MyjjbnE!*9}z zhl5}%Qhw7_Lou~b8n9Q`;)KlfS&L~9EF~OoOI^%G;sLb5kTR~OE(sx|3D8Fjiy6X1 zyNo%md9G?UL1e4s0uKaL!;nUX0VcFg2MRr~75t((t%`k)T|)_ZPU-EG)UxPvK$Cz1 znj{F*XThv6(*t-IlnMgO6bCX&h=)K(Cc7}BT0ep4Fo#5^7{`P~%YH#YTaviONCq&0 zqRvA&tRN_om$p;lWL&D4K+}6(Q@=RiW`>sQRqy5RSmFo*qq4g8;Msr#G$}mN3U23R ztO=0~5Gh8lcbqMQD6!Rlgrn$j2u0IZ>i+;gTwo4i0@oXAlrHc$oe&5vpq!481XzKw z)`_SOda6{a=OMR&IlM^~sX4$82U*66+nPs&PDldiN@cFFPT5+czpxy#5V+Am&`fpk zkzU|YWe$~aaZwnk09MbISVRnuXbm5+DieuJO767KZV?3zXd(;(^{_}yP{0A96?lBG zwyP?87SBc!K%r3bpp*$ihEkc_OO&Ds2)b8|r=JO{U@YLk$E}z`LwQ-kL4YFx%_^sn za(jFgsVlORf!Y>;h9DI?tAo>JLOeyd?{YW1)Qg55F;F@1U0@b8hJ=7G;l#>kb#d`U zBmw6~AEp4GD*|y**E|Q#2Co>^Iv&@T5WY4P2EjXX;lK%Ka^R#?kFdmmM0Ynme;7i* znGo2!6>NQU0R&WuvuXzo?BiItXQV69u@*WWOd+%v0V*U6I3D+j6%fdR8k@!qK$W=W zC~=-72g)#qm`2Rl{GM9Pwjihi>FOsg7P)M~ep+qdz==Scq#_YtS|a}LE^5HFG5N%f_TJ$(>!%<9# zh=2)7kyx4tjg!U!PACOZ_lwXaSKf}P0Iyrl17siqEqb8jn#s?O*z${iSK~L+VmHDR zteVmHfH2(=6*m119yu}bs5J*<(rKl5$6clSv?1BJ7tc1XH1pLAhylqUHs3%M>`dLF zveYUk4&Cw2vt`2#@KVzb)d>;~W8c?|4MizxJeW0E9-1TF%csYTZ06|8%K)q)-M1uo zp=j0rd3!oBgc&G6Nt|-0-Nk;!%PS&K6w{%BC)^6kq3Vs3X-SWSbr_<^05n#S z*2)=1Z-OIw!)fgNnOvAC7Mp=B zgzV>(eZ4g`t>6`#(y~^)TnZOcM2b-IG}DQi5D=gh0Ib*;UzP;e9W?yoZG0<47N~TL z1)3DVnnZXwC+84t7vR-udXHu~<3S*Rro9K#*^S=V8{diiWUN}Q^)>!7Fdh{NzD7Lr z$DMJ2nWVWk)z^>isCC>S56POo^^o}!pLg#UQ5Co?R8#W##n!L@UmAdMvl}8LxcKk>X=oF+jU;yhA3ce zO`ULl*~)b8l5r^h$apcHUojma?Q|@%oSF9HA=; zwztX&d5+t`xDWyWt*Arm2%&BQ(b-T7q&90Jfq(?>#cUcn$Cd=VY6oi{#wClDg;K6j zqd$Bd!>w@&0-&bGj^LORdhvo3_#34Z{{W?_3}sh|(GjMxK9`eYxB`(`3}^J0alGWb z%H1flm4N!v2ar)wAcPpZL_kgASj9|*Vo2c?0|6LiJ>7eFZ*NIDg%bCy);3Wt2x@;h z6cz8&j?ihhx8K%Qe>W0p8|}txVt4V^_r_A)yp!cn`CYj46$4)cfOM&oaTS{W2j_?b z0K{}K?NL*qnDp)@Vk1t{5K5|Is+pjpvlo)5Ah@j29&@Tb{VWThch+v?NzxQ0DsD%v zI>#}hs0*Of50ZSi5-=-Mv}s=O%-3{)-5U-sC!7uwBw89cETWbzgisT(QuoStY~Hl- zjdA+L3T~n*_! zdw43mG^Q=q3hbHyF*I}LXxX!9wiV4sdmvRA1+9Wo09fQy5W*2c9|c!jSD*cp6$&{$ zpBSChG_3ref%(S!e4%+pi>trWIeu%vT~Q$k3MW%6%OD3#B5y$Dwk&OF7EJ3T|h#5cm3WY@l*;21Nb3D4KgLZ<~g( zqRb!x)&~T%UOrqZnga>ThQmeSsgf2bTDBk8B`Y^t;+$$ue@v}x4o6Jb4S0TV=fu=i z3zTTfP&mf*G!O`&IyFrO%Q+u|ML+=tqCsVdi4(mKD1;v4yrltHVy9FjS7%Nd7i65! zDHWyhqjX**+NhrS;edd6GgzdM#G-S#$ln;lvKA=;N`=*4 zIn4#;G@Vqb155ZEajiH;=#dAMABmndmoY?mVj>$Itq8=WNkf1R4ug-^{{VA?r_NA8 zeKTFL2np*%`f=?zK%=b;InqBSHe)9m4U_@2M;k+jIx05P!mep2xdCfUBa48%f4x+Z zYDTxN3y^$pSyH79hS1*^b~?ARjlu*4c;VIJbt+v2uuzipEx{%PTv`@ zTWV0{@d(q?hnY5tQWv2#{pTaP<(i4p!52s;Ap9o;6UbOxNGB{Q?e&#f=pbUkzf99F z*05=?6(=&C6zG(_%yWu1HLqR);JKhGm;pf zJ}QVRt2rPK-_9XdJ(zmm8pLP^8dbkH3oRhhAQ*Od>fk=m-D!F00`Svt(#R5vXuCiw zqtBaw(vJqO_xQn;Az2-h&Oll#)8`oxkB$Mx?Q8nvpbI(N%?de&*~ca<;T~h-=M!Qb zaQ^_F@mdQ_bJqLGe!cm@G$5CJe{L11kDu@Nc$NswZ~NAA7gZmV@_lB~KF5%yj>HmO zVk#SjY9z4L1G5I^kQ^hX#2thS#K+oQJ-fP}w>mK|H9Gf&jblVX7ilbcJ;(m7R_Ht+ zvqG9RTb+nCwJoZy`McJz2msI>U=aK?dXI5u!C|HCPIRey=d1B>wFvAg7sjiuaYfY} zOBRMfhhTvh7#^UjyVCf3MYRF=%*lLnKS0(E{{TQjONxL^?qQ)1nY@v)c%pn^7TWGf z?XSf7#7K+M1Zi&j+lR!%YTjNi=3q}U z5hoA>R#qEv3Qn-$Q}6ATeyB5&g8mol0(k&0#s@=~78;0ANN^ulsfL_wHLYfK~-pb=H)1GX$!dm?J`veU=Wp~0?MHw>=^K1gTg%c&F_i; zQK`HlpeRKF6@DuhH_^_mDsF|;G`e7%0sZ{o4p#4-V`km)uJZUa;qh_UJRNdi`58X3 zcgWYgUR&S&Ol?~?pBdoSUG2w4*c^GyVGGdWqi?r!(bi;x${3c#c8w(Iz-m*Ibk+1vx|vqzk2bEkz^G?>Qs&8+q^$-V({U4 z5GNxwF;8D*KOmr=Z!%>LzzD0PIjipo@m&n)PtrNIP^2jVf-LfV;|b|X8`KSouI7v@ zgF|PkHG^uVZ@u_{i*zS4--bS#6SOfRL&^g}jGCm2Q5&{5L0s5{a zHh^dXtTE_sJJthF1gHwMj^k(`K{KIlih=;EE%y;f=~&Qsh}0o_uH2+L)B;Iez=+zH z+f(PfZ=bgW0c&adHIegb1b-Nyl-B<6m7hAs0@vfLTNsS~+*xjqdj0W%QVy|-*iO87 z#ibWGhQaFLUZOH4o8iDSf1&l8*p1&;3o4}FkJ99V5S(6d^iBBI^Kvq@*Tw>Jf#`o> z?={QdBm0vz2oeBLAGQX`E`9$1&P^X{I~m$v%dA2x18a&>gghV|d-~To#OXy{niM?| zJfd=AStd%uG@}DR&KTf{&(1zKAKh7m;V~V5eg0}7K-Iuh`VITTry<@E1pfe1T;Yf3 z0<$Gz-yv`SYt%8JUiK-77-}dR=A3?*=m$+J%cHdhXL8Sc{Rpv{vub0aKPM7 z4e;Z@6_TZGJN-Ub5FW@uw9|#|d&Z=w!7hn_fS2GqIRJSpQc$Rs{9$8MSao0^Ub_0g zv2*p+M_w^cP(Z>IXImevQ7XEsqWRVU$++s#(`ib3?;0Wv8K@bs-uN?TprH~CYTc7& zyTBv@0if}Vy9mK+a7zY-wD>;ZkDr3v;6RteW+_**wu23bxL%*7eq2pBYriu#NwdKxh z=%p4J*c}H7I%+bm7OcIy*Q<(hm!l4C*Ej|M^??ue35Osf)0?n8%oVkE8^<9x zNjOdWW>ViU1`^Q&cAW4`C)-nMOGv@y99Lh;^{jV6~msRgV#Rf+^q~4%uq#6%_rU~hjCZymJFvqkA(Aq3Fq`Zoc@@yEWtJ- z?EW!8D!L~*RMMXHKdvCB9Zqjq1e7L^?qLhr#M!Pe1xc=O)%E!IlFv*A*80vHvO5RL zdiTaw6jOurGM1fJT^woT8%e>m5)`@=oItt&_~!!xj^m1%Cye4d)1CL4^A2q$=d?UVUi8EJNNh0JtOo zHKPRo0AV};&*Q}YlDx@!ONAEFcnjs@mGBijh zt_EN+8Ye5kJQ!@pOAij-hqDYEo&-eZ`^o~vyV>5lY;Yeo_e>us2d1Ny-41encFZmMCtBJr{9$#Xl)W#mbJ(HLuTDh&IEn6UGAzV?pqH{_&EU zpsoHsazz;LW#{WH2RS$B4;T&D2EG(1A7bXZ6t5Fus8^M3^MTRUEE^4+oQF7=ZG>kg z%GMKH4--IC@EOwfZ@fQZ%R^k(O+|XA3s9+1xbnjH$b6YU$1s+J5FvUDRA&vTk2*e! zux|GFBYtw49tTDN1eg)j@q_XH)dvaKIGaZ0gwEy|K^x?_fU9@Q2b{JEq5l5ZA=S=+ zc-SYTajzDMjyl!){A3_~5pE*>ImEjJQ2fa~>m9`E!7jnsWuLY!2#;6>lSv!o6OL(# zJ}%Ae23OH0F3Of|jCZagLN_;DW{|Rfx-+SK0s3Q@esvZKhaM>qGKH;qzdGI!a$wth>SKXd^^_AbRv|GP=@2{NN@gE{0 ziV}2WrjcCR0btcNarPTlotvBDmL|B?yyB+2 zm0SKjc*!6Dc*tO1_Y&Z|n!#E(g|9Cr8mOGd3JZR*^b5o{iM=`Fc^mGQG2(xWT_%Dr z%ggZOUuqgT)knPHxS^fi_u&zTA#lOkiACjG;@nq+UjZP1>;P4OSYnUEh|nK~yxmne zE)kUyw?c%{{Qe9{R6^iJ^|;5coSk5-QK@ylrL*yZs?zdT2hVKR;}CHKSSJ zc3Rm$UV3u-r#fLKx!PU3He*24HNP0wSkv*sRBhw&gRXa`VkVF7@gl;2YCYe1ZyLdG z^~+~p&MbU;{`}ze^~b!sJY*0&I*)q7TZ|ZeUzuPJV^9ZC+2nnkT#61)Dl&Uq`-Si* z_f4I@0isprH*%79Pv|AdYp{ZA_CdwBhN{a?0^*p$|4}Ln7GFl08BY6 zTkp;^5|KnCNY|9P@75<4APV_)=pFjX3RQ(H+r1)vnLtnqDXuoF;5RDg+=A=Np5Js9~f?gUvu(2cnZ4V`Wyxkgik24cbr#Wp^tz z=;_e0JV*Ul;vN!R!LtWwSw&K9}3 zz_*u8(Lp)K^fK&r0X|!gA9?kRW8wZimRy&IX79XW-9GreX5&8|zrVH>@7A}5`FqP# zi|?F4l5_Z<_k~^3u*Uuj90+Q-ZLxUY?}&B23;zIkC(L=8T#pC^;KyN1tfrOr|?$jEvxs}p70yH!YEA$_kT~L2BMCP()dw^=%`{!k_wWw zs;R+b#F^lOS`sizY`zJNYfN9|G^^aiA36QpIV6ExbnGt~Ty(0XH`$|~Sa?A81i+VD z^Sj0vffjkLPsRYdpQXkP`Q~Krd;T#5upkRT4R^@#EW<3)F;M}nP0bx((G@QsxAXiS zvT)ojB9%|w>9cvZLldE4e$XZ7z~aSJXRkhfKgMi_A*sd@_w%gX&Dn6i24_P-R~YE$UmeM zPq(?@0FeuO<>WVx2jd_Z8iID5wXRRIu0%ErX}D3n3*V#bQEYs80Yxp8Sr`i!Hvkvk zH{Jj%T{;*q*kNYkTJs2l`WPpOoewBboKttQU^F-QVZFT~! zf*JmXzz3QnRB~7d3o}e~i?JP<(^Dk5ZGjQ`Nu}rK0)mwlC5&D+M-Y(fVVgD>$ZxVf zKC^u`TMyq6H611R)BI!@3Z}F-{{Ylx z1uZ+{`~B8bJqI0Yf4}XU0SF&N_m*C64TTf8_rZZ}Dz~T93rynBiP1TU!4eI0OUWos zf(G}KlDn(VTEa!7+ujmLp0ER0AMrq<4c2Yj`7tVB%Ak;wlzvmTbD1{RpXC|kd7Lt5yaYUSYHva&3lkvc8*Ecj?lE9#-8+icblbkbz_Oh!QD$_%X#YHZry07>5>o?9paELNVYV88L!q`w` z1cw@N=<5Ou4kifEK0oju@rX__PO>eBH1Ym$7(tB!K1+{_9fWZm0<3q6uZl4!ql3!z z%$^sVTEJ%#lvHl1=gW(yQGwSko=zX1TyBF2z9<&Y1I@DzZhVaJo_=ruO-^*!H}yHw zl$6UE+AuP0HId#0!?Y|a5~bQBYlp)iK$k)t^Nmwq zwSv+l;$1ibv3Ch$^5h3n_H^VMwBnn5{{Vh47C|l7e!n;=A)x&K08E9xK!TwlstT^O zAe*GRq~uwTx|okft(}yJqUl1$o(3UY(Bq~OkX|8BIvNeM++$dga3%-*l({jjOM@|Q zc=u7k*p2H5)n1Ka>{Z=drKmkI5xsc9?LbLpgLO_9SMh~yN17b%uJ3-ZSDS(pm7}BQ z2?uMSP9u(<4?Db;FJOYv2K3@jB*vW8c6(wsz?cW73{Z+mpdwvqaDW*Rl+#Vd+o!yI zhHaQxJGdfXX}Bj&&Q@ z{{XDNZ)p+oCKJ~OWOQhQ#Qy*ob<#9Z%Lt0A_lso+D)(An6#epQ@-2}Dj=(4cD4c-& zKTcxPqKn-{V+Efgi>l02mk({%OP< zV_?U#7)5%+B~#G2x*kp@@R(^2AoY~+WZ$30B*tk(y+g%$9Fyk`50sp)&fCQH%Ay4g zpd~nUF#eNRP+Uhupp0>A(+(RqAW{(%W9D4IOh^wJ!^^rxcu#>R>?WeqffvKX3Z*(- zC&yVgk4!2yN=~KyGiLlJRp2KtZ}B3^MBm;401x~G{{Wqr5Zoe?-#Ex(_%cs4;loP` z*jEfpcd7bgdd1i-X@5mR(_?Y6u5-O04KaLcRn?|gaE#>(hGb40H!S5JxnN2 z3I70W5(nNW6faU>`L!yx0-#zTkJC`9wd1EO(^&?(!RfWEQ%B*Gwd S{<^`{{{Z-Z>OYV7U;o)Qe0X#K literal 0 HcmV?d00001 From 18b609fa6ed3c06c0dc3156cbb7409c9494c0bc2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 17 Jan 2016 07:55:34 +0100 Subject: [PATCH 028/142] flake8: Fix compat with pep8 1.7.0 --- mopidy/audio/scan.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ca2c308c..fd5d2d49 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -141,8 +141,9 @@ def _process(pipeline, timeout_ms): 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) + types = ( + gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | + gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) previous = clock.get_time() while timeout > 0: From 0ac77ebb475129e3530fc8cf6936200389460bc0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 17 Jan 2016 21:06:53 +0100 Subject: [PATCH 029/142] docs: Remove Raspbmc and XBian workarounds Ref https://github.com/mopidy/mopidy/issues/1326#issuecomment-172323148 Raspbmc is replaced by OSMC that works with Mopidy without conflicts. XBian I'm not sure about, but the workaround is still searchable in older version of our docs, and we'll soon be moving to GStreamer 1.x, probably affecting the dep on libtag. --- docs/installation/raspberrypi.rst | 59 ------------------------------- 1 file changed, 59 deletions(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index 386ffb30..c2fd10ad 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -88,62 +88,3 @@ directly by running: - Auto (HDMI if connected, else 3.5mm jack): ``sudo amixer cset numid=3 0`` - Use 3.5mm jack: ``sudo amixer cset numid=3 1`` - Use HDMI: ``sudo amixer cset numid=3 2`` - - -Workarounds for Raspbmc -======================= - -.. note:: - - This section is probably outdated. Get in contact if you can confirm that - this is no longer an issue or if this section needs other updates. - -Due to a dependency version problem where XBMC uses another version of -``libtag`` than what Debian originally ships with, you might have to make some -minor changes for Raspbmc to start properly after installing Mopidy. - -If you notice that XBMC is not starting but gets stuck in a loop, -you need to make the following changes:: - - sudo ln -sf /home/pi/.xbmc-current/xbmc-bin/lib/xbmc/system/libtag.so.1 \ - /usr/lib/arm-linux-gnueabihf/libtag.so.1 - -However, this will not persist the changes. To persist the changes edit -:file:`/etc/ld.so.conf.d/arm-linux-gnueabihf.conf` and add the following at the -top:: - - /home/pi/.xbmc-current/xbmc-bin/lib/xbmc/system - -It's very important to add it at the top of the file as this indicates the -priority of the folder in which to look for shared libraries. - -XBMC doesn't play nicely with the system wide installed version of libtag that -got installed together with Mopidy, but rather vendors in its own version. - -More info about this issue can be found in `this post -`_. - -Please note that if you're running Xbian or another XBMC distribution these -instructions might vary for your system. - - -Workarounds for XBian -===================== - -.. note:: - - This section is probably outdated. Get in contact if you can confirm that - this is no longer an issue or if this section needs other updates. - -Similar to the Raspbmc issue outlined above, it's not possible to install -Mopidy on XBian without first resolving a dependency problem between -``gstreamer0.10-plugins-good`` and ``libtag1c2a``. More information can be -found in `this post -`_. - -Run the following commands to remedy this and then install Mopidy as normal:: - - cd /tmp - wget http://apt.xbian.org/pool/stable/rpi-wheezy/l/libtag1c2a/libtag1c2a_1.7.2-1_armhf.deb - sudo dpkg -i libtag1c2a_1.7.2-1_armhf.deb - rm libtag1c2a_1.7.2-1_armhf.deb From f15b4e732716087a6e40787ab2c718d7a0c8f99b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 17 Jan 2016 22:14:43 +0100 Subject: [PATCH 030/142] mpd: Move setvol tests to its own test case --- tests/mpd/protocol/test_playback.py | 77 +++++++++++++++-------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/tests/mpd/protocol/test_playback.py b/tests/mpd/protocol/test_playback.py index b9adb646..470491e9 100644 --- a/tests/mpd/protocol/test_playback.py +++ b/tests/mpd/protocol/test_playback.py @@ -80,41 +80,6 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase): self.assertTrue(self.core.tracklist.repeat.get()) self.assertInResponse('OK') - def test_setvol_below_min(self): - self.send_request('setvol "-10"') - self.assertEqual(0, self.core.mixer.get_volume().get()) - self.assertInResponse('OK') - - def test_setvol_min(self): - self.send_request('setvol "0"') - self.assertEqual(0, self.core.mixer.get_volume().get()) - self.assertInResponse('OK') - - def test_setvol_middle(self): - self.send_request('setvol "50"') - self.assertEqual(50, self.core.mixer.get_volume().get()) - self.assertInResponse('OK') - - def test_setvol_max(self): - self.send_request('setvol "100"') - self.assertEqual(100, self.core.mixer.get_volume().get()) - self.assertInResponse('OK') - - def test_setvol_above_max(self): - self.send_request('setvol "110"') - self.assertEqual(100, self.core.mixer.get_volume().get()) - self.assertInResponse('OK') - - def test_setvol_plus_is_ignored(self): - self.send_request('setvol "+10"') - self.assertEqual(10, self.core.mixer.get_volume().get()) - self.assertInResponse('OK') - - def test_setvol_without_quotes(self): - self.send_request('setvol 50') - self.assertEqual(50, self.core.mixer.get_volume().get()) - self.assertInResponse('OK') - def test_single_off(self): self.send_request('single "0"') self.assertFalse(self.core.tracklist.single.get()) @@ -451,9 +416,47 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') -class PlaybackOptionsHandlerNoneMixerTest(protocol.BaseTestCase): +class VolumeTest(protocol.BaseTestCase): + + def test_setvol_below_min(self): + self.send_request('setvol "-10"') + self.assertEqual(0, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_setvol_min(self): + self.send_request('setvol "0"') + self.assertEqual(0, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_setvol_middle(self): + self.send_request('setvol "50"') + self.assertEqual(50, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_setvol_max(self): + self.send_request('setvol "100"') + self.assertEqual(100, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_setvol_above_max(self): + self.send_request('setvol "110"') + self.assertEqual(100, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_setvol_plus_is_ignored(self): + self.send_request('setvol "+10"') + self.assertEqual(10, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_setvol_without_quotes(self): + self.send_request('setvol 50') + self.assertEqual(50, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + +class VolumeWithNoMixerTest(protocol.BaseTestCase): enable_mixer = False - def test_setvol_max_error(self): + def test_setvol_without_mixer_fails(self): self.send_request('setvol "100"') self.assertInResponse('ACK [52@0] {setvol} problems setting volume') From 14444fe24f1e2990bad45e0d239fbdcf87a69ede Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 17 Jan 2016 22:28:23 +0100 Subject: [PATCH 031/142] mpd: Add volume command --- docs/changelog.rst | 3 +++ mopidy/mpd/protocol/playback.py | 24 +++++++++++++++++++ tests/mpd/protocol/test_playback.py | 36 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e8135398..135da15a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -39,6 +39,9 @@ Bug fix release. - MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1343`) +- MPD: Add ``volume`` command that was reintroduced, though still as a + deprecated command, in MPD 0.18 and is in use by some clients like mpc. + - Proxy: Handle case where :confval:`proxy/port` is either missing from config or set to an empty string. (PR: :issue:`1371`) diff --git a/mopidy/mpd/protocol/playback.py b/mopidy/mpd/protocol/playback.py index 333e1ccb..c9dcc341 100644 --- a/mopidy/mpd/protocol/playback.py +++ b/mopidy/mpd/protocol/playback.py @@ -426,3 +426,27 @@ def stop(context): Stops playing. """ context.core.playback.stop() + + +@protocol.commands.add('volume', change=protocol.INT) +def volume(context, change): + """ + *musicpd.org, playback section:* + + ``volume {CHANGE}`` + + Changes volume by amount ``CHANGE``. + + Note: ``volume`` is deprecated, use ``setvol`` instead. + """ + if change < -100 or change > 100: + raise exceptions.MpdArgError('Invalid volume value', command='volume') + + old_volume = context.core.mixer.get_volume().get() + if old_volume is None: + raise exceptions.MpdSystemError('problems setting volume') + + new_volume = min(max(0, old_volume + change), 100) + success = context.core.mixer.set_volume(new_volume).get() + if not success: + raise exceptions.MpdSystemError('problems setting volume') diff --git a/tests/mpd/protocol/test_playback.py b/tests/mpd/protocol/test_playback.py index 470491e9..de02ae36 100644 --- a/tests/mpd/protocol/test_playback.py +++ b/tests/mpd/protocol/test_playback.py @@ -453,6 +453,38 @@ class VolumeTest(protocol.BaseTestCase): self.assertEqual(50, self.core.mixer.get_volume().get()) self.assertInResponse('OK') + def test_volume_plus(self): + self.core.mixer.set_volume(50) + + self.send_request('volume +20') + + self.assertEqual(70, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_volume_minus(self): + self.core.mixer.set_volume(50) + + self.send_request('volume -20') + + self.assertEqual(30, self.core.mixer.get_volume().get()) + self.assertInResponse('OK') + + def test_volume_less_than_minus_100(self): + self.core.mixer.set_volume(50) + + self.send_request('volume -110') + + self.assertEqual(50, self.core.mixer.get_volume().get()) + self.assertInResponse('ACK [2@0] {volume} Invalid volume value') + + def test_volume_more_than_plus_100(self): + self.core.mixer.set_volume(50) + + self.send_request('volume +110') + + self.assertEqual(50, self.core.mixer.get_volume().get()) + self.assertInResponse('ACK [2@0] {volume} Invalid volume value') + class VolumeWithNoMixerTest(protocol.BaseTestCase): enable_mixer = False @@ -460,3 +492,7 @@ class VolumeWithNoMixerTest(protocol.BaseTestCase): def test_setvol_without_mixer_fails(self): self.send_request('setvol "100"') self.assertInResponse('ACK [52@0] {setvol} problems setting volume') + + def test_volume_without_mixer_failes(self): + self.send_request('volume +100') + self.assertInResponse('ACK [52@0] {volume} problems setting volume') From 450b1d0f7c8039283abe2f37a6f0f2b1e65e4ef4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 21:21:35 +0100 Subject: [PATCH 032/142] docs: Remove section on missing Debian-packaged extensions For apt.mopidy.com, none of this is an issue. For Ubuntu: - none of this is an issue in 15.10 or 16.04 LTS. - 15.04 is soon out of support. - 14.04 LTS is old enough that you'll have to read older docs to expect facts matching your installation. For Debian: - none of this is an issue in sid or stretch. - Jessie this might be an issue, but the same sentiment as for Ubuntu 14.04 LTS applies. Fixes #1399 --- docs/debian.rst | 15 ------------- docs/installation/debian.rst | 41 ------------------------------------ 2 files changed, 56 deletions(-) diff --git a/docs/debian.rst b/docs/debian.rst index f761c4b0..d7279567 100644 --- a/docs/debian.rst +++ b/docs/debian.rst @@ -112,18 +112,3 @@ from a regular Mopidy setup you'll want to know about. - You can check if Mopidy is currently running as a system service by running:: sudo service mopidy status - -- 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. - 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 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. diff --git a/docs/installation/debian.rst b/docs/installation/debian.rst index a04dfa25..5b4bf841 100644 --- a/docs/installation/debian.rst +++ b/docs/installation/debian.rst @@ -87,44 +87,3 @@ about any other requirements needed for the extension to work properly. For a full list of available Mopidy extensions, including those not installable from apt.mopidy.com, see :ref:`ext`. - - -Missing extensions -================== - -If you've installed a Mopidy extension with pip, restarted Mopidy, and Mopidy -doesn't find the extension, there's probably a simple explanation and solution. - -Mopidy installed with APT can detect and use Mopidy extensions installed with -both APT and pip. APT installs Mopidy as :file:`/usr/bin/mopidy`. - -Mopidy installed with pip can only detect Mopidy extensions installed with pip. -pip usually installs Mopidy as :file:`/usr/local/bin/mopidy`. - -If you have Mopidy installed from both APT and pip, then the pip-installed -Mopidy will probably shadow the APT-installed Mopidy because -:file:`/usr/local/bin` usually has precedence over :file:`/usr/bin` in the -``PATH`` environment variable. To check if this is the case on your system, you -can use ``which`` to see what installation of Mopidy you use when you run -``mopidy`` in your shell:: - - $ which mopidy - /usr/local/bin/mopidy - -If this is the case on your system, the recommended solution is to check that -you have Mopidy installed from APT too:: - - $ /usr/bin/mopidy --version - Mopidy 0.19.5 - -And then uninstall the pip-installed Mopidy:: - - sudo pip uninstall mopidy - -Depending on what shell you use, the shell may still try to use -:file:`/usr/local/bin/mopidy` even if it no longer exists. Check again with -``which mopidy`` what your shell believes is the right ``mopidy`` executable to -run. If the shell is still confused, you may need to restart it, or in the case -of zsh, run ``rehash`` to update the shell. - -For more details on why this works this way, see :ref:`debian`. From e69ea22cdd2da485da3f215c774a38dffc1fcb27 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 21:40:55 +0100 Subject: [PATCH 033/142] docs: Remove old cruft from Debian package page Part of #1398 --- docs/debian.rst | 47 ++++++++++------------------------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/docs/debian.rst b/docs/debian.rst index d7279567..25df7a4b 100644 --- a/docs/debian.rst +++ b/docs/debian.rst @@ -26,30 +26,19 @@ Running as a system service =========================== The Debian package comes with an init script. It starts Mopidy as a system -service running as the ``mopidy`` user, which is created by the package. +service running as the ``mopidy`` user. The user is created by the package. -The Debian package version 0.18.3-1 and older starts Mopidy as a system -service by default. Version 0.18.3-2 and newer asks if you want to run Mopidy -as a system service, defaulting to not doing so. +The Debian package might ask if you want to run Mopidy as a system service. If +you don't get the question, your system is probably configured to ignore +questions at that priority level during installs, and defaults to not enabling +the Mopidy service. -If you're running 0.18.3-2 or newer, and you've changed your mind about whether -or not to run Mopidy as a system service, just run the following command to +If you didn't get the question or if you've changed your mind about whether or +not to run Mopidy as a system service, just run the following command to reconfigure the package:: sudo dpkg-reconfigure mopidy -If you're running 0.18.3-1 or older, and don't want to use the init script to -run Mopidy as a system service, but instead just run Mopidy manually using your -own user, you need to disable the init script and stop Mopidy by running:: - - sudo update-rc.d mopidy disable - sudo service mopidy stop - -This way of disabling the system service is compatible with the improved -0.18.3-2 or newer version of the Debian package, so if you later upgrade to a -newer version, you can change your mind using the ``dpkg-reconfigure`` command -above. - Differences when running as a system service ============================================ @@ -59,20 +48,9 @@ from a regular Mopidy setup you'll want to know about. - All configuration is in :file:`/etc/mopidy`, not in your user's home directory. The main configuration file is :file:`/etc/mopidy/mopidy.conf`. - You can do all your changes in this file. - -- Mopidy extensions installed from Debian packages will sometimes install - additional configuration files in :file:`/usr/share/mopidy/conf.d/`. These - files just provide different defaults for the extension when run as a system - service. You can override anything from :file:`/usr/share/mopidy/conf.d/` in - the :file:`/etc/mopidy/mopidy.conf` configuration file. - - Previously, the extension's default config was installed in - :file:`/etc/mopidy/extensions.d/`. This was removed with the Debian - package mopidy 0.19.4-3. If you have modified any files in - :file:`/etc/mopidy/extensions.d/`, you should redo your modifications in - :file:`/etc/mopidy/mopidy.conf` and delete the - :file:`/etc/mopidy/extensions.d/` directory. + This is the configuration file with the highest priority, so it can override + configs from all other config files. Thus, you can do all your changes in + this file. - The init script runs Mopidy as the ``mopidy`` user. The ``mopidy`` user will need read access to any local music you want Mopidy to play. @@ -97,11 +75,6 @@ from a regular Mopidy setup you'll want to know about. sudo mopidyctl local scan - Previously, you used ``sudo service mopidy run `` instead of - ``mopidyctl``. This was deprecated in Debian package version 0.19.4-3 in - favor of ``mopidyctl``, which also work for systems using systemd instead of - sysvinit and traditional init scripts. - - Mopidy is started, stopped, and restarted just like any other system service:: From 123614ad69bc7c46947e71056e4ac4fcdcae26ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:05:03 +0100 Subject: [PATCH 034/142] docs: Repurpose Debian pkg page as a service page Fixes #1398 --- docs/debian.rst | 87 ---------------------------------- docs/index.rst | 2 +- docs/installation/debian.rst | 2 +- docs/running.rst | 21 ++------ docs/service.rst | 92 ++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 106 deletions(-) delete mode 100644 docs/debian.rst create mode 100644 docs/service.rst diff --git a/docs/debian.rst b/docs/debian.rst deleted file mode 100644 index 25df7a4b..00000000 --- a/docs/debian.rst +++ /dev/null @@ -1,87 +0,0 @@ -.. _debian: - -*************** -Debian packages -*************** - -The Mopidy Debian package, ``mopidy``, is available from `apt.mopidy.com -`__ as well as from Debian, Ubuntu and other -Debian-based Linux distributions. - -Some extensions are also available from all of these sources, while others, -like Mopidy-Spotify and its dependencies, are only available from -apt.mopidy.com. This may either be temporary until the package is uploaded to -Debian and with time propagates to the other distributions. It may also be more -long term, like in the Mopidy-Spotify case where there is uncertainities around -licensing and distribution of non-free packages. - - -Installation -============ - -See :ref:`debian-install`. - - -Running as a system service -=========================== - -The Debian package comes with an init script. It starts Mopidy as a system -service running as the ``mopidy`` user. The user is created by the package. - -The Debian package might ask if you want to run Mopidy as a system service. If -you don't get the question, your system is probably configured to ignore -questions at that priority level during installs, and defaults to not enabling -the Mopidy service. - -If you didn't get the question or if you've changed your mind about whether or -not to run Mopidy as a system service, just run the following command to -reconfigure the package:: - - sudo dpkg-reconfigure mopidy - - -Differences when running as a system service -============================================ - -If you want to run Mopidy using the init script, there's a few differences -from a regular Mopidy setup you'll want to know about. - -- All configuration is in :file:`/etc/mopidy`, not in your user's home - directory. The main configuration file is :file:`/etc/mopidy/mopidy.conf`. - This is the configuration file with the highest priority, so it can override - configs from all other config files. Thus, you can do all your changes in - this file. - -- The init script runs Mopidy as the ``mopidy`` user. The ``mopidy`` user will - need read access to any local music you want Mopidy to play. - -- To run Mopidy subcommands with the same user and config files as the init - script uses, you can use ``sudo mopidyctl ``. In other words, - where you'll usually run:: - - mopidy config - - You should instead run the following to inspect the system service's - configuration:: - - sudo mopidyctl config - - The same applies to scanning your local music collection. Where you'll - normally run:: - - mopidy local scan - - You should instead run:: - - sudo mopidyctl local scan - -- Mopidy is started, stopped, and restarted just like any other system - service:: - - sudo service mopidy start - sudo service mopidy stop - sudo service mopidy restart - -- You can check if Mopidy is currently running as a system service by running:: - - sudo service mopidy status diff --git a/docs/index.rst b/docs/index.rst index 70d14a73..e6b2da98 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,8 +81,8 @@ announcements related to Mopidy and Mopidy extensions. installation/index config running + service troubleshooting - debian .. _ext: diff --git a/docs/installation/debian.rst b/docs/installation/debian.rst index 5b4bf841..728467c4 100644 --- a/docs/installation/debian.rst +++ b/docs/installation/debian.rst @@ -48,7 +48,7 @@ and armhf (compatible with Raspberry Pi 1 and 2). sudo apt-get update sudo apt-get install mopidy -#. Before continuing, make sure you've read the :ref:`debian` section to learn +#. Before continuing, make sure you've read the :ref:`service` section to learn about the differences between running Mopidy as a system service and manually as your own system user. diff --git a/docs/running.rst b/docs/running.rst index e329ccaa..1aa0a657 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -39,21 +39,8 @@ using ``pkill``:: pkill mopidy -Init scripts -============ +Running as a service +==================== -- The ``mopidy`` package at `apt.mopidy.com `__ comes - with an `sysvinit init script - `_. For - more details, see the :ref:`debian` section of the docs. - -- The ``mopidy`` package in `Arch Linux - `__ comes with a systemd init - script. - -- A blog post by Benjamin Guillet explains how to `Daemonize Mopidy and Launch - It at Login on OS X - `_. - -- Issue :issue:`266` contains a bunch of init scripts for Mopidy, including - Upstart init scripts. +Once you're done exploring Mopidy and want to run it as a proper service, check +out :ref:`service`. diff --git a/docs/service.rst b/docs/service.rst new file mode 100644 index 00000000..a2664d77 --- /dev/null +++ b/docs/service.rst @@ -0,0 +1,92 @@ +.. _service: + +******************** +Running as a service +******************** + +If you want to run Mopidy as a service using either an init script or a systemd +service, there's a few differences from running Mopidy as your own user you'll +want to know about. The following applies to Debian, Ubuntu, Raspbian, and +Arch. Hopefully, other distributions packaging Mopidy will make sure this works +the same way on their distribution. + + +Configuration +============= + +All configuration is in :file:`/etc/mopidy`, not in your user's home directory. + +The main configuration file is :file:`/etc/mopidy/mopidy.conf`. If there are +more than one configuration file, this is the configuration file with the +highest priority, so it can override configs from all other config files. +Thus, you can do all your changes in this file. + + +mopidy User +=========== + +The init script runs Mopidy as the ``mopidy`` user, which is automatically +created when you install the Mopidy package. The ``mopidy`` user will need read +access to any local music you want Mopidy to play. + + +Subcommands +=========== + +To run Mopidy subcommands with the same user and config files as the service +uses, you can use ``sudo mopidyctl ``. In other words, where you'll +usually run:: + + mopidy config + +You should instead run the following to inspect the service's configuration:: + + sudo mopidyctl config + +The same applies to scanning your local music collection. Where you'll normally +run:: + + mopidy local scan + +You should instead run:: + + sudo mopidyctl local scan + + +Service management with systemd +=============================== + +On modern systems using systemd you can enable the Mopidy service by running:: + + sudo systemctl enable mopidy + +This will make Mopidy start when the system boots. + +Mopidy is started, stopped, and restarted just like any other systemd service:: + + sudo systemctl start mopidy + sudo systemctl stop mopidy + sudo systemctl restart mopidy + +You can check if Mopidy is currently running as a service by running:: + + sudo systemctl status mopidy + + +Service management on Debian +============================ + +On Debian systems (both those using systemd and not) you can enable the Mopidy +service by running:: + + sudo dpkg-reconfigure mopidy + +Mopidy can be started, stopped, and restarted using the ``service`` command:: + + sudo service mopidy start + sudo service mopidy stop + sudo service mopidy restart + +You can check if Mopidy is currently running as a service by running:: + + sudo service mopidy status From bd0c88b22b10342dd552a31859be22eb0ddc656d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:06:28 +0100 Subject: [PATCH 035/142] docs: Fix title capitalization --- docs/service.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/service.rst b/docs/service.rst index a2664d77..7e7f7232 100644 --- a/docs/service.rst +++ b/docs/service.rst @@ -22,7 +22,7 @@ highest priority, so it can override configs from all other config files. Thus, you can do all your changes in this file. -mopidy User +mopidy user =========== The init script runs Mopidy as the ``mopidy`` user, which is automatically From 2b0c121e6c7aa960ee3531f409d50bd7c1664f15 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:21:20 +0100 Subject: [PATCH 036/142] docs: Refer to service docs from Arch and Debian install docs --- docs/installation/arch.rst | 3 ++- docs/installation/debian.rst | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/installation/arch.rst b/docs/installation/arch.rst index c5675403..59928a3a 100644 --- a/docs/installation/arch.rst +++ b/docs/installation/arch.rst @@ -16,7 +16,8 @@ If you are running Arch Linux, you can install Mopidy using the pacman -Syu #. Finally, you need to set a couple of :doc:`config values `, and - then you're ready to :doc:`run Mopidy `. + then you're ready to :doc:`run Mopidy ` or run Mopidy as a + :ref:`service `. Installing extensions diff --git a/docs/installation/debian.rst b/docs/installation/debian.rst index 728467c4..a1ad339e 100644 --- a/docs/installation/debian.rst +++ b/docs/installation/debian.rst @@ -48,12 +48,9 @@ and armhf (compatible with Raspberry Pi 1 and 2). sudo apt-get update sudo apt-get install mopidy -#. Before continuing, make sure you've read the :ref:`service` section to learn - about the differences between running Mopidy as a system service and - manually as your own system user. - #. Finally, you need to set a couple of :doc:`config values `, and then - you're ready to :doc:`run Mopidy `. + you're ready to :doc:`run Mopidy ` or run Mopidy as a + :ref:`service `. When a new release of Mopidy is out, and you can't wait for you system to figure it out for itself, run the following to upgrade right away:: From 0ea3de2c6831e6c47c562da860264ad6e04b0a93 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:21:37 +0100 Subject: [PATCH 037/142] docs: Refer to OS X service from service docs --- docs/installation/osx.rst | 2 ++ docs/service.rst | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/docs/installation/osx.rst b/docs/installation/osx.rst index e9ce16e3..45c554ea 100644 --- a/docs/installation/osx.rst +++ b/docs/installation/osx.rst @@ -86,6 +86,8 @@ For a full list of available Mopidy extensions, including those not installable from Homebrew, see :ref:`ext`. +.. _osx-service: + Running Mopidy automatically on login ===================================== diff --git a/docs/service.rst b/docs/service.rst index 7e7f7232..e99e1645 100644 --- a/docs/service.rst +++ b/docs/service.rst @@ -90,3 +90,9 @@ Mopidy can be started, stopped, and restarted using the ``service`` command:: You can check if Mopidy is currently running as a service by running:: sudo service mopidy status + + +Service on OS X +=============== + +If you're installing Mopidy on OS X, see :ref:`osx-service`. From 77e2e088fd9db9c3197b60535d3ed8aa9a936bc2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:34:10 +0100 Subject: [PATCH 038/142] docs: Cleanup changelog --- docs/changelog.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e8135398..2f54ec67 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,12 +9,12 @@ v1.1.2 (UNRELEASED) Bug fix release. -- Main: Catch errors when loading :confval:`logging/config_file`. (Fixes: - :issue:`1320`) +- Main: Catch errors when loading the :confval:`logging/config_file` file. + (Fixes: :issue:`1320`) -- Core: If changing to another track while - the player is paused, the new track would not be added to the history or - marked as currently playing. (Fixes: :issue:`1352`, PR: :issue:`1356`) +- Core: If changing to another track while the player is paused, the new track + would not be added to the history or marked as currently playing. (Fixes: + :issue:`1352`, PR: :issue:`1356`) - Core: Skips over unplayable tracks if the user attempts to change tracks while paused, like we already did if in playing state. (Fixes :issue:`1378`, From b09e667127609c04ac7786fe29e3cdcea589041a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:44:11 +0100 Subject: [PATCH 039/142] mpd: Fix review comments --- docs/changelog.rst | 1 + mopidy/mpd/protocol/playback.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 135da15a..6e6d0bde 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -41,6 +41,7 @@ Bug fix release. - MPD: Add ``volume`` command that was reintroduced, though still as a deprecated command, in MPD 0.18 and is in use by some clients like mpc. + (Fixes: :issue:`1393`, PR: :issue:`1397`) - Proxy: Handle case where :confval:`proxy/port` is either missing from config or set to an empty string. (PR: :issue:`1371`) diff --git a/mopidy/mpd/protocol/playback.py b/mopidy/mpd/protocol/playback.py index c9dcc341..48aaae2c 100644 --- a/mopidy/mpd/protocol/playback.py +++ b/mopidy/mpd/protocol/playback.py @@ -440,7 +440,7 @@ def volume(context, change): Note: ``volume`` is deprecated, use ``setvol`` instead. """ if change < -100 or change > 100: - raise exceptions.MpdArgError('Invalid volume value', command='volume') + raise exceptions.MpdArgError('Invalid volume value') old_volume = context.core.mixer.get_volume().get() if old_volume is None: From b0b3bd85dcd84505538d9e85fe9f28b762563a1d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:53:27 +0100 Subject: [PATCH 040/142] docs: Update copyright years --- docs/authors.rst | 2 +- docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/authors.rst b/docs/authors.rst index 90ec6f23..f4f93d56 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -4,7 +4,7 @@ Authors ******* -Mopidy is copyright 2009-2015 Stein Magnus Jodal and contributors. Mopidy is +Mopidy is copyright 2009-2016 Stein Magnus Jodal and contributors. Mopidy is licensed under the `Apache License, Version 2.0 `_. diff --git a/docs/conf.py b/docs/conf.py index cbb2f228..3a93cc90 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -93,14 +93,14 @@ source_suffix = '.rst' master_doc = 'index' project = 'Mopidy' -copyright = '2009-2015, Stein Magnus Jodal and contributors' +copyright = '2009-2016, Stein Magnus Jodal and contributors' from mopidy.internal.versioning import get_version release = get_version() version = '.'.join(release.split('.')[:2]) # To make the build reproducible, avoid using today's date in the manpages -today = '2015' +today = '2016' exclude_trees = ['_build'] From 89888cde48c2357c1e2ed4798ea66cd8196273cb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:52:38 +0100 Subject: [PATCH 041/142] Bump version to 1.1.2 --- mopidy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index df9aacc3..59d0444e 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.1' +__version__ = '1.1.2' From 6d0e3ec698f93eb1823c90b87518f7779b1d169a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 22:53:02 +0100 Subject: [PATCH 042/142] docs: Update changelog for 1.1.2 --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a6ebfc61..8054ac82 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog This changelog is used to track all major changes to Mopidy. -v1.1.2 (UNRELEASED) +v1.1.2 (2016-01-18) =================== Bug fix release. From 37a34a734c60de66a74bd951afa7ca17bddc958a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 23:01:46 +0100 Subject: [PATCH 043/142] docs: Address @trygveaa's review comments --- docs/service.rst | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/service.rst b/docs/service.rst index e99e1645..2b608ed6 100644 --- a/docs/service.rst +++ b/docs/service.rst @@ -14,20 +14,16 @@ the same way on their distribution. Configuration ============= -All configuration is in :file:`/etc/mopidy`, not in your user's home directory. - -The main configuration file is :file:`/etc/mopidy/mopidy.conf`. If there are -more than one configuration file, this is the configuration file with the -highest priority, so it can override configs from all other config files. -Thus, you can do all your changes in this file. +All configuration is in :file:`/etc/mopidy/mopidy.conf`, not in your user's +home directory. mopidy user =========== -The init script runs Mopidy as the ``mopidy`` user, which is automatically -created when you install the Mopidy package. The ``mopidy`` user will need read -access to any local music you want Mopidy to play. +The Mopidy service runs as the ``mopidy`` user, which is automatically created +when you install the Mopidy package. The ``mopidy`` user will need read access +to any local music you want Mopidy to play. Subcommands From 1c19dd5d861113d8f6138d63838fadf3379bd238 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jan 2016 23:05:59 +0100 Subject: [PATCH 044/142] docs: Update authors --- .mailmap | 1 + AUTHORS | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.mailmap b/.mailmap index 0682f673..8f98ce5b 100644 --- a/.mailmap +++ b/.mailmap @@ -27,3 +27,4 @@ Ronald Zielaznicki Kyle Heyne Tom Roth Eric Jahn +Loïck Bonniot diff --git a/AUTHORS b/AUTHORS index a370ce6c..94ec5baf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -67,3 +67,8 @@ - Danilo Bargen - Bjørnar Snoksrud - Giorgos Logiotatidis +- Ben Evans +- vrs01 +- Loïck Bonniot +- Cadel Watson +- Daniel Hahler From d13910026198501bbb1fc2bf7ff0cb0a3c36bcdd Mon Sep 17 00:00:00 2001 From: Thomas Kemmer Date: Thu, 28 Jan 2016 21:07:51 +0100 Subject: [PATCH 045/142] Fix #1410: Link mopidy-local-{images,sqlite} to mopidy repo. --- docs/ext/backends.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ext/backends.rst b/docs/ext/backends.rst index 7a9dc506..2349006b 100644 --- a/docs/ext/backends.rst +++ b/docs/ext/backends.rst @@ -115,7 +115,7 @@ Bundled with Mopidy. See :ref:`ext-local`. Mopidy-Local-Images =================== -https://github.com/tkem/mopidy-local-images +https://github.com/mopidy/mopidy-local-images Extension which plugs into Mopidy-Local to allow Web clients access to album art embedded in local media files. Not to be used on its own, @@ -126,7 +126,7 @@ local library provider being used. Mopidy-Local-SQLite =================== -https://github.com/tkem/mopidy-local-sqlite +https://github.com/mopidy/mopidy-local-sqlite Extension which plugs into Mopidy-Local to use an SQLite database to keep track of your local media. This extension lets you browse your music collection From 2232260d1b5141b18794df83a1f5c0a747ca302a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Jan 2016 07:50:43 +0100 Subject: [PATCH 046/142] tests: Fix typo, don't use deprecated API --- tests/core/test_playback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 3ddc51f3..bef06510 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -231,7 +231,7 @@ class TestPreviousHandling(BaseTest): self.assertIn(tl_tracks[1], self.core.tracklist.tl_tracks) -class TestPlayUnknownHanlding(BaseTest): +class TestPlayUnknownHandling(BaseTest): tracks = [Track(uri='unknown:a', length=1234), Track(uri='dummy:b', length=1234)] @@ -263,7 +263,7 @@ class TestConsumeHandling(BaseTest): tl_track = self.core.tracklist.get_tl_tracks()[0] self.core.playback.play(tl_track) - self.core.tracklist.consume = True + self.core.tracklist.set_consume(True) self.replay_events() self.core.playback.next() From d046974aaf520af38ea8475055f2a239f8fee90c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 1 Sep 2015 23:58:48 +0200 Subject: [PATCH 047/142] gst1: Remove IcySrc It was a workaround for icy:// support on GStreamer 0.10. --- mopidy/audio/actor.py | 4 +-- mopidy/audio/icy.py | 63 ------------------------------------------- 2 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 mopidy/audio/icy.py diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index b8b3d9a4..9645c4af 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -14,7 +14,7 @@ import gst.pbutils # noqa import pykka from mopidy import exceptions -from mopidy.audio import icy, utils +from mopidy.audio import utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.internal import deprecation, process @@ -27,8 +27,6 @@ logger = logging.getLogger(__name__) # set_state on a pipeline. gst_logger = logging.getLogger('mopidy.audio.gst') -icy.register() - _GST_STATE_MAPPING = { gst.STATE_PLAYING: PlaybackState.PLAYING, gst.STATE_PAUSED: PlaybackState.PAUSED, diff --git a/mopidy/audio/icy.py b/mopidy/audio/icy.py deleted file mode 100644 index dd59baae..00000000 --- a/mopidy/audio/icy.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import gobject - -import pygst -pygst.require('0.10') -import gst # noqa - - -class IcySrc(gst.Bin, gst.URIHandler): - __gstdetails__ = ('IcySrc', - 'Src', - 'HTTP src wrapper for icy:// support.', - 'Mopidy') - - srcpad_template = gst.PadTemplate( - 'src', gst.PAD_SRC, gst.PAD_ALWAYS, - gst.caps_new_any()) - - __gsttemplates__ = (srcpad_template,) - - def __init__(self): - super(IcySrc, self).__init__() - self._httpsrc = gst.element_make_from_uri(gst.URI_SRC, 'http://') - try: - self._httpsrc.set_property('iradio-mode', True) - except TypeError: - pass - self.add(self._httpsrc) - - self._srcpad = gst.GhostPad('src', self._httpsrc.get_pad('src')) - self.add_pad(self._srcpad) - - @classmethod - def do_get_type_full(cls): - return gst.URI_SRC - - @classmethod - def do_get_protocols_full(cls): - return [b'icy', b'icyx'] - - def do_set_uri(self, uri): - if uri.startswith('icy://'): - return self._httpsrc.set_uri(b'http://' + uri[len('icy://'):]) - elif uri.startswith('icyx://'): - return self._httpsrc.set_uri(b'https://' + uri[len('icyx://'):]) - else: - return False - - def do_get_uri(self): - uri = self._httpsrc.get_uri() - if uri.startswith('http://'): - return b'icy://' + uri[len('http://'):] - else: - return b'icyx://' + uri[len('https://'):] - - -def register(): - # Only register icy if gst install can't handle it on it's own. - if not gst.element_make_from_uri(gst.URI_SRC, 'icy://'): - gobject.type_register(IcySrc) - gst.element_register( - IcySrc, IcySrc.__name__.lower(), gst.RANK_MARGINAL) From 8c82f4773ffd48eec76a19bd78d76cb0e9f27a2d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:24:02 +0200 Subject: [PATCH 048/142] gst1: Update imports to use PyGI --- mopidy/__main__.py | 8 +- mopidy/audio/actor.py | 161 +++++++++++----------- mopidy/audio/scan.py | 69 +++++----- mopidy/audio/utils.py | 64 ++++----- mopidy/commands.py | 8 +- mopidy/internal/deps.py | 17 ++- mopidy/internal/network.py | 28 ++-- mopidy/internal/playlists.py | 4 - tests/audio/test_actor.py | 42 +++--- tests/audio/test_scan.py | 8 +- tests/internal/network/test_connection.py | 96 ++++++------- tests/internal/network/test_server.py | 12 +- tests/internal/test_deps.py | 17 ++- tests/internal/test_path.py | 4 +- 14 files changed, 263 insertions(+), 275 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index fbc750af..c1cf42f9 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -7,12 +7,12 @@ import sys import textwrap try: - import gobject # noqa + from gi.repository import GObject, Gst except ImportError: print(textwrap.dedent(""" - ERROR: The gobject Python package was not found. + ERROR: The GObject and Gst Python packages were not found. - Mopidy requires GStreamer (and GObject) to work. These are C libraries + Mopidy requires GStreamer and GObject to work. These are C libraries with a number of dependencies themselves, and cannot be installed with the regular Python tools like pip. @@ -21,7 +21,7 @@ except ImportError: """)) raise -gobject.threads_init() +GObject.threads_init() try: # Make GObject's mainloop the event loop for python-dbus diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 9645c4af..3595092e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -4,12 +4,9 @@ import logging import os import threading -import gobject - -import pygst -pygst.require('0.10') -import gst # noqa -import gst.pbutils # noqa +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst import pykka @@ -28,9 +25,9 @@ logger = logging.getLogger(__name__) gst_logger = logging.getLogger('mopidy.audio.gst') _GST_STATE_MAPPING = { - gst.STATE_PLAYING: PlaybackState.PLAYING, - gst.STATE_PAUSED: PlaybackState.PAUSED, - gst.STATE_NULL: PlaybackState.STOPPED} + Gst.STATE_PLAYING: PlaybackState.PLAYING, + Gst.STATE_PAUSED: PlaybackState.PAUSED, + Gst.STATE_NULL: PlaybackState.STOPPED} class _Signals(object): @@ -118,9 +115,9 @@ class _Appsrc(object): if buffer_ is None: gst_logger.debug('Sending appsrc end-of-stream event.') - return self._source.emit('end-of-stream') == gst.FLOW_OK + return self._source.emit('end-of-stream') == Gst.FLOW_OK else: - return self._source.emit('push-buffer', buffer_) == gst.FLOW_OK + return self._source.emit('push-buffer', buffer_) == Gst.FLOW_OK def _on_signal(self, element, clocktime, func): # This shim is used to ensure we always return true, and also handles @@ -133,29 +130,29 @@ class _Appsrc(object): # TODO: expose this as a property on audio when #790 gets further along. -class _Outputs(gst.Bin): +class _Outputs(Gst.Bin): def __init__(self): - gst.Bin.__init__(self, 'outputs') + Gst.Bin.__init__(self, 'outputs') - self._tee = gst.element_factory_make('tee') + self._tee = Gst.element_factory_make('tee') self.add(self._tee) - ghost_pad = gst.GhostPad('sink', self._tee.get_pad('sink')) + ghost_pad = Gst.GhostPad('sink', self._tee.get_pad('sink')) self.add_pad(ghost_pad) # Add an always connected fakesink which respects the clock so the tee # doesn't fail even if we don't have any outputs. - fakesink = gst.element_factory_make('fakesink') + fakesink = Gst.element_factory_make('fakesink') fakesink.set_property('sync', True) self._add(fakesink) def add_output(self, description): # XXX This only works for pipelines not in use until #790 gets done. try: - output = gst.parse_bin_from_description( + output = Gst.parse_bin_from_description( description, ghost_unconnected_pads=True) - except gobject.GError as ex: + except GObject.GError as ex: logger.error( 'Failed to create audio output "%s": %s', description, ex) raise exceptions.AudioException(bytes(ex)) @@ -164,7 +161,7 @@ class _Outputs(gst.Bin): logger.info('Audio output set to "%s"', description) def _add(self, element): - queue = gst.element_factory_make('queue') + queue = Gst.element_factory_make('queue') self.add(element) self.add(queue) queue.link(element) @@ -234,28 +231,28 @@ class _Handler(object): self._event_handler_id = None def on_message(self, bus, msg): - if msg.type == gst.MESSAGE_STATE_CHANGED and msg.src == self._element: + if msg.type == Gst.MESSAGE_STATE_CHANGED and msg.src == self._element: self.on_playbin_state_changed(*msg.parse_state_changed()) - elif msg.type == gst.MESSAGE_BUFFERING: + elif msg.type == Gst.MESSAGE_BUFFERING: self.on_buffering(msg.parse_buffering(), msg.structure) - elif msg.type == gst.MESSAGE_EOS: + elif msg.type == Gst.MESSAGE_EOS: self.on_end_of_stream() - elif msg.type == gst.MESSAGE_ERROR: + elif msg.type == Gst.MESSAGE_ERROR: self.on_error(*msg.parse_error()) - elif msg.type == gst.MESSAGE_WARNING: + elif msg.type == Gst.MESSAGE_WARNING: self.on_warning(*msg.parse_warning()) - elif msg.type == gst.MESSAGE_ASYNC_DONE: + elif msg.type == Gst.MESSAGE_ASYNC_DONE: self.on_async_done() - elif msg.type == gst.MESSAGE_TAG: + elif msg.type == Gst.MESSAGE_TAG: self.on_tag(msg.parse_tag()) - elif msg.type == gst.MESSAGE_ELEMENT: - if gst.pbutils.is_missing_plugin_message(msg): + elif msg.type == Gst.MESSAGE_ELEMENT: + if Gst.pbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) def on_event(self, pad, event): - if event.type == gst.EVENT_NEWSEGMENT: + if event.type == Gst.EVENT_NEWSEGMENT: self.on_new_segment(*event.parse_new_segment()) - elif event.type == gst.EVENT_SINK_MESSAGE: + elif event.type == Gst.EVENT_SINK_MESSAGE: # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() @@ -268,17 +265,17 @@ class _Handler(object): old_state.value_name, new_state.value_name, pending_state.value_name) - if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL: + if new_state == Gst.STATE_READY and pending_state == Gst.STATE_NULL: # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. - new_state = gst.STATE_NULL - pending_state = gst.STATE_VOID_PENDING + new_state = Gst.STATE_NULL + pending_state = Gst.STATE_VOID_PENDING - if pending_state != gst.STATE_VOID_PENDING: + if pending_state != Gst.STATE_VOID_PENDING: return # Ignore intermediate state changes - if new_state == gst.STATE_READY: + if new_state == Gst.STATE_READY: return # Ignore READY state as it's GStreamer specific new_state = _GST_STATE_MAPPING[new_state] @@ -297,23 +294,23 @@ class _Handler(object): AudioListener.send('stream_changed', uri=None) if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ: - gst.DEBUG_BIN_TO_DOT_FILE( - self._audio._playbin, gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') + Gst.DEBUG_BIN_TO_DOT_FILE( + self._audio._playbin, Gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') def on_buffering(self, percent, structure=None): if structure and structure.has_field('buffering-mode'): - if structure['buffering-mode'] == gst.BUFFERING_LIVE: + if structure['buffering-mode'] == Gst.BUFFERING_LIVE: return # Live sources stall in paused. level = logging.getLevelName('TRACE') if percent < 10 and not self._audio._buffering: - self._audio._playbin.set_state(gst.STATE_PAUSED) + self._audio._playbin.set_state(Gst.STATE_PAUSED) self._audio._buffering = True level = logging.DEBUG if percent == 100: self._audio._buffering = False - if self._audio._target_state == gst.STATE_PLAYING: - self._audio._playbin.set_state(gst.STATE_PLAYING) + if self._audio._target_state == Gst.STATE_PLAYING: + self._audio._playbin.set_state(Gst.STATE_PLAYING) level = logging.DEBUG gst_logger.log(level, 'Got buffering message: percent=%d%%', percent) @@ -346,12 +343,12 @@ class _Handler(object): AudioListener.send('tags_changed', tags=tags.keys()) def on_missing_plugin(self, msg): - desc = gst.pbutils.missing_plugin_message_get_description(msg) - debug = gst.pbutils.missing_plugin_message_get_installer_detail(msg) + desc = Gst.pbutils.missing_plugin_message_get_description(msg) + debug = Gst.pbutils.missing_plugin_message_get_installer_detail(msg) gst_logger.debug('Got missing-plugin message: description:%s', desc) logger.warning('Could not find a %s to handle media.', desc) - if gst.pbutils.install_plugins_supported(): + if Gst.pbutils.install_plugins_supported(): logger.info('You might be able to fix this by running: ' 'gst-installer "%s"', debug) # TODO: store the missing plugins installer info in a file so we can @@ -362,7 +359,7 @@ class _Handler(object): gst_logger.debug('Got new-segment event: update=%s rate=%s format=%s ' 'start=%s stop=%s position=%s', update, rate, format_.value_name, start, stop, position) - position_ms = position // gst.MSECOND + position_ms = position // Gst.MSECOND logger.debug('Audio event: position_changed(position=%s)', position_ms) AudioListener.send('position_changed', position=position_ms) @@ -389,7 +386,7 @@ class Audio(pykka.ThreadingActor): super(Audio, self).__init__() self._config = config - self._target_state = gst.STATE_NULL + self._target_state = Gst.STATE_NULL self._buffering = False self._tags = {} @@ -411,7 +408,7 @@ class Audio(pykka.ThreadingActor): self._setup_playbin() self._setup_outputs() self._setup_audio_sink() - except gobject.GError as ex: + except GObject.GError as ex: logger.exception(ex) process.exit_process() @@ -422,19 +419,19 @@ class Audio(pykka.ThreadingActor): def _setup_preferences(self): # TODO: move out of audio actor? # Fix for https://github.com/mopidy/mopidy/issues/604 - registry = gst.registry_get_default() + registry = Gst.registry_get_default() jacksink = registry.find_feature( - 'jackaudiosink', gst.TYPE_ELEMENT_FACTORY) + 'jackaudiosink', Gst.TYPE_ELEMENT_FACTORY) if jacksink: - jacksink.set_rank(gst.RANK_SECONDARY) + jacksink.set_rank(Gst.RANK_SECONDARY) def _setup_playbin(self): - playbin = gst.element_factory_make('playbin2') + playbin = Gst.element_factory_make('playbin2') playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO # TODO: turn into config values... playbin.set_property('buffer-size', 5 << 20) # 5MB - playbin.set_property('buffer-duration', 5 * gst.SECOND) + playbin.set_property('buffer-duration', 5 * Gst.SECOND) self._signals.connect(playbin, 'source-setup', self._on_source_setup) self._signals.connect(playbin, 'about-to-finish', @@ -448,13 +445,13 @@ class Audio(pykka.ThreadingActor): self._handler.teardown_event_handling() self._signals.disconnect(self._playbin, 'about-to-finish') self._signals.disconnect(self._playbin, 'source-setup') - self._playbin.set_state(gst.STATE_NULL) + self._playbin.set_state(Gst.STATE_NULL) def _setup_outputs(self): # We don't want to use outputs for regular testing, so just install # an unsynced fakesink when someone asks for a 'testoutput'. if self._config['audio']['output'] == 'testoutput': - self._outputs = gst.element_factory_make('fakesink') + self._outputs = Gst.element_factory_make('fakesink') else: self._outputs = _Outputs() try: @@ -465,23 +462,23 @@ class Audio(pykka.ThreadingActor): self._handler.setup_event_handling(self._outputs.get_pad('sink')) def _setup_audio_sink(self): - audio_sink = gst.Bin('audio-sink') + audio_sink = Gst.Bin('audio-sink') # Queue element to buy us time between the about to finish event and # the actual switch, i.e. about to switch can block for longer thanks # to this queue. # TODO: make the min-max values a setting? - queue = gst.element_factory_make('queue') + queue = Gst.element_factory_make('queue') queue.set_property('max-size-buffers', 0) queue.set_property('max-size-bytes', 0) - queue.set_property('max-size-time', 3 * gst.SECOND) - queue.set_property('min-threshold-time', 1 * gst.SECOND) + queue.set_property('max-size-time', 3 * Gst.SECOND) + queue.set_property('min-threshold-time', 1 * Gst.SECOND) audio_sink.add(queue) audio_sink.add(self._outputs) if self.mixer: - volume = gst.element_factory_make('volume') + volume = Gst.element_factory_make('volume') audio_sink.add(volume) queue.link(volume) volume.link(self._outputs) @@ -489,7 +486,7 @@ class Audio(pykka.ThreadingActor): else: queue.link(self._outputs) - ghost_pad = gst.GhostPad('sink', queue.get_pad('sink')) + ghost_pad = Gst.GhostPad('sink', queue.get_pad('sink')) audio_sink.add_pad(ghost_pad) self._playbin.set_property('audio-sink', audio_sink) @@ -561,7 +558,7 @@ class Audio(pykka.ThreadingActor): :type seek_data: callable which takes time position in ms """ self._appsrc.prepare( - gst.Caps(bytes(caps)), need_data, enough_data, seek_data) + Gst.Caps(bytes(caps)), need_data, enough_data, seek_data) self._playbin.set_property('uri', 'appsrc://') def emit_data(self, buffer_): @@ -577,7 +574,7 @@ class Audio(pykka.ThreadingActor): Returns :class:`True` if data was delivered. :param buffer_: buffer to pass to appsrc - :type buffer_: :class:`gst.Buffer` or :class:`None` + :type buffer_: :class:`Gst.Buffer` or :class:`None` :rtype: boolean """ return self._appsrc.push(buffer_) @@ -616,9 +613,9 @@ class Audio(pykka.ThreadingActor): :rtype: int """ try: - gst_position = self._playbin.query_position(gst.FORMAT_TIME)[0] + gst_position = self._playbin.query_position(Gst.FORMAT_TIME)[0] return utils.clocktime_to_millisecond(gst_position) - except gst.QueryError: + except Gst.QueryError: # TODO: take state into account for this and possibly also return # None as the unknown value instead of zero? logger.debug('Position query failed') @@ -635,7 +632,7 @@ class Audio(pykka.ThreadingActor): # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) result = self._playbin.seek_simple( - gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, gst_position) + Gst.Format(Gst.FORMAT_TIME), Gst.SEEK_FLAG_FLUSH, gst_position) gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result @@ -645,7 +642,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(gst.STATE_PLAYING) + return self._set_state(Gst.STATE_PLAYING) def pause_playback(self): """ @@ -653,7 +650,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(gst.STATE_PAUSED) + return self._set_state(Gst.STATE_PAUSED) def prepare_change(self): """ @@ -664,7 +661,7 @@ class Audio(pykka.ThreadingActor): is that GStreamer will reset all its state when it changes to :attr:`gst.STATE_READY`. """ - return self._set_state(gst.STATE_READY) + return self._set_state(Gst.STATE_READY) def stop_playback(self): """ @@ -673,7 +670,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ self._buffering = False - return self._set_state(gst.STATE_NULL) + return self._set_state(Gst.STATE_NULL) def wait_for_state_change(self): """Block until any pending state changes are complete. @@ -689,7 +686,7 @@ class Audio(pykka.ThreadingActor): """ def sync_handler(bus, message): self._handler.on_message(bus, message) - return gst.BUS_DROP + return Gst.BUS_DROP bus = self._playbin.get_bus() bus.set_sync_handler(sync_handler) @@ -710,9 +707,9 @@ class Audio(pykka.ThreadingActor): "READY" -> "NULL" "READY" -> "PAUSED" - :param state: State to set playbin to. One of: `gst.STATE_NULL`, - `gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`. - :type state: :class:`gst.State` + :param state: State to set playbin to. One of: `Gst.STATE_NULL`, + `Gst.STATE_READY`, `Gst.STATE_PAUSED` and `Gst.STATE_PLAYING`. + :type state: :class:`Gst.State` :rtype: :class:`True` if successfull, else :class:`False` """ self._target_state = state @@ -720,7 +717,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('State change to %s: result=%s', state.value_name, result.value_name) - if result == gst.STATE_CHANGE_FAILURE: + if result == Gst.STATE_CHANGE_FAILURE: logger.warning( 'Setting GStreamer state to %s failed', state.value_name) return False @@ -740,25 +737,25 @@ class Audio(pykka.ThreadingActor): :param track: the current track :type track: :class:`mopidy.models.Track` """ - taglist = gst.TagList() + taglist = Gst.TagList() artists = [a for a in (track.artists or []) if a.name] # Default to blank data to trick shoutcast into clearing any previous # values it might have. - taglist[gst.TAG_ARTIST] = ' ' - taglist[gst.TAG_TITLE] = ' ' - taglist[gst.TAG_ALBUM] = ' ' + taglist[Gst.TAG_ARTIST] = ' ' + taglist[Gst.TAG_TITLE] = ' ' + taglist[Gst.TAG_ALBUM] = ' ' if artists: - taglist[gst.TAG_ARTIST] = ', '.join([a.name for a in artists]) + taglist[Gst.TAG_ARTIST] = ', '.join([a.name for a in artists]) if track.name: - taglist[gst.TAG_TITLE] = track.name + taglist[Gst.TAG_TITLE] = track.name if track.album and track.album.name: - taglist[gst.TAG_ALBUM] = track.album.name + taglist[Gst.TAG_ALBUM] = track.album.name - event = gst.event_new_tag(taglist) + event = Gst.event_new_tag(taglist) # TODO: check if we get this back on our own bus? self._playbin.send_event(event) gst_logger.debug('Sent tag event: track=%s', track.uri) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fd5d2d49..ba6adaf0 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -3,10 +3,9 @@ from __future__ import ( import collections -import pygst -pygst.require('0.10') -import gst # noqa -import gst.pbutils # noqa +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst, GstPbutils from mopidy import exceptions from mopidy.audio import utils @@ -15,7 +14,7 @@ from mopidy.internal import encoding _Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) -_RAW_AUDIO = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') +_RAW_AUDIO = Gst.Caps(b'audio/x-raw-int; audio/x-raw-float') # TODO: replace with a scan(uri, timeout=1000, proxy_config=None)? @@ -59,7 +58,7 @@ class Scanner(object): duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: - pipeline.set_state(gst.STATE_NULL) + pipeline.set_state(Gst.STATE_NULL) del pipeline return _Result(uri, tags, duration, seekable, mime, have_audio) @@ -68,17 +67,17 @@ class Scanner(object): # Turns out it's _much_ faster to just create a new pipeline for every as # decodebins and other elements don't seem to take well to being reused. def _setup_pipeline(uri, proxy_config=None): - src = gst.element_make_from_uri(gst.URI_SRC, uri) + src = Gst.element_make_from_uri(Gst.URI_SRC, uri) if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) - typefind = gst.element_factory_make('typefind') - decodebin = gst.element_factory_make('decodebin2') + typefind = Gst.element_factory_make('typefind') + decodebin = Gst.element_factory_make('decodebin2') - pipeline = gst.element_factory_make('pipeline') + pipeline = Gst.element_factory_make('pipeline') for e in (src, typefind, decodebin): pipeline.add(e) - gst.element_link_many(src, typefind, decodebin) + Gst.element_link_many(src, typefind, decodebin) if proxy_config: utils.setup_proxy(src, proxy_config) @@ -91,13 +90,13 @@ def _setup_pipeline(uri, proxy_config=None): def _have_type(element, probability, caps, decodebin): decodebin.set_property('sink-caps', caps) - struct = gst.Structure('have-type') + struct = Gst.Structure('have-type') struct['caps'] = caps.get_structure(0) - element.get_bus().post(gst.message_new_application(element, struct)) + element.get_bus().post(Gst.message_new_application(element, struct)) def _pad_added(element, pad, pipeline): - sink = gst.element_factory_make('fakesink') + sink = Gst.element_factory_make('fakesink') sink.set_property('sync', False) pipeline.add(sink) @@ -105,29 +104,29 @@ def _pad_added(element, pad, pipeline): pad.link(sink.get_pad('sink')) if pad.get_caps().is_subset(_RAW_AUDIO): - struct = gst.Structure('have-audio') - element.get_bus().post(gst.message_new_application(element, struct)) + struct = Gst.Structure('have-audio') + element.get_bus().post(Gst.message_new_application(element, struct)) def _start_pipeline(pipeline): - if pipeline.set_state(gst.STATE_PAUSED) == gst.STATE_CHANGE_NO_PREROLL: - pipeline.set_state(gst.STATE_PLAYING) + if pipeline.set_state(Gst.STATE_PAUSED) == Gst.STATE_CHANGE_NO_PREROLL: + pipeline.set_state(Gst.STATE_PLAYING) def _query_duration(pipeline): try: - duration = pipeline.query_duration(gst.FORMAT_TIME, None)[0] - except gst.QueryError: + duration = pipeline.query_duration(Gst.FORMAT_TIME, None)[0] + except Gst.QueryError: return None if duration < 0: return None else: - return duration // gst.MSECOND + return duration // Gst.MSECOND def _query_seekable(pipeline): - query = gst.query_new_seeking(gst.FORMAT_TIME) + query = Gst.query_new_seeking(Gst.FORMAT_TIME) pipeline.query(query) return query.parse_seeking()[1] @@ -135,15 +134,15 @@ def _query_seekable(pipeline): def _process(pipeline, timeout_ms): clock = pipeline.get_clock() bus = pipeline.get_bus() - timeout = timeout_ms * gst.MSECOND + timeout = timeout_ms * Gst.MSECOND 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) + Gst.MESSAGE_ELEMENT | Gst.MESSAGE_APPLICATION | Gst.MESSAGE_ERROR | + Gst.MESSAGE_EOS | Gst.MESSAGE_ASYNC_DONE | Gst.MESSAGE_TAG) previous = clock.get_time() while timeout > 0: @@ -151,29 +150,29 @@ def _process(pipeline, timeout_ms): if message is None: break - elif message.type == gst.MESSAGE_ELEMENT: - if gst.pbutils.is_missing_plugin_message(message): + elif message.type == Gst.MESSAGE_ELEMENT: + if GstPbutils.is_missing_plugin_message(message): missing_message = message - elif message.type == gst.MESSAGE_APPLICATION: + elif message.type == Gst.MESSAGE_APPLICATION: if message.structure.get_name() == 'have-type': mime = message.structure['caps'].get_name() if mime.startswith('text/') or mime == 'application/xml': return tags, mime, have_audio elif message.structure.get_name() == 'have-audio': have_audio = True - elif message.type == gst.MESSAGE_ERROR: + elif message.type == Gst.MESSAGE_ERROR: error = encoding.locale_decode(message.parse_error()[0]) 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: + elif message.type == Gst.MESSAGE_EOS: return tags, mime, have_audio - elif message.type == gst.MESSAGE_ASYNC_DONE: + elif message.type == Gst.MESSAGE_ASYNC_DONE: if message.src == pipeline: return tags, mime, have_audio - elif message.type == gst.MESSAGE_TAG: + elif message.type == Gst.MESSAGE_TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) @@ -189,15 +188,13 @@ if __name__ == '__main__': import os import sys - import gobject - from mopidy.internal import path - gobject.threads_init() + GObject.threads_init() scanner = Scanner(5000) for uri in sys.argv[1:]: - if not gst.uri_is_valid(uri): + if not Gst.uri_is_valid(uri): uri = path.path_to_uri(os.path.abspath(uri)) try: result = scanner.scan(uri) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index bc527df7..aa0b1d63 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -4,9 +4,9 @@ import datetime import logging import numbers -import pygst -pygst.require('0.10') -import gst # noqa +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst from mopidy import compat, httpclient from mopidy.models import Album, Artist, Track @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) def calculate_duration(num_samples, sample_rate): """Determine duration of samples using GStreamer helper for precise math.""" - return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate) + return Gst.util_uint64_scale(num_samples, Gst.SECOND, sample_rate) def create_buffer(data, capabilites=None, timestamp=None, duration=None): @@ -25,10 +25,10 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): Mainly intended to keep gst imports out of non-audio modules. """ - buffer_ = gst.Buffer(data) + buffer_ = Gst.Buffer(data) if capabilites: if isinstance(capabilites, compat.string_types): - capabilites = gst.caps_from_string(capabilites) + capabilites = Gst.caps_from_string(capabilites) buffer_.set_caps(capabilites) if timestamp: buffer_.timestamp = timestamp @@ -39,12 +39,12 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): def millisecond_to_clocktime(value): """Convert a millisecond time to internal GStreamer time.""" - return value * gst.MSECOND + return value * Gst.MSECOND def clocktime_to_millisecond(value): """Convert an internal GStreamer time to millisecond time.""" - return value // gst.MSECOND + return value // Gst.MSECOND def supported_uri_schemes(uri_schemes): @@ -55,9 +55,9 @@ def supported_uri_schemes(uri_schemes): :rtype: set of URI schemes we can support via this GStreamer install. """ supported_schemes = set() - registry = gst.registry_get_default() + registry = Gst.registry_get_default() - for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY): + for factory in registry.get_feature_list(Gst.TYPE_ELEMENT_FACTORY): for uri in factory.get_uri_protocols(): if uri in uri_schemes: supported_schemes.add(uri) @@ -95,37 +95,37 @@ def convert_tags_to_track(tags): album_kwargs = {} track_kwargs = {} - track_kwargs['composers'] = _artists(tags, gst.TAG_COMPOSER) - track_kwargs['performers'] = _artists(tags, gst.TAG_PERFORMER) - track_kwargs['artists'] = _artists(tags, gst.TAG_ARTIST, + track_kwargs['composers'] = _artists(tags, Gst.TAG_COMPOSER) + track_kwargs['performers'] = _artists(tags, Gst.TAG_PERFORMER) + track_kwargs['artists'] = _artists(tags, Gst.TAG_ARTIST, 'musicbrainz-artistid', 'musicbrainz-sortname') album_kwargs['artists'] = _artists( - tags, gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') + tags, Gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') - track_kwargs['genre'] = '; '.join(tags.get(gst.TAG_GENRE, [])) - track_kwargs['name'] = '; '.join(tags.get(gst.TAG_TITLE, [])) + track_kwargs['genre'] = '; '.join(tags.get(Gst.TAG_GENRE, [])) + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_TITLE, [])) if not track_kwargs['name']: - track_kwargs['name'] = '; '.join(tags.get(gst.TAG_ORGANIZATION, [])) + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_ORGANIZATION, [])) track_kwargs['comment'] = '; '.join(tags.get('comment', [])) if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_LOCATION, [])) + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_LOCATION, [])) if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_COPYRIGHT, [])) + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_COPYRIGHT, [])) - track_kwargs['track_no'] = tags.get(gst.TAG_TRACK_NUMBER, [None])[0] - track_kwargs['disc_no'] = tags.get(gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] - track_kwargs['bitrate'] = tags.get(gst.TAG_BITRATE, [None])[0] + track_kwargs['track_no'] = tags.get(Gst.TAG_TRACK_NUMBER, [None])[0] + track_kwargs['disc_no'] = tags.get(Gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] + track_kwargs['bitrate'] = tags.get(Gst.TAG_BITRATE, [None])[0] track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0] - album_kwargs['name'] = tags.get(gst.TAG_ALBUM, [None])[0] - album_kwargs['num_tracks'] = tags.get(gst.TAG_TRACK_COUNT, [None])[0] - album_kwargs['num_discs'] = tags.get(gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] + album_kwargs['name'] = tags.get(Gst.TAG_ALBUM, [None])[0] + album_kwargs['num_tracks'] = tags.get(Gst.TAG_TRACK_COUNT, [None])[0] + album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - if tags.get(gst.TAG_DATE) and tags.get(gst.TAG_DATE)[0]: - track_kwargs['date'] = tags[gst.TAG_DATE][0].isoformat() + if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: + track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} @@ -142,7 +142,7 @@ def setup_proxy(element, config): """Configure a GStreamer element with proxy settings. :param element: element to setup proxy in. - :type element: :class:`gst.GstElement` + :type element: :class:`Gst.GstElement` :param config: proxy settings to use. :type config: :class:`dict` """ @@ -155,7 +155,7 @@ def setup_proxy(element, config): def convert_taglist(taglist): - """Convert a :class:`gst.Taglist` to plain Python types. + """Convert a :class:`Gst.Taglist` to plain Python types. Knows how to convert: @@ -172,7 +172,7 @@ def convert_taglist(taglist): 0.10.36/gstreamer/html/gstreamer-GstTagList.html :param taglist: A GStreamer taglist to be converted. - :type taglist: :class:`gst.Taglist` + :type taglist: :class:`Gst.Taglist` :rtype: dictionary of tag keys with a list of values. """ result = {} @@ -187,13 +187,13 @@ def convert_taglist(taglist): values = [values] for value in values: - if isinstance(value, gst.Date): + if isinstance(value, Gst.Date): try: date = datetime.date(value.year, value.month, value.day) result[key].append(date) except ValueError: logger.debug('Ignoring invalid date: %r = %r', key, value) - elif isinstance(value, gst.Buffer): + elif isinstance(value, Gst.Buffer): result[key].append(bytes(value)) elif isinstance( value, (compat.string_types, bool, numbers.Number)): diff --git a/mopidy/commands.py b/mopidy/commands.py index 4890c722..872d5773 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -7,9 +7,7 @@ import logging import os import sys -import glib - -import gobject +from gi.repository import GLib, GObject import pykka @@ -21,7 +19,7 @@ from mopidy.internal import deps, process, timer, versioning logger = logging.getLogger(__name__) _default_config = [] -for base in glib.get_system_config_dirs() + (glib.get_user_config_dir(),): +for base in GLib.get_system_config_dirs() + (GLib.get_user_config_dir(),): _default_config.append(os.path.join(base, b'mopidy', b'mopidy.conf')) DEFAULT_CONFIG = b':'.join(_default_config) @@ -286,7 +284,7 @@ class RootCommand(Command): help='`section/key=value` values to override config options') def run(self, args, config): - loop = gobject.MainLoop() + loop = GObject.MainLoop() mixer_class = self.get_mixer_class(config, args.registry['mixer']) backend_classes = args.registry['backend'] diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 1f363657..3744db87 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -5,11 +5,11 @@ import os import platform import sys -import pkg_resources +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst -import pygst -pygst.require('0.10') -import gst # noqa +import pkg_resources from mopidy.internal import formatting @@ -110,8 +110,7 @@ def pkg_info(project_name=None, include_extras=False): def gstreamer_info(): other = [] - other.append('Python wrapper: gst-python %s' % ( - '.'.join(map(str, gst.get_pygst_version())))) + other.append('Python wrapper: python-gi %s' % gi.__version__) found_elements = [] missing_elements = [] @@ -135,8 +134,8 @@ def gstreamer_info(): return { 'name': 'GStreamer', - 'version': '.'.join(map(str, gst.get_gst_version())), - 'path': os.path.dirname(gst.__file__), + 'version': '.'.join(map(str, Gst.version())), + 'path': os.path.dirname(gi.__file__), 'other': '\n'.join(other), } @@ -187,6 +186,6 @@ def _gstreamer_check_elements(): ] known_elements = [ factory.get_name() for factory in - gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)] + Gst.registry_get_default().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] return [ (element, element in known_elements) for element in elements_to_check] diff --git a/mopidy/internal/network.py b/mopidy/internal/network.py index 4b8b35fe..c956d795 100644 --- a/mopidy/internal/network.py +++ b/mopidy/internal/network.py @@ -7,7 +7,7 @@ import socket import sys import threading -import gobject +from gi.repository import GObject import pykka @@ -67,7 +67,7 @@ def format_hostname(hostname): class Server(object): - """Setup listener and register it with gobject's event loop.""" + """Setup listener and register it with GObject's event loop.""" def __init__(self, host, port, protocol, protocol_kwargs=None, max_connections=5, timeout=30): @@ -87,7 +87,7 @@ class Server(object): return sock def register_server_socket(self, fileno): - gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection) + GObject.io_add_watch(fileno, GObject.IO_IN, self.handle_connection) def handle_connection(self, fd, flags): try: @@ -132,7 +132,7 @@ class Server(object): class Connection(object): # NOTE: the callback code is _not_ run in the actor's thread, but in the # same one as the event loop. If code in the callbacks blocks, the rest of - # gobject code will likely be blocked as well... + # GObject code will likely be blocked as well... # # Also note that source_remove() return values are ignored on purpose, a # false return value would only tell us that what we thought was registered @@ -211,14 +211,14 @@ class Connection(object): return self.disable_timeout() - self.timeout_id = gobject.timeout_add_seconds( + self.timeout_id = GObject.timeout_add_seconds( self.timeout, self.timeout_callback) def disable_timeout(self): """Deactivate timeout mechanism.""" if self.timeout_id is None: return - gobject.source_remove(self.timeout_id) + GObject.source_remove(self.timeout_id) self.timeout_id = None def enable_recv(self): @@ -226,9 +226,9 @@ class Connection(object): return try: - self.recv_id = gobject.io_add_watch( + self.recv_id = GObject.io_add_watch( self.sock.fileno(), - gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP, self.recv_callback) except socket.error as e: self.stop('Problem with connection: %s' % e) @@ -236,7 +236,7 @@ class Connection(object): def disable_recv(self): if self.recv_id is None: return - gobject.source_remove(self.recv_id) + GObject.source_remove(self.recv_id) self.recv_id = None def enable_send(self): @@ -244,9 +244,9 @@ class Connection(object): return try: - self.send_id = gobject.io_add_watch( + self.send_id = GObject.io_add_watch( self.sock.fileno(), - gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP, self.send_callback) except socket.error as e: self.stop('Problem with connection: %s' % e) @@ -255,11 +255,11 @@ class Connection(object): if self.send_id is None: return - gobject.source_remove(self.send_id) + GObject.source_remove(self.send_id) self.send_id = None def recv_callback(self, fd, flags): - if flags & (gobject.IO_ERR | gobject.IO_HUP): + if flags & (GObject.IO_ERR | GObject.IO_HUP): self.stop('Bad client flags: %s' % flags) return True @@ -283,7 +283,7 @@ class Connection(object): return True def send_callback(self, fd, flags): - if flags & (gobject.IO_ERR | gobject.IO_HUP): + if flags & (GObject.IO_ERR | GObject.IO_HUP): self.stop('Bad client flags: %s' % flags) return True diff --git a/mopidy/internal/playlists.py b/mopidy/internal/playlists.py index f8e654af..e80588c9 100644 --- a/mopidy/internal/playlists.py +++ b/mopidy/internal/playlists.py @@ -2,10 +2,6 @@ from __future__ import absolute_import, unicode_literals import io -import pygst -pygst.require('0.10') -import gst # noqa - from mopidy.compat import configparser from mopidy.internal import validation diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 0cfbdaf3..e1841561 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -3,15 +3,13 @@ from __future__ import absolute_import, unicode_literals import threading import unittest -import gobject -gobject.threads_init() +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst +GObject.threads_init() import mock -import pygst -pygst.require('0.10') -import gst # noqa - import pykka from mopidy import audio @@ -520,17 +518,17 @@ class AudioStateTest(unittest.TestCase): def test_state_does_not_change_when_in_gst_ready_state(self): self.audio._handler.on_playbin_state_changed( - gst.STATE_NULL, gst.STATE_READY, gst.STATE_VOID_PENDING) + Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) def test_state_changes_from_stopped_to_playing_on_play(self): self.audio._handler.on_playbin_state_changed( - gst.STATE_NULL, gst.STATE_READY, gst.STATE_PLAYING) + Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_PLAYING) self.audio._handler.on_playbin_state_changed( - gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_PLAYING) + Gst.STATE_READY, Gst.STATE_PAUSED, Gst.STATE_PLAYING) self.audio._handler.on_playbin_state_changed( - gst.STATE_PAUSED, gst.STATE_PLAYING, gst.STATE_VOID_PENDING) + Gst.STATE_PAUSED, Gst.STATE_PLAYING, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state) @@ -538,7 +536,7 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_VOID_PENDING) + Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state) @@ -546,12 +544,12 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_NULL) + Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_NULL) self.audio._handler.on_playbin_state_changed( - gst.STATE_PAUSED, gst.STATE_READY, gst.STATE_NULL) + Gst.STATE_PAUSED, Gst.STATE_READY, Gst.STATE_NULL) # We never get the following call, so the logic must work without it # self.audio._handler.on_playbin_state_changed( - # gst.STATE_READY, gst.STATE_NULL, gst.STATE_VOID_PENDING) + # Gst.STATE_READY, Gst.STATE_NULL, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) @@ -565,17 +563,17 @@ class AudioBufferingTest(unittest.TestCase): def test_pause_when_buffer_empty(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.STATE_PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) self.assertTrue(self.audio._buffering) def test_stay_paused_when_buffering_finished(self): playbin = self.audio._playbin self.audio.pause_playback() - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) playbin.set_state.reset_mock() self.audio._handler.on_buffering(100) @@ -585,11 +583,11 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_paused_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.STATE_PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) self.audio.pause_playback() playbin.set_state.reset_mock() @@ -600,13 +598,13 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_stopped_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.STATE_PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) playbin.set_state.reset_mock() self.audio.stop_playback() - playbin.set_state.assert_called_with(gst.STATE_NULL) + playbin.set_state.assert_called_with(Gst.STATE_NULL) self.assertFalse(self.audio._buffering) diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index 8c2b9af3..08def2af 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -3,8 +3,12 @@ from __future__ import absolute_import, unicode_literals import os import unittest -import gobject -gobject.threads_init() +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +GObject.threads_init() +Gst.init(None) from mopidy import exceptions from mopidy.audio import scan diff --git a/tests/internal/network/test_connection.py b/tests/internal/network/test_connection.py index 8ae7d15c..291bbc46 100644 --- a/tests/internal/network/test_connection.py +++ b/tests/internal/network/test_connection.py @@ -5,7 +5,7 @@ import logging import socket import unittest -import gobject +from gi.repository import GObject from mock import Mock, call, patch, sentinel @@ -162,27 +162,27 @@ class ConnectionTest(unittest.TestCase): network.Connection.stop(self.mock, sentinel.reason) network.logger.log(any_int, any_unicode) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_recv_registers_with_gobject(self): self.mock.recv_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno - gobject.io_add_watch.return_value = sentinel.tag + GObject.io_add_watch.return_value = sentinel.tag network.Connection.enable_recv(self.mock) - gobject.io_add_watch.assert_called_once_with( + GObject.io_add_watch.assert_called_once_with( sentinel.fileno, - gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP, self.mock.recv_callback) self.assertEqual(sentinel.tag, self.mock.recv_id) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_recv_already_registered(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.recv_id = sentinel.tag network.Connection.enable_recv(self.mock) - self.assertEqual(0, gobject.io_add_watch.call_count) + self.assertEqual(0, GObject.io_add_watch.call_count) def test_enable_recv_does_not_change_tag(self): self.mock.recv_id = sentinel.tag @@ -191,20 +191,20 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_recv(self.mock) self.assertEqual(sentinel.tag, self.mock.recv_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_recv_deregisters(self): self.mock.recv_id = sentinel.tag network.Connection.disable_recv(self.mock) - gobject.source_remove.assert_called_once_with(sentinel.tag) + GObject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.recv_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_recv_already_deregistered(self): self.mock.recv_id = None network.Connection.disable_recv(self.mock) - self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(0, GObject.source_remove.call_count) self.assertEqual(None, self.mock.recv_id) def test_enable_recv_on_closed_socket(self): @@ -216,27 +216,27 @@ class ConnectionTest(unittest.TestCase): self.mock.stop.assert_called_once_with(any_unicode) self.assertEqual(None, self.mock.recv_id) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_send_registers_with_gobject(self): self.mock.send_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno - gobject.io_add_watch.return_value = sentinel.tag + GObject.io_add_watch.return_value = sentinel.tag network.Connection.enable_send(self.mock) - gobject.io_add_watch.assert_called_once_with( + GObject.io_add_watch.assert_called_once_with( sentinel.fileno, - gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP, self.mock.send_callback) self.assertEqual(sentinel.tag, self.mock.send_id) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_send_already_registered(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.send_id = sentinel.tag network.Connection.enable_send(self.mock) - self.assertEqual(0, gobject.io_add_watch.call_count) + self.assertEqual(0, GObject.io_add_watch.call_count) def test_enable_send_does_not_change_tag(self): self.mock.send_id = sentinel.tag @@ -245,20 +245,20 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_send(self.mock) self.assertEqual(sentinel.tag, self.mock.send_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_send_deregisters(self): self.mock.send_id = sentinel.tag network.Connection.disable_send(self.mock) - gobject.source_remove.assert_called_once_with(sentinel.tag) + GObject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.send_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_send_already_deregistered(self): self.mock.send_id = None network.Connection.disable_send(self.mock) - self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(0, GObject.source_remove.call_count) self.assertEqual(None, self.mock.send_id) def test_enable_send_on_closed_socket(self): @@ -269,36 +269,36 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_send(self.mock) self.assertEqual(None, self.mock.send_id) - @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + @patch.object(GObject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_clears_existing_timeouts(self): self.mock.timeout = 10 network.Connection.enable_timeout(self.mock) self.mock.disable_timeout.assert_called_once_with() - @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + @patch.object(GObject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_add_gobject_timeout(self): self.mock.timeout = 10 - gobject.timeout_add_seconds.return_value = sentinel.tag + GObject.timeout_add_seconds.return_value = sentinel.tag network.Connection.enable_timeout(self.mock) - gobject.timeout_add_seconds.assert_called_once_with( + GObject.timeout_add_seconds.assert_called_once_with( 10, self.mock.timeout_callback) self.assertEqual(sentinel.tag, self.mock.timeout_id) - @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + @patch.object(GObject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_does_not_add_timeout(self): self.mock.timeout = 0 network.Connection.enable_timeout(self.mock) - self.assertEqual(0, gobject.timeout_add_seconds.call_count) + self.assertEqual(0, GObject.timeout_add_seconds.call_count) self.mock.timeout = -1 network.Connection.enable_timeout(self.mock) - self.assertEqual(0, gobject.timeout_add_seconds.call_count) + self.assertEqual(0, GObject.timeout_add_seconds.call_count) self.mock.timeout = None network.Connection.enable_timeout(self.mock) - self.assertEqual(0, gobject.timeout_add_seconds.call_count) + self.assertEqual(0, GObject.timeout_add_seconds.call_count) def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self): self.mock.timeout = 0 @@ -313,20 +313,20 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_timeout(self.mock) self.assertEqual(0, self.mock.disable_timeout.call_count) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_timeout_deregisters(self): self.mock.timeout_id = sentinel.tag network.Connection.disable_timeout(self.mock) - gobject.source_remove.assert_called_once_with(sentinel.tag) + GObject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.timeout_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_timeout_already_deregistered(self): self.mock.timeout_id = None network.Connection.disable_timeout(self.mock) - self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(0, GObject.source_remove.call_count) self.assertEqual(None, self.mock.timeout_id) def test_queue_send_acquires_and_releases_lock(self): @@ -372,7 +372,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup(self): @@ -380,7 +380,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup_and_io_err(self): @@ -389,7 +389,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, - gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) + GObject.IO_IN | GObject.IO_HUP | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_sends_data_to_actor(self): @@ -398,7 +398,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.actor_ref.tell.assert_called_once_with( {'received': 'data'}) @@ -409,7 +409,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_gets_no_data(self): @@ -418,7 +418,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.assertEqual(self.mock.mock_calls, [ call.sock.recv(any_int), call.disable_recv(), @@ -431,7 +431,7 @@ class ConnectionTest(unittest.TestCase): for error in (errno.EWOULDBLOCK, errno.EINTR): self.mock.sock.recv.side_effect = socket.error(error, '') self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.assertEqual(0, self.mock.stop.call_count) def test_recv_callback_unrecoverable_error(self): @@ -439,7 +439,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.recv.side_effect = socket.error self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_err(self): @@ -450,7 +450,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_hup(self): @@ -461,7 +461,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_hup_and_io_err(self): @@ -473,7 +473,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, - gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) + GObject.IO_IN | GObject.IO_HUP | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_acquires_and_releases_lock(self): @@ -484,7 +484,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.send.return_value = 0 self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.send_lock.acquire.assert_called_once_with(False) self.mock.send_lock.release.assert_called_once_with() @@ -496,7 +496,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.send.return_value = 0 self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.send_lock.acquire.assert_called_once_with(False) self.assertEqual(0, self.mock.sock.send.call_count) @@ -507,7 +507,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send.return_value = '' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.disable_send.assert_called_once_with() self.mock.send.assert_called_once_with('data') self.assertEqual('', self.mock.send_buffer) @@ -519,7 +519,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send.return_value = 'ta' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.send.assert_called_once_with('data') self.assertEqual('ta', self.mock.send_buffer) diff --git a/tests/internal/network/test_server.py b/tests/internal/network/test_server.py index af8effd2..1df25dbc 100644 --- a/tests/internal/network/test_server.py +++ b/tests/internal/network/test_server.py @@ -4,7 +4,7 @@ import errno import socket import unittest -import gobject +from gi.repository import GObject from mock import Mock, patch, sentinel @@ -91,11 +91,11 @@ class ServerTest(unittest.TestCase): network.Server.create_server_socket( self.mock, sentinel.host, sentinel.port) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_register_server_socket_sets_up_io_watch(self): network.Server.register_server_socket(self.mock, sentinel.fileno) - gobject.io_add_watch.assert_called_once_with( - sentinel.fileno, gobject.IO_IN, self.mock.handle_connection) + GObject.io_add_watch.assert_called_once_with( + sentinel.fileno, GObject.IO_IN, self.mock.handle_connection) def test_handle_connection(self): self.mock.accept_connection.return_value = ( @@ -103,7 +103,7 @@ class ServerTest(unittest.TestCase): self.mock.maximum_connections_exceeded.return_value = False self.assertTrue(network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN)) + self.mock, sentinel.fileno, GObject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.init_connection.assert_called_once_with( @@ -116,7 +116,7 @@ class ServerTest(unittest.TestCase): self.mock.maximum_connections_exceeded.return_value = True self.assertTrue(network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN)) + self.mock, sentinel.fileno, GObject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.reject_connection.assert_called_once_with( diff --git a/tests/internal/test_deps.py b/tests/internal/test_deps.py index 27e6f629..ea102b47 100644 --- a/tests/internal/test_deps.py +++ b/tests/internal/test_deps.py @@ -4,14 +4,14 @@ import platform import sys import unittest +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst + import mock import pkg_resources -import pygst -pygst.require('0.10') -import gst # noqa - from mopidy.internal import deps @@ -74,12 +74,11 @@ class DepsTest(unittest.TestCase): self.assertEqual('GStreamer', result['name']) self.assertEqual( - '.'.join(map(str, gst.get_gst_version())), result['version']) - self.assertIn('gst', result['path']) + '.'.join(map(str, Gst.version())), result['version']) + self.assertIn('gi', result['path']) self.assertNotIn('__init__.py', result['path']) - self.assertIn('Python wrapper: gst-python', result['other']) - self.assertIn( - '.'.join(map(str, gst.get_pygst_version())), result['other']) + self.assertIn('Python wrapper: python-gi', result['other']) + self.assertIn(gi.__version__, result['other']) self.assertIn('Relevant elements:', result['other']) @mock.patch('pkg_resources.get_distribution') diff --git a/tests/internal/test_path.py b/tests/internal/test_path.py index 8aa8f7c1..751e7c6e 100644 --- a/tests/internal/test_path.py +++ b/tests/internal/test_path.py @@ -7,7 +7,7 @@ import shutil import tempfile import unittest -import glib +from gi.repository import GLib from mopidy import compat, exceptions from mopidy.internal import path @@ -215,7 +215,7 @@ class ExpandPathTest(unittest.TestCase): def test_xdg_subsititution(self): self.assertEqual( - glib.get_user_data_dir() + b'/foo', + GLib.get_user_data_dir() + b'/foo', path.expand_path(b'$XDG_DATA_DIR/foo')) def test_xdg_subsititution_unknown(self): From 1d269af210858859a346fe20c47ba5fa2e07ca3d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:27:59 +0200 Subject: [PATCH 049/142] gst1: Call Gst.init() and remove sys.argv hack GStreamer no longer use sys.argv directly. If you want GStreamer to handle command line arguments, you must pass them explicitly to Gst.init(). --- mopidy/__main__.py | 10 ++-------- mopidy/audio/scan.py | 1 + tests/audio/test_actor.py | 2 ++ tests/audio/test_scan.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index c1cf42f9..c91740a3 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -22,6 +22,7 @@ except ImportError: raise GObject.threads_init() +Gst.init() try: # Make GObject's mainloop the event loop for python-dbus @@ -33,13 +34,6 @@ except ImportError: import pykka.debug - -# Extract any command line arguments. This needs to be done before GStreamer is -# imported, so that GStreamer doesn't hijack e.g. ``--help``. -mopidy_args = sys.argv[1:] -sys.argv[1:] = [] - - from mopidy import commands, config as config_lib, ext from mopidy.internal import encoding, log, path, process, versioning @@ -73,7 +67,7 @@ def main(): data.command.set(extension=data.extension) root_cmd.add_child(data.extension.ext_name, data.command) - args = root_cmd.parse(mopidy_args) + args = root_cmd.parse(sys.argv[1:]) config, config_errors = config_lib.load( args.config_files, diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ba6adaf0..573d2fab 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -191,6 +191,7 @@ if __name__ == '__main__': from mopidy.internal import path GObject.threads_init() + Gst.init() scanner = Scanner(5000) for uri in sys.argv[1:]: diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index e1841561..48d3704b 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -6,7 +6,9 @@ import unittest import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst + GObject.threads_init() +Gst.init() import mock diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index 08def2af..ab995285 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -8,7 +8,7 @@ gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst GObject.threads_init() -Gst.init(None) +Gst.init() from mopidy import exceptions from mopidy.audio import scan From f00f24ffded4408c577d5fe1a20fd88a959e816d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:32:11 +0200 Subject: [PATCH 050/142] gst1: Replace element_factory_make() with ElementFactory.make() --- mopidy/audio/actor.py | 14 +++++++------- mopidy/audio/scan.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 3595092e..7dd5971e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -135,7 +135,7 @@ class _Outputs(Gst.Bin): def __init__(self): Gst.Bin.__init__(self, 'outputs') - self._tee = Gst.element_factory_make('tee') + self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) ghost_pad = Gst.GhostPad('sink', self._tee.get_pad('sink')) @@ -143,7 +143,7 @@ class _Outputs(Gst.Bin): # Add an always connected fakesink which respects the clock so the tee # doesn't fail even if we don't have any outputs. - fakesink = Gst.element_factory_make('fakesink') + fakesink = Gst.ElementFactory.make('fakesink') fakesink.set_property('sync', True) self._add(fakesink) @@ -161,7 +161,7 @@ class _Outputs(Gst.Bin): logger.info('Audio output set to "%s"', description) def _add(self, element): - queue = Gst.element_factory_make('queue') + queue = Gst.ElementFactory.make('queue') self.add(element) self.add(queue) queue.link(element) @@ -426,7 +426,7 @@ class Audio(pykka.ThreadingActor): jacksink.set_rank(Gst.RANK_SECONDARY) def _setup_playbin(self): - playbin = Gst.element_factory_make('playbin2') + playbin = Gst.ElementFactory.make('playbin2') playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO # TODO: turn into config values... @@ -451,7 +451,7 @@ class Audio(pykka.ThreadingActor): # We don't want to use outputs for regular testing, so just install # an unsynced fakesink when someone asks for a 'testoutput'. if self._config['audio']['output'] == 'testoutput': - self._outputs = Gst.element_factory_make('fakesink') + self._outputs = Gst.ElementFactory.make('fakesink') else: self._outputs = _Outputs() try: @@ -468,7 +468,7 @@ class Audio(pykka.ThreadingActor): # the actual switch, i.e. about to switch can block for longer thanks # to this queue. # TODO: make the min-max values a setting? - queue = Gst.element_factory_make('queue') + queue = Gst.ElementFactory.make('queue') queue.set_property('max-size-buffers', 0) queue.set_property('max-size-bytes', 0) queue.set_property('max-size-time', 3 * Gst.SECOND) @@ -478,7 +478,7 @@ class Audio(pykka.ThreadingActor): audio_sink.add(self._outputs) if self.mixer: - volume = Gst.element_factory_make('volume') + volume = Gst.ElementFactory.make('volume') audio_sink.add(volume) queue.link(volume) volume.link(self._outputs) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 573d2fab..3263f035 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -71,10 +71,10 @@ def _setup_pipeline(uri, proxy_config=None): if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) - typefind = Gst.element_factory_make('typefind') - decodebin = Gst.element_factory_make('decodebin2') + typefind = Gst.ElementFactory.make('typefind') + decodebin = Gst.ElementFactory.make('decodebin2') - pipeline = Gst.element_factory_make('pipeline') + pipeline = Gst.ElementFactory.make('pipeline') for e in (src, typefind, decodebin): pipeline.add(e) Gst.element_link_many(src, typefind, decodebin) @@ -96,7 +96,7 @@ def _have_type(element, probability, caps, decodebin): def _pad_added(element, pad, pipeline): - sink = Gst.element_factory_make('fakesink') + sink = Gst.ElementFactory.make('fakesink') sink.set_property('sync', False) pipeline.add(sink) From ab24222eb6e725ff32c169fe641659f74c41606e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:33:02 +0200 Subject: [PATCH 051/142] gst1: Replace gst.element_link_many() --- mopidy/audio/scan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 3263f035..8967a180 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -77,7 +77,8 @@ def _setup_pipeline(uri, proxy_config=None): pipeline = Gst.ElementFactory.make('pipeline') for e in (src, typefind, decodebin): pipeline.add(e) - Gst.element_link_many(src, typefind, decodebin) + src.link(typefind) + typefind.link(decodebin) if proxy_config: utils.setup_proxy(src, proxy_config) From dfaed1e4c23cb397b7b21564b55de14ce44c8dca Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:36:59 +0200 Subject: [PATCH 052/142] gst1: Replace STATE_* with State.* --- mopidy/audio/actor.py | 45 ++++++++++++++++++++------------------- mopidy/audio/scan.py | 6 +++--- tests/audio/test_actor.py | 32 ++++++++++++++-------------- 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 7dd5971e..bcd424bf 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -25,9 +25,10 @@ logger = logging.getLogger(__name__) gst_logger = logging.getLogger('mopidy.audio.gst') _GST_STATE_MAPPING = { - Gst.STATE_PLAYING: PlaybackState.PLAYING, - Gst.STATE_PAUSED: PlaybackState.PAUSED, - Gst.STATE_NULL: PlaybackState.STOPPED} + Gst.State.PLAYING: PlaybackState.PLAYING, + Gst.State.PAUSED: PlaybackState.PAUSED, + Gst.State.NULL: PlaybackState.STOPPED, +} class _Signals(object): @@ -265,17 +266,17 @@ class _Handler(object): old_state.value_name, new_state.value_name, pending_state.value_name) - if new_state == Gst.STATE_READY and pending_state == Gst.STATE_NULL: + if new_state == Gst.State.READY and pending_state == Gst.State.NULL: # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. - new_state = Gst.STATE_NULL - pending_state = Gst.STATE_VOID_PENDING + new_state = Gst.State.NULL + pending_state = Gst.State.VOID_PENDING - if pending_state != Gst.STATE_VOID_PENDING: + if pending_state != Gst.State.VOID_PENDING: return # Ignore intermediate state changes - if new_state == Gst.STATE_READY: + if new_state == Gst.State.READY: return # Ignore READY state as it's GStreamer specific new_state = _GST_STATE_MAPPING[new_state] @@ -304,13 +305,13 @@ class _Handler(object): level = logging.getLevelName('TRACE') if percent < 10 and not self._audio._buffering: - self._audio._playbin.set_state(Gst.STATE_PAUSED) + self._audio._playbin.set_state(Gst.State.PAUSED) self._audio._buffering = True level = logging.DEBUG if percent == 100: self._audio._buffering = False - if self._audio._target_state == Gst.STATE_PLAYING: - self._audio._playbin.set_state(Gst.STATE_PLAYING) + if self._audio._target_state == Gst.State.PLAYING: + self._audio._playbin.set_state(Gst.State.PLAYING) level = logging.DEBUG gst_logger.log(level, 'Got buffering message: percent=%d%%', percent) @@ -386,7 +387,7 @@ class Audio(pykka.ThreadingActor): super(Audio, self).__init__() self._config = config - self._target_state = Gst.STATE_NULL + self._target_state = Gst.State.NULL self._buffering = False self._tags = {} @@ -445,7 +446,7 @@ class Audio(pykka.ThreadingActor): self._handler.teardown_event_handling() self._signals.disconnect(self._playbin, 'about-to-finish') self._signals.disconnect(self._playbin, 'source-setup') - self._playbin.set_state(Gst.STATE_NULL) + self._playbin.set_state(Gst.State.NULL) def _setup_outputs(self): # We don't want to use outputs for regular testing, so just install @@ -642,7 +643,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(Gst.STATE_PLAYING) + return self._set_state(Gst.State.PLAYING) def pause_playback(self): """ @@ -650,7 +651,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(Gst.STATE_PAUSED) + return self._set_state(Gst.State.PAUSED) def prepare_change(self): """ @@ -659,9 +660,9 @@ class Audio(pykka.ThreadingActor): This function *MUST* be called before changing URIs or doing changes like updating data that is being pushed. The reason for this is that GStreamer will reset all its state when it changes to - :attr:`gst.STATE_READY`. + :attr:`Gst.State.READY`. """ - return self._set_state(Gst.STATE_READY) + return self._set_state(Gst.State.READY) def stop_playback(self): """ @@ -670,7 +671,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ self._buffering = False - return self._set_state(Gst.STATE_NULL) + return self._set_state(Gst.State.NULL) def wait_for_state_change(self): """Block until any pending state changes are complete. @@ -695,7 +696,7 @@ class Audio(pykka.ThreadingActor): """ Internal method for setting the raw GStreamer state. - .. digraph:: gst_state_transitions + .. digraph:: Gst.State.transitions graph [rankdir="LR"]; node [fontsize=10]; @@ -707,8 +708,8 @@ class Audio(pykka.ThreadingActor): "READY" -> "NULL" "READY" -> "PAUSED" - :param state: State to set playbin to. One of: `Gst.STATE_NULL`, - `Gst.STATE_READY`, `Gst.STATE_PAUSED` and `Gst.STATE_PLAYING`. + :param state: State to set playbin to. One of: `Gst.State.NULL`, + `Gst.State.READY`, `Gst.State.PAUSED` and `Gst.State.PLAYING`. :type state: :class:`Gst.State` :rtype: :class:`True` if successfull, else :class:`False` """ @@ -717,7 +718,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('State change to %s: result=%s', state.value_name, result.value_name) - if result == Gst.STATE_CHANGE_FAILURE: + if result == Gst.State.CHANGE_FAILURE: logger.warning( 'Setting GStreamer state to %s failed', state.value_name) return False diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 8967a180..c77be700 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -58,7 +58,7 @@ class Scanner(object): duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: - pipeline.set_state(Gst.STATE_NULL) + pipeline.set_state(Gst.State.NULL) del pipeline return _Result(uri, tags, duration, seekable, mime, have_audio) @@ -110,8 +110,8 @@ def _pad_added(element, pad, pipeline): def _start_pipeline(pipeline): - if pipeline.set_state(Gst.STATE_PAUSED) == Gst.STATE_CHANGE_NO_PREROLL: - pipeline.set_state(Gst.STATE_PLAYING) + if pipeline.set_state(Gst.State.PAUSED) == Gst.State.CHANGE_NO_PREROLL: + pipeline.set_state(Gst.State.PLAYING) def _query_duration(pipeline): diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 48d3704b..ea5e5f25 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -520,17 +520,17 @@ class AudioStateTest(unittest.TestCase): def test_state_does_not_change_when_in_gst_ready_state(self): self.audio._handler.on_playbin_state_changed( - Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_VOID_PENDING) + Gst.State.NULL, Gst.State.READY, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) def test_state_changes_from_stopped_to_playing_on_play(self): self.audio._handler.on_playbin_state_changed( - Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_PLAYING) + Gst.State.NULL, Gst.State.READY, Gst.State.PLAYING) self.audio._handler.on_playbin_state_changed( - Gst.STATE_READY, Gst.STATE_PAUSED, Gst.STATE_PLAYING) + Gst.State.READY, Gst.State.PAUSED, Gst.State.PLAYING) self.audio._handler.on_playbin_state_changed( - Gst.STATE_PAUSED, Gst.STATE_PLAYING, Gst.STATE_VOID_PENDING) + Gst.State.PAUSED, Gst.State.PLAYING, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state) @@ -538,7 +538,7 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_VOID_PENDING) + Gst.State.PLAYING, Gst.State.PAUSED, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state) @@ -546,12 +546,12 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_NULL) + Gst.State.PLAYING, Gst.State.PAUSED, Gst.State.NULL) self.audio._handler.on_playbin_state_changed( - Gst.STATE_PAUSED, Gst.STATE_READY, Gst.STATE_NULL) + Gst.State.PAUSED, Gst.State.READY, Gst.State.NULL) # We never get the following call, so the logic must work without it # self.audio._handler.on_playbin_state_changed( - # Gst.STATE_READY, Gst.STATE_NULL, Gst.STATE_VOID_PENDING) + # Gst.State.READY, Gst.State.NULL, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) @@ -565,17 +565,17 @@ class AudioBufferingTest(unittest.TestCase): def test_pause_when_buffer_empty(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(Gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.State.PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) self.assertTrue(self.audio._buffering) def test_stay_paused_when_buffering_finished(self): playbin = self.audio._playbin self.audio.pause_playback() - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) playbin.set_state.reset_mock() self.audio._handler.on_buffering(100) @@ -585,11 +585,11 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_paused_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(Gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.State.PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) self.audio.pause_playback() playbin.set_state.reset_mock() @@ -600,13 +600,13 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_stopped_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(Gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.State.PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) playbin.set_state.reset_mock() self.audio.stop_playback() - playbin.set_state.assert_called_with(Gst.STATE_NULL) + playbin.set_state.assert_called_with(Gst.State.NULL) self.assertFalse(self.audio._buffering) From 74cf32ede23890e521e5ea328e5b9e671f13498c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:37:54 +0200 Subject: [PATCH 053/142] gst1: Update SEEK_FLAG_* with SeekFlags.* --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bcd424bf..9f880982 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -633,7 +633,7 @@ class Audio(pykka.ThreadingActor): # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) result = self._playbin.seek_simple( - Gst.Format(Gst.FORMAT_TIME), Gst.SEEK_FLAG_FLUSH, gst_position) + Gst.Format(Gst.FORMAT_TIME), Gst.SeekFlags.FLUSH, gst_position) gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result From 5d6981d70ef03b8a27b5e8ca76d6d98b7277bdf8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:38:35 +0200 Subject: [PATCH 054/142] gst1: Update FORMAT_* with Format.* --- mopidy/audio/actor.py | 4 ++-- mopidy/audio/scan.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 9f880982..f338b377 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -614,7 +614,7 @@ class Audio(pykka.ThreadingActor): :rtype: int """ try: - gst_position = self._playbin.query_position(Gst.FORMAT_TIME)[0] + gst_position = self._playbin.query_position(Gst.Format.TIME)[0] return utils.clocktime_to_millisecond(gst_position) except Gst.QueryError: # TODO: take state into account for this and possibly also return @@ -633,7 +633,7 @@ class Audio(pykka.ThreadingActor): # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) result = self._playbin.seek_simple( - Gst.Format(Gst.FORMAT_TIME), Gst.SeekFlags.FLUSH, gst_position) + Gst.Format.TIME, Gst.SeekFlags.FLUSH, gst_position) gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c77be700..bb778dc1 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -116,7 +116,7 @@ def _start_pipeline(pipeline): def _query_duration(pipeline): try: - duration = pipeline.query_duration(Gst.FORMAT_TIME, None)[0] + duration = pipeline.query_duration(Gst.Format.TIME, None)[0] except Gst.QueryError: return None @@ -127,7 +127,7 @@ def _query_duration(pipeline): def _query_seekable(pipeline): - query = Gst.query_new_seeking(Gst.FORMAT_TIME) + query = Gst.query_new_seeking(Gst.Format.TIME) pipeline.query(query) return query.parse_seeking()[1] From 8e771e89701f8c545d80def4d4cf5303486f83d5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:39:22 +0200 Subject: [PATCH 055/142] gst1: Update GhostPad() with GhostPad.new() --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index f338b377..8241f056 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -139,7 +139,7 @@ class _Outputs(Gst.Bin): self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) - ghost_pad = Gst.GhostPad('sink', self._tee.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', self._tee.get_pad('sink')) self.add_pad(ghost_pad) # Add an always connected fakesink which respects the clock so the tee @@ -487,7 +487,7 @@ class Audio(pykka.ThreadingActor): else: queue.link(self._outputs) - ghost_pad = Gst.GhostPad('sink', queue.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', queue.get_pad('sink')) audio_sink.add_pad(ghost_pad) self._playbin.set_property('audio-sink', audio_sink) From e402c9816c8cbb5bb4ed28b9b457008600ddf3cb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:45:26 +0200 Subject: [PATCH 056/142] gst1: Replace get_caps() with query_caps() --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index bb778dc1..389b7360 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -104,7 +104,7 @@ def _pad_added(element, pad, pipeline): sink.sync_state_with_parent() pad.link(sink.get_pad('sink')) - if pad.get_caps().is_subset(_RAW_AUDIO): + if pad.query_caps().is_subset(_RAW_AUDIO): struct = Gst.Structure('have-audio') element.get_bus().post(Gst.message_new_application(element, struct)) From 1cf450940a599295c6617d8db7ccf99234cdd14b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:48:35 +0200 Subject: [PATCH 057/142] gst1: Replace get_pad() with get_static_pad() --- mopidy/audio/actor.py | 7 ++++--- mopidy/audio/scan.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 8241f056..8f45a7b5 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -139,7 +139,7 @@ class _Outputs(Gst.Bin): self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) - ghost_pad = Gst.GhostPad.new('sink', self._tee.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', self._tee.get_static_pad('sink')) self.add_pad(ghost_pad) # Add an always connected fakesink which respects the clock so the tee @@ -460,7 +460,8 @@ class Audio(pykka.ThreadingActor): except exceptions.AudioException: process.exit_process() # TODO: move this up the chain - self._handler.setup_event_handling(self._outputs.get_pad('sink')) + self._handler.setup_event_handling( + self._outputs.get_static_pad('sink')) def _setup_audio_sink(self): audio_sink = Gst.Bin('audio-sink') @@ -487,7 +488,7 @@ class Audio(pykka.ThreadingActor): else: queue.link(self._outputs) - ghost_pad = Gst.GhostPad.new('sink', queue.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', queue.get_static_pad('sink')) audio_sink.add_pad(ghost_pad) self._playbin.set_property('audio-sink', audio_sink) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 389b7360..2880e67c 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -102,7 +102,7 @@ def _pad_added(element, pad, pipeline): pipeline.add(sink) sink.sync_state_with_parent() - pad.link(sink.get_pad('sink')) + pad.link(sink.get_static_pad('sink')) if pad.query_caps().is_subset(_RAW_AUDIO): struct = Gst.Structure('have-audio') From 01bf8b773fcb9dd7d6135b4d8009c42ec3374fd2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:51:57 +0200 Subject: [PATCH 058/142] gst1: Replace buffer.timestamp with buffer.pts --- mopidy/audio/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index aa0b1d63..100654d6 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -31,7 +31,7 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): capabilites = Gst.caps_from_string(capabilites) buffer_.set_caps(capabilites) if timestamp: - buffer_.timestamp = timestamp + buffer_.pts = timestamp if duration: buffer_.duration = duration return buffer_ From 6c59205efe476254b2272f330df607e56b08d1ae Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:55:55 +0200 Subject: [PATCH 059/142] gst1: Replace 'struct[x] = y' with 'struct.set_value(x, y)' --- mopidy/audio/scan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 2880e67c..780ca10a 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -91,8 +91,8 @@ def _setup_pipeline(uri, proxy_config=None): def _have_type(element, probability, caps, decodebin): decodebin.set_property('sink-caps', caps) - struct = Gst.Structure('have-type') - struct['caps'] = caps.get_structure(0) + struct = Gst.Structure.new_empty('have-type') + struct.set_value('caps', caps.get_structure(0)) element.get_bus().post(Gst.message_new_application(element, struct)) From aa3650bf34ce1e823cbf57860cb3c7868c59b71c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:57:12 +0200 Subject: [PATCH 060/142] gst1: Update query_new_duration() --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 780ca10a..3f221636 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -127,7 +127,7 @@ def _query_duration(pipeline): def _query_seekable(pipeline): - query = Gst.query_new_seeking(Gst.Format.TIME) + query = Gst.Query.new_seeking(Gst.Format.TIME) pipeline.query(query) return query.parse_seeking()[1] From c8ad7e3a414bb8452d7c4ee7c7f96fbd53b19865 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:59:50 +0200 Subject: [PATCH 061/142] gst1: Replace Caps() with Caps.from_string() And audio/x-raw-int and audio/x-raw-float with audio/x-raw --- mopidy/audio/actor.py | 2 +- mopidy/audio/scan.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 8f45a7b5..d51519be 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -560,7 +560,7 @@ class Audio(pykka.ThreadingActor): :type seek_data: callable which takes time position in ms """ self._appsrc.prepare( - Gst.Caps(bytes(caps)), need_data, enough_data, seek_data) + Gst.Caps.from_string(caps), need_data, enough_data, seek_data) self._playbin.set_property('uri', 'appsrc://') def emit_data(self, buffer_): diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 3f221636..550b6c14 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -14,8 +14,6 @@ from mopidy.internal import encoding _Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) -_RAW_AUDIO = Gst.Caps(b'audio/x-raw-int; audio/x-raw-float') - # TODO: replace with a scan(uri, timeout=1000, proxy_config=None)? class Scanner(object): @@ -104,7 +102,7 @@ def _pad_added(element, pad, pipeline): sink.sync_state_with_parent() pad.link(sink.get_static_pad('sink')) - if pad.query_caps().is_subset(_RAW_AUDIO): + if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): struct = Gst.Structure('have-audio') element.get_bus().post(Gst.message_new_application(element, struct)) From 63750d28fb1804438c295423be27df0b13ed91c8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:02:12 +0200 Subject: [PATCH 062/142] gst1: Replace playbin2 with playbin --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index d51519be..92319525 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -257,7 +257,7 @@ class _Handler(object): # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() - if msg.structure.has_name('playbin2-stream-changed'): + if msg.structure.has_name('playbin-stream-changed'): self.on_stream_changed(msg.structure['uri']) return True @@ -427,7 +427,7 @@ class Audio(pykka.ThreadingActor): jacksink.set_rank(Gst.RANK_SECONDARY) def _setup_playbin(self): - playbin = Gst.ElementFactory.make('playbin2') + playbin = Gst.ElementFactory.make('playbin') playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO # TODO: turn into config values... From 2920f83065bd200615d923a2a74ca07459f63bab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:02:23 +0200 Subject: [PATCH 063/142] gst1: Replace decodebin2 with decodebin --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 550b6c14..c0db7583 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -70,7 +70,7 @@ def _setup_pipeline(uri, proxy_config=None): raise exceptions.ScannerError('GStreamer can not open: %s' % uri) typefind = Gst.ElementFactory.make('typefind') - decodebin = Gst.ElementFactory.make('decodebin2') + decodebin = Gst.ElementFactory.make('decodebin') pipeline = Gst.ElementFactory.make('pipeline') for e in (src, typefind, decodebin): From 1007d42dd16182f6dbd2d4494b1f04e55a06f331 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:06:47 +0200 Subject: [PATCH 064/142] gst1: GLib.get_system_config_dirs() now returns a list --- mopidy/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 872d5773..af861032 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -19,7 +19,7 @@ from mopidy.internal import deps, process, timer, versioning logger = logging.getLogger(__name__) _default_config = [] -for base in GLib.get_system_config_dirs() + (GLib.get_user_config_dir(),): +for base in GLib.get_system_config_dirs() + [GLib.get_user_config_dir()]: _default_config.append(os.path.join(base, b'mopidy', b'mopidy.conf')) DEFAULT_CONFIG = b':'.join(_default_config) From 8aad1d184605e4d2d6a53a046b314f3d42adab1e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:14:21 +0200 Subject: [PATCH 065/142] gst1: Replace registry_get_default() with registry.get() --- mopidy/audio/actor.py | 2 +- mopidy/audio/utils.py | 2 +- mopidy/internal/deps.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92319525..bee87b43 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -420,7 +420,7 @@ class Audio(pykka.ThreadingActor): def _setup_preferences(self): # TODO: move out of audio actor? # Fix for https://github.com/mopidy/mopidy/issues/604 - registry = Gst.registry_get_default() + registry = Gst.Registry.get() jacksink = registry.find_feature( 'jackaudiosink', Gst.TYPE_ELEMENT_FACTORY) if jacksink: diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 100654d6..5e8d3512 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -55,7 +55,7 @@ def supported_uri_schemes(uri_schemes): :rtype: set of URI schemes we can support via this GStreamer install. """ supported_schemes = set() - registry = Gst.registry_get_default() + registry = Gst.Registry.get() for factory in registry.get_feature_list(Gst.TYPE_ELEMENT_FACTORY): for uri in factory.get_uri_protocols(): diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 3744db87..c42f28fb 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -186,6 +186,6 @@ def _gstreamer_check_elements(): ] known_elements = [ factory.get_name() for factory in - Gst.registry_get_default().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] + Gst.Registry.get().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] return [ (element, element in known_elements) for element in elements_to_check] From a2b009c581e1e08c4af82cbad4e718115acd6cb3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:18:47 +0200 Subject: [PATCH 066/142] gst1: Replace TYPE_ELEMENT_FACTORY with ElementFactory --- mopidy/audio/actor.py | 3 +-- mopidy/audio/utils.py | 2 +- mopidy/internal/deps.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bee87b43..fe029500 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -421,8 +421,7 @@ class Audio(pykka.ThreadingActor): # TODO: move out of audio actor? # Fix for https://github.com/mopidy/mopidy/issues/604 registry = Gst.Registry.get() - jacksink = registry.find_feature( - 'jackaudiosink', Gst.TYPE_ELEMENT_FACTORY) + jacksink = registry.find_feature('jackaudiosink', Gst.ElementFactory) if jacksink: jacksink.set_rank(Gst.RANK_SECONDARY) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 5e8d3512..00f2c56a 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -57,7 +57,7 @@ def supported_uri_schemes(uri_schemes): supported_schemes = set() registry = Gst.Registry.get() - for factory in registry.get_feature_list(Gst.TYPE_ELEMENT_FACTORY): + for factory in registry.get_feature_list(Gst.ElementFactory): for uri in factory.get_uri_protocols(): if uri in uri_schemes: supported_schemes.add(uri) diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index c42f28fb..6c93a8fa 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -186,6 +186,6 @@ def _gstreamer_check_elements(): ] known_elements = [ factory.get_name() for factory in - Gst.Registry.get().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] + Gst.Registry.get().get_feature_list(Gst.ElementFactory)] return [ (element, element in known_elements) for element in elements_to_check] From 38bcdae1bf76dff338001f4c02904a7ac026c0e1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:20:01 +0200 Subject: [PATCH 067/142] gst1: Replace RANK_SECONDARY with Rank.SECONDARY --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index fe029500..0abf9aa5 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -423,7 +423,7 @@ class Audio(pykka.ThreadingActor): registry = Gst.Registry.get() jacksink = registry.find_feature('jackaudiosink', Gst.ElementFactory) if jacksink: - jacksink.set_rank(Gst.RANK_SECONDARY) + jacksink.set_rank(Gst.Rank.SECONDARY) def _setup_playbin(self): playbin = Gst.ElementFactory.make('playbin') From 3f8ebc83c1a73491f33f93de64f05a3c43db9297 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:21:01 +0200 Subject: [PATCH 068/142] gst1: Replace ghost_unconnected_pads with ghost_unlinked_pads --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 0abf9aa5..8ecc0f37 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -152,7 +152,7 @@ class _Outputs(Gst.Bin): # XXX This only works for pipelines not in use until #790 gets done. try: output = Gst.parse_bin_from_description( - description, ghost_unconnected_pads=True) + description, ghost_unlinked_pads=True) except GObject.GError as ex: logger.error( 'Failed to create audio output "%s": %s', description, ex) From 9c0547d039fa51c2a7bf4653ce419a07a0193a87 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:23:50 +0200 Subject: [PATCH 069/142] gst1: Replace {add,remove}_event_probe() with {add,remove}_event() --- mopidy/audio/actor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 8ecc0f37..b6a441f4 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -219,7 +219,8 @@ class _Handler(object): def setup_event_handling(self, pad): self._pad = pad - self._event_handler_id = pad.add_event_probe(self.on_event) + self._event_handler_id = pad.add_probe( + Gst.PadProbeType.EVENT_BOTH, self.on_event) def teardown_message_handling(self): bus = self._element.get_bus() @@ -228,7 +229,7 @@ class _Handler(object): self._message_handler_id = None def teardown_event_handling(self): - self._pad.remove_event_probe(self._event_handler_id) + self._pad.remove_probe(self._event_handler_id) self._event_handler_id = None def on_message(self, bus, msg): From bd077591d0fff3bd2fc2bfd974db4ce076c14ba9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:25:27 +0200 Subject: [PATCH 070/142] gst1: Replace element_make_from_uri() with Element.make_from_uri() --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c0db7583..49b44f79 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -65,7 +65,7 @@ class Scanner(object): # Turns out it's _much_ faster to just create a new pipeline for every as # decodebins and other elements don't seem to take well to being reused. def _setup_pipeline(uri, proxy_config=None): - src = Gst.element_make_from_uri(Gst.URI_SRC, uri) + src = Gst.Element.make_from_uri(Gst.URI_SRC, uri) if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) From 3c2f83f6a6a9a378ea1dbcbce578659c89460fbf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:25:50 +0200 Subject: [PATCH 071/142] gst1: Replace Gst.URI_SRC with Gst.URIType.SRC --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 49b44f79..0518da8b 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -65,7 +65,7 @@ class Scanner(object): # Turns out it's _much_ faster to just create a new pipeline for every as # decodebins and other elements don't seem to take well to being reused. def _setup_pipeline(uri, proxy_config=None): - src = Gst.Element.make_from_uri(Gst.URI_SRC, uri) + src = Gst.Element.make_from_uri(Gst.URIType.SRC, uri) if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) From 67f4d57964f3df35c1d3644617c73b543f8d8626 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:31:28 +0200 Subject: [PATCH 072/142] gst1: Replace MESSAGE_* with MessageType.* --- mopidy/audio/actor.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index b6a441f4..e9d9f49c 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -233,21 +233,23 @@ class _Handler(object): self._event_handler_id = None def on_message(self, bus, msg): - if msg.type == Gst.MESSAGE_STATE_CHANGED and msg.src == self._element: + if ( + msg.type == Gst.MessageType.STATE_CHANGED and + msg.src == self._element): self.on_playbin_state_changed(*msg.parse_state_changed()) - elif msg.type == Gst.MESSAGE_BUFFERING: + elif msg.type == Gst.MessageType.BUFFERING: self.on_buffering(msg.parse_buffering(), msg.structure) - elif msg.type == Gst.MESSAGE_EOS: + elif msg.type == Gst.MessageType.EOS: self.on_end_of_stream() - elif msg.type == Gst.MESSAGE_ERROR: + elif msg.type == Gst.MessageType.ERROR: self.on_error(*msg.parse_error()) - elif msg.type == Gst.MESSAGE_WARNING: + elif msg.type == Gst.MessageType.WARNING: self.on_warning(*msg.parse_warning()) - elif msg.type == Gst.MESSAGE_ASYNC_DONE: + elif msg.type == Gst.MessageType.ASYNC_DONE: self.on_async_done() - elif msg.type == Gst.MESSAGE_TAG: + elif msg.type == Gst.MessageType.TAG: self.on_tag(msg.parse_tag()) - elif msg.type == Gst.MESSAGE_ELEMENT: + elif msg.type == Gst.MessageType.ELEMENT: if Gst.pbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) From e621d8055a011957582e195816ca74464eae60f4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:32:15 +0200 Subject: [PATCH 073/142] gst1: Replace gst.pbutils with GstPbutils --- mopidy/audio/actor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index e9d9f49c..4d57e86e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -6,7 +6,7 @@ import threading import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import GObject, Gst, GstPbutils import pykka @@ -250,7 +250,7 @@ class _Handler(object): elif msg.type == Gst.MessageType.TAG: self.on_tag(msg.parse_tag()) elif msg.type == Gst.MessageType.ELEMENT: - if Gst.pbutils.is_missing_plugin_message(msg): + if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) def on_event(self, pad, event): @@ -347,12 +347,12 @@ class _Handler(object): AudioListener.send('tags_changed', tags=tags.keys()) def on_missing_plugin(self, msg): - desc = Gst.pbutils.missing_plugin_message_get_description(msg) - debug = Gst.pbutils.missing_plugin_message_get_installer_detail(msg) + desc = GstPbutils.missing_plugin_message_get_description(msg) + debug = GstPbutils.missing_plugin_message_get_installer_detail(msg) gst_logger.debug('Got missing-plugin message: description:%s', desc) logger.warning('Could not find a %s to handle media.', desc) - if Gst.pbutils.install_plugins_supported(): + if GstPbutils.install_plugins_supported(): logger.info('You might be able to fix this by running: ' 'gst-installer "%s"', debug) # TODO: store the missing plugins installer info in a file so we can From 1911ea0c103813ab03fcdc7075151ba3ccabe633 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:34:20 +0200 Subject: [PATCH 074/142] gst1: Replace STATE_CHANGE_* with StateChangeReturn.* --- mopidy/audio/actor.py | 2 +- mopidy/audio/scan.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 4d57e86e..92abe4bc 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -721,7 +721,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('State change to %s: result=%s', state.value_name, result.value_name) - if result == Gst.State.CHANGE_FAILURE: + if result == Gst.StateChangeReturn.FAILURE: logger.warning( 'Setting GStreamer state to %s failed', state.value_name) return False diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0518da8b..7cc5b1e5 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -108,7 +108,8 @@ def _pad_added(element, pad, pipeline): def _start_pipeline(pipeline): - if pipeline.set_state(Gst.State.PAUSED) == Gst.State.CHANGE_NO_PREROLL: + result = pipeline.set_state(Gst.State.PAUSED) + if result == Gst.StateChangeReturn.NO_PREROLL: pipeline.set_state(Gst.State.PLAYING) From 7c473eed070a1138855b99d7bbbb3eae37f565c5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:36:24 +0200 Subject: [PATCH 075/142] gst1: Replace MESSAGE_* with MessageType.* --- mopidy/audio/scan.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 7cc5b1e5..5f2d75b5 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -141,8 +141,13 @@ def _process(pipeline, timeout_ms): missing_message = None types = ( - Gst.MESSAGE_ELEMENT | Gst.MESSAGE_APPLICATION | Gst.MESSAGE_ERROR | - Gst.MESSAGE_EOS | Gst.MESSAGE_ASYNC_DONE | Gst.MESSAGE_TAG) + Gst.MessageType.ELEMENT | + Gst.MessageType.APPLICATION | + Gst.MessageType.ERROR | + Gst.MessageType.EOS | + Gst.MessageType.ASYNC_DONE | + Gst.MessageType.TAG + ) previous = clock.get_time() while timeout > 0: @@ -150,29 +155,29 @@ def _process(pipeline, timeout_ms): if message is None: break - elif message.type == Gst.MESSAGE_ELEMENT: + elif message.type == Gst.MessageType.ELEMENT: if GstPbutils.is_missing_plugin_message(message): missing_message = message - elif message.type == Gst.MESSAGE_APPLICATION: + elif message.type == Gst.MessageType.APPLICATION: if message.structure.get_name() == 'have-type': mime = message.structure['caps'].get_name() if mime.startswith('text/') or mime == 'application/xml': return tags, mime, have_audio elif message.structure.get_name() == 'have-audio': have_audio = True - elif message.type == Gst.MESSAGE_ERROR: + elif message.type == Gst.MessageType.ERROR: error = encoding.locale_decode(message.parse_error()[0]) 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: + elif message.type == Gst.MessageType.EOS: return tags, mime, have_audio - elif message.type == Gst.MESSAGE_ASYNC_DONE: + elif message.type == Gst.MessageType.ASYNC_DONE: if message.src == pipeline: return tags, mime, have_audio - elif message.type == Gst.MESSAGE_TAG: + elif message.type == Gst.MessageType.TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) From 1b47b6341e7dfa8a3ea96c211585c0fa47131423 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:37:31 +0200 Subject: [PATCH 076/142] gst1: Replace message_new_application() with Message.new_application() --- mopidy/audio/scan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 5f2d75b5..0eb4067e 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -91,7 +91,7 @@ def _have_type(element, probability, caps, decodebin): decodebin.set_property('sink-caps', caps) struct = Gst.Structure.new_empty('have-type') struct.set_value('caps', caps.get_structure(0)) - element.get_bus().post(Gst.message_new_application(element, struct)) + element.get_bus().post(Gst.Message.new_application(element, struct)) def _pad_added(element, pad, pipeline): @@ -104,7 +104,7 @@ def _pad_added(element, pad, pipeline): if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): struct = Gst.Structure('have-audio') - element.get_bus().post(Gst.message_new_application(element, struct)) + element.get_bus().post(Gst.Message.new_application(element, struct)) def _start_pipeline(pipeline): From e6a4042c3e6ce21af9c3fa490d69b9d960e7d1e9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:47:52 +0200 Subject: [PATCH 077/142] gst1: Replace message.structure with message.get_structure() --- mopidy/audio/actor.py | 8 ++++---- mopidy/audio/scan.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92abe4bc..ea452c22 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -238,7 +238,7 @@ class _Handler(object): msg.src == self._element): self.on_playbin_state_changed(*msg.parse_state_changed()) elif msg.type == Gst.MessageType.BUFFERING: - self.on_buffering(msg.parse_buffering(), msg.structure) + self.on_buffering(msg.parse_buffering(), msg.get_structure()) elif msg.type == Gst.MessageType.EOS: self.on_end_of_stream() elif msg.type == Gst.MessageType.ERROR: @@ -260,8 +260,8 @@ class _Handler(object): # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() - if msg.structure.has_name('playbin-stream-changed'): - self.on_stream_changed(msg.structure['uri']) + if msg.get_structure().has_name('playbin-stream-changed'): + self.on_stream_changed(msg.get_structure().get_string('uri')) return True def on_playbin_state_changed(self, old_state, new_state, pending_state): @@ -303,7 +303,7 @@ class _Handler(object): def on_buffering(self, percent, structure=None): if structure and structure.has_field('buffering-mode'): - if structure['buffering-mode'] == Gst.BUFFERING_LIVE: + if structure.get_enum('buffering-mode') == Gst.BufferingMode.LIVE: return # Live sources stall in paused. level = logging.getLevelName('TRACE') diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0eb4067e..fbe80585 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -159,16 +159,16 @@ def _process(pipeline, timeout_ms): if GstPbutils.is_missing_plugin_message(message): missing_message = message elif message.type == Gst.MessageType.APPLICATION: - if message.structure.get_name() == 'have-type': - mime = message.structure['caps'].get_name() + if message.get_structure().get_name() == 'have-type': + mime = message.get_structure()['caps'].get_name() if mime.startswith('text/') or mime == 'application/xml': return tags, mime, have_audio - elif message.structure.get_name() == 'have-audio': + elif message.get_structure().get_name() == 'have-audio': have_audio = True elif message.type == Gst.MessageType.ERROR: error = encoding.locale_decode(message.parse_error()[0]) if missing_message and not mime: - caps = missing_message.structure['detail'] + caps = missing_message.get_structure()['detail'] mime = caps.get_structure(0).get_name() return tags, mime, have_audio raise exceptions.ScannerError(error) From a0714455cd89b2eab9346c3d5d60ae942be492bf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:52:48 +0200 Subject: [PATCH 078/142] gst1: Use methods to get struct fields --- mopidy/audio/scan.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fbe80585..9d7ecde5 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -160,15 +160,16 @@ def _process(pipeline, timeout_ms): missing_message = message elif message.type == Gst.MessageType.APPLICATION: if message.get_structure().get_name() == 'have-type': - mime = message.get_structure()['caps'].get_name() - if mime.startswith('text/') or mime == 'application/xml': + mime = message.get_structure().get_value('caps').get_name() + if mime and ( + mime.startswith('text/') or mime == 'application/xml'): return tags, mime, have_audio elif message.get_structure().get_name() == 'have-audio': have_audio = True elif message.type == Gst.MessageType.ERROR: error = encoding.locale_decode(message.parse_error()[0]) if missing_message and not mime: - caps = missing_message.get_structure()['detail'] + caps = missing_message.get_structure().get_value('detail') mime = caps.get_structure(0).get_name() return tags, mime, have_audio raise exceptions.ScannerError(error) From f95e307ba06cda9d534db6dd37c6517942bd9055 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:05:59 +0200 Subject: [PATCH 079/142] gst1: Replace BUS_DROP with BusSyncReply.DROP --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index ea452c22..ed118f8d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -690,7 +690,7 @@ class Audio(pykka.ThreadingActor): """ def sync_handler(bus, message): self._handler.on_message(bus, message) - return Gst.BUS_DROP + return Gst.BusSyncReply.DROP bus = self._playbin.get_bus() bus.set_sync_handler(sync_handler) From 275f9d50623d80fab3bcbb7860dd69e292da07b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:20:11 +0200 Subject: [PATCH 080/142] gst1: Buffers no longer have caps --- mopidy/audio/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 00f2c56a..3d9aad36 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -8,7 +8,7 @@ import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -from mopidy import compat, httpclient +from mopidy import httpclient from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) @@ -24,12 +24,11 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): """Create a new GStreamer buffer based on provided data. Mainly intended to keep gst imports out of non-audio modules. + + .. versionchanged:: 1.2 + ``capabilites`` argument is no longer in use """ - buffer_ = Gst.Buffer(data) - if capabilites: - if isinstance(capabilites, compat.string_types): - capabilites = Gst.caps_from_string(capabilites) - buffer_.set_caps(capabilites) + buffer_ = Gst.Buffer.new_wrapped(data) if timestamp: buffer_.pts = timestamp if duration: From 3d98a77a3c687d0ce429446c106aff675050cedb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:21:21 +0200 Subject: [PATCH 081/142] gst1: Replace FLOW_* with FlowReturn.* --- mopidy/audio/actor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index ed118f8d..75c207dd 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -116,9 +116,11 @@ class _Appsrc(object): if buffer_ is None: gst_logger.debug('Sending appsrc end-of-stream event.') - return self._source.emit('end-of-stream') == Gst.FLOW_OK + result = self._source.emit('end-of-stream') + return result == Gst.FlowReturn.OK else: - return self._source.emit('push-buffer', buffer_) == Gst.FLOW_OK + result = self._source.emit('push-buffer', buffer_) + return result == Gst.FlowReturn.OK def _on_signal(self, element, clocktime, func): # This shim is used to ensure we always return true, and also handles From 8a846b860595d48aec5a5d0a1361f6a225f7327e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:30:36 +0200 Subject: [PATCH 082/142] gst1: Replace EVENT_* with EventType.* --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 75c207dd..92e6ceb2 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -256,9 +256,9 @@ class _Handler(object): self.on_missing_plugin(msg) def on_event(self, pad, event): - if event.type == Gst.EVENT_NEWSEGMENT: + if event.type == Gst.EventType.SEGMENT: self.on_new_segment(*event.parse_new_segment()) - elif event.type == Gst.EVENT_SINK_MESSAGE: + elif event.type == Gst.EventType.SINK_MESSAGE: # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() From 6c9e2d4d3465470fb834870aadfcd76504e939b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:33:58 +0200 Subject: [PATCH 083/142] gst1: Add timeout to get_state() --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92e6ceb2..3d6284bc 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -683,7 +683,7 @@ class Audio(pykka.ThreadingActor): Should only be used by tests. """ - self._playbin.get_state() + self._playbin.get_state(timeout=1) def enable_sync_handler(self): """Enable manual processing of messages from bus. From ee51983cfdf72b632ba1e40ef54fc003b035fd5c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 21:30:32 +0200 Subject: [PATCH 084/142] gst1: Replace TagList() with TagList.new_empty() --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 3d6284bc..510fcbba 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -743,7 +743,7 @@ class Audio(pykka.ThreadingActor): :param track: the current track :type track: :class:`mopidy.models.Track` """ - taglist = Gst.TagList() + taglist = Gst.TagList.new_empty() artists = [a for a in (track.artists or []) if a.name] # Default to blank data to trick shoutcast into clearing any previous From 3765e90bc744c3a43bd350738ee21afa3162fa0d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 22 Sep 2015 22:24:57 +0200 Subject: [PATCH 085/142] gst1: Replace DEBUG_BIN_TO_DOT_FILE with debug_bin_to_dot_file --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 510fcbba..94fa5aea 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -300,8 +300,8 @@ class _Handler(object): AudioListener.send('stream_changed', uri=None) if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ: - Gst.DEBUG_BIN_TO_DOT_FILE( - self._audio._playbin, Gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') + Gst.debug_bin_to_dot_file( + self._audio._playbin, Gst.DebugGraphDetails.ALL, 'mopidy') def on_buffering(self, percent, structure=None): if structure and structure.has_field('buffering-mode'): From ef40854b8629e88fe591f7801828876833a2cb0a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 21:32:57 +0100 Subject: [PATCH 086/142] gst1: Update index into query_position() result --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 94fa5aea..7cbe9393 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -619,7 +619,7 @@ class Audio(pykka.ThreadingActor): :rtype: int """ try: - gst_position = self._playbin.query_position(Gst.Format.TIME)[0] + gst_position = self._playbin.query_position(Gst.Format.TIME)[1] return utils.clocktime_to_millisecond(gst_position) except Gst.QueryError: # TODO: take state into account for this and possibly also return From ee99bedf3919506653ae71a2cc9b4074cd34de2a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 21:39:09 +0100 Subject: [PATCH 087/142] gst1: Gst.Bin() no longer takes a name --- mopidy/audio/actor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 7cbe9393..c51449c5 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -136,7 +136,8 @@ class _Appsrc(object): class _Outputs(Gst.Bin): def __init__(self): - Gst.Bin.__init__(self, 'outputs') + Gst.Bin.__init__(self) + # TODO gst1: Set 'outputs' as the Bin name for easier debugging self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) @@ -468,7 +469,7 @@ class Audio(pykka.ThreadingActor): self._outputs.get_static_pad('sink')) def _setup_audio_sink(self): - audio_sink = Gst.Bin('audio-sink') + audio_sink = Gst.ElementFactory.make('bin', 'audio-sink') # Queue element to buy us time between the about to finish event and # the actual switch, i.e. about to switch can block for longer thanks From 5277ad5ff573bd0d4b1256bd1c3d022fcc4c3877 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 21:54:01 +0100 Subject: [PATCH 088/142] gst1: Update get_enum() to include enum type it expects --- mopidy/audio/actor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index c51449c5..e2ee4041 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -305,8 +305,10 @@ class _Handler(object): self._audio._playbin, Gst.DebugGraphDetails.ALL, 'mopidy') def on_buffering(self, percent, structure=None): - if structure and structure.has_field('buffering-mode'): - if structure.get_enum('buffering-mode') == Gst.BufferingMode.LIVE: + if structure is not None and structure.has_field('buffering-mode'): + buffering_mode = structure.get_enum( + 'buffering-mode', Gst.BufferingMode) + if buffering_mode == Gst.BufferingMode.LIVE: return # Live sources stall in paused. level = logging.getLevelName('TRACE') From 87b1c9455c89b9dbd245d5851d55659e2b2621b6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 23:37:22 +0100 Subject: [PATCH 089/142] gst1: Update query_duration() usage --- mopidy/audio/scan.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 9d7ecde5..c51762b9 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -114,15 +114,12 @@ def _start_pipeline(pipeline): def _query_duration(pipeline): - try: - duration = pipeline.query_duration(Gst.Format.TIME, None)[0] - except Gst.QueryError: + success, duration = pipeline.query_duration(Gst.Format.TIME) + + if not success or duration < 0: return None - if duration < 0: - return None - else: - return duration // Gst.MSECOND + return duration // Gst.MSECOND def _query_seekable(pipeline): From 01cf013b098964b1fd7ddd3efb80d27c4dbc4bcd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 23:39:12 +0100 Subject: [PATCH 090/142] gst1: Update query_position() usage --- mopidy/audio/actor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index e2ee4041..a2401ea9 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -621,15 +621,16 @@ class Audio(pykka.ThreadingActor): :rtype: int """ - try: - gst_position = self._playbin.query_position(Gst.Format.TIME)[1] - return utils.clocktime_to_millisecond(gst_position) - except Gst.QueryError: + success, position = self._playbin.query_position(Gst.Format.TIME) + + if not success: # TODO: take state into account for this and possibly also return # None as the unknown value instead of zero? logger.debug('Position query failed') return 0 + return utils.clocktime_to_millisecond(position) + def set_position(self, position): """ Set position in milliseconds. From 20b1c21b0b5847f2c0bb33eb050e3fc3020b83a6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 23:52:17 +0100 Subject: [PATCH 091/142] gst1: Avoid using pipeline.get_clock() Often the clock isn't available for use. gst_pipeline_clock() which is always available requires Gst 1.6. --- mopidy/audio/scan.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c51762b9..718f2d6e 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -2,6 +2,7 @@ from __future__ import ( absolute_import, division, print_function, unicode_literals) import collections +import time import gi gi.require_version('Gst', '1.0') @@ -129,9 +130,7 @@ def _query_seekable(pipeline): def _process(pipeline, timeout_ms): - clock = pipeline.get_clock() bus = pipeline.get_bus() - timeout = timeout_ms * Gst.MSECOND tags = {} mime = None have_audio = False @@ -146,9 +145,10 @@ def _process(pipeline, timeout_ms): Gst.MessageType.TAG ) - previous = clock.get_time() + timeout = timeout_ms + previous = int(time.time() * 1000) while timeout > 0: - message = bus.timed_pop_filtered(timeout, types) + message = bus.timed_pop_filtered(timeout * Gst.MSECOND, types) if message is None: break @@ -180,7 +180,7 @@ def _process(pipeline, timeout_ms): # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) - now = clock.get_time() + now = int(time.time() * 1000) timeout -= now - previous previous = now From fc54a17b44b903f7d46426b6e9bf892b2443faed Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 21:29:09 +0100 Subject: [PATCH 092/142] gst1: require_version('Gst', '1.0') before use --- mopidy/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index c91740a3..6d399bd4 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -7,6 +7,8 @@ import sys import textwrap try: + import gi + gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst except ImportError: print(textwrap.dedent(""" From da19c8be56320fa92e7e4c9c04f92b701d5ce884 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 21:45:10 +0100 Subject: [PATCH 093/142] gst1: on_new_segment() gets a Segment struct --- mopidy/audio/actor.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index a2401ea9..5506475d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -258,7 +258,7 @@ class _Handler(object): def on_event(self, pad, event): if event.type == Gst.EventType.SEGMENT: - self.on_new_segment(*event.parse_new_segment()) + self.on_new_segment(event.parse_new_segment()) elif event.type == Gst.EventType.SINK_MESSAGE: # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. @@ -364,11 +364,18 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? - def on_new_segment(self, update, rate, format_, start, stop, position): - gst_logger.debug('Got new-segment event: update=%s rate=%s format=%s ' - 'start=%s stop=%s position=%s', update, rate, - format_.value_name, start, stop, position) - position_ms = position // Gst.MSECOND + def on_new_segment(self, segment): + gst_logger.debug( + 'Got new-segment event: ' + 'rate=%(rate)s format=%(format)s start=%(start)s stop=%(stop)s ' + 'position=%(position)s', { + 'rate': segment.rate, + 'format': Gst.Format.get_name(segment.format), + 'start': segment.start, + 'stop': segment.stop, + 'position': segment.position + }) + position_ms = segment.position // Gst.MSECOND logger.debug('Audio event: position_changed(position=%s)', position_ms) AudioListener.send('position_changed', position=position_ms) From 3792b8c9006b91ef61fd6da5dfb1e482857108c8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 22:00:59 +0100 Subject: [PATCH 094/142] gst1: Use Gst.CLOCK_TIME_NONE to block for state changes in tests --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 5506475d..5beb840a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -694,7 +694,7 @@ class Audio(pykka.ThreadingActor): Should only be used by tests. """ - self._playbin.get_state(timeout=1) + self._playbin.get_state(timeout=Gst.CLOCK_TIME_NONE) def enable_sync_handler(self): """Enable manual processing of messages from bus. From 13567d271a30aa483aee9ecf60abf4d79c96118a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 22:39:08 +0100 Subject: [PATCH 095/142] gst1: Update taglist translator --- mopidy/audio/utils.py | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 3d9aad36..cc312b73 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -import datetime +import collections import logging import numbers @@ -8,7 +8,7 @@ import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -from mopidy import httpclient +from mopidy import compat, httpclient from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) @@ -154,7 +154,7 @@ def setup_proxy(element, config): def convert_taglist(taglist): - """Convert a :class:`Gst.Taglist` to plain Python types. + """Convert a :class:`Gst.TagList` to plain Python types. Knows how to convert: @@ -167,37 +167,26 @@ def convert_taglist(taglist): Unknown types will be ignored and debug logged. Tag keys are all strings defined as part GStreamer under GstTagList_. - .. _GstTagList: http://gstreamer.freedesktop.org/data/doc/gstreamer/\ -0.10.36/gstreamer/html/gstreamer-GstTagList.html + .. _GstTagList: https://developer.gnome.org/gstreamer/stable/\ +gstreamer-GstTagList.html :param taglist: A GStreamer taglist to be converted. - :type taglist: :class:`Gst.Taglist` + :type taglist: :class:`Gst.TagList` :rtype: dictionary of tag keys with a list of values. """ - result = {} + result = collections.defaultdict(list) - # Taglists are not really dicts, hence the lack of .items() and - # explicit use of .keys() - for key in taglist.keys(): - result.setdefault(key, []) + for n in range(taglist.n_tags()): + tag = taglist.nth_tag_name(n) - values = taglist[key] - if not isinstance(values, list): - values = [values] + for i in range(taglist.get_tag_size(tag)): + value = taglist.get_value_index(tag, i) - for value in values: - if isinstance(value, Gst.Date): - try: - date = datetime.date(value.year, value.month, value.day) - result[key].append(date) - except ValueError: - logger.debug('Ignoring invalid date: %r = %r', key, value) - elif isinstance(value, Gst.Buffer): - result[key].append(bytes(value)) - elif isinstance( - value, (compat.string_types, bool, numbers.Number)): - result[key].append(value) + if isinstance(value, Gst.DateTime): + result[tag].append(value.to_iso8601_string()) + if isinstance(value, (compat.string_types, bool, numbers.Number)): + result[tag].append(value) else: - logger.debug('Ignoring unknown data: %r = %r', key, value) + logger.debug('Ignoring unknown tag data: %r = %r', tag, value) return result From 3e4bd16be2b648901719ee27da859bd749be31fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 23 Nov 2015 00:10:39 +0100 Subject: [PATCH 096/142] gst1: Replace playbin-stream-changed with Gst.MessageType.STREAM_START --- mopidy/audio/actor.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 5beb840a..e6dca996 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -255,16 +255,12 @@ class _Handler(object): elif msg.type == Gst.MessageType.ELEMENT: if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) + elif msg.type == Gst.MessageType.STREAM_START: + self.on_stream_changed(self._audio._playbin.get_property('uri')) def on_event(self, pad, event): if event.type == Gst.EventType.SEGMENT: self.on_new_segment(event.parse_new_segment()) - elif event.type == Gst.EventType.SINK_MESSAGE: - # Handle stream changed messages when they reach our output bin. - # If we listen for it on the bus we get one per tee branch. - msg = event.parse_sink_message() - if msg.get_structure().has_name('playbin-stream-changed'): - self.on_stream_changed(msg.get_structure().get_string('uri')) return True def on_playbin_state_changed(self, old_state, new_state, pending_state): From 29a194cb551196f790a2949c52654a1218628e23 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 23 Nov 2015 21:25:20 +0100 Subject: [PATCH 097/142] gst1: Use new API for TagList creation --- mopidy/audio/actor.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index e6dca996..4de7f833 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -743,7 +743,7 @@ class Audio(pykka.ThreadingActor): """ Set track metadata for currently playing song. - Only needs to be called by sources such as `appsrc` which do not + Only needs to be called by sources such as ``appsrc`` which do not already inject tags in playbin, e.g. when using :meth:`emit_data` to deliver raw audio data to GStreamer. @@ -753,20 +753,27 @@ class Audio(pykka.ThreadingActor): taglist = Gst.TagList.new_empty() artists = [a for a in (track.artists or []) if a.name] + def set_value(tag, value): + gobject_value = GObject.Value() + gobject_value.init(GObject.TYPE_STRING) + gobject_value.set_string(value) + taglist.add_value( + Gst.TagMergeMode.REPLACE, Gst.TAG_ARTIST, gobject_value) + # Default to blank data to trick shoutcast into clearing any previous # values it might have. - taglist[Gst.TAG_ARTIST] = ' ' - taglist[Gst.TAG_TITLE] = ' ' - taglist[Gst.TAG_ALBUM] = ' ' + set_value(Gst.TAG_ARTIST, ' ') + set_value(Gst.TAG_TITLE, ' ') + set_value(Gst.TAG_ALBUM, ' ') if artists: - taglist[Gst.TAG_ARTIST] = ', '.join([a.name for a in artists]) + set_value(Gst.TAG_ARTIST, ', '.join([a.name for a in artists])) if track.name: - taglist[Gst.TAG_TITLE] = track.name + set_value(Gst.TAG_TITLE, track.name) if track.album and track.album.name: - taglist[Gst.TAG_ALBUM] = track.album.name + set_value(Gst.TAG_ALBUM, track.album.name) event = Gst.event_new_tag(taglist) # TODO: check if we get this back on our own bus? From ce198ba9f83cf4ee2467025a8f99f459c66c7cef Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 27 Nov 2015 15:07:16 +0100 Subject: [PATCH 098/142] gst1: Update pad probe callback to match new signature --- mopidy/audio/actor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 4de7f833..80462767 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -223,7 +223,7 @@ class _Handler(object): def setup_event_handling(self, pad): self._pad = pad self._event_handler_id = pad.add_probe( - Gst.PadProbeType.EVENT_BOTH, self.on_event) + Gst.PadProbeType.EVENT_BOTH, self.on_pad_event) def teardown_message_handling(self): bus = self._element.get_bus() @@ -258,10 +258,11 @@ class _Handler(object): elif msg.type == Gst.MessageType.STREAM_START: self.on_stream_changed(self._audio._playbin.get_property('uri')) - def on_event(self, pad, event): + def on_pad_event(self, pad, pad_probe_info): + event = pad_probe_info.get_event() if event.type == Gst.EventType.SEGMENT: self.on_new_segment(event.parse_new_segment()) - return True + return Gst.PadProbeReturn.OK def on_playbin_state_changed(self, old_state, new_state, pending_state): gst_logger.debug('Got state-changed message: old=%s new=%s pending=%s', From 592f5dec53caa0797b80180b240a32ccceb41f90 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 11:46:29 +0100 Subject: [PATCH 099/142] gst1: Remove deprecated GObject.threads_init() Ref https://wiki.gnome.org/Projects/PyGObject/Threading "The requirement to call GObject.threads_init() has been removed from PyGObject 3.10.2 when using Python native threads with GI (via the threading module) as well as with GI repositories which manage their own threads that may call back into Python (like GStreamer callbacks). The GObject.threads_init() function will still exist for the entire 3.x series for compatibility reasons but emits a deprecation warning." --- mopidy/__main__.py | 11 +++++------ mopidy/audio/scan.py | 3 +-- tests/audio/test_actor.py | 3 +-- tests/audio/test_scan.py | 3 +-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 6d399bd4..06b7658d 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -9,21 +9,20 @@ import textwrap try: import gi gi.require_version('Gst', '1.0') - from gi.repository import GObject, Gst + from gi.repository import Gst except ImportError: print(textwrap.dedent(""" - ERROR: The GObject and Gst Python packages were not found. + ERROR: The GStreamer Python package was not found. - Mopidy requires GStreamer and GObject to work. These are C libraries - with a number of dependencies themselves, and cannot be installed with - the regular Python tools like pip. + Mopidy requires GStreamer to work. GStreamer is a C library with a + number of dependencies itself, and cannot be installed with the regular + Python tools like pip. Please see http://docs.mopidy.com/en/latest/installation/ for instructions on how to install the required dependencies. """)) raise -GObject.threads_init() Gst.init() try: diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 718f2d6e..fdd97784 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -6,7 +6,7 @@ import time import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst, GstPbutils +from gi.repository import Gst, GstPbutils from mopidy import exceptions from mopidy.audio import utils @@ -193,7 +193,6 @@ if __name__ == '__main__': from mopidy.internal import path - GObject.threads_init() Gst.init() scanner = Scanner(5000) diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index ea5e5f25..0cf89418 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -5,9 +5,8 @@ import unittest import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import Gst -GObject.threads_init() Gst.init() import mock diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index ab995285..6e3ba001 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -5,9 +5,8 @@ import unittest import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import Gst -GObject.threads_init() Gst.init() from mopidy import exceptions From eb4c742015612a0f9a39a64657fff4d6a8d981f7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 30 Nov 2015 15:27:25 +0100 Subject: [PATCH 100/142] gst1: Run gst.init() if needed everywhere using Gst --- mopidy/__main__.py | 4 ++-- mopidy/audio/actor.py | 1 + mopidy/audio/scan.py | 3 +-- mopidy/internal/deps.py | 1 + tests/audio/test_actor.py | 2 -- tests/audio/test_scan.py | 6 ------ 6 files changed, 5 insertions(+), 12 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 06b7658d..1d9e8314 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -22,8 +22,8 @@ except ImportError: instructions on how to install the required dependencies. """)) raise - -Gst.init() +else: + Gst.init() try: # Make GObject's mainloop the event loop for python-dbus diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 80462767..c5de90dc 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -7,6 +7,7 @@ import threading import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst, GstPbutils +Gst.is_initialized() or Gst.init() import pykka diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fdd97784..f4bbd3a0 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -7,6 +7,7 @@ import time import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GstPbutils +Gst.is_initialized() or Gst.init() from mopidy import exceptions from mopidy.audio import utils @@ -193,8 +194,6 @@ if __name__ == '__main__': from mopidy.internal import path - Gst.init() - scanner = Scanner(5000) for uri in sys.argv[1:]: if not Gst.uri_is_valid(uri): diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 6c93a8fa..8947025f 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -8,6 +8,7 @@ import sys import gi gi.require_version('Gst', '1.0') from gi.repository import Gst +Gst.is_initialized() or Gst.init() import pkg_resources diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 0cf89418..41f730e8 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -7,8 +7,6 @@ import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -Gst.init() - import mock import pykka diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index 6e3ba001..411ce805 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -3,12 +3,6 @@ from __future__ import absolute_import, unicode_literals import os import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - -Gst.init() - from mopidy import exceptions from mopidy.audio import scan from mopidy.internal import path as path_lib From 812e53b8953c6690d0f774580c6bc7eedb991dc3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 30 Nov 2015 15:29:32 +0100 Subject: [PATCH 101/142] gst1: Replace parse_new_segment() with parse_segment() Fixes 4 unit tests --- mopidy/audio/actor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index c5de90dc..811b1ae6 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -262,7 +262,7 @@ class _Handler(object): def on_pad_event(self, pad, pad_probe_info): event = pad_probe_info.get_event() if event.type == Gst.EventType.SEGMENT: - self.on_new_segment(event.parse_new_segment()) + self.on_segment(event.parse_segment()) return Gst.PadProbeReturn.OK def on_playbin_state_changed(self, old_state, new_state, pending_state): @@ -362,9 +362,9 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? - def on_new_segment(self, segment): + def on_segment(self, segment): gst_logger.debug( - 'Got new-segment event: ' + 'Got SEGMENT pad event: ' 'rate=%(rate)s format=%(format)s start=%(start)s stop=%(stop)s ' 'position=%(position)s', { 'rate': segment.rate, From 226c937ffc74e4cb2e83375afa6b0ed4b33bde3d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 00:04:10 +0100 Subject: [PATCH 102/142] gst1: Tune log messages --- mopidy/audio/actor.py | 88 ++++++++++++++++++++++++----------------- mopidy/core/playback.py | 2 +- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 811b1ae6..10073121 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -20,9 +20,9 @@ from mopidy.internal import deprecation, process logger = logging.getLogger(__name__) -# This logger is only meant for debug logging of low level gstreamer info such +# This logger is only meant for debug logging of low level GStreamer info such # as callbacks, event, messages and direct interaction with GStreamer such as -# set_state on a pipeline. +# set_state() on a pipeline. gst_logger = logging.getLogger('mopidy.audio.gst') _GST_STATE_MAPPING = { @@ -237,22 +237,26 @@ class _Handler(object): self._event_handler_id = None def on_message(self, bus, msg): - if ( - msg.type == Gst.MessageType.STATE_CHANGED and - msg.src == self._element): - self.on_playbin_state_changed(*msg.parse_state_changed()) + if msg.type == Gst.MessageType.STATE_CHANGED: + if msg.src != self._element: + return + old_state, new_state, pending_state = msg.parse_state_changed() + self.on_playbin_state_changed(old_state, new_state, pending_state) elif msg.type == Gst.MessageType.BUFFERING: self.on_buffering(msg.parse_buffering(), msg.get_structure()) elif msg.type == Gst.MessageType.EOS: self.on_end_of_stream() elif msg.type == Gst.MessageType.ERROR: - self.on_error(*msg.parse_error()) + error, debug = msg.parse_error() + self.on_error(error, debug) elif msg.type == Gst.MessageType.WARNING: - self.on_warning(*msg.parse_warning()) + error, debug = msg.parse_warning() + self.on_warning(error, debug) elif msg.type == Gst.MessageType.ASYNC_DONE: self.on_async_done() elif msg.type == Gst.MessageType.TAG: - self.on_tag(msg.parse_tag()) + taglist = msg.parse_tag() + self.on_tag(taglist) elif msg.type == Gst.MessageType.ELEMENT: if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) @@ -266,14 +270,16 @@ class _Handler(object): return Gst.PadProbeReturn.OK def on_playbin_state_changed(self, old_state, new_state, pending_state): - gst_logger.debug('Got state-changed message: old=%s new=%s pending=%s', - old_state.value_name, new_state.value_name, - pending_state.value_name) + gst_logger.debug( + 'Got STATE_CHANGED bus message: old=%s new=%s pending=%s', + old_state.value_name, new_state.value_name, + pending_state.value_name) if new_state == Gst.State.READY and pending_state == Gst.State.NULL: # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. + # TODO/Gst1: Is this workaround still needed? new_state = Gst.State.NULL pending_state = Gst.State.VOID_PENDING @@ -320,31 +326,37 @@ class _Handler(object): self._audio._playbin.set_state(Gst.State.PLAYING) level = logging.DEBUG - gst_logger.log(level, 'Got buffering message: percent=%d%%', percent) + gst_logger.log( + level, 'Got BUFFERING bus message: percent=%d%%', percent) def on_end_of_stream(self): - gst_logger.debug('Got end-of-stream message.') + gst_logger.debug('Got EOS (end of stream) bus message.') logger.debug('Audio event: reached_end_of_stream()') self._audio._tags = {} AudioListener.send('reached_end_of_stream') def on_error(self, error, debug): - gst_logger.error(str(error).decode('utf-8')) - if debug: - gst_logger.debug(debug.decode('utf-8')) + error_msg = str(error).decode('utf-8') + debug_msg = debug.decode('utf-8') + gst_logger.debug( + 'Got ERROR bus message: error=%r debug=%r', error_msg, debug_msg) + gst_logger.error('GStreamer error: %s', error_msg) # TODO: is this needed? self._audio.stop_playback() def on_warning(self, error, debug): - gst_logger.warning(str(error).decode('utf-8')) - if debug: - gst_logger.debug(debug.decode('utf-8')) + error_msg = str(error).decode('utf-8') + debug_msg = debug.decode('utf-8') + gst_logger.warning('GStreamer warning: %s', error_msg) + gst_logger.debug( + 'Got WARNING bus message: error=%r debug=%r', error_msg, debug_msg) def on_async_done(self): - gst_logger.debug('Got async-done.') + gst_logger.debug('Got ASYNC_DONE bus message.') def on_tag(self, taglist): tags = utils.convert_taglist(taglist) + gst_logger.debug('Got TAG bus message: tags=%r', dict(tags)) self._audio._tags.update(tags) logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) AudioListener.send('tags_changed', tags=tags.keys()) @@ -352,8 +364,8 @@ class _Handler(object): def on_missing_plugin(self, msg): desc = GstPbutils.missing_plugin_message_get_description(msg) debug = GstPbutils.missing_plugin_message_get_installer_detail(msg) - - gst_logger.debug('Got missing-plugin message: description:%s', desc) + gst_logger.debug( + 'Got missing-plugin bus message: description=%r', desc) logger.warning('Could not find a %s to handle media.', desc) if GstPbutils.install_plugins_supported(): logger.info('You might be able to fix this by running: ' @@ -362,6 +374,11 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? + def on_stream_changed(self, uri): + gst_logger.debug('Got STREAM_CHANGED bus message: uri=%r', uri) + logger.debug('Audio event: stream_changed(uri=%r)', uri) + AudioListener.send('stream_changed', uri=uri) + def on_segment(self, segment): gst_logger.debug( 'Got SEGMENT pad event: ' @@ -374,14 +391,9 @@ class _Handler(object): 'position': segment.position }) position_ms = segment.position // Gst.MSECOND - logger.debug('Audio event: position_changed(position=%s)', position_ms) + logger.debug('Audio event: position_changed(position=%r)', position_ms) AudioListener.send('position_changed', position=position_ms) - def on_stream_changed(self, uri): - gst_logger.debug('Got stream-changed message: uri=%s', uri) - logger.debug('Audio event: stream_changed(uri=%s)', uri) - AudioListener.send('stream_changed', uri=uri) - # TODO: create a player class which replaces the actors internals class Audio(pykka.ThreadingActor): @@ -478,7 +490,7 @@ class Audio(pykka.ThreadingActor): def _setup_audio_sink(self): audio_sink = Gst.ElementFactory.make('bin', 'audio-sink') - # Queue element to buy us time between the about to finish event and + # Queue element to buy us time between the about-to-finish event and # the actual switch, i.e. about to switch can block for longer thanks # to this queue. # TODO: make the min-max values a setting? @@ -517,11 +529,12 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('Got about-to-finish event.') if self._about_to_finish_callback: - logger.debug('Running about to finish callback.') + logger.debug('Running about-to-finish callback.') self._about_to_finish_callback() def _on_source_setup(self, element, source): - gst_logger.debug('Got source-setup: element=%s', source) + gst_logger.debug( + 'Got source-setup signal: element=%s', source.__class__.__name__) if source.get_factory().get_name() == 'appsrc': self._appsrc.configure(source) @@ -646,9 +659,9 @@ class Audio(pykka.ThreadingActor): """ # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) + gst_logger.debug('Sending flushing seek: position=%r', gst_position) result = self._playbin.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH, gst_position) - gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result def start_playback(self): @@ -729,8 +742,9 @@ class Audio(pykka.ThreadingActor): """ self._target_state = state result = self._playbin.set_state(state) - gst_logger.debug('State change to %s: result=%s', state.value_name, - result.value_name) + gst_logger.debug( + 'Changing state to %s: result=%s', state.value_name, + result.value_name) if result == Gst.StateChangeReturn.FAILURE: logger.warning( @@ -777,10 +791,12 @@ class Audio(pykka.ThreadingActor): if track.album and track.album.name: set_value(Gst.TAG_ALBUM, track.album.name) + gst_logger.debug( + 'Sending TAG event for track %r: %r', + track.uri, taglist.to_string()) event = Gst.event_new_tag(taglist) # TODO: check if we get this back on our own bus? self._playbin.send_event(event) - gst_logger.debug('Sent tag event: track=%s', track.uri) def get_current_tags(self): """ diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 7170969e..63259f7d 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -459,7 +459,7 @@ class PlaybackController(object): if time_position < 0: time_position = 0 elif time_position > tl_track.track.length: - # TODO: gstreamer will trigger a about to finish for us, use that? + # TODO: GStreamer will trigger a about-to-finish for us, use that? self.next() return True From 7a3d5ff13ce9928ab02f99dab6903435c51968d3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 00:05:00 +0100 Subject: [PATCH 103/142] gst1: Replace event_new_tag() with Event.new_tag() --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 10073121..40e32992 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -794,7 +794,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug( 'Sending TAG event for track %r: %r', track.uri, taglist.to_string()) - event = Gst.event_new_tag(taglist) + event = Gst.Event.new_tag(taglist) # TODO: check if we get this back on our own bus? self._playbin.send_event(event) From 780c493af36f1416ba7c89c335999938391fa0b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 00:13:56 +0100 Subject: [PATCH 104/142] gst1: Replace Structure(...) with Stricture.new_empty(...) --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index f4bbd3a0..fb0773d6 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -105,7 +105,7 @@ def _pad_added(element, pad, pipeline): pad.link(sink.get_static_pad('sink')) if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): - struct = Gst.Structure('have-audio') + struct = Gst.Structure.new_empty('have-audio') element.get_bus().post(Gst.Message.new_application(element, struct)) From 0ef3da5ed328f8657793ed25bd743ac723e4053a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 02:02:23 +0100 Subject: [PATCH 105/142] travis: Replace GStreamer 0.10 with 1.x --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 964ae89f..2acbf87e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: before_install: - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573 - "sudo apt-get update -qq" - - "sudo apt-get install -y graphviz-dev gstreamer0.10-plugins-good python-gst0.10" + - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good python-gst-1.0" install: - "pip install tox" From dd466ed89549dadf7a6f649c631f465618d83150 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 02:35:00 +0100 Subject: [PATCH 106/142] docs: Update GStreamer install docs --- docs/installation/source.rst | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index 204cc1df..d9994c6b 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -37,36 +37,33 @@ please follow the directions :ref:`here `. On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the following steps. -#. Then you'll need to install 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 +#. Then you'll need to install GStreamer 1.x (>= 1.2), 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:: - sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ - gstreamer0.10-plugins-ugly gstreamer0.10-tools + sudo apt-get install python-gst-1.0 gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-ugly gstreamer1.0-tools If you use Arch Linux, install the following packages from the official repository:: - sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \ - gstreamer0.10-ugly-plugins + sudo pacman -S gst-python gst-plugins-good gst-plugins-ugly If you use Fedora you can install GStreamer like this:: - sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \ - gstreamer0.10-plugins-ugly gstreamer0.10-tools + # TODO Update to GStreamer 1 + sudo yum install -y python-gstreamer1 gstreamer1-plugins-good \ + gstreamer1-plugins-ugly - If you use Gentoo you need to be careful because GStreamer 0.10 is in a - different lower slot than 1.0, the default. Your emerge commands will need - to include the slot:: + If you use Gentoo you can install GStreamer like this:: - emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \ - gst-plugins-ugly:0.10 gst-plugins-meta:0.10 + emerge -av gst-python gst-plugins-good gst-plugins-ugly gst-plugins-meta - ``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you - want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc. + ``gst-plugins-meta`` is the one that actually pulls in the plugins you want, + so pay attention to the USE flags, e.g. ``alsa``, ``mp3``, etc. #. Install the latest release of Mopidy:: From efbfb39e868d1782ee1a7ce74a127c0bca22405b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 02:42:46 +0100 Subject: [PATCH 107/142] docs: Update changelog --- docs/changelog.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 22d80ad3..b8d0ee02 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,12 @@ v1.2.0 (UNRELEASED) Feature release. +Dependencies +------------ + +- Mopidy now requires GStreamer 1.x, as we've finally ported from GStreamer + 0.10. + Core API -------- @@ -123,6 +129,14 @@ Cleanups - Catch errors when loading :confval:`logging/config_file`. (Fixes: :issue:`1320`) +Audio +----- + +- **Breaking:** The audio scanner now returns ISO-8601 formatted strings + instead of :class:`~datetime.datetime` objects for dates found in tags. + Because of this change, we can now return years without months or days, which + matches the semantics of the date fields in our data models. + Gapless ------- From 45dae063474950aadc54b450a6a669056907ca02 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 01:11:15 +0100 Subject: [PATCH 108/142] gst1: Keep the pending URI for the stream_changed event --- mopidy/audio/actor.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 40e32992..aeace16d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -261,7 +261,7 @@ class _Handler(object): if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) elif msg.type == Gst.MessageType.STREAM_START: - self.on_stream_changed(self._audio._playbin.get_property('uri')) + self.on_stream_start() def on_pad_event(self, pad, pad_probe_info): event = pad_probe_info.get_event() @@ -374,8 +374,9 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? - def on_stream_changed(self, uri): - gst_logger.debug('Got STREAM_CHANGED bus message: uri=%r', uri) + def on_stream_start(self): + gst_logger.debug('Got STREAM_START bus message') + uri = self._audio._pending_uri logger.debug('Audio event: stream_changed(uri=%r)', uri) AudioListener.send('stream_changed', uri=uri) @@ -415,6 +416,7 @@ class Audio(pykka.ThreadingActor): self._target_state = Gst.State.NULL self._buffering = False self._tags = {} + self._pending_uri = None self._playbin = None self._outputs = None @@ -561,6 +563,7 @@ class Audio(pykka.ThreadingActor): current_volume = None self._tags = {} # TODO: add test for this somehow + self._pending_uri = uri self._playbin.set_property('uri', uri) if self.mixer is not None and current_volume is not None: @@ -586,7 +589,9 @@ class Audio(pykka.ThreadingActor): """ self._appsrc.prepare( Gst.Caps.from_string(caps), need_data, enough_data, seek_data) - self._playbin.set_property('uri', 'appsrc://') + uri = 'appsrc://' + self._pending_uri = uri + self._playbin.set_property('uri', uri) def emit_data(self, buffer_): """ From ef5281488b41485c01bbd588070bd81c376e9f14 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 13:51:13 +0100 Subject: [PATCH 109/142] gst1: Fix buffer.pts not being set if 0 --- mopidy/audio/utils.py | 4 ++-- tests/audio/test_utils.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index cc312b73..0f7f1957 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -29,9 +29,9 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): ``capabilites`` argument is no longer in use """ buffer_ = Gst.Buffer.new_wrapped(data) - if timestamp: + if timestamp is not None: buffer_.pts = timestamp - if duration: + if duration is not None: buffer_.duration = duration return buffer_ diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index 0b497dad..d3e81ef2 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -3,10 +3,27 @@ from __future__ import absolute_import, unicode_literals import datetime import unittest +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst + +import pytest + from mopidy.audio import utils from mopidy.models import Album, Artist, Track +class TestCreateBuffer(object): + + def test_creates_buffer(self): + buf = utils.create_buffer(b'123', timestamp=0, duration=1000000) + + assert isinstance(buf, Gst.Buffer) + assert buf.pts == 0 + assert buf.duration == 1000000 + assert buf.get_size() == len(b'123') + + # TODO: keep ids without name? # TODO: current test is trying to test everything at once with a complete tags # set, instead we might want to try with a minimal one making testing easier. From 7926ef1f127e89a9923545da665b8829087894c7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 13:52:05 +0100 Subject: [PATCH 110/142] gst1: Fail if trying to create buffers without audio Which causes lots of failed assertion messages from GStreamer --- mopidy/audio/utils.py | 3 +++ tests/audio/test_utils.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 0f7f1957..a8627001 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -28,6 +28,9 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): .. versionchanged:: 1.2 ``capabilites`` argument is no longer in use """ + if not data: + raise ValueError( + 'Cannot create buffer without data: length=%d' % len(data)) buffer_ = Gst.Buffer.new_wrapped(data) if timestamp is not None: buffer_.pts = timestamp diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index d3e81ef2..e10613d2 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -23,6 +23,12 @@ class TestCreateBuffer(object): assert buf.duration == 1000000 assert buf.get_size() == len(b'123') + def test_fails_if_data_has_zero_length(self): + with pytest.raises(ValueError) as excinfo: + utils.create_buffer(b'', timestamp=0, duration=1000000) + + assert 'Cannot create buffer without data' in str(excinfo.value) + # TODO: keep ids without name? # TODO: current test is trying to test everything at once with a complete tags From bf6e97e5b9a45b023f24e0b6e04673cd71ddaebe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Dec 2015 22:22:56 +0100 Subject: [PATCH 111/142] gst1: Fix querying of duration of MP3s --- mopidy/audio/scan.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fb0773d6..188eb26c 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -115,13 +115,23 @@ def _start_pipeline(pipeline): pipeline.set_state(Gst.State.PLAYING) -def _query_duration(pipeline): +def _query_duration(pipeline, timeout=100): success, duration = pipeline.query_duration(Gst.Format.TIME) + if success and duration >= 0: + return duration // Gst.MSECOND - if not success or duration < 0: + result = pipeline.set_state(Gst.State.PLAYING) + if result == Gst.StateChangeReturn.FAILURE: return None - return duration // Gst.MSECOND + gst_timeout = timeout * Gst.MSECOND + bus = pipeline.get_bus() + bus.timed_pop_filtered(gst_timeout, Gst.MessageType.DURATION_CHANGED) + + success, duration = pipeline.query_duration(Gst.Format.TIME) + if success and duration >= 0: + return duration // Gst.MSECOND + return None def _query_seekable(pipeline): From 844dc257df5107c77253a7158ab60924c976d2e2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 17 Dec 2015 22:02:56 +0100 Subject: [PATCH 112/142] audio: Don't bother creating decoders in audio scanner The decoders don't produce metadata and to the best of my knowledge we don't need the raw audio for duration calculation. But to play it safe this keeps in place the caps check in pad added to trigger 'have-audio'. --- mopidy/audio/scan.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 188eb26c..c99d86ef 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -13,6 +13,17 @@ from mopidy import exceptions from mopidy.audio import utils from mopidy.internal import encoding +# GST_ELEMENT_FACTORY_LIST: +_DECODER = 1 << 0 +_AUDIO = 1 << 50 +_DEMUXER = 1 << 5 +_DEPAYLOADER = 1 << 8 +_PARSER = 1 << 6 + +# GST_TYPE_AUTOPLUG_SELECT_RESULT: +_SELECT_TRY = 0 +_SELECT_EXPOSE = 1 + _Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) @@ -85,6 +96,7 @@ def _setup_pipeline(uri, proxy_config=None): typefind.connect('have-type', _have_type, decodebin) decodebin.connect('pad-added', _pad_added, pipeline) + decodebin.connect('autoplug-select', _autoplug_select) return pipeline @@ -105,10 +117,21 @@ def _pad_added(element, pad, pipeline): pad.link(sink.get_static_pad('sink')) if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): + # Probably won't happen due to autoplug-select fix, but lets play it + # safe until we've tested more. struct = Gst.Structure.new_empty('have-audio') element.get_bus().post(Gst.Message.new_application(element, struct)) +def _autoplug_select(element, pad, caps, factory): + if factory.list_is_type(_DECODER | _AUDIO): + struct = Gst.Structure.new_empty('have-audio') + element.get_bus().post(Gst.Message.new_application(element, struct)) + if not factory.list_is_type(_DEMUXER | _DEPAYLOADER | _PARSER): + return _SELECT_EXPOSE + return _SELECT_TRY + + def _start_pipeline(pipeline): result = pipeline.set_state(Gst.State.PAUSED) if result == Gst.StateChangeReturn.NO_PREROLL: From b3aeb9b50838f325ac8df5e19eb97a10b75be7d1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 17 Dec 2015 22:10:48 +0100 Subject: [PATCH 113/142] audio: Move signal helper to utils. --- mopidy/audio/actor.py | 36 +++--------------------------------- mopidy/audio/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index aeace16d..193d825e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -32,43 +32,13 @@ _GST_STATE_MAPPING = { } -class _Signals(object): - - """Helper for tracking gobject signal registrations""" - - def __init__(self): - self._ids = {} - - def connect(self, element, event, func, *args): - """Connect a function + args to signal event on an element. - - Each event may only be handled by one callback in this implementation. - """ - assert (element, event) not in self._ids - self._ids[(element, event)] = element.connect(event, func, *args) - - def disconnect(self, element, event): - """Disconnect whatever handler we have for and element+event pair. - - Does nothing it the handler has already been removed. - """ - signal_id = self._ids.pop((element, event), None) - if signal_id is not None: - element.disconnect(signal_id) - - def clear(self): - """Clear all registered signal handlers.""" - for element, event in self._ids.keys(): - element.disconnect(self._ids.pop((element, event))) - - # TODO: expose this as a property on audio? class _Appsrc(object): """Helper class for dealing with appsrc based playback.""" def __init__(self): - self._signals = _Signals() + self._signals = utils.Signals() self.reset() def reset(self): @@ -181,7 +151,7 @@ class SoftwareMixer(object): self._element = None self._last_volume = None self._last_mute = None - self._signals = _Signals() + self._signals = utils.Signals() def setup(self, element, mixer_ref): self._element = element @@ -424,7 +394,7 @@ class Audio(pykka.ThreadingActor): self._handler = _Handler(self) self._appsrc = _Appsrc() - self._signals = _Signals() + self._signals = utils.Signals() if mixer and self._config['audio']['mixer'] == 'software': self.mixer = SoftwareMixer(mixer) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index a8627001..6c38c058 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -193,3 +193,33 @@ gstreamer-GstTagList.html logger.debug('Ignoring unknown tag data: %r = %r', tag, value) return result + + +class Signals(object): + + """Helper for tracking gobject signal registrations""" + + def __init__(self): + self._ids = {} + + def connect(self, element, event, func, *args): + """Connect a function + args to signal event on an element. + + Each event may only be handled by one callback in this implementation. + """ + assert (element, event) not in self._ids + self._ids[(element, event)] = element.connect(event, func, *args) + + def disconnect(self, element, event): + """Disconnect whatever handler we have for and element+event pair. + + Does nothing it the handler has already been removed. + """ + signal_id = self._ids.pop((element, event), None) + if signal_id is not None: + element.disconnect(signal_id) + + def clear(self): + """Clear all registered signal handlers.""" + for element, event in self._ids.keys(): + element.disconnect(self._ids.pop((element, event))) From ded059b5c97d60e4e59b3c5e3f50279812ea968b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 17 Dec 2015 22:14:39 +0100 Subject: [PATCH 114/142] audio: Cleanup the signals we connect in the scanner Without this fix we simply crash due to using up all the available FDs on the system. --- mopidy/audio/scan.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c99d86ef..f7d8fd67 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -61,7 +61,7 @@ class Scanner(object): """ timeout = int(timeout or self._timeout_ms) tags, duration, seekable, mime = None, None, None, None - pipeline = _setup_pipeline(uri, self._proxy_config) + pipeline, signals = _setup_pipeline(uri, self._proxy_config) try: _start_pipeline(pipeline) @@ -69,6 +69,7 @@ class Scanner(object): duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: + signals.clear() pipeline.set_state(Gst.State.NULL) del pipeline @@ -94,11 +95,12 @@ def _setup_pipeline(uri, proxy_config=None): if proxy_config: utils.setup_proxy(src, proxy_config) - typefind.connect('have-type', _have_type, decodebin) - decodebin.connect('pad-added', _pad_added, pipeline) - decodebin.connect('autoplug-select', _autoplug_select) + signals = utils.Signals() + signals.connect(typefind, 'have-type', _have_type, decodebin) + signals.connect(decodebin, 'pad-added', _pad_added, pipeline) + signals.connect(decodebin, 'autoplug-select', _autoplug_select) - return pipeline + return pipeline, signals def _have_type(element, probability, caps, decodebin): From f0c7d25db6b9cf844561c4351ef07ec976a7ff15 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 17 Dec 2015 22:47:26 +0100 Subject: [PATCH 115/142] audio: Reduce log level for unknown tag data --- mopidy/audio/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 6c38c058..989fac4b 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -12,6 +12,7 @@ from mopidy import compat, httpclient from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) +TRACE = logging.getLevelName('TRACE') def calculate_duration(num_samples, sample_rate): @@ -190,7 +191,8 @@ gstreamer-GstTagList.html if isinstance(value, (compat.string_types, bool, numbers.Number)): result[tag].append(value) else: - logger.debug('Ignoring unknown tag data: %r = %r', tag, value) + logger.log( + TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) return result From 31c894030d01a47255f727f79a3f0735df025f4c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 17 Dec 2015 23:57:03 +0100 Subject: [PATCH 116/142] audio: Move tag helpers to mopidy.audio.tags --- mopidy/audio/actor.py | 4 +- mopidy/audio/scan.py | 4 +- mopidy/audio/tags.py | 132 +++++++++++++++++++ mopidy/audio/utils.py | 123 +----------------- mopidy/file/library.py | 4 +- mopidy/local/commands.py | 12 +- mopidy/stream/actor.py | 4 +- tests/audio/test_tags.py | 261 ++++++++++++++++++++++++++++++++++++++ tests/audio/test_utils.py | 258 ------------------------------------- 9 files changed, 408 insertions(+), 394 deletions(-) create mode 100644 mopidy/audio/tags.py create mode 100644 tests/audio/test_tags.py diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 193d825e..ca25f4dd 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -12,7 +12,7 @@ Gst.is_initialized() or Gst.init() import pykka from mopidy import exceptions -from mopidy.audio import utils +from mopidy.audio import tags as tags_lib, utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.internal import deprecation, process @@ -325,7 +325,7 @@ class _Handler(object): gst_logger.debug('Got ASYNC_DONE bus message.') def on_tag(self, taglist): - tags = utils.convert_taglist(taglist) + tags = tags_lib.convert_taglist(taglist) gst_logger.debug('Got TAG bus message: tags=%r', dict(tags)) self._audio._tags.update(tags) logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index f7d8fd67..ed1c6424 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -10,7 +10,7 @@ from gi.repository import Gst, GstPbutils Gst.is_initialized() or Gst.init() from mopidy import exceptions -from mopidy.audio import utils +from mopidy.audio import tags as tags_lib, utils from mopidy.internal import encoding # GST_ELEMENT_FACTORY_LIST: @@ -214,7 +214,7 @@ def _process(pipeline, timeout_ms): elif message.type == Gst.MessageType.TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. - tags.update(utils.convert_taglist(taglist)) + tags.update(tags_lib.convert_taglist(taglist)) now = int(time.time() * 1000) timeout -= now - previous diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py new file mode 100644 index 00000000..ba2b021a --- /dev/null +++ b/mopidy/audio/tags.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import, unicode_literals + +import collections +import logging +import numbers + +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst +Gst.is_initialized() or Gst.init() + +from mopidy import compat +from mopidy.models import Album, Artist, Track + + +logger = logging.getLogger(__name__) +TRACE = logging.getLevelName('TRACE') + + +def convert_taglist(taglist): + """Convert a :class:`Gst.TagList` to plain Python types. + + Knows how to convert: + + - Dates + - Buffers + - Numbers + - Strings + - Booleans + + Unknown types will be ignored and trace logged. Tag keys are all strings + defined as part GStreamer under GstTagList_. + + .. _GstTagList: https://developer.gnome.org/gstreamer/stable/\ +gstreamer-GstTagList.html + + :param taglist: A GStreamer taglist to be converted. + :type taglist: :class:`Gst.TagList` + :rtype: dictionary of tag keys with a list of values. + """ + result = collections.defaultdict(list) + + for n in range(taglist.n_tags()): + tag = taglist.nth_tag_name(n) + + for i in range(taglist.get_tag_size(tag)): + value = taglist.get_value_index(tag, i) + + if isinstance(value, Gst.DateTime): + result[tag].append(value.to_iso8601_string()) + if isinstance(value, (compat.string_types, bool, numbers.Number)): + result[tag].append(value) + else: + logger.log( + TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) + + return result + + +# TODO: split based on "stream" and "track" based conversion? i.e. handle data +# from radios in it's own helper instead? +def convert_tags_to_track(tags): + """Convert our normalized tags to a track. + + :param tags: dictionary of tag keys with a list of values + :type tags: :class:`dict` + :rtype: :class:`mopidy.models.Track` + """ + album_kwargs = {} + track_kwargs = {} + + track_kwargs['composers'] = _artists(tags, Gst.TAG_COMPOSER) + track_kwargs['performers'] = _artists(tags, Gst.TAG_PERFORMER) + track_kwargs['artists'] = _artists(tags, Gst.TAG_ARTIST, + 'musicbrainz-artistid', + 'musicbrainz-sortname') + album_kwargs['artists'] = _artists( + tags, Gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') + + track_kwargs['genre'] = '; '.join(tags.get(Gst.TAG_GENRE, [])) + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_TITLE, [])) + if not track_kwargs['name']: + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_ORGANIZATION, [])) + + track_kwargs['comment'] = '; '.join(tags.get('comment', [])) + if not track_kwargs['comment']: + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_LOCATION, [])) + if not track_kwargs['comment']: + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_COPYRIGHT, [])) + + track_kwargs['track_no'] = tags.get(Gst.TAG_TRACK_NUMBER, [None])[0] + track_kwargs['disc_no'] = tags.get(Gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] + track_kwargs['bitrate'] = tags.get(Gst.TAG_BITRATE, [None])[0] + track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0] + + album_kwargs['name'] = tags.get(Gst.TAG_ALBUM, [None])[0] + album_kwargs['num_tracks'] = tags.get(Gst.TAG_TRACK_COUNT, [None])[0] + album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] + album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] + + if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: + track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() + + # Clear out any empty values we found + track_kwargs = {k: v for k, v in track_kwargs.items() if v} + album_kwargs = {k: v for k, v in album_kwargs.items() if v} + + # Only bother with album if we have a name to show. + if album_kwargs.get('name'): + track_kwargs['album'] = Album(**album_kwargs) + + return Track(**track_kwargs) + + +def _artists( + tags, artist_name, artist_id=None, artist_sortname=None): + + # Name missing, don't set artist + if not tags.get(artist_name): + return None + # One artist name and either id or sortname, include all available fields + if len(tags[artist_name]) == 1 and \ + (artist_id in tags or artist_sortname in tags): + attrs = {'name': tags[artist_name][0]} + if artist_id in tags: + attrs['musicbrainz_id'] = tags[artist_id][0] + if artist_sortname in tags: + attrs['sortname'] = tags[artist_sortname][0] + return [Artist(**attrs)] + + # Multiple artist, provide artists with name only to avoid ambiguity. + return [Artist(name=name) for name in tags[artist_name]] diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 989fac4b..6a11c7a3 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -1,18 +1,10 @@ from __future__ import absolute_import, unicode_literals -import collections -import logging -import numbers - import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -from mopidy import compat, httpclient -from mopidy.models import Album, Artist, Track - -logger = logging.getLogger(__name__) -TRACE = logging.getLevelName('TRACE') +from mopidy import httpclient def calculate_duration(num_samples, sample_rate): @@ -68,79 +60,6 @@ def supported_uri_schemes(uri_schemes): return supported_schemes -def _artists(tags, artist_name, artist_id=None, artist_sortname=None): - # Name missing, don't set artist - if not tags.get(artist_name): - return None - # One artist name and either id or sortname, include all available fields - if len(tags[artist_name]) == 1 and \ - (artist_id in tags or artist_sortname in tags): - attrs = {'name': tags[artist_name][0]} - if artist_id in tags: - attrs['musicbrainz_id'] = tags[artist_id][0] - if artist_sortname in tags: - attrs['sortname'] = tags[artist_sortname][0] - return [Artist(**attrs)] - - # Multiple artist, provide artists with name only to avoid ambiguity. - return [Artist(name=name) for name in tags[artist_name]] - - -# TODO: split based on "stream" and "track" based conversion? i.e. handle data -# from radios in it's own helper instead? -def convert_tags_to_track(tags): - """Convert our normalized tags to a track. - - :param tags: dictionary of tag keys with a list of values - :type tags: :class:`dict` - :rtype: :class:`mopidy.models.Track` - """ - album_kwargs = {} - track_kwargs = {} - - track_kwargs['composers'] = _artists(tags, Gst.TAG_COMPOSER) - track_kwargs['performers'] = _artists(tags, Gst.TAG_PERFORMER) - track_kwargs['artists'] = _artists(tags, Gst.TAG_ARTIST, - 'musicbrainz-artistid', - 'musicbrainz-sortname') - album_kwargs['artists'] = _artists( - tags, Gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') - - track_kwargs['genre'] = '; '.join(tags.get(Gst.TAG_GENRE, [])) - track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_TITLE, [])) - if not track_kwargs['name']: - track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_ORGANIZATION, [])) - - track_kwargs['comment'] = '; '.join(tags.get('comment', [])) - if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_LOCATION, [])) - if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_COPYRIGHT, [])) - - track_kwargs['track_no'] = tags.get(Gst.TAG_TRACK_NUMBER, [None])[0] - track_kwargs['disc_no'] = tags.get(Gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] - track_kwargs['bitrate'] = tags.get(Gst.TAG_BITRATE, [None])[0] - track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0] - - album_kwargs['name'] = tags.get(Gst.TAG_ALBUM, [None])[0] - album_kwargs['num_tracks'] = tags.get(Gst.TAG_TRACK_COUNT, [None])[0] - album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] - album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - - if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: - track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() - - # Clear out any empty values we found - track_kwargs = {k: v for k, v in track_kwargs.items() if v} - album_kwargs = {k: v for k, v in album_kwargs.items() if v} - - # Only bother with album if we have a name to show. - if album_kwargs.get('name'): - track_kwargs['album'] = Album(**album_kwargs) - - return Track(**track_kwargs) - - def setup_proxy(element, config): """Configure a GStreamer element with proxy settings. @@ -157,46 +76,6 @@ def setup_proxy(element, config): element.set_property('proxy-pw', config.get('password')) -def convert_taglist(taglist): - """Convert a :class:`Gst.TagList` to plain Python types. - - Knows how to convert: - - - Dates - - Buffers - - Numbers - - Strings - - Booleans - - Unknown types will be ignored and debug logged. Tag keys are all strings - defined as part GStreamer under GstTagList_. - - .. _GstTagList: https://developer.gnome.org/gstreamer/stable/\ -gstreamer-GstTagList.html - - :param taglist: A GStreamer taglist to be converted. - :type taglist: :class:`Gst.TagList` - :rtype: dictionary of tag keys with a list of values. - """ - result = collections.defaultdict(list) - - for n in range(taglist.n_tags()): - tag = taglist.nth_tag_name(n) - - for i in range(taglist.get_tag_size(tag)): - value = taglist.get_value_index(tag, i) - - if isinstance(value, Gst.DateTime): - result[tag].append(value.to_iso8601_string()) - if isinstance(value, (compat.string_types, bool, numbers.Number)): - result[tag].append(value) - else: - logger.log( - TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) - - return result - - class Signals(object): """Helper for tracking gobject signal registrations""" diff --git a/mopidy/file/library.py b/mopidy/file/library.py index 20ac0632..09fa2cf1 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -7,7 +7,7 @@ import sys import urllib2 from mopidy import backend, exceptions, models -from mopidy.audio import scan, utils +from mopidy.audio import scan, tags from mopidy.internal import path @@ -83,7 +83,7 @@ class FileLibraryProvider(backend.LibraryProvider): try: result = self._scanner.scan(uri) - track = utils.convert_tags_to_track(result.tags).copy( + track = tags.convert_tags_to_track(result.tags).copy( uri=uri, length=result.duration) except exceptions.ScannerError as e: logger.warning('Failed looking up %s: %s', uri, e) diff --git a/mopidy/local/commands.py b/mopidy/local/commands.py index d61cf441..ead874a0 100644 --- a/mopidy/local/commands.py +++ b/mopidy/local/commands.py @@ -6,7 +6,7 @@ import os import time from mopidy import commands, compat, exceptions -from mopidy.audio import scan, utils +from mopidy.audio import scan, tags from mopidy.internal import path from mopidy.local import translator @@ -140,18 +140,18 @@ class ScanCommand(commands.Command): relpath = translator.local_track_uri_to_path(uri, media_dir) file_uri = path.path_to_uri(os.path.join(media_dir, relpath)) result = scanner.scan(file_uri) - tags, duration = result.tags, result.duration if not result.playable: logger.warning('Failed %s: No audio found in file.', uri) - elif duration < MIN_DURATION_MS: + elif result.duration < MIN_DURATION_MS: logger.warning('Failed %s: Track shorter than %dms', uri, MIN_DURATION_MS) else: mtime = file_mtimes.get(os.path.join(media_dir, relpath)) - track = utils.convert_tags_to_track(tags).replace( - uri=uri, length=duration, last_modified=mtime) + track = tags.convert_tags_to_track(result.tags).replace( + uri=uri, length=result.duration, last_modified=mtime) if library.add_supports_tags_and_duration: - library.add(track, tags=tags, duration=duration) + library.add( + track, tags=result.tags, duration=result.duration) else: library.add(track) logger.debug('Added %s', track.uri) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index 5f88b13b..c2e39652 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -8,7 +8,7 @@ import time import pykka from mopidy import audio as audio_lib, backend, exceptions, stream -from mopidy.audio import scan, utils +from mopidy.audio import scan, tags from mopidy.compat import urllib from mopidy.internal import http, playlists from mopidy.models import Track @@ -60,7 +60,7 @@ class StreamLibraryProvider(backend.LibraryProvider): try: result = self._scanner.scan(uri) - track = utils.convert_tags_to_track(result.tags).replace( + track = tags.convert_tags_to_track(result.tags).replace( uri=uri, length=result.duration) except exceptions.ScannerError as e: logger.warning('Problem looking up %s: %s', uri, e) diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py new file mode 100644 index 00000000..355af68e --- /dev/null +++ b/tests/audio/test_tags.py @@ -0,0 +1,261 @@ +from __future__ import absolute_import, unicode_literals + +import datetime +import unittest + +from mopidy.audio import tags +from mopidy.models import Album, Artist, Track + + +# TODO: keep ids without name? +# TODO: current test is trying to test everything at once with a complete tags +# set, instead we might want to try with a minimal one making testing easier. +class TagsToTrackTest(unittest.TestCase): + + def setUp(self): # noqa: N802 + self.tags = { + 'album': ['album'], + 'track-number': [1], + 'artist': ['artist'], + 'composer': ['composer'], + 'performer': ['performer'], + 'album-artist': ['albumartist'], + 'title': ['track'], + 'track-count': [2], + 'album-disc-number': [2], + 'album-disc-count': [3], + 'date': [datetime.date(2006, 1, 1,)], + 'container-format': ['ID3 tag'], + 'genre': ['genre'], + 'comment': ['comment'], + 'musicbrainz-trackid': ['trackid'], + 'musicbrainz-albumid': ['albumid'], + 'musicbrainz-artistid': ['artistid'], + 'musicbrainz-sortname': ['sortname'], + 'musicbrainz-albumartistid': ['albumartistid'], + 'bitrate': [1000], + } + + artist = Artist(name='artist', musicbrainz_id='artistid', + sortname='sortname') + composer = Artist(name='composer') + performer = Artist(name='performer') + albumartist = Artist(name='albumartist', + musicbrainz_id='albumartistid') + + album = Album(name='album', num_tracks=2, num_discs=3, + musicbrainz_id='albumid', artists=[albumartist]) + + self.track = Track(name='track', date='2006-01-01', + genre='genre', track_no=1, disc_no=2, + comment='comment', musicbrainz_id='trackid', + album=album, bitrate=1000, artists=[artist], + composers=[composer], performers=[performer]) + + def check(self, expected): + actual = tags.convert_tags_to_track(self.tags) + self.assertEqual(expected, actual) + + def test_track(self): + self.check(self.track) + + def test_missing_track_no(self): + del self.tags['track-number'] + self.check(self.track.replace(track_no=None)) + + def test_multiple_track_no(self): + self.tags['track-number'].append(9) + self.check(self.track) + + def test_missing_track_disc_no(self): + del self.tags['album-disc-number'] + self.check(self.track.replace(disc_no=None)) + + def test_multiple_track_disc_no(self): + self.tags['album-disc-number'].append(9) + self.check(self.track) + + def test_missing_track_name(self): + del self.tags['title'] + self.check(self.track.replace(name=None)) + + def test_multiple_track_name(self): + self.tags['title'] = ['name1', 'name2'] + self.check(self.track.replace(name='name1; name2')) + + def test_missing_track_musicbrainz_id(self): + del self.tags['musicbrainz-trackid'] + self.check(self.track.replace(musicbrainz_id=None)) + + def test_multiple_track_musicbrainz_id(self): + self.tags['musicbrainz-trackid'].append('id') + self.check(self.track) + + def test_missing_track_bitrate(self): + del self.tags['bitrate'] + self.check(self.track.replace(bitrate=None)) + + def test_multiple_track_bitrate(self): + self.tags['bitrate'].append(1234) + self.check(self.track) + + def test_missing_track_genre(self): + del self.tags['genre'] + self.check(self.track.replace(genre=None)) + + def test_multiple_track_genre(self): + self.tags['genre'] = ['genre1', 'genre2'] + self.check(self.track.replace(genre='genre1; genre2')) + + def test_missing_track_date(self): + del self.tags['date'] + self.check(self.track.replace(date=None)) + + def test_multiple_track_date(self): + self.tags['date'].append(datetime.date(2030, 1, 1)) + self.check(self.track) + + def test_missing_track_comment(self): + del self.tags['comment'] + self.check(self.track.replace(comment=None)) + + def test_multiple_track_comment(self): + self.tags['comment'] = ['comment1', 'comment2'] + self.check(self.track.replace(comment='comment1; comment2')) + + def test_missing_track_artist_name(self): + del self.tags['artist'] + self.check(self.track.replace(artists=[])) + + def test_multiple_track_artist_name(self): + self.tags['artist'] = ['name1', 'name2'] + artists = [Artist(name='name1'), Artist(name='name2')] + self.check(self.track.replace(artists=artists)) + + def test_missing_track_artist_musicbrainz_id(self): + del self.tags['musicbrainz-artistid'] + artist = list(self.track.artists)[0].replace(musicbrainz_id=None) + self.check(self.track.replace(artists=[artist])) + + def test_multiple_track_artist_musicbrainz_id(self): + self.tags['musicbrainz-artistid'].append('id') + self.check(self.track) + + def test_missing_track_composer_name(self): + del self.tags['composer'] + self.check(self.track.replace(composers=[])) + + def test_multiple_track_composer_name(self): + self.tags['composer'] = ['composer1', 'composer2'] + composers = [Artist(name='composer1'), Artist(name='composer2')] + self.check(self.track.replace(composers=composers)) + + def test_missing_track_performer_name(self): + del self.tags['performer'] + self.check(self.track.replace(performers=[])) + + def test_multiple_track_performe_name(self): + self.tags['performer'] = ['performer1', 'performer2'] + performers = [Artist(name='performer1'), Artist(name='performer2')] + self.check(self.track.replace(performers=performers)) + + def test_missing_album_name(self): + del self.tags['album'] + self.check(self.track.replace(album=None)) + + def test_multiple_album_name(self): + self.tags['album'].append('album2') + self.check(self.track) + + def test_missing_album_musicbrainz_id(self): + del self.tags['musicbrainz-albumid'] + album = self.track.album.replace(musicbrainz_id=None, + images=[]) + self.check(self.track.replace(album=album)) + + def test_multiple_album_musicbrainz_id(self): + self.tags['musicbrainz-albumid'].append('id') + self.check(self.track) + + def test_missing_album_num_tracks(self): + del self.tags['track-count'] + album = self.track.album.replace(num_tracks=None) + self.check(self.track.replace(album=album)) + + def test_multiple_album_num_tracks(self): + self.tags['track-count'].append(9) + self.check(self.track) + + def test_missing_album_num_discs(self): + del self.tags['album-disc-count'] + album = self.track.album.replace(num_discs=None) + self.check(self.track.replace(album=album)) + + def test_multiple_album_num_discs(self): + self.tags['album-disc-count'].append(9) + self.check(self.track) + + def test_missing_album_artist_name(self): + del self.tags['album-artist'] + album = self.track.album.replace(artists=[]) + self.check(self.track.replace(album=album)) + + def test_multiple_album_artist_name(self): + self.tags['album-artist'] = ['name1', 'name2'] + artists = [Artist(name='name1'), Artist(name='name2')] + album = self.track.album.replace(artists=artists) + self.check(self.track.replace(album=album)) + + def test_missing_album_artist_musicbrainz_id(self): + del self.tags['musicbrainz-albumartistid'] + albumartist = list(self.track.album.artists)[0] + albumartist = albumartist.replace(musicbrainz_id=None) + album = self.track.album.replace(artists=[albumartist]) + self.check(self.track.replace(album=album)) + + def test_multiple_album_artist_musicbrainz_id(self): + self.tags['musicbrainz-albumartistid'].append('id') + self.check(self.track) + + def test_stream_organization_track_name(self): + del self.tags['title'] + self.tags['organization'] = ['organization'] + self.check(self.track.replace(name='organization')) + + def test_multiple_organization_track_name(self): + del self.tags['title'] + self.tags['organization'] = ['organization1', 'organization2'] + self.check(self.track.replace(name='organization1; organization2')) + + # TODO: combine all comment types? + def test_stream_location_track_comment(self): + del self.tags['comment'] + self.tags['location'] = ['location'] + self.check(self.track.replace(comment='location')) + + def test_multiple_location_track_comment(self): + del self.tags['comment'] + self.tags['location'] = ['location1', 'location2'] + self.check(self.track.replace(comment='location1; location2')) + + def test_stream_copyright_track_comment(self): + del self.tags['comment'] + self.tags['copyright'] = ['copyright'] + self.check(self.track.replace(comment='copyright')) + + def test_multiple_copyright_track_comment(self): + del self.tags['comment'] + self.tags['copyright'] = ['copyright1', 'copyright2'] + self.check(self.track.replace(comment='copyright1; copyright2')) + + def test_sortname(self): + self.tags['musicbrainz-sortname'] = ['another_sortname'] + artist = Artist(name='artist', sortname='another_sortname', + musicbrainz_id='artistid') + self.check(self.track.replace(artists=[artist])) + + def test_missing_sortname(self): + del self.tags['musicbrainz-sortname'] + artist = Artist(name='artist', sortname=None, + musicbrainz_id='artistid') + self.check(self.track.replace(artists=[artist])) diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index e10613d2..0ce15bcb 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -1,8 +1,5 @@ from __future__ import absolute_import, unicode_literals -import datetime -import unittest - import gi gi.require_version('Gst', '1.0') from gi.repository import Gst @@ -10,7 +7,6 @@ from gi.repository import Gst import pytest from mopidy.audio import utils -from mopidy.models import Album, Artist, Track class TestCreateBuffer(object): @@ -28,257 +24,3 @@ class TestCreateBuffer(object): utils.create_buffer(b'', timestamp=0, duration=1000000) assert 'Cannot create buffer without data' in str(excinfo.value) - - -# TODO: keep ids without name? -# TODO: current test is trying to test everything at once with a complete tags -# set, instead we might want to try with a minimal one making testing easier. -class TagsToTrackTest(unittest.TestCase): - - def setUp(self): # noqa: N802 - self.tags = { - 'album': ['album'], - 'track-number': [1], - 'artist': ['artist'], - 'composer': ['composer'], - 'performer': ['performer'], - 'album-artist': ['albumartist'], - 'title': ['track'], - 'track-count': [2], - 'album-disc-number': [2], - 'album-disc-count': [3], - 'date': [datetime.date(2006, 1, 1,)], - 'container-format': ['ID3 tag'], - 'genre': ['genre'], - 'comment': ['comment'], - 'musicbrainz-trackid': ['trackid'], - 'musicbrainz-albumid': ['albumid'], - 'musicbrainz-artistid': ['artistid'], - 'musicbrainz-sortname': ['sortname'], - 'musicbrainz-albumartistid': ['albumartistid'], - 'bitrate': [1000], - } - - artist = Artist(name='artist', musicbrainz_id='artistid', - sortname='sortname') - composer = Artist(name='composer') - performer = Artist(name='performer') - albumartist = Artist(name='albumartist', - musicbrainz_id='albumartistid') - - album = Album(name='album', num_tracks=2, num_discs=3, - musicbrainz_id='albumid', artists=[albumartist]) - - self.track = Track(name='track', date='2006-01-01', - genre='genre', track_no=1, disc_no=2, - comment='comment', musicbrainz_id='trackid', - album=album, bitrate=1000, artists=[artist], - composers=[composer], performers=[performer]) - - def check(self, expected): - actual = utils.convert_tags_to_track(self.tags) - self.assertEqual(expected, actual) - - def test_track(self): - self.check(self.track) - - def test_missing_track_no(self): - del self.tags['track-number'] - self.check(self.track.replace(track_no=None)) - - def test_multiple_track_no(self): - self.tags['track-number'].append(9) - self.check(self.track) - - def test_missing_track_disc_no(self): - del self.tags['album-disc-number'] - self.check(self.track.replace(disc_no=None)) - - def test_multiple_track_disc_no(self): - self.tags['album-disc-number'].append(9) - self.check(self.track) - - def test_missing_track_name(self): - del self.tags['title'] - self.check(self.track.replace(name=None)) - - def test_multiple_track_name(self): - self.tags['title'] = ['name1', 'name2'] - self.check(self.track.replace(name='name1; name2')) - - def test_missing_track_musicbrainz_id(self): - del self.tags['musicbrainz-trackid'] - self.check(self.track.replace(musicbrainz_id=None)) - - def test_multiple_track_musicbrainz_id(self): - self.tags['musicbrainz-trackid'].append('id') - self.check(self.track) - - def test_missing_track_bitrate(self): - del self.tags['bitrate'] - self.check(self.track.replace(bitrate=None)) - - def test_multiple_track_bitrate(self): - self.tags['bitrate'].append(1234) - self.check(self.track) - - def test_missing_track_genre(self): - del self.tags['genre'] - self.check(self.track.replace(genre=None)) - - def test_multiple_track_genre(self): - self.tags['genre'] = ['genre1', 'genre2'] - self.check(self.track.replace(genre='genre1; genre2')) - - def test_missing_track_date(self): - del self.tags['date'] - self.check(self.track.replace(date=None)) - - def test_multiple_track_date(self): - self.tags['date'].append(datetime.date(2030, 1, 1)) - self.check(self.track) - - def test_missing_track_comment(self): - del self.tags['comment'] - self.check(self.track.replace(comment=None)) - - def test_multiple_track_comment(self): - self.tags['comment'] = ['comment1', 'comment2'] - self.check(self.track.replace(comment='comment1; comment2')) - - def test_missing_track_artist_name(self): - del self.tags['artist'] - self.check(self.track.replace(artists=[])) - - def test_multiple_track_artist_name(self): - self.tags['artist'] = ['name1', 'name2'] - artists = [Artist(name='name1'), Artist(name='name2')] - self.check(self.track.replace(artists=artists)) - - def test_missing_track_artist_musicbrainz_id(self): - del self.tags['musicbrainz-artistid'] - artist = list(self.track.artists)[0].replace(musicbrainz_id=None) - self.check(self.track.replace(artists=[artist])) - - def test_multiple_track_artist_musicbrainz_id(self): - self.tags['musicbrainz-artistid'].append('id') - self.check(self.track) - - def test_missing_track_composer_name(self): - del self.tags['composer'] - self.check(self.track.replace(composers=[])) - - def test_multiple_track_composer_name(self): - self.tags['composer'] = ['composer1', 'composer2'] - composers = [Artist(name='composer1'), Artist(name='composer2')] - self.check(self.track.replace(composers=composers)) - - def test_missing_track_performer_name(self): - del self.tags['performer'] - self.check(self.track.replace(performers=[])) - - def test_multiple_track_performe_name(self): - self.tags['performer'] = ['performer1', 'performer2'] - performers = [Artist(name='performer1'), Artist(name='performer2')] - self.check(self.track.replace(performers=performers)) - - def test_missing_album_name(self): - del self.tags['album'] - self.check(self.track.replace(album=None)) - - def test_multiple_album_name(self): - self.tags['album'].append('album2') - self.check(self.track) - - def test_missing_album_musicbrainz_id(self): - del self.tags['musicbrainz-albumid'] - album = self.track.album.replace(musicbrainz_id=None, - images=[]) - self.check(self.track.replace(album=album)) - - def test_multiple_album_musicbrainz_id(self): - self.tags['musicbrainz-albumid'].append('id') - self.check(self.track) - - def test_missing_album_num_tracks(self): - del self.tags['track-count'] - album = self.track.album.replace(num_tracks=None) - self.check(self.track.replace(album=album)) - - def test_multiple_album_num_tracks(self): - self.tags['track-count'].append(9) - self.check(self.track) - - def test_missing_album_num_discs(self): - del self.tags['album-disc-count'] - album = self.track.album.replace(num_discs=None) - self.check(self.track.replace(album=album)) - - def test_multiple_album_num_discs(self): - self.tags['album-disc-count'].append(9) - self.check(self.track) - - def test_missing_album_artist_name(self): - del self.tags['album-artist'] - album = self.track.album.replace(artists=[]) - self.check(self.track.replace(album=album)) - - def test_multiple_album_artist_name(self): - self.tags['album-artist'] = ['name1', 'name2'] - artists = [Artist(name='name1'), Artist(name='name2')] - album = self.track.album.replace(artists=artists) - self.check(self.track.replace(album=album)) - - def test_missing_album_artist_musicbrainz_id(self): - del self.tags['musicbrainz-albumartistid'] - albumartist = list(self.track.album.artists)[0] - albumartist = albumartist.replace(musicbrainz_id=None) - album = self.track.album.replace(artists=[albumartist]) - self.check(self.track.replace(album=album)) - - def test_multiple_album_artist_musicbrainz_id(self): - self.tags['musicbrainz-albumartistid'].append('id') - self.check(self.track) - - def test_stream_organization_track_name(self): - del self.tags['title'] - self.tags['organization'] = ['organization'] - self.check(self.track.replace(name='organization')) - - def test_multiple_organization_track_name(self): - del self.tags['title'] - self.tags['organization'] = ['organization1', 'organization2'] - self.check(self.track.replace(name='organization1; organization2')) - - # TODO: combine all comment types? - def test_stream_location_track_comment(self): - del self.tags['comment'] - self.tags['location'] = ['location'] - self.check(self.track.replace(comment='location')) - - def test_multiple_location_track_comment(self): - del self.tags['comment'] - self.tags['location'] = ['location1', 'location2'] - self.check(self.track.replace(comment='location1; location2')) - - def test_stream_copyright_track_comment(self): - del self.tags['comment'] - self.tags['copyright'] = ['copyright'] - self.check(self.track.replace(comment='copyright')) - - def test_multiple_copyright_track_comment(self): - del self.tags['comment'] - self.tags['copyright'] = ['copyright1', 'copyright2'] - self.check(self.track.replace(comment='copyright1; copyright2')) - - def test_sortname(self): - self.tags['musicbrainz-sortname'] = ['another_sortname'] - artist = Artist(name='artist', sortname='another_sortname', - musicbrainz_id='artistid') - self.check(self.track.replace(artists=[artist])) - - def test_missing_sortname(self): - del self.tags['musicbrainz-sortname'] - artist = Artist(name='artist', sortname=None, - musicbrainz_id='artistid') - self.check(self.track.replace(artists=[artist])) From 9fde0bec553fe8535edad36a3bae3970d167eb9b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 01:54:55 +0100 Subject: [PATCH 117/142] audio, timer: Fix trace log stmt --- mopidy/audio/tags.py | 5 +++-- mopidy/internal/timer.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index ba2b021a..746c1aff 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -10,11 +10,11 @@ from gi.repository import Gst Gst.is_initialized() or Gst.init() from mopidy import compat +from mopidy.internal import log from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) -TRACE = logging.getLevelName('TRACE') def convert_taglist(taglist): @@ -52,7 +52,8 @@ gstreamer-GstTagList.html result[tag].append(value) else: logger.log( - TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) + log.TRACE_LOG_LEVEL, + 'Ignoring unknown tag data: %r = %r', tag, value) return result diff --git a/mopidy/internal/timer.py b/mopidy/internal/timer.py index b8dcb30d..7da02e55 100644 --- a/mopidy/internal/timer.py +++ b/mopidy/internal/timer.py @@ -4,13 +4,14 @@ import contextlib import logging import time +from mopidy.internal import log + logger = logging.getLogger(__name__) -TRACE = logging.getLevelName('TRACE') @contextlib.contextmanager -def time_logger(name, level=TRACE): +def time_logger(name, level=log.TRACE_LOG_LEVEL): start = time.time() yield logger.log(level, '%s took %dms', name, (time.time() - start) * 1000) From 8b543bad44ab62877e5074f409da9c6c97fa7a20 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 00:05:25 +0100 Subject: [PATCH 118/142] local: URIs should be unicode Any non-ASCII content is uriencoded anyway. --- mopidy/local/translator.py | 4 ++-- tests/local/test_translator.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mopidy/local/translator.py b/mopidy/local/translator.py index 6fc53f63..16842f59 100644 --- a/mopidy/local/translator.py +++ b/mopidy/local/translator.py @@ -42,11 +42,11 @@ def path_to_local_track_uri(relpath): URI.""" if isinstance(relpath, compat.text_type): relpath = relpath.encode('utf-8') - return b'local:track:%s' % urllib.quote(relpath) + return 'local:track:%s' % urllib.quote(relpath) def path_to_local_directory_uri(relpath): """Convert path relative to :confval:`local/media_dir` directory URI.""" if isinstance(relpath, compat.text_type): relpath = relpath.encode('utf-8') - return b'local:directory:%s' % urllib.quote(relpath) + return 'local:directory:%s' % urllib.quote(relpath) diff --git a/tests/local/test_translator.py b/tests/local/test_translator.py index e28de173..7839cd58 100644 --- a/tests/local/test_translator.py +++ b/tests/local/test_translator.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import pytest +from mopidy import compat from mopidy.local import translator @@ -89,7 +90,9 @@ def test_path_to_file_uri(path, uri): (b'\x00\x01\x02', 'local:track:%00%01%02'), ]) def test_path_to_local_track_uri(path, uri): - assert translator.path_to_local_track_uri(path) == uri + result = translator.path_to_local_track_uri(path) + assert isinstance(result, compat.text_type) + assert result == uri @pytest.mark.parametrize('path,uri', [ @@ -99,4 +102,6 @@ def test_path_to_local_track_uri(path, uri): (b'\x00\x01\x02', 'local:directory:%00%01%02'), ]) def test_path_to_local_directory_uri(path, uri): - assert translator.path_to_local_directory_uri(path) == uri + result = translator.path_to_local_directory_uri(path) + assert isinstance(result, compat.text_type) + assert result == uri From df62997186b195abb0fc1b5344f58e82772a39fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 01:31:24 +0100 Subject: [PATCH 119/142] audio: Decode tags to unicode --- mopidy/audio/tags.py | 6 ++-- tests/audio/test_tags.py | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 746c1aff..c5376906 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -47,8 +47,10 @@ gstreamer-GstTagList.html value = taglist.get_value_index(tag, i) if isinstance(value, Gst.DateTime): - result[tag].append(value.to_iso8601_string()) - if isinstance(value, (compat.string_types, bool, numbers.Number)): + result[tag].append(value.to_iso8601_string().decode('utf-8')) + elif isinstance(value, bytes): + result[tag].append(value.decode('utf-8', 'replace')) + elif isinstance(value, (compat.text_type, bool, numbers.Number)): result[tag].append(value) else: logger.log( diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 355af68e..19a2a804 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -1,12 +1,72 @@ +# encoding: utf-8 + from __future__ import absolute_import, unicode_literals import datetime import unittest +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +from mopidy import compat from mopidy.audio import tags from mopidy.models import Album, Artist, Track +class TestConvertTaglist(object): + + def make_taglist(self, tag, values): + taglist = Gst.TagList.new_empty() + + for value in values: + if isinstance(value, Gst.DateTime): + taglist.add_value(Gst.TagMergeMode.APPEND, tag, value) + continue + + gobject_value = GObject.Value() + if isinstance(value, bytes): + gobject_value.init(GObject.TYPE_STRING) + gobject_value.set_string(value) + elif isinstance(value, int): + gobject_value.init(GObject.TYPE_UINT) + gobject_value.set_uint(value) + gobject_value.init(GObject.TYPE_VALUE) + gobject_value.set_value(value) + else: + raise TypeError + taglist.add_value(Gst.TagMergeMode.APPEND, tag, gobject_value) + + return taglist + + def test_date_time_tag(self): + taglist = self.make_taglist(Gst.TAG_DATE_TIME, [ + Gst.DateTime.new_from_iso8601_string(b'2014-01-07') + ]) + + result = tags.convert_taglist(taglist) + + assert isinstance(result[Gst.TAG_DATE_TIME][0], compat.text_type) + assert result[Gst.TAG_DATE_TIME][0] == '2014-01-07' + + def test_string_tag(self): + taglist = self.make_taglist(Gst.TAG_ARTIST, [b'ABBA', b'ACDC']) + + result = tags.convert_taglist(taglist) + + assert isinstance(result[Gst.TAG_ARTIST][0], compat.text_type) + assert result[Gst.TAG_ARTIST][0] == 'ABBA' + assert isinstance(result[Gst.TAG_ARTIST][1], compat.text_type) + assert result[Gst.TAG_ARTIST][1] == 'ACDC' + + def test_integer_tag(self): + taglist = self.make_taglist(Gst.TAG_BITRATE, [17]) + + result = tags.convert_taglist(taglist) + + assert result[Gst.TAG_BITRATE][0] == 17 + + # TODO: keep ids without name? # TODO: current test is trying to test everything at once with a complete tags # set, instead we might want to try with a minimal one making testing easier. From 0fa78b8e3943d871b517ba7e5cbc514b67230839 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 01:37:04 +0100 Subject: [PATCH 120/142] gst1: Fix datetime tag conversion --- mopidy/audio/tags.py | 3 +-- tests/audio/test_tags.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index c5376906..85b56d4f 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -101,8 +101,7 @@ def convert_tags_to_track(tags): album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: - track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() + track_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 19a2a804..8a1116be 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -41,13 +41,13 @@ class TestConvertTaglist(object): def test_date_time_tag(self): taglist = self.make_taglist(Gst.TAG_DATE_TIME, [ - Gst.DateTime.new_from_iso8601_string(b'2014-01-07') + Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12') ]) result = tags.convert_taglist(taglist) assert isinstance(result[Gst.TAG_DATE_TIME][0], compat.text_type) - assert result[Gst.TAG_DATE_TIME][0] == '2014-01-07' + assert result[Gst.TAG_DATE_TIME][0] == '2014-01-07T14:13:12Z' def test_string_tag(self): taglist = self.make_taglist(Gst.TAG_ARTIST, [b'ABBA', b'ACDC']) @@ -84,7 +84,7 @@ class TagsToTrackTest(unittest.TestCase): 'track-count': [2], 'album-disc-number': [2], 'album-disc-count': [3], - 'date': [datetime.date(2006, 1, 1,)], + 'date': ['2006-01-01'], 'container-format': ['ID3 tag'], 'genre': ['genre'], 'comment': ['comment'], @@ -172,7 +172,7 @@ class TagsToTrackTest(unittest.TestCase): self.check(self.track.replace(date=None)) def test_multiple_track_date(self): - self.tags['date'].append(datetime.date(2030, 1, 1)) + self.tags['date'].append('2030-01-01') self.check(self.track) def test_missing_track_comment(self): From f877ac08071f1eabbeaa99856e6d442aa6567285 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 02:21:37 +0100 Subject: [PATCH 121/142] audio: Add support for GLib.Date tag values --- mopidy/audio/tags.py | 7 ++++++- tests/audio/test_tags.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 85b56d4f..78c09775 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -1,12 +1,13 @@ from __future__ import absolute_import, unicode_literals import collections +import datetime import logging import numbers import gi gi.require_version('Gst', '1.0') -from gi.repository import Gst +from gi.repository import GLib, Gst Gst.is_initialized() or Gst.init() from mopidy import compat @@ -46,6 +47,10 @@ gstreamer-GstTagList.html for i in range(taglist.get_tag_size(tag)): value = taglist.get_value_index(tag, i) + if isinstance(value, GLib.Date): + date = datetime.date( + value.get_year(), value.get_month(), value.get_day()) + result[tag].append(date.isoformat().decode('utf-8')) if isinstance(value, Gst.DateTime): result[tag].append(value.to_iso8601_string().decode('utf-8')) elif isinstance(value, bytes): diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 8a1116be..4619273b 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -2,12 +2,11 @@ from __future__ import absolute_import, unicode_literals -import datetime import unittest import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import GLib, GObject, Gst from mopidy import compat from mopidy.audio import tags @@ -20,7 +19,7 @@ class TestConvertTaglist(object): taglist = Gst.TagList.new_empty() for value in values: - if isinstance(value, Gst.DateTime): + if isinstance(value, (GLib.Date, Gst.DateTime)): taglist.add_value(Gst.TagMergeMode.APPEND, tag, value) continue @@ -39,6 +38,15 @@ class TestConvertTaglist(object): return taglist + def test_date_tag(self): + date = GLib.Date.new_dmy(7, 1, 2014) + taglist = self.make_taglist(Gst.TAG_DATE, [date]) + + result = tags.convert_taglist(taglist) + + assert isinstance(result[Gst.TAG_DATE][0], compat.text_type) + assert result[Gst.TAG_DATE][0] == '2014-01-07' + def test_date_time_tag(self): taglist = self.make_taglist(Gst.TAG_DATE_TIME, [ Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12') From 9657004b7705e69b317b3482d688921c3b4df44c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 02:15:10 +0100 Subject: [PATCH 122/142] audio: Move date tag from Track to Album The Track model doesn't have a date attribute. --- mopidy/audio/tags.py | 2 +- tests/audio/test_tags.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 78c09775..bdf58600 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -106,7 +106,7 @@ def convert_tags_to_track(tags): album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - track_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] + album_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 4619273b..6dfa909d 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -111,10 +111,11 @@ class TagsToTrackTest(unittest.TestCase): albumartist = Artist(name='albumartist', musicbrainz_id='albumartistid') - album = Album(name='album', num_tracks=2, num_discs=3, + album = Album(name='album', date='2006-01-01', + num_tracks=2, num_discs=3, musicbrainz_id='albumid', artists=[albumartist]) - self.track = Track(name='track', date='2006-01-01', + self.track = Track(name='track', genre='genre', track_no=1, disc_no=2, comment='comment', musicbrainz_id='trackid', album=album, bitrate=1000, artists=[artist], @@ -177,7 +178,8 @@ class TagsToTrackTest(unittest.TestCase): def test_missing_track_date(self): del self.tags['date'] - self.check(self.track.replace(date=None)) + self.check( + self.track.replace(album=self.track.album.replace(date=None))) def test_multiple_track_date(self): self.tags['date'].append('2030-01-01') From e68c4668fec489d5d35732715a8fbfb75403b916 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 02:15:45 +0100 Subject: [PATCH 123/142] audio: Fallback to datetime tag if no date tag --- mopidy/audio/tags.py | 4 ++++ tests/audio/test_tags.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index bdf58600..79ab346c 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -107,6 +107,10 @@ def convert_tags_to_track(tags): album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] album_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] + if not album_kwargs['date']: + datetime = tags.get(Gst.TAG_DATE_TIME, [None])[0] + if datetime is not None: + album_kwargs['date'] = datetime.split('T')[0] # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 6dfa909d..6a838a27 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -185,6 +185,11 @@ class TagsToTrackTest(unittest.TestCase): self.tags['date'].append('2030-01-01') self.check(self.track) + def test_datetime_instead_of_date(self): + del self.tags['date'] + self.tags['datetime'] = ['2006-01-01T14:13:12Z'] + self.check(self.track) + def test_missing_track_comment(self): del self.tags['comment'] self.check(self.track.replace(comment=None)) From df6db63dd4069c22e75b8599353882e27cae4643 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 23:47:17 +0100 Subject: [PATCH 124/142] gst1: Remove clearified TODO --- mopidy/audio/actor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index ca25f4dd..160dc8a8 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -249,7 +249,6 @@ class _Handler(object): # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. - # TODO/Gst1: Is this workaround still needed? new_state = Gst.State.NULL pending_state = Gst.State.VOID_PENDING From 190abc3513e5af7d9d9cf0746d6595e94c48aab2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 13 Jan 2016 23:29:24 +0100 Subject: [PATCH 125/142] gst1: Use default queue settings Removing this queue seems to break appsrc about to finish. --- mopidy/audio/actor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 160dc8a8..92ccb44a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -464,12 +464,10 @@ class Audio(pykka.ThreadingActor): # Queue element to buy us time between the about-to-finish event and # the actual switch, i.e. about to switch can block for longer thanks # to this queue. - # TODO: make the min-max values a setting? + # TODO: See if settings should be set to minimize latency. Previous + # setting breaks appsrc, and settings before that broke on a few + # systems. So leave the default to play it safe. queue = Gst.ElementFactory.make('queue') - queue.set_property('max-size-buffers', 0) - queue.set_property('max-size-bytes', 0) - queue.set_property('max-size-time', 3 * Gst.SECOND) - queue.set_property('min-threshold-time', 1 * Gst.SECOND) audio_sink.add(queue) audio_sink.add(self._outputs) From 3cf8cdb3d97ee5a77dbb794d623505a4383d3312 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Jan 2016 21:49:19 +0100 Subject: [PATCH 126/142] travis: Add gstreamer1.0-plugins-bad to deps --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2acbf87e..f46d5ae2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: before_install: - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573 - "sudo apt-get update -qq" - - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good python-gst-1.0" + - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad python-gst-1.0" install: - "pip install tox" From 1c4b36f66aa7414a29bb4e55d1efeda3e66bd523 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 13:05:52 +0100 Subject: [PATCH 127/142] gst1: gi.require_version() GstPbutils before importing it --- mopidy/audio/actor.py | 1 + mopidy/audio/scan.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92ccb44a..bb08eb5a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -6,6 +6,7 @@ import threading import gi gi.require_version('Gst', '1.0') +gi.require_version('GstPbutils', '1.0') from gi.repository import GObject, Gst, GstPbutils Gst.is_initialized() or Gst.init() diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ed1c6424..0ed26401 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -6,6 +6,7 @@ import time import gi gi.require_version('Gst', '1.0') +gi.require_version('GstPbutils', '1.0') from gi.repository import Gst, GstPbutils Gst.is_initialized() or Gst.init() From 906a48eaf7d7f21f80fcf3f9f7b1590aff815656 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 13:14:43 +0100 Subject: [PATCH 128/142] gst1: Fix digraph name It was probably broken by some regexp replacement. --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bb08eb5a..6f444758 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -696,7 +696,7 @@ class Audio(pykka.ThreadingActor): """ Internal method for setting the raw GStreamer state. - .. digraph:: Gst.State.transitions + .. digraph:: gst_state_transitions graph [rankdir="LR"]; node [fontsize=10]; From dce7e1551d654ae8471a1c09f1426a0f8a1336f4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 13:37:16 +0100 Subject: [PATCH 129/142] gst1: Simplify Gentoo install docs --- docs/installation/source.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index d9994c6b..b4b7ad3f 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -60,7 +60,7 @@ please follow the directions :ref:`here `. If you use Gentoo you can install GStreamer like this:: - emerge -av gst-python gst-plugins-good gst-plugins-ugly gst-plugins-meta + emerge -av gst-python gst-plugins-meta ``gst-plugins-meta`` is the one that actually pulls in the plugins you want, so pay attention to the USE flags, e.g. ``alsa``, ``mp3``, etc. From 7daed284165ed8faa42298f6ef78686dfca29901 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 20:02:32 +0100 Subject: [PATCH 130/142] docs: ==dev installs are deprecated --- docs/installation/source.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index 204cc1df..410ba6af 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -76,11 +76,6 @@ please follow the directions :ref:`here `. `_. To upgrade Mopidy to future releases, just rerun this command. - Alternatively, if you want to track Mopidy development closer, you may - install a snapshot of Mopidy's ``develop`` Git branch using pip:: - - sudo pip install --allow-unverified=mopidy mopidy==dev - #. Finally, you need to set a couple of :doc:`config values `, and then you're ready to :doc:`run Mopidy `. From b143898cd3236ded1ba517b2346cbfa748d03884 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 20:27:36 +0100 Subject: [PATCH 131/142] gst1: Adjust list of GStreamer packages needed on Arch --- docs/installation/source.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index b4b7ad3f..ed738dda 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -50,11 +50,18 @@ please follow the directions :ref:`here `. If you use Arch Linux, install the following packages from the official repository:: - sudo pacman -S gst-python gst-plugins-good gst-plugins-ugly + sudo pacman -S python2-gobject gst-python gst-plugins-good + gst-plugins-ugly + + .. warning:: + + ``gst-python`` installs GStreamer GI overrides for Python 3. As far as + we know, Arch currently lacks a package with the corresponding overrides + built for Python 2. If a ``gst-python2`` package is added, it will + depend on ``python2-gobject``, so we can then shorten this package list. If you use Fedora you can install GStreamer like this:: - # TODO Update to GStreamer 1 sudo yum install -y python-gstreamer1 gstreamer1-plugins-good \ gstreamer1-plugins-ugly From d9f53d5da3c059a2dad97d6bdb163d92cb2c6db4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 23:06:45 +0100 Subject: [PATCH 132/142] gst1: Move all gi imports to a helper module --- mopidy/__main__.py | 20 +------------------- mopidy/audio/actor.py | 7 +------ mopidy/audio/scan.py | 7 +------ mopidy/audio/tags.py | 6 +----- mopidy/audio/utils.py | 5 +---- mopidy/internal/deps.py | 6 +----- mopidy/internal/gi.py | 33 +++++++++++++++++++++++++++++++++ tests/audio/test_actor.py | 5 +---- tests/audio/test_tags.py | 5 +---- tests/audio/test_utils.py | 5 +---- tests/internal/test_deps.py | 5 +---- 11 files changed, 43 insertions(+), 61 deletions(-) create mode 100644 mopidy/internal/gi.py diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 1d9e8314..ee87b82d 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -4,26 +4,8 @@ import logging import os import signal import sys -import textwrap -try: - import gi - gi.require_version('Gst', '1.0') - from gi.repository import Gst -except ImportError: - print(textwrap.dedent(""" - ERROR: The GStreamer Python package was not found. - - Mopidy requires GStreamer to work. GStreamer is a C library with a - number of dependencies itself, and cannot be installed with the regular - Python tools like pip. - - Please see http://docs.mopidy.com/en/latest/installation/ for - instructions on how to install the required dependencies. - """)) - raise -else: - Gst.init() +from mopidy.internal.gi import Gst # noqa: Import to initialize try: # Make GObject's mainloop the event loop for python-dbus diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 6f444758..834bee55 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -4,12 +4,6 @@ import logging import os import threading -import gi -gi.require_version('Gst', '1.0') -gi.require_version('GstPbutils', '1.0') -from gi.repository import GObject, Gst, GstPbutils -Gst.is_initialized() or Gst.init() - import pykka from mopidy import exceptions @@ -17,6 +11,7 @@ from mopidy.audio import tags as tags_lib, utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.internal import deprecation, process +from mopidy.internal.gi import GObject, Gst, GstPbutils logger = logging.getLogger(__name__) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0ed26401..0b6831ea 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -4,15 +4,10 @@ from __future__ import ( import collections import time -import gi -gi.require_version('Gst', '1.0') -gi.require_version('GstPbutils', '1.0') -from gi.repository import Gst, GstPbutils -Gst.is_initialized() or Gst.init() - from mopidy import exceptions from mopidy.audio import tags as tags_lib, utils from mopidy.internal import encoding +from mopidy.internal.gi import Gst, GstPbutils # GST_ELEMENT_FACTORY_LIST: _DECODER = 1 << 0 diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 79ab346c..62784bc0 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -5,13 +5,9 @@ import datetime import logging import numbers -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GLib, Gst -Gst.is_initialized() or Gst.init() - from mopidy import compat from mopidy.internal import log +from mopidy.internal.gi import GLib, Gst from mopidy.models import Album, Artist, Track diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 6a11c7a3..774de53d 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -1,10 +1,7 @@ from __future__ import absolute_import, unicode_literals -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - from mopidy import httpclient +from mopidy.internal.gi import Gst def calculate_duration(num_samples, sample_rate): diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 8947025f..cc72d371 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -5,14 +5,10 @@ import os import platform import sys -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst -Gst.is_initialized() or Gst.init() - import pkg_resources from mopidy.internal import formatting +from mopidy.internal.gi import Gst, gi def format_dependency_list(adapters=None): diff --git a/mopidy/internal/gi.py b/mopidy/internal/gi.py new file mode 100644 index 00000000..16931a90 --- /dev/null +++ b/mopidy/internal/gi.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, unicode_literals + +import textwrap + + +try: + import gi + gi.require_version('Gst', '1.0') + gi.require_version('GstPbutils', '1.0') + from gi.repository import GLib, GObject, Gst, GstPbutils +except ImportError: + print(textwrap.dedent(""" + ERROR: A GObject Python package was not found. + + Mopidy requires GStreamer to work. GStreamer is a C library with a + number of dependencies itself, and cannot be installed with the regular + Python tools like pip. + + Please see http://docs.mopidy.com/en/latest/installation/ for + instructions on how to install the required dependencies. + """)) + raise +else: + Gst.is_initialized() or Gst.init() + + +__all__ = [ + 'GLib', + 'GObject', + 'Gst', + 'GstPbutils', + 'gi', +] diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 41f730e8..2bcc792a 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -3,10 +3,6 @@ from __future__ import absolute_import, unicode_literals import threading import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - import mock import pykka @@ -14,6 +10,7 @@ import pykka from mopidy import audio from mopidy.audio.constants import PlaybackState from mopidy.internal import path +from mopidy.internal.gi import Gst from tests import dummy_audio, path_to_data_dir diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 6a838a27..01475124 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -4,12 +4,9 @@ from __future__ import absolute_import, unicode_literals import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GLib, GObject, Gst - from mopidy import compat from mopidy.audio import tags +from mopidy.internal.gi import GLib, GObject, Gst from mopidy.models import Album, Artist, Track diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index 0ce15bcb..99c99eb6 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -1,12 +1,9 @@ from __future__ import absolute_import, unicode_literals -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - import pytest from mopidy.audio import utils +from mopidy.internal.gi import Gst class TestCreateBuffer(object): diff --git a/tests/internal/test_deps.py b/tests/internal/test_deps.py index ea102b47..84c79d9c 100644 --- a/tests/internal/test_deps.py +++ b/tests/internal/test_deps.py @@ -4,15 +4,12 @@ import platform import sys import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - import mock import pkg_resources from mopidy.internal import deps +from mopidy.internal.gi import Gst, gi class DepsTest(unittest.TestCase): From 1daf5825580d31e3f2825b5b5edfaa2aed8146fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 23:12:16 +0100 Subject: [PATCH 133/142] gst1: Check GStreamer version on start If GStreamer is too old, it fails like this: $ mopidy ERROR: Mopidy requires GStreamer >= 1.2, but found GStreamer 1.0.0. --- mopidy/internal/gi.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mopidy/internal/gi.py b/mopidy/internal/gi.py index 16931a90..320aa611 100644 --- a/mopidy/internal/gi.py +++ b/mopidy/internal/gi.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +import sys import textwrap @@ -24,6 +25,14 @@ else: Gst.is_initialized() or Gst.init() +REQUIRED_GST_VERSION = (1, 2) + +if Gst.version() < REQUIRED_GST_VERSION: + sys.exit( + 'ERROR: Mopidy requires GStreamer >= %s, but found %s.' % ( + '.'.join(map(str, REQUIRED_GST_VERSION)), Gst.version_string())) + + __all__ = [ 'GLib', 'GObject', From eda91cfa962668e5c10059b0ae487fa8066462ec Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 23:27:06 +0100 Subject: [PATCH 134/142] gst1: Add missing __future__ import --- mopidy/internal/gi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/internal/gi.py b/mopidy/internal/gi.py index 320aa611..122d03b8 100644 --- a/mopidy/internal/gi.py +++ b/mopidy/internal/gi.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, print_function, unicode_literals import sys import textwrap From af43612630892fc3cc8be9b0f13109b1a89b1198 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Feb 2016 23:58:00 +0100 Subject: [PATCH 135/142] audio: Add a TODO and some notes on duration handling --- mopidy/audio/actor.py | 1 + mopidy/audio/scan.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 834bee55..db923e6d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -746,6 +746,7 @@ class Audio(pykka.ThreadingActor): # Default to blank data to trick shoutcast into clearing any previous # values it might have. + # TODO: Verify if this works at all, likely it doesn't. set_value(Gst.TAG_ARTIST, ' ') set_value(Gst.TAG_TITLE, ' ') set_value(Gst.TAG_ALBUM, ' ') diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0b6831ea..c63405b0 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -137,6 +137,11 @@ def _start_pipeline(pipeline): def _query_duration(pipeline, timeout=100): + # 1. Try and get a duration, return if success. + # 2. Some formats need to play some buffers before duration is found. + # 3. Wait for a duration change event. + # 4. Try and get a duration again. + success, duration = pipeline.query_duration(Gst.Format.TIME) if success and duration >= 0: return duration // Gst.MSECOND From 7df7b9d5f9de766ea0cd9542821a988d13fdcb46 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 10:43:45 +0100 Subject: [PATCH 136/142] gst1: Add Audio API changes to changelog --- docs/changelog.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b8d0ee02..df8405f7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -137,6 +137,25 @@ Audio Because of this change, we can now return years without months or days, which matches the semantics of the date fields in our data models. +- **Breaking:** :meth:`mopidy.audio.Audio.set_appsrc`'s ``caps`` argument has + changed format due to the upgrade from GStreamer 0.10 to GStreamer 1. As + far as we know, this is only used by Mopidy-Spotify. As an example, with + GStreamer 0.10 the Mopidy-Spotify caps was:: + + audio/x-raw-int, endianness=(int)1234, channels=(int)2, width=(int)16, + depth=(int)16, signed=(boolean)true, rate=(int)44100 + + With GStreamer 1 this changes to:: + + audio/x-raw,format=S16LE,rate=44100,channels=2,layout=interleaved + + If you Mopidy backend uses ``set_appsrc()``, please refer to GStreamer + documentation for details on the new caps string format. + +- **Deprecated:** :func:`mopidy.audio.utils.create_buffer`'s ``capabilities`` + argument is no longer in use and will be removed in the future. As far as we + know, this is only used by Mopidy-Spotify. + Gapless ------- From e18ee4798f87e67e063bccddff2d9b81f5da6904 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 15:00:57 +0100 Subject: [PATCH 137/142] gst1: Fix docs typo --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index df8405f7..b7c0bc5a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -149,7 +149,7 @@ Audio audio/x-raw,format=S16LE,rate=44100,channels=2,layout=interleaved - If you Mopidy backend uses ``set_appsrc()``, please refer to GStreamer + If your Mopidy backend uses ``set_appsrc()``, please refer to GStreamer documentation for details on the new caps string format. - **Deprecated:** :func:`mopidy.audio.utils.create_buffer`'s ``capabilities`` From 00ed7e549c6edc2b4f4d4e8c26ae7a72e4af8e26 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 22:13:58 +0100 Subject: [PATCH 138/142] gst1: Length will always be zero, leave it out --- mopidy/audio/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 774de53d..5f42733d 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -19,8 +19,7 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): ``capabilites`` argument is no longer in use """ if not data: - raise ValueError( - 'Cannot create buffer without data: length=%d' % len(data)) + raise ValueError('Cannot create buffer without data') buffer_ = Gst.Buffer.new_wrapped(data) if timestamp is not None: buffer_.pts = timestamp From 673b1b7bdc5b1fad1e196c4436ac9dc2afe10a40 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 22:15:58 +0100 Subject: [PATCH 139/142] gst1: Fix typo in docstring --- mopidy/audio/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 5f42733d..8bc5279d 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -88,7 +88,7 @@ class Signals(object): self._ids[(element, event)] = element.connect(event, func, *args) def disconnect(self, element, event): - """Disconnect whatever handler we have for and element+event pair. + """Disconnect whatever handler we have for an element+event pair. Does nothing it the handler has already been removed. """ From 0ca898fccde5f68324cf0392e70a21e8ddac3176 Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Wed, 3 Feb 2016 00:56:27 +0100 Subject: [PATCH 140/142] docs: Add a note about running as a service under config If only reading the config page, you might not have realized that the config is located in another place when running Mopidy as a service. --- docs/config.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 382c860e..71c08edb 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -25,6 +25,10 @@ create the configuration file yourself, or run the ``mopidy`` command, and it will create an empty config file for you and print what config values must be set to successfully start Mopidy. +If running Mopidy as a service, the location of the config file and other +details documented here differs a bit. See :ref:`service` for details about +this. + When you have created the configuration file, open it in a text editor, and add the config values you want to change. If you want to keep the default for a config value, you **should not** add it to the config file, but leave it out so From 7a06a71e6e66de04cda9f2013a75a5875821af1a Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Wed, 3 Feb 2016 00:34:51 +0100 Subject: [PATCH 141/142] docs: Add info about PulseAudio when running as a service When using PulseAudio and running Mopidy as a service, some configuration has to be added for this. This documents what you have to do. The setup is based on these: https://wiki.archlinux.org/index.php/PulseAudio/Examples#PulseAudio_over_network https://github.com/mopidy/mopidy/issues/954#issuecomment-73369712 --- docs/service.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/service.rst b/docs/service.rst index 2b608ed6..8ffb6bd2 100644 --- a/docs/service.rst +++ b/docs/service.rst @@ -92,3 +92,46 @@ Service on OS X =============== If you're installing Mopidy on OS X, see :ref:`osx-service`. + + +Configure PulseAudio +==================== + +When using PulseAudio, you will typically have a PulseAudio server run by your +main user. Since Mopidy is running as its own user, it can't access this server +directly. Running PulseAudio as a system-wide daemon is discouraged by upstream +(see `here +`_ +for details). Rather you can configure PulseAudio and Mopidy so Mopidy sends +the sound to the PulseAudio server already running as your main user. + +First, configure PulseAudio to accept sound over tcp from localhost by +uncommenting or adding the tcp module to :file:`/etc/pulse/default.pa` or +:file:`$XDG_CONFIG_HOME/pulse/default.pa` (typically +:file:`~/.config/pulse/default.pa`):: + + ### Network access (may be configured with paprefs, so leave this commented + ### here if you plan to use paprefs) + #load-module module-esound-protocol-tcp + load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1 + #load-module module-zeroconf-publish + +Next, configure Mopidy to use this PulseAudio server:: + + [audio] + output = pulsesink server=127.0.0.1 + +After this, restart both PulseAudio and Mopidy:: + + pulseaudio --kill + start-pulseaudio-x11 + sudo systemctl restart mopidy + +If you are not running any X server, run ``pulseaudio --start`` instead of +``start-pulseaudio-x11``. + +If you don't want to hard code the output in your Mopidy config, you can +instead of adding any config to Mopidy add this to +:file:`~mopidy/.pulse/client.conf`:: + + default-server=127.0.0.1 From 4e39f0969af5e28d8b93ddaa866b29c42a5d1cbe Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Wed, 3 Feb 2016 13:41:04 +0100 Subject: [PATCH 142/142] docs: Capitalize TCP in service docs --- docs/service.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/service.rst b/docs/service.rst index 8ffb6bd2..10c47a68 100644 --- a/docs/service.rst +++ b/docs/service.rst @@ -105,8 +105,8 @@ directly. Running PulseAudio as a system-wide daemon is discouraged by upstream for details). Rather you can configure PulseAudio and Mopidy so Mopidy sends the sound to the PulseAudio server already running as your main user. -First, configure PulseAudio to accept sound over tcp from localhost by -uncommenting or adding the tcp module to :file:`/etc/pulse/default.pa` or +First, configure PulseAudio to accept sound over TCP from localhost by +uncommenting or adding the TCP module to :file:`/etc/pulse/default.pa` or :file:`$XDG_CONFIG_HOME/pulse/default.pa` (typically :file:`~/.config/pulse/default.pa`)::