foxygit / Torlinkc Log in
commits tags

/src/sources/subsplease.cpp · 2.89 KB

raw
#include "torlinkc/sources/subsplease.hpp"

#include <array>
#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 {

constexpr std::array<const char*, 3> kResPreference = {"1080", "720", "480"};

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::optional<json> pickBest(const json& downloads) {
  for (const char* res : kResPreference) {
    for (const auto& d : downloads) {
      if (d.value("res", "") == res && !d.value("magnet", "").empty()) return d;
    }
  }
  for (const auto& d : downloads) {
    if (!d.value("magnet", "").empty()) return d;
  }
  return std::nullopt;
}

}  // namespace

Source subspleaseSource() {
  Source s;
  s.id = "subsplease";
  s.label = "SubsPlease";
  s.groups = {SourceGroup::Anime};
  s.homepage = "https://subsplease.org";
  // The SubsPlease API has no swarm data; every result reports seeders: 0.
  s.reportsHealth = false;
  s.search = [](const std::string& query, const SearchOptions& opts) {
    const std::string q = trim(query);
    std::string qs = "tz=UTC";
    qs += q.empty() ? "&f=latest" : "&f=search&s=" + encodeURIComponent(q);

    FetchOptions fetchOpts;
    fetchOpts.stopToken = opts.stopToken;
    HttpResponse res = fetchResilient("https://subsplease.org/api/?" + qs, fetchOpts);
    if (!res.ok()) throw HttpError(res.status, "SubsPlease returned " + std::to_string(res.status));

    const json root = json::parse(res.body);
    std::vector<TorrentResult> out;
    if (!root.is_object()) return out;

    static const std::regex kSizeRe(R"([?&]xl=(\d+))");

    for (const auto& entry : root.items()) {
      const json& e = entry.value();
      const auto downloads = e.value("downloads", json::array());
      auto dl = pickBest(downloads);
      if (!dl) continue;
      const std::string magnet = dl->value("magnet", "");
      auto parsed = parseMagnet(magnet);
      if (!parsed) continue;

      const std::string show = e.value("show", "Unknown");
      const std::string episode = e.value("episode", "");
      const std::string resStr = dl->value("res", "?");

      std::smatch m;
      const std::int64_t sizeBytes = std::regex_search(magnet, m, kSizeRe) ? std::stoll(m[1].str()) : 0;

      TorrentResult r;
      r.infoHash = parsed->infoHash;
      r.name = show + (episode.empty() ? "" : " - " + episode) + " [" + resStr + "p]";
      r.sizeBytes = sizeBytes;
      r.source = "subsplease";
      r.magnet = parsed->magnet;
      r.added = parseDateToUnixSeconds(e.value("release_date", ""));
      out.push_back(std::move(r));
    }
    return out;
  };
  return s;
}

}  // namespace torlinkc