Ubuntu Opportunistic Developers Week March 2010 - Writing a Rhythmbox plug-in - Stuart Langridge - Mar 2 2010

(12:00:58 PM) aquarius: Hi, everyone, I'm Stuart Langridge
(12:01:03 PM) aquarius: Today I'm going to talk about making a Rhythmbox plugin.
(12:01:17 PM) aquarius: but first I should say: cool talk about goocanvas, rickspencer3 :)
(12:01:21 PM) aquarius: If you're using Lernid, you'll see slides with code examples. If you don't have Lernid, don't worry; all the code for this plugin is available, and you can look through it to see what I'm talking about, or you can load the slide deck yourself from http://www.kryogenix.org/code/RBMicroBlog
(12:01:38 PM) aquarius: Specifically, I'm going to talk about making a Python plugin. If you're hoping for in-depth C knowledge, you're talking to the wrong dude.
(12:01:47 PM) aquarius: Feel free to ask questions in #ubuntu-classroom-chat, and I'll answer them at the end; if someone could collect them, that'd be great.
(12:01:58 PM) aquarius: or, actually, I'll use ClassBot :)
(12:02:03 PM) aquarius: What with this being Opportunistic Developer Week and all, I'm aiming for something which scratches a little itch.
(12:02:15 PM) aquarius: The itch I want to scratch is explained by a tweet I saw from @mandel_macaque, which said
(12:02:19 PM) aquarius: [SLIDE 2]
(12:02:24 PM) aquarius: ♥ Space Oddity by David Bowie #lastfm: http://bit.ly/15zv4
(12:02:33 PM) aquarius: and I thought: hey, that's quite cool, he's tweeting songs that he's playing that he likes.
(12:02:39 PM) aquarius: I'd like to do that. But I don't use last.fm, I use Rhythmbox to play my music.
(12:02:49 PM) aquarius: So why not have a "tweet this song" button, so I can hit it when a song that I like comes along?
(12:02:57 PM) aquarius: A Rhythmbox Python plugin comes in two parts: an .rb-plugin file, which describes your plugin, and a Python module.
(12:03:09 PM) aquarius: [SLIDE 3]
(12:03:18 PM) aquarius: Our first stage, then, is to set up the plugin itself.
(12:03:32 PM) aquarius: Create the Rhythmbox plugins folder, which you might already have:
(12:03:35 PM) aquarius: mkdir -p ~/.gnome2/rhythmbox/plugins
(12:03:47 PM) aquarius: Now, create a folder for our plugin, which we'll call RBMicroBlog
(12:03:53 PM) aquarius: mkdir -p ~/.gnome2/rhythmbox/plugins/RBMicroBlog
(12:04:02 PM) aquarius: In there create a __init__.py file:
(12:04:05 PM) aquarius: touch ~/.gnome2/rhythmbox/plugins/RBMicroBlog/__init__.py
(12:04:14 PM) aquarius: and an .rb-plugin file:
(12:04:15 PM) aquarius: gedit ~/.gnome2/rhythmbox/plugins/RBMicroBlog/RBMicroBlog.rb-plugin
(12:04:23 PM) aquarius: Your rb-plugin file has a specific format:
(12:04:27 PM) aquarius: [SLIDE 4]
(12:04:29 PM) edouard is now known as edouardp
(12:04:30 PM) aquarius: [RB Plugin]
(12:04:31 PM) aquarius: Loader=python
(12:04:47 PM) aquarius: Module=RBMicroBlog
(12:04:48 PM) aquarius: IAge=1
(12:04:50 PM) aquarius: Name=Microblogging
(12:04:52 PM) aquarius: Description=Microblog what you're listening to
(12:04:55 PM) aquarius: Authors=Stuart Langridge <sil@kryogenix.org>
(12:04:56 PM) aquarius: Copyright=Copyright © 2010 Stuart Langridge
(12:05:00 PM) aquarius: Website=http://www.kryogenix.org/code/rhythmbox-microblog
(12:05:19 PM) aquarius: The important lines in that are the two that tell Rhythmbox "this is a Python plugin", and "the module name for this plugin is RBMicroBlog".
(12:05:21 PM) aquarius: [SLIDE 5]
(12:06:07 PM) aquarius: Python, as you will (hopefully) know, treats a folder called X with an __init__.py file inside as a module named X.
(12:06:18 PM) aquarius: So, we have our RBMicroBlog folder, with __init__.py inside, which is therefore a Python module
(12:06:25 PM) aquarius: and also in that folder a .rb-plugin file which says "this plugin is provided by the RBMicroBlog module".
(12:06:33 PM) aquarius: Right now, then, you have a plugin which doesn't do anything. Which means it has no bugs, true, but isn't that useful.
(12:06:48 PM) aquarius: On to editing the Python. Opportunistic development is fun!
(12:07:14 PM) aquarius: Basically, we want our plugin to do this:
(12:07:16 PM) aquarius: 1. be a plugin that can be enabled and disabled
(12:07:17 PM) aquarius: 2. put a "tweet this" button on the toolbar, with the Gwibber icon in it, and a tooltip of "microblog this song"
(12:07:18 PM) aquarius: 3. when the button is pressed, work out which song is currently playing...
(12:07:23 PM) aquarius: 4. and use the Gwibber API to microblog a message saying "Listening to: One Vision by Queen"
(12:07:50 PM) aquarius: So, start with the basics.
(12:07:51 PM) aquarius: [SLIDE 6]
(12:08:00 PM) aquarius: A Rhythmbox plugin is built as a subclass of rb.Plugin, named the same as the plugin module.
(12:08:18 PM) aquarius: So, a basic plugin would look like:
(12:08:19 PM) aquarius: import rb
(12:08:22 PM) aquarius: class RBMicroBlog(rb.Plugin):
(12:08:22 PM) aquarius:     def activate(self, shell):
(12:08:32 PM) aquarius:         print "Activate!"
(12:08:34 PM) aquarius:     def deactivate(self, shell):
(12:08:36 PM) aquarius:         print "Deactivate!"
(12:08:49 PM) aquarius: Your plugin's "activate" function is called when the plugin is loaded, so it can do setup, and "deactivate" is called when the plugin is unloaded.
(12:08:58 PM) aquarius: So, start Rhythmbox -- your plugin hasn't been enabled yet, so in Edit > Plugins, find "Microblogging", and tick it to turn it on.
(12:09:01 PM) aquarius: [SLIDE 7]
(12:09:38 PM) aquarius: You might be thinking: where does the output from my print statement go?
(12:09:58 PM) aquarius: Rhythmbox hides output from plugins unless you want to see it. So, quit Rhythmbox, and restart it as "rhythmbox -D RBMicroBlog"
(12:10:11 PM) aquarius: Now, when you tick your Microblogging plugin on and off in Edit > Plugins, you should see output in the terminal:
(12:10:14 PM) aquarius: (15:34:36) [0x8756028] [RBMicroBlog.activate] RBMicroBlog/__init__.py:9: Activate!
(12:10:23 PM) aquarius: (15:34:38) [0x8756028] [RBMicroBlog.deactivate] RBMicroBlog/__init__.py:13: Deactivate!
(12:10:38 PM) aquarius: Now, you've got an .rb-plugin file and a Python module, and it's loaded. That's the basics of every Python plugin. The only hard remaining bit is to, y'know, actually write the code that does what you want.
(12:11:01 PM) aquarius: On, then, to stage 2: put a "tweet this" button on the toolbar.
(12:11:05 PM) aquarius: This is about adding some UI, and is documented at http://live.gnome.org/RhythmboxPlugins/WritingGuide#Adding_UI
(12:11:51 PM) aquarius: There are also more examples at http://live.gnome.org/Rhythmbox%20Plugins/Python%20Plugin%20Examples which is a very useful page.
(12:12:09 PM) aquarius: If you come across more things that you want to do in Rhythmbox, please add them to that page!
(12:12:19 PM) aquarius: (Also, adding those Rhythmbox plugin "snippets" to the python-snippets project in Launchpad would be pretty cool.)
(12:12:30 PM) aquarius: Adding UI requires two stages: first, you define where you want to add the UI with some XML
(12:12:36 PM) aquarius: [SLIDE 8]
(12:12:48 PM) aquarius: and then you actually hook up a gtk.ActionGroup to the UI, as defined by the XML
(12:12:52 PM) aquarius: [SLIDE 9]
(12:13:07 PM) aquarius: To be honest, you can just borrow that bit from another plugin. Understanding gtk.Actions and gtk.ActionGroups is fine for those that want to, but if you just want to add a toolbar button and a menu item, steal the code from somewhere else (like this plugin).
(12:13:37 PM) aquarius: Now, you'll have a button on the toolbar, and a menu item in the Tools menu. The button has no icon, though.
(12:14:10 PM) aquarius: A bit more cookbook programming, now: how to load an icon and add it. The icon we want is /usr/share/pixmaps/gwibber.svg
(12:14:54 PM) aquarius: So, some code to load that icon and make it available for your button
(12:14:56 PM) aquarius: [SLIDE 10]
(12:15:08 PM) aquarius: Again, don't worry too much about understanding this: the way Gtk deals with icons is pretty complicated. Borrow it for your own projects.
(12:15:30 PM) aquarius: Now, you have a button on the toolbar and a menu item, and we've connected them up to call a function self.microblog.
(12:15:42 PM) aquarius: Stage 3 is to implement that.
(12:15:56 PM) aquarius: Handler functions, like this one, will be called with two parameters, "event" and "shell".
(12:16:43 PM) aquarius: We want our microblog plugin to find out what's currently playing.
(12:16:49 PM) aquarius: There are three ways you might approach the problem of "what do I do to find out what's playing?"
(12:17:01 PM) aquarius: You could know already (if you're Jonoathan Matthew, the genius Rhythmbox maintainer)
(12:17:10 PM) aquarius: (er, Jonathan)
(12:17:28 PM) aquarius: (er, Jonathan)
(12:17:30 PM) aquarius: You could look it up in the documentation (there isn't all that much; the pages linked above cover some things, but not lots)
(12:17:39 PM) aquarius: Or you can do what I do, which is poke around in the Python objects.
(12:17:47 PM) aquarius: For this, enable the Python Console plugin in Rhythmbox, and then run it from the Tools menu.
(12:18:02 PM) aquarius: Now you have a Python console which is hooked up to Rhythmbox. You get a variable called "shell" for free, which is the Rhythmbox shell (and you'll notice that this shell is also being passed to your handler function).
(12:18:21 PM) aquarius: In Python, to list all the properties of an object, use dir(). So, in the Python console, say: dir(shell)
(12:18:30 PM) aquarius: [SLIDE 11]
(12:18:41 PM) aquarius: Hm, that "get_player" looks useful.
(12:18:45 PM) aquarius: So, say: shell.get_player()
(12:18:52 PM) aquarius: <rb.ShellPlayer object at 0xa3ff9dc (RBShellPlayer at 0x9b68800)>
(12:19:03 PM) aquarius: OK, and what's a "ShellPlayer"?
(12:19:06 PM) aquarius: >>> player = shell.get_player()
(12:19:10 PM) aquarius: >>> dir(player)
(12:19:11 PM) aquarius: and "get_playing_entry" again looks useful
(12:19:34 PM) aquarius: >>> player.get_playing_entry()
(12:19:44 PM) aquarius: <RhythmDBEntry at 0xb3543150>
(12:20:04 PM) aquarius: THe way Rhythmbox is set up is that there's a database, the RhythmDB, and each song in that database is defined by an "entry", of type RhythmDBEntry.
(12:20:25 PM) aquarius: So, we've got the "entry" for the currently playing song. But an entry doesn't help much; we want the artist and the title.
(12:20:45 PM) aquarius: Looking at the properties of the entry doesn't help much here, either.
(12:20:50 PM) aquarius: Fortunately, the documentation comes to the rescue: http://live.gnome.org/Rhythmbox%20Plugins/Python%20Plugin%20Examples#How_do_I_get_the_metadata_details_of_a_song.3F
(12:21:27 PM) aquarius: So, given an entry, we can get its title with shell.props.db.entry_get(entry, rhythmdb.PROP_TITLE)
(12:21:39 PM) aquarius: The first attempt at our microblog handler function, then, could look like this:
(12:21:45 PM) aquarius:     def microblog(self, event, shell):
(12:21:46 PM) aquarius:         entry = shell.get_player().get_playing_entry()
(12:21:47 PM) aquarius:         title = shell.props.db.entry_get(entry, rhythmdb.PROP_TITLE)
(12:21:48 PM) aquarius:         artist = shell.props.db.entry_get(entry, rhythmdb.PROP_ARTIST)
(12:22:22 PM) aquarius:         print "Listening to: %s by %s" % (title, artist)
(12:22:26 PM) aquarius: and that should, when we hit the button, print out the message to the terminal.
(12:22:55 PM) aquarius: And indeed it does (remember, rhythmbox -D RBMicroBlog !)
(12:22:59 PM) aquarius: (16:38:44) [0x907a028] [RBMicroBlog.microblog] RBMicroBlog/__init__.py:112: Listening to: You Shook Me All Night Long by AC/DC
(12:23:16 PM) aquarius: One more thing, though: what if there's nothing playing?
(12:23:30 PM) aquarius: Well, poking around in dir(shell.get_player()) a bit more in the Python console reveals "get_playing".
(12:23:48 PM) aquarius: So let's make the button not do anything if we're not playing.
(12:23:51 PM) aquarius: Just add one new line to the top of the microblog function:
(12:23:53 PM) aquarius: if not shell.get_player().get_playing(): return
(12:24:17 PM) aquarius: OK, so now we have a button which prints out our microblog message. The final stage is to use Gwibber to actually post it for us.
(12:24:39 PM) aquarius: Fortunately, someone's already done the work for us here. There's a snippet in the acire program which shows how to post a message via gwibber.
(12:24:48 PM) aquarius: (If you don't know about acire, there's a session about it on Thursday; it's a library of useful Python snippets, and it's great.)
(12:25:02 PM) aquarius: You can see the snippet itself, if you don't use acire, in Launchpad at http://bazaar.launchpad.net/~jonobacon/python-snippets/trunk/annotate/head:/gwibber/sendmessage.py
(12:25:25 PM) aquarius: So add a couple more lines to our microblog function:
(12:25:26 PM) aquarius:         gw = gwibber.lib.GwibberPublic()
(12:25:26 PM) aquarius:         gw.SendMessage("Listening to: %s by %s" % (title, artist))
(12:25:32 PM) aquarius: [SLIDE 12]
(12:25:39 PM) aquarius: and we're all done!
(12:25:53 PM) aquarius: This has been an incredibly brief tour of creating a Rhythmbox plugin, which ties into other cool bits of the Ubuntu platform.
(12:26:01 PM) aquarius: If this has interested you, a few things you might want to try are:
(12:26:03 PM) aquarius: Make the plugin tell you when it's successfully posted!
(12:26:07 PM) aquarius: Change the plugin to disable the button and menu item when nothing is playing (you'll want to connect to Rhythmbox's start playing and stop playing signals)
(12:26:16 PM) aquarius: Make "quickly create rhythmbox-plugin" work so no-one has to remember all the boilerplate
(12:26:25 PM) aquarius: OK, that's it for the talk. I'll take some questions now.
(12:26:41 PM) aquarius: strycore_lernid> IconSource, IconSet, IconFactory ... seems a bit confusing , at least to me
(12:27:01 PM) aquarius: it is. This is why I suggest just copying the code from an existing plugin, like RBMicroBlog :)
(12:27:15 PM) aquarius: you don't really have to worry about why it works, just that it does
(12:27:25 PM) aquarius: enli> what PROP stands for?
(12:27:39 PM) aquarius: "property", as ems confirmed on #ubuntu-classroom-chat
(12:27:55 PM) aquarius: so PROP_ARTIST is the "artist" property of a RhythmDBEntry (song)
(12:28:07 PM) mode (+v ClassBot) by ChanServ
(12:29:11 PM) aquarius: eviltwin> QUESTION: Surely we should disable the button when playback stops and reenable it when playback starts rather than a dirty, dirty if statement?
(12:29:21 PM) aquarius: hence the "extra credit" part at the end of the talk ;)
(12:29:31 PM) aquarius: definitely that would be the way to do it, yes
(12:29:53 PM) aquarius: but I've tried to keep RBMicroBlog tiny so that it's a simple thing that can be learned from
(12:30:12 PM) aquarius: if someone wants to trick it up into a proper application, that's a great idea :)
(12:30:24 PM) ClassBot: danyR asked: No way to get the plugin available to the masses? or in vanilla-lucid? just what i was looking for!
(12:30:32 PM) aquarius: well...
(12:30:39 PM) aquarius: to do that, you'd need to package it
(12:30:58 PM) aquarius: you can do that, certainly
(12:31:05 PM) aquarius: but I'm not the person you want to talk to about that
(12:31:35 PM) aquarius: one of the reasons that I'd really like to see "quickly create rhythmbox-plugin" is that it could set up the packaging stuff for you as well
(12:31:57 PM) ClassBot: stevec49 asked: For those looking to add plugin support to their own applications, do you think RhythmBox is a good model to follow?
(12:32:10 PM) aquarius: broadly, yes
(12:32:40 PM) aquarius: Rhythmbox plugins have a number of special methods (like activate and deactivate, but there are lots more) because there are lots of places in the Rhythmbox startup and usage procedures that plugins might want to plug into.
(12:33:21 PM) aquarius: I, personally, like plugins to be one file rather than two, so when I implement Python plugin loading for an app, I put the stuff that's in .rb-plugin into the Python plugin itself, as variables.
(12:33:48 PM) aquarius: because I like single-file deployment of plugins -- it's easier to install a plugin if it's one file that you drop in a folder.
(12:33:55 PM) aquarius: but that's flat out personal perference on my part :)
(12:34:09 PM) ClassBot: eviltwin asked: is this a plugin that you've written and made available somewhere? if so, where's the project homepage/code repo?
(12:34:26 PM) aquarius: http://www.kryogenix.org/code/RBMicroBlog
(12:34:33 PM) aquarius: the plugin code is downloadable from there
(12:35:03 PM) ClassBot: danyR asked: Shouldn't this be available in out-of-the-box Lucid? "What i'm listening to" sharing?
(12:36:18 PM) aquarius: if someone makes RBMicroBlog better, so that it's worthy of inclusion, sure. You might have missed out on getting it into Rhythmbox in lucid by default, but it would make it into universe, I'm sure, and could them be installed
(12:36:28 PM) ClassBot: oskude asked: does a rhythmbox plugin have access to the audio stream/data ?
(12:37:15 PM) aquarius: it does, yes; you can work with the underlying gstreamer code. http://live.gnome.org/RhythmboxPlugins/WritingGuide#Adding_elements_to_the_GStreamer_playback_pipeline has details about that.
(12:37:43 PM) ClassBot: stevec49 asked: Is the RhythmBox "shell" itself a python module?
(12:37:48 PM) aquarius: sort of.
(12:38:02 PM) aquarius: it's passed to your handler functions, and to your plugin
(12:38:11 PM) aquarius: and it's available in the Python console
(12:38:22 PM) aquarius: but it only exists if you're a Rhythmbox plugin
(12:38:39 PM) aquarius: you can't do "import rb.shell" from a random Python program and get access to control Rhythmbox
(12:38:54 PM) aquarius: if you want to control Rhythmbox from an outside application, use D-Bus.
(12:39:07 PM) ClassBot: quigs asked: is the UI stuff not included in the package you provided?
(12:39:22 PM) aquarius: does it not work, then?
(12:40:07 PM) aquarius: ha. quigs points out there is a bug in the deactivate handler in the plugin :)
(12:40:15 PM) aquarius: my fault.
(12:40:43 PM) aquarius: I'll take a look at that later
(12:41:27 PM) aquarius: Any other questions?
(12:41:48 PM) aquarius: There is a rhythmbox-plugins package in lucid, which you may already have installed, which contains lots of plugins
(12:42:17 PM) aquarius: and there are others at http://live.gnome.org/RhythmboxPlugins/ThirdParty
(12:42:47 PM) aquarius: and http://live.gnome.org/RhythmboxPlugins/WishList lsts some plugins that people want but don't have, if you want to work on something but aren't sure what
(12:43:21 PM) aquarius: what with Rhythmbox being the default media player, more plugins will make it do more stuff and generally be cooler, which I'm all in favour of :0
(12:43:55 PM) ClassBot: duanedesign_ asked: Where does the XML file go?
(12:44:10 PM) aquarius: I normally just put the XML in a string inside my plugin
(12:44:28 PM) aquarius: since it only defines where a button or menu go, it's normally pretty short
(12:45:09 PM) aquarius: if you want to put it in a separate file, then put the file in the folder with the plugin, and use shell.find_file("myxmlfile") to get its pathname, that way it'll work no matter where your plugin is installed
(12:46:24 PM) aquarius: OK, cool, no more questions?
(12:47:23 PM) ClassBot: strycore_lernid asked: It's a bit off topic, but in the third party plugins there are some that say "It is not an Internet radio server", GloveSoap and IceCast can do the job, but is there a way to send the audio stream to a server without opeing ports on a firewall ?
(12:47:28 PM) aquarius: erm
(12:47:38 PM) aquarius: there might be. I don't know the answer there, I'm afraid.
(12:48:04 PM) aquarius: the people in #gstreamer may be able to give you some pointers on whether there are gstreamer elements that can do that, which you could add to the pipeline
(12:48:54 PM) aquarius: so, we get to finish a little bit early and everyone can go get a cup of tea before kenvandine explains how to *really* use the Gwibber API, beyond the two lines I've just used!
(12:49:03 PM) aquarius: thanks, all

MeetingLogs/OpWeek1003/Rythmbox (last edited 2010-03-02 20:02:30 by pool-71-182-100-128)