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.
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.
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
.
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)
.
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.
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.
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).
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.
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); });
3.15.153.69