Skip to content

Commit

Permalink
Release v3.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
kingosticks committed Nov 28, 2022
2 parents 0c80dca + 459f319 commit afe5554
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 21 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ubuntu-devel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
main:
name: "Test: Python 3.9"
name: "Test: Python 3.10"
runs-on: ubuntu-20.04

# The container should be automatically updated from time to time.
Expand All @@ -26,4 +26,4 @@ jobs:
python3-gst-1.0 \
python3 \
tox
- run: tox -e py39
- run: tox -e py310
14 changes: 14 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ This changelog is used to track all major changes to Mopidy.
For older releases, see :ref:`history`.


v3.4.0 (2022-11-28)
===================

- Config: Handle DBus "Algorithm plain is not supported" error. (PR: :issue:`2061`)

- File: Fix uppercase ``excluded_file_extensions``. (PR: :issue:`2063`)

- Add :meth:`mopidy.backend.PlaybackProvider.on_source_setup` which can be
implemented by Backend playback providers that want to set GStreamer source
properties in the ``source-setup`` callback. (PR: :issue:`2060`)

- HTTP: Improve handling of ``allowed_origins`` config setting. (PR: :issue:`2054`)


v3.3.0 (2022-04-29)
===================

Expand Down
16 changes: 16 additions & 0 deletions mopidy/audio/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ def __init__(self, config, mixer):
self._outputs = None
self._queue = None
self._about_to_finish_callback = None
self._source_setup_callback = None

self._handler = _Handler(self)
self._appsrc = _Appsrc()
Expand Down Expand Up @@ -571,6 +572,10 @@ def _on_source_setup(self, element, source):
else:
self._appsrc.reset()

if self._source_setup_callback:
logger.debug("Running source-setup callback")
self._source_setup_callback(source)

if self._live_stream and hasattr(source.props, "is_live"):
gst_logger.debug("Enabling live stream mode")
source.set_live(True)
Expand Down Expand Up @@ -665,6 +670,17 @@ def emit_data(self, buffer_):
"""
return self._appsrc.push(buffer_)

def set_source_setup_callback(self, callback):
"""
Configure audio to use a source-setup callback.
This should be used to modify source-specific properties such as login
details.
:param callable callback: Callback to run when we setup the source.
"""
self._source_setup_callback = callback

def set_about_to_finish_callback(self, callback):
"""
Configure audio to use an about-to-finish callback.
Expand Down
19 changes: 18 additions & 1 deletion mopidy/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
Uri = str
UriScheme = str

GstElement = TypeVar("GstElement")


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -198,7 +200,7 @@ def search(
.. versionadded:: 1.0
The ``exact`` param which replaces the old ``find_exact``.
"""
pass
return []


@pykka.traversable
Expand Down Expand Up @@ -295,6 +297,20 @@ def should_download(self, uri: Uri) -> bool:
"""
return False

def on_source_setup(self, source: GstElement) -> None:
"""
Called when a new GStreamer source is created, allowing us to configure
the source. This runs in the audio thread so should not block.
*MAY be reimplemented by subclass.*
:param source: the GStreamer source element
:type source: GstElement
.. versionadded:: 3.4
"""
pass

def change_track(self, track: Track) -> bool:
"""
Switch to provided track.
Expand All @@ -317,6 +333,7 @@ def change_track(self, track: Track) -> bool:
logger.debug("Backend translated URI from %s to %s", track.uri, uri)
if not uri:
return False
self.audio.set_source_setup_callback(self.on_source_setup).get()
self.audio.set_uri(
uri,
live_stream=self.is_live(uri),
Expand Down
7 changes: 6 additions & 1 deletion mopidy/config/keyring.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ def fetch():
return []

service = _service(bus)
session = service.OpenSession("plain", EMPTY_STRING)[1]
try:
session = service.OpenSession("plain", EMPTY_STRING)[1]
except dbus.exceptions.DBusException as e:
logger.debug("%s (%s)", FETCH_ERROR, e)
return []

items, locked = service.SearchItems({"service": "mopidy"})

if not locked and not items:
Expand Down
2 changes: 1 addition & 1 deletion mopidy/file/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def browse(self, uri):

if (
self._excluded_file_extensions
and dir_entry.suffix in self._excluded_file_extensions
and dir_entry.suffix.lower() in self._excluded_file_extensions
):
continue

Expand Down
5 changes: 4 additions & 1 deletion mopidy/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ def get_config_schema(self):
schema["port"] = config_lib.Port()
schema["static_dir"] = config_lib.Deprecated()
schema["zeroconf"] = config_lib.String(optional=True)
schema["allowed_origins"] = config_lib.List(optional=True)
schema["allowed_origins"] = config_lib.List(
unique=True,
subtype=config_lib.String(transformer=lambda x: x.lower()),
)
schema["csrf_protection"] = config_lib.Boolean(optional=True)
schema["default_app"] = config_lib.String(optional=True)
return schema
Expand Down
12 changes: 5 additions & 7 deletions mopidy/http/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ def mopidy_app_factory(config, core):
logger.warning(
"HTTP Cross-Site Request Forgery protection is disabled"
)
allowed_origins = {
x.lower() for x in config["http"]["allowed_origins"] if x
}

return [
(
r"/ws/?",
WebSocketHandler,
{
"core": core,
"allowed_origins": allowed_origins,
"allowed_origins": config["http"]["allowed_origins"],
"csrf_protection": config["http"]["csrf_protection"],
},
),
Expand All @@ -46,7 +44,7 @@ def mopidy_app_factory(config, core):
JsonRpcHandler,
{
"core": core,
"allowed_origins": allowed_origins,
"allowed_origins": config["http"]["allowed_origins"],
"csrf_protection": config["http"]["csrf_protection"],
},
),
Expand Down Expand Up @@ -177,13 +175,13 @@ def check_origin(origin, request_headers, allowed_origins):
if origin is None:
logger.warning("HTTP request denied for missing Origin header")
return False
allowed_origins.add(request_headers.get("Host"))
host_header = request_headers.get("Host")
parsed_origin = urllib.parse.urlparse(origin).netloc.lower()
# Some frameworks (e.g. Apache Cordova) use local files. Requests from
# these files don't really have a sensible Origin so the browser sets the
# header to something like 'file://' or 'null'. This results here in an
# empty parsed_origin which we choose to allow.
if parsed_origin and parsed_origin not in allowed_origins:
if parsed_origin and parsed_origin not in allowed_origins | {host_header}:
logger.warning('HTTP request denied for Origin "%s"', origin)
return False
return True
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = Mopidy
version = 3.3.0
version = 3.4.0
url = https://mopidy.com/
project_urls =
Documentation = https://docs.mopidy.com/
Expand Down
46 changes: 46 additions & 0 deletions tests/audio/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ def possibly_trigger_fake_playback_error(self, uri):
def possibly_trigger_fake_about_to_finish(self):
pass

def possibly_trigger_fake_source_setup(self):
pass


class DummyMixin:
audio_class = dummy_audio.DummyAudio
Expand All @@ -59,6 +62,11 @@ def possibly_trigger_fake_about_to_finish(self):
if callback:
callback()

def possibly_trigger_fake_source_setup(self):
callback = self.audio.get_source_setup_callback().get()
if callback:
callback()


class AudioTest(BaseTest):
def test_start_playback_existing_file(self):
Expand Down Expand Up @@ -444,6 +452,19 @@ def callback():

# TODO: test tag states within gaples

def test_source_setup(self):
mock_callback = mock.Mock()

self.audio.prepare_change()
self.audio.set_source_setup_callback(mock_callback).get()
self.audio.set_uri(self.uris[0])
self.audio.start_playback()

self.possibly_trigger_fake_source_setup()
self.audio.wait_for_state_change().get()

mock_callback.assert_called_once()

# TODO: this does not belong in this testcase
def test_current_tags_are_blank_to_begin_with(self):
assert not self.audio.get_current_tags().get()
Expand Down Expand Up @@ -688,3 +709,28 @@ def test_download_flag_is_not_passed_to_playbin_if_set_appsrc( # noqa: B950
self.audio.set_appsrc("")

playbin.set_property.assert_has_calls([mock.call("flags", 0x02)])


class SourceSetupCallbackTest(unittest.TestCase):
def setUp(self): # noqa: N802
config = {"proxy": {}}
self.audio = audio.Audio(config=config, mixer=None)
self.audio._playbin = mock.Mock(spec=["set_property"])

self.source = mock.MagicMock()
# Avoid appsrc.configure()
self.source.get_factory.get_name = mock.Mock(return_value="not_appsrc")

def test_source_setup_callback(self):
mock_callback = mock.MagicMock()
self.audio.set_source_setup_callback(mock_callback)

self.audio._on_source_setup("dummy", self.source)

mock_callback.assert_called_once_with(self.source)

self.audio.set_source_setup_callback(None)

self.audio._on_source_setup("dummy", self.source)

mock_callback.assert_called_once()
23 changes: 18 additions & 5 deletions tests/dummy_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def __init__(self, config=None, mixer=None):
self.state = audio.PlaybackState.STOPPED
self._volume = 0
self._position = 0
self._callback = None
self._source_setup_callback = None
self._about_to_finish_callback = None
self._uri = None
self._stream_changed = False
self._live_stream = False
Expand Down Expand Up @@ -58,6 +59,7 @@ def pause_playback(self):

def prepare_change(self):
self._uri = None
self._source_setup_callback = None
return True

def stop_playback(self):
Expand All @@ -76,8 +78,11 @@ def set_metadata(self, track):
def get_current_tags(self):
return self._tags

def set_source_setup_callback(self, callback):
self._source_setup_callback = callback

def set_about_to_finish_callback(self, callback):
self._callback = callback
self._about_to_finish_callback = callback

def enable_sync_handler(self):
pass
Expand Down Expand Up @@ -121,14 +126,22 @@ def trigger_fake_tags_changed(self, tags):
self._tags.update(tags)
audio.AudioListener.send("tags_changed", tags=self._tags.keys())

def get_source_setup_callback(self):
# This needs to be called from outside the actor or we lock up.
def wrapper():
if self._source_setup_callback:
self._source_setup_callback()

return wrapper

def get_about_to_finish_callback(self):
# This needs to be called from outside the actor or we lock up.
def wrapper():
if self._callback:
if self._about_to_finish_callback:
self.prepare_change()
self._callback()
self._about_to_finish_callback()

if not self._uri or not self._callback:
if not self._uri or not self._about_to_finish_callback:
self._tags = {}
audio.AudioListener.send("reached_end_of_stream")
else:
Expand Down
2 changes: 1 addition & 1 deletion tests/http/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def get_app(self):
handlers.WebSocketHandler,
{
"core": self.core,
"allowed_origins": set(),
"allowed_origins": frozenset(),
"csrf_protection": True,
},
)
Expand Down
2 changes: 1 addition & 1 deletion tests/http/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def get_config(self):
"hostname": "127.0.0.1",
"port": 6680,
"zeroconf": "",
"allowed_origins": [],
"allowed_origins": frozenset(),
"csrf_protection": True,
"default_app": "mopidy",
}
Expand Down

0 comments on commit afe5554

Please sign in to comment.