How To Build Personal Web API Server Using Raspberry PI And Node.js

Introduction 

I have various streaming boxes (Apple TV, Roku, Fire TV, Nexus Player) because I prefer to watch video tutorials on TV rather than watching on my laptop. However, we don't have apps for all the sites that provide video tutorials. For example, MSDN channel9 doesn't have an app for any streaming boxes. So, I decided to use Roku sample video player app to play channel 9 videos. This app expects the video content to be defined in a certain XML format. So, I just have to build the Web APIs to return the XML output dynamically. I also prefer not to host the Web APIs on the internet. I just want to host it on my home network. So, I need a web server that will always be on so, I decided to use my existing Raspberry PI 3 as a web server.

Setup and Creating Web API

I initially thought of using .NET Core on Windows IoT but later, decided to use Raspian OS and NodeJS because I wanted to try NodeJS with some real-world applications.

Setting up Raspian OS on Raspberry PI was pretty simple. I just followed this excellent article on how to setup Raspian on Raspberry PI. Also, I configured the remote connection and shared the work folder to deploy the files so I no longer needed my Raspberry PI connected to my TV. I just placed the Raspberry PI along with my other stream boxes and connected through my laptop using the remote desktop.

I started the NodeJS Web API with the following Node packages to parse the Channel 9 feeds.

  • Express
  • Cheerio
  • XMLBuilder

Express is one of the most famous Node.js web application frameworks to create Web APIs quickly and easily. Cheerio is used for parsing the DOM elements. XMLBuilder is used to construct the XML output in an easy way.

To start with, I created the web server with routing as follows,

  1. var express = require('express')    
  2.   , app = express()    
  3.   , server = require('http').createServer(app)    
  4.   , path = require('path')    
  5.      
  6.  var parser = require('./lib/parser');    
  7.      
  8.  app.set('port', 4567);    
  9.  app.use(express.static(path.join(__dirname, 'public')));    
  10.      
  11.  //Routes    
  12.  app.get('/'function (req, res) {    
  13.   res.sendFile(__dirname + '/public/index.html');    
  14.  });    
  15.      
  16.  app.get('/Ch9'function (req, res) {    
  17.   parser.Ch9(function (result) {    
  18.    res.set('Content-Type''text/xml')    
  19.    res.send(200, result)    
  20.   })    
  21.  });    
  22.      
  23.  app.get('/Ch9/:topic/List'function (req, res) {    
  24.   var topic = req.params.topic;    
  25.   parser.Ch9List(topic, function (result) {    
  26.    res.set('Content-Type''text/xml')    
  27.    res.send(200, result)    
  28.   });    
  29.  });    
  30.      
  31.  app.get('/Ch9/:topic/:title/View'function (req, res) {    
  32.   var topic = req.params.topic;    
  33.   var title = req.params.title;    
  34.   parser.Ch9View(topic, title, function (result) {    
  35.    res.set('Content-Type''text/xml')    
  36.    res.send(200, result)    
  37.   });    
  38.  });    

I created the parser library class and added the following code to parse the Channel 9 RSS content to fetch the video URLs.

  1. const topicLists = ["Shows""Events""Series""Blogs"];    
  2.  const ch9BaseURL = "http://channel9.msdn.com/Browse/";    
  3.  exports.Ch9 = function (callback) {    
  4.    var xml = builder.create('categories');    
  5.    var cIndex;    
  6.    for (cIndex in topicLists) {    
  7.      var category = xml.ele("category");    
  8.      category.att("title", topicLists[cIndex]);    
  9.      category.att("description", topicLists[cIndex]);    
  10.      
  11.      var catLeaf = category.ele("categoryLeaf");    
  12.      catLeaf.att("title", topicLists[cIndex]);    
  13.      catLeaf.att("description", topicLists[cIndex]);    
  14.      catLeaf.att("feed", appBaseURL + "Ch9/" + topicLists[cIndex] + "/List/");    
  15.    }    
  16.    xml.end({ pretty: true });    
  17.    callback(xml.toString());    
  18.  }    
  19.      
  20.  exports.Ch9List = function (topic, callback) {    
  21.    var xml;    
  22.    request(ch9BaseURL + topic + '/rss?sort=recent'function (error, response, html) {    
  23.      if (!error && response.statusCode == 200) {    
  24.        var $ = cheerio.load(html, { ignoreWhitespace: true, xmlMode: true });    
  25.        xml = builder.create('categories');    
  26.        var currIndex = 0;    
  27.        $('item').each(function (i, element) {    
  28.          var category = xml.ele("category");    
  29.          category.att("title", $(element).find('title').text());    
  30.          category.att("description", $(element).find('description').text());    
  31.      
  32.          var catLeaf = category.ele("categoryLeaf");    
  33.          catLeaf.att("title", $(element).find('title').text());    
  34.          catLeaf.att("description", $(element).find('description').text());    
  35.          catLeaf.att("feed", appBaseURL + "Ch9/" + topic + "/" + encodeURIComponent($(element).find('title').text()) + "/View/");    
  36.          currIndex++;    
  37.        });    
  38.      
  39.        xml.end({ pretty: true });    
  40.        callback(xml.toString());    
  41.      }    
  42.    });    
  43.  }    
  44.      
  45.  exports.Ch9View = function (topic, title, callback) {    
  46.    var xml;    
  47.    request(ch9BaseURL + topic + '/rss?sort=recent'function (error, response, html) {    
  48.      if (!error && response.statusCode == 200) {    
  49.        var $ = cheerio.load(html, { ignoreWhitespace: true, xmlMode: true });    
  50.        var curElement = $('item').filter(function (i, el) {    
  51.          return $(this).find('title').text() === title;    
  52.        });    
  53.        var feedURL = $(curElement).find('c9\\:feed').text();    
  54.        if (!feedURL)    
  55.          feedURL = $(curElement).find('c9\\:feed').text() + "/Rss";    
  56.      
  57.        request(feedURL, function (error, response, html) {    
  58.          var $ = cheerio.load(html, { ignoreWhitespace: true, xmlMode: true });    
  59.          xml = builder.create('feed');    
  60.          var loopIndex = 0;    
  61.          $('item').each(function (i, element) {    
  62.            var url = $(element).find('media\\:content').filter(function (i, el) {    
  63.              return $(this).attr('url').includes("_high");    
  64.            }).attr("url");    
  65.      
  66.            var item = xml.ele('item');    
  67.            item.att('sdImg', $(this).children('media\\:thumbnail').attr('url'));    
  68.            item.att('hdImg', $(this).children('media\\:thumbnail').attr('url'));    
  69.            item.att('thumbnailURL', $(this).children('media\\:thumbnail').attr('url'));    
  70.            item.ele('title', $(this).children('title').text());    
  71.            item.ele('contentId', loopIndex);    
  72.            item.ele('streamFormat''mp4');    
  73.            var media = item.ele('media');    
  74.            media.ele('streamUrl', url);    
  75.            media.ele('thumbnailURL', $(this).children('media\\:thumbnail').attr('url'));    
  76.            loopIndex++;    
  77.          });    
  78.          xml.end({ pretty: true });    
  79.          callback(xml.toString());    
  80.        });    
  81.      }    
  82.    });    
  83.  }    

That's it. We have just implemented the web API using NodeJS with just a few lines of code. If I run my application using http://localhost:4567/ch9, it responds back with xml output.

The deployment is super easy too. Just copy the folder excluding Node_Modules folder. You don't have to deploy the node_modules folder. You can run the NPM Update command to get the node_modules folder from PI Server after deploying it to save some deployment time.

To run the App Server, just open the terminal from Raspian and run it using the following  Node APP.js command and your personal web server is ready to serve.

I have modified the UrlCategoryFeed in categoryFeed.brs in the Roku app before I deployed it.

Update : Roku recently updated their video channel sample app and the new app can be found hereYou can find all my articles at https://jeevasubburaj.com

Happy Coding!!