@jackwener/opencli 1.7.2 → 1.7.4
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 +18 -15
- package/README.zh-CN.md +31 -15
- package/cli-manifest.json +1265 -101
- package/clis/barchart/flow.js +1 -1
- package/clis/barchart/greeks.js +2 -2
- package/clis/barchart/options.js +2 -2
- package/clis/barchart/quote.js +1 -1
- package/clis/bilibili/favorite.js +18 -13
- package/clis/bilibili/feed.js +202 -48
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -2
- package/clis/chatgpt/image.js +97 -0
- package/clis/chatgpt/utils.js +297 -0
- package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/ax.js +6 -3
- package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
- package/clis/discord-app/delete.js +114 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +279 -10
- package/clis/douban/utils.test.js +296 -1
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/ke/chengjiao.js +77 -0
- package/clis/ke/ershoufang.js +100 -0
- package/clis/ke/utils.js +104 -0
- package/clis/ke/xiaoqu.js +77 -0
- package/clis/ke/zufang.js +94 -0
- package/clis/maimai/search-talents.js +172 -0
- package/clis/mubu/doc.js +40 -0
- package/clis/mubu/docs.js +43 -0
- package/clis/mubu/notes.js +244 -0
- package/clis/mubu/recent.js +27 -0
- package/clis/mubu/search.js +62 -0
- package/clis/mubu/utils.js +304 -0
- package/clis/reuters/search.js +1 -1
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/xiaohongshu/comments.js +20 -8
- package/clis/xiaohongshu/comments.test.js +69 -12
- package/clis/xiaohongshu/creator-note-detail.js +2 -0
- package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
- package/clis/xiaohongshu/creator-notes-summary.js +4 -0
- package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
- package/clis/xiaohongshu/creator-notes.js +1 -0
- package/clis/xiaohongshu/creator-profile.js +1 -0
- package/clis/xiaohongshu/creator-stats.js +1 -0
- package/clis/xiaohongshu/download.js +18 -7
- package/clis/xiaohongshu/download.test.js +42 -0
- package/clis/xiaohongshu/navigation.test.js +34 -0
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +17 -10
- package/clis/xiaohongshu/note.test.js +66 -11
- package/clis/xiaohongshu/publish.js +1 -0
- package/clis/xiaohongshu/search.js +1 -0
- package/clis/xiaohongshu/user.js +1 -0
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +49 -0
- package/clis/xiaoyuzhou/download.test.js +125 -0
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/yahoo-finance/quote.js +1 -1
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +44 -5
- package/dist/src/browser/bridge.d.ts +2 -0
- package/dist/src/browser/bridge.js +51 -14
- package/dist/src/browser/cdp.js +11 -2
- package/dist/src/browser/daemon-client.d.ts +2 -0
- package/dist/src/browser/dom-snapshot.js +13 -1
- package/dist/src/browser/page.d.ts +4 -1
- package/dist/src/browser/page.js +48 -8
- package/dist/src/browser/page.test.js +61 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.d.ts +1 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.d.ts +1 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +45 -35
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/daemon.js +7 -0
- package/dist/src/doctor.d.ts +2 -0
- package/dist/src/doctor.js +82 -10
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.js +13 -0
- package/dist/src/execution.js +36 -9
- package/dist/src/execution.test.js +23 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -8
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +7 -1
- package/dist/src/plugin.js +23 -1
- package/dist/src/plugin.test.js +15 -1
- package/dist/src/registry.js +3 -4
- package/dist/src/types.d.ts +3 -1
- package/dist/src/update-check.d.ts +14 -0
- package/dist/src/update-check.js +48 -3
- package/dist/src/update-check.test.d.ts +1 -0
- package/dist/src/update-check.test.js +31 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +35 -8
package/clis/douban/utils.js
CHANGED
|
@@ -7,6 +7,79 @@ const DOUBAN_PHOTO_PAGE_SIZE = 30;
|
|
|
7
7
|
const MAX_DOUBAN_PHOTOS = 500;
|
|
8
8
|
const clampLimit = (limit) => clamp(limit || 20, 1, 50);
|
|
9
9
|
const clampPhotoLimit = (limit) => clamp(limit || 120, 1, MAX_DOUBAN_PHOTOS);
|
|
10
|
+
const DOUBAN_SEARCH_READY_SELECTOR = '.item-root .title-text, .item-root .title a, .result-list .result-item h3 a';
|
|
11
|
+
const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
12
|
+
function firstNonEmpty(values) {
|
|
13
|
+
for (const value of values) {
|
|
14
|
+
const normalized = normalizeText(value);
|
|
15
|
+
if (normalized)
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
function splitDoubanPeople(value) {
|
|
21
|
+
return normalizeText(value)
|
|
22
|
+
.split(/\s*\/\s*/)
|
|
23
|
+
.map((entry) => normalizeText(entry))
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
function parseDoubanBookInfoText(infoText) {
|
|
27
|
+
const lines = String(infoText || '')
|
|
28
|
+
.replace(/\r/g, '\n')
|
|
29
|
+
.split('\n')
|
|
30
|
+
.map((line) => normalizeText(line))
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
const map = {};
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const match = line.match(/^([^::]+)\s*[::]\s*(.*)$/);
|
|
35
|
+
if (!match)
|
|
36
|
+
continue;
|
|
37
|
+
const label = normalizeText(match[1]);
|
|
38
|
+
const value = normalizeText(match[2]);
|
|
39
|
+
if (!label)
|
|
40
|
+
continue;
|
|
41
|
+
map[label] = value;
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
function parseDoubanRating(value) {
|
|
46
|
+
const normalized = normalizeText(value);
|
|
47
|
+
if (!normalized)
|
|
48
|
+
return 0;
|
|
49
|
+
const parsed = Number.parseFloat(normalized);
|
|
50
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
51
|
+
}
|
|
52
|
+
function parseDoubanCount(value) {
|
|
53
|
+
const normalized = normalizeText(value).replace(/[^\d]/g, '');
|
|
54
|
+
if (!normalized)
|
|
55
|
+
return 0;
|
|
56
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
57
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
58
|
+
}
|
|
59
|
+
function parseDoubanPageCount(value) {
|
|
60
|
+
const match = normalizeText(value).match(/(\d+)/);
|
|
61
|
+
if (!match)
|
|
62
|
+
return null;
|
|
63
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
64
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
65
|
+
}
|
|
66
|
+
function extractDoubanPublishYear(value) {
|
|
67
|
+
const match = normalizeText(value).match(/\b(19|20)\d{2}\b/);
|
|
68
|
+
return match?.[0] || '';
|
|
69
|
+
}
|
|
70
|
+
function splitDoubanTitle(fullTitle) {
|
|
71
|
+
const normalized = normalizeText(fullTitle);
|
|
72
|
+
if (!normalized)
|
|
73
|
+
return { title: '', originalTitle: '' };
|
|
74
|
+
const match = normalized.match(/^([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+(?:\s*[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef·::!?]+)*)\s+(.+)$/);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return { title: normalized, originalTitle: '' };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
title: normalizeText(match[1]),
|
|
80
|
+
originalTitle: normalizeText(match[2]),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
10
83
|
async function ensureDoubanReady(page) {
|
|
11
84
|
const state = await page.evaluate(`
|
|
12
85
|
(() => {
|
|
@@ -20,6 +93,34 @@ async function ensureDoubanReady(page) {
|
|
|
20
93
|
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.');
|
|
21
94
|
}
|
|
22
95
|
}
|
|
96
|
+
function isDetachedPageError(error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error || '');
|
|
98
|
+
return /Detached while handling command|Debugger is not attached to the tab|Target closed|No tab with id/i.test(message);
|
|
99
|
+
}
|
|
100
|
+
async function withDetachedRetry(task, options = {}) {
|
|
101
|
+
const attempts = Math.max(1, options.attempts || 2);
|
|
102
|
+
let lastError;
|
|
103
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
104
|
+
try {
|
|
105
|
+
return await task();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
lastError = error;
|
|
109
|
+
if (attempt >= attempts - 1 || !isDetachedPageError(error)) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw lastError;
|
|
115
|
+
}
|
|
116
|
+
function buildDoubanSearchUrl(type, keyword) {
|
|
117
|
+
const url = new URL(`https://search.douban.com/${encodeURIComponent(type)}/subject_search`);
|
|
118
|
+
url.searchParams.set('search_text', String(keyword || ''));
|
|
119
|
+
if (String(type || '').trim() === 'book') {
|
|
120
|
+
url.searchParams.set('cat', '1001');
|
|
121
|
+
}
|
|
122
|
+
return url.toString();
|
|
123
|
+
}
|
|
23
124
|
export function normalizeDoubanSubjectId(subjectId) {
|
|
24
125
|
const normalized = String(subjectId || '').trim();
|
|
25
126
|
if (!/^\d+$/.test(normalized)) {
|
|
@@ -68,6 +169,144 @@ export function getDoubanPhotoExtension(url) {
|
|
|
68
169
|
return ext ? ext.replace(/[?#].*$/, '') : '.jpg';
|
|
69
170
|
}
|
|
70
171
|
}
|
|
172
|
+
export function normalizeDoubanBookSubject(raw) {
|
|
173
|
+
const info = parseDoubanBookInfoText(raw?.infoText);
|
|
174
|
+
const title = firstNonEmpty([raw?.title]);
|
|
175
|
+
const subtitle = firstNonEmpty([raw?.subtitle, info['副标题']]);
|
|
176
|
+
const originalTitle = firstNonEmpty([raw?.originalTitle, info['原作名']]);
|
|
177
|
+
const authors = splitDoubanPeople(firstNonEmpty([info['作者']]));
|
|
178
|
+
const translators = splitDoubanPeople(firstNonEmpty([info['译者']]));
|
|
179
|
+
const publisher = firstNonEmpty([info['出版社'], info['出品方']]);
|
|
180
|
+
const publishDate = firstNonEmpty([info['出版年']]);
|
|
181
|
+
const publishYear = extractDoubanPublishYear(publishDate);
|
|
182
|
+
const pageCount = parseDoubanPageCount(info['页数']);
|
|
183
|
+
const binding = firstNonEmpty([info['装帧']]);
|
|
184
|
+
const price = firstNonEmpty([info['定价']]);
|
|
185
|
+
const series = firstNonEmpty([info['丛书']]);
|
|
186
|
+
const isbnRaw = firstNonEmpty([info['ISBN']]).replace(/[^\dxX]/g, '');
|
|
187
|
+
const isbn10 = isbnRaw.length === 10 ? isbnRaw : '';
|
|
188
|
+
const isbn13 = isbnRaw.length === 13 ? isbnRaw : '';
|
|
189
|
+
return {
|
|
190
|
+
id: normalizeDoubanSubjectId(raw?.id),
|
|
191
|
+
type: 'book',
|
|
192
|
+
title,
|
|
193
|
+
subtitle,
|
|
194
|
+
originalTitle,
|
|
195
|
+
authors,
|
|
196
|
+
translators,
|
|
197
|
+
publisher,
|
|
198
|
+
publishDate,
|
|
199
|
+
publishYear,
|
|
200
|
+
pageCount,
|
|
201
|
+
binding,
|
|
202
|
+
price,
|
|
203
|
+
series,
|
|
204
|
+
isbn10,
|
|
205
|
+
isbn13,
|
|
206
|
+
rating: parseDoubanRating(raw?.rating),
|
|
207
|
+
ratingCount: parseDoubanCount(raw?.ratingCount),
|
|
208
|
+
summary: normalizeText(raw?.summary),
|
|
209
|
+
cover: firstNonEmpty([raw?.cover]),
|
|
210
|
+
url: firstNonEmpty([raw?.url]),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function loadDoubanMovieSubject(page, subjectId) {
|
|
214
|
+
const normalizedId = normalizeDoubanSubjectId(subjectId);
|
|
215
|
+
const data = await withDetachedRetry(async () => {
|
|
216
|
+
await page.goto(`https://movie.douban.com/subject/${normalizedId}/`, { waitUntil: 'load', settleMs: 1500 });
|
|
217
|
+
await ensureDoubanReady(page);
|
|
218
|
+
await page.wait({ selector: 'span[property="v:itemreviewed"], #info', timeout: 8 }).catch(() => { });
|
|
219
|
+
return page.evaluate(`
|
|
220
|
+
(() => {
|
|
221
|
+
const id = ${JSON.stringify(normalizedId)};
|
|
222
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
223
|
+
const { title, originalTitle } = (${splitDoubanTitle.toString()})(normalize(document.querySelector('span[property="v:itemreviewed"]')?.textContent || ''));
|
|
224
|
+
const year = normalize(document.querySelector('.year')?.textContent).replace(/[()()]/g, '');
|
|
225
|
+
const rating = parseFloat(normalize(document.querySelector('strong[property="v:average"]')?.textContent || '0')) || 0;
|
|
226
|
+
const ratingCount = parseInt(normalize(document.querySelector('span[property="v:votes"]')?.textContent || '0'), 10) || 0;
|
|
227
|
+
const genres = Array.from(document.querySelectorAll('span[property="v:genre"]'))
|
|
228
|
+
.map((node) => normalize(node.textContent))
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.join(',');
|
|
231
|
+
const directors = Array.from(document.querySelectorAll('a[rel="v:directedBy"]'))
|
|
232
|
+
.map((node) => normalize(node.textContent))
|
|
233
|
+
.filter(Boolean)
|
|
234
|
+
.join(',');
|
|
235
|
+
const casts = Array.from(document.querySelectorAll('a[rel="v:starring"]'))
|
|
236
|
+
.slice(0, 5)
|
|
237
|
+
.map((node) => normalize(node.textContent))
|
|
238
|
+
.filter(Boolean);
|
|
239
|
+
const infoText = document.querySelector('#info')?.textContent || '';
|
|
240
|
+
let country = [];
|
|
241
|
+
const countryMatch = infoText.match(/制片国家\\/地区:\\s*([^\\n]+)/);
|
|
242
|
+
if (countryMatch) {
|
|
243
|
+
country = countryMatch[1].trim().split(/\\s*\\/\\s*/).filter(Boolean);
|
|
244
|
+
}
|
|
245
|
+
const durationRaw = normalize(document.querySelector('span[property="v:runtime"]')?.textContent || '');
|
|
246
|
+
const durationMatch = durationRaw.match(/(\\d+)/);
|
|
247
|
+
const summary = normalize(document.querySelector('span[property="v:summary"]')?.textContent || '');
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
type: 'movie',
|
|
251
|
+
title,
|
|
252
|
+
originalTitle,
|
|
253
|
+
year,
|
|
254
|
+
rating,
|
|
255
|
+
ratingCount,
|
|
256
|
+
genres,
|
|
257
|
+
directors,
|
|
258
|
+
casts,
|
|
259
|
+
country,
|
|
260
|
+
duration: durationMatch ? parseInt(durationMatch[1], 10) : null,
|
|
261
|
+
summary: summary.slice(0, 200),
|
|
262
|
+
url: 'https://movie.douban.com/subject/' + id + '/',
|
|
263
|
+
};
|
|
264
|
+
})()
|
|
265
|
+
`);
|
|
266
|
+
});
|
|
267
|
+
return data;
|
|
268
|
+
}
|
|
269
|
+
async function loadDoubanBookSubject(page, subjectId) {
|
|
270
|
+
const normalizedId = normalizeDoubanSubjectId(subjectId);
|
|
271
|
+
const data = await withDetachedRetry(async () => {
|
|
272
|
+
await page.goto(`https://book.douban.com/subject/${normalizedId}/`, { waitUntil: 'load', settleMs: 1500 });
|
|
273
|
+
await ensureDoubanReady(page);
|
|
274
|
+
await page.wait({ selector: 'h1 span, #info', timeout: 8 }).catch(() => { });
|
|
275
|
+
return page.evaluate(`
|
|
276
|
+
(() => {
|
|
277
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
278
|
+
const pickSummary = () => {
|
|
279
|
+
const nodes = Array.from(document.querySelectorAll('#link-report .intro, .related_info .intro'));
|
|
280
|
+
for (let i = nodes.length - 1; i >= 0; i -= 1) {
|
|
281
|
+
const text = normalize(nodes[i]?.textContent);
|
|
282
|
+
if (text) return text;
|
|
283
|
+
}
|
|
284
|
+
return '';
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
id: ${JSON.stringify(normalizedId)},
|
|
288
|
+
title: normalize(document.querySelector('h1 span')?.textContent || document.querySelector('h1')?.textContent || ''),
|
|
289
|
+
subtitle: '',
|
|
290
|
+
originalTitle: '',
|
|
291
|
+
infoText: document.querySelector('#info')?.innerText || document.querySelector('#info')?.textContent || '',
|
|
292
|
+
rating: normalize(document.querySelector('strong.rating_num, strong[property="v:average"]')?.textContent || ''),
|
|
293
|
+
ratingCount: normalize(document.querySelector('a.rating_people > span, span[property="v:votes"]')?.textContent || ''),
|
|
294
|
+
summary: pickSummary(),
|
|
295
|
+
cover: document.querySelector('#mainpic img')?.getAttribute('src') || '',
|
|
296
|
+
url: location.href,
|
|
297
|
+
};
|
|
298
|
+
})()
|
|
299
|
+
`);
|
|
300
|
+
});
|
|
301
|
+
return normalizeDoubanBookSubject(data);
|
|
302
|
+
}
|
|
303
|
+
export async function loadDoubanSubjectDetail(page, subjectId, subjectType = 'movie') {
|
|
304
|
+
const type = String(subjectType || 'movie').trim() === 'book' ? 'book' : 'movie';
|
|
305
|
+
if (type === 'book') {
|
|
306
|
+
return loadDoubanBookSubject(page, subjectId);
|
|
307
|
+
}
|
|
308
|
+
return loadDoubanMovieSubject(page, subjectId);
|
|
309
|
+
}
|
|
71
310
|
export async function loadDoubanSubjectPhotos(page, subjectId, options = {}) {
|
|
72
311
|
const normalizedId = normalizeDoubanSubjectId(subjectId);
|
|
73
312
|
const type = String(options.type || 'Rb').trim() || 'Rb';
|
|
@@ -293,42 +532,71 @@ export async function loadDoubanMovieHot(page, limit) {
|
|
|
293
532
|
`);
|
|
294
533
|
return Array.isArray(data) ? data : [];
|
|
295
534
|
}
|
|
535
|
+
export function inferDoubanSearchResultType(searchType, item = {}) {
|
|
536
|
+
const fallbackType = String(searchType || '').trim() || 'movie';
|
|
537
|
+
if (fallbackType !== 'movie') {
|
|
538
|
+
return fallbackType;
|
|
539
|
+
}
|
|
540
|
+
const moreUrl = String(item.moreUrl || item.more_url || '').trim();
|
|
541
|
+
const isTv = moreUrl.match(/is_tv:\s*['"]?([01])['"]?/)?.[1] || '';
|
|
542
|
+
if (isTv === '1') {
|
|
543
|
+
return 'tvshow';
|
|
544
|
+
}
|
|
545
|
+
const labels = Array.isArray(item.labels)
|
|
546
|
+
? item.labels
|
|
547
|
+
.map((label) => typeof label === 'string' ? label.trim() : String(label?.text || '').trim())
|
|
548
|
+
.filter(Boolean)
|
|
549
|
+
: [];
|
|
550
|
+
return labels.includes('剧集') ? 'tvshow' : fallbackType;
|
|
551
|
+
}
|
|
296
552
|
export async function searchDouban(page, type, keyword, limit) {
|
|
297
553
|
const safeLimit = clampLimit(limit);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
await
|
|
301
|
-
|
|
554
|
+
const inferDoubanSearchResultTypeSource = inferDoubanSearchResultType.toString();
|
|
555
|
+
const searchUrl = buildDoubanSearchUrl(type, keyword);
|
|
556
|
+
const data = await withDetachedRetry(async () => {
|
|
557
|
+
await page.goto(searchUrl, { waitUntil: 'load', settleMs: 1500 });
|
|
558
|
+
await ensureDoubanReady(page);
|
|
559
|
+
await page.wait({ selector: DOUBAN_SEARCH_READY_SELECTOR, timeout: 8 }).catch(() => { });
|
|
560
|
+
return page.evaluate(`
|
|
302
561
|
(async () => {
|
|
303
562
|
const type = ${JSON.stringify(type)};
|
|
563
|
+
const inferDoubanSearchResultType = ${inferDoubanSearchResultTypeSource};
|
|
304
564
|
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
305
565
|
const seen = new Set();
|
|
306
566
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
567
|
+
const rawItems = Array.isArray(window.__DATA__?.items) ? window.__DATA__.items : [];
|
|
568
|
+
const rawItemsById = new Map(
|
|
569
|
+
rawItems
|
|
570
|
+
.map((item) => [String(item?.id || '').trim(), item])
|
|
571
|
+
.filter(([id]) => id),
|
|
572
|
+
);
|
|
307
573
|
|
|
308
574
|
for (let i = 0; i < 20; i += 1) {
|
|
309
575
|
if (document.querySelector('.item-root .title-text, .item-root .title a')) break;
|
|
310
576
|
await sleep(300);
|
|
311
577
|
}
|
|
312
578
|
|
|
313
|
-
const items = Array.from(document.querySelectorAll('.item-root'));
|
|
579
|
+
const items = Array.from(document.querySelectorAll('.item-root, .result-list .result-item'));
|
|
314
580
|
|
|
315
581
|
const results = [];
|
|
316
582
|
for (const el of items) {
|
|
317
|
-
const titleEl = el.querySelector('.title-text, .title a, a[title]');
|
|
583
|
+
const titleEl = el.querySelector('.title-text, .title a, .title h3 a, h3 a, a[title]');
|
|
318
584
|
const title = normalize(titleEl?.textContent) || normalize(titleEl?.getAttribute('title'));
|
|
319
|
-
let url = titleEl?.getAttribute('href') || '';
|
|
585
|
+
let url = titleEl?.getAttribute('href') || el.querySelector('a[href*="/subject/"]')?.getAttribute('href') || '';
|
|
320
586
|
if (!title || !url) continue;
|
|
321
587
|
if (!url.startsWith('http')) url = 'https://search.douban.com' + url;
|
|
322
588
|
if (!url.includes('/subject/') || seen.has(url)) continue;
|
|
323
589
|
seen.add(url);
|
|
590
|
+
const id = url.match(/subject\\/(\\d+)/)?.[1] || '';
|
|
591
|
+
const rawItem = rawItemsById.get(id) || {};
|
|
324
592
|
const ratingText = normalize(el.querySelector('.rating_nums')?.textContent);
|
|
325
593
|
const abstract = normalize(
|
|
326
|
-
el.querySelector('.meta.abstract, .meta, .abstract, p')?.textContent,
|
|
594
|
+
el.querySelector('.meta.abstract, .meta, .abstract, .subject-abstract, p')?.textContent,
|
|
327
595
|
);
|
|
328
596
|
results.push({
|
|
329
597
|
rank: results.length + 1,
|
|
330
|
-
id
|
|
331
|
-
type,
|
|
598
|
+
id,
|
|
599
|
+
type: inferDoubanSearchResultType(type, rawItem),
|
|
332
600
|
title,
|
|
333
601
|
rating: ratingText.includes('.') ? parseFloat(ratingText) : 0,
|
|
334
602
|
abstract: abstract.slice(0, 100) + (abstract.length > 100 ? '...' : ''),
|
|
@@ -340,6 +608,7 @@ export async function searchDouban(page, type, keyword, limit) {
|
|
|
340
608
|
return results;
|
|
341
609
|
})()
|
|
342
610
|
`);
|
|
611
|
+
});
|
|
343
612
|
return Array.isArray(data) ? data : [];
|
|
344
613
|
}
|
|
345
614
|
/**
|
|
@@ -1,19 +1,105 @@
|
|
|
1
|
+
import vm from 'node:vm';
|
|
1
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getDoubanPhotoExtension,
|
|
5
|
+
inferDoubanSearchResultType,
|
|
6
|
+
loadDoubanSubjectDetail,
|
|
7
|
+
loadDoubanSubjectPhotos,
|
|
8
|
+
normalizeDoubanBookSubject,
|
|
9
|
+
normalizeDoubanSubjectId,
|
|
10
|
+
promoteDoubanPhotoUrl,
|
|
11
|
+
resolveDoubanPhotoAssetUrl,
|
|
12
|
+
searchDouban,
|
|
13
|
+
} from './utils.js';
|
|
14
|
+
|
|
15
|
+
function createFakeNode(text = '', attrs = {}) {
|
|
16
|
+
return {
|
|
17
|
+
textContent: text,
|
|
18
|
+
getAttribute(name) {
|
|
19
|
+
return attrs[name] || '';
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createFakeSearchItem({ title, url, rating, abstract, cover }) {
|
|
25
|
+
return {
|
|
26
|
+
querySelector(selector) {
|
|
27
|
+
if (selector === '.title-text, .title a, .title h3 a, h3 a, a[title]') {
|
|
28
|
+
return createFakeNode(title, { href: url, title });
|
|
29
|
+
}
|
|
30
|
+
if (selector === '.rating_nums') {
|
|
31
|
+
return createFakeNode(rating);
|
|
32
|
+
}
|
|
33
|
+
if (selector === '.meta.abstract, .meta, .abstract, .subject-abstract, p') {
|
|
34
|
+
return createFakeNode(abstract);
|
|
35
|
+
}
|
|
36
|
+
if (selector === 'a[href*="/subject/"]') {
|
|
37
|
+
return createFakeNode('', { href: url });
|
|
38
|
+
}
|
|
39
|
+
if (selector === 'img') {
|
|
40
|
+
return createFakeNode('', { src: cover });
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runSearchEvaluate(script, rawItems, domItems) {
|
|
48
|
+
const document = {
|
|
49
|
+
querySelector(selector) {
|
|
50
|
+
if (selector === '.item-root .title-text, .item-root .title a') {
|
|
51
|
+
return domItems[0]?.querySelector('.title-text, .title a, .title h3 a, h3 a, a[title]') || null;
|
|
52
|
+
}
|
|
53
|
+
if (selector === '.item-root .title-text, .item-root .title a, .result-list .result-item h3 a') {
|
|
54
|
+
return domItems[0]?.querySelector('.title-text, .title a, .title h3 a, h3 a, a[title]') || null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
querySelectorAll(selector) {
|
|
59
|
+
if (selector === '.item-root') {
|
|
60
|
+
return domItems;
|
|
61
|
+
}
|
|
62
|
+
if (selector === '.item-root, .result-list .result-item') {
|
|
63
|
+
return domItems;
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return vm.runInNewContext(script, {
|
|
70
|
+
Map,
|
|
71
|
+
Promise,
|
|
72
|
+
document,
|
|
73
|
+
window: { __DATA__: { items: rawItems } },
|
|
74
|
+
location: {
|
|
75
|
+
href: 'https://search.douban.com/movie/subject_search?search_text=%E5%B0%84%E9%9B%95%E8%8B%B1%E9%9B%84%E4%BC%A0',
|
|
76
|
+
origin: 'https://search.douban.com',
|
|
77
|
+
},
|
|
78
|
+
setTimeout(fn) {
|
|
79
|
+
fn();
|
|
80
|
+
return 0;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
3
85
|
describe('douban utils', () => {
|
|
4
86
|
it('normalizes valid subject ids', () => {
|
|
5
87
|
expect(normalizeDoubanSubjectId(' 30382501 ')).toBe('30382501');
|
|
6
88
|
});
|
|
89
|
+
|
|
7
90
|
it('rejects invalid subject ids', () => {
|
|
8
91
|
expect(() => normalizeDoubanSubjectId('tt30382501')).toThrow('Invalid Douban subject ID');
|
|
9
92
|
});
|
|
93
|
+
|
|
10
94
|
it('promotes thumbnail urls to large photo urls', () => {
|
|
11
95
|
expect(promoteDoubanPhotoUrl('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp')).toBe('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp');
|
|
12
96
|
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
97
|
});
|
|
98
|
+
|
|
14
99
|
it('rejects non-http photo urls during promotion', () => {
|
|
15
100
|
expect(promoteDoubanPhotoUrl('data:image/gif;base64,abc')).toBe('');
|
|
16
101
|
});
|
|
102
|
+
|
|
17
103
|
it('prefers lazy-loaded photo urls over data placeholders', () => {
|
|
18
104
|
expect(resolveDoubanPhotoAssetUrl([
|
|
19
105
|
'',
|
|
@@ -21,9 +107,11 @@ describe('douban utils', () => {
|
|
|
21
107
|
'data:image/gif;base64,abc',
|
|
22
108
|
], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('https://img1.doubanio.com/view/photo/m/public/p2913450214.webp');
|
|
23
109
|
});
|
|
110
|
+
|
|
24
111
|
it('drops unsupported non-http photo urls when no real image url exists', () => {
|
|
25
112
|
expect(resolveDoubanPhotoAssetUrl(['data:image/gif;base64,abc', 'blob:https://movie.douban.com/example'], 'https://movie.douban.com/subject/30382501/photos?type=Rb')).toBe('');
|
|
26
113
|
});
|
|
114
|
+
|
|
27
115
|
it('removes the default photo cap when scanning for an exact photo id', async () => {
|
|
28
116
|
const evaluate = vi.fn()
|
|
29
117
|
.mockResolvedValueOnce({ blocked: false, title: 'Some Movie', href: 'https://movie.douban.com/subject/30382501/photos?type=Rb' })
|
|
@@ -57,8 +145,215 @@ describe('douban utils', () => {
|
|
|
57
145
|
expect(scanScript).toContain(`const limit = ${Number.MAX_SAFE_INTEGER};`);
|
|
58
146
|
expect(scanScript).toContain('for (let pageIndex = 0; photos.length < limit; pageIndex += 1)');
|
|
59
147
|
});
|
|
148
|
+
|
|
60
149
|
it('keeps image extensions when download urls contain query params', () => {
|
|
61
150
|
expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.webp?foo=1')).toBe('.webp');
|
|
62
151
|
expect(getDoubanPhotoExtension('https://img1.doubanio.com/view/photo/l/public/p2913450214.jpeg')).toBe('.jpeg');
|
|
63
152
|
});
|
|
153
|
+
|
|
154
|
+
it('maps tv series results to tvshow in searchDouban output', async () => {
|
|
155
|
+
const domItems = [
|
|
156
|
+
createFakeSearchItem({
|
|
157
|
+
title: '射雕英雄传 (2017)',
|
|
158
|
+
url: 'https://movie.douban.com/subject/26663086/',
|
|
159
|
+
rating: '7.9',
|
|
160
|
+
abstract: '中国大陆 / 剧情 / 武侠 / 古装 / 45分钟',
|
|
161
|
+
cover: 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2411844029.webp',
|
|
162
|
+
}),
|
|
163
|
+
createFakeSearchItem({
|
|
164
|
+
title: '射雕英雄传:侠之大者 (2025)',
|
|
165
|
+
url: 'https://movie.douban.com/subject/36289423/',
|
|
166
|
+
rating: '5.2',
|
|
167
|
+
abstract: '中国大陆 / 武侠 / 146分钟',
|
|
168
|
+
cover: 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2917502509.webp',
|
|
169
|
+
}),
|
|
170
|
+
];
|
|
171
|
+
const rawItems = [
|
|
172
|
+
{
|
|
173
|
+
id: 26663086,
|
|
174
|
+
labels: [{ text: '剧集' }, { text: '可播放' }],
|
|
175
|
+
more_url: "onclick=\"moreurl(this,{from:'mv_subject_search',subject_id:'26663086',is_tv:'1'})\"",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 36289423,
|
|
179
|
+
labels: [{ text: '可播放' }],
|
|
180
|
+
more_url: "onclick=\"moreurl(this,{from:'mv_subject_search',subject_id:'36289423',is_tv:'0'})\"",
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
const page = {
|
|
184
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
185
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
186
|
+
evaluate: vi.fn()
|
|
187
|
+
.mockResolvedValueOnce({ blocked: false, title: '射雕英雄传 - 电影 - 豆瓣搜索', href: 'https://search.douban.com/movie/subject_search?search_text=%E5%B0%84%E9%9B%95%E8%8B%B1%E9%9B%84%E4%BC%A0' })
|
|
188
|
+
.mockImplementationOnce((script) => runSearchEvaluate(script, rawItems, domItems)),
|
|
189
|
+
};
|
|
190
|
+
await expect(searchDouban(page, 'movie', '射雕英雄传', 20)).resolves.toMatchObject([
|
|
191
|
+
{ id: '26663086', type: 'tvshow', title: '射雕英雄传 (2017)' },
|
|
192
|
+
{ id: '36289423', type: 'movie', title: '射雕英雄传:侠之大者 (2025)' },
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('normalizes douban book subject raw data into structured fields', () => {
|
|
197
|
+
const normalized = normalizeDoubanBookSubject({
|
|
198
|
+
id: '2567698',
|
|
199
|
+
title: '小狗钱钱',
|
|
200
|
+
subtitle: '让孩子和家长共同成长的财商童话',
|
|
201
|
+
originalTitle: 'Ein Hund namens Money',
|
|
202
|
+
infoText: `
|
|
203
|
+
作者: [德] 博多·舍费尔
|
|
204
|
+
出版社: 南海出版公司
|
|
205
|
+
副标题: 让孩子和家长共同成长的财商童话
|
|
206
|
+
原作名: Ein Hund namens Money
|
|
207
|
+
译者: 王钟欣 / 余茜
|
|
208
|
+
出版年: 2014-1-1
|
|
209
|
+
页数: 208
|
|
210
|
+
定价: 26.00元
|
|
211
|
+
装帧: 平装
|
|
212
|
+
丛书: 新经典文库·爱心树童书
|
|
213
|
+
ISBN: 9787544270871
|
|
214
|
+
`,
|
|
215
|
+
rating: '8.9',
|
|
216
|
+
ratingCount: '12345',
|
|
217
|
+
summary: '理财启蒙故事',
|
|
218
|
+
cover: 'https://img9.doubanio.com/view/subject/l/public/s29618581.jpg',
|
|
219
|
+
url: 'https://book.douban.com/subject/2567698/',
|
|
220
|
+
});
|
|
221
|
+
expect(normalized).toMatchObject({
|
|
222
|
+
id: '2567698',
|
|
223
|
+
type: 'book',
|
|
224
|
+
title: '小狗钱钱',
|
|
225
|
+
subtitle: '让孩子和家长共同成长的财商童话',
|
|
226
|
+
originalTitle: 'Ein Hund namens Money',
|
|
227
|
+
authors: ['[德] 博多·舍费尔'],
|
|
228
|
+
translators: ['王钟欣', '余茜'],
|
|
229
|
+
publisher: '南海出版公司',
|
|
230
|
+
publishDate: '2014-1-1',
|
|
231
|
+
publishYear: '2014',
|
|
232
|
+
pageCount: 208,
|
|
233
|
+
binding: '平装',
|
|
234
|
+
price: '26.00元',
|
|
235
|
+
series: '新经典文库·爱心树童书',
|
|
236
|
+
isbn13: '9787544270871',
|
|
237
|
+
rating: 8.9,
|
|
238
|
+
ratingCount: 12345,
|
|
239
|
+
summary: '理财启蒙故事',
|
|
240
|
+
cover: 'https://img9.doubanio.com/view/subject/l/public/s29618581.jpg',
|
|
241
|
+
url: 'https://book.douban.com/subject/2567698/',
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('loads book subject details from book.douban.com when type=book', async () => {
|
|
246
|
+
const page = {
|
|
247
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
248
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
249
|
+
evaluate: vi.fn()
|
|
250
|
+
.mockResolvedValueOnce({ blocked: false, title: '小狗钱钱 (豆瓣)', href: 'https://book.douban.com/subject/2567698/' })
|
|
251
|
+
.mockResolvedValueOnce({
|
|
252
|
+
id: '2567698',
|
|
253
|
+
title: '小狗钱钱',
|
|
254
|
+
subtitle: '',
|
|
255
|
+
originalTitle: '',
|
|
256
|
+
infoText: `
|
|
257
|
+
作者: [德] 博多·舍费尔
|
|
258
|
+
出版社: 南海出版公司
|
|
259
|
+
出版年: 2014-1-1
|
|
260
|
+
ISBN: 9787544270871
|
|
261
|
+
`,
|
|
262
|
+
rating: '8.9',
|
|
263
|
+
ratingCount: '12345',
|
|
264
|
+
summary: '理财启蒙故事',
|
|
265
|
+
cover: 'https://img9.doubanio.com/view/subject/l/public/s29618581.jpg',
|
|
266
|
+
url: 'https://book.douban.com/subject/2567698/',
|
|
267
|
+
}),
|
|
268
|
+
};
|
|
269
|
+
const detail = await loadDoubanSubjectDetail(page, '2567698', 'book');
|
|
270
|
+
expect(page.goto).toHaveBeenCalledWith('https://book.douban.com/subject/2567698/', {
|
|
271
|
+
waitUntil: 'load',
|
|
272
|
+
settleMs: 1500,
|
|
273
|
+
});
|
|
274
|
+
expect(page.wait).toHaveBeenCalledWith({ selector: 'h1 span, #info', timeout: 8 });
|
|
275
|
+
expect(detail).toMatchObject({
|
|
276
|
+
id: '2567698',
|
|
277
|
+
type: 'book',
|
|
278
|
+
title: '小狗钱钱',
|
|
279
|
+
authors: ['[德] 博多·舍费尔'],
|
|
280
|
+
publisher: '南海出版公司',
|
|
281
|
+
isbn13: '9787544270871',
|
|
282
|
+
rating: 8.9,
|
|
283
|
+
ratingCount: 12345,
|
|
284
|
+
url: 'https://book.douban.com/subject/2567698/',
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('retries transient detached navigation errors when loading douban search results', async () => {
|
|
289
|
+
const page = {
|
|
290
|
+
goto: vi.fn()
|
|
291
|
+
.mockRejectedValueOnce(new Error('Detached while handling command'))
|
|
292
|
+
.mockResolvedValueOnce(undefined),
|
|
293
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
294
|
+
evaluate: vi.fn()
|
|
295
|
+
.mockResolvedValueOnce({
|
|
296
|
+
blocked: false,
|
|
297
|
+
title: '经济学思维 - 豆瓣搜索',
|
|
298
|
+
href: 'https://search.douban.com/book/subject_search?search_text=%E7%BB%8F%E6%B5%8E%E5%AD%A6%E6%80%9D%E7%BB%B4&cat=1001',
|
|
299
|
+
})
|
|
300
|
+
.mockResolvedValueOnce([
|
|
301
|
+
{
|
|
302
|
+
rank: 1,
|
|
303
|
+
id: '26895402',
|
|
304
|
+
type: 'book',
|
|
305
|
+
title: '经济学思维',
|
|
306
|
+
rating: 7.9,
|
|
307
|
+
abstract: '李子畅 / 中信出版社 / 2016-7',
|
|
308
|
+
url: 'https://book.douban.com/subject/26895402/',
|
|
309
|
+
cover: 'https://img1.doubanio.com/view/subject/m/public/s29000000.jpg',
|
|
310
|
+
},
|
|
311
|
+
]),
|
|
312
|
+
};
|
|
313
|
+
const results = await searchDouban(page, 'book', '经济学思维', 3);
|
|
314
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://search.douban.com/book/subject_search?search_text=%E7%BB%8F%E6%B5%8E%E5%AD%A6%E6%80%9D%E7%BB%B4&cat=1001', {
|
|
315
|
+
waitUntil: 'load',
|
|
316
|
+
settleMs: 1500,
|
|
317
|
+
});
|
|
318
|
+
expect(page.goto).toHaveBeenCalledTimes(2);
|
|
319
|
+
expect(page.wait).toHaveBeenCalledWith({
|
|
320
|
+
selector: '.item-root .title-text, .item-root .title a, .result-list .result-item h3 a',
|
|
321
|
+
timeout: 8,
|
|
322
|
+
});
|
|
323
|
+
expect(results).toEqual([
|
|
324
|
+
{
|
|
325
|
+
rank: 1,
|
|
326
|
+
id: '26895402',
|
|
327
|
+
type: 'book',
|
|
328
|
+
title: '经济学思维',
|
|
329
|
+
rating: 7.9,
|
|
330
|
+
abstract: '李子畅 / 中信出版社 / 2016-7',
|
|
331
|
+
url: 'https://book.douban.com/subject/26895402/',
|
|
332
|
+
cover: 'https://img1.doubanio.com/view/subject/m/public/s29000000.jpg',
|
|
333
|
+
},
|
|
334
|
+
]);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('inferDoubanSearchResultType', () => {
|
|
339
|
+
it('returns tvshow for movie search results marked as TV', () => {
|
|
340
|
+
expect(inferDoubanSearchResultType('movie', {
|
|
341
|
+
moreUrl: "onclick=\"moreurl(this,{is_tv:'1'})\"",
|
|
342
|
+
labels: [{ text: '剧集' }],
|
|
343
|
+
})).toBe('tvshow');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('returns movie when a movie search result has no TV signal', () => {
|
|
347
|
+
expect(inferDoubanSearchResultType('movie', {
|
|
348
|
+
moreUrl: "onclick=\"moreurl(this,{is_tv:'0'})\"",
|
|
349
|
+
labels: [{ text: '可播放' }],
|
|
350
|
+
})).toBe('movie');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('preserves non-movie search types', () => {
|
|
354
|
+
expect(inferDoubanSearchResultType('book', {
|
|
355
|
+
moreUrl: '',
|
|
356
|
+
labels: [{ text: '图书' }],
|
|
357
|
+
})).toBe('book');
|
|
358
|
+
});
|
|
64
359
|
});
|