@jackwener/opencli 1.7.22 → 1.8.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/README.md +35 -194
- package/README.zh-CN.md +42 -260
- package/cli-manifest.json +8160 -4392
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +13 -6
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +7 -3
- package/clis/twitter/search.test.js +41 -0
- package/clis/twitter/shared.js +155 -0
- package/clis/twitter/shared.test.js +465 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +98 -11
- package/clis/weread/search.js +32 -9
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +166 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +252 -2
- package/clis/xiaohongshu/creator-notes.test.js +90 -1
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +2 -19
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +17 -16
- package/clis/zhihu/collection.test.js +50 -3
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -17
- package/clis/zhihu/question.test.js +113 -11
- package/clis/zhihu/search.js +195 -43
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
package/clis/pixiv/utils.js
CHANGED
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
9
9
|
const PIXIV_DOMAIN = 'www.pixiv.net';
|
|
10
|
+
|
|
11
|
+
function unwrapEvaluateResult(payload) {
|
|
12
|
+
if (payload && !Array.isArray(payload) && typeof payload === 'object' && 'session' in payload && 'data' in payload) {
|
|
13
|
+
return payload.data;
|
|
14
|
+
}
|
|
15
|
+
return payload;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractPixivErrorMessage(payload) {
|
|
19
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
20
|
+
const candidates = [
|
|
21
|
+
payload.message,
|
|
22
|
+
payload.errorMessage,
|
|
23
|
+
payload.error?.message,
|
|
24
|
+
payload.error,
|
|
25
|
+
];
|
|
26
|
+
const found = candidates.find((value) => typeof value === 'string' && value.trim());
|
|
27
|
+
return found ? found.trim() : '';
|
|
28
|
+
}
|
|
10
29
|
/**
|
|
11
30
|
* Navigate to Pixiv (to attach cookies) then fetch a Pixiv Ajax API endpoint.
|
|
12
31
|
*
|
|
@@ -21,27 +40,57 @@ const PIXIV_DOMAIN = 'www.pixiv.net';
|
|
|
21
40
|
* @throws CommandExecutionError on 404 or other HTTP errors
|
|
22
41
|
*/
|
|
23
42
|
export async function pixivFetch(page, path, opts = {}) {
|
|
24
|
-
|
|
43
|
+
try {
|
|
44
|
+
await page.goto(`https://${PIXIV_DOMAIN}`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new CommandExecutionError(`Pixiv navigation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
25
48
|
const qs = opts.params
|
|
26
49
|
? '?' + Object.entries(opts.params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
|
|
27
50
|
: '';
|
|
28
51
|
const url = `https://${PIXIV_DOMAIN}${path}${qs}`;
|
|
29
|
-
|
|
52
|
+
let data;
|
|
53
|
+
try {
|
|
54
|
+
data = unwrapEvaluateResult(await page.evaluate(`
|
|
30
55
|
(async () => {
|
|
31
56
|
const res = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
let json = null;
|
|
59
|
+
if (text) {
|
|
60
|
+
try { json = JSON.parse(text); } catch {}
|
|
61
|
+
}
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
return {
|
|
64
|
+
__httpError: res.status,
|
|
65
|
+
message: json?.message || json?.errorMessage || json?.error?.message || (typeof json?.error === 'string' ? json.error : '') || text.slice(0, 200),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!json) return { __malformed: true, message: 'invalid JSON' };
|
|
69
|
+
return json;
|
|
34
70
|
})()
|
|
35
|
-
`);
|
|
71
|
+
`));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new CommandExecutionError(`Pixiv request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
36
75
|
if (data?.__httpError) {
|
|
37
76
|
const status = data.__httpError;
|
|
38
77
|
if (status === 401 || status === 403) {
|
|
39
78
|
throw new AuthRequiredError(PIXIV_DOMAIN, 'Authentication required — please log in to Pixiv in Chrome');
|
|
40
79
|
}
|
|
80
|
+
const message = extractPixivErrorMessage(data);
|
|
41
81
|
if (status === 404) {
|
|
42
|
-
throw new CommandExecutionError(opts.notFoundMsg || `Pixiv resource not found (HTTP 404)`);
|
|
82
|
+
throw new CommandExecutionError(message || opts.notFoundMsg || `Pixiv resource not found (HTTP 404)`);
|
|
43
83
|
}
|
|
44
|
-
throw new CommandExecutionError(`Pixiv request failed (HTTP ${status})`);
|
|
84
|
+
throw new CommandExecutionError(message ? `Pixiv request failed (HTTP ${status}): ${message}` : `Pixiv request failed (HTTP ${status})`);
|
|
85
|
+
}
|
|
86
|
+
if (!data || Array.isArray(data) || typeof data !== 'object' || data.__malformed) {
|
|
87
|
+
throw new CommandExecutionError('Pixiv request returned malformed JSON payload');
|
|
88
|
+
}
|
|
89
|
+
if (data.error === true) {
|
|
90
|
+
throw new CommandExecutionError(extractPixivErrorMessage(data) || 'Pixiv API returned an error');
|
|
91
|
+
}
|
|
92
|
+
if (!('body' in data)) {
|
|
93
|
+
throw new CommandExecutionError('Pixiv request returned malformed API payload');
|
|
45
94
|
}
|
|
46
95
|
return data?.body;
|
|
47
96
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* PowerChina search — browser DOM extraction with multi-entry URL probing.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
-
import { AuthRequiredError } from '@jackwener/opencli/errors';
|
|
5
|
+
import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
6
6
|
import {
|
|
7
7
|
cleanText,
|
|
8
8
|
normalizeDate,
|
|
@@ -215,7 +215,7 @@ cli({
|
|
|
215
215
|
);
|
|
216
216
|
|
|
217
217
|
if (rows.length === 0 && extractedRows.length > 0) {
|
|
218
|
-
throw new
|
|
218
|
+
throw new EmptyResultError('powerchina search', 'extracted only navigation/portal rows, no bid entries matched');
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
if (rows.length === 0) {
|
|
@@ -227,7 +227,7 @@ cli({
|
|
|
227
227
|
);
|
|
228
228
|
}
|
|
229
229
|
if (apiFailure) {
|
|
230
|
-
throw new
|
|
230
|
+
throw new EmptyResultError('powerchina search', `api/dom yielded no result: ${apiFailure}`);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
2
4
|
import { __test__ } from './search.js';
|
|
5
|
+
import './search.js';
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.unstubAllGlobals();
|
|
9
|
+
});
|
|
3
10
|
|
|
4
11
|
describe('powerchina search helpers', () => {
|
|
5
12
|
it('builds candidate URLs with keyword variants', () => {
|
|
@@ -64,4 +71,23 @@ describe('powerchina search helpers', () => {
|
|
|
64
71
|
expect(mapped?.date).toBe('2026-04-07');
|
|
65
72
|
expect(mapped?.url).toBe('https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/getInfo/2409419657');
|
|
66
73
|
});
|
|
74
|
+
|
|
75
|
+
it('throws EmptyResultError when extraction only finds navigation rows', async () => {
|
|
76
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')));
|
|
77
|
+
const cmd = getRegistry().get('powerchina/search');
|
|
78
|
+
const page = {
|
|
79
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
80
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
81
|
+
evaluate: vi.fn().mockResolvedValue([
|
|
82
|
+
{
|
|
83
|
+
title: '首页',
|
|
84
|
+
url: 'https://bid.powerchina.cn/',
|
|
85
|
+
date: '',
|
|
86
|
+
contextText: '首页',
|
|
87
|
+
},
|
|
88
|
+
]),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
await expect(cmd.func(page, { query: '电梯', limit: 1 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
92
|
+
});
|
|
67
93
|
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioral tests for the extractRedditMedia helper.
|
|
3
|
+
*
|
|
4
|
+
* The helper is duplicated inline inside each adapter's browser-side evaluate()
|
|
5
|
+
* source (see popular.js / hot.js / search.js / frontpage.js / subreddit.js).
|
|
6
|
+
* Per-adapter tests grep that the same function-name + spread call is present.
|
|
7
|
+
* This file pins the helper's behavior against representative Reddit-JSON
|
|
8
|
+
* fixtures.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
function decodeHtml(s) {
|
|
13
|
+
if (typeof s !== 'string' || !s) return '';
|
|
14
|
+
return s
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/gi, "'")
|
|
20
|
+
.replace(/'/g, "'");
|
|
21
|
+
}
|
|
22
|
+
function extractRedditMedia(d) {
|
|
23
|
+
const post_hint = d?.post_hint || '';
|
|
24
|
+
const url_overridden_by_dest = d?.url_overridden_by_dest || '';
|
|
25
|
+
const preview_image_url = decodeHtml(d?.preview?.images?.[0]?.source?.url || '');
|
|
26
|
+
const gallery_urls = [];
|
|
27
|
+
const items = d?.gallery_data?.items;
|
|
28
|
+
const meta = d?.media_metadata;
|
|
29
|
+
if (Array.isArray(items) && meta) {
|
|
30
|
+
for (const it of items) {
|
|
31
|
+
const m = it && meta[it.media_id];
|
|
32
|
+
const u = m?.s?.u;
|
|
33
|
+
if (u) gallery_urls.push(decodeHtml(u));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { post_hint, url_overridden_by_dest, preview_image_url, gallery_urls };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('extractRedditMedia', () => {
|
|
40
|
+
it('returns empty fields for a plain text/self post', () => {
|
|
41
|
+
expect(extractRedditMedia({ is_self: true, selftext: 'hi' })).toEqual({
|
|
42
|
+
post_hint: '',
|
|
43
|
+
url_overridden_by_dest: '',
|
|
44
|
+
preview_image_url: '',
|
|
45
|
+
gallery_urls: [],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('extracts post_hint, url_overridden_by_dest, preview_image_url for a single-image post', () => {
|
|
50
|
+
const post = {
|
|
51
|
+
post_hint: 'image',
|
|
52
|
+
url_overridden_by_dest: 'https://i.redd.it/abc.jpg',
|
|
53
|
+
preview: {
|
|
54
|
+
images: [{ source: { url: 'https://preview.redd.it/abc.jpg?width=640&s=xyz' } }],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
expect(extractRedditMedia(post)).toEqual({
|
|
58
|
+
post_hint: 'image',
|
|
59
|
+
url_overridden_by_dest: 'https://i.redd.it/abc.jpg',
|
|
60
|
+
preview_image_url: 'https://preview.redd.it/abc.jpg?width=640&s=xyz',
|
|
61
|
+
gallery_urls: [],
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('extracts gallery_urls in gallery_data.items order, HTML-decoded', () => {
|
|
66
|
+
const post = {
|
|
67
|
+
post_hint: '',
|
|
68
|
+
url_overridden_by_dest: 'https://www.reddit.com/gallery/xyz',
|
|
69
|
+
gallery_data: {
|
|
70
|
+
items: [{ media_id: 'idB' }, { media_id: 'idA' }, { media_id: 'idC' }],
|
|
71
|
+
},
|
|
72
|
+
media_metadata: {
|
|
73
|
+
idA: { s: { u: 'https://preview.redd.it/idA.jpg?width=1&a=1' } },
|
|
74
|
+
idB: { s: { u: 'https://preview.redd.it/idB.jpg?width=1&a=2' } },
|
|
75
|
+
idC: { s: { u: 'https://preview.redd.it/idC.jpg?width=1&a=3' } },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const out = extractRedditMedia(post);
|
|
79
|
+
expect(out.gallery_urls).toEqual([
|
|
80
|
+
'https://preview.redd.it/idB.jpg?width=1&a=2',
|
|
81
|
+
'https://preview.redd.it/idA.jpg?width=1&a=1',
|
|
82
|
+
'https://preview.redd.it/idC.jpg?width=1&a=3',
|
|
83
|
+
]);
|
|
84
|
+
expect(out.url_overridden_by_dest).toBe('https://www.reddit.com/gallery/xyz');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('extracts post_hint for a hosted:video post', () => {
|
|
88
|
+
const post = {
|
|
89
|
+
post_hint: 'hosted:video',
|
|
90
|
+
url_overridden_by_dest: 'https://v.redd.it/xyz',
|
|
91
|
+
preview: { images: [{ source: { url: 'https://preview.redd.it/thumb.jpg' } }] },
|
|
92
|
+
};
|
|
93
|
+
const out = extractRedditMedia(post);
|
|
94
|
+
expect(out.post_hint).toBe('hosted:video');
|
|
95
|
+
expect(out.url_overridden_by_dest).toBe('https://v.redd.it/xyz');
|
|
96
|
+
expect(out.preview_image_url).toBe('https://preview.redd.it/thumb.jpg');
|
|
97
|
+
expect(out.gallery_urls).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('extracts post_hint for an external link post', () => {
|
|
101
|
+
const post = {
|
|
102
|
+
post_hint: 'link',
|
|
103
|
+
url_overridden_by_dest: 'https://example.com/article',
|
|
104
|
+
};
|
|
105
|
+
expect(extractRedditMedia(post)).toEqual({
|
|
106
|
+
post_hint: 'link',
|
|
107
|
+
url_overridden_by_dest: 'https://example.com/article',
|
|
108
|
+
preview_image_url: '',
|
|
109
|
+
gallery_urls: [],
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('HTML-decodes preview URLs that arrive with & separators', () => {
|
|
114
|
+
const post = {
|
|
115
|
+
preview: {
|
|
116
|
+
images: [{ source: { url: 'https://x/?a=1&b=2&c=3' } }],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
expect(extractRedditMedia(post).preview_image_url).toBe('https://x/?a=1&b=2&c=3');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('skips gallery entries whose media_metadata is missing', () => {
|
|
123
|
+
const post = {
|
|
124
|
+
gallery_data: { items: [{ media_id: 'present' }, { media_id: 'orphan' }] },
|
|
125
|
+
media_metadata: {
|
|
126
|
+
present: { s: { u: 'https://preview.redd.it/present.jpg' } },
|
|
127
|
+
// 'orphan' intentionally absent
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
expect(extractRedditMedia(post).gallery_urls).toEqual([
|
|
131
|
+
'https://preview.redd.it/present.jpg',
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('tolerates null/undefined input without throwing', () => {
|
|
136
|
+
expect(extractRedditMedia(null)).toEqual({
|
|
137
|
+
post_hint: '',
|
|
138
|
+
url_overridden_by_dest: '',
|
|
139
|
+
preview_image_url: '',
|
|
140
|
+
gallery_urls: [],
|
|
141
|
+
});
|
|
142
|
+
expect(extractRedditMedia(undefined)).toEqual({
|
|
143
|
+
post_hint: '',
|
|
144
|
+
url_overridden_by_dest: '',
|
|
145
|
+
preview_image_url: '',
|
|
146
|
+
gallery_urls: [],
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
package/clis/reddit/frontpage.js
CHANGED
|
@@ -10,22 +10,60 @@ cli({
|
|
|
10
10
|
args: [
|
|
11
11
|
{ name: 'limit', type: 'int', default: 15 },
|
|
12
12
|
],
|
|
13
|
-
columns: ['title', 'subreddit', 'author', 'upvotes', 'comments', 'url'],
|
|
13
|
+
columns: ['title', 'subreddit', 'author', 'upvotes', 'comments', 'url', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
|
|
14
14
|
pipeline: [
|
|
15
15
|
{ navigate: 'https://www.reddit.com' },
|
|
16
16
|
{ evaluate: `(async () => {
|
|
17
|
-
|
|
17
|
+
function decodeHtml(s) {
|
|
18
|
+
if (typeof s !== 'string' || !s) return '';
|
|
19
|
+
return s
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/gi, "'")
|
|
25
|
+
.replace(/'/g, "'");
|
|
26
|
+
}
|
|
27
|
+
function extractRedditMedia(d) {
|
|
28
|
+
const post_hint = d?.post_hint || '';
|
|
29
|
+
const url_overridden_by_dest = d?.url_overridden_by_dest || '';
|
|
30
|
+
const preview_image_url = decodeHtml(d?.preview?.images?.[0]?.source?.url || '');
|
|
31
|
+
const gallery_urls = [];
|
|
32
|
+
const items = d?.gallery_data?.items;
|
|
33
|
+
const meta = d?.media_metadata;
|
|
34
|
+
if (Array.isArray(items) && meta) {
|
|
35
|
+
for (const it of items) {
|
|
36
|
+
const m = it && meta[it.media_id];
|
|
37
|
+
const u = m?.s?.u;
|
|
38
|
+
if (u) gallery_urls.push(decodeHtml(u));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { post_hint, url_overridden_by_dest, preview_image_url, gallery_urls };
|
|
42
|
+
}
|
|
43
|
+
const res = await fetch('/r/all.json?limit=\${{ args.limit }}&raw_json=1', { credentials: 'include' });
|
|
18
44
|
const j = await res.json();
|
|
19
|
-
return j?.data?.children || []
|
|
45
|
+
return (j?.data?.children || []).map(c => ({
|
|
46
|
+
title: c.data.title,
|
|
47
|
+
subreddit: c.data.subreddit_name_prefixed,
|
|
48
|
+
author: c.data.author,
|
|
49
|
+
upvotes: c.data.score,
|
|
50
|
+
comments: c.data.num_comments,
|
|
51
|
+
url: 'https://www.reddit.com' + c.data.permalink,
|
|
52
|
+
...extractRedditMedia(c.data),
|
|
53
|
+
}));
|
|
20
54
|
})()
|
|
21
55
|
` },
|
|
22
56
|
{ map: {
|
|
23
|
-
title: '${{ item.
|
|
24
|
-
subreddit: '${{ item.
|
|
25
|
-
author: '${{ item.
|
|
26
|
-
upvotes: '${{ item.
|
|
27
|
-
comments: '${{ item.
|
|
28
|
-
url: '
|
|
57
|
+
title: '${{ item.title }}',
|
|
58
|
+
subreddit: '${{ item.subreddit }}',
|
|
59
|
+
author: '${{ item.author }}',
|
|
60
|
+
upvotes: '${{ item.upvotes }}',
|
|
61
|
+
comments: '${{ item.comments }}',
|
|
62
|
+
url: '${{ item.url }}',
|
|
63
|
+
post_hint: '${{ item.post_hint }}',
|
|
64
|
+
url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
|
|
65
|
+
preview_image_url: '${{ item.preview_image_url }}',
|
|
66
|
+
gallery_urls: '${{ item.gallery_urls }}',
|
|
29
67
|
} },
|
|
30
68
|
{ limit: '${{ args.limit }}' },
|
|
31
69
|
],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import './frontpage.js';
|
|
4
|
+
|
|
5
|
+
describe('reddit frontpage adapter', () => {
|
|
6
|
+
const command = getRegistry().get('reddit/frontpage');
|
|
7
|
+
|
|
8
|
+
it('exposes the full frontpage shape including the 4 media columns', () => {
|
|
9
|
+
expect(command?.columns).toEqual([
|
|
10
|
+
'title', 'subreddit', 'author', 'upvotes', 'comments', 'url',
|
|
11
|
+
'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls',
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shapes children into the intermediate-object pattern with media spread in', () => {
|
|
16
|
+
// Refactored from item.data.* templating to a uniform pre-shaped object
|
|
17
|
+
// so gallery_urls (array-valued) can be set directly in evaluate.
|
|
18
|
+
expect(command?.pipeline?.[1]?.evaluate).toContain('function extractRedditMedia');
|
|
19
|
+
expect(command?.pipeline?.[1]?.evaluate).toContain('...extractRedditMedia(c.data)');
|
|
20
|
+
expect(command?.pipeline?.[1]?.evaluate).toContain('/r/all.json?limit=${{ args.limit }}&raw_json=1');
|
|
21
|
+
expect(command?.pipeline?.[2]?.map).toMatchObject({
|
|
22
|
+
title: '${{ item.title }}',
|
|
23
|
+
subreddit: '${{ item.subreddit }}',
|
|
24
|
+
author: '${{ item.author }}',
|
|
25
|
+
upvotes: '${{ item.upvotes }}',
|
|
26
|
+
comments: '${{ item.comments }}',
|
|
27
|
+
url: '${{ item.url }}',
|
|
28
|
+
post_hint: '${{ item.post_hint }}',
|
|
29
|
+
url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
|
|
30
|
+
preview_image_url: '${{ item.preview_image_url }}',
|
|
31
|
+
gallery_urls: '${{ item.gallery_urls }}',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
package/clis/reddit/home.js
CHANGED
|
@@ -15,6 +15,35 @@ export function parseRedditHomeLimit(raw) {
|
|
|
15
15
|
return n;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function decodeRedditHtml(value) {
|
|
19
|
+
if (typeof value !== 'string' || !value) return '';
|
|
20
|
+
return value
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"')
|
|
25
|
+
.replace(/'/gi, "'")
|
|
26
|
+
.replace(/'/g, "'");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function extractRedditMedia(d) {
|
|
30
|
+
const galleryUrls = [];
|
|
31
|
+
const items = d?.gallery_data?.items;
|
|
32
|
+
const meta = d?.media_metadata;
|
|
33
|
+
if (Array.isArray(items) && meta && typeof meta === 'object') {
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const url = meta[item?.media_id]?.s?.u;
|
|
36
|
+
if (typeof url === 'string' && url) galleryUrls.push(decodeRedditHtml(url));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
post_hint: typeof d?.post_hint === 'string' ? d.post_hint : '',
|
|
41
|
+
url_overridden_by_dest: decodeRedditHtml(d?.url_overridden_by_dest || ''),
|
|
42
|
+
preview_image_url: decodeRedditHtml(d?.preview?.images?.[0]?.source?.url || ''),
|
|
43
|
+
gallery_urls: galleryUrls,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
18
47
|
cli({
|
|
19
48
|
site: 'reddit',
|
|
20
49
|
name: 'home',
|
|
@@ -26,7 +55,7 @@ cli({
|
|
|
26
55
|
args: [
|
|
27
56
|
{ name: 'limit', type: 'int', default: 25, help: `Number of posts (1–${REDDIT_HOME_MAX_LIMIT})` },
|
|
28
57
|
],
|
|
29
|
-
columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url'],
|
|
58
|
+
columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
|
|
30
59
|
func: async (page, kwargs) => {
|
|
31
60
|
const limit = parseRedditHomeLimit(kwargs.limit);
|
|
32
61
|
await page.goto('https://www.reddit.com');
|
|
@@ -103,6 +132,7 @@ cli({
|
|
|
103
132
|
postId: d.id,
|
|
104
133
|
author: typeof d.author === 'string' ? d.author : null,
|
|
105
134
|
url: d.permalink ? 'https://www.reddit.com' + d.permalink : null,
|
|
135
|
+
...extractRedditMedia(d),
|
|
106
136
|
});
|
|
107
137
|
}
|
|
108
138
|
if (rows.length === 0) {
|
package/clis/reddit/home.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
3
|
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
-
import { parseRedditHomeLimit } from './home.js';
|
|
4
|
+
import { extractRedditMedia, parseRedditHomeLimit } from './home.js';
|
|
5
5
|
import './home.js';
|
|
6
6
|
|
|
7
7
|
function makePage(result) {
|
|
@@ -33,7 +33,10 @@ describe('reddit home command', () => {
|
|
|
33
33
|
expect(command).toBeDefined();
|
|
34
34
|
expect(command.access).toBe('read');
|
|
35
35
|
expect(command.browser).toBe(true);
|
|
36
|
-
expect(command.columns).toEqual([
|
|
36
|
+
expect(command.columns).toEqual([
|
|
37
|
+
'rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url',
|
|
38
|
+
'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls',
|
|
39
|
+
]);
|
|
37
40
|
});
|
|
38
41
|
|
|
39
42
|
it('parseRedditHomeLimit accepts [1,100] and rejects out-of-range / non-integer without silent clamp', () => {
|
|
@@ -81,20 +84,60 @@ describe('reddit home command', () => {
|
|
|
81
84
|
{
|
|
82
85
|
rank: 1, title: 'Title for a1', subreddit: 'r/dummy', score: 100, comments: 10,
|
|
83
86
|
postId: 'a1', author: 'someone', url: 'https://www.reddit.com/r/dummy/comments/a1/title/',
|
|
87
|
+
post_hint: '', url_overridden_by_dest: '', preview_image_url: '', gallery_urls: [],
|
|
84
88
|
},
|
|
85
89
|
{
|
|
86
90
|
rank: 2, title: 'Title for b2', subreddit: 'r/dummy', score: 250, comments: 42,
|
|
87
91
|
postId: 'b2', author: 'someone', url: 'https://www.reddit.com/r/dummy/comments/b2/title/',
|
|
92
|
+
post_hint: '', url_overridden_by_dest: '', preview_image_url: '', gallery_urls: [],
|
|
88
93
|
},
|
|
89
94
|
]);
|
|
90
95
|
// Row shape must match declared columns exactly.
|
|
91
96
|
for (const row of rows) {
|
|
92
97
|
expect(Object.keys(row).sort()).toEqual(
|
|
93
|
-
[
|
|
98
|
+
[
|
|
99
|
+
'author', 'comments', 'gallery_urls', 'postId', 'post_hint', 'preview_image_url',
|
|
100
|
+
'rank', 'score', 'subreddit', 'title', 'url', 'url_overridden_by_dest',
|
|
101
|
+
],
|
|
94
102
|
);
|
|
95
103
|
}
|
|
96
104
|
});
|
|
97
105
|
|
|
106
|
+
it('surfaces media route fields from the personalized home feed', async () => {
|
|
107
|
+
const entries = [makeEntry('a1', {
|
|
108
|
+
post_hint: 'image',
|
|
109
|
+
url_overridden_by_dest: 'https://i.redd.it/a.jpg',
|
|
110
|
+
preview: {
|
|
111
|
+
images: [{ source: { url: 'https://preview.redd.it/a.jpg?x=1&y=2' } }],
|
|
112
|
+
},
|
|
113
|
+
gallery_data: { items: [{ media_id: 'm2' }, { media_id: 'm1' }] },
|
|
114
|
+
media_metadata: {
|
|
115
|
+
m1: { s: { u: 'https://preview.redd.it/m1.jpg?x=1&y=1' } },
|
|
116
|
+
m2: { s: { u: 'https://preview.redd.it/m2.jpg?x=1&y=2' } },
|
|
117
|
+
},
|
|
118
|
+
})];
|
|
119
|
+
const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 25 });
|
|
120
|
+
|
|
121
|
+
expect(rows[0]).toMatchObject({
|
|
122
|
+
post_hint: 'image',
|
|
123
|
+
url_overridden_by_dest: 'https://i.redd.it/a.jpg',
|
|
124
|
+
preview_image_url: 'https://preview.redd.it/a.jpg?x=1&y=2',
|
|
125
|
+
gallery_urls: [
|
|
126
|
+
'https://preview.redd.it/m2.jpg?x=1&y=2',
|
|
127
|
+
'https://preview.redd.it/m1.jpg?x=1&y=1',
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('extractRedditMedia tolerates missing media without throwing', () => {
|
|
133
|
+
expect(extractRedditMedia({ is_self: true })).toEqual({
|
|
134
|
+
post_hint: '',
|
|
135
|
+
url_overridden_by_dest: '',
|
|
136
|
+
preview_image_url: '',
|
|
137
|
+
gallery_urls: [],
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
98
141
|
it('applies the post-fetch limit slice (defence in depth vs Reddit overshoot)', async () => {
|
|
99
142
|
const entries = Array.from({ length: 30 }, (_, i) => makeEntry(`p${i}`));
|
|
100
143
|
const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 5 });
|
package/clis/reddit/hot.js
CHANGED
|
@@ -13,10 +13,36 @@ cli({
|
|
|
13
13
|
},
|
|
14
14
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of posts' },
|
|
15
15
|
],
|
|
16
|
-
columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url'],
|
|
16
|
+
columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
|
|
17
17
|
pipeline: [
|
|
18
18
|
{ navigate: 'https://www.reddit.com' },
|
|
19
19
|
{ evaluate: `(async () => {
|
|
20
|
+
function decodeHtml(s) {
|
|
21
|
+
if (typeof s !== 'string' || !s) return '';
|
|
22
|
+
return s
|
|
23
|
+
.replace(/&/g, '&')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>')
|
|
26
|
+
.replace(/"/g, '"')
|
|
27
|
+
.replace(/'/gi, "'")
|
|
28
|
+
.replace(/'/g, "'");
|
|
29
|
+
}
|
|
30
|
+
function extractRedditMedia(d) {
|
|
31
|
+
const post_hint = d?.post_hint || '';
|
|
32
|
+
const url_overridden_by_dest = d?.url_overridden_by_dest || '';
|
|
33
|
+
const preview_image_url = decodeHtml(d?.preview?.images?.[0]?.source?.url || '');
|
|
34
|
+
const gallery_urls = [];
|
|
35
|
+
const items = d?.gallery_data?.items;
|
|
36
|
+
const meta = d?.media_metadata;
|
|
37
|
+
if (Array.isArray(items) && meta) {
|
|
38
|
+
for (const it of items) {
|
|
39
|
+
const m = it && meta[it.media_id];
|
|
40
|
+
const u = m?.s?.u;
|
|
41
|
+
if (u) gallery_urls.push(decodeHtml(u));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { post_hint, url_overridden_by_dest, preview_image_url, gallery_urls };
|
|
45
|
+
}
|
|
20
46
|
const sub = \${{ args.subreddit | json }};
|
|
21
47
|
const path = sub ? '/r/' + sub + '/hot.json' : '/hot.json';
|
|
22
48
|
const limit = \${{ args.limit }};
|
|
@@ -32,6 +58,7 @@ cli({
|
|
|
32
58
|
author: c.data.author,
|
|
33
59
|
postId: c.data.id,
|
|
34
60
|
url: 'https://www.reddit.com' + c.data.permalink,
|
|
61
|
+
...extractRedditMedia(c.data),
|
|
35
62
|
}));
|
|
36
63
|
})()
|
|
37
64
|
` },
|
|
@@ -44,6 +71,10 @@ cli({
|
|
|
44
71
|
postId: '${{ item.postId }}',
|
|
45
72
|
author: '${{ item.author }}',
|
|
46
73
|
url: '${{ item.url }}',
|
|
74
|
+
post_hint: '${{ item.post_hint }}',
|
|
75
|
+
url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
|
|
76
|
+
preview_image_url: '${{ item.preview_image_url }}',
|
|
77
|
+
gallery_urls: '${{ item.gallery_urls }}',
|
|
47
78
|
} },
|
|
48
79
|
{ limit: '${{ args.limit }}' },
|
|
49
80
|
],
|
package/clis/reddit/hot.test.js
CHANGED
|
@@ -6,7 +6,10 @@ describe('reddit hot adapter', () => {
|
|
|
6
6
|
const command = getRegistry().get('reddit/hot');
|
|
7
7
|
|
|
8
8
|
it('registers postId, author, and url columns in the hot-list shape', () => {
|
|
9
|
-
expect(command?.columns).toEqual([
|
|
9
|
+
expect(command?.columns).toEqual([
|
|
10
|
+
'rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url',
|
|
11
|
+
'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls',
|
|
12
|
+
]);
|
|
10
13
|
expect(command?.pipeline?.[1]?.evaluate).toContain('postId: c.data.id');
|
|
11
14
|
expect(command?.pipeline?.[1]?.evaluate).toContain("'https://www.reddit.com' + c.data.permalink");
|
|
12
15
|
expect(command?.pipeline?.[2]?.map).toMatchObject({
|
|
@@ -15,4 +18,15 @@ describe('reddit hot adapter', () => {
|
|
|
15
18
|
url: '${{ item.url }}',
|
|
16
19
|
});
|
|
17
20
|
});
|
|
21
|
+
|
|
22
|
+
it('surfaces post_hint, url_overridden_by_dest, preview_image_url, gallery_urls via extractRedditMedia', () => {
|
|
23
|
+
expect(command?.pipeline?.[1]?.evaluate).toContain('function extractRedditMedia');
|
|
24
|
+
expect(command?.pipeline?.[1]?.evaluate).toContain('...extractRedditMedia(c.data)');
|
|
25
|
+
expect(command?.pipeline?.[2]?.map).toMatchObject({
|
|
26
|
+
post_hint: '${{ item.post_hint }}',
|
|
27
|
+
url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
|
|
28
|
+
preview_image_url: '${{ item.preview_image_url }}',
|
|
29
|
+
gallery_urls: '${{ item.gallery_urls }}',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
18
32
|
});
|