bbc_player.py - an exercise in simplicity with Python and Qt
BBC radio is available worldwide through a Flash player that even works on Linux so what brought me to write my own script to replace it? Flash sucks. The web player is slow and buggy (once stopped, it will resume playback on its own in about an hour, right when you’re having a VoIP call).
So I wanted a replacement. Something simple - select the radio station from a list and launch VLC with the corresponding ASX playlist URL. Luckily, Neil Cooper already compiled such a list. While we’re at it, let’s make it a GUI interface and get a taste of Qt through the PySide Python binding. Nothing fancy, no icons, no systray, no menus, not even a “close” button - the one provided by the window manager will do just fine.
Long story short: get bbc_player.py from github, chmod and execute it. Long story even longer: hang around to see the complexity behind these simple requirements.
The PySide API reference is hosted (temporarily?) on github so that’s where I went digging for info after getting through the beginner tutorials. Some vertically stacked buttons with the radio name as labels is the simplest UI I came up with. Since we have 56 stations, the container for those buttons needs to be scrollable. Simple, right? No need to mess with the Qt designer for this one, we’ll do it in code.
Our class inherits from QMainWindow and in the __init__() we set a title and a minimum size for our window:
- self.setWindowTitle(WIN_TITLE)
- self.setMinimumSize(300, 600)
To get our buttons to do something we need to connect the ‘clicked’ signal to a function of our own. We cannot pass arguments to this function, but we can get the clicked button object with self.sender() so we can pass what we need as custom attributes in the button. Not very elegant but good enough. Here’s our UI construction:
- self.scroll_area = QScrollArea()
- self.widget = QWidget()
- self.layout = QVBoxLayout()
- self.widget.setLayout(self.layout)
- self.scroll_area.setWidgetResizable(True)
- self.scroll_area.setWidget(self.widget)
- self.setCentralWidget(self.scroll_area)
- for name, url in STATIONS:
- button = QPushButton(name.replace('&', '&&'))
- button.args = {
- 'name': name,
- 'url': url,
- }
- button.clicked.connect(self.listen)
- self.layout.addWidget(button)
While we’re at it, let’s give the user the possibility to specify another player instead of VLC and any arguments that need to go with it. The argparse module provides all the features we need and then some. When we’re done with argument parsing we’ll have the results in self.player_prog and self.player_args:
- def listen(self):
- pressed_button = self.sender()
- for button in self.widget.findChildren(QPushButton):
- if button != pressed_button and not button.isEnabled():
- button.setEnabled(True)
- break
- pressed_button.setEnabled(False)
- # stop the running player instance before starting another one
- if self.player:
- if self.player.poll() is None:
- self.player.terminate()
- self.player.wait()
- cmd = [self.player_prog]
- cmd.extend(self.player_args)
- cmd.append(pressed_button.args['url'])
- try:
- self.player = subprocess.Popen(cmd)
- except Exception, e:
- msg_box = QMessageBox()
- msg_box.setText('Couldn\'t launch\n"%s"' % ' '.join(cmd))
- msg_box.setInformativeText(unicode(e))
- msg_box.exec_()
- pressed_button.setEnabled(True)
- self.setWindowTitle('%s - %s' % (pressed_button.args['name'], WIN_TITLE))
- def check_player(self):
- if self.player and self.player.poll() is not None:
- # the player has been stopped
- self.player = None
- self.timer.stop()
- self.setWindowTitle(WIN_TITLE)
- for button in self.widget.findChildren(QPushButton):
- if not button.isEnabled():
- button.setEnabled(True)
- break
Category: Python



Discussion
Nice post. Very concise and useful. I like your approach of attaching custom attributes to the QPushButton.
I tend to do this sort of thing using lambda. I'm not sure if one way is better than the other, just an observation:
button.clicked.connect(lambda: self.listen(name, url))
http://codrspace.com/durden/using-lambda-with-pyqt-signals/
I've also seen people solve this problem with functools.partial:
http://eli.thegreenplace.net/2011/04/25/passing-extra-arguments-to-pyqt-slot/
Yes, using the lambda is a valid alternative but self.sender() no longer works inside self.listen() - or inside the lambda for that matter.
One other thing: since the lambda's body is not evalued until the signal is triggered, a naive implementation would make all the buttons play the last radio station (because 'name' and 'url' point to it at the end of the loop).
The only way it works is something like this:
button.clicked.connect(lambda _button=button, _name=name, _url=url: self.listen(_button, _name, _url))
Yes, it's ugly...
Neat article.
For future reference:
http://blog.scoopz.com/2011/05/05/listen-to-any-bbc-radio-live-stream-using-vlc-including-radio-1-and-1xtra/
Leave a Comment :
Leave a Comment