audio: Add an 'artwork' property

And use it to track the existence of artwork for the current file. This
could either be a cover.jpg file in the same directory as the currently
playing track or embedded artwork found in the Gst.TagList.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-07-19 15:51:22 -04:00
parent 7ff1a3d60c
commit ee8db58fb2
7 changed files with 64 additions and 5 deletions

View File

@ -114,7 +114,7 @@ class Application(Adw.Application):
self.mpris.app.connect("Quit", self.win.close)
for tag in ["artist", "album", "album-artist", "album-disc-number",
"title", "track-number", "duration", "file"]:
"title", "track-number", "duration", "file", "artwork"]:
self.player.bind_property(tag, self.mpris.player, tag)
for (prop, mpris_prop) in [("have-track", "CanPlay"),
("have-track", "CanPause"),

View File

@ -6,6 +6,7 @@ from gi.repository import GLib
from gi.repository import Gst
from . import replaygain
from .. import path
from .. import tmpdir
UPDATE_INTERVAL = 100
@ -24,6 +25,7 @@ class Player(GObject.GObject):
position = GObject.Property(type=float, default=0)
duration = GObject.Property(type=float, default=0)
volume = GObject.Property(type=float, default=1.0)
artwork = GObject.Property(type=GObject.TYPE_PYOBJECT)
file = GObject.Property(type=GObject.TYPE_PYOBJECT)
playing = GObject.Property(type=bool, default=False)
@ -92,8 +94,16 @@ class Player(GObject.GObject):
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"]:
"title", "track-number", "artwork"]:
match tag:
case "artwork":
(res, sample) = taglist.get_sample("image")
if res:
buffer = sample.get_buffer()
(res, map) = buffer.map(Gst.MapFlags.READ)
if res:
value = tmpdir.cover_jpg(map.data)
buffer.unmap(map)
case "track-number" | "album-disc-number":
(res, value) = taglist.get_uint(tag)
case _:
@ -107,11 +117,13 @@ class Player(GObject.GObject):
print(f"audio: loading {uri}")
self._playbin.set_property("uri", uri)
def __reset_properties(self, *, duration: float = 0.0) -> None:
def __reset_properties(self, *, duration: float = 0.0,
artwork: pathlib.Path | None = None) -> None:
for tag in ["artist", "album-artist", "album", "title"]:
self.set_property(tag, "")
for tag in ["album-disc-number", "track-number", "position"]:
self.set_property(tag, 0)
self.artwork = artwork
self.duration = duration
def __update_position(self) -> bool:
@ -176,7 +188,9 @@ class Player(GObject.GObject):
"""Signal that a new URI has started."""
print("audio: file loaded")
(res, dur) = self._playbin.query_duration(Gst.Format.TIME)
self.__reset_properties(duration=(dur / Gst.USECOND if res else 0))
cover = self.file.parent / "cover.jpg"
self.__reset_properties(duration=(dur / Gst.USECOND if res else 0),
artwork=(cover if cover.is_file() else None))
self.have_track = True
@GObject.Signal

View File

@ -38,6 +38,7 @@ class Player(dbus.Object):
trackid = GObject.Property(type=int)
file = GObject.Property(type=GObject.TYPE_PYOBJECT)
artwork = GObject.Property(type=GObject.TYPE_PYOBJECT)
def __init__(self):
"""Initialize the mpris2 application object."""
@ -47,7 +48,8 @@ class Player(dbus.Object):
"""Notify DBus when tags change."""
match property:
case "artist" | "album" | "album-artist" | "album-disc-number" | \
"title" | "track-number" | "duration" | "trackid" | "uri":
"title" | "track-number" | "duration" | "trackid" | \
"uri" | "artwork":
changed = GLib.Variant("a{sv}", self.Metadata)
self.properties_changed({"Metadata": changed})
case "PlaybackStatus":
@ -73,6 +75,8 @@ class Player(dbus.Object):
res["mpris:length"] = GLib.Variant("x", self.duration)
res["xesam:url"] = GLib.Variant("s", self.file.as_uri())
if self.artwork is not None:
res["mpris:artUrl"] = GLib.Variant("s", self.artwork.as_uri())
if len(self.artist) > 0:
res["xesam:artist"] = GLib.Variant("as", [self.artist])
if len(self.album) > 0:

View File

@ -1,6 +1,7 @@
# Copyright 2022 (c) Anna Schumaker.
"""Tests our GObject audio player wrapping a GStreamer Playbin element."""
import io
import pathlib
import unittest
import unittest.mock
import emmental.audio
@ -58,6 +59,7 @@ class TestAudio(unittest.TestCase):
self.player.file = tests.util.TRACK_OGG
self.player.duration = 10
self.player.position = 8
self.player.artwork = pathlib.Path("/a/b/c.jpg")
eos = Gst.Message.new_eos(self.player._playbin)
self.player._playbin.get_bus().post(eos)
@ -70,6 +72,7 @@ class TestAudio(unittest.TestCase):
for prop in ["album-disc-number", "track-number",
"position", "duration"]:
self.assertEqual(self.player.get_property(prop), 0)
self.assertIsNone(self.player.artwork)
self.assertEqual(self.player.get_state(), Gst.State.READY)
self.assertEqual(self.player.status, "Stopped")
@ -254,6 +257,36 @@ class TestAudio(unittest.TestCase):
self.assertRegex(mock_stdout.getvalue(),
r"audio: setting ReplayGain mode to 'disabled'")
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
def test_artwork(self, mock_stdout: io.StringIO):
"""Test that we handle album artwork."""
self.assertIsNone(self.player.artwork)
with unittest.mock.patch.object(pathlib.Path, "is_file",
return_value=False):
self.player.file = tests.util.TRACK_OGG
self.player.pause()
self.main_loop()
self.assertIsNone(self.player.artwork)
self.player.stop()
self.player.pause()
self.main_loop()
self.assertEqual(self.player.artwork, tests.util.COVER_JPG)
buffer = Gst.Buffer.new_wrapped_bytes(tests.util.COVER_JPG_BYTES)
taglist = Gst.TagList.new_empty()
taglist.add_value(Gst.TagMergeMode.APPEND, "image",
Gst.Sample.new(buffer))
tag = Gst.Message.new_tag(self.player._playbin, taglist)
self.player._playbin.get_bus().post(tag)
self.main_loop()
expected = emmental.tmpdir.cover_jpg()
self.assertEqual(self.player.artwork,
emmental.tmpdir.cover_jpg())
self.assertTrue(expected.is_file())
def test_shutdown(self):
"""Test that the shutdown function works as expected."""
self.player.shutdown()

View File

@ -4,6 +4,7 @@ import pathlib
import unittest
import unittest.mock
import emmental.mpris2.player
import tests.util
from gi.repository import GLib
from gi.repository import Gio
@ -57,6 +58,7 @@ class TestPlayer(unittest.TestCase):
self.assertEqual(self.player.duration, 0)
self.assertEqual(self.player.trackid, 0)
self.assertIsNone(self.player.file)
self.assertIsNone(self.player.artwork)
self.assertDictEqual(self.player.Metadata, {})
@ -66,11 +68,14 @@ class TestPlayer(unittest.TestCase):
self.player.title = "Test Title"
self.player.duration = 12345
self.player.file = pathlib.Path("/a/b/c.ogg")
self.player.artwork = tests.util.COVER_JPG
cover_uri = tests.util.COVER_JPG.as_uri()
res = {"mpris:trackid":
GLib.Variant("o", "/com/nowheycreamery/emmental/0"),
"mpris:length": GLib.Variant("x", 12345),
"xesam:url": GLib.Variant("s", "file:///a/b/c.ogg"),
"mpris:artUrl": GLib.Variant("s", cover_uri),
"xesam:album": GLib.Variant("s", "Test Album"),
"xesam:albumArtist": GLib.Variant("as", ["Test Album Artist"]),
"xesam:artist": GLib.Variant("as", ["Test Artist"]),

View File

@ -4,9 +4,12 @@ import pathlib
import unittest
import emmental.db
from gi.repository import GObject
from gi.repository import GLib
TRACK_OGG = pathlib.Path(__file__).parent / "track.ogg"
COVER_JPG = pathlib.Path(__file__).parent / "cover.jpg"
COVER_JPG_BYTES = GLib.Bytes(COVER_JPG.open("rb").read())
class TestCase(unittest.TestCase):

BIN
tests/util/cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB