foxygit / Torlinkc Log in
commits tags

/src/sources/bittorrented.cpp · 3.08 KB

raw
#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