Estimated reading time : 10 minutes
Tags : linux daemon python go working
Table of contents :
Strawberry is a music player for Linux, while not being beautiful, it is the best I've found so far.
You can find it here, and its source code here.
I would like to display what music I'm listening to on my website, in a small 'widget' on a side, without having to do anything manually.
The informations I want to display are :
To achieve this, I thought of multiple solutions, each with their pros and cons.
On Linux, you can find which files are opened by a process in /proc/<pid>/fd/
. For example, if I want to know what files are opened by the process with the pid 1234
, I can do ls -l /proc/1234/fd/
.
So, I could use inotify
to watch the files opened by Strawberry, and when a new file is opened, I could check if it is a music file, and if it is, I could fetch the cover, resize it, and display it on my website.
I have NO idea on how to use D-Bus, and I found almost no documentation on how to use it with Strawberry.
As stated before, I have no idea on how to use D-Bus, but if you're in the same situation as me, let's try to figure it out together !
D-Bus is a message bus system, which allows applications to communicate with each other.
A cool feature of D-Bus is that you can subscribe to events, and get notified when they happen. Isn't that perfect for our use case ? (it is)
Looking at the repo of Strawberry, I found this little directory called dbus
, which contains a bunch of .xml
files.
metatypes.h
org.freedesktop.DBus.ObjectManager.xml
org.freedesktop.Notifications.xml
org.freedesktop.UDisks2.Block.xml
org.freedesktop.UDisks2.Drive.xml
org.freedesktop.UDisks2.Filesystem.xml
org.freedesktop.UDisks2.Job.xml
org.gnome.SettingsDaemon.MediaKeys.xml
org.kde.KGlobalAccel.Component.xml
org.kde.KGlobalAccel.xml
org.mate.SettingsDaemon.MediaKeys.xml
org.mpris.MediaPlayer2.Player.xml
org.mpris.MediaPlayer2.Playlists.xml
org.mpris.MediaPlayer2.TrackList.xml
org.mpris.MediaPlayer2.xml
The most interesting ones are every org.mpris.MediaPlayer2.*.xml
files, which are the ones we're going to use.
MPRIS (Media Player Remote Interfacing Specification) is a standard D-Bus interface which aims to provide a common programmatic API for controlling media players. It provides a mechanism for discovery, querying and basic playback control of compliant media players, as well as a track list interface which is used to add context to the active media item.
So, MPRIS is a standard for media players, which allows you to control them, and get informations about them.
After looking a bit on the internet, I found this StackOverflow question (I'm not the kind of person to look at documentations)
The author's code contains some hints :
import dbus
bus = dbus.SessionBus()
proxy = bus.get_object('org.mpris.MediaPlayer2.rhythmbox','/org/mpris/MediaPlayer2')
player = dbus.Interface(proxy, 'org.mpris.MediaPlayer2.Player')
playlists = dbus.Interface(proxy, 'org.mpris.MediaPlayer2.Playlists')
tracklist = dbus.Interface(proxy, 'org.mpris.MediaPlayer2.TrackList')
This code seems to be for Rhythmbox, but it should work for Strawberry too. If we replace rhythmbox
by strawberry
, we should be able to control Strawberry.
Here's the content of the file org.mpris.MediaPlayer2.Player.xml
:
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name='org.mpris.MediaPlayer2.Player'>
<method name='Next'/>
<method name='Previous'/>
<method name='Pause'/>
<method name='PlayPause'/>
<method name='Stop'/>
<method name='Play'/>
<method name='Seek'>
<arg direction='in' name='Offset' type='x'/>
</method>
<method name='SetPosition'>
<arg direction='in' name='TrackId' type='o'/>
<arg direction='in' name='Position' type='x'/>
</method>
<method name='OpenUri'>
<arg direction='in' name='Uri' type='s'/>
</method>
<signal name='Seeked'>
<arg name='Position' type='x'/>
</signal>
<property name='PlaybackStatus' type='s' access='read'/>
<property name='LoopStatus' type='s' access='readwrite'/>
<property name='Rate' type='d' access='readwrite'/>
<property name='Shuffle' type='b' access='readwrite'/>
<property name='Metadata' type='a{sv}' access='read'>
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QVariantMap"/>
</property>
<property name='Rating' type='d' access='readwrite'/>
<property name='Volume' type='d' access='readwrite'/>
<property name='Position' type='x' access='read'/>
<property name='MinimumRate' type='d' access='read'/>
<property name='MaximumRate' type='d' access='read'/>
<property name='CanGoNext' type='b' access='read'/>
<property name='CanGoPrevious' type='b' access='read'/>
<property name='CanPlay' type='b' access='read'/>
<property name='CanPause' type='b' access='read'/>
<property name='CanSeek' type='b' access='read'/>
<property name='CanControl' type='b' access='read'/>
</interface>
</node>
There's an interesting property called Metadata
which is of type a{sv}
(a dictionary of strings and variants). And is read-only.
import dbus
# Initialize a D-Bus session bus
bus = dbus.SessionBus()
# Get an object manager on the player
proxy = bus.get_object('org.mpris.MediaPlayer2.strawberry','/org/mpris/MediaPlayer2')
player = dbus.Interface(proxy, 'org.mpris.MediaPlayer2.Player')
# Get the metadata
metadata_interface = dbus.Interface(proxy, dbus_interface='org.freedesktop.DBus.Properties')
metadata = metadata_interface.Get('org.mpris.MediaPlayer2.Player', 'Metadata')
# Print the metadata
print(metadata)
And indeed, it works !
# /!\ Cleaned up output because dbus has its own types
{
"bitrate": 320,
"mpris:artUrl": 'file:///tmp/strawberry-cover-DuwAFC.jpg',
"mpris:length": 180872000,
"mpris:trackid": '/org/strawberrymusicplayer/strawberry/Track/3215',
"xesam:album": 'Melody Mountain',
"xesam:artist": ['Kupla'],
"xesam:contentCreated": '2023-01-28T22:55:36',
"xesam:title": 'Melody Mountain',
"xesam:trackNumber": 7,
"xesam:url": 'file:///...'
}
Wanted information | Key in the dictionary | Type |
---|---|---|
Name of the song | xesam:title | str |
Author | xesam:artist | List[str] |
mpris:artUrl | str | |
Current time | mpris:length | int |
Total time | xesam:contentCreated | str |
What's a bit annoying is that the cover file is a URI that points to a temporary file, but we cannot access it.
Possibly due to the fact that I've installed it through flatpak ?
So, we have the URI of the cover, but we cannot access it. What can we do ?
Well we have two solutions :
cover.jpg
or folder.jpg
(or any other name) in the same folder as the music fileIn theory, D-Bus allows you to subscribe to events, and get notified when they happen.
This is perfect for us, because we want to get notified when :
import dbus
import dbus.mainloop.glib
from gi.repository import GLib
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
# Get an object manager on the player
bus = dbus.SessionBus()
proxy = bus.get_object('org.mpris.MediaPlayer2.strawberry','/org/mpris/MediaPlayer2')
# Get the player interface
player = dbus.Interface(proxy, 'org.mpris.MediaPlayer2.Player')
# Get the metadata interface (a bit of a confusion here, will be refactored later)
metadata_interface = dbus.Interface(proxy, dbus_interface='org.freedesktop.DBus.Properties')
playback_status_interface = dbus.Interface(proxy, dbus_interface='org.freedesktop.DBus.Properties')
# Subscribe to the Seeked signal
def on_seeked(position):
print(f'Seeked to {position} µs')
# Subscribe to the PropertiesChanged signal
def on_properties_changed(interface_name, changed_properties, invalidated_properties):
if 'Metadata' in changed_properties:
metadata = changed_properties['Metadata']
print(f'Track changed: {metadata}')
# Subscribe to the PropertiesChanged signal
def on_playback_changed(interface_name, changed_properties, invalidated_properties):
if 'PlaybackStatus' in changed_properties:
playback_status = changed_properties['PlaybackStatus']
print(f'Playback status changed: {playback_status}')
playback_status_interface.connect_to_signal('PropertiesChanged', on_playback_changed)
metadata_interface.connect_to_signal('PropertiesChanged', on_properties_changed)
bus.add_signal_receiver(on_seeked, 'Seeked', 'org.mpris.MediaPlayer2.Player')
# Wait for the signals
loop = GLib.MainLoop()
loop.run()
And it works !
Now that we have all the informations we need, we can make the REST API in our server.
Since I only use Strawberry on a computer that is in the same network as my server, I will just authorize HTTP requests that are coming from the local network.
POST /api/strawberry
This endpoint will be used to get the informations about the current song, and the cover.
It will receive a request when the song's metadata changes.
Example payload :
{
"title": "Melody Mountain",
"artists": ["Kupla"],
"cover": "<base64 encoded image>",
"length": 2147483647
}
Field name | Type | Description |
---|---|---|
title | str | The title of the song |
artists | List[str] | The artists of the song |
cover | str | The cover of the song, encoded in base64 |
length | int | The length of the song, in µs |
PATCH /api/strawberry/seek
This endpoint will be used to change the current time of the song in the front-end.
Example payload :
{
"position": 2147483647
}
If the position > length, the position will be set to 0
Field name | Type | Description |
---|---|---|
position | int | The new position of the song, in µs |
PATCH /api/strawberry/state
This endpoint will be used to change the state of the song in the front-end.
Example payload :
{
"playing": true
}
Field name | Type | Description |
---|---|---|
playing | bool | The new state of the song |
Once our routes are defined, and the daemon reports its events to the API, we have to implements the packets, and send them to the clients.
Each packets that are sent to the client are raw bytes.
On my website I consider three packet types
Type | Description |
---|---|
uint16 | Title length (in bytes) |
string | Title (UTF-8) |
uint16 | Artists length (separator: , ) |
string[] | Artists (UTF-8) |
uint32 | Cover length |
string | Cover (base64 encoded) |
uint32 | Song length (in µs) |
uint32 | Current time (in µs) |
Type | Description |
---|---|
uint32 | New position (in µs) |
Type | Description |
---|---|
uint8 (bool) | New state (0: paused, 1: playing) |
I'm aware of few cases that are not properly handled by the daemon.
The daemon will wait for it to start, and then it will connect to it, however, if Strawberry gets closed, the daemon will not wait for it to start again.
For now, nothing is open-sourced because there's no point in doing so, and is too specific to my setup anyway.
However, I hope that this article will give you some ideas on how to do something similar, or even better, and I'd be happy to hear about it !