Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Douban CLI - browse movies, TV shows, books, and personal collections from the t
- `list` 热门豆列

### Auth & Social / 登录与社交
- `login` 登录并缓存 Cookie
- `login` 登录并缓存 Cookie(支持 `--cookie` 手动导入,详见下方「登录方式」)
- `whoami` 查看当前登录用户
- `logout` 清除本地登录态
- `mark` 标记想看/看过/在看
Expand Down Expand Up @@ -118,6 +118,29 @@ douban whoami
douban logout
```

#### 登录方式

`douban login` 默认尝试自动登录(打开浏览器 → 提取 Cookie)。在 macOS 上,自动提取豆瓣 Cookie 需授予 keychain 权限以解密豆瓣网的 cookie,常因钥匙串/磁盘权限失败而抓取不到登录态。若不授权 keychain,可选用以下方式替代。

**1. 手动导入 Cookie**

```bash
# 1) Netscape cookies.txt 文件(用「Get cookies.txt LOCALLY」等扩展导出整站 cookie)
douban login --cookie www.douban.com_cookies.txt

# 2) 或直接传 Cookie 字符串(F12 → Network → 请求头里的 cookie 整行复制)
douban login --cookie "dbcl2=xxxxx"
```

请确认 Cookie 中包含 `dbcl2` 这一项。

**2. 使用 puppeteer 登录**。为避免 ~300MB Chromium 下载,puppeteer 没有加入 `package.json` 。如需浏览器驱动的自动登录,可自行安装:

```bash
npm i puppeteer
douban login # 安装后会自动走 puppeteer 自动登录流程
```

### 标记/评分/评论(需登录)

```bash
Expand Down
65 changes: 65 additions & 0 deletions src/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { parseCookieHeader, parseNetscapeCookies } from '../auth.ts';

describe('auth.parseCookieHeader', () => {
it('从完整 Cookie 字符串中提取 dbcl2 与 ck', () => {
const result = parseCookieHeader('bid=abc; dbcl2="123456789"; ck=ABCD; ll="en"');
expect(result).toEqual({ dbcl2: '123456789', ck: 'ABCD' });
});

it('缺少 ck 时仅返回 dbcl2', () => {
const result = parseCookieHeader('dbcl2=998877');
expect(result).toEqual({ dbcl2: '998877' });
expect(result?.ck).toBeUndefined();
});

it('去掉 dbcl2 值两端的引号', () => {
const result = parseCookieHeader('dbcl2="quoted_value"');
expect(result?.dbcl2).toBe('quoted_value');
});

it('缺少 dbcl2 返回 null', () => {
expect(parseCookieHeader('bid=abc; ck=ABCD')).toBeNull();
});

it('空字符串返回 null', () => {
expect(parseCookieHeader('')).toBeNull();
});

it('容忍整行请求头的 Cookie: 前缀', () => {
const result = parseCookieHeader('Cookie: dbcl2="ajksdf"; ck=ABCD');
expect(result).toEqual({ dbcl2: 'ajksdf', ck: 'ABCD' });
});
});

describe('auth.parseNetscapeCookies', () => {
it('从 Netscape cookies.txt 提取 dbcl2 与 ck', () => {
const content = [
'# Netscape HTTP Cookie File',
'',
'.douban.com\tTRUE\t/\tFALSE\t1800000000\tbid\tfakeBidValue123',
'.douban.com\tTRUE\t/\tFALSE\t1800000000\tdbcl2\t"100000001:FAKE_TOKEN_123"',
'.douban.com\tTRUE\t/\tFALSE\t0\tck\tfakeCKvalue'
].join('\n');
expect(parseNetscapeCookies(content)).toEqual({ dbcl2: '100000001:FAKE_TOKEN_123', ck: 'fakeCKvalue' });
});

it('兼容 #HttpOnly_ 前缀的行(不是注释)', () => {
const content = '#HttpOnly_.douban.com\tTRUE\t/\tTRUE\t1800000000\tdbcl2\t"abc:def"';
expect(parseNetscapeCookies(content)).toEqual({ dbcl2: 'abc:def' });
});

it('去掉值两端的引号', () => {
const content = '.douban.com\tTRUE\t/\tFALSE\t0\tdbcl2\t"quoted_token"';
expect(parseNetscapeCookies(content)?.dbcl2).toBe('quoted_token');
});

it('缺少 dbcl2 返回 null', () => {
const content = '.douban.com\tTRUE\t/\tFALSE\t0\tck\tABCD';
expect(parseNetscapeCookies(content)).toBeNull();
});

it('空内容返回 null', () => {
expect(parseNetscapeCookies('')).toBeNull();
});
});
Loading