feat: extract upload date for 1337x results

Parse the "Date uploaded" field from 1337x detail pages into TorrentResult.added, matching the other sources. Reuses the detail-page fetch already done for the magnet, so no extra requests are made.
This commit is contained in:
Anand Hegde
2026-06-30 11:07:35 -04:00
committed by GitHub
parent 81f449ffda
commit 3c5b5597d9
2 changed files with 52 additions and 6 deletions
+23
View File
@@ -0,0 +1,23 @@
import { describe, it, expect } from "vitest";
import { parseUploadDate } from "./x1337";
const detail = (span: string) =>
`<ul class="list"><li><strong>Date uploaded</strong><span>${span}</span> </li></ul>`;
describe("parseUploadDate", () => {
it("parses the 'Mon. Dayth \\'YY' format to a UTC unix timestamp", () => {
const ts = parseUploadDate(detail("Jun. 26th '26"));
expect(ts).toBe(Math.floor(Date.UTC(2026, 5, 26) / 1000));
});
it("handles single-digit days and other ordinals", () => {
expect(parseUploadDate(detail("Jan. 1st '24"))).toBe(Math.floor(Date.UTC(2024, 0, 1) / 1000));
expect(parseUploadDate(detail("Mar. 3rd '25"))).toBe(Math.floor(Date.UTC(2025, 2, 3) / 1000));
expect(parseUploadDate(detail("Dec. 22nd '23"))).toBe(Math.floor(Date.UTC(2023, 11, 22) / 1000));
});
it("returns undefined when the field is missing or unparseable", () => {
expect(parseUploadDate("<div>no date here</div>")).toBeUndefined();
expect(parseUploadDate(detail("sometime"))).toBeUndefined();
});
});
+29 -6
View File
@@ -46,11 +46,33 @@ async function fetchText(url: string, opts: SearchOptions, retries: number): Pro
return res.text();
}
async function detailMagnet(base: string, path: string, opts: SearchOptions): Promise<string | null> {
const MONTHS: Record<string, number> = {
jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
};
// 1337x detail pages render "Date uploaded" as e.g. "Jun. 26th '26".
export function parseUploadDate(html: string): number | undefined {
const m = html.match(/Date uploaded<\/strong>\s*<span>\s*([A-Za-z]{3})\.?\s+(\d{1,2})[a-z]{2}\s*'(\d{2})/i);
if (!m) return undefined;
const month = MONTHS[m[1]!.toLowerCase()];
if (month === undefined) return undefined;
const day = Number(m[2]);
const year = 2000 + Number(m[3]);
const secs = Math.floor(Date.UTC(year, month, day) / 1000);
return Number.isNaN(secs) ? undefined : secs;
}
async function detailInfo(
base: string,
path: string,
opts: SearchOptions,
): Promise<{ magnet: string; added?: number } | null> {
try {
const html = await fetchText(`${base}${path}`, opts, 1);
const raw = html.match(/magnet:\?xt=urn:btih:[^"'<>\s]+/i)?.[0];
return raw ? unescapeEntities(raw) : null;
if (!raw) return null;
return { magnet: unescapeEntities(raw), added: parseUploadDate(html) };
} catch {
return null;
}
@@ -97,9 +119,9 @@ async function search(
const rows = matched.slice(0, MAX_DETAILS);
const settled = await Promise.all(
rows.map(async (row): Promise<TorrentResult | null> => {
const magnet = await detailMagnet(base, row.path, opts);
const infoHash = magnet?.match(/urn:btih:([a-zA-Z0-9]+)/i)?.[1]?.toLowerCase();
if (!magnet || !infoHash) return null;
const detail = await detailInfo(base, row.path, opts);
const infoHash = detail?.magnet?.match(/urn:btih:([a-zA-Z0-9]+)/i)?.[1]?.toLowerCase();
if (!detail || !infoHash) return null;
return {
infoHash,
name: row.name,
@@ -107,7 +129,8 @@ async function search(
seeders: row.seeders,
leechers: row.leechers,
source,
magnet,
magnet: detail.magnet,
added: detail.added,
};
}),
);