Informations

Estimated reading time : 10 minutes

Tags : linux daemon python go working

Table of contents :

Goal

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 :

  1. Name of the sond
  2. Author
  3. The cover of the album
  4. Current time
  5. Total time

Ideas

To achieve this, I thought of multiple solutions, each with their pros and cons.

1. Watching what files are opened by Strawberry

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.

Pros

Cons

2. Reading process memory

Pros

Cons

3. Using Strawberry's D-Bus interface

I have NO idea on how to use D-Bus, and I found almost no documentation on how to use it with Strawberry.

Pros

Cons

D-Bus ?

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 !

What is D-Bus ?

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)

How to use D-Bus ?

Looking at the repo of Strawberry, I found this little directory called dbus, which contains a bunch of .xml files.

The most interesting ones are every org.mpris.MediaPlayer2.*.xml files, which are the ones we're going to use.

What is MPRIS ?

The Arch Linux wiki says

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.

Interacting with MPRIS

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.

Getting informations on the current song

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 informationKey in the dictionaryType
Name of the songxesam:titlestr
Authorxesam:artistList[str]
Covermpris:artUrlstr
Current timempris:lengthint
Total timexesam:contentCreatedstr

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 ?

Getting the cover

So, we have the URI of the cover, but we cannot access it. What can we do ?

Well we have two solutions :

  1. Get the cover.jpg or folder.jpg (or any other name) in the same folder as the music file
  2. Extract manually the cover from the music file (last resort, because it's slow)

What about events ?

In 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 :

  1. The song changes
  2. We seek in the song
  3. We pause/play the song.
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 !

Creating the API

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 nameTypeDescription
titlestrThe title of the song
artistsList[str]The artists of the song
coverstrThe cover of the song, encoded in base64
lengthintThe 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 nameTypeDescription
positionintThe 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 nameTypeDescription
playingboolThe new state of the song

Interfacing the front-end with the API

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.

Protocol

Each packets that are sent to the client are raw bytes.

On my website I consider three packet types

  1. States, which are sent when a client connects and every few seconds ~ minutes
  2. Events, which are sent an event fires somewhere (eg. seeking, song change)
  3. Hybrids, sent to the client when it connects, and when an event fires, and every few seconds ~ minutes (if the state changed)

Song information (hybrid)

TypeDescription
uint16Title length (in bytes)
stringTitle (UTF-8)
uint16Artists length (separator: ,)
string[]Artists (UTF-8)
uint32Cover length
stringCover (base64 encoded)
uint32Song length (in µs)
uint32Current time (in µs)

Seek (event)

TypeDescription
uint32New position (in µs)

State (hybrid)

TypeDescription
uint8 (bool)New state (0: paused, 1: playing)

Final flaws

I'm aware of few cases that are not properly handled by the daemon.

What if Strawberry is not running ?

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.

Conclusion

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 !