douban-skill
Export and sync Douban (豆瓣) book/movie/music/game collections to local CSV files via Frodo API. Supports full export (all history) and RSS incremental sync (recent items). Use when the user wants to export Douban reading/watching/listening/gaming history, back up their Douban data, set up incremental sync, or mentions 豆瓣/douban collections. Triggers on: 豆瓣, douban, 读书记录, 观影记录, 书影音, 导出豆瓣.
1How It Works & Mechanism
Bypassing the web front-end as the primary route: Douban's web interface employs PoW (Proof of Work) anti-scraping mechanisms, and the author confirmed that 7 different scraping approaches all failed. Only the Frodo API (official mobile app backend) remains viable—the core reason this skill works is direct communication with the mobile API.
Douban uses PoW (Proof of Work) challenges on web pages, blocking all HTTP scraping. We tested 7 approaches — only the Frodo API works. **Do NOT attempt** web scraping
Login-free signature for Frodo requests: The Python script sends GET requests to https://frodo.douban.com/api/v2/user/{id}/interests with query parameters apiKey/_ts/_sig/os_rom; _sig is the HMAC-SHA1 of 'GET&URL-encoded-path×tamp' (using a hardcoded secret), with request headers spoofed as the Douban Android App.
raw = '&'.join(['GET', urllib.parse.quote(url_path, safe=''), timestamp])
Transparent credential strategy: The hardcoded API_KEY and HMAC_SECRET in the script are public credentials from the Douban Android APK, shared among all app users without identifying individuals; .gitleaks.toml explicitly whitelists these two matches as false positives.
API_KEY = '0dad551ec0f84ed02907ff5c42e8ec70' HMAC_SECRET = b'bf7dddc7c9cfe6f7' BASE_URL = 'https://frodo.douban.com'
Full export logic: Iterates through combinations across 4 categories (book/movie/music/game) × 3 statuses (done/doing/mark) with paginated requests. Termination is determined by total rather than page size (to handle short pages caused by delisted items), with rate limiting at 1.5s/page + 2s/category + retry backoff, ultimately writing aggregated output CSV files.
# --- Rate Limiting --- # 1.5s between pages, 2s between categories. Tested with 1200+ items. PAGE_DELAY = 1.5 CATEGORY_DELAY = 2.0 ITEMS_PER_PAGE = 50 MAX_PAGES_SAFETY = 500 # Guard against infinite pagination loops
Incremental sync via RSS: douban-rss-sync.mjs fetches https://www.douban.com/feed/people/{id}/interests (~recent 10 items, no pagination), utilizing ~/.douban-rss-state.json state along with CSV row deduplication to perform daily incremental syncs.
const RSS_URL = `https://www.douban.com/feed/people/${DOUBAN_USER}/interests`;
Excel-compatible CSV output: UTF-8 BOM (utf-8-sig) format with fixed columns title/url/date/rating/status/comment, rating converted to a ★ star string, and date formatted strictly as YYYY-MM-DD.
def write_csv(filepath, rows): """Write rows to a CSV file with UTF-8 BOM for Excel compatibility.""" with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| api | Douban Frodo API(官方移动 App 后端,HMAC-SHA1 签名 + 硬编码公开 app 凭证 + 伪装 App UA) |
| network | Douban RSS 订阅源(增量同步) |
| network | subject URL 前缀(book/movie/music/game 域名,仅写入 CSV 作为链接,不请求) |
4Recommended Governance & Evidence
- scripts/: Execute local script or command: scripts/douban-frodo-export.py (python3 or uv run)
- scripts/ 或 SKILL.md: Declares dynamic installation of external language package dependencies
- 网络端点: Declares external network access: GET https://frodo.douban.com/api/v2/user/{user_id}/interests?type=…&status=
- 本地工作目录: File read/write and artifact output: ~/Downloads/douban-sync/<user_id>/{书,影视,音乐,游戏}.csv (UTF-8 BOM; DOUBAN_OUTPUT_
5Second-Pass Verification
- [ok] Frodo 端点与签名实现真实存在 — fetch_json 用 urllib.request.urlopen 请求 f'{BASE_URL}{api_path}?{query}';compute_signature 只签 urlencoded path;apiKey/_ts/_sig/os_rom 参数逐字在 preflight_check/fetch_all_interests 中。
- [ok] 硬编码凭证属性(公共 APK 凭证非个人密钥) — 脚本注释 + .gitleaks.toml allowlist(regexes 列两个值)互相印证:'the Frodo API key and HMAC secret are the PUBLIC Android APK credentials';脚本不读任何个人凭证。
- [ok] 分页判停按 total 而非页大小(历史 bug 已修) — fetch_all_interests 中 'if len(all_items) >= total: break' 与 troubleshooting.md 的 639→499 教训记录一致;MAX_PAGES_SAFETY=500 保险存在。
- [ok] RSS 增量与去重逻辑 — douban-rss-sync.mjs 的 RSS_URL、STATE_FILE (.douban-rss-state.json)、isAlreadyInFile 去重、CSV_HEADER BOM 前缀 '' 逐字可查。
- [ok] 注入面反查:user_id 白名单双实现 — python:re.match(r'^[A-Za-z0-9._-]+$', user_id) 后 sys.exit(1);mjs:同正则校验 + douban.com/people/ URL 提取;两处都在发起请求前。
- [ok] 网络端点清单无漏网 — 全目录扫描仅 frodo.douban.com(运行时)与 www.douban.com(RSS);subject URL 前缀仅拼 CSV 链接;无遥测/无第三方回调。
- [ok] 功能声明 vs 实际能力夸大检查 — description 的每项能力(全量导出/RSS 增量/CSV BOM/免登录/preflight)都有代码行兑现;限制清单(长评笔记广播不导出等)与实现一致(interests API 无评论正文导出)。
- [ok] 元数据(commit/license/stars) — 本地 HEAD == pin d5c4678cb5d4fd6acc9c922690df035dbd33d247;GitHub API:MIT、stars 1385(2026-09-09 实采)、pushed_at 2026-09-09T12:33:29Z。
6Verdict & Scenarios
afd320841b31f4f4…