Displaying Shoutcast Now Playing Information on Your Web Site

Want to pull listener stats and now playing and recently played from your shoutcast server(s)? This seems to be a common question, so here’s a small tutorial on how to accomplish this. This code is based on what I’ve done over at AudioProbe, so I’ve left in a lot of stuff as an example. If you have any questions, please do ask. 😛

First, create shoutcast-config.php
This file holds all of the configuration details for your shoutcast servers. I keep this in a separate file since I include this in other applications on the site. You can add as many servers as you want, but it makes sense to ensure they are all playing the same program.

<?php
//server config...you can add as many servers as you want in following format...just simply copy and paste this code block to add a new server (update the info of course)
$serv["host"][] = "listen.audioprobe.net"; # host address  or IP address DO NOT add "http://"
$serv["port"][] = 80; # port number
$serv["passwd"][] = "pass"; # admin password
?>

Next, create yql.php
This file is what takes care of retrieving the statistics from each server. The statistics are returned to pollstation.js (below) and placed on the page. The only part of this you will need to modify is at the bottom.

<?php
require_once('shoutcast-config.php');
for ($count = 0; $count < count($serv["host"]); $count++) {
    $mysession = curl_init();
    curl_setopt($mysession, CURLOPT_URL, "http://".$serv["host"][$count].":".$serv["port"][$count]."/admin.cgi?mode=viewxml");
    curl_setopt($mysession, CURLOPT_HEADER, false);
    curl_setopt($mysession, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($mysession, CURLOPT_POST, false);
    curl_setopt($mysession, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
    curl_setopt($mysession, CURLOPT_USERPWD, "admin:".$serv["passwd"][$count]);
    curl_setopt($mysession, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6");
    curl_setopt($mysession, CURLOPT_CONNECTTIMEOUT, 2);
    $xml = curl_exec($mysession);
    curl_close($mysession);
 
    $result[]=xml2array($xml);
}
 
/** 
 * xml2array() will convert the given XML text to an array in the XML structure. 
 * Link: http://www.bin-co.com/php/scripts/xml2array/ 
 * Arguments : $contents - The XML text 
 *                $get_attributes - 1 or 0. If this is 1 the function will get the attributes as well as the tag values - this results in a different array structure in the return value.
 *                $priority - Can be 'tag' or 'attribute'. This will change the way the resulting array sturcture. For 'tag', the tags are given more importance.
 * Return: The parsed XML in an array form. Use print_r() to see the resulting array structure. 
 * Examples: $array =  xml2array(file_get_contents('feed.xml')); 
 *              $array =  xml2array(file_get_contents('feed.xml', 1, 'attribute')); 
 */ 
function xml2array($contents, $get_attributes=1, $priority = 'tag') { 
    if(!$contents) return array(); 
 
    if(!function_exists('xml_parser_create')) { 
        //print "'xml_parser_create()' function not found!"; 
        return array(); 
    } 
 
    //Get the XML parser of PHP - PHP must have this module for the parser to work 
    $parser = xml_parser_create(''); 
    xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8"); # http://minutillo.com/steve/weblog/2004/6/17/php-xml-and-character-encodings-a-tale-of-sadness-rage-and-data-loss 
    xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); 
    xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); 
    xml_parse_into_struct($parser, trim($contents), $xml_values); 
    xml_parser_free($parser); 
 
    if(!$xml_values) return;//Hmm... 
 
    //Initializations 
    $xml_array = array(); 
    $parents = array(); 
    $opened_tags = array(); 
    $arr = array(); 
 
    $current = &$xml_array; //Refference 
 
    //Go through the tags. 
    $repeated_tag_index = array();//Multiple tags with same name will be turned into an array 
    foreach($xml_values as $data) { 
        unset($attributes,$value);//Remove existing values, or there will be trouble 
 
        //This command will extract these variables into the foreach scope 
        // tag(string), type(string), level(int), attributes(array). 
        extract($data);//We could use the array by itself, but this cooler. 
 
        $result = array(); 
        $attributes_data = array(); 
 
        if(isset($value)) { 
            if($priority == 'tag') $result = $value; 
            else $result['value'] = $value; //Put the value in a assoc array if we are in the 'Attribute' mode 
        } 
 
        //Set the attributes too. 
        if(isset($attributes) and $get_attributes) { 
            foreach($attributes as $attr => $val) { 
                if($priority == 'tag') $attributes_data[$attr] = $val; 
                else $result['attr'][$attr] = $val; //Set all the attributes in a array called 'attr' 
            } 
        } 
 
        //See tag status and do the needed. 
        if($type == "open") {//The starting of the tag '<tag>' 
            $parent[$level-1] = &$current; 
            if(!is_array($current) or (!in_array($tag, array_keys($current)))) { //Insert New tag 
 
 
                $current[$tag] = $result; 
                if($attributes_data) $current[$tag. '_attr'] = $attributes_data; 
                $repeated_tag_index[$tag.'_'.$level] = 1; 
 
                $current = &$current[$tag]; 
 
            } else { //There was another element with the same tag name 
 
                if(isset($current[$tag][0])) {//If there is a 0th element it is already an array 
                    $current[$tag][$repeated_tag_index[$tag.'_'.$level]] = $result; 
                    $repeated_tag_index[$tag.'_'.$level]++; 
                } else {//This section will make the value an array if multiple tags with the same name appear together
                    $current[$tag] = array($current[$tag],$result);//This will combine the existing item and the new item together to make an array
                    $repeated_tag_index[$tag.'_'.$level] = 2; 
 
                    if(isset($current[$tag.'_attr'])) { //The attribute of the last(0th) tag must be moved as well 
                        $current[$tag]['0_attr'] = $current[$tag.'_attr']; 
                        unset($current[$tag.'_attr']); 
                    } 
 
                } 
                $last_item_index = $repeated_tag_index[$tag.'_'.$level]-1; 
                $current = &$current[$tag][$last_item_index]; 
            } 
 
        } elseif($type == "complete") { //Tags that ends in 1 line '<tag />' 
            //See if the key is already taken. 
            if(!isset($current[$tag])) { //New Key 
                $current[$tag] = $result; 
                $repeated_tag_index[$tag.'_'.$level] = 1; 
                if($priority == 'tag' and $attributes_data) $current[$tag. '_attr'] = $attributes_data; 
 
            } else { //If taken, put all things inside a list(array) 
                if(isset($current[$tag][0]) and is_array($current[$tag])) {//If it is already an array... 
 
                    // ...push the new element into that array. 
                    $current[$tag][$repeated_tag_index[$tag.'_'.$level]] = $result; 
 
                    if($priority == 'tag' and $get_attributes and $attributes_data) { 
                        $current[$tag][$repeated_tag_index[$tag.'_'.$level] . '_attr'] = $attributes_data; 
                    } 
                    $repeated_tag_index[$tag.'_'.$level]++; 
 
                } else { //If it is not an array... 
                    $current[$tag] = array($current[$tag],$result); //...Make it an array using using the existing value and the new value
                    $repeated_tag_index[$tag.'_'.$level] = 1; 
                    if($priority == 'tag' and $get_attributes) { 
                        if(isset($current[$tag.'_attr'])) { //The attribute of the last(0th) tag must be moved as well
 
                            $current[$tag]['0_attr'] = $current[$tag.'_attr']; 
                            unset($current[$tag.'_attr']); 
                        } 
 
                        if($attributes_data) { 
                            $current[$tag][$repeated_tag_index[$tag.'_'.$level] . '_attr'] = $attributes_data; 
                        } 
                    } 
                    $repeated_tag_index[$tag.'_'.$level]++; //0 and 1 index is already taken 
                } 
            } 
 
        } elseif($type == 'close') { //End of tag '</tag>' 
            $current = &$parent[$level-1]; 
        } 
    } 
 
    return($xml_array); 
}  
    foreach($result as $r) {
        $listeners+=$r[SHOUTCASTSERVER][CURRENTLISTENERS];
    }
 
    $i=0;
    while($i!=10) {
        $songs.="<br/>".trim($r[SHOUTCASTSERVER][SONGHISTORY][SONG][$i][TITLE]);
        $i++;
    }
 
    $listenerstext="<a href="http://loudcity.com/stations/audioprobe-net/files/show/tunein.html" style="color: #ffffff" target="_blank">Join our other listeners now!</a> Listen live by clicking the above link.$listenerstext";
 
    if($listeners > 0) {
        $listenerstext.=" There are currently <a href="http://audioprobe.net/status.php" style="color: #ffffff;text-decoration:none;">$listeners</a> listeners tuned in.";
    }    
 
    $result = $listenerstext.$songs;
 
    echo htmlspecialchars($_GET[callback]).'([{"results": "'.addslashes(trim($result)).'"}])';
?>

pollstation.js
This is what handles placing the information on the page. I’ve left in my custom stuff at the bottom. You’ll most certainly want to customize it. Based on some feedback, I have provided a basic version as well as a version with some customization that is used on AudioProbe. The customization consists of linking to info pages for the songs, album art, etc. Note that this is for advanced users only since you will need to build the pages to serve the required stuff.

Change the “var yql” line to match the URL of your yql.php file. For example, if my domain were “domain.com” and I have placed yql.php in the root directory this would be the result: var yql = ‘http://domain.com/yql.php?callback=?’;

jQuery(document).ready(function() {
    pollstation();
    //refresh the data every 30 seconds
    setInterval(pollstation, 30000);
});
 
// Accepts a url and a callback function to run.  
function requestCrossDomain( callback ) {  
    // Take the provided url, and add it to a YQL query. Make sure you encode it!  
    var yql = 'http://audioprobe.net/yql.php?callback=?';
    // Request that YSQL string, and run a callback function.  
    // Pass a defined function to prevent cache-busting.  
    jQuery.getJSON( yql, cbFunc );
 
    function cbFunc(data) {  
    // If we have something to work with...  
    if ( data ) {  
        // Strip out all script tags, for security reasons. there shouldn't be any, however
        data = data[0].results.replace(/<script[^>]*>[sS]*?</script>/gi, '');
        data = data.replace(/<html[^>]*>/gi, '');
        data = data.replace(/</html>/gi, '');
        data = data.replace(/<body[^>]*>/gi, '');
        data = data.replace(/</body>/gi, '');
 
        // If the user passed a callback, and it  
        // is a function, call it, and send through the data var.  
        if ( typeof callback === 'function') {  
            callback(data);  
        }  
    }  
    // Else, Maybe we requested a site that doesn't exist, and nothing returned.  
    else throw new Error('Nothing returned from getJSON.');  
    }  
}  
 
function pollstation() {
    requestCrossDomain(function(stationdata) {
        //make our data into an array
        var lines = stationdata.split('<br/>');
 
        //update number of listeners
        jQuery('#listeners').html(lines[0]);
 
        //transform the song title into [artist] - [title] ([year])
        s_info=lines[1].split(" - ");
 
        //remove the artist from the title
        title=jQuery.trim(s_info[1]);
 
        //remove the year from the title
        cleantitle=jQuery.trim(s_info[1].replace(/ (d{4})/,''));
 
        //keep just the year
        new_year=title.replace(cleantitle, '');
 
        //get rid of parenthesis around the year
        new_year=new_year.replace(/ (/,'');
        new_year=new_year.replace(/)/,'');
 
 
        //update the current artist and song title
        jQuery('#currentsong').html(jQuery.trim(cleantitle) + '<br /><em>' + jQuery.trim(s_info[0]) + '</em><<br />' + jQuery.trim(new_year));
 
        //update the previously played songs
        for (var i = 1; i <= 10; i++) {            
            jQuery('#prevsong' + i).html(lines[i + 1]);
        }
    } );
}

//customized for album art and linking
jQuery(document).ready(function() {
    pollstation();
    //refresh the data every 30 seconds
    setInterval(pollstation, 30000);
});
 
// Accepts a url and a callback function to run.  
function requestCrossDomain( callback ) {  
    // Take the provided url, and add it to a YQL query. Make sure you encode it!  
    var yql = 'http://audioprobe.net/yql.php?callback=?';
    // Request that YSQL string, and run a callback function.  
    // Pass a defined function to prevent cache-busting.  
    jQuery.getJSON( yql, cbFunc );
 
    function cbFunc(data) {  
    // If we have something to work with...  
    if ( data ) {  
        // Strip out all script tags, for security reasons. there shouldn't be any, however
        data = data[0].results.replace(/<script[^>]*>[sS]*?</script>/gi, '');
        data = data.replace(/<html[^>]*>/gi, '');
        data = data.replace(/</html>/gi, '');
        data = data.replace(/<body[^>]*>/gi, '');
        data = data.replace(/</body>/gi, '');
 
        // If the user passed a callback, and it  
        // is a function, call it, and send through the data var.  
        if ( typeof callback === 'function') {  
            callback(data);  
        }  
    }  
    // Else, Maybe we requested a site that doesn't exist, and nothing returned.  
    else throw new Error('Nothing returned from getJSON.');  
    }  
}  
 
function pollstation() {
    requestCrossDomain(function(stationdata) {
        //make our data into an array
        var lines = stationdata.split('<br/>');
 
        //update number of listeners
        jQuery('#listeners').html(lines[0]);
 
        //update the album art
        jQuery('#songsearch').html('<img src="http://audioprobe.net/art.php?query=' + encodeURIComponent(jQuery.trim(lines[1])) + '" />');            
 
        //transform the song title into [artist] - [title] ([year])
        s_info=lines[1].split(" - ");
 
        //remove the artist from the title
        title=jQuery.trim(s_info[1]);
 
        //remove the year from the title
        cleantitle=jQuery.trim(s_info[1].replace(/ (d{4})/,''));
 
        //keep just the year
        new_year=title.replace(cleantitle, '');
 
        //get rid of parenthesis around the year
        new_year=new_year.replace(/ (/,'');
        new_year=new_year.replace(/)/,'');
 
        //if a special show, let's identify it and properly format it
        var index = cleantitle.indexOf("[Aural Pleasure]");
        if(index != -1) {
            //remove the show title from the title of the song
            cleantitle=cleantitle.replace(/ [Aural Pleasure]/,'');
 
            //replace the year with the song
            new_year='Aural Pleasure';
 
            //update the album art for the show
            jQuery('#songsearch').html('<img src="http://audioprobe.net/auralpleasure.jpg" alt="Aural Pleasure" />');
        }
 
        //update the current artist and song title
        jQuery('#currentsong').html('<a href="http://audioprobe.net/redirect.php?song=' + encodeURIComponent(jQuery.trim(lines[1])) + '" title="view song information"  style="text-decoration:none;" target="_blank"><span style="font-weight:bold;color:#993333;font-size: 14px;">' + jQuery.trim(cleantitle) + '</span><br /><span style="font-weight:bold;color:#333333;font-size: 12px;"><em>' + jQuery.trim(s_info[0]) + '</em></span><br /><div style="font-weight:bold;font-size: 14px;">' + jQuery.trim(new_year) + '</div></a>');
 
        //update the previously played songs
        for (var i = 1; i <= 10; i++) {            
            jQuery('#prevsong' + i).html('<a href="http://audioprobe.net/redirect.php?song=' + encodeURIComponent(jQuery.trim(lines[i+1])) + '" title="view song information"  style="text-decoration:none;font-weight:normal" target="_blank">' + lines[i + 1] + '</a>');
        }
    } );
}

Obtain jQuery and place it somewhere on your server or use a preferred CDN service.

index.html
This is an example of how the code could be displayed on your page.

<html>
<head>
<script src="/jquery.min.js" type="text/javascript"></script>
<script src="/pollstation.js" type="text/javascript"></script>
</head>
<body>
Currently Playing:
<div id="currentsong"></div>
<br /><br />
Listeners:
<span id="listeners"></span>
<br /><br />
Recently Played:
    <table>
    <tr><th>Recently Played Songs</th></tr>
    <tr><td><span id="prevsong1"></span></td></tr>
    <tr><td><span id="prevsong2"></span></td></tr>
    <tr><td><span id="prevsong3"></span></td></tr>
    <tr><td><span id="prevsong4"></span></td></tr>
    <tr><td><span id="prevsong5"></span></td></tr>
    <tr><td><span id="prevsong6"></span></td></tr>
    <tr><td><span id="prevsong7"></span></td></tr>
    <tr><td><span id="prevsong8"></span></td></tr>
    <tr><td><span id="prevsong9"></span></td></tr>
    <tr><td><span id="prevsong10"></span></td></tr>
    </table>
</body>
</html>

Good luck!

Update (March 13, 2012): I’ve made a few changes based on some feedback I’ve gotten. The sample pollstation.js with all the extra code was confusing so I added a “lite” version. It was also unclear that pollstation.js needed to be modified to reflect the location of yql.php. Also, please note that this script will work with Shoutcast DNAS version 2 with a few modifications. Instructions (untested) can be found in the comments.
Update (June 7, 2012): Updated shoutcast-config.php to note that “http://” should not be included in host.

  • Hello there. Your script is great and I want to use it on my website but can you explain me what exactly I need to change in code so that it will display info from my server? I’m a totally new in coding.
    I will provide you with server details if you need it.
    Best regards Alx

    • admin

      Hi Alex,

      Assuming you keep everything in one directory, config.php is the only thing you have to modify. Please let me know if that doesn’t make sense.

  • rikk

    Hi, somebody try it?
    I did it but I don’t know why I just can have audioprobe radio now playing info even If I put my own IP, port and password.

    I hope somebody can help me.

    Regards

    • admin

      You need to change this line in pollstation.js to the url of your yql.php file: var yql = ‘http://audioprobe.net/yql.php?callback=?’;

      You’ll need to clear your browser’s cache after doing this.

  • gkarmas

    Hey great tutorial there. Btw I have a problem, if I change the php to var yql = ‘http://myradiol.com/yql.php?callback=?’; the html does not get the new data from my station. It works like a charm with audioprobe.net.
    I noticed that jQuery(‘#currentsong’).html(‘<a href="http://audioprobe.net/redirect.php?song=&#039; + … and jQuery('#prevsong' + i).html('<a href="http://audioprobe.net/redirect.php?song=&#039; + … I dont have those two php files and I have no idea what they do. Also, the yql.php gives me the following error, Warning: curl_setopt() [function.curl-setopt]: CURLOPT_FOLLOWLOCATION cannot be activated when safe_mode is enabled or an open_basedir is set in …yql.php on line 11
    ([{"results": "Join our other listeners now! Listen live by clicking the above link. There are current …. and then shows the listeners and songs right. What is the problem ?

    • admin

      There are two options here: 1) disable PHP safe mode, or 2) remove the following CURLOPT_FOLLOWLOCATION line in yql.php.

      The code pointing to the files you pointed out is example code to show how to support some form of search. You’ll notice that if you look at the live example, you can click on the song titles and you will be taken to the song info page. You can safely remove the hyperlink parts, but be sure to leave the part that prints the song.

  • jose

    will this work on Shoutcast dnas2? I have scripts working on dnas1.9.8 but when I upgraded to dnas2, all scripts won’t work.

    • admin

      I think specifying the server id will be enough. I haven’t tested this since I haven’t bothered upgrading, but perhaps it will work.

      In yql.php, Find:
      curl_setopt($mysession, CURLOPT_URL, “http://”.$serv[“host”][$count].”:”.$serv[“port”][$count].”/admin.cgi?mode=viewxml”);

      Replace with:
      curl_setopt($mysession, CURLOPT_URL, “http://”.$serv[“host”][$count].”:”.$serv[“port”][$count].”/admin.cgi?mode=viewxml&sid=”.$serv[“sid”][$count]);

      In shoutcast-config.php, add this for each server:
      $serv[“sid”][] = “1”; # the id of the server goes here

  • Phil Buck

    Hi, my name is Phil and I really enjoy using the code you posted here to run my independent streaming radio station. It has worked great for me to display the current artist and a list of recently played. Thank you for posting it!

    I have been interested in the linking and display album art additions for sometime, but I’ll admit they are just a bit over my head. I am pretty good at learning from an example and tweaking it to make it work. I was wondering if you might be willing to share some examples of the art.php and redirect.php files that you used to make your more advanced version of this script. Thanks in advance for any help you can offer.

    • audioprobe

      Album art is probably too advanced for non-programmer types, but I’ll explain how I would go about doing it anyway. Perhaps someone will do it and share. I’ve had a few false starts on the new site, but haven’t ever gotten as far as album art before I scrapped the whole thing. You wouldn’t want what I was using in the old site since it has a lot of dependencies and is quite messy.

      What I would do is create a table in the database to hold the artist’s name, the link to the artwork (never host the artwork yourself or you’ll eventually have a bad time like me), and the time it was last updated.

      In the art.php script I would then check the table for an entry for Madonna. If it doesn’t exist, call the last.fm api to get the image url and insert the relevant data (updating the last updated timestamp of course). If an entry for the artist already exists, I would check the last time it was updated. If greater than 24 hours I would then verify that the image url is still valid (doesn’t return 404 status code). If valid, update the timestamp to now. If less than 24 hours, simply serve the image…no additional checks required.

      This will give you a good idea of how to interact with the last.fm api: http://techslides.com/lastfm-api-with-php/