@jackwener/opencli 1.4.1 → 1.5.1
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.
- package/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +21 -1
- package/README.md +35 -6
- package/README.zh-CN.md +12 -5
- package/SKILL.md +2 -0
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/cdp.js +5 -0
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/browser/page.d.ts +3 -0
- package/dist/browser/page.js +24 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1567 -108
- package/dist/cli.js +68 -6
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/bluesky/feeds.yaml +29 -0
- package/dist/clis/bluesky/followers.yaml +33 -0
- package/dist/clis/bluesky/following.yaml +33 -0
- package/dist/clis/bluesky/profile.yaml +27 -0
- package/dist/clis/bluesky/search.yaml +34 -0
- package/dist/clis/bluesky/starter-packs.yaml +34 -0
- package/dist/clis/bluesky/thread.yaml +32 -0
- package/dist/clis/bluesky/trending.yaml +27 -0
- package/dist/clis/bluesky/user.yaml +34 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +2 -1
- package/dist/clis/twitter/search.test.js +2 -0
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/twitter/trending.js +29 -61
- package/dist/clis/v2ex/hot.yaml +17 -3
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +179 -47
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +131 -0
- package/dist/clis/xiaohongshu/search.d.ts +8 -1
- package/dist/clis/xiaohongshu/search.js +20 -1
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
- package/dist/clis/xiaohongshu/search.test.js +32 -1
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +48 -42
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +11 -4
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/engine.test.js +42 -0
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +17 -34
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +66 -8
- package/dist/execution.test.d.ts +1 -0
- package/dist/execution.test.js +40 -0
- package/dist/external.js +6 -1
- package/dist/hooks.js +2 -0
- package/dist/main.js +6 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin-scaffold.d.ts +28 -0
- package/dist/plugin-scaffold.js +142 -0
- package/dist/plugin-scaffold.test.d.ts +4 -0
- package/dist/plugin-scaffold.test.js +83 -0
- package/dist/plugin.d.ts +82 -11
- package/dist/plugin.js +870 -84
- package/dist/plugin.test.js +1032 -17
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +2 -2
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +3 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/bluesky.md +53 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +6 -2
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +97 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/package.json +1 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +2 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +8 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/browser/page.ts +24 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +69 -6
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/bluesky/feeds.yaml +29 -0
- package/src/clis/bluesky/followers.yaml +33 -0
- package/src/clis/bluesky/following.yaml +33 -0
- package/src/clis/bluesky/profile.yaml +27 -0
- package/src/clis/bluesky/search.yaml +34 -0
- package/src/clis/bluesky/starter-packs.yaml +34 -0
- package/src/clis/bluesky/thread.yaml +32 -0
- package/src/clis/bluesky/trending.yaml +27 -0
- package/src/clis/bluesky/user.yaml +34 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +2 -0
- package/src/clis/twitter/search.ts +3 -1
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/twitter/trending.ts +29 -77
- package/src/clis/v2ex/hot.yaml +17 -3
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +151 -0
- package/src/clis/xiaohongshu/publish.ts +206 -54
- package/src/clis/xiaohongshu/search.test.ts +39 -1
- package/src/clis/xiaohongshu/search.ts +19 -1
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +49 -48
- package/src/doctor.ts +15 -5
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/engine.test.ts +38 -0
- package/src/errors.ts +26 -63
- package/src/execution.test.ts +47 -0
- package/src/execution.ts +67 -9
- package/src/external.ts +6 -1
- package/src/hooks.ts +1 -0
- package/src/main.ts +7 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin-scaffold.test.ts +98 -0
- package/src/plugin-scaffold.ts +170 -0
- package/src/plugin.test.ts +1104 -17
- package/src/plugin.ts +1101 -86
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +3 -3
- package/src/serialization.ts +4 -0
- package/src/types.ts +3 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +1 -1
- package/tests/e2e/browser-public.test.ts +62 -4
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +10 -0
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getRegistry } from '../../registry.js';
|
|
4
|
+
const { mockHttpDownload, mockLoadDoubanSubjectPhotos, mockMkdirSync } = vi.hoisted(() => ({
|
|
5
|
+
mockHttpDownload: vi.fn(),
|
|
6
|
+
mockLoadDoubanSubjectPhotos: vi.fn(),
|
|
7
|
+
mockMkdirSync: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../../download/index.js', () => ({
|
|
10
|
+
httpDownload: mockHttpDownload,
|
|
11
|
+
sanitizeFilename: vi.fn((value) => value.replace(/\s+/g, '_')),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('./utils.js', async () => {
|
|
14
|
+
const actual = await vi.importActual('./utils.js');
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
loadDoubanSubjectPhotos: mockLoadDoubanSubjectPhotos,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
vi.mock('../../download/progress.js', () => ({
|
|
21
|
+
formatBytes: vi.fn((size) => `${size} B`),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('node:fs', () => ({
|
|
24
|
+
mkdirSync: mockMkdirSync,
|
|
25
|
+
}));
|
|
26
|
+
await import('./download.js');
|
|
27
|
+
let cmd;
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
cmd = getRegistry().get('douban/download');
|
|
30
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
31
|
+
});
|
|
32
|
+
function toPosixPath(value) {
|
|
33
|
+
return value.replaceAll(path.sep, '/');
|
|
34
|
+
}
|
|
35
|
+
describe('douban download', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
mockHttpDownload.mockReset();
|
|
38
|
+
mockLoadDoubanSubjectPhotos.mockReset();
|
|
39
|
+
mockMkdirSync.mockReset();
|
|
40
|
+
});
|
|
41
|
+
it('downloads douban poster images and merges metadata into the result', async () => {
|
|
42
|
+
const page = {};
|
|
43
|
+
mockLoadDoubanSubjectPhotos.mockResolvedValue({
|
|
44
|
+
subjectId: '30382501',
|
|
45
|
+
subjectTitle: 'The Wandering Earth 2',
|
|
46
|
+
type: 'Rb',
|
|
47
|
+
photos: [
|
|
48
|
+
{
|
|
49
|
+
index: 1,
|
|
50
|
+
photoId: '2913450214',
|
|
51
|
+
title: 'Main poster',
|
|
52
|
+
imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450214.webp',
|
|
53
|
+
thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450214.webp',
|
|
54
|
+
detailUrl: 'https://movie.douban.com/photos/photo/2913450214/',
|
|
55
|
+
page: 1,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
index: 2,
|
|
59
|
+
photoId: '2913450215',
|
|
60
|
+
title: 'Character poster',
|
|
61
|
+
imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
|
|
62
|
+
thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
|
|
63
|
+
detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
|
|
64
|
+
page: 1,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
mockHttpDownload
|
|
69
|
+
.mockResolvedValueOnce({ success: true, size: 1200 })
|
|
70
|
+
.mockResolvedValueOnce({ success: true, size: 980 });
|
|
71
|
+
const result = await cmd.func(page, {
|
|
72
|
+
id: '30382501',
|
|
73
|
+
type: 'Rb',
|
|
74
|
+
limit: 20,
|
|
75
|
+
output: '/tmp/douban-test',
|
|
76
|
+
});
|
|
77
|
+
expect(mockLoadDoubanSubjectPhotos).toHaveBeenCalledWith(page, '30382501', {
|
|
78
|
+
type: 'Rb',
|
|
79
|
+
limit: 20,
|
|
80
|
+
});
|
|
81
|
+
expect(mockMkdirSync).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(toPosixPath(mockMkdirSync.mock.calls[0][0])).toBe('/tmp/douban-test/30382501');
|
|
83
|
+
expect(mockMkdirSync.mock.calls[0][1]).toEqual({ recursive: true });
|
|
84
|
+
expect(mockHttpDownload).toHaveBeenCalledTimes(2);
|
|
85
|
+
expect(mockHttpDownload.mock.calls[0]?.[0]).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp');
|
|
86
|
+
expect(toPosixPath(mockHttpDownload.mock.calls[0]?.[1])).toBe('/tmp/douban-test/30382501/30382501_001_2913450214_Main_poster.webp');
|
|
87
|
+
expect(mockHttpDownload.mock.calls[0]?.[2]).toEqual(expect.objectContaining({
|
|
88
|
+
headers: { Referer: 'https://movie.douban.com/photos/photo/2913450214/' },
|
|
89
|
+
timeout: 60000,
|
|
90
|
+
}));
|
|
91
|
+
expect(mockHttpDownload.mock.calls[1]?.[0]).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg');
|
|
92
|
+
expect(toPosixPath(mockHttpDownload.mock.calls[1]?.[1])).toBe('/tmp/douban-test/30382501/30382501_002_2913450215_Character_poster.jpg');
|
|
93
|
+
expect(mockHttpDownload.mock.calls[1]?.[2]).toEqual(expect.objectContaining({
|
|
94
|
+
headers: { Referer: 'https://movie.douban.com/photos/photo/2913450215/' },
|
|
95
|
+
timeout: 60000,
|
|
96
|
+
}));
|
|
97
|
+
expect(result).toEqual([
|
|
98
|
+
{
|
|
99
|
+
index: 1,
|
|
100
|
+
title: 'Main poster',
|
|
101
|
+
photo_id: '2913450214',
|
|
102
|
+
image_url: 'https://img1.doubanio.com/view/photo/l/public/p2913450214.webp',
|
|
103
|
+
detail_url: 'https://movie.douban.com/photos/photo/2913450214/',
|
|
104
|
+
status: 'success',
|
|
105
|
+
size: '1200 B',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
index: 2,
|
|
109
|
+
title: 'Character poster',
|
|
110
|
+
photo_id: '2913450215',
|
|
111
|
+
image_url: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
|
|
112
|
+
detail_url: 'https://movie.douban.com/photos/photo/2913450215/',
|
|
113
|
+
status: 'success',
|
|
114
|
+
size: '980 B',
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
it('downloads only the requested photo when photo-id is provided', async () => {
|
|
119
|
+
const page = {};
|
|
120
|
+
mockLoadDoubanSubjectPhotos.mockResolvedValue({
|
|
121
|
+
subjectId: '30382501',
|
|
122
|
+
subjectTitle: 'The Wandering Earth 2',
|
|
123
|
+
type: 'Rb',
|
|
124
|
+
photos: [
|
|
125
|
+
{
|
|
126
|
+
index: 2,
|
|
127
|
+
photoId: '2913450215',
|
|
128
|
+
title: 'Character poster',
|
|
129
|
+
imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
|
|
130
|
+
thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
|
|
131
|
+
detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
|
|
132
|
+
page: 1,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
mockHttpDownload.mockResolvedValueOnce({ success: true, size: 980 });
|
|
137
|
+
const result = await cmd.func(page, {
|
|
138
|
+
id: '30382501',
|
|
139
|
+
type: 'Rb',
|
|
140
|
+
'photo-id': '2913450215',
|
|
141
|
+
output: '/tmp/douban-test',
|
|
142
|
+
});
|
|
143
|
+
expect(mockLoadDoubanSubjectPhotos).toHaveBeenCalledWith(page, '30382501', {
|
|
144
|
+
type: 'Rb',
|
|
145
|
+
targetPhotoId: '2913450215',
|
|
146
|
+
});
|
|
147
|
+
expect(mockHttpDownload).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(mockHttpDownload.mock.calls[0]?.[0]).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg');
|
|
149
|
+
expect(toPosixPath(mockHttpDownload.mock.calls[0]?.[1])).toBe('/tmp/douban-test/30382501/30382501_002_2913450215_Character_poster.jpg');
|
|
150
|
+
expect(mockHttpDownload.mock.calls[0]?.[2]).toEqual(expect.objectContaining({
|
|
151
|
+
headers: { Referer: 'https://movie.douban.com/photos/photo/2913450215/' },
|
|
152
|
+
timeout: 60000,
|
|
153
|
+
}));
|
|
154
|
+
expect(result).toEqual([
|
|
155
|
+
{
|
|
156
|
+
index: 2,
|
|
157
|
+
title: 'Character poster',
|
|
158
|
+
photo_id: '2913450215',
|
|
159
|
+
image_url: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
|
|
160
|
+
detail_url: 'https://movie.douban.com/photos/photo/2913450215/',
|
|
161
|
+
status: 'success',
|
|
162
|
+
size: '980 B',
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
it('rejects invalid subject ids before attempting browser work', async () => {
|
|
167
|
+
await expect(cmd.func({}, { id: 'movie-30382501' })).rejects.toThrow('Invalid Douban subject ID');
|
|
168
|
+
expect(mockHttpDownload).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { loadDoubanSubjectPhotos, normalizeDoubanSubjectId } from './utils.js';
|
|
3
|
+
cli({
|
|
4
|
+
site: 'douban',
|
|
5
|
+
name: 'photos',
|
|
6
|
+
description: '获取电影海报/剧照图片列表',
|
|
7
|
+
domain: 'movie.douban.com',
|
|
8
|
+
strategy: Strategy.COOKIE,
|
|
9
|
+
args: [
|
|
10
|
+
{ name: 'id', positional: true, required: true, help: '电影 subject ID' },
|
|
11
|
+
{ name: 'type', default: 'Rb', help: '豆瓣 photos 的 type 参数,默认 Rb(海报)' },
|
|
12
|
+
{ name: 'limit', type: 'int', default: 120, help: '最多返回多少张图片' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['index', 'title', 'image_url', 'detail_url'],
|
|
15
|
+
func: async (page, kwargs) => {
|
|
16
|
+
const subjectId = normalizeDoubanSubjectId(String(kwargs.id || ''));
|
|
17
|
+
const data = await loadDoubanSubjectPhotos(page, subjectId, {
|
|
18
|
+
type: String(kwargs.type || 'Rb'),
|
|
19
|
+
limit: Number(kwargs.limit) || 120,
|
|
20
|
+
});
|
|
21
|
+
return data.photos.map((photo) => ({
|
|
22
|
+
subject_id: data.subjectId,
|
|
23
|
+
subject_title: data.subjectTitle,
|
|
24
|
+
type: data.type,
|
|
25
|
+
index: photo.index,
|
|
26
|
+
photo_id: photo.photoId,
|
|
27
|
+
title: photo.title,
|
|
28
|
+
image_url: photo.imageUrl,
|
|
29
|
+
thumb_url: photo.thumbUrl,
|
|
30
|
+
detail_url: photo.detailUrl,
|
|
31
|
+
page: photo.page,
|
|
32
|
+
}));
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
* Douban adapter utilities.
|
|
3
3
|
*/
|
|
4
4
|
import type { IPage } from '../../types.js';
|
|
5
|
+
export interface DoubanSubjectPhoto {
|
|
6
|
+
index: number;
|
|
7
|
+
photoId: string;
|
|
8
|
+
title: string;
|
|
9
|
+
imageUrl: string;
|
|
10
|
+
thumbUrl: string;
|
|
11
|
+
detailUrl: string;
|
|
12
|
+
page: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DoubanSubjectPhotosResult {
|
|
15
|
+
subjectId: string;
|
|
16
|
+
subjectTitle: string;
|
|
17
|
+
type: string;
|
|
18
|
+
photos: DoubanSubjectPhoto[];
|
|
19
|
+
}
|
|
20
|
+
export interface LoadDoubanSubjectPhotosOptions {
|
|
21
|
+
type?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
targetPhotoId?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function normalizeDoubanSubjectId(subjectId: string): string;
|
|
26
|
+
export declare function promoteDoubanPhotoUrl(url: string, size?: 's' | 'm' | 'l'): string;
|
|
27
|
+
export declare function resolveDoubanPhotoAssetUrl(candidates: Array<string | null | undefined>, baseUrl?: string): string;
|
|
28
|
+
export declare function getDoubanPhotoExtension(url: string): string;
|
|
29
|
+
export declare function loadDoubanSubjectPhotos(page: IPage, subjectId: string, options?: LoadDoubanSubjectPhotosOptions): Promise<DoubanSubjectPhotosResult>;
|
|
5
30
|
export declare function loadDoubanBookHot(page: IPage, limit: number): Promise<any[]>;
|
|
6
31
|
export declare function loadDoubanMovieHot(page: IPage, limit: number): Promise<any[]>;
|
|
7
32
|
export declare function searchDouban(page: IPage, type: string, keyword: string, limit: number): Promise<any[]>;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Douban adapter utilities.
|
|
3
3
|
*/
|
|
4
|
-
import { CliError } from '../../errors.js';
|
|
4
|
+
import { ArgumentError, CliError, EmptyResultError } from '../../errors.js';
|
|
5
|
+
const DOUBAN_PHOTO_PAGE_SIZE = 30;
|
|
6
|
+
const MAX_DOUBAN_PHOTOS = 500;
|
|
5
7
|
function clampLimit(limit) {
|
|
6
8
|
return Math.max(1, Math.min(limit || 20, 50));
|
|
7
9
|
}
|
|
10
|
+
function clampPhotoLimit(limit) {
|
|
11
|
+
return Math.max(1, Math.min(limit || 120, MAX_DOUBAN_PHOTOS));
|
|
12
|
+
}
|
|
8
13
|
async function ensureDoubanReady(page) {
|
|
9
14
|
const state = await page.evaluate(`
|
|
10
15
|
(() => {
|
|
@@ -18,6 +23,190 @@ async function ensureDoubanReady(page) {
|
|
|
18
23
|
throw new CliError('AUTH_REQUIRED', 'Douban requires a logged-in browser session before these commands can load data.', 'Please sign in to douban.com in the browser that opencli reuses, then rerun the command.');
|
|
19
24
|
}
|
|
20
25
|
}
|
|
26
|
+
export function normalizeDoubanSubjectId(subjectId) {
|
|
27
|
+
const normalized = String(subjectId || '').trim();
|
|
28
|
+
if (!/^\d+$/.test(normalized)) {
|
|
29
|
+
throw new ArgumentError(`Invalid Douban subject ID: ${subjectId}`);
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
export function promoteDoubanPhotoUrl(url, size = 'l') {
|
|
34
|
+
const normalized = String(url || '').trim();
|
|
35
|
+
if (!normalized)
|
|
36
|
+
return '';
|
|
37
|
+
if (/^[a-z]+:/i.test(normalized) && !/^https?:/i.test(normalized))
|
|
38
|
+
return '';
|
|
39
|
+
return normalized.replace(/\/view\/photo\/[^/]+\/public\//, `/view/photo/${size}/public/`);
|
|
40
|
+
}
|
|
41
|
+
export function resolveDoubanPhotoAssetUrl(candidates, baseUrl = '') {
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
const normalized = String(candidate || '').trim();
|
|
44
|
+
if (!normalized)
|
|
45
|
+
continue;
|
|
46
|
+
let resolved = normalized;
|
|
47
|
+
try {
|
|
48
|
+
resolved = baseUrl
|
|
49
|
+
? new URL(normalized, baseUrl).toString()
|
|
50
|
+
: new URL(normalized).toString();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
resolved = normalized;
|
|
54
|
+
}
|
|
55
|
+
if (/^https?:\/\//i.test(resolved)) {
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
export function getDoubanPhotoExtension(url) {
|
|
62
|
+
const normalized = String(url || '').trim();
|
|
63
|
+
if (!normalized)
|
|
64
|
+
return '.jpg';
|
|
65
|
+
try {
|
|
66
|
+
const ext = new URL(normalized).pathname.match(/\.(jpe?g|png|gif|webp|avif|bmp)$/i)?.[0];
|
|
67
|
+
return ext || '.jpg';
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
const ext = normalized.match(/\.(jpe?g|png|gif|webp|avif|bmp)(?:$|[?#])/i)?.[0];
|
|
71
|
+
return ext ? ext.replace(/[?#].*$/, '') : '.jpg';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function loadDoubanSubjectPhotos(page, subjectId, options = {}) {
|
|
75
|
+
const normalizedId = normalizeDoubanSubjectId(subjectId);
|
|
76
|
+
const type = String(options.type || 'Rb').trim() || 'Rb';
|
|
77
|
+
const targetPhotoId = String(options.targetPhotoId || '').trim();
|
|
78
|
+
const safeLimit = targetPhotoId ? Number.MAX_SAFE_INTEGER : clampPhotoLimit(Number(options.limit) || 120);
|
|
79
|
+
const resolvePhotoAssetUrlSource = resolveDoubanPhotoAssetUrl.toString();
|
|
80
|
+
const galleryUrl = `https://movie.douban.com/subject/${normalizedId}/photos?type=${encodeURIComponent(type)}`;
|
|
81
|
+
await page.goto(galleryUrl);
|
|
82
|
+
await page.wait(2);
|
|
83
|
+
await ensureDoubanReady(page);
|
|
84
|
+
const data = await page.evaluate(`
|
|
85
|
+
(async () => {
|
|
86
|
+
const subjectId = ${JSON.stringify(normalizedId)};
|
|
87
|
+
const type = ${JSON.stringify(type)};
|
|
88
|
+
const limit = ${safeLimit};
|
|
89
|
+
const targetPhotoId = ${JSON.stringify(targetPhotoId)};
|
|
90
|
+
const pageSize = ${DOUBAN_PHOTO_PAGE_SIZE};
|
|
91
|
+
const resolveDoubanPhotoAssetUrl = ${resolvePhotoAssetUrlSource};
|
|
92
|
+
|
|
93
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
94
|
+
const toAbsoluteUrl = (value) => {
|
|
95
|
+
if (!value) return '';
|
|
96
|
+
try {
|
|
97
|
+
return new URL(value, location.origin).toString();
|
|
98
|
+
} catch {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const promotePhotoUrl = (value) => {
|
|
103
|
+
const absolute = toAbsoluteUrl(value);
|
|
104
|
+
if (!absolute) return '';
|
|
105
|
+
if (/^[a-z]+:/i.test(absolute) && !/^https?:/i.test(absolute)) return '';
|
|
106
|
+
return absolute.replace(/\\/view\\/photo\\/[^/]+\\/public\\//, '/view/photo/l/public/');
|
|
107
|
+
};
|
|
108
|
+
const buildPageUrl = (start) => {
|
|
109
|
+
const url = new URL(location.href);
|
|
110
|
+
url.searchParams.set('type', type);
|
|
111
|
+
if (start > 0) url.searchParams.set('start', String(start));
|
|
112
|
+
else url.searchParams.delete('start');
|
|
113
|
+
return url.toString();
|
|
114
|
+
};
|
|
115
|
+
const getTitle = (doc) => {
|
|
116
|
+
const raw = normalize(doc.querySelector('#content h1')?.textContent)
|
|
117
|
+
|| normalize(doc.querySelector('title')?.textContent);
|
|
118
|
+
return raw.replace(/\\s*\\(豆瓣\\)\\s*$/, '');
|
|
119
|
+
};
|
|
120
|
+
const extractPhotos = (doc, pageNumber) => {
|
|
121
|
+
const nodes = Array.from(doc.querySelectorAll('.poster-col3 li, .poster-col3l li, .article li'));
|
|
122
|
+
const rows = [];
|
|
123
|
+
for (const node of nodes) {
|
|
124
|
+
const link = node.querySelector('a[href*="/photos/photo/"]');
|
|
125
|
+
const img = node.querySelector('img');
|
|
126
|
+
if (!link || !img) continue;
|
|
127
|
+
|
|
128
|
+
const detailUrl = toAbsoluteUrl(link.getAttribute('href') || '');
|
|
129
|
+
const photoId = detailUrl.match(/\\/photo\\/(\\d+)/)?.[1] || '';
|
|
130
|
+
const thumbUrl = resolveDoubanPhotoAssetUrl([
|
|
131
|
+
img.getAttribute('data-origin'),
|
|
132
|
+
img.getAttribute('data-src'),
|
|
133
|
+
img.getAttribute('src'),
|
|
134
|
+
], location.href);
|
|
135
|
+
const imageUrl = promotePhotoUrl(thumbUrl);
|
|
136
|
+
const title = normalize(link.getAttribute('title'))
|
|
137
|
+
|| normalize(img.getAttribute('alt'))
|
|
138
|
+
|| (photoId ? 'photo_' + photoId : 'photo_' + String(rows.length + 1));
|
|
139
|
+
|
|
140
|
+
if (!detailUrl || !thumbUrl || !imageUrl) continue;
|
|
141
|
+
|
|
142
|
+
rows.push({
|
|
143
|
+
photoId,
|
|
144
|
+
title,
|
|
145
|
+
imageUrl,
|
|
146
|
+
thumbUrl,
|
|
147
|
+
detailUrl,
|
|
148
|
+
page: pageNumber,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return rows;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const subjectTitle = getTitle(document);
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
const photos = [];
|
|
157
|
+
|
|
158
|
+
for (let pageIndex = 0; photos.length < limit; pageIndex += 1) {
|
|
159
|
+
let doc = document;
|
|
160
|
+
if (pageIndex > 0) {
|
|
161
|
+
const response = await fetch(buildPageUrl(pageIndex * pageSize), { credentials: 'include' });
|
|
162
|
+
if (!response.ok) break;
|
|
163
|
+
const html = await response.text();
|
|
164
|
+
doc = new DOMParser().parseFromString(html, 'text/html');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const pagePhotos = extractPhotos(doc, pageIndex + 1);
|
|
168
|
+
if (!pagePhotos.length) break;
|
|
169
|
+
|
|
170
|
+
let appended = 0;
|
|
171
|
+
let foundTarget = false;
|
|
172
|
+
for (const photo of pagePhotos) {
|
|
173
|
+
const key = photo.photoId || photo.detailUrl || photo.imageUrl;
|
|
174
|
+
if (seen.has(key)) continue;
|
|
175
|
+
seen.add(key);
|
|
176
|
+
photos.push({
|
|
177
|
+
index: photos.length + 1,
|
|
178
|
+
...photo,
|
|
179
|
+
});
|
|
180
|
+
appended += 1;
|
|
181
|
+
if (targetPhotoId && photo.photoId === targetPhotoId) {
|
|
182
|
+
foundTarget = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (photos.length >= limit) break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (foundTarget || pagePhotos.length < pageSize || appended === 0) break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
subjectId,
|
|
193
|
+
subjectTitle,
|
|
194
|
+
type,
|
|
195
|
+
photos,
|
|
196
|
+
};
|
|
197
|
+
})()
|
|
198
|
+
`);
|
|
199
|
+
const photos = Array.isArray(data?.photos) ? data.photos : [];
|
|
200
|
+
if (!photos.length) {
|
|
201
|
+
throw new EmptyResultError('douban photos', 'No photos found. Try a different subject ID or a different --type value such as Rb.');
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
subjectId: normalizedId,
|
|
205
|
+
subjectTitle: String(data?.subjectTitle || '').trim(),
|
|
206
|
+
type,
|
|
207
|
+
photos,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
21
210
|
export async function loadDoubanBookHot(page, limit) {
|
|
22
211
|
const safeLimit = clampLimit(limit);
|
|
23
212
|
await page.goto('https://book.douban.com/chart');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getDoubanPhotoExtension, loadDoubanSubjectPhotos, normalizeDoubanSubjectId, promoteDoubanPhotoUrl, resolveDoubanPhotoAssetUrl, } from './utils.js';
|
|
3
|
+
describe('douban utils', () => {
|
|
4
|
+
it('normalizes valid subject ids', () => {
|
|
5
|
+
expect(normalizeDoubanSubjectId(' 30382501 ')).toBe('30382501');
|
|
6
|
+
});
|
|
7
|
+
it('rejects invalid subject ids', () => {
|
|
8
|
+
expect(() => normalizeDoubanSubjectId('tt30382501')).toThrow('Invalid Douban subject ID');
|
|
9
|
+
});
|
|
10
|
+
it('promotes thumbnail urls to large photo urls', () => {
|
|
11
|
+
expect(promoteDoubanPhotoUrl('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp')).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp');
|
|
12
|
+
expect(promoteDoubanPhotoUrl('https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2578474613.jpg')).toBe('https://img9.doubanio.com/view/photo/l/public/p2578474613.jpg');
|
|
13
|
+
});
|
|
14
|
+
it('rejects non-http photo urls during promotion', () => {
|
|
15
|
+
expect(promoteDoubanPhotoUrl('data:image/gif;base64,abc')).toBe('');
|
|
16
|
+
});
|
|
17
|
+
it('prefers lazy-loaded photo urls over data placeholders', () => {
|
|
18
|
+
expect(resolveDoubanPhotoAssetUrl([
|
|
19
|
+
'',
|
|
20
|
+
'https://img1.doubanio.com/view/photo/m/public/p2913450214.webp',
|
|
21
|
+
'data:image/gif;base64,abc',
|
|
22
|
+
], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp');
|
|
23
|
+
});
|
|
24
|
+
it('drops unsupported non-http photo urls when no real image url exists', () => {
|
|
25
|
+
expect(resolveDoubanPhotoAssetUrl(['data:image/gif;base64,abc', 'blob:https://movie.douban.com/example'], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('');
|
|
26
|
+
});
|
|
27
|
+
it('removes the default photo cap when scanning for an exact photo id', async () => {
|
|
28
|
+
const evaluate = vi.fn()
|
|
29
|
+
.mockResolvedValueOnce({ blocked: false, title: 'Some Movie', href: 'https://movie.douban.com/subject/30382501/photos?type=Rb' })
|
|
30
|
+
.mockResolvedValueOnce({
|
|
31
|
+
subjectId: '30382501',
|
|
32
|
+
subjectTitle: 'The Wandering Earth 2',
|
|
33
|
+
type: 'Rb',
|
|
34
|
+
photos: [
|
|
35
|
+
{
|
|
36
|
+
index: 731,
|
|
37
|
+
photoId: '2913450215',
|
|
38
|
+
title: 'Character poster',
|
|
39
|
+
imageUrl: 'https://img1.doubanio.com/view/photo/l/public/p2913450215.jpg',
|
|
40
|
+
thumbUrl: 'https://img1.doubanio.com/view/photo/m/public/p2913450215.jpg',
|
|
41
|
+
detailUrl: 'https://movie.douban.com/photos/photo/2913450215/',
|
|
42
|
+
page: 25,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
const page = {
|
|
47
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
evaluate,
|
|
50
|
+
};
|
|
51
|
+
await loadDoubanSubjectPhotos(page, '30382501', {
|
|
52
|
+
type: 'Rb',
|
|
53
|
+
targetPhotoId: '2913450215',
|
|
54
|
+
});
|
|
55
|
+
const scanScript = evaluate.mock.calls[1]?.[0];
|
|
56
|
+
expect(scanScript).toContain('const targetPhotoId = "2913450215";');
|
|
57
|
+
expect(scanScript).toContain(`const limit = ${Number.MAX_SAFE_INTEGER};`);
|
|
58
|
+
expect(scanScript).toContain('for (let pageIndex = 0; photos.length < limit; pageIndex += 1)');
|
|
59
|
+
});
|
|
60
|
+
it('keeps image extensions when download urls contain query params', () => {
|
|
61
|
+
expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp?foo=1')).toBe('.webp');
|
|
62
|
+
expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.jpeg')).toBe('.jpeg');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|