Files
nanocode/skills/xiaohongshu-cli/xhs_cli/formatter_renderers.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

314 lines
10 KiB
Python

"""Rich renderers for human-readable CLI output."""
from __future__ import annotations
from typing import Any
from rich.panel import Panel
from rich.table import Table
from .formatter_normalizers import (
normalize_comments,
normalize_creator_notes,
normalize_feed,
normalize_note_detail,
normalize_notifications,
normalize_search_results,
normalize_topics,
normalize_user_info,
normalize_user_posts,
normalize_users,
)
from .formatter_utils import coerce_int, console, format_count, print_error, print_info
HOME_URL = "https://www.xiaohongshu.com"
def _build_note_url(
note_id: str,
xsec_token: str = "",
source: str = "pc_feed",
route: str = "explore",
) -> str:
"""Build a full note URL, including xsec_token when available."""
if xsec_token:
return f"{HOME_URL}/{route}/{note_id}?xsec_token={xsec_token}&xsec_source={source}"
return note_id # bare ID fallback
def _build_note_link(
note_id: str,
xsec_token: str = "",
source: str = "pc_feed",
route: str = "explore",
) -> str:
url = _build_note_url(note_id, xsec_token=xsec_token, source=source, route=route)
label = f"{route}/{note_id}"
if xsec_token:
return f"[link={url}][dim]{label}[/dim][/link]"
return f"[dim]{label}[/dim]"
def render_user_info(data: dict[str, Any]) -> None:
"""Render user profile info as a Rich panel."""
user = normalize_user_info(data)
nickname = user["nickname"]
red_id = user["red_id"]
desc = user["desc"]
ip_location = user["ip_location"]
user_id = user["user_id"]
gender_val = user["gender"]
gender = "♂️" if gender_val == 0 else "♀️" if gender_val == 1 else ""
stats = user["stats"]
table = Table(show_header=False, box=None, padding=(0, 2))
table.add_column("Key", style="dim")
table.add_column("Value")
table.add_row("昵称", f"[bold]{nickname}[/bold] {gender}")
if red_id:
table.add_row("小红书号", red_id)
if user_id:
table.add_row("User ID", user_id)
if desc:
table.add_row("简介", desc)
if ip_location:
table.add_row("IP 属地", ip_location)
if "fans" in stats:
table.add_row("粉丝", format_count(stats["fans"]))
if "follows" in stats:
table.add_row("关注", format_count(stats["follows"]))
if "interaction" in stats:
table.add_row("获赞与收藏", format_count(stats["interaction"]))
console.print(Panel(table, title=f"👤 {nickname}", border_style="blue"))
def render_note(data: dict[str, Any]) -> None:
"""Render a note as a Rich panel."""
note = normalize_note_detail(data)
if not note:
print_error("No note data found")
return
title = note["title"]
desc = note["desc"]
nickname = note["author"]
liked_count = note["liked_count"]
collected_count = note["collected_count"]
comment_count = note["comment_count"]
share_count = note["share_count"]
table = Table(show_header=False, box=None, padding=(0, 2))
table.add_column("Key", style="dim")
table.add_column("Value")
table.add_row("作者", f"[bold]{nickname}[/bold]")
table.add_row("标题", f"[bold]{title}[/bold]")
if desc:
display_desc = desc[:500] + "..." if len(desc) > 500 else desc
table.add_row("正文", display_desc)
tags = note["tags"]
if tags:
tag_str = " ".join(f"[cyan]#{tag}[/cyan]" for tag in tags)
table.add_row("标签", tag_str)
stats_str = (
f"❤️ {format_count(liked_count)} "
f"⭐ {format_count(collected_count)} "
f"💬 {format_count(comment_count)} "
f"🔗 {format_count(share_count)}"
)
table.add_row("数据", stats_str)
if note["image_count"]:
table.add_row("图片", f"{note['image_count']} 张")
console.print(Panel(table, title=f"📝 {title}", border_style="green"))
def render_search_results(data: dict[str, Any]) -> None:
"""Render search results as a Rich table."""
normalized = normalize_search_results(data)
items = normalized["items"]
if not items:
print_info("No results found")
return
has_next = normalized["has_more"]
table = Table(title="搜索结果", show_lines=True)
table.add_column("#", style="dim", width=3)
table.add_column("标题", width=30)
table.add_column("作者", width=10)
table.add_column("❤️", justify="right", width=8)
table.add_column("类型", width=4)
table.add_column("链接", style="cyan", no_wrap=True)
for i, item in enumerate(items, 1):
note_type = "📹" if item["note_type"] == "video" else "📷"
link = _build_note_link(
item["note_id"],
item.get("xsec_token", ""),
source="pc_search",
route="search_result",
)
table.add_row(str(i), item["title"], item["author"], item["liked"], note_type, link)
console.print(table)
if has_next:
print_info("More results available — use --page to paginate")
def render_comments(data: dict[str, Any]) -> None:
"""Render comments as a Rich display."""
comments = normalize_comments(data)
if not comments:
print_info("No comments found")
return
for comment in comments:
nickname = comment["nickname"]
content = comment["content"]
like_count = format_count(comment["like_count"])
sub_comment_count = coerce_int(comment["sub_comment_count"])
header = f"[bold]{nickname}[/bold] [dim]❤️ {like_count}[/dim]"
if sub_comment_count > 0:
header += f" [dim]💬 {sub_comment_count} replies[/dim]"
console.print(header)
console.print(f" {content}")
console.print()
def render_feed(data: dict[str, Any]) -> None:
"""Render feed items as a Rich table."""
items = normalize_feed(data)
if not items:
print_info("No feed items")
return
table = Table(title="推荐页", show_lines=True)
table.add_column("#", style="dim", width=3)
table.add_column("标题", width=30)
table.add_column("作者", width=10)
table.add_column("❤️", justify="right", width=8)
table.add_column("链接", style="cyan", no_wrap=True)
for i, item in enumerate(items, 1):
link = _build_note_link(item["note_id"], item.get("xsec_token", ""), source="pc_feed")
table.add_row(str(i), item["title"], item["author"], item["liked"], link)
console.print(table)
def render_user_posts(notes: list[dict[str, Any]]) -> None:
"""Render a list of user's notes."""
normalized = normalize_user_posts(notes)
if not normalized:
print_info("No notes found")
return
table = Table(title="用户笔记", show_lines=True)
table.add_column("#", style="dim", width=3)
table.add_column("标题", width=30)
table.add_column("❤️", justify="right", width=8)
table.add_column("类型", width=4)
table.add_column("ID", style="dim", width=24)
for i, note in enumerate(normalized, 1):
note_type = "📹" if note["note_type"] == "video" else "📷"
table.add_row(str(i), note["title"], note["liked"], note_type, note["note_id"])
console.print(table)
def render_topics(data: Any) -> None:
"""Render topic search results."""
topics = normalize_topics(data)
if not topics:
print_info("No topics found")
return
table = Table(title="话题", show_lines=True)
table.add_column("#", style="dim", width=3)
table.add_column("话题名", width=15)
table.add_column("热度", justify="right", width=10)
table.add_column("ID", style="dim", width=24)
for i, topic in enumerate(topics, 1):
table.add_row(str(i), f"#{topic['name']}", format_count(topic["view_num"]), topic["topic_id"])
console.print(table)
def render_users(data: Any) -> None:
"""Render user search/list results."""
users = normalize_users(data)
if not users:
print_info("No users found")
return
table = Table(title="用户列表", show_lines=True)
table.add_column("#", style="dim", width=3)
table.add_column("昵称", width=14)
table.add_column("小红书号", width=12)
table.add_column("粉丝", justify="right", width=8)
table.add_column("ID", style="dim", width=24)
for i, user in enumerate(users, 1):
table.add_row(str(i), user["nickname"], user["red_id"], format_count(user["fans"]), user["user_id"])
console.print(table)
def render_creator_notes(data: Any) -> None:
"""Render creator's own note list."""
notes = normalize_creator_notes(data)
if not notes:
print_info("No notes found")
return
table = Table(title="我的笔记", show_lines=True)
table.add_column("#", style="dim", width=3)
table.add_column("标题", width=30)
table.add_column("❤️", justify="right", width=8)
table.add_column("💬", justify="right", width=6)
table.add_column("状态", width=6)
table.add_column("ID", style="dim", width=24)
for i, note in enumerate(notes, 1):
status = "✅" if note["status"] in (None, 0, "published") else "⏳"
table.add_row(str(i), note["title"], note["liked"], note["comment_count"], status, note["note_id"])
console.print(table)
def render_notifications(data: dict[str, Any], notif_type: str) -> None:
"""Render notification messages."""
import time as _time
messages = normalize_notifications(data)
if not messages:
print_info("No notifications")
return
table = Table(title=f"通知 — {notif_type}", show_lines=True)
table.add_column("#", style="dim", width=3)
table.add_column("用户", width=12)
table.add_column("内容", width=40)
table.add_column("时间", width=12)
for i, msg in enumerate(messages[:20], 1):
nickname = msg["nickname"]
title = msg["title"]
note_content = msg["note_content"]
display = title + (f"\n{note_content[:30]}" if note_content else "")
ts = msg["time"]
time_str = _time.strftime("%m-%d %H:%M", _time.localtime(ts)) if ts else ""
table.add_row(str(i), nickname, display, time_str)
console.print(table)