commits
tags
#include "torlinkc/sources/bittorrented.hpp"
#include <regex>
#include <nlohmann/json.hpp>
#include "torlinkc/sources/magnet.hpp"
#include "torlinkc/util/date_parse.hpp"
#include "torlinkc/util/net.hpp"
#include "torlinkc/util/url_encode.hpp"
using nlohmann::json;
namespace torlinkc {
namespace {
// The index requires a real query (the API rejects fewer than 3 characters),
// so an empty browse returns nothing rather than erroring.
constexpr std::size_t kMinQuery = 3;
const char* kBase = "https://bittorrented.com";
std::string trim(const std::string& s) {
auto first = s.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return "";
auto last = s.find_last_not_of(" \t\r\n");
return s.substr(first, last - first + 1);
}
std::string toLower(std::string s) {
for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return s;
}
} // namespace
std::vector<TorrentResult> mapBittorrentedResults(const json& results, const std::string& sourceId) {
static const std::regex kHexHash(R"(^[a-f0-9]{40}$)");
std::vector<TorrentResult> out;
for (const auto& r : results) {
const std::string infoHash = toLower(r.value("torrent_infohash", ""));
if (!std::regex_match(infoHash, kHexHash)) continue;
std::string name = r.value("torrent_name", "");
if (name.empty()) name = infoHash;
TorrentResult result;
result.infoHash = infoHash;
result.name = name;
result.sizeBytes = r.value("torrent_total_size", static_cast<std::int64_t>(0));
if (auto it = r.find("torrent_seeders"); it != r.end() && it->is_number()) result.seeders = it->get<int>();
if (auto it = r.find("torrent_leechers"); it != r.end() && it->is_number()) result.leechers = it->get<int>();
if (auto it = r.find("torrent_file_count"); it != r.end() && it->is_number()) {
result.numFiles = it->get<int>();
}
result.source = sourceId;
result.magnet = buildMagnet(infoHash, name);
result.added = parseDateToUnixSeconds(r.value("torrent_created_at", ""));
out.push_back(std::move(result));
}
return out;
}
Source bittorrentedSource() {
Source s;
s.id = "bittorrented";
s.label = "BitTorrented";
s.groups = {SourceGroup::Movies, SourceGroup::TV};
s.homepage = kBase;
s.reportsHealth = true;
s.search = [](const std::string& query, const SearchOptions& opts) -> std::vector<TorrentResult> {
const std::string q = trim(query);
if (q.size() < kMinQuery) return {};
FetchOptions fetchOpts;
fetchOpts.retries = 1;
fetchOpts.stopToken = opts.stopToken;
fetchOpts.headers = {"Accept: application/json"};
const std::string url = std::string(kBase) + "/api/search/torrents?q=" + encodeURIComponent(q) +
"&type=video&limit=50&sortBy=seeders&sortOrder=desc";
HttpResponse res = fetchResilient(url, fetchOpts);
if (!res.ok()) throw HttpError(res.status, "BitTorrented returned " + std::to_string(res.status));
const json root = json::parse(res.body);
return mapBittorrentedResults(root.value("results", json::array()), "bittorrented");
};
return s;
}
} // namespace torlinkc