├── .gitignore ├── LICENSE ├── README.md ├── cli.js ├── health.test.js ├── index.js ├── package.json ├── test_torrent_dir ├── Lenin - The State and Revolution [audiobook] by dessalines~20161013-170437.torrent ├── Trotsky - Fascism - What it is and How to Fight it [audiobook] by dessalines.torrent └── infohashes.txt └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Torrent-Tracker-Health 2 | 3 | Get health info for torrents. This module is based on [torrent-tracker](https://github.com/vankasteelj/torrent-tracker) and returns the seeds, peers, completed, and information about torrents, given a file, magnet link, or directory. 4 | 5 | ## Quickstart 6 | 7 | `$ npm install -g torrent-tracker-health` 8 | 9 | ## Usage 10 | 11 | ### Code 12 | 13 | ```js 14 | var torrentHealth = require('torrent-tracker-health'); 15 | var uri = // Could be a magnet link, torrent file location, torrent directory, or line delimited list of infohashes. 16 | torrentHealth(uri, options).then(res => { 17 | console.log(res); 18 | }); 19 | ``` 20 | 21 | ### Global / CLI and options 22 | 23 | ```sh 24 | $ npm i -g torrent-tracker-health 25 | $ torrent-tracker-health -h 26 | Usage: torrent-tracker-health [options] 27 | --torrent: the torrent file, magnet link, torrent dir, or a file of line-delimited list of infohashes 28 | --trackers= [Optional] {tracker1/announce, tracker2/announce}, uses a default list otherwise 29 | --batchSize: [Optional] The number of torrents to include in the scrape request (Default 50) 30 | --showAllFetches: [Optional] Shows all the scrapes, instead of choosing the one with the most seeders(Default false) 31 | --debug: [Optional] (Default false) 32 | ``` 33 | 34 | ### Output 35 | 36 | ```json 37 | $ torrent-tracker-health --torrent test_torrent_dir 38 | { 39 | "results": [ 40 | { 41 | "name": "Lenin - The State and Revolution [audiobook] by dessalines", 42 | "hash": "bf60338d499a40e4e99ca8edffda9447402a29de", 43 | "length": 293313300, 44 | "created": "2016-10-14T07:03:14.000Z", 45 | "files": [...], 46 | "tracker": "udp://tracker.coppersurfer.tk:6969/announce", 47 | "seeders": 12, 48 | "leechers": 2, 49 | "completed": 2598 50 | }, 51 | { 52 | "name": "Trotsky - Fascism - What it is and How to Fight it [audiobook] by dessalines", 53 | "hash": "d1f28f0c1b89ddd9a39205bef0be3715d117f91b", 54 | "length": 134145561, 55 | "created": "2016-11-13T13:12:47.000Z", 56 | "files": [...], 57 | "tracker": "udp://exodus.desync.com:6969/announce", 58 | "seeders": 3, 59 | "leechers": 0, 60 | "completed": 73 61 | } 62 | ], 63 | "options": { 64 | "batchSize": 50, 65 | "trackers": [ 66 | "udp://tracker.coppersurfer.tk:6969/announce", 67 | "udp://tracker.internetwarriors.net:1337/announce", 68 | "udp://tracker.opentrackr.org:1337/announce", 69 | "udp://exodus.desync.com:6969/announce", 70 | "udp://explodie.org:6969/announce" 71 | ], 72 | "showAllFetches": false 73 | } 74 | } 75 | ``` 76 | 77 | ### Testing 78 | 79 | `npm test` -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var torrentHealth = require('./index.js'), 3 | argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (argv._.length !== 0 || !argv.torrent) { 6 | console.error([ 7 | 'Usage: torrent-tracker-health [options]', 8 | ' --torrent: the torrent file, magnet link, torrent dir, or a file of line-delimited list of infohashes', 9 | ' --trackers= [Optional] {tracker1/announce, tracker2/announce}, uses a default list otherwise', 10 | ' --batchSize: [Optional] The number of torrents to include in the scrape request (Default 50)', 11 | ' --showAllFetches: [Optional] Shows all the scrapes, instead of choosing the one with the most seeders(Default false)', 12 | ' --debug: [Optional] (Default false)', 13 | ].join('\n')); 14 | process.exit(1); 15 | } else { 16 | var opts = { 17 | batchSize: argv.batchSize, 18 | trackers: argv.trackers, 19 | showAllFetches: argv.showAllFetches, 20 | debug: argv.debug 21 | } 22 | if (argv.torrent) { 23 | torrentHealth(argv.torrent, opts).then(r => { 24 | console.log(JSON.stringify(r, null, 2)); 25 | }).catch(function (err) { 26 | console.log('error:', err); 27 | }); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /health.test.js: -------------------------------------------------------------------------------- 1 | var torrentHealth = require('./index.js'), 2 | Client = require('bittorrent-tracker'), 3 | utils = require('./utils'); 4 | 5 | const hashes = [ 6 | 'bf60338d499a40e4e99ca8edffda9447402a29de', 7 | 'd1f28f0c1b89ddd9a39205bef0be3715d117f91b' 8 | ]; 9 | 10 | const magnetLink = 'magnet:?xt=urn:btih:bf60338d499a40e4e99ca8edffda9447402a29de&dn=Lenin%20-%20The%20State%20and%20Revolution%20[audiobook]%20by%20dessalines&tr=udp://tracker.coppersurfer.tk:6969/announce&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://tracker.internetwarriors.net:1337/announce&tr=udp://9.rarbg.to:2710/announce&tr=udp://exodus.desync.com:6969/announce&tr=udp://tracker1.itzmx.com:8080/announce&tr=http://tracker3.itzmx.com:6961/announce&tr=udp://explodie.org:6969/announce'; 11 | 12 | const dir = 'test_torrent_dir'; 13 | const torrentFile = 'test_torrent_dir/Lenin - The State and Revolution [audiobook] by dessalines~20161013-170437.torrent'; 14 | const infoHashesFile = dir + '/infohashes.txt'; 15 | 16 | jest.setTimeout(60000); 17 | 18 | test('Tracker Scrape', () => { 19 | Client.scrape({ announce: utils.defaultTrackers[0], infoHash: hashes }, function (err, data) { 20 | expect(data[hashes[0]].infoHash).toEqual(hashes[0]); 21 | expect(data[hashes[1]].infoHash).toEqual(hashes[1]); 22 | }); 23 | }); 24 | 25 | test('Torrent directory', async () => { 26 | let res = await torrentHealth(dir); 27 | expect(res.results.length == 2); 28 | expect(res.results[0].name).toEqual('Lenin - The State and Revolution [audiobook] by dessalines'); 29 | expect(res.results[1].name).toEqual('Trotsky - Fascism - What it is and How to Fight it [audiobook] by dessalines'); 30 | expect(res.results[0].seeders).toBeGreaterThan(1); 31 | }); 32 | 33 | test('Infohashes file', async () => { 34 | let res = await torrentHealth(infoHashesFile); 35 | expect(res.results.length == 2); 36 | expect(res.results[0].seeders).toBeGreaterThan(1); 37 | expect(res.results[1].seeders).toBeGreaterThan(1); 38 | }); 39 | 40 | test('Torrent directory show all fetches', async () => { 41 | let res = await torrentHealth(dir, { showAllFetches: true }); 42 | expect(res.results[0].fetches.length).toBeGreaterThan(3); 43 | }); 44 | 45 | test('Magnet link', async () => { 46 | let res = await torrentHealth(magnetLink); 47 | expect(res.results[0].name).toEqual('Lenin - The State and Revolution [audiobook] by dessalines'); 48 | }); 49 | 50 | test('Single File', async () => { 51 | let res = await torrentHealth(torrentFile); 52 | expect(res.results[0].name).toEqual('Lenin - The State and Revolution [audiobook] by dessalines'); 53 | }); 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'), 2 | Client = require('bittorrent-tracker'), 3 | readTorrent = require('read-torrent'); 4 | 5 | function read(uri, options) { 6 | return new Promise(async (resolve, reject) => { 7 | 8 | // If its a torrent directory or infohash file, collect them up 9 | var uris = utils.collectUris(uri); 10 | 11 | options = utils.rearrange(options); 12 | 13 | // Get only the ones without read errors, and uniq infohashes 14 | var allTorrents = await Promise.all(uris.map(p => singleRead(p, options).catch(e => e))); 15 | var noErrors = allTorrents.filter(result => !(result instanceof Error)); 16 | var torrents = noErrors.filter((e, i) => noErrors.findIndex(a => a.hash === e.hash) === i); 17 | 18 | resolve({ 19 | torrents: torrents, 20 | options: options 21 | }); 22 | }); 23 | } 24 | 25 | function singleRead(uri, options) { 26 | return new Promise((resolve, reject) => { 27 | readTorrent(uri, (err, info) => { 28 | if (!err) { 29 | // Make sure info.announce is an array 30 | if (!Array.isArray(info.announce)) { 31 | info.announce = info.announce ? [info.announce] : []; 32 | } 33 | 34 | // Removing some extra fields from files 35 | if (info.files) { 36 | info.files.forEach(f => { 37 | delete f.name; 38 | delete f.offset; 39 | }); 40 | } 41 | var created = (info.created) ? info.created : new Date().toISOString(); 42 | resolve({ 43 | name: info.name, 44 | hash: info.infoHash, 45 | length: info.length, 46 | created: created, 47 | files: info.files 48 | }); 49 | } else { 50 | utils.debug('Error in read-torrent: ' + err.message + ' for torrent uri: ' + uri); 51 | reject(err); 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | async function scrapeAll(req) { 58 | 59 | // Loop over trackers and hashes in batches 60 | var hashes = req.torrents.map(t => t.hash); 61 | let slices = utils.chunk(hashes, req.options.batchSize); 62 | 63 | for (const subhashes of slices) { 64 | for (const trUri of req.options.trackers) { 65 | let data = await scrape(trUri, subhashes); 66 | 67 | // Add the peer counts to the req 68 | Object.entries(data).map(e => { 69 | var torrent = req.torrents.find(t => t.hash === e[0]); 70 | if (torrent) { 71 | if (!torrent.fetches) torrent.fetches = []; 72 | torrent.fetches.push({ 73 | seeders: e[1].complete, 74 | completed: e[1].downloaded, 75 | leechers: e[1].incomplete, 76 | tracker: trUri, 77 | }); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | return req; 84 | } 85 | 86 | function scrape(trUri, infohashes) { 87 | return new Promise((resolve, reject) => { 88 | Client.scrape({ announce: trUri, infoHash: infohashes }, (err, data) => { 89 | 90 | if (err) { 91 | if (err.message === 'timed out' || err.code === 'ETIMEDOUT') { 92 | utils.debug('Scrape timed out for ' + trUri); 93 | } else { 94 | utils.debug('Error in torrent-tracker: ' + err.message); 95 | } 96 | resolve({ 97 | tracker: trUri, 98 | error: err.message 99 | }); 100 | } else { 101 | utils.debug('Scrape successful for ' + trUri); 102 | 103 | // Coerce single fetch as same structure as multiple 104 | var firstHash = infohashes[0]; 105 | if (data[firstHash] == undefined) { 106 | var map = {}; 107 | map[firstHash] = data; 108 | data = map; 109 | } 110 | 111 | resolve(data); 112 | } 113 | }); 114 | }); 115 | } 116 | 117 | function calc(res) { 118 | var options = res.options; 119 | var torrents = res.torrents; 120 | 121 | // Early return if they want all the fetches 122 | if (options.showAllFetches) { 123 | return { 124 | results: torrents, 125 | options: options 126 | } 127 | } 128 | 129 | var maxes = torrents.map(f => { 130 | var maxFetch = f.fetches.reduce((a, b) => a.seeders > b.seeders ? a : b); 131 | return { 132 | name: f.name, 133 | hash: f.hash, 134 | length: f.length, 135 | created: f.created, 136 | files: f.files, 137 | tracker: maxFetch.tracker, 138 | seeders: maxFetch.seeders, 139 | leechers: maxFetch.leechers, 140 | completed: maxFetch.completed, 141 | } 142 | }); 143 | 144 | return { 145 | results: maxes, 146 | options: options 147 | } 148 | } 149 | 150 | module.exports = function (uri, options) { 151 | return read(uri, options) 152 | .then(scrapeAll) 153 | .then(calc); 154 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torrent-tracker-health", 3 | "version": "1.1.7", 4 | "description": "Get health info about a torrent file or magnet link", 5 | "main": "index.js", 6 | "bin": { 7 | "torrent-tracker-health": "cli.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/vankasteelj/torrent-tracker-health.git" 12 | }, 13 | "license": "MIT", 14 | "maintainers": [ 15 | { 16 | "name": "vankasteelj", 17 | "email": "vankasteelj@gmail.com" 18 | }, 19 | { 20 | "name": "Dessalines", 21 | "email": "dessalines790@gmail.com" 22 | } 23 | ], 24 | "scripts": { 25 | "test": "jest" 26 | }, 27 | "engines": { 28 | "node": ">= 8.0.0" 29 | }, 30 | "dependencies": { 31 | "bittorrent-tracker": "^9.10.1", 32 | "minimist": "^1.2.0", 33 | "read-torrent": "^1.3.1" 34 | }, 35 | "devDependencies": { 36 | "jest": "^23.6.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test_torrent_dir/Lenin - The State and Revolution [audiobook] by dessalines~20161013-170437.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dessalines/torrent-tracker-health/ff09e44593f67ae1af703a0040e666cf9985436d/test_torrent_dir/Lenin - The State and Revolution [audiobook] by dessalines~20161013-170437.torrent -------------------------------------------------------------------------------- /test_torrent_dir/Trotsky - Fascism - What it is and How to Fight it [audiobook] by dessalines.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dessalines/torrent-tracker-health/ff09e44593f67ae1af703a0040e666cf9985436d/test_torrent_dir/Trotsky - Fascism - What it is and How to Fight it [audiobook] by dessalines.torrent -------------------------------------------------------------------------------- /test_torrent_dir/infohashes.txt: -------------------------------------------------------------------------------- 1 | bf60338d499a40e4e99ca8edffda9447402a29de 2 | d1f28f0c1b89ddd9a39205bef0be3715d117f91b -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'); 3 | 4 | var _debug = false; 5 | function debug() { 6 | if (!_debug) return; 7 | console.debug.apply(console, arguments); 8 | } 9 | 10 | const defaultTrackers = [ 11 | "udp://tracker.coppersurfer.tk:6969/announce", 12 | "udp://tracker.leechers-paradise.org:6969/announce", 13 | "udp://tracker.openbittorrent.com:80/announce", 14 | "udp://9.rarbg.to:2710/announce", 15 | "udp://9.rarbg.me:2710/announce", 16 | "udp://tracker.opentrackr.org:1337/announce", 17 | "udp://tracker.internetwarriors.net:1337/announce", 18 | "udp://exodus.desync.com:6969/announce", 19 | "udp://tracker.tiny-vps.com:6969/announce", 20 | "udp://retracker.lanta-net.ru:2710/announce", 21 | "udp://open.demonii.si:1337/announce", 22 | "udp://torrentclub.tech:6969/announce", 23 | "udp://denis.stalker.upeer.me:6969/announce", 24 | "udp://tracker3.itzmx.com:6961/announce", 25 | "udp://tracker.torrent.eu.org:451/announce", 26 | "udp://open.stealth.si:80/announce", 27 | "udp://tracker.cyberia.is:6969/announce", 28 | "udp://tracker.moeking.me:6969/announce", 29 | "udp://explodie.org:6969/announce", 30 | "udp://ipv4.tracker.harry.lu:80/announce", 31 | ]; 32 | 33 | function rearrange(options) { 34 | if (!options) options = {}; 35 | 36 | _debug = (!options.debug) ? false : true; 37 | 38 | if (!options.trackers) { 39 | options.trackers = defaultTrackers; 40 | } 41 | 42 | // Make sure options.trackers is an array 43 | if (!Array.isArray(options.trackers)) { 44 | options.trackers = options.trackers ? [options.trackers] : []; 45 | } 46 | 47 | if (!options.batchSize) { 48 | options.batchSize = 50; 49 | } 50 | 51 | if (!options.showAllFetches) { 52 | options.showAllFetches = false; 53 | } 54 | 55 | return options; 56 | } 57 | 58 | function isDir(path) { 59 | try { 60 | var stat = fs.lstatSync(path); 61 | return stat.isDirectory(); 62 | } catch (e) { 63 | return false; 64 | } 65 | } 66 | 67 | function isFile(path) { 68 | try { 69 | var stat = fs.lstatSync(path); 70 | return stat.isFile(); 71 | } catch (e) { 72 | return false; 73 | } 74 | } 75 | 76 | function collectUris(uri) { 77 | if (isDir(uri)) { 78 | return torrentFilesInDir(uri); 79 | } 80 | // If its a file that doesn't end in .torrent 81 | else if (isFile(uri) && uri.split('.').pop() !== 'torrent') { 82 | return fs.readFileSync(uri).toString().trim().split('\n').map(f => 'magnet:?xt=urn:btih:' + f); 83 | } 84 | // If its a magnet link or single torrent file 85 | else { 86 | return [uri]; 87 | } 88 | } 89 | 90 | function torrentFilesInDir(dir) { 91 | return fs.readdirSync(dir).map(file => path.join(dir, file)).filter(f => f.endsWith('.torrent')); 92 | } 93 | 94 | function chunk(inputArray, perChunk) { 95 | return inputArray.reduce((all, one, i) => { 96 | const ch = Math.floor(i / perChunk); 97 | all[ch] = [].concat((all[ch] || []), one); 98 | return all 99 | }, []); 100 | } 101 | 102 | module.exports = { rearrange, torrentFilesInDir, isDir, isFile, collectUris, chunk, debug, defaultTrackers }; 103 | --------------------------------------------------------------------------------