Pete's Log: Writing a RhythmBox Plugin in Python

Entry #1953, (Coding, Hacking, & CS stuff)
(posted when I was 42 years old.)

I use RhythmBox as my music player because that's the default that Debian gave me. And from what I can tell, it's the default in several other distributions. It does everything I want it to, so I haven't had any reason to switch. Since it supports plugins, it seemed reasonable enough to target my MQTT music status plugin at it. It works, so I'm happy. And it didn't actually take too much time to reach the first working prototype. But I decided it shouldn't have the MQTT settings hardcoded, and thus I lost several hours to figuring out how to make my plugin have a preferences dialog. I guess it was worth it in the end, so "lost" isn't really the right word.

I did have a few things going against me: I don't really know Python and I think it's been about twenty years since I last did any Linux desktop development. But it also didn't help that the documentation is sparse and also on more than one occasion incorrect. I do want to give a shout out to the Paho MQTT Python library which is well documented and was incredibly easy to use. I had expected the MQTT part of this project to offer the most resistance, but it was a complete non-issue.

Anyway, here are a few notes on Rhythmbox Plugin development in Python.

Preferences

The Rhythmbox Plugin Writing Guide makes no mention of how to store settings for your plugin. If you go into the Rhythmbox preferences and click on the plugins tab, there is a built-in mechanism for plugin preferences. The Preferences button on the dialog will be enabled or disabled based on the plugin supporting preferences. So I took a look at the source for a few plugins that supported preferences and copied some code from those and got something to work. Once I had something working, a fortunate DuckDuckGo search for "do_create_configure_widget" led me to the plugin documentation for Gedit, which apparently uses a similar plugin architecture.

So the way to make Rhythmbox see your preferences support is to make your class inherit from PeasGtk.Configurable and then to implement a method called do_create_configure_widget which returns a widget for your dialog. This can be done either in a separate class from your plugin (which is then simply imported in your main plugin python file) or directly in your plugin class. I opted for the latter, but most Rhythmbox plugins seem to do the former.

The backing store for the preferences can be any persistent storage of your choice. I found several different approaches among different plugins I looked at, but the most common one seems to be to use Gio.Settings, which is also what Rhythmbox itself uses for its settings. To define your settings schema, you need to create a schema file with a filename that ends in .gschema.xml. This file needs to be placed somewhere GLib will look for it and compiled with glib-compile-schemas. Most Rhythmbox plugins appear to install their gschema.xml file into /usr/lib/glib-2.0/schemas, but I didn't like that since my other plugin files can all be installed in my user home directory. I shouldn't need to sudo to install my plugin.

Some persistent searching led to this askubuntu answer and the $HOME/.local/share/glib-2.0/schemas/ path. Much better. So installing my schema looks like this now:

SCHEMA_PATH="${HOME}/.local/share/glib-2.0/schemas/" mkdir -p $SCHEMA_PATH cp "${SCRIPT_PATH}/org.gnome.rhythmbox.plugins.rbmqtt.gschema.xml" "$SCHEMA_PATH" glib-compile-schemas "$SCHEMA_PATH"

To create the .ui file for my preferences dialog, I used glade. It crashed a couple times and kept bugging me to take a survey, but it was still easier than editing the xml myself.

Getting status updates

The two main things I publish to MQTT are the current status (playing/paused/inactive) and the current song. You can get notified when these change by connecting to a couple events (probably in your do_activate method:

shell = self.object shell_player = shell.props.shell_player self.psc_id = shell_player.connect("playing-song-changed", self.playing_song_changed) self.pc_id = shell_player.connect("playing-changed", self.playing_changed)

The callbacks then look something like this:

def playing_changed(self, sp, playing): print("playing" if playing else "paused") def playing_song_changed(self, shell, entry): if (entry is None): artist = "" album = "" title = "" else: artist = entry.get_string(RB.RhythmDBPropType.ARTIST) album = entry.get_string(RB.RhythmDBPropType.ALBUM) title = entry.get_string(RB.RhythmDBPropType.TITLE) print(f"Now playing {title} by {artist} from {album}")

Getting status at activation

The above callbacks get me 99% of what I want to know. The only issue is I don't get anything posted to MQTT until something changes. To get the current status, I found the following:

shell = self.object shell_player = shell.props.shell_player # get the current song entry = shell_player.get_playing_entry() # get current playing status - not sure why get_playing returns a tuple, but so it is playing = shell_player.get_playing()[1]

Putting it all together

Anyway, I made a rb-mqtt-plugin webpage and I put the source code on Codeberg in case anyone else wants it or just wants to see all the above in action. It's only been tested on one system and for one use case, so it's quite likely to be buggy. Enjoy.