torrentsearch
Version:
Torrent search from command line
573 lines (502 loc) • 22.9 kB
JavaScript
;
var torrent_search = require("torrentflix/lib/torrent_search.js");
var terminalWidgets = require("terminal-widgets");
var chalk = require('chalk');
var fileSizeParser = require('filesize-parser');
var dateParser = require("date.js");
var util = require("util");
var fs = require("fs");
var domain = require("domain");
var path = require("path");
var torrent_sources = {
"kickass": { name: 'Kickass', url: "https://kat.cr" },
"limetorrents": { name: 'LimeTorrents', url: "http://limetorrents.cc" },
"extratorrent": { name: 'ExtraTorrent', url: "http://extratorrent.cc" },
"sky": { name: 'Skytorrents', url: "https://skytorrents.in" },
"strike": { name: 'GetStrike', url: "https://getstrike.net" },
"yts": { name: 'YTS', url: "https://yts.ag" },
"tpb": { name: 'The Pirate Bay', url: "https://thepiratebay.la" },
"btdigg": { name: 'BTDigg', url: "https://btdigg.org" },
"seedpeer": { name: 'Seedpeer', url: "http://seedpeer.eu" },
"leetx": { name: '1337x', url: "https://1337x.to" },
"nyaa": { name: 'Nyaa', url: "http://www.nyaa.se" },
"tokyotosho": { name: 'Tokiotosho', url: "https://www.tokyotosho.info" },
"cpasbien": { name: 'Cpasbien', url: "http://www.cpasbien.tv" },
"eztv": { name: 'Eztv', url: "https://www.eztv.ag" },
"rarbg": { name: 'Rarbg', url: "https://torrentapi.org" },
"xbit": { name: 'Xbit', url: "https://xbit.pw" },
"zooqle": { name: 'Zooqle', url: "https://zooqle.com" },
"torrentproject": { name: 'TorrentProject', url: "https://www.torrentproject.se" }
}
// extract sites urls
try {
var script_config_vars = fs.readFileSync("node_modules/torrentflix/lib/cli.js", "utf-8");
var str_config_vars = script_config_vars.match(/var config_vars = ({[\s\S]*?});/);
var config_vars = eval("(" + str_config_vars[1] + ")");
torrent_sources = config_vars.torrent_sources;
} catch(e) {
console.log(chalk.yellow("Warning: Could not extract torrent sources urls from torrentflix, using hardcoded urls instead"));
}
var scrapers = [];
scrapers = [
function() { var obj = require("torrentflix/lib/limetorrents.js"); return { name: "limetorrents", search: function(query) { return obj.search(query, torrent_sources["limetorrents"].url); } } }(),
function() { var obj = require("torrentflix/lib/thepiratebay.js"); return { name: "thepiratebay", search: function(query) { return obj.search(query, torrent_sources["tpb"].url); } } }(),
function() { var obj = require("torrentflix/lib/yts.js"); return { name: "yts", search: function(query) { return obj.search(query, torrent_sources["yts"].url); } } }(),
function() { var obj = require("torrentflix/lib/1337x.js"); return { name: "1337x", search: function(query) { return obj.search(query, torrent_sources["leetx"].url); } } }(),
function() { var obj = require("torrentflix/lib/nyaa.js"); return { name: "nyaa", search: function(query) { return obj.search(query, torrent_sources["nyaa"].url); } } }(),
function() { var obj = require("torrentflix/lib/kickass.js"); return { name: "kickass", search: function(query) { return obj.search(query, torrent_sources["kickass"].url); } } }(),
function() { var obj = require("torrentflix/lib/tokyotosho.js"); return { name: "tokyotosho", search: function(query) { return obj.search(query, torrent_sources["tokyotosho"].url); } } }(),
function() { var obj = require("torrentflix/lib/eztv.js"); return { name: "eztv", search: function(query) { return obj.search(query, torrent_sources["eztv"].url); } } }(),
function() { var obj = require("torrentflix/lib/xbit.js"); return { name: "xbit", search: function(query) { return obj.search(query, torrent_sources["xbit"].url); } } }(),
function() { var obj = require("torrentflix/lib/zooqle.js"); return { name: "zooqle", search: function(query) { return obj.search(query, torrent_sources["zooqle"].url); } } }(),
function() { var obj = require("torrentflix/lib/skytorrents.js"); return { name: "sky", search: function(query) { return obj.search(query, torrent_sources["sky"].url); } } }(),
function() { var obj = require("torrentflix/lib/rarbg.js"); return { name: "rarbg", search: function(query) { return obj.search(query, torrent_sources["rarbg"].url); } } }()
];
//scrapers = [
// scrapers[scrapers.length - 1]
//];
if (false) { // plugins developments disabled for now
var qbittorrent_search = function(query, engine) {
// https://github.com/qbittorrent/qBittorrent/wiki/How-to-write-a-search-plugin
return new Promise((resolve, reject) => {
const spawn = require('child_process').spawn;
const cmd = spawn('python2', ['plugins/qbittorrent/nova/nova2.py', engine, 'all', query]);
const readline = require('readline');
const rl = readline.createInterface({
input: cmd.stdout
});
var data = [];
rl.on('line', (input) => {
// format: link|name|size|seeds|leech|engine_url
var splits = input.split('|');
data.push({
torrent_link: splits[0], title: splits[1], size: splits[2], seeds: splits[3], leechs: splits[4]
});
});
cmd.stderr.on('data', (data) => {
//console.log(`stderr: ${data}`);
});
cmd.on('close', (code) => {
//console.log(`child process exited with code ${code}`);
if(code >= 0)
resolve(data);
else
reject("error code " + code);
});
})
}
// load search plugins
var pluginsPath = path.join(__dirname, "plugins");
/* === disabled for now
var torrentflixPluginsPath = path.join(pluginsPath, "torrentflix");
require("fs").readdirSync(torrentflixPluginsPath).forEach(function(filename) {
var obj = require(path.join(torrentflixPluginsPath, filename));
var parsedPath = path.parse(filename);
if(torrent_sources[parsedPath.name] === undefined) {
console.log(chalk.yellow("Warning: unknown url for torrentflix plugin '" + parsedPath.name + "', plugin skipped"));
return;
}
scrapers.push({
name: parsedPath.name,
search: function(query) {
return obj.search(query, torrent_sources[parsedPath.name].url);
}
})
});
*/
/*
Installation:
mkdir plugins
cd plugins
mkdir qbittorrent
cd qbittorrent
svn export "https://github.com/qbittorrent/qBittorrent.git/trunk/src/searchengine/nova"
svn export --force "https://github.com/qbittorrent/search-plugins.git/trunk/nova/engines"
*/
var qbittorrentPluginsPath = path.join(pluginsPath, "qbittorrent/nova/engines");
try {
require("fs").readdirSync(qbittorrentPluginsPath).forEach(function(filename) {
var parsedPath = path.parse(filename);
if(parsedPath.ext === ".py" && parsedPath.name != "__init__") {
scrapers.push({
name: parsedPath.name,
search: function(query) {
return qbittorrent_search(query, parsedPath.name);
}
})
}
});
} catch(e) {}
}
//scrapers = [ scrapers[0] ];
//scrapers = [ ];
var results = [], allResults = [];
// ============
// UI
// ============
var widgetContext = new terminalWidgets.WidgetContext();
var uiWidth = function() {
if(overrideUiWidth) return overrideUiWidth;
if(process.platform.startsWith("win")) return Math.min(100, process.stdout.columns); // on windows process.stdout.columns return the number of columns of the buffer, not the number of visible columns
else return process.stdout.columns;
};
var resultsMenuItemStyle = function(selected, focused) {
if(selected && focused) return chalk.black.bgWhite;
else if(selected && !focused) return chalk.gray.bgWhite;
else return function(str){ return str; };
}
var scraperStyle = function(status) {
switch(status) {
case "ok": return chalk.green;
case "error": return chalk.red;
}
return function(str){ return str; };
}
var nostyle = function(str) { return str; };
var printable = function(str) {
return (str || "").toString().trim().replace(/[^\x20-\x7E]/g, ".");
}
var scrapersStatusLabel = new terminalWidgets.Label({
width: function() { return Math.floor(uiWidth()*80/100); },
height: function() { return scrapers.length; },
render: function(item, width) {
return scraperStyle(scrapers[item].status) (terminalWidgets.padRight(scrapers[item].name + ": " + scrapers[item].status + ", " + scrapers[item].status_message, width));
}
});
var resultsMenuMaxTextScroll = 0, newResultsMenuMaxTextScroll = 0;
var resultsMenuRedrawTimeout = null;
var resultsMenu = new terminalWidgets.VMenu({
width: function() { return uiWidth() - resultsMenuVScrollBar.callback.width(); },
height: function() { return 10 - resultsMenuHScrollBar.callback.height(); },
itemsCount: function() { return results.length; },
maxTextScroll: function() { return resultsMenuMaxTextScroll; },
render: function(item, line, current, width, hScroll) {
var torrent = results[item];
var columnWidths = [ Math.floor(width*5/100), 1, 0, 1, Math.floor(width*5/100), 1, Math.floor(width*15/100), 1, Math.floor(width*15/100), 1, Math.floor(width*5/100), 1, Math.floor(width*5/100) ];
columnWidths[2] = width - columnWidths.reduce(function(a,b) { return a + b; });
newResultsMenuMaxTextScroll = Math.max(newResultsMenuMaxTextScroll, printable(torrent.title).length + width - columnWidths[2]);
if(resultsMenuMaxTextScroll != newResultsMenuMaxTextScroll && !resultsMenuRedrawTimeout) {
resultsMenuRedrawTimeout = setTimeout(function() {
resultsMenuMaxTextScroll = newResultsMenuMaxTextScroll;
widgetContext.draw();
resultsMenuRedrawTimeout = null;
}, 0);
} // async redraw (we can't redraw here)
var line = terminalWidgets.padRightStop(torrent.scraper.name, columnWidths[0], hScroll) + "|"
+ terminalWidgets.padRightStop(printable(torrent.title), columnWidths[2], hScroll) + "|"
+ terminalWidgets.padRightStop(printable(torrent.torrent_verified), columnWidths[4], hScroll) + "|"
+ terminalWidgets.padRightStop(printable(torrent.date_added), columnWidths[6], hScroll) + "|"
+ terminalWidgets.padRightStop(printable(torrent.size), columnWidths[8], hScroll) + "|"
+ terminalWidgets.padRightStop(printable(torrent.seeds), columnWidths[10], hScroll) + "|"
+ terminalWidgets.padRightStop(printable(torrent.leechs), columnWidths[12], hScroll);
return resultsMenuItemStyle(current, widgetContext.focusedWidget === resultsMenu) (line);
},
itemSelected: function(item) {
if(results[item])
processTorrent(results[item], function(success, message) {
widgetContext.setWidget(layout, false); // keep output of the command
if(success) {
} else {
logMessage("ERROR", "processTorrent", String(message), encodeURI(results[item].torrent_link || results[item].torrent_site));
}
widgetContext.draw();
});
},
handleKeyEvent: function(key) {
if(key >= "\x20") { // printable + backspace
widgetContext.setFocus( searchFieldInput );
searchFieldInput.handleKeyEvent(key);
return true;
}
return false;
}
});
var resultsMenuVScrollBar = resultsMenu.newVScrollBar({
height: function() { return resultsMenu.callback.height(); },
width: function() { return (resultsMenu.callback.itemsCount() > resultsMenu.callback.height()) ? 1 : 0; },
render: function(component, line, width) {
return (component === 0 ? chalk.bgBlue : nostyle)(terminalWidgets.padRight("", width));
}
});
var resultsMenuHScrollBar = resultsMenu.newHScrollBar({
height: function() { return (resultsMenu.callback.maxTextScroll() > 0) ? 1 : 0; },
width: function() { return resultsMenu.callback.width(); },
render: function(component, line, width) {
return (component === 0 ? chalk.bgBlue : nostyle)(terminalWidgets.padRight("", width));
}
});
var parseDate = function(str) {
var ret;
// cleanup
str = (str || "").toString().trim()
.replace(/[^\x20-\x7E]/g, ".");
ret = new Date(str);
//console.log("Date.parse(\"" + str + "\") = " + ret );
if(!isNaN(ret)) {
return ret;
}
if(str === "" || str.toLowerCase() === (new Date("")).toString().toLowerCase()) // "Invalid Date" // "Invalid Date"
return new Date(-8640000000000000); // http://stackoverflow.com/questions/11526504/minimum-and-maximum-date
str = str
.replace(/^a /g, "1 ");
try {
ret = dateParser(str);
} catch(e) {
logMessage("ERROR", "parseDate", e, str);
return new Date(-8640000000000000); // http://stackoverflow.com/questions/11526504/minimum-and-maximum-date
}
var currentDate = new Date();
if(ret > currentDate) ret = currentDate - (ret - currentDate); // sometimes the date is an age
return ret;
}
var parseFileSize = function(str) {
try {
// cleanup
str = (str || "").toString().trim()
.replace(/[^\x20-\x7E]/g, "")
.replace(/[^:]*:/,"")
.replace(/\.([a-zA-Z])/g,"$1")
.replace(/([kmg])[o]/gi, "$1B")
.replace(/(byte)($|[^s])/gi, "$1s$2");
return fileSizeParser(str);
} catch(e) {
logMessage("ERROR", "parseFileSize", e, str);
return 0;
}
}
var parseCount = function(str) {
str = (str || "").toString().trim()
.replace(/,/g, "");
return Number(str);
}
var sortOptionsMenuItemStyle = function(selected, focused) {
if(selected && focused) return chalk.black.bgWhite;
else return function(str){ return str; };
}
var sortOptionSelected = -1;
var sortOptions = [
{ name: "seeds", compareFunction: function(a, b) { return parseCount(b.seeds) - parseCount(a.seeds); } },
{ name: "age", compareFunction: function(a, b) { return parseDate(b.date_added || "") - parseDate(a.date_added || ""); } },
{ name: "size", compareFunction: function(a, b) { return parseFileSize(b.size || "") - parseFileSize(a.size || "");} }
];
var refreshResults = function() {
results = allResults.filter(function(item) {
return item.title.toLowerCase().indexOf(searchFieldInputText[0].toLowerCase()) >= 0;
});
if(sortOptionSelected >= 0)
results.sort( sortOptions[sortOptionSelected].compareFunction );
resultsMenu.moveCurrentItem({ row: 0, col: 0 }); // refresh the view if needed
}
var sortOptionsMenu = new terminalWidgets.VMenu({
width: function() { return uiWidth() - scrapersStatusLabel.callback.width(); },
height: function() { return sortOptions.length; },
itemsCount: function() { return sortOptions.length; },
render: function(item, line, current, width, hScroll) {
return sortOptionsMenuItemStyle(current, widgetContext.focusedWidget === sortOptionsMenu)(terminalWidgets.padBoth((item == sortOptionSelected ? "*" : "") + sortOptions[item].name, width));
},
itemSelected: function(item) {
sortOptionSelected = item;
refreshResults();
widgetContext.setFocus(resultsMenu);
widgetContext.draw();
}
});
var searchFieldHeaderText = "Search: ";
var searchFieldHeaderLabel = new terminalWidgets.Label({
width: function() { return searchFieldHeaderText.length; },
height: function() { return 1; },
render: function(item, width) {
return (widgetContext.focusedWidget === searchFieldInput ? chalk.white : chalk.gray)(searchFieldHeaderText);
}
});
var searchFieldInputText = [];
var searchFieldInput = new terminalWidgets.Input(searchFieldInputText, {
width: function() { return Math.max(0, uiWidth() - searchFieldHeaderText.length); },
height: function() { return 1; },
textMaxLines: function() { return 1 },
render: function(component, line, start, width) {
return (widgetContext.focusedWidget === searchFieldInput ? chalk.white : chalk.gray)((component === 0 ? chalk.bgGreen : nostyle)(terminalWidgets.padRight(searchFieldInputText[line].substr(start, width), width)));
},
textModified : function() {
refreshResults();
},
handleKeyEvent: function(key) {
if(key < "\x20") { // not printable
widgetContext.setFocus( resultsMenu );
resultsMenu.handleKeyEvent(key);
return true;
}
return false;
}
});
var debugMessages = Array(3);
var debugMessagesTop = 0, debugMessagesCurrent = 0;
var logMessage = function(level, module, message, data) {
if(debugMessages[debugMessagesCurrent]) debugMessagesTop = (debugMessagesTop+1) % debugMessages.length;
debugMessages[debugMessagesCurrent] = { date: new Date(), module: module, message: message, data: data };
debugMessagesCurrent = (debugMessagesCurrent+1) % debugMessages.length;
}
var debugLabel = new terminalWidgets.Label({
width: function() { return uiWidth(); },
height: function() { return (debugMessages[debugMessagesTop] === undefined)? 0 : debugMessages.length; },
render: function(item, width) {
var line = debugMessages[(debugMessagesTop+item) % debugMessages.length] ? debugMessages[(debugMessagesTop+item) % debugMessages.length].message.replace(/\n/,"") : "";
return chalk.yellow(terminalWidgets.padRight(line, width));
}
});
var resultsMenuScrollBarPad = new terminalWidgets.Label({
width: function() { return resultsMenuVScrollBar.callback.width(); },
height: function() { return resultsMenuHScrollBar.callback.height(); },
render: function(item, width) {
return terminalWidgets.padRight(" ", width);
}
});
var layout = new terminalWidgets.VBoxLayout([
new terminalWidgets.HBoxLayout([ scrapersStatusLabel, sortOptionsMenu ]),
new terminalWidgets.HBoxLayout([ resultsMenu, resultsMenuVScrollBar ]),
new terminalWidgets.HBoxLayout([ resultsMenuHScrollBar, resultsMenuScrollBarPad ]),
//resultsMenuHScrollBar,
new terminalWidgets.HBoxLayout([ searchFieldHeaderLabel, searchFieldInput ]),
debugLabel
]);
widgetContext.setWidget(layout);
widgetContext.setFocus(resultsMenu);
var tabOrder = [ resultsMenu, sortOptionsMenu, searchFieldInput ];
// ===================
// Main
// ===================
var externalCommand = [];
var query = "";
var verbose = false;
var help = false;
var overrideUiWidth = null;
for(var i = 2 ; i < process.argv.length ; ++i) {
if(process.argv[i].startsWith("--exec")) {
var separator = process.argv[i].substr("--exec".length) || ";";
while(++i < process.argv.length && process.argv[i] != separator) {
externalCommand.push(process.argv[i]);
}
}
else if(process.argv[i] == "-v") verbose = true;
else if(process.argv[i] == "-c" && i < process.argv.length-1) {
overrideUiWidth = parseInt(process.argv[++i]);
}
else if(["-h", "-?", "--help"].indexOf(process.argv[i]) >= 0) {
console.log("Usage: " + process.argv[0] + " " + process.argv[1] + " query-string [--exec command [initial-arguments]]");
console.log(" --exec command: command to start when selecting a torrent");
console.log(" the command will be started with initial-arguments list, with {} replaced by the torrent link");
console.log(" --help: this help");
console.log(" --version: version number");
console.log(" -c [NUM]: number of columns in the UI");
process.exit(0); // EX_OK
}
else if(["-v", "--version"].indexOf(process.argv[i]) >= 0) {
console.log(require("./package.json").version);
process.exit(0); // EX_OK
}
else query = process.argv[i];
}
if(query === "") {
console.error("Missing query-string, check usage with -h");
process.exit(64); // EX_USAGE
}
if(externalCommand.length === 0) externalCommand = [ "echo", "{}" ];
var i;
for (i in scrapers) { (function() {
var scraper = scrapers[i];
var d = domain.create();
scraper.status = "pending";
scraper.status_message = "request sent";
d.on('error', function(err) {
scraper.status = "error";
scraper.status_message = err;
widgetContext.draw();
});
d.run( function() {
scraper.search(query).then(
function (data) {
scraper.status = "ok";
scraper.status_message = "" + data.length + " torrents found";
data.forEach( function(item) { item.title = item.title || "<UNKNOW>"; item.scraper = scraper; } );
allResults.push.apply(allResults, data);
refreshResults();
widgetContext.draw();
}, function (err) {
throw err;
}
);
});
})(); }
var resolveTorrentLink = function(torrent, callback) {
if(torrent.torrent_link) {
//console.log(encodeURI(torrent.torrent_link));
setTimeout( function() { callback(true, torrent.torrent_link); }, 0 );
} else if (torrent.torrent_site) {
torrent_search.torrentSearch(encodeURI(torrent.torrent_site)).then(
function(data) {
torrent.torrent_link = data;
callback(true);
}, function(err) {
callback(false, err);
}
);
} else {
callback(false, "", "don't know how to download this torrent");
}
}
var processTorrent = function(torrent, callback) {
if(!torrent.torrent_link) {
logMessage("INFO", "parseTorrent", "Resolving torrent link...");
resolveTorrentLink(torrent, function(success, err){
if(success) processTorrent(torrent, callback);
else callback(false, err);
});
return;
}
executeExternalCommand(torrent, callback);
}
var externalCommandRunning = false;
var ignoreSignal = function() {};
var executeExternalCommand = function(torrent, callback) {
var spawn = require('child_process').spawn;
process.stdin.setRawMode(false);
var translatedCommand = externalCommand.map( function(item) {
switch(item) {
case "{}": return torrent.torrent_link;
default: return item;
}
});
logMessage("INFO", "executeExternalCommand", "Starting ext command...");
widgetContext.draw();
setTimeout(function() { // delay execution so that the widget finishes impending drawing
var child = spawn(translatedCommand[0], translatedCommand.slice(1), { stdio: "inherit" } );
process.on('SIGINT', ignoreSignal);
externalCommandRunning = true;
child.on('close', function(code, signal) {
console.log("");i // add extra "\n" to the child output else we will overwrite the last line
logMessage("INFO", "executeExternalCommand", "Ext command result: " + (signal || code));
externalCommandRunning = false;
process.stdin.setRawMode(true);
process.removeListener('SIGINT', ignoreSignal);
callback(true);
});
},0);
}
process.stdout.write("\u001b[?7l"); // disable line wrap (linewrap causes problems when resizing the window quickly: the widget is rendred with a size assumption but upon display it does not match the window size anymore)
process.on("exit", function() { process.stdout.write("\u001b[?7h"); }); // reenable linewrap on exit
process.stdin.setRawMode(true);
var stdinListener = function() {
if(externalCommandRunning) return; // child process will process the input
var key;
while((key = process.stdin.read()) != null) {
if(key.compare(new Buffer.from([ 3 ])) == 0) process.exit(0); // EX_OK
else if(key.compare(new Buffer.from([ 9 ])) == 0) {
widgetContext.setFocus( tabOrder[ (tabOrder.indexOf(widgetContext.focusedWidget) + 1) % tabOrder.length ] );
}
else widgetContext.handleKeyEvent(key);
widgetContext.draw();
}
};
process.stdin.on('readable', stdinListener);
process.stdout.on('resize', function() { if(externalCommandRunning) return; searchFieldInput.moveCursor({line: 0, col: 0}); widgetContext.draw(); });
widgetContext.draw();