docs(plan): on-demand site detail page parsing (HHClub details.php)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
94fd48bff2
commit
ac478673bc
39 files changed
+2446
-339
No files matched your search
@@ -29,7 +29,8 @@ import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
import { delegateToAgent, SearchDownloadDialog } from "./search-download-dialog.tsx";
|
||||
import { gbToBytes, INVENTORY_STATE, promoLabel, SITE_STATUS, SORT_OPTIONS } from "./search-labels.ts";
|
||||
|
||||
// 资源段:跨站结果的筛选(站点/分辨率/来源/大小/免费/H&R)、排序、游标;行操作同站点工作台。
|
||||
// 资源段(SEARCH-2 第二阶段):按作品或关键词、在选中的站点上搜;筛选(站点/分辨率/来源/大小/免费/H&R)、排序、游标;
|
||||
// 行操作同站点工作台(库存叠加、下载、委托 Agent)。
|
||||
|
||||
/** URL 里的资源筛选(字符串形态,空串 = 不筛) */
|
||||
export type ReleaseFilterState = {
|
||||
@@ -55,12 +56,25 @@ export const RELEASE_FILTER_KEYS = [
|
||||
"order",
|
||||
] as const satisfies readonly (keyof ReleaseFilterState)[];
|
||||
|
||||
export function toApiFilters(q: string, f: ReleaseFilterState): ReleaseSearchFilters {
|
||||
/** 搜什么:关键词,或按作品(服务端生成查询词) */
|
||||
export type ReleaseTarget = { q?: string; entityId?: string; tmdb?: string };
|
||||
|
||||
/** URL 参数 work → 搜索目标:local:<id> / tmdb:<type>:<id> / q:<关键词> */
|
||||
export function parseWorkRef(ref: string): ReleaseTarget | null {
|
||||
if (ref.startsWith("local:")) return { entityId: ref.slice(6) };
|
||||
const m = /^tmdb:(movie|tv):(\d+)$/.exec(ref);
|
||||
if (m) return { tmdb: `${m[1]}:${m[2]}` };
|
||||
if (ref.startsWith("q:") && ref.slice(2).trim()) return { q: ref.slice(2).trim() };
|
||||
return null;
|
||||
}
|
||||
|
||||
export function toApiFilters(target: ReleaseTarget, f: ReleaseFilterState, siteIds = ""): ReleaseSearchFilters {
|
||||
const bool = (v: string) => (v === "true" || v === "false" ? v : undefined);
|
||||
const sort = SORT_OPTIONS.some((o) => o.value === f.sort) ? (f.sort as ReleaseSearchFilters["sort"]) : undefined;
|
||||
return {
|
||||
q,
|
||||
sites: f.sites || undefined,
|
||||
...target,
|
||||
sites: siteIds || undefined,
|
||||
siteFilter: f.sites || undefined,
|
||||
resolution: f.resolution || undefined,
|
||||
source: f.source || undefined,
|
||||
minSize: gbToBytes(f.minGb),
|
||||
@@ -75,17 +89,22 @@ export function toApiFilters(q: string, f: ReleaseFilterState): ReleaseSearchFil
|
||||
const csvList = (s: string) => (s ? s.split(",").filter(Boolean) : []);
|
||||
|
||||
export function ReleaseResults({
|
||||
q,
|
||||
target,
|
||||
siteIds,
|
||||
filters,
|
||||
setFilter,
|
||||
resetFilters,
|
||||
onChangeSites,
|
||||
}: {
|
||||
q: string;
|
||||
target: ReleaseTarget;
|
||||
/** 选中的站点(逗号分隔);空 = 服务端按默认站点设置 */
|
||||
siteIds: string;
|
||||
onChangeSites?: () => void;
|
||||
filters: ReleaseFilterState;
|
||||
setFilter: (k: keyof ReleaseFilterState, v: string) => void;
|
||||
resetFilters: () => void;
|
||||
}) {
|
||||
const api = useMemo(() => toApiFilters(q, filters), [q, filters]);
|
||||
const api = useMemo(() => toApiFilters(target, filters, siteIds), [target, filters, siteIds]);
|
||||
const res = useReleaseSearch(api);
|
||||
const refresh = useRefreshReleaseSearch();
|
||||
const pages = res.data?.pages ?? [];
|
||||
@@ -109,20 +128,29 @@ export function ReleaseResults({
|
||||
) : null}
|
||||
</h2>
|
||||
{first ? (
|
||||
<p className="mt-0.5 text-meta text-muted-foreground tabular-nums">
|
||||
{first.cached ? "缓存结果" : "站点实时结果"} · 抓取于 {relTime(first.fetchedAt)} · {first.tookMs} ms
|
||||
<p className="mt-0.5 text-meta break-words text-muted-foreground tabular-nums">
|
||||
{first.sites.length} 个站点 · {first.cached ? "缓存结果" : "站点实时结果"} · 抓取于{" "}
|
||||
{relTime(first.fetchedAt)} · {first.tookMs} ms
|
||||
{first.queries?.length ? ` · 查询词 ${first.queries.map((x) => `「${x}」`).join("")}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={refresh.isPending || res.isFetching}
|
||||
onClick={() => refresh.mutate(api, { onError: (e) => toast.error(errorMessage(e, "刷新失败")) })}
|
||||
>
|
||||
<RefreshCw className={cn(refresh.isPending && "animate-spin")} />
|
||||
{refresh.isPending ? "查询中…" : "刷新"}
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{onChangeSites ? (
|
||||
<Button variant="ghost" size="sm" onClick={onChangeSites}>
|
||||
换站点
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={refresh.isPending || res.isFetching}
|
||||
onClick={() => refresh.mutate(api, { onError: (e) => toast.error(errorMessage(e, "刷新失败")) })}
|
||||
>
|
||||
<RefreshCw className={cn(refresh.isPending && "animate-spin")} />
|
||||
{refresh.isPending ? "查询中…" : "刷新"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{first ? <CoverageNotice page={first} /> : null}
|
||||
@@ -143,8 +171,8 @@ export function ReleaseResults({
|
||||
<EmptyState
|
||||
plain
|
||||
icon={PackageSearch}
|
||||
title={filtered ? "没有符合筛选条件的资源" : "各站点都没有找到资源"}
|
||||
description={filtered ? "放宽筛选条件再看。" : "换个关键词,或到站点工作台检查站点状态。"}
|
||||
title={filtered ? "没有符合筛选条件的资源" : "所选站点都没有找到资源"}
|
||||
description={filtered ? "放宽筛选条件再看。" : "换站点或换关键词再搜,或到站点页检查站点状态。"}
|
||||
className="py-12"
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user