by Peter Kelly (critter)
There was a thread in the PCLinuxOS forums recently about listening to internet
radio stations. I hadn't seen the thread but Paul Arnote, the magazine editor,
followed the thread and was particularly interested in a little one line script
posted by forum member dm+. It allowed the user to select and play a station
from the impressive list of stations provided by 'Great Little Radio Player', a
nice little utility that can be found, free of cost, in the PCLinuxOS
repositories.
Paul thought that this would make a good basis for a magazine article. His idea
was to expand on this method to provide a tray resident utility that would
enable a user to build a list of favourite internet radio stations and then
select one for playing but then minimise to the tray when not required.
Unfortunately, Paul's busy work schedule, coupled with looking after his young
son and producing the magazine, meant that he couldn't really find the time to
develop the idea and so he asked me if I would like to tackle it. This is an
excerpt from an e-mail he sent me:
"I kind of got the idea from the
radio station thread in the forum that's currently going on. What sparked
the idea was a bash command by dm+ (IIRC, it appears on the third page of the
thread). He uses Zenity to select which online stream to play. So, I was
thinking ... what about a bash script that created a "persistent" window on the
desktop, where the user can switch between the streams in their list of
"favorites" stations? Plus, when you "minimize" the window, it minimizes to the
notification area. Once there, left clicking the mouse gives you the choice of
redisplaying the "persistent" window, or changing the station by selecting one
of the stations in your favorites list, which is then displayed in the
left-click menu. Right clicking with the mouse will give you the choice of
redisplaying the "persistent" window, or exiting the program. I'm not sure how
problematic this would be, but think it should be "doable" from a bash script.
You could get by adequately by omitting the listing of the favorites list in
the left-click menu, and just redisplay the "persistent" window. From the
redisplay of the window, the user could either select another station, or exit
the program. The command to call to play the stream is mplayer -playlist [URL]
... and it works very well, at least from a command line prompt."
This article is intended to present a basic script that may be useful to some
readers, and to demonstrate the use of some intermediate shell scripting
techniques. I will attempt to explain the code and my reason for including some
of the features. The code is not too demanding but some basic scripting
experience is presumed.
Defining the script
The first thing that I did was to make a list of features to be included.
- This should be a bash script that all users could run and modify from a
standard installation of PCLinuxOS. Editor's Note: It would also be a good idea to already have
GLRP (Great Little Radio Player) installed. If it's not installed, you can
install it from Synaptic.
- Any tools or programs used by the script should, if not already
installed, be available from the PCLinuxOS repositories.
- The script should be a tray resident application with a tooltip and
icon.
- Left or right mouse clicking on the icon should be captured and produce
some action.
- One of these actions should be to display a menu of available
functions.
- Exiting the application should also stop the stream playback.
- The first time the application runs should be detected and the necessary
files and directories created and initialized.
- Menu functions should include selecting a favorite station and adding a
new favorite from the master list provided by glrp (Great Little Radio
Player).
- There should be some form of display to show the name of the currently
selected radio stream.
Most of the above list can be accomplished using standard shell scripting
techniques, but being tray resident and detecting mouse clicks to provide a
menu is not something that I have previously attempted. Being lazy and not
wishing to re-invent the wheel, I turned to the PCLinuxOS repositories to
search for something that would do some of these things for me. I discovered a
nice little utility called 'alltray'. I re-named the script posted by dm+ to
'radio_play' and ran this line.
First Attempts
alltray ./radio_play -m "Edit List:zenity --text-info --editable \
--filename=station_list"
While this produced promising results, it wasn't really up to the task at hand,
so I looked at the original script once more. The script works just fine, but
is not resident in the tray and does not provide menus. Zenity is an excellent
set of routines that I have used on many occasions, but in 2009 it was forked
and reappeared as yad (yet another dialog), with many new features added. Of
particular interest in yad is the notification tool, which promised to solve
most of the tray area problems. I decided to use yad.
Starting to write the script
Where to start? I always feel better when I have something written to disk and
I always give my scripts their own folder, at least until they are complete. I
created a new folder and changed to it.
Mkdir ~/radio_streamer; cd ~/radio_streamer
The application needs an icon so I searched /usr/share/icons for something
suitable, copied it to my new folder and renamed it streamer.png.
Next I opened a text editor and created a file named radio_streamer.sh with the
single line of text
#!/usr/bin/env bash
That line will tell the system to use the bash shell to interpret the code that
follows. The contents of my folder now looked like this:
ls
radio_streamer.sh streamer.png
Now all I have to do is write the code.
A lot of the problems that people have when writing scripts comes from a lack
of order in the code. I like to declare any variables right at the start and
assign any strings that I might use to some of these variables. This helps to
make the final code easier to read.
YAD
The key to this script is the yad notification object, so I'll explain how I
implemented it in this script. A yad application is called with the name of the
application preceded by a double dash. This is followed by a series of relevant
double dash options to control the application. This can lead to a long and
unwieldy command and so I use the backslash line continuation character to
improve readability. The bash shell treats such text as a single line. There
are a lot of possible options that can be applied to a yad notification, but
the line for this script looks like this:
yad --notification \
--kill-parent \
--listen \
--image="$ICON" \
--text="$HINT" \
--command="bash -c l_click" <&3 &
The first line is the one that actually starts the notification. Next is
--kill-parent, which will send a signal to the parent process (bash) when the
yad notification exits. The default signal is SIGTERM, so here the bash process
which started yad is terminated when yad exits.
Line 3 is the clever bit --listen tells yad to listen on its standard input
(stdin) for commands. We haven't yet specified a menu for a right mouse click,
which could be done with the option --menu=string, but with this option we can
instead send the menu string to stdin, which allows us to dynamically change
the menu on the fly. The yad notification commands that can be sent to stdin
are: icon, tooltip, visible, action, menu and quit. So we could, for example,
change the icon according to conditions.
The icon is set with the option --image=string, and the tool-tip with the
option --text=string.
In the last line, we specify what command should be executed when the tray icon
is left clicked, to use file descriptor 3 for its stdin and then to run in the
background returning control of the script to bash.
Using file descriptors and pipes for redirection
A file descriptor is a pointer to a file or data stream and the available
pointers are numbered from 0. Normally stdin, stdout and stderr are assigned to
file descriptors 0, 1 and 2. This is how the system knows where to read and
write stuff. As we don't want information from other sources going to our
notification object, we have to temporarily change things. Our stdin is
redirected to FD3, all other process still use FD0 for their stdin.
Before we can use this redirection, it needs to be set up, and to do this, we
create a special type of file known as a pipe. A pipe is a 'first in first out'
object. just like a pipe in real life. What is pushed in first at one end is
first to emerge at the other end. These are also known as "fifo"s, and the
Linux command to create one is mkfifo filename. Once we have our pipe file, we
can use standard file redirection to associate FD3 with the pipe
mkfifo "$PIPE"
exec 3<> "$PIPE"
Declaring the variables
That's the difficult bit out of the way, but let's just back up a bit. We have
referred to 'filename', 'string' and some variables, so these should be
declared before we use them and, as I stated earlier, I like to do this at the
start of the script. After the bash header line in our otherwise empty script
file add
HINT=" Radio streamer - left-click exit - right click menu "
This will be the tool-tip defined in the yad --notification block above, and is
displayed when the mouse pointer hovers over the icon.
The icon to be used in the script is resident in the same folder as the script,
and to tell bash where exactly that might be we can use the following line of
code
$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/
Now you may or may not understand how that works, but don't worry about it. We
all use gadgets everyday, such as phones, microwave ovens or whatever, without
necessarily understanding how they work. I see no difference in programming.
This is a gadget that works to return the directory from which the script was
executed, so use it and add it to your toolbox. Actually, if you take it piece
by piece, it is not too difficult to understand, but it is difficult to
remember, so I copy and paste it from a file of similar little 'time-savers.'
The variable holding the path and the filename of the icon then becomes
ICON=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/streamer.png
Add that line to the script file also.
The string that makes up the menu that is displayed on a right mouse click
follows the format
title!command|title!command|...
Title is a string that will be displayed on the drop down menu, the ! Signals
the end of that string, command is the command that would be executed if that
menu item were selected and | separates menu items. Our menu variable
definition becomes
MENU="Play from a favorite station! bash -c play_favorite|\
Add a station to your favorites! bash -c add_station|\
Display current station name! bash -c show_name"
Those strange commands, 'play_favorite', 'add_station' and 'show_name', that we
instruct the menu items to execute, will be explained later. The MENU string is
echoed to the notification object through its stdin (FD3)
echo "menu:$MENU" >&3
Obviously this must be done in the script after the tray object has be set up.
For the pipe file, we need to use a filename that is unique, and of course
there is a Linux command that will help us to do this called mktemp. Again it
is of no real consequence to understand exactly how this works so add this line
to the script file
PIPE=$(mktemp -u /tmp/r_streamer.XXXXXXXX)
We need to tell our script where to find the list of stations and this, when
'Great Little Radio Player' is installed, can be found as
/home/$USER/.config/glrp/stations.csv
Lines like that in the main code of the script do not help readability, and so
we assign the string to a variable.
STATION_LIST="/home/$USER/.config/glrp/stations.csv"
We also need somewhere to store our own configuration files and our list of
favorites. The directory will be:
PREFIX="/home/$USER/.config/radio_streamer"
And the favorites file:
MY_FAVORITES="$PREFIX/favorites"
The functions - first_run
When the script starts, it should check if this is the first time that it has
been executed, and whether the files and directories in which it will look for
its files do actually exist and contain some useful data. If not, then they
must be created and some data added.
To achieve this, I wrote the function named first_run. Functions are simply a
block of code that is executed, when called, and may, or may not, return a
value. The definition of the function must appear before it can be called. The
order in which the functions are defined is unimportant, as the code is read
into memory and executed as needed, making functions very efficient.
The first_run function is designed to set up the necessary file structure for
the rest of the script to access, and can therefore be the first function to be
defined. However, after writing the function, I found that it was dependent
upon at least one other function in order to operate. This is how I defined the
first_run function. The line numbers are not part of the function. They are
there for reference only.
The comments should explain most of the workings of the function. The inclusion
of comments make later modification of the code much easier.
Lines 1-5 look for the existence of the directory, and create it if necessary.
Lines 6-11 look for the favorites file. If that exists, then there is no need
to continue. If not, then it is created and the function continues.
Lines 13-20 display an explanatory yad dialog. Pressing the OK button causes
yad to return a value of 1, while pressing cancel returns 2.
Lines 21-30 check the value returned by the dialog in the bash variable $?,
which always contains the exit code of the last executed command. When control
returns from the first_run function to the main script, the value returned by
the exit statement can be checked, and if the user has pressed the cancel
button, then the entire script can be abandoned. If the user pressed OK, then
the script is allowed to continue.
This is the code that actually executes the first_run function and takes the
appropriate action. All other functions are executed by interaction with the
tray object.
If OK was pressed, then the command add_station is executed. As there is no
standard command of that name, then we have to create one. That is done by
defining a function with that name. Having added a station to our list of
favorites, we first stop any activity that mplayer is currently engaged in,
play our selected stream, and save the url to the file current_url to be used
on next startup. If the cancel button was pressed, then the function exits and
the function returns a value of 1. It is important to be clear about where a
returned value is from.
The add_station function
Now we need to define the add_station function which is simply
The function add_station calls another function named get_station, exits if the
user pressed enter without selecting a station and then it echoes the values of
some variables, separated by commas, to the end of our favorites file. We
haven't seen those new variables before because they were created and assigned
to in the get_station function, which we haven't yet defined. If you look back
to when we defined the MENU string you will see that add_station is one of the
commands we requested to be executed.
Which came first?
The first_run function calls the function add_station, and add_station calls
another function named get_station. This chicken and egg situation of which
function to create first and of calling a function that has not yet been
defined can be confusing, and so a little thought is required to determine the
structure of the script. The method that I employ is to use an editor that
supports 'code folding.' There are many such editors available, from the
notorious vim to graphical programmers editors such as scite and KDE's own
Kwrite. Code folding allows you to write a function header followed by an
explanatory comment, and only add the code once you know what you are going to
put in there. You can fill the function body with more comments as you think of
them, and hide away all but the header when you don't need to see it. This
makes it much easier to see the structure of the script.
The screenshot above is shown using an editor called Editra (it is in the
PCLinuxOS repositories). Clicking on the plus and minus signs expands or
collapses the code.
Exporting
Also in the screen shot, lines 123-126 export the functions. When a function is
called, it is executed in a sub-shell which does not inherit the parent shells
environment. Exporting functions and variables makes them available for use in
sub-shells. Functions must be exported with -f option. By doing, this the
function add_station can call the function get_station, which will eventually
be defined in the parent shell. If the function get_station exports the
variables s_name, s_url, etc., they will be available to the function
add_station.
We need to export some of our variables from the main script to make them
available to our functions. Add the following between the variable declarations
and the function code.
# Make some variables available globally
export STATION_LIST
export MY_FAVORITES
export PIPE
Note that the exported variables are only copies of the originals. If the
function add_station alters one of the inherited variables, the original
variable in the function get_station is unchanged.
The get_station function
At first glance, this looks to be a complex function but most of the code is
there for setting up a yad multicolumn list object.
The list of stations in the file pointed to by the variable STATION_LIST is in
the form of fields that are both double quoted and comma separated. Each line
is of the form "Name","URL","Genre","Location","Favorite"
The yad list object expects the column data to be unquoted, to be separated by
newline characters, and to be supplied without the quotes. Each record of
fields must be separated by a vertical bar character '|'. Lines 2-4 perform the
conversion.
In line 2, the stations file is piped to the next command in line 3.
Line 3 uses awk, setting the field separator to be a comma. Awk then prints out
the first four fields in each line of the file, separating each of them with a
newline character, and ending the output with a vertical bar character. The
fifth field is not used by us, and is therefore simply discarded.
In line 4, the sed utility substitutes the double quote with nothing s/"//. The
final "g" means globally, so replace every occurrence in the line. The effect
of this is to remove all of the quotes.
Now that we have the data in the form that yad can accept it, we can pipe it to
yad --list in line 5.
Line 6 sets the dimensions of the list dialog
Line 7 sets the text that will appear in the title bar of the dialog window
Lines 8-11 set the titles for the columns
Line 12 tells yad not to display the URL column. Although we do need it, we do
not need to see it.
Yad will try to use 'pango markup' to display the text. This is useful if you
want colored or bold text. Unfortunately, our import file may contain
characters such as '&,' which it would attempt to interpret as markup
symbols. To prevent this, we turn off the mark up feature in line 13.
Some of the data may be too long to fit in the column. The --ellipsize option
allows us to show continuation as an ellipsis (...), and also to set the
position of the ellipsis. I chose to set it at the end of the field in line 14
to ensure the start of the field is always displayed.
The column that is of most interest will probably be the station's name, and in
line 15, this column is expanded to show as much of the name as possible. This
is a compromise, as to some extent, the width of the columns is determined by
the length of the column name and the geometry of the dialog. Expanding this
column ensures that any 'spare' space is directed here.
Line 16 tells yad which columns of the selected data line to output. A zero
here means all columns, including the URL column that we decided not to
display. This ends the list dialog definition, and the output is then piped to
the sed command in line 17. This sed command replaces the vertical bar
characters supplied by yad with commas, which are easier to work with in the
next awk statements. Lines 18-21 each assign one output field to a variable and
lines 23-26 export the variables.
This is the other function called from the right click menu. The file piped to
the list dialog is now our favorites file. The title has changed and mplayer is
called with the selected station URL. The two options passed to mplayer remove
remote control access and turn off almost all text output, as neither of these
features are of any use to us.
The awk command in line 3 is interesting. I'm not going to explain it, and it
is another of my little 'time-savers.' It removes duplicate lines. The
favorites file may well contain duplicates. That isn't a problem in itself, but
we don't need to display the duplicates here. If you want to clean up the file,
use a command such as
uniq -u <(cat favorites) > new_favorites ; mv -f new_favorites favorites
This line uses process substitution to filter out duplicate lines from the
contents of the original and write the filtered data to a new file. The old
file is then overwritten by the new file. Make a backup first!
Line 17 removes the final vertical bar character from the output produced by
yad. Line 19 saves the newly selected url to the file current_url for future
use. Lines 20 & 21 produce the new output, and line 22 calls the function
show_name, which we haven't yet written.
The show_name function
This function is called by the play_station function, by the right click menu,
and at the very beginning of the scripts operation, when the last played
station is recalled from the file current_url.
For this function, I decided to use a little pop-up notification utility named
notify-send, which pop up a little box in the corner of the screen for a few
seconds, and then gets out of the way.
Notify-send takes the following options in this function:
-t a time period in milliseconds to remain visible.
-i in icon filename to display.
And a string of text to display.
show_name() {
current_name=$(grep $(cat $PREFIX/current_url) $PREFIX/favorites | \
awk -F, '{print $1}')
notify-send -t 3000 -i $ICON "Now playing from $current_name"
}
To get the station name of the currently playing URL, we use the grep command
to search for it in our favorites file, and then use awk to isolate the station
name and put it in the variable current_name. The notify-send command uses
$ICON for the icon to display, a time of three seconds (3,000 milliseconds) to
remain visible, and displays some text constructed from some literal text and
the value held by the variable current_name.
The l_click function
Left clicking on the objects icon gives us a means to exit and to terminate
mplayer. This is too easy to do unintentionally and so we need to confirm that
this is what the user really wants.
The function is referenced in the notification setup block by the --command
option. When called, the function displays a confirmation dialog, and if the
user pressed No, then the function is exited and the script continues. If Yes,
is selected then the 'quit' command is sent to yad via FD3, the mplayer process
is terminated, and the pipe file is deleted. The script then ends.
The script structure
To recap, the basic structure of the script should look like this:
bash header
variable declarations
variable exports
function definitions
function exports
call first_run function
create the pipe file
redirect data through the pipe mechanism
set up the tray mechanism and background it
echo the menu string to the tray object.
If you use an editor that offers code folding you should be able to see this
structure with very little scrolling.
Bugs!
Every decent program has bugs, at least that's my experience. The first one
that I noticed in this script was that on changing stations the previous output
continued along with the new output. This was solved by adding the line
killall mplayer
before the line that starts playing the new stream in the play_favorite
function.
The next problem was that some stations would not play even though the same
station would play when called directly from the command line. This was caused
by yad adding a vertical bar character to the end of the url that got passed to
mplayer. A simple sed statement filters this out. The end of the play_favorite
function now looks like this.
More bugs will undoubtedly appear, and fixing them may introduce other,
unforeseen problems. This the joy of programming.
Adding features
Mplayer is designed to play movies but is also very capable when streaming
audio, as we have done here. There are many more features documented in the man
pages that you may like to add. For example, mplayer is capable of capturing
the audio stream and writing it to a file. This may be useful if something that
you want to listen to is being broadcast at an inconvenient time. To save the
stream to a file named stream.mp3 use a line like this:
mplayer -dumpaudio -dumpfile stream.mp3 stream_url
Adding a real URL in place of stream.url. This could be launched and terminated
by a tool such as cron or at. There may be legal considerations in your locale
for storing unlicensed digital works.
The complete script
Below is the complete script. You can also download it from the magazine
website, here.
Editor's Note: It wouldn't be too much work to make this script even more
"feature rich." You could add an ability to manually enter new stations,
instead of relying on only those that are packaged with GLRP. You could also
add in the ability to edit the "Favorites stations" list (as it it now, you
will need to launch the ~/.config/radio_streamer/favorites list in a text
editor to delete the stations you no longer wish to listen to from your
favorites list). For these and other enhancements, we will leave as a learning
exercise, should you choose to accept the challenge.
|