diff --git a/package.json b/package.json
index 43b90d3..ff538eb 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"swiper": "^11.2.8",
"tailwind-merge": "^2.6.0",
"vidstack": "^0.6.15",
+ "xml2js": "^0.6.2",
"zod": "^3.24.1"
},
"devDependencies": {
@@ -73,6 +74,7 @@
"@types/react": "^18.3.18",
"@types/react-dom": "^19.1.6",
"@types/testing-library__jest-dom": "^5.14.9",
+ "@types/xml2js": "^0.4.14",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "^10.4.20",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6323be9..4d14143 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -119,6 +119,9 @@ importers:
vidstack:
specifier: ^0.6.15
version: 0.6.15
+ xml2js:
+ specifier: ^0.6.2
+ version: 0.6.2
zod:
specifier: ^3.24.1
version: 3.25.67
@@ -162,6 +165,9 @@ importers:
'@types/testing-library__jest-dom':
specifier: ^5.14.9
version: 5.14.9
+ '@types/xml2js':
+ specifier: ^0.4.14
+ version: 0.4.14
'@typescript-eslint/eslint-plugin':
specifier: ^5.62.0
version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
@@ -1182,28 +1188,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@next/swc-linux-arm64-musl@14.2.33':
resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@next/swc-linux-x64-gnu@14.2.33':
resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@next/swc-linux-x64-musl@14.2.33':
resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@next/swc-win32-arm64-msvc@14.2.33':
resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==}
@@ -1620,6 +1622,9 @@ packages:
'@types/validator@13.15.10':
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
+ '@types/xml2js@0.4.14':
+ resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
+
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
@@ -1729,49 +1734,41 @@ packages:
resolution: {integrity: sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.9.0':
resolution: {integrity: sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.9.0':
resolution: {integrity: sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.9.0':
resolution: {integrity: sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.9.0':
resolution: {integrity: sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw==}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.9.0':
resolution: {integrity: sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.9.0':
resolution: {integrity: sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.9.0':
resolution: {integrity: sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.9.0':
resolution: {integrity: sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ==}
@@ -4666,6 +4663,9 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ sax@1.4.3:
+ resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
+
saxes@5.0.1:
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
engines: {node: '>=10'}
@@ -5499,6 +5499,14 @@ packages:
xml-name-validator@3.0.0:
resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
+ xml2js@0.6.2:
+ resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
+ engines: {node: '>=4.0.0'}
+
+ xmlbuilder@11.0.1:
+ resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
+ engines: {node: '>=4.0'}
+
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
@@ -7305,6 +7313,10 @@ snapshots:
'@types/validator@13.15.10': {}
+ '@types/xml2js@0.4.14':
+ dependencies:
+ '@types/node': 24.0.3
+
'@types/yargs-parser@21.0.3': {}
'@types/yargs@16.0.9':
@@ -10812,6 +10824,8 @@ snapshots:
safer-buffer@2.1.2: {}
+ sax@1.4.3: {}
+
saxes@5.0.1:
dependencies:
xmlchars: 2.2.0
@@ -11845,6 +11859,13 @@ snapshots:
xml-name-validator@3.0.0: {}
+ xml2js@0.6.2:
+ dependencies:
+ sax: 1.4.3
+ xmlbuilder: 11.0.1
+
+ xmlbuilder@11.0.1: {}
+
xmlchars@2.2.0: {}
xmlhttprequest-ssl@2.1.2: {}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index c3f25ec..11d2095 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -2762,6 +2762,7 @@ const OpenListConfigComponent = ({
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [rootPath, setRootPath] = useState('/');
+ const [offlineDownloadPath, setOfflineDownloadPath] = useState('/');
const [scanInterval, setScanInterval] = useState(0);
const [videos, setVideos] = useState([]);
const [refreshing, setRefreshing] = useState(false);
@@ -2780,6 +2781,7 @@ const OpenListConfigComponent = ({
setUsername(config.OpenListConfig.Username || '');
setPassword(config.OpenListConfig.Password || '');
setRootPath(config.OpenListConfig.RootPath || '/');
+ setOfflineDownloadPath(config.OpenListConfig.OfflineDownloadPath || '/');
setScanInterval(config.OpenListConfig.ScanInterval || 0);
}
}, [config]);
@@ -2819,6 +2821,7 @@ const OpenListConfigComponent = ({
Username: username,
Password: password,
RootPath: rootPath,
+ OfflineDownloadPath: offlineDownloadPath,
ScanInterval: scanInterval,
}),
});
@@ -3111,6 +3114,23 @@ const OpenListConfigComponent = ({
+
+
+
setOfflineDownloadPath(e.target.value)}
+ disabled={!enabled}
+ placeholder='/'
+ className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed'
+ />
+
+ 动漫磁力等离线下载任务的保存目录,默认为根目录 /
+
+
+
@@ -975,7 +1055,7 @@ function SearchPageClient() {
)}
>
- ) : (
+ ) : activeTab === 'pansou' ? (
<>
{/* 网盘搜索结果 */}
@@ -988,6 +1068,19 @@ function SearchPageClient() {
triggerSearch={triggerPansouSearch}
/>
>
+ ) : (
+ <>
+ {/* ACG 磁力搜索结果 */}
+
+
+ 动漫磁力搜索结果
+
+
+
+ >
)}
) : searchHistory.length > 0 ? (
@@ -1018,11 +1111,20 @@ function SearchPageClient() {
if (activeTab === 'video') {
// 影视搜索
router.push(
- `/search?q=${encodeURIComponent(item.trim())}`
+ `/search?q=${encodeURIComponent(item.trim())}&type=video`
);
} else if (activeTab === 'pansou') {
// 网盘搜索
+ router.push(
+ `/search?q=${encodeURIComponent(item.trim())}&type=pansou`
+ );
setTriggerPansouSearch(prev => !prev);
+ } else if (activeTab === 'acg') {
+ // ACG 磁力搜索
+ router.push(
+ `/search?q=${encodeURIComponent(item.trim())}&type=acg`
+ );
+ setTriggerAcgSearch(prev => !prev);
}
}}
className='px-4 py-2 bg-gray-500/10 hover:bg-gray-300 rounded-full text-sm text-gray-700 transition-colors duration-200 dark:bg-gray-700/50 dark:hover:bg-gray-600 dark:text-gray-300'
diff --git a/src/components/AcgSearch.tsx b/src/components/AcgSearch.tsx
new file mode 100644
index 0000000..31a14de
--- /dev/null
+++ b/src/components/AcgSearch.tsx
@@ -0,0 +1,327 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+
+import { AlertCircle, Download, ExternalLink, Loader2 } from 'lucide-react';
+import { useEffect, useState } from 'react';
+
+import Toast, { ToastProps } from '@/components/Toast';
+
+interface AcgSearchItem {
+ title: string;
+ link: string;
+ guid: string;
+ pubDate: string;
+ torrentUrl: string;
+ description: string;
+ images: string[];
+}
+
+interface AcgSearchResult {
+ keyword: string;
+ page: number;
+ total: number;
+ items: AcgSearchItem[];
+}
+
+interface AcgSearchProps {
+ keyword: string;
+ triggerSearch?: boolean;
+ onError?: (error: string) => void;
+}
+
+export default function AcgSearch({
+ keyword,
+ triggerSearch,
+ onError,
+}: AcgSearchProps) {
+ const [loading, setLoading] = useState(false);
+ const [results, setResults] = useState
(null);
+ const [error, setError] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [downloadingId, setDownloadingId] = useState(null);
+ const [showNameDialog, setShowNameDialog] = useState(false);
+ const [selectedItem, setSelectedItem] = useState(null);
+ const [customName, setCustomName] = useState('');
+ const [toast, setToast] = useState(null);
+
+ // 执行搜索
+ const performSearch = async (page: number) => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const response = await fetch('/api/acg/search', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ keyword: keyword.trim(),
+ page,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || '搜索失败');
+ }
+
+ const data: AcgSearchResult = await response.json();
+ setResults(data);
+ setCurrentPage(page);
+ } catch (err: any) {
+ const errorMsg = err.message || '搜索失败,请稍后重试';
+ setError(errorMsg);
+ onError?.(errorMsg);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ // triggerSearch 变化时触发搜索(无论是 true 还是 false)
+ if (triggerSearch === undefined) {
+ return;
+ }
+
+ const currentKeyword = keyword.trim();
+ if (!currentKeyword) {
+ return;
+ }
+
+ performSearch(1);
+ }, [triggerSearch]);
+
+ // 处理页码变化
+ const handlePageChange = (newPage: number) => {
+ if (newPage < 1 || loading) return;
+ performSearch(newPage);
+ };
+
+ // 打开命名弹窗
+ const handleOpenDownloadDialog = (item: AcgSearchItem) => {
+ setSelectedItem(item);
+ setCustomName(keyword.trim());
+ setShowNameDialog(true);
+ };
+
+ // 确认下载
+ const handleConfirmDownload = async () => {
+ if (!selectedItem || !customName.trim()) {
+ return;
+ }
+
+ setDownloadingId(selectedItem.guid);
+ setShowNameDialog(false);
+
+ try {
+ const response = await fetch('/api/acg/download', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ url: selectedItem.torrentUrl,
+ name: customName.trim(),
+ }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || '添加下载任务失败');
+ }
+
+ setToast({
+ message: data.message || '已添加到离线下载队列',
+ type: 'success',
+ onClose: () => setToast(null),
+ });
+ } catch (err: any) {
+ setToast({
+ message: err.message || '添加下载任务失败',
+ type: 'error',
+ onClose: () => setToast(null),
+ });
+ } finally {
+ setDownloadingId(null);
+ setSelectedItem(null);
+ setCustomName('');
+ }
+ };
+
+ if (loading && !results) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!results || results.total === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* 搜索结果统计 */}
+
+
+ 第 {currentPage} 页,
+ 共 {results.total} 个结果
+
+
+ {/* 分页按钮 */}
+
+
+
+
+
+
+ {/* 结果列表 */}
+
+ {results.items.map((item) => (
+
+ {/* 标题 */}
+
+ {item.title}
+
+
+ {/* 发布时间 */}
+
+ {new Date(item.pubDate).toLocaleString('zh-CN')}
+
+
+ {/* 图片预览 */}
+ {item.images && item.images.length > 0 && (
+
+ {item.images.slice(0, 3).map((img, imgIndex) => (
+

+ ))}
+
+ )}
+
+ {/* 操作按钮 */}
+
+
+
+
+ 详情
+
+
+
+ ))}
+
+
+ {/* 命名弹窗 */}
+ {showNameDialog && (
+
+
+
+ 设置资源名称
+
+
setCustomName(e.target.value)}
+ placeholder='请输入资源名称'
+ className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500'
+ autoFocus
+ />
+
+
+
+
+
+
+ )}
+
+ {/* Toast 提示 */}
+ {toast &&
}
+
+ );
+}
diff --git a/src/components/PansouSearch.tsx b/src/components/PansouSearch.tsx
index db0b702..47c7d5a 100644
--- a/src/components/PansouSearch.tsx
+++ b/src/components/PansouSearch.tsx
@@ -58,8 +58,8 @@ export default function PansouSearch({
const [selectedType, setSelectedType] = useState('all'); // 'all' 表示显示全部
useEffect(() => {
- // 只在 triggerSearch 变化时执行搜索,不响应 keyword 变化
- if (!triggerSearch) {
+ // triggerSearch 变化时触发搜索(无论是 true 还是 false)
+ if (triggerSearch === undefined) {
return;
}
diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts
index 4484435..070c40d 100644
--- a/src/lib/admin.types.ts
+++ b/src/lib/admin.types.ts
@@ -106,6 +106,7 @@ export interface AdminConfig {
Username: string; // 账号(用于登录获取Token)
Password: string; // 密码(用于登录获取Token)
RootPath: string; // 根目录路径,默认 "/"
+ OfflineDownloadPath: string; // 离线下载目录,默认 "/"
LastRefreshTime?: number; // 上次刷新时间戳
ResourceCount?: number; // 资源数量
ScanInterval?: number; // 定时扫描间隔(分钟),0表示关闭,最低60分钟