feat(sites): structure release detail pages and rework the detail drawer (DETAIL-1)

Adapters (pure functions + tests):
- detail-description: strip HTML/BBCode residue, drop site template blocks and
  ad/signature lines (configurable rules), parse PT info blocks (◎ / 【】 / 『』 /
  「键:」) into infoBlock and body paragraphs
- mediainfo: MediaInfo and BDInfo (quick summary + full report) into a summary
  of container/duration/bitrate/size, video (HDR/DV), audio and subtitle tracks
- file-tree: file list into a flattened directory tree with sizes and
  main/sample/subtitle/extra roles
- formatDetailPage combines them; the server computes it when reading the
  cached detail page, so existing caches benefit without a refetch

Contracts: SiteReleasePage gains infoBlock, paragraphs, media, fileTree.

Web: drawer now reads top-down: meta row (promo/H&R, size, peers, upload time,
IMDb/TMDB/豆瓣 tags) → 资料 → 简介 (collapsible) → 技术规格 (raw folded) →
截图 grid with keyboard lightbox → 文件 tree; inventory overlay moved into the
fixed footer next to 下载.

Fixtures: 6 desensitized samples from the local cache (site.detail and RSS
descriptions) plus synthetic multi-track MediaInfo, BDInfo, bracket blocks and
directory file lists.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 20:50:42 +08:00
1 parent c846e91cc4
commit 9be9813021
16 files changed
+1992 -175

No files matched your search

@@ -1,4 +1,4 @@
import type { SiteDetailPage } from "@umt/adapters";
import { formatDetailPage, type SiteDetailPage } from "@umt/adapters";
import type { SiteReleasePage } from "@umt/contracts";
import { sql } from "@umt/db";
import { sharedCache, storageSettings } from "../../../shared-cache.ts";
@@ -99,6 +99,12 @@ export async function detailScreenshotRequest(
export async function loadDetailPage(rt: SourcesRuntime, srId: string): Promise<SiteReleasePage | null> {
const p = await loadStored(rt, srId);
if (!p) return null;
// 展示结构每次现算:旧缓存也能用,解析规则改了不必清缓存
const formatted = formatDetailPage({
descriptionText: p.descriptionText ?? null,
mediaInfo: p.mediaInfo ?? null,
files: p.files ?? [],
});
return {
fetchedAt: p.fetchedAt,
descriptionText: p.descriptionText,
@@ -118,6 +124,7 @@ export async function loadDetailPage(rt: SourcesRuntime, srId: string): Promise<
files: p.files,
tags: p.tags,
uploader: p.uploader,
...formatted,
};
}