rdt-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdt_cli/__init__.py +3 -0
- rdt_cli/__main__.py +5 -0
- rdt_cli/auth.py +174 -0
- rdt_cli/cli.py +72 -0
- rdt_cli/client.py +356 -0
- rdt_cli/commands/__init__.py +0 -0
- rdt_cli/commands/_common.py +353 -0
- rdt_cli/commands/auth.py +105 -0
- rdt_cli/commands/browse.py +386 -0
- rdt_cli/commands/post.py +183 -0
- rdt_cli/commands/search.py +227 -0
- rdt_cli/commands/social.py +163 -0
- rdt_cli/constants.py +83 -0
- rdt_cli/exceptions.py +69 -0
- rdt_cli/index_cache.py +77 -0
- rdt_cli-0.2.0.dist-info/METADATA +398 -0
- rdt_cli-0.2.0.dist-info/RECORD +19 -0
- rdt_cli-0.2.0.dist-info/WHEEL +4 -0
- rdt_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Browse commands: feed, subreddit, popular, all, user, open."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ..client import RedditClient
|
|
12
|
+
from ..constants import SORT_OPTIONS, TIME_FILTERS
|
|
13
|
+
from ..index_cache import save_index
|
|
14
|
+
from ._common import (
|
|
15
|
+
compact_posts,
|
|
16
|
+
console,
|
|
17
|
+
format_score,
|
|
18
|
+
format_time,
|
|
19
|
+
handle_command,
|
|
20
|
+
listing_options,
|
|
21
|
+
maybe_print_structured,
|
|
22
|
+
open_url,
|
|
23
|
+
optional_auth,
|
|
24
|
+
require_auth,
|
|
25
|
+
save_output_to_file,
|
|
26
|
+
structured_output_options,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Default title truncation length
|
|
32
|
+
_TITLE_MAX = 50
|
|
33
|
+
_FULL_TITLE_MAX = 200
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── Helpers ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _render_post_table(
|
|
40
|
+
posts: list[dict], title: str,
|
|
41
|
+
show_subreddit: bool = True, full_text: bool = False,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Render a list of posts as a Rich table."""
|
|
44
|
+
if not posts:
|
|
45
|
+
console.print("[yellow]No posts found[/yellow]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
save_index(posts, source=title[:40])
|
|
49
|
+
max_title = _FULL_TITLE_MAX if full_text else _TITLE_MAX
|
|
50
|
+
|
|
51
|
+
table = Table(title=title, show_lines=True)
|
|
52
|
+
table.add_column("#", style="dim", width=3)
|
|
53
|
+
table.add_column("Score", style="yellow", width=6, justify="right")
|
|
54
|
+
if show_subreddit:
|
|
55
|
+
table.add_column("Subreddit", style="magenta", max_width=15)
|
|
56
|
+
table.add_column(
|
|
57
|
+
"Title", style="bold cyan",
|
|
58
|
+
max_width=max_title if not full_text else None,
|
|
59
|
+
)
|
|
60
|
+
table.add_column("Author", style="green", max_width=14)
|
|
61
|
+
table.add_column("💬", style="dim", width=5, justify="right")
|
|
62
|
+
table.add_column("Time", style="dim", max_width=10)
|
|
63
|
+
|
|
64
|
+
for i, post in enumerate(posts, 1):
|
|
65
|
+
title_text = post.get("title", "-")
|
|
66
|
+
if post.get("stickied"):
|
|
67
|
+
title_text = f"📌 {title_text}"
|
|
68
|
+
if post.get("over_18"):
|
|
69
|
+
title_text = f"🔞 {title_text}"
|
|
70
|
+
if post.get("is_video"):
|
|
71
|
+
title_text = f"🎬 {title_text}"
|
|
72
|
+
|
|
73
|
+
if not full_text:
|
|
74
|
+
title_text = title_text[:max_title]
|
|
75
|
+
|
|
76
|
+
row = [
|
|
77
|
+
str(i),
|
|
78
|
+
format_score(post.get("score", 0)),
|
|
79
|
+
]
|
|
80
|
+
if show_subreddit:
|
|
81
|
+
row.append(f"r/{post.get('subreddit', '?')}")
|
|
82
|
+
row.extend([
|
|
83
|
+
title_text,
|
|
84
|
+
post.get("author", "-")[:14],
|
|
85
|
+
str(post.get("num_comments", 0)),
|
|
86
|
+
format_time(post.get("created_utc", 0)),
|
|
87
|
+
])
|
|
88
|
+
table.add_row(*row)
|
|
89
|
+
|
|
90
|
+
console.print(table)
|
|
91
|
+
console.print("\n [dim]💡 Use [bold]rdt show <#>[/bold] to read a post[/dim]")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _listing_render(
|
|
95
|
+
data: dict, title: str,
|
|
96
|
+
show_subreddit: bool = True, next_cmd: str = "",
|
|
97
|
+
full_text: bool = False,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Common render for listing endpoints."""
|
|
100
|
+
posts = RedditClient._extract_posts(data)
|
|
101
|
+
_render_post_table(posts, title, show_subreddit=show_subreddit, full_text=full_text)
|
|
102
|
+
cursor = RedditClient._extract_after(data)
|
|
103
|
+
if cursor and next_cmd:
|
|
104
|
+
console.print(f" [dim]▸ More: {next_cmd} --after {cursor}[/dim]")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _handle_listing(
|
|
108
|
+
cred, *, action, data_title: str, next_cmd: str = "",
|
|
109
|
+
show_subreddit: bool = True,
|
|
110
|
+
as_json: bool, as_yaml: bool,
|
|
111
|
+
output_file: str | None = None,
|
|
112
|
+
full_text: bool = False,
|
|
113
|
+
compact: bool = False,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Unified listing handler with --output/--full-text/--compact support."""
|
|
116
|
+
from ..exceptions import RedditApiError
|
|
117
|
+
from ._common import exit_for_error, run_client_action
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
data = run_client_action(cred, action)
|
|
121
|
+
|
|
122
|
+
# --output: save to file
|
|
123
|
+
if output_file:
|
|
124
|
+
out_data = data
|
|
125
|
+
if compact:
|
|
126
|
+
posts = RedditClient._extract_posts(data)
|
|
127
|
+
out_data = compact_posts(posts)
|
|
128
|
+
save_output_to_file(out_data, output_file)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# --compact: strip fields for structured output
|
|
132
|
+
if compact and (as_json or as_yaml):
|
|
133
|
+
posts = RedditClient._extract_posts(data)
|
|
134
|
+
data = compact_posts(posts)
|
|
135
|
+
|
|
136
|
+
# --json/--yaml: structured output
|
|
137
|
+
if maybe_print_structured(data, as_json=as_json, as_yaml=as_yaml):
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Rich render
|
|
141
|
+
_listing_render(
|
|
142
|
+
data, data_title,
|
|
143
|
+
show_subreddit=show_subreddit,
|
|
144
|
+
next_cmd=next_cmd,
|
|
145
|
+
full_text=full_text,
|
|
146
|
+
)
|
|
147
|
+
except RedditApiError as exc:
|
|
148
|
+
exit_for_error(exc, as_json=as_json, as_yaml=as_yaml)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── feed ────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@click.command()
|
|
155
|
+
@click.option("-n", "--limit", default=25, type=int, help="Number of posts (default: 25)")
|
|
156
|
+
@click.option("--after", default=None, help="Pagination cursor")
|
|
157
|
+
@listing_options
|
|
158
|
+
def feed(
|
|
159
|
+
limit: int, after: str | None,
|
|
160
|
+
as_json: bool, as_yaml: bool,
|
|
161
|
+
output_file: str | None, full_text: bool, compact: bool,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Browse your home feed (requires login)"""
|
|
164
|
+
cred = require_auth()
|
|
165
|
+
_handle_listing(
|
|
166
|
+
cred,
|
|
167
|
+
action=lambda c: c.get_home(limit=limit, after=after),
|
|
168
|
+
data_title="🏠 Home Feed",
|
|
169
|
+
next_cmd="rdt feed",
|
|
170
|
+
as_json=as_json, as_yaml=as_yaml,
|
|
171
|
+
output_file=output_file, full_text=full_text, compact=compact,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ── popular ─────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@click.command()
|
|
179
|
+
@click.option("-n", "--limit", default=25, type=int, help="Number of posts")
|
|
180
|
+
@click.option("--after", default=None, help="Pagination cursor")
|
|
181
|
+
@listing_options
|
|
182
|
+
def popular(
|
|
183
|
+
limit: int, after: str | None,
|
|
184
|
+
as_json: bool, as_yaml: bool,
|
|
185
|
+
output_file: str | None, full_text: bool, compact: bool,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Browse /r/popular"""
|
|
188
|
+
cred = optional_auth()
|
|
189
|
+
_handle_listing(
|
|
190
|
+
cred,
|
|
191
|
+
action=lambda c: c.get_popular(limit=limit, after=after),
|
|
192
|
+
data_title="🔥 Popular",
|
|
193
|
+
next_cmd="rdt popular",
|
|
194
|
+
as_json=as_json, as_yaml=as_yaml,
|
|
195
|
+
output_file=output_file, full_text=full_text, compact=compact,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── all ─────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@click.command(name="all")
|
|
203
|
+
@click.option("-n", "--limit", default=25, type=int, help="Number of posts")
|
|
204
|
+
@click.option("--after", default=None, help="Pagination cursor")
|
|
205
|
+
@listing_options
|
|
206
|
+
def all_cmd(
|
|
207
|
+
limit: int, after: str | None,
|
|
208
|
+
as_json: bool, as_yaml: bool,
|
|
209
|
+
output_file: str | None, full_text: bool, compact: bool,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Browse /r/all"""
|
|
212
|
+
cred = optional_auth()
|
|
213
|
+
_handle_listing(
|
|
214
|
+
cred,
|
|
215
|
+
action=lambda c: c.get_all(limit=limit, after=after),
|
|
216
|
+
data_title="🌍 r/all",
|
|
217
|
+
next_cmd="rdt all",
|
|
218
|
+
as_json=as_json, as_yaml=as_yaml,
|
|
219
|
+
output_file=output_file, full_text=full_text, compact=compact,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ── sub (subreddit) ─────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@click.command()
|
|
227
|
+
@click.argument("subreddit")
|
|
228
|
+
@click.option("-s", "--sort", type=click.Choice(SORT_OPTIONS), default="hot", help="Sort order")
|
|
229
|
+
@click.option(
|
|
230
|
+
"-t", "--time", "time_filter",
|
|
231
|
+
type=click.Choice(TIME_FILTERS), default=None,
|
|
232
|
+
help="Time filter (for top/controversial)",
|
|
233
|
+
)
|
|
234
|
+
@click.option("-n", "--limit", default=25, type=int, help="Number of posts")
|
|
235
|
+
@click.option("--after", default=None, help="Pagination cursor")
|
|
236
|
+
@listing_options
|
|
237
|
+
def sub(
|
|
238
|
+
subreddit: str, sort: str, time_filter: str | None, limit: int,
|
|
239
|
+
after: str | None, as_json: bool, as_yaml: bool,
|
|
240
|
+
output_file: str | None, full_text: bool, compact: bool,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Browse a subreddit (e.g., rdt sub python)"""
|
|
243
|
+
cred = optional_auth()
|
|
244
|
+
emoji = {"hot": "🔥", "new": "🆕", "top": "🏆", "rising": "📈"}.get(sort, "📋")
|
|
245
|
+
_handle_listing(
|
|
246
|
+
cred,
|
|
247
|
+
action=lambda c: c.get_subreddit(
|
|
248
|
+
subreddit, sort=sort, limit=limit, after=after, time_filter=time_filter,
|
|
249
|
+
),
|
|
250
|
+
data_title=f"{emoji} r/{subreddit} ({sort})",
|
|
251
|
+
show_subreddit=False,
|
|
252
|
+
next_cmd=f"rdt sub {subreddit} -s {sort}",
|
|
253
|
+
as_json=as_json, as_yaml=as_yaml,
|
|
254
|
+
output_file=output_file, full_text=full_text, compact=compact,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── sub-info ────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@click.command("sub-info")
|
|
262
|
+
@click.argument("subreddit")
|
|
263
|
+
@structured_output_options
|
|
264
|
+
def sub_info(subreddit: str, as_json: bool, as_yaml: bool) -> None:
|
|
265
|
+
"""View subreddit info (subscribers, description)"""
|
|
266
|
+
cred = optional_auth()
|
|
267
|
+
|
|
268
|
+
def _render(data: dict) -> None:
|
|
269
|
+
name = data.get("display_name_prefixed", f"r/{subreddit}")
|
|
270
|
+
desc = data.get("public_description", data.get("description", ""))
|
|
271
|
+
subs = data.get("subscribers", 0)
|
|
272
|
+
active = data.get("accounts_active", 0)
|
|
273
|
+
created = data.get("created_utc", 0)
|
|
274
|
+
nsfw = "🔞 NSFW" if data.get("over18") else ""
|
|
275
|
+
|
|
276
|
+
text = (
|
|
277
|
+
f"[bold cyan]{name}[/bold cyan] {nsfw}\n"
|
|
278
|
+
f"👥 {subs:,} subscribers · 🟢 {active:,} online\n"
|
|
279
|
+
f"📅 Created: {format_time(created)}\n"
|
|
280
|
+
)
|
|
281
|
+
if desc:
|
|
282
|
+
text += f"\n{desc[:300]}"
|
|
283
|
+
|
|
284
|
+
panel = Panel(text, title=f"📋 {name}", border_style="cyan")
|
|
285
|
+
console.print(panel)
|
|
286
|
+
|
|
287
|
+
handle_command(
|
|
288
|
+
cred,
|
|
289
|
+
action=lambda c: c.get_subreddit_about(subreddit),
|
|
290
|
+
render=_render, as_json=as_json, as_yaml=as_yaml,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ── user ────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@click.command()
|
|
298
|
+
@click.argument("username")
|
|
299
|
+
@structured_output_options
|
|
300
|
+
def user(username: str, as_json: bool, as_yaml: bool) -> None:
|
|
301
|
+
"""View a user's profile"""
|
|
302
|
+
cred = optional_auth()
|
|
303
|
+
|
|
304
|
+
def _render(data: dict) -> None:
|
|
305
|
+
name = data.get("name", username)
|
|
306
|
+
karma_post = data.get("link_karma", 0)
|
|
307
|
+
karma_comment = data.get("comment_karma", 0)
|
|
308
|
+
created = data.get("created_utc", 0)
|
|
309
|
+
is_gold = "⭐ " if data.get("is_gold") else ""
|
|
310
|
+
|
|
311
|
+
text = (
|
|
312
|
+
f"[bold cyan]u/{name}[/bold cyan] {is_gold}\n"
|
|
313
|
+
f"📊 Post karma: {karma_post:,} · Comment karma: {karma_comment:,}\n"
|
|
314
|
+
f"📅 Account age: {format_time(created)}\n"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
panel = Panel(text, title=f"👤 u/{name}", border_style="green")
|
|
318
|
+
console.print(panel)
|
|
319
|
+
|
|
320
|
+
handle_command(
|
|
321
|
+
cred, action=lambda c: c.get_user_about(username),
|
|
322
|
+
render=_render, as_json=as_json, as_yaml=as_yaml,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ── user-posts ──────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@click.command("user-posts")
|
|
330
|
+
@click.argument("username")
|
|
331
|
+
@click.option("-n", "--limit", default=25, type=int, help="Number of posts")
|
|
332
|
+
@click.option("--after", default=None, help="Pagination cursor")
|
|
333
|
+
@listing_options
|
|
334
|
+
def user_posts(
|
|
335
|
+
username: str, limit: int, after: str | None,
|
|
336
|
+
as_json: bool, as_yaml: bool,
|
|
337
|
+
output_file: str | None, full_text: bool, compact: bool,
|
|
338
|
+
) -> None:
|
|
339
|
+
"""View a user's submitted posts"""
|
|
340
|
+
cred = optional_auth()
|
|
341
|
+
_handle_listing(
|
|
342
|
+
cred,
|
|
343
|
+
action=lambda c: c.get_user_posts(username, limit=limit, after=after),
|
|
344
|
+
data_title=f"📝 u/{username}'s posts",
|
|
345
|
+
as_json=as_json, as_yaml=as_yaml,
|
|
346
|
+
output_file=output_file, full_text=full_text, compact=compact,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ── open ────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@click.command(name="open")
|
|
354
|
+
@click.argument("id_or_index")
|
|
355
|
+
def open_post(id_or_index: str) -> None:
|
|
356
|
+
"""Open a post in the browser (by ID or index number)
|
|
357
|
+
|
|
358
|
+
Examples:
|
|
359
|
+
rdt open 3 # open result #3 in browser
|
|
360
|
+
rdt open 1abc123 # open by post ID
|
|
361
|
+
"""
|
|
362
|
+
from ..index_cache import get_item_by_index
|
|
363
|
+
|
|
364
|
+
# Try as short-index
|
|
365
|
+
try:
|
|
366
|
+
idx = int(id_or_index)
|
|
367
|
+
item = get_item_by_index(idx)
|
|
368
|
+
if item:
|
|
369
|
+
permalink = item.get("permalink", "")
|
|
370
|
+
if permalink:
|
|
371
|
+
url = f"https://reddit.com{permalink}"
|
|
372
|
+
console.print(f"[dim]Opening: {url}[/dim]")
|
|
373
|
+
open_url(url)
|
|
374
|
+
return
|
|
375
|
+
console.print(f"[yellow]Index {idx} not found in cache[/yellow]")
|
|
376
|
+
return
|
|
377
|
+
except ValueError:
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
# Bare ID or URL
|
|
381
|
+
if id_or_index.startswith("http"):
|
|
382
|
+
open_url(id_or_index)
|
|
383
|
+
else:
|
|
384
|
+
url = f"https://reddit.com/comments/{id_or_index}"
|
|
385
|
+
console.print(f"[dim]Opening: {url}[/dim]")
|
|
386
|
+
open_url(url)
|
rdt_cli/commands/post.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Post commands: read, show, comments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from ..index_cache import get_index_info, get_item_by_index
|
|
11
|
+
from ._common import (
|
|
12
|
+
console,
|
|
13
|
+
handle_command,
|
|
14
|
+
optional_auth,
|
|
15
|
+
structured_output_options,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Helpers ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _render_post_detail(data: list | dict) -> None:
|
|
25
|
+
"""Render a post with its comments."""
|
|
26
|
+
if isinstance(data, list) and len(data) >= 1:
|
|
27
|
+
# [post_listing, comments_listing]
|
|
28
|
+
post_listing = data[0]
|
|
29
|
+
post_children = post_listing.get("data", {}).get("children", [])
|
|
30
|
+
post = post_children[0].get("data", {}) if post_children else {}
|
|
31
|
+
|
|
32
|
+
comments_listing = data[1] if len(data) > 1 else {}
|
|
33
|
+
comment_children = comments_listing.get("data", {}).get("children", [])
|
|
34
|
+
else:
|
|
35
|
+
post = data if isinstance(data, dict) else {}
|
|
36
|
+
comment_children = []
|
|
37
|
+
|
|
38
|
+
# Render post
|
|
39
|
+
title = post.get("title", "Untitled")
|
|
40
|
+
author = post.get("author", "?")
|
|
41
|
+
subreddit = post.get("subreddit", "?")
|
|
42
|
+
score = post.get("score", 0)
|
|
43
|
+
num_comments = post.get("num_comments", 0)
|
|
44
|
+
selftext = post.get("selftext", "")
|
|
45
|
+
url = post.get("url", "")
|
|
46
|
+
is_self = post.get("is_self", True)
|
|
47
|
+
permalink = post.get("permalink", "")
|
|
48
|
+
|
|
49
|
+
post_text = (
|
|
50
|
+
f"[bold cyan]{title}[/bold cyan]\n"
|
|
51
|
+
f"[dim]r/{subreddit}[/dim] · [green]u/{author}[/green] · "
|
|
52
|
+
f"[yellow]⬆ {score}[/yellow] · 💬 {num_comments}\n"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not is_self and url:
|
|
56
|
+
post_text += f"\n🔗 {url}\n"
|
|
57
|
+
|
|
58
|
+
if selftext:
|
|
59
|
+
# Truncate very long posts
|
|
60
|
+
if len(selftext) > 1500:
|
|
61
|
+
selftext = selftext[:1500] + "\n\n... [truncated]"
|
|
62
|
+
post_text += f"\n{selftext}"
|
|
63
|
+
|
|
64
|
+
if permalink:
|
|
65
|
+
post_text += f"\n\n[dim]https://reddit.com{permalink}[/dim]"
|
|
66
|
+
|
|
67
|
+
panel = Panel(post_text, title="📰 Post", border_style="cyan")
|
|
68
|
+
console.print(panel)
|
|
69
|
+
|
|
70
|
+
# Render comments
|
|
71
|
+
if comment_children:
|
|
72
|
+
console.print()
|
|
73
|
+
_render_comments(comment_children, depth=0, max_depth=3)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _render_comments(children: list[dict], depth: int = 0, max_depth: int = 3) -> None:
|
|
77
|
+
"""Recursively render comment tree."""
|
|
78
|
+
for child in children:
|
|
79
|
+
if child.get("kind") != "t1":
|
|
80
|
+
continue
|
|
81
|
+
comment = child.get("data", {})
|
|
82
|
+
author = comment.get("author", "[deleted]")
|
|
83
|
+
body = comment.get("body", "")
|
|
84
|
+
score = comment.get("score", 0)
|
|
85
|
+
|
|
86
|
+
indent = " " * depth
|
|
87
|
+
score_color = "yellow" if score > 0 else "red" if score < 0 else "dim"
|
|
88
|
+
|
|
89
|
+
# Truncate long comments
|
|
90
|
+
if len(body) > 300:
|
|
91
|
+
body = body[:300] + "..."
|
|
92
|
+
|
|
93
|
+
console.print(
|
|
94
|
+
f"{indent}[green]u/{author}[/green] [{score_color}]⬆ {score}[/{score_color}]"
|
|
95
|
+
)
|
|
96
|
+
for line in body.split("\n"):
|
|
97
|
+
console.print(f"{indent} {line}")
|
|
98
|
+
console.print()
|
|
99
|
+
|
|
100
|
+
# Render replies
|
|
101
|
+
if depth < max_depth:
|
|
102
|
+
replies = comment.get("replies", "")
|
|
103
|
+
if isinstance(replies, dict):
|
|
104
|
+
reply_children = replies.get("data", {}).get("children", [])
|
|
105
|
+
if reply_children:
|
|
106
|
+
_render_comments(reply_children, depth=depth + 1, max_depth=max_depth)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── read ────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@click.command()
|
|
113
|
+
@click.argument("post_id")
|
|
114
|
+
@click.option(
|
|
115
|
+
"-s", "--sort", default="best",
|
|
116
|
+
type=click.Choice(["best", "top", "new", "controversial", "old", "qa"]),
|
|
117
|
+
help="Comment sort",
|
|
118
|
+
)
|
|
119
|
+
@click.option("-n", "--limit", default=25, type=int, help="Number of comments")
|
|
120
|
+
@structured_output_options
|
|
121
|
+
def read(post_id: str, sort: str, limit: int, as_json: bool, as_yaml: bool) -> None:
|
|
122
|
+
"""Read a post and its comments by ID
|
|
123
|
+
|
|
124
|
+
Example: rdt read 1abc123
|
|
125
|
+
"""
|
|
126
|
+
cred = optional_auth()
|
|
127
|
+
handle_command(
|
|
128
|
+
cred,
|
|
129
|
+
action=lambda c: c.get_post_comments(post_id=post_id, sort=sort, limit=limit),
|
|
130
|
+
render=_render_post_detail,
|
|
131
|
+
as_json=as_json,
|
|
132
|
+
as_yaml=as_yaml,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── show (short-index) ──────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@click.command()
|
|
140
|
+
@click.argument("index", type=int)
|
|
141
|
+
@click.option(
|
|
142
|
+
"-s", "--sort", default="best",
|
|
143
|
+
type=click.Choice(["best", "top", "new", "controversial", "old", "qa"]),
|
|
144
|
+
help="Comment sort",
|
|
145
|
+
)
|
|
146
|
+
@click.option("-n", "--limit", default=25, type=int, help="Number of comments")
|
|
147
|
+
@structured_output_options
|
|
148
|
+
def show(index: int, sort: str, limit: int, as_json: bool, as_yaml: bool) -> None:
|
|
149
|
+
"""Read a post by its index from last listing (e.g., rdt show 3)
|
|
150
|
+
|
|
151
|
+
Use after rdt feed, rdt sub, rdt search, etc.
|
|
152
|
+
"""
|
|
153
|
+
item = get_item_by_index(index)
|
|
154
|
+
if not item:
|
|
155
|
+
info = get_index_info()
|
|
156
|
+
if not info.get("exists"):
|
|
157
|
+
console.print("[yellow]No cached results. Run rdt feed, rdt sub, or rdt search first.[/yellow]")
|
|
158
|
+
else:
|
|
159
|
+
console.print(f"[yellow]Index {index} out of range (total: {info.get('count', 0)})[/yellow]")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
post_id = item.get("id", "")
|
|
163
|
+
if not post_id:
|
|
164
|
+
console.print("[red]❌ Cached item has no post ID[/red]")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Show brief info from cache
|
|
168
|
+
console.print(
|
|
169
|
+
f" [dim]#{index}[/dim] [cyan]{item.get('title', '-')[:60]}[/cyan] "
|
|
170
|
+
f"[dim]r/{item.get('subreddit', '?')}[/dim] "
|
|
171
|
+
f"[yellow]⬆ {item.get('score', 0)}[/yellow]"
|
|
172
|
+
)
|
|
173
|
+
console.print()
|
|
174
|
+
|
|
175
|
+
# Fetch full post + comments
|
|
176
|
+
cred = optional_auth()
|
|
177
|
+
handle_command(
|
|
178
|
+
cred,
|
|
179
|
+
action=lambda c: c.get_post_comments(post_id=post_id, sort=sort, limit=limit),
|
|
180
|
+
render=_render_post_detail,
|
|
181
|
+
as_json=as_json,
|
|
182
|
+
as_yaml=as_yaml,
|
|
183
|
+
)
|