弹幕磁力搜索增加动漫花园
This commit is contained in:
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ interface AcgSearchProps {
|
|||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AcgSearchSource = 'acgrip' | 'mikan';
|
type AcgSearchSource = 'acgrip' | 'mikan' | 'dmhy';
|
||||||
|
|
||||||
export default function AcgSearch({
|
export default function AcgSearch({
|
||||||
keyword,
|
keyword,
|
||||||
@@ -56,13 +56,19 @@ export default function AcgSearch({
|
|||||||
const performSearch = async (page: number, isLoadMore = false) => {
|
const performSearch = async (page: number, isLoadMore = false) => {
|
||||||
if (isLoadingMoreRef.current) return;
|
if (isLoadingMoreRef.current) return;
|
||||||
if (source === 'mikan' && page > 1) return;
|
if (source === 'mikan' && page > 1) return;
|
||||||
|
if (source === 'dmhy' && page > 1) return;
|
||||||
|
|
||||||
isLoadingMoreRef.current = true;
|
isLoadingMoreRef.current = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
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, {
|
const response = await fetch(apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -85,12 +91,12 @@ export default function AcgSearch({
|
|||||||
// 追加新数据
|
// 追加新数据
|
||||||
setAllItems(prev => [...prev, ...data.items]);
|
setAllItems(prev => [...prev, ...data.items]);
|
||||||
// 如果当前页没有结果,说明没有更多了
|
// 如果当前页没有结果,说明没有更多了
|
||||||
setHasMore(source === 'acgrip' && data.items.length > 0);
|
setHasMore(source !== 'mikan' && source !== 'dmhy' && data.items.length > 0);
|
||||||
} else {
|
} else {
|
||||||
// 新搜索,重置数据
|
// 新搜索,重置数据
|
||||||
setAllItems(data.items);
|
setAllItems(data.items);
|
||||||
// 如果第一页有结果,假设可能还有更多
|
// 如果第一页有结果,假设可能还有更多
|
||||||
setHasMore(source === 'acgrip' && data.items.length > 0);
|
setHasMore(source !== 'mikan' && source !== 'dmhy' && data.items.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
@@ -141,6 +147,7 @@ export default function AcgSearch({
|
|||||||
// 加载更多数据
|
// 加载更多数据
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (source === 'mikan') return;
|
if (source === 'mikan') return;
|
||||||
|
if (source === 'dmhy') return;
|
||||||
if (!loading && hasMore && !isLoadingMoreRef.current) {
|
if (!loading && hasMore && !isLoadingMoreRef.current) {
|
||||||
performSearch(currentPage + 1, true);
|
performSearch(currentPage + 1, true);
|
||||||
}
|
}
|
||||||
@@ -332,7 +339,7 @@ export default function AcgSearch({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 加载更多指示器 */}
|
{/* 加载更多指示器 */}
|
||||||
{source === 'acgrip' && hasMore && (
|
{source !== 'mikan' && source !== 'dmhy' && hasMore && (
|
||||||
<div ref={loadMoreRef} className='flex items-center justify-center py-8'>
|
<div ref={loadMoreRef} className='flex items-center justify-center py-8'>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<Loader2 className='mx-auto h-6 w-6 animate-spin text-green-600 dark:text-green-400' />
|
<Loader2 className='mx-auto h-6 w-6 animate-spin text-green-600 dark:text-green-400' />
|
||||||
@@ -392,6 +399,7 @@ export default function AcgSearch({
|
|||||||
options={[
|
options={[
|
||||||
{ label: 'ACG.RIP', value: 'acgrip' },
|
{ label: 'ACG.RIP', value: 'acgrip' },
|
||||||
{ label: '蜜柑', value: 'mikan' },
|
{ label: '蜜柑', value: 'mikan' },
|
||||||
|
{ label: '动漫花园', value: 'dmhy' },
|
||||||
]}
|
]}
|
||||||
active={source}
|
active={source}
|
||||||
onChange={(value) => setSource(value as AcgSearchSource)}
|
onChange={(value) => setSource(value as AcgSearchSource)}
|
||||||
|
|||||||
Reference in New Issue
Block a user