mirror of
https://github.com/zoriya/flood.git
synced 2025-12-06 07:16:18 +00:00
Initial commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
bower_components
|
||||
public/stylesheets
|
||||
public/scripts
|
||||
72
app.js
Normal file
72
app.js
Normal file
@@ -0,0 +1,72 @@
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var favicon = require('serve-favicon');
|
||||
var logger = require('morgan');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var bodyParser = require('body-parser');
|
||||
|
||||
var routes = require('./routes/index');
|
||||
var torrents = require('./routes/torrents');
|
||||
var client = require('./routes/client');
|
||||
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
|
||||
// uncomment after placing your favicon in /public
|
||||
//app.use(favicon(__dirname + '/public/favicon.ico'));
|
||||
app.use(logger('dev'));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
req.socket.on("error", function(err) {
|
||||
console.log(err);
|
||||
});
|
||||
res.socket.on("error", function(err) {
|
||||
console.log(err);
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/', routes);
|
||||
app.use('/torrents', torrents);
|
||||
app.use('/client', client);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
var err = new Error('Not Found');
|
||||
err.status = 404;
|
||||
next(err);
|
||||
});
|
||||
|
||||
// error handlers
|
||||
|
||||
// development error handler
|
||||
// will print stacktrace
|
||||
if (app.get('env') === 'development') {
|
||||
app.use(function(err, req, res, next) {
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// production error handler
|
||||
// no stacktraces leaked to user
|
||||
app.use(function(err, req, res, next) {
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: {}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = app;
|
||||
90
bin/www
Executable file
90
bin/www
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('flood:server');
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
84
gulpfile.js
Normal file
84
gulpfile.js
Normal file
@@ -0,0 +1,84 @@
|
||||
var gulp = require('gulp'),
|
||||
sass = require('gulp-sass'),
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
watch = require('gulp-watch'),
|
||||
notify = require('gulp-notify'),
|
||||
browserSync = require('browser-sync'),
|
||||
browserify = require('browserify'),
|
||||
watchify = require('watchify'),
|
||||
reactify = require('reactify'),
|
||||
source = require('vinyl-source-stream');
|
||||
|
||||
var supportedBrowsers = ['last 2 versions', '> 1%', 'ie >= 8', 'Firefox ESR', 'Opera >= 12'],
|
||||
jsFiles = [];
|
||||
|
||||
var sourceDir = './source/',
|
||||
destDir = './public/';
|
||||
|
||||
var reload = browserSync.reload;
|
||||
|
||||
function handleErrors() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
notify.onError({
|
||||
title: "Compile Error",
|
||||
message: "<%= error.message %>"
|
||||
}).apply(this, args);
|
||||
this.emit('end');
|
||||
}
|
||||
|
||||
gulp.task('browser-sync', function() {
|
||||
|
||||
return browserSync.init({
|
||||
port: 3001
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('styles', function() {
|
||||
|
||||
return gulp.src(sourceDir + 'sass/style.scss')
|
||||
.pipe(sass({
|
||||
errLogToConsole: true
|
||||
}))
|
||||
.pipe(autoprefixer({
|
||||
browsers: supportedBrowsers,
|
||||
map: true
|
||||
}))
|
||||
.pipe(browserSync.reload({
|
||||
stream: true
|
||||
}))
|
||||
.pipe(gulp.dest(destDir + 'stylesheets'));
|
||||
});
|
||||
|
||||
gulp.task('scripts', function() {
|
||||
|
||||
var bundler = watchify(browserify({
|
||||
entries: [sourceDir + '/scripts/app.js'],
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
fullPaths: true
|
||||
}));
|
||||
|
||||
bundler.transform(reactify);
|
||||
|
||||
function rebundle() {
|
||||
|
||||
var stream = bundler.bundle();
|
||||
|
||||
return stream.on('error', handleErrors)
|
||||
.pipe(source('app.js'))
|
||||
.pipe(gulp.dest(destDir + 'scripts/'));
|
||||
}
|
||||
|
||||
bundler.on('update', function() {
|
||||
rebundle();
|
||||
});
|
||||
|
||||
return rebundle();
|
||||
});
|
||||
|
||||
gulp.task('watch', function () {
|
||||
gulp.watch(sourceDir + 'scripts/**/*.js', ['scripts', reload]);
|
||||
gulp.watch(sourceDir + 'sass/**/*.scss', ['styles', reload]);
|
||||
});
|
||||
|
||||
gulp.task('default', ['scripts', 'styles', 'watch', 'browser-sync']);
|
||||
194
models/client.js
Normal file
194
models/client.js
Normal file
@@ -0,0 +1,194 @@
|
||||
var rTorrent = require('./rtorrent');
|
||||
var util = require('util');
|
||||
|
||||
function client() {
|
||||
|
||||
if((this instanceof client) === false) {
|
||||
return new client();
|
||||
}
|
||||
};
|
||||
|
||||
var defaults = {
|
||||
torrentProperties: [
|
||||
'hash',
|
||||
'name',
|
||||
|
||||
'state',
|
||||
'stateChanged',
|
||||
'isActive',
|
||||
|
||||
'uploadRate',
|
||||
'uploadTotal',
|
||||
'downloadRate',
|
||||
'downloadTotal',
|
||||
'ratio',
|
||||
|
||||
'bytesDone',
|
||||
'sizeBytes',
|
||||
|
||||
'chunkSize',
|
||||
'chunksCompleted',
|
||||
|
||||
'peersAccounted',
|
||||
'peersComplete',
|
||||
'peerExchange',
|
||||
'peersNotConnected',
|
||||
'trackerFocus',
|
||||
|
||||
'basePath',
|
||||
'creationDate',
|
||||
|
||||
'customSeedingTime',
|
||||
'customAddTime'
|
||||
],
|
||||
torrentPropertyMethods: [
|
||||
'main',
|
||||
|
||||
'd.get_hash=',
|
||||
'd.get_name=',
|
||||
|
||||
'd.get_state=',
|
||||
'd.get_state_changed=',
|
||||
'd.is_active=',
|
||||
|
||||
'd.get_up_rate=',
|
||||
'd.get_up_total=',
|
||||
'd.get_down_rate=',
|
||||
'd.get_down_total=',
|
||||
'd.get_ratio=',
|
||||
|
||||
'd.get_bytes_done=',
|
||||
'd.get_size_bytes=',
|
||||
|
||||
'd.get_chunk_size=',
|
||||
'd.get_completed_chunks=',
|
||||
|
||||
'd.get_peers_accounted=',
|
||||
'd.get_peers_complete=',
|
||||
'd.get_peer_exchange=',
|
||||
'd.get_peers_not_connected=',
|
||||
'd.get_tracker_focus=',
|
||||
|
||||
'd.get_base_path=',
|
||||
'd.get_creation_date=',
|
||||
|
||||
'd.get_custom=seedingtime',
|
||||
'd.get_custom=addtime'
|
||||
],
|
||||
clientProperties: [
|
||||
'uploadRate',
|
||||
'uploadTotal',
|
||||
|
||||
'downloadRate',
|
||||
'downloadTotal'
|
||||
],
|
||||
clientPropertyMethods: [
|
||||
'get_up_rate',
|
||||
'get_up_total',
|
||||
|
||||
'get_down_rate',
|
||||
'get_down_total'
|
||||
]
|
||||
};
|
||||
|
||||
var mapProps = function(props, data) {
|
||||
|
||||
var mappedObject = [];
|
||||
|
||||
if (data[0].length === 1) {
|
||||
|
||||
mappedObject = {};
|
||||
|
||||
for (i = 0, len = data.length; i < len; i++) {
|
||||
mappedObject[props[i]] = data[i][0];
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
for (i = 0, lenI = data.length; i < lenI; i++) {
|
||||
|
||||
mappedObject[i] = {};
|
||||
|
||||
for (a = 0, lenA = props.length; a < lenA; a++) {
|
||||
mappedObject[i][props[a]] = data[i][a];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return mappedObject;
|
||||
};
|
||||
|
||||
var createMulticallRequest = function(data) {
|
||||
|
||||
var methodCall = [];
|
||||
|
||||
for (i = 0, len = data.length; i < len; i++) {
|
||||
methodCall.push({
|
||||
'methodName': data[i],
|
||||
'params': []
|
||||
});
|
||||
}
|
||||
|
||||
return methodCall;
|
||||
}
|
||||
|
||||
client.prototype.getTorrentList = function(callback) {
|
||||
|
||||
try {
|
||||
|
||||
rTorrent.get('d.multicall', defaults.torrentPropertyMethods)
|
||||
.then(function(data) {
|
||||
callback(null, mapProps(defaults.torrentProperties, data));
|
||||
}, function(error) {
|
||||
callback(error, null)
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
client.prototype.stopTorrent = function(hash, callback) {
|
||||
|
||||
if (!util.isArray(hash)) {
|
||||
hash = [hash];
|
||||
}
|
||||
|
||||
rTorrent.get('d.stop', hash).then(function(data) {
|
||||
callback(null, data);
|
||||
}, function(error) {
|
||||
console.log(error);
|
||||
callback(error, null);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
client.prototype.startTorrent = function(hash, callback) {
|
||||
|
||||
if (!util.isArray(hash)) {
|
||||
hash = [hash];
|
||||
}
|
||||
|
||||
rTorrent.get('d.start', hash).then(function(data) {
|
||||
callback(null, data);
|
||||
}, function(error) {
|
||||
console.log(error);
|
||||
callback(error, null);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
client.prototype.getClientStats = function(callback) {
|
||||
|
||||
rTorrent.get('system.multicall', [createMulticallRequest(defaults.clientPropertyMethods)])
|
||||
.then(function(data) {
|
||||
callback(null, mapProps(defaults.clientProperties, data));
|
||||
}, function(error) {
|
||||
callback(error, null);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = client;
|
||||
15
models/clientStats.js
Normal file
15
models/clientStats.js
Normal file
@@ -0,0 +1,15 @@
|
||||
var client = require('./client')();
|
||||
|
||||
function clientStats() {
|
||||
|
||||
if((this instanceof clientStats) === false) {
|
||||
return new clientStats();
|
||||
}
|
||||
};
|
||||
|
||||
clientStats.prototype.getStats = function(callback) {
|
||||
|
||||
client.getClientStats(callback);
|
||||
};
|
||||
|
||||
module.exports = clientStats;
|
||||
188
models/date_formatter.js
Normal file
188
models/date_formatter.js
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* @class DateFormatter
|
||||
* The DateFormatter supports decoding from and encoding to
|
||||
* ISO8601 formatted strings. Accepts formats with and without
|
||||
* hyphen/colon separators and correctly parses zoning info.
|
||||
*/
|
||||
var DateFormatter = function (opts) {
|
||||
this.opts = {}
|
||||
this.setOpts(opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Default options for DateFormatter
|
||||
* @static
|
||||
* @see DateFormatter#setOpts
|
||||
*/
|
||||
DateFormatter.DEFAULT_OPTIONS = {
|
||||
colons: true
|
||||
, hyphens: false
|
||||
, local: true
|
||||
, ms: false
|
||||
, offset: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular Expression that disects ISO 8601 formatted strings into
|
||||
* an array of parts.
|
||||
* @static
|
||||
*/
|
||||
DateFormatter.ISO8601 = new RegExp(
|
||||
'([0-9]{4})([-]?([0-9]{2}))([-]?([0-9]{2}))'
|
||||
+ '(T([0-9]{2})(((:?([0-9]{2}))?((:?([0-9]{2}))?(\.([0-9]+))?))?)'
|
||||
+ '(Z|([+-]([0-9]{2}(:?([0-9]{2}))?)))?)?'
|
||||
)
|
||||
|
||||
/**
|
||||
* Sets options for encoding Date objects to ISO8601 strings.
|
||||
* Omitting the 'opts' argument will reset all options to the default.
|
||||
*
|
||||
* @param {Object} opts - Options (optional)
|
||||
* @param {Boolean} opts.colons - Enable/disable formatting the time portion
|
||||
* with a colon as separator (default: true)
|
||||
* @param {Boolean} opts.hyphens - Enable/disable formatting the date portion
|
||||
* with a hyphen as separator (default: false)
|
||||
* @param {Boolean} opts.local - Encode as local time instead of UTC
|
||||
* (default: true)
|
||||
* @param {Boolean} opts.ms - Enable/Disable output of milliseconds
|
||||
* (default: false)
|
||||
* @param {Boolean} opts.offset - Enable/Disable output of UTC offset
|
||||
* (default: false)
|
||||
*/
|
||||
DateFormatter.prototype.setOpts = function (opts) {
|
||||
if (!opts) opts = DateFormatter.DEFAULT_OPTIONS
|
||||
|
||||
var ctx = this;
|
||||
Object.keys(DateFormatter.DEFAULT_OPTIONS).forEach(function (k) {
|
||||
ctx.opts[k] = opts.hasOwnProperty(k) ?
|
||||
opts[k] : DateFormatter.DEFAULT_OPTIONS[k]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a date time stamp following the ISO8601 format to a JavaScript Date
|
||||
* object.
|
||||
*
|
||||
* @param {String} time - String representation of timestamp.
|
||||
* @return {Date} - Date object from timestamp.
|
||||
*/
|
||||
DateFormatter.prototype.decodeIso8601 = function(time) {
|
||||
var dateParts = time.toString().match(DateFormatter.ISO8601)
|
||||
if (!dateParts) {
|
||||
throw new Error('Expected a ISO8601 datetime but got \'' + time + '\'')
|
||||
}
|
||||
|
||||
var date = [
|
||||
[dateParts[1], dateParts[3] || '01', dateParts[5] || '01'].join('-')
|
||||
, 'T'
|
||||
, [
|
||||
dateParts[7] || '00'
|
||||
, dateParts[11] || '00'
|
||||
, dateParts[14] || '00'
|
||||
].join(':')
|
||||
, '.'
|
||||
, dateParts[16] || '000'
|
||||
].join('')
|
||||
|
||||
date += (dateParts[17] !== undefined) ?
|
||||
dateParts[17] +
|
||||
((dateParts[19] && dateParts[20] === undefined) ? '00' : '') :
|
||||
DateFormatter.formatCurrentOffset(new Date(date))
|
||||
|
||||
return new Date(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JavaScript Date object to an ISO8601 timestamp.
|
||||
*
|
||||
* @param {Date} date - Date object.
|
||||
* @return {String} - String representation of timestamp.
|
||||
*/
|
||||
DateFormatter.prototype.encodeIso8601 = function(date) {
|
||||
var parts = this.opts.local ?
|
||||
DateFormatter.getLocalDateParts(date) :
|
||||
DateFormatter.getUTCDateParts(date)
|
||||
|
||||
return [
|
||||
[parts[0],parts[1],parts[2]].join(this.opts.hyphens ? '-' : '')
|
||||
, 'T'
|
||||
, [parts[3],parts[4],parts[5]].join(this.opts.colons ? ':' : '')
|
||||
, (this.opts.ms) ? '.' + parts[6] : ''
|
||||
, (this.opts.local) ? ((this.opts.offset) ?
|
||||
DateFormatter.formatCurrentOffset(date) : '') : 'Z'
|
||||
].join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get an array of zero-padded date parts,
|
||||
* in UTC
|
||||
*
|
||||
* @param {Date} date - Date Object
|
||||
* @return {String[]}
|
||||
*/
|
||||
DateFormatter.getUTCDateParts = function (date) {
|
||||
return [
|
||||
date.getUTCFullYear()
|
||||
, DateFormatter.zeroPad(date.getUTCMonth()+1,2)
|
||||
, DateFormatter.zeroPad(date.getUTCDate(),2)
|
||||
, DateFormatter.zeroPad(date.getUTCHours(), 2)
|
||||
, DateFormatter.zeroPad(date.getUTCMinutes(), 2)
|
||||
, DateFormatter.zeroPad(date.getUTCSeconds(), 2)
|
||||
, DateFormatter.zeroPad(date.getUTCMilliseconds(), 3)]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to get an array of zero-padded date parts,
|
||||
* in the local time zone
|
||||
*
|
||||
* @param {Date} date - Date Object
|
||||
* @return {String[]}
|
||||
*/
|
||||
DateFormatter.getLocalDateParts = function (date) {
|
||||
return [
|
||||
date.getFullYear()
|
||||
, DateFormatter.zeroPad(date.getMonth()+1,2)
|
||||
, DateFormatter.zeroPad(date.getDate(),2)
|
||||
, DateFormatter.zeroPad(date.getHours(), 2)
|
||||
, DateFormatter.zeroPad(date.getMinutes(), 2)
|
||||
, DateFormatter.zeroPad(date.getSeconds(), 2)
|
||||
, DateFormatter.zeroPad(date.getMilliseconds(), 3)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to pad the digits with 0s to meet date formatting
|
||||
* requirements.
|
||||
*
|
||||
* @param {Number} digit - The number to pad.
|
||||
* @param {Number} length - Length of digit string, prefix with 0s if not
|
||||
* already length.
|
||||
* @return {String} - String with the padded digit
|
||||
*/
|
||||
DateFormatter.zeroPad = function (digit, length) {
|
||||
var padded = '' + digit
|
||||
while (padded.length < length) {
|
||||
padded = '0' + padded
|
||||
}
|
||||
|
||||
return padded
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the current timezone to default decoding to
|
||||
* rather than UTC. (for backward compatibility)
|
||||
*
|
||||
* @return {String} - in the format /Z|[+-]\d{2}:\d{2}/
|
||||
*/
|
||||
DateFormatter.formatCurrentOffset = function (d) {
|
||||
var offset = (d || new Date()).getTimezoneOffset()
|
||||
return (offset === 0) ? 'Z' : [
|
||||
(offset < 0) ? '+' : '-'
|
||||
, DateFormatter.zeroPad(Math.abs(Math.floor(offset/60)),2)
|
||||
, ':'
|
||||
, DateFormatter.zeroPad(Math.abs(offset%60),2)
|
||||
].join('')
|
||||
}
|
||||
|
||||
// export an instance of DateFormatter only.
|
||||
module.exports = new DateFormatter()
|
||||
323
models/deserializer.js
Normal file
323
models/deserializer.js
Normal file
@@ -0,0 +1,323 @@
|
||||
var sax = require('sax')
|
||||
, dateFormatter = require('./date_formatter')
|
||||
|
||||
var Deserializer = function(encoding) {
|
||||
this.type = null
|
||||
this.responseType = null
|
||||
this.stack = []
|
||||
this.marks = []
|
||||
this.data = []
|
||||
this.methodname = null
|
||||
this.encoding = encoding || 'utf8'
|
||||
this.value = false
|
||||
this.callback = null
|
||||
this.error = null
|
||||
|
||||
this.parser = sax.createStream()
|
||||
this.parser.on('opentag', this.onOpentag.bind(this))
|
||||
this.parser.on('closetag', this.onClosetag.bind(this))
|
||||
this.parser.on('text', this.onText.bind(this))
|
||||
this.parser.on('cdata', this.onCDATA.bind(this))
|
||||
this.parser.on('end', this.onDone.bind(this))
|
||||
this.parser.on('error', this.onError.bind(this))
|
||||
}
|
||||
|
||||
Deserializer.prototype.deserializeMethodResponse = function(stream, callback) {
|
||||
var that = this
|
||||
|
||||
this.callback = function(error, result) {
|
||||
if (error) {
|
||||
callback(error)
|
||||
}
|
||||
else if (result.length > 1) {
|
||||
callback(new Error('Response has more than one param'))
|
||||
}
|
||||
else if (that.type !== 'methodresponse') {
|
||||
callback(new Error('Not a method response'))
|
||||
}
|
||||
else if (!that.responseType) {
|
||||
callback(new Error('Invalid method response'))
|
||||
}
|
||||
else {
|
||||
callback(null, result[0])
|
||||
}
|
||||
}
|
||||
|
||||
stream.setEncoding(this.encoding)
|
||||
stream.on('error', this.onError.bind(this))
|
||||
stream.pipe(this.parser)
|
||||
}
|
||||
|
||||
Deserializer.prototype.deserializeMethodCall = function(stream, callback) {
|
||||
var that = this
|
||||
|
||||
this.callback = function(error, result) {
|
||||
if (error) {
|
||||
callback(error)
|
||||
}
|
||||
else if (that.type !== 'methodcall') {
|
||||
callback(new Error('Not a method call'))
|
||||
}
|
||||
else if (!that.methodname) {
|
||||
callback(new Error('Method call did not contain a method name'))
|
||||
}
|
||||
else {
|
||||
callback(null, that.methodname, result)
|
||||
}
|
||||
}
|
||||
|
||||
stream.setEncoding(this.encoding)
|
||||
stream.on('error', this.onError.bind(this))
|
||||
stream.pipe(this.parser)
|
||||
}
|
||||
|
||||
Deserializer.prototype.onDone = function() {
|
||||
var that = this
|
||||
|
||||
if (!this.error) {
|
||||
if (this.type === null || this.marks.length) {
|
||||
this.callback(new Error('Invalid XML-RPC message'))
|
||||
}
|
||||
else if (this.responseType === 'fault') {
|
||||
var createFault = function(fault) {
|
||||
var error = new Error('XML-RPC fault' + (fault.faultString ? ': ' + fault.faultString : ''))
|
||||
error.code = fault.faultCode
|
||||
error.faultCode = fault.faultCode
|
||||
error.faultString = fault.faultString
|
||||
return error
|
||||
}
|
||||
this.callback(createFault(this.stack[0]))
|
||||
}
|
||||
else {
|
||||
this.callback(undefined, this.stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Error handling needs a little thinking. There are two different kinds of
|
||||
// errors:
|
||||
// 1. Low level errors like network, stream or xml errors. These don't
|
||||
// require special treatment. They only need to be forwarded. The IO
|
||||
// is already stopped in these cases.
|
||||
// 2. Protocol errors: Invalid tags, invalid values &c. These happen in
|
||||
// our code and we should tear down the IO and stop parsing.
|
||||
// Currently all errors end here. Guess I'll split it up.
|
||||
Deserializer.prototype.onError = function(msg) {
|
||||
if (!this.error) {
|
||||
if (typeof msg === 'string') {
|
||||
this.error = new Error(msg)
|
||||
}
|
||||
else {
|
||||
this.error = msg
|
||||
}
|
||||
this.callback(this.error)
|
||||
}
|
||||
}
|
||||
|
||||
Deserializer.prototype.push = function(value) {
|
||||
this.stack.push(value)
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// SAX Handlers
|
||||
//==============================================================================
|
||||
|
||||
Deserializer.prototype.onOpentag = function(node) {
|
||||
if (node.name === 'ARRAY' || node.name === 'STRUCT') {
|
||||
this.marks.push(this.stack.length)
|
||||
}
|
||||
this.data = []
|
||||
this.value = (node.name === 'VALUE')
|
||||
}
|
||||
|
||||
Deserializer.prototype.onText = function(text) {
|
||||
this.data.push(text)
|
||||
}
|
||||
|
||||
Deserializer.prototype.onCDATA = function(cdata) {
|
||||
this.data.push(cdata)
|
||||
}
|
||||
|
||||
Deserializer.prototype.onClosetag = function(el) {
|
||||
var data = this.data.join('')
|
||||
try {
|
||||
switch(el) {
|
||||
case 'BOOLEAN':
|
||||
this.endBoolean(data)
|
||||
break
|
||||
case 'INT':
|
||||
case 'I4':
|
||||
this.endInt(data)
|
||||
break
|
||||
case 'I8':
|
||||
this.endI8(data)
|
||||
break
|
||||
case 'DOUBLE':
|
||||
this.endDouble(data)
|
||||
break
|
||||
case 'STRING':
|
||||
case 'NAME':
|
||||
this.endString(data)
|
||||
break
|
||||
case 'ARRAY':
|
||||
this.endArray(data)
|
||||
break
|
||||
case 'STRUCT':
|
||||
this.endStruct(data)
|
||||
break
|
||||
case 'BASE64':
|
||||
this.endBase64(data)
|
||||
break
|
||||
case 'DATETIME.ISO8601':
|
||||
this.endDateTime(data)
|
||||
break
|
||||
case 'VALUE':
|
||||
this.endValue(data)
|
||||
break
|
||||
case 'PARAMS':
|
||||
this.endParams(data)
|
||||
break
|
||||
case 'FAULT':
|
||||
this.endFault(data)
|
||||
break
|
||||
case 'METHODRESPONSE':
|
||||
this.endMethodResponse(data)
|
||||
break
|
||||
case 'METHODNAME':
|
||||
this.endMethodName(data)
|
||||
break
|
||||
case 'METHODCALL':
|
||||
this.endMethodCall(data)
|
||||
break
|
||||
case 'NIL':
|
||||
this.endNil(data)
|
||||
break
|
||||
case 'DATA':
|
||||
case 'PARAM':
|
||||
case 'MEMBER':
|
||||
// Ignored by design
|
||||
break
|
||||
default:
|
||||
this.onError('Unknown XML-RPC tag \'' + el + '\'')
|
||||
break
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
Deserializer.prototype.endNil = function(data) {
|
||||
this.push(null)
|
||||
this.value = false
|
||||
}
|
||||
|
||||
Deserializer.prototype.endBoolean = function(data) {
|
||||
if (data === '1') {
|
||||
this.push(true)
|
||||
}
|
||||
else if (data === '0') {
|
||||
this.push(false)
|
||||
}
|
||||
else {
|
||||
throw new Error('Illegal boolean value \'' + data + '\'')
|
||||
}
|
||||
this.value = false
|
||||
}
|
||||
|
||||
Deserializer.prototype.endInt = function(data) {
|
||||
var value = parseInt(data, 10)
|
||||
if (isNaN(value)) {
|
||||
throw new Error('Expected an integer but got \'' + data + '\'')
|
||||
}
|
||||
else {
|
||||
this.push(value)
|
||||
this.value = false
|
||||
}
|
||||
}
|
||||
|
||||
Deserializer.prototype.endDouble = function(data) {
|
||||
var value = parseFloat(data)
|
||||
if (isNaN(value)) {
|
||||
throw new Error('Expected a double but got \'' + data + '\'')
|
||||
}
|
||||
else {
|
||||
this.push(value)
|
||||
this.value = false
|
||||
}
|
||||
}
|
||||
|
||||
Deserializer.prototype.endString = function(data) {
|
||||
this.push(data)
|
||||
this.value = false
|
||||
}
|
||||
|
||||
Deserializer.prototype.endArray = function(data) {
|
||||
var mark = this.marks.pop()
|
||||
this.stack.splice(mark, this.stack.length - mark, this.stack.slice(mark))
|
||||
this.value = false
|
||||
}
|
||||
|
||||
Deserializer.prototype.endStruct = function(data) {
|
||||
var mark = this.marks.pop()
|
||||
, struct = {}
|
||||
, items = this.stack.slice(mark)
|
||||
, i = 0
|
||||
|
||||
for (; i < items.length; i += 2) {
|
||||
struct[items[i]] = items[i + 1]
|
||||
}
|
||||
this.stack.splice(mark, this.stack.length - mark, struct)
|
||||
this.value = false
|
||||
}
|
||||
|
||||
Deserializer.prototype.endBase64 = function(data) {
|
||||
var buffer = new Buffer(data, 'base64')
|
||||
this.push(buffer)
|
||||
this.value = false
|
||||
}
|
||||
|
||||
Deserializer.prototype.endDateTime = function(data) {
|
||||
var date = dateFormatter.decodeIso8601(data)
|
||||
this.push(date)
|
||||
this.value = false
|
||||
}
|
||||
|
||||
var isInteger = /^-?\d+$/
|
||||
Deserializer.prototype.endI8 = function(data) {
|
||||
if (!isInteger.test(data)) {
|
||||
throw new Error('Expected integer (I8) value but got \'' + data + '\'')
|
||||
}
|
||||
else {
|
||||
this.endString(data)
|
||||
}
|
||||
}
|
||||
|
||||
Deserializer.prototype.endValue = function(data) {
|
||||
if (this.value) {
|
||||
this.endString(data)
|
||||
}
|
||||
}
|
||||
|
||||
Deserializer.prototype.endParams = function(data) {
|
||||
this.responseType = 'params'
|
||||
}
|
||||
|
||||
Deserializer.prototype.endFault = function(data) {
|
||||
this.responseType = 'fault'
|
||||
}
|
||||
|
||||
Deserializer.prototype.endMethodResponse = function(data) {
|
||||
this.type = 'methodresponse'
|
||||
}
|
||||
|
||||
Deserializer.prototype.endMethodName = function(data) {
|
||||
this.methodname = data
|
||||
}
|
||||
|
||||
Deserializer.prototype.endMethodCall = function(data) {
|
||||
this.type = 'methodcall'
|
||||
}
|
||||
|
||||
module.exports = Deserializer
|
||||
720
models/rtorrent.js
Normal file
720
models/rtorrent.js
Normal file
@@ -0,0 +1,720 @@
|
||||
var xmlrpc = require('xmlrpc')
|
||||
// var fs = require('fs');
|
||||
// var path = require('path');
|
||||
// var request = require('request');
|
||||
// var portscanner = require('portscanner');
|
||||
// var readTorrent = require('read-torrent');
|
||||
// var logger = require('winston');
|
||||
var Q = require('q');
|
||||
// var nconf = require('nconf');
|
||||
var net = require('net');
|
||||
// var rimraf = require('rimraf');
|
||||
var Deserializer = require('./deserializer');
|
||||
var Serializer = require('./serializer');
|
||||
|
||||
// function htmlspecialchars(str) {
|
||||
// return str.replace(/\&/ig,'&').replace(/\'/ig,'"').replace(/\'/ig,''').replace(/\</ig,'<').replace(/\>/ig,'>');
|
||||
// }
|
||||
|
||||
// if (!(nconf.get('rtorrent:option') === 'scgi' || nconf.get('rtorrent:option') === 'xmlrpc')) {
|
||||
// var err = new Error('Config for rtorrent option is not valid. Please check config.json rtorrent.option property.');
|
||||
// logger.error(err.message);
|
||||
// throw err;
|
||||
// }
|
||||
|
||||
// logger.info('Connect to rtorrent via', nconf.get('rtorrent:option'));
|
||||
|
||||
// need something to test connection to rtorrent first...
|
||||
|
||||
var rtorrent = {};
|
||||
|
||||
rtorrent.get = function(api, array) {
|
||||
var stream = net.connect(5000, 'localhost');
|
||||
var deferred = Q.defer();
|
||||
var xml;
|
||||
var length = 0;
|
||||
|
||||
stream.setEncoding('UTF8');
|
||||
|
||||
try {
|
||||
xml = Serializer.serializeMethodCall(api, array);
|
||||
} catch (error) {
|
||||
console.trace(error);
|
||||
}
|
||||
|
||||
var head = [
|
||||
'CONTENT_LENGTH' + String.fromCharCode(0) + xml.length + String.fromCharCode(0),
|
||||
'SCGI' + String.fromCharCode(0) + '1' + String.fromCharCode(0)
|
||||
];
|
||||
|
||||
head.forEach(function (item) {
|
||||
length += item.length;
|
||||
});
|
||||
|
||||
stream.write(length + ':');
|
||||
|
||||
head.forEach(function (item) {
|
||||
stream.write(item);
|
||||
});
|
||||
|
||||
stream.write(',');
|
||||
stream.write(xml);
|
||||
|
||||
var deserializer = new Deserializer('utf8');
|
||||
deserializer.deserializeMethodResponse(stream, function (err, data) {
|
||||
|
||||
if (err) {
|
||||
return deferred.reject(err);
|
||||
}
|
||||
return deferred.resolve(data);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
|
||||
}
|
||||
|
||||
// function methodCall (api, array) {
|
||||
//
|
||||
// if (nconf.get('rtorrent:option') === 'xmlrpc') {
|
||||
// return xmlrpcMethodCall(api, array);
|
||||
// }
|
||||
//
|
||||
// if (nconf.get('rtorrent:option') === 'scgi') {
|
||||
// return scgiMethodCall(api, array);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// rtorrent.init = function () {
|
||||
// return createThrottleSettings()
|
||||
// .then(function () {
|
||||
// logger.info('Finished creating throttle settings.');
|
||||
// }, function (err) {
|
||||
// if (err.code == 'ECONNREFUSED') {
|
||||
// throw new Error('Unable to connect to rtorrent.');
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// function createThrottleSettings () {
|
||||
// logger.info('Creating throttle settings.');
|
||||
// var upload_throttles = [];
|
||||
// var download_throttles = [];
|
||||
// var throttle_settings = [];
|
||||
// var createThrottleSettingList = [];
|
||||
//
|
||||
// var throttleSpeed = 16;
|
||||
// for (var i = 5 - 1; i >= 0; i--) {
|
||||
// upload_throttles.push({
|
||||
// display: 'Up_' + throttleSpeed,
|
||||
// name: 'up_' + i,
|
||||
// up: throttleSpeed,
|
||||
// down: 0,
|
||||
// direction: 'up'
|
||||
// });
|
||||
// throttleSpeed = throttleSpeed * 2
|
||||
// }
|
||||
//
|
||||
// throttleSpeed = 16;
|
||||
// for (var i = 5 - 1; i >= 0; i--) {
|
||||
// download_throttles.push({
|
||||
// display: 'Down_' + throttleSpeed,
|
||||
// name: 'down_' + i,
|
||||
// up: 0,
|
||||
// down: throttleSpeed,
|
||||
// direction: 'down'
|
||||
// });
|
||||
// throttleSpeed = throttleSpeed * 2
|
||||
// }
|
||||
//
|
||||
// throttle_settings = upload_throttles.concat(download_throttles);
|
||||
//
|
||||
// for (var i = throttle_settings.length - 1; i >= 0; i--) {
|
||||
// createThrottleSettingList.push(createThrottleSetting(throttle_settings[i]));
|
||||
// }
|
||||
//
|
||||
// return Q.all(createThrottleSettingList);
|
||||
// }
|
||||
//
|
||||
// function createThrottleSetting (throttleSetting) {
|
||||
// switch(throttleSetting.direction) {
|
||||
// case 'up':
|
||||
// return rtorrent.throttleUp('' + throttleSetting.name, '' + throttleSetting.up);
|
||||
// break;
|
||||
// case 'down':
|
||||
// return rtorrent.throttleDown('' + throttleSetting.name, '' + throttleSetting.up);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// rtorrent.throttleUp = function (name, value) {
|
||||
// return methodCall('throttle_up', [name, value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.throttleDown = function (name, value) {
|
||||
// return methodCall('throttle_down', [name, value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.setThrottle = function (hash, throttle_name) {
|
||||
// return rtorrent.pauseTorrent(hash)
|
||||
// .then(function () {
|
||||
// return rtorrent.setThrottleName(hash, throttle_name);
|
||||
// })
|
||||
// .then(function () {
|
||||
// return rtorrent.startTorrent(hash);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setThrottleName = function (hash, throttle_name) {
|
||||
// return methodCall('d.set_throttle_name', [hash, throttle_name]);
|
||||
// }
|
||||
//
|
||||
// // get_complete, is_open, is_hash_checking, get_state
|
||||
// // need to figure out better way of getting the status
|
||||
// function getStatus (value) {
|
||||
// if (value[0] === '1' && value[1] === '1' && value[2] === '0' && value[3] === '1') {
|
||||
// return 'seeding';
|
||||
// } else if (value[0] === '1' && value[1] === '0' && value[2] === '0' && value[3] === '0') {
|
||||
// return 'finished';
|
||||
// } else if (value[0] === '0' && value[1] === '1' && value[2] === '0' && value[3] === '1') {
|
||||
// return 'downloading';
|
||||
// } else if (value[0] === '0' && value[1] === '0' && value[2] === '0' && value[3] === '1') {
|
||||
// // stopped in the middle
|
||||
// return 'stopped';
|
||||
// } else if (value[0] === '0' && value[1] === '0' && value[2] === '0' && value[3] === '0') {
|
||||
// // i dont know stopped
|
||||
// return 'stopped';
|
||||
// } else if (value[0] === '0' && value[1] === '1' && value[2] === '0' && value[3] === '0') {
|
||||
// return 'paused';
|
||||
// } else if (value[0] === '1' && value[1] === '1' && value[2] === '0' && value[3] === '0') {
|
||||
// // seeding pause
|
||||
// return 'paused';
|
||||
// } else if (value[0] === '1' && value[1] === '0' && value[2] === '0' && value[3] === '1') {
|
||||
// return 'finished';
|
||||
// } else if (value[2] === '1') {
|
||||
// return 'checking';
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// function adaptTorrentArray (torrent) {
|
||||
// return {
|
||||
// name: torrent[0],
|
||||
// hash: torrent[1],
|
||||
// id: torrent[1],
|
||||
// size: parseInt(torrent[2], 10),
|
||||
// downloaded: parseInt(torrent[3], 10),
|
||||
// uploaded: parseInt(torrent[12], 10),
|
||||
// dl_speed: parseInt(torrent[4], 10),
|
||||
// ul_speed: parseInt(torrent[5], 10),
|
||||
// percent_downloaded: (torrent[3] / torrent[2]).toFixed(4),
|
||||
// time_remaining: (torrent[2] - torrent[3]) / torrent[4] | 0,
|
||||
// status: getStatus(torrent.slice(6, 10)),
|
||||
// seeds: parseInt(torrent[10], 10),
|
||||
// peers: parseInt(torrent[11], 10),
|
||||
// total_peers: 0,
|
||||
// total_seeds: 0
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// rtorrent.getTorrents = function () {
|
||||
// return methodCall('d.multicall', ['main', 'd.name=', 'd.hash=', 'd.size_bytes=', 'd.bytes_done=', 'd.get_down_rate=', 'd.get_up_rate=', 'd.get_complete=', 'd.is_open=', 'd.is_hash_checking=', 'd.get_state=', 'd.get_peers_complete=', 'd.get_peers_accounted=', 'd.get_up_total='])
|
||||
// .then(function (data) {
|
||||
//
|
||||
// // If array is empty, return empty array
|
||||
// if (data.length === 0) {
|
||||
// return [];
|
||||
// }
|
||||
//
|
||||
// // Adapt array from rtorrent properly for consumption by client
|
||||
// var torrents = data.map(function (torrent) {
|
||||
// return adaptTorrentArray(torrent);
|
||||
// });
|
||||
//
|
||||
// // Declare multical array specifically for getting torrent data
|
||||
// var systemMultiCallArray = [];
|
||||
//
|
||||
// // Loop through torrents from main call and push method call to get peers and seeds
|
||||
// // Note: The order in which it is pushed matters. The returned array from rtorrent will be
|
||||
// // an array of array of array of array of values
|
||||
// torrents.forEach(function (torrent) {
|
||||
//
|
||||
// // Push peers first for the torrent
|
||||
// systemMultiCallArray.push({
|
||||
// methodName: 't.multicall',
|
||||
// params: [torrent.hash, 'd.get_hash=', 't.get_scrape_incomplete=']
|
||||
// });
|
||||
//
|
||||
// // Push seeds second for the torrent
|
||||
// systemMultiCallArray.push({
|
||||
// methodName: 't.multicall',
|
||||
// params: [torrent.hash, 'd.get_hash=', 't.get_scrape_complete=']
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// // Do the system.multicall and return promise
|
||||
// // Inside the resolve function, we loop through the array
|
||||
// return methodCall('system.multicall', [systemMultiCallArray]).then(function(data) {
|
||||
// var numberArray = [];
|
||||
//
|
||||
// // The length of data should be equal to the length of systemMultiCallArray
|
||||
// data.forEach(function(item) {
|
||||
// // Each item in the array has an array of arrays
|
||||
// item.forEach(function(itemagain) {
|
||||
// // Map and reduce the array to get the number
|
||||
// var number = itemagain.map(function (value) {
|
||||
// return parseInt(value, 10);
|
||||
// })
|
||||
// .reduce(function (a, b) {
|
||||
// return a + b;
|
||||
// }, 0);
|
||||
// // Push the number to a clean array so that we can place it correctly back into
|
||||
// // the torrent object to return to client
|
||||
// numberArray.push(number);
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// // Map torrents and shift from numberArray to get the correct order
|
||||
// // Peers is first, followed by seeds.
|
||||
// // Return each torrent and finally return torrents back to caller.
|
||||
// return torrents.map(function (torrent) {
|
||||
// torrent.total_peers = numberArray.shift();
|
||||
// torrent.total_seeds = numberArray.shift();
|
||||
// return torrent;
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.loadTorrentFile = function (filepath) {
|
||||
// return methodCall('load', [filepath, 'd.set_custom=x-filename']);
|
||||
// }
|
||||
//
|
||||
// rtorrent.loadTorrentStart = function (url) {
|
||||
// return methodCall('load_start', [url]);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// rtorrent.getTorrentMetaData = function (torrent) {
|
||||
// var deferred = Q.defer();
|
||||
//
|
||||
// readTorrent(torrent.url, {}, function (err, data) {
|
||||
// if (err) {
|
||||
// deferred.reject(err);
|
||||
// }
|
||||
//
|
||||
// deferred.resolve(data);
|
||||
// });
|
||||
//
|
||||
// return deferred.promise;
|
||||
// }
|
||||
//
|
||||
// rtorrent.getHash = function (hash) {
|
||||
// return methodCall('d.get_hash', [hash]);
|
||||
// }
|
||||
//
|
||||
// function checkPathExists (path) {
|
||||
// var deferred = Q.defer();
|
||||
// fs.exists(path, function (exists) {
|
||||
// if (exists) {
|
||||
// deferred.resolve(exists);
|
||||
// }
|
||||
//
|
||||
// deferred.reject(new Error('Path does not exist.'));
|
||||
// })
|
||||
// return deferred.promise;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// rtorrent.loadTorrent = function (torrent) {
|
||||
// return rtorrent.getTorrentMetaData(torrent)
|
||||
// .then(function (data) {
|
||||
// var hash = data.infoHash.toUpperCase();
|
||||
// logger.info('Retrieved hash from torrent', hash);
|
||||
//
|
||||
// // Check if torrent path is passed as a parameter
|
||||
// if (torrent.path) {
|
||||
// // Check if path exists
|
||||
// logger.info('Checking if path exists.');
|
||||
//
|
||||
// return checkPathExists(torrent.path)
|
||||
// .then(function (data) {
|
||||
//
|
||||
// logger.info('Directory exists.');
|
||||
//
|
||||
// // Load torrent but do not start
|
||||
// return methodCall('load', [torrent.url])
|
||||
// .then(function () {
|
||||
// return Q.delay(500)
|
||||
// .then(function () {
|
||||
// // Get torrent hash
|
||||
// return rtorrent.getHash(hash)
|
||||
// .then(function () {
|
||||
// return rtorrent.setTorrentDirectory(hash, torrent.path)
|
||||
// .then(function () {
|
||||
// return rtorrent.startTorrent(hash);
|
||||
// });
|
||||
// });
|
||||
// })
|
||||
// });
|
||||
// }, function () {
|
||||
//
|
||||
// logger.info('Directory does not exist.');
|
||||
//
|
||||
// var joinedPath = path.join('/', torrent.path);
|
||||
//
|
||||
// return Q.nfcall(fs.mkdir, joinedPath)
|
||||
// .then(function () {
|
||||
// logger.info('Created directory', joinedPath);
|
||||
//
|
||||
// logger.info('Setting directory of torrent to', joinedPath);
|
||||
//
|
||||
// // Load torrent but do not start
|
||||
// return methodCall('load', [torrent.url])
|
||||
// .then(function () {
|
||||
// return Q.delay(500)
|
||||
// .then(function () {
|
||||
// // Get torrent hash
|
||||
// return rtorrent.getHash(hash)
|
||||
// .then(function () {
|
||||
// return rtorrent.setTorrentDirectory(hash, joinedPath)
|
||||
// .then(function () {
|
||||
// return rtorrent.startTorrent(hash);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// }, function (err) {
|
||||
//
|
||||
// if (err.code == 'EACCES') {
|
||||
// throw new Error('Unable to create directory for torrent due to permissions.', hash);
|
||||
// }
|
||||
//
|
||||
// // THrow error if not EACESS
|
||||
// throw err;
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// // Start torrent if no path is passed
|
||||
// return rtorrent.loadTorrentStart(torrent.url);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setTorrentDirectory = function (hash, path) {
|
||||
// return methodCall('d.set_directory', [hash, path]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.startTorrent = function (hash) {
|
||||
// return methodCall('d.start', [hash])
|
||||
// .then(function () {
|
||||
// return methodCall('d.resume', [hash]);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.stopTorrent = function (hash) {
|
||||
// return methodCall('d.close', [hash]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.pauseTorrent = function (hash) {
|
||||
// return methodCall('d.stop', [hash]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.removeTorrent = function (hash) {
|
||||
// return methodCall('d.erase', [hash]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.deleteTorrentData = function (hash) {
|
||||
// return rtorrent.stopTorrent(hash).then(function() {
|
||||
// return rtorrent.isMultiFile(hash).then(function(data) {
|
||||
// return rtorrent.getTorrentDirectory(hash).then(function(dir) {
|
||||
// if (data === '1') {
|
||||
// logger.info(hash, 'is a multifile torrent.');
|
||||
// logger.info('Deleting directory path/file', dir);
|
||||
// return deleteData(dir).then(function(data) {
|
||||
// return rtorrent.removeTorrent(hash);
|
||||
// });
|
||||
// } else {
|
||||
// logger.info(hash, 'is a single file torrent.');
|
||||
// return rtorrent.getTorrentName(hash).then(function(name) {
|
||||
// logger.info('Deleting directory path/file', dir + '/' + name)
|
||||
// return deleteData(dir + '/' + name).then(function(data) {
|
||||
// return rtorrent.removeTorrent(hash);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// // function deleteData (path) {
|
||||
// // var deferred = Q.defer();
|
||||
// //
|
||||
// // rimraf(path, function(err, results) {
|
||||
// // if (err) {
|
||||
// // deferred.reject(err)
|
||||
// // }
|
||||
// //
|
||||
// // deferred.resolve(results);
|
||||
// // });
|
||||
// //
|
||||
// // return deferred.promise;
|
||||
// // }
|
||||
//
|
||||
// rtorrent.getTorrentName = function (hash) {
|
||||
// return methodCall('d.get_name', [hash]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getBasePath = function (hash) {
|
||||
// return methodCall('d.get_base_path', [hash]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.isMultiFile = function (hash) {
|
||||
// return methodCall('d.is_multi_file', [hash]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getTorrentDirectory = function (hash) {
|
||||
// return methodCall('d.get_directory', [hash]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getNetworkListenPort = function () {
|
||||
// return methodCall('network.listen.port', []);
|
||||
// }
|
||||
//
|
||||
// rtorrent.setPriority = function (priority) {
|
||||
// return methodCall('d.set_priority', [hash, priority]);
|
||||
// }
|
||||
//
|
||||
// // change to use nconf
|
||||
// rtorrent.getPortStatus = function (port) {
|
||||
// var deferred = Q.defer();
|
||||
//
|
||||
// portscanner.checkPortStatus(port, 'localhost', function (err, data) {
|
||||
// if (err) {
|
||||
// return deferred.reject(err);
|
||||
// }
|
||||
//
|
||||
// return deferred.resolve(data);
|
||||
// });
|
||||
//
|
||||
// return deferred.promise;
|
||||
// }
|
||||
//
|
||||
// rtorrent.getTotalPeers = function (hash) {
|
||||
// return rtorrent.getScrapeIncomplete(hash)
|
||||
// .then(function (data) {
|
||||
// return data.map(function (value) {
|
||||
// return parseInt(value, 10);
|
||||
// })
|
||||
// .reduce(function (a, b) {
|
||||
// return a + b;
|
||||
// }, 0);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.getTotalSeeds = function (hash) {
|
||||
// return rtorrent.getScrapeComplete(hash)
|
||||
// .then(function (data) {
|
||||
// return data.map(function (value) {
|
||||
// return parseInt(value, 10);
|
||||
// }).reduce(function (a, b) {
|
||||
// return a + b;
|
||||
// }, 0);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.getScrapeIncomplete = function (hash) {
|
||||
// return methodCall('t.multicall', [hash, 'd.get_hash=', 't.get_scrape_incomplete=']);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getScrapeComplete = function (hash) {
|
||||
// return methodCall('t.multicall', [hash, 'd.get_hash=', 't.get_scrape_complete=']);
|
||||
// }
|
||||
//
|
||||
// // get_port_range
|
||||
// // returns string of port range
|
||||
// rtorrent.getPortRange = function () {
|
||||
// return methodCall('get_port_range', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'port_range': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setPortRange = function (value) {
|
||||
// return methodCall('set_port_range', [value]);
|
||||
// }
|
||||
//
|
||||
// // get_port_open
|
||||
// // returns 1 or 0
|
||||
// // Opens listening port
|
||||
// rtorrent.getPortOpen = function () {
|
||||
// return methodCall('get_port_open', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'port_open': data == 1 ? true : false
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setPortOpen = function (value) {
|
||||
// return methodCall('set_port_open', [value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getUploadSlots = function () {
|
||||
// return methodCall('get_max_uploads', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'max_uploads': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setUploadSlots = function (value) {
|
||||
// return methodCall('set_max_uploads', [value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getUploadSlotsGlobal = function () {
|
||||
// return methodCall('get_max_uploads_global', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'max_uploads_global': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setUploadSlotsGlobal = function (value) {
|
||||
// return methodCall('set_max_uploads_global', [value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getDownloadSlotsGlobal = function () {
|
||||
// return methodCall('get_max_downloads_global', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'max_downloads_global': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setDownloadSlotsGlobal = function (value) {
|
||||
// return methodCall('set_max_downloads_global', [value]);
|
||||
// }
|
||||
//
|
||||
// // get_port_random
|
||||
// // returns 1 or 0
|
||||
// // Randomize port each time rTorrent starts
|
||||
// rtorrent.getPortRandom = function () {
|
||||
// return methodCall('get_port_random', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'port_random': data == 1 ? true : false
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setPortRandom = function (value) {
|
||||
// return methodCall('set_port_random', [value]);
|
||||
// }
|
||||
//
|
||||
// // get_download_rate
|
||||
// // returns value in bytes
|
||||
// rtorrent.getGlobalMaximumDownloadRate = function () {
|
||||
// return methodCall('get_download_rate', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'global_max_download_rate': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// // set_download_rate
|
||||
// // requires value in bytes
|
||||
// rtorrent.setGlobalMaximumDownloadRate = function (value) {
|
||||
// return methodCall('set_download_rate', [value]);
|
||||
// }
|
||||
//
|
||||
// // get_upload_rate
|
||||
// // returns value in bytes
|
||||
// rtorrent.getGlobalMaximumUploadRate = function () {
|
||||
// return methodCall('get_upload_rate', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'global_max_upload_rate': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.getMinNumberPeers = function () {
|
||||
// return methodCall('get_min_peers', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'min_peers': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setMinNumberPeers = function (value) {
|
||||
// return methodCall('set_min_peers', [value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getMinNumberSeeds = function () {
|
||||
// return methodCall('get_min_peers_seed', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'min_seeds': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setMinNumberSeeds = function (value) {
|
||||
// return methodCall('set_min_peers_seed', [value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getMaxNumberPeers = function () {
|
||||
// return methodCall('get_max_peers', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'max_peers': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setMaxNumberPeers = function (value) {
|
||||
// return methodCall('set_max_peers', [value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getMaxNumberSeeds = function () {
|
||||
// return methodCall('get_max_peers_seed', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'max_seeds': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setMaxNumberSeeds = function (value) {
|
||||
// return methodCall('set_max_peers_seed', [value]);
|
||||
// }
|
||||
//
|
||||
// // set_upload_rate
|
||||
// // requires value in bytes
|
||||
// rtorrent.setGlobalMaximumUploadRate = function (value) {
|
||||
// return methodCall('set_upload_rate', [value]);
|
||||
// }
|
||||
//
|
||||
// rtorrent.getDirectory = function () {
|
||||
// return methodCall('get_directory', [])
|
||||
// .then(function (data) {
|
||||
// return {
|
||||
// 'download_directory': data
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// rtorrent.setDirectory = function (value) {
|
||||
// return methodCall('set_directory', [value]);
|
||||
// }
|
||||
|
||||
module.exports = rtorrent;
|
||||
196
models/serializer.js
Normal file
196
models/serializer.js
Normal file
@@ -0,0 +1,196 @@
|
||||
var xmlBuilder = require('xmlbuilder')
|
||||
, dateFormatter = require('./date_formatter')
|
||||
|
||||
/**
|
||||
* Creates the XML for an XML-RPC method call.
|
||||
*
|
||||
* @param {String} method - The method name.
|
||||
* @param {Array} params - Params to pass in the call.
|
||||
* @param {Function} callback - function (error, xml) { ... }
|
||||
* - {Object|null} error - Any errors that occurred while building the XML,
|
||||
* otherwise null.
|
||||
* - {String} xml - The method call XML.
|
||||
*/
|
||||
exports.serializeMethodCall = function(method, params) {
|
||||
var params = params || [];
|
||||
|
||||
var xml = xmlBuilder
|
||||
.create('methodCall')
|
||||
.ele('methodName')
|
||||
.txt(method)
|
||||
.up()
|
||||
.ele('params');
|
||||
|
||||
params.forEach(function(param) {
|
||||
serializeValue(param, xml.ele('param'))
|
||||
});
|
||||
|
||||
// Includes the <?xml ...> declaration
|
||||
|
||||
var xmlString = xml.doc().toString()
|
||||
return xmlString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the XML for an XML-RPC method response.
|
||||
*
|
||||
* @param {mixed} value - The value to pass in the response.
|
||||
* @param {Function} callback - function (error, xml) { ... }
|
||||
* - {Object|null} error - Any errors that occurred while building the XML,
|
||||
* otherwise null.
|
||||
* - {String} xml - The method response XML.
|
||||
*/
|
||||
exports.serializeMethodResponse = function(result) {
|
||||
var xml = xmlBuilder.create()
|
||||
. begin('methodResponse', { version: '1.0' })
|
||||
. ele('params')
|
||||
. ele('param')
|
||||
|
||||
serializeValue(result, xml)
|
||||
|
||||
// Includes the <?xml ...> declaration
|
||||
return xml.doc().toString()
|
||||
}
|
||||
|
||||
exports.serializeFault = function(fault) {
|
||||
var xml = xmlBuilder.create()
|
||||
. begin('methodResponse', { version: '1.0' })
|
||||
. ele('fault')
|
||||
|
||||
serializeValue(fault, xml)
|
||||
|
||||
// Includes the <?xml ...> declaration
|
||||
return xml.doc().toString()
|
||||
}
|
||||
|
||||
function serializeValue(value, xml) {
|
||||
var stack = [ { value: value, xml: xml } ]
|
||||
, current = null
|
||||
, valueNode = null
|
||||
, next = null
|
||||
|
||||
while (stack.length > 0) {
|
||||
current = stack[stack.length - 1]
|
||||
|
||||
if (current.index !== undefined) {
|
||||
// Iterating a compound
|
||||
next = getNextItemsFrame(current)
|
||||
if (next) {
|
||||
stack.push(next)
|
||||
}
|
||||
else {
|
||||
stack.pop()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// we're about to add a new value (compound or simple)
|
||||
valueNode = current.xml.ele('value')
|
||||
switch(typeof current.value) {
|
||||
case 'boolean':
|
||||
appendBoolean(current.value, valueNode)
|
||||
stack.pop()
|
||||
break
|
||||
case 'string':
|
||||
appendString(current.value, valueNode)
|
||||
stack.pop()
|
||||
break
|
||||
case 'number':
|
||||
appendNumber(current.value, valueNode)
|
||||
stack.pop()
|
||||
break
|
||||
case 'object':
|
||||
if (current.value === null) {
|
||||
valueNode.ele('nil')
|
||||
stack.pop()
|
||||
}
|
||||
else if (current.value instanceof Date) {
|
||||
appendDatetime(current.value, valueNode)
|
||||
stack.pop()
|
||||
}
|
||||
else if (Buffer.isBuffer(current.value)) {
|
||||
appendBuffer(current.value, valueNode)
|
||||
stack.pop()
|
||||
}
|
||||
else {
|
||||
if (Array.isArray(current.value)) {
|
||||
current.xml = valueNode.ele('array').ele('data')
|
||||
}
|
||||
else {
|
||||
current.xml = valueNode.ele('struct')
|
||||
current.keys = Object.keys(current.value)
|
||||
}
|
||||
current.index = 0
|
||||
next = getNextItemsFrame(current)
|
||||
if (next) {
|
||||
stack.push(next)
|
||||
}
|
||||
else {
|
||||
stack.pop()
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
stack.pop()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNextItemsFrame(frame) {
|
||||
var nextFrame = null
|
||||
|
||||
if (frame.keys) {
|
||||
if (frame.index < frame.keys.length) {
|
||||
var key = frame.keys[frame.index++]
|
||||
, member = frame.xml.ele('member').ele('name').text(key).up()
|
||||
nextFrame = {
|
||||
value: frame.value[key]
|
||||
, xml: member
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (frame.index < frame.value.length) {
|
||||
nextFrame = {
|
||||
value: frame.value[frame.index]
|
||||
, xml: frame.xml
|
||||
}
|
||||
frame.index++
|
||||
}
|
||||
|
||||
return nextFrame
|
||||
}
|
||||
|
||||
function appendBoolean(value, xml) {
|
||||
xml.ele('boolean').txt(value ? 1 : 0)
|
||||
}
|
||||
|
||||
var illegalChars = /^(?![^<&]*]]>[^<&]*)[^<&]*$/
|
||||
function appendString(value, xml) {
|
||||
if (value.length === 0) {
|
||||
xml.ele('string')
|
||||
}
|
||||
else if (!illegalChars.test(value)) {
|
||||
xml.ele('string').d(value)
|
||||
}
|
||||
else {
|
||||
xml.ele('string').txt(value)
|
||||
}
|
||||
}
|
||||
|
||||
function appendNumber(value, xml) {
|
||||
if (value % 1 == 0) {
|
||||
xml.ele('int').txt(value)
|
||||
}
|
||||
else {
|
||||
xml.ele('double').txt(value)
|
||||
}
|
||||
}
|
||||
|
||||
function appendDatetime(value, xml) {
|
||||
xml.ele('dateTime.iso8601').txt(dateFormatter.encodeIso8601(value))
|
||||
}
|
||||
|
||||
function appendBuffer(value, xml) {
|
||||
xml.ele('base64').txt(value.toString('base64'))
|
||||
}
|
||||
25
models/torrents.js
Normal file
25
models/torrents.js
Normal file
@@ -0,0 +1,25 @@
|
||||
var client = require('./client')();
|
||||
|
||||
function torrents() {
|
||||
|
||||
if((this instanceof torrents) === false) {
|
||||
return new torrents();
|
||||
}
|
||||
};
|
||||
|
||||
torrents.prototype.listTorrents = function(callback) {
|
||||
|
||||
client.getTorrentList(callback);
|
||||
};
|
||||
|
||||
torrents.prototype.stopTorrent = function(hash, callback) {
|
||||
|
||||
client.stopTorrent(hash, callback);
|
||||
};
|
||||
|
||||
torrents.prototype.startTorrent = function(hash, callback) {
|
||||
|
||||
client.startTorrent(hash, callback);
|
||||
};
|
||||
|
||||
module.exports = torrents;
|
||||
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "flood",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "~1.12.0",
|
||||
"cookie-parser": "~1.3.4",
|
||||
"debug": "~2.1.1",
|
||||
"es6-promise": "^2.0.1",
|
||||
"express": "~4.12.2",
|
||||
"flux": "^2.0.1",
|
||||
"jade": "~1.9.2",
|
||||
"keymirror": "^0.1.1",
|
||||
"morgan": "~1.5.1",
|
||||
"object-assign": "^2.0.0",
|
||||
"q": "^1.2.0",
|
||||
"react": "^0.13.1",
|
||||
"sax": "^0.6.1",
|
||||
"serve-favicon": "~2.2.0",
|
||||
"xmlbuilder": "^2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.5.3",
|
||||
"browserify": "^9.0.3",
|
||||
"flux": "^2.0.1",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-autoprefixer": "^2.1.0",
|
||||
"gulp-sass": "^1.3.3",
|
||||
"gulp-watch": "^4.2.1",
|
||||
"keymirror": "^0.1.1",
|
||||
"object-assign": "^2.0.0",
|
||||
"react": "^0.13.1",
|
||||
"reactify": "^1.1.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^2.6.2",
|
||||
"xmldom": "^0.1.19"
|
||||
}
|
||||
}
|
||||
18
routes/client.js
Normal file
18
routes/client.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var express = require('express');
|
||||
var xmlrpc = require('xmlrpc');
|
||||
var router = express.Router();
|
||||
var clientStats = require('../models/clientStats')();
|
||||
|
||||
router.get('/', function(req, res, next) {
|
||||
|
||||
});
|
||||
|
||||
router.get('/stats', function(req, res, next) {
|
||||
|
||||
clientStats.getStats(function(error, results) {
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
9
routes/index.js
Normal file
9
routes/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
36
routes/torrents.js
Normal file
36
routes/torrents.js
Normal file
@@ -0,0 +1,36 @@
|
||||
var express = require('express');
|
||||
var xmlrpc = require('xmlrpc');
|
||||
var router = express.Router();
|
||||
var torrents = require('../models/torrents')();
|
||||
|
||||
router.get('/', function(req, res, next) {
|
||||
|
||||
});
|
||||
|
||||
router.get('/list', function(req, res, next) {
|
||||
|
||||
torrents.listTorrents(function(error, results) {
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
router.get('/:hash/stop', function(req, res, next) {
|
||||
|
||||
var hash = req.params.hash;
|
||||
|
||||
torrents.stopTorrent(hash, function(error, results) {
|
||||
res.json(results);
|
||||
})
|
||||
});
|
||||
|
||||
router.get('/:hash/start', function(req, res, next) {
|
||||
|
||||
var hash = req.params.hash;
|
||||
|
||||
torrents.startTorrent(hash, function(error, results) {
|
||||
res.json(results);
|
||||
})
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
28
source/sass/base/_layout.scss
Normal file
28
source/sass/base/_layout.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flood {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
flex: 25%;
|
||||
min-width: 200px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.torrent {
|
||||
|
||||
&__list {
|
||||
flex: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
7
source/sass/base/_main.scss
Normal file
7
source/sass/base/_main.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
body {
|
||||
background: $background;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
10
source/sass/base/_typography.scss
Normal file
10
source/sass/base/_typography.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
body {
|
||||
color: $foreground;
|
||||
font-family: $font;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-style: normal;
|
||||
margin-left: 0.3em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
27
source/sass/objects/_client-stats.scss
Normal file
27
source/sass/objects/_client-stats.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.client-stats {
|
||||
padding: 30px 20px;
|
||||
|
||||
.speed,
|
||||
.transferred {
|
||||
display: block;
|
||||
font-size: 1.25em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.speed {
|
||||
color: $client-stats--primary;
|
||||
}
|
||||
|
||||
.transferred {
|
||||
color: $client-stats--secondary;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.client-stat {
|
||||
|
||||
& + .client-stat {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
10
source/sass/objects/_filter-bar.scss
Normal file
10
source/sass/objects/_filter-bar.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.filter-bar {
|
||||
background: $filter-bar--background;
|
||||
color: $filter-bar--foreground;
|
||||
padding: 30px 0;
|
||||
|
||||
&__item {
|
||||
border-bottom: 1px solid $filter-bar--border;
|
||||
}
|
||||
|
||||
}
|
||||
10
source/sass/objects/_status-filter.scss
Normal file
10
source/sass/objects/_status-filter.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.status-filter {
|
||||
font-size: 0.85em;
|
||||
padding: 30px 0;
|
||||
|
||||
&__item {
|
||||
cursor: pointer;
|
||||
padding: 3px 20px;
|
||||
}
|
||||
|
||||
}
|
||||
59
source/sass/objects/_torrents.scss
Normal file
59
source/sass/objects/_torrents.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
.torrent {
|
||||
|
||||
&,
|
||||
&__header {
|
||||
display: flex;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
background: $torrent--header--background;
|
||||
border-bottom: 1px solid $torrent--header--border;
|
||||
border-top: 1px solid $torrent--header--border;
|
||||
color: $torrent--header--foreground;
|
||||
display: flex;
|
||||
font-size: 0.5em;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.torrent {
|
||||
|
||||
&__details,
|
||||
&__list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&__detail {
|
||||
|
||||
&--primary,
|
||||
&--secondary {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
|
||||
.torrent & {
|
||||
color: $torrent--primary--foreground;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
display: flex;
|
||||
|
||||
&--sub {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
18
source/sass/style.scss
Normal file
18
source/sass/style.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
@import "bower_components/inuit-defaults/settings.defaults";
|
||||
@import "bower_components/inuit-functions/tools.functions";
|
||||
@import "bower_components/inuit-mixins/tools.mixins";
|
||||
@import "bower_components/inuit-normalize/generic.normalize";
|
||||
@import "bower_components/inuit-reset/generic.reset";
|
||||
@import "bower_components/inuit-box-sizing/generic.box-sizing";
|
||||
@import "bower_components/inuit-page/base.page";
|
||||
|
||||
@import 'tools/variables';
|
||||
|
||||
@import 'base/main';
|
||||
@import 'base/layout';
|
||||
@import 'base/typography';
|
||||
|
||||
@import 'objects/torrents';
|
||||
@import 'objects/filter-bar';
|
||||
@import 'objects/client-stats';
|
||||
@import 'objects/status-filter';
|
||||
17
source/sass/tools/_colors.scss
Normal file
17
source/sass/tools/_colors.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
$background: #2e2a30;
|
||||
$foreground: #686469;
|
||||
|
||||
$filter-bar--background: #1b191c;
|
||||
$filter-bar--foreground: #7f7e80;
|
||||
$filter-bar--border: #161316;
|
||||
|
||||
$client-stats--primary: #e4dde6;
|
||||
$client-stats--secondary: #8b858d;
|
||||
$client-stats--tertiary: #363337;
|
||||
|
||||
$torrent--primary--foreground: #aaa8ab;
|
||||
$torrent--secondary--foreground: #686469;
|
||||
|
||||
$torrent--header--foreground: #5b595c;
|
||||
$torrent--header--background: #29262b;
|
||||
$torrent--header--border: #221f24;
|
||||
5
source/sass/tools/_variables.scss
Normal file
5
source/sass/tools/_variables.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@import 'colors';
|
||||
|
||||
@import url(http://fonts.googleapis.com/css?family=Roboto:400italic,700italic,300,700,300italic,400);
|
||||
|
||||
$font: 'Roboto', sans-srif;
|
||||
40
source/scripts/actions/TorrentActions.js
Normal file
40
source/scripts/actions/TorrentActions.js
Normal file
@@ -0,0 +1,40 @@
|
||||
var AppDispatcher = require('../dispatcher/AppDispatcher');
|
||||
var TorrentConstants = require('../constants/TorrentConstants');
|
||||
|
||||
var performAction = function(action, hash, success, error) {
|
||||
|
||||
$.ajax({
|
||||
url: '/torrents/' + hash + '/' + action,
|
||||
dataType: 'json',
|
||||
|
||||
success: function(data) {
|
||||
success(data);
|
||||
}.bind(this),
|
||||
|
||||
error: function(xhr, status, err) {
|
||||
console.error(torrentsData, status, err.toString());
|
||||
}.bind(this)
|
||||
});
|
||||
};
|
||||
|
||||
var TorrentActions = {
|
||||
|
||||
stop: function(hash) {
|
||||
performAction('stop', hash, function(data) {
|
||||
AppDispatcher.dispatch({
|
||||
actionType: TorrentConstants.TORRENT_STOP
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
start: function(hash) {
|
||||
performAction('start', hash, function(data) {
|
||||
AppDispatcher.dispatch({
|
||||
actionType: TorrentConstants.TORRENT_START
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = TorrentActions;
|
||||
5
source/scripts/app.js
Normal file
5
source/scripts/app.js
Normal file
@@ -0,0 +1,5 @@
|
||||
var React = require('react');
|
||||
var FloodApp = require('./components/FloodApp');
|
||||
var mountNode = document.getElementById('app');
|
||||
|
||||
React.render(<FloodApp />, mountNode);
|
||||
26
source/scripts/components/FloodApp.js
Normal file
26
source/scripts/components/FloodApp.js
Normal file
@@ -0,0 +1,26 @@
|
||||
var React = require('react');
|
||||
var FilterBar = require('./filter-bar/FilterBar');
|
||||
var TorrentList = require('./torrent-list/TorrentList');
|
||||
|
||||
var FloodApp = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return null;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<div className="flood">
|
||||
<FilterBar />
|
||||
<TorrentList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = FloodApp;
|
||||
19
source/scripts/components/action-bar/Action.js
Normal file
19
source/scripts/components/action-bar/Action.js
Normal file
@@ -0,0 +1,19 @@
|
||||
var React = require('react');
|
||||
|
||||
var Action = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var classString = 'action action--' + this.props.slug;
|
||||
|
||||
return (
|
||||
<span className={classString} onClick={this.props.clickHandler}>{this.props.label}</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Action;
|
||||
29
source/scripts/components/action-bar/ActionBar.js
Normal file
29
source/scripts/components/action-bar/ActionBar.js
Normal file
@@ -0,0 +1,29 @@
|
||||
var React = require('react');
|
||||
var Action = require('./Action.js');
|
||||
|
||||
var FilterBar = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
handleClick: function(event) {
|
||||
console.log('click ' + event.target);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<nav className="filter-bar">
|
||||
<div className="actions">
|
||||
<Action label="Add Torrent" slug="add-torrent" clickHandler={this.handleClick} />
|
||||
<Action label="Remove Torrent" slug="remove-torrent" clickHandler={this.handleClick} />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = FilterBar;
|
||||
93
source/scripts/components/filter-bar/ClientStats.js
Normal file
93
source/scripts/components/filter-bar/ClientStats.js
Normal file
@@ -0,0 +1,93 @@
|
||||
var React = require('react');
|
||||
var ClientStore = require('../../stores/ClientStore');
|
||||
var format = require('../../helpers/formatData');
|
||||
|
||||
var getClientStats = function() {
|
||||
return {
|
||||
clientStats: ClientStore.getStats()
|
||||
}
|
||||
}
|
||||
|
||||
var Speed = React.createClass({
|
||||
getInitialState: function() {
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<span className="speed">
|
||||
{this.props.value}
|
||||
<em className="unit">{this.props.unit}</em>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DataTransferred = React.createClass({
|
||||
getInitialState: function() {
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<span className="transferred">
|
||||
{this.props.value}
|
||||
<em className="unit">{this.props.unit}</em>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ClientStats = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
clientStats: {
|
||||
speed: {
|
||||
upload: 0,
|
||||
download: 0
|
||||
},
|
||||
transferred: {
|
||||
upload: 0,
|
||||
download: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
ClientStore.addChangeListener(this._onChange);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
ClientStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var uploadSpeed = format.data(this.state.clientStats.speed.upload, '/s');
|
||||
var uploadTotal = format.data(this.state.clientStats.transferred.upload);
|
||||
var downloadSpeed = format.data(this.state.clientStats.speed.download, '/s');
|
||||
var downloadTotal = format.data(this.state.clientStats.transferred.download);
|
||||
|
||||
return (
|
||||
<div className="client-stats filter-bar__item">
|
||||
<div className="client-stat client-stat--upload">
|
||||
<Speed value={uploadSpeed.value} unit={uploadSpeed.unit} />
|
||||
<DataTransferred value={uploadTotal.value} unit={uploadTotal.unit} />
|
||||
</div>
|
||||
<div className="client-stat client-stat--download">
|
||||
<Speed value={downloadSpeed.value} unit={downloadSpeed.unit} />
|
||||
<DataTransferred value={downloadTotal.value} unit={downloadTotal.unit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_onChange: function() {
|
||||
this.setState(getClientStats);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = ClientStats;
|
||||
28
source/scripts/components/filter-bar/FilterBar.js
Normal file
28
source/scripts/components/filter-bar/FilterBar.js
Normal file
@@ -0,0 +1,28 @@
|
||||
var React = require('react');
|
||||
var ClientStats = require('./ClientStats');
|
||||
var StatusFilter = require('./StatusFilter');
|
||||
|
||||
var FilterBar = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
handleClick: function(event) {
|
||||
console.log('click ' + event.target);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<nav className="filter-bar">
|
||||
<ClientStats />
|
||||
<StatusFilter />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = FilterBar;
|
||||
40
source/scripts/components/filter-bar/StatusFilter.js
Normal file
40
source/scripts/components/filter-bar/StatusFilter.js
Normal file
@@ -0,0 +1,40 @@
|
||||
var React = require('react');
|
||||
|
||||
var ClientStats = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var filters = [
|
||||
'All',
|
||||
'Downloading',
|
||||
'Completed',
|
||||
'Active',
|
||||
'Inactive',
|
||||
'Error'
|
||||
];
|
||||
|
||||
var filterEls = filters.map(function(filter) {
|
||||
|
||||
var filterSlug = filter.toLowerCase();
|
||||
var classString = 'status-filter__item status-filter__item--' + filterSlug;
|
||||
|
||||
return (
|
||||
<li className={classString}>{filter}</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="status-filter filter-bar__item">
|
||||
{filterEls}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = ClientStats;
|
||||
65
source/scripts/components/torrent-list/Torrent.js
Normal file
65
source/scripts/components/torrent-list/Torrent.js
Normal file
@@ -0,0 +1,65 @@
|
||||
var React = require('react');
|
||||
var TorrentActions = require('../../actions/TorrentActions.js');
|
||||
var format = require('../../helpers/formatData');
|
||||
|
||||
var Torrent = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var torrent = this.props.data;
|
||||
|
||||
var uploadRate = format.data(torrent.uploadRate, '/s');
|
||||
var uploadTotal = format.data(torrent.uploadTotal);
|
||||
var downloadRate = format.data(torrent.downloadRate, '/s');
|
||||
var downloadTotal = format.data(torrent.downloadTotal);
|
||||
|
||||
return (
|
||||
<li className="torrent">
|
||||
<span className="torrent__detail--primary">{torrent.name}</span>
|
||||
<ul className="torrent__details torrent__detail--secondary">
|
||||
<li className="torrent__detail--secondary--sub">{torrent.state}</li>
|
||||
<li className="torrent__detail--secondary--sub">
|
||||
{uploadRate.value}
|
||||
<em className="unit">{uploadRate.unit}</em>
|
||||
</li>
|
||||
<li className="torrent__detail--secondary--sub">
|
||||
{uploadTotal.value}
|
||||
<em className="unit">{uploadTotal.unit}</em>
|
||||
</li>
|
||||
<li className="torrent__detail--secondary--sub">
|
||||
{downloadRate.value}
|
||||
<em className="unit">{downloadRate.unit}</em>
|
||||
</li>
|
||||
<li className="torrent__detail--secondary--sub">
|
||||
{downloadTotal.value}
|
||||
<em className="unit">{downloadTotal.unit}</em>
|
||||
</li>
|
||||
<li className="torrent__detail--secondary--sub">
|
||||
{torrent.ratio}
|
||||
</li>
|
||||
<li className="torrent__detail--secondary--sub" onClick={this._onStart}>
|
||||
Start
|
||||
</li>
|
||||
<li className="torrent__detail--secondary--sub" onClick={this._onStop}>
|
||||
Stop
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
},
|
||||
|
||||
_onStop: function() {
|
||||
TorrentActions.stop(this.props.data.hash);
|
||||
},
|
||||
|
||||
_onStart: function() {
|
||||
TorrentActions.start(this.props.data.hash);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Torrent;
|
||||
65
source/scripts/components/torrent-list/TorrentList.js
Normal file
65
source/scripts/components/torrent-list/TorrentList.js
Normal file
@@ -0,0 +1,65 @@
|
||||
var React = require('react');
|
||||
var Torrent = require('./Torrent');
|
||||
var TorrentStore = require('../../stores/TorrentStore.js')
|
||||
|
||||
var getTorrentList = function() {
|
||||
return {
|
||||
allTorrents: TorrentStore.getAll()
|
||||
}
|
||||
}
|
||||
|
||||
var TorrentList = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
|
||||
return {
|
||||
allTorrents: []
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
TorrentStore.addChangeListener(this._onChange);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
TorrentStore.removeChangeListener(this._onChange);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var torrents = this.state.allTorrents;
|
||||
|
||||
var torrentList = torrents.map(function(torrent) {
|
||||
|
||||
return (
|
||||
<Torrent key={torrent.hash} data={torrent} />
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="torrent__list">
|
||||
<header className="torrent__header">
|
||||
<span className="torrent__detail--primary">Name</span>
|
||||
<div className="torrent__detail--secondary">
|
||||
<span className="torrent__detail--secondary--sub">State</span>
|
||||
<span className="torrent__detail--secondary--sub">Up</span>
|
||||
<span className="torrent__detail--secondary--sub"> </span>
|
||||
<span className="torrent__detail--secondary--sub">Down</span>
|
||||
<span className="torrent__detail--secondary--sub"> </span>
|
||||
<span className="torrent__detail--secondary--sub">Ratio</span>
|
||||
<span className="torrent__detail--secondary--sub">Start</span>
|
||||
<span className="torrent__detail--secondary--sub">Stop</span>
|
||||
</div>
|
||||
</header>
|
||||
{torrentList}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
|
||||
_onChange: function() {
|
||||
this.setState(getTorrentList);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = TorrentList;
|
||||
7
source/scripts/constants/ClientConstants.js
Normal file
7
source/scripts/constants/ClientConstants.js
Normal file
@@ -0,0 +1,7 @@
|
||||
var keyMirror = require('keymirror');
|
||||
|
||||
module.exports = keyMirror({
|
||||
ADD_TORRENT: 'client--add-torrent',
|
||||
REMOVE_TORRENT: 'client--remove-torrent',
|
||||
CLIENT_STATS_CHANGE: 'client--stats-change'
|
||||
});
|
||||
7
source/scripts/constants/TorrentConstants.js
Normal file
7
source/scripts/constants/TorrentConstants.js
Normal file
@@ -0,0 +1,7 @@
|
||||
var keyMirror = require('keymirror');
|
||||
|
||||
module.exports = keyMirror({
|
||||
TORRENT_STOP: 'torrent--stop',
|
||||
TORRENT_START: 'torrent--start',
|
||||
TORRENT_LIST_CHANGE: 'torrent-list--change'
|
||||
});
|
||||
3
source/scripts/dispatcher/AppDispatcher.js
Normal file
3
source/scripts/dispatcher/AppDispatcher.js
Normal file
@@ -0,0 +1,3 @@
|
||||
var Dispatcher = require('flux').Dispatcher;
|
||||
|
||||
module.exports = new Dispatcher();
|
||||
47
source/scripts/helpers/formatData.js
Normal file
47
source/scripts/helpers/formatData.js
Normal file
@@ -0,0 +1,47 @@
|
||||
var format = {
|
||||
|
||||
data: function(bytes, extraUnits, precision) {
|
||||
|
||||
var precision = precision || 2;
|
||||
|
||||
var kilobyte = 1024,
|
||||
megabyte = kilobyte * 1024,
|
||||
gigabyte = megabyte * 1024,
|
||||
terabyte = gigabyte * 1024,
|
||||
value = '',
|
||||
unit = '';
|
||||
|
||||
if ((bytes >= 0) && (bytes < kilobyte)) {
|
||||
value = bytes;
|
||||
unit = 'B';
|
||||
} else if ((bytes >= kilobyte) && (bytes < megabyte)) {
|
||||
value = (bytes / kilobyte).toFixed(precision);
|
||||
unit = 'kB';
|
||||
} else if ((bytes >= megabyte) && (bytes < gigabyte)) {
|
||||
value = (bytes / megabyte).toFixed(precision);
|
||||
unit = 'MB';
|
||||
} else if ((bytes >= gigabyte) && (bytes < terabyte)) {
|
||||
value = (bytes / gigabyte).toFixed(precision);
|
||||
unit = 'GB';
|
||||
} else if (bytes >= terabyte) {
|
||||
value = (bytes / terabyte).toFixed(precision);
|
||||
unit = 'TB';
|
||||
} else {
|
||||
value = bytes;
|
||||
unit = 'B';
|
||||
}
|
||||
|
||||
if (extraUnits) {
|
||||
unit += extraUnits;
|
||||
}
|
||||
|
||||
return {
|
||||
'value': value,
|
||||
'unit': unit
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = format;
|
||||
82
source/scripts/stores/ClientStore.js
Normal file
82
source/scripts/stores/ClientStore.js
Normal file
@@ -0,0 +1,82 @@
|
||||
var AppDispatcher = require('../dispatcher/AppDispatcher');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var ClientConstants = require('../constants/ClientConstants');
|
||||
var assign = require('object-assign');
|
||||
|
||||
var _stats = {};
|
||||
|
||||
var ClientStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
getStats: function() {
|
||||
return _stats;
|
||||
},
|
||||
|
||||
emitChange: function() {
|
||||
this.emit(ClientConstants.CLIENT_STATS_CHANGE);
|
||||
},
|
||||
|
||||
addChangeListener: function(callback) {
|
||||
this.on(ClientConstants.CLIENT_STATS_CHANGE, callback);
|
||||
},
|
||||
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener(ClientConstants.CLIENT_STATS_CHANGE, callback);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var dispatcherIndex = AppDispatcher.register(function(action) {
|
||||
|
||||
var text;
|
||||
|
||||
switch(action.actionType) {
|
||||
|
||||
case ClientConstants.ADD_TORRENT:
|
||||
getClientStats();
|
||||
break;
|
||||
|
||||
case ClientConstants.REMOVE_TORRENT:
|
||||
getClientStats();
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
var getClientStats = function(callback) {
|
||||
|
||||
|
||||
$.ajax({
|
||||
url: '/client/stats',
|
||||
dataType: 'json',
|
||||
|
||||
success: function(data) {
|
||||
|
||||
_stats = {
|
||||
speed: {
|
||||
upload: data.uploadRate,
|
||||
download: data.downloadRate
|
||||
},
|
||||
transferred: {
|
||||
upload: data.uploadTotal,
|
||||
download: data.downloadTotal
|
||||
}
|
||||
};
|
||||
|
||||
ClientStore.emitChange();
|
||||
|
||||
}.bind(this),
|
||||
|
||||
error: function(xhr, status, err) {
|
||||
console.error('/client/stats', status, err.toString());
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
getClientStats();
|
||||
setInterval(getClientStats, 5000);
|
||||
|
||||
module.exports = ClientStore;
|
||||
69
source/scripts/stores/TorrentStore.js
Normal file
69
source/scripts/stores/TorrentStore.js
Normal file
@@ -0,0 +1,69 @@
|
||||
var AppDispatcher = require('../dispatcher/AppDispatcher');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var TorrentConstants = require('../constants/TorrentConstants');
|
||||
var assign = require('object-assign');
|
||||
|
||||
var _torrents = [];
|
||||
|
||||
var getTorrentList = function(callback) {
|
||||
|
||||
$.ajax({
|
||||
url: '/torrents/list',
|
||||
dataType: 'json',
|
||||
|
||||
success: function(data) {
|
||||
_torrents = data;
|
||||
TorrentStore.emitChange();
|
||||
}.bind(this),
|
||||
|
||||
error: function(xhr, status, err) {
|
||||
console.error('/torrents/list', status, err.toString());
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
getTorrentList();
|
||||
setInterval(getTorrentList, 5000);
|
||||
|
||||
var TorrentStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
getAll: function() {
|
||||
return _torrents;
|
||||
},
|
||||
|
||||
emitChange: function() {
|
||||
this.emit(TorrentConstants.TORRENT_LIST_CHANGE);
|
||||
},
|
||||
|
||||
addChangeListener: function(callback) {
|
||||
this.on(TorrentConstants.TORRENT_LIST_CHANGE, callback);
|
||||
},
|
||||
|
||||
removeChangeListener: function(callback) {
|
||||
this.removeListener(TorrentConstants.TORRENT_LIST_CHANGE, callback);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var dispatcherIndex = AppDispatcher.register(function(action) {
|
||||
|
||||
var text;
|
||||
|
||||
switch(action.actionType) {
|
||||
|
||||
case TorrentConstants.TORRENT_STOP:
|
||||
getTorrentList();
|
||||
break;
|
||||
|
||||
case TorrentConstants.TORRENT_START:
|
||||
getTorrentList();
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TorrentStore;
|
||||
6
views/error.jade
Normal file
6
views/error.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
4
views/index.jade
Normal file
4
views/index.jade
Normal file
@@ -0,0 +1,4 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
div#app.container
|
||||
11
views/layout.jade
Normal file
11
views/layout.jade
Normal file
@@ -0,0 +1,11 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet' href='/stylesheets/style.css')
|
||||
script(src='//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js')
|
||||
body
|
||||
block content
|
||||
script(src='/scripts/app.js')
|
||||
script#__bs_script__.
|
||||
document.write("<script async src='http://HOST:3001/browser-sync/browser-sync-client.2.5.3.js'><\/script>".replace("HOST", location.hostname));
|
||||
Reference in New Issue
Block a user