feedscope 0.1.0__tar.gz
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.
- feedscope-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +82 -0
- feedscope-0.1.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
- feedscope-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +32 -0
- feedscope-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +21 -0
- feedscope-0.1.0/.github/dependabot.yml +22 -0
- feedscope-0.1.0/.github/workflows/ci.yml +50 -0
- feedscope-0.1.0/.github/workflows/release.yml +54 -0
- feedscope-0.1.0/.gitignore +13 -0
- feedscope-0.1.0/CHANGELOG.md +25 -0
- feedscope-0.1.0/CODE_OF_CONDUCT.md +79 -0
- feedscope-0.1.0/CONTRIBUTING.md +142 -0
- feedscope-0.1.0/LICENSE +21 -0
- feedscope-0.1.0/PKG-INFO +302 -0
- feedscope-0.1.0/README.md +271 -0
- feedscope-0.1.0/README.zh-CN.md +271 -0
- feedscope-0.1.0/SECURITY.md +39 -0
- feedscope-0.1.0/docs/release.md +66 -0
- feedscope-0.1.0/pyproject.toml +99 -0
- feedscope-0.1.0/scripts/demo_gif.py +143 -0
- feedscope-0.1.0/scripts/screenshot.py +828 -0
- feedscope-0.1.0/scripts/x_probe.py +62 -0
- feedscope-0.1.0/src/feedscope/__init__.py +3 -0
- feedscope-0.1.0/src/feedscope/__main__.py +4 -0
- feedscope-0.1.0/src/feedscope/cli.py +154 -0
- feedscope-0.1.0/src/feedscope/core/__init__.py +1 -0
- feedscope-0.1.0/src/feedscope/core/browser.py +367 -0
- feedscope-0.1.0/src/feedscope/core/config.py +367 -0
- feedscope-0.1.0/src/feedscope/core/const.py +64 -0
- feedscope-0.1.0/src/feedscope/core/display.py +62 -0
- feedscope-0.1.0/src/feedscope/core/http.py +104 -0
- feedscope-0.1.0/src/feedscope/core/i18n/__init__.py +202 -0
- feedscope-0.1.0/src/feedscope/core/i18n/en.toml +447 -0
- feedscope-0.1.0/src/feedscope/core/i18n/zh-CN.toml +444 -0
- feedscope-0.1.0/src/feedscope/core/media.py +150 -0
- feedscope-0.1.0/src/feedscope/core/models.py +86 -0
- feedscope-0.1.0/src/feedscope/core/registry.py +53 -0
- feedscope-0.1.0/src/feedscope/core/samples.py +68 -0
- feedscope-0.1.0/src/feedscope/core/source.py +668 -0
- feedscope-0.1.0/src/feedscope/core/text.py +54 -0
- feedscope-0.1.0/src/feedscope/core/timefmt.py +53 -0
- feedscope-0.1.0/src/feedscope/core/tree.py +134 -0
- feedscope-0.1.0/src/feedscope/doctor.py +54 -0
- feedscope-0.1.0/src/feedscope/login.py +168 -0
- feedscope-0.1.0/src/feedscope/probe.py +131 -0
- feedscope-0.1.0/src/feedscope/report.py +19 -0
- feedscope-0.1.0/src/feedscope/sources/__init__.py +38 -0
- feedscope-0.1.0/src/feedscope/sources/hn/__init__.py +66 -0
- feedscope-0.1.0/src/feedscope/sources/hn/fetch.py +82 -0
- feedscope-0.1.0/src/feedscope/sources/hn/parse.py +154 -0
- feedscope-0.1.0/src/feedscope/sources/reddit/__init__.py +178 -0
- feedscope-0.1.0/src/feedscope/sources/reddit/fetch.py +181 -0
- feedscope-0.1.0/src/feedscope/sources/reddit/parse.py +318 -0
- feedscope-0.1.0/src/feedscope/sources/rss/__init__.py +64 -0
- feedscope-0.1.0/src/feedscope/sources/rss/fetch.py +94 -0
- feedscope-0.1.0/src/feedscope/sources/rss/parse.py +116 -0
- feedscope-0.1.0/src/feedscope/sources/telegram/__init__.py +61 -0
- feedscope-0.1.0/src/feedscope/sources/telegram/fetch.py +79 -0
- feedscope-0.1.0/src/feedscope/sources/telegram/parse.py +182 -0
- feedscope-0.1.0/src/feedscope/sources/v2ex/__init__.py +64 -0
- feedscope-0.1.0/src/feedscope/sources/v2ex/fetch.py +46 -0
- feedscope-0.1.0/src/feedscope/sources/v2ex/parse.py +116 -0
- feedscope-0.1.0/src/feedscope/sources/x/__init__.py +68 -0
- feedscope-0.1.0/src/feedscope/sources/x/fetch.py +490 -0
- feedscope-0.1.0/src/feedscope/sources/x/parse.py +535 -0
- feedscope-0.1.0/src/feedscope/sources/zhihu/__init__.py +81 -0
- feedscope-0.1.0/src/feedscope/sources/zhihu/fetch.py +402 -0
- feedscope-0.1.0/src/feedscope/sources/zhihu/parse.py +235 -0
- feedscope-0.1.0/src/feedscope/subscriptions.py +423 -0
- feedscope-0.1.0/src/feedscope/ui/__init__.py +10 -0
- feedscope-0.1.0/src/feedscope/ui/account.py +125 -0
- feedscope-0.1.0/src/feedscope/ui/app.py +105 -0
- feedscope-0.1.0/src/feedscope/ui/dialogs.py +127 -0
- feedscope-0.1.0/src/feedscope/ui/feedback.py +64 -0
- feedscope-0.1.0/src/feedscope/ui/focus_nav.py +113 -0
- feedscope-0.1.0/src/feedscope/ui/labels.py +38 -0
- feedscope-0.1.0/src/feedscope/ui/layout.py +66 -0
- feedscope-0.1.0/src/feedscope/ui/list_render.py +42 -0
- feedscope-0.1.0/src/feedscope/ui/screens/__init__.py +1 -0
- feedscope-0.1.0/src/feedscope/ui/screens/answer.py +296 -0
- feedscope-0.1.0/src/feedscope/ui/screens/detail.py +274 -0
- feedscope-0.1.0/src/feedscope/ui/screens/language.py +59 -0
- feedscope-0.1.0/src/feedscope/ui/screens/main.py +736 -0
- feedscope-0.1.0/src/feedscope/ui/screens/main_setup.py +226 -0
- feedscope-0.1.0/src/feedscope/ui/screens/reading.py +384 -0
- feedscope-0.1.0/src/feedscope/ui/screens/source_setup.py +217 -0
- feedscope-0.1.0/src/feedscope/ui/screens/subscriptions.py +196 -0
- feedscope-0.1.0/src/feedscope/ui/theme.py +45 -0
- feedscope-0.1.0/src/feedscope/ui/wrap.py +157 -0
- feedscope-0.1.0/tests/conftest.py +172 -0
- feedscope-0.1.0/tests/test_account.py +305 -0
- feedscope-0.1.0/tests/test_architecture.py +171 -0
- feedscope-0.1.0/tests/test_browser.py +59 -0
- feedscope-0.1.0/tests/test_channel.py +218 -0
- feedscope-0.1.0/tests/test_config.py +298 -0
- feedscope-0.1.0/tests/test_display.py +30 -0
- feedscope-0.1.0/tests/test_doctor.py +56 -0
- feedscope-0.1.0/tests/test_guard.py +41 -0
- feedscope-0.1.0/tests/test_hn_parse.py +176 -0
- feedscope-0.1.0/tests/test_hn_source.py +169 -0
- feedscope-0.1.0/tests/test_http.py +93 -0
- feedscope-0.1.0/tests/test_i18n.py +145 -0
- feedscope-0.1.0/tests/test_i18n_no_hardcoded.py +119 -0
- feedscope-0.1.0/tests/test_language_screen.py +261 -0
- feedscope-0.1.0/tests/test_login.py +264 -0
- feedscope-0.1.0/tests/test_media.py +44 -0
- feedscope-0.1.0/tests/test_parsers.py +1118 -0
- feedscope-0.1.0/tests/test_probe.py +147 -0
- feedscope-0.1.0/tests/test_reddit_parse.py +186 -0
- feedscope-0.1.0/tests/test_reddit_source.py +499 -0
- feedscope-0.1.0/tests/test_rss_parse.py +149 -0
- feedscope-0.1.0/tests/test_rss_source.py +227 -0
- feedscope-0.1.0/tests/test_samples.py +58 -0
- feedscope-0.1.0/tests/test_scripts_smoke.py +23 -0
- feedscope-0.1.0/tests/test_source_caps.py +153 -0
- feedscope-0.1.0/tests/test_subs_screen.py +367 -0
- feedscope-0.1.0/tests/test_subscriptions.py +444 -0
- feedscope-0.1.0/tests/test_telegram_parse.py +133 -0
- feedscope-0.1.0/tests/test_telegram_source.py +244 -0
- feedscope-0.1.0/tests/test_text.py +62 -0
- feedscope-0.1.0/tests/test_theme.py +17 -0
- feedscope-0.1.0/tests/test_timefmt.py +22 -0
- feedscope-0.1.0/tests/test_tree.py +128 -0
- feedscope-0.1.0/tests/test_tui_smoke.py +1248 -0
- feedscope-0.1.0/tests/test_v2ex_parse.py +140 -0
- feedscope-0.1.0/tests/test_v2ex_source.py +124 -0
- feedscope-0.1.0/tests/test_wrap.py +68 -0
- feedscope-0.1.0/tests/test_x_fetch.py +319 -0
- feedscope-0.1.0/tests/test_zhihu_fetch.py +496 -0
- feedscope-0.1.0/uv.lock +853 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
name: Bug report / 问题报告
|
|
2
|
+
description: Something is broken, renders wrong, or fails to fetch / 抓取失败、显示异常
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to report! 谢谢反馈!
|
|
9
|
+
|
|
10
|
+
Pasting `feedscope doctor` output up front (item 4) usually saves a
|
|
11
|
+
round trip — it carries the version, OS, Python and Chrome state in
|
|
12
|
+
four lines.
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: what-happened
|
|
15
|
+
attributes:
|
|
16
|
+
label: What happened / 发生了什么
|
|
17
|
+
description: And what did you expect instead? 期望的行为是什么?
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: dropdown
|
|
21
|
+
id: source
|
|
22
|
+
attributes:
|
|
23
|
+
label: Affected source / 受影响的源
|
|
24
|
+
multiple: true
|
|
25
|
+
options:
|
|
26
|
+
- Zhihu
|
|
27
|
+
- X (Twitter)
|
|
28
|
+
- Reddit
|
|
29
|
+
- Hacker News
|
|
30
|
+
- V2EX
|
|
31
|
+
- RSS
|
|
32
|
+
- Telegram
|
|
33
|
+
- TUI / app-wide (not source-specific)
|
|
34
|
+
- type: textarea
|
|
35
|
+
id: steps
|
|
36
|
+
attributes:
|
|
37
|
+
label: Steps to reproduce / 复现步骤
|
|
38
|
+
placeholder: |
|
|
39
|
+
1. `feedscope`
|
|
40
|
+
2. Tab to Reddit, wait for Popular
|
|
41
|
+
3. ...
|
|
42
|
+
validations:
|
|
43
|
+
required: true
|
|
44
|
+
- type: textarea
|
|
45
|
+
id: doctor
|
|
46
|
+
attributes:
|
|
47
|
+
label: "`feedscope doctor` output / doctor 输出"
|
|
48
|
+
description: Run it in a terminal and paste the whole output — it includes version and environment in one shot.
|
|
49
|
+
placeholder: |
|
|
50
|
+
FeedScope doctor v0.1.0
|
|
51
|
+
OK Python 3.12
|
|
52
|
+
...
|
|
53
|
+
render: text
|
|
54
|
+
validations:
|
|
55
|
+
required: true
|
|
56
|
+
- type: textarea
|
|
57
|
+
id: probe
|
|
58
|
+
attributes:
|
|
59
|
+
label: Probe sample (if a site redesign is suspected) / probe 样本(怀疑改版时)
|
|
60
|
+
description: |
|
|
61
|
+
`feedscope probe <source>` saves raw samples to disk — paste the
|
|
62
|
+
relevant excerpt if the error says a source may have changed. Mind
|
|
63
|
+
that samples can contain your feed content.
|
|
64
|
+
render: text
|
|
65
|
+
- type: dropdown
|
|
66
|
+
id: os
|
|
67
|
+
attributes:
|
|
68
|
+
label: OS / 操作系统
|
|
69
|
+
options:
|
|
70
|
+
- macOS
|
|
71
|
+
- Linux (Debian / Ubuntu)
|
|
72
|
+
- Linux (Arch)
|
|
73
|
+
- Linux (other)
|
|
74
|
+
- Windows (not supported yet — say more below)
|
|
75
|
+
- Other
|
|
76
|
+
validations:
|
|
77
|
+
required: true
|
|
78
|
+
- type: textarea
|
|
79
|
+
id: context
|
|
80
|
+
attributes:
|
|
81
|
+
label: Additional context / 其他补充
|
|
82
|
+
description: Logs, screenshots, config excerpts (minus anything sensitive) — anything that helps.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Questions & discussions / 交流讨论
|
|
4
|
+
url: https://github.com/howardli/FeedScope/discussions
|
|
5
|
+
about: Usage questions, setups and ideas that aren't bugs or concrete features
|
|
6
|
+
- name: Security issues / 安全问题
|
|
7
|
+
url: https://github.com/howardli/FeedScope/security/policy
|
|
8
|
+
about: Please don't report vulnerabilities in public issues — see the security policy
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Feature request / 功能建议
|
|
2
|
+
description: Suggest a new source, feed, or behavior / 新源、新流或行为建议
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: What problem does this solve? / 它解决什么问题
|
|
9
|
+
description: Not the feature itself — the pain behind it. 不是功能本身,是它背后的痛点。
|
|
10
|
+
placeholder: I can't ... / 我没法……
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: solution
|
|
15
|
+
attributes:
|
|
16
|
+
label: What would it look like? / 你期望的形态
|
|
17
|
+
description: Command, key, feed, source — sketch it however is natural.
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: dropdown
|
|
21
|
+
id: area
|
|
22
|
+
attributes:
|
|
23
|
+
label: Area / 领域
|
|
24
|
+
options:
|
|
25
|
+
- New source (a new site / 新站点)
|
|
26
|
+
- New feed in an existing source
|
|
27
|
+
- Reading / detail view
|
|
28
|
+
- Login & accounts
|
|
29
|
+
- RSS / subscriptions
|
|
30
|
+
- Config & source manager
|
|
31
|
+
- i18n
|
|
32
|
+
- Other
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## Summary / 摘要
|
|
2
|
+
|
|
3
|
+
<!-- What changed and why. One or two sentences, then the detail that matters. -->
|
|
4
|
+
|
|
5
|
+
## Type of change / 变更类型
|
|
6
|
+
|
|
7
|
+
- [ ] Bug fix
|
|
8
|
+
- [ ] New feature
|
|
9
|
+
- [ ] New source (registered alphabetically in `sources/__init__.py`; caps tests updated)
|
|
10
|
+
- [ ] Refactor / internals
|
|
11
|
+
- [ ] Docs / assets
|
|
12
|
+
|
|
13
|
+
## Checklist / 自查
|
|
14
|
+
|
|
15
|
+
- [ ] Tests added or updated for the behavior change (fixture-driven, offline)
|
|
16
|
+
- [ ] `uv run ruff check src/ tests/ scripts/` clean
|
|
17
|
+
- [ ] `uv run mypy` clean
|
|
18
|
+
- [ ] `uv run pytest` green
|
|
19
|
+
- [ ] User-facing change has a `CHANGELOG.md` entry under `[Unreleased]`
|
|
20
|
+
- [ ] UI-visible change → screenshots / demo GIF regenerated (`scripts/screenshot.py`, `scripts/demo_gif.py`)
|
|
21
|
+
- [ ] New user-visible strings go through i18n catalogs (no hardcoded copy)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: pip
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
commit-message:
|
|
8
|
+
prefix: "chore(deps)"
|
|
9
|
+
groups:
|
|
10
|
+
python-deps:
|
|
11
|
+
patterns:
|
|
12
|
+
- "*"
|
|
13
|
+
- package-ecosystem: github-actions
|
|
14
|
+
directory: "/"
|
|
15
|
+
schedule:
|
|
16
|
+
interval: weekly
|
|
17
|
+
commit-message:
|
|
18
|
+
prefix: "chore(ci)"
|
|
19
|
+
groups:
|
|
20
|
+
actions-deps:
|
|
21
|
+
patterns:
|
|
22
|
+
- "*"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
- run: uv sync
|
|
15
|
+
- run: uv run ruff check src/ tests/ scripts/
|
|
16
|
+
- run: uv run mypy
|
|
17
|
+
|
|
18
|
+
test:
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
# classifiers 声明支持的平台都在矩阵里被真跑过(macOS 的 Chrome 路径 /
|
|
23
|
+
# platformdirs 目录差异以前只有本机验证);playwright 只装包不装浏览器,
|
|
24
|
+
# 测试基线全离线,hosted runner 直接可跑
|
|
25
|
+
os: [ubuntu-latest, macos-latest]
|
|
26
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
27
|
+
runs-on: ${{ matrix.os }}
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: astral-sh/setup-uv@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: ${{ matrix.python-version }}
|
|
33
|
+
- run: uv sync --python ${{ matrix.python-version }}
|
|
34
|
+
- run: uv run pytest tests/ --cov --cov-fail-under=80
|
|
35
|
+
# 测试基线全离线(解析 fixture / TUI 冒烟 / 登录流程),hosted runner 直接可跑;
|
|
36
|
+
# feedscope probe 巡检需要真实登录态与 Chrome,只能挂 self-hosted runner 定时跑(见 README)
|
|
37
|
+
# coverage 门槛在 pyproject(fail_under=80,当前实测 ~89%):防倒退,新增模块应抬高它
|
|
38
|
+
|
|
39
|
+
build:
|
|
40
|
+
# 打包配置随时可构建:pyproject 的 packaging 回归在这里红,而不是发版当天才炸
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
- uses: astral-sh/setup-uv@v5
|
|
45
|
+
- run: uv build
|
|
46
|
+
- uses: actions/upload-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist-preview
|
|
49
|
+
path: dist/
|
|
50
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Release pipeline: push a tag `v*` → build → publish to PyPI (OIDC trusted
|
|
2
|
+
# publishing, no tokens) → GitHub Release with the artifacts.
|
|
3
|
+
#
|
|
4
|
+
# One-time setup lives in docs/release.md (PyPI pending publisher + the
|
|
5
|
+
# `pypi` GitHub environment). After that, a release is: changelog entry,
|
|
6
|
+
# version bump, `git tag vX.Y.Z && git push --tags`.
|
|
7
|
+
|
|
8
|
+
name: Release
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags:
|
|
13
|
+
- "v*"
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: astral-sh/setup-uv@v5
|
|
21
|
+
- run: uv build # sdist + wheel; version is read from src/feedscope/__init__.py
|
|
22
|
+
- uses: actions/upload-artifact@v4
|
|
23
|
+
with:
|
|
24
|
+
name: dist
|
|
25
|
+
path: dist/
|
|
26
|
+
if-no-files-found: error
|
|
27
|
+
|
|
28
|
+
publish-pypi:
|
|
29
|
+
needs: build
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment: pypi # create this environment once; see docs/release.md
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write # OIDC trusted publishing — no PyPI token stored anywhere
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/download-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
40
|
+
|
|
41
|
+
github-release:
|
|
42
|
+
needs: publish-pypi
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
permissions:
|
|
45
|
+
contents: write
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: dist
|
|
50
|
+
path: dist/
|
|
51
|
+
- uses: softprops/action-gh-release@v2
|
|
52
|
+
with:
|
|
53
|
+
files: dist/*
|
|
54
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-11-07
|
|
9
|
+
|
|
10
|
+
Initial release.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Multi-source aggregation in one terminal UI (`Tab` to switch): Zhihu Hot / Recommend / Following, X For You / Following, Reddit Home / Popular / custom subreddits, Hacker News Front / New / Ask / Show, V2EX Hot / Latest, Telegram channel subscriptions
|
|
15
|
+
- RSS subscriptions: any RSS / Atom feed; site-homepage feed auto-discovery; OPML import (idempotent merge) / export; "All" stream merged by publish time with cross-feed dedup; conditional requests (ETag / Last-Modified) keep refreshes polite to sources
|
|
16
|
+
- Three-layer in-terminal reading (feed → detail → discussion) loaded per layer on demand; HN-style deep threads flattened to two levels with `@parent` prefixes preserving who-replies-to-whom
|
|
17
|
+
- Login loop: in-app Chrome QR / password login with auto-refresh on completion, `Ctrl+U` to switch accounts; session-revocation detection that never leaves you guessing (X 401 semantics, Reddit `token_v2` JWT local decode — zero network, zero forgery)
|
|
18
|
+
- Resident browser-tab data channels for Zhihu / X / Reddit: TLS, header order and cookies fully consistent with the login session; X GraphQL queryIds self-heal by harvesting the current deployment's JS chunks; Reddit rides the website's own HTML channel with self-healing client version
|
|
19
|
+
- Auto Grok translation for foreign-language X posts; media rendered as clickable `[图片]` / `[视频]` markers linking to originals
|
|
20
|
+
- Bilingual UI (中文 / English), per-language TOML catalogs; a new language is one new TOML file, auto-discovered, enforced by key-parity and no-hardcoded-copy tests
|
|
21
|
+
- Source manager (`F2`): enable sources on demand, per-source feed `hide` config, subscription managers for RSS / Reddit / Telegram
|
|
22
|
+
- Editor-inspired VS Code Dark+ theme; every keybinding is a non-printing key (works while an IME is active); top tab bar and sidebar are clickable
|
|
23
|
+
- Layout-change self-check: 0 items parsed → immediate "site may have changed" error; `feedscope probe` inspects all sources with raw samples saved to disk; `feedscope doctor` environment self-check; `feedscope rss` CLI for subscription management
|
|
24
|
+
|
|
25
|
+
[0.1.0]: https://github.com/howardli/FeedScope/releases/tag/v0.1.0
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity
|
|
10
|
+
and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
|
18
|
+
community include:
|
|
19
|
+
|
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
+
and learning from the experience
|
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the
|
|
26
|
+
overall community
|
|
27
|
+
|
|
28
|
+
Examples of unacceptable behavior include:
|
|
29
|
+
|
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or
|
|
31
|
+
advances of any kind
|
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
+
* Public or private harassment
|
|
34
|
+
* Publishing others' private information, such as a physical or email
|
|
35
|
+
address, without their explicit permission
|
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
+
professional setting
|
|
38
|
+
|
|
39
|
+
## Enforcement Responsibilities
|
|
40
|
+
|
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
+
or harmful.
|
|
45
|
+
|
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
+
decisions when appropriate.
|
|
50
|
+
|
|
51
|
+
## Scope
|
|
52
|
+
|
|
53
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
+
an individual is officially representing the community in public spaces.
|
|
55
|
+
Examples of representing our community include using an official e-mail
|
|
56
|
+
address, posting via an official social media account, or acting as an appointed
|
|
57
|
+
representative at an online or offline event.
|
|
58
|
+
|
|
59
|
+
## Enforcement
|
|
60
|
+
|
|
61
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
+
reported to the community leaders responsible for enforcement — the maintainer
|
|
63
|
+
([@howardli](https://github.com/howardli)) via a private message on GitHub.
|
|
64
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
|
65
|
+
|
|
66
|
+
All community leaders are obligated to respect the privacy and security of the
|
|
67
|
+
reporter of any incident.
|
|
68
|
+
|
|
69
|
+
## Attribution
|
|
70
|
+
|
|
71
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
72
|
+
version 2.1, available at
|
|
73
|
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
74
|
+
|
|
75
|
+
Impact assessments and some wording are drawn from the [Baseline Code of
|
|
76
|
+
Conduct](https://baseathon.github.io/baseline-code-of-conduct/).
|
|
77
|
+
|
|
78
|
+
[homepage]: https://www.contributor-covenant.org
|
|
79
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Contributing to FeedScope
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in making FeedScope better. Bug reports, new sources,
|
|
4
|
+
new languages and doc improvements are all welcome — the two most natural
|
|
5
|
+
contributions for this codebase are **adding a source** and **adding a
|
|
6
|
+
language**, both designed to need almost no code outside their own files.
|
|
7
|
+
|
|
8
|
+
A note on language: the README is bilingual (English / 中文); code comments in
|
|
9
|
+
this repo are written in Chinese. Either language is fine in issues and PRs.
|
|
10
|
+
|
|
11
|
+
## Getting started
|
|
12
|
+
|
|
13
|
+
Prerequisites: Python 3.11+, [uv](https://docs.astral.sh/uv/), and a local
|
|
14
|
+
Google Chrome (the app drives it; no `playwright install` needed).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git clone https://github.com/howardli/FeedScope.git
|
|
18
|
+
cd FeedScope
|
|
19
|
+
uv sync # create the venv and install all dependencies
|
|
20
|
+
uv run feedscope # run the app from source
|
|
21
|
+
uv run pytest # run the full test suite — all offline, ~1 min
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The full quality gate (same as CI):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv run ruff check src/ tests/ scripts/
|
|
28
|
+
uv run mypy # src/ is typed and held at zero errors
|
|
29
|
+
uv run pytest tests/ --cov # coverage floor: 80%
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Project layout
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/feedscope/
|
|
36
|
+
core/ models, config, registry, i18n, text/wrap/timefmt, tree,
|
|
37
|
+
browser channel — the foundation everything else builds on
|
|
38
|
+
sources/ one package per site: zhihu / x / reddit / hn / v2ex / rss / telegram
|
|
39
|
+
ui/ Textual screens and widgets
|
|
40
|
+
cli.py argparse entry point (tui / login / probe / doctor / rss)
|
|
41
|
+
tests/ offline suite: fixture-driven parsers, TUI smoke via Textual Pilot,
|
|
42
|
+
plus architecture / i18n / registry guard tests
|
|
43
|
+
scripts/ screenshot.py (README images), demo_gif.py, x_probe.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Layering is enforced by `tests/test_architecture.py`, not just convention:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
ui → top-level business (cli / login / subscriptions / probe / doctor / report)
|
|
50
|
+
→ sources → core
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Dependencies only point downward; `parse.py` modules are pure leaves (no IO);
|
|
54
|
+
UI never reads source-private keys. If the guard blocks you, read its
|
|
55
|
+
docstring — the exceptions are documented there.
|
|
56
|
+
|
|
57
|
+
## Adding a new source
|
|
58
|
+
|
|
59
|
+
The highest-value contribution. The architecture is plugin-shaped: a source is
|
|
60
|
+
one package under `src/feedscope/sources/`, registered by a single import —
|
|
61
|
+
UI and core need zero changes.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
sources/<id>/
|
|
65
|
+
fetch.py data layer: get raw data (httpx / resident browser tab /
|
|
66
|
+
DOM extraction); incremental loaders that hold resources live
|
|
67
|
+
here. No UI.
|
|
68
|
+
parse.py parsing layer: raw dict/HTML → FeedItem / Answer / DetailDoc /
|
|
69
|
+
markdown. Pure functions, directly testable against fixtures.
|
|
70
|
+
No network, no IO — it is the bottommost leaf.
|
|
71
|
+
__init__.py orchestration: the @register'd Source subclass — capability
|
|
72
|
+
declaration and open_* dispatch. First-screen guard via
|
|
73
|
+
require_feed_items; login gate via core.source.login_gate
|
|
74
|
+
if the site needs one.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Step by step:
|
|
78
|
+
|
|
79
|
+
1. **Read an existing source closest to your target shape.** RSS / Telegram
|
|
80
|
+
are subscription-style; HN / Reddit are three-layer with discussion trees;
|
|
81
|
+
V2EX is two-layer. `sources/__init__.py`'s docstring is the authoritative
|
|
82
|
+
template.
|
|
83
|
+
2. **Write `parse.py` first** and drive it with saved fixtures
|
|
84
|
+
(`tests/test_<id>_parse.py`). This is where most of the logic should live,
|
|
85
|
+
because it is trivially testable offline.
|
|
86
|
+
3. **Write `fetch.py`**: use `core/http.py` for plain HTTP, or the resident
|
|
87
|
+
browser-tab channel (`core/browser.py`) when the site blocks non-browser
|
|
88
|
+
fingerprints — see the Reddit / X sources for the pattern.
|
|
89
|
+
4. **Write the `Source` subclass** in `__init__.py` with its capability flags
|
|
90
|
+
(`supports_detail`, `supports_comments`, `subscribable`, `feeds`,
|
|
91
|
+
`feed_labels`, `detail_kinds`). Inherit the shared skeletons instead of
|
|
92
|
+
rewriting mechanics:
|
|
93
|
+
- subscription-style → `core.source.SubscribedSource` (the "All" stream's
|
|
94
|
+
error handling comes free; inject `_fetch_merged` / `_open_one`)
|
|
95
|
+
- detail fetched as one full tree → `SliceDetailLoader` (floor / reply
|
|
96
|
+
paging = in-memory slicing, zero code)
|
|
97
|
+
- body carried by the list → `StaticDetailLoader` (opening a detail costs
|
|
98
|
+
zero requests)
|
|
99
|
+
- deep discussion flattening → `core.tree` (`@parent` prefix rule has a
|
|
100
|
+
single implementation)
|
|
101
|
+
5. **Register it** by adding one import line to `sources/__init__.py`, in
|
|
102
|
+
**alphabetical order by `source_id`** — import order = source-picker row
|
|
103
|
+
order = default Tab order. `tests/test_source_caps.py` and
|
|
104
|
+
`tests/test_registry_order_is_tab_order` go red if you get this wrong.
|
|
105
|
+
6. **All UI copy through i18n** (`core/i18n.t()`) — never hardcode user-visible
|
|
106
|
+
strings; `tests/test_i18n_no_hardcoded.py` blocks it in CI. Source
|
|
107
|
+
vocabulary words (unit labels like "回复" / "comment") go through the same
|
|
108
|
+
catalogs.
|
|
109
|
+
7. **Run the full gate** (see above) and add a caps test entry if the source
|
|
110
|
+
introduces a new capability combination.
|
|
111
|
+
|
|
112
|
+
## Adding a language
|
|
113
|
+
|
|
114
|
+
Copy `src/feedscope/core/i18n/en.toml` to `<code>.toml` (e.g. `ja.toml`), set
|
|
115
|
+
the native name in `[meta]`, translate the messages. The language picker
|
|
116
|
+
discovers it automatically — zero code changes. `tests/test_i18n.py` enforces
|
|
117
|
+
an identical key tree across catalogs, so a missing translation fails CI.
|
|
118
|
+
|
|
119
|
+
## UI changes → refresh the images
|
|
120
|
+
|
|
121
|
+
README screenshots and the demo GIF are generated from the real app driven by
|
|
122
|
+
offline stub sources, so they can never drift from the code:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
uv run python scripts/screenshot.py # README stills → docs/*.png
|
|
126
|
+
uv run --with pillow python scripts/demo_gif.py # demo GIF → docs/demo.gif
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If your change affects what's on screen, re-run the relevant script and commit
|
|
130
|
+
the images together with the code.
|
|
131
|
+
|
|
132
|
+
## Pull requests
|
|
133
|
+
|
|
134
|
+
- Keep PRs focused; one logical change each
|
|
135
|
+
- New behavior needs tests (offline; fixture-driven — look at existing
|
|
136
|
+
`tests/test_*_parse.py` for the pattern)
|
|
137
|
+
- User-facing changes get a `CHANGELOG.md` entry under `[Unreleased]`
|
|
138
|
+
- CI (ruff + mypy + pytest on Python 3.11–3.13, macOS & Linux) must be green
|
|
139
|
+
- Match the existing style: Chinese comments that explain *why*, ruff's
|
|
140
|
+
line-length 100 in `src/`
|
|
141
|
+
|
|
142
|
+
Thank you! 🎉
|
feedscope-0.1.0/LICENSE
ADDED
|
@@ -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.
|