├── lib ├── dirty │ ├── index.js │ ├── set.js │ └── dirty.js ├── packdb.js ├── downloadHandler.js └── xdcclogger.js ├── .bowerrc ├── .travis.yml ├── public ├── favicon.ico ├── img │ ├── bc_logo.png │ └── pp_logo.png ├── js │ ├── user.js │ ├── directives.js │ ├── controllers │ │ ├── packetcount.js │ │ ├── searchbar.js │ │ ├── notificationcount.js │ │ ├── settings.js │ │ ├── dbsettings.js │ │ ├── serveradd.js │ │ ├── serversettings.js │ │ ├── packetlist.js │ │ ├── downloads.js │ │ └── dashboard.js │ ├── app.js │ ├── services.js │ └── filters.js └── css │ └── app.css ├── .npmignore ├── .gitignore ├── views ├── partials │ ├── settings │ │ ├── addchannel.jade │ │ ├── channelitem.jade │ │ ├── index.jade │ │ ├── about.jade │ │ ├── editserver.jade │ │ ├── addserver.jade │ │ └── database.jade │ ├── packetlist │ │ ├── tablefoot.jade │ │ ├── column.jade │ │ ├── tablehead.jade │ │ └── index.jade │ ├── dashboard │ │ ├── index.jade │ │ ├── serverpie.jade │ │ ├── packetpie.jade │ │ └── dlnotification.jade │ └── downloads │ │ ├── index.jade │ │ └── column.jade ├── layout.jade └── index.jade ├── routes ├── index.js ├── socket.js └── api.js ├── bower.json ├── package.json ├── README.md ├── server.js └── .jshintrc /lib/dirty/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dirty'); -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components/" 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "7" -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaVarga/slingxdcc/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/bc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaVarga/slingxdcc/HEAD/public/img/bc_logo.png -------------------------------------------------------------------------------- /public/img/pp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaVarga/slingxdcc/HEAD/public/img/pp_logo.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.db 10 | settings.json 11 | *.key 12 | *.crt 13 | 14 | *~ 15 | 16 | .idea 17 | node_modules 18 | public/bower_components 19 | 20 | pids 21 | logs 22 | results 23 | 24 | .gitignore 25 | npm-debug.log 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.db 10 | settings.json 11 | *.key 12 | *.crt 13 | 14 | *~ 15 | 16 | .idea 17 | node_modules 18 | 19 | pids 20 | logs 21 | results 22 | 23 | public/bower_components 24 | 25 | npm-debug.log 26 | /.settings/ 27 | /.project 28 | -------------------------------------------------------------------------------- /views/partials/settings/addchannel.jade: -------------------------------------------------------------------------------- 1 | form(role="joinChannel", ng-submit="joinChannels()").input-group 2 | input.form-control(placeholder="Add channel", type="text", ng-model="joinChanStr") 3 | span.input-group-btn 4 | button.btn.btn-default(type="submit", tooltip="Add multiple channels separated by space", tooltip-trigger="mouseenter", tooltip-placement="left") 5 | span.glyphicon.glyphicon-plus -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype 2 | html(ng-app="myApp") 3 | head 4 | meta(charset='utf8') 5 | base(href='/') 6 | title Slingxdcc 7 | link(rel='stylesheet', href='bower_components/bootstrap/dist/css/bootstrap.min.css') 8 | link(rel='stylesheet', href='bower_components/angular-bootstrap/ui-bootstrap-csp.css') 9 | link(rel='stylesheet', href='/css/app.css') 10 | body 11 | block body -------------------------------------------------------------------------------- /views/partials/packetlist/tablefoot.jade: -------------------------------------------------------------------------------- 1 | td(colspan="8") 2 | ul.pagination-sm(uib-pagination="", ng-model="currentPage", total-items="numPackets", force-ellipses="true", ng-change="pageChanged()", items-per-page="pageItemLimit", num-pages="smallnumPages", page="currentPage", boundary-link-numbers="true", max-size="9", boundary-links="true", on-select-page="setPage(page)", previous-text="‹", next-text="›", first-text="«", last-text="»") 3 | 4 | -------------------------------------------------------------------------------- /public/js/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ -------------------------------------------------------------------------------- /views/partials/settings/channelitem.jade: -------------------------------------------------------------------------------- 1 | .row 2 | span.col-xs-8 {{channel}} 3 | span(tooltip-placement="left", tooltip="{{isObserved(channel)|observTooltip}}").col-xs-2 4 | span(ng-class="isObserved(channel) ? 'glyphicon-eye-open text-success' : 'glyphicon-eye-close text-danger'",ng-click="toggleObserv(channel)").glyphicon 5 | span(tooltip-placement="left", tooltip="Part {{channel}}").col-xs-2 6 | span(ng-click="partChannel(channel)").text-danger.glyphicon.glyphicon-remove -------------------------------------------------------------------------------- /views/partials/dashboard/index.jade: -------------------------------------------------------------------------------- 1 | div(ng-controller="DashboardCtrl").dashboard 2 | .row 3 | .col-lg-12 4 | button(type="button",ng-click="getData()").btn.btn-default.pull-right 5 | span.glyphicon.glyphicon-refresh 6 | h1 Dashboard 7 | .row 8 | .col-lg-6 9 | include packetpie 10 | .col-lg-6 11 | include serverpie 12 | .row 13 | .col-lg-6 14 | include dlnotification 15 | .col-lg-6 -------------------------------------------------------------------------------- /views/partials/dashboard/serverpie.jade: -------------------------------------------------------------------------------- 1 | h3 Server 2 | hr 3 | .col-sm-8 4 | canvas.chart-doughnut(donutchart,chart-options="chartOptions",chart-data="chartServerData",height="300px", chart-colors="chartColors", chart-labels="chartServerLabels") 5 | .col-sm-4 6 | ul.list-group 7 | li.list-group-item 8 | span Online: 9 | span.label.label-success.pull-right {{onServerPercentage() | number:1}}% 10 | li.list-group-item 11 | span Offline: 12 | span.label.label-danger.pull-right {{offServerPercentage() | number:1}}% -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | /* 11 | * GET home page. 12 | */ 13 | 14 | exports.index = function(req, res){ 15 | res.render('index'); 16 | }; 17 | 18 | exports.partials = function (req, res) { 19 | var name = req.params.name; 20 | res.render('partials/' + name); 21 | }; -------------------------------------------------------------------------------- /public/js/directives.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Directives */ 12 | 13 | 14 | angular.module('myApp.directives', []). 15 | directive('appVersion', ['version', function(version) { 16 | return function(scope, elm, attrs) { 17 | elm.text(version); 18 | }; 19 | }]); 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slingxdcc", 3 | "version": "0.1.1", 4 | "authors": [ 5 | "Daniel Varga " 6 | ], 7 | "description": "XDCC download manager with web frontend written in node.js", 8 | "keywords": [ 9 | "XDCC", 10 | "downloadmanager", 11 | "download", 12 | "manager", 13 | "dcc", 14 | "irc", 15 | "angular", 16 | "bootstrap" 17 | ], 18 | "license": "Lizenzangabe, z. B. MIT", 19 | "homepage": "Homepage zum Package, falls vorhanden", 20 | "dependencies": { 21 | "bootstrap": "^3.3.7", 22 | "angular": "^1.6.4", 23 | "angular-bootstrap": "^2.5.0", 24 | "chart.js": "^2.5.0", 25 | "angular-route": "^1.6.4", 26 | "angular-animate": "^1.6.4", 27 | "angular-chart.js": "^1.1.1", 28 | "socket.io": "^1.7.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /views/partials/settings/index.jade: -------------------------------------------------------------------------------- 1 | div(ng-controller="SettingsCtrl") 2 | .row 3 | .col-lg-12 4 | button(type="button",ng-click="getData()").btn.btn-default.pull-right 5 | span.glyphicon.glyphicon-refresh 6 | h1 Settings 7 | uib-accordion 8 | div(uib-accordion-group='', close-others="true", is-open="true") 9 | div(uib-accordion-heading='') Irc servers 10 | uib-tabset 11 | include editserver 12 | include addserver 13 | div(uib-accordion-group='') 14 | div(uib-accordion-heading='') Database 15 | include database 16 | div(uib-accordion-group='') 17 | uib-accordion-heading About 18 | include about 19 | -------------------------------------------------------------------------------- /views/partials/dashboard/packetpie.jade: -------------------------------------------------------------------------------- 1 | h3 Packets/Database 2 | hr 3 | .col-sm-8 4 | canvas.chart-doughnut(donutchart,chart-options="chartOptions",chart-data="chartPacketData",height="300px", chart-colors="chartColors", chart-labels="chartPacketLabels") 5 | .col-sm-4 6 | ul.list-group 7 | li.list-group-item 8 | span Unique: 9 | span.label.label-success.pull-right {{unqPacketPercentage() | number:1}}% 10 | li.list-group-item 11 | span Redundant: 12 | span.label.label-danger.pull-right {{redPacketPercentage() | number:1}}% 13 | li.list-group-item 14 | span(ng-class="{collapse:!(compacting.autoCompacting)}") Next database compacting: {{compacting.nextCompacting | date:'dd.MM HH:mm'}} 15 | span.label.label-warning.center-block(ng-class="{collapse:(compacting.autoCompacting)}") No Database Cleanup -------------------------------------------------------------------------------- /views/partials/packetlist/column.jade: -------------------------------------------------------------------------------- 1 | tr(ng-repeat="packet in packets") 2 | td 3 | span {{packet.server}} 4 | td 5 | span {{packet.nick}} 6 | td 7 | span {{packet.nr}} 8 | td 9 | span {{packet.downloads}} 10 | td 11 | span {{packet.filesize|bytes}} 12 | td 13 | span(tooltip-placement="bottom", tooltip="{{packet.filename}}") {{packet.filename|truncate:50}} 14 | td 15 | span(tooltip-placement="bottom", tooltip="{{packet.lastseen | date:'medium'}}") {{packet.lastseen| timeago}} 16 | td 17 | button(type="button",ng-click="startDownload(packet)", ng-class="{collapse:!dlActive(packet)}").btn.btn-default.btn-xs 18 | span.glyphicon.glyphicon-download 19 | button(type="button",ng-click="cancelDownload(packet)", ng-class="{collapse:dlActive(packet)}").btn.btn-default.btn-xs 20 | span.glyphicon.glyphicon-stop -------------------------------------------------------------------------------- /public/js/controllers/packetcount.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Searchbarcontroller handels the searches */ 12 | angular.module('myApp') 13 | .controller('PacketcountCtrl',['$scope', 'socket', function ($scope, socket){ 14 | 15 | socket.on('send:packetCount', function(data){ 16 | $scope.packetCount = data; 17 | }); 18 | 19 | $scope.$on('$destroy', function () { 20 | socket.off('send:packetCount'); 21 | }); 22 | 23 | }]); 24 | -------------------------------------------------------------------------------- /views/partials/downloads/index.jade: -------------------------------------------------------------------------------- 1 | div(ng-controller="DownloadsCtrl").downloads 2 | .row 3 | .col-lg-12 4 | button(type="button",ng-click="getData()").btn.btn-default.pull-right 5 | span.glyphicon.glyphicon-refresh 6 | h1 Downloads 7 | .table-responsive 8 | table.table.table-hover.table-condensed.table-striped 9 | thead 10 | tr 11 | th(style="width:200px") Bot 12 | th(style="width:40px") Q 13 | th Filename 14 | th(style="width:100px") Size 15 | th(style="width:85px") Speed 16 | th(style="width:50px") 17 | tfoot 18 | tr 19 | td 20 | td 21 | td 22 | td 23 | td 24 | span(ng-class="{collapse:(speedsum <= 0||dlList.length < 2)}") {{speedsum|bytes}}/s 25 | td 26 | tbody 27 | include column 28 | 29 | 30 | -------------------------------------------------------------------------------- /views/partials/settings/about.jade: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-9 3 | h4 License 4 | hr 5 | .monospace 6 | span "THE BEER-WARE LICENSE" (Revision 42): 7 | br 8 | span varga.daniel@gmx.de wrote this. As long as you retain this notice you 9 | br 10 | span can do whatever you want with this stuff. If we meet some day, and you think 11 | br 12 | span this stuff is worth it, you can buy me a beer in return Daniel Varga 13 | 14 | .col-md-3.donate 15 | h4 Buy me a beer 16 | hr 17 | ul.list-group 18 | li.list-group-item 19 | a(href="bitcoin:1CSUh96R8DDv1yr65NUZaQke5jpCAAXrCt") 20 | .row 21 | .col-xs-3 22 | span.bc-logo 23 | .col-xs-9 24 | span Pay my beer via bitcoin! 25 | li.list-group-item 26 | a(href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7WMJYNQAS9KU2", target="_blank") 27 | .row 28 | .col-xs-3 29 | span.pp-logo 30 | .col-xs-9 31 | span Pay my beer via paypal! 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Daniel Varga", 3 | "name": "slingxdcc", 4 | "description": "XDCC download manager with web frontend written in node.js", 5 | "keywords": [ 6 | "XDCC", 7 | "downloadmanager", 8 | "download", 9 | "manager", 10 | "dcc", 11 | "irc", 12 | "angular", 13 | "bootstrap" 14 | ], 15 | "version": "0.1.1", 16 | "repository": { 17 | "type": "git", 18 | "url": "http://github.com/DaVarga/slingxdcc" 19 | }, 20 | "bugs": { 21 | "url": "http://github.com/DaVarga/slingxdcc/issues" 22 | }, 23 | "engines": { 24 | "node": ">=0.1.13" 25 | }, 26 | "bin": { 27 | "slingxdcc": "server.js" 28 | }, 29 | "scripts": { 30 | "preinstall": "npm install bower", 31 | "postinstall": "bower install" 32 | }, 33 | "readmeFilename": "README.md", 34 | "license": "BEER-WARE", 35 | "dependencies": { 36 | "axdcc": "0.2.23", 37 | "body-parser": "~1.17.1", 38 | "common-errors": "^1.0.0", 39 | "dirty-query": "~0.1.1", 40 | "errorhandler": "~1.5.0", 41 | "express": "~4.15.2", 42 | "irc": "~0.5.2", 43 | "jade": "~1.11.0", 44 | "log4js": "^1.1.1", 45 | "method-override": "~2.3.8", 46 | "morgan": "~1.8.1", 47 | "nconf": "0.8.4", 48 | "node-fs": "0.1.7", 49 | "socket.io": "~1.7.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/dirty/set.js: -------------------------------------------------------------------------------- 1 | function Set(initial) { 2 | this._items = {}; 3 | this._length = 0; 4 | (initial || []).forEach(function(item){ 5 | this.add(item); 6 | }, this); 7 | } 8 | 9 | Set.prototype.add = function(item) { 10 | if (!this.contains(item)) this._length++; 11 | this._items[item] = true; 12 | return this; 13 | } 14 | 15 | Set.prototype.push = Set.prototype.add; 16 | 17 | Set.prototype.empty = function() { 18 | return this._length === 0; 19 | } 20 | 21 | Set.prototype.contains = function(item) { 22 | return this._items.hasOwnProperty(item); 23 | } 24 | 25 | Set.prototype.remove = function(item) { 26 | if (this.contains(item)) this._length--; 27 | delete this._items[item]; 28 | return this; 29 | } 30 | 31 | Set.prototype.toArray = function() { 32 | var retVal = []; 33 | for (var item in this._items) { 34 | if (this.contains(item)) retVal.push(item); 35 | } 36 | return retVal; 37 | } 38 | 39 | Set.prototype.difference = function(other, returnArray) { 40 | var result = returnArray ? [] : new Set(); 41 | for (var item in this._items) { 42 | if (this.contains(item) && !other.contains(item)) result.push(item); 43 | } 44 | return result; 45 | } 46 | 47 | Set.prototype.__defineGetter__("length", function(){ 48 | return this._length; 49 | }); 50 | 51 | 52 | module.exports = Set; -------------------------------------------------------------------------------- /views/partials/packetlist/tablehead.jade: -------------------------------------------------------------------------------- 1 | tr 2 | th.server(ng-click="setSorting('server')") Server 3 | span(ng-class="{desc:'glyphicon-chevron-down', asc:'glyphicon-chevron-up', none:''}[sorted('server')]").glyphicon 4 | th.bot(ng-click="setSorting('nick')") Bot 5 | span(ng-class="{desc:'glyphicon-chevron-down', asc:'glyphicon-chevron-up', none:''}[sorted('nick')]").glyphicon 6 | th.packetnr(ng-click="setSorting('nr')") # 7 | span(ng-class="{desc:'glyphicon-chevron-down', asc:'glyphicon-chevron-up', none:''}[sorted('nr')]").glyphicon 8 | th.gets(ng-click="setSorting('downloads')") Gets 9 | span(ng-class="{desc:'glyphicon-chevron-down', asc:'glyphicon-chevron-up', none:''}[sorted('downloads')]").glyphicon 10 | th.size(ng-click="setSorting('filesize')") Size 11 | span(ng-class="{desc:'glyphicon-chevron-down', asc:'glyphicon-chevron-up', none:''}[sorted('filesize')]").glyphicon 12 | th.filename(ng-click="setSorting('filename')") Filename 13 | span(ng-class="{desc:'glyphicon-chevron-down', asc:'glyphicon-chevron-up', none:''}[sorted('filename')]").glyphicon 14 | th.lastseen(ng-click="setSorting('lastseen')") Last seen 15 | span(ng-class="{desc:'glyphicon-chevron-down', asc:'glyphicon-chevron-up', none:''}[sorted('lastseen')]").glyphicon 16 | th.download -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | // Declare app level module which depends on filters, and services 12 | angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.directives', 'ui.bootstrap', 'chart.js', 'ngRoute']). 13 | config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider){ 14 | $routeProvider 15 | .when('/', { 16 | templateUrl: 'partials/dashboard/' 17 | }) 18 | .when('/packets', { 19 | templateUrl: 'partials/packetlist/' 20 | }) 21 | .when('/settings', { 22 | templateUrl: 'partials/settings/' 23 | }) 24 | .when('/downloads', { 25 | templateUrl: 'partials/downloads/' 26 | }) 27 | .otherwise({ 28 | redirectTo: '/' 29 | }); 30 | $locationProvider.html5Mode(true); 31 | }]); 32 | 33 | -------------------------------------------------------------------------------- /views/partials/packetlist/index.jade: -------------------------------------------------------------------------------- 1 | div(ng-controller="PacketListCtrl") 2 | .row 3 | .col-lg-12 4 | div(ng-class="{collapse:numPackets!=0}") 5 | .alert.alert-warning 6 | 7 | button(type="button",ng-click="setPage(1)").btn.btn-default.pull-right 8 | span.glyphicon.glyphicon-refresh 9 | 10 | span 11 | | Your search 12 | b "{{searchString}}" 13 | |did not match any packets. 14 | p Suggenstions: 15 | ul 16 | li Make sure all words are spelled correctly. 17 | li Try other keywords. 18 | 19 | 20 | 21 | div(ng-class="{collapse:numPackets==0}") 22 | 23 | button(type="button",ng-click="setPage(1)").btn.btn-default.pull-right 24 | span.glyphicon.glyphicon-refresh 25 | 26 | h1(ng-class="{collapse:(searchString)}") List of all packets 27 | h1(ng-class="{collapse:(!searchString)}") Search for {{searchString}} 28 | 29 | 30 | 31 | .table-responsive 32 | table(style="opacity:{{getOpacity()}}").table.table-hover.table-striped.table-condensed.packets 33 | thead 34 | include tablehead 35 | tfoot 36 | include tablefoot 37 | tbody 38 | include column -------------------------------------------------------------------------------- /routes/socket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Serve content over a socket 3 | */ 4 | 5 | var logger = require("../lib/xdcclogger"); 6 | var packdb = require("../lib/packdb"); 7 | var downloadHandler = require("../lib/downloadHandler"); 8 | 9 | module.exports = function (socket) { 10 | var lastPacketCount = 0; 11 | 12 | setInterval(function () { 13 | if(lastPacketCount != packdb.numberOfPackets()){ 14 | lastPacketCount = packdb.numberOfPackets(); 15 | 16 | var abspackets = packdb.numberOfPackets(); 17 | var redpackets = packdb.numberOfRedundantPackets(); 18 | 19 | socket.emit('send:packetCount', { 20 | absPackets : abspackets, 21 | redPackets : redpackets 22 | }); 23 | } 24 | 25 | }, 100); 26 | 27 | logger.on("irc_error",function(srvkey){ 28 | socket.emit('send:irc_error', { 29 | server: logger.getIrcServers()[srvkey] 30 | }); 31 | }); 32 | 33 | logger.on("irc_connected", function(srvkey){ 34 | socket.emit('send:irc_connected', { 35 | server: logger.getIrcServers()[srvkey] 36 | }); 37 | }); 38 | 39 | downloadHandler.on("dlerror",function(data){ 40 | socket.emit('send:dlerror', data); 41 | }); 42 | 43 | downloadHandler.on("dlsuccess",function(data){ 44 | socket.emit('send:dlsuccess', data); 45 | }); 46 | 47 | downloadHandler.on("dlprogress",function(data){ 48 | socket.emit('send:dlprogress', data); 49 | }); 50 | 51 | downloadHandler.on("dlstart",function(data){ 52 | socket.emit('send:dlstart', data); 53 | }); 54 | 55 | }; -------------------------------------------------------------------------------- /public/js/services.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Services */ 12 | 13 | 14 | // Demonstrate how to register services 15 | // In this case it is a simple value service. 16 | angular.module('myApp.services', []). 17 | factory('socket', function ($rootScope) { 18 | var socket = io.connect(); 19 | return { 20 | on: function (eventName, callback) { 21 | socket.on(eventName, function () { 22 | var args = arguments; 23 | $rootScope.$apply(function () { 24 | callback.apply(socket, args); 25 | }); 26 | }); 27 | }, 28 | off: function (eventName, callback) { 29 | socket.removeListener(eventName, callback); 30 | }, 31 | emit: function (eventName, data, callback) { 32 | socket.emit(eventName, data, function () { 33 | var args = arguments; 34 | $rootScope.$apply(function () { 35 | if (callback) { 36 | callback.apply(socket, args); 37 | } 38 | }); 39 | }); 40 | } 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /views/partials/downloads/column.jade: -------------------------------------------------------------------------------- 1 | tr(ng-repeat="(key, pack) in dlList", ng-class="(pack.queuePos == 0) ? 'dlactive' : 'dlqueued'") 2 | td 3 | span(tooltip="Server: {{pack.server}}, Pack#: {{pack.nr}}") {{pack.nick}} 4 | td 5 | span.queuepos {{pack.queuePos}} 6 | td 7 | .row 8 | .col-xs-12 9 | span(tooltip-placement="bottom", tooltip="{{pack.filename}}") {{pack.filename|truncate:100}} 10 | .row 11 | .col-xs-12 12 | .progress(ng-class="{collapse:!(pack.realsize > 0)}", tooltip="{{pack.progress}}% ~ {{pack.received|bytes}} of {{pack.realsize|bytes}}") 13 | .progress-bar(ng-class="{'progress-bar-success':(pack.speed > 0)}", role='progressbar', aria-valuenow='{{pack.progress}}', aria-valuemin='0', aria-valuemax='100', style='width: {{pack.progress}}%;') 14 | td 15 | span(ng-class="{collapse:!(pack.received > 0)}").dlreceived {{pack.received|bytes}}/
16 | span(ng-class="{collapse:(pack.realsize > 0)}") {{pack.filesize|bytes}} 17 | span(ng-class="{collapse:!(pack.realsize > 0)}") {{pack.realsize|bytes}} 18 | td 19 | span(tooltip="ETA: {{pack.eta|timeago}}", ng-class="{collapse:!(pack.speed)}").dlspeed {{pack.speed|bytes}}/s 20 | .btn-group.queueupdown 21 | button(type="button",ng-click="downqueue(pack)", ng-class="{disabled:fistqueue(pack,key)}").btn.btn-default.btn-xs 22 | span.glyphicon.glyphicon-arrow-up 23 | button(type="button",ng-click="upqueue(pack)", ng-class="{disabled:lastqueue(pack,key)}").btn.btn-default.btn-xs 24 | span.glyphicon.glyphicon-arrow-down 25 | td 26 | button(type="button",ng-click="cancelDownload(pack)").btn.btn-default.btn-xs 27 | span.glyphicon.glyphicon-stop -------------------------------------------------------------------------------- /public/js/controllers/searchbar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Searchbarcontroller handels the searches */ 12 | angular.module('myApp') 13 | .controller('SearchBarCtrl', ['$scope', '$rootScope', '$http', '$location', '$timeout', function($scope, $rootScope, $http, $location, $timeout){ 14 | $scope.history = []; 15 | $scope.packetCount = 0; 16 | var rescuedSearch = ""; 17 | 18 | 19 | $http({method: 'GET', url: '/api/packet/'}).then(function (response){ 20 | $scope.packetCount = response.data; 21 | }); 22 | 23 | $scope.setSearch = function (){ 24 | if ($scope.history.indexOf($scope.searchString.toLowerCase()) !== -1){ 25 | $scope.history.splice($scope.history.indexOf($scope.searchString.toLowerCase()), 1); 26 | } 27 | 28 | $scope.history.push($scope.searchString.toLowerCase()); 29 | $rootScope.searchString = angular.copy($scope.searchString); 30 | $scope.$emit("setSearch"); 31 | $location.path("packets"); 32 | }; 33 | 34 | $scope.rescueSearch = function(){ 35 | rescuedSearch = $scope.searchString; 36 | $scope.searchString = ""; 37 | }; 38 | 39 | $scope.restoreSearch = function(){ 40 | $scope.searchString = rescuedSearch; 41 | $timeout(function(){ 42 | $(".topsearch").select(); 43 | },0); 44 | }; 45 | 46 | $scope.selectHistory = function (item){ 47 | $scope.searchString = item; 48 | $rootScope.searchString = angular.copy($scope.searchString); 49 | $rootScope.$emit("setSearch"); 50 | }; 51 | 52 | }]); -------------------------------------------------------------------------------- /public/js/controllers/notificationcount.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Notification controller */ 12 | angular.module('myApp') 13 | .controller('NotificationCountCtrl', ['$scope', '$http', 'socket', '$rootScope', function ($scope, $http, socket, $rootScope){ 14 | $scope.notificationCount = { 15 | dlstart: 0, 16 | dlsuccess: 0, 17 | dlerror: 0 18 | }; 19 | 20 | $rootScope.$on('notificationsClear',function(){ 21 | $scope.notificationCount = { 22 | dlstart: 0, 23 | dlsuccess: 0, 24 | dlerror: 0 25 | }; 26 | }); 27 | 28 | socket.on('send:dlstart',function(data){ 29 | $scope.notificationCount.dlstart++; 30 | }); 31 | 32 | socket.on('send:dlerror',function(data){ 33 | $scope.notificationCount.dlerror++; 34 | }); 35 | 36 | socket.on('send:dlsuccess',function(data){ 37 | $scope.notificationCount.dlsuccess++; 38 | }); 39 | 40 | $scope.$on('$destroy', function () { 41 | socket.off('send:dlerror'); 42 | socket.off('send:dlstart'); 43 | socket.off('send:dlsuccess'); 44 | }); 45 | 46 | $scope.getData = function (){ 47 | $http.get('/api/downloads/notifications/count/').then(function (response){ 48 | $scope.notificationCount = response.data; 49 | }); 50 | }; 51 | 52 | $scope.clearNotifications = function(){ 53 | $http.delete('/api/downloads/notifications/count/').then(function (response){ 54 | $scope.notificationCount = { 55 | dlstart: 0, 56 | dlsuccess: 0, 57 | dlerror: 0 58 | }; 59 | }); 60 | }; 61 | 62 | $scope.getData(); 63 | 64 | 65 | }]); 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Slingxdcc 2 | =================== 3 | Slingxdcc is an XDCC download manager completely written in javascript. 4 | 5 | requires [Node.js](http://nodejs.org) 6 | 7 | Tested on Windows and Linux 8 | 9 | Installation via source 10 | ------------ 11 | 12 | Grab the source or clone the repo, 13 | then in terminal: 14 | 15 | $ npm install 16 | $ node slingxdcc 17 | 18 | Installation via npm 19 | ------------ 20 | 21 | # npm install -g slingxdcc 22 | 23 | Make sure you config your downloads folder! 24 | Then start via: 25 | 26 | $ slingxdcc 27 | 28 | 29 | Point your browser to localhost:3000 30 | 31 | Enable https 32 | ------------ 33 | 34 | Follow this guide till Step 4: [How to create a self-signed SSL Certificate](http://www.akadia.com/services/ssh_test_certificate.html) 35 | 36 | Copy server.key and server.crt into ssl directory and make sure ssl is activated in config/settings.json 37 | 38 | settings.json 39 | ------------ 40 | 41 | The settings.json is located at $HOME/.slingxdcc/config/settings.json 42 | 43 | { 44 | "webserver": { 45 | "port": 3000 // Webserver port 46 | "ssl": true, // Use https 47 | "ssl.crt": "ssl/server.crt", // Path to ssl.crt 48 | "ssl.key": "ssl/server.key" // Path to ssl.key 49 | }, 50 | "logger": { 51 | "packRegex": "#(\\d+)\\s+(\\d+)x\\s+\\[\\s*[><]?([0-9\\.]+)([TGMKtga 52 | k]?)\\]\\s+(.*)", // Regex for pack information 53 | "packdb": "packets.db" // Path to packdb file 54 | "autocleandb": true, // Clean redundant entries from packdb 55 | "cleandb_Xminutes": 60 // Clean every X minutes 56 | "redundantPercentage": 2, // If there are more then 25% redundant 57 | "servers": {} // Servers and channels, can be edited via GUI 58 | }, 59 | "downloadHandler": { 60 | "destination": "downloads/", // Downloads folder 61 | "resumeDownloads": true, // Resume or overwrite downloads 62 | "refreshInterval": 1 // Interval in seconds progress update is displayed 63 | }, 64 | "packetList": { 65 | "sortBy": "lastseen" // Sort search by, can be edited via GUI 66 | "sortOrder": "desc", // Sort order, can be edited via GUI 67 | "filterDiscon": true // Filter offline downloads 68 | "pageItemLimit": 20 // Items per page 69 | }, 70 | "downloads": {} 71 | } 72 | -------------------------------------------------------------------------------- /views/partials/settings/editserver.jade: -------------------------------------------------------------------------------- 1 | uib-tab(ng-repeat="serv in servers") 2 | uib-tab-heading 3 | span(ng-class="servers[serv.key].connected ? 'label-success' : 'label-danger'").label {{serv.key}} 4 | .row(ng-controller="ServerSettingsCtrl", ng-init="init(serv.key)") 5 | .col-md-3 6 | h4 Server settings 7 | hr 8 | form.form-horizontal(role="{server.key}settings",ng-submit="editServer()") 9 | ul.list-group 10 | li.list-group-item 11 | .form-group 12 | label(for="host").col-md-2.control-label Host: 13 | .col-md-10 14 | input(type="text",id="host" ng-model="server.host", required).form-control 15 | .form-group 16 | label(for="port").col-md-2.control-label Port: 17 | .col-md-10 18 | input(type="text",id="port" ng-model="server.port", required).form-control 19 | .form-group 20 | label(for="nick").col-md-2.control-label Nick: 21 | .col-md-10 22 | input(type="text",id="nick" ng-model="server.nick", required).form-control 23 | li.list-group-item 24 | button.btn.btn-default(type="submit").pull-right 25 | span Save   26 | span.glyphicon.glyphicon-ok 27 | button.btn.btn-danger(type="delete" ng-click="removeServer()") 28 | span Delete   29 | span.glyphicon.glyphicon-remove 30 | 31 | .col-md-4 32 | h4 Channels 33 | hr 34 | ul.list-group 35 | li(ng-repeat="channel in server.channels").list-group-item 36 | include channelitem 37 | li.list-group-item 38 | include addchannel 39 | 40 | 41 | 42 | .col-md-5 43 | h4 Errors 44 | hr 45 | .alert.alert-success(ng-class="{hide:hasErrors()}") No errors recorded 46 | .alert.alert-danger(ng-repeat="error in server.error | reverse") 47 | .row 48 | .col-xs-9 49 | strong {{error.message.args[2]}}:  50 | span {{error.message.args[1]}} 51 | .col-xs-3 52 | span.pull-right {{error.errtime | timeago}} -------------------------------------------------------------------------------- /public/js/controllers/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Settingscontroller parent controller for settings */ 12 | 13 | angular.module('myApp') 14 | .controller('SettingsCtrl', ['$scope', '$http', 'socket', function ($scope, $http, socket){ 15 | $scope.servers = {}; 16 | $scope.packetCount = {}; 17 | $scope.compacting = {}; 18 | 19 | $scope.filter = {}; 20 | 21 | socket.on('send:irc_connected', function(data){ 22 | if(typeof $scope.servers[data.server.key] == 'undefined'){ 23 | getServerData(); 24 | }else{ 25 | $scope.servers[data.server.key].connected = true; 26 | } 27 | }); 28 | 29 | socket.on('send:irc_error', function(data){ 30 | if(typeof $scope.servers[data.server.key] == 'undefined'){ 31 | getServerData(); 32 | }else{ 33 | $scope.servers[data.server.key].error = data.server.error; 34 | } 35 | }); 36 | 37 | $scope.$on('$destroy', function () { 38 | socket.off('send:irc_connected'); 39 | socket.off('send:irc_error'); 40 | }); 41 | 42 | $scope.getData = function (){ 43 | getServerData(); 44 | getDbData(); 45 | }; 46 | 47 | function getServerData(){ 48 | $http.get('/api/server/').then(function (response){ 49 | for (var i in response.data){ 50 | response.data[i].key = i; 51 | } 52 | $scope.servers = response.data; 53 | }); 54 | } 55 | 56 | function getDbData(){ 57 | $http.get('/api/db/compacting/').then(function (response){ 58 | angular.extend($scope.compacting, response.data); 59 | if(response.data.autoCompacting){ 60 | $('.dbsettings .compactingsettings input').prop('disabled', true); 61 | } 62 | }); 63 | $http.get('/api/db/compactingfilter/').then(function (response){ 64 | $scope.filter.compactingfilter = response.data.filter; 65 | $scope.filter.tmpfilter = response.data.filter ? response.data.filter : 24; 66 | $scope.filter.autoDeleting = (response.data.filter ? true : false); 67 | }); 68 | $http.get('/api/packet/').then(function (response) { 69 | angular.extend($scope.packetCount,response.data); 70 | $scope.redPercentage = $scope.packetCount.redPackets / ($scope.packetCount.absPackets + $scope.packetCount.redPackets) * 100; 71 | }); 72 | } 73 | 74 | $scope.getData(); 75 | 76 | $('.collapse').collapse({ 77 | toggle: false 78 | }) 79 | 80 | }]); 81 | 82 | //SettingsCtrl.$inject = ['$scope', '$http', 'socket']; -------------------------------------------------------------------------------- /public/js/controllers/dbsettings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Database settings controller */ 12 | 13 | 14 | angular.module('myApp') 15 | .controller('DbSettingsCtrl', ['$scope', '$http', 'socket', function ($scope, $http, socket){ 16 | socket.on('send:packetCount', function(data){ 17 | angular.extend($scope.packetCount, data); 18 | $scope.redPercentage = $scope.packetCount.redPackets / ($scope.packetCount.absPackets + $scope.packetCount.redPackets) * 100; 19 | }); 20 | 21 | $scope.$on('$destroy', function () { 22 | socket.off('send:packetCount'); 23 | }); 24 | 25 | 26 | $scope.compactDb = function(){ 27 | $http.put('/api/db/compacting/'); 28 | $scope.redPercentage = 0; 29 | $scope.packetCount.redPackets = 0; 30 | }; 31 | 32 | $scope.setFilter = function(){ 33 | var hours = isNaN(parseInt($scope.filter.tmpfilter)) ? 24 : parseInt($scope.filter.tmpfilter); 34 | $http.put('/api/db/compactingfilter/',{filter: hours}).then(function (response){ 35 | $scope.filter.compactingfilter = hours; 36 | $scope.filter.autoDeleting = true; 37 | }); 38 | }; 39 | 40 | $scope.toggleAutoDeleting = function(){ 41 | if($scope.filter.autoDeleting){ 42 | $http.put('/api/db/compactingfilter/',{filter: false}); 43 | }else{ 44 | $scope.setFilter(); 45 | } 46 | }; 47 | 48 | $scope.toggleCompacting = function(){ 49 | if($scope.compacting.autoCompacting){ 50 | $http.delete('/api/db/compacting/').then(function (response){ 51 | angular.extend($scope.compacting, response.data); 52 | $('.dbsettings .compactingsettings input').prop('disabled', false); 53 | }); 54 | }else{ 55 | var interval = isNaN(parseInt($scope.compacting.interval)) ? 60 : parseInt($scope.compacting.interval); 56 | if(interval < 10) 57 | interval = 10; 58 | if(interval > 60 * 24) 59 | interval = 60 * 24; 60 | 61 | var percentage = isNaN(parseInt($scope.compacting.redPercentage)) ? 25 : parseInt($scope.compacting.redPercentage); 62 | if(percentage < 0) 63 | percentage = 0; 64 | if(percentage > 500) 65 | percentage = 500; 66 | 67 | $http.post('/api/db/compacting/',{minutes: interval, percentage: percentage}).then(function (response){ 68 | angular.extend($scope.compacting, response.data); 69 | $('.dbsettings .compactingsettings input').prop('disabled', true); 70 | }); 71 | } 72 | }; 73 | 74 | $('#toggle').button() 75 | }]); 76 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block body 4 | .navbar.navbar-inverse.navbar-static-top(role="navigation") 5 | .navbar-header 6 | button.navbar-toggle(type="button", data-toggle="collapse", data-target=".navbar-ex1-collapse") 7 | span.sr-only Toggle navigation 8 | span.icon-bar 9 | span.icon-bar 10 | span.icon-bar 11 | a.navbar-brand(href="/") Slingxdcc 12 | .navcount(ng-controller='NotificationCountCtrl') 13 | span(tooltip="Downloads started", tooltip-placement="bottom").badge.badge-info {{notificationCount.dlstart}} 14 | span   15 | span(tooltip="Downloads finished", tooltip-placement="bottom").badge.badge-success {{notificationCount.dlsuccess}} 16 | span   17 | span(tooltip="Download errors", tooltip-placement="bottom").badge.badge-danger {{notificationCount.dlerror}} 18 | 19 | 20 | 21 | .collapse.navbar-collapse.navbar-ex1-collapse 22 | ul.nav.navbar-nav 23 | li 24 | a(href="packets") Packets 25 | li 26 | a(href="downloads") Downloads 27 | li 28 | a(href="settings") Settings 29 | 30 | form.navbar-form.navbar-right(ng-controller='SearchBarCtrl', role="search", ng-submit="setSearch()") 31 | .right-inner-addon 32 | span.glyphicon.glyphicon-search 33 | input.topsearch.form-control(type="text", placeholder="Search", ng-model="searchString", typeahead="item for item in history | limitTo:8 | filter:$viewValue", ng-blur="rescueSearch()", ng-focus="restoreSearch()") 34 | span(ng-controller='PacketcountCtrl').navbar-text.pull-right Packets # {{packetCount.absPackets}} 35 | 36 | .container 37 | div(ng-view) 38 | 39 | script(src='socket.io/socket.io.js') 40 | script(src='bower_components/jquery/dist/jquery.min.js') 41 | script(src='bower_components/bootstrap/dist/js/bootstrap.min.js') 42 | script(src='bower_components/angular/angular.min.js') 43 | script(src='bower_components/angular-route/angular-route.min.js') 44 | script(src='bower_components/angular-animate/angular-animate.min.js') 45 | script(src='bower_components/angular-bootstrap/ui-bootstrap-tpls.js') 46 | script(src='bower_components/chart.js/dist/Chart.bundle.js') 47 | script(src='bower_components/angular-chart.js/dist/angular-chart.min.js') 48 | script(src='js/app.js') 49 | script(src='js/services.js') 50 | //script(src='js/lib/angles.js') 51 | script(src='js/controllers/dashboard.js') 52 | script(src='js/controllers/packetlist.js') 53 | script(src='js/controllers/searchbar.js') 54 | script(src='js/controllers/serveradd.js') 55 | script(src='js/controllers/serversettings.js') 56 | script(src='js/controllers/dbsettings.js') 57 | script(src='js/controllers/settings.js') 58 | script(src='js/controllers/downloads.js') 59 | script(src='js/controllers/packetcount.js') 60 | script(src='js/controllers/notificationcount.js') 61 | script(src='js/filters.js') 62 | script(src='js/directives.js') 63 | script(src='js/user.js') -------------------------------------------------------------------------------- /views/partials/settings/addserver.jade: -------------------------------------------------------------------------------- 1 | uib-tab.pull-right 2 | uib-tab-heading   3 | span.glyphicon.glyphicon-plus 4 | .row(ng-controller="ServerAddCtrl") 5 | .col-md-6 6 | h4 Add new server 7 | hr 8 | form.form-horizontal(role="addServer") 9 | ul.list-group 10 | li.list-group-item 11 | .form-group(ng-class="{'has-warning':!isKeyUniqe()}") 12 | label(for="host").col-md-2.control-label Name: 13 | .col-md-10 14 | input(type="text",id="host" ng-model="nServConf.key", placeholder="Server name must be unique", required).form-control 15 | .form-group 16 | label(for="host").col-md-2.control-label Host: 17 | .col-md-10 18 | input(type="text",id="host" ng-model="nServConf.host", placeholder="Hostname or ip adress", required).form-control 19 | .form-group 20 | label(for="port").col-md-2.control-label Port: 21 | .col-md-10 22 | input(type="text",id="port" ng-model="nServConf.port", placeholder="Port", required).form-control 23 | .form-group 24 | label(for="nick").col-md-2.control-label Nick: 25 | .col-md-10 26 | input(type="text",id="nick" ng-model="nServConf.nick", placeholder="Nickname", required).form-control 27 | .alert.alert-warning.pull-left(ng-class="{hide:isKeyUniqe()}") Non unique key will overwrite existing sever 28 | li.list-group-item 29 | button.btn.btn-default(type="submit" ng-click="addServer()") 30 | span Save   31 | span.glyphicon.glyphicon-ok 32 | 33 | .col-md-6 34 | h4 Channels 35 | hr 36 | ul.list-group 37 | li(ng-repeat="channel in nServConf.channels").list-group-item 38 | .row 39 | span.col-xs-8 {{channel}} 40 | span(tooltip-placement="left", tooltip="{{isObserved(channel)|observTooltip}}").col-xs-2 41 | span(ng-class="{false:'glyphicon-eye-close text-danger', true:'glyphicon-eye-open text-success'}[isObserved(channel)]",ng-click="toggleObserv(channel)").glyphicon 42 | span(tooltip-placement="left", tooltip="Part {{channel}}").col-xs-2 43 | span(ng-click="partChannel(channel)").text-danger.glyphicon.glyphicon-ban-circle 44 | li.list-group-item 45 | form(role="joinChannel", ng-submit="joinChannels()").input-group 46 | input.form-control(placeholder="Add channel", type="text", ng-model="joinChanStr") 47 | span.input-group-btn 48 | button.btn.btn-default(type="submit", tooltip="Add multiple channels separated by space", tooltip-trigger="mouseenter", tooltip-placement="left") 49 | span.glyphicon.glyphicon-plus -------------------------------------------------------------------------------- /views/partials/dashboard/dlnotification.jade: -------------------------------------------------------------------------------- 1 | .dashnoti(ng-controller="NotificationCtrl") 2 | button(type="button",ng-click="clearNotifications()").pull-right.btn.btn-xs.btn-default clear 3 | h3 Download notifications 4 | hr 5 | ul.list-group 6 | li(ng-repeat='notification in notifications | reverse', ng-switch="notification.type").list-group-item 7 | div(ng-switch-when="dlsuccess") 8 | .col-lg-1 9 | .row 10 | span(tooltip-append-to-body="true", tooltip="Server:{{notification.packObj.server}}, Bot:{{notification.packObj.nick}}, Pack:{{notification.packObj.nr}}", tooltip-placement="right").badge.badge-success 11 | span.glyphicon.glyphicon-thumbs-up 12 | .col-lg-8 13 | .row 14 | span(tooltip-append-to-body="true", tooltip-placement="bottom", tooltip="{{notification.packObj.filename}}") {{notification.packObj.filename | truncate:40}} 15 | .row 16 | span Download Finished 17 | .col-lg-3 18 | .row 19 | span(tooltip-append-to-body="true", tooltip="{{notification.time | date:'medium'}}", tooltip-placement="left").pull-right {{notification.time | timeago}} 20 | div(ng-switch-when="dlstart") 21 | .col-lg-1 22 | .row 23 | span(tooltip-append-to-body="true", tooltip="Server:{{notification.packObj.server}}, Bot:{{notification.packObj.nick}}, Pack:{{notification.packObj.nr}}", tooltip-placement="right").badge.badge-info 24 | span.glyphicon.glyphicon-play 25 | .col-lg-8 26 | .row 27 | span(tooltip-append-to-body="true", tooltip-placement="bottom", tooltip="{{notification.packObj.filename}}") {{notification.packObj.filename | truncate:40}} 28 | .row 29 | span Download Started 30 | .col-lg-3 31 | .row 32 | span(tooltip-append-to-body="true", tooltip="{{notification.time | date:'medium'}}", tooltip-placement="left").pull-right {{notification.time | timeago}} 33 | div(ng-switch-default) 34 | .col-lg-1 35 | .row 36 | span(tooltip-append-to-body="true", tooltip="Server:{{notification.packObj.server}}, Bot:{{notification.packObj.nick}}, Pack:{{notification.packObj.nr}}", tooltip-placement="right").badge.badge-danger 37 | span.glyphicon.glyphicon-thumbs-down 38 | .col-lg-8 39 | .row 40 | span(tooltip-append-to-body="true", tooltip-placement="bottom", tooltip="{{notification.packObj.filename}}") {{notification.packObj.filename | truncate:40}} 41 | .row 42 | span(tooltip-append-to-body="true", tooltip="{{getError(notification)}}") {{getError(notification) | truncate:50}} 43 | .col-lg-3 44 | .row 45 | span(tooltip-append-to-body="true", tooltip="{{notification.time | date:'medium'}}", tooltip-placement="left").pull-right {{notification.time | timeago}} 46 | .row 47 | button.pull-right.btn-default.btn.btn-xs(tooltip-append-to-body="true", tooltip="Search packet", tooltip-placement="left" ,type="button", ng-click="searchPacket(notification.packObj.filename)") 48 | span.glyphicon.glyphicon-search 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/js/controllers/serveradd.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Serveraddcontroller for creating a new server in settings */ 12 | angular.module('myApp') 13 | .controller('ServerAddCtrl', ['$scope', '$http', function ($scope, $http){ 14 | $scope.joinChanStr = ""; 15 | $scope.nServConf = { 16 | key : "", 17 | host : "", 18 | port : "", 19 | nick : "", 20 | channels : [], 21 | observchannels: [] 22 | }; 23 | 24 | $scope.addServer = function (){ 25 | if ($scope.nServConf.key.length === 0 || $scope.nServConf.host.length === 0 || parseInt($scope.nServConf.port) > 65535 || parseInt($scope.nServConf.port) < 0 || $scope.nServConf.nick.length === 0){ 26 | return; 27 | } 28 | 29 | var server = { 30 | srvkey : $scope.nServConf.key, 31 | host : $scope.nServConf.host, 32 | port : $scope.nServConf.port, 33 | nick : $scope.nServConf.nick, 34 | channels : $scope.nServConf.channels.length > 0 ? $scope.nServConf.channels.join(' ') : "", 35 | observchannels: $scope.nServConf.observchannels.length > 0 ? $scope.nServConf.observchannels.join(' ') : "" 36 | }; 37 | 38 | $http.post('/api/server/', server).then(function (data){ 39 | $scope.servers[$scope.nServConf.key] = {}; 40 | $scope.nServConf.connected = false; 41 | angular.copy($scope.nServConf, $scope.servers[$scope.nServConf.key]); 42 | $scope.joinChanStr = ""; 43 | $scope.nServConf = { 44 | key : "", 45 | host : "", 46 | port : "", 47 | nick : "", 48 | channels : [], 49 | observchannels: [] 50 | }; 51 | 52 | $scope.getData(); 53 | }); 54 | }; 55 | 56 | $scope.joinChannels = function (){ 57 | if ($scope.joinChanStr.length > 0){ 58 | $scope.nServConf.channels = $scope.nServConf.channels.concat($scope.joinChanStr.split(" ")); 59 | $scope.joinChanStr = ""; 60 | } 61 | }; 62 | 63 | $scope.partChannel = function (channel){ 64 | $scope.nServConf.channels.splice($scope.nServConf.channels.indexOf(channel), 1); 65 | }; 66 | 67 | $scope.toggleObserv = function (channel){ 68 | if ($scope.isObserved(channel)){ 69 | $scope.nServConf.observchannels.splice($scope.nServConf.observchannels.indexOf(channel), 1); 70 | }else{ 71 | $scope.nServConf.observchannels.push(channel); 72 | } 73 | }; 74 | 75 | $scope.isObserved = function (channel){ 76 | if ($scope.nServConf.observchannels.indexOf(channel) !== -1){ 77 | return true; 78 | }else{ 79 | return false; 80 | } 81 | }; 82 | 83 | $scope.isKeyUniqe = function (){ 84 | if(typeof $scope.nServConf.key !== "undefined" && $scope.nServConf.key.length > 0){ 85 | return (typeof $scope.servers[$scope.nServConf.key] === "undefined"); 86 | } 87 | return true; 88 | }; 89 | }]); -------------------------------------------------------------------------------- /public/js/controllers/serversettings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* ServerSettingsCtrl for editing a server in settings */ 12 | angular.module('myApp') 13 | .controller('ServerSettingsCtrl', ['$scope', '$http', 'socket', function ($scope, $http, socket){ 14 | $scope.joinChanStr = ""; 15 | $scope.init = function (key){ 16 | $scope.server = $scope.servers[key]; 17 | }; 18 | 19 | $scope.$on('$destroy', function () { 20 | socket.off('send:irc_error'); 21 | }); 22 | 23 | $scope.editServer = function (){ 24 | if ($scope.server.host.length === 0 || parseInt($scope.server.port) > 65535 || parseInt($scope.server.port) < 0 || $scope.server.nick.length === 0) { 25 | return; 26 | } 27 | 28 | var server = { 29 | srvkey : $scope.server.key, 30 | host : $scope.server.host, 31 | port : $scope.server.port, 32 | nick : $scope.server.nick, 33 | channels : $scope.server.channels.length > 0 ? $scope.server.channels.join(' ') : "", 34 | observchannels: $scope.server.observchannels.length > 0 ? $scope.server.observchannels.join(' ') : "" 35 | }; 36 | $http.post('/api/server/', server).then(function (response){ 37 | $scope.server.connected = false; 38 | angular.copy($scope.server,$scope.servers[server.key]); 39 | $scope.joinChanStr = ""; 40 | $scope.getData(); 41 | }); 42 | }; 43 | 44 | $scope.removeServer = function (){ 45 | $http.delete('/api/server/' + $scope.server.key).then(function (response){ 46 | delete $scope.servers[$scope.server.key]; 47 | }); 48 | }; 49 | 50 | $scope.joinChannels = function (){ 51 | if ($scope.joinChanStr.length > 0){ 52 | $http.put('/api/channel/', {srvkey: $scope.server.key, channels: $scope.joinChanStr, type: "join"}).then(function (response){ 53 | $scope.server.channels = $scope.server.channels.concat($scope.joinChanStr.split(" ")); 54 | $scope.joinChanStr = ""; 55 | }); 56 | } 57 | }; 58 | 59 | $scope.partChannel = function (channel){ 60 | $http.put('/api/channel/', {srvkey: $scope.server.key, channels: channel, type: "part"}).then(function (response){ 61 | $scope.server.channels.splice($scope.server.channels.indexOf(channel), 1); 62 | if ($scope.isObserved(channel)){ 63 | $scope.server.observchannels.splice($scope.server.observchannels.indexOf(channel), 1); 64 | } 65 | }); 66 | }; 67 | 68 | $scope.toggleObserv = function (channel){ 69 | if ($scope.isObserved(channel)){ 70 | $http.put('/api/channel/', {srvkey: $scope.server.key, channels: channel, type: "unobserv"}).then(function (response){ 71 | $scope.server.observchannels.splice($scope.server.observchannels.indexOf(channel), 1); 72 | }); 73 | }else{ 74 | $http.put('/api/channel/', {srvkey: $scope.server.key, channels: channel, type: "observ"}).then(function (response){ 75 | $scope.server.observchannels.push(channel); 76 | }); 77 | } 78 | }; 79 | 80 | $scope.hasErrors = function (){ 81 | return ($scope.server.error.length > 0); 82 | }; 83 | 84 | $scope.isObserved = function (channel){ 85 | return ($scope.server.observchannels.indexOf(channel) !== -1); 86 | }; 87 | 88 | }]); 89 | 90 | //ServerSettingsCtrl.$inject = ['$scope', '$http', 'socket']; -------------------------------------------------------------------------------- /views/partials/settings/database.jade: -------------------------------------------------------------------------------- 1 | .dbsettings(ng-controller="DbSettingsCtrl") 2 | .row 3 | .col-sm-4 4 | h4 Next compacting 5 | button(tooltip="Compact database now", tooltip-placement="left", type="button",ng-click="compactDb()").btn.btn-xs.btn-success.pull-right 6 | span.glyphicon.glyphicon-compressed 7 | hr 8 | span(ng-class="{collapse:!(compacting.autoCompacting)}") 9 | | Next database compacting at  10 | strong {{compacting.nextCompacting | date:'dd.MM HH:mm'}} 11 | |  if more than  12 | strong {{compacting.redPercentage}} % 13 | |  of the packets are redundant. 14 | span(ng-class="{collapse:!(filter.autoDeleting)}") Packets older than 15 | strong  {{filter.compactingfilter}} h 16 | |  will be deleted while compacting! 17 | p(ng-class="{collapse:(compacting.autoCompacting)}") No database compacting scheduled 18 | 19 | .col-sm-4 20 | h4 Compacting schedule 21 | button(ng-attr-tooltip="{{compacting.autoCompacting && 'Auto compacting active' || 'Auto compacting inactive' }}", tooltip-placement="left" ,type="button", ng-model="compacting.autoCompacting", btn-checkbox, btn-checkbox-true="true", btn-checkbox-false="false", ng-class="compacting.autoCompacting ? 'btn-info' : 'btn-default'", ng-click="toggleCompacting()").btn.btn-xs.pull-right 22 | span.glyphicon.glyphicon-repeat 23 | div.compactingsettings(ng-class="{disabled:compacting.autoCompacting}") 24 | hr 25 | 26 | form.form-horizontal 27 | .form-group 28 | label(for="mins").col-md-7.control-label Compact interval: 29 | .col-md-5 30 | .input-group 31 | input(type="text",id="mins" ng-model="compacting.interval").form-control.input-sm 32 | span.input-group-addon min 33 | .form-group 34 | label(for="percent").col-md-7.control-label Minimum redundance: 35 | .col-md-5 36 | .input-group 37 | input(type="text",id="percent" ng-model="compacting.redPercentage").form-control.input-sm 38 | span.input-group-addon % 39 | 40 | .col-sm-4 41 | h4 Delete old packets on compacting 42 | button(ng-attr-tooltip="{{filter.autoDeleting && 'Deleting old packets' || 'Keeping old packets' }}", tooltip-placement="left" ,type="button", ng-model="filter.autoDeleting", btn-checkbox, btn-checkbox-true="true", btn-checkbox-false="false", ng-class="filter.autoDeleting ? 'btn-info' : 'btn-default'", ng-click="toggleAutoDeleting()").btn.btn-xs.pull-right 43 | span(ng-class="filter.autoDeleting ? 'glyphicon-ok-circle' : 'glyphicon-ban-circle'").glyphicon 44 | hr 45 | form(ng-submit="setFilter()").autodeletesettings.form-horizontal 46 | .form-group 47 | label(for="hours").col-md-7.control-label Maximal package age: 48 | .col-md-5 49 | .input-group 50 | input(type="text",id="hours" ng-model="filter.tmpfilter").form-control.input-sm 51 | span.input-group-addon h 52 | .form-group 53 | .col-md-offset-7.col-md-5 54 | button(type="submit" id="agesubmit").btn.btn-default.btn-sm.pull-right 55 | span Save  56 | span.glyphicon.glyphicon-ok 57 | .row 58 | .col-sm-4 59 | h4 Package statistics 60 | hr 61 | p 62 | span.label.label-default {{packetCount.redPackets}} 63 | span   redundant Packets 64 | p 65 | span.label.label-success {{packetCount.absPackets}} 66 | span   effective Packets 67 | p 68 | span.label(ng-class="{'label-success':(redPercentage <= compacting.redPercentage), 'label-warning':(redPercentage > compacting.redPercentage && redPercentage <=compacting.redPercentage*2), 'label-danger':(redPercentage > compacting.redPercentage*2)}") {{redPercentage | number:1}} % 69 | span   redundant 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/js/filters.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Filters */ 12 | 13 | angular.module('myApp.filters', []).filter('interpolate', ['version', function (version){ 14 | return function (text){ 15 | return String(text).replace(/\%VERSION\%/mg, version); 16 | } 17 | }]).filter('truncate',function (){ 18 | return function (text, length, end){ 19 | if (isNaN(length)) 20 | length = 10; 21 | 22 | if (end === undefined) 23 | end = "..."; 24 | 25 | if (text.length <= length || text.length - end.length <= length){ 26 | return text; 27 | }else{ 28 | return String(text).substring(0, length - end.length) + end; 29 | } 30 | 31 | }; 32 | }).filter('reverse',function (){ 33 | return function (items){ 34 | if (typeof items === 'undefined') 35 | return; 36 | return items.slice().reverse(); 37 | }; 38 | }).filter('observTooltip',function (){ 39 | return function (bool){ 40 | if (bool){ 41 | return "Click to un-observe channel"; 42 | }else{ 43 | return "Click to observe channel"; 44 | } 45 | }; 46 | }).filter('timeago', function (){ 47 | return function (input, p_allowFuture){ 48 | var substitute = function (stringOrFunction, number, strings){ 49 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, dateDifference) : stringOrFunction; 50 | var value = (strings.numbers && strings.numbers[number]) || number; 51 | return string.replace(/%d/i, value); 52 | }, nowTime = (new Date()).getTime(), date = (new Date(input)).getTime(), //refreshMillis= 6e4, //A minute 53 | allowFuture = p_allowFuture || false, strings = { 54 | prefixAgo : null, 55 | prefixFromNow: null, 56 | suffixAgo : "", 57 | suffixFromNow: "in", 58 | seconds : "less than a minute", 59 | minute : "about a minute", 60 | minutes : "%d minutes", 61 | hour : "about an hour", 62 | hours : "about %d hours", 63 | day : "a day", 64 | days : "%d days", 65 | month : "about a month", 66 | months : "%d months", 67 | year : "about a year", 68 | years : "%d years" 69 | }, dateDifference = nowTime - date, words, seconds = Math.abs(dateDifference) / 1000, minutes = seconds / 60, hours = minutes / 60, days = hours / 24, years = days / 365, separator = strings.wordSeparator === undefined ? " " : strings.wordSeparator, 70 | 71 | // var strings = this.settings.strings; 72 | prefix = strings.prefixAgo, suffix = strings.suffixAgo; 73 | 74 | if (allowFuture){ 75 | if (dateDifference < 0){ 76 | prefix = strings.prefixFromNow; 77 | suffix = strings.suffixFromNow; 78 | } 79 | } 80 | 81 | words = seconds < 45 && substitute(strings.seconds, Math.round(seconds), strings) || seconds < 90 && substitute(strings.minute, 1, strings) || minutes < 45 && substitute(strings.minutes, Math.round(minutes), strings) || minutes < 90 && substitute(strings.hour, 1, strings) || hours < 24 && substitute(strings.hours, Math.round(hours), strings) || hours < 42 && substitute(strings.day, 1, strings) || days < 30 && substitute(strings.days, Math.round(days), strings) || days < 45 && substitute(strings.month, 1, strings) || days < 365 && substitute(strings.months, Math.round(days / 30), strings) || years < 1.5 && substitute(strings.year, 1, strings) || substitute(strings.years, Math.round(years), strings); 82 | 83 | return $.trim([prefix, words, suffix].join(separator)); 84 | // conditional based on optional argument 85 | // if (somethingElse) { 86 | // out = out.toUpperCase(); 87 | // } 88 | // return out; 89 | } 90 | }) 91 | .filter('bytes', function() { 92 | return function(bytes, precision) { 93 | if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-'; 94 | if (typeof precision === 'undefined') precision = 1; 95 | var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'], 96 | number = Math.floor(Math.log(bytes) / Math.log(1024)); 97 | if(bytes == 0) return 0 + ' ' + units[0]; 98 | return (bytes / Math.pow(1024, number)).toFixed(precision) + ' ' + units[number]; 99 | } 100 | }); -------------------------------------------------------------------------------- /public/js/controllers/packetlist.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Packetlistcontroller handels the packetlisting */ 12 | angular.module('myApp') 13 | .controller('PacketListCtrl', ['$scope', '$rootScope', '$http', '$log', function($scope, $rootScope, $http, $log){ 14 | var loadDone = false; 15 | $scope.searchString = $rootScope.searchString ? $rootScope.searchString : ""; 16 | 17 | var dlList = []; 18 | 19 | function getDownloads(){ 20 | $http.get('/api/downloads/').then(function (response){ 21 | dlList = []; 22 | $scope.dlQueue = response.data.dlQueue; 23 | jQuery.each($scope.dlQueue,function(srvkey,srvcol){ 24 | jQuery.each(srvcol,function(botname,botqueue){ 25 | jQuery.each(botqueue,function(queuePos,pack){ 26 | dlList.push(pack); 27 | }); 28 | }); 29 | }); 30 | }); 31 | } 32 | 33 | $rootScope.$on("setSearch",function(){ 34 | $scope.searchString = $rootScope.searchString; 35 | $scope.setPage(1); 36 | }); 37 | 38 | $scope.setPage = function (pageNo){ 39 | $scope.currentPage = pageNo; 40 | refreshPageScope(); 41 | }; 42 | 43 | $scope.pageChanged = function() { 44 | refreshPageScope(); 45 | }; 46 | 47 | $scope.setPage(1); 48 | refreshSortScope(); 49 | getDownloads(); 50 | 51 | $scope.getOpacity = function(){ 52 | if(loadDone){ 53 | return 1; 54 | }else{ 55 | return 0.5; 56 | } 57 | }; 58 | 59 | $scope.getCurrentPage = function(){ 60 | return $scope.currentPage; 61 | }; 62 | 63 | $scope.setSorting = function (by){ 64 | var value = { 65 | sortBy : $scope.sorting.sortBy, 66 | sortOrder: $scope.sorting.sortOrder 67 | }; 68 | value.sortBy = by; 69 | if (by === $scope.sorting.sortBy){ 70 | if (value.sortOrder !== 'desc'){ 71 | value.sortOrder = 'desc'; 72 | }else{ 73 | value.sortOrder = 'asc'; 74 | } 75 | } 76 | $http.put('/api/packet/sorting/', value).then(function (){ 77 | $scope.sorting = value; 78 | $scope.setPage(1); 79 | }); 80 | }; 81 | 82 | $scope.startDownload = function(packet){ 83 | $http.post('/api/downloads/', {packObj:packet}).then(function (response){ 84 | if(response.data.success){ 85 | dlList.push(packet); 86 | } 87 | }); 88 | }; 89 | 90 | $scope.cancelDownload = function(packet){ 91 | $http.put('/api/downloads/cancel/', {packObj:packet}).then(function (response){ 92 | if(response.data.success){ 93 | dlList = removeArrayItem(dlList,packet); 94 | } 95 | 96 | }); 97 | }; 98 | 99 | function refreshPageScope(){ 100 | loadDone = false; 101 | var url; 102 | if ($scope.searchString.length > 0){ 103 | url = '/api/packet/search/' + $scope.searchString + '/' + $scope.currentPage + '/'; 104 | }else{ 105 | url = '/api/packet/list/' + $scope.currentPage + '/'; 106 | } 107 | $http({method: 'GET', url: url}).then(function (response){ 108 | $scope.numPages = response.data.numPages; 109 | $scope.numPackets = response.data.numPackets; 110 | $scope.packets = response.data.packets; 111 | $scope.pageItemLimit = response.data.pageItemLimit; 112 | loadDone = true; 113 | }); 114 | } 115 | 116 | function refreshSortScope(){ 117 | $http({method: 'GET', url: '/api/packet/sorting/'}).then(function (response){ 118 | $scope.sorting = response.data; 119 | $scope.sorted = function (col){ 120 | if ($scope.sorting.sortBy === col){ 121 | return $scope.sorting.sortOrder; 122 | }else{ 123 | return 'none'; 124 | } 125 | }; 126 | }); 127 | } 128 | 129 | function getDownloadIndex(packet){ 130 | var index = -1; 131 | for (var i=0; i wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 'use strict'; 10 | 11 | /* Downloads controller */ 12 | angular.module('myApp') 13 | .controller('DownloadsCtrl', ['$scope', '$http', 'socket', function ($scope, $http, socket){ 14 | 15 | $scope.dlList = []; 16 | $scope.speedsum = 0; 17 | 18 | socket.on('send:dlstart',function(data){ 19 | for (var i=0; i<$scope.dlList.length; i++) { 20 | if($scope.dlList[i].server === data.packObj.server && $scope.dlList[i].nick === data.packObj.nick && $scope.dlList[i].nr === data.packObj.nr){ 21 | angular.extend($scope.dlList[i], data.packObj); 22 | $scope.dlList[i].queuePos = 0; 23 | break; 24 | } 25 | } 26 | }); 27 | 28 | socket.on('send:dlprogress',function(data){ 29 | for (var i=0; i<$scope.dlList.length; i++) { 30 | if($scope.dlList[i].server === data.packObj.server && $scope.dlList[i].nick === data.packObj.nick && $scope.dlList[i].nr === data.packObj.nr){ 31 | 32 | data.packObj.progress = parseInt(data.packObj.received / $scope.dlList[i].realsize * 1000)/10; 33 | 34 | $scope.speedsum -= $scope.dlList[i].speed; 35 | $scope.speedsum += data.packObj.speed; 36 | 37 | angular.extend($scope.dlList[i], data.packObj); 38 | var secondsleft = parseInt(($scope.dlList[i].realsize - $scope.dlList[i].received) / $scope.dlList[i].speed); 39 | $scope.dlList[i].eta = parseInt(Date.now() + secondsleft*1000); 40 | break; 41 | } 42 | } 43 | }); 44 | 45 | socket.on('send:dlerror',function(data){ 46 | removeArrayItem($scope.dlList, data.packObj); 47 | $scope.speedsum = speedsum(); 48 | }); 49 | 50 | socket.on('send:dlsuccess',function(data){ 51 | removeArrayItem($scope.dlList, data.packObj); 52 | $scope.speedsum = speedsum(); 53 | }); 54 | 55 | $scope.$on('$destroy', function () { 56 | socket.off('send:dlerror'); 57 | socket.off('send:dlstart'); 58 | socket.off('send:dlprogress'); 59 | socket.off('send:dlsuccess'); 60 | }); 61 | 62 | $scope.getData = function(){ 63 | $http.get('/api/downloads/').then(function (response){ 64 | $scope.dlList = queuesToArray(response.data.dlQueue); 65 | $scope.speedsum = speedsum(); 66 | }); 67 | }; 68 | 69 | $scope.cancelDownload = function(packet){ 70 | $http.put('/api/downloads/cancel/', {packObj:packet}).then(function (response){ 71 | if(response.data.success){ 72 | removeArrayItem($scope.dlList, packet); 73 | $scope.speedsum = speedsum(); 74 | } 75 | }); 76 | }; 77 | 78 | $scope.rowClass = function(pack){ 79 | if(pack.queuePos === 0){ 80 | return 'dlactive'; 81 | }else{ 82 | return 'dlqueued'; 83 | } 84 | }; 85 | 86 | 87 | 88 | $scope.upqueue = function(packet){ 89 | $http.put('/api/downloads/upqueue/', {packObj:packet}).then(function (response){ 90 | if(response.data.success){ 91 | $scope.getData(); 92 | } 93 | }); 94 | }; 95 | 96 | $scope.downqueue = function(packet){ 97 | $http.put('/api/downloads/downqueue/', {packObj:packet}).then(function (response){ 98 | if(response.data.success){ 99 | $scope.getData(); 100 | } 101 | }); 102 | }; 103 | 104 | $scope.fistqueue = function(packet, index){ 105 | return (packet.queuePos === 1); 106 | }; 107 | 108 | $scope.lastqueue = function(packet, index){ 109 | return (typeof $scope.dlList[index+1] === "undefined" || !($scope.dlList[index+1].queuePos > packet.queuePos)); 110 | }; 111 | 112 | function queuesToArray(queues){ 113 | var array = []; 114 | jQuery.each(queues,function(srvkey,srvcol){ 115 | jQuery.each(srvcol,function(botname,botqueue){ 116 | jQuery.each(botqueue,function(queuePos,pack){ 117 | pack.queuePos = queuePos; 118 | if(pack.received > 0){ 119 | pack.progress = parseInt(pack.received / pack.realsize * 1000)/10; 120 | } 121 | array.push(pack); 122 | }); 123 | }); 124 | }); 125 | return array; 126 | } 127 | 128 | function getDownloadIndex(packet){ 129 | var index = -1; 130 | for (var i=0; i<$scope.dlList.length; i++) { 131 | if($scope.dlList[i].server === packet.server && $scope.dlList[i].nick === packet.nick && $scope.dlList[i].nr === packet.nr){ 132 | index = i; 133 | break; 134 | } 135 | } 136 | return index; 137 | } 138 | 139 | function removeArrayItem(array, item) { 140 | var id = getDownloadIndex(item); 141 | if (id !== -1) { 142 | array.splice(id, 1); 143 | } 144 | return array; 145 | } 146 | 147 | function speedsum() { 148 | var speedsum = 0; 149 | for (var i = 0; i < $scope.dlList.length; i++) { 150 | if ($scope.dlList[i].speed > 0) { 151 | speedsum += $scope.dlList[i].speed; 152 | }else{ 153 | $scope.dlList[i].speed = 0; 154 | } 155 | } 156 | return speedsum; 157 | } 158 | 159 | $scope.getData(); 160 | }]); 161 | -------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | /* app css stylesheet */ 10 | 11 | table thead tr th span.glyphicon { 12 | width: 14px; 13 | padding-left: 5px; 14 | } 15 | 16 | .topsearch { 17 | width: 85px; 18 | color: #ffffff; 19 | background-color: #626262; 20 | border-color: #151515; 21 | transition: all 0.5s ease; 22 | -webkit-transition: all 0.5s ease; 23 | } 24 | 25 | .topsearch:focus { 26 | width: 210px; 27 | color: #555555; 28 | background-color: #ffffff; 29 | border-color: #cccccc; 30 | border-radius: 15px 31 | } 32 | 33 | .left-inner-addon { 34 | position: relative; 35 | } 36 | .left-inner-addon input { 37 | padding-left: 25px; 38 | } 39 | .left-inner-addon span.glyphicon { 40 | position: absolute; 41 | left: 8px; 42 | padding: 8px 0px; 43 | } 44 | 45 | .right-inner-addon { 46 | position: relative; 47 | } 48 | .right-inner-addon input { 49 | padding-right: 25px; 50 | } 51 | .right-inner-addon span.glyphicon { 52 | position: absolute; 53 | right: 8px; 54 | padding: 8px 0px; 55 | } 56 | 57 | .dashboard .col-lg-6 { 58 | overflow: hidden; 59 | } 60 | .boxcenter{ 61 | margin: 0 auto; 62 | } 63 | 64 | .downloads .table tbody>tr>td{ 65 | padding: 8px; 66 | vertical-align: middle; 67 | border-top: 1px solid #ddd; 68 | } 69 | 70 | .downloads .progress { 71 | height: 10px; 72 | margin-bottom: 3px; 73 | margin-top: 10px; 74 | } 75 | 76 | .progress-bar { 77 | -webkit-transition: background-color 0.6s ease, width 0.6s ease; 78 | transition: background-color 0.6s ease, width 0.6s ease; 79 | } 80 | 81 | tr.dlqueued { 82 | color: #838383; 83 | } 84 | 85 | tr.dlqueued td:first-child{ 86 | padding-left: 25px !important; 87 | } 88 | 89 | tr.dlqueued .progress, tr.dlqueued .dlspeed , tr.dlactive .queueupdown, tr.dlactive .queuepos, tr.dlqueued .dlreceived{ 90 | display: none; 91 | } 92 | 93 | .badge[href]:hover, 94 | .badge[href]:focus { 95 | color: #ffffff; 96 | text-decoration: none; 97 | cursor: pointer; 98 | } 99 | 100 | .badge-default { 101 | background-color: #999999; 102 | } 103 | 104 | .badge-default[href]:hover, 105 | .badge-default[href]:focus { 106 | background-color: #808080; 107 | } 108 | 109 | .badge-primary { 110 | background-color: #428bca; 111 | } 112 | 113 | .badge-primary[href]:hover, 114 | .badge-primary[href]:focus { 115 | background-color: #3071a9; 116 | } 117 | 118 | .badge-success { 119 | background-color: #5cb85c; 120 | } 121 | 122 | .badge-success[href]:hover, 123 | .badge-success[href]:focus { 124 | background-color: #449d44; 125 | } 126 | 127 | .badge-info { 128 | background-color: #5bc0de; 129 | } 130 | 131 | .badge-info[href]:hover, 132 | .badge-info[href]:focus { 133 | background-color: #31b0d5; 134 | } 135 | 136 | .badge-warning { 137 | background-color: #f0ad4e; 138 | } 139 | 140 | .badge-warning[href]:hover, 141 | .badge-warning[href]:focus { 142 | background-color: #ec971f; 143 | } 144 | 145 | .badge-danger { 146 | background-color: #d9534f; 147 | } 148 | 149 | .badge-danger[href]:hover, 150 | .badge-danger[href]:focus { 151 | background-color: #c9302c; 152 | } 153 | 154 | .navcount{ 155 | position:absolute; 156 | vertical-align: middle; 157 | opacity: .8; 158 | } 159 | 160 | .navbar-brand:hover .navcount{ 161 | opacity: 1; 162 | } 163 | 164 | .dashnoti ul { 165 | height: 285px; 166 | overflow-y: auto; 167 | overflow-x:visible; 168 | } 169 | 170 | .dashnoti .list-group-item { 171 | height: 66px; 172 | border-color: #afafaf; 173 | } 174 | 175 | .dashnoti ul li.dlsuccess{ 176 | background-color: #daf2da; 177 | } 178 | 179 | .dashnoti ul li.dlerror{ 180 | background-color: #f2dada; 181 | } 182 | 183 | .dashnoti ul li.dlstart{ 184 | background-color: #dae7f2; 185 | } 186 | 187 | .panel-title{ 188 | font-weight: 500; 189 | font-size: 24px; 190 | } 191 | 192 | .panel-heading .glyphicon{ 193 | line-height: 22px; 194 | } 195 | 196 | .dbsettings .input-group-addon{ 197 | width: 50px; 198 | } 199 | 200 | .dbsettings .disabled{ 201 | opacity: 0.7; 202 | } 203 | 204 | .dashboard .collapse{ 205 | display: none !important; 206 | } 207 | 208 | .dropdown-menu{ 209 | overflow-x: hidden; 210 | } 211 | 212 | .monospace{ 213 | font-family: monospace; 214 | font-size: 11px; 215 | } 216 | 217 | .bc-logo{ 218 | width: 42px; 219 | height: 42px; 220 | display: block; 221 | background: url("../img/bc_logo.png"); 222 | } 223 | 224 | .pp-logo{ 225 | width: 42px; 226 | height: 42px; 227 | display: block; 228 | background: url("../img/pp_logo.png"); 229 | } 230 | .donate li.list-group-item{ 231 | -webkit-transition: background-color 0.6s ease; 232 | -moz-transition: background-color 0.6s ease; 233 | -ms-transition: background-color 0.6s ease; 234 | -o-transition: background-color 0.6s ease; 235 | transition: background-color 0.6s ease; 236 | } 237 | 238 | .donate li.list-group-item:hover{ 239 | background-color: #ffe8d8; 240 | } 241 | 242 | .tooltip-inner{ 243 | word-wrap: break-word; 244 | max-width: 300px; 245 | } 246 | 247 | table.packets .server{ 248 | width: 10%; 249 | } 250 | 251 | table.packets .bot{ 252 | width: 15%; 253 | } 254 | 255 | table.packets .packetnr{ 256 | width: 4%; 257 | } 258 | 259 | table.packets .gets{ 260 | width: 5%; 261 | } 262 | 263 | table.packets .size{ 264 | width: 11%; 265 | } 266 | 267 | table.packets .lastseen{ 268 | width: 15%; 269 | } 270 | 271 | table.packets .download{ 272 | width: 40px; 273 | } -------------------------------------------------------------------------------- /public/js/controllers/dashboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('myApp').controller('DashboardCtrl', ['$scope', '$http', '$timeout', function DashboardCtrl($scope, $http, $timeout) { 4 | 5 | $scope.notifications = []; 6 | $scope.servers = {}; 7 | $scope.onServers = 0; 8 | $scope.numServers = 0; 9 | $scope.compacting = {}; 10 | $scope.packetCount = { 11 | redPackets: 0, 12 | absPackets: 0 13 | }; 14 | 15 | // Initial data for charts, will be filled by live data afterwards 16 | $scope.chartServerData = [0,0]; 17 | $scope.chartPacketData = [0,0,0]; 18 | 19 | // Default chart colors 20 | $scope.chartColors =['#5cb85c', '#d9534f', '#999']; 21 | // Labels for Packet chart 22 | $scope.chartPacketLabels =['Unique', 'Redundant']; 23 | // Labels for Server chart 24 | $scope.chartServerLabels =['Online', 'Offline']; 25 | 26 | 27 | 28 | 29 | $scope.redPacketPercentage = function () { 30 | return $scope.packetCount.redPackets / ($scope.packetCount.absPackets + $scope.packetCount.redPackets) * 100; 31 | }; 32 | 33 | $scope.unqPacketPercentage = function () { 34 | return 100 - $scope.redPacketPercentage(); 35 | }; 36 | 37 | 38 | $scope.onServerPercentage = function () { 39 | return $scope.onServers / $scope.numServers * 100; 40 | }; 41 | 42 | $scope.offServerPercentage = function () { 43 | return 100 - $scope.onServerPercentage(); 44 | }; 45 | 46 | $scope.getData = function () { 47 | 48 | // retrieve status of servers 49 | $http.get('/api/server/').then(function (response) { 50 | $scope.onServers = 0; 51 | $scope.numServers = 0; 52 | for (var i in response.data) { 53 | response.data[i].key = i; 54 | if (response.data[i].connected) { 55 | $scope.onServers++; 56 | } 57 | $scope.numServers++; 58 | } 59 | $scope.servers = response.data; 60 | 61 | $scope.chartServerData = [ $scope.onServers, $scope.numServers - $scope.onServers]; 62 | 63 | }); 64 | 65 | // retrieve status of packet information 66 | $http.get('/api/packet/').then(function (response) { 67 | angular.extend($scope.packetCount, response.data); 68 | $scope.chartPacketData = [ $scope.packetCount.absPackets, $scope.packetCount.redPackets ]; 69 | 70 | }); 71 | 72 | // retrieve status of compacting information 73 | $http.get('/api/db/compacting/').then(function (response) { 74 | angular.extend($scope.compacting, response.data); 75 | }); 76 | 77 | // retrieve notifiations 78 | $http.get('/api/downloads/notifications/').then(function (response){ 79 | $scope.notifications = response.data; 80 | }); 81 | 82 | }; 83 | 84 | 85 | function deaktivateAnimation() { 86 | $scope.chartOptions.animation.animateRotate = false; 87 | } 88 | 89 | $scope.chartOptions = { 90 | responsive: false, 91 | maintainAspectRatio: false, 92 | 93 | //Boolean - Whether we should show a stroke on each segment 94 | segmentShowStroke: true, 95 | 96 | //String - The colour of each segment stroke 97 | segmentStrokeColor: "#ffffff", 98 | 99 | //Number - The width of each segment stroke 100 | segmentStrokeWidth: 2, 101 | 102 | //Boolean - Whether we should animate the chart 103 | animation: { 104 | numSteps: 100, 105 | easing: "easeOutQuart", 106 | //Function - Will fire on animation completion. 107 | onComplete: deaktivateAnimation 108 | }, 109 | 110 | onAnimationComplete: deaktivateAnimation 111 | }; 112 | 113 | 114 | 115 | $scope.getData(); 116 | var timeout; 117 | 118 | function repeat() { 119 | timeout = $timeout(function () { 120 | $scope.getData(); 121 | repeat(); 122 | }, 30000); 123 | } 124 | 125 | $scope.$on('$destroy', function () { 126 | $timeout.cancel(timeout); 127 | }); 128 | 129 | repeat(); 130 | }]); 131 | 132 | 133 | 134 | /* Notification controller */ 135 | angular.module('myApp').controller('NotificationCtrl', ['$scope', '$http', 'socket', '$rootScope', '$location', function ($scope, $http, socket, $rootScope, $location){ 136 | 137 | socket.on('send:dlstart',function(data){ 138 | data.type = 'dlstart'; 139 | $scope.notifications.push(data); 140 | }); 141 | 142 | socket.on('send:dlerror',function(data){ 143 | data.type = 'dlerror'; 144 | $scope.notifications.push(data); 145 | }); 146 | 147 | socket.on('send:dlsuccess',function(data){ 148 | data.type = 'dlsuccess'; 149 | $scope.notifications.push(data); 150 | }); 151 | 152 | $scope.$on('$destroy', function () { 153 | socket.off('send:dlerror'); 154 | socket.off('send:dlstart'); 155 | socket.off('send:dlsuccess'); 156 | }); 157 | 158 | $scope.clearNotifications = function(){ 159 | $http.delete('/api/downloads/notifications/').then(function (response){ 160 | $scope.notifications=[]; 161 | $rootScope.$emit('notificationsClear'); 162 | }); 163 | }; 164 | 165 | $scope.getError = function(notification){ 166 | 167 | function errordecoder(notification){ 168 | var error = notification.error; 169 | console.log(error); 170 | if(error === "filename mismatch"){ 171 | return "Filename mismatch, got "+notification.gotFile; 172 | } 173 | if(typeof error === "string"){ 174 | return error; 175 | } 176 | if(error.code === "ETIMEDOUT"){ 177 | return "Connection timeout"; 178 | } 179 | if(error.code === "ECONNREFUSED"){ 180 | return "Connection refused"; 181 | } 182 | if(error.code === "ECONNRESET"){ 183 | return "Connection reset"; 184 | } 185 | return error.code; 186 | } 187 | 188 | if(notification.type === "dlerror"){ 189 | return errordecoder(notification); 190 | }else{ 191 | return ""; 192 | } 193 | 194 | 195 | }; 196 | 197 | $scope.searchPacket = function(name){ 198 | $rootScope.searchString = angular.copy(name); 199 | $scope.$emit("setSearch"); 200 | $location.path("packets"); 201 | }; 202 | 203 | }]); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * ---------------------------------------------------------------------------- 4 | * "THE BEER-WARE LICENSE" (Revision 42): 5 | * wrote this file. As long as you retain this notice you 6 | * can do whatever you want with this stuff. If we meet some day, and you think 7 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 8 | * ---------------------------------------------------------------------------- 9 | */ 10 | 11 | /** 12 | * Module dependencies 13 | */ 14 | 15 | var nodefs = require("node-fs"); 16 | 17 | var homePath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; 18 | var appHome = homePath+'/.slingxdcc/'; 19 | 20 | nodefs.mkdir(appHome+"config",0775,true,function(){ 21 | 22 | 23 | var logger = require("./lib/xdcclogger"), 24 | downloadHandler = require("./lib/downloadHandler"), 25 | express = require('express'), 26 | morgan = require('morgan'), 27 | bodyParser = require('body-parser'), 28 | methodOverride = require('method-override'), 29 | 30 | https = require('https'), 31 | http = require('http'), 32 | path = require('path'), 33 | fs = require('fs'), 34 | io = require('socket.io'), 35 | nconf = require('nconf'), 36 | log4js = require("log4js"), 37 | appLogger = log4js.getLogger('app') 38 | httpLogger = log4js.getLogger('http'), 39 | errors = require('common-errors'); 40 | 41 | nconf.add('settings', {type: 'file', file: appHome+'/config/settings.json'}); 42 | 43 | nconf.defaults({ 44 | "webserver": { 45 | "port": 3000, 46 | "ssl": true, 47 | "ssl.crt": appHome+"ssl/server.crt", 48 | "ssl.key": appHome+"ssl/server.key" 49 | } 50 | }); 51 | 52 | nconf.set('webserver',nconf.get('webserver')); 53 | nconf.save(); 54 | 55 | process.on('SIGINT', function () { 56 | console.log('Shuting down'); 57 | downloadHandler.exit(); 58 | logger.exit(); 59 | process.exit(); 60 | }); 61 | 62 | nconf.load(function(){ 63 | 64 | var routes = require('./routes'), 65 | api = require('./routes/api'); 66 | 67 | var app = module.exports = express(); 68 | 69 | 70 | /** 71 | * Configuration 72 | */ 73 | 74 | // all environments 75 | app.set('port', nconf.get('webserver:port')); 76 | app.set('views', __dirname + '/views'); 77 | app.set('view engine', 'jade'); 78 | 79 | // app.use(morgan('dev')); 80 | app.use(bodyParser.json()); 81 | app.use(bodyParser.urlencoded({extended:false})); 82 | app.use(errors.middleware.crashProtector()); 83 | app.use(methodOverride()); 84 | app.use(express.static(path.join(__dirname, 'public'))); 85 | app.use(log4js.connectLogger(httpLogger, { level: 'auto', format: ':remote-addr - :method :url HTTP/:http-version :status - :response-time ms',nolog: '\\.gif|\\.jpg$' })); 86 | 87 | /** 88 | * Routes 89 | */ 90 | 91 | app.get('/', routes.index); 92 | app.get('/partials/:name', routes.partials); 93 | 94 | // JSON API 95 | 96 | app.get('/api/packet/', api.getNumPackets); 97 | 98 | app.get('/api/packet/get/:id', api.packet); 99 | app.get('/api/packet/search/:string/', api.packetSearch); 100 | app.get('/api/packet/search/:string/:page', api.packetSearchPaged); 101 | app.get('/api/packet/list/', api.packetList); 102 | app.get('/api/packet/list/:page', api.packetListPaged); 103 | 104 | app.get('/api/packet/sorting/', api.getSorting); 105 | app.get('/api/packet/filter/', api.getFilter); 106 | app.get('/api/packet/pagelimit/', api.getPageLimit); 107 | 108 | app.get('/api/server/', api.getServer); 109 | 110 | app.get('/api/db/compacting/', api.getNextCompacting); 111 | app.get('/api/db/compactingfilter/', api.getCompactingFilter); 112 | app.get('/api/downloads/', api.getDownloads); 113 | app.get('/api/downloads/notifications/', api.getDlNotifications); 114 | app.get('/api/downloads/notifications/count/', api.getDlNotificationCount); 115 | 116 | app.put('/api/packet/sorting/', api.setSorting); 117 | app.put('/api/packet/filter/', api.setFilter); 118 | app.put('/api/packet/pagelimit/', api.setPageLimit); 119 | app.put('/api/db/compacting/', api.compactDb); 120 | app.put('/api/db/compactingfilter/', api.setCompactingFilter); 121 | app.put('/api/channel/', api.channels); 122 | app.put('/api/downloads/upqueue/', api.upQueueDownload); 123 | app.put('/api/downloads/downqueue/', api.downQueueDownload); 124 | app.put('/api/downloads/cancel', api.cancelDownload); 125 | 126 | 127 | 128 | app.post('/api/server/', api.addServer); 129 | app.post('/api/downloads/', api.startDownload); 130 | app.post('/api/db/compacting/', api.startCompactCronjob); 131 | 132 | app.delete('/api/server/:key', api.removeServer); 133 | app.delete('/api/downloads/notifications/', api.clearDlNotifications); 134 | app.delete('/api/downloads/notifications/count/', api.clearDlNotificationCount); 135 | app.delete('/api/db/compacting', api.stopCompactCronjob); 136 | 137 | app.get('*', routes.index); 138 | 139 | app.use(errors.middleware.errorHandler); 140 | 141 | 142 | /** 143 | * Create server 144 | */ 145 | 146 | 147 | fs.readFile(nconf.get('webserver:ssl.key'), function (errorkey, key){ 148 | fs.readFile(nconf.get('webserver:ssl.crt'), function (errorcrt, crt){ 149 | var server; 150 | if ((errorcrt || errorkey) && nconf.get('webserver:ssl')){ 151 | server = http.createServer(app); 152 | appLogger.info('No key or cert found, !!!Fallback!!! to Http'); 153 | }else if(nconf.get('webserver:ssl')){ 154 | server = https.createServer({key: key, cert: crt}, app); 155 | }else{ 156 | server = http.createServer(app); 157 | } 158 | // Hook Socket.io into Express 159 | io = io.listen(server); 160 | // Socket.io Communication 161 | io.sockets.on('connection', require('./routes/socket')); 162 | 163 | /** 164 | * Start Server 165 | */ 166 | 167 | server.listen(app.get('port'), function (){ 168 | appLogger.info('Server listening on port ' + app.get('port')); 169 | }); 170 | }); 171 | }); 172 | 173 | }); 174 | }); -------------------------------------------------------------------------------- /lib/packdb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | var packdb = function packdb() { 10 | //defining a var instead of this (works for variable & function) will create a private definition 11 | var dirty = require("./dirty/dirty"), 12 | query = require("dirty-query").query, 13 | nconf = require("nconf"), 14 | log4js = require('log4js'), 15 | logger = log4js.getLogger('packdb'); 16 | 17 | var homePath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; 18 | var appHome = homePath+'/.slingxdcc/'; 19 | 20 | nconf.add('settings', {type: 'file', file: appHome+'config/settings.json'}); 21 | 22 | nconf.defaults({ 23 | "logger": { 24 | "packdb": appHome+"packets.db", 25 | "autocleandb" : true, 26 | "cleandb_Xminutes": 60, 27 | "redundantPercentage": 25, 28 | "removePacketsOlder_Xhours": false 29 | } 30 | }); 31 | nconf.set('logger',nconf.get('logger')); 32 | 33 | nconf.save(); 34 | 35 | var packDb = dirty.Dirty(nconf.get('logger:packdb')); 36 | 37 | 38 | packDb.on('load', function () { 39 | this.setCompactingFilter(nconf.get('logger:removePacketsOlder_Xhours')); 40 | packDb.compact(); 41 | }) 42 | 43 | this.onServerKeys = []; 44 | 45 | var nextCompacting = nconf.get('logger:autocleandb') ? new Date().getTime() + nconf.get('logger:cleandb_Xminutes') * 60 * 1000 : 0; 46 | 47 | if (nconf.get('logger:autocleandb')) { 48 | var minutes = nconf.get('logger:cleandb_Xminutes'); 49 | var redPercentage = nconf.get('logger:redundantPercentage'); 50 | cronID = setInterval(function () { 51 | if (packDb.redundantLength / (packDb.redundantLength + packDb.length) * 100 > redPercentage) { 52 | packDb.compact(); 53 | } 54 | nextCompacting = new Date().getTime() + minutes * 60 * 1000; 55 | }, minutes * 1000 * 60); 56 | } 57 | 58 | var self = this; 59 | 60 | var cronID; 61 | 62 | this.getNextCompacting = function(){ 63 | return nextCompacting; 64 | } 65 | 66 | this.numberOfRedundantPackets = function () { 67 | return packDb.redundantLength; 68 | } 69 | 70 | this.compactDb = function () { 71 | packDb.compact(); 72 | } 73 | 74 | this.stopCompactCronjob = function () { 75 | if (cronID) { 76 | clearInterval(cronID); 77 | } 78 | nconf.set('logger:autocleandb', false); 79 | nconf.save(); 80 | nextCompacting = 0; 81 | } 82 | 83 | this.startCompactCronjob = function (minutes, redPercentage) { 84 | if (cronID) { 85 | clearInterval(cronID); 86 | } 87 | nextCompacting = new Date().getTime() + minutes * 60 * 1000; 88 | cronID = setInterval(function () { 89 | if (packDb.redundantLength / (packDb.redundantLength + packDb.length) * 100 > redPercentage) { 90 | packDb.compact(); 91 | } 92 | nextCompacting = new Date().getTime() + minutes * 60 * 1000; 93 | }, minutes * 1000 * 60); 94 | nconf.set('logger:autocleandb', true); 95 | nconf.set('logger:cleandb_Xminutes',minutes); 96 | nconf.set('logger:redundantPercentage',redPercentage); 97 | nconf.save(); 98 | } 99 | 100 | this.addPack = function (packObj) { 101 | if (packObj !== null) { 102 | packDb.set(packObj.server + '#' + packObj.nick + '#' + packObj.nr, packObj); 103 | } 104 | } 105 | 106 | this.numberOfPackets = function () { 107 | return packDb.size(); 108 | }; 109 | 110 | this.getPacket = function (key) { 111 | return packDb.get(key); 112 | }; 113 | 114 | this.searchPackets = function (string, sortBy, sortOrder, filterDiscon) { 115 | var q = queryBuilder(null, null, sortBy, sortOrder, string, filterDiscon, null); 116 | logger.debug('Search packages:', string, sortBy, sortOrder, filterDiscon, JSON.stringify(q)); 117 | return query(packDb, q.query, q.options); 118 | }; 119 | 120 | this.searchPacketsPaged = function (string, limit, page, sortBy, sortOrder, filterDiscon, cb) { 121 | if (parseInt(page) == 1) query(packDb, "reset_cache"); 122 | 123 | var q = queryBuilder(limit, page, sortBy, sortOrder, string, filterDiscon, cb); 124 | 125 | return query(packDb, q.query, q.options); 126 | }; 127 | 128 | this.setCompactingFilter = function(hours){ 129 | if(hours != false){ 130 | nconf.set('logger:removePacketsOlder_Xhours',hours); 131 | packDb.setCompactingFilter(function(key,value){ 132 | return (value.lastseen < (parseInt(new Date().getTime()) - hours * 1000 * 60 * 60)); 133 | }); 134 | }else{ 135 | nconf.set('logger:removePacketsOlder_Xhours',false); 136 | packDb.setCompactingFilter(function(key,value){ 137 | return false; 138 | }) 139 | } 140 | nconf.save(); 141 | } 142 | 143 | this.getCompactingFilter = function(){ 144 | return nconf.get('logger:removePacketsOlder_Xhours'); 145 | } 146 | 147 | function queryBuilder(limit, page, sortBy, sortOrder, search, filterDiscon, cb) { 148 | var buildquery = { 149 | query: {}, 150 | options: {} 151 | }; 152 | 153 | if (limit !== null) { 154 | buildquery.options = { 155 | sortBy: sortBy, 156 | order: sortOrder, 157 | limit: limit, 158 | page: page, 159 | cache: true, 160 | pager: cb 161 | }; 162 | } else { 163 | buildquery.options = { 164 | sortBy: sortBy, 165 | order: sortOrder 166 | }; 167 | } 168 | 169 | if (typeof search !== 'string') { 170 | buildquery.query = {nr: {$has: true}}; 171 | } else { 172 | var words = search.toLowerCase().split(" "); 173 | buildquery.query.filename = { 174 | $cb: function (attr) { 175 | attr = attr.toLowerCase(); 176 | for (var i in words) { 177 | if (attr.indexOf(words[i]) < 0) { 178 | return false; 179 | } 180 | } 181 | return true; 182 | } 183 | }; 184 | } 185 | 186 | if (filterDiscon === true) { 187 | buildquery.query.server = { 188 | $in: self.onServerKeys 189 | }; 190 | } 191 | return buildquery; 192 | } 193 | 194 | 195 | if (packdb.caller != packdb.getInstance) { 196 | throw new Error("This object cannot be instantiated"); 197 | } 198 | }; 199 | 200 | 201 | /* ************************************************************************ 202 | SINGLETON CLASS DEFINITION 203 | ************************************************************************ */ 204 | packdb.instance = null; 205 | 206 | /** 207 | * Singleton getInstance definition 208 | * @return singleton class 209 | */ 210 | packdb.getInstance = function () { 211 | if (this.instance === null) { 212 | this.instance = new packdb(); 213 | } 214 | return this.instance; 215 | }; 216 | module.exports = packdb.getInstance(); 217 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // -------------------------------------------------------------------- 3 | // JSHint Nodeclipse Configuration v0.18 4 | // Strict Edition with some relaxations and switch to Node.js, no `use strict` 5 | // by Ory Band, Michael Haschke, Paul Verest 6 | // https://github.com/Nodeclipse/nodeclipse-1/blob/master/org.nodeclipse.ui/templates/common-templates/.jshintrc 7 | // JSHint Documentation is at http://www.jshint.com/docs/options/ 8 | // JSHint Integration v0.9.10 comes with JSHInt 2.5.6 , see https://github.com/eclipsesource/jshint-eclipse 9 | // -------------------------------------------------------------------- 10 | // from https://gist.github.com/haschek/2595796 11 | // 12 | // This is a options template for [JSHint][1], using [JSHint example][2] 13 | // and [Ory Band's example][3] as basis and setting config values to 14 | // be most strict: 15 | // 16 | // * set all enforcing options to true 17 | // * set all relaxing options to false 18 | // * set all environment options to false, except the node value 19 | // * set all JSLint legacy options to false 20 | // 21 | // [1]: http://www.jshint.com/ 22 | // [2]: https://github.com/jshint/node-jshint/blob/master/example/config.json //404 23 | // [3]: https://github.com/oryband/dotfiles/blob/master/jshintrc 24 | // [4]: http://www.jshint.com/options/ 25 | // 26 | // @author http://michael.haschke.biz/ 27 | // @license http://unlicense.org/ 28 | 29 | // == Enforcing Options =============================================== 30 | // 31 | // These options tell JSHint to be more strict towards your code. Use 32 | // them if you want to allow only a safe subset of JavaScript, very 33 | // useful when your codebase is shared with a big number of developers 34 | // with different skill levels. Was all true. 35 | 36 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 37 | "curly" : true, // Require {} for every new block or scope. 38 | "eqeqeq" : true, // Require triple equals i.e. `===`. 39 | "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. 40 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 41 | "latedef" : true, // Prohibit variable use before definition. 42 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 43 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 44 | "noempty" : true, // Prohibit use of empty blocks. 45 | "nonew" : true, // Prohibit use of constructors for side-effects. 46 | "plusplus" : false, // Prohibit use of `++` & `--`. //coding style related only 47 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 48 | "undef" : true, // Require all non-global variables be declared before they are used. 49 | "strict" : false, // Require `use strict` pragma in every file. 50 | "trailing" : true, // Prohibit trailing whitespaces. 51 | 52 | // == Relaxing Options ================================================ 53 | // 54 | // These options allow you to suppress certain types of warnings. Use 55 | // them only if you are absolutely positive that you know what you are 56 | // doing. Was all false. 57 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 58 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 59 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 60 | "eqnull" : false, // Tolerate use of `== null`. 61 | //"es5" : true, // Allow EcmaScript 5 syntax. // es5 is default https://github.com/jshint/jshint/issues/1411 62 | "esnext" : false, // Allow ES.next (ECMAScript 6) specific features such as `const` and `let`. 63 | "evil" : false, // Tolerate use of `eval`. 64 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 65 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 66 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 67 | "iterator" : false, // Allow usage of __iterator__ property. 68 | "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. 69 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 70 | "laxcomma" : true, // Suppress warnings about comma-first coding style. 71 | "loopfunc" : false, // Allow functions to be defined within loops. 72 | "maxerr" : 100, // This options allows you to set the maximum amount of warnings JSHint will produce before giving up. Default is 50. 73 | "multistr" : false, // Tolerate multi-line strings. 74 | "onecase" : false, // Tolerate switches with just one case. 75 | "proto" : false, // Tolerate __proto__ property. This property is deprecated. 76 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 77 | "scripturl" : false, // Tolerate script-targeted URLs. 78 | "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 79 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 80 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 81 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 82 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 83 | 84 | // == Environments ==================================================== 85 | // 86 | // These options pre-define global variables that are exposed by 87 | // popular JavaScript libraries and runtime environments—such as 88 | // browser or node.js. TODO JSHint Documentation has more, but it is not clear since what JSHint version they appeared 89 | "browser" : false, // Standard browser globals e.g. `window`, `document`. 90 | "couch" : false, // Enable globals exposed by CouchDB. 91 | "devel" : false, // Allow development statements e.g. `console.log();`. 92 | "dojo" : false, // Enable globals exposed by Dojo Toolkit. 93 | "jquery" : false, // Enable globals exposed by jQuery JavaScript library. 94 | "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. 95 | "node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment. 96 | "nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape. 97 | "phantom" : false, //?since version? This option defines globals available when your core is running inside of the PhantomJS runtime environment. 98 | "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. 99 | "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. 100 | "worker" : false, //?since version? This option defines globals available when your code is running inside of a Web Worker. 101 | "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. 102 | "yui" : false, //?since version? This option defines globals exposed by the YUI JavaScript framework. 103 | 104 | // == JSLint Legacy =================================================== 105 | // 106 | // These options are legacy from JSLint. Aside from bug fixes they will 107 | // not be improved in any way and might be removed at any point. 108 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 109 | "onevar" : false, // Allow only one `var` statement per function. 110 | "passfail" : false, // Stop on first error. 111 | "white" : false, // Check against strict whitespace and indentation rules. 112 | 113 | // == Undocumented Options ============================================ 114 | // 115 | // While Michael have found these options in [example1][2] and [example2][3] (already gone 404) 116 | // they are not described in the [JSHint Options documentation][4]. 117 | 118 | "predef" : [ // Extra globals. 119 | //"exampleVar", 120 | //"anotherCoolGlobal", 121 | //"iLoveDouglas" 122 | "Java", "JavaFX", "$ARG", "angular", "jQuery" //no effect 123 | ] 124 | //, "indent" : 2 // Specify indentation spacing 125 | } -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | /* 11 | * Serve JSON to our AngularJS client 12 | */ 13 | 14 | var logger = require("../lib/xdcclogger"); 15 | var packdb = require("../lib/packdb"); 16 | var downloadHandler = require("../lib/downloadHandler"); 17 | var nconf = require("nconf"); 18 | 19 | var homePath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; 20 | var appHome = homePath+'/.slingxdcc/'; 21 | 22 | nconf.add('settings', {type: 'file', file: appHome+'config/settings.json'}); 23 | 24 | nconf.defaults({ 25 | "packetList": { 26 | "sortBy": "lastseen", 27 | "sortOrder": "desc", 28 | "filterDiscon": true, 29 | "pageItemLimit": 20 30 | } 31 | }); 32 | nconf.set('packetList',nconf.get('packetList')); 33 | nconf.save(); 34 | 35 | var notificationCount = { 36 | dlerror : 0, 37 | dlsuccess : 0, 38 | dlstart: 0 39 | }; 40 | 41 | downloadHandler.on('dlerror',function(){ 42 | notificationCount.dlerror++; 43 | }); 44 | downloadHandler.on('dlsuccess',function(){ 45 | notificationCount.dlsuccess++; 46 | }); 47 | downloadHandler.on('dlstart',function(){ 48 | notificationCount.dlstart++; 49 | }); 50 | 51 | // GET 52 | 53 | exports.packet = function (req, res){ 54 | var id = req.params.id; 55 | res.json(packdb.getPacket(id)); 56 | }; 57 | 58 | exports.packetSearch = function (req, res){ 59 | var string = req.params.string; 60 | var packets = packdb.searchPackets(string, nconf.get("packetList:sortBy"), nconf.get("packetList:sortOrder"), nconf.get("packetList:filterDiscon")); 61 | res.json(packets); 62 | }; 63 | 64 | exports.packetSearchPaged = function (req, res){ 65 | var string = req.params.string; 66 | var page = parseInt(req.params.page); 67 | packdb.searchPacketsPaged(string, nconf.get("packetList:pageItemLimit"), page, nconf.get("packetList:sortBy"), nconf.get("packetList:sortOrder"), nconf.get("packetList:filterDiscon"), function (pages, result){ 68 | res.json({ 69 | numPages: pages, 70 | numPackets: pages*nconf.get("packetList:pageItemLimit"), 71 | pageItemLimit: nconf.get("packetList:pageItemLimit"), 72 | packets : result 73 | }); 74 | }); 75 | }; 76 | 77 | exports.packetList = function (req, res){ 78 | var packets = packdb.searchPackets(null, nconf.get("packetList:sortBy"), nconf.get("packetList:sortOrder"), nconf.get("packetList:filterDiscon")); 79 | res.json(packets); 80 | }; 81 | 82 | exports.packetListPaged = function (req, res){ 83 | var page = parseInt(req.params.page); 84 | packdb.searchPacketsPaged(null, nconf.get("packetList:pageItemLimit"), page, nconf.get("packetList:sortBy"), nconf.get("packetList:sortOrder"), nconf.get("packetList:filterDiscon"), function (pages, result){ 85 | res.json({ 86 | numPages: pages, 87 | numPackets: pages*nconf.get("packetList:pageItemLimit"), 88 | pageItemLimit: nconf.get("packetList:pageItemLimit"), 89 | packets: result 90 | }); 91 | }); 92 | }; 93 | 94 | 95 | exports.getSorting = function (req, res){ 96 | res.json({ 97 | sortBy : nconf.get("packetList:sortBy"), 98 | sortOrder: nconf.get("packetList:sortOrder") 99 | }); 100 | }; 101 | 102 | exports.getFilter = function (req, res){ 103 | res.json(nconf.get("packetList:filterDiscon")); 104 | }; 105 | 106 | exports.getPageLimit = function (req, res){ 107 | res.json(nconf.get("packetList:pageItemLimit")); 108 | }; 109 | 110 | exports.getServer = function (req, res){ 111 | res.json(logger.getIrcServers()); 112 | }; 113 | 114 | exports.getNumPackets = function (req, res){ 115 | var abspackets = packdb.numberOfPackets(); 116 | var redpackets = packdb.numberOfRedundantPackets(); 117 | 118 | res.json({ 119 | absPackets : abspackets, 120 | redPackets : redpackets, 121 | }); 122 | }; 123 | 124 | exports.getNextCompacting = function (req, res){ 125 | res.json({ 126 | nextCompacting: packdb.getNextCompacting(), 127 | autoCompacting: nconf.get('logger:autocleandb'), 128 | redPercentage: nconf.get('logger:redundantPercentage'), 129 | interval: nconf.get('logger:cleandb_Xminutes') 130 | }); 131 | }; 132 | 133 | exports.getCompactingFilter = function (req, res){ 134 | res.json({ 135 | filter: nconf.get('logger:removePacketsOlder_Xhours') 136 | }); 137 | }; 138 | 139 | exports.getDownloads = function (req, res){ 140 | res.json({dlQueue:downloadHandler.getDownloads()}); 141 | }; 142 | 143 | exports.getDlNotifications = function (req, res){ 144 | res.json(downloadHandler.getNotifications()); 145 | }; 146 | 147 | exports.getDlNotificationCount = function (req, res){ 148 | res.json(notificationCount); 149 | }; 150 | 151 | // PUT 152 | 153 | exports.compactDb = function (req, res){ 154 | packdb.compactDb(); 155 | res.json({success: true}); 156 | }; 157 | 158 | exports.setCompactingFilter = function (req, res){ 159 | packdb.setCompactingFilter(req.body.filter) 160 | res.json({ 161 | filter: req.body.filter 162 | }); 163 | } 164 | 165 | exports.setSorting = function (req, res){ 166 | nconf.set("packetList:sortBy",req.body.sortBy); 167 | nconf.set("packetList:sortOrder",req.body.sortOrder); 168 | res.json({ 169 | sortBy : nconf.get("packetList:sortBy"), 170 | sortOrder: nconf.get("packetList:sortOrder") 171 | }); 172 | nconf.save(); 173 | }; 174 | 175 | exports.setFilter = function (req, res){ 176 | nconf.set("packetList:filterDiscon",req.body.filterDiscon); 177 | res.json(nconf.get("packetList:filterDiscon")); 178 | nconf.save(); 179 | }; 180 | 181 | exports.setPageLimit = function (req, res){ 182 | nconf.set("packetList:pageItemLimit",parseInt(req.body.limit)); 183 | res.json(nconf.get("packetList:pageItemLimit")); 184 | nconf.save(); 185 | }; 186 | 187 | exports.channels = function (req, res){ 188 | var type = req.body.type; 189 | var srvkey = req.body.srvkey; 190 | var channels = req.body.channels.length > 0 ? req.body.channels.toString().split(" ") : []; 191 | if(channels.length > 0){ 192 | switch (type){ 193 | case 'join': 194 | logger.joinChannels(srvkey, channels); 195 | break; 196 | case 'part': 197 | logger.partChannels(srvkey, channels); 198 | break; 199 | case 'observ': 200 | logger.observChannels(srvkey, channels); 201 | break; 202 | case 'unobserv': 203 | logger.unobservChannels(srvkey, channels); 204 | break; 205 | } 206 | } 207 | res.json({type: type, srvkey: srvkey, channels: channels}); 208 | }; 209 | 210 | 211 | exports.upQueueDownload = function (req, res){ 212 | var packObj = req.body.packObj; 213 | var success = downloadHandler.upqueue(packObj); 214 | res.json({success:success}); 215 | }; 216 | 217 | exports.downQueueDownload = function (req, res){ 218 | var packObj = req.body.packObj; 219 | var success = downloadHandler.downqueue(packObj); 220 | res.json({success:success}); 221 | }; 222 | 223 | exports.cancelDownload = function (req,res){ 224 | var success = downloadHandler.cancelDownload(req.body.packObj); 225 | res.json({success: success}); 226 | }; 227 | 228 | // POST 229 | exports.addServer = function (req, res){ 230 | logger.addServer(req.body.srvkey, { 231 | host : req.body.host, 232 | port : parseInt(req.body.port), 233 | nick : req.body.nick, 234 | channels : req.body.channels.length > 0 ? req.body.channels.toString().split(" ") : [], 235 | observchannels: req.body.observchannels.length > 0 ? req.body.observchannels.toString().split(" ") : [] 236 | }); 237 | res.json(req.body); 238 | }; 239 | 240 | exports.startDownload = function (req,res){ 241 | var success = downloadHandler.startDownload(req.body.packObj); 242 | res.json({success: success}); 243 | }; 244 | 245 | exports.startCompactCronjob = function (req, res){ 246 | packdb.startCompactCronjob(req.body.minutes,req.body.percentage); 247 | res.json({ 248 | nextCompacting: packdb.getNextCompacting(), 249 | autoCompacting: true, 250 | redPercentage: req.body.percentage, 251 | interval: req.body.minutes 252 | }); 253 | }; 254 | 255 | // DELETE 256 | exports.removeServer = function (req, res){ 257 | var srvkey = req.params.key; 258 | var servers = logger.getIrcServers(); 259 | if (servers[srvkey] !== "undefined"){ 260 | logger.removeServer(srvkey); 261 | res.json(true); 262 | }else{ 263 | res.json(false); 264 | } 265 | }; 266 | 267 | exports.clearDlNotifications = function (req, res){ 268 | downloadHandler.clearNotifications(); 269 | notificationCount = { 270 | dlerror : 0, 271 | dlsuccess : 0, 272 | dlstart: 0 273 | }; 274 | res.json(true); 275 | }; 276 | 277 | exports.clearDlNotificationCount = function (req, res){ 278 | notificationCount = { 279 | dlerror : 0, 280 | dlsuccess : 0, 281 | dlstart: 0 282 | }; 283 | res.json(true); 284 | }; 285 | 286 | exports.stopCompactCronjob = function (req, res){ 287 | packdb.stopCompactCronjob(); 288 | res.json({ 289 | nextCompacting: 0, 290 | autoCompacting : false, 291 | redPercentage: nconf.get('logger:redundantPercentage'), 292 | interval: nconf.get('logger:cleandb_Xminutes') 293 | }); 294 | }; 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /lib/dirty/dirty.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | util = require('util'), 3 | EventEmitter = require('events').EventEmitter, 4 | Set = require('./set'); 5 | 6 | /** 7 | * Constructor function 8 | */ 9 | var Dirty = exports.Dirty = function(path) { 10 | if (!(this instanceof Dirty)) return new Dirty(path); 11 | 12 | EventEmitter.call(this); 13 | 14 | this.path = path; 15 | this.writeBundle = 1000; 16 | 17 | this._docs = {}; 18 | this._queue = []; 19 | this._readStream = null; 20 | this._writeStream = null; 21 | this._compactingFilter = null; 22 | this._indexFns = {}; 23 | this._length = 0; 24 | this._redundantLength = 0; 25 | this._load(); 26 | var self = this; 27 | this.on('compacted', function(){ 28 | self._endCompacting(); 29 | }); 30 | this.on('compactingError', function(){ 31 | self._queue = self._queueBackup.concat(self._queue); 32 | self._redundantLength += self._redundantLengthBackup; 33 | self._endCompacting(); 34 | }); 35 | }; 36 | 37 | util.inherits(Dirty, EventEmitter); 38 | Dirty.Dirty = Dirty; 39 | module.exports = Dirty; 40 | 41 | /** 42 | * set() stores a JSON object in the database at key 43 | * cb is fired when the data is persisted. 44 | * In memory, this is immediate- on disk, it will take some time. 45 | */ 46 | Dirty.prototype.set = function(key, val, cb) { 47 | this._updateDocs(key, val); 48 | if (!cb) { 49 | this._queue.push(key); 50 | } else { 51 | this._queue.push([key, cb]); 52 | } 53 | this._maybeFlush(); 54 | }; 55 | 56 | Dirty.prototype._updateDocs = function(key, val, skipRedundantRows) { 57 | this._updateIndexes(key, val); 58 | if (key in this._docs) { 59 | this._length--; 60 | if (!skipRedundantRows) this._redundantLength++; 61 | } 62 | if (val === undefined) { 63 | if (!skipRedundantRows) this._redundantLength++; 64 | delete this._docs[key]; 65 | } else { 66 | this._length++; 67 | this._docs[key] = val; 68 | } 69 | }; 70 | 71 | /** 72 | * Get the value stored at a key in the database 73 | * This is synchronous since a cache is maintained in-memory 74 | */ 75 | Dirty.prototype.get = function(key) { 76 | return this._clone(this._docs[key]); 77 | }; 78 | 79 | Dirty.prototype._clone = function(obj) { 80 | if (Object.prototype.toString.call(obj) === '[object Array]') { 81 | return obj.slice(); 82 | } 83 | 84 | if (Object.prototype.toString.call(obj) !== '[object Object]') { 85 | return obj; 86 | } 87 | var retval = {}; 88 | for (var k in obj) { 89 | retval[k] = this._clone(obj[k]); 90 | } 91 | return retval; 92 | }; 93 | 94 | /** 95 | * Get total number of stored keys 96 | */ 97 | Dirty.prototype.size = function() { 98 | return this.length; 99 | }; 100 | 101 | /** 102 | * Remove a key and the value stored there 103 | */ 104 | Dirty.prototype.rm = function(key, cb) { 105 | this.set(key, undefined, cb); 106 | }; 107 | 108 | 109 | /** 110 | * Iterate over keys, applying match function 111 | */ 112 | Dirty.prototype.forEach = function(fn) { 113 | for (var key in this._docs) { 114 | if (fn(key, this._docs[key]) === false) { 115 | break; 116 | } 117 | } 118 | }; 119 | 120 | 121 | // Called when a dirty connection is instantiated 122 | Dirty.prototype._load = function() { 123 | var self = this, buffer = '', length = 0; 124 | 125 | if (!this.path) { 126 | process.nextTick(function() { 127 | self.emit('load', 0); 128 | }); 129 | return; 130 | } 131 | 132 | this._readStream = fs.createReadStream(this.path, { 133 | encoding: 'utf-8', 134 | flags: 'r' 135 | }); 136 | 137 | this._readStream 138 | .on('error', function(err) { 139 | if (err.code === 'ENOENT') { 140 | self.emit('load', 0); 141 | return; 142 | } 143 | 144 | self.emit('error', err); 145 | }) 146 | .on('data', function(chunk) { 147 | buffer += chunk; 148 | if (chunk.lastIndexOf('\n') == -1) return; 149 | var arr = buffer.split('\n'); 150 | buffer = arr.pop(); 151 | arr.forEach(function(rowStr) { 152 | if (!rowStr) { 153 | self.emit('error', new Error('Empty lines never appear in a healthy database')); 154 | return; 155 | } 156 | 157 | var row; 158 | try { 159 | row = JSON.parse(rowStr); 160 | if (!('key' in row)) { 161 | throw new Error(); 162 | } 163 | } catch (e) { 164 | self.emit('error', new Error('Could not load corrupted row: '+rowStr)); 165 | return ''; 166 | } 167 | self._updateDocs(row.key, row.val); 168 | return ''; 169 | }); 170 | }) 171 | .on('end', function() { 172 | if (buffer.length) { 173 | self.emit('error', new Error('Corrupted row at the end of the db: '+buffer)); 174 | } 175 | self.emit('load', self._length); 176 | }); 177 | this._recreateWriteStream(); 178 | }; 179 | 180 | Dirty.prototype._recreateWriteStream = function(){ 181 | var self = this; 182 | this._writeStream = fs.createWriteStream(this.path, { 183 | encoding: 'utf-8', 184 | flags: 'a' 185 | }); 186 | 187 | this._writeStream.on('drain', function() { 188 | self._writeDrain(); 189 | }); 190 | }; 191 | 192 | Dirty.prototype._writeDrain = function() { 193 | this.flushing = false; 194 | 195 | if (!this._queue.length) { 196 | this.emit('drain'); 197 | } else { 198 | this._maybeFlush(); 199 | } 200 | }; 201 | 202 | Dirty.prototype._maybeFlush = function() { 203 | if (this.flushing || !this._queue.length || this.compacting) { 204 | return; 205 | } 206 | this._flush(); 207 | }; 208 | 209 | Dirty.prototype._flush = function() { 210 | var self = this, 211 | length = this._queue.length, 212 | bundleLength = 0, 213 | bundleStr = '', 214 | key, 215 | cbs = []; 216 | this.flushing = true; 217 | 218 | function callbacks(err, cbs) { 219 | while (cbs.length) { 220 | cbs.shift()(err); 221 | } 222 | } 223 | 224 | for (var i = 0; i < length; i++) { 225 | key = this._queue[i]; 226 | if (Array.isArray(key)) { 227 | cbs.push(key[1]); 228 | key = key[0]; 229 | } 230 | 231 | bundleStr += JSON.stringify({key: key, val: this._docs[key]})+'\n'; 232 | bundleLength++; 233 | 234 | if (bundleLength < this.writeBundle && i < length - 1) { 235 | continue; 236 | } 237 | 238 | (function(cbs) { 239 | var isDrained; 240 | 241 | if (!self.path) { 242 | process.nextTick(function() { 243 | callbacks(null, cbs); 244 | self._writeDrain(); 245 | }); 246 | return; 247 | } 248 | 249 | isDrained = self._writeStream.write(bundleStr, function(err) { 250 | if (isDrained) { 251 | self._writeDrain(); 252 | } 253 | 254 | if (!cbs.length && err) { 255 | self.emit('error', err); 256 | return; 257 | } 258 | 259 | callbacks(err, cbs); 260 | }); 261 | 262 | })(cbs); 263 | 264 | bundleStr = ''; 265 | bundleLength = 0; 266 | cbs = []; 267 | } 268 | 269 | this._queue = []; 270 | }; 271 | 272 | Dirty.prototype.__defineGetter__("_compactPath", function() { 273 | return this.path + ".compact"; 274 | }); 275 | 276 | Dirty.prototype.__defineGetter__('length', function(){ 277 | return this._length; 278 | }); 279 | 280 | Dirty.prototype.__defineGetter__('redundantLength', function(){ 281 | return this._redundantLength; 282 | }); 283 | 284 | Dirty.prototype.compact = function(cb) { 285 | if (this.compacting) return; 286 | var self = this; 287 | if (this.flushing) { 288 | this.once('drain', function(){ 289 | this.compact(cb); 290 | }); 291 | } else { 292 | this.compacting = true; 293 | this._startCompacting(); 294 | } 295 | }; 296 | 297 | Dirty.prototype._startCompacting = function() { 298 | var self = this; 299 | this._queueBackup = this._queue; 300 | this._queue = []; 301 | this._redundantLengthBackup = this._redundantLength; 302 | this._redundantLength = 0; 303 | var ws = fs.createWriteStream(this._compactPath, { 304 | encoding: 'utf-8', 305 | flags: 'w' 306 | }); 307 | ws.on("error", function(){ 308 | self.emit('compactingError'); 309 | }); 310 | this._writeCompactedData(ws); 311 | }; 312 | 313 | Dirty.prototype._moveCompactedDataOverOriginal = function() { 314 | var self = this; 315 | fs.rename(this._compactPath, this.path, function(err){ 316 | self._recreateWriteStream(); 317 | if (err) self.emit('compactingError'); 318 | else self.emit('compacted'); 319 | }); 320 | } 321 | 322 | Dirty.prototype._endCompacting = function() { 323 | this._queueBackup = []; 324 | this._redundantLengthBackup = 0; 325 | this.compacting = false; 326 | this._maybeFlush(); 327 | }; 328 | 329 | Dirty.prototype._writeCompactedData = function(ws) { 330 | var keys = []; 331 | var self = this; 332 | for (var k in this._docs) { keys.push(k); } 333 | 334 | var writeToStream = function() { 335 | if (keys.length === 0) { 336 | ws.once('finish', function(){ 337 | self._writeStream.once('finish', function(){ 338 | self._moveCompactedDataOverOriginal(); 339 | }); 340 | self._writeStream.end(); 341 | }) 342 | ws.end(); 343 | return; 344 | } 345 | var bundleStr = buildBundle(); 346 | var isDrained = ws.write(bundleStr); 347 | if (isDrained) { 348 | process.nextTick(writeToStream); 349 | } else { 350 | ws.once('drain', writeToStream); 351 | } 352 | } 353 | 354 | var buildBundle = function() { 355 | var bundleLength = 0, 356 | bundleStr = ''; 357 | var compact = (typeof self._compactingFilter == "function"); 358 | for (var i=0; i< keys.length; i++) { 359 | var doc = self._docs[keys[i]]; 360 | if (compact && self._compactingFilter(keys[i],doc)) { 361 | self._updateDocs(keys[i], undefined, true); 362 | continue; 363 | } 364 | bundleStr += JSON.stringify({key: keys[i], val: doc})+'\n'; 365 | bundleLength++; 366 | if (bundleLength >= self.writeBundle) { 367 | keys = keys.slice(i+1); 368 | return bundleStr; 369 | } 370 | } 371 | keys = []; 372 | return bundleStr; 373 | } 374 | writeToStream(); 375 | }; 376 | 377 | Dirty.prototype.setCompactingFilter = function(filter) { 378 | this._compactingFilter = filter; 379 | }; 380 | 381 | Dirty.prototype.addIndex = function(index, indexFn) { 382 | this._indexFns[index] = {indexFn: indexFn, keyMap: {}}; 383 | }; 384 | 385 | Dirty.prototype._deleteKeyFromIndexedKeys = function(keyMap, indexValues, key) { 386 | indexValues.forEach(function(indexValue){ 387 | var keys = keyMap[indexValue]; 388 | keys.remove(key) 389 | if (keys.empty()) delete keyMap[indexValue]; 390 | }); 391 | }; 392 | 393 | Dirty.prototype._addKeyToIndexedKeys = function(keyMap, indexValues, key) { 394 | indexValues.forEach(function(indexValue){ 395 | var keys = keyMap[indexValue] || new Set(); 396 | keys.push(key); 397 | keyMap[indexValue] = keys; 398 | }) 399 | }; 400 | 401 | Dirty.prototype._updateIndex = function(index, key, newVal) { 402 | var indexFn = this._indexFns[index].indexFn; 403 | var keyMap = this._indexFns[index].keyMap; 404 | if (key in this._docs) { 405 | var oldIndexValues = indexFn(key, this._docs[key]); 406 | if (newVal != undefined) { 407 | var newIndexValues = indexFn(key, newVal); 408 | var indexesToDeleteFrom = new Set(oldIndexValues).difference(new Set(newIndexValues)).toArray(); 409 | var indexesToAddTo = new Set(newIndexValues).difference(new Set(oldIndexValues)).toArray(); 410 | this._deleteKeyFromIndexedKeys(keyMap, indexesToDeleteFrom, key); 411 | this._addKeyToIndexedKeys(keyMap, indexesToAddTo, key); 412 | } else this._deleteKeyFromIndexedKeys(keyMap, oldIndexValues, key); 413 | } else { 414 | if (newVal != undefined) { 415 | var newIndexValues = indexFn(key, newVal); 416 | this._addKeyToIndexedKeys(keyMap, newIndexValues, key); 417 | } 418 | } 419 | }; 420 | 421 | Dirty.prototype._updateIndexes = function(key, newVal) { 422 | for (var index in this._indexFns) { 423 | this._updateIndex(index, key, newVal); 424 | }; 425 | }; 426 | 427 | Dirty.prototype.find = function(index, value) { 428 | var self = this; 429 | var validKeys = this._indexFns[index].keyMap[value]; 430 | return !validKeys ? [] : 431 | validKeys.toArray().map(function(k){ 432 | return {key: k, val: self.get(k)}; 433 | }); 434 | }; 435 | 436 | Dirty.prototype.indexValues = function(index) { 437 | var keys = []; 438 | for (var k in this._indexFns[index].keyMap){ 439 | keys.push(k); 440 | } 441 | return keys; 442 | }; 443 | -------------------------------------------------------------------------------- /lib/downloadHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | var downloadHanlder = function downloadHanlder() { 10 | 11 | 12 | var nconf = require("nconf"), 13 | axdcc = require("axdcc"), 14 | logger = require("./xdcclogger"), 15 | packdb = require("./packdb"), 16 | nodefs = require("node-fs"); 17 | 18 | 19 | var homePath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; 20 | var appHome = homePath+'/.slingxdcc/'; 21 | 22 | nconf.add('settings', {type: 'file', file: appHome+'config/settings.json'}); 23 | 24 | nconf.defaults({ 25 | "downloadHandler": { 26 | "destination": appHome+"downloads/", 27 | "resumeDownloads": true, 28 | "refreshInterval": 1 29 | }, 30 | "downloads": {} 31 | }); 32 | nconf.set('downloadHandler',nconf.get('downloadHandler')); 33 | nodefs.mkdirSync(nconf.get('downloadHandler:destination'),0777,true); 34 | nconf.save(); 35 | 36 | var interval = nconf.get('downloadHandler:refreshInterval'); 37 | var requests = {}; 38 | var dlQueues = nconf.get('downloads'); 39 | var self = this; 40 | 41 | var notifications = []; 42 | 43 | for (var server in dlQueues) { 44 | if (dlQueues.hasOwnProperty(server)) { 45 | var attr = dlQueues[server]; 46 | for (var nick in attr) { 47 | if (attr.hasOwnProperty(nick)) { 48 | var queue = attr[nick]; 49 | if (queue.length > 0) { 50 | createRequest(queue[0]); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | this.startDownload = function (packObj) { 58 | if (typeof dlQueues[packObj.server] === "undefined") { 59 | dlQueues[packObj.server] = {}; 60 | requests[packObj.server] = {}; 61 | } 62 | if (typeof dlQueues[packObj.server][packObj.nick] === "undefined") { 63 | dlQueues[packObj.server][packObj.nick] = []; 64 | requests[packObj.server][packObj.nick] = {}; 65 | } 66 | 67 | if (downloadQueuePosition(packObj) == -1) { 68 | packObj.progress = 0; 69 | packObj.lastProgress = Date.now(); 70 | packObj.received = 0; 71 | dlQueues[packObj.server][packObj.nick].push(packObj); 72 | if (downloadQueuePosition(packObj) == 0) { 73 | createRequest(packObj); 74 | } 75 | nconf.set('downloads', dlQueues); 76 | nconf.save(); 77 | return true; 78 | } 79 | return false; 80 | }; 81 | 82 | this.cancelDownload = function (packObj) { 83 | if (!validpack(packObj)) 84 | return false; 85 | 86 | dequeueDownload(packObj); 87 | return true; 88 | }; 89 | 90 | this.getDownloads = function () { 91 | return dlQueues; 92 | }; 93 | 94 | this.upqueue = function (packObj) { 95 | if (!validpack(packObj)) { 96 | return false; 97 | } 98 | 99 | var oldindex = downloadQueuePosition(packObj); 100 | var maxindex = dlQueues[packObj.server][packObj.nick].length - 1; 101 | 102 | if (oldindex >= maxindex) { 103 | return false; 104 | } 105 | 106 | var old = dlQueues[packObj.server][packObj.nick][oldindex]; 107 | dlQueues[packObj.server][packObj.nick][oldindex] = dlQueues[packObj.server][packObj.nick][oldindex + 1]; 108 | dlQueues[packObj.server][packObj.nick][oldindex + 1] = old; 109 | nconf.set('downloads', dlQueues); 110 | nconf.save(); 111 | return true; 112 | }; 113 | 114 | this.downqueue = function (packObj) { 115 | if (!validpack(packObj)) { 116 | return false; 117 | } 118 | 119 | var oldindex = downloadQueuePosition(packObj); 120 | 121 | if (oldindex <= 1) { 122 | return false; 123 | } 124 | 125 | var old = dlQueues[packObj.server][packObj.nick][oldindex]; 126 | dlQueues[packObj.server][packObj.nick][oldindex] = dlQueues[packObj.server][packObj.nick][oldindex - 1]; 127 | dlQueues[packObj.server][packObj.nick][oldindex - 1] = old; 128 | nconf.set('downloads', dlQueues); 129 | nconf.save(); 130 | return true; 131 | }; 132 | 133 | this.getNotifications = function(){ 134 | return notifications; 135 | }; 136 | 137 | this.clearNotifications = function(){ 138 | notifications = []; 139 | }; 140 | 141 | this.exit = function(){ 142 | for (var server in requests) { 143 | if (requests.hasOwnProperty(server)) { 144 | var attr = requests[server]; 145 | for (var nick in attr) { 146 | if (attr.hasOwnProperty(nick)) { 147 | var request = attr[nick].request; 148 | request.emit("cancel") 149 | } 150 | } 151 | } 152 | } 153 | }; 154 | 155 | function createRequest(packObj) { 156 | if (typeof requests[packObj.server] === "undefined") { 157 | requests[packObj.server] = {}; 158 | } 159 | if (typeof requests[packObj.server][packObj.nick] === "undefined") { 160 | requests[packObj.server][packObj.nick] = {}; 161 | } 162 | var notification; 163 | requests[packObj.server][packObj.nick].connectHandler = function (pack) { 164 | if (pack.filename.replace(/\s/g, '').toLowerCase() != packObj.filename.replace(/\s/g, '').toLowerCase()) { 165 | 166 | packdb.addPack({ 167 | server: packObj.server, 168 | nick: packObj.nick, 169 | nr: parseInt(packObj.nr), 170 | downloads: 0, 171 | filesize: parseInt(packObj.realsize), 172 | filename: pack.filename, 173 | lastseen: new Date().getTime() 174 | }); 175 | 176 | notification = { 177 | packObj: { 178 | server: packObj.server, 179 | nick: packObj.nick, 180 | nr: packObj.nr, 181 | filename: packObj.filename 182 | }, 183 | error: "filename mismatch", 184 | gotFile: pack.filename, 185 | time: new Date().getTime() 186 | }; 187 | 188 | self.emit('dlerror', notification); 189 | notification.type = "dlerror"; 190 | notifications.push(notification); 191 | 192 | requests[packObj.server][packObj.nick].request.emit("cancel"); 193 | dequeueDownload(packObj); 194 | } else { 195 | dlQueues[packObj.server][packObj.nick][0].realsize = pack.filesize; 196 | 197 | notification = { 198 | packObj: { 199 | server: packObj.server, 200 | nick: packObj.nick, 201 | nr: packObj.nr, 202 | filename: packObj.filename, 203 | realsize: parseInt(packObj.realsize) 204 | }, 205 | time: new Date().getTime() 206 | }; 207 | 208 | self.emit('dlstart', notification); 209 | notification.type = "dlstart"; 210 | notifications.push(notification); 211 | } 212 | }; 213 | 214 | requests[packObj.server][packObj.nick].progressHandler = function (pack, received) { 215 | 216 | var receivedDelta = received - packObj.received; 217 | 218 | packObj.received = received; 219 | 220 | self.emit('dlprogress', {packObj: { 221 | server: packObj.server, 222 | nick: packObj.nick, 223 | nr: packObj.nr, 224 | speed: parseInt(receivedDelta / interval), 225 | received : packObj.received 226 | }}); 227 | 228 | }; 229 | 230 | requests[packObj.server][packObj.nick].completeHandler = function (pack) { 231 | notification = { 232 | packObj: { 233 | server: packObj.server, 234 | nick: packObj.nick, 235 | nr: packObj.nr, 236 | filename: packObj.filename 237 | }, 238 | time: new Date().getTime() 239 | }; 240 | 241 | self.emit('dlsuccess', notification); 242 | notification.type = "dlsuccess"; 243 | notifications.push(notification); 244 | dequeueDownload(packObj); 245 | }; 246 | 247 | requests[packObj.server][packObj.nick].errorHandler = function (pack, error) { 248 | notification = { 249 | packObj: { 250 | server: packObj.server, 251 | nick: packObj.nick, 252 | nr: packObj.nr, 253 | received : pack.received, 254 | filename: packObj.filename 255 | }, 256 | error: error, 257 | time: new Date().getTime() 258 | }; 259 | 260 | self.emit('dlerror', notification); 261 | notification.type = "dlerror"; 262 | notifications.push(notification); 263 | 264 | requests[packObj.server][packObj.nick].request.emit("cancel"); 265 | dequeueDownload(packObj); 266 | }; 267 | 268 | if (typeof logger.getIrcServers()[packObj.server] === "undefined" || logger.getIrcServers()[packObj.server].connected == false) { 269 | function startPending(srvKey){ 270 | if (srvKey == packObj.server) { 271 | startRequest(packObj); 272 | } 273 | } 274 | 275 | logger.on("irc_connected", startPending); 276 | self.on("dequeuePending#"+packObj.server+"#"+packObj.nick+"#"+packObj.nr,function(){ 277 | logger.removeListener("irc_connected", startPending); 278 | self.removeAllListeners("dequeuePending#"+packObj.server+"#"+packObj.nick+"#"+packObj.nr); 279 | }); 280 | } else { 281 | startRequest(packObj); 282 | } 283 | 284 | 285 | function startRequest(packObj) { 286 | requests[packObj.server][packObj.nick].request = new axdcc.Request(logger.getIrcServer(packObj.server), { 287 | pack: packObj.nr, 288 | nick: packObj.nick, 289 | path: nconf.get('downloadHandler:destination'), 290 | resume: nconf.get('downloadHandler:resumeDownloads'), 291 | progressInterval: interval 292 | }) 293 | .once("dlerror", requests[packObj.server][packObj.nick].errorHandler) 294 | .once("connect", requests[packObj.server][packObj.nick].connectHandler) 295 | .on("progress", requests[packObj.server][packObj.nick].progressHandler) 296 | .once("complete", requests[packObj.server][packObj.nick].completeHandler); 297 | 298 | requests[packObj.server][packObj.nick].request.emit("start"); 299 | } 300 | 301 | } 302 | 303 | function dequeueDownload(packObj) { 304 | var index = downloadQueuePosition(packObj); 305 | if (index != -1) { 306 | 307 | if (index == 0) { 308 | dlQueues[packObj.server][packObj.nick].shift(); 309 | if(typeof requests[packObj.server][packObj.nick].request !== "undefined"){ 310 | requests[packObj.server][packObj.nick].request.emit("cancel"); 311 | requests[packObj.server][packObj.nick].request.removeAllListeners(); 312 | delete requests[packObj.server][packObj.nick]; 313 | if (dlQueues[packObj.server][packObj.nick].length > 0) { 314 | createRequest(dlQueues[packObj.server][packObj.nick][0]); 315 | } 316 | }else{ 317 | self.emit("dequeuePending#"+packObj.server+"#"+packObj.nick+"#"+packObj.nr); 318 | } 319 | } else { 320 | dlQueues[packObj.server][packObj.nick].splice(index, 1); 321 | } 322 | 323 | 324 | if (dlQueues[packObj.server][packObj.nick].length == 0) 325 | delete dlQueues[packObj.server][packObj.nick]; 326 | if (Object.keys(dlQueues[packObj.server]).length == 0) 327 | delete dlQueues[packObj.server]; 328 | 329 | nconf.set('downloads', dlQueues); 330 | nconf.save(); 331 | } 332 | 333 | 334 | } 335 | 336 | function downloadQueuePosition(packObj) { 337 | var length = dlQueues[packObj.server][packObj.nick].length, 338 | element = null; 339 | for (var i = 0; i < length; i++) { 340 | element = dlQueues[packObj.server][packObj.nick][i]; 341 | if (element.nr == packObj.nr) { 342 | return i; 343 | } 344 | } 345 | return -1; 346 | } 347 | 348 | function validpack(packObj) { 349 | return !(typeof dlQueues[packObj.server] === "undefined" || typeof dlQueues[packObj.server][packObj.nick] === "undefined"); 350 | } 351 | 352 | if (downloadHanlder.caller != downloadHanlder.getInstance) { 353 | throw new Error("This object cannot be instantiated"); 354 | } 355 | }; 356 | 357 | 358 | /* ************************************************************************ 359 | SINGLETON CLASS DEFINITION 360 | ************************************************************************ */ 361 | downloadHanlder.instance = null; 362 | 363 | /** 364 | * Singleton getInstance definition 365 | * @return singleton class 366 | */ 367 | downloadHanlder.getInstance = function () { 368 | if (this.instance === null) { 369 | this.instance = new downloadHanlder(); 370 | } 371 | return this.instance; 372 | }; 373 | downloadHanlder.prototype = Object.create(require("events").EventEmitter.prototype); 374 | module.exports = downloadHanlder.getInstance(); 375 | -------------------------------------------------------------------------------- /lib/xdcclogger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Daniel Varga 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | var xdcclogger = function xdcclogger() { 10 | //defining a var instead of this (works for variable & function) will create a private definition 11 | var dirty = require("./dirty/dirty"), 12 | query = require("dirty-query").query, 13 | irc = require("irc"), 14 | packdb = require("./packdb"), 15 | nconf = require("nconf"), 16 | log4js = require('log4js'), 17 | logger = log4js.getLogger('xdcclogger'); 18 | 19 | var homePath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; 20 | var appHome = homePath+'/.slingxdcc/'; 21 | 22 | nconf.add('settings', {type: 'file', file: appHome+'config/settings.json'}); 23 | 24 | nconf.defaults({ 25 | "logger": { 26 | "packRegex": "#(\\d+)\\s+(\\d+)x\\s+\\[\\s*[><]?([0-9\\.]+)([TGMKtgmk]?)\\]\\s+(.*)" 27 | } 28 | }); 29 | nconf.set('logger',nconf.get('logger')); 30 | nconf.save(); 31 | 32 | var onlineServers = []; 33 | 34 | var ircServers = {}; 35 | 36 | var packRegex = new RegExp(nconf.get('logger:packRegex')); 37 | 38 | init(); 39 | 40 | var self = this; 41 | 42 | self.on('irc_connected', function (key) { 43 | logger.debug(key, 'connected'); 44 | }); 45 | 46 | 47 | this.addServer = function (srvkey, options) { 48 | //if connection already exists remove it 49 | if (typeof ircServers[srvkey] !== "undefined") { 50 | this.removeServer(srvkey); 51 | } 52 | 53 | var observchannels = (options.observchannels ? options.observchannels : []); 54 | var channels = (options.channels ? options.channels : []); 55 | //create new irc Client 56 | var iclient = new irc.Client(options.host, options.nick, { 57 | userName: options.nick, 58 | realName: options.nick, 59 | port: options.port, 60 | channels: channels, // is read only and not managed 61 | debug: false, 62 | stripColors: true 63 | }); 64 | if (typeof ircServers[srvkey] !== "undefined") { 65 | self.removeServer(srvkey); 66 | } 67 | ircServers[srvkey] = iclient; 68 | ircServers[srvkey].observchannels = observchannels; 69 | ircServers[srvkey].channels = channels; 70 | ircServers[srvkey].error = []; 71 | //register error Handler 72 | iclient.on("error", function (message) { 73 | ircServers[srvkey].error.push({ 74 | message: message, 75 | errtime: new Date().getTime() 76 | }); 77 | self.emit("irc_error", srvkey); 78 | }); 79 | //make persistent 80 | nconf.set('logger:servers:' + srvkey, { 81 | host: options.host, 82 | port: options.port, 83 | nick: options.nick, 84 | channels: channels, 85 | observchannels: observchannels 86 | }); 87 | nconf.save(); 88 | iclient.once("registered", function () { 89 | //remember serverkey 90 | onlineServers.push(srvkey); 91 | onlineServers = uniqueArray(onlineServers); 92 | packdb.onServerKeys = onlineServers; 93 | 94 | //observ the channels 95 | self.observChannels(srvkey, observchannels); 96 | self.emit("irc_connected", srvkey); 97 | }); 98 | }; 99 | 100 | 101 | this.removeServer = function (srvkey) { 102 | //if connection exists 103 | if (typeof ircServers[srvkey] !== "undefined") { 104 | //disconnect & remove all listeners 105 | ircServers[srvkey].disconnect(); 106 | ircServers[srvkey].removeAllListeners(); 107 | delete ircServers[srvkey]; 108 | //remove it from db 109 | onlineServers = removeArrayItem(onlineServers, srvkey); 110 | packdb.onServerKeys = onlineServers; 111 | nconf.clear('logger:servers:' + srvkey); 112 | nconf.save(); 113 | } 114 | }; 115 | 116 | this.joinChannels = function (srvkey, channels) { 117 | //if connection exists 118 | if (typeof ircServers[srvkey] !== "undefined") { 119 | channels.forEach(function (channel, i) { 120 | channel = channel.toLowerCase(); 121 | logger.debug('Join channel', channel, 'on server', srvkey); 122 | //join the channel 123 | 124 | ircServers[srvkey].join(channel); 125 | //modify the channels array 126 | ircServers[srvkey].channels.push(channel); 127 | }); 128 | ircServers[srvkey].channels = uniqueArray(ircServers[srvkey].channels); 129 | 130 | //get the old settings from db 131 | var tmpServer = nconf.get('logger:servers:' + srvkey); 132 | 133 | if (typeof tmpServer.channels === "undefined") { 134 | tmpServer.channels = []; 135 | } 136 | tmpServer.channels = clone(ircServers[srvkey].channels); 137 | //make persistent 138 | nconf.set('logger:servers:' + srvkey, tmpServer); 139 | nconf.save(); 140 | } 141 | }; 142 | 143 | this.partChannels = function (srvkey, channels) { 144 | //if connection exists 145 | if (typeof ircServers[srvkey] !== "undefined") { 146 | channels.forEach(function (channel, i) { 147 | channel = channel.toLowerCase(); 148 | logger.debug('Part channel', channel, 'on server', srvkey); 149 | //unregister listener 150 | ircServers[srvkey].removeAllListeners("message" + channel); 151 | //modify the channels array 152 | ircServers[srvkey].observchannels = removeArrayItem(ircServers[srvkey].observchannels, channel); 153 | //part/leave the channel 154 | ircServers[srvkey].part(channel); 155 | //modify the channels array 156 | ircServers[srvkey].channels = removeArrayItem(ircServers[srvkey].channels, channel); 157 | }); 158 | 159 | //get the old settings from db 160 | var tmpServer = nconf.get('logger:servers:' + srvkey); 161 | tmpServer.channels = clone(ircServers[srvkey].channels); 162 | tmpServer.observchannels = clone(ircServers[srvkey].observchannels); 163 | //make persistent 164 | nconf.set('logger:servers:' + srvkey, tmpServer); 165 | nconf.save(); 166 | } 167 | }; 168 | 169 | this.observChannels = function (srvkey, channels) { 170 | //if connection exists 171 | if (typeof ircServers[srvkey] !== "undefined") { 172 | channels.forEach(function (channel, i) { 173 | channel = channel.toLowerCase(); 174 | logger.debug('Register listener for channel', channel, 'on server', srvkey); 175 | //register listener 176 | ircServers[srvkey].on("message" + channel, function (nick, text, message) { 177 | logPack(nick, text, srvkey); 178 | }); 179 | //modify the channels array 180 | ircServers[srvkey].observchannels.push(channel); 181 | }); 182 | ircServers[srvkey].observchannels = uniqueArray(ircServers[srvkey].observchannels); 183 | //get the old settings from db 184 | var tmpServer = nconf.get('logger:servers:' + srvkey); 185 | tmpServer.observchannels = ircServers[srvkey].observchannels; 186 | //make persistent 187 | nconf.set('logger:servers:' + srvkey, tmpServer); 188 | nconf.save(); 189 | } 190 | }; 191 | 192 | this.unobservChannels = function (srvkey, channels) { 193 | //if connection exists 194 | if (typeof ircServers[srvkey] !== "undefined") { 195 | channels.forEach(function (channel, i) { 196 | channel = channel.toLowerCase(); 197 | logger.debug('Deregister listener for channel', channel, 'on server', srvkey); 198 | //unregister listener 199 | ircServers[srvkey].removeAllListeners("message" + channel); 200 | //modify the channels array 201 | ircServers[srvkey].observchannels = removeArrayItem(ircServers[srvkey].observchannels, channel); 202 | }); 203 | //get the old settings from db 204 | var tmpServer = nconf.get('logger:servers:' + srvkey); 205 | tmpServer.observchannels = ircServers[srvkey].observchannels; 206 | //make persistent 207 | nconf.set('logger:servers:' + srvkey, tmpServer); 208 | nconf.save(); 209 | } 210 | }; 211 | 212 | this.getIrcServer = function (srvkey) { 213 | if (typeof ircServers[srvkey] !== "undefined") { 214 | return ircServers[srvkey]; 215 | } 216 | return null; 217 | }; 218 | 219 | this.getIrcServers = function () { 220 | var servers = {}; 221 | for (var i in ircServers) { 222 | servers[i] = { 223 | host: ircServers[i].opt.server, 224 | port: ircServers[i].opt.port, 225 | nick: ircServers[i].opt.nick, 226 | channels: ircServers[i].opt.channels, 227 | observchannels: ircServers[i].observchannels, 228 | motd: ircServers[i].motd, 229 | connected: (onlineServers.indexOf(i) != -1), 230 | key: i, 231 | error: ircServers[i].error 232 | } 233 | } 234 | return servers; 235 | }; 236 | 237 | this.exit = function(){ 238 | for (var key in ircServers) { 239 | ircServers[key].disconnect(); 240 | } 241 | }; 242 | 243 | function logPack(nick, text, srvkey) { 244 | 245 | if (text.charAt(0) != '#') return; 246 | var packinfo = text.match(packRegex); 247 | if (packinfo !== null) { 248 | var suffix = 1; 249 | switch (packinfo[4].toUpperCase()){ 250 | case "T": 251 | suffix *= 1024; 252 | case "G": 253 | suffix *= 1024; 254 | case "M": 255 | suffix *= 1024; 256 | case "K": 257 | suffix *= 1024; 258 | } 259 | //logger.debug('Add pack to db', nick, packinfo[5]); 260 | packdb.addPack({ 261 | server: srvkey, 262 | nick: nick, 263 | nr: parseInt(packinfo[1]), 264 | downloads: parseInt(packinfo[2]), 265 | filesize: parseInt(packinfo[3]*suffix), 266 | filename: packinfo[5], 267 | lastseen: new Date().getTime() 268 | }); 269 | } 270 | } 271 | 272 | function init() { 273 | function join(srvkey, srv) { 274 | if (typeof srv !== "undefined") { 275 | var observchannels = (srv.observchannels ? srv.observchannels : []); 276 | //create new irc Client 277 | var iclient = new irc.Client(srv.host, srv.nick, { 278 | userName: srv.nick, 279 | realName: srv.nick, 280 | port: srv.port, 281 | channels: srv.channels, 282 | debug: false, 283 | stripColors: true 284 | }); 285 | ircServers[srvkey] = iclient; 286 | ircServers[srvkey].observchannels = observchannels; 287 | ircServers[srvkey].channels = srv.channels; 288 | ircServers[srvkey].error = []; 289 | //register error Handler 290 | iclient.on("error", function (message) { 291 | ircServers[srvkey].error.push({ 292 | message: message, 293 | errtime: new Date().getTime() 294 | }); 295 | self.emit("irc_error", srvkey); 296 | }); 297 | iclient.once("registered", function () { 298 | //remember serverkey 299 | onlineServers.push(srvkey); 300 | onlineServers = uniqueArray(onlineServers); 301 | packdb.onServerKeys = onlineServers; 302 | //observ the channels 303 | srv.observchannels.forEach(function (channel, i) { 304 | logger.info('Register listener for channel', channel, 'on server', srvkey); 305 | //register listener 306 | ircServers[srvkey].on("message" + channel, function (nick, text, message) { 307 | logPack(nick, text, srvkey); 308 | }); 309 | }); 310 | self.emit("irc_connected", srvkey); 311 | }); 312 | } 313 | } 314 | 315 | var servers = nconf.get('logger:servers'); 316 | // logger.info('Found servers', servers); 317 | for (var srvkey in servers) { 318 | join(clone(srvkey), clone(servers[srvkey])); 319 | 320 | } 321 | } 322 | 323 | function uniqueArray(array) { 324 | var u = {}, a = []; 325 | for (var i = 0, l = array.length; i < l; ++i) { 326 | if (u.hasOwnProperty(array[i])) { 327 | continue; 328 | } 329 | a.push(array[i]); 330 | u[array[i]] = 1; 331 | } 332 | return a; 333 | } 334 | 335 | function removeArrayItem(array, item) { 336 | var id = array.indexOf(item); 337 | if (id != -1) array.splice(id, 1); 338 | return array; 339 | } 340 | 341 | function clone(obj) { 342 | // Handle the 3 simple types, and null or undefined 343 | if (null == obj || "object" != typeof obj) return obj; 344 | 345 | // Handle Date 346 | if (obj instanceof Date) { 347 | var copy = new Date(); 348 | copy.setTime(obj.getTime()); 349 | return copy; 350 | } 351 | 352 | // Handle Array 353 | if (obj instanceof Array) { 354 | var copy = []; 355 | for (var i = 0, len = obj.length; i < len; i++) { 356 | copy[i] = clone(obj[i]); 357 | } 358 | return copy; 359 | } 360 | 361 | // Handle Object 362 | if (obj instanceof Object) { 363 | var copy = {}; 364 | for (var attr in obj) { 365 | if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); 366 | } 367 | return copy; 368 | } 369 | 370 | throw new Error("Unable to copy obj! Its type isn't supported."); 371 | } 372 | 373 | if (xdcclogger.caller != xdcclogger.getInstance) { 374 | throw new Error("This object cannot be instantiated"); 375 | } 376 | }; 377 | 378 | 379 | /* ************************************************************************ 380 | SINGLETON CLASS DEFINITION 381 | ************************************************************************ */ 382 | xdcclogger.instance = null; 383 | 384 | /** 385 | * Singleton getInstance definition 386 | * @return singleton class 387 | */ 388 | xdcclogger.getInstance = function () { 389 | if (this.instance === null) { 390 | this.instance = new xdcclogger(); 391 | } 392 | return this.instance; 393 | }; 394 | xdcclogger.prototype = Object.create(require("events").EventEmitter.prototype); 395 | module.exports = xdcclogger.getInstance(); 396 | --------------------------------------------------------------------------------