mpris2: Add an Mpris2 DBus Connection
And implement the MediaPlayer2 interface on top of it. Implements: #7 ("Add MPRIS2 Support") Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
b1cd1706ed
commit
767f0c1584
|
@ -6,3 +6,4 @@
|
|||
*.patch
|
||||
PKGBUILD
|
||||
emmental.gresource*
|
||||
emmental/mpris2/*.xml
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
[submodule "aur"]
|
||||
path = aur
|
||||
url = ssh://aur@aur.archlinux.org/emmental.git
|
||||
[submodule "mpris-spec"]
|
||||
path = mpris-spec
|
||||
url = https://github.com/freedesktop/mpris-spec.git
|
||||
|
|
15
Makefile
15
Makefile
|
@ -6,11 +6,12 @@ export EMMENTAL_LIB = ${PREFIX}/lib/emmental
|
|||
export EMMENTAL_BIN = ${PREFIX}/bin
|
||||
export EMMENTAL_SHARE = ${PREFIX}/share
|
||||
|
||||
all: emmental.gresource flake8
|
||||
all: emmental.gresource mpris2 flake8
|
||||
|
||||
clean:
|
||||
find . -type f -name "*gresource*" -exec rm {} \+
|
||||
find . -type d -name __pycache__ -exec rm -r {} \+
|
||||
find emmental/mpris2/ -type f -name "*.xml" -exec rm {} \+
|
||||
find data/ -type d -name "Test Album" -exec rm -r {} \+
|
||||
find data/ -type d -name "Test Library" -exec rm -r {} \+
|
||||
|
||||
|
@ -18,6 +19,16 @@ clean:
|
|||
flake8:
|
||||
flake8 emmental/ tests/
|
||||
|
||||
mpris-spec/Makefile:
|
||||
git submodule init mpris-spec
|
||||
git submodule update
|
||||
|
||||
emmental/mpris2/MediaPlayer2.xml: mpris-spec/Makefile
|
||||
cp mpris-spec/spec/org.mpris.MediaPlayer2.xml emmental/mpris2/MediaPlayer2.xml
|
||||
|
||||
.PHONY: mpris2
|
||||
mpris2: emmental/mpris2/MediaPlayer2.xml
|
||||
|
||||
.PHONY: emmental.gresource.xml
|
||||
emmental.gresource.xml:
|
||||
exec tools/find-resources.py
|
||||
|
@ -49,7 +60,7 @@ pkgbuild:
|
|||
cd aur && makepkg --printsrcinfo > .SRCINFO
|
||||
|
||||
.PHONY: pytest
|
||||
pytest: emmental.gresource
|
||||
pytest: emmental.gresource mpris2
|
||||
pytest
|
||||
|
||||
.PHONY: tests
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"""Set up our Application."""
|
||||
from . import gsetup
|
||||
from . import db
|
||||
from . import mpris2
|
||||
from . import options
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
|
@ -16,6 +17,7 @@ class Application(Adw.Application):
|
|||
"""Our custom Adw.Application."""
|
||||
|
||||
db = GObject.Property(type=db.Connection)
|
||||
mpris = GObject.Property(type=mpris2.Connection)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an Application."""
|
||||
|
@ -35,6 +37,7 @@ class Application(Adw.Application):
|
|||
"""Handle the Adw.Application::startup signal."""
|
||||
Adw.Application.do_startup(self)
|
||||
self.db = db.Connection()
|
||||
self.mpris = mpris2.Connection()
|
||||
|
||||
gsetup.add_style()
|
||||
self.db.load()
|
||||
|
@ -46,6 +49,9 @@ class Application(Adw.Application):
|
|||
def do_shutdown(self) -> None:
|
||||
"""Handle the Adw.Application::shutdown signal."""
|
||||
Adw.Application.do_shutdown(self)
|
||||
if self.mpris is not None:
|
||||
self.mpris.disconnect()
|
||||
self.mpris = None
|
||||
if self.db is not None:
|
||||
self.db.close()
|
||||
self.db = None
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Implement the MPRIS2 Specification."""
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
from . import application
|
||||
|
||||
MPRIS2_ID = f"org.mpris.MediaPlayer2.emmental{'-debug' if __debug__ else ''}"
|
||||
|
||||
|
||||
class Connection(GObject.GObject):
|
||||
"""Our Mpris2 Object."""
|
||||
|
||||
dbus = GObject.Property(type=Gio.DBusConnection)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Mpris2."""
|
||||
super().__init__()
|
||||
self.app = application.Application()
|
||||
|
||||
self.bind_property("dbus", self.app, "dbus")
|
||||
|
||||
self._busid = Gio.bus_own_name(Gio.BusType.SESSION, MPRIS2_ID,
|
||||
Gio.BusNameOwnerFlags.NONE,
|
||||
self.__on_bus_acquired, None,
|
||||
self.__on_name_lost)
|
||||
|
||||
def __del__(self):
|
||||
"""Clean up."""
|
||||
self.disconnect()
|
||||
|
||||
def __on_bus_acquired(self, dbus: Gio.DBusConnection, name: str) -> None:
|
||||
self.dbus = dbus
|
||||
self.app.register(dbus)
|
||||
|
||||
def __on_name_lost(self, dbus: Gio.DBusConnection, name: str) -> None:
|
||||
self.app.unregister(dbus)
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from dbus."""
|
||||
if self.dbus:
|
||||
self.app.unregister(self.dbus)
|
||||
self.dbus = None
|
||||
|
||||
if self._busid:
|
||||
Gio.bus_unown_name(self._busid)
|
||||
self._busid = None
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Our Mpris2 Application dbus Object."""
|
||||
import pathlib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from . import dbus
|
||||
|
||||
MPRIS2_XML = pathlib.Path(__file__).parent / "MediaPlayer2.xml"
|
||||
|
||||
|
||||
class Application(dbus.Object):
|
||||
"""The mpris2 Application dbus object."""
|
||||
|
||||
CanQuit = GObject.Property(type=bool, default=True)
|
||||
Fullscreen = GObject.Property(type=bool, default=False)
|
||||
CanSetFullscreen = GObject.Property(type=bool, default=True)
|
||||
CanRaise = GObject.Property(type=bool, default=True)
|
||||
HasTrackList = GObject.Property(type=bool, default=False)
|
||||
Identity = GObject.Property(type=str, default="Emmental Music Player")
|
||||
DesktopEntry = GObject.Property(type=str, default="emmental")
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the mpris2 application object."""
|
||||
super().__init__(xml=MPRIS2_XML)
|
||||
|
||||
def do_notify(self, property: str) -> None:
|
||||
"""Notify DBus when the Fullscreen property changes."""
|
||||
match property:
|
||||
case "Fullscreen":
|
||||
value = GLib.Variant("b", self.Fullscreen)
|
||||
self.properties_changed({property: value})
|
||||
|
||||
@GObject.Property
|
||||
def SupportedUriSchemes(self) -> list[str]:
|
||||
"""URI schemes supported by Emmental."""
|
||||
return ["file"]
|
||||
|
||||
@GObject.Property
|
||||
def SupportedMimeTypes(self) -> list[str]:
|
||||
"""Mime Types supported by Emmental."""
|
||||
return ["audio"]
|
||||
|
||||
@GObject.Signal
|
||||
def Raise(self) -> None:
|
||||
"""Raise the window."""
|
||||
|
||||
@GObject.Signal
|
||||
def Quit(self) -> None:
|
||||
"""Quit Emmental."""
|
|
@ -0,0 +1,89 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Our generic dbus object."""
|
||||
import pathlib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
|
||||
OBJECT_PATH = "/org/mpris/MediaPlayer2"
|
||||
|
||||
|
||||
class Object(GObject.GObject):
|
||||
"""A generic dbus object."""
|
||||
|
||||
dbus = GObject.Property(type=Gio.DBusConnection)
|
||||
nodeinfo = GObject.Property(type=Gio.DBusNodeInfo)
|
||||
interface = GObject.Property(type=Gio.DBusInterfaceInfo)
|
||||
registration = GObject.Property(type=int)
|
||||
|
||||
def __init__(self, xml: pathlib.Path = None):
|
||||
"""Initialize a dbus Object."""
|
||||
super().__init__()
|
||||
if xml and xml.is_file():
|
||||
with open(xml, 'r') as f:
|
||||
self.nodeinfo = Gio.DBusNodeInfo.new_for_xml(f.read())
|
||||
self.interface = self.nodeinfo.interfaces[0]
|
||||
self.connect("notify", self.__on_notify)
|
||||
|
||||
def __on_notify(self, object: GObject.GObject, param) -> None:
|
||||
self.do_notify(param.name)
|
||||
|
||||
def do_notify(self, property: str) -> None:
|
||||
"""Handle a property value changing."""
|
||||
|
||||
def link_property(self, property: str, object: GObject.GObject,
|
||||
object_property: str) -> None:
|
||||
"""Link a dbus property to the object."""
|
||||
self.bind_property(property, object, object_property,
|
||||
GObject.BindingFlags.BIDIRECTIONAL)
|
||||
|
||||
def properties_changed(self, changed: dict) -> None:
|
||||
"""Emit the org.freedesktop.DBus.PropertiesChanged signal."""
|
||||
if self.dbus is None:
|
||||
return
|
||||
|
||||
args = GLib.Variant.new_tuple(GLib.Variant("s", self.interface.name),
|
||||
GLib.Variant("a{sv}", changed),
|
||||
GLib.Variant("as", []))
|
||||
self.dbus.emit_signal(None, OBJECT_PATH,
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"PropertiesChanged", args)
|
||||
|
||||
def register(self, dbus: Gio.DBusConnection):
|
||||
"""Register this dbus Object."""
|
||||
self.registration = dbus.register_object(OBJECT_PATH, self.interface,
|
||||
self.__method_call,
|
||||
self.__get_property,
|
||||
self.__set_property)
|
||||
|
||||
def unregister(self, dbus: Gio.DBusConnection):
|
||||
"""Unregister this Object from the bus."""
|
||||
dbus.unregister_object(self.registration)
|
||||
self.registration = 0
|
||||
|
||||
def __method_call(self, dbus: Gio.DBusConnection, sender: str, object: str,
|
||||
interface: str, method: str, parameters: GLib.Variant,
|
||||
invocation: Gio.DBusMethodInvocation) -> None:
|
||||
if object != OBJECT_PATH or interface != self.interface.name:
|
||||
return None
|
||||
|
||||
self.emit(method, *parameters.unpack())
|
||||
invocation.return_value(GLib.Variant.new_tuple())
|
||||
|
||||
def __get_property(self, dbus: Gio.DBusConnection, sender: str,
|
||||
object: str, interface: str,
|
||||
property: str) -> GLib.Variant | None:
|
||||
if object != OBJECT_PATH or interface != self.interface.name:
|
||||
return None
|
||||
|
||||
if property_info := self.interface.lookup_property(property):
|
||||
return GLib.Variant(property_info.signature,
|
||||
self.get_property(property))
|
||||
|
||||
def __set_property(self, dbus: Gio.DBusConnection, sender: str,
|
||||
object: str, interface: str, property: str,
|
||||
value: GLib.Variant) -> bool:
|
||||
if object != OBJECT_PATH or interface != self.interface.name:
|
||||
return None
|
||||
self.set_property(property, value.unpack())
|
||||
return True
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 51e5848f9f763864568db233bffe98e3cb72bf13
|
|
@ -0,0 +1,44 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Tests our Mpris Application object."""
|
||||
import pathlib
|
||||
import unittest
|
||||
import emmental.mpris2.application
|
||||
from gi.repository import Gio
|
||||
|
||||
|
||||
class TestRoot(unittest.TestCase):
|
||||
"""Test the mpris2 application object."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
self.app = emmental.mpris2.application.Application()
|
||||
|
||||
def test_xml_files(self):
|
||||
"""Test that the interface definition files exist."""
|
||||
mpris2 = pathlib.Path(emmental.mpris2.__file__).parent
|
||||
self.assertEqual(emmental.mpris2.application.MPRIS2_XML,
|
||||
mpris2 / "MediaPlayer2.xml")
|
||||
self.assertTrue(emmental.mpris2.application.MPRIS2_XML.is_file())
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the application object is configured correctly."""
|
||||
self.assertIsInstance(self.app, emmental.mpris2.dbus.Object)
|
||||
self.assertIsInstance(self.app.nodeinfo, Gio.DBusNodeInfo)
|
||||
self.assertIsInstance(self.app.interface, Gio.DBusInterfaceInfo)
|
||||
|
||||
def test_properties(self):
|
||||
"""Test Application properties."""
|
||||
self.assertTrue(self.app.CanQuit)
|
||||
self.assertFalse(self.app.Fullscreen)
|
||||
self.assertTrue(self.app.CanSetFullscreen)
|
||||
self.assertTrue(self.app.CanRaise)
|
||||
self.assertFalse(self.app.HasTrackList)
|
||||
self.assertEqual(self.app.Identity, "Emmental Music Player")
|
||||
self.assertEqual(self.app.DesktopEntry, "emmental")
|
||||
self.assertEqual(self.app.SupportedUriSchemes, ["file"])
|
||||
self.assertEqual(self.app.SupportedMimeTypes, ["audio"])
|
||||
|
||||
def test_signals(self):
|
||||
"""Test that Application signals exist."""
|
||||
self.app.emit("Raise")
|
||||
self.app.emit("Quit")
|
|
@ -0,0 +1,24 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Test our generic dbus object."""
|
||||
import unittest
|
||||
import emmental.mpris2.dbus
|
||||
from gi.repository import GObject
|
||||
|
||||
|
||||
class TestDbusObject(unittest.TestCase):
|
||||
"""Tests our generic mpris2 object."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
self.object = emmental.mpris2.dbus.Object()
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the object is set up properly."""
|
||||
self.assertIsInstance(self.object, GObject.GObject)
|
||||
self.assertIsNone(self.object.dbus)
|
||||
self.assertIsNone(self.object.nodeinfo)
|
||||
self.assertIsNone(self.object.interface)
|
||||
self.assertEqual(self.object.registration, 0)
|
||||
|
||||
self.assertEqual(emmental.mpris2.dbus.OBJECT_PATH,
|
||||
"/org/mpris/MediaPlayer2")
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Test our root mpris2 object."""
|
||||
import unittest
|
||||
import emmental.mpris2
|
||||
|
||||
|
||||
class TestMpris2(unittest.TestCase):
|
||||
"""Test our Mpris2 Connection."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
self.mpris2 = emmental.mpris2.Connection()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
self.mpris2.disconnect()
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the Mpris Object is created properly."""
|
||||
self.assertIsInstance(self.mpris2, emmental.mpris2.GObject.GObject)
|
||||
self.assertIsInstance(self.mpris2.app,
|
||||
emmental.mpris2.application.Application)
|
||||
self.assertIsNone(self.mpris2.dbus)
|
||||
self.assertGreater(self.mpris2._busid, 0)
|
||||
|
||||
self.assertEqual(emmental.mpris2.MPRIS2_ID,
|
||||
"org.mpris.MediaPlayer2.emmental-debug")
|
|
@ -37,16 +37,24 @@ class TestEmmental(unittest.TestCase):
|
|||
mock_load: unittest.mock.Mock):
|
||||
"""Test that the startup signal works as expected."""
|
||||
self.assertIsNone(self.application.db)
|
||||
self.assertIsNone(self.application.mpris)
|
||||
|
||||
self.application.emit("startup")
|
||||
self.assertIsInstance(self.application.db, emmental.db.Connection)
|
||||
self.assertIsInstance(self.application.mpris,
|
||||
emmental.mpris2.Connection)
|
||||
|
||||
mock_startup.assert_called()
|
||||
mock_load.assert_called()
|
||||
|
||||
def test_shutdown(self):
|
||||
"""Test that the shutdown signal works as expected."""
|
||||
db = self.application.db = emmental.db.Connection()
|
||||
mpris = self.application.mpris = emmental.mpris2.Connection()
|
||||
|
||||
self.application.emit("shutdown")
|
||||
self.assertIsNone(self.application.db)
|
||||
self.assertIsNone(self.application.mpris)
|
||||
|
||||
self.assertIsNone(mpris.dbus)
|
||||
self.assertFalse(db.connected)
|
||||
|
|
Loading…
Reference in New Issue