audio: Add properties for track tags

I set these properties when the bus sends us tag messages, and wire them
up do the mpris2.Player object to notify dbus of their values.

These properties are cleared on both EOS and when a new file is started.
This is to account for the user changing the file mid-playback.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-06-23 16:33:59 -04:00
parent 35d0d815ca
commit 318b2564ce
5 changed files with 134 additions and 1 deletions

View File

@ -87,6 +87,9 @@ class Application(Adw.Application):
self.mpris.app.connect("Raise", self.win.present)
self.mpris.app.connect("Quit", self.win.close)
for tag in ["artist", "album", "album-artist", "album-disc-number",
"title", "track-number", "file"]:
self.player.bind_property(tag, self.mpris.player, tag)
self.mpris.player.link_property("Volume", self.win.header, "volume")
self.mpris.player.connect("OpenPath", self.__load_path)

View File

@ -10,6 +10,12 @@ from .. import path
class Player(GObject.GObject):
"""Wraps a GStreamer Playbin with an interface for our application."""
artist = GObject.Property(type=str)
album = GObject.Property(type=str)
album_artist = GObject.Property(type=str)
album_disc_number = GObject.Property(type=int)
title = GObject.Property(type=str)
track_number = GObject.Property(type=int)
volume = GObject.Property(type=float, default=1.0)
file = GObject.Property(type=GObject.TYPE_PYOBJECT)
@ -30,6 +36,7 @@ class Player(GObject.GObject):
bus.add_signal_watch()
bus.connect("message::eos", self.__msg_eos)
bus.connect("message::stream-start", self.__msg_stream_start)
bus.connect("message::tag", self.__msg_tags)
self.bind_property("volume", self._playbin, "volume")
@ -42,12 +49,30 @@ class Player(GObject.GObject):
self.emit("file-loaded",
path.from_uri(self._playbin.get_property("current-uri")))
def __msg_tags(self, bus: Gst.Bus, message: Gst.Message) -> None:
taglist = message.parse_tag()
for tag in ["artist", "album", "album-artist", "album-disc-number",
"title", "track-number"]:
match tag:
case "track-number" | "album-disc-number":
(res, value) = taglist.get_uint(tag)
case _:
(res, value) = taglist.get_string(tag)
if res and self.get_property(tag) != value:
self.set_property(tag, value)
def __notify_file(self, player: GObject.GObject, param) -> None:
if self.file:
uri = self.file.as_uri()
print(f"audio: loading {uri}")
self._playbin.set_property("uri", uri)
def __reset_properties(self) -> None:
for tag in ["artist", "album-artist", "album", "title"]:
self.set_property(tag, "")
for tag in ["album-disc-number", "track-number"]:
self.set_property(tag, 0)
def get_replaygain(self) -> tuple[bool, str | None]:
"""Get the current ReplayGain mode."""
mode = self._replaygain.mode
@ -74,6 +99,7 @@ class Player(GObject.GObject):
def file_loaded(self, file: pathlib.Path) -> None:
"""Signal that a new URI has started."""
print("audio: file loaded")
self.__reset_properties()
self.have_track = True
@GObject.Signal
@ -81,5 +107,6 @@ class Player(GObject.GObject):
"""Signal that the current track has ended."""
print("audio: end of stream")
self.set_state_sync(Gst.State.READY)
self.__reset_properties()
self.have_track = False
self.file = None

View File

@ -2,10 +2,12 @@
"""Our Mpris2 Player dbus Object."""
import pathlib
from gi.repository import GObject
from gi.repository import GLib
from .. import path
from . import dbus
PLAYER_XML = pathlib.Path(__file__).parent / "Player.xml"
OBJECT_PATH = "/com/nowheycreamery/emmental"
class Player(dbus.Object):
@ -26,14 +28,53 @@ class Player(dbus.Object):
CanSeek = GObject.Property(type=bool, default=False)
CanControl = GObject.Property(type=bool, default=True)
artist = GObject.Property(type=str)
album = GObject.Property(type=str)
album_artist = GObject.Property(type=str)
album_disc_number = GObject.Property(type=int)
title = GObject.Property(type=str)
track_number = GObject.Property(type=int)
trackid = GObject.Property(type=int)
file = GObject.Property(type=GObject.TYPE_PYOBJECT)
def __init__(self):
"""Initialize the mpris2 application object."""
super().__init__(xml=PLAYER_XML)
def do_notify(self, property: str) -> None:
"""Notify DBus when tags change."""
match property:
case "artist" | "album" | "album-artist" | "album-disc-number" | \
"title" | "track-number" | "trackid" | "uri":
changed = GLib.Variant("a{sv}", self.Metadata)
self.properties_changed({"Metadata": changed})
@GObject.Property
def Metadata(self) -> dict:
"""Metadata for the current Track."""
return {}
res = dict()
if self.file:
trackid = f"{OBJECT_PATH}/{self.trackid}"
res["mpris:trackid"] = GLib.Variant("o", trackid)
res["xesam:url"] = GLib.Variant("s", self.file.as_uri())
if len(self.artist) > 0:
res["xesam:artist"] = GLib.Variant("as", [self.artist])
if len(self.album) > 0:
res["xesam:album"] = GLib.Variant("s", self.album)
if len(self.album_artist) > 0:
res["xesam:albumArtist"] = GLib.Variant("as",
[self.album_artist])
if self.album_disc_number > 0:
res["xesam:discNumber"] = GLib.Variant("u",
self.album_disc_number)
if len(self.title) > 0:
res["xesam:title"] = GLib.Variant("s", self.title)
if self.track_number > 0:
res["xesam:trackNumber"] = GLib.Variant("u", self.track_number)
return res
@GObject.Signal
def Next(self) -> None:

View File

@ -54,6 +54,12 @@ class TestAudio(unittest.TestCase):
self.main_loop()
self.assertRegex(mock_stdout.getvalue(), "audio: end of stream")
for prop in ["artist", "album-artist", "album", "title"]:
self.assertEqual(self.player.get_property(prop), "")
for prop in ["album-disc-number", "track-number"]:
self.assertEqual(self.player.get_property(prop), 0)
self.assertEqual(self.player.get_state(), Gst.State.READY)
self.assertFalse(self.player.have_track)
self.assertIsNone(self.player.file)
@ -64,6 +70,8 @@ class TestAudio(unittest.TestCase):
self.player.file = tests.util.TRACK_OGG
self.assertEqual(self.player._playbin.get_property("uri"),
tests.util.TRACK_OGG.as_uri())
self.assertEqual(self.player.title, "")
self.assertEqual(self.player.album, "")
started = unittest.mock.Mock()
self.player.connect("file-loaded", started)
@ -76,6 +84,30 @@ class TestAudio(unittest.TestCase):
f"audio: loading {tests.util.TRACK_OGG.as_uri()}\n"
"audio: file loaded\n")
self.player.emit("file-loaded", tests.util.TRACK_OGG)
for prop in ["artist", "album-artist", "album", "title"]:
self.assertEqual(self.player.get_property(prop), "")
for prop in ["album-disc-number", "track-number"]:
self.assertEqual(self.player.get_property(prop), 0)
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_tags(self, mock_stdout: io.StringIO):
"""Test that the tag properties work as expected."""
for prop in ["artist", "album", "album-artist", "title"]:
self.assertEqual(self.player.get_property(prop), "")
for prop in ["album-disc-number", "track-number"]:
self.assertEqual(self.player.get_property(prop), 0)
self.player.file = tests.util.TRACK_OGG
self.player.set_state_sync(Gst.State.PLAYING)
self.main_loop()
self.assertEqual(self.player.artist, "Test Artist")
self.assertEqual(self.player.album, "Test Album")
self.assertEqual(self.player.album_artist, "Test Album Artist")
self.assertEqual(self.player.album_disc_number, 1)
self.assertEqual(self.player.title, "Test Title")
self.assertEqual(self.player.track_number, 1)
def test_volume(self):
"""Test that the volume property works as expected."""
self.assertEqual(self.player.volume, 1.0)

View File

@ -4,6 +4,7 @@ import pathlib
import unittest
import unittest.mock
import emmental.mpris2.player
from gi.repository import GLib
from gi.repository import Gio
@ -20,6 +21,8 @@ class TestPlayer(unittest.TestCase):
self.assertEqual(emmental.mpris2.player.PLAYER_XML,
mpris2 / "Player.xml")
self.assertTrue(emmental.mpris2.player.PLAYER_XML.is_file())
self.assertEqual(emmental.mpris2.player.OBJECT_PATH,
"/com/nowheycreamery/emmental")
def test_init(self):
"""Test that the application object is configured correctly."""
@ -45,6 +48,33 @@ class TestPlayer(unittest.TestCase):
self.assertFalse(self.player.CanSeek)
self.assertTrue(self.player.CanControl)
def test_metadata(self):
"""Test formatting metadata properties."""
self.assertEqual(self.player.artist, "")
self.assertEqual(self.player.album_artist, "")
self.assertEqual(self.player.album, "")
self.assertEqual(self.player.title, "")
self.assertEqual(self.player.trackid, 0)
self.assertIsNone(self.player.file)
self.assertDictEqual(self.player.Metadata, {})
self.player.artist = "Test Artist"
self.player.album_artist = "Test Album Artist"
self.player.album = "Test Album"
self.player.title = "Test Title"
self.player.file = pathlib.Path("/a/b/c.ogg")
res = {"mpris:trackid":
GLib.Variant("o", "/com/nowheycreamery/emmental/0"),
"xesam:url": GLib.Variant("s", "file:///a/b/c.ogg"),
"xesam:album": GLib.Variant("s", "Test Album"),
"xesam:albumArtist": GLib.Variant("as", ["Test Album Artist"]),
"xesam:artist": GLib.Variant("as", ["Test Artist"]),
"xesam:title": GLib.Variant("s", "Test Title")}
self.assertDictEqual(self.player.Metadata, res)
def test_signals(self):
"""Test that Player signals exist."""
self.player.emit("Next")