HHClub (憨憨) was checked read-only against a real account (one detail
request; list, user data and health came from the product's own fetch).
Parsing fixes:
- list titles carried the site's new-torrent marker "[新]" (58 of 100
rows; the detail page title has none); stripped with a replace filter
- refreshing a detail overwrote seeders / leechers / snatched / category
name / H&R with null because the HHClub div detail page lacks them;
detail values now fall back to the list snapshot (mergeDetailOverList),
and an unknown detail promotion keeps the list promotion
- level "贵宾" is VIP (umt.vipLevels)
- sanitized minimal fixture HHANCLUB_LIST_PAGE_2026 + tests; README marks
HHClub as verified 2026-09-29
Size, counts, category ids/names, +08:00 time and subtitle were correct.
Request restraint:
- site definitions may carry umt.defaults; merged with settings.sites
(longer intervals, fewer pages): new settings defaultSnapshotMaxPages 1,
defaultHealthCheckIntervalSec 6 h, defaultDetailCacheSec 24 h; default
burst 2. HHClub: snapshot 60 min page 1 only, health 6 h, search
concurrency 1, detail cache 24 h, 1 token / 3 s burst 2
- periodic / manual snapshots are capped to that page count; scheduled
health checks skip sites checked within the interval; detail refresh is
served from cache inside the window; live searches on one site queue
- every rate-limiter token is counted per site per hour
(site_request_count, migration 0046); GET /sites/{id}/request-stats;
site overview and resource list show requests in the last 24 hours
Resource list: two-layer rows. Main line: title + spec tags (episodes,
resolution, source, codec, HDR, group, 国语/中字, site tags) + promotion
with time left + H&R. Second line: subtitle (one line, ellipsis) and
fixed-width category / size / seed-leech-snatched / upload time
(relative, absolute on hover) columns, stale badge, inventory overlay;
folds into a card on phones.
Also aligns identity_candidate.normalized_title order in db types with
the migrations (schema drift test).
Takes effect after the stack is rebuilt (migration 0046).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
import { ArrowRight } from "lucide-react";
|
|
import { Link } from "react-router";
|
|
import { consolePath } from "~/components/console/nav.ts";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { BrowserProvidersBlock } from "./browser-providers.tsx";
|
|
import { num, str } from "./resource-section.tsx";
|
|
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
|
|
|
// 站点默认(site-center §8):站点没单独设置时用的限流、快照周期、UA、代理;浏览器服务(BR-1)。
|
|
// 站点、凭证、账号在「站点」页管理;站点级覆盖在站点的"编辑"里。
|
|
|
|
const DEFAULTS_CARD: CardSpec = {
|
|
anchor: "defaults",
|
|
title: "默认值",
|
|
description: "站点没有单独设置时使用。限流不会比站点定义自带的更快。",
|
|
fields: [
|
|
{
|
|
name: "sites.defaultRateLimit.requestsPerMinute",
|
|
label: "每分钟请求数",
|
|
kind: "number",
|
|
hint: "每个站点各自计数",
|
|
},
|
|
{ name: "sites.defaultRateLimit.concurrency", label: "并发", kind: "number", hint: "同一站点同时进行的请求" },
|
|
{
|
|
name: "sites.defaultSnapshotIntervalSec",
|
|
label: "快照周期(分钟)",
|
|
kind: "number",
|
|
hint: "站点开了定时快照但没设周期时使用;5–10080",
|
|
},
|
|
{
|
|
name: "sites.defaultSnapshotMaxPages",
|
|
label: "快照页数",
|
|
kind: "number",
|
|
hint: "定时和手动快照最多抓几页列表;站点定义更少时按站点;1–20",
|
|
},
|
|
{
|
|
name: "sites.defaultHealthCheckIntervalSec",
|
|
label: "健康检测间隔(小时)",
|
|
kind: "number",
|
|
hint: "同一站点两次定时检测至少隔这么久;站点定义更长时按站点",
|
|
},
|
|
{
|
|
name: "sites.defaultDetailCacheSec",
|
|
label: "详情缓存(小时)",
|
|
kind: "number",
|
|
hint: "期内不重复抓同一条资源详情;0 为不缓存",
|
|
},
|
|
{
|
|
name: "sites.userDataMaxSeedingPages",
|
|
label: "做种列表页数上限",
|
|
kind: "number",
|
|
hint: "拉取用户数据时最多翻几页做种列表;1–50",
|
|
},
|
|
{
|
|
name: "sites.userAgent",
|
|
label: "User-Agent",
|
|
kind: "text",
|
|
hint: "账号没填 UA 时,登录和拉取用户数据用这个",
|
|
wide: true,
|
|
},
|
|
{
|
|
name: "sites.proxyUrl",
|
|
label: "代理地址",
|
|
kind: "url",
|
|
placeholder: "http://127.0.0.1:7890",
|
|
hint: "只对开了「使用代理」的站点生效;留空不用代理",
|
|
wide: true,
|
|
},
|
|
],
|
|
fromSettings: (s) => ({
|
|
"sites.defaultRateLimit.requestsPerMinute": n(s.sites.defaultRateLimit.requestsPerMinute),
|
|
"sites.defaultRateLimit.concurrency": n(s.sites.defaultRateLimit.concurrency),
|
|
"sites.defaultSnapshotIntervalSec": n(Math.round(s.sites.defaultSnapshotIntervalSec / 60)),
|
|
"sites.userDataMaxSeedingPages": n(s.sites.userDataMaxSeedingPages),
|
|
"sites.defaultSnapshotMaxPages": n(s.sites.defaultSnapshotMaxPages),
|
|
"sites.defaultHealthCheckIntervalSec": n(s.sites.defaultHealthCheckIntervalSec / 3600),
|
|
"sites.defaultDetailCacheSec": n(s.sites.defaultDetailCacheSec / 3600),
|
|
"sites.userAgent": s.sites.userAgent,
|
|
"sites.proxyUrl": s.sites.proxyUrl ?? "",
|
|
}),
|
|
toPatch: (v, s) => {
|
|
const min = num(v["sites.defaultSnapshotIntervalSec"]);
|
|
const rpm = num(v["sites.defaultRateLimit.requestsPerMinute"]);
|
|
const concurrency = num(v["sites.defaultRateLimit.concurrency"]);
|
|
const healthH = num(v["sites.defaultHealthCheckIntervalSec"]);
|
|
const detailH = num(v["sites.defaultDetailCacheSec"]);
|
|
return {
|
|
sites: {
|
|
defaultRateLimit: {
|
|
...s.sites.defaultRateLimit,
|
|
requestsPerMinute: rpm,
|
|
concurrency,
|
|
},
|
|
defaultSnapshotIntervalSec: min === undefined ? undefined : Math.round(min * 60),
|
|
userDataMaxSeedingPages: num(v["sites.userDataMaxSeedingPages"]),
|
|
defaultSnapshotMaxPages: num(v["sites.defaultSnapshotMaxPages"]),
|
|
defaultHealthCheckIntervalSec: healthH === undefined ? undefined : Math.round(healthH * 3600),
|
|
defaultDetailCacheSec: detailH === undefined ? undefined : Math.round(detailH * 3600),
|
|
userAgent: str(v["sites.userAgent"]) || undefined,
|
|
proxyUrl: str(v["sites.proxyUrl"]) || null,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
export function SitesSection() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="pg-slab flex flex-wrap items-center justify-between gap-3 rounded-xl px-4 py-3">
|
|
<p className="text-caption text-muted-foreground">
|
|
站点和凭证在「站点」页管理;单个站点的限流、周期、代理在站点的"编辑"里改。
|
|
</p>
|
|
<Button asChild size="sm" variant="outline">
|
|
<Link to={consolePath("sites")}>
|
|
打开站点页
|
|
<ArrowRight />
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
<SettingsGate>{(s) => <SettingsCard section="sites" spec={DEFAULTS_CARD} settings={s} />}</SettingsGate>
|
|
<BrowserProvidersBlock />
|
|
</div>
|
|
);
|
|
}
|