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:
parent
35d0d815ca
commit
318b2564ce
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue