import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { Rss, Sparkles } from 'lucide-react' import { aiAPI, type ExternalMediaResult, type SearchIntent } from '../api/ai' import { imageURL } from '../api/client' import { mediaAPI } from '../api/library' import { subscriptionsAPI } from '../api/subscriptions' import { MediaCard } from '../components/MediaCard' import type { Media } from '../types' export function SearchPage() { const [q, setQ] = useState('') const [items, setItems] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [aiOn, setAiOn] = useState(false) const [aiAvailable, setAiAvailable] = useState(false) const [intent, setIntent] = useState(null) const [hasSearched, setHasSearched] = useState(false) const [externalItems, setExternalItems] = useState([]) const [subscribing, setSubscribing] = useState('') useEffect(() => { aiAPI .status() .then((s) => setAiAvailable(s.enabled)) .catch(() => setAiAvailable(false)) }, []) const doQuickSearch = useCallback((query: string) => { if (!query.trim()) { setItems([]) setHasSearched(false) setLoading(false) return } setHasSearched(true) setError('') mediaAPI .search(query, 60) .then((d) => { setItems(d.items ?? []) setExternalItems([]) setIntent(null) }) .catch((err) => { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '搜索失败' setError(msg) toast.error(msg) }) .finally(() => setLoading(false)) }, []) // Fast LIKE search-as-you-type when AI mode is OFF. useEffect(() => { if (aiOn) return setLoading(true) const t = setTimeout(() => doQuickSearch(q), 300) return () => clearTimeout(t) }, [q, aiOn, doQuickSearch]) const onAISubmit = async (e: FormEvent) => { e.preventDefault() if (!q.trim()) return setLoading(true) setError('') setHasSearched(true) try { const data = await aiAPI.smartSearch(q) setItems(data.items ?? []) setExternalItems(data.external_items ?? []) setIntent(data.intent) } catch (err) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'AI 搜索失败' setError(msg) toast.error(msg) } finally { setLoading(false) } } const showEmpty = !loading && !error && hasSearched && items.length === 0 const showIdle = !loading && !error && !hasSearched return (

搜索

{aiOn ? (
) => setQ(e.target.value)} />
) : ( ) => setQ(e.target.value)} /> )} {intent && (
AI 解析: {JSON.stringify(intent)}
)} {loading && (
搜索中…
)} {error && (
{error}
)} {showIdle && (

输入关键词开始搜索

支持电影、电视剧、动漫等媒体内容的快速搜索

)} {showEmpty && (

未找到匹配的媒体

尝试其他关键词,或者添加媒体库后执行扫描

)} {items.length > 0 && ( <>
本地媒体库
{items.map((m) => ( ))}
)} {externalItems.length > 0 && ( { const keyword = item.subscribe_keyword || item.title const key = `${item.source}:${keyword}` setSubscribing(key) try { const feed = `site-search://search?keyword=${encodeURIComponent(keyword)}&source=${encodeURIComponent(item.source)}` const sub = await subscriptionsAPI.create({ name: `${item.title} 自动订阅`, feed_url: feed, filter: keyword, media_type: item.media_type, enabled: true, }) const run = await subscriptionsAPI.runNow(sub.id) toast.success( run.queued > 0 ? `已订阅并加入 ${run.queued} 个下载` : '已订阅,暂未在 PT 站点找到可下载资源', ) } catch (err) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '订阅失败' toast.error(msg) } finally { setSubscribing('') } }} /> )}
) } function ExternalResults({ items, busyKey, onSubscribe, }: { items: ExternalMediaResult[] busyKey: string onSubscribe: (item: ExternalMediaResult) => Promise }) { return (

外部数据源

来自 TMDb / 豆瓣 / Bangumi。电影入队最佳资源;剧集/动漫优先整季或全集包,否则按集批量入队。

{items.map((item) => { const keyword = item.subscribe_keyword || item.title const key = `${item.source}:${keyword}` return (
{item.poster_url ? ( {item.title} ) : null}
{item.source} {item.media_type && {item.media_type}} {item.year ? {item.year} : null} {item.rating ? ★ {item.rating.toFixed(1)} : null}

{item.title}

{item.overview || `订阅关键词:${keyword}`}

) })}
) }