Building My Internet Radio Part 1

In this post, I will begin to lay out the code, and explain how I built the web front-end that displays the metadata and information when a visitor comes to the page. The website uses the following technologies:

AWS EC-2 linux  running Apache with PHP 7.2.14, Mongo Shell, Javascript, jQuery, MongoDB for database.

For the PHP I had to install the following mods:
mongodb, SimpleXML and curl.

I decided to use Mongo for storing the metadata once it’s captured from the lastfm api, or the musicbrainz api (if lastfm returns an empty result) so the application will not have to ask for it again if it’s already in the database. Saves on calls to their api, and increases performance.¬† Afterall, no point in getting data from the same place again and again if we’ve already got a copy from the first call, right? Right.

Ok. With that, I created the first part, and that was the beginning of my journey which lead me to the point I’m at today.

Here’s my PHP include/config.inc.php file, which is used throughout the application.

 define('SHOUTCAST_HOST', 'http://xxx.xxx.xxx.xxx:8000'); define('SHOUTCAST_ADMIN_PASS', 'password'); define('SCROBBLER_API', 'myscrobblerapikeyfromlastfm'); define('APPLICATION_NAME', 'My Radio'); define('NOW_PLAYING_TXT', 'Now Playing'); define('SITE_URL', 'http://example.com'); 

Next, I set up statistics.php which gathers the artist and title of the current track being played from the shoutcast server:

/**
* Get statistic data as well as current playing info
* to be displayed
*/
require_once('include/config.inc.php');

// call stats from shoutcast server
$json = file_get_contents(SHOUTCAST_HOST .'/statistics?json=1');
echo $json;
exit;

This call spits out the json data back to the ajax caller:

{"totalstreams":1,"activestreams":1,
"currentlisteners":0,"peaklisteners":4,
"maxlisteners":512,"uniquelisteners":0,
"averagetime":0,
"version":"2.6.0.750 (posix(linux x64))",
"streams":[{"id":1,
"currentlisteners":0,
"peaklisteners":4,
"maxlisteners":512,
"uniquelisteners":0,"averagetime":0,
"servergenre":"Hawkwynd Radio",
"serverurl":"http:\/\/stream.hawkwynd.com",
"servertitle":"Hawkwynd Radio - March into Spring",
"songtitle":"Ram Jam - Black Betty",
"streamhits":31,"streamstatus":1,"backupstatus":0,
"streamlisted":0,"streamlistederror":1852142707,
"streampath":"\/","streamuptime":64535,
"bitrate":"128","samplerate":"48000",
"content":"audio\/mpeg"}]
}

Ok, so now I know the Artist and the Title of the song being played. The next step is to query last.fm and ask for details about this artist/song pair.

So, in my index.php code, I place this javascript block which calls statistics function on page load, and then every 10 seconds thereafter to refresh to page with the metadata it receives:

$(document).ready(function(){
statistics(); // query server for stream data on page load.
// refresh the screen every 10 seconds to update the track/artist info
setInterval(function(){
statistics();
}, 10000); // 10 seconds calls to refresh
});

Armed with the artist and title, we dig into the lastfm api, and make a call to get a boatload of data from it’s response.

function lastfm(a,t){
   $.getJSON('lookup.php', {
      track: t,
      artist: a
}).done(function(results){
    callback(results);
}).fail(function() {
    callback(null);
 failed(a,t);
 });
}

And now, I present my lookup code, which I will explain in detail in Part 2 of this series.

error_reporting(E_STRICT);
ini_set('display_errors', 1);

/**
* Date: 2/25/19
* stream.hawkwynd.com - scottfleming
* Scrobble an artist and title and return a shit-ton 
* of data about the artist,album, and tracks on the album
*/

require_once('include/config.inc.php');
require 'mongodb/vendor/autoload.php';
require 'guzzle/vendor/autoload.php';

// keep real spacing for search of mongo
$tt = $_GET['track'];
$ar = $_GET['artist'];

// check the mongodb if we have it listed
$internalFind = do_find($tt,$ar);
if($internalFind->album->mbid){ 
  echo json_encode($internalFind);
  exit;
}

// check the failed db if it's a failed listing.
$fail = do_findfail($tt, $ar);
if($fail->artist){
  exit();
}

// Nothing returned on Mongo, and its not in the failed table, 
// so let's call lastFM for it.
$track = rawurlencode($_GET['track']);
$artist = rawurlencode($_GET['artist']);

$out = new stdClass();

$trackSearch = json_decode( file_get_contents('http://ws.audioscrobbler.com/2.0/?method=track.search&api_key='.SCROBBLER_API.'&track='.$track.
'&artist='.$artist. '&format=json') );

// we will take the first group of results for now
$result = $trackSearch->results->trackmatches->track[0];

// throw out the stuff we don't want
unset($out->url, $out->streamable, $out->listeners);

// if we have an mbid, then we'll gobble that shit up
if($result->mbid){
  $trackId = $result->mbid;
  $trackFind = json_decode(file_get_contents(
  'http://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key='.
SCROBBLER_API.'&mbid='. $trackId.'&format=json'
));

$artistInfo = ArtistInfoById($trackFind->track->artist->mbid);

if($artistInfo) {

$out->artist->name = $artistInfo[0]['name'];
$out->artist->mbid = $artistInfo[0]['mbid'];
$out->artist->summary = do_trunc( str_replace('Read more on 
Last.fm','', strip_tags( $artistInfo[0]['summary'] ) ), 200);
$out->track->name = $trackFind->track->name;
$out->track->mbid = $trackFind->track->mbid;
$out->track->duration = $trackFind->track->duration;
$out->album->title = $trackFind->track->album->title;
$out->album->mbid = $trackFind->track->album->mbid;
$out->album->image = $trackFind->track->album->image[2]->{"#text"}; 
// large image size

$releaseDate = getLPRelease( $trackFind->track->album->mbid);

// set the release date of the album
$out->album->releaseDate = $releaseDate['first_release_date'];

$details = get_release_details($trackFind->track->album->mbid);
$label = $details->{"label-info"}[0]; 

// first element of array, fuck the others.
$out->album->label = $label->label->name;
$out->album->lid = $label->label->id;

$out->artist->members = get_members($trackFind->track->artist->mbid);
$out->status = "lastFM"; // to let me know where I got this info from

}

echo json_encode($out, true); // return result

do_dbUpdate($out); // update mongo db to add new record found.
}
exit;

/**
* @param $out object
*
*/

function do_trunc($file, $maxlen)
{

if ( strlen($file) > $maxlen ){
return substr($file,0,strrpos($file,". ",$maxlen-strlen($file)) + 1);
}else{
return($file);
}

}

function do_dbUpdate($out)
{
  $collection = (new MongoDB\Client)->stream->lastfm;
  $updateResult = $collection->findOneAndUpdate(
   ['$and' => [
         [ 'track-mbid' => $out->track->mbid ]
      ]
  ],
   ['$set' => [
        'artist-name' => $out->artist->name,
        'artist-mbid' => $out->artist->mbid,
        'artist-summary' => $out->artist->summary,
        'track-name' => $out->track->name,
        'track-mbid' => $out->track->mbid,
        'track-duration' => $out->track->duration,
        'album-name' => $out->album->title,
        'album-mbid' => $out->album->mbid,
        'album-released' => $out->album->releaseDate,
        'album-image' => $out->album->image,
        'album-label' => $out->album->label,
        'album-lid' => $out->album->lid,
        'artist-members' => $out->artist->members
        ]
 ],
   ['upsert' => true]
);

}

function do_find($t, $a)
{
$out = new stdClass();
$collection = (new MongoDB\Client)->stream->lastfm;
$cursor = $collection->find(
['$and' => [
            [ 'track-name' => $t ],
            [ 'artist-name' => $a ]
         ]
]
);

foreach($cursor as $row){
$out->artist->name = $row->{"artist-name"};
$out->artist->mbid = $row->{"artist-mbid"};
$out->artist->summary = $row->{"artist-summary"};
$out->track->name = $row->{"track-name"};
$out->track->mbid = $row->{"track-mbid"};
$out->track->duration = $row->{"track-duration"};
$out->album->title = $row->{"album-name"};
$out->album->image = $row->{"album-image"};
$out->album->mbid = $row{"album-mbid"};
$out->album->releaseDate= $row{"album-released"};
$out->album->label = $row{"album-label"};
$out->artist->members = $row{"artist-members"};
$out->status = "MongoDB";
}

return $out;

}

function do_findfail($t, $a)
{
$out = new stdClass();
$collection = (new MongoDB\Client)->stream->lastfm_fail;
$cursor = $collection->find(
[ 'title' => new MongoDB\BSON\Regex($t, 'i') ]
);

foreach($cursor as $row){
$out->artist->name = $row->{"artist"};
$out->track->name = $row->{"title"};
$out->status = "MongoDB";
}

return $out;

}

/**
* @param $mbid
* @return array
* @desc since last.fm doesn't supply the release date, 
* we'll get it from musicbrainz api and update our 
* content with it.
*/
function getLPRelease($mbid)
{
$url = 'http://musicbrainz.org/ws/2/release/'.$mbid.'?inc=release-groups&fmt=xml';
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $url); // get the url contents
curl_setopt($ch, CURLOPT_USERAGENT, "hawkwyndRadio/1.1");
$data = curl_exec($ch); // execute curl request
curl_close($ch);

$xml = simplexml_load_string($data);
$first_release_date = $xml->release->{"release-group"}->{"first-release-date"};
$title = (string) $xml->release->{"release-group"}->title;
$formatted_release_year = date('Y', strtotime($first_release_date));

$out = array('title' => $title, 'first_release_date' => $formatted_release_year, 'mbid' => $mbid);

return $out;
}

function AlbumTracksList($alid, $tracklist=array())
{
$albumInfo = json_decode( file_get_contents('http://ws.audioscrobbler.com/2.0/?method=album.getInfo&api_key='.SCROBBLER_API.'&mbid='. $alid .'&format=json') );

foreach($albumInfo->album->tracks->track as $track)
{
array_push($tracklist, array('track_no' => $track->{"@attr"}->rank, 'title' => $track->name));

}
return $tracklist;
}

function ArtistInfoById($arid, $artistResult=array())
{
$artistInfo = json_decode( file_get_contents('http://ws.audioscrobbler.com/2.0/?method=artist.getInfo&api_key='
.SCROBBLER_API.'&mbid='.$arid.'&format=json') );
$tagArr=[];

foreach($artistInfo->artist->tags->tag as $tag)
{
array_push($tagArr, $tag->name);
}

if($artistInfo->artist->mbid)
{
array_push(
$artistResult, array(
'name' => $artistInfo->artist->name,
'mbid' => $artistInfo->artist->mbid,
'tags' => json_encode( $tagArr ),
'summary' => $artistInfo->artist->bio->summary,
'content' => $artistInfo->artist->bio->content
)
);
return $artistResult;
}

}

function fetch($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $url); // get the url contents
curl_setopt($ch, CURLOPT_USERAGENT, "hawkwyndRadio/1.1");
$data = curl_exec($ch); // execute curl request
curl_close($ch);
return $data;
}

function tags($t , $tags=array()){
function cmp($a, $b)
{
return strcmp($a->name, $b->name);
}
foreach($t as $tag)
{
unset($tag->url);
}
usort($t, "cmp");

return $t;
}

function get_release_details($rid)
{
$client = new \GuzzleHttp\Client();
$url = "http://musicbrainz.org/ws/2/release/$rid?inc=labels&fmt=json";

$response = $client->request('GET', $url);
return json_decode( $response->getBody() );

}

function get_members($arid)
{
$payload = $members = [];
$data = json_decode( do_member_lookup($arid) );
$payload['group_name'] = $data->name;
$begin = (!empty($data->{"life-span"}->begin)) ? date('Y', strtotime($data->{"life-span"}->begin)) : '';
$payload['group_begin'] = $begin;
$end = (!empty($data->{"life-span"}->end)) ? date('Y', strtotime($data->{"life-span"}->end)) : '';
$payload['group_end'] = $end;

foreach( $data->relations as $relation )
{
if($relation->type == 'member of band') {
$begin = (!empty($relation->begin)) ? date('Y', strtotime($relation->begin)) : '';
$end = (!empty($relation->end)) ? date('Y', strtotime($relation->end)) : '';

$instruments = implode(', ', $relation->attributes);

array_push($members, array('member_name' => $relation->artist->name, 'begin' => $begin, 'end' => $end, 'instruments' => $instruments ) );

}
}
$payload['members'] = $members;
return $payload;
}

function do_member_lookup($arid)
{
$client = new \GuzzleHttp\Client();
$url = "http://musicbrainz.org/ws/2/artist/".$arid."?inc=artist-rels&fmt=json";
$response = $client->request('GET', $url);
return $response->getBody() ;
}