📜 ⬆️ ⬇️

Automating Internet Radio on Linux

Hi, `whoami`.

In this post I will tell you about one of the methods of automation of Internet broadcasting - not the most reliable, but the most budget. Immediately I warn you that this system is sharpened to use it under Linux, although with the help of the “carpentry tool” familiar to many, you can also implement it under Windows. This article is designed for beginners IT-Schnick, so many points I tried to "chew". How I did it, it's up to you, my dear reader, but if I managed to interest you, please under the cat.

Foreword


So, first let me describe the goals that I set for myself:
1. Broadcasting non-stop 24/7/365 (not counting the power outages in the house).
2. Broadcast schedule. That is, at a certain time interval of the day, compositions of a certain style / genre should be played on the air.
3. Support hot plug and unplug master / dj.
4. Site Requirements:
4.a. A modest implementation of voting for sounding compositions and, accordingly, the rating TOP-20/30 / how many.
4.b. Information about the current track, the current style / genre and, if necessary, the number of listeners.

Now a little about what was in my “stash” (more precisely, in the closet):
- 2003 home computer, AMD Athlon 1.8 GHz, which has long been working as a home server (by the way, I reduced the clock frequency to 1.1 GHz to save power);
- Gentoo Linux operating system;
- access to the global network ~ 10Mbit / s + dedicated IP;
')

Setting up an Icecast server


So let's go. I will not describe the installation of programs, because in most distributions, they are available in the repositories and installed / assembled as a single command.

Icecast 2.3.2 was chosen as the server, as the source client for non-stop ices (I don’t remember the version).

After installation, you need to configure Icecast as follows.

File /etc/icecast2/icecast.xml:
<icecast> <limits> <sources>2</sources> <burst-size>32768</burst-size> <threadpool>5</threadpool> </limits> <authentication> <admin-user>admin</admin-user> <admin-password>_</admin-password> </authentication> <directory> <yp-url-timeout>15</yp-url-timeout> <yp-url>http://dir.xiph.org/cgi-bin/yp-cgi</yp-url> </directory> <directory> <yp-url-timeout>15</yp-url-timeout> <yp-url>http://www.oddsock.org/cgi-bin/yp-cgi</yp-url> </directory> <hostname>_IP_</hostname> <listen-socket> <port>__</port> <!—  8000 --> </listen-socket> <fileserve>1</fileserve> <!--,    :   Icecast,    ,  ,     xml-    ,    --> <paths> <basedir>/usr/share/icecast</basedir> <logdir>/var/log/icecast</logdir> <webroot>/home/www/icecast</webroot> <adminroot>/home/www/icecast/admin</adminroot> <alias source="/" dest="/status.xsl"/> </paths> <logging> <accesslog>access.log</accesslog> <errorlog>error.log</errorlog> <loglevel>3</loglevel> </logging> <!--   -.       , ..      ices,     . --> <mount> <mount-name>/non-stop</mount-name> <password>_-</password> <max-listeners>___</max-listeners> <charset>cp1251</charset> <public>0</public> <stream-name>_</stream-name> <!-- , Habr.FM Non-Stop --> <stream-description>24/7 Non-stop music</stream-description> <stream-url>_</stream-url> <genre>Electronic</genre> <!-— --> <bitrate>128</bitrate> <type>audio/mpeg</type> <subtype>mp3</subtype> <!--   mp3,    . --> <hidden>0</hidden> </mount> <!--    mountpoint  ,        --> <mount> <mount-name>/live</mount-name> <password>__</password> <max-listeners>100</max-listeners> <!--     : ,        mountpoint /live,      ,   ( )   -.      ,        /live.     . --> <fallback-mount>/non-stop</fallback-mount> <fallback-override>1</fallback-override> <fallback-when-full>0</fallback-when-full> <charset>cp1251</charset> <public>1</public> <stream-name>_</stream-name> <stream-description>_</stream-description> <stream-url>_</stream-url> <genre>Electronic</genre> <!--   --> <bitrate>128</bitrate> <type>audio/mpeg</type> <subtype>mp3</subtype> <hidden>0</hidden> </mount> <security> <chroot>0</chroot> <changeowner> <user>icecast</user> <!-- ,      Icecast --> <group>nogroup</group> <!--   --> </changeowner> </security> </icecast> 

Ices setting


With the setting of ices everything is easier:

File /etc/ices.conf:
 <ices:Configuration xmlns:ices="http://www.icecast.org/projects/ices"> <Playlist> <!--      (    ). --> <File>/home/PUBLIC/Music/playlist.m3u</File> <Randomize>0</Randomize> <!--   , ..    . --> <Type>builtin</Type> <Module>ices</Module> <Crossfade>1</Crossfade> <!--   ,   (.  <Reencode>) --> </Playlist> <Server> <Hostname>localhost</Hostname> <Port>_</Port> <Password>_</Password> <Protocol>http</Protocol> </Server> <Execution> <Background>1</Background> <Verbose>1</Verbose> <Base_Directory>/tmp</Base_Directory> </Execution> <Stream> <!--   <Server> (. ) --> <Server> <Hostname>localhost</Hostname> <Port>_</Port> <Password>_</Password> <Protocol>http</Protocol> </Server> <Mountpoint>/non-stop</Mountpoint> <!--       , ..    . --> <Name>_</Name> <Genre>Electronic</Genre> <!--  --> <Description>24/7 Non-stop music</Description> <URL>_</URL> <Bitrate>128</Bitrate> <Public>1</Public> <!--    . ices        ,      ,      . --> <Reencode>0</Reencode> <Samplerate>-1</Samplerate> <Channels>2</Channels> </Stream> </ices:Configuration> 

So, the Icecast server is configured, and you can already start it (usually, /etc/init.d/icecast start).
Ices is also set, but it's too early to start it, because there is no playlist for non-stop.
Actually we will fix it now ...

Playlist Formation


A small preface. I was not raising this radio station alone, but with a friend who has a much larger supply of music on his computer than I did. Previously, a virtual private network (VPN) was made between our computers and the server, so we could safely share files. It was decided to store all the tracks for non-stop in a separate directory on the server, which is available in the samba-ball, and in which my colleague can download the tracks (and maybe delete).

The directory structure is simple:

Music
--Genre1
---- File1.mp3
---- File2.mp3
---- ...
---- playlist.m3u
--Genre2
---- File1.mp3
---- File2.mp3
---- ...
---- playlist.m3u
...
playlist.m3u

Those. There are several subdirectories for various styles in the main Music catalog (remember, I was talking about a non-stop schedule?). Each such subdirectory contains mp3-files and one playlist.

So let's start with a small BASH script for creating playlists for all styles.

Music_find.sh file (thanks to differentlocal for simplifying my script)
 #!/bin/bash #     MUSICDIR=/home/PUBLIC/Music cd $MUSICDIR for i in *; do cd $MUSICDIR/$i find `pwd` -name "*.mp3" > playlist.m3u done 

Note: for people far from electronic music, I explain that Breaks, Chill, Hardcore are just styles of electronic music.

And since my colleague is far from Linux, and cannot access SSH and run this script after updating the contents of the directory, it was decided to entrust this mission to the Great Krona:
 # crontab -e 10,40 * * * * /root/scripts/radio/music_find.sh 

Now every 30 minutes the script will run and update playlists.

However, if you haven’t yet forgotten, the “main” playlist was specified in the ices config, which is not formed in this script.

Here you need to remember the schedule. The idea is simple to ugliness: at a certain time (according to the schedule of the broadcast) copy and replace the desired playlist in the root directory of the music (in my case / home / PUBLIC / Music). At first I thought to implement the schedule completely on BASH, but then I remembered that good Kron is always ready to help us and do all the dirty work for us. Thus, a script appeared that implements the change of the playlist according to the schedule.

However, first lyrical digression ...

Do you remember when configuring ices I turned off the randomization function? You probably wondered why? There are two reasons:
1. God knows how ices performs randomization. I used to (sorry) as much as possible to control the situation. Therefore, it is better to do randomization the way you want your soul to calm down.
2. If you read this article, you probably know the term "jingle". So, if it is intended to insert jingles on the air (for example, every three tracks), then ices is powerless, because he does not know what it is. This is another reason to write your own randomization program.

BASH, of course, is a good thing, but for this task I chose the C ++ language. Below is the source code of a C ++ program that simply reads the contents of a playlist file created earlier (the name is passed to it as a parameter), mixes and writes to the same file.

The source code of the randomization program:
 #include <iostream> #include <fstream> #include <cstdlib> #include <ctime> #include <string> #include <vector> using namespace std; int main(const int argc, const char *argv[]) { if (argc<2) { cout << "ERROR: no argument recieved." << endl; return 1; } vector<string> list; string line; ifstream infile(argv[1]); if (infile.fail()) return 1; cout << "Using file: " << argv[1] << endl; while (!infile.eof()) { getline(infile,line); list.push_back(line); } infile.close(); cout << "End of file reached." << endl; int n = list.size(); if (n>1) { cout << "Begin shuffle." << endl; srand(time(0)); string temp; for (int i=0; i<(n-1); i++) { int r = i + (rand() % (ni)); temp = list[i]; list[i] = list[r]; list[r] = temp; } cout << "Finished shuffle." << endl; ofstream outfile(argv[1]); for (int i=0; i<n; i++) outfile << list[i] << endl; outfile.close(); cout << "File succuessfully updated." << endl; return 0; } return 1; } 

In this program, I did not implement jingle insertion, but I sincerely hope that you, my dear reader, if necessary, will easily tune this code for your needs or, even better, write your own.

Schedule implementation


So, the lyrical digression is over, let's return to the schedule:

The playlist_update file:
 #!/bin/bash MUSICDIR=/home/PUBLIC/Music cd $MUSICDIR cp -f $1/playlist.m3u playlist.m3u /root/scripts/radio/shuffle playlist.m3u >> /dev/null echo "$1" > /home/www/HabrFM.ru/genre_non-stop.txt if ps -A | grep ices then killall -HUP ices else /etc/init.d/ices start fi 

First, the script performs copying with the replacement of the desired playlist file, then starts the program of randomization, writes the current genre into a text file (why, you will find out later)
and sends a “reread playlist” signal ices. If ices has completed its work before, the script will launch it again. It is important to know that ices will start playing a new playlist only when it finishes playing the current track. Therefore, the schedule will not look very elegant, but it can be entrusted to our friend Kron.

I give an example with my own styles:
 crontab -e 58 01 * * * /root/scripts/radio/playlist_update Breaks 58 03 * * * /root/scripts/radio/playlist_update Chill 58 09 * * * /root/scripts/radio/playlist_update Dance 58 14 * * * /root/scripts/radio/playlist_update House 58 17 * * * /root/scripts/radio/playlist_update Trance 58 21 * * * /root/scripts/radio/playlist_update Hardstyle 58 23 * * * /root/scripts/radio/playlist_update Hardcore 

Ta-da-dam! Now the construction of the broadcast schedule is completed. When there is no DJ, he plays a non-stop on schedule, when a DJ appears - the listeners are automatically redirected to his “air”. However, if you remember, there are still unrealized tasks concerning the site.

Database of musical compositions


Oh yeah, I completely forgot. To implement the voting system, I used a MySQL database. So work, my friend, to acquire such, if you have not already done so.

So, we need to store in the database information about all the tracks that are present in the “root music directory”. We need to create a database (in my example, this is radio), and in it there are two tables of the following structure:

Table songs
id INT (11) AUTO_INCREMENT PRIMARY_KEY
Genre VARCHAR (15)
Title VARCHAR (100)
Filename VARCHAR (200)
Rate INT (11)

Table votes
id int (11)
ip VARCHAR (16)

Now about the sad. Firstly, not all mp3 files (even licensed ones) have correct ID3 tags, and most of them do not have them. Secondly, I could not find a script to read ID3 tags from files. So I had to make some sacrifices. Namely: using the TagScanner program, manually edit the ID3 tags of all files, and then use the same program to rename the files according to their already correct ID3 tags. I selected the following template:

<counter>. <Artist> - <Composition> .mp3

I will not describe in this article work with this program. Let me just say that it is crucial to save ALL ID3 tags NOT in UTF-8. In the program settings there is a corresponding option. in addition, all '&' characters must be replaced with, for example, “and”. The program allows you to do this at once for all files.

OK, suppose we now have the correct file names that match the pattern, and the database has the correct structure. Further, actually, one more script, but already in PHP (so do not forget to install it at your leisure).

File db_update.php:
 #!/usr/bin/php <?php $MUSICDIR="/home/PUBLIC/Music"; $hostname = "localhost"; $username = "radio"; //     $password = "12345"; //   $dbName = "radio"; //   mysql_connect($hostname,$username,$password) OR die("Can't connect to database."); mysql_select_db($dbName) or die(mysql_error()); $Gen = array('Dance','House','Trance','Hardstyle','Hardcore','Chill','Breaks','Pumping'); $sql = "SELECT * FROM radio.songs"; $all = mysql_query($sql); $num_before = mysql_num_rows($all); echo "There are $num_before records in database.\n"; echo "\n"; echo "Searching for non-existing file names...\n"; $deleted = 0; while($row = mysql_fetch_array($all, MYSQL_ASSOC)) { $id = $row['id']; $db_filename = $row['Filename']; $exist = @fopen($db_filename,"r"); if (!$exist) { echo "Deleting: [$id] $db_filename\n"; $sql = "DELETE FROM radio.songs WHERE id=$id"; mysql_query($sql); $deleted++; } } if (!$deleted) { echo "Nothing deleted.\n"; } else { echo "Total deleted: $deleted records.\n"; } echo "\n"; $added = 0; echo "Searching for new tracks...\n"; for ($i=0;$i<count($Gen);$i++) { $genre=$Gen[$i]; $fp = fopen("$MUSICDIR/$genre/playlist.m3u","r"); while (!feof($fp)) { $filename = fgets($fp); if (strpos($filename,".mp3")) { $filename = substr($filename,0,strlen($filename)-1); $sql = sprintf("SELECT * FROM radio.songs WHERE Filename='%s'",mysql_real_escape_string($filename)); $res = mysql_query($sql); $num = mysql_num_rows($res); if ($num == 0) { $title = strstr($filename," "); $start = strpos($filename,". ")+2; $len = strpos($filename,".mp3") - $start; $title = substr($filename,$start,$len); $filename = substr($filename,0,strpos($filename,".mp3")+4); $sql = sprintf("INSERT INTO radio.songs ( `id`, `Genre`, `Title`, `Filename`, `Rate` ) VALUES ( NULL, '$genre', '%s', '%s', '0' )", mysql_real_escape_string($title),mysql_real_escape_string($filename)); mysql_query($sql) or die(mysql_error()); $added++; echo "Adding $filename\n"; } } } } if (!$added) { echo "Nothing added.\n"; } else { echo "Total added: $added records.\n"; } echo "\n"; $sql = "SELECT * FROM radio.songs"; $all = mysql_query($sql); $num_after = mysql_num_rows($all); if ($num_before == $num_after) { echo "There are still $num_after records in database.\n"; } else { echo "There are $num_after records in database.\n"; } mysql_close(); ?> 

The above script first deletes records about tracks from the database that are no longer in the corresponding directory, and then adds new ones, if any.

It would be logical to run this script immediately after creating playlists, so add one line to the music_find.sh file (after replacing the path with your own):

 /root/scripts/radio/db_update.php 

Now when creating playlists, the information in the database will be automatically updated.

The second table (votes) leave for a snack.

Collection of broadcast information


Here I will not particularly go into details. Your attention is invited to a php-script, recording the name of the current track, style and number of listeners in text files. It is assumed that these files are in the root of our site, i.e. if you, my friend, do not have a web server installed, then hurry to install it.

File icecast_status.php:
 #!/usr/bin/php <?php $STATS_FILE = 'http://IP__:_/status.xsl'; $DOCROOT = '/var/www/HabrFM.ru'; //   $hostname = "localhost"; $username = "radio"; //     $password = "12345"; //   $dbName = "radio"; //   mysql_connect($hostname,$username,$password) OR die("Can't connect to database."); mysql_select_db($dbName) or die(mysql_error()); for($i=1;$i<13;$i++) { $fp = fopen($STATS_FILE,'r'); if(!$fp) { die("Unable to connect to Icecast server."); } $stats_file_contents = ''; while(!feof($fp)) { $stats_file_contents .= fread($fp,1024); } fclose($fp); $radio_info = array(); $radio_info['genre'] = ''; $radio_info['listeners'] = ''; $radio_info['now_playing'] = ''; $temp = array(); $search_for = "<td\s[^>]*class=\"streamdata\">(.*)<\/td>"; $search_td = array('<td class="streamdata">','</td>'); if(preg_match_all("/$search_for/siU",$stats_file_contents,$matches)) { foreach($matches[0] as $match) { $to_push = str_replace($search_td,'',$match); $to_push = trim($to_push); array_push($temp,$to_push); } } $radio_info['listeners'] = $temp[5]; $radio_info['now_playing'] = $temp[9]; if(strpos($stats_file_contents,'/live')) { $radio_info['genre'] = "DJ On-Air"; } else { $fp = fopen("$DOCROOT/genre_non-stop.txt","r"); $radio_info['genre'] = fgets($fp); fclose($fp); $radio_info['genre'] = substr($radio_info['genre'],0,strlen($radio_info['genre'])-1); } if ($radio_info['genre'] == "DJ On-Air"){ $rate = "1000+"; } else { $sql = sprintf("SELECT * FROM songs WHERE ( Genre='%s' AND Title='%s' )", mysql_real_escape_string($radio_info['genre']), mysql_real_escape_string($radio_info['now_playing'])); $res = mysql_query($sql) or die(); $row = mysql_fetch_array($res, MYSQL_ASSOC); $rate = $row['Rate']; $id = $row['id']; } $fp = fopen("$DOCROOT/now_playing.txt","w"); fputs($fp,$radio_info['now_playing']); fclose($fp); $fp = fopen("$DOCROOT/id.txt","w"); fputs($fp,$id); fclose($fp); $fp = fopen("$DOCROOT/listeners.txt","w"); if ($radio_info['listeners'] > 0) { fputs($fp,'<span style="color:green; font-weight: bold;">'.$radio_info['listeners'].'</span>'); } else { fputs($fp,'<span style="color:black; font-weight: bold;">'.$radio_info['listeners'].'</span>'); } fclose($fp); $fp = fopen("$DOCROOT/genre.txt","w"); fputs($fp,$radio_info['genre']); fclose($fp); $fp = fopen("$DOCROOT/rate.txt","w"); if ($rate > 0) { fputs($fp,'<span style="color:green; font-weight: bold;">+'.$rate.'</span>'); } if ($rate < 0) { fputs($fp,'<span style="color:red; font-weight: bold;">'.$rate.'</span>'); } if ($rate == 0) { fputs($fp,'<span style="color:black; font-weight: bold;">'.$rate.'</span>'); } fclose($fp); sleep(5); } ?> 


This script must be run every minute, and it runs 12 times with delays of 5 seconds. This “bicycle” is connected with the fact that our friend Kron does not imply the existence of time units less than 1 minute. And we need to update this information at least once every 5 seconds.

Well, ask Krona (for the last time) to run this script every minute:

 crontab -e */1 * * * * /root/scripts/radio/icecast_status.php 

OK, now there are already 5 great files in the root of our site:
• now_playing.txt - artist and name of the current track;
• id.txt - a unique number in the database of the current composition;
• genre.txt - style of the track, if it plays non-stop, or the string “DJ On-Air”, if there is a DJ on the air;
• listeners.txt - the number of listeners (including HTML formatting: if it is greater than 0, then in green, if zero - in black);
• rate.txt - track rating (also including HTML formatting).

Wow, we almost got what we wanted (or what I wanted). It remains to implement the vote and, in fact, display the necessary information on the site.

Voting for compositions


The first thing I thought about when I wanted to implement the vote was - how to block repeated votes for one track? Usually they use cookies, but I have never worked with them (yes, this also happens), and decided to block by IP address. Therefore, there are only two fields in the votes table: id and ip.

The vote.php file (should be located at the root of the site):
 <?php //, ,     $DOCROOT = '/var/www/HabrFM.ru'; $hostname = "localhost"; $username = "radio"; $password = "12345"; $dbName = "radio"; mysql_connect($hostname,$username,$password) OR die("Can't connect to database."); mysql_select_db($dbName) or die(mysql_error()); $fp = fopen("$DOCROOT/id.txt", "r"); $id = fgets($fp); fclose($fp); $sql = sprintf("SELECT * FROM votes WHERE ( id='%s' AND ip='%s' )", $id, mysql_real_escape_string($_SERVER['REMOTE_ADDR'])); $res = mysql_query($sql); $num = mysql_num_rows($res); if ($num == 0) { $type = $_GET['type']; if ($type == 'plus') { $sql = sprintf("UPDATE songs SET Rate=Rate+1 WHERE id='%s'", $id); } else { if ($type == 'minus') { $sql = sprintf("UPDATE songs SET Rate=Rate-1 WHERE id='%s'", $id); } else { die('Irregular argument.'); } } if (mysql_query($sql)) { $sql = sprintf("INSERT INTO votes (`id`, `ip`) VALUES ('%s', '%s')", $id, mysql_real_escape_string($_SERVER['REMOTE_ADDR'])); mysql_query($sql); echo '  .'; } else { echo ' .'; } } else { echo "     ."; } mysql_close(); ?> 


This script takes one parameter - the type of voting (for or against). Accordingly, the challenge
 vote.php?type=plus 

will add 1 rating to the current track, and
 vote.php?type=minus 

takes 1 rating.

Display information on the site


So, there is a vote. It remains, except that to display on the site all the necessary information. I will not provide specific HTML code. Just to remind you, if you suddenly forgot about the existence of the wonderful jQuery framework.

Let elements or with id = 'now_playing' for the name of a track, with id = 'genre' for the genre, etc., be in the right places of the HTML code.
Then it is convenient to embed the following call (of course, the jQuery library needs to be added to the site directory and connected):
 <script> function show() { $.ajax({ url: "now_playing.txt", cache: false, success: function(html){ $("#now_playing").html(html); } }); $.ajax({ url: "genre.txt", cache: false, success: function(html){ $("#genre").html(html); } }); $.ajax({ url: "listeners.txt", cache: false, success: function(html){ $("#listeners").html(html); } }); $.ajax({ url: "rate.txt", cache: false, success: function(html){ $("#rate").html(html); } }); } $(document).ready(function(){ show(); setInterval('show()',5000); }); </script> 


And then every 5 seconds the information on the page will be updated (and without reloading the page).

Remember, at the beginning of the article, I announced the display of the TOP20 rating? Yes, that would be great.

So, the last script that produces a table of the best (type = 1) or worst (type = 2) tracks of a particular style / genre in the following format:
| Composition | Rating |

File top20.php (better to finish the tool under your site):
 <?php $Gen = array('Dance','House','Trance','Hardstyle','Hardcore','Chill','Breaks','Pumping'); //    :) $hostname = "localhost"; $username = "radio"; $password = "12345"; $dbName = "radio"; mysql_connect($hostname,$username,$password) OR die("Can't connect to database."); mysql_select_db($dbName) or die(mysql_error()); $genre = $_GET["genre"]; $type = $_GET["type"]; echo "<br>\n"; if (!(in_array($genre,$Gen))) { die("Irregular argument."); } $sql = sprintf("SELECT Title, Rate FROM songs WHERE (Genre='%s'",mysql_real_escape_string($genre)); if ($type==1) { $sql = $sql . " AND Rate>0) ORDER BY Rate DESC LIMIT 20"; } else if ($type==2) { $sql = $sql . " AND Rate<0) ORDER BY Rate ASC LIMIT 20"; } else { die("Irregular argument"); } $res = mysql_query($sql); if (mysql_num_rows($res)>0) { echo '<table border="1" style="font-family: Verdana,Geneva; font-size: 10;" cellspacing="0" width="100%">'; echo "\n"; echo '<tr><td align="left">â„–</td><td align="left" width="100%"> - </td><td align="right"></td></tr>'; echo "\n"; } else { echo "   .     -       ."; } $i = 1; while ($row = mysql_fetch_array($res, MYSQL_ASSOC)) { echo "<tr>"; echo '<td align="left">' . $i . '.</td>'; echo '<td align="left">' . $row['Title'] . '</td>'; echo '<td align="right">' . $row['Rate'] . '</td>'; echo "</tr>\n"; $i++; } if (mysql_num_rows($res)>0) { echo '</table>'; } mysql_close(); ?> 

That's all. Next, you need to embed this script in the right place of the HTML code (you can use the same jQuery, only without setInterval ('show ()', 5000). But this I leave to you, my young friend, as homework.

I sincerely hope that my story will be useful to you or just interesting.

Source: https://habr.com/ru/post/131116/


All Articles