SLOPSHOPPER

movie-finder-th

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

newcommandmodelprocessnetwork
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · movie-finder-th
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /movie ⎿ movie-finder-th: 🎬 Movie Finder TH (Mod 30) ⎿ movie-finder-th: ======================================== ⎿ movie-finder-th: /movie search <ชื่อ> ค้นหนัง (Wikipedia ไทย/อังกฤษ) ⎿ movie-finder-th: /movie series <ชื่อ> ค้นซีรีส์ (TVmaze: แนว คะแนน ช่อง) ⎿ movie-finder-th: /movie info <ชื่อเรื่อง> เรื่องย่อ ⎿ movie-finder-th: /movie watch <ชื่อ> [แพลตฟอร์ม] เปิดหน้าค้นหา: netflix youtube disney prime apple (ค่าเริ่มต้น netflix) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Mod 30: Movie Finder TH

ค้นหนังจาก 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 / listwatchlist
/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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. ค้นหนังจาก Wikipedia และกรองเฉพาะภาพยนตร์ (รองรับปี พ.ศ.)
  2. ค้นซีรีส์จาก TVmaze
  3. เปิดเว็บด้วยเบราว์เซอร์ของเครื่องอย่างปลอดภัย
  4. คำสั่ง /movie, watchlist และขอคำแนะนำจาก Claude

หมายเหตุ

Mod เล่นวิดีโอเองไม่ได้ และไม่รู้ว่าเรื่องไหนมีบนแพลตฟอร์มไหนในไทย (ไม่มี API สาธารณะ) จึงเปิดหน้าค้นหาของแพลตฟอร์มนั้นให้ ต้องมีบัญชีของแพลตฟอร์มเอง

Source 1 files
hooks/register.tsx 232 lines
1/**
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