Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ jobs:
- name: "Test: Python 3.9"
python: "3.9"
tox: py39
- name: "Test: Python 3.10"
python: "3.10"
tox: py310
coverage: true
- name: "Lint: check-manifest"
python: "3.9"
python: "3.10"
tox: check-manifest
- name: "Lint: flake8"
python: "3.9"
python: "3.10"
tox: flake8

name: ${{ matrix.name }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
python-version: '3.10'
- name: "Install dependencies"
run: python3 -m pip install build
- name: "Build package"
Expand Down
47 changes: 35 additions & 12 deletions mopidy_mpris/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def on_start(self):
try:
self.mpris = Server(self.config, self.core)
self.mpris.publish()
except Exception as e:
logger.warning("MPRIS frontend setup failed (%s)", e)
except Exception:
logger.exception("MPRIS frontend setup failed")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to note that this will change the output from something short like e.g. "MPRIS frontend setup failed (unable to bind to port)" to a full stack trace.

I don't remember the typical errors here, so I guess this is OK.

self.stop()

def on_stop(self):
Expand All @@ -45,21 +45,46 @@ def track_playback_resumed(self, tl_track, time_position):

def track_playback_started(self, tl_track):
_emit_properties_changed(
self.mpris.player, ["PlaybackStatus", "Metadata"]
self.mpris.player,
[
"PlaybackStatus",
"Metadata",
"CanGoNext",
"CanGoPrevious",
"CanPlay",
],
)

def track_playback_ended(self, tl_track, time_position):
_emit_properties_changed(
self.mpris.player, ["PlaybackStatus", "Metadata"]
self.mpris.player,
[
"PlaybackStatus",
"Metadata",
"CanGoNext",
"CanGoPrevious",
"CanPlay",
],
)

def playback_state_changed(self, old_state, new_state):
_emit_properties_changed(
self.mpris.player, ["PlaybackStatus", "Metadata"]
self.mpris.player,
[
"PlaybackStatus",
"Metadata",
"CanGoNext",
"CanGoPrevious",
"CanPlay",
],
)

def tracklist_changed(self):
pass # TODO Implement if adding tracklist support
# TODO Implement tracklist support
_emit_properties_changed(
self.mpris.player,
["CanGoNext", "CanGoPrevious", "CanPlay"],
)

def playlists_loaded(self):
_emit_properties_changed(self.mpris.playlists, ["PlaylistCount"])
Expand Down Expand Up @@ -92,9 +117,7 @@ def stream_title_changed(self, title):


def _emit_properties_changed(interface, changed_properties):
props_with_new_values = [
(p, getattr(interface, p)) for p in changed_properties
]
interface.PropertiesChanged(
interface.INTERFACE, dict(props_with_new_values), []
)
props_with_new_values = {
p: getattr(interface, p) for p in changed_properties
}
interface.PropertiesChanged(interface.INTERFACE, props_with_new_values, [])
10 changes: 2 additions & 8 deletions mopidy_mpris/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ def PlayPause(self):
state = self.core.playback.get_state().get()
if state == PlaybackState.PLAYING:
self.core.playback.pause().get()
elif state == PlaybackState.PAUSED:
self.core.playback.resume().get()
elif state == PlaybackState.STOPPED:
else:
self.core.playback.play().get()
Comment on lines +94 to 95
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't remember how this works. Will .play() correctly resume from the middle of the track where you paused?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems so, https://docs.mopidy.com/stable/_modules/mopidy/core/playback/#PlaybackController.play

elif tl_track is None and self.get_state() == PlaybackState.PAUSED:
    self.resume()
    return


def Stop(self):
Expand All @@ -108,11 +106,7 @@ def Play(self):
if not self.CanPlay:
logger.debug("%s.Play not allowed", self.INTERFACE)
return
state = self.core.playback.get_state().get()
if state == PlaybackState.PAUSED:
self.core.playback.resume().get()
else:
self.core.playback.play().get()
self.core.playback.play().get()

def Seek(self, offset):
logger.debug("%s.Seek called", self.INTERFACE)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["setuptools >= 30.3.0", "wheel"]


[tool.black]
target-version = ["py37", "py38"]
target-version = ["py37", "py38", "py39", "py310"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cherry-picked the Python 3.10 commit to main, so this will disappear if you rebase.

line-length = 80


Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ classifiers =
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Multimedia :: Sound/Audio :: Players


Expand Down
15 changes: 15 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class DictFields:
"""Asserts all required key-value pairs of the dict match.

assert DictFields({"a": 1}) == {"a": 1, "b": 5}
assert DictFields({"a": 1}) != {"a": 2}
"""

def __init__(self, required: dict):
self.required = required

def __eq__(self, other: dict) -> bool:
return {**other, **self.required} == other

def __repr__(self) -> str:
return f"DictFields({self.required.__repr__()})"
53 changes: 52 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import pytest
from unittest.mock import Mock, PropertyMock

import pytest
from mopidy.core import Core

from mopidy_mpris.frontend import MprisFrontend
from mopidy_mpris.player import Player
from mopidy_mpris.playlists import Playlists
from mopidy_mpris.root import Root
from mopidy_mpris.server import Server

from tests import dummy_audio, dummy_backend, dummy_mixer


@pytest.fixture
def config():
return {
"core": {"max_tracklist_length": 10000},
"mpris": {"bus_type": "session"},
}


Expand Down Expand Up @@ -40,3 +48,46 @@ def core(config, backend, mixer, audio):
).proxy()
yield actor
actor.stop()


@pytest.fixture
def root(config, core):
return Root(config, core)


@pytest.fixture
def player(config, core):
return Player(config, core)


@pytest.fixture
def playlists(config, core):
return Playlists(config, core)


@pytest.fixture
def server(monkeypatch, config, core, player, root, playlists):
with monkeypatch.context() as m:
m.setattr("mopidy_mpris.server.Player", lambda *_: player)
m.setattr("mopidy_mpris.server.Root", lambda *_: root)
m.setattr("mopidy_mpris.server.Playlists", lambda *_: playlists)
return Server(config, core)


@pytest.fixture
def frontend(request, monkeypatch, config, core, server):
monkeypatch.setattr("mopidy_mpris.frontend.Server.publish", Mock())
monkeypatch.setattr("mopidy_mpris.frontend.Server.unpublish", Mock())
monkeypatch.setattr(
"mopidy_mpris.interface.Interface.PropertiesChanged", PropertyMock()
)
server_dep = (
Mock(side_effect=Exception)
if getattr(request, "param", None) == "fail"
else lambda *_: server
)
with monkeypatch.context() as m:
m.setattr("mopidy_mpris.frontend.Server", server_dep)
frontend = MprisFrontend.start(config, core).proxy()
yield frontend
frontend.stop()
24 changes: 21 additions & 3 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from mopidy_mpris import frontend as frontend_mod
from mopidy_mpris import player, playlists, root, server

from . import DictFields


@pytest.fixture
def frontend():
Expand Down Expand Up @@ -50,7 +52,7 @@ def test_track_playback_started_changes_playback_status_and_metadata(frontend):

frontend.mpris.player.PropertiesChanged.assert_called_with(
player.Player.INTERFACE,
{"Metadata": "...", "PlaybackStatus": "Playing"},
DictFields({"Metadata": "...", "PlaybackStatus": "Playing"}),
[],
)

Expand All @@ -63,7 +65,7 @@ def test_track_playback_ended_changes_playback_status_and_metadata(frontend):

frontend.mpris.player.PropertiesChanged.assert_called_with(
player.Player.INTERFACE,
{"Metadata": "...", "PlaybackStatus": "Stopped"},
DictFields({"Metadata": "...", "PlaybackStatus": "Stopped"}),
[],
)

Expand All @@ -78,7 +80,23 @@ def test_playback_state_changed_changes_playback_status_and_metadata(frontend):

frontend.mpris.player.PropertiesChanged.assert_called_with(
player.Player.INTERFACE,
{"Metadata": "...", "PlaybackStatus": "Stopped"},
DictFields({"Metadata": "...", "PlaybackStatus": "Stopped"}),
[],
)


def test_tracklist_changed_enabled_actions(frontend):
frontend.mpris.player.CanGoNext = True
frontend.mpris.player.CanGoPrevious = False
frontend.mpris.player.CanPlay = True

frontend.tracklist_changed()

frontend.mpris.player.PropertiesChanged.assert_called_with(
player.Player.INTERFACE,
DictFields(
{"CanGoNext": True, "CanGoPrevious": False, "CanPlay": True}
),
[],
)

Expand Down
59 changes: 59 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from time import sleep
from unittest.mock import call

import pytest
from mopidy.models import Track

import mopidy_mpris

from . import DictFields


def player_call(can_play, can_go_next, can_go_previous):
vals = DictFields(
{
"CanPlay": can_play,
"CanGoNext": can_go_next,
"CanGoPrevious": can_go_previous,
}
)
return call("org.mpris.MediaPlayer2.Player", vals, [])


def assert_called(can_play, can_go_next, can_go_previous):
# Beware the multithreading! Wait for the signal to get emitted.
sleep(0.1)
mopidy_mpris.interface.Interface.PropertiesChanged.assert_has_calls(
[player_call(can_play, can_go_next, can_go_previous)]
)
mopidy_mpris.interface.Interface.PropertiesChanged.reset_mock()
Comment on lines +12 to +29
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would read better with player_call inlined and by requiring the arguments to assert_called to be kwargs:

Suggested change
def player_call(can_play, can_go_next, can_go_previous):
vals = DictFields(
{
"CanPlay": can_play,
"CanGoNext": can_go_next,
"CanGoPrevious": can_go_previous,
}
)
return call("org.mpris.MediaPlayer2.Player", vals, [])
def assert_called(can_play, can_go_next, can_go_previous):
# Beware the multithreading! Wait for the signal to get emitted.
sleep(0.1)
mopidy_mpris.interface.Interface.PropertiesChanged.assert_has_calls(
[player_call(can_play, can_go_next, can_go_previous)]
)
mopidy_mpris.interface.Interface.PropertiesChanged.reset_mock()
def assert_called(*, can_play, can_go_next, can_go_previous):
# Beware the multithreading! Wait for the signal to get emitted.
sleep(0.1)
mopidy_mpris.interface.Interface.PropertiesChanged.assert_has_calls(
[
call(
"org.mpris.MediaPlayer2.Player",
DictFields({
"CanPlay": can_play,
"CanGoNext": can_go_next,
"CanGoPrevious": can_go_previous,
}),
[],
),
]
)
mopidy_mpris.interface.Interface.PropertiesChanged.reset_mock()



def test_cold_start(frontend, core, player):
assert not player.CanPlay
assert not player.CanGoNext
assert not player.CanGoPrevious
core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]).get()
assert_called(True, True, False)


def test_tracklist_full_to_empty(frontend, core, player):
core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]).get()
assert_called(True, True, False)
core.tracklist.clear().get()
assert_called(False, False, False)


def test_tracklist_changed_empty_while_playing(frontend, core, player):
core.tracklist.add([Track(uri="dummy:a")]).get()
assert_called(True, True, False)
core.playback.play().get()
assert_called(True, True, True)
core.tracklist.clear().get()
assert_called(False, False, False)


@pytest.mark.parametrize("frontend", ["fail"], indirect=True)
def test_frontend_fail_stops_actor(frontend):
sleep(0.1)
assert not frontend.actor_ref.is_alive()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py37, py38, py39, check-manifest, flake8
envlist = py37, py38, py39, py310, check-manifest, flake8

[testenv]
sitepackages = true
Expand Down