commits
tags
// Phase 1 deliverable: a throwaway console harness proving the core engine
// (queue + persistence + bootguard + one source) end-to-end, ahead of any
// TUI. `search` hits apibay and prints results; `run` restores persisted
// state (in safe mode if the previous run was killed mid-restore), adds an
// optional magnet/info hash, and prints live progress once a second until
// Ctrl+C -- or until something less polite than Ctrl+C ends it, which is
// exactly the case bootguard exists to survive.
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <iostream>
#include <thread>
#include "torlinkc/config/config.hpp"
#include "torlinkc/config/paths.hpp"
#include "torlinkc/engine/bootguard.hpp"
#include "torlinkc/engine/persist.hpp"
#include "torlinkc/engine/queue.hpp"
#include "torlinkc/engine/reconcile.hpp"
#include "torlinkc/sources/cache.hpp"
#include "torlinkc/sources/magnet.hpp"
#include "torlinkc/sources/piratebay.hpp"
#include "torlinkc/util/format.hpp"
using namespace torlinkc;
namespace {
std::atomic<bool> gStop{false};
void handleSignal(int) { gStop = true; }
void printUsage() {
std::cout << "usage: torlinkc_core_cli search <query>\n"
" torlinkc_core_cli run [magnet|infohash] [download-dir]\n";
}
void doSearch(const std::string& query) {
SearchCache cache;
std::vector<TorrentResult> results = cache.cachedSearch(tpbMoviesSource(), query);
auto tv = cache.cachedSearch(tpbTvSource(), query);
results.insert(results.end(), tv.begin(), tv.end());
if (results.empty()) {
std::cout << "no results\n";
return;
}
for (const auto& r : results) {
std::cout << "[" << r.source << "] " << r.name << " (" << formatBytes(static_cast<double>(r.sizeBytes))
<< ", seeders=" << r.seeders << ", leechers=" << r.leechers << ")\n"
<< " " << r.magnet << "\n";
}
}
void printItem(const QueueItem& it) {
std::cout << " " << it.name.substr(0, 60) << " [" << downloadStatusToString(it.status) << "] " << it.progress
<< "% "
<< formatBytes(static_cast<double>(it.downloadedBytes)) << "/"
<< formatBytes(static_cast<double>(it.totalBytes)) << " " << formatBytes(it.speed) << "/s peers="
<< it.peers;
if (it.error) std::cout << " error=" << *it.error;
std::cout << "\n";
}
void printSeed(const SeedItem& s) {
std::cout << " " << s.name.substr(0, 60) << " [" << seedStatusToString(s.status) << "] "
<< formatBytes(static_cast<double>(s.uploaded)) << " up " << formatBytes(s.uploadSpeed)
<< "/s peers=" << s.peers << "\n";
}
int runHarness(const std::vector<std::string>& args) {
std::signal(SIGINT, handleSignal);
std::signal(SIGTERM, handleSignal);
Config config = loadConfig();
std::cout << "state dir: " << paths::queueFile() << "\n";
std::cout << "download dir: " << config.downloadDir << "\n";
const bool safe = wasBootInterrupted();
if (safe) {
std::cout << "*** previous run did not shut down cleanly -- restoring in SAFE MODE (all paused) ***\n";
}
armBootMarker();
DownloadQueue queue;
queue.setTrackers(config.trackers);
queue.restoreHistory(loadHistory());
queue.restore(reconcileQueue(loadQueue()), RestoreOptions{safe});
queue.restoreSeeds(loadSeeds(), RestoreOptions{safe});
if (args.size() >= 2) {
auto parsed = parseInput(args[1]);
if (!parsed) {
std::cerr << "not a magnet URI or info hash: " << args[1] << "\n";
return 1;
}
const std::string dir = args.size() >= 3 ? args[2] : config.downloadDir;
queue.add(AddInput{parsed->infoHash, parsed->name, parsed->magnet, "", std::nullopt}, dir);
std::cout << "added " << parsed->name << " -> " << dir << "\n";
}
std::cout << "running. Ctrl+C to stop cleanly (kill -9 <pid> to test crash recovery instead).\n";
const auto bootAt = std::chrono::steady_clock::now();
bool settled = false;
while (!gStop) {
queue.tick();
if (!settled && std::chrono::steady_clock::now() - bootAt > std::chrono::milliseconds(kBootSettleMs)) {
// The boot survived long enough to be worth trusting.
disarmBootMarker();
settled = true;
}
auto items = queue.getItems();
auto seeds = queue.getSeeds();
if (!items.empty() || !seeds.empty()) {
std::cout << "\033[2J\033[H"; // clear screen for a readable live view
std::cout << "Downloads:\n";
for (const auto& it : items) printItem(it);
std::cout << "Seeding:\n";
for (const auto& s : seeds) printSeed(s);
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
std::cout << "\nshutting down cleanly\n";
queue.suspend();
return 0;
}
} // namespace
int main(int argc, char** argv) {
std::vector<std::string> args(argv + 1, argv + argc);
if (args.empty()) {
printUsage();
return 1;
}
if (args[0] == "search") {
if (args.size() < 2) {
printUsage();
return 1;
}
doSearch(args[1]);
return 0;
}
if (args[0] == "run") return runHarness(args);
printUsage();
return 1;
}