"""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)