feedscope 0.1.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.
Files changed (72) hide show
  1. feedscope/__init__.py +3 -0
  2. feedscope/__main__.py +4 -0
  3. feedscope/cli.py +154 -0
  4. feedscope/core/__init__.py +1 -0
  5. feedscope/core/browser.py +367 -0
  6. feedscope/core/config.py +367 -0
  7. feedscope/core/const.py +64 -0
  8. feedscope/core/display.py +62 -0
  9. feedscope/core/http.py +104 -0
  10. feedscope/core/i18n/__init__.py +202 -0
  11. feedscope/core/i18n/en.toml +447 -0
  12. feedscope/core/i18n/zh-CN.toml +444 -0
  13. feedscope/core/media.py +150 -0
  14. feedscope/core/models.py +86 -0
  15. feedscope/core/registry.py +53 -0
  16. feedscope/core/samples.py +68 -0
  17. feedscope/core/source.py +668 -0
  18. feedscope/core/text.py +54 -0
  19. feedscope/core/timefmt.py +53 -0
  20. feedscope/core/tree.py +134 -0
  21. feedscope/doctor.py +54 -0
  22. feedscope/login.py +168 -0
  23. feedscope/probe.py +131 -0
  24. feedscope/report.py +19 -0
  25. feedscope/sources/__init__.py +38 -0
  26. feedscope/sources/hn/__init__.py +66 -0
  27. feedscope/sources/hn/fetch.py +82 -0
  28. feedscope/sources/hn/parse.py +154 -0
  29. feedscope/sources/reddit/__init__.py +178 -0
  30. feedscope/sources/reddit/fetch.py +181 -0
  31. feedscope/sources/reddit/parse.py +318 -0
  32. feedscope/sources/rss/__init__.py +64 -0
  33. feedscope/sources/rss/fetch.py +94 -0
  34. feedscope/sources/rss/parse.py +116 -0
  35. feedscope/sources/telegram/__init__.py +61 -0
  36. feedscope/sources/telegram/fetch.py +79 -0
  37. feedscope/sources/telegram/parse.py +182 -0
  38. feedscope/sources/v2ex/__init__.py +64 -0
  39. feedscope/sources/v2ex/fetch.py +46 -0
  40. feedscope/sources/v2ex/parse.py +116 -0
  41. feedscope/sources/x/__init__.py +68 -0
  42. feedscope/sources/x/fetch.py +490 -0
  43. feedscope/sources/x/parse.py +535 -0
  44. feedscope/sources/zhihu/__init__.py +81 -0
  45. feedscope/sources/zhihu/fetch.py +402 -0
  46. feedscope/sources/zhihu/parse.py +235 -0
  47. feedscope/subscriptions.py +423 -0
  48. feedscope/ui/__init__.py +10 -0
  49. feedscope/ui/account.py +125 -0
  50. feedscope/ui/app.py +105 -0
  51. feedscope/ui/dialogs.py +127 -0
  52. feedscope/ui/feedback.py +64 -0
  53. feedscope/ui/focus_nav.py +113 -0
  54. feedscope/ui/labels.py +38 -0
  55. feedscope/ui/layout.py +66 -0
  56. feedscope/ui/list_render.py +42 -0
  57. feedscope/ui/screens/__init__.py +1 -0
  58. feedscope/ui/screens/answer.py +296 -0
  59. feedscope/ui/screens/detail.py +274 -0
  60. feedscope/ui/screens/language.py +59 -0
  61. feedscope/ui/screens/main.py +736 -0
  62. feedscope/ui/screens/main_setup.py +226 -0
  63. feedscope/ui/screens/reading.py +384 -0
  64. feedscope/ui/screens/source_setup.py +217 -0
  65. feedscope/ui/screens/subscriptions.py +196 -0
  66. feedscope/ui/theme.py +45 -0
  67. feedscope/ui/wrap.py +157 -0
  68. feedscope-0.1.0.dist-info/METADATA +302 -0
  69. feedscope-0.1.0.dist-info/RECORD +72 -0
  70. feedscope-0.1.0.dist-info/WHEEL +4 -0
  71. feedscope-0.1.0.dist-info/entry_points.txt +2 -0
  72. feedscope-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.5
2
+ Name: feedscope
3
+ Version: 0.1.0
4
+ Summary: FeedScope (观流): a terminal feed reader — Zhihu / X / Reddit / HN / V2EX / RSS / Telegram, all in one terminal
5
+ Project-URL: Homepage, https://github.com/howardli/FeedScope
6
+ Project-URL: Repository, https://github.com/howardli/FeedScope
7
+ Project-URL: Issues, https://github.com/howardli/FeedScope/issues
8
+ Project-URL: Changelog, https://github.com/howardli/FeedScope/blob/main/CHANGELOG.md
9
+ Author: howardli
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: cli,reader,rss,terminal,textual,tui,twitter,zhihu
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Terminals
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: feedparser>=6
24
+ Requires-Dist: html2text>=2024.2.26
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: platformdirs>=4.0
27
+ Requires-Dist: playwright>=1.45
28
+ Requires-Dist: rich>=13
29
+ Requires-Dist: textual>=1.0
30
+ Description-Content-Type: text/markdown
31
+
32
+ # FeedScope
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/feedscope.svg)](https://pypi.org/project/feedscope/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/feedscope.svg)](https://pypi.org/project/feedscope/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
37
+ [![CI](https://github.com/howardli/FeedScope/actions/workflows/ci.yml/badge.svg)](https://github.com/howardli/FeedScope/actions/workflows/ci.yml)
38
+
39
+ > Terminal-first feed reader — Zhihu, X (Twitter), Reddit, Hacker News, V2EX, RSS and Telegram channels, all in one terminal.
40
+
41
+ **Language**: English · [中文](README.zh-CN.md)
42
+
43
+ ## Demo
44
+
45
+ Zhihu Hot → X timeline → post detail (auto translation, quoted post) → Reddit → post detail → HN → story detail → reply tree → V2EX → topic detail — everything below is just `Tab` and `Enter`:
46
+
47
+ ![FeedScope demo](docs/demo.gif)
48
+
49
+ ## Features
50
+
51
+ - **Multi-source aggregation**: Zhihu Hot / Recommend / Following, X For You / Following, Reddit Home / Popular / custom subreddits, HN Front / New / Ask / Show, V2EX Hot / Latest, Telegram channel subscriptions — one UI, `Tab` to switch
52
+ - **RSS subscriptions**: any RSS / Atom feed; the "All" stream merges by publish time (cross-feed dedup, a dead feed never drags others down); read feed-provided full text in-terminal; conditional requests keep refreshes polite to sources
53
+ - **Full in-terminal reading**: feed → question → answers → comments, loaded per layer on demand
54
+ - **HN discussion tree**: top-level comments as floors, reply trees one `Enter` deeper; deeply nested threads are flattened to two levels with `@parent` prefixes preserving who-replies-to-whom
55
+ - **Auto Grok translation for foreign posts on X**; media markers `[图片]` / `[视频]` are clickable links to the original image / video
56
+ - **Login loop**: in-app Chrome QR / password login when a source needs it; `Ctrl+U` to switch accounts
57
+ - **Enable sources on demand**: pick at first launch, `F2` anytime; site order = the order you tick
58
+ - **Bilingual UI (中文 / English)**: pick the language at first launch (pre-selected from your system), change anytime with `Ctrl+L` (applies after restart); message catalogs live in one file per language, and a new language is just one new TOML file
59
+ - **Editor-inspired theme**: VS Code Dark+ oriented dark palette, easy on the eyes for long reads
60
+ - **IME-friendly keys**: every binding is a non-printing key (Tab / arrows / function keys), works while a Chinese IME is active
61
+ - **Layout-change self-check**: 0 items parsed → immediate "site may have changed" error; `feedscope probe` inspects all sources and drops raw samples
62
+
63
+ ## Screenshots
64
+
65
+ Zhihu Hot (feed sidebar on the left, meta column right-aligned and adaptive):
66
+
67
+ ![Zhihu hot list](docs/screenshot-zhihu.png)
68
+
69
+ X For-You timeline (bylines show "name @handle" in full; media markers link to originals):
70
+
71
+ ![X timeline](docs/screenshot-x.png)
72
+
73
+ X post detail (media links, translation block, quoted post):
74
+
75
+ ![X post detail](docs/screenshot-detail.png)
76
+
77
+ Reddit Popular (meta column "N pts · N comments · r/sub"; add subreddits in `F2`):
78
+
79
+ ![Reddit popular list](docs/screenshot-reddit.png)
80
+
81
+ Reddit post detail (markdown self-text + comment floors with scores; moderator stickies flagged `[置顶]`):
82
+
83
+ ![Reddit post detail](docs/screenshot-reddit-detail.png)
84
+
85
+ HN Front page (no login needed; "N pts · N comments · author" doubles as a discussion-heat radar):
86
+
87
+ ![HN front page](docs/screenshot-hn.png)
88
+
89
+ HN story detail (one-line article link + top-level comments as floors, each floor headed by "N replies"):
90
+
91
+ ![HN story detail](docs/screenshot-hn-detail.png)
92
+
93
+ HN reply tree of one comment (deep threads flattened to two levels, cross-level replies carry an `@parent` prefix):
94
+
95
+ ![HN reply tree](docs/screenshot-hn-thread.png)
96
+
97
+ V2EX Hot (no login; the "node · N replies · user" meta column scans as board + heat at a glance):
98
+
99
+ ![V2EX hot list](docs/screenshot-v2ex.png)
100
+
101
+ V2EX topic detail (markdown body + replies as floors; floor numbers match the site's floor culture):
102
+
103
+ ![V2EX topic detail](docs/screenshot-v2ex-detail.png)
104
+
105
+ ## Install
106
+
107
+ Requires Python 3.11+ and a local Google Chrome (no `playwright install` needed). macOS and Linux; Windows is not supported yet.
108
+
109
+ ```bash
110
+ pipx install feedscope # or: uv tool install feedscope
111
+ ```
112
+
113
+ From source (development):
114
+
115
+ ```bash
116
+ git clone https://github.com/howardli/FeedScope.git
117
+ cd FeedScope
118
+ uv sync # dev / test environment
119
+ uv tool install --editable . # global `feedscope` command; code edits take effect immediately
120
+ ```
121
+
122
+ - Run `feedscope doctor` after install to self-check the environment
123
+ - Slow PyPI? Use a mirror: `export UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple`
124
+ - Dev / test: `uv run feedscope` and `uv run pytest` — see [CONTRIBUTING](CONTRIBUTING.md)
125
+
126
+ ## Usage
127
+
128
+ ```bash
129
+ feedscope
130
+ ```
131
+
132
+ ### Keyboard
133
+
134
+ | Key | Action |
135
+ | --- | --- |
136
+ | `Tab` / `Shift+Tab` | Switch site (afterwards the keyboard lands in the left sidebar to pick a feed; `Enter` returns to the list) |
137
+ | `←` / `→` | Region moves: from the list `←` enters the sidebar (staying on the current feed) and `→` hits the right edge; from the sidebar `→` returns to the list and `←` goes to the source bar; in the source bar switches site in place; prev / next article in details; prev / next floor in reply layers |
138
+ | `↑` / `↓` | Move selection / scroll body; `↑` on the first row of any region climbs to the top source bar |
139
+ | `Enter` | Open detail; inside a detail, open the discussion layer of the floor you are reading; return to the list from the sidebar / source bar |
140
+ | `Ctrl+N` | Load more (list / floors / comments, each layer independently) |
141
+ | `Ctrl+O` | Open current item in browser |
142
+ | `F5` | Refresh |
143
+ | `Ctrl+U` | Account: login / logout |
144
+ | `Ctrl+L` | UI language: pick and save; restart `feedscope` to apply |
145
+ | `F2` | Manage sources: toggle enable / disable, saved on confirm; `→` opens subscription manager (RSS / Reddit / Telegram) |
146
+ | `Esc` | Go back one layer (from the sidebar / source bar, back to the list first) |
147
+ | `Ctrl+Q` | Quit (double `Ctrl+C` as fallback) |
148
+
149
+ All keys are non-printing — they work while a Chinese IME is active; typing printable characters by mistake shows a hint. The top tab bar and the left sidebar are clickable and drive the same code path as the keyboard. Arrow keys follow CLI conventions: `↑`/`↓` move the selection within the current list; `↑` on the first row of any region climbs to the source bar (the top region sits above everything), and `↓` from the source bar descends to the sidebar (inverse of the sidebar's first-feed `↑`); other region moves are horizontal — in the source bar `←`/`→` switches site in place; in the sidebar `↑`/`↓` switches feed in place, `→` returns to the list and `←` goes to the source bar; in the list `←` enters the sidebar (staying on the current feed) and `→` hits the right edge; `Enter` / `Esc` return to the list at any time. Dialogs (source manager / subscriptions / input / confirm / account) support the same arrow-key focus movement: `↓` past the bottom of the list enters the button row, `↑` returns to the list, `←`/`→` move between buttons, and `Enter` presses the focused button.
150
+
151
+ ### UI Language
152
+
153
+ The first launch asks for the language (pre-selected from your system locale; `Enter` to confirm, `Esc` keeps the default). Change it anytime with `Ctrl+L`; the choice is saved to the config and **takes effect when you restart `feedscope`** (counts and unit words are baked into fetched data in the process language, so a fixed-per-process language is the most robust model). **English is the default when no language is configured** — 中文用户在首启回车即选简体中文,或 `Ctrl+L` 随时切换。Subcommands (`doctor` / `probe` / `rss` / `login`) follow the same language as the TUI:
154
+
155
+ ```toml
156
+ language = "zh-CN" # or "en" (the default); delete the line to fall back to English (only a fresh install asks again)
157
+ ```
158
+
159
+ Adding a language: copy `src/feedscope/core/i18n/en.toml` to `<code>.toml` (e.g. `ja.toml`), set its native name in `[meta]` (`name = "日本語"`) and translate the messages — the language picker discovers it automatically, no code changes. A parity test (`tests/test_i18n.py`) enforces identical key trees across catalogs, and an AST guard (`tests/test_i18n_no_hardcoded.py`) blocks hardcoded copy in `src`, so a missing translation turns the CI red.
160
+
161
+ ### Login / Logout
162
+
163
+ Zhihu, X and Reddit require login (no anonymous feeds); HN, V2EX, RSS and Telegram work out of the box. Opening a logged-out source pops an in-app dialog; choosing "login" opens Chrome and auto-refreshes when done — no TUI restart. Dismiss it and you won't be asked again this session (`Ctrl+U` still works manually). `Ctrl+U` also switches accounts. From the CLI: `feedscope login [zhihu|x|reddit]`.
164
+
165
+ A session revoked server-side (password change, logout elsewhere, periodic rotation) never leaves you guessing: X's 401 is unambiguous (stale cookies dropped, re-login guided); Zhihu, Reddit and X all fetch through resident real-browser channel tabs (TLS, header order and cookies fully consistent with the login session, immune to non-browser-fingerprint blocking). Reddit's criterion is a local JWT decode of token_v2 (logged-in `sub="user"`, anonymous `sub="loid"`) — its login cookies are issued to anonymous traffic too (merely opening the login page sets one, so a cookie appearing ≠ login complete), while a local decode is zero-network and unforgeable. Both "login success" and the login gate use it, making a re-login loop structurally impossible.
166
+
167
+ ### Sources
168
+
169
+ | Source | Feeds | Login |
170
+ | --- | --- | --- |
171
+ | Zhihu | Hot / Recommend / Following | ✅ |
172
+ | X (Twitter) | For You / Following | ✅ |
173
+ | Reddit | Home / Popular / custom subreddits | ✅ |
174
+ | HN | Front / New / Ask / Show | — |
175
+ | V2EX | Hot / Latest | — |
176
+ | RSS | All / one per subscription | — |
177
+ | Telegram | All / one per channel | — |
178
+
179
+ ### Subcommands
180
+
181
+ ```bash
182
+ feedscope [tui] # start the TUI (default)
183
+ feedscope login [source] # browser login (defaults to zhihu; e.g. x, reddit)
184
+ feedscope probe [source…] # inspect all sources; raw samples saved to disk
185
+ feedscope doctor # environment self-check (Chrome / profile / config)
186
+ feedscope rss add <url> [--key k --label name] # add: feed URL / site homepage (auto-discovered) / OPML path
187
+ feedscope rss list | remove <key> # list / remove subscriptions
188
+ feedscope rss import <x.opml> | export [x.opml] # bulk import (idempotent merge) / export
189
+ ```
190
+
191
+ ### Source & Feed Config
192
+
193
+ First launch shows a picker; `F2` reopens it anytime. Selections are saved to user config (`~/.config/feedscope/sources.toml` on Linux; `FEEDSCOPE_CONFIG` overrides), which you can also hand-edit; restart to apply:
194
+
195
+ ```toml
196
+ language = "zh-CN" # UI language (maintained by first-run / Ctrl+L)
197
+ enabled = ["zhihu", "x", "reddit", "hn", "v2ex", "rss"] # order = site order; remove an id to disable
198
+
199
+ [zhihu]
200
+ hide = ["follow"] # hide built-in feeds you don't want
201
+
202
+ [reddit.feeds] # subreddit table: key = feed name (names colliding with built-in home/popular are rejected)
203
+ rust = { label = "Rust", url = "https://www.reddit.com/r/rust/" }
204
+
205
+ [telegram.feeds] # channel table: key = feed name (avoid the aggregate name all)
206
+ durov = { url = "https://t.me/durov/" }
207
+
208
+ [rss.feeds] # RSS subscription table: key = feed name (avoid the reserved key all)
209
+ ruanyq = { label = "Ruanyifeng Weekly", url = "https://www.ruanyifeng.com/blog/atom.xml" }
210
+ hnrss = { url = "https://hnrss.org/newest?q=rust" } # label defaults to key (keyword alerts still fit hnrss; for HN itself use the built-in source)
211
+
212
+ [rss]
213
+ hide = ["all"] # hide the "All" aggregate, or a subscription key to hide one feed
214
+ ```
215
+
216
+ Omitting `enabled` = everything enabled; saving from the manager preserves per-source `hide` sections and `feeds` tables (`[rss.feeds]` / `[reddit.feeds]` / `[telegram.feeds]`). To reorder sites: untick then re-tick in `F2` (moves to the end).
217
+
218
+ ### RSS Subscriptions
219
+
220
+ Three ways to manage, pick any:
221
+
222
+ - **In the TUI (`F2`)**: on the RSS row press `→` to open the subscription manager — `Insert` add, `Enter` rename, `Delete` remove, `↑↓` select; changes save instantly; RSS re-ticks itself on return
223
+ - **CLI**: `feedscope rss add / list / remove`; migrating from another reader: `feedscope rss import subs.opml` (OPML from Feedly / Inoreader / NetNewsWire etc.; existing keys are skipped); `export` anytime so your subscriptions are never locked in
224
+ - **Hand-edit config**: edit the `[rss.feeds]` table (see above), then tick RSS in `F2`
225
+
226
+ Convenience behaviors of the add dialog (identical to `rss add`):
227
+
228
+ - Paste an RSS/Atom URL, **or a site homepage** — the declared feed is auto-discovered
229
+ - Paste **a local OPML path** = bulk import (drag the file into the terminal from Finder)
230
+ - Label defaults to the feed's own title (fetched once); add / import auto-enables RSS
231
+
232
+ With zero subscriptions RSS doesn't appear in the source list (`F2` can still open its manager to add the first one). Behavior notes:
233
+
234
+ - The "All" stream merges every subscription by publish time (newest first); the same URL across feeds (reposts / aggregators) is deduped; one dead feed only loses its own items, all dead = error
235
+ - Details read feed-provided content (`content` full text first, `summary` fallback); images become direct links; summary-only feeds show short bodies — `Ctrl+O` to read the rest in a browser
236
+ - Refreshes are conditional requests (ETag / Last-Modified) — unchanged feeds don't re-transfer bodies
237
+ - RSS has no login and no comment layer
238
+
239
+ ### Hacker News
240
+
241
+ No login, no API key; data comes from the [public Algolia HN index](https://hn.algolia.com/api) (the same engine behind HN's own site search). Behavior notes:
242
+
243
+ - Same three-layer shape as Zhihu: story (link posts: body = one article link; Ask/Show: self-text in full) → top-level comments as floors → `Enter` opens a floor's reply tree
244
+ - Meta column shows "N pts · N comments · author"; deep threads flatten to two levels, cross-level replies carry an `@parent` prefix preserving the conversation thread
245
+ - The index lags a few minutes and lists are sorted by points (not official rank); floors with no replies don't advertise `Enter`
246
+
247
+ ### V2EX
248
+
249
+ No login; data comes from official public v1 endpoints (hot / latest / replies, no token needed; [official API reference](https://www.v2ex.com/help/api)). Behavior notes:
250
+
251
+ - Two-layer shape: topic (markdown body; auto-embedded images become `[图片]` markers, `@` mentions become clickable links) → replies flattened as floors; V2EX replies have no nesting ("sub-replies" are just `@` text), so there is no third layer
252
+ - Meta column shows "node · N replies · user"; floor numbers match the site's floor culture, and a floor's permalink is the `#reply` anchor on the topic page
253
+ - The API returns everything in one shot (list fits a page, replies unpaginated) — floor paging costs zero requests; the body rides along with the list, so opening a detail costs exactly 1 request
254
+
255
+ ### X (Twitter)
256
+
257
+ Login required; data comes from the web GraphQL endpoints — requests are issued by a resident browser tab's real Chrome (same channel design as Zhihu / Reddit: TLS, header order and cookies fully consistent with the login session; cf_clearance-style fingerprint blocking simply doesn't apply to in-page fetch). The endpoint queryIds rotate with X deploys: this source starts from snapshot constants and **self-heals by harvesting on failure** — on error it reopens the tab, collects the current deployment's JS chunks, greps out fresh queryIds and retries — turning X redesign days from "run a script and hand-copy constants" into "recovers automatically the same day". Behavior notes:
258
+
259
+ - Meta column shows "name @handle"; foreign-language tweets get an auto Grok translation block; media become clickable `[图片]` / `[视频]` markers to originals / direct video links
260
+ - Detail: full focal tweet + expanded quote chain; replies page along the ShowMore cursor; retweets (RT) jump to the original tweet's conversation
261
+ - Search is out: SearchTimeline's transaction-id gating requires a header only X's own JS computes — in-page fetch can't get in either
262
+
263
+ ### Reddit
264
+
265
+ Login required; data comes from **the website's own HTML channel** — the legacy `.json` endpoints have been 403-only for third parties since 2026, so this source rides what the new frontend itself uses: listing pages as SSR HTML, pagination following the `faceplate-partial` load-after chain. Requests are issued by a resident browser tab's real Chrome (same channel design as Zhihu: TLS, header order and cookies fully consistent with the login session, immune to non-browser-fingerprint blocking); `x-reddit-client-version` is harvested from page responses automatically and self-heals on 403. Behavior notes:
266
+
267
+ - Same three-layer shape as HN: post (self posts: HTML converted to markdown; link posts: one article link; media posts: `[图片]` / `[视频]` markers to originals) → top-level comments as floors → `Enter` opens a floor's reply tree
268
+ - Login criterion: a local JWT decode of token_v2 (logged-in `sub="user"`, anonymous `sub="loid"`) — the mere "presence" of reddit_session/token_v2 can be produced by anonymous traffic; the decode is the hard proof. Login polling and the gate share this one check: zero network, zero forgery
269
+ - Meta column shows "N pts · N comments · r/sub"; floors carry scores, moderator stickies are flagged `[置顶]`; deep threads flatten to two levels with `@parent` prefixes (same as HN)
270
+ - "Home" aggregates the subreddits your Reddit account follows on the site (empty for a fresh account that follows nothing — expected); subscribe to what you want instead: `F2` → Reddit row → `→`, type a subreddit name or URL (`rust`, `r/rust`, a site link all work) and it becomes its own feed — no need to follow it on reddit.com
271
+ - The comment tree arrives in one request (huge threads are truncated) with "N more collapsed" hints where cut; floor / reply paging is all in-memory, zero extra requests
272
+
273
+ ### Telegram Channels
274
+
275
+ No login; data comes from each channel's public web preview (`t.me/s/<channel>`, server-rendered HTML readable as long as the owner keeps preview on). Behavior notes:
276
+
277
+ - Pure subscription source shaped exactly like RSS: `F2` → Telegram row → `→` opens the manager; type a channel name or link (`durov`, `@durov`, `t.me/durov`, even a post link); label defaults to the channel's own name
278
+ - The "All" stream merges every channel by publish time (newest first), one dead channel only loses its own items; each channel is also its own feed (individually hideable)
279
+ - Meta column "N views" is the channel-post heat radar; the "All" stream prefixes the channel name; inline HTML becomes markdown, images/videos become `[图片]` / `[视频]` markers (videos have no direct link — the marker points to the post page)
280
+ - Details read list-carried content (zero requests); `Ctrl+N` in a single-channel feed pages back through history via the `?before=` cursor; no comment layer (channel discussion groups aren't in the preview)
281
+
282
+ ## Community
283
+
284
+ - **Bugs & features** — [issues](https://github.com/howardli/FeedScope/issues): the bug template asks for `feedscope doctor` output, which carries version + environment in a few lines
285
+ - **Questions & chat** — [discussions](https://github.com/howardli/FeedScope/discussions)
286
+ - **Code** — [CONTRIBUTING](CONTRIBUTING.md): adding a new source is designed to be a one-package change, and a new UI language is one TOML file
287
+ - **Security** — [SECURITY.md](SECURITY.md): where your login cookies live and how to report issues privately
288
+ - **Changelog** — [CHANGELOG.md](CHANGELOG.md)
289
+
290
+ Status: **0.x, active development.** Config may gain fields between minors; breaking changes wait for 1.0. A site redesign can break its source at any time — `feedscope probe` pinpoints it, patch releases ship updated parsers.
291
+
292
+ ## Disclaimer
293
+
294
+ This project accesses Zhihu, X and Reddit web APIs through your local browser session, for personal learning and research only:
295
+
296
+ - Follow the laws of your region and each site's user agreements; account-risk is your own
297
+ - Ships with no credentials; login state stays in the local browser profile and is never shared
298
+ - Breakage from site redesigns is expected wear (`feedscope probe` helps locate them); do not use for bulk scraping or commercial purposes
299
+
300
+ ## License
301
+
302
+ [MIT](LICENSE)
@@ -0,0 +1,72 @@
1
+ feedscope/__init__.py,sha256=Smhk5Txm-0CaAjOSBc94_fT-FjSlFucvA5dO3iXD_sY,141
2
+ feedscope/__main__.py,sha256=vhKkcKiOSyZanYu4L3ZvVz7f1znfcLUCx6DWGYeuCrQ,88
3
+ feedscope/cli.py,sha256=TptDJSedyVKQRUD6Fnz6ITh7CbjhvUg-zQ_JqFEKMNo,5698
4
+ feedscope/doctor.py,sha256=bLNpkSCzEGFQPihWGR3AOV-RGMk2jfBX1pvaymQvOgg,1626
5
+ feedscope/login.py,sha256=RaDGB-P_6FkFJg4MPhuML2wlazU4FsEqKywiwI7XPHI,7340
6
+ feedscope/probe.py,sha256=yoR8f7IDv-dgtZEg3LT8j_y1CWc3py1Fv-OceOtL_uM,5195
7
+ feedscope/report.py,sha256=ToA3a3xDw9LME-6-pQcmEw-mimv-0t92Jg2MOaB4xK4,754
8
+ feedscope/subscriptions.py,sha256=ahErqXnflJ2cQIqOLSbsfCPE1O-ampN2RkABORn2NoM,17830
9
+ feedscope/core/__init__.py,sha256=7kGkJR7M3awEQy4nwL-LIE9v5qxmqsLJ6KJ9VMEbP90,86
10
+ feedscope/core/browser.py,sha256=O-n8fT0v5mwMxle32Z5zgsuFtueFJSLFXcidQ25th3E,15618
11
+ feedscope/core/config.py,sha256=Sg6XVDRjo2lga0ai7SOgluqS_wNKPecsGCXfhdz-4vQ,15372
12
+ feedscope/core/const.py,sha256=QiOAoyexYppgL89MN5e6KTzVgaMU9-w2Q9G9AgLONFA,2440
13
+ feedscope/core/display.py,sha256=rjprEUgGnWpM5zxAtFHgt1_s1G3qv02F6Fuh8ZSx6Co,2836
14
+ feedscope/core/http.py,sha256=ejoLr6KSZEbVlEPaSmweHOZ4wEGY03_R1vCIDrhM8Yk,3947
15
+ feedscope/core/media.py,sha256=abgEIkgtlI5a9HSE2Hk5VIkaXzZulGW06v5IsB3OJSk,6396
16
+ feedscope/core/models.py,sha256=d4eJqdvq5HlYN4FB6p6pldHu_b7b74sXwK_ZgePls-Q,3574
17
+ feedscope/core/registry.py,sha256=gThnyDh8PmvlrRkvgWvIYs1wim3qRvwFWLZZnFVFAPI,1826
18
+ feedscope/core/samples.py,sha256=_nqF9N3t47W2pKWH5hK0iZE_zfcoCHrd2Kz10kBgOF4,2586
19
+ feedscope/core/source.py,sha256=rOwhoq2sxwH_Ee8gQ5-EtfgAVhQt8chf6Ci8hjKyum8,28981
20
+ feedscope/core/text.py,sha256=g6oKEl25x6JF7YStWCREeeI8HAAF3qHCsFoexW1sWMs,1923
21
+ feedscope/core/timefmt.py,sha256=bmTasakPmpIeJF8csXQ0mfETtU2VW3l1NstnJHqCouE,1905
22
+ feedscope/core/tree.py,sha256=ogLEG02KuKP7wXD_9X4MQDIKaptwlpYcLJ9krlfKIZo,5142
23
+ feedscope/core/i18n/__init__.py,sha256=sCeOKv7QmUONn_P04uUWwv8d9hkXPXdaQ-TVZjALYPc,7982
24
+ feedscope/core/i18n/en.toml,sha256=fLq_QZkQWRB8AogSTrFmUa27ozxSbl3y38zISzyM-G0,18760
25
+ feedscope/core/i18n/zh-CN.toml,sha256=U-XETEYtERPtds03wI2GhUNXdISAhUMLVn5_wiESgM0,17954
26
+ feedscope/sources/__init__.py,sha256=Mm8pwp6NndYFZXWgmOYRxEo54_DLG3_kIJgxU1i2cKk,2294
27
+ feedscope/sources/hn/__init__.py,sha256=y6Q9v-HnnojHtEORrlQWCjz2_ryh3IBlJ-7CaDjqY5I,2994
28
+ feedscope/sources/hn/fetch.py,sha256=c34KimBhdGPQaMwQWJL-Cjisk7oVqFcr9m2Hj1_ff-I,3418
29
+ feedscope/sources/hn/parse.py,sha256=Dn97R_WQtjozI4hMfkIynuP5pWR-EtbuzwLkf86gtzM,5816
30
+ feedscope/sources/reddit/__init__.py,sha256=YOzvc7hfj3tLOun_ijUKnD5H_JI4rNLNWSlKJLC4w7A,9006
31
+ feedscope/sources/reddit/fetch.py,sha256=gfdhxp6gJk0bBiwR4_9mCpcE5z1Sdh4iNaTLhemdJ_Q,8557
32
+ feedscope/sources/reddit/parse.py,sha256=D4PGyx6TIdwsCiIu6pvrM_64hMtTWD41bYKFCrhSA78,12468
33
+ feedscope/sources/rss/__init__.py,sha256=q3CUJFKt_1J3KdQfB41OMbdx58VrBC38nM4gU1UKymQ,2784
34
+ feedscope/sources/rss/fetch.py,sha256=2Z_q8rl4LrCm-GoRA2tB2CYSzRZb8KtcPXPc8-7yYsA,3583
35
+ feedscope/sources/rss/parse.py,sha256=38MAeLoDvvF0Lc5i2EaY2oEU9cj3g2ITiTOwPpyCurQ,4389
36
+ feedscope/sources/telegram/__init__.py,sha256=mjiXK0ZyLHQQOeMfxhrvuUk0882xg02JSlJwHG_On0A,3016
37
+ feedscope/sources/telegram/fetch.py,sha256=9Ipo5tV02F-9UqPYSBuhzYcICbVCXP26f6Q-mK89MGk,3350
38
+ feedscope/sources/telegram/parse.py,sha256=XcxUm1_TyCY4IaZPHTffIedpCzkj_MnI_0ZHWoroLNs,8252
39
+ feedscope/sources/v2ex/__init__.py,sha256=Q9u9T0joH35JrzYPxgcimnBz3a5671eUJW6uvqkQ7yQ,2963
40
+ feedscope/sources/v2ex/fetch.py,sha256=IVXEma7yFSaZNl6Q4l0te0xSDQaQJrA__tjjLQDOCXM,2032
41
+ feedscope/sources/v2ex/parse.py,sha256=AuILabDTeqe-lDNJsRhQVbMTCsaaCNCzyAv1AO6nDoc,4292
42
+ feedscope/sources/x/__init__.py,sha256=fthdU1ZhSz37mMfEUt_jNBTN2zf_tvPij8y701ottF8,2753
43
+ feedscope/sources/x/fetch.py,sha256=pZuCFSO0piFOumY2c_3ViZx7ua_QuaWX2Z4Ud4vHZY8,22285
44
+ feedscope/sources/x/parse.py,sha256=4b80ppBDHW1oFervQ8VoBTHCfpFLdn1s3IIbBaFCoM8,24247
45
+ feedscope/sources/zhihu/__init__.py,sha256=hSHTdU1OSGMZvIxuDOMohUcxcRLi9AJdiS03yqv229Y,3229
46
+ feedscope/sources/zhihu/fetch.py,sha256=F9t70Ze86x0ChETdvrO2ishbD3lwGFIoO575O9qAinE,18459
47
+ feedscope/sources/zhihu/parse.py,sha256=Sy9kyg_gU5f4aVh0RfbjkCA1LgHgksfEPFtFhQjAgeo,10972
48
+ feedscope/ui/__init__.py,sha256=vgh_xqhGi9Ez0ME3fA95ktwCS4WInGz2wGXbCJ2hGg8,465
49
+ feedscope/ui/account.py,sha256=fN_qzwTwlBcaQB7Vp2QhfMuESCtPdBVoapwQMMoPT_Y,6129
50
+ feedscope/ui/app.py,sha256=oKRmD6FAYGf-IsVGHvv9x_o47A1rmxRR1jhSafBLMjk,4865
51
+ feedscope/ui/dialogs.py,sha256=Yf2VdBXsbynzGMEjbW9B7ABAlyaeMqEjNrQZiawIcYw,5052
52
+ feedscope/ui/feedback.py,sha256=mmZ7O7ZyvVaDz1Qc19vdATjdN2Zu6yW8emaYF0yzbGM,2616
53
+ feedscope/ui/focus_nav.py,sha256=Mygu6ImErnrtlB9H4InttbWBCAFB74bkrMAtaSkRd_c,4809
54
+ feedscope/ui/labels.py,sha256=KTskl88I3ekf6dYOXaOjKjfa6NSKzos6O7dpkhs3b0M,1583
55
+ feedscope/ui/layout.py,sha256=CxzR2gPTtGor7R65tlvDFGoJ2xE6-hw7n_Nn9ChgXLo,3230
56
+ feedscope/ui/list_render.py,sha256=j43TtFdZmWGnkk8bSA_plN96qr2dBx8xW3x4U8CJmDI,1690
57
+ feedscope/ui/theme.py,sha256=bGqKu8ihzBIWLrapSwm-oEH98ahC81JpDpuzrBSzz2M,2208
58
+ feedscope/ui/wrap.py,sha256=weLRh_94aVN1chcdelICg_YJRJ7BOMxmXcAyJ3QTRYE,6112
59
+ feedscope/ui/screens/__init__.py,sha256=wv_lGxH6cAY5g8O7r1I7ibpkhCH7xzkIxUZ99hTJlC8,102
60
+ feedscope/ui/screens/answer.py,sha256=pkAa0Re67_X5A5D7qJJ2lWlaU9ZNyAsxqnsbKa2sVV8,12637
61
+ feedscope/ui/screens/detail.py,sha256=FvXXyFjrusiq9yN2IH-FVx08hKoXwhl8SB-ZrnsOQCY,12380
62
+ feedscope/ui/screens/language.py,sha256=04qq1RwfmwOmzAUN0a_0rEgzSPmRUBFgww_dPdd6sPQ,2480
63
+ feedscope/ui/screens/main.py,sha256=2cZ3lT7-gAqAE03Dpf4MUOSZzIOu49IkoPEHZV4W1hM,34147
64
+ feedscope/ui/screens/main_setup.py,sha256=8vojvk3A4BaQ2A_g3BOfsCk8b_9DhJAw1X63JvyJvFY,10988
65
+ feedscope/ui/screens/reading.py,sha256=ulRt89RXAFwOwZK4wW7mTG0f9ZMiyrvJnLQPgH7DZAw,17503
66
+ feedscope/ui/screens/source_setup.py,sha256=uvYlBO3naL2Hm1oP13GiPMmNTwk7rhexhsAyMW8yU4E,9667
67
+ feedscope/ui/screens/subscriptions.py,sha256=f5EzUC1t-0vBmjTe3Dn4b8lSO955A6s-OYv2fG9xPgs,8011
68
+ feedscope-0.1.0.dist-info/METADATA,sha256=2zo60KgUUw_sEOTPO5jD7NWZnlEZR0qVQ0sL6wZMrx4,21823
69
+ feedscope-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
70
+ feedscope-0.1.0.dist-info/entry_points.txt,sha256=aey4GMy_4ZcnjH3AjKZ-ZdsEMLiAHWFLTKQKVlCzzqs,49
71
+ feedscope-0.1.0.dist-info/licenses/LICENSE,sha256=Bt5d7VUv7XERCzikfMR50exYUP82MiX2H94kiICr3eU,1065
72
+ feedscope-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ feedscope = feedscope.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 howardli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.