Permalink
Browse files

Initial work on shoutcast server. REALLY rudimentary but it streams m…

…usic. :) Copy of sys.pump in the code for the purposes of custom pump functionality, will likely change this to just use the eventing the way it should be and use sys.pump.

Signed-off-by: Nick Campbell <nicholas.j.campbell@gmail.com>
  • Loading branch information...
0 parents commit 8295dfbfd4a192fe78b13eb69c22a83a7d21e4dd @ncb000gt committed Oct 20, 2010
Showing with 313 additions and 0 deletions.
  1. +169 −0 lib/file.js
  2. +144 −0 shoutcast.js
169 lib/file.js
@@ -0,0 +1,169 @@
+var path = require('path');
+var fs = require('fs');
+var sys = require('sys');
+
+exports.mkdirs = function (_path, mode, callback) {
+ _path = exports.path.abspath(_path);
+
+ var dirs = _path.split("/");
+ var walker = [dirs.shift()];
+
+ var walk = function (ds, acc, m, cb) {
+ if (ds.length > 0) {
+ var d = ds.shift();
+ acc.push(d);
+ var dir = acc.join("/");
+
+ fs.stat(dir, function (err, stat) {
+ if (err) {
+ // file does not exist
+ if (err.errno == 2) {
+ fs.mkdir(dir, m, function (erro) {
+ if (erro && erro.errno != 17) {
+ sys.p(erro);
+ return cb(new Error("Failed to make " + dir + "\n" + erro));
+ } else {
+ return walk(ds, acc, m, cb);
+ }
+ });
+ } else {
+ return cb(err);
+ }
+ } else {
+ if (stat.isDirectory()) {
+ return walk(ds, acc, m, cb);
+ } else {
+ return cb(new Error("Failed to mkdir " + dir + ": File exists\n"));
+ }
+ }
+ });
+ } else {
+ return cb();
+ }
+ };
+ return walk(dirs, walker, mode, callback);
+};
+
+exports.mkdirsSync = function (_path, mode) {
+ if (_path[0] !== "/") {
+ _path = path.join(process.cwd(), _path)
+ }
+
+ var dirs = _path.split("/");
+ var walker = [dirs.shift()];
+
+ dirs.reduce(function (acc, d) {
+ acc.push(d);
+ var dir = acc.join("/");
+
+ try {
+ var stat = fs.statSync(dir);
+ if (!stat.isDirectory()) {
+ throw "Failed to mkdir " + dir + ": File exists";
+ }
+ } catch (err) {
+ fs.mkdirSync(dir, mode);
+ }
+ return acc;
+ }, walker);
+};
+
+exports.walk = function (start, callback) {
+ fs.lstat(start, function (err, stat) {
+ if (err) { return callback(err) }
+ if (stat.isDirectory()) {
+
+ fs.readdir(start, function (err, files) {
+ var coll = files.reduce(function (acc, i) {
+ var abspath = path.join(start, i);
+
+ if (fs.statSync(abspath).isDirectory()) {
+ exports.walk(abspath, callback);
+ acc.dirs.push(abspath);
+ } else {
+ acc.names.push(abspath);
+ }
+
+ return acc;
+ }, {"names": [], "dirs": []});
+
+ return callback(null, start, coll.dirs, coll.names);
+ });
+ } else {
+ return callback(new Error("path: " + start + " is not a directory"));
+ }
+ });
+};
+
+exports.walkSync = function (start, callback) {
+ var stat = fs.statSync(start);
+
+ if (stat.isDirectory()) {
+ var filenames = fs.readdirSync(start);
+
+ var coll = filenames.reduce(function (acc, name) {
+ var abspath = path.join(start, name);
+
+ if (fs.statSync(abspath).isDirectory()) {
+ acc.dirs.push(name);
+ } else {
+ acc.names.push(name);
+ }
+
+ return acc;
+ }, {"names": [], "dirs": []});
+
+ callback(start, coll.dirs, coll.names);
+
+ coll.dirs.forEach(function (d) {
+ var abspath = path.join(start, d);
+ exports.walkSync(abspath, callback);
+ });
+
+ } else {
+ throw new Error("path: " + start + " is not a directory");
+ }
+};
+
+exports.path = {};
+
+exports.path.abspath = function (to) {
+ var from;
+ switch (to.charAt(0)) {
+ case "~": from = process.env.HOME; to = to.substr(1); break
+ case "/": from = ""; break
+ default : from = process.cwd(); break
+ }
+ return path.join(from, to);
+}
+
+exports.path.relativePath = function (base, compare) {
+ base = base.split("/");
+ compare = compare.split("/");
+
+ if (base[0] == "") {
+ base.shift();
+ }
+
+ if (compare[0] == "") {
+ compare.shift();
+ }
+
+ var l = compare.length;
+
+ for (var i = 0; i < l; i++) {
+ if (!base[i] || (base[i] != compare[i])) {
+ return compare.slice(i).join("/");
+ }
+ }
+
+ return ""
+};
+
+exports.path.join = function (head, tail) {
+ if (head == "") {
+ return tail;
+ } else {
+ return path.join(head, tail);
+ }
+};
144 shoutcast.js
@@ -0,0 +1,144 @@
+var net = require("net")
+,fs = require("fs")
+,sys = require("sys")
+,http = require('http')
+,file = require('./lib/file');
+
+var song_queue = {
+ songs: [],
+ idx: 0
+};
+
+var STATION = {
+ NAME: 'Node Shoutcasts R Us',
+ GENRE: 'Musak',
+ URL: 'http://github.com/ncb000gt/node-shoutcast/',
+ NOTICE: 'I\'m a little teapot...'
+};
+
+var config = require('/usr/local/etc/node-shoutcast/config');
+sys.debug('config: ' + JSON.stringify(config));
+if ('STATION' in config) {
+ var config_station = config.STATION;
+ if ('NAME' in config_station) {
+ STATION.NAME = config_station.NAME;
+ } else if ('GENRE' in config_station) {
+ STATION.GENRE = config_station.GENRE;
+ } else if ('URL' in config_station) {
+ STATION.URL = config_station.URL;
+ } else if ('NOTICE' in config_station) {
+ STATION.NOTICE = config_station.NOTICE;
+ }
+}
+
+var PLAYLIST = config.PLAYLIST;
+var QUEUE_SIZE = config.QUEUE_SIZE || 10;
+
+sys.debug('PLAYLIST: ' + PLAYLIST);
+var stats = fs.statSync(PLAYLIST);
+if (stats.isDirectory()) {
+ file.walkSync(PLAYLIST, function(p, dirs, files) {
+ var len = files.length;
+ for (var i = 0; i < len; i++) {
+ var file = files[i];
+ if (file.indexOf('.mp3') >= 0) {
+ song_queue.songs.push(p+'/'+file);
+ }
+ if (song_queue.songs.length > QUEUE_SIZE) {
+ break;
+ }
+ }
+ });
+}
+
+var headers = {
+ 'icy-notice1': STATION.NOTICE,
+ 'icy-notice2': "NodeJS Streaming Shoutcast Server/v0.1",
+ 'icy-name': STATION.NAME,
+ 'icy-genre': STATION.GENRE,
+ 'icy-url': STATION.URL,
+ 'Content-Type': 'audio/mpegurl',
+ 'icy-pub':'0',
+ 'icy-br':'56',
+ 'icy-metaint': '0'//1024'
+};
+
+var currentSong = null;
+var filetoread = "/home/ncampbell/Music/Amazon MP3/David Guetta/One Love (Deluxe Version)/Memories (Feat Kid Cudi).mp3";
+
+var bytesOut = 0;
+
+http.createServer(
+ function (req, res) {
+ var o = req.headers;
+ for (var p in o) {
+ sys.puts(p + ': ' + o[p]);
+ }
+
+ sys.puts('Starting stream...');
+
+ res.writeHead(200, headers);
+
+ //TODO: pop song and make read song as a stream separate from the request, requests should just tie into the already running stream
+ var song = song_queue.songs[song_queue.idx];
+ sys.debug('song: ' + song);
+ var fStream = fs.createReadStream(song, {bufferSize:1024});
+
+ pump(fStream, res, function() { sys.puts("No more data. Closing."); });
+
+ }).listen(7000);
+
+setInterval(function() {
+ sys.debug('Total Bytes Written: ' + bytesOut);
+ }, 3000);
+
+sys.puts('Server running at http://0.0.0.0:7000/');
+
+function pump(readStream, writeStream, callback) {
+ var callbackCalled = false;
+
+ function call (a, b, c) {
+ if (callback && !callbackCalled) {
+ callback(a, b, c);
+ callbackCalled = true;
+ }
+ }
+
+ if (!readStream.pause) readStream.pause = function () {readStream.emit("pause")};
+ if (!readStream.resume) readStream.resume = function () {readStream.emit("resume")};
+
+ readStream.addListener("data", function (chunk) {
+ bytesOut+=chunk.length;
+ if (writeStream.write(chunk) === false) readStream.pause();
+ });
+
+ writeStream.addListener("pause", function () {
+ readStream.pause();
+ });
+
+ writeStream.addListener("drain", function () {
+ readStream.resume();
+ });
+
+ writeStream.addListener("resume", function () {
+ readStream.resume();
+ });
+
+ readStream.addListener("end", function () {
+ writeStream.end();
+ });
+
+ readStream.addListener("close", function () {
+ call();
+ });
+
+ readStream.addListener("error", function (err) {
+ writeStream.end();
+ call(err);
+ });
+
+ writeStream.addListener("error", function (err) {
+ readStream.destroy();
+ call(err);
+ });
+};

0 comments on commit 8295dfb

Please sign in to comment.