banner
Previous Page
PCLinuxOS Magazine
PCLinuxOS
Article List
Disclaimer
Next Page

Make Your Own Streaming Internet Radio Program


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.







Previous Page              Top              Next Page