diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 7a04f405b..efea4b325 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -9,6 +9,24 @@
+
+
+
+
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 000000000..727b8b533
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index ff4579fd6..77f15ed45 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,9 @@ npm update -g cross-seed
`cross-seed` will look for a configuration file at `~/.cross-seed/config.js`
(`AppData\Local\cross-seed\config.js` on Windows). In the configuration file ,
you can specify all of the same flags you specified on the command line, but
-after that, you won't have to specify them on the command line any more.
+after that, you won't have to specify them on the command line any more. If you
+would like to use a different directory than the default, you can set the
+`CONFIG_DIR` environment variable.
To create a configuration file, run
diff --git a/src/cmd.js b/src/cmd.js
index ab1364bb9..60409ea4e 100755
--- a/src/cmd.js
+++ b/src/cmd.js
@@ -1,10 +1,42 @@
#!/usr/bin/env node
-const { program } = require("commander");
+const { program, Command } = require("commander");
const chalk = require("chalk");
const packageDotJson = require("../package.json");
-const main = require("./index");
+const { main } = require("./index");
const { CONFIG, generateConfig } = require("./configuration");
+const { clear: clearCache } = require("./cache");
+const { serve } = require("./server");
+
+function addSharedOptions() {
+ return this.requiredOption(
+ "-u, --jackett-server-url ",
+ "Your Jackett server url",
+ CONFIG.jackettServerUrl
+ )
+ .requiredOption(
+ "-k, --jackett-api-key ",
+ "Your Jackett API key",
+ CONFIG.jackettApiKey
+ )
+ .requiredOption(
+ "-t, --trackers ",
+ "Comma-separated list of Jackett tracker ids to search",
+ CONFIG.trackers && CONFIG.trackers.join(",")
+ )
+ .requiredOption(
+ "-i, --torrent-dir ",
+ "Directory with torrent files",
+ CONFIG.torrentDir
+ )
+ .requiredOption(
+ "-s, --output-dir ",
+ "Directory to save results in",
+ CONFIG.outputDir
+ );
+}
+// monkey patch Command with this addSharedOptions function
+Command.prototype.addSharedOptions = addSharedOptions;
program.name(packageDotJson.name);
program.description(chalk.yellow.bold("cross-seed"));
@@ -22,20 +54,31 @@ program
program
.command("clear-cache")
.description("Clear the cache of downloaded-and-rejected torrents")
- .action(() => require("./cache").clear());
+ .action(clearCache);
+
+program
+ .command("daemon")
+ .description("Start the cross-serve daemon")
+ .addSharedOptions()
+ .action(async (command) => {
+ const options = command.opts();
+ options.trackers = options.trackers.split(",").filter((e) => e !== "");
+ try {
+ await serve(options);
+ } catch (e) {
+ console.error(chalk.bold.red(e.message));
+ }
+ });
program
.command("search")
.description("Search for cross-seeds\n")
+ .addSharedOptions()
.requiredOption(
- "-u, --jackett-server-url ",
- "Your Jackett server url",
- CONFIG.jackettServerUrl
- )
- .requiredOption(
- "-k, --jackett-api-key ",
- "Your Jackett API key",
- CONFIG.jackettApiKey
+ "-o, --offset ",
+ "Offset to start from",
+ (n) => parseInt(n),
+ CONFIG.offset || 0
)
.requiredOption(
"-d, --delay ",
@@ -43,37 +86,16 @@ program
parseFloat,
CONFIG.delay || 10
)
- .requiredOption(
- "-t, --trackers ",
- "Comma-separated list of Jackett tracker ids to search",
- CONFIG.trackers && CONFIG.trackers.join(",")
- )
- .requiredOption(
- "-i, --torrent-dir ",
- "Directory with torrent files",
- CONFIG.torrentDir
- )
- .requiredOption(
- "-s, --output-dir ",
- "Directory to save results in",
- CONFIG.outputDir
- )
- .requiredOption(
- "-o, --offset ",
- "Offset to start from",
- (n) => parseInt(n),
- CONFIG.offset || 0
- )
.option(
"-e, --include-episodes",
"Include single-episode torrents in the search",
CONFIG.includeEpisodes || false
)
- .action((command) => {
+ .action(async (command) => {
const options = command.opts();
options.trackers = options.trackers.split(",").filter((e) => e !== "");
try {
- main(options);
+ await main(options);
} catch (e) {
console.error(chalk.bold.red(e.message));
}
diff --git a/src/config.template.js b/src/config.template.js
index 887da0ffb..b09b5aa93 100644
--- a/src/config.template.js
+++ b/src/config.template.js
@@ -4,6 +4,9 @@
// it here as a default.
module.exports = {
+ // every time the configuration is changed, this will increment.
+ configVersion: 1,
+
jackettServerUrl: "http://localhost:9117/jackett",
jackettApiKey: "YOUR_JACKETT_API_KEY_HERE",
@@ -31,4 +34,8 @@ module.exports = {
// Whether to search for single episode torrents
includeEpisodes: false,
+
+ // added in configVersion 1
+ //
+ watchIntervalMins: 10,
};
diff --git a/src/configuration.js b/src/configuration.js
index 6a35cc30a..2845cc4b0 100644
--- a/src/configuration.js
+++ b/src/configuration.js
@@ -1,7 +1,9 @@
const fs = require("fs");
const path = require("path");
-const chalk = require('chalk');
+const chalk = require("chalk");
const packageDotJson = require("../package.json");
+const configTemplate = require("./config.template");
+const { CONFIG_TEMPLATE_URL } = require("./constants");
let CONFIG = {};
@@ -26,8 +28,28 @@ function generateConfig() {
return dest;
}
+function printUpdateInstructions(missingKeys) {
+ const configPath = path.join(appDir(), "config.js");
+ console.error(chalk.red`
+Error: Your configuration file is out of date.
+Missing: ${missingKeys.join(", ")}
+Please update at ${configPath}.
+When you are done, set the configVersion to ${configTemplate.configVersion}.
+It may help to read the template, at ${CONFIG_TEMPLATE_URL}
+`);
+}
+
try {
- CONFIG = require(path.join(appDir(), "config.js"));
+ const configPath = path.join(appDir(), "config.js");
+ CONFIG = require(configPath);
+ const { configVersion = 0 } = CONFIG;
+ if (configVersion < configTemplate.configVersion) {
+ const missingKeys = Object.keys(configTemplate).filter(
+ (k) => !CONFIG.includes(k)
+ );
+ printUpdateInstructions(missingKeys);
+ process.exitCode = 1;
+ }
} catch (_) {}
module.exports = { CONFIG, appDir, createAppDir, generateConfig };
diff --git a/src/constants.js b/src/constants.js
index d5b93eea6..70dbe758b 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,9 +1,11 @@
const EP_REGEX = /^(?.+)[. ](?S\d\d)(?E\d\d)/i;
const SEASON_REGEX = /^(?.+)[. ](?S\d\d)(?!E\d\d)/i;
-const MOVIE_REGEX = /^(?.+)[. ](?\d{4})/i
+const MOVIE_REGEX = /^(?.+)[. ](?\d{4})/i;
const EXTENSIONS = ["mkv", "mp4", "avi"];
+const CONFIG_TEMPLATE_URL =
+ "https://github.com/mmgoodnow/cross-seed/blob/master/src/config.template.js";
// because I'm sick of intellij whining at me
const result = {
Link: undefined,
@@ -12,6 +14,12 @@ const result = {
Title: undefined,
Size: undefined,
Guid: undefined,
-}
+};
-module.exports = { EP_REGEX, SEASON_REGEX, MOVIE_REGEX, EXTENSIONS };
+module.exports = {
+ EP_REGEX,
+ SEASON_REGEX,
+ MOVIE_REGEX,
+ EXTENSIONS,
+ CONFIG_TEMPLATE_URL,
+};
diff --git a/src/index.js b/src/index.js
index fcc6e6087..a614ddd48 100755
--- a/src/index.js
+++ b/src/index.js
@@ -2,8 +2,13 @@ const fs = require("fs");
const chalk = require("chalk");
const { stripExtension } = require("./utils");
-const { loadTorrentDir, saveTorrentFile } = require("./torrent");
-const { filterTorrentFile } = require("./preFilter");
+const {
+ loadTorrentDir,
+ saveTorrentFile,
+ getInfoHashesToExclude,
+ getTorrentByName,
+} = require("./torrent");
+const { filterTorrentFile, filterDupes } = require("./preFilter");
const { assessResult } = require("./decide");
const { makeJackettRequest, validateJackettApi } = require("./jackett");
@@ -15,7 +20,7 @@ async function findOnOtherSites(info, hashesToExclude, config) {
try {
response = await makeJackettRequest(query, config);
} catch (e) {
- console.error(chalk.red`error querying Jackett for ${query}`)
+ console.error(chalk.red`error querying Jackett for ${query}`);
return 0;
}
const results = response.data.Results;
@@ -50,12 +55,20 @@ async function findMatchesBatch(samples, hashesToExclude, config) {
return totalFound;
}
+async function searchForSingleTorrentByName(name, config) {
+ const { torrentDir } = config;
+ const hashesToExclude = getInfoHashesToExclude(torrentDir);
+ const meta = getTorrentByName(torrentDir, name);
+ console.log(meta);
+ return findOnOtherSites(meta, hashesToExclude, config);
+}
+
async function main(config) {
const { torrentDir, offset, outputDir, includeEpisodes } = config;
const parsedTorrents = loadTorrentDir(torrentDir);
const hashesToExclude = parsedTorrents.map((t) => t.infoHash);
- const filteredTorrents = parsedTorrents.filter((e, i, a) =>
- filterTorrentFile(e, i, a, includeEpisodes)
+ const filteredTorrents = filterDupes(parsedTorrents).filter(
+ filterTorrentFile(includeEpisodes)
);
const samples = filteredTorrents.slice(offset);
@@ -83,4 +96,4 @@ async function main(config) {
);
}
-module.exports = main;
+module.exports = { main, searchForSingleTorrentByName };
diff --git a/src/jackett.js b/src/jackett.js
index 605f0c00e..743be2ea6 100644
--- a/src/jackett.js
+++ b/src/jackett.js
@@ -47,7 +47,7 @@ function makeJackettRequest(name, config) {
const params = {
apikey: jackettApiKey,
Query: reformatTitleForSearching(name),
- ["Tracker[]"]: trackers,
+ "Tracker[]": trackers,
};
const opts = {
diff --git a/src/preFilter.js b/src/preFilter.js
index 092b3d56d..26cbf90c6 100644
--- a/src/preFilter.js
+++ b/src/preFilter.js
@@ -1,7 +1,7 @@
const path = require("path");
const { EP_REGEX, EXTENSIONS } = require("./constants");
-function filterTorrentFile(info, index, arr, includeEpisodes) {
+const filterTorrentFile = (includeEpisodes) => (info) => {
const { files } = info;
if (
!includeEpisodes &&
@@ -12,7 +12,7 @@ function filterTorrentFile(info, index, arr, includeEpisodes) {
}
const allVideos = files.every((file) =>
- EXTENSIONS.map(e => `.${e}`).includes(path.extname(file.path))
+ EXTENSIONS.map((e) => `.${e}`).includes(path.extname(file.path))
);
if (!allVideos) return false;
@@ -20,10 +20,16 @@ function filterTorrentFile(info, index, arr, includeEpisodes) {
const notNested = files.every(cb);
if (!notNested) return false;
- const firstOccurrence = arr.findIndex((e) => e.name === info.name);
- if (index !== firstOccurrence) return false;
-
return true;
+};
+
+function filterDupes(metaFiles) {
+ return metaFiles.filter((info, index) => {
+ const firstOccurrence = metaFiles.findIndex(
+ (e) => e.name === info.name
+ );
+ return index === firstOccurrence;
+ });
}
-module.exports = { filterTorrentFile };
+module.exports = { filterTorrentFile, filterDupes };
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 000000000..9617e0f32
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,48 @@
+const fs = require("fs");
+const http = require("http");
+const chalk = require("chalk");
+
+const { searchForSingleTorrentByName } = require("./index");
+const { validateJackettApi } = require("./jackett");
+
+const handleRequest = (config) => (req, res) => {
+ if (req.method !== "POST") {
+ res.writeHead(405);
+ res.end();
+ return;
+ }
+
+ let chunks = [];
+ req.on("data", (chunk) => chunks.push(chunk.toString()));
+ req.on("end", async () => {
+ const name = chunks.join("");
+ console.log(name);
+ try {
+ await searchForSingleTorrentByName(name, config);
+ } catch (e) {
+ res.writeHead(500);
+ res.end();
+ console.error(e.stack);
+ return;
+ }
+
+ res.writeHead(204);
+ res.end();
+ });
+};
+
+async function serve(config) {
+ const { outputDir } = config;
+ // try {
+ // await validateJackettApi(config);
+ // } catch (e) {
+ // return;
+ // }
+
+ fs.mkdirSync(outputDir, { recursive: true });
+ const server = http.createServer(handleRequest(config));
+ server.listen(2468);
+ console.log("Server is running on port 2468");
+}
+
+module.exports = { serve };
diff --git a/src/torrent.js b/src/torrent.js
index 5e5cb7fd6..e82f994d3 100644
--- a/src/torrent.js
+++ b/src/torrent.js
@@ -25,17 +25,42 @@ function saveTorrentFile(tracker, tag = "", info, outputDir) {
fs.writeFileSync(path.join(outputDir, filename), buf, { mode: 0o644 });
}
-function loadTorrentDir(torrentDir) {
- const dirContents = fs
+function findAllTorrentFilesInDir(torrentDir) {
+ return fs
.readdirSync(torrentDir)
.filter((fn) => path.extname(fn) === ".torrent")
.map((fn) => path.join(torrentDir, fn));
+}
+
+function getInfoHashesToExclude(torrentDir) {
+ return findAllTorrentFilesInDir(torrentDir).map((pathname) =>
+ path.basename(pathname, ".torrent").toLowerCase()
+ );
+}
+
+function loadTorrentDir(torrentDir) {
+ const dirContents = findAllTorrentFilesInDir(torrentDir);
return dirContents.map(parseTorrentFromFilename);
}
+function getTorrentByName(torrentDir, name) {
+ const dirContents = findAllTorrentFilesInDir(torrentDir);
+ const findResult = dirContents.find((filename) => {
+ const meta = parseTorrentFromFilename(filename);
+ return meta.name === name;
+ });
+ if (findResult === undefined) {
+ const message = `Error: could not find a torrent with the name ${name}`;
+ throw new Error(message);
+ }
+ return parseTorrentFromFilename(findResult);
+}
+
module.exports = {
parseTorrentFromFilename,
parseTorrentFromURL,
saveTorrentFile,
loadTorrentDir,
+ getTorrentByName,
+ getInfoHashesToExclude,
};