diff --git a/docs/sessions/og-card-261015/progress/og-meta-progress.md b/docs/sessions/og-card-261015/progress/og-meta-progress.md new file mode 100644 index 00000000..b1804a9a --- /dev/null +++ b/docs/sessions/og-card-261015/progress/og-meta-progress.md @@ -0,0 +1,13 @@ +# OG 页面标签(og-meta)进度 + +Task: 分享页 `/view/{view_token}` 有卡片时输出 X / Open Graph 大图标签 +Dispatch: dlg-20261016-050109-9e5512 + +--- + +## 段1:实现 + 测试 + 红验收尾 + +- 当前阶段:verifying +- 本段结论:`view_transcript` 成功渲染 `transcript.html` 时用 `load_share_card` 读卡;`base.html` 新增默认空的 `og_image_meta` block,`transcript.html` 有卡才覆写 5 个标签并把 `og:description` 换成第一条要点。`tests/og_card/test_view_meta.py` 7 条全绿(有卡 / 无卡 / 不合契约 / 缓存目录缺失 / 转义 / mtime `?v=` / `?page=` 导出)。两次最小注入红验均为 AssertionError:`{% if share_card %}`→`{% if true %}` 让无卡页冒出 5 个标签;`headline|safe` 让引号拆断 meta 属性。还原后 `tests/og_card` 58 passed。 +- 关键决策与已否决方案:核实 `get_view_data_by_token` 成功/interrupted 载荷已含 `cache_dir`(`view_token_resolver.py:147`),成功路径直接用该字段;目录字段缺失时才回退 `get_cache_by_view_token`(与 `/og/` 同款),未改 ViewTokenResolver。读卡+stat 放进 `view_transcript` 内嵌函数经 `asyncio.to_thread` 执行,拒绝再抽模块级 helper(单消费者)。无卡不输出任何降级图。 +- 下一步唯一动作:推送 `card/vta-261016-og-meta2`,开 draft PR 到 main。 diff --git a/src/video_transcript_api/api/routes/views.py b/src/video_transcript_api/api/routes/views.py index 9159db8a..4c316a3a 100644 --- a/src/video_transcript_api/api/routes/views.py +++ b/src/video_transcript_api/api/routes/views.py @@ -1528,9 +1528,45 @@ async def view_transcript( if view_data["status"] in _CONTENT_VIEW_STATUSES: stats = await asyncio.to_thread(_prepare_success_view, view_data) + def _read_share_card_for_view() -> tuple[Optional[Dict[str, Any]], Optional[str]]: + """Load a contract-valid share card for the transcript page. + + Runs in a worker thread (SQLite/file IO). Missing dir, missing + json, invalid contract, or a json deleted between load and stat + all mean "no card": callers must not emit twitter/og image tags. + """ + cache_dir = view_data.get("cache_dir") + if not cache_dir: + cache_data = ViewTokenResolver(cache_manager).get_cache_by_view_token( + view_token + ) + cache_dir = (cache_data or {}).get("file_path") + if not cache_dir or not os.path.isdir(cache_dir): + return None, None + card = load_share_card(cache_dir) + if not card: + return None, None + try: + mtime_ns = (Path(cache_dir) / CARD_FILENAME).stat().st_mtime_ns + except FileNotFoundError: + logger.warning( + "分享卡片 JSON 在读取后消失: view_token={}", view_token + ) + return None, None + return card, f"{get_base_url()}/og/{view_token}.png?v={mtime_ns}" + + share_card, og_image_url = await asyncio.to_thread(_read_share_card_for_view) + return templates.TemplateResponse( "transcript.html", - {"request": request, **view_data, "view_token": view_token, "stats": stats}, + { + "request": request, + **view_data, + "view_token": view_token, + "stats": stats, + "share_card": share_card, + "og_image_url": og_image_url, + }, ) except Exception as exc: diff --git a/src/web/templates/base.html b/src/web/templates/base.html index 41ac9e5a..1e0f3bb4 100644 --- a/src/web/templates/base.html +++ b/src/web/templates/base.html @@ -18,6 +18,7 @@ + {% block og_image_meta %}{% endblock %} {% block title %}视频转录查看器{% endblock %}