commits
tags
#include "torlinkc/sources/x1337.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <ctime>
#include <map>
#include <regex>
#include <sstream>
#include <unordered_set>
#include "torlinkc/sources/rss.hpp"
#include "torlinkc/util/format.hpp"
#include "torlinkc/util/net.hpp"
#include "torlinkc/util/url_encode.hpp"
namespace torlinkc {
namespace {
constexpr std::array<const char*, 4> kHosts = {"1337x.to", "1337x.st", "x1337x.ws", "1337xx.to"};
// Shared mutable state across the two 1337x sources (movies + TV), which the
// Phase 3 SearchAggregator queries concurrently on separate threads -- an
// atomic (vs. the TS original's plain module-level `let`) is what makes that
// safe here.
std::atomic<int> gWorkingHostIndex{0};
constexpr int kMaxDetails = 4;
const std::unordered_set<std::string> kStopWords = {"the", "a", "an", "of", "and", "or", "to"};
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) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
return s;
}
std::vector<std::string> splitWhitespace(const std::string& s) {
std::istringstream iss(s);
std::vector<std::string> out;
std::string tok;
while (iss >> tok) out.push_back(tok);
return out;
}
struct Row {
std::string name;
std::string path;
int seeders = 0;
int leechers = 0;
std::int64_t sizeBytes = 0;
};
// Splits like JS's String.split(/<tr[\s>]/i): n matches produce n+1 segments,
// segment[0] being whatever preceded the first match.
std::vector<std::string> splitOnTrTag(const std::string& html) {
static const std::regex kTrTag(R"(<tr[\s>])", std::regex::icase);
std::vector<std::string> segments;
auto begin = std::sregex_iterator(html.begin(), html.end(), kTrTag);
auto end = std::sregex_iterator();
std::size_t lastEnd = 0;
for (auto it = begin; it != end; ++it) {
segments.push_back(html.substr(lastEnd, static_cast<std::size_t>(it->position()) - lastEnd));
lastEnd = static_cast<std::size_t>(it->position() + it->length());
}
segments.push_back(html.substr(lastEnd));
return segments;
}
std::vector<Row> parseRows(const std::string& html) {
auto start = html.find("table-list");
if (start == std::string::npos) return {};
static const std::regex kLink(R"re(href="(/torrent/[^"]+)"[^>]*>([^<]+)</a>)re", std::regex::icase);
static const std::regex kSize(R"(class="coll-4 size[^"]*">\s*([\d.]+\s*[KMGT]i?B))", std::regex::icase);
static const std::regex kSeeds(R"(class="coll-2 seeds[^"]*">\s*(\d+))", std::regex::icase);
static const std::regex kLeeches(R"(class="coll-3 leeches[^"]*">\s*(\d+))", std::regex::icase);
std::vector<Row> out;
auto segments = splitOnTrTag(html.substr(start));
for (std::size_t i = 1; i < segments.size(); ++i) {
const std::string& tr = segments[i];
std::smatch link;
if (!std::regex_search(tr, link, kLink)) continue;
Row row;
row.path = link[1].str();
row.name = unescapeEntities(trim(link[2].str()));
std::smatch m;
row.sizeBytes = std::regex_search(tr, m, kSize) ? parseSize(m[1].str()) : 0;
row.seeders = std::regex_search(tr, m, kSeeds) ? std::stoi(m[1].str()) : 0;
row.leechers = std::regex_search(tr, m, kLeeches) ? std::stoi(m[1].str()) : 0;
out.push_back(std::move(row));
}
return out;
}
const std::map<std::string, int>& monthTable() {
static const std::map<std::string, int> months = {{"jan", 0}, {"feb", 1}, {"mar", 2}, {"apr", 3}, {"may", 4},
{"jun", 5}, {"jul", 6}, {"aug", 7}, {"sep", 8}, {"oct", 9},
{"nov", 10}, {"dec", 11}};
return months;
}
struct DetailInfo {
std::string magnet;
std::optional<std::int64_t> added;
};
std::optional<DetailInfo> fetchDetail(const std::string& base, const std::string& path,
const std::stop_token& stopToken) {
try {
FetchOptions opts;
opts.retries = 1;
opts.stopToken = stopToken;
HttpResponse res = fetchResilient(base + path, opts);
if (!res.ok()) return std::nullopt;
static const std::regex kMagnetRe(R"(magnet:\?xt=urn:btih:[^"'<>\s]+)", std::regex::icase);
std::smatch m;
if (!std::regex_search(res.body, m, kMagnetRe)) return std::nullopt;
DetailInfo info;
info.magnet = unescapeEntities(m[0].str());
info.added = parseUploadDate(res.body);
return info;
} catch (const std::exception&) {
return std::nullopt;
}
}
std::vector<TorrentResult> search(const std::string& query, const std::string& category, const std::string& source,
const std::stop_token& stopToken) {
const std::string q = trim(query);
std::string path;
if (!q.empty()) {
std::string encoded = encodeURIComponent(q);
std::string withPlus;
withPlus.reserve(encoded.size());
for (std::size_t i = 0; i < encoded.size();) {
if (encoded.compare(i, 3, "%20") == 0) {
withPlus += '+';
i += 3;
} else {
withPlus += encoded[i];
i += 1;
}
}
path = "/category-search/" + withPlus + "/" + category + "/1/";
} else {
path = category == "Movies" ? "/popular-movies" : "/popular-tv";
}
std::string base, html;
std::exception_ptr lastError;
const int startIdx = gWorkingHostIndex.load();
for (std::size_t i = 0; i < kHosts.size(); ++i) {
const std::size_t hostIdx = (static_cast<std::size_t>(startIdx) + i) % kHosts.size();
const std::string candidate = std::string("https://") + kHosts[hostIdx];
try {
FetchOptions opts;
opts.retries = i == 0 ? 2 : 0;
opts.stopToken = stopToken;
HttpResponse res = fetchResilient(candidate + path, opts);
if (!res.ok()) throw HttpError(res.status, "1337x returned " + std::to_string(res.status));
html = res.body;
base = candidate;
gWorkingHostIndex.store(static_cast<int>(hostIdx));
break;
} catch (const std::exception&) {
lastError = std::current_exception();
if (stopToken.stop_requested()) std::rethrow_exception(lastError);
}
}
if (base.empty()) {
if (lastError) std::rethrow_exception(lastError);
throw HttpError(0, "1337x unreachable");
}
const auto all = parseRows(html);
const auto tokens = splitWhitespace(toLower(q));
std::vector<std::string> meaningful;
for (const auto& t : tokens)
if (!kStopWords.count(t)) meaningful.push_back(t);
const std::vector<std::string>& need = meaningful.empty() ? tokens : meaningful;
std::vector<Row> matched;
if (!need.empty()) {
for (const auto& r : all) {
const std::string name = toLower(r.name);
bool matchesAll = true;
for (const auto& t : need) {
if (name.find(t) == std::string::npos) {
matchesAll = false;
break;
}
}
if (matchesAll) matched.push_back(r);
}
} else {
matched = all;
}
std::stable_sort(matched.begin(), matched.end(), [](const Row& a, const Row& b) { return a.seeders > b.seeders; });
if (matched.size() > static_cast<std::size_t>(kMaxDetails)) matched.resize(kMaxDetails);
static const std::regex kInfoHashRe(R"(urn:btih:([a-zA-Z0-9]+))", std::regex::icase);
std::vector<TorrentResult> out;
for (const auto& row : matched) {
auto detail = fetchDetail(base, row.path, stopToken);
if (!detail) continue;
std::smatch m;
if (!std::regex_search(detail->magnet, m, kInfoHashRe)) continue;
TorrentResult r;
r.infoHash = toLower(m[1].str());
r.name = row.name;
r.sizeBytes = row.sizeBytes;
r.seeders = row.seeders;
r.leechers = row.leechers;
r.source = source;
r.magnet = detail->magnet;
r.added = detail->added;
out.push_back(std::move(r));
}
return out;
}
} // namespace
std::optional<std::int64_t> parseUploadDate(const std::string& html) {
static const std::regex kRe(R"(Date uploaded</strong>\s*<span>\s*([A-Za-z]{3})\.?\s+(\d{1,2})[a-z]{2}\s*'(\d{2}))",
std::regex::icase);
std::smatch m;
if (!std::regex_search(html, m, kRe)) return std::nullopt;
const auto it = monthTable().find(toLower(m[1].str()));
if (it == monthTable().end()) return std::nullopt;
std::tm tm{};
tm.tm_year = 2000 + std::stoi(m[3].str()) - 1900;
tm.tm_mon = it->second;
tm.tm_mday = std::stoi(m[2].str());
const time_t secs = timegm(&tm);
return secs == static_cast<time_t>(-1) ? std::nullopt : std::optional<std::int64_t>(secs);
}
Source x1337MoviesSource() {
Source s;
s.id = "x1337-movies";
s.label = "1337x";
s.groups = {SourceGroup::Movies};
s.homepage = "https://1337x.to";
s.reportsHealth = true;
s.search = [](const std::string& query, const SearchOptions& opts) {
return search(query, "Movies", "x1337-movies", opts.stopToken);
};
return s;
}
Source x1337TvSource() {
Source s;
s.id = "x1337-tv";
s.label = "1337x";
s.groups = {SourceGroup::TV};
s.homepage = "https://1337x.to";
s.reportsHealth = true;
s.search = [](const std::string& query, const SearchOptions& opts) {
return search(query, "TV", "x1337-tv", opts.stopToken);
};
return s;
}
} // namespace torlinkc