"""Tweet formatter for terminal output (rich) and JSON export.""" from __future__ import annotations import json from typing import List, Optional from rich.console import Console from rich.panel import Panel from rich.table import Table from rich.text import Text from .models import Tweet, UserProfile def format_number(n): # type: (int) -> str """Format number with K/M suffixes.""" if n >= 1_000_000: return "%.1fM" % (n / 1_000_000) if n >= 1_000: return "%.1fK" % (n / 1_000) return str(n) def print_tweet_table(tweets, console=None, title=None): # type: (List[Tweet], Optional[Console], Optional[str]) -> None """Print tweets as a rich table.""" if console is None: console = Console() if not title: title = "๐Ÿ“ฑ Twitter โ€” %d tweets" % len(tweets) table = Table(title=title, show_lines=True, expand=True) table.add_column("#", style="dim", width=3, justify="right") table.add_column("Author", style="cyan", width=18, no_wrap=True) table.add_column("Tweet", ratio=3) table.add_column("Stats", style="green", width=22, no_wrap=True) table.add_column("Score", style="yellow", width=6, justify="right") for i, tweet in enumerate(tweets): # Author verified = " โœ“" if tweet.author.verified else "" author_text = "@%s%s" % (tweet.author.screen_name, verified) if tweet.is_retweet and tweet.retweeted_by: author_text += "\n๐Ÿ”„ @%s" % tweet.retweeted_by # Tweet text (truncated) text = tweet.text.replace("\n", " ").strip() if len(text) > 120: text = text[:117] + "..." # Media indicators if tweet.media: media_icons = [] for m in tweet.media: if m.type == "photo": media_icons.append("๐Ÿ“ท") elif m.type == "video": media_icons.append("๐Ÿ“น") else: media_icons.append("๐ŸŽž๏ธ") text += " " + " ".join(media_icons) # Quoted tweet if tweet.quoted_tweet: qt = tweet.quoted_tweet qt_text = qt.text.replace("\n", " ")[:60] text += "\nโ”Œ @%s: %s" % (qt.author.screen_name, qt_text) # Tweet link text += "\n๐Ÿ”— x.com/%s/status/%s" % (tweet.author.screen_name, tweet.id) # Stats stats = ( "โค๏ธ %s ๐Ÿ”„ %s\n๐Ÿ’ฌ %s ๐Ÿ‘๏ธ %s" % ( format_number(tweet.metrics.likes), format_number(tweet.metrics.retweets), format_number(tweet.metrics.replies), format_number(tweet.metrics.views), ) ) # Score score_str = "%.1f" % tweet.score if tweet.score else "-" table.add_row(str(i + 1), author_text, text, stats, score_str) console.print(table) def print_tweet_detail(tweet, console=None): # type: (Tweet, Optional[Console]) -> None """Print a single tweet in detail using a rich panel.""" if console is None: console = Console() verified = " โœ“" if tweet.author.verified else "" header = "@%s%s (%s)" % (tweet.author.screen_name, verified, tweet.author.name) body_parts = [] if tweet.is_retweet and tweet.retweeted_by: body_parts.append("๐Ÿ”„ Retweeted by @%s\n" % tweet.retweeted_by) body_parts.append(tweet.text) if tweet.media: body_parts.append("") for m in tweet.media: icon = "๐Ÿ“ท" if m.type == "photo" else ("๐Ÿ“น" if m.type == "video" else "๐ŸŽž๏ธ") body_parts.append("%s %s: %s" % (icon, m.type, m.url)) if tweet.urls: body_parts.append("") for url in tweet.urls: body_parts.append("๐Ÿ”— %s" % url) if tweet.quoted_tweet: qt = tweet.quoted_tweet body_parts.append("") body_parts.append("โ”Œโ”€โ”€ Quoted @%s โ”€โ”€" % qt.author.screen_name) body_parts.append(qt.text[:200]) body_parts.append("") body_parts.append( "โค๏ธ %s ๐Ÿ”„ %s ๐Ÿ’ฌ %s ๐Ÿ”– %s ๐Ÿ‘๏ธ %s" % ( format_number(tweet.metrics.likes), format_number(tweet.metrics.retweets), format_number(tweet.metrics.replies), format_number(tweet.metrics.bookmarks), format_number(tweet.metrics.views), ) ) body_parts.append( "๐Ÿ• %s ยท https://x.com/%s/status/%s" % (tweet.created_at, tweet.author.screen_name, tweet.id) ) console.print(Panel( "\n".join(body_parts), title=header, border_style="blue", expand=True, )) def print_filter_stats(original_count, filtered, console=None): # type: (int, List[Tweet], Optional[Console]) -> None """Print filter statistics.""" if console is None: console = Console() console.print( "๐Ÿ“Š Filter: %d โ†’ %d tweets" % (original_count, len(filtered)) ) if filtered: top_score = filtered[0].score bottom_score = filtered[-1].score console.print( " Score range: %.1f ~ %.1f" % (bottom_score, top_score) ) def tweets_to_json(tweets): # type: (List[Tweet]) -> str """Export tweets as JSON string.""" result = [] for t in tweets: d = { "id": t.id, "text": t.text, "author": { "id": t.author.id, "name": t.author.name, "screenName": t.author.screen_name, "profileImageUrl": t.author.profile_image_url, "verified": t.author.verified, }, "metrics": { "likes": t.metrics.likes, "retweets": t.metrics.retweets, "replies": t.metrics.replies, "quotes": t.metrics.quotes, "views": t.metrics.views, "bookmarks": t.metrics.bookmarks, }, "createdAt": t.created_at, "media": [ {"type": m.type, "url": m.url, "width": m.width, "height": m.height} for m in t.media ], "urls": t.urls, "isRetweet": t.is_retweet, "retweetedBy": t.retweeted_by, "lang": t.lang, "score": t.score, } if t.quoted_tweet: qt = t.quoted_tweet d["quotedTweet"] = { "id": qt.id, "text": qt.text, "author": {"screenName": qt.author.screen_name, "name": qt.author.name}, } result.append(d) return json.dumps(result, ensure_ascii=False, indent=2) def print_user_profile(user, console=None): # type: (UserProfile, Optional[Console]) -> None """Print user profile as a rich panel.""" if console is None: console = Console() verified = " โœ“" if user.verified else "" header = "@%s%s (%s)" % (user.screen_name, verified, user.name) lines = [] if user.bio: lines.append(user.bio) lines.append("") if user.location: lines.append("๐Ÿ“ %s" % user.location) if user.url: lines.append("๐Ÿ”— %s" % user.url) if user.location or user.url: lines.append("") lines.append( "๐Ÿ‘ฅ %s followers ยท %s following ยท %s tweets ยท %s likes" % ( format_number(user.followers_count), format_number(user.following_count), format_number(user.tweets_count), format_number(user.likes_count), ) ) if user.created_at: lines.append("๐Ÿ“… Joined %s" % user.created_at) lines.append("๐Ÿ”— x.com/%s" % user.screen_name) console.print(Panel( "\n".join(lines), title=header, border_style="cyan", expand=True, )) def print_user_table(users, console=None, title=None): # type: (List[UserProfile], Optional[Console], Optional[str]) -> None """Print a list of users as a rich table.""" if console is None: console = Console() if not title: title = "๐Ÿ‘ฅ Users โ€” %d" % len(users) table = Table(title=title, show_lines=True, expand=True) table.add_column("#", style="dim", width=3, justify="right") table.add_column("User", style="cyan", width=20, no_wrap=True) table.add_column("Bio", ratio=3) table.add_column("Stats", style="green", width=22, no_wrap=True) for i, user in enumerate(users): verified = " โœ“" if user.verified else "" user_text = "@%s%s\n%s" % (user.screen_name, verified, user.name) bio = (user.bio or "").replace("\n", " ").strip() if len(bio) > 100: bio = bio[:97] + "..." stats = ( "๐Ÿ‘ฅ %s followers\n๐Ÿ“ %s following" % ( format_number(user.followers_count), format_number(user.following_count), ) ) table.add_row(str(i + 1), user_text, bio, stats) console.print(table)