From d105b15e029eb82585f96a748341a0ae4bc8f407 Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Fri, 21 Oct 2022 10:03:28 -0400 Subject: [PATCH] mpris2: Add a Player object This begins to implement the MediaPlayer2.Player interface. The properties and signals are there, and I expect to fully implement them as Emmental development goes on. Implements: #7 ("Add MPRIS2 Support") Signed-off-by: Anna Schumaker --- Makefile | 5 ++- emmental/mpris2/__init__.py | 6 ++++ emmental/mpris2/player.py | 71 +++++++++++++++++++++++++++++++++++++ tests/mpris2/test_mpris2.py | 2 ++ tests/mpris2/test_player.py | 58 ++++++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 emmental/mpris2/player.py create mode 100644 tests/mpris2/test_player.py diff --git a/Makefile b/Makefile index 4d54c71..b2edc93 100644 --- a/Makefile +++ b/Makefile @@ -26,8 +26,11 @@ mpris-spec/Makefile: emmental/mpris2/MediaPlayer2.xml: mpris-spec/Makefile cp mpris-spec/spec/org.mpris.MediaPlayer2.xml emmental/mpris2/MediaPlayer2.xml +emmental/mpris2/Player.xml: mpris-spec/Makefile + cp mpris-spec/spec/org.mpris.MediaPlayer2.Player.xml emmental/mpris2/Player.xml + .PHONY: mpris2 -mpris2: emmental/mpris2/MediaPlayer2.xml +mpris2: emmental/mpris2/MediaPlayer2.xml emmental/mpris2/Player.xml .PHONY: emmental.gresource.xml emmental.gresource.xml: diff --git a/emmental/mpris2/__init__.py b/emmental/mpris2/__init__.py index 2bd418e..773dcb3 100644 --- a/emmental/mpris2/__init__.py +++ b/emmental/mpris2/__init__.py @@ -3,6 +3,7 @@ from gi.repository import GObject from gi.repository import Gio from . import application +from . import player MPRIS2_ID = f"org.mpris.MediaPlayer2.emmental{'-debug' if __debug__ else ''}" @@ -16,8 +17,10 @@ class Connection(GObject.GObject): """Initialize Mpris2.""" super().__init__() self.app = application.Application() + self.player = player.Player() self.bind_property("dbus", self.app, "dbus") + self.bind_property("dbus", self.player, "dbus") self._busid = Gio.bus_own_name(Gio.BusType.SESSION, MPRIS2_ID, Gio.BusNameOwnerFlags.NONE, @@ -31,14 +34,17 @@ class Connection(GObject.GObject): def __on_bus_acquired(self, dbus: Gio.DBusConnection, name: str) -> None: self.dbus = dbus self.app.register(dbus) + self.player.register(dbus) def __on_name_lost(self, dbus: Gio.DBusConnection, name: str) -> None: self.app.unregister(dbus) + self.player.unregister(dbus) def disconnect(self): """Disconnect from dbus.""" if self.dbus: self.app.unregister(self.dbus) + self.player.unregister(self.dbus) self.dbus = None if self._busid: diff --git a/emmental/mpris2/player.py b/emmental/mpris2/player.py new file mode 100644 index 0000000..a185934 --- /dev/null +++ b/emmental/mpris2/player.py @@ -0,0 +1,71 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Our Mpris2 Player dbus Object.""" +import pathlib +from gi.repository import GObject +from . import dbus + +PLAYER_XML = pathlib.Path(__file__).parent / "Player.xml" + + +class Player(dbus.Object): + """The mpris2 Player dbus object.""" + + PlaybackStatus = GObject.Property(type=str, default="Stopped") + LoopStatus = GObject.Property(type=str, default="None") + Rate = GObject.Property(type=float, default=1.0) + Shuffle = GObject.Property(type=bool, default=False) + Volume = GObject.Property(type=float, default=1.0) + Position = GObject.Property(type=float, default=0) + MinimumRate = GObject.Property(type=float, default=1.0) + MaximumRate = GObject.Property(type=float, default=1.0) + CanGoNext = GObject.Property(type=bool, default=False) + CanGoPrevious = GObject.Property(type=bool, default=False) + CanPlay = GObject.Property(type=bool, default=False) + CanPause = GObject.Property(type=bool, default=False) + CanSeek = GObject.Property(type=bool, default=False) + CanControl = GObject.Property(type=bool, default=True) + + def __init__(self): + """Initialize the mpris2 application object.""" + super().__init__(xml=PLAYER_XML) + + @GObject.Property + def Metadata(self) -> dict: + """Metadata for the current Track.""" + return {} + + @GObject.Signal + def Next(self) -> None: + """Skip to the next track.""" + + @GObject.Signal + def Previous(self) -> None: + """Skip to the previous track.""" + + @GObject.Signal + def Pause(self) -> None: + """Pause playback.""" + + @GObject.Signal + def PlayPause(self) -> None: + """Toggle playback status.""" + + @GObject.Signal + def Stop(self) -> None: + """Stop playback.""" + + @GObject.Signal + def Play(self) -> None: + """Start or resume playback.""" + + @GObject.Signal(arg_types=(float,)) + def Seek(self, offset: float) -> None: + """Seek forward or backward by the given offset.""" + + @GObject.Signal(arg_types=(str, float)) + def SetPosition(self, trackid: str, position: float) -> None: + """Set the current track position in microseconds.""" + + @GObject.Signal(arg_types=(str,)) + def OpenUri(self, uri: str) -> None: + """Open the given uri.""" diff --git a/tests/mpris2/test_mpris2.py b/tests/mpris2/test_mpris2.py index 5d8c63d..79094c3 100644 --- a/tests/mpris2/test_mpris2.py +++ b/tests/mpris2/test_mpris2.py @@ -20,6 +20,8 @@ class TestMpris2(unittest.TestCase): self.assertIsInstance(self.mpris2, emmental.mpris2.GObject.GObject) self.assertIsInstance(self.mpris2.app, emmental.mpris2.application.Application) + self.assertIsInstance(self.mpris2.player, + emmental.mpris2.player.Player) self.assertIsNone(self.mpris2.dbus) self.assertGreater(self.mpris2._busid, 0) diff --git a/tests/mpris2/test_player.py b/tests/mpris2/test_player.py new file mode 100644 index 0000000..ab2af9c --- /dev/null +++ b/tests/mpris2/test_player.py @@ -0,0 +1,58 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Tests our Mpris Player object.""" +import pathlib +import unittest +import emmental.mpris2.player +from gi.repository import Gio + + +class TestPlayer(unittest.TestCase): + """Test the mpris2 player object.""" + + def setUp(self): + """Set up common variables.""" + self.player = emmental.mpris2.player.Player() + + def test_xml_files(self): + """Test that the interface definition files exist.""" + mpris2 = pathlib.Path(emmental.mpris2.__file__).parent + self.assertEqual(emmental.mpris2.player.PLAYER_XML, + mpris2 / "Player.xml") + self.assertTrue(emmental.mpris2.player.PLAYER_XML.is_file()) + + def test_init(self): + """Test that the application object is configured correctly.""" + self.assertIsInstance(self.player, emmental.mpris2.dbus.Object) + self.assertIsInstance(self.player.nodeinfo, Gio.DBusNodeInfo) + self.assertIsInstance(self.player.interface, Gio.DBusInterfaceInfo) + + def test_properties(self): + """Test Player properties.""" + self.assertEqual(self.player.PlaybackStatus, "Stopped") + self.assertEqual(self.player.LoopStatus, "None") + self.assertEqual(self.player.Rate, 1.0) + self.assertFalse(self.player.Shuffle) + self.assertEqual(self.player.Volume, 1.0) + self.assertEqual(self.player.Position, 0) + self.assertDictEqual(self.player.Metadata, {}) + self.assertEqual(self.player.MinimumRate, 1.0) + self.assertEqual(self.player.MaximumRate, 1.0) + self.assertFalse(self.player.CanGoNext) + self.assertFalse(self.player.CanGoPrevious) + self.assertFalse(self.player.CanPlay) + self.assertFalse(self.player.CanPause) + self.assertFalse(self.player.CanSeek) + self.assertTrue(self.player.CanControl) + + def test_signals(self): + """Test that Player signals exist.""" + self.player.emit("Next") + self.player.emit("Previous") + self.player.emit("Pause") + self.player.emit("PlayPause") + self.player.emit("Stop") + self.player.emit("Play") + self.player.emit("Seek", 12345) + self.player.emit("SetPosition", + "/com/nowheycreamery/emmental/123", 12345) + self.player.emit("OpenUri", "/a/b/c.ogg")