|
1 | 1 | import { cli, Strategy } from '@jackwener/opencli/registry'; |
| 2 | +import { loadDoubanSubjectDetail } from './utils.js'; |
| 3 | + |
2 | 4 | cli({ |
3 | 5 | site: 'douban', |
4 | 6 | name: 'subject', |
5 | | - description: '获取电影详情', |
| 7 | + description: '获取豆瓣条目详情', |
6 | 8 | domain: 'movie.douban.com', |
7 | 9 | strategy: Strategy.COOKIE, |
8 | 10 | browser: true, |
| 11 | + navigateBefore: false, |
9 | 12 | args: [ |
10 | | - { name: 'id', required: true, positional: true, help: '电影 ID' }, |
| 13 | + { name: 'id', required: true, positional: true, help: '豆瓣条目 ID' }, |
| 14 | + { name: 'type', default: 'movie', choices: ['movie', 'book'], help: '条目类型(movie=电影, book=图书)' }, |
11 | 15 | ], |
12 | 16 | columns: [ |
13 | 17 | 'id', |
| 18 | + 'type', |
14 | 19 | 'title', |
| 20 | + 'subtitle', |
15 | 21 | 'originalTitle', |
| 22 | + 'authors', |
| 23 | + 'translators', |
| 24 | + 'publisher', |
| 25 | + 'publishDate', |
| 26 | + 'publishYear', |
| 27 | + 'pageCount', |
| 28 | + 'binding', |
| 29 | + 'price', |
| 30 | + 'series', |
| 31 | + 'isbn10', |
| 32 | + 'isbn13', |
16 | 33 | 'year', |
17 | 34 | 'rating', |
18 | 35 | 'ratingCount', |
|
24 | 41 | 'summary', |
25 | 42 | 'url', |
26 | 43 | ], |
27 | | - pipeline: [ |
28 | | - { navigate: 'https://movie.douban.com/subject/${{ args.id }}' }, |
29 | | - { evaluate: `(async () => { |
30 | | - const id = '\${{ args.id }}'; |
31 | | -
|
32 | | - // Wait for page to load |
33 | | - await new Promise(r => setTimeout(r, 2000)); |
34 | | -
|
35 | | - // Extract title - v:itemreviewed contains "中文名 OriginalName" |
36 | | - const titleEl = document.querySelector('span[property="v:itemreviewed"]'); |
37 | | - const fullTitle = titleEl?.textContent?.trim() || ''; |
38 | | -
|
39 | | - // Split title and originalTitle |
40 | | - // Douban format: "中文名 OriginalName" - split by first space that separates CJK from non-CJK |
41 | | - let title = fullTitle; |
42 | | - let originalTitle = ''; |
43 | | - const titleMatch = fullTitle.match(/^([\\u4e00-\\u9fff\\u3000-\\u303f\\uff00-\\uffef]+(?:\\s*[\\u4e00-\\u9fff\\u3000-\\u303f\\uff00-\\uffef·::!?]+)*)\\s+(.+)$/); |
44 | | - if (titleMatch) { |
45 | | - title = titleMatch[1].trim(); |
46 | | - originalTitle = titleMatch[2].trim(); |
47 | | - } |
48 | | -
|
49 | | - // Extract year |
50 | | - const yearEl = document.querySelector('.year'); |
51 | | - const year = yearEl?.textContent?.trim().replace(/[()()]/g, '') || ''; |
52 | | -
|
53 | | - // Extract rating |
54 | | - const ratingEl = document.querySelector('strong[property="v:average"]'); |
55 | | - const rating = parseFloat(ratingEl?.textContent || '0'); |
56 | | -
|
57 | | - // Extract rating count |
58 | | - const ratingCountEl = document.querySelector('span[property="v:votes"]'); |
59 | | - const ratingCount = parseInt(ratingCountEl?.textContent || '0', 10); |
60 | | -
|
61 | | - // Extract genres |
62 | | - const genreEls = document.querySelectorAll('span[property="v:genre"]'); |
63 | | - const genres = Array.from(genreEls).map(el => el.textContent?.trim()).filter(Boolean).join(','); |
64 | | -
|
65 | | - // Extract directors |
66 | | - const directorEls = document.querySelectorAll('a[rel="v:directedBy"]'); |
67 | | - const directors = Array.from(directorEls).map(el => el.textContent?.trim()).filter(Boolean).join(','); |
68 | | -
|
69 | | - // Extract casts |
70 | | - const castEls = document.querySelectorAll('a[rel="v:starring"]'); |
71 | | - const casts = Array.from(castEls).slice(0, 5).map(el => el.textContent?.trim()).filter(Boolean); |
72 | | -
|
73 | | - // Extract info section for country and duration |
74 | | - const infoEl = document.querySelector('#info'); |
75 | | - const infoText = infoEl?.textContent || ''; |
76 | | -
|
77 | | - // Extract country/region from #info as list |
78 | | - let country = []; |
79 | | - const countryMatch = infoText.match(/制片国家\\/地区:\\s*([^\\n]+)/); |
80 | | - if (countryMatch) { |
81 | | - country = countryMatch[1].trim().split(/\\s*\\/\\s*/).filter(Boolean); |
82 | | - } |
83 | | -
|
84 | | - // Extract duration from #info as pure number in min |
85 | | - const durationEl = document.querySelector('span[property="v:runtime"]'); |
86 | | - let durationRaw = durationEl?.textContent?.trim() || ''; |
87 | | - if (!durationRaw) { |
88 | | - const durationMatch = infoText.match(/片长:\\s*([^\\n]+)/); |
89 | | - if (durationMatch) { |
90 | | - durationRaw = durationMatch[1].trim(); |
91 | | - } |
92 | | - } |
93 | | - const durationNumMatch = durationRaw.match(/(\\d+)/); |
94 | | - const duration = durationNumMatch ? parseInt(durationNumMatch[1], 10) : null; |
95 | | -
|
96 | | - // Extract summary |
97 | | - const summaryEl = document.querySelector('span[property="v:summary"]'); |
98 | | - const summary = summaryEl?.textContent?.trim() || ''; |
99 | | -
|
100 | | - return [{ |
101 | | - id, |
102 | | - title, |
103 | | - originalTitle, |
104 | | - year, |
105 | | - rating, |
106 | | - ratingCount, |
107 | | - genres, |
108 | | - directors, |
109 | | - casts, |
110 | | - country, |
111 | | - duration, |
112 | | - summary: summary.substring(0, 200), |
113 | | - url: \`https://movie.douban.com/subject/\${id}\` |
114 | | - }]; |
115 | | -})() |
116 | | -` }, |
117 | | - ], |
| 44 | + func: async (page, args) => [await loadDoubanSubjectDetail(page, args.id, args.type)], |
118 | 45 | }); |
0 commit comments