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() ;
}

Building My Internet Radio Station

Streaming services are everywhere these days. Pandora, Spotify, Google Play, YouTube, etc. – the list goes on and on. At first, I was happy just being able to tune in to Pandora and listening to what they would deliver.

Customizing the music Pandora delivers is fairly limited, and often entertaining because it introduced me to music I’d never heard before. But that’s the limitation – I couldn’t control much else. I was at the mercy of Pandora’s selections, even though I could pay a monthly fee and have some additional control. I didn’t feel paying for a music service was in my best interest, so I began to research other options.

I wanted total control of my music, and have the ability to:

  1. Listen to it anywhere as long as I was able to get internet access.
  2. Listen on any device; PC, laptop, phone, tablet, iPad, iPhone.
  3. Control the playlist and deliver content as I wanted in the order I wanted to deliver it.
  4. Have the ability to broadcast “Live”, and be able to talk over the music, or provide a narrative, much like that of a real radio station.
  5. Share my broadcast with anyone, and eventually host broadcasts with other like-minded people interested in doing a live broadcast.
  6. Be able to broadcast a live event, like a band playing a concert.

With all that in mind, I began to research options to do these things. I quickly discovered several free, open source options. The first I came across was twitch.tv — it allowed you to stream from your computer, and share it to others, but it required visitors going to twitch.tv’s site, and contained advertisements, and other panels of distraction – no good for me.

Next, I tried Airtime – a free software which provided the ability to schedule play of mp3 files, but lacked the flexibility I wanted in control. Also, no live broadcasting was possible.

Finally, after hours of research, questions in forums, and google searches, I found the solid solution – build it yourself from scratch! I dug into How-To’s and assorted StackOverflow discussions in my quest to have my own live radio show streaming on the internet. With the excitement of a 12-year old with his first Playboy mag, I started compiling the list of things I would need to have to build a basic streaming server. I created a list of ‘must-haves’ in order to achieve my objective.

  • Linux based – fast and open source operating system (free)
  • Secure, and efficient, fast and reliable.
  • Flexible and dynamic to allow for growth

So, my first step was to get a linux server. I created an AWS account, and created an EC-2 Instance running amazon-linux. I shelled in and began the build.

Shoutcast server

This link outlines the process of installing the shoutcast server.

With that done, I then set out to find a suitable client software which I could publish my stream to the shoutcast server.

Mixxx dj software

Mixxx is free ( I like that ). It does amazing things that DJ’s need to provide a great product. Excellent management of files and meta data of the tracks as well the ability to perform live broadcasts to my shoutcast server! I was thrilled to learn of it’s capabilities, and then amazed at all the wild features it has. Number one was the Auto DJ function which plays a list of songs, and gives me the ability to patch in a microphone and talk during the broadcast! After gathering up a PC, installing the software, and connecting it through a mixer, I was up and running! I now had a source to get my stream out there, and a url to connect to it. But I wasn’t done yet.

Web stream interface

In order to listen to my stream, I needed to connect to it from anywhere and from any device I wanted. I could download the m3u file and play it in iTunes, or VLC or some other app, but I wasn’t happy with that, because not everyone in the circle of friends/world knew how to handle an .m3u file. It had to be simpler than that for everyone else to access the station.

I sat down and began researching it – quickly learned the code required to connect to the stream. Here it is:

<audio id="MediaPlayer1" src="http://hawkwynd.com:8000/;" controls="controls"></audio>

 

It was the beginning of a project which would be weeks in the making. With each revision of my code, I built more and more into it. A curiosity became an obsession, and I was heads-down on my way to building my interface for a web-based streaming client! Giddy as a school-boy, I created my “wish-list” of features for my web client:

  1. Display the artist and track name on the application.
  2. Display the album and display it’s picture on the application.
  3. Display the year the album was released, and the label it was released on.
  4. Display a history of songs played in the last hour or so.
  5. Show some information about the artist/band being played to provide interactive reading for the user.
  6. Dig down deep and find some other useful tidbits of information for reading to teach users a bit of history factoids about members, etc.
  7. A way to easily share the link to social media (facebook) so others could easily join in the broadcast and spread the word of my newfound obsession.
  8. Lastly, show some ‘nerdly’ statistics, and provide a status of the stream, including who else was listening to the broadcast.
  9. Code it all in php with some jQuery to make it truly dynamic.

I figured #1 and #2 was quite the list, so I decided to ‘keep it simple’. Yeah, right. It quickly spiraled into the 9 items listed above, and the more I built, the more I wasn’t satisfied with the features! This went on night after night – I’d come home from work and jump right on the computer to pick up coding where I left off, all the while listening to my playlist (another story on how that evolved and continues to evolve another time).

After 3 weeks, I was ready to release it to a few test subjects. It was received with fair feedback, which told me I might be trying to re-invent the wheel. Why would anyone want this when they’re already using iTunes, and other shit stored on their phones, listening to their playlists? What could I offer that they don’t already have?

What indeed. I started to become despondent. Was I wasting my time?

If you build it – they will come

To this day, I continue to share, stream and once a week on Friday night, I host a live broadcast from 9pm to 1am. I remain vigilant to share the link on social media, and try to encourage my friends to listen in. Rome wasn’t built in a day. I get excited when I see 3 listeners and I’m not one of them. Some day, my little project will have a following, so I will work on honing my broadcast “radio voice” and hopefully will find a niche that will attract listeners who share in my love of find music.

http://stream.hawkwynd.com

Hope to see you online!