web -> desktop

This commit is contained in:
Alexey Kasyanchuk
2018-01-31 19:02:28 +03:00
parent 0e4888ab76
commit d8afce8964
95 changed files with 10679 additions and 1893 deletions

View File

@ -1,55 +0,0 @@
import React, { Component } from 'react';
import './app.css';
import './router';
import PagesPie from './pages-pie.js';
import registerServiceWorker from './registerServiceWorker';
import injectTapEventPlugin from 'react-tap-event-plugin';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
var io = require("socket.io-client");
window.torrentSocket = io(document.location.protocol + '//' + document.location.hostname + (process.env.NODE_ENV === 'production' ? '/' : ':8095/'));
// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
injectTapEventPlugin();
registerServiceWorker();
let loadersCount = 0;
let appReady = false;
window.customLoader = (func, onLoading, onLoaded) => {
loadersCount++;
if(onLoading) {
onLoading();
}
return (...args) => {
func(...args);
if(onLoaded) {
onLoaded();
}
loadersCount--;
}
};
window.isReady = () => {
return (appReady && loadersCount === 0)
}
class App extends Component {
componentDidMount() {
window.router()
appReady = true;
}
componentWillUnmount() {
appReady = false;
}
render() {
return (
<MuiThemeProvider>
<PagesPie />
</MuiThemeProvider>
);
}
}
export default App;

86
src/app/app.js Normal file
View File

@ -0,0 +1,86 @@
import React, { Component } from 'react';
import './app.css';
import './router';
import PagesPie from './pages-pie.js';
//import registerServiceWorker from './registerServiceWorker';
import injectTapEventPlugin from 'react-tap-event-plugin';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
const { ipcRenderer, remote } = require('electron');
//var io = require("socket.io-client");
//window.torrentSocket = io(document.location.protocol + '//' + document.location.hostname + (process.env.NODE_ENV === 'production' ? '/' : ':8095/'));
window.torrentSocket = {}
window.torrentSocket.callbacks = {}
window.torrentSocket.on = (name, func) => {
ipcRenderer.on(name, (event, data) => {
func(data)
});
}
window.torrentSocket.off = (name, func) => {
if(!func)
ipcRenderer.removeListener(name);
else
ipcRenderer.removeListener(name, func);
}
window.torrentSocket.emit = (name, ...data) => {
if(typeof data[data.length - 1] === 'function')
{
const id = Math.random().toString(36).substring(5)
window.torrentSocket.callbacks[id] = data[data.length - 1];
data[data.length - 1] = {callback: id}
}
ipcRenderer.send(name, data)
}
ipcRenderer.on('callback', (event, id, data) => {
const callback = window.torrentSocket.callbacks[id]
if(callback)
callback(data)
delete window.torrentSocket.callbacks[id]
});
// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
injectTapEventPlugin();
//registerServiceWorker();
let loadersCount = 0;
let appReady = false;
window.customLoader = (func, onLoading, onLoaded) => {
loadersCount++;
if(onLoading) {
onLoading();
}
return (...args) => {
func(...args);
if(onLoaded) {
onLoaded();
}
loadersCount--;
}
};
window.isReady = () => {
return (appReady && loadersCount === 0)
}
class App extends Component {
componentDidMount() {
window.router()
appReady = true;
}
componentWillUnmount() {
appReady = false;
}
render() {
return (
<MuiThemeProvider>
<PagesPie />
</MuiThemeProvider>
);
}
}
export default App;

View File

@ -22,7 +22,6 @@ export default (props) => {
C243.779,80.572,238.768,71.728,220.195,71.427z"/>
</svg>
<iframe data-aa='405459' src='//ad.a-ads.com/405459?size=468x60' scrolling='no' style={{width: '100%', height: '60px', border: '0px', padding: '0', overflow: 'hidden'}} allowTransparency='true'></iframe>
<div className='fs0-75 pad0-75'>Don't hesitate and visit the banners, we are trying to survive among dark blue sea</div>
</div>
)

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

@ -34,7 +34,7 @@ export default class IndexPage extends Page {
}
render() {
return (
<div>
<div id='index-window'>
<Header />
<Search />
<div className='column center w100p pad0-75'>

View File

@ -13,5 +13,5 @@ import './index.css';
ReactDOM.render(
<App />,
document.getElementById('root')
document.getElementById('mount-point')
);

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import InputRange from 'react-input-range';
import 'react-input-range/lib/css/index.css';
import './input-range.css';
import formatBytes from './format-bytes'
import SelectField from 'material-ui/SelectField';

83
src/app/input-range.css Normal file
View File

@ -0,0 +1,83 @@
.input-range__slider {
appearance: none;
background: #3f51b5;
border: 1px solid #3f51b5;
border-radius: 100%;
cursor: pointer;
display: block;
height: 1rem;
margin-left: -0.5rem;
margin-top: -0.65rem;
outline: none;
position: absolute;
top: 50%;
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;
width: 1rem; }
.input-range__slider:active {
transform: scale(1.3); }
.input-range__slider:focus {
box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2); }
.input-range--disabled .input-range__slider {
background: #cccccc;
border: 1px solid #cccccc;
box-shadow: none;
transform: none; }
.input-range__slider-container {
transition: left 0.3s ease-out; }
.input-range__label {
color: #aaaaaa;
font-family: "Helvetica Neue", san-serif;
font-size: 0.8rem;
transform: translateZ(0);
white-space: nowrap; }
.input-range__label--min,
.input-range__label--max {
bottom: -1.4rem;
position: absolute; }
.input-range__label--min {
left: 0; }
.input-range__label--max {
right: 0; }
.input-range__label--value {
position: absolute;
top: -1.8rem; }
.input-range__label-container {
left: -50%;
position: relative; }
.input-range__label--max .input-range__label-container {
left: 50%; }
.input-range__track {
background: #eeeeee;
border-radius: 0.3rem;
cursor: pointer;
display: block;
height: 0.3rem;
position: relative;
transition: left 0.3s ease-out, width 0.3s ease-out; }
.input-range--disabled .input-range__track {
background: #eeeeee; }
.input-range__track--background {
left: 0;
margin-top: -0.15rem;
position: absolute;
right: 0;
top: 50%; }
.input-range__track--active {
background: #3f51b5; }
.input-range {
height: 1rem;
position: relative;
width: 100%; }
/*# sourceMappingURL=input-range.css.map */

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import InputRange from 'react-input-range';
import 'react-input-range/lib/css/index.css';
import './input-range.css';
import formatBytes from './format-bytes'
import SelectField from 'material-ui/SelectField';

View File

@ -1,5 +1,4 @@
import router from 'page';
window.router = router;
//import router from 'page';
import PagesPie from './pages-pie.js';
import IndexPage from './index-page.js'
@ -8,6 +7,48 @@ import DMCAPage from './dmca-page.js'
import AdminPage from './admin-page.js'
import TopPage from './top-page.js'
let routers = {}
const router = (page, callback) => {
if(!callback)
{
if(!page)
routers['/'].callback()
else
{
const p = page.split('/')
const pg = routers[`${p[0]}/${p[1]}`]
if(!pg)
return
p.splice(0, 2)
const params = {}
for(let i = 0; i < p.length; i++)
{
params[pg.args[i]] = p[i]
}
console.log(params)
pg.callback({
params
})
}
return;
}
const p = page.split('/')
routers[`${p[0]}/${p[1]}`] = {callback}
routers[`${p[0]}/${p[1]}`].args = []
for(let i = 2; i < p.length; i++)
{
if(p[i].startsWith(':'))
routers[`${p[0]}/${p[1]}`].args.push(p[i].substring(1))
}
}
window.router = router;
router('/', () => {
//singleton
let pie = new PagesPie;

View File

@ -19,6 +19,37 @@ import formatBytes from './format-bytes'
let session;
class TorrentsStatistic extends Component {
constructor(props)
{
super(props)
this.stats = props.stats || {}
}
componentDidMount()
{
this.newTorrentFunc = (torrent) => {
this.stats.size += torrent.size;
this.stats.torrents++;
this.stats.files += torrent.files;
this.forceUpdate()
}
window.torrentSocket.on('newTorrent', this.newTorrentFunc);
}
componentWillUnmount()
{
if(this.newTorrentFunc)
window.torrentSocket.off('newTorrent', this.newTorrentFunc);
}
render()
{
return (
<div className='fs0-75 pad0-75' style={{color: 'rgba(0, 0, 0, 0.541176)'}}>you have information about {this.stats.torrents} torrents and around {this.stats.files} files and { formatBytes(this.stats.size, 1) } of data</div>
)
}
}
export default class Search extends Component {
constructor(props)
{
@ -299,10 +330,8 @@ export default class Search extends Component {
}
{
this.stats
?
<div className='fs0-75 pad0-75' style={{color: 'rgba(0, 0, 0, 0.541176)'}}>we have information about {this.stats.torrents} torrents and around {this.stats.files} files and { formatBytes(this.stats.size, 1) } of data</div>
:
null
&&
<TorrentsStatistic stats={this.stats} />
}
{
this.state.searchingIndicator

View File

@ -314,6 +314,10 @@ export default class TorrentPage extends Page {
target="_self"
label="Download"
secondary={true}
onClick={(e) => {
e.preventDefault();
window.open(`magnet:?xt=urn:btih:${this.torrent.hash}`, '_self')
}}
icon={<svg fill='white' viewBox="0 0 24 24"><path d="M17.374 20.235c2.444-2.981 6.626-8.157 6.626-8.157l-3.846-3.092s-2.857 3.523-6.571 8.097c-4.312 5.312-11.881-2.41-6.671-6.671 4.561-3.729 8.097-6.57 8.097-6.57l-3.092-3.842s-5.173 4.181-8.157 6.621c-2.662 2.175-3.76 4.749-3.76 7.24 0 5.254 4.867 10.139 10.121 10.139 2.487 0 5.064-1.095 7.253-3.765zm4.724-7.953l-1.699 2.111-1.74-1.397 1.701-2.114 1.738 1.4zm-10.386-10.385l1.4 1.738-2.113 1.701-1.397-1.74 2.11-1.699z"/></svg>}
/>
<div className='fs0-75 pad0-75 center column' style={{color: 'rgba(0, 0, 0, 0.541176)'}}><div>BTIH:</div><div>{this.torrent.hash}</div></div>

View File

@ -0,0 +1,311 @@
// This is main process of Electron, started as first thing when your
// app starts. It runs through entire life of your application.
// It doesn't have any windows which you can see on screen, but we can open
// window from here.
import path from "path";
import url from "url";
import { app, Menu, ipcMain, Tray } from "electron";
import { devMenuTemplate } from "./menu/dev_menu_template";
import { editMenuTemplate } from "./menu/edit_menu_template";
import createWindow from "./helpers/window";
// Special module holding environment variables which you declared
// in config/env_xxx.json file.
import env from "env";
import spiderCall from './spider'
const { spawn, exec } = require('child_process')
const fs = require('fs')
const setApplicationMenu = () => {
const menus = [editMenuTemplate];
if (env.name !== "production") {
menus.push(devMenuTemplate);
}
Menu.setApplicationMenu(Menu.buildFromTemplate(menus));
};
// Save userData in separate folders for each environment.
// Thanks to this you can use production and development versions of the app
// on same machine like those are two separate apps.
if (env.name !== "production") {
const userDataPath = app.getPath("userData");
app.setPath("userData", `${userDataPath} (${env.name})`);
}
let sphinx = undefined
let spider = undefined
const util = require('util');
if (!fs.existsSync(app.getPath("userData"))){
fs.mkdirSync(app.getPath("userData"));
}
const logFile = fs.createWriteStream(app.getPath("userData") + '/rats.log', {flags : 'w'});
const logStdout = process.stdout;
console.log = (...d) => {
logFile.write(util.format(...d) + '\n');
logStdout.write(util.format(...d) + '\n');
};
const getSphinxPath = () => {
if (fs.existsSync('./searchd')) {
return './searchd'
}
if (/^win/.test(process.platform) && fs.existsSync('./searchd.exe')) {
return './searchd.exe'
}
if (fs.existsSync(fs.realpathSync(__dirname) + '/searchd')) {
return fs.realpathSync(__dirname) + '/searchd'
}
if (fs.existsSync(fs.realpathSync(path.join(__dirname, '/../../..')) + '/searchd')) {
return fs.realpathSync(path.join(__dirname, '/../../..')) + '/searchd'
}
try {
if (process.platform === 'darwin' && fs.existsSync(fs.realpathSync(path.join(__dirname, '/../../../MacOS')) + '/searchd')) {
return fs.realpathSync(path.join(__dirname, '/../../../MacOS')) + '/searchd'
}
} catch (e) {}
if (/^win/.test(process.platform) && fs.existsSync('imports/win/searchd.exe')) {
return 'imports/win/searchd.exe'
}
if (process.platform === 'linux' && fs.existsSync('imports/linux/searchd')) {
return 'imports/linux/searchd'
}
if (process.platform === 'darwin' && fs.existsSync('imports/darwin/searchd')) {
return 'imports/darwin/searchd'
}
return 'searchd'
}
const writeSphinxConfig = (path) => {
const config = `
index torrents
{
type = rt
path = ${path}/database/torrents
rt_attr_string = hash
rt_attr_string = name
rt_field = nameIndex
rt_attr_bigint = size
rt_attr_uint = files
rt_attr_uint = piecelength
rt_attr_timestamp = added
rt_attr_string = ipv4
rt_attr_uint = port
rt_attr_string = contentType
rt_attr_string = contentCategory
rt_attr_uint = seeders
rt_attr_uint = leechers
rt_attr_uint = completed
rt_attr_timestamp = trackersChecked
rt_attr_uint = good
rt_attr_uint = bad
}
index files
{
type = rt
path = ${path}/database/files
rt_attr_string = path
rt_field = pathIndex
rt_attr_string = hash
rt_attr_bigint = size
}
index statistic
{
type = rt
path = ${path}/database/statistic
rt_attr_bigint = size
rt_attr_bigint = files
rt_attr_uint = torrents
}
searchd
{
listen = 9312
listen = 9306:mysql41
read_timeout = 5
max_children = 30
seamless_rotate = 1
preopen_indexes = 1
unlink_old = 1
workers = threads # for RT to work
pid_file = ${path}/searchd.pid
log = ${path}/searchd.log
query_log = ${path}/query.log
binlog_path = ${path}
}
`;
if (!fs.existsSync(`${path}/database`)){
fs.mkdirSync(`${path}/database`);
}
fs.writeFileSync(`${path}/sphinx.conf`, config)
console.log(`writed sphinx config to ${path}`)
}
const sphinxPath = path.resolve(getSphinxPath())
console.log('Sphinx Path:', sphinxPath)
let closerPath = sphinxPath.replace('searchd', 'consolekill')
if(/^win/.test(process.platform))
{
console.log('windows console closer path: ', closerPath)
console.log('cmd path', process.env.COMSPEC || 'cmd')
}
const startSphinx = (callback) => {
const sphinxConfigDirectory = app.getPath("userData")
writeSphinxConfig(sphinxConfigDirectory)
if(/^win/.test(process.platform))
sphinx = spawn(process.env.COMSPEC || 'cmd', ['/c', sphinxPath, '--config', `${sphinxConfigDirectory}/sphinx.conf`])
else
sphinx = spawn(sphinxPath, ['--config', `${sphinxConfigDirectory}/sphinx.conf`])
sphinx.stdout.on('data', (data) => {
console.log(`sphinx: ${data}`)
if (data.includes('accepting connections')) {
console.log('catched sphinx start')
if(callback)
callback()
}
})
sphinx.on('close', (code, signal) => {
console.log(`sphinx closed with code ${code} and signal ${signal}`)
app.quit()
})
}
let tray = undefined
app.on("ready", () => {
startSphinx(() => {
setApplicationMenu();
const mainWindow = createWindow("main", {
width: 1000,
height: 600
});
mainWindow.loadURL(
url.format({
pathname: path.join(__dirname, "app.html"),
protocol: "file:",
slashes: true
})
);
if (env.name === "development") {
mainWindow.openDevTools();
}
tray = new Tray('resources/icons/512x512.png')
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
mainWindow.on('show', () => {
tray.setHighlightMode('always')
})
mainWindow.on('hide', () => {
tray.setHighlightMode('never')
})
mainWindow.on('minimize', (event) => {
event.preventDefault();
mainWindow.hide();
});
var contextMenu = Menu.buildFromTemplate([
{ label: 'Show', click: function(){
mainWindow.show();
} },
{ label: 'Quit', click: function(){
app.isQuiting = true;
if (sphinx)
stop()
else
app.quit()
} }
]);
tray.setContextMenu(contextMenu)
tray.setToolTip('Rats on The Boat search')
mainWindow.webContents.on('will-navigate', e => { e.preventDefault() })
mainWindow.webContents.on('new-window', (event, url, frameName) => {
if(frameName == '_self')
{
event.preventDefault()
mainWindow.loadURL(url)
}
})
spider = spiderCall((...data) => mainWindow.webContents.send(...data), (message, callback) => {
ipcMain.on(message, (event, arg) => {
if(Array.isArray(arg) && typeof arg[arg.length - 1] === 'object' && arg[arg.length - 1].callback)
{
const id = arg[arg.length - 1].callback
arg[arg.length - 1] = (responce) => {
mainWindow.webContents.send('callback', id, responce)
}
}
callback.apply(null, arg)
})
})
})
});
let stopProtect = false
const stop = () => {
if(stopProtect)
return
stopProtect = true
if(tray)
tray.destroy()
if(spider)
{
if(/^win/.test(process.platform))
spider.stop(() => exec(`${closerPath} ${sphinx.pid}`))
else
spider.stop(() => sphinx.kill())
}
else
{
if(/^win/.test(process.platform))
exec(`${closerPath} ${sphinx.pid}`)
else
sphinx.kill()
}
}
app.on("window-all-closed", () => {
if (sphinx)
stop()
else
app.quit()
});
app.on('before-quit', () => {
if (sphinx)
stop()
})

View File

@ -0,0 +1,98 @@
'use strict'
const Emiter = require('events')
var util = require('util');
var net = require('net');
var PeerQueue = require('./peer-queue');
var Wire = require('./wire');
const debug = require('debug')('downloader');
const config = require('../config')
class Client extends Emiter
{
constructor(options) {
super();
this.timeout = config.downloader.timeout;
this.maxConnections = config.downloader.maxConnections;
debug('timeout', this.timeout)
debug('maxConnections', this.maxConnections)
this.activeConnections = 0;
this.peers = new PeerQueue(this.maxConnections);
this.on('download', this._download);
// if (typeof options.ignore === 'function') {
// this.ignore = options.ignore;
//}
//else {
this.ignore = function (infohash, rinfo, ignore) {
ignore(false);
};
// }
}
_next(infohash, successful) {
var req = this.peers.shift(infohash, successful);
if (req) {
this.ignore(req.infohash.toString('hex'), req.rinfo, (drop) => {
if (!drop) {
this.emit('download', req.rinfo, req.infohash);
}
});
}
}
_download(rinfo, infohash)
{
debug('start download', infohash.toString('hex'), 'connections', this.activeConnections);
this.activeConnections++;
var successful = false;
var socket = new net.Socket();
socket.setTimeout(this.timeout || 5000);
socket.connect(rinfo.port, rinfo.address, () => {
var wire = new Wire(infohash);
socket.pipe(wire).pipe(socket);
wire.on('metadata', (metadata, infoHash) => {
successful = true;
debug('successfuly downloader', infoHash, rinfo);
this.emit('complete', metadata, infoHash, rinfo);
socket.destroy();
});
wire.on('fail', () => {
socket.destroy();
});
wire.sendHandshake();
});
socket.on('error', (err) => {
socket.destroy();
});
socket.on('timeout', (err) => {
socket.destroy();
});
socket.once('close', () => {
this.activeConnections--;
this._next(infohash, successful);
});
}
add(rinfo, infohash) {
this.peers.push({infohash: infohash, rinfo: rinfo});
if (this.activeConnections < this.maxConnections && this.peers.length() > 0) {
this._next();
}
}
isIdle() {
return this.peers.length() === 0;
}
}
module.exports = Client;

View File

@ -0,0 +1,33 @@
let startTime = process.hrtime()
let startUsage = process.cpuUsage()
let keepTime = process.hrtime()
let keepUsage = process.cpuUsage()
let sw = false
setInterval(() => {
if(!sw) {
keepTime = process.hrtime();
keepUsage = process.cpuUsage();
sw = true;
} else {
startTime = keepTime;
startUsage = keepUsage;
sw = false;
}
}, 500)
module.exports = () => {
function secNSec2ms (secNSec) {
return secNSec[0] * 1000 + secNSec[1] / 1000000
}
var elapTime = process.hrtime(startTime)
var elapUsage = process.cpuUsage(startUsage)
var elapTimeMS = secNSec2ms(elapTime)
var elapUserMS = elapUsage.user
var elapSystMS = elapUsage.system
return Math.round(100 * ((elapUserMS + elapSystMS) / 1000) / elapTimeMS)
}

View File

@ -0,0 +1,55 @@
'use strict';
var PeerQueue = function (maxSize, perLimit) {
this.maxSize = maxSize || 200;
this.perLimit = perLimit || 10;
this.peers = {};
this.reqs = [];
};
PeerQueue.prototype._shift = function () {
if (this.length() > 0) {
var req = this.reqs.shift();
this.peers[req.infohash.toString('hex')] = [];
return req;
}
};
PeerQueue.prototype.push = function (peer) {
var infohashHex = peer.infohash.toString('hex');
var peers = this.peers[infohashHex];
if (peers && peers.length < this.perLimit) {
peers.push(peer);
}
else if (this.length() < this.maxSize) {
this.reqs.push(peer);
}
};
PeerQueue.prototype.shift = function (infohash, successful) {
if (infohash) {
var infohashHex = infohash.toString('hex');
if (successful === true) {
delete this.peers[infohashHex];
}
else {
var peers = this.peers[infohashHex];
if (peers) {
if (peers.length > 0) {
return peers.shift();
}
else {
delete this.peers[infohashHex];
}
}
}
}
return this._shift();
};
PeerQueue.prototype.length = function () {
return this.reqs.length;
};
module.exports = PeerQueue;

285
src/background/bt/spider.js Normal file
View File

@ -0,0 +1,285 @@
'use strict'
const dgram = require('dgram')
const Emiter = require('events')
const bencode = require('bencode')
const {Table, Node} = require('./table')
const Token = require('./token')
const cpuUsage = require('./cpu-usage')
const config = require('../config')
const fs = require('fs')
const _debug = require('debug')
const cpuDebug = _debug('spider:cpu')
const trafficDebug = _debug('spider:traffic')
const bootstraps = [{
address: 'router.bittorrent.com',
port: 6881
}, {
address: 'router.utorrent.com',
port: 6881
}, {
address: 'dht.transmissionbt.com',
port: 6881
}, {
address: 'dht.aelitis.com',
port: 6881
}]
function isValidPort(port) {
return port > 0 && port < (1 << 16)
}
function generateTid() {
return parseInt(Math.random() * 99).toString()
}
class Spider extends Emiter {
constructor(client) {
super()
const options = arguments.length? arguments[0]: {}
this.udp = dgram.createSocket('udp4')
this.table = new Table(options.tableCaption || 1000)
this.bootstraps = options.bootstraps || bootstraps
this.token = new Token()
this.client = client
this.ignore = false; // ignore all requests
this.initialized = false;
this.trafficSpeed = 0
this.walkInterval = config.spider.walkInterval;
this.cpuLimit = config.spider.cpuLimit;
this.cpuInterval = config.spider.cpuInterval;
}
send(message, address) {
const data = bencode.encode(message)
this.udp.send(data, 0, data.length, address.port, address.address)
}
findNode(id, address) {
const message = {
t: generateTid(),
y: 'q',
q: 'find_node',
a: {
id: id,
target: Node.generateID()
}
}
this.send(message, address)
}
join() {
this.bootstraps.forEach((bootstrap) => {
this.findNode(this.table.id, bootstrap)
})
}
walk() {
if(this.closing)
return
if(!this.client || this.client.isIdle()) {
if(
!this.ignore
&& (this.cpuLimit <= 0 || cpuUsage() < this.cpuLimit + this.cpuInterval)
&& (config.trafficMax <= 0 || this.trafficSpeed == 0 || this.trafficSpeed < config.trafficMax)
)
{
const node = this.table.shift()
if (node) {
this.findNode(Node.neighbor(node.id, this.table.id), {address: node.address, port: node.port})
}
}
}
setTimeout(()=>this.walk(), this.walkInterval)
}
onFoundNodes(data) {
const nodes = Node.decodeNodes(data)
nodes.forEach((node) => {
if (node.id != this.table.id && isValidPort(node.port)) {
this.table.add(node)
}
})
this.emit('nodes', nodes)
}
onFindNodeRequest(message, address) {
if(this.cpuLimit > 0 && cpuUsage() > this.cpuLimit) {
return
}
if(config.trafficIgnoreDHT && config.trafficMax > 0 && this.trafficSpeed > 0 && this.trafficSpeed > config.trafficMax) {
return
}
const {t: tid, a: {id: nid, target: infohash}} = message
if (tid === undefined || target.length != 20 || nid.length != 20) {
return
}
this.send({
t: tid,
y: 'r',
r: {
id: Node.neighbor(nid, this.table.id),
nodes: Node.encodeNodes(this.table.first())
}
}, address)
}
onGetPeersRequest(message, address) {
if(this.cpuLimit > 0 && cpuUsage() > this.cpuLimit) {
return
}
if(config.trafficIgnoreDHT && config.trafficMax > 0 && this.trafficSpeed > 0 && this.trafficSpeed > config.trafficMax) {
return
}
const {t: tid, a: {id: nid, info_hash: infohash}} = message
if (tid === undefined || infohash.length != 20 || nid.length != 20) {
return
}
this.send({
t: tid,
y: 'r',
r: {
id: Node.neighbor(nid, this.table.id),
nodes: Node.encodeNodes(this.table.first()),
token: this.token.token
}
}, address)
this.emit('unensureHash', infohash.toString('hex').toUpperCase())
}
onAnnouncePeerRequest(message, address) {
let {t: tid, a: {info_hash: infohash, token: token, id: id, implied_port: implied, port: port}} = message
if (!tid) return
if (!this.token.isValid(token)) return
port = (implied != undefined && implied != 0) ? address.port : (port || 0)
if (!isValidPort(port)) return
this.send({ t: tid, y: 'r', r: { id: Node.neighbor(id, this.table.id) } }, address)
let addressPair = {
address: address.address,
port: port
};
this.emit('ensureHash', infohash.toString('hex').toUpperCase(), addressPair)
if(this.client && !this.ignore) {
cpuDebug('cpu usage:' + cpuUsage())
if(this.cpuLimit <= 0 || cpuUsage() <= this.cpuLimit + this.cpuInterval) {
this.client.add(addressPair, infohash);
}
}
}
onPingRequest(message, address) {
if(this.cpuLimit > 0 && cpuUsage() > this.cpuLimit) {
return
}
if(config.trafficIgnoreDHT && config.trafficMax > 0 && this.trafficSpeed > 0 && this.trafficSpeed > config.trafficMax) {
return
}
this.send({ t: message.t, y: 'r', r: { id: Node.neighbor(message.a.id, this.table.id) } }, address)
}
parse(data, address) {
try {
const message = bencode.decode(data)
if (message.y.toString() == 'r' && message.r.nodes) {
this.onFoundNodes(message.r.nodes)
} else if (message.y.toString() == 'q') {
switch(message.q.toString()) {
case 'get_peers':
this.onGetPeersRequest(message, address)
break
case 'announce_peer':
this.onAnnouncePeerRequest(message, address)
break
case 'find_node':
this.onFindNodeRequest(message, address)
break
case 'ping':
this.onPingRequest(message, address)
break
}
}
} catch (err) {}
}
listen(port) {
if(this.initialized)
return
this.initialized = true
this.udp.bind(port)
this.udp.on('listening', () => {
console.log(`Listen DHT protocol on ${this.udp.address().address}:${this.udp.address().port}`)
})
this.udp.on('message', (data, addr) => {
this.parse(data, addr)
})
this.udp.on('error', (err) => {})
this.joinInterval = setInterval(() => {
if(!this.client || this.client.isIdle()) {
this.join()
}
}, 3000)
this.join()
this.walk()
if(config.trafficMax > 0)
{
trafficDebug('inore dht traffic', config.trafficIgnoreDHT)
const path = `/sys/class/net/${config.trafficInterface}/statistics/rx_bytes`
if(fs.existsSync(path))
{
trafficDebug('limitation', config.trafficMax / 1024, 'kbps/s')
let traffic = 0
this.trafficInterval = setInterval(() => {
fs.readFile(path, (err, newTraffic) => {
if(err)
return
if(traffic === 0)
traffic = newTraffic
this.trafficSpeed = (newTraffic - traffic) / config.trafficUpdateTime
trafficDebug('traffic rx', this.trafficSpeed / 1024, 'kbps/s')
traffic = newTraffic
})
}, 1000 * config.trafficUpdateTime)
}
}
}
close(callback)
{
clearInterval(this.joinInterval)
if(this.trafficInterval)
clearInterval(this.trafficInterval)
this.closing = true
this.udp.close(() => {
this.initialized = false
if(callback)
callback()
})
}
}
module.exports = Spider

View File

@ -0,0 +1,72 @@
'use strict'
const crypto = require('crypto')
class Node {
static generateID() {
return crypto.createHash('sha1').update(crypto.randomBytes(20)).digest()
}
constructor(id) {
this.id = id || Node.generateNodeID()
}
static neighbor(target, id) {
return Buffer.concat([target.slice(0, 10), id.slice(10)])
}
static encodeNodes(nodes) {
return Buffer.concat(nodes.map((node)=> Buffer.concat([node.id, Node.encodeIP(node.address), Node.encodePort(node.port)])))
}
static decodeNodes(data) {
const nodes = []
for (let i = 0; i + 26 <= data.length; i += 26) {
nodes.push({
id: data.slice(i, i + 20),
address: `${data[i + 20]}.${data[i + 21]}.${data[i + 22]}.${data[i + 23]}`,
port: data.readUInt16BE(i + 24)
})
}
return nodes
}
static encodeIP(ip) {
return Buffer.from(ip.split('.').map((i)=>parseInt(i)))
}
static encodePort(port) {
const data = Buffer.alloc(2)
data.writeUInt16BE(port, 0)
return data
}
}
class Table{
constructor(cap) {
this.id = Node.generateID()
this.nodes = []
this.caption = cap
}
add(node) {
if (this.nodes.length < this.caption) {
this.nodes.push(node)
}
}
shift() {
return this.nodes.shift()
}
size() {
return this.nodes.length;
}
first() {
if(this.nodes.length >= 8) {
return this.nodes.slice(0, 8)
}else if(this.nodes.length > 0) {
return new Array(8).join().split(',').map(()=> this.nodes[0])
}
return []
}
}
module.exports = {Table, Node}

View File

@ -0,0 +1,16 @@
'use strict'
module.exports = class {
constructor() {
this.generate()
setInterval(()=> this.generate(), 60000*15)
}
isValid(t) {
return t.toString() === this.token.toString()
}
generate() {
this.token = new Buffer([parseInt(Math.random()*200), parseInt(Math.random()*200)])
}
}

View File

@ -0,0 +1,129 @@
const dgram = require('dgram');
const server = dgram.createSocket("udp4")
const config = require('../config');
const debug = require('debug')('peers-scrape');
const ACTION_CONNECT = 0
const ACTION_ANNOUNCE = 1
const ACTION_SCRAPE = 2
const ACTION_ERROR = 3
const connectionIdHigh = 0x417
const connectionIdLow = 0x27101980
const requests = {};
let message = function (buf, host, port) {
server.send(buf, 0, buf.length, port, host, function(err, bytes) {
if (err) {
console.log(err.message);
}
});
};
let connectTracker = function(connection) {
debug('start screape connection');
let buffer = new Buffer(16);
const transactionId = Math.floor((Math.random()*100000)+1);
buffer.fill(0);
buffer.writeUInt32BE(connectionIdHigh, 0);
buffer.writeUInt32BE(connectionIdLow, 4);
buffer.writeUInt32BE(ACTION_CONNECT, 8);
buffer.writeUInt32BE(transactionId, 12);
// очистка старых соединений
for(transaction in requests) {
if((new Date).getTime() - requests[transaction].date.getTime() > config.udpTrackersTimeout) {
delete requests[transaction];
}
}
requests[transactionId] = connection;
message(buffer, connection.host, connection.port);
};
let scrapeTorrent = function (connectionIdHigh, connectionIdLow, transactionId) {
let connection = requests[transactionId];
if(!connection)
return;
debug('start scrape');
let buffer = new Buffer(56)
buffer.fill(0);
buffer.writeUInt32BE(connectionIdHigh, 0);
buffer.writeUInt32BE(connectionIdLow, 4);
buffer.writeUInt32BE(ACTION_SCRAPE, 8);
buffer.writeUInt32BE(transactionId, 12);
buffer.write(connection.hash, 16, buffer.length, 'hex');
// do scrape
message(buffer, connection.host, connection.port);
};
server.on("message", function (msg, rinfo) {
let buffer = new Buffer(msg)
const action = buffer.readUInt32BE(0, 4);
const transactionId = buffer.readUInt32BE(4, 4);
if(!(transactionId in requests))
return;
debug("returned action: " + action);
debug("returned transactionId: " + transactionId);
if (action === ACTION_CONNECT) {
debug("connect response");
let connectionIdHigh = buffer.readUInt32BE(8, 4);
let connectionIdLow = buffer.readUInt32BE(12, 4);
scrapeTorrent(connectionIdHigh, connectionIdLow, transactionId);
} else if (action === ACTION_SCRAPE) {
debug("scrape response");
let seeders = buffer.readUInt32BE(8, 4);
let completed = buffer.readUInt32BE(12, 4);
let leechers = buffer.readUInt32BE(16, 4);
let connection = requests[transactionId];
connection.callback({
host: connection.host,
port: connection.port,
hash: connection.hash,
seeders,
completed,
leechers
})
delete requests[transactionId];
} else if (action === ACTION_ERROR) {
delete requests[transactionId];
console.log("error in scrape response");
}
});
let getPeersStatistic = (host, port, hash, callback) => {
let connection = {
host, port, hash, callback, date: new Date()
}
connectTracker(connection);
}
server.on("listening", function () {
var address = server.address();
console.log("listening udp tracker respose on " + address.address + ":" + address.port);
});
server.bind(config.udpTrackersPort);
module.exports = getPeersStatistic;
//getPeersStatistic('tracker.glotorrents.com', 6969, "d096ff66557a5ea7030680967610e38b37434ea8", (data) => {
// console.log(data)
//});

247
src/background/bt/wire.js Normal file
View File

@ -0,0 +1,247 @@
'use strict';
var stream = require('stream');
var crypto = require('crypto');
var util = require('util');
var BitField = require('bitfield');
var bencode = require('bencode');
var {Node} = require('./table');
var BT_RESERVED = new Buffer([0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01]);
var BT_PROTOCOL = new Buffer('BitTorrent protocol');
var PIECE_LENGTH = Math.pow(2, 14);
var MAX_METADATA_SIZE = 10000000;
var BITFIELD_GROW = 1000;
var EXT_HANDSHAKE_ID = 0;
var BT_MSG_ID = 20;
var Wire = function(infohash) {
stream.Duplex.call(this);
this._bitfield = new BitField(0, { grow: BITFIELD_GROW });
this._infohash = infohash;
this._buffer = [];
this._bufferSize = 0;
this._next = null;
this._nextSize = 0;
this._metadata = null;
this._metadataSize = null;
this._numPieces = 0;
this._ut_metadata = null;
this._onHandshake();
}
util.inherits(Wire, stream.Duplex);
Wire.prototype._onMessageLength = function (buffer) {
if (buffer.length >= 4) {
var length = buffer.readUInt32BE(0);
if (length > 0) {
this._register(length, this._onMessage)
}
}
};
Wire.prototype._onMessage = function (buffer) {
this._register(4, this._onMessageLength)
if (buffer[0] == BT_MSG_ID) {
this._onExtended(buffer.readUInt8(1), buffer.slice(2));
}
};
Wire.prototype._onExtended = function(ext, buf) {
if (ext === 0) {
try {
this._onExtHandshake(bencode.decode(buf));
}
catch (err) {
this._fail();
}
}
else {
this._onPiece(buf);
}
};
Wire.prototype._register = function (size, next) {
this._nextSize = size;
this._next = next;
};
Wire.prototype.end = function() {
stream.Duplex.prototype.end.apply(this, arguments);
};
Wire.prototype._onHandshake = function() {
this._register(1, function(buffer) {
if (buffer.length == 0) {
this.end();
return this._fail();
}
var pstrlen = buffer.readUInt8(0);
this._register(pstrlen + 48, function(handshake) {
var protocol = handshake.slice(0, pstrlen);
if (protocol.toString() !== BT_PROTOCOL.toString()) {
this.end();
this._fail();
return;
}
handshake = handshake.slice(pstrlen);
if ( !!(handshake[5] & 0x10) ) {
this._register(4, this._onMessageLength);
this._sendExtHandshake();
}
else {
this._fail();
}
}.bind(this));
}.bind(this));
};
Wire.prototype._onExtHandshake = function(extHandshake) {
if (!extHandshake.metadata_size || !extHandshake.m.ut_metadata
|| extHandshake.metadata_size > MAX_METADATA_SIZE) {
this._fail();
return;
}
this._metadataSize = extHandshake.metadata_size;
this._numPieces = Math.ceil(this._metadataSize / PIECE_LENGTH);
this._ut_metadata = extHandshake.m.ut_metadata;
this._requestPieces();
}
Wire.prototype._requestPieces = function() {
this._metadata = new Buffer(this._metadataSize);
for (var piece = 0; piece < this._numPieces; piece++) {
this._requestPiece(piece);
}
};
Wire.prototype._requestPiece = function(piece) {
var msg = Buffer.concat([
new Buffer([BT_MSG_ID]),
new Buffer([this._ut_metadata]),
bencode.encode({msg_type: 0, piece: piece})
]);
this._sendMessage(msg);
};
Wire.prototype._sendPacket = function(packet) {
this.push(packet);
};
Wire.prototype._sendMessage = function(msg) {
var buf = new Buffer(4);
buf.writeUInt32BE(msg.length, 0);
this._sendPacket(Buffer.concat([buf, msg]));
};
Wire.prototype.sendHandshake = function() {
var peerID = Node.generateID();
var packet = Buffer.concat([
new Buffer([BT_PROTOCOL.length]),
BT_PROTOCOL, BT_RESERVED, this._infohash, peerID
]);
this._sendPacket(packet);
};
Wire.prototype._sendExtHandshake = function() {
var msg = Buffer.concat([
new Buffer([BT_MSG_ID]),
new Buffer([EXT_HANDSHAKE_ID]),
bencode.encode({m: {ut_metadata: 1}})
]);
this._sendMessage(msg);
};
Wire.prototype._onPiece = function(piece) {
var dict, trailer;
try {
var str = piece.toString();
var trailerIndex = str.indexOf('ee') + 2;
dict = bencode.decode(str.substring(0, trailerIndex));
trailer = piece.slice(trailerIndex);
}
catch (err) {
this._fail();
return;
}
if (dict.msg_type != 1) {
this._fail();
return;
}
if (trailer.length > PIECE_LENGTH) {
this._fail();
return;
}
trailer.copy(this._metadata, dict.piece * PIECE_LENGTH);
this._bitfield.set(dict.piece);
this._checkDone();
};
Wire.prototype._checkDone = function () {
var done = true;
for (var piece = 0; piece < this._numPieces; piece++) {
if (!this._bitfield.get(piece)) {
done = false;
break;
}
}
if (!done) {
return
}
this._onDone(this._metadata);
};
Wire.prototype._onDone = function(metadata) {
try {
var info = bencode.decode(metadata).info;
if (info) {
metadata = bencode.encode(info);
}
}
catch (err) {
this._fail();
return;
}
var infohash = crypto.createHash('sha1').update(metadata).digest('hex');
if (this._infohash.toString('hex') != infohash ) {
this._fail();
return false;
}
this.emit('metadata', {info: bencode.decode(metadata, 'utf8')}, this._infohash);
};
Wire.prototype._fail = function() {
this.emit('fail');
};
Wire.prototype._write = function (buf, encoding, next) {
this._bufferSize += buf.length;
this._buffer.push(buf);
while (this._bufferSize >= this._nextSize) {
var buffer = Buffer.concat(this._buffer);
this._bufferSize -= this._nextSize;
this._buffer = this._bufferSize
? [buffer.slice(this._nextSize)]
: [];
this._next(buffer.slice(0, this._nextSize));
}
next(null);
}
Wire.prototype._read = function() {
// do nothing
};
module.exports = Wire;

83
src/background/config.js Normal file
View File

@ -0,0 +1,83 @@
let config = {
indexer: true,
domain: 'ratsontheboat.org',
httpPort: 8095,
spiderPort: 4445,
udpTrackersPort: 4446,
udpTrackersTimeout: 3 * 60 * 1000,
sitemapMaxSize: 25000,
sphinx: {
host : 'localhost',
port : 9306,
connectionLimit: 30
},
mysql: {
host : 'localhost',
user : 'btsearch',
password : 'pirateal100x',
database : 'btsearch',
connectionLimit: 40
},
spider: {
walkInterval: 5,
cpuLimit: 0,
cpuInterval: 10,
},
downloader: {
maxConnections: 200,
timeout: 5000
},
cleanup: true,
cleanupDiscLimit: 7 * 1024 * 1024 * 1024,
spaceQuota: false,
spaceDiskLimit: 7 * 1024 * 1024 * 1024,
trafficInterface: 'enp2s0',
trafficMax: 0,
trafficUpdateTime: 3, //secs
trafficIgnoreDHT: false
}
const fs = require('fs');
const debug = require('debug')('config')
const configProxy = new Proxy(config, {
set: (target, prop, value, receiver) => {
target[prop] = value
if(!fs.existsSync('config.json'))
fs.writeFileSync('config.json', '{}')
fs.readFile('config.json', 'utf8', (err, data) => {
let obj = JSON.parse(data)
obj[prop] = value;
fs.writeFileSync('config.json', JSON.stringify(obj, null, 4), 'utf8');
debug('saving config.json:', prop, '=', value)
})
}
})
config.load = () => {
debug('loading configuration')
if(fs.existsSync('config.json'))
{
debug('finded configuration config.json')
const data = fs.readFileSync('config.json', 'utf8')
const obj = JSON.parse(data);
for(let prop in obj)
{
config[prop] = obj[prop]
debug('config.json:', prop, '=', obj[prop])
}
}
return configProxy
}
module.exports = configProxy.load()

View File

@ -0,0 +1,3 @@
{
"indexer": true
}

View File

@ -0,0 +1,84 @@
// This helper remembers the size and position of your windows (and restores
// them in that place after app relaunch).
// Can be used for more than one window, just construct many
// instances of it and give each different name.
import { app, BrowserWindow, screen } from "electron";
import jetpack from "fs-jetpack";
export default (name, options) => {
const userDataDir = jetpack.cwd(app.getPath("userData"));
const stateStoreFile = `window-state-${name}.json`;
const defaultSize = {
width: options.width,
height: options.height
};
let state = {};
let win;
const restore = () => {
let restoredState = {};
try {
restoredState = userDataDir.read(stateStoreFile, "json");
} catch (err) {
// For some reason json can't be read (might be corrupted).
// No worries, we have defaults.
}
return Object.assign({}, defaultSize, restoredState);
};
const getCurrentPosition = () => {
const position = win.getPosition();
const size = win.getSize();
return {
x: position[0],
y: position[1],
width: size[0],
height: size[1]
};
};
const windowWithinBounds = (windowState, bounds) => {
return (
windowState.x >= bounds.x &&
windowState.y >= bounds.y &&
windowState.x + windowState.width <= bounds.x + bounds.width &&
windowState.y + windowState.height <= bounds.y + bounds.height
);
};
const resetToDefaults = () => {
const bounds = screen.getPrimaryDisplay().bounds;
return Object.assign({}, defaultSize, {
x: (bounds.width - defaultSize.width) / 2,
y: (bounds.height - defaultSize.height) / 2
});
};
const ensureVisibleOnSomeDisplay = windowState => {
const visible = screen.getAllDisplays().some(display => {
return windowWithinBounds(windowState, display.bounds);
});
if (!visible) {
// Window is partially or fully not visible now.
// Reset it to safe defaults.
return resetToDefaults();
}
return windowState;
};
const saveState = () => {
if (!win.isMinimized() && !win.isMaximized()) {
Object.assign(state, getCurrentPosition());
}
userDataDir.write(stateStoreFile, state, { atomic: true });
};
state = ensureVisibleOnSomeDisplay(restore());
win = new BrowserWindow(Object.assign({}, options, state));
win.on("close", saveState);
return win;
};

View File

@ -0,0 +1,28 @@
import { app, BrowserWindow } from "electron";
export const devMenuTemplate = {
label: "Development",
submenu: [
{
label: "Reload",
accelerator: "CmdOrCtrl+R",
click: () => {
BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache();
}
},
{
label: "Toggle DevTools",
accelerator: "Alt+CmdOrCtrl+I",
click: () => {
BrowserWindow.getFocusedWindow().toggleDevTools();
}
},
{
label: "Quit",
accelerator: "CmdOrCtrl+Q",
click: () => {
app.quit();
}
}
]
};

View File

@ -0,0 +1,12 @@
export const editMenuTemplate = {
label: "Edit",
submenu: [
{ label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
{ label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
{ type: "separator" },
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
{ label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
]
};

979
src/background/spider.js Normal file
View File

@ -0,0 +1,979 @@
const config = require('./config');
const client = new (require('./bt/client'))
const spider = new (require('./bt/spider'))(client)
const mysql = require('mysql');
const getPeersStatisticUDP = require('./bt/udp-tracker-request')
//var express = require('express');
//var app = express();
//var server = require('http').Server(app);
//var io = require('socket.io')(server);
//var sm = require('sitemap');
//var phantomjs = require('phantomjs-prebuilt')
var ipaddr = require('ipaddr.js');
//const disk = require('diskusage');
const os = require('os');
let rootPath = os.platform() === 'win32' ? 'c:' : '/';
const _debug = require('debug')
const cleanupDebug = _debug('main:cleanup');
const balanceDebug = _debug('main:balance');
const fakeTorrentsDebug = _debug('main:fakeTorrents');
const quotaDebug = _debug('main:quota');
const {torrentTypeDetect} = require('../app/content');
// Start server
//server.listen(config.httpPort);
//console.log('Listening web server on', config.httpPort, 'port')
module.exports = function (send, recive)
{
let torrentsId = 1;
let filesId = 1;
let mysqlPool = mysql.createPool({
connectionLimit: config.mysql.connectionLimit,
host : config.sphinx.host,
port : config.sphinx.port
});
let sphinx = mysql.createPool({
connectionLimit: config.sphinx.connectionLimit,
host : config.sphinx.host,
port : config.sphinx.port
});
const udpTrackers = [
{
host: 'tracker.coppersurfer.tk',
port: 6969
},
{
host: 'tracker.leechers-paradise.org',
port: 6969
},
{
host: 'tracker.opentrackr.org',
port: 1337
},
{
host: '9.rarbg.me',
port: 2710
}
]
let mysqlSingle;
function handleListenerDisconnect() {
mysqlSingle = mysql.createConnection({
host : config.sphinx.host,
port : config.sphinx.port
});
mysqlSingle.connect(function(mysqlError) {
if (mysqlError) {
console.error('error connecting: ' + mysqlError.stack);
return;
}
mysqlSingle.query("SELECT MAX(`id`) as mx from torrents", (err, rows) => {
if(err)
return
if(rows[0] && rows[0].mx >= 1)
torrentsId = rows[0].mx + 1;
})
mysqlSingle.query("SELECT MAX(`id`) as mx from files", (err, rows) => {
if(err)
return
if(rows[0] &&rows[0].mx >= 1)
filesId = rows[0].mx + 1;
})
});
mysqlSingle.on('error', function(err) {
console.log('db error', err);
if(err.code === 'PROTOCOL_CONNECTION_LOST') { // Connection to the MySQL server is usually
handleListenerDisconnect(); // lost due to either server restart, or a
} else { // connnection idle timeout (the wait_timeout
throw err; // server variable configures this)
}
});
const query = mysqlSingle.query;
mysqlSingle.query = (...args) => {
let callback, i;
for(i = 1; i < args.length; i++)
{
if(typeof args[i] == 'function')
{
callback = args[i];
break;
}
}
if(callback)
{
pushDatabaseBalance();
args[i] = (...a) => {
popDatabaseBalance();
callback(...a)
}
}
else if(args.length <= 2)
{
pushDatabaseBalance();
args.push(() => {
popDatabaseBalance();
});
}
query.apply(mysqlSingle, args)
}
mysqlSingle.insertValues = (table, values, callback) => {
let names = '';
let data = '';
for(const val in values)
{
names += '`' + val + '`,';
data += mysqlSingle.escape(values[val]) + ',';
}
names = names.slice(0, -1)
data = data.slice(0, -1)
let query = `INSERT INTO ${table}(${names}) VALUES(${data})`;
if(callback)
return mysqlSingle.query(query, (...responce) => callback(...responce))
else
return mysqlSingle.query(query)
}
}
handleListenerDisconnect();
/*
app.use(express.static('build', {index: false}));
app.get('/sitemap.xml', function(req, res) {
mysqlPool.query('SELECT count(*) as cnt FROM `torrents` WHERE contentCategory != \'xxx\' OR contentCategory IS NULL', function (error, rows, fields) {
if(!rows) {
return;
}
let urls = []
for(let i = 0; i < Math.ceil(rows[0].cnt / config.sitemapMaxSize); i++)
urls.push(`http://${config.domain}/sitemap${i+1}.xml`);
res.header('Content-Type', 'application/xml');
res.send( sm.buildSitemapIndex({
urls
}));
});
});
app.get('/sitemap:id.xml', function(req, res) {
if(req.params.id < 1)
return;
let page = (req.params.id - 1) * config.sitemapMaxSize
mysqlPool.query('SELECT hash FROM `torrents` WHERE contentCategory != \'xxx\' OR contentCategory IS NULL LIMIT ?, ?', [page, config.sitemapMaxSize], function (error, rows, fields) {
if(!rows) {
return;
}
let sitemap = sm.createSitemap ({
hostname: 'http://' + config.domain,
cacheTime: 600000
});
sitemap.add({url: '/'});
for(let i = 0; i < rows.length; i++)
sitemap.add({url: '/torrent/' + rows[i].hash});
sitemap.toXML( function (err, xml) {
if (err) {
return res.status(500).end();
}
res.header('Content-Type', 'application/xml');
res.send( xml );
});
});
});
app.get('*', function(req, res)
{
if(typeof req.query['_escaped_fragment_'] != 'undefined')
{
let program = phantomjs.exec('phantom.js', 'http://' + config.domain + req.path)
let body = '';
let timeout = setTimeout(() => {
program.kill();
}, 45000)
program.stderr.pipe(process.stderr)
program.stdout.on('data', (chunk) => {
body += chunk;
});
program.on('exit', code => {
clearTimeout(timeout);
res.header('Content-Type', 'text/html');
res.send( body );
})
return;
}
res.sendfile(__dirname + '/build/index.html');
});
*/
// start
function baseRowData(row)
{
return {
hash: row.hash,
name: row.name,
size: row.size,
files: row.files,
filesList: row.filesList,
piecelength: row.piecelength,
added: row.added ? (typeof row.added === 'object' ? row.added.getTime() : row.added) : (new Date()).getTime(),
contentType: row.contentType || row.contenttype,
contentCategory: row.contentCategory || row.contentcategory,
seeders: row.seeders,
completed: row.completed,
leechers: row.leechers,
trackersChecked: row.trackersChecked ? row.trackersChecked.getTime() : undefined,
good: row.good,
bad: row.bad,
}
}
let topCache = {};
setInterval(() => {
topCache = {};
}, 24 * 60 * 60 * 1000);
//io.on('connection', function(socket)
//{
recive('recentTorrents', function(callback)
{
if(typeof callback != 'function')
return;
mysqlPool.query('SELECT * FROM `torrents` ORDER BY added DESC LIMIT 0,10', function (error, rows, fields) {
if(!rows) {
callback(undefined)
return;
}
let torrents = [];
rows.forEach((row) => {
torrents.push(baseRowData(row));
});
callback(torrents)
});
});
recive('statistic', function(callback)
{
if(typeof callback != 'function')
return;
mysqlPool.query('SELECT count(*) AS torrents, sum(size) AS sz FROM `torrents`', function (error, rows, fields) {
if(!rows) {
console.error(error)
callback(undefined)
return;
}
let result = {torrents: rows[0].torrents || 0, size: rows[0].sz || 0}
mysqlPool.query('SELECT count(*) AS files FROM `files`', function (error, rows, fields) {
if(!rows) {
console.error(error)
callback(undefined)
return;
}
result.files = rows[0].files || 0
callback(result)
})
});
});
recive('torrent', function(hash, options, callback)
{
if(hash.length != 40)
return;
if(typeof callback != 'function')
return;
mysqlPool.query('SELECT * FROM `torrents` WHERE `hash` = ?', hash, function (error, rows, fields) {
if(!rows || rows.length == 0) {
callback(undefined)
return;
}
let torrent = rows[0];
if(options.files)
{
mysqlPool.query('SELECT * FROM `files` WHERE `hash` = ?', hash, function (error, rows, fields) {
torrent.filesList = rows;
callback(baseRowData(torrent))
});
}
else
{
callback(baseRowData(torrent))
}
});
});
recive('searchTorrent', function(text, navigation, callback)
{
if(typeof callback != 'function')
return;
if(!text || text.length <= 2) {
callback(undefined);
return;
}
const safeSearch = !!navigation.safeSearch;
const index = navigation.index || 0;
const limit = navigation.limit || 10;
let args = [text, index, limit];
const orderBy = navigation.orderBy;
let order = '';
let where = '';
if(orderBy && orderBy.length > 0)
{
const orderDesc = navigation.orderDesc ? 'DESC' : 'ASC';
args.splice(1, 0, orderBy);
order = 'ORDER BY ?? ' + orderDesc;
}
if(safeSearch)
{
where += " and contentCategory != 'xxx' ";
}
if(navigation.type && navigation.type.length > 0)
{
where += ' and contentType = ' + mysqlPool.escape(navigation.type) + ' ';
}
if(navigation.size)
{
if(navigation.size.max > 0)
where += ' and size < ' + mysqlPool.escape(navigation.size.max) + ' ';
if(navigation.size.min > 0)
where += ' and size > ' + mysqlPool.escape(navigation.size.min) + ' ';
}
if(navigation.files)
{
if(navigation.files.max > 0)
where += ' and files < ' + mysqlPool.escape(navigation.files.max) + ' ';
if(navigation.files.min > 0)
where += ' and files > ' + mysqlPool.escape(navigation.files.min) + ' ';
}
console.log(navigation, where)
let searchList = [];
//args.splice(orderBy && orderBy.length > 0 ? 1 : 0, 1);
//mysqlPool.query('SELECT * FROM `torrents` WHERE `name` like \'%' + text + '%\' ' + where + ' ' + order + ' LIMIT ?,?', args, function (error, rows, fields) {
sphinx.query('SELECT * FROM `torrents` WHERE MATCH(?) ' + where + ' ' + order + ' LIMIT ?,?', args, function (error, rows, fields) {
if(!rows) {
console.log(error)
callback(undefined)
return;
}
rows.forEach((row) => {
searchList.push(baseRowData(row));
});
callback(searchList);
});
});
recive('searchFiles', function(text, navigation, callback)
{
if(typeof callback != 'function')
return;
if(!text || text.length <= 2) {
callback(undefined);
return;
}
const safeSearch = !!navigation.safeSearch;
const index = navigation.index || 0;
const limit = navigation.limit || 10;
let args = [text, index, limit];
const orderBy = navigation.orderBy;
let order = '';
let where = '';
if(orderBy && orderBy.length > 0)
{
const orderDesc = navigation.orderDesc ? 'DESC' : 'ASC';
args.splice(1, 0, orderBy);
order = 'ORDER BY ?? ' + orderDesc;
}
/*
if(safeSearch)
{
where += " and contentCategory != 'xxx' ";
}
if(navigation.type && navigation.type.length > 0)
{
where += ' and contentType = ' + mysqlPool.escape(navigation.type) + ' ';
}
if(navigation.size)
{
if(navigation.size.max > 0)
where += ' and torrentSize < ' + mysqlPool.escape(navigation.size.max) + ' ';
if(navigation.size.min > 0)
where += ' and torrentSize > ' + mysqlPool.escape(navigation.size.min) + ' ';
}
if(navigation.files)
{
if(navigation.files.max > 0)
where += ' and files < ' + mysqlPool.escape(navigation.files.max) + ' ';
if(navigation.files.min > 0)
where += ' and files > ' + mysqlPool.escape(navigation.files.min) + ' ';
}
*/
let search = {};
//args.splice(orderBy && orderBy.length > 0 ? 1 : 0, 1);
//mysqlPool.query('SELECT * FROM `files` inner join torrents on(torrents.hash = files.hash) WHERE files.path like \'%' + text + '%\' ' + where + ' ' + order + ' LIMIT ?,?', args, function (error, rows, fields) {
sphinx.query('SELECT * FROM `files` WHERE MATCH(?) ' + where + ' ' + order + ' LIMIT ?,?', args, function (error, files, fields) {
if(!files) {
console.log(error)
callback(undefined)
return;
}
if(files.length === 0)
{
callback(undefined)
return;
}
for(const file of files)
{
if(!search[file.hash])
{
search[file.hash] = { path: [] }
}
search[file.hash].path.push(file.path)
}
const inSql = Object.keys(search).map(hash => sphinx.escape(hash)).join(',');
sphinx.query(`SELECT * FROM torrents WHERE hash IN(${inSql})`, (err, torrents) => {
if(!torrents) {
console.log(err)
return;
}
for(const torrent of torrents)
{
search[torrent.hash] = Object.assign(torrent, search[torrent.hash])
}
callback(Object.values(search));
})
});
});
recive('checkTrackers', function(hash)
{
if(hash.length != 40)
return;
updateTorrentTrackers(hash);
});
recive('topTorrents', function(type, callback)
{
let where = '';
let max = 20;
if(type && type.length > 0)
{
where += ' and contentType = ' + mysqlPool.escape(type) + ' ';
max = 15;
if(type == 'hours')
{
where = ' and `added` > ' + Math.floor(Date.now() / 1000) - (60 * 60 * 24)
}
if(type == 'week')
{
where = ' and `added` > ' + Math.floor(Date.now() / 1000) - (60 * 60 * 24 * 7)
}
if(type == 'month')
{
where = ' and `added` > ' + Math.floor(Date.now() / 1000) - (60 * 60 * 24 * 30)
}
}
const query = `SELECT * FROM torrents WHERE seeders > 0 and contentCategory != 'xxx' ${where} ORDER BY seeders DESC LIMIT ${max}`;
if(topCache[query])
{
callback(topCache[query]);
return;
}
mysqlPool.query(query, function (error, rows) {
if(!rows || rows.length == 0) {
callback(undefined)
return;
}
rows = rows.map((row) => {
return baseRowData(row);
});
topCache[query] = rows;
callback(rows);
});
});
recive('admin', function(callback)
{
if(typeof callback != 'function')
return;
callback({
dhtDisabled: !config.indexer
})
});
recive('setAdmin', function(options, callback)
{
if(typeof options !== 'object')
return;
config.indexer = !options.dhtDisabled;
spider.ignore = !config.indexer;
if(!config.indexer)
showFakeTorrents()
else {
hideFakeTorrents()
spider.listen(config.spiderPort)
}
if(typeof callback === 'function')
callback(true)
});
let socketIPV4 = () => {
let ip = socket.request.connection.remoteAddress;
if (ipaddr.IPv4.isValid(ip)) {
// all ok
} else if (ipaddr.IPv6.isValid(ip)) {
let ipv6 = ipaddr.IPv6.parse(ip);
if (ipv6.isIPv4MappedAddress()) {
ip = ipv6.toIPv4Address().toString();
}
}
return ip
};
recive('vote', function(hash, isGood, callback)
{
if(hash.length != 40)
return;
if(typeof callback != 'function')
return;
const ip = socketIPV4();
isGood = !!isGood;
mysqlPool.query('SELECT * FROM `torrents_actions` WHERE `hash` = ? AND (`action` = \'good\' OR `action` = \'bad\') AND ipv4 = ?', [hash, ip], function (error, rows, fields) {
if(!rows) {
console.error(error);
}
if(rows.length > 0) {
callback(false)
return
}
mysqlPool.query('SELECT good, bad FROM `torrents` WHERE `hash` = ?', hash, function (error, rows, fields) {
if(!rows || rows.length == 0)
return;
let {good, bad} = rows[0];
const action = isGood ? 'good' : 'bad';
mysqlPool.query('INSERT INTO `torrents_actions` SET ?', {hash, action, ipv4: ip}, function(err, result) {
if(!result) {
console.error(err);
}
mysqlPool.query('UPDATE torrents SET ' + action + ' = ' + action + ' + 1 WHERE hash = ?', hash, function(err, result) {
if(!result) {
console.error(err);
}
if(isGood) {
good++;
} else {
bad++;
}
send('vote', {
hash, good, bad
});
callback(true)
});
});
});
});
});
//});
let undoneQueries = 0;
let pushDatabaseBalance = () => {
undoneQueries++;
if(undoneQueries >= 5000)
{
balanceDebug('start balance mysql, queries:', undoneQueries);
spider.ignore = true;
}
};
let popDatabaseBalance = () => {
undoneQueries--;
balanceDebug('balanced, queries left:', undoneQueries);
if(undoneQueries == 0)
{
balanceDebug('balance done');
spider.ignore = !config.indexer;
}
};
// обновление статистики
/*
setInterval(() => {
let stats = {};
mysqlPool.query('SELECT COUNT(*) as tornum FROM `torrents`', function (error, rows, fields) {
stats.torrents = rows[0].tornum;
mysqlPool.query('SELECT COUNT(*) as filesnum, SUM(`size`) as filesizes FROM `files`', function (error, rows, fields) {
stats.files = rows[0].filesnum;
stats.size = rows[0].filesizes;
send('newStatistic', stats);
mysqlPool.query('DELETE FROM `statistic`', function (err, result) {
if(!result) {
console.error(err);
}
mysqlPool.query('INSERT INTO `statistic` SET ?', stats, function(err, result) {
if(!result) {
console.error(err);
}
});
})
});
});
}, 10 * 60 * 1000)
*/
const updateTorrentTrackers = (hash) => {
let maxSeeders = 0, maxLeechers = 0, maxCompleted = 0;
mysqlSingle.query('UPDATE torrents SET trackersChecked = ? WHERE hash = ?', [Math.floor(Date.now() / 1000), hash], function(err, result) {
if(!result) {
console.error(err);
return
}
udpTrackers.forEach((tracker) => {
getPeersStatisticUDP(tracker.host, tracker.port, hash, ({seeders, completed, leechers}) => {
if(seeders == 0 && completed == 0 && leechers == 0)
return;
if(seeders < maxSeeders)
{
return;
}
if(seeders == maxSeeders && leechers < maxLeechers)
{
return;
}
if(seeders == maxSeeders && leechers == maxLeechers && completed <= maxCompleted)
{
return;
}
maxSeeders = seeders;
maxLeechers = leechers;
maxCompleted = completed;
let checkTime = new Date();
mysqlSingle.query('UPDATE torrents SET seeders = ?, completed = ?, leechers = ?, trackersChecked = ? WHERE hash = ?', [seeders, completed, leechers, Math.floor(checkTime.getTime() / 1000), hash], function(err, result) {
if(!result) {
console.error(err);
return
}
send('trackerTorrentUpdate', {
hash,
seeders,
completed,
leechers,
trackersChecked: checkTime.getTime()
});
});
});
});
});
}
const cleanupTorrents = (cleanTorrents = 1) => {
if(!config.cleanup)
return;
/*
disk.check(rootPath, function(err, info) {
if (err) {
console.log(err);
} else {
const {available, free, total} = info;
if(free < config.cleanupDiscLimit)
{
mysqlSingle.query(`SELECT * FROM torrents WHERE added < DATE_SUB(NOW(), INTERVAL 6 hour) ORDER BY seeders ASC, files DESC, leechers ASC, completed ASC LIMIT ${cleanTorrents}`, function(err, torrents) {
if(!torrents)
return;
torrents.forEach((torrent) => {
if(torrent.seeders > 0){
cleanupDebug('this torrent is ok', torrent.name);
return
}
cleanupDebug('cleanup torrent', torrent.name, '[seeders', torrent.seeders, ', files', torrent.files, ']', 'free', (free / (1024 * 1024)) + "mb");
mysqlSingle.query('DELETE FROM files WHERE hash = ?', torrent.hash);
mysqlSingle.query('DELETE FROM torrents WHERE hash = ?', torrent.hash);
})
});
}
else
cleanupDebug('enough free space', (free / (1024 * 1024)) + "mb");
}
});
*/
}
const updateTorrent = (metadata, infohash, rinfo) => {
console.log('writing torrent', metadata.info.name, 'to database');
const hash = infohash.toString('hex');
let size = metadata.info.length ? metadata.info.length : 0;
let filesCount = 1;
let filesArray = [];
if(metadata.info.files && metadata.info.files.length > 0)
{
filesCount = metadata.info.files.length;
size = 0;
for(let i = 0; i < metadata.info.files.length; i++)
{
let file = metadata.info.files[i];
let filePath = file.path.join('/');
let fileQ = {
id: filesId++,
hash: hash,
path: filePath,
pathIndex: filePath,
size: file.length,
};
filesArray.push(fileQ);
size += file.length;
}
}
else
{
let fileQ = {
id: filesId++,
hash: hash,
path: metadata.info.name,
pathIndex: metadata.info.name,
size: size,
};
filesArray.push(fileQ);
}
let filesToAdd = filesArray.length;
mysqlSingle.query('SELECT count(*) as files_count FROM files WHERE hash = ?', [hash], function(err, rows) {
if(!rows)
return
const db_files = rows[0]['files_count'];
if(db_files !== filesCount)
{
mysqlSingle.query('DELETE FROM files WHERE hash = ?', hash, function (err, result) {
if(err)
{
return;
}
filesArray.forEach((file) => {
mysqlSingle.insertValues('files', file, function(err, result) {
if(!result) {
console.log(file);
console.error(err);
return
}
if(--filesToAdd === 0) {
send('filesReady', hash);
}
});
});
})
}
})
var torrentQ = {
id: torrentsId++,
hash: hash,
name: metadata.info.name,
nameIndex: metadata.info.name,
size: size,
files: filesCount,
piecelength: metadata.info['piece length'],
ipv4: rinfo.address,
port: rinfo.port,
added: Math.floor(Date.now() / 1000)
};
torrentTypeDetect(torrentQ, filesArray);
mysqlSingle.query("SELECT id FROM torrents WHERE hash = ?", hash, (err, single) => {
if(!single)
{
console.log(err)
return
}
if(single.length > 0)
{
return
}
mysqlSingle.insertValues('torrents', torrentQ, function(err, result) {
if(result) {
send('newTorrent', {
hash: hash,
name: metadata.info.name,
size: size,
files: filesCount,
piecelength: metadata.info['piece length'],
contentType: torrentQ.contentType,
contentCategory: torrentQ.contentCategory,
});
updateTorrentTrackers(hash);
}
else
{
console.log(torrentQ);
console.error(err);
}
});
})
}
client.on('complete', function (metadata, infohash, rinfo) {
cleanupTorrents(1); // clean old torrents before writing new
if(config.spaceQuota && config.spaceDiskLimit > 0)
{
disk.check(rootPath, function(err, info) {
if (err) {
console.log(err);
} else {
const {available, free, total} = info;
if(free >= config.spaceDiskLimit)
{
hideFakeTorrents(); // also enable fake torrents;
updateTorrent(metadata, infohash, rinfo);
}
else
{
quotaDebug('ignore torrent', metadata.info.name, 'free space', (free / (1024 * 1024)) + "mb");
showFakeTorrents(); // also enable fake torrents;
}
}
});
}
else
{
updateTorrent(metadata, infohash, rinfo);
}
});
// spider.on('nodes', (nodes)=>console.log('foundNodes'))
let fakeTorrents = [];
function showFakeTorrentsPage(page)
{
mysqlSingle.query('SELECT * FROM torrents LIMIT ?, 100', [page], function(err, torrents) {
if(!torrents)
return;
torrents.forEach((torrent, index) => {
const fk = fakeTorrents.push(setTimeout(() => {
delete fakeTorrents[fk-1];
send('newTorrent', baseRowData(torrent));
updateTorrentTrackers(torrent.hash);
fakeTorrentsDebug('fake torrent', torrents.name, 'index, page:', index, page);
}, 700 * index))
})
const fk = fakeTorrents.push(setTimeout(()=>{
delete fakeTorrents[fk-1];
showFakeTorrentsPage(torrents.length > 0 ? page + torrents.length : 0)
}, 700 * torrents.length))
});
}
function showFakeTorrents()
{
fakeTorrentsDebug('showing fake torrents');
hideFakeTorrents()
showFakeTorrentsPage(0);
}
function hideFakeTorrents()
{
fakeTorrents.forEach((fk) => {
clearTimeout(fk)
})
fakeTorrents = []
fakeTorrentsDebug('hidding fake torrents');
}
if(config.indexer) {
spider.listen(config.spiderPort)
} else {
showFakeTorrents();
}
if(config.cleanup && config.indexer)
{
cleanupDebug('cleanup enabled');
cleanupDebug('cleanup disc limit', (config.cleanupDiscLimit / (1024 * 1024)) + 'mb');
}
if(config.spaceQuota)
{
quotaDebug('disk quota enabled');
}
this.stop = (callback) => {
console.log('closing spider')
mysqlPool.end(() => spider.close(() => {
mysqlSingle.destroy()
callback()
}))
}
return this
}