ค้นหาหนังและซีรีส์ (Wikipedia, TVmaze) เปิดค้นใน Netflix YouTube Disney+ Prime ทำ watchlist ให้คะแนน และให้ Claude แนะนำเรื่องต่อไป

ค้นหนังจาก Wikipedia (ไทยและอังกฤษ) และซีรีส์จาก TVmaze (แนว คะแนน ช่อง) ดูเรื่องย่อ เปิดหน้าค้นหาบน Netflix, YouTube, Disney+, Prime Video หรือ Apple TV ดูตัวอย่างหนัง ทำ watchlist บันทึกเรื่องที่ดูแล้วพร้อมให้คะแนน และให้ Claude แนะนำเรื่องต่อไปจากเรื่องที่ชอบ
| คำสั่ง | ทำอะไร | ||||
|---|---|---|---|---|---|
/movie search <ชื่อ> | ค้นหนัง | ||||
/movie series <ชื่อ> | ค้นซีรีส์ | ||||
/movie info <ชื่อเรื่อง> | เรื่องย่อ | ||||
| `/movie watch <ชื่อ> [netflix\ | youtube\ | disney\ | prime\ | apple]` | เปิดหน้าค้นหาบนแพลตฟอร์ม |
/movie trailer <ชื่อ> | ค้นตัวอย่างบน YouTube | ||||
/movie add / remove / list | watchlist | ||||
/movie watched <ชื่อ> [1-10] | บันทึกว่าดูแล้ว + คะแนน | ||||
/movie history | ประวัติ | ||||
/movie recommend | ให้ Claude แนะนำ |
/plugin install movie-finder-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
Mod เล่นวิดีโอเองไม่ได้ และไม่รู้ว่าเรื่องไหนมีบนแพลตฟอร์มไหนในไทย (ไม่มี API สาธารณะ) จึงเปิดหน้าค้นหาของแพลตฟอร์มนั้นให้ ต้องมีบัญชีของแพลตฟอร์มเอง
hooks/register.tsx 232 lines1/**
2 * ============================================================================
3 * MOD 30: Movie Finder TH - ค้นหนัง/ซีรีส์ ทำ watchlist และเปิดดูบนแพลตฟอร์ม (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: Letterboxd, JustWatch
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. ค้นหนังจาก Wikipedia (ไทย + อังกฤษ) และซีรีส์จาก TVmaze พร้อมเรื่องย่อ
9 * 2. เปิดหน้าค้นหาบน Netflix, YouTube, Disney+, Prime Video, Apple TV หรือดูตัวอย่าง (trailer)
10 * 3. Watchlist, บันทึกว่าดูแล้วพร้อมให้คะแนน, ประวัติ
11 * 4. ให้ Claude แนะนำเรื่องต่อไปจากเรื่องที่ชอบ
12 *
13 * หมายเหตุ: Mod เล่นวิดีโอเองไม่ได้ และไม่รู้ว่าเรื่องไหนมีบนแพลตฟอร์มไหน (ไม่มี API สาธารณะ)
14 * จึงเปิดหน้าค้นหาของแพลตฟอร์มนั้นให้ ต้องมีบัญชีของแพลตฟอร์มเอง
15 */
16
17import type { EngineInterface, Register } from 'claude-code'
18
19interface Title {
20 name: string
21 kind: 'movie' | 'series'
22 summary: string
23 source: string
24 year?: string
25}
26interface Entry {
27 name: string
28 kind: 'movie' | 'series'
29 addedAt: number
30 watchedAt?: number
31 rating?: number // 1-10
32}
33
34const UA = { 'User-Agent': 'claude-code-movie-finder-th/1.0 (educational mod)' }
35const PLATFORMS: Record<string, { name: string; url: (q: string) => string }> = {
36 netflix: { name: 'Netflix', url: q => `https://www.netflix.com/search?q=${q}` },
37 youtube: { name: 'YouTube', url: q => `https://www.youtube.com/results?search_query=${q}` },
38 disney: { name: 'Disney+', url: q => `https://www.disneyplus.com/search?q=${q}` },
39 prime: { name: 'Prime Video', url: q => `https://www.primevideo.com/search/ref=atv_nb_sr?phrase=${q}` },
40 apple: { name: 'Apple TV', url: q => `https://tv.apple.com/search?term=${q}` },
41}
42
43let list: Entry[] = []
44let isWindows = false
45let isMac = false
46
47async function save($: EngineInterface): Promise<void> {
48 await $.store.set('movies', list)
49}
50
51// ==========================================================================
52// บทที่ 1: ค้นหนังจาก Wikipedia (กรองเฉพาะหน้าที่คำอธิบายบอกว่าเป็นภาพยนตร์)
53// ==========================================================================
54async function getJson($: EngineInterface, url: string): Promise<unknown> {
55 const res = await $.http.fetch(url, { headers: UA })
56 if (!res.ok) throw new Error(`HTTP ${res.status}`)
57 return JSON.parse(res.text)
58}
59
60async function searchMovies($: EngineInterface, query: string): Promise<Title[]> {
61 const isThai = /[-]/.test(query)
62 const sites = isThai ? ['th', 'en'] : ['en', 'th']
63 const found: Title[] = []
64 for (const lang of sites) {
65 const body = (await getJson($, `https://${lang}.wikipedia.org/w/rest.php/v1/search/title?q=${encodeURIComponent(query)}&limit=8`).catch(() => ({ pages: [] }))) as { pages: { title: string; description?: string | null }[] }
66 for (const p of body.pages) {
67 const d = p.description ?? ''
68 if (!/film|movie|ภาพยนตร์|หนัง/i.test(d)) continue
69 if (found.some(f => f.name === p.title)) continue
70 const buddhistYear = /พ\.ศ\.\s*(\d{4})/.exec(d)?.[1] // วิกิไทยใช้ปี พ.ศ.
71 found.push({ name: p.title, kind: 'movie', summary: d, source: `Wikipedia (${lang})`, year: buddhistYear ? `พ.ศ. ${buddhistYear}` : /\b(19|20)\d{2}\b/.exec(d)?.[0] })
72 }
73 if (found.length >= 5) break
74 }
75 return found.slice(0, 6)
76}
77
78// ==========================================================================
79// บทที่ 2: ค้นซีรีส์จาก TVmaze (ฟรี ไม่ต้องใช้ key มีคะแนน แนว และช่อง/แพลตฟอร์ม)
80// ==========================================================================
81async function searchSeries($: EngineInterface, query: string): Promise<Title[]> {
82 const body = (await getJson($, `https://api.tvmaze.com/search/shows?q=${encodeURIComponent(query)}`)) as {
83 show: { name: string; premiered?: string | null; genres?: string[]; rating?: { average?: number | null }; network?: { name?: string } | null; webChannel?: { name?: string } | null; summary?: string | null }
84 }[]
85 return body.slice(0, 6).map(({ show: s }) => ({
86 name: s.name,
87 kind: 'series' as const,
88 year: s.premiered?.slice(0, 4),
89 source: 'TVmaze',
90 summary: [s.genres?.join('/'), s.rating?.average ? `⭐ ${s.rating.average}` : '', s.webChannel?.name ?? s.network?.name ?? '', (s.summary ?? '').replace(/<[^>]+>/g, '').slice(0, 140)].filter(Boolean).join(' · '),
91 }))
92}
93
94// เรื่องย่อจาก Wikipedia
95async function summaryOf($: EngineInterface, title: string): Promise<string> {
96 for (const lang of /[-]/.test(title) ? ['th', 'en'] : ['en', 'th']) {
97 const body = (await getJson($, `https://${lang}.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title.replace(/ /g, '_'))}`).catch(() => undefined)) as { title?: string; description?: string; extract?: string; content_urls?: { desktop?: { page?: string } } } | undefined
98 if (body?.extract) return `🎬 ${body.title}\n${body.description ?? ''}\n\n${body.extract.slice(0, 700)}\n\n${body.content_urls?.desktop?.page ?? ''}`
99 }
100 return `ไม่พบเรื่องย่อของ "${title}" ลอง /movie search ${title}`
101}
102
103// ==========================================================================
104// บทที่ 3: เปิดหน้าเว็บด้วย browser ของเครื่อง (ส่ง URL ผ่าน env กันคำสั่งแทรก)
105// ==========================================================================
106async function openUrl($: EngineInterface, url: string): Promise<void> {
107 const argv = isWindows ? ['powershell', '-NoProfile', '-NonInteractive', '-Command', 'Start-Process $env:MF_URL'] : isMac ? ['open', url] : ['xdg-open', url]
108 const r = await $.process.run(argv, { env: { MF_URL: url }, timeoutMs: 20_000 })
109 if (r.exitCode !== 0) throw new Error(r.stderr.trim() || `exit ${r.exitCode}`)
110}
111
112function titleLine(t: Title, i: number): string {
113 return `${i + 1}. ${t.kind === 'series' ? '📺' : '🎬'} ${t.name}${t.year ? ` (${t.year})` : ''}\n ${t.summary}`
114}
115
116const HELP = `🎬 Movie Finder TH (Mod 30)
117========================================
118/movie search <ชื่อ> ค้นหนัง (Wikipedia ไทย/อังกฤษ)
119/movie series <ชื่อ> ค้นซีรีส์ (TVmaze: แนว คะแนน ช่อง)
120/movie info <ชื่อเรื่อง> เรื่องย่อ
121/movie watch <ชื่อ> [แพลตฟอร์ม] เปิดหน้าค้นหา: ${Object.keys(PLATFORMS).join(' ')} (ค่าเริ่มต้น netflix)
122/movie trailer <ชื่อ> ค้นตัวอย่างบน YouTube
123/movie add <ชื่อ> | remove <ชื่อ> | list watchlist
124/movie watched <ชื่อ> [คะแนน 1-10] บันทึกว่าดูแล้ว
125/movie history
126/movie recommend ให้ Claude แนะนำจากเรื่องที่ให้คะแนนสูง`
127
128export const register: Register = on => {
129 on('session.start', async ($, e, next) => {
130 isWindows = /^[A-Za-z]:/.test(e.cwd)
131 isMac = !isWindows && e.cwd.startsWith('/Users/')
132 const saved = await $.store.get('movies')
133 list = Array.isArray(saved) ? (saved as Entry[]) : []
134 await $.command.register({ name: 'movie', description: 'ค้นหนัง/ซีรีส์ watchlist และเปิดดูบน Netflix YouTube ฯลฯ (/movie help)', argumentHint: 'search|series|info|watch|add|watched|recommend' })
135 return next(e)
136 })
137
138 // ==========================================================================
139 // บทที่ 4: คำสั่ง /movie
140 // ==========================================================================
141 on('command.run', { command: 'movie' }, async ($, e) => {
142 const [sub = 'help', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
143 const query = rest.join(' ')
144 const now = await $.clock.now()
145
146 try {
147 switch (sub) {
148 case 'search':
149 case 'series': {
150 if (!query) return { text: `วิธีใช้: /movie ${sub} <ชื่อ>` }
151 const results = sub === 'search' ? await searchMovies($, query) : await searchSeries($, query)
152 if (results.length === 0) return { text: `ไม่พบ "${query}"${sub === 'search' ? ' (ถ้าเป็นซีรีส์ ลอง /movie series)' : ''}` }
153 return { text: [`🔎 ผลค้นหา "${query}"`, ...results.map(titleLine), '', 'ดูเรื่องย่อ: /movie info <ชื่อ> | เปิดดู: /movie watch <ชื่อ>'].join('\n') }
154 }
155 case 'info':
156 return { text: query ? await summaryOf($, query) : 'วิธีใช้: /movie info Inception' }
157 case 'watch':
158 case 'trailer': {
159 const last = rest.at(-1)?.toLowerCase() ?? ''
160 const platform = sub === 'trailer' ? 'youtube' : PLATFORMS[last] ? last : 'netflix'
161 const name = sub === 'watch' && PLATFORMS[last] ? rest.slice(0, -1).join(' ') : query
162 if (!name) return { text: `วิธีใช้: /movie ${sub} <ชื่อ>${sub === 'watch' ? ' [netflix|youtube|disney|prime|apple]' : ''}` }
163 const p = PLATFORMS[platform] ?? PLATFORMS.netflix!
164 const url = p.url(encodeURIComponent(sub === 'trailer' ? `${name} trailer` : name))
165 await openUrl($, url)
166 return { text: `▶️ เปิด${sub === 'trailer' ? 'ตัวอย่าง' : 'หน้าค้นหา'} "${name}" บน ${p.name}\n${url}` }
167 }
168 case 'add': {
169 if (!query) return { text: 'วิธีใช้: /movie add <ชื่อ>' }
170 if (list.some(x => x.name === query)) return { text: `ℹ️ "${query}" อยู่ใน watchlist แล้ว` }
171 list.push({ name: query, kind: 'movie', addedAt: now })
172 await save($)
173 return { text: `➕ เพิ่ม "${query}" ใน watchlist แล้ว (${list.filter(x => !x.watchedAt).length} เรื่องรอดู)` }
174 }
175 case 'remove':
176 list = list.filter(x => x.name !== query)
177 await save($)
178 return { text: `🗑️ เอา "${query}" ออกแล้ว` }
179 case 'list': {
180 const todo = list.filter(x => !x.watchedAt)
181 return { text: todo.length ? ['📋 Watchlist (รอดู)', ...todo.map(x => ` • ${x.name}`)].join('\n') : 'Watchlist ว่าง เพิ่มด้วย /movie add <ชื่อ>' }
182 }
183 case 'watched': {
184 const maybeRating = Number(rest.at(-1))
185 const hasRating = Number.isInteger(maybeRating) && maybeRating >= 1 && maybeRating <= 10 && rest.length > 1
186 const name = hasRating ? rest.slice(0, -1).join(' ') : query
187 if (!name) return { text: 'วิธีใช้: /movie watched Inception 9' }
188 const entry = list.find(x => x.name === name) ?? { name, kind: 'movie' as const, addedAt: now }
189 entry.watchedAt = now
190 if (hasRating) entry.rating = maybeRating
191 list = [...list.filter(x => x.name !== name), entry]
192 await save($)
193 return { text: `✅ ดู "${name}" แล้ว${hasRating ? ` ให้ ${'⭐'.repeat(Math.round(maybeRating / 2))} ${maybeRating}/10` : ''}` }
194 }
195 case 'history': {
196 const done = list.filter(x => x.watchedAt).sort((a, b) => (b.watchedAt ?? 0) - (a.watchedAt ?? 0))
197 return { text: done.length ? ['🍿 ดูแล้ว', ...done.map(x => ` ${new Date(x.watchedAt ?? 0).toLocaleDateString('th-TH')} ${x.name}${x.rating ? ` ${x.rating}/10` : ''}`)].join('\n') : 'ยังไม่มีประวัติ' }
198 }
199 case 'recommend': {
200 const liked = list.filter(x => (x.rating ?? 0) >= 7).map(x => `${x.name} (${x.rating}/10)`)
201 const disliked = list.filter(x => x.rating !== undefined && x.rating <= 4).map(x => x.name)
202 if (liked.length === 0) return { text: 'ให้คะแนนเรื่องที่ชอบก่อน เช่น /movie watched Inception 9' }
203 const ask = (model: string) =>
204 $.model.complete({
205 model,
206 system: 'You recommend films and series. Answer in Thai. Suggest 5 real, well-known titles that are not in the lists given, each with the year and one short line on why it fits. Do not invent titles.',
207 prompt: `Liked: ${liked.join(', ')}\nDisliked: ${disliked.join(', ') || 'none'}\nAlready seen or listed: ${list.map(x => x.name).join(', ')}`,
208 maxTokens: 800,
209 })
210 // ใช้ haiku ก่อน ถ้าใช้ไม่ได้ (เช่น องค์กรไม่อนุญาต) ใช้ model ของ session
211 let r = await ask('haiku').catch(() => undefined)
212 if (!r?.isAnswered) r = await ask(await $.session.model())
213 return { text: r.isAnswered ? `🎯 แนะนำจากเรื่องที่คุณชอบ\n${r.text.trim()}` : `❌ ขอคำแนะนำไม่สำเร็จ (${r.reason})` }
214 }
215 default:
216 return { text: HELP }
217 }
218 } catch (err) {
219 return { text: `❌ ${sub} ไม่สำเร็จ: ${err instanceof Error ? err.message : String(err)}` }
220 }
221 })
222}
223
224/**
225 * ========================================================================
226 * การบ้าน / ต่อยอด
227 * ========================================================================
228 * 1. ใช้ TMDB API (ต้องสมัครรับ key ฟรี) จะได้โปสเตอร์ คะแนน และรู้ว่าเรื่องไหนมีบนแพลตฟอร์มไหนในไทย
229 * 2. แผงแสดง watchlist พร้อมปุ่มกดเปิดดู (ดูตัวอย่างปุ่มใน Mod 11)
230 * 3. แจ้งเตือนเมื่อซีรีส์ที่ติดตามมีตอนใหม่ (TVmaze มีวันฉายตอนถัดไป)
231 */
232