diff --git a/src/app/api/acg/dmhy/route.ts b/src/app/api/acg/dmhy/route.ts new file mode 100644 index 0000000..d8cc158 --- /dev/null +++ b/src/app/api/acg/dmhy/route.ts @@ -0,0 +1,134 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NextRequest, NextResponse } from 'next/server'; +import { parseStringPromise } from 'xml2js'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; + +export const runtime = 'nodejs'; + +/** + * POST /api/acg/dmhy + * 搜索 动漫花园 (share.dmhy.org) RSS(仅管理员和站长可用) + * - http://share.dmhy.org/topics/rss/rss.xml?keyword=xxx + * - RSS 不支持分页(page>1 返回空 items) + */ +export async function POST(req: NextRequest) { + try { + const authInfo = getAuthInfoFromCookie(req); + if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) { + return NextResponse.json( + { error: '无权限访问' }, + { status: 403 } + ); + } + + const { keyword, page = 1 } = await req.json(); + + if (!keyword || typeof keyword !== 'string') { + return NextResponse.json( + { error: '搜索关键词不能为空' }, + { status: 400 } + ); + } + + const trimmedKeyword = keyword.trim(); + if (!trimmedKeyword) { + return NextResponse.json( + { error: '搜索关键词不能为空' }, + { status: 400 } + ); + } + + const pageNum = parseInt(String(page), 10); + if (isNaN(pageNum) || pageNum < 1) { + return NextResponse.json( + { error: '页码必须是大于0的整数' }, + { status: 400 } + ); + } + + if (pageNum > 1) { + return NextResponse.json({ + keyword: trimmedKeyword, + page: pageNum, + total: 0, + items: [], + }); + } + + const baseUrl = 'http://share.dmhy.org/topics/rss/rss.xml'; + const params = new URLSearchParams({ keyword: trimmedKeyword }); + const searchUrl = `${baseUrl}?${params.toString()}`; + + const response = await fetch(searchUrl, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + }, + }); + + if (!response.ok) { + throw new Error(`DMHY API 请求失败: ${response.status}`); + } + + const xmlData = await response.text(); + const parsed = await parseStringPromise(xmlData); + + if (!parsed?.rss?.channel?.[0]?.item) { + return NextResponse.json({ + keyword: trimmedKeyword, + page: pageNum, + total: 0, + items: [], + }); + } + + const items = parsed.rss.channel[0].item; + + const results = items.map((item: any) => { + const title = item.title?.[0] || ''; + const link = item.link?.[0] || ''; + const guid = item.guid?.[0] || link || `${title}-${item.pubDate?.[0] || ''}`; + const pubDate = item.pubDate?.[0] || ''; + const description = item.description?.[0] || ''; + const torrentUrl = item.enclosure?.[0]?.$?.url || ''; + + // 提取描述中的图片(如果有) + let images: string[] = []; + if (description) { + const imgMatches = description.match(/src="([^"]+)"/g); + if (imgMatches) { + images = imgMatches + .map((match: string) => { + const urlMatch = match.match(/src="([^"]+)"/); + return urlMatch ? urlMatch[1] : ''; + }) + .filter(Boolean); + } + } + + return { + title, + link, + guid, + pubDate, + torrentUrl, + description, + images, + }; + }); + + return NextResponse.json({ + keyword: trimmedKeyword, + page: pageNum, + total: results.length, + items: results, + }); + } catch (error: any) { + console.error('DMHY 搜索失败:', error); + return NextResponse.json( + { error: error.message || '搜索失败' }, + { status: 500 } + ); + } +} diff --git a/src/components/AcgSearch.tsx b/src/components/AcgSearch.tsx index eef3b94..9a180c7 100644 --- a/src/components/AcgSearch.tsx +++ b/src/components/AcgSearch.tsx @@ -30,7 +30,7 @@ interface AcgSearchProps { onError?: (error: string) => void; } -type AcgSearchSource = 'acgrip' | 'mikan'; +type AcgSearchSource = 'acgrip' | 'mikan' | 'dmhy'; export default function AcgSearch({ keyword, @@ -56,13 +56,19 @@ export default function AcgSearch({ const performSearch = async (page: number, isLoadMore = false) => { if (isLoadingMoreRef.current) return; if (source === 'mikan' && page > 1) return; + if (source === 'dmhy' && page > 1) return; isLoadingMoreRef.current = true; setLoading(true); setError(null); try { - const apiUrl = source === 'mikan' ? '/api/acg/mikan' : '/api/acg/acgrip'; + const apiUrl = + source === 'mikan' + ? '/api/acg/mikan' + : source === 'dmhy' + ? '/api/acg/dmhy' + : '/api/acg/acgrip'; const response = await fetch(apiUrl, { method: 'POST', headers: { @@ -85,12 +91,12 @@ export default function AcgSearch({ // 追加新数据 setAllItems(prev => [...prev, ...data.items]); // 如果当前页没有结果,说明没有更多了 - setHasMore(source === 'acgrip' && data.items.length > 0); + setHasMore(source !== 'mikan' && source !== 'dmhy' && data.items.length > 0); } else { // 新搜索,重置数据 setAllItems(data.items); // 如果第一页有结果,假设可能还有更多 - setHasMore(source === 'acgrip' && data.items.length > 0); + setHasMore(source !== 'mikan' && source !== 'dmhy' && data.items.length > 0); } setCurrentPage(page); @@ -141,6 +147,7 @@ export default function AcgSearch({ // 加载更多数据 const loadMore = useCallback(() => { if (source === 'mikan') return; + if (source === 'dmhy') return; if (!loading && hasMore && !isLoadingMoreRef.current) { performSearch(currentPage + 1, true); } @@ -332,7 +339,7 @@ export default function AcgSearch({ {/* 加载更多指示器 */} - {source === 'acgrip' && hasMore && ( + {source !== 'mikan' && source !== 'dmhy' && hasMore && (
@@ -392,6 +399,7 @@ export default function AcgSearch({ options={[ { label: 'ACG.RIP', value: 'acgrip' }, { label: '蜜柑', value: 'mikan' }, + { label: '动漫花园', value: 'dmhy' }, ]} active={source} onChange={(value) => setSource(value as AcgSearchSource)}