#!/usr/bin/env node
import chalk from "chalk";
import { Option, program } from "commander";
import { getApiKey, resetApiKey } from "./auth.js";
import { generateConfig, getFileConfig } from "./configuration.js";
import { Action, LinkType, MatchMode, PROGRAM_NAME, PROGRAM_VERSION, } from "./constants.js";
import { db } from "./db.js";
import { updateTorrentCache } from "./decide.js";
import { diffCmd } from "./diff.js";
import { CrossSeedError } from "./errors.js";
import { clearIndexerFailures } from "./indexers.js";
import { injectSavedTorrents, restoreFromTorrentCache } from "./inject.js";
import { jobsLoop } from "./jobs.js";
import { bulkSearch, scanRssFeeds } from "./pipeline.js";
import { sendTestNotification } from "./pushNotifier.js";
import { serve } from "./server.js";
import { withFullRuntime, withMinimalRuntime } from "./startup.js";
import { indexTorrentsAndDataDirs, parseTorrentFromPath } from "./torrent.js";
import { fallback } from "./utils.js";
let fileConfig;
try {
    fileConfig = await getFileConfig();
}
catch (e) {
    if (e instanceof CrossSeedError) {
        console.error(e.message);
        process.exit(1);
    }
    throw e;
}
const apiKeyOption = new Option("--api-key <key>", "Provide your own API key to override the autogenerated one.").default(fileConfig.apiKey);
function createCommandWithSharedOptions(name, description) {
    return program
        .command(name)
        .description(description)
        .option("-T, --torznab <urls...>", "Torznab urls with apikey included (separated by spaces)", 
    // @ts-expect-error commander supports non-string defaults
    fallback(fileConfig.torznab))
        .option("--use-client-torrents", "Use torrents from your client for matching", fallback(fileConfig.useClientTorrents, false))
        .option("--no-use-client-torrents", "Don't use torrents from your client for matching")
        .option("--ignore-non-relevant-files-to-resume", "Ignore certain known irrelevant files when calculating resume size", fallback(fileConfig.ignoreNonRelevantFilesToResume, false))
        .option("--no-ignore-non-relevant-files-to-resume", "Don't ignore certain known irrelevant files when calculating resume size")
        .option("--data-dirs <dirs...>", "Directories to use if searching by data instead of torrents (separated by spaces)", 
    // @ts-expect-error commander supports non-string defaults
    fallback(fileConfig.dataDirs))
        .addOption(new Option("--match-mode <mode>", `"strict" will require all file names to match exactly. "flexible" allows for file renames. "partial" is like "flexible" but it ignores small files like .nfo/.srt if missing.`)
        .default(fallback(fileConfig.matchMode, MatchMode.STRICT))
        .choices(Object.values(MatchMode))
        .makeOptionMandatory())
        .option("--skip-recheck", "Skip rechecking torrents before resuming, unless necessary.", fallback(fileConfig.skipRecheck, true))
        .option("--no-skip-recheck", "Recheck every torrent before resuming, even if unnecessary.")
        .option("--auto-resume-max-download <number>", "The maximum size in bytes remaining for a torrent to be resumed", parseInt, fallback(fileConfig.autoResumeMaxDownload, 52428800))
        .option("--link-category <cat>", "Torrent client category to set on linked torrents", fallback(fileConfig.linkCategory, "cross-seed-link"))
        .option("--link-dir <dir>", "Directory to link the data for matches to", fileConfig.linkDir)
        .option("--link-dirs <dirs...>", "Directories to link the data for matches to", 
    // @ts-expect-error commander supports non-string defaults
    fallback(fileConfig.linkDirs))
        .option("--flat-linking", "Use flat linking directory structure (without individual tracker folders)", fallback(fileConfig.flatLinking, false))
        .addOption(new Option("--link-type <type>", "Use links of this type to inject data-based matches into your client")
        .default(fallback(fileConfig.linkType, LinkType.SYMLINK))
        .choices(Object.values(LinkType))
        .makeOptionMandatory())
        .option("--max-data-depth <depth>", "Max depth to look for searchees in dataDirs", (n) => parseInt(n), fallback(fileConfig.maxDataDepth, 2))
        .option("-i, --torrent-dir <dir>", "Directory with torrent files", fileConfig.torrentDir)
        .option("-s, --output-dir <dir>", "Directory to save results in", fileConfig.outputDir)
        .option("--include-non-videos", "Include torrents which contain non-video files", fallback(fileConfig.includeNonVideos, false))
        .option("--no-include-non-videos", "Don't include torrents which contain non-videos")
        .option("--include-single-episodes", "Include single episode torrents in the search", fallback(fileConfig.includeSingleEpisodes, false))
        .option("--no-include-single-episodes", "Don't include single episode torrents in the search")
        .option("--season-from-episodes <decimal>", "Match season packs from episode torrents", parseFloat, fallback(fileConfig.seasonFromEpisodes, null))
        .option("--no-season-from-episodes", "Don't match season packs from episode torrents")
        .option("--fuzzy-size-threshold <decimal>", "The size difference allowed to be considered a match.", parseFloat, fallback(fileConfig.fuzzySizeThreshold, 0.02))
        .option("-x, --exclude-older <cutoff>", "Exclude torrents first seen more than n minutes ago. Bypasses the -a flag.", fileConfig.excludeOlder)
        .option("-r, --exclude-recent-search <cutoff>", "Exclude torrents which have been searched more recently than n minutes ago. Bypasses the -a flag.", fileConfig.excludeRecentSearch)
        .option("-v, --verbose", "Log verbose output", false)
        .addOption(new Option("-A, --action <action>", "If set to 'inject', cross-seed will attempt to add the found torrents to your torrent client.")
        .default(fallback(fileConfig.action, Action.SAVE))
        .choices(Object.values(Action)))
        .option("--torrent-clients <clients...>", "The the client prefix, readonly status, and urls of your torrent clients.", 
    // @ts-expect-error commander supports non-string defaults
    fallback(fileConfig.torrentClients, []))
        .option("--rtorrent-rpc-url <url>", "The url of your rtorrent XMLRPC interface.", fileConfig.rtorrentRpcUrl)
        .option("--qbittorrent-url <url>", "The url of your qBittorrent webui.", fileConfig.qbittorrentUrl)
        .option("--transmission-rpc-url <url>", "The url of your Transmission RPC interface.", fileConfig.transmissionRpcUrl)
        .option("--deluge-rpc-url <url>", "The url of your Deluge JSON-RPC interface.", fileConfig.delugeRpcUrl)
        .option("--duplicate-categories", "Create and inject using categories with the same save paths as your normal categories", fallback(fileConfig.duplicateCategories, false))
        .option("--notification-webhook-urls <urls...>", "cross-seed will send POST requests to these urls with a JSON payload of { title, body, extra }", 
    // @ts-expect-error commander supports non-string defaults
    fileConfig.notificationWebhookUrls)
        .option("--notification-webhook-url <url>", "cross-seed will send POST requests to this url with a JSON payload of { title, body, extra }", fileConfig.notificationWebhookUrl)
        .option("-d, --delay <delay>", "Pause duration (seconds) between searches", parseFloat, fallback(fileConfig.delay, 30))
        .option("--snatch-timeout <timeout>", "Timeout for unresponsive snatches", fallback(fileConfig.snatchTimeout, "30 seconds"))
        .option("--search-timeout <timeout>", "Timeout for unresponsive searches", fallback(fileConfig.searchTimeout, "2 minutes"))
        .option("--search-limit <number>", "The number of searches before stops", (n) => parseInt(n), fallback(fileConfig.searchLimit, 0))
        .option("--block-list <strings...>", "The infohashes and/or strings in torrent name to block from cross-seed", 
    // @ts-expect-error commander supports non-string defaults
    fallback(fileConfig.blockList, []))
        .option("--sonarr <urls...>", "Sonarr API URL(s)", 
    // @ts-expect-error commander supports non-string defaults
    fileConfig.sonarr)
        .option("--radarr <urls...>", "Radarr API URL(s)", 
    // @ts-expect-error commander supports non-string defaults
    fileConfig.radarr);
}
program.name(PROGRAM_NAME);
program.description(chalk.yellow.bold(`${PROGRAM_NAME} v${PROGRAM_VERSION}`));
program.version(PROGRAM_VERSION, "-V, --version", "output the current version");
program
    .command("gen-config")
    .description("Generate a config file")
    .action(withMinimalRuntime(generateConfig));
program
    .command("update-torrent-cache-trackers")
    .description("Update announce urls in the torrent cache")
    .usage("<old-announce-url> <new-announce-url>")
    .argument("old-announce-url", 'A substring of the announce url to replace, e.g. update-torrent-cache-trackers "old.example.com" "new.example.com"')
    .argument("new-announce-url", 'A substring of the new announce url to replace the old one with, e.g. update-torrent-cache-trackers "myoldpasskey" "mynewpasskey"')
    .action(withMinimalRuntime(updateTorrentCache));
program
    .command("diff")
    .description("Analyze two torrent files for cross-seed compatibility")
    .argument("searchee")
    .argument("candidate")
    .action(withMinimalRuntime(diffCmd, { migrate: false }));
program
    .command("tree")
    .description("Print a torrent's file tree")
    .argument("torrent")
    .action(withMinimalRuntime(async (torrentPath) => {
    console.log("Use `cross-seed diff` to compare two .torrent files");
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { category, isSingleFileTorrent, raw, tags, ...meta } = await parseTorrentFromPath(torrentPath);
    console.log(meta);
}, { migrate: false }));
program
    .command("clear-indexer-failures")
    .description("Clear the cached details of indexers (failures and caps)")
    .action(withMinimalRuntime(clearIndexerFailures));
program
    .command("clear-cache")
    .description("Clear the cache without causing torrents to be re-snatched and reset the timestamps for excludeOlder and excludeRecentSearch")
    .action(withMinimalRuntime(async () => {
    console.log("Clearing cache...");
    await db("decision").whereNull("info_hash").del();
    await db("timestamp").del();
}));
program
    .command("clear-client-cache")
    .description("Clear cross-seed's cache of your client's torrents. Only necessary if you have recently changed clients or modified the torrents in client and don't want to wait on the cleanup job.")
    .action(withMinimalRuntime(async () => {
    console.log("Clearing client cache...");
    await db("torrent").del();
    await db("client_searchee").del();
    await db("data").del();
    await db("ensemble").del();
}));
program
    .command("api-key")
    .description("Show the api key")
    .addOption(apiKeyOption)
    .action(withMinimalRuntime(getApiKey));
program
    .command("reset-api-key")
    .description("Reset the api key")
    .action(withMinimalRuntime(resetApiKey));
createCommandWithSharedOptions("daemon", "Start the cross-seed daemon")
    .option("-p, --port <port>", "Listen on a custom port", (n) => parseInt(n), fallback(fileConfig.port, 2468))
    .option("--host <host>", "Bind to a specific IP address", fileConfig.host)
    .option("--no-port", "Do not listen on any port")
    .option("--search-cadence <cadence>", "Run searches on a schedule. Format: https://github.com/vercel/ms", fileConfig.searchCadence)
    .option("--rss-cadence <cadence>", "Run an rss scan on a schedule. Format: https://github.com/vercel/ms", fileConfig.rssCadence)
    .addOption(apiKeyOption)
    .action(withFullRuntime(async (options) => {
    await indexTorrentsAndDataDirs({ startup: true });
    // technically this will never resolve, but it's necessary to keep the process running
    await Promise.all([serve(options.port, options.host), jobsLoop()]);
}));
createCommandWithSharedOptions("rss", "Run an rss scan").action(withFullRuntime(async () => {
    await indexTorrentsAndDataDirs({ startup: true });
    await scanRssFeeds();
}));
createCommandWithSharedOptions("search", "Search for cross-seeds")
    .addOption(new Option("--torrents <torrents...>", "torrent files separated by spaces").hideHelp())
    .option("--no-exclude-older", "Don't Exclude torrents based on when they were first seen.")
    .option("--no-exclude-recent-search", "Don't Exclude torrents based on when they were last searched.")
    .action(withFullRuntime(() => bulkSearch()));
createCommandWithSharedOptions("inject", "Inject saved cross-seeds into your client (without filtering, see docs)")
    .option("--inject-dir <dir>", "Directory of torrent files to try to inject", fallback(fileConfig.injectDir, fileConfig.outputDir))
    .option("--ignore-titles", "Searchee and candidate titles do not need to pass the fuzzy matching check (useful if `cross-seed inject` erroneously rejects by title)", fallback(fileConfig.ignoreTitles, false))
    .option("--no-ignore-titles", "Searchee and candidate titles need to pass the fuzzy matching check (default)")
    .action(withFullRuntime(injectSavedTorrents));
createCommandWithSharedOptions("restore", "Use snatched torrents from torrent_cache to attempt to restore cross seeds. Will need to run `cross-seed inject` afterwards with dataDirs configured.").action(withFullRuntime(restoreFromTorrentCache));
createCommandWithSharedOptions("test-notification", "Send a test notification").action(withFullRuntime(sendTestNotification));
program.showHelpAfterError("(add --help for additional information)");
await program.parseAsync();
//# sourceMappingURL=cmd.js.map