Extending the module's API

There are many ways we can extend our module, for example, we could make it support more MP3 types, but this is merely leg work. It just takes finding out the different sync words and bitrates for different types of MP3, and then adding these to the relevant places.

For a more interesting venture, we could extend the API, creating more options for our module users.

Since we use a stream to read our MP3 file, we could allow the user to pass in either a filename or a stream of MP3 data, offering both ease (with a simple filename) and flexibility (with streams). This way we could start a download stream, STDIN stream, or in fact any stream of MP3 data.

Getting ready

We'll pick up our module from where we left it at the end of Allowing for multiple instances in the There's more... section of the previous recipe.

How to do it...

First, we'll add some more tests for our new API. In tests/index.js, we'll pull out the callback function from the mp3dat.stat call into the global scope, and we'll call it cb:

function cb (err, stats) {
  should.ifError(err);

  //expected properties
  stats.should.have.property('duration'),

  //...all the other unit tests here

  console.log('passed'),

};

Now we'll call stat along with a method which we're going to write and name: statStream:

mp3dat.statStream({stream: fs.createReadStream(testFile),
  size: fs.statSync(testFile).size}, cb);
  
mp3dat2.stat(testFile, cb);

Notice we're using two Mp3dat instances (mp3dat and mp3dat2). So we can run stat and statStream tests side by side. Since we're creating a readStream, we require fs at the top of our [tests/index.js] file.

var should = require('should'),
var fs = require('fs'),
var mp3dat = require('../index.js'),
  mp3dat2 = mp3dat.spawnInstance();

We'll also put a few top-level should tests in for the statStream method as follows:

should.exist(mp3dat);
mp3dat.should.have.property('stat'),
mp3dat.stat.should.be.an.instanceof(Function);
mp3dat.should.have.property('statStream'),
mp3dat.statStream.should.be.an.instanceof(Function);

Now to live up to our tests expectations.

Within lib/index.js, we add a new method to the prototype of Mp3dat. Instead of taking a filename for the first parameter, it will accept an object (which we'll call opts) that must contain stream and size properties:

Mp3dat.prototype.statStream = function (opts, cb) {
  var self = this,
    errTxt = 'First arg must be options object with stream and size',
    validOpts = ({}).toString.call(opts) === '[object Object]'
      && opts.stream
      && opts.size
      && 'pause' in opts.stream
      && !isNaN(+opts.size);
   lib
  if (!validOpts) {
    cb(new Error(errTxt));
    return;
  }
        
  self.size = opts.size;
  self.f = opts.stream.path;
  
  self.stream = opts.stream;
  
  self._findBitRate(function (err, bitrate) {
    if (err) { cb(err); return; }
    self._buildStats(cb);
  });    

}

Finally, just a few modifications to _findBitRate and we're done.

Mp3dat.prototype._findBitRate = function(cb) {
  var self = this,
    stream = self.stream || fs.createReadStream(self.f);
  stream
    .on('data', function (data) {
      var i = 0;
       for (i; i < data.length; i += 2) {
        if (data.readUInt16LE(i) === 64511) {
          self.bitrate = self._bitrates[data.toString('hex', i + 2, i + 3)[0]];
          this.destroy();
          cb(null);
          break;
        };
//rest of the _findBitRate function...

We conditionally hook onto either a passed in stream, or we create a stream from a given filename.

Let's run our tests (from the mp3dat folder):

node tests

The result should be:

passed
passed

One for stat, one for statStream.

How it works...

We were already using a stream to retrieve our data. We simply expose this interface to the user by modifying _findBitRate so it either generates its own stream from a filename, or if a stream is present in the parent constructors properties (self.stream), it simply plugs that stream into the process that was already in place.

We then make this functionality available to the module user by defining a new API method: statStream. We conceptualize this first by making tests for it, then define it through Mp3dat.prototype.

The statStream method is similar to the stat method (in fact, we could merge them, see There's more...). Aside from checking the validity of the input, it simply adds one more property to an Mp3dat instance: the stream property, which is taken from opts.stream. For convenience, we cross reference opts.stream.path with self.f (this may or may not be available depending on the type of stream). This is essentially redundant but may be useful for debugging purposes on the users part.

At the top of statStream we have the validOpts variable, which has a series of expressions connected by&& conditionals. This is shorthand for a bunch of if statements. If any of these expression tests fail, the opts object is not valid. One expression of interest is'pause' in opts.stream, which tests whether opts.stream is definitely a stream or inherited from a stream (all streams have a pause method, and in checks for the property throughout the entire prototype chain). Another noteworthy expression among the validOpts tests is !isNaN(+opts.size), which checks whether opts.size is a valid number. The + which precedes it converts it to a Number type and !isNaN checks that it isn't"not a number" (there is no isNumber in JavaScript so we use !isNaN).

There's more...

Now we have this new method. Let's write some more examples. We'll also see how we can merge statStream and stat together, and further enhance our module by causing it to emit events.

Making the STDIN stream example

To demonstrate usage with other streams we might write an example using the process.stdin stream as follows:

//to use try :
// cat ../test/test.mp3 | node stdin_stream.js 82969
// the argument (82969) is the size in bytes of the mp3


if (!process.argv[2]) {
  process.stderr.write('
Need mp3 size in bytes

'),
  process.exit();
}

var mp3dat = require('../'),
process.stdin.resume();
mp3dat.statStream({stream : process.stdin, size: process.argv[2]}, function (err, stats) {
  if (err) { console.log(err); }
  console.log(stats);
});

We've included comments in the example to ensure our users understand how to use it. All we do here is receive the process.stdin stream and the file size, then pass them to our statStream method.

Making the PUT upload stream example

In the Handling file uploads recipe ofChapter 2, Exploring the HTTP Object, we created a PUT upload implementation in the There's more... section of that recipe.

We'll take the put_upload_form.html file from that recipe, and create a new file called HTTP_PUT_stream.js in our mp3dat/examples folder.

var mp3dat = require('../../mp3dat'),
var http = require('http'),
var fs = require('fs'),
var form = fs.readFileSync('put_upload_form.html'),
http.createServer(function (req, res) {
  if (req.method === "PUT") {
    mp3dat.statStream({stream: req, size:req.headers['content-length']}, function (err, stats) {
      if (err) { console.log(err); return; }
      console.log(stats);
    });
    
  }
  if (req.method === "GET") {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end(form);
  }
}).listen(8080);

Here, we create a server that serves the put_upload_form.html file. The HTML file allows us to specify a file to upload (which must be a valid MP3 file), and then sends it to the server.

In our server, we pass req (which is a stream) to the stream property and req.headers['content-length'] which gives us the size of MP3 in bytes as specified by the browser via the Content-Length header.

We then finish by logging stats to the console (we could also extend this example by sending stats back to the browser in JSON form).

Merging stat and statStream

There's a lot of similar code between stat and statStream. With a bit of restructuring, we can merge them into one method, allowing the user to pass either a string containing a filename or an object containing stream and size properties straight into the stat method.

First, we'd need to update our tests and examples. In test/index.js, we should remove the following code:

mp3dat.should.have.property('statStream'),
mp3dat.statStream.should.be.an.instanceof(Function);

Since we're merging statStream into stat, our two calls to stat and statStream should become:

mp3dat.stat({stream: fs.createReadStream(testFile),
    size: fs.statSync(testFile).size}, cb);
mp3dat2.stat(testFile, cb);

The statStream line in examples/stdin_stream.js should become:

mp3dat.stat({stream : process.stdin, size: process.argv[2]}

In HTTP_PUT_stream.js it should be:

mp3dat.stat({stream: req, size: req.headers['content-length']}

In lib/index.js, we trash the streamStat method, inserting a _compile method:

Mp3dat.prototype._compile =  function (err, fstatsOpts, cb) {
  var self = this;
  self.size = fstatsOpts.size;
  self.stream = fstatsOpts.stream;
    self._findBitRate(function (err, bitrate) {
    if (err) { cb(err); return; }
    self._buildStats(cb);
  });    
}

Finally, we modify our Mp3dat.prototype.stat method as follows:

Mp3dat.prototype.stat = function (f, cb) {
  var self = this, isOptsObj = ({}).toString.call(f) === '[object Object]';
    
  if (isOptsObj) {
    var opts = f, validOpts = opts.stream && opts.size
      && 'pause' in opts.stream && !isNaN(+opts.size);
    errTxt = 'First arg must be options object with stream and size'
        
    if (!validOpts) { cb(new Error(errTxt)); return; }
    
    self.f = opts.stream.path;
    self._compile(null, opts, cb);
    return;
  }
  
  self.f = f;
  fs.stat(f, function (err, fstats) {
    self._compile.call(self, err, fstats, cb);
  });
}

The code that actually generates the stats has been placed into the _compile method. If the first argument is an object, we assume a stream and stats take on the role of the former statStream, calling _compile and returning from the function early. If not, we assume a filename and invoke _compile within the fs.stat callback with JavaScript's call method, ensuring our this / self variable carries through the _compile method.

Integrating the EventEmitter

Throughout this book, we have generally received data from modules via callback parameters or through listening for events. We can extend our modules interface further, allowing users to listen for events by causing Node's EventEmitter to adopt our Mp3dat constructor.

We need to require the events and util modules, then hook up Mp3dat with EventEmitter by assigning the this object of Mp3dat to it, and then give it the super powers of Mp3dat EventEmitter by using util.inherits:

var fs = require('fs'),
  EventEmitter = require('events').EventEmitter,
  util = require('util'),

function Mp3dat() {
  if (!(this instanceof Mp3dat)) {
    return new Mp3dat();
  }  
  EventEmitter.call(this);
  this.stats = {duration:{}};
}

util.inherits(Mp3dat, EventEmitter);

All we do now is go through the existing methods of Mp3dat and insert the emit events in relevant places. We can emit the bitrate once it's found as follows:

Mp3dat.prototype._findBitRate = function(cb) {
//beginning of _findBitRate method
       for (i; i < data.length; i += 2) {
        if (data.readUInt16LE(i) === 64511) {
          self.bitrate = self._bitrates[data.toString('hex', i + 2, i + 3)[0]];
          this.destroy();
          self.emit('bitrate', self.bitrate);
          cb(null);
          break;
        };
 //rest of _findBitRate method

Where we would callback with an error, we can also emit that error as shown in the following code:

//last part of _findBitRate method
  }).on('end', function () {
    var err = new Error('could not find bitrate, is this definately an MPEG-1 MP3?'),
    self.emit('error', err);
    cb(err);
  });

Then there's the time signature:

Mp3dat.prototype._timesig = function () {
 //_timesig function code....
  self.emit('timesig', ts);
  return ts;
}

And of course, the stats object:

Mp3dat.prototype._buildStats = function (cb) {
//_buildStats code
  self._timeProcessor(hours, function (duration) {
   //_timeProcessor code
    self.emit('stats', self.stats);
    if (cb) { cb(null, self.stats); }    
  });
}

We've also added if (cb) to _buildStats, since a callback may no longer be necessary if the user opts to listen for events instead.

If a module user is dynamically generating the Mp3dat instances, they may wish to have a way to hook into a spawned instance event:

Mp3dat.prototype.spawnInstance = function () {
  var m = Mp3dat();
  this.emit('spawn', m);
  return m;
}

Finally, to allow chaining, we can also return the Mp3dat instance from the stat function from two places. First within the isOptsObj block as follows:

Mp3dat.prototype.stat = function (f, cb) {
//stat code
  if (isOptsObj) {
    //other code here
    self._compile(null, opts, cb);
    return self;
  }

Then right at the end of the function, as shown in the following code:

  //prior stat code
  self.f = f;
  fs.stat(f, function (err, fstats) {
    self._compile.call(self, err, fstats, cb);
  });
  return self;
}

This is because we return early from the function depending on the detected input (filename or stream), so we have to return self from two places.

Now we can write an example for our new user interface. Let's make a new file in mp3dat/examples called event_emissions.js.

var mp3dat = require('../index'),

mp3dat
  .stat('../test/test.mp3')
  .on('bitrate', function (bitrate) {
    console.log('Got bitrate:', bitrate);
  })
  .on('timesig', function (timesig) {
     console.log('Got timesig:', timesig);
  })
  .on('stats', function (stats) {
     console.log('Got stats:', stats);
     mp3dat.spawnInstance();
  })
  .on('error', function (err) {
     console.log('Error:', err);
  })
  .on('spawn', function (mp3dat2) {
    console.log('Second mp3dat', mp3dat2);
  });

See also

  • Creating a test-driven module API discussed in this chapter
  • Handling file uploads discussed In Chapter 2,Exploring the HTTP Object
  • Deploying a module to npm discussed in this chapter
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.153.69