├── .npmrc ├── demo └── demo.png ├── client ├── postcss.config.js ├── src │ ├── vite-env.d.ts │ ├── index.css │ ├── assets │ │ ├── logo.svg │ │ ├── r.svg │ │ └── react.svg │ ├── main.tsx │ ├── App.tsx │ ├── App.css │ ├── components │ │ ├── Player.tsx │ │ ├── EmptySourcesState.tsx │ │ ├── HelpAndFeedbackModal.tsx │ │ ├── SettingsModal.tsx │ │ └── Layout.tsx │ ├── services │ │ └── api.ts │ ├── types │ │ └── video.ts │ ├── context │ │ └── SettingsContext.tsx │ ├── pages │ │ ├── play │ │ │ └── PlayPage.tsx │ │ ├── home │ │ │ └── HomePage.tsx │ │ └── search │ │ │ └── SearchPage.tsx │ └── data │ │ └── mockData.ts ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── tailwind.config.js ├── index.html ├── tsconfig.json ├── eslint.config.js ├── package.json └── public │ └── vite.svg ├── vercel.json ├── Dockerfile.server ├── Dockerfile.client ├── .gitignore ├── docker-compose.yml ├── server ├── package.json ├── tsconfig.json ├── src │ ├── routes │ │ └── movies.ts │ └── server.ts └── pnpm-lock.yaml ├── package.json ├── PROJECT_SUMMARY.md ├── README.md └── pnpm-lock.yaml /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@heroui/* -------------------------------------------------------------------------------- /demo/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1595901624/ReelFlix/HEAD/demo/demo.png -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "pnpm run build", 3 | "outputDirectory": "client/dist", 4 | "installCommand": "pnpm install-all", 5 | "framework": null 6 | } -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_API_SOURCES: string; 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv; 9 | } -------------------------------------------------------------------------------- /Dockerfile.server: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY server/package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY server/ . 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 3000 14 | 15 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | extensions: ['.ts', '.tsx', '.js', '.jsx'] 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .animate-fade-in { 7 | animation: fadeIn 0.5s ease-in-out; 8 | } 9 | } 10 | 11 | @keyframes fadeIn { 12 | from { opacity: 0; } 13 | to { opacity: 1; } 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile.client: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS build 2 | 3 | WORKDIR /app 4 | 5 | COPY client/package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY client/ . 10 | 11 | RUN npm run build 12 | 13 | FROM nginx:alpine 14 | 15 | COPY --from=build /app/dist /usr/share/nginx/html 16 | 17 | EXPOSE 80 18 | 19 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .env 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Build outputs 10 | dist/ 11 | build/ 12 | 13 | # IDE files 14 | .vscode/ 15 | .idea/ 16 | 17 | # Client build outputs 18 | client/dist/ 19 | client/node_modules/ 20 | 21 | # Server build outputs 22 | server/dist/ 23 | server/node_modules/ 24 | 25 | # System files 26 | .DS_Store 27 | Thumbs.db -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import {heroui} from "@heroui/theme"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | "./index.html", 7 | "./src/**/*.{js,ts,jsx,tsx}", 8 | "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}", 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: "class", 14 | plugins: [heroui()], 15 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | client: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.client 8 | ports: 9 | - "8080:80" 10 | depends_on: 11 | - server 12 | 13 | server: 14 | build: 15 | context: . 16 | dockerfile: Dockerfile.server 17 | ports: 18 | - "3000:3000" 19 | environment: 20 | - NODE_ENV=production -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ReelFlix 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | R 9 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { HeroUIProvider } from '@heroui/react'; 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | // 获取根元素 8 | const rootElement = document.getElementById('root'); 9 | if (!rootElement) { 10 | throw new Error('Failed to find the root element'); 11 | } 12 | 13 | // 渲染应用 14 | createRoot(rootElement).render( 15 | 16 | 17 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /client/src/assets/r.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "types": ["vite/client"], 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | /* JSX support */ 24 | "jsx": "react-jsx" 25 | }, 26 | "include": ["src"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 2 | import { SettingsProvider } from './context/SettingsContext'; 3 | import Layout from './components/Layout'; 4 | import HomePage from './pages/home/HomePage'; 5 | import PlayPage from './pages/play/PlayPage'; 6 | import SearchPage from './pages/search/SearchPage'; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | } /> 15 | } /> 16 | } /> 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 1200px; 3 | margin: 0 auto; 4 | padding: 20px; 5 | } 6 | 7 | .card { 8 | max-width: 600px; 9 | margin: 0 auto; 10 | padding: 24px; 11 | border-radius: 8px; 12 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 13 | } 14 | 15 | .counter-section, .search-section { 16 | margin: 20px 0; 17 | padding: 20px; 18 | border-radius: 8px; 19 | background-color: #f5f5f5; 20 | } 21 | 22 | .counter-section h2, .search-section h2 { 23 | margin-top: 0; 24 | } 25 | 26 | .counter-section p { 27 | font-size: 18px; 28 | font-weight: bold; 29 | margin: 10px 0; 30 | } 31 | 32 | .search-section input { 33 | width: 100%; 34 | margin-bottom: 10px; 35 | } 36 | 37 | h1 { 38 | color: #333; 39 | text-align: center; 40 | } 41 | 42 | h2 { 43 | color: #555; 44 | } 45 | 46 | p { 47 | color: #666; 48 | } 49 | -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist']), 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | extends: [ 12 | js.configs.recommended, 13 | reactHooks.configs.flat.recommended, 14 | reactRefresh.configs.vite, 15 | ], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | ecmaFeatures: { jsx: true }, 22 | sourceType: 'module', 23 | }, 24 | }, 25 | rules: { 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | }, 28 | }, 29 | ]) 30 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "ReelFlix 在线影视平台服务端", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "start": "node dist/server.js", 8 | "dev": "nodemon --exec ts-node src/server.ts", 9 | "build": "tsc", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "express", 14 | "movie", 15 | "streaming", 16 | "api" 17 | ], 18 | "author": "ReelFlix Team", 19 | "license": "ISC", 20 | "packageManager": "pnpm@10.7.1", 21 | "dependencies": { 22 | "axios": "^1.13.2", 23 | "cors": "^2.8.5", 24 | "dotenv": "^17.2.3", 25 | "express": "^5.2.1" 26 | }, 27 | "devDependencies": { 28 | "@types/cors": "^2.8.19", 29 | "@types/express": "^5.0.6", 30 | "@types/node": "^24.10.1", 31 | "nodemon": "^3.1.11", 32 | "ts-node": "^10.9.2", 33 | "typescript": "^5.9.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "removeComments": true, 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | "strictFunctionTypes": true, 20 | "noImplicitThis": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "moduleResolution": "node", 24 | "baseUrl": "./", 25 | "paths": { 26 | "@/*": ["src/*"] 27 | } 28 | }, 29 | "include": [ 30 | "src/**/*" 31 | ], 32 | "exclude": [ 33 | "node_modules", 34 | "dist" 35 | ] 36 | } -------------------------------------------------------------------------------- /server/src/routes/movies.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from 'express'; 2 | 3 | const router: Router = Router(); 4 | 5 | // 获取所有电影 6 | router.get('/', (req: Request, res: Response) => { 7 | res.json({ 8 | message: '获取电影列表', 9 | data: [] 10 | }); 11 | }); 12 | 13 | // 根据ID获取电影 14 | router.get('/:id', (req: Request, res: Response) => { 15 | const { id } = req.params; 16 | res.json({ 17 | message: `获取电影 ID: ${id}`, 18 | data: null 19 | }); 20 | }); 21 | 22 | // 添加新电影 23 | router.post('/', (req: Request, res: Response) => { 24 | res.json({ 25 | message: '添加新电影', 26 | data: req.body 27 | }); 28 | }); 29 | 30 | // 更新电影 31 | router.put('/:id', (req: Request, res: Response) => { 32 | const { id } = req.params; 33 | res.json({ 34 | message: `更新电影 ID: ${id}`, 35 | data: req.body 36 | }); 37 | }); 38 | 39 | // 删除电影 40 | router.delete('/:id', (req: Request, res: Response) => { 41 | const { id } = req.params; 42 | res.json({ 43 | message: `删除电影 ID: ${id}` 44 | }); 45 | }); 46 | 47 | export default router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reelflix", 3 | "version": "1.0.0", 4 | "description": "在线影视平台", 5 | "main": "index.js", 6 | "scripts": { 7 | "install-all": "pnpm install && pnpm install && cd client && pnpm install && cd ../server && pnpm install", 8 | "dev": "concurrently \"pnpm run dev:client\" \"pnpm run dev:server\"", 9 | "dev:client": "cd client && pnpm run dev", 10 | "dev:server": "cd server && pnpm run dev", 11 | "build": "concurrently \"pnpm run build:client\" \"pnpm run build:server\"", 12 | "build:client": "cd client && pnpm run build", 13 | "build:server": "cd server && pnpm run build", 14 | "start": "concurrently \"pnpm run start:client\" \"pnpm run start:server\"", 15 | "start:client": "cd client && pnpm run preview", 16 | "start:server": "cd server && pnpm start" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "express", 21 | "movie", 22 | "streaming" 23 | ], 24 | "author": "ReelFlix Team", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "concurrently": "^8.2.2" 28 | } 29 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@heroicons/react": "^2.2.0", 14 | "@heroui/react": "2.8.5", 15 | "@heroui/system": "2.4.23", 16 | "@heroui/theme": "2.4.23", 17 | "axios": "^1.13.2", 18 | "framer-motion": "^12.23.25", 19 | "hls.js": "^1.6.15", 20 | "react": "^19.2.0", 21 | "react-dom": "^19.2.0", 22 | "react-router-dom": "^7.10.1" 23 | }, 24 | "devDependencies": { 25 | "@eslint/js": "^9.39.1", 26 | "@types/node": "^24.10.1", 27 | "@types/react": "^19.2.7", 28 | "@types/react-dom": "^19.2.3", 29 | "@vitejs/plugin-react": "^5.1.1", 30 | "autoprefixer": "^10.4.22", 31 | "eslint": "^9.39.1", 32 | "eslint-plugin-react-hooks": "^7.0.1", 33 | "eslint-plugin-react-refresh": "^0.4.24", 34 | "globals": "^16.5.0", 35 | "postcss": "^8.5.6", 36 | "tailwindcss": "^3.4.18", 37 | "typescript": "^5.9.3", 38 | "vite": "^7.2.4", 39 | "vite-tsconfig-paths": "5.1.4" 40 | } 41 | } -------------------------------------------------------------------------------- /client/src/components/Player.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import Hls from 'hls.js'; 3 | 4 | interface PlayerProps { 5 | src: string; 6 | poster?: string; 7 | } 8 | 9 | export default function Player({ src, poster }: PlayerProps) { 10 | const videoRef = useRef(null); 11 | 12 | useEffect(() => { 13 | const video = videoRef.current; 14 | if (!video) return; 15 | 16 | if (Hls.isSupported()) { 17 | const hls = new Hls(); 18 | hls.loadSource(src); 19 | hls.attachMedia(video); 20 | hls.on(Hls.Events.MANIFEST_PARSED, () => { 21 | // video.play().catch(() => {}); // Auto-play might be blocked 22 | }); 23 | return () => { 24 | hls.destroy(); 25 | }; 26 | } else if (video.canPlayType('application/vnd.apple.mpegurl')) { 27 | video.src = src; 28 | video.addEventListener('loadedmetadata', () => { 29 | // video.play().catch(() => {}); 30 | }); 31 | } 32 | }, [src]); 33 | 34 | return ( 35 |
36 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import { VideoListResponse } from '../types/video'; 2 | 3 | const API_BASE_URL = 'http://localhost:3000'; // 服务器地址 4 | 5 | export const fetchCategories = async (baseUrl: string): Promise => { 6 | const params = new URLSearchParams({ 7 | baseUrl 8 | }); 9 | 10 | const response = await fetch(`${API_BASE_URL}/api/proxy/vod?${params.toString()}`); 11 | if (!response.ok) { 12 | throw new Error('Network response was not ok'); 13 | } 14 | return response.json(); 15 | }; 16 | 17 | export const fetchVideoList = async ( 18 | baseUrl: string, 19 | page: number = 1, 20 | typeIds?: number[], 21 | keyword?: string 22 | ): Promise => { 23 | const params = new URLSearchParams({ 24 | baseUrl, 25 | ac: 'videolist', 26 | pg: page.toString() 27 | }); 28 | 29 | if (typeIds && typeIds.length > 0) { 30 | typeIds.forEach(id => params.append('t', id.toString())); 31 | } 32 | 33 | if (keyword) { 34 | params.append('wd', keyword); 35 | } 36 | 37 | const response = await fetch(`${API_BASE_URL}/api/proxy/vod?${params.toString()}`); 38 | if (!response.ok) { 39 | throw new Error('Network response was not ok'); 40 | } 41 | return response.json(); 42 | }; 43 | 44 | export const fetchVideoDetail = async (baseUrl: string, id: number): Promise => { 45 | const params = new URLSearchParams({ 46 | baseUrl, 47 | ac: 'videolist', 48 | ids: id.toString() 49 | }); 50 | 51 | const response = await fetch(`${API_BASE_URL}/api/proxy/vod?${params.toString()}`); 52 | if (!response.ok) { 53 | throw new Error('Network response was not ok'); 54 | } 55 | return response.json(); 56 | }; 57 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Application, Request, Response } from 'express'; 2 | import cors from 'cors'; 3 | import dotenv from 'dotenv'; 4 | import axios from 'axios'; 5 | 6 | // 加载环境变量 7 | dotenv.config(); 8 | 9 | const app: Application = express(); 10 | const PORT: number = parseInt(process.env.PORT as string, 10) || 3000; 11 | 12 | // 中间件 13 | app.use(cors()); 14 | app.use(express.json()); 15 | 16 | // 代理路由 - 转发到外部 API 17 | app.get('/api/proxy/vod', async (req: Request, res: Response): Promise => { 18 | try { 19 | const baseUrl = req.query.baseUrl as string; 20 | if (!baseUrl) { 21 | res.status(400).json({ error: 'Missing baseUrl parameter' }); 22 | return; 23 | } 24 | 25 | // 构建目标 URL 26 | const targetUrl = new URL(baseUrl); 27 | Object.keys(req.query).forEach(key => { 28 | if (key !== 'baseUrl') { 29 | targetUrl.searchParams.append(key, req.query[key] as string); 30 | } 31 | }); 32 | 33 | // 转发请求 34 | const response = await axios.get(targetUrl.toString(), { 35 | timeout: 10000, // 10秒超时 36 | headers: { 37 | 'User-Agent': 'ReelFlix/1.0' 38 | } 39 | }); 40 | 41 | // 返回响应 42 | res.json(response.data); 43 | } catch (error: any) { 44 | console.error('Proxy error:', error.message); 45 | res.status(500).json({ 46 | error: 'Failed to fetch from external API', 47 | details: error.message 48 | }); 49 | } 50 | }); 51 | 52 | // 路由 53 | import moviesRouter from './routes/movies'; 54 | app.use('/api/movies', moviesRouter); 55 | 56 | // 基本路由 57 | app.get('/', (req: Request, res: Response) => { 58 | res.json({ message: '欢迎来到 ReelFlix 在线影视平台 API' }); 59 | }); 60 | 61 | // 启动服务器 62 | app.listen(PORT, () => { 63 | console.log(`服务器运行在端口 ${PORT}`); 64 | }); 65 | 66 | export default app; -------------------------------------------------------------------------------- /client/src/components/EmptySourcesState.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, CardBody } from "@heroui/react"; 2 | 3 | interface EmptySourcesStateProps { 4 | onOpenSettings: () => void; 5 | } 6 | 7 | export default function EmptySourcesState({ onOpenSettings }: EmptySourcesStateProps) { 8 | return ( 9 |
10 | 11 | 12 |
13 |
📺
14 |

欢迎使用 ReelFlix

15 |

16 | 您还没有配置任何视频源 17 |

18 |

19 | 请添加至少一个视频源来开始浏览和观看内容。您可以在设置中添加和管理您的视频源。 20 |

21 |
22 | 23 |
24 | 32 |
33 | 34 |
35 |

如何添加视频源?

36 |
37 |

• 点击上方的"添加视频源"按钮

38 |

• 输入视频源的名称和API地址

39 |

• 确保API地址格式正确(如:https://api.example.com/provide/vod/)

40 |

• 保存后即可开始浏览内容

41 |
42 |
43 |
44 |
45 |
46 | ); 47 | } -------------------------------------------------------------------------------- /client/src/components/HelpAndFeedbackModal.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from "@heroui/react"; 2 | 3 | export default function HelpAndFeedbackModal({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: () => void }) { 4 | return ( 5 | 6 | 7 | {(onClose) => ( 8 | <> 9 | 帮助与反馈 10 | 11 |
12 |
13 |

免责声明

14 |

15 | ReelFlix 仅作为视频内容聚合平台,不对所展示内容的版权、合法性或准确性承担任何责任。用户应自行评估并承担使用风险。 16 |

17 |
18 |
19 |

广告警告

20 |

21 | 请谨慎对待应用内外的广告信息。本平台不对广告内容的真实性或可靠性负责。 22 |

23 |
24 |
25 |

反馈

26 |

27 | 如果您遇到任何问题或有建议,请通过 GitHub 反馈:https://github.com/1595901624/ReelFlix 28 |

29 |
30 |
31 |
32 | 33 | 36 | 37 | 38 | )} 39 |
40 |
41 | ); 42 | } -------------------------------------------------------------------------------- /PROJECT_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # ReelFlix 项目概要 2 | 3 | ## 项目结构 4 | 5 | ``` 6 | ReelFlix/ 7 | ├── client/ # React 前端应用 (TypeScript + Tailwind CSS) 8 | │ ├── public/ # 静态资源 9 | │ ├── src/ # 源代码 10 | │ │ ├── assets/ # 静态资源 11 | │ │ ├── components/ # React 组件 12 | │ │ ├── pages/ # 页面组件 13 | │ │ ├── App.tsx # 主应用组件 14 | │ │ └── main.tsx # 应用入口 15 | │ ├── index.html # HTML 模板 16 | │ ├── tsconfig.json # TypeScript 配置 17 | │ ├── tailwind.config.js # Tailwind CSS 配置 18 | │ └── package.json # 前端依赖配置 19 | ├── server/ # Express 服务端 (TypeScript) 20 | │ ├── src/ # TypeScript 源代码 21 | │ │ ├── controllers/ # 控制器 22 | │ │ ├── middleware/ # 中间件 23 | │ │ ├── models/ # 数据模型 24 | │ │ ├── routes/ # 路由 25 | │ │ ├── utils/ # 工具函数 26 | │ │ └── server.ts # 服务端入口 27 | │ ├── dist/ # 编译后的 JavaScript 代码 28 | │ ├── .env # 环境变量 29 | │ ├── tsconfig.json # TypeScript 配置 30 | │ └── package.json # 服务端依赖配置 31 | ├── package.json # 根目录配置和脚本 32 | ├── pnpm-lock.yaml # 依赖锁定文件 33 | └── README.md # 项目说明 34 | ``` 35 | 36 | ## 技术栈 37 | 38 | ### 前端 39 | - React 19 (TypeScript) 40 | - Vite 构建工具 41 | - HeroUI 组件库 42 | - Tailwind CSS v3 43 | - Framer Motion 动画库 44 | 45 | ### 后端 46 | - Express 5 (TypeScript) 47 | - Node.js 48 | - dotenv 环境变量管理 49 | - cors 跨域处理 50 | 51 | ### 开发工具 52 | - pnpm 包管理器 53 | - concurrently 并行执行脚本 54 | - TypeScript 类型检查 55 | 56 | ## 运行项目 57 | 58 | ### 开发模式 59 | 60 | 同时启动前端和服务端: 61 | ```bash 62 | pnpm run dev 63 | ``` 64 | 65 | 单独启动前端: 66 | ```bash 67 | pnpm run dev:client 68 | ``` 69 | 70 | 单独启动服务端: 71 | ```bash 72 | pnpm run dev:server 73 | ``` 74 | 75 | ### 生产构建 76 | 77 | 构建前后端应用: 78 | ```bash 79 | pnpm run build 80 | ``` 81 | 82 | 分别构建前端和后端: 83 | ```bash 84 | pnpm run build:client 85 | pnpm run build:server 86 | ``` 87 | 88 | 启动生产环境应用: 89 | ```bash 90 | pnpm run start 91 | ``` 92 | ## 环境配置 93 | 94 | ### 前端 95 | 前端应用运行在 `http://localhost:5173` 96 | 97 | ### 后端 98 | 后端服务运行在 `http://localhost:3000`,端口可通过 `.env` 文件中的 `PORT` 变量修改。 99 | 100 | ## 项目特点 101 | 102 | 1. **现代化技术栈** - 使用最新的 React 19 和 Express 5,完全基于 TypeScript 103 | 2. **样式系统升级** - 采用 Tailwind CSS v3,提供更灵活的样式定制 104 | 3. **类型安全** - 前后端均使用 TypeScript,提供完整的类型检查 105 | 4. **组件化设计** - 基于 HeroUI 组件库构建用户界面 106 | 5. **清晰的架构** - 前后端分离,结构清晰易于维护 107 | 6. **开发友好** - 支持热重载和并发开发 108 | 7. **可扩展性** - 模块化的代码结构便于功能扩展 -------------------------------------------------------------------------------- /client/src/types/video.ts: -------------------------------------------------------------------------------- 1 | export interface VideoItem { 2 | vod_id: number; 3 | type_id: number; 4 | type_id_1: number; 5 | group_id: number; 6 | vod_name: string; 7 | vod_sub: string; 8 | vod_en: string; 9 | vod_status: number; 10 | vod_letter: string; 11 | vod_color: string; 12 | vod_tag: string; 13 | vod_class: string; 14 | vod_pic: string; 15 | vod_pic_thumb: string; 16 | vod_pic_slide: string; 17 | vod_pic_screenshot: string | null; 18 | vod_actor: string; 19 | vod_director: string; 20 | vod_writer: string; 21 | vod_behind: string; 22 | vod_blurb: string; 23 | vod_remarks: string; 24 | vod_pubdate: string; 25 | vod_total: number; 26 | vod_serial: string; 27 | vod_tv: string; 28 | vod_weekday: string; 29 | vod_area: string; 30 | vod_lang: string; 31 | vod_year: string; 32 | vod_version: string; 33 | vod_state: string; 34 | vod_author: string; 35 | vod_jumpurl: string; 36 | vod_tpl: string; 37 | vod_tpl_play: string; 38 | vod_tpl_down: string; 39 | vod_isend: number; 40 | vod_lock: number; 41 | vod_level: number; 42 | vod_copyright: number; 43 | vod_points: number; 44 | vod_points_play: number; 45 | vod_points_down: number; 46 | vod_hits: number; 47 | vod_hits_day: number; 48 | vod_hits_week: number; 49 | vod_hits_month: number; 50 | vod_duration: string; 51 | vod_up: number; 52 | vod_down: number; 53 | vod_score: string; 54 | vod_score_all: number; 55 | vod_score_num: number; 56 | vod_time: string; 57 | vod_time_add: number; 58 | vod_time_hits: number; 59 | vod_time_make: number; 60 | vod_trysee: number; 61 | vod_douban_id: number; 62 | vod_douban_score: string; 63 | vod_reurl: string; 64 | vod_rel_vod: string; 65 | vod_rel_art: string; 66 | vod_pwd: string; 67 | vod_pwd_url: string; 68 | vod_pwd_play: string; 69 | vod_pwd_play_url: string; 70 | vod_pwd_down: string; 71 | vod_pwd_down_url: string; 72 | vod_content: string; 73 | vod_play_from: string; 74 | vod_play_server: string; 75 | vod_play_note: string; 76 | vod_play_url: string; 77 | vod_down_from: string; 78 | vod_down_server: string; 79 | vod_down_note: string; 80 | vod_down_url: string; 81 | vod_plot: number; 82 | vod_plot_name: string; 83 | vod_plot_detail: string; 84 | type_name: string; 85 | } 86 | 87 | export interface VideoListResponse { 88 | code: number; 89 | msg: string; 90 | page: number; 91 | pagecount: number; 92 | limit: string; 93 | total: number; 94 | list: VideoItem[]; 95 | class?: Category[]; 96 | } 97 | 98 | export interface Category { 99 | type_id: number; 100 | type_pid: number; 101 | type_name: string; 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReelFlix - 在线影视平台库 2 | 3 | 这是一个使用 React + Vite + HeroUI + Tailwind CSS 构建的前端和 Express (TypeScript) 构建的服务端的简约风格的在线影视平台。该项目旨在为用户提供一个简洁、高效且响应迅速的影视内容浏览和播放体验。并不涉及任何版权内容的存储或分发,仅作为个人视频内容的聚合和展示平台。 4 | 5 | ![项目预览](./demo/demo.png) 6 | 7 | ## 项目结构 8 | 9 | - `client/` - React 前端应用 (TypeScript + Tailwind CSS) 10 | - `server/` - Express 服务端 (TypeScript) 11 | 12 | ## 技术栈 13 | 14 | ### 前端 15 | - React 19 (TypeScript) 16 | - Vite 构建工具 17 | - HeroUI 组件库 18 | - Tailwind CSS v3 19 | - Framer Motion 动画库 20 | 21 | ### 后端 22 | - Express 5 (TypeScript) 23 | - Node.js 24 | - dotenv 环境变量管理 25 | - cors 跨域处理 26 | 27 | ### 开发工具 28 | - pnpm 包管理器 29 | - concurrently 并行执行脚本 30 | - TypeScript 类型检查 31 | 32 | ## 快速开始 33 | 34 | ### 先决条件 35 | - Node.js >= 19 36 | - pnpm >= 10 37 | 38 | 39 | ### 本地运行 40 | 41 | ```bash 42 | # 安装所有子项目依赖 43 | pnpm install-all 44 | # 同时启动前端和服务端 45 | pnpm run dev 46 | 47 | ## 其它命令 48 | # 只启动前端 49 | pnpm run dev:client 50 | 51 | # 只启动服务端 52 | pnpm run dev:server 53 | ``` 54 | 55 | ### 生产构建 56 | 57 | ```bash 58 | # 构建前后端应用 59 | pnpm run build 60 | 61 | # 分别构建前端和后端 62 | pnpm run build:client 63 | pnpm run build:server 64 | ``` 65 | 66 | ### Docker 部署(未测试) 67 | 68 | 项目支持使用 Docker 进行部署。 69 | 70 | #### 先决条件 71 | - Docker >= 20.10 72 | - Docker Compose >= 2.0 73 | 74 | #### 使用 Docker Compose 部署 75 | 76 | ```bash 77 | # 构建并启动服务 78 | docker-compose up --build 79 | 80 | # 后台运行 81 | docker-compose up --build -d 82 | 83 | # 停止服务 84 | docker-compose down 85 | ``` 86 | 87 | 服务将在以下端口运行: 88 | - 前端: http://localhost:8080 89 | - 后端: http://localhost:3000 90 | 91 | #### 单独构建镜像 92 | 93 | ```bash 94 | # 构建前端镜像 95 | docker build -f Dockerfile.client -t reelflix-client . 96 | 97 | # 构建后端镜像 98 | docker build -f Dockerfile.server -t reelflix-server . 99 | 100 | # 运行容器 101 | docker run -p 8080:80 reelflix-client 102 | docker run -p 3000:3000 reelflix-server 103 | ``` 104 | 105 | ### Vercel 部署(推荐) 106 | 107 | 项目支持使用 Vercel 部署。 108 | 109 | 1. git fork 本项目到您的 GitHub 账户。 110 | 2. 登录 Vercel 并导入您的仓库。 111 | 3. 直接 Deploy 112 | 113 | 114 | ## 项目特点 115 | 116 | 1. **现代化技术栈** - 使用最新的 React 19 和 Express 5,完全基于 TypeScript 117 | 2. **响应式设计** - 基于 HeroUI 组件库和 Tailwind CSS 构建的响应式用户界面 118 | 3. **类型安全** - 前后端均使用 TypeScript,提供完整的类型检查 119 | 4. **清晰的架构** - 前后端分离,结构清晰易于维护 120 | 5. **开发友好** - 支持热重载和并发开发 121 | 6. **可扩展性** - 模块化的代码结构便于功能扩展 122 | 123 | ## 免责声明 124 | 125 | ReelFlix 仅作为视频内容聚合平台,不对所展示内容的版权、合法性或准确性承担任何责任。用户应自行评估并承担使用风险。 126 | 127 | 请谨慎对待应用内外的广告信息。本平台不对广告内容的真实性或可靠性负责。 128 | 129 | 如果您遇到任何问题或有建议,请通过 GitHub 反馈:[https://github.com/1595901624/ReelFlix](https://github.com/1595901624/ReelFlix) 130 | 131 | ## TODO 132 | 133 | - [ ] PC 版 coming soon 134 | - [ ] 添加更多视频源支持 135 | - [ ] 优化移动端体验 136 | - [ ] 实现用户收藏功能 137 | 138 | ## 项目文档 139 | 140 | 详细的项目说明请查看 [PROJECT_SUMMARY.md](PROJECT_SUMMARY.md) 文件。 141 | 142 | ## 贡献 143 | 144 | 欢迎提交 Issue 和 Pull Request 来改进这个项目。 145 | 146 | ## 许可证 147 | 148 | MIT License - 详见 [LICENSE](LICENSE) 文件。 149 | -------------------------------------------------------------------------------- /client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/context/SettingsContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, useEffect } from 'react'; 2 | import { Category } from '../types/video'; 3 | import { fetchCategories } from '../services/api'; 4 | 5 | export interface ApiSource { 6 | name: string; 7 | url: string; 8 | } 9 | 10 | interface SettingsContextType { 11 | sources: ApiSource[]; 12 | currentSourceIndex: number; 13 | currentSource: ApiSource; 14 | categories: Category[]; 15 | addSource: (source: ApiSource) => void; 16 | removeSource: (index: number) => void; 17 | setCurrentSourceIndex: (index: number) => void; 18 | } 19 | 20 | const defaultSources: ApiSource[] = []; 21 | 22 | const SettingsContext = createContext(undefined); 23 | 24 | export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 25 | const [sources, setSources] = useState(() => { 26 | const saved = localStorage.getItem('reelFlix_sources'); 27 | return saved ? JSON.parse(saved) : defaultSources; 28 | }); 29 | 30 | const [currentSourceIndex, setCurrentSourceIndex] = useState(() => { 31 | const saved = localStorage.getItem('reelFlix_currentSourceIndex'); 32 | return saved ? parseInt(saved, 10) : 0; 33 | }); 34 | 35 | const [categories, setCategories] = useState([]); 36 | 37 | // 添加保护措施处理空源的情况 38 | const currentSource = sources.length > 0 ? (sources[currentSourceIndex] || sources[0]) : { name: '', url: '' }; 39 | 40 | useEffect(() => { 41 | localStorage.setItem('reelFlix_sources', JSON.stringify(sources)); 42 | }, [sources]); 43 | 44 | useEffect(() => { 45 | localStorage.setItem('reelFlix_currentSourceIndex', currentSourceIndex.toString()); 46 | }, [currentSourceIndex]); 47 | 48 | useEffect(() => { 49 | const fetchSourceCategories = async () => { 50 | // 如果没有可用的源或当前源无效,清空分类并返回 51 | if (sources.length === 0 || !currentSource?.url) { 52 | setCategories([]); 53 | return; 54 | } 55 | 56 | const cacheKey = `reelFlix_categories_${currentSource.url}`; 57 | const cached = localStorage.getItem(cacheKey); 58 | 59 | if (cached) { 60 | try { 61 | setCategories(JSON.parse(cached)); 62 | return; 63 | } catch (e) { 64 | console.error("Failed to parse cached categories", e); 65 | localStorage.removeItem(cacheKey); 66 | } 67 | } 68 | 69 | try { 70 | const res = await fetchCategories(currentSource.url); 71 | if (res.class) { 72 | setCategories(res.class); 73 | localStorage.setItem(cacheKey, JSON.stringify(res.class)); 74 | } 75 | } catch (err) { 76 | console.error('Failed to fetch categories:', err); 77 | setCategories([]); 78 | } 79 | }; 80 | 81 | fetchSourceCategories(); 82 | }, [currentSource, sources.length]); 83 | 84 | const addSource = (source: ApiSource) => { 85 | setSources([...sources, source]); 86 | }; 87 | 88 | const removeSource = (index: number) => { 89 | const newSources = sources.filter((_, i) => i !== index); 90 | setSources(newSources); 91 | 92 | // 如果删除后没有源了,将当前源索引设为0 93 | if (newSources.length === 0) { 94 | setCurrentSourceIndex(0); 95 | } else if (currentSourceIndex >= index && currentSourceIndex > 0) { 96 | setCurrentSourceIndex(currentSourceIndex - 1); 97 | } 98 | }; 99 | 100 | return ( 101 | 110 | {children} 111 | 112 | ); 113 | }; 114 | 115 | export const useSettings = () => { 116 | const context = useContext(SettingsContext); 117 | if (context === undefined) { 118 | throw new Error('useSettings must be used within a SettingsProvider'); 119 | } 120 | return context; 121 | }; 122 | -------------------------------------------------------------------------------- /client/src/components/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | Modal, 4 | ModalContent, 5 | ModalHeader, 6 | ModalBody, 7 | ModalFooter, 8 | Button, 9 | Input, 10 | Listbox, 11 | ListboxItem, 12 | Chip 13 | } from "@heroui/react"; 14 | import { useSettings } from '../context/SettingsContext'; 15 | 16 | interface SettingsModalProps { 17 | isOpen: boolean; 18 | onOpenChange: () => void; 19 | } 20 | 21 | export default function SettingsModal({ isOpen, onOpenChange }: SettingsModalProps) { 22 | const { sources, currentSourceIndex, addSource, removeSource, setCurrentSourceIndex } = useSettings(); 23 | const [newSourceName, setNewSourceName] = useState(''); 24 | const [newSourceUrl, setNewSourceUrl] = useState(''); 25 | 26 | const handleAddSource = () => { 27 | if (newSourceName && newSourceUrl) { 28 | addSource({ name: newSourceName, url: newSourceUrl }); 29 | setNewSourceName(''); 30 | setNewSourceUrl(''); 31 | } 32 | }; 33 | 34 | return ( 35 | 36 | 37 | {(onClose) => ( 38 | <> 39 | 设置 - 数据源配置 40 | 41 |
42 |
43 |

添加新源

44 |
45 | 52 | 59 |
60 | 63 |
64 | 65 |
66 |

现有源列表

67 |

点击选择当前使用的源

68 |
69 | setCurrentSourceIndex(Number(key))} 72 | className="p-0" 73 | > 74 | {sources.map((source, index) => ( 75 | 80 | {index === currentSourceIndex && 当前使用} 81 | 92 |
93 | } 94 | description={source.url} 95 | className={index === currentSourceIndex ? "bg-primary-50 dark:bg-primary-900/20" : ""} 96 | > 97 | {source.name} 98 | 99 | ))} 100 | 101 |
102 |
103 | 104 |
105 | 106 | 109 | 110 | 111 | )} 112 |
113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /client/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo } from 'react'; 2 | import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; 3 | import { 4 | Navbar, 5 | NavbarBrand, 6 | NavbarContent, 7 | NavbarItem, 8 | Link, 9 | Input, 10 | DropdownItem, 11 | DropdownTrigger, 12 | Dropdown, 13 | DropdownMenu, 14 | Avatar, 15 | useDisclosure 16 | } from "@heroui/react"; 17 | import { useSettings } from '../context/SettingsContext'; 18 | import SettingsModal from './SettingsModal'; 19 | import HelpAndFeedbackModal from './HelpAndFeedbackModal'; 20 | 21 | interface LayoutProps { 22 | children: React.ReactNode; 23 | } 24 | 25 | export default function Layout({ children }: LayoutProps) { 26 | const { currentSource, categories, sources } = useSettings(); 27 | const navigate = useNavigate(); 28 | const location = useLocation(); 29 | const [searchParams] = useSearchParams(); 30 | const currentCategory = searchParams.get('category'); 31 | const { isOpen, onOpen, onOpenChange } = useDisclosure(); 32 | const { isOpen: isHelpOpen, onOpen: onOpenHelp, onOpenChange: onOpenChangeHelp } = useDisclosure(); 33 | const [searchQuery, setSearchQuery] = useState(''); 34 | 35 | const mainCategories = useMemo(() => { 36 | return categories.filter(cat => cat.type_pid === 0); 37 | }, [categories]); 38 | 39 | const handleSearch = (e: React.KeyboardEvent) => { 40 | if (e.key === 'Enter' && searchQuery.trim()) { 41 | navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`); 42 | } 43 | }; 44 | 45 | return ( 46 |
47 | 48 | 49 | 50 | {/* Navigation Bar */} 51 | 52 | 53 | navigate('/')}> 54 |

ReelFlix

55 |
56 | 57 | {sources.length > 0 && ( 58 | <> 59 | 60 | navigate('/')} 64 | > 65 | 最近更新 66 | 67 | 68 | {mainCategories.map(cat => ( 69 | 70 | navigate(`/?category=${cat.type_id}`)} 74 | > 75 | {cat.type_name} 76 | 77 | 78 | ))} 79 | 80 | )} 81 | 82 |
83 | 84 | 85 | {sources.length > 0 && ( 86 | 97 | 98 | 99 | 100 | } 101 | type="search" 102 | value={searchQuery} 103 | onValueChange={setSearchQuery} 104 | onKeyDown={handleSearch} 105 | /> 106 | )} 107 | 108 | 109 | 118 | 119 | 120 | 121 |

当前媒资展示源

122 |

{sources.length > 0 ? currentSource.name : '未配置'}

123 |
124 | 设置 125 | 帮助与反馈 126 | {/* 127 | 退出登录 128 | */} 129 |
130 |
131 |
132 |
133 | 134 |
135 | {children} 136 |
137 | 138 |
139 |

© 2025 ReelFlix. All rights reserved.

140 |
141 |
142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /client/src/pages/play/PlayPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useMemo } from 'react'; 2 | import { useParams, useSearchParams } from 'react-router-dom'; 3 | import { Button, Card, CardBody, Spinner, Chip, ScrollShadow, useDisclosure } from "@heroui/react"; 4 | import { useSettings } from '../../context/SettingsContext'; 5 | import { fetchVideoDetail } from '../../services/api'; 6 | import { VideoItem } from '../../types/video'; 7 | import Player from '../../components/Player'; 8 | import EmptySourcesState from '../../components/EmptySourcesState'; 9 | import SettingsModal from '../../components/SettingsModal'; 10 | 11 | interface Episode { 12 | name: string; 13 | url: string; 14 | } 15 | 16 | interface PlayGroup { 17 | name: string; 18 | episodes: Episode[]; 19 | } 20 | 21 | export default function PlayPage() { 22 | const { id } = useParams<{ id: string }>(); 23 | const [searchParams] = useSearchParams(); 24 | const sourceUrlParam = searchParams.get('source'); 25 | 26 | const { currentSource, sources } = useSettings(); 27 | const { isOpen, onOpen, onOpenChange } = useDisclosure(); 28 | const [video, setVideo] = useState(null); 29 | const [loading, setLoading] = useState(true); 30 | const [error, setError] = useState(null); 31 | const [currentGroupIndex, setCurrentGroupIndex] = useState(0); 32 | const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(0); 33 | 34 | // Use source from query param if available, otherwise use current context source 35 | const activeSourceUrl = sourceUrlParam || currentSource.url; 36 | 37 | useEffect(() => { 38 | if (!id || sources.length === 0) return; 39 | 40 | setLoading(true); 41 | fetchVideoDetail(activeSourceUrl, parseInt(id)) 42 | .then(res => { 43 | if (res.list && res.list.length > 0) { 44 | setVideo(res.list[0]); 45 | } else { 46 | setError('Video not found'); 47 | } 48 | }) 49 | .catch(err => { 50 | console.error(err); 51 | setError('Failed to load video'); 52 | }) 53 | .finally(() => setLoading(false)); 54 | }, [id, activeSourceUrl]); 55 | 56 | const playGroups = useMemo(() => { 57 | if (!video) return []; 58 | 59 | const froms = video.vod_play_from.split('$$$'); 60 | const urls = video.vod_play_url.split('$$$'); 61 | 62 | return froms.map((from, index) => { 63 | const urlGroup = urls[index] || ''; 64 | const episodes = urlGroup.split('#').map(ep => { 65 | const [name, url] = ep.split('$'); 66 | return { name, url }; 67 | }).filter(ep => ep.url && ep.url.endsWith('.m3u8')); // Filter to only .m3u8 URLs 68 | 69 | return { 70 | name: from, 71 | episodes 72 | }; 73 | }).filter(group => group.episodes.length > 0); // Filter out groups with no valid episodes 74 | }, [video]); 75 | 76 | const currentEpisode = playGroups[currentGroupIndex]?.episodes[currentEpisodeIndex]; 77 | 78 | if (loading) return
; 79 | if (error || !video) return
{error || 'Video not found'}
; 80 | 81 | return ( 82 | <> 83 | 84 | {sources.length === 0 ? ( 85 | 86 | ) : ( 87 |
88 |
89 | 90 |
91 |
92 | {currentEpisode ? ( 93 | <> 94 |
95 | 正在播放: {video.vod_name} - {currentEpisode.name} 96 |
97 | 98 |

重要提示:请勿相信视频播放器中的任何广告!!!

99 | 100 | ) : ( 101 |
102 | No playable source found 103 |
104 | )} 105 | 106 |
107 |

{video.vod_name}

108 |
109 | {video.type_name} 110 | {video.vod_year} 111 | {video.vod_area} 112 | {video.vod_lang} 113 |
114 |
115 |
116 |
117 | 118 |
119 | 120 | 121 |

播放源

122 |
123 | {playGroups.map((group, idx) => ( 124 | 136 | ))} 137 |
138 | 139 |

选集

140 | 141 |
142 | {playGroups[currentGroupIndex]?.episodes.map((ep, idx) => ( 143 | 153 | ))} 154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | )} 163 | 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | concurrently: 12 | specifier: ^8.2.2 13 | version: 8.2.2 14 | 15 | packages: 16 | 17 | '@babel/runtime@7.28.4': 18 | resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} 19 | engines: {node: '>=6.9.0'} 20 | 21 | ansi-regex@5.0.1: 22 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 23 | engines: {node: '>=8'} 24 | 25 | ansi-styles@4.3.0: 26 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 27 | engines: {node: '>=8'} 28 | 29 | chalk@4.1.2: 30 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 31 | engines: {node: '>=10'} 32 | 33 | cliui@8.0.1: 34 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 35 | engines: {node: '>=12'} 36 | 37 | color-convert@2.0.1: 38 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 39 | engines: {node: '>=7.0.0'} 40 | 41 | color-name@1.1.4: 42 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 43 | 44 | concurrently@8.2.2: 45 | resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} 46 | engines: {node: ^14.13.0 || >=16.0.0} 47 | hasBin: true 48 | 49 | date-fns@2.30.0: 50 | resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} 51 | engines: {node: '>=0.11'} 52 | 53 | emoji-regex@8.0.0: 54 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 55 | 56 | escalade@3.2.0: 57 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 58 | engines: {node: '>=6'} 59 | 60 | get-caller-file@2.0.5: 61 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 62 | engines: {node: 6.* || 8.* || >= 10.*} 63 | 64 | has-flag@4.0.0: 65 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 66 | engines: {node: '>=8'} 67 | 68 | is-fullwidth-code-point@3.0.0: 69 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 70 | engines: {node: '>=8'} 71 | 72 | lodash@4.17.21: 73 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 74 | 75 | require-directory@2.1.1: 76 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 77 | engines: {node: '>=0.10.0'} 78 | 79 | rxjs@7.8.2: 80 | resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} 81 | 82 | shell-quote@1.8.3: 83 | resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} 84 | engines: {node: '>= 0.4'} 85 | 86 | spawn-command@0.0.2: 87 | resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} 88 | 89 | string-width@4.2.3: 90 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 91 | engines: {node: '>=8'} 92 | 93 | strip-ansi@6.0.1: 94 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 95 | engines: {node: '>=8'} 96 | 97 | supports-color@7.2.0: 98 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 99 | engines: {node: '>=8'} 100 | 101 | supports-color@8.1.1: 102 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 103 | engines: {node: '>=10'} 104 | 105 | tree-kill@1.2.2: 106 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 107 | hasBin: true 108 | 109 | tslib@2.8.1: 110 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 111 | 112 | wrap-ansi@7.0.0: 113 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 114 | engines: {node: '>=10'} 115 | 116 | y18n@5.0.8: 117 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 118 | engines: {node: '>=10'} 119 | 120 | yargs-parser@21.1.1: 121 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 122 | engines: {node: '>=12'} 123 | 124 | yargs@17.7.2: 125 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 126 | engines: {node: '>=12'} 127 | 128 | snapshots: 129 | 130 | '@babel/runtime@7.28.4': {} 131 | 132 | ansi-regex@5.0.1: {} 133 | 134 | ansi-styles@4.3.0: 135 | dependencies: 136 | color-convert: 2.0.1 137 | 138 | chalk@4.1.2: 139 | dependencies: 140 | ansi-styles: 4.3.0 141 | supports-color: 7.2.0 142 | 143 | cliui@8.0.1: 144 | dependencies: 145 | string-width: 4.2.3 146 | strip-ansi: 6.0.1 147 | wrap-ansi: 7.0.0 148 | 149 | color-convert@2.0.1: 150 | dependencies: 151 | color-name: 1.1.4 152 | 153 | color-name@1.1.4: {} 154 | 155 | concurrently@8.2.2: 156 | dependencies: 157 | chalk: 4.1.2 158 | date-fns: 2.30.0 159 | lodash: 4.17.21 160 | rxjs: 7.8.2 161 | shell-quote: 1.8.3 162 | spawn-command: 0.0.2 163 | supports-color: 8.1.1 164 | tree-kill: 1.2.2 165 | yargs: 17.7.2 166 | 167 | date-fns@2.30.0: 168 | dependencies: 169 | '@babel/runtime': 7.28.4 170 | 171 | emoji-regex@8.0.0: {} 172 | 173 | escalade@3.2.0: {} 174 | 175 | get-caller-file@2.0.5: {} 176 | 177 | has-flag@4.0.0: {} 178 | 179 | is-fullwidth-code-point@3.0.0: {} 180 | 181 | lodash@4.17.21: {} 182 | 183 | require-directory@2.1.1: {} 184 | 185 | rxjs@7.8.2: 186 | dependencies: 187 | tslib: 2.8.1 188 | 189 | shell-quote@1.8.3: {} 190 | 191 | spawn-command@0.0.2: {} 192 | 193 | string-width@4.2.3: 194 | dependencies: 195 | emoji-regex: 8.0.0 196 | is-fullwidth-code-point: 3.0.0 197 | strip-ansi: 6.0.1 198 | 199 | strip-ansi@6.0.1: 200 | dependencies: 201 | ansi-regex: 5.0.1 202 | 203 | supports-color@7.2.0: 204 | dependencies: 205 | has-flag: 4.0.0 206 | 207 | supports-color@8.1.1: 208 | dependencies: 209 | has-flag: 4.0.0 210 | 211 | tree-kill@1.2.2: {} 212 | 213 | tslib@2.8.1: {} 214 | 215 | wrap-ansi@7.0.0: 216 | dependencies: 217 | ansi-styles: 4.3.0 218 | string-width: 4.2.3 219 | strip-ansi: 6.0.1 220 | 221 | y18n@5.0.8: {} 222 | 223 | yargs-parser@21.1.1: {} 224 | 225 | yargs@17.7.2: 226 | dependencies: 227 | cliui: 8.0.1 228 | escalade: 3.2.0 229 | get-caller-file: 2.0.5 230 | require-directory: 2.1.1 231 | string-width: 4.2.3 232 | y18n: 5.0.8 233 | yargs-parser: 21.1.1 234 | -------------------------------------------------------------------------------- /client/src/pages/home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from 'react'; 2 | import { useNavigate, useSearchParams } from 'react-router-dom'; 3 | import { 4 | Card, 5 | CardBody, 6 | CardFooter, 7 | Image, 8 | Button, 9 | Chip, 10 | Spinner, 11 | useDisclosure 12 | } from "@heroui/react"; 13 | import { useSettings } from '../../context/SettingsContext'; 14 | import { fetchVideoList } from '../../services/api'; 15 | import { VideoItem } from '../../types/video'; 16 | import SettingsModal from '../../components/SettingsModal'; 17 | import EmptySourcesState from '../../components/EmptySourcesState'; 18 | 19 | export default function HomePage() { 20 | const { currentSource, categories, sources } = useSettings(); 21 | const navigate = useNavigate(); 22 | const [searchParams] = useSearchParams(); 23 | const selectedTab = searchParams.get('category') || 'home'; 24 | 25 | const [videos, setVideos] = useState([]); 26 | const [loading, setLoading] = useState(true); 27 | const [error, setError] = useState(null); 28 | const { isOpen, onOpen, onOpenChange } = useDisclosure(); 29 | const [activeSlide, setActiveSlide] = useState(0); 30 | const [selectedSubCategory, setSelectedSubCategory] = useState(null); 31 | 32 | // Main categories (type_pid === 0) 33 | const mainCategories = useMemo(() => { 34 | return categories.filter(cat => cat.type_pid === 0); 35 | }, [categories]); 36 | 37 | // Sub categories for selected tab 38 | const subCategories = useMemo(() => { 39 | if (selectedTab === 'home') return []; 40 | const mainCat = mainCategories.find(cat => cat.type_id.toString() === selectedTab); 41 | if (!mainCat) return []; 42 | return categories.filter(cat => cat.type_pid === mainCat.type_id); 43 | }, [selectedTab, categories, mainCategories]); 44 | 45 | // Reset sub-category when tab changes 46 | useEffect(() => { 47 | setSelectedSubCategory(null); 48 | }, [selectedTab]); 49 | 50 | // Get type IDs to fetch 51 | const getTypeIdsForFetch = (): number[] | undefined => { 52 | if (selectedTab === 'home') return undefined; // All types 53 | 54 | if (selectedSubCategory) { 55 | return [selectedSubCategory]; // Single sub-category 56 | } 57 | 58 | // Get all sub-category IDs for the selected main category 59 | const subs = subCategories.map(cat => cat.type_id); 60 | return subs.length > 0 ? subs : [parseInt(selectedTab)]; 61 | }; 62 | 63 | useEffect(() => { 64 | // 如果没有可用源,跳过加载 65 | if (sources.length === 0 || !currentSource.url) { 66 | setLoading(false); 67 | setVideos([]); 68 | return; 69 | } 70 | 71 | setLoading(true); 72 | const typeIds = getTypeIdsForFetch(); 73 | fetchVideoList(currentSource.url, 1, typeIds) 74 | .then(res => { 75 | setVideos(res.list || []); 76 | setError(null); 77 | }) 78 | .catch(err => { 79 | console.error(err); 80 | setError('Failed to load videos. Please check your API source settings.'); 81 | }) 82 | .finally(() => setLoading(false)); 83 | }, [currentSource, selectedTab, selectedSubCategory, sources.length]); 84 | 85 | useEffect(() => { 86 | if (videos.length === 0) return; 87 | const interval = setInterval(() => { 88 | setActiveSlide(prev => (prev + 1) % Math.min(videos.length, 5)); 89 | }, 5000); 90 | return () => clearInterval(interval); 91 | }, [videos]); 92 | 93 | const featuredVideos = videos.slice(0, 5); 94 | const currentFeatured = featuredVideos[activeSlide]; 95 | const otherVideos = videos.length > 5 ? videos.slice(5) : []; 96 | 97 | const handlePlay = (id: number) => { 98 | navigate(`/play/${id}`); 99 | }; 100 | 101 | return ( 102 |
103 | 104 | 105 | {/* 如果没有源,显示空状态页面 */} 106 | {sources.length === 0 ? ( 107 | 108 | ) : ( 109 | <> 110 | {/* Sub-category Filters */} 111 | {subCategories.length > 0 && ( 112 |
113 | 121 | {subCategories.map(cat => ( 122 | 131 | ))} 132 |
133 | )} 134 | 135 | {loading ? ( 136 |
137 | 138 |
139 | ) : error ? ( 140 |
141 |

{error}

142 | 143 |
144 | ) : ( 145 | <> 146 | {/* Hero Section - Carousel */} 147 | {currentFeatured && ( 148 |
149 | {/* Background Image with Transition */} 150 |
151 | {currentFeatured.vod_name} 157 | 158 | {/* Content Overlay */} 159 |
160 |
161 | {currentFeatured.type_name} 162 | {currentFeatured.vod_year} 163 | {currentFeatured.vod_area} 164 |
165 |

{currentFeatured.vod_name}

166 |

{currentFeatured.vod_sub}

167 |

168 | {currentFeatured.vod_blurb || currentFeatured.vod_content.replace(/<[^>]*>?/gm, '').substring(0, 150) + '...'} 169 |

170 |
171 | 179 |
180 |
181 | 182 | {/* Carousel Indicators */} 183 |
184 | {featuredVideos.map((_, idx) => ( 185 |
192 |
193 | )} 194 | 195 | {/* Content Grid */} 196 |
197 |
198 |

最新更新

199 |
200 | 201 |
202 | {otherVideos.map((video) => ( 203 | handlePlay(video.vod_id)} 208 | className="border-none bg-transparent hover:scale-105 transition-transform duration-200" 209 | > 210 | 211 | {/* Blurred background for small images */} 212 |
216 | {video.vod_name} 224 |
225 | {video.vod_remarks || 'HD'} 226 |
227 |
228 | 229 | 230 | {video.vod_name} 231 |

{video.type_name} • {video.vod_year}

232 |
233 | 234 | ))} 235 |
236 |
237 | 238 | )} 239 | 240 | )} 241 |
242 | ); 243 | } -------------------------------------------------------------------------------- /client/src/pages/search/SearchPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo, useRef } from 'react'; 2 | import { useSearchParams } from 'react-router-dom'; 3 | import { 4 | Card, 5 | CardBody, 6 | CardFooter, 7 | Image, 8 | Chip, 9 | Spinner, 10 | Listbox, 11 | ListboxItem, 12 | useDisclosure 13 | } from "@heroui/react"; 14 | import { useSettings } from '../../context/SettingsContext'; 15 | import { fetchVideoList } from '../../services/api'; 16 | import { VideoItem } from '../../types/video'; 17 | import EmptySourcesState from '../../components/EmptySourcesState'; 18 | import SettingsModal from '../../components/SettingsModal'; 19 | 20 | interface SearchResultItem extends VideoItem { 21 | sourceName: string; 22 | sourceUrl: string; 23 | } 24 | 25 | export default function SearchPage() { 26 | const [searchParams] = useSearchParams(); 27 | const query = searchParams.get('q') || ''; 28 | const { sources } = useSettings(); 29 | const { isOpen, onOpen, onOpenChange } = useDisclosure(); 30 | 31 | const [results, setResults] = useState([]); 32 | const [loading, setLoading] = useState(false); 33 | const [error, setError] = useState(null); 34 | 35 | // NOTE: Avoid using the literal key "all" because the underlying selection API may treat 36 | // it as a special value (select-all). Using a dedicated sentinel prevents occasional 37 | // mis-selection and wrong filtering. 38 | const ALL_SOURCES_KEY = '__all_sources__'; 39 | const [selectedSourceFilter, setSelectedSourceFilter] = useState(ALL_SOURCES_KEY); 40 | 41 | // Throttling ref 42 | const lastSearchTimeRef = useRef(0); 43 | // 用于清理定时器 44 | const searchCleanupRef = useRef<(() => void) | null>(null); 45 | // 用于忽略旧的搜索结果(防止并发/节流导致的“串结果”) 46 | const searchTokenRef = useRef(0); 47 | // 用于跟踪搜索状态 48 | const searchCompletionRef = useRef({ 49 | total: 0, 50 | completed: 0, 51 | successCount: 0 52 | }); 53 | 54 | useEffect(() => { 55 | if (!query || sources.length === 0) return; 56 | 57 | const performSearch = async () => { 58 | const token = ++searchTokenRef.current; 59 | // 重置状态 60 | setLoading(true); 61 | setError(null); 62 | setResults([]); 63 | 64 | // 重置搜索完成跟踪 65 | searchCompletionRef.current = { 66 | total: sources.length, 67 | completed: 0, 68 | successCount: 0 69 | }; 70 | 71 | // 清理之前的清理函数 72 | if (searchCleanupRef.current) { 73 | searchCleanupRef.current(); 74 | } 75 | 76 | try { 77 | // 为每个源创建独立的搜索任务 78 | sources.forEach(async (source) => { 79 | try { 80 | const response = await fetchVideoList(source.url, 1, undefined, query); 81 | // 如果这次返回已经不是最新的一轮搜索,忽略 82 | if (token !== searchTokenRef.current) return; 83 | 84 | const sourceResults = (response.list || []).map(item => ({ 85 | ...item, 86 | sourceName: source.name, 87 | sourceUrl: source.url 88 | })); 89 | 90 | // 更新结果状态 - 每个源返回结果后立即更新 91 | setResults(prevResults => { 92 | if (token !== searchTokenRef.current) return prevResults; 93 | const newResults = [...prevResults, ...sourceResults]; 94 | return newResults; 95 | }); 96 | 97 | // 增加成功计数 98 | searchCompletionRef.current.successCount += 1; 99 | } catch (err) { 100 | console.error(`Failed to fetch from ${source.name}:`, err); 101 | // 即使单个源出错也不影响其他源 102 | } finally { 103 | if (token !== searchTokenRef.current) return; 104 | // 更新完成计数 105 | searchCompletionRef.current.completed += 1; 106 | 107 | // 检查是否所有源都已完成 108 | if (searchCompletionRef.current.completed >= searchCompletionRef.current.total) { 109 | // 延迟检查最终结果 110 | setTimeout(() => { 111 | if (token !== searchTokenRef.current) return; 112 | // 检查是否有任何结果 113 | setResults(currentResults => { 114 | if (token !== searchTokenRef.current) return currentResults; 115 | if (currentResults.length === 0 && searchCompletionRef.current.successCount === 0) { 116 | setError('未找到相关结果'); 117 | } 118 | setLoading(false); 119 | return currentResults; 120 | }); 121 | }, 100); 122 | } 123 | } 124 | }); 125 | 126 | } catch (err) { 127 | console.error(err); 128 | setError('搜索过程中发生错误'); 129 | setLoading(false); 130 | } finally { 131 | lastSearchTimeRef.current = Date.now(); 132 | } 133 | }; 134 | 135 | const now = Date.now(); 136 | const timeSinceLast = now - lastSearchTimeRef.current; 137 | const minInterval = 5000; // 5 seconds 138 | 139 | let timer: number; 140 | 141 | if (timeSinceLast < minInterval) { 142 | const delay = minInterval - timeSinceLast; 143 | console.log(`Throttling search. Waiting ${delay}ms`); 144 | timer = setTimeout(performSearch, delay); 145 | } else { 146 | performSearch(); 147 | } 148 | 149 | // 设置清理函数 150 | searchCleanupRef.current = () => { 151 | if (timer) clearTimeout(timer); 152 | }; 153 | 154 | return () => { 155 | // 组件卸载时执行清理 156 | if (searchCleanupRef.current) { 157 | searchCleanupRef.current(); 158 | } 159 | }; 160 | }, [query, sources]); 161 | 162 | // If the selected source was removed/edited in settings, fallback to "all" 163 | useEffect(() => { 164 | if (selectedSourceFilter === ALL_SOURCES_KEY) return; 165 | const stillExists = sources.some(s => s.url === selectedSourceFilter); 166 | if (!stillExists) { 167 | setSelectedSourceFilter(ALL_SOURCES_KEY); 168 | } 169 | }, [sources, selectedSourceFilter]); 170 | 171 | const handlePlay = (id: number, sourceUrl: string) => { 172 | // 在新标签页中打开播放页面 173 | const url = `/play/${id}?source=${encodeURIComponent(sourceUrl)}`; 174 | window.open(url, '_blank'); 175 | }; 176 | 177 | const filteredResults = useMemo(() => { 178 | if (selectedSourceFilter === ALL_SOURCES_KEY) return results; 179 | return results.filter(r => r.sourceUrl === selectedSourceFilter); 180 | }, [results, selectedSourceFilter]); 181 | 182 | // Group sources for the sidebar 183 | const sourceCounts = useMemo(() => { 184 | const counts: Record = {}; 185 | results.forEach(r => { 186 | counts[r.sourceUrl] = (counts[r.sourceUrl] || 0) + 1; 187 | }); 188 | return counts; 189 | }, [results]); 190 | 191 | const filterItems = useMemo(() => [ 192 | { key: ALL_SOURCES_KEY, label: "全部", count: results.length, icon: "🌐" }, 193 | ...sources.map(source => ({ 194 | key: source.url, 195 | label: source.name, 196 | count: sourceCounts[source.url] || 0, 197 | icon: "📺" 198 | })) 199 | ], [sources, results.length, sourceCounts]); 200 | 201 | return ( 202 | <> 203 | 204 | {sources.length === 0 ? ( 205 | 206 | ) : ( 207 |
208 | {/* Sidebar - Source Filter */} 209 |
210 |
211 |

搜索源

212 | 213 | { 222 | // HeroUI/NextUI selection can be: Set | 'all'. Also allow empty set. 223 | if (keys === 'all') { 224 | setSelectedSourceFilter(ALL_SOURCES_KEY); 225 | return; 226 | } 227 | 228 | const selected = Array.from(keys as Set)[0]; 229 | setSelectedSourceFilter(selected ? String(selected) : ALL_SOURCES_KEY); 230 | }} 231 | > 232 | {(item) => ( 233 | {item.icon}} endContent={{item.count}}> 234 | {item.label} 235 | 236 | )} 237 | 238 | 239 |
240 |
241 | 242 | {/* Main Content - Results */} 243 |
244 |

245 | 搜索结果: {query} 246 |

247 | 248 | {loading && results.length === 0 ? ( 249 |
250 | 251 |
252 | ) : error && results.length === 0 ? ( 253 |
254 |

{error}

255 |
256 | ) : ( 257 | <> 258 | {filteredResults.length === 0 && results.length > 0 ? ( 259 |
260 |

该源下没有找到结果

261 |
262 | ) : ( 263 |
264 | {filteredResults.map((video) => ( 265 | handlePlay(video.vod_id, video.sourceUrl)} 270 | className="border-none bg-transparent hover:scale-105 transition-transform duration-200" 271 | > 272 | 273 | {/* Blurred background for small images */} 274 |
278 | {video.vod_name} 286 |
287 | 288 | {video.vod_remarks || 'HD'} 289 | 290 | 291 | {video.sourceName} 292 | 293 |
294 |
295 | 296 | 297 | {video.vod_name} 298 |

{video.type_name} • {video.vod_year}

299 |
300 | 301 | ))} 302 |
303 | )} 304 | 305 | )} 306 |
307 |
308 | )} 309 | 310 | ); 311 | } 312 | -------------------------------------------------------------------------------- /client/src/data/mockData.ts: -------------------------------------------------------------------------------- 1 | import { VideoListResponse } from '../types/video'; 2 | 3 | export const mockVideoData: VideoListResponse = { 4 | "code": 1, 5 | "msg": "数据列表", 6 | "page": 1, 7 | "pagecount": 4398, 8 | "limit": "20", 9 | "total": 87941, 10 | "list": [ 11 | { 12 | "vod_id": 88887, 13 | "type_id": 33, 14 | "type_id_1": 27, 15 | "group_id": 0, 16 | "vod_name": "曼哈顿金牌经纪第二季", 17 | "vod_sub": "Owning Manhattan Season 2", 18 | "vod_en": "manhadunjinpaijingjidierji", 19 | "vod_status": 1, 20 | "vod_letter": "M", 21 | "vod_color": "", 22 | "vod_tag": "真人秀", 23 | "vod_class": "真人秀,欧美综艺", 24 | "vod_pic": "https://img.jisuimage.com/cover/0e770b4a7e05a938625b9c97a6eec6f2.jpg", 25 | "vod_pic_thumb": "", 26 | "vod_pic_slide": "", 27 | "vod_pic_screenshot": null, 28 | "vod_actor": "莱恩·斯汉特", 29 | "vod_director": "", 30 | "vod_writer": "", 31 | "vod_behind": "", 32 | "vod_blurb": "Netflix has ordered a second season of Owning Manhattan, the real estate series starring Ryan Serhan", 33 | "vod_remarks": "第8期完结", 34 | "vod_pubdate": "2025-12-05(美国)", 35 | "vod_total": 1, 36 | "vod_serial": "0", 37 | "vod_tv": "", 38 | "vod_weekday": "", 39 | "vod_area": "美国", 40 | "vod_lang": "英语", 41 | "vod_year": "2025", 42 | "vod_version": "", 43 | "vod_state": "", 44 | "vod_author": "", 45 | "vod_jumpurl": "", 46 | "vod_tpl": "", 47 | "vod_tpl_play": "", 48 | "vod_tpl_down": "", 49 | "vod_isend": 0, 50 | "vod_lock": 0, 51 | "vod_level": 0, 52 | "vod_copyright": 0, 53 | "vod_points": 0, 54 | "vod_points_play": 0, 55 | "vod_points_down": 0, 56 | "vod_hits": 585, 57 | "vod_hits_day": 935, 58 | "vod_hits_week": 711, 59 | "vod_hits_month": 922, 60 | "vod_duration": "", 61 | "vod_up": 863, 62 | "vod_down": 419, 63 | "vod_score": "0.0", 64 | "vod_score_all": 0, 65 | "vod_score_num": 0, 66 | "vod_time": "2025-12-07 16:21:03", 67 | "vod_time_add": 1765091882, 68 | "vod_time_hits": 0, 69 | "vod_time_make": 0, 70 | "vod_trysee": 0, 71 | "vod_douban_id": 37019481, 72 | "vod_douban_score": "0.0", 73 | "vod_reurl": "", 74 | "vod_rel_vod": "", 75 | "vod_rel_art": "", 76 | "vod_pwd": "", 77 | "vod_pwd_url": "", 78 | "vod_pwd_play": "", 79 | "vod_pwd_play_url": "", 80 | "vod_pwd_down": "", 81 | "vod_pwd_down_url": "", 82 | "vod_content": "Netflix has ordered a second season of Owning Manhattan, the real estate series starring Ryan Serhant.", 83 | "vod_play_from": "jsyun$$$jsm3u8", 84 | "vod_play_server": "$$$", 85 | "vod_play_note": "$$$", 86 | "vod_play_url": "第1期$https://bf.jisuziyuanbf.com/play/bqxR0V2a#第2期$https://bf.jisuziyuanbf.com/play/e1w1lq0b#第3期$https://bf.jisuziyuanbf.com/play/egJYjGZd#第4期$https://bf.jisuziyuanbf.com/play/e5ynpvYe#第5期$https://bf.jisuziyuanbf.com/play/bo2RroXa#第6期$https://bf.jisuziyuanbf.com/play/e9r6wwDa#第7期$https://bf.jisuziyuanbf.com/play/bqxR040a#第8期完结$https://bf.jisuziyuanbf.com/play/azpPJjZd$$$第1期$https://bf.jisuziyuanbf.com/play/bqxR0V2a/index.m3u8#第2期$https://bf.jisuziyuanbf.com/play/e1w1lq0b/index.m3u8#第3期$https://bf.jisuziyuanbf.com/play/egJYjGZd/index.m3u8#第4期$https://bf.jisuziyuanbf.com/play/e5ynpvYe/index.m3u8#第5期$https://bf.jisuziyuanbf.com/play/bo2RroXa/index.m3u8#第6期$https://bf.jisuziyuanbf.com/play/e9r6wwDa/index.m3u8#第7期$https://bf.jisuziyuanbf.com/play/bqxR040a/index.m3u8#第8期完结$https://bf.jisuziyuanbf.com/play/azpPJjZd/index.m3u8", 87 | "vod_down_from": "", 88 | "vod_down_server": "", 89 | "vod_down_note": "", 90 | "vod_down_url": "", 91 | "vod_plot": 0, 92 | "vod_plot_name": "", 93 | "vod_plot_detail": "", 94 | "type_name": "欧美综艺" 95 | }, 96 | { 97 | "vod_id": 84329, 98 | "type_id": 6, 99 | "type_id_1": 1, 100 | "group_id": 0, 101 | "vod_name": "二人逃避", 102 | "vod_sub": "", 103 | "vod_en": "errentaobi", 104 | "vod_status": 1, 105 | "vod_letter": "E", 106 | "vod_color": "", 107 | "vod_tag": "剧情,喜剧,同性", 108 | "vod_class": "剧情,喜剧,同性,日剧", 109 | "vod_pic": "https://img.jisuimage.com/cover/d9323637459c0742dbbe58ac05563206.jpg", 110 | "vod_pic_thumb": "", 111 | "vod_pic_slide": "", 112 | "vod_pic_screenshot": null, 113 | "vod_actor": "岩本莲加,富里奈央", 114 | "vod_director": "東かほり", 115 | "vod_writer": "", 116 | "vod_behind": "", 117 | "vod_blurb": "本剧改编自田口囁一的同名作品。故事围绕以“可爱”为最大优点的无业“前辈”与被截稿日期穷追不舍的漫画家“后辈”共同展开的逃避现实题材喜剧。两人时而尝试用匪夷所思的方法封印现代人寸不离身的手机,时而在公园", 118 | "vod_remarks": "第10集", 119 | "vod_pubdate": "2025-10-04(日本)", 120 | "vod_total": 1, 121 | "vod_serial": "0", 122 | "vod_tv": "", 123 | "vod_weekday": "", 124 | "vod_area": "日本", 125 | "vod_lang": "日语", 126 | "vod_year": "2025", 127 | "vod_version": "", 128 | "vod_state": "", 129 | "vod_author": "", 130 | "vod_jumpurl": "", 131 | "vod_tpl": "", 132 | "vod_tpl_play": "", 133 | "vod_tpl_down": "", 134 | "vod_isend": 0, 135 | "vod_lock": 0, 136 | "vod_level": 0, 137 | "vod_copyright": 0, 138 | "vod_points": 0, 139 | "vod_points_play": 0, 140 | "vod_points_down": 0, 141 | "vod_hits": 112, 142 | "vod_hits_day": 898, 143 | "vod_hits_week": 906, 144 | "vod_hits_month": 131, 145 | "vod_duration": "", 146 | "vod_up": 629, 147 | "vod_down": 988, 148 | "vod_score": "0.0", 149 | "vod_score_all": 0, 150 | "vod_score_num": 0, 151 | "vod_time": "2025-12-07 16:21:02", 152 | "vod_time_add": 1759658582, 153 | "vod_time_hits": 0, 154 | "vod_time_make": 0, 155 | "vod_trysee": 0, 156 | "vod_douban_id": 37505352, 157 | "vod_douban_score": "0.0", 158 | "vod_reurl": "", 159 | "vod_rel_vod": "", 160 | "vod_rel_art": "", 161 | "vod_pwd": "", 162 | "vod_pwd_url": "", 163 | "vod_pwd_play": "", 164 | "vod_pwd_play_url": "", 165 | "vod_pwd_down": "", 166 | "vod_pwd_down_url": "", 167 | "vod_content": "本剧改编自田口囁一的同名作品。故事围绕以“可爱”为最大优点的无业“前辈”与被截稿日期穷追不舍的漫画家“后辈”共同展开的逃避现实题材喜剧。两人时而尝试用匪夷所思的方法封印现代人寸不离身的手机,时而在公园重拾童心,时而用稿费购买豪华食材……在这个人人强调时间性价比与成本效益的时代,她们不顾时间与金钱,更不顾及后果,只要灵光一闪就立刻行动,尽情上演一场场“逃避现实”的戏码。", 168 | "vod_play_from": "jsyun$$$jsm3u8", 169 | "vod_play_server": "$$$", 170 | "vod_play_note": "$$$", 171 | "vod_play_url": "第1集$https://bf.jisuziyuanbf.com/play/Pdy1NDWd#第2集$https://bf.jisuziyuanbf.com/play/BeXjZmlb#第3集$https://bf.jisuziyuanbf.com/play/Rb4EQE7d#第4集$https://bf.jisuziyuanbf.com/play/rb2zXDjb#第5集$https://bf.jisuziyuanbf.com/play/6dBljJYa#第6集$https://bf.jisuziyuanbf.com/play/erkZEpKa#第7集$https://bf.jisuziyuanbf.com/play/aADqQoPe#第8集$https://bf.jisuziyuanbf.com/play/elYQv0ra#第9集$https://bf.jisuziyuanbf.com/play/bo2RKzNa#第10集$https://bf.jisuziyuanbf.com/play/bYEpALMb$$$第1集$https://bf.jisuziyuanbf.com/play/Pdy1NDWd/index.m3u8#第2集$https://bf.jisuziyuanbf.com/play/BeXjZmlb/index.m3u8#第3集$https://bf.jisuziyuanbf.com/play/Rb4EQE7d/index.m3u8#第4集$https://bf.jisuziyuanbf.com/play/rb2zXDjb/index.m3u8#第5集$https://bf.jisuziyuanbf.com/play/6dBljJYa/index.m3u8#第6集$https://bf.jisuziyuanbf.com/play/erkZEpKa/index.m3u8#第7集$https://bf.jisuziyuanbf.com/play/aADqQoPe/index.m3u8#第8集$https://bf.jisuziyuanbf.com/play/elYQv0ra/index.m3u8#第9集$https://bf.jisuziyuanbf.com/play/bo2RKzNa/index.m3u8#第10集$https://bf.jisuziyuanbf.com/play/bYEpALMb/index.m3u8", 172 | "vod_down_from": "", 173 | "vod_down_server": "", 174 | "vod_down_note": "", 175 | "vod_down_url": "", 176 | "vod_plot": 0, 177 | "vod_plot_name": "", 178 | "vod_plot_detail": "", 179 | "type_name": "日剧" 180 | }, 181 | { 182 | "vod_id": 88897, 183 | "type_id": 29, 184 | "type_id_1": 0, 185 | "group_id": 0, 186 | "vod_name": "12月6日25-26赛季德甲联赛 海登海姆VS弗赖堡", 187 | "vod_sub": "", 188 | "vod_en": "12yue6ri2526saijidejialiansaihaidenghaimuVSfulaibao", 189 | "vod_status": 1, 190 | "vod_letter": "1", 191 | "vod_color": "", 192 | "vod_tag": "", 193 | "vod_class": "体育赛事", 194 | "vod_pic": "https://img.jisuimage.com/cover/d1efa44d011d4b401e653eacf18fd721.jpg", 195 | "vod_pic_thumb": "", 196 | "vod_pic_slide": "", 197 | "vod_pic_screenshot": null, 198 | "vod_actor": "", 199 | "vod_director": "", 200 | "vod_writer": "", 201 | "vod_behind": "", 202 | "vod_blurb": "暂无简介", 203 | "vod_remarks": "正片", 204 | "vod_pubdate": "", 205 | "vod_total": 1, 206 | "vod_serial": "0", 207 | "vod_tv": "", 208 | "vod_weekday": "", 209 | "vod_area": "", 210 | "vod_lang": "", 211 | "vod_year": "2025", 212 | "vod_version": "", 213 | "vod_state": "", 214 | "vod_author": "", 215 | "vod_jumpurl": "", 216 | "vod_tpl": "", 217 | "vod_tpl_play": "", 218 | "vod_tpl_down": "", 219 | "vod_isend": 1, 220 | "vod_lock": 0, 221 | "vod_level": 0, 222 | "vod_copyright": 0, 223 | "vod_points": 0, 224 | "vod_points_play": 0, 225 | "vod_points_down": 0, 226 | "vod_hits": 877, 227 | "vod_hits_day": 163, 228 | "vod_hits_week": 281, 229 | "vod_hits_month": 939, 230 | "vod_duration": "", 231 | "vod_up": 963, 232 | "vod_down": 351, 233 | "vod_score": "0.0", 234 | "vod_score_all": 0, 235 | "vod_score_num": 0, 236 | "vod_time": "2025-12-07 16:18:02", 237 | "vod_time_add": 1765095482, 238 | "vod_time_hits": 0, 239 | "vod_time_make": 0, 240 | "vod_trysee": 0, 241 | "vod_douban_id": 0, 242 | "vod_douban_score": "0.0", 243 | "vod_reurl": "", 244 | "vod_rel_vod": "", 245 | "vod_rel_art": "", 246 | "vod_pwd": "", 247 | "vod_pwd_url": "", 248 | "vod_pwd_play": "", 249 | "vod_pwd_play_url": "", 250 | "vod_pwd_down": "", 251 | "vod_pwd_down_url": "", 252 | "vod_content": "暂无简介", 253 | "vod_play_from": "jsyun$$$jsm3u8", 254 | "vod_play_server": "$$$", 255 | "vod_play_note": "$$$", 256 | "vod_play_url": "正片$https://bf.jisuziyuanbf.com/play/e1w1llmb$$$正片$https://bf.jisuziyuanbf.com/play/e1w1llmb/index.m3u8", 257 | "vod_down_from": "", 258 | "vod_down_server": "", 259 | "vod_down_note": "", 260 | "vod_down_url": "", 261 | "vod_plot": 0, 262 | "vod_plot_name": "", 263 | "vod_plot_detail": "", 264 | "type_name": "体育赛事" 265 | }, 266 | { 267 | "vod_id": 88896, 268 | "type_id": 29, 269 | "type_id_1": 0, 270 | "group_id": 0, 271 | "vod_name": "12月6日25-26赛季德甲联赛 奥格斯堡VS勒沃库森", 272 | "vod_sub": "", 273 | "vod_en": "12yue6ri2526saijidejialiansaiaogesibaoVSlewokusen", 274 | "vod_status": 1, 275 | "vod_letter": "1", 276 | "vod_color": "", 277 | "vod_tag": "", 278 | "vod_class": "体育赛事", 279 | "vod_pic": "https://img.jisuimage.com/cover/545a6cf5d728f5c462e5d8722a177baa.jpg", 280 | "vod_pic_thumb": "", 281 | "vod_pic_slide": "", 282 | "vod_pic_screenshot": null, 283 | "vod_actor": "", 284 | "vod_director": "", 285 | "vod_writer": "", 286 | "vod_behind": "", 287 | "vod_blurb": "暂无简介", 288 | "vod_remarks": "正片", 289 | "vod_pubdate": "", 290 | "vod_total": 1, 291 | "vod_serial": "0", 292 | "vod_tv": "", 293 | "vod_weekday": "", 294 | "vod_area": "", 295 | "vod_lang": "", 296 | "vod_year": "2025", 297 | "vod_version": "", 298 | "vod_state": "", 299 | "vod_author": "", 300 | "vod_jumpurl": "", 301 | "vod_tpl": "", 302 | "vod_tpl_play": "", 303 | "vod_tpl_down": "", 304 | "vod_isend": 1, 305 | "vod_lock": 0, 306 | "vod_level": 0, 307 | "vod_copyright": 0, 308 | "vod_points": 0, 309 | "vod_points_play": 0, 310 | "vod_points_down": 0, 311 | "vod_hits": 191, 312 | "vod_hits_day": 638, 313 | "vod_hits_week": 766, 314 | "vod_hits_month": 112, 315 | "vod_duration": "", 316 | "vod_up": 860, 317 | "vod_down": 307, 318 | "vod_score": "0.0", 319 | "vod_score_all": 0, 320 | "vod_score_num": 0, 321 | "vod_time": "2025-12-07 16:18:02", 322 | "vod_time_add": 1765095482, 323 | "vod_time_hits": 0, 324 | "vod_time_make": 0, 325 | "vod_trysee": 0, 326 | "vod_douban_id": 0, 327 | "vod_douban_score": "0.0", 328 | "vod_reurl": "", 329 | "vod_rel_vod": "", 330 | "vod_rel_art": "", 331 | "vod_pwd": "", 332 | "vod_pwd_url": "", 333 | "vod_pwd_play": "", 334 | "vod_pwd_play_url": "", 335 | "vod_pwd_down": "", 336 | "vod_pwd_down_url": "", 337 | "vod_content": "暂无简介", 338 | "vod_play_from": "jsyun$$$jsm3u8", 339 | "vod_play_server": "$$$", 340 | "vod_play_note": "$$$", 341 | "vod_play_url": "正片$https://bf.jisuziyuanbf.com/play/e738rK1e$$$正片$https://bf.jisuziyuanbf.com/play/e738rK1e/index.m3u8", 342 | "vod_down_from": "", 343 | "vod_down_server": "", 344 | "vod_down_note": "", 345 | "vod_down_url": "", 346 | "vod_plot": 0, 347 | "vod_plot_name": "", 348 | "vod_plot_detail": "", 349 | "type_name": "体育赛事" 350 | }, 351 | { 352 | "vod_id": 88895, 353 | "type_id": 29, 354 | "type_id_1": 0, 355 | "group_id": 0, 356 | "vod_name": "12月6日25-26赛季西甲联赛 比利亚雷亚尔VS赫塔菲", 357 | "vod_sub": "", 358 | "vod_en": "12yue6ri2526saijixijialiansaibiliyaleiyaerVShetafei", 359 | "vod_status": 1, 360 | "vod_letter": "1", 361 | "vod_color": "", 362 | "vod_tag": "", 363 | "vod_class": "体育赛事", 364 | "vod_pic": "https://img.jisuimage.com/cover/520b811ebc3d9ab82bb52132109ceb7e.jpg", 365 | "vod_pic_thumb": "", 366 | "vod_pic_slide": "", 367 | "vod_pic_screenshot": null, 368 | "vod_actor": "", 369 | "vod_director": "", 370 | "vod_writer": "", 371 | "vod_behind": "", 372 | "vod_blurb": "暂无简介", 373 | "vod_remarks": "正片", 374 | "vod_pubdate": "", 375 | "vod_total": 1, 376 | "vod_serial": "0", 377 | "vod_tv": "", 378 | "vod_weekday": "", 379 | "vod_area": "", 380 | "vod_lang": "", 381 | "vod_year": "2025", 382 | "vod_version": "", 383 | "vod_state": "", 384 | "vod_author": "", 385 | "vod_jumpurl": "", 386 | "vod_tpl": "", 387 | "vod_tpl_play": "", 388 | "vod_tpl_down": "", 389 | "vod_isend": 1, 390 | "vod_lock": 0, 391 | "vod_level": 0, 392 | "vod_copyright": 0, 393 | "vod_points": 0, 394 | "vod_points_play": 0, 395 | "vod_points_down": 0, 396 | "vod_hits": 669, 397 | "vod_hits_day": 879, 398 | "vod_hits_week": 78, 399 | "vod_hits_month": 171, 400 | "vod_duration": "", 401 | "vod_up": 96, 402 | "vod_down": 910, 403 | "vod_score": "0.0", 404 | "vod_score_all": 0, 405 | "vod_score_num": 0, 406 | "vod_time": "2025-12-07 16:15:02", 407 | "vod_time_add": 1765095302, 408 | "vod_time_hits": 0, 409 | "vod_time_make": 0, 410 | "vod_trysee": 0, 411 | "vod_douban_id": 0, 412 | "vod_douban_score": "0.0", 413 | "vod_reurl": "", 414 | "vod_rel_vod": "", 415 | "vod_rel_art": "", 416 | "vod_pwd": "", 417 | "vod_pwd_url": "", 418 | "vod_pwd_play": "", 419 | "vod_pwd_play_url": "", 420 | "vod_pwd_down": "", 421 | "vod_pwd_down_url": "", 422 | "vod_content": "暂无简介", 423 | "vod_play_from": "jsyun$$$jsm3u8", 424 | "vod_play_server": "$$$", 425 | "vod_play_note": "$$$", 426 | "vod_play_url": "正片$https://bf.jisuziyuanbf.com/play/dR6YqqEd$$$正片$https://bf.jisuziyuanbf.com/play/dR6YqqEd/index.m3u8", 427 | "vod_down_from": "", 428 | "vod_down_server": "", 429 | "vod_down_note": "", 430 | "vod_down_url": "", 431 | "vod_plot": 0, 432 | "vod_plot_name": "", 433 | "vod_plot_detail": "", 434 | "type_name": "体育赛事" 435 | } 436 | ] 437 | }; 438 | -------------------------------------------------------------------------------- /server/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | axios: 12 | specifier: ^1.13.2 13 | version: 1.13.2 14 | cors: 15 | specifier: ^2.8.5 16 | version: 2.8.5 17 | dotenv: 18 | specifier: ^17.2.3 19 | version: 17.2.3 20 | express: 21 | specifier: ^5.2.1 22 | version: 5.2.1 23 | devDependencies: 24 | '@types/cors': 25 | specifier: ^2.8.19 26 | version: 2.8.19 27 | '@types/express': 28 | specifier: ^5.0.6 29 | version: 5.0.6 30 | '@types/node': 31 | specifier: ^24.10.1 32 | version: 24.10.1 33 | nodemon: 34 | specifier: ^3.1.11 35 | version: 3.1.11 36 | ts-node: 37 | specifier: ^10.9.2 38 | version: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) 39 | typescript: 40 | specifier: ^5.9.3 41 | version: 5.9.3 42 | 43 | packages: 44 | 45 | '@cspotcode/source-map-support@0.8.1': 46 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 47 | engines: {node: '>=12'} 48 | 49 | '@jridgewell/resolve-uri@3.1.2': 50 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 51 | engines: {node: '>=6.0.0'} 52 | 53 | '@jridgewell/sourcemap-codec@1.5.5': 54 | resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 55 | 56 | '@jridgewell/trace-mapping@0.3.9': 57 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 58 | 59 | '@tsconfig/node10@1.0.12': 60 | resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} 61 | 62 | '@tsconfig/node12@1.0.11': 63 | resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 64 | 65 | '@tsconfig/node14@1.0.3': 66 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 67 | 68 | '@tsconfig/node16@1.0.4': 69 | resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 70 | 71 | '@types/body-parser@1.19.6': 72 | resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} 73 | 74 | '@types/connect@3.4.38': 75 | resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} 76 | 77 | '@types/cors@2.8.19': 78 | resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} 79 | 80 | '@types/express-serve-static-core@5.1.0': 81 | resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} 82 | 83 | '@types/express@5.0.6': 84 | resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} 85 | 86 | '@types/http-errors@2.0.5': 87 | resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} 88 | 89 | '@types/node@24.10.1': 90 | resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} 91 | 92 | '@types/qs@6.14.0': 93 | resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} 94 | 95 | '@types/range-parser@1.2.7': 96 | resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} 97 | 98 | '@types/send@1.2.1': 99 | resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} 100 | 101 | '@types/serve-static@2.2.0': 102 | resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} 103 | 104 | accepts@2.0.0: 105 | resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} 106 | engines: {node: '>= 0.6'} 107 | 108 | acorn-walk@8.3.4: 109 | resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} 110 | engines: {node: '>=0.4.0'} 111 | 112 | acorn@8.15.0: 113 | resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 114 | engines: {node: '>=0.4.0'} 115 | hasBin: true 116 | 117 | anymatch@3.1.3: 118 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 119 | engines: {node: '>= 8'} 120 | 121 | arg@4.1.3: 122 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 123 | 124 | asynckit@0.4.0: 125 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 126 | 127 | axios@1.13.2: 128 | resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} 129 | 130 | balanced-match@1.0.2: 131 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 132 | 133 | binary-extensions@2.3.0: 134 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 135 | engines: {node: '>=8'} 136 | 137 | body-parser@2.2.1: 138 | resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} 139 | engines: {node: '>=18'} 140 | 141 | brace-expansion@1.1.12: 142 | resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} 143 | 144 | braces@3.0.3: 145 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 146 | engines: {node: '>=8'} 147 | 148 | bytes@3.1.2: 149 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 150 | engines: {node: '>= 0.8'} 151 | 152 | call-bind-apply-helpers@1.0.2: 153 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 154 | engines: {node: '>= 0.4'} 155 | 156 | call-bound@1.0.4: 157 | resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 158 | engines: {node: '>= 0.4'} 159 | 160 | chokidar@3.6.0: 161 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 162 | engines: {node: '>= 8.10.0'} 163 | 164 | combined-stream@1.0.8: 165 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 166 | engines: {node: '>= 0.8'} 167 | 168 | concat-map@0.0.1: 169 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 170 | 171 | content-disposition@1.0.1: 172 | resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} 173 | engines: {node: '>=18'} 174 | 175 | content-type@1.0.5: 176 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 177 | engines: {node: '>= 0.6'} 178 | 179 | cookie-signature@1.2.2: 180 | resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} 181 | engines: {node: '>=6.6.0'} 182 | 183 | cookie@0.7.2: 184 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 185 | engines: {node: '>= 0.6'} 186 | 187 | cors@2.8.5: 188 | resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 189 | engines: {node: '>= 0.10'} 190 | 191 | create-require@1.1.1: 192 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 193 | 194 | debug@4.4.3: 195 | resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 196 | engines: {node: '>=6.0'} 197 | peerDependencies: 198 | supports-color: '*' 199 | peerDependenciesMeta: 200 | supports-color: 201 | optional: true 202 | 203 | delayed-stream@1.0.0: 204 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 205 | engines: {node: '>=0.4.0'} 206 | 207 | depd@2.0.0: 208 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 209 | engines: {node: '>= 0.8'} 210 | 211 | diff@4.0.2: 212 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 213 | engines: {node: '>=0.3.1'} 214 | 215 | dotenv@17.2.3: 216 | resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} 217 | engines: {node: '>=12'} 218 | 219 | dunder-proto@1.0.1: 220 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 221 | engines: {node: '>= 0.4'} 222 | 223 | ee-first@1.1.1: 224 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 225 | 226 | encodeurl@2.0.0: 227 | resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 228 | engines: {node: '>= 0.8'} 229 | 230 | es-define-property@1.0.1: 231 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 232 | engines: {node: '>= 0.4'} 233 | 234 | es-errors@1.3.0: 235 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 236 | engines: {node: '>= 0.4'} 237 | 238 | es-object-atoms@1.1.1: 239 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 240 | engines: {node: '>= 0.4'} 241 | 242 | es-set-tostringtag@2.1.0: 243 | resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} 244 | engines: {node: '>= 0.4'} 245 | 246 | escape-html@1.0.3: 247 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 248 | 249 | etag@1.8.1: 250 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 251 | engines: {node: '>= 0.6'} 252 | 253 | express@5.2.1: 254 | resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} 255 | engines: {node: '>= 18'} 256 | 257 | fill-range@7.1.1: 258 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 259 | engines: {node: '>=8'} 260 | 261 | finalhandler@2.1.1: 262 | resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} 263 | engines: {node: '>= 18.0.0'} 264 | 265 | follow-redirects@1.15.11: 266 | resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} 267 | engines: {node: '>=4.0'} 268 | peerDependencies: 269 | debug: '*' 270 | peerDependenciesMeta: 271 | debug: 272 | optional: true 273 | 274 | form-data@4.0.5: 275 | resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} 276 | engines: {node: '>= 6'} 277 | 278 | forwarded@0.2.0: 279 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 280 | engines: {node: '>= 0.6'} 281 | 282 | fresh@2.0.0: 283 | resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} 284 | engines: {node: '>= 0.8'} 285 | 286 | fsevents@2.3.3: 287 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 288 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 289 | os: [darwin] 290 | 291 | function-bind@1.1.2: 292 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 293 | 294 | get-intrinsic@1.3.0: 295 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 296 | engines: {node: '>= 0.4'} 297 | 298 | get-proto@1.0.1: 299 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 300 | engines: {node: '>= 0.4'} 301 | 302 | glob-parent@5.1.2: 303 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 304 | engines: {node: '>= 6'} 305 | 306 | gopd@1.2.0: 307 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 308 | engines: {node: '>= 0.4'} 309 | 310 | has-flag@3.0.0: 311 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 312 | engines: {node: '>=4'} 313 | 314 | has-symbols@1.1.0: 315 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 316 | engines: {node: '>= 0.4'} 317 | 318 | has-tostringtag@1.0.2: 319 | resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 320 | engines: {node: '>= 0.4'} 321 | 322 | hasown@2.0.2: 323 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 324 | engines: {node: '>= 0.4'} 325 | 326 | http-errors@2.0.1: 327 | resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} 328 | engines: {node: '>= 0.8'} 329 | 330 | iconv-lite@0.7.0: 331 | resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} 332 | engines: {node: '>=0.10.0'} 333 | 334 | ignore-by-default@1.0.1: 335 | resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} 336 | 337 | inherits@2.0.4: 338 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 339 | 340 | ipaddr.js@1.9.1: 341 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 342 | engines: {node: '>= 0.10'} 343 | 344 | is-binary-path@2.1.0: 345 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 346 | engines: {node: '>=8'} 347 | 348 | is-extglob@2.1.1: 349 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 350 | engines: {node: '>=0.10.0'} 351 | 352 | is-glob@4.0.3: 353 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 354 | engines: {node: '>=0.10.0'} 355 | 356 | is-number@7.0.0: 357 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 358 | engines: {node: '>=0.12.0'} 359 | 360 | is-promise@4.0.0: 361 | resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} 362 | 363 | make-error@1.3.6: 364 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 365 | 366 | math-intrinsics@1.1.0: 367 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 368 | engines: {node: '>= 0.4'} 369 | 370 | media-typer@1.1.0: 371 | resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} 372 | engines: {node: '>= 0.8'} 373 | 374 | merge-descriptors@2.0.0: 375 | resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} 376 | engines: {node: '>=18'} 377 | 378 | mime-db@1.52.0: 379 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 380 | engines: {node: '>= 0.6'} 381 | 382 | mime-db@1.54.0: 383 | resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} 384 | engines: {node: '>= 0.6'} 385 | 386 | mime-types@2.1.35: 387 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 388 | engines: {node: '>= 0.6'} 389 | 390 | mime-types@3.0.2: 391 | resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} 392 | engines: {node: '>=18'} 393 | 394 | minimatch@3.1.2: 395 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 396 | 397 | ms@2.1.3: 398 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 399 | 400 | negotiator@1.0.0: 401 | resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} 402 | engines: {node: '>= 0.6'} 403 | 404 | nodemon@3.1.11: 405 | resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} 406 | engines: {node: '>=10'} 407 | hasBin: true 408 | 409 | normalize-path@3.0.0: 410 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 411 | engines: {node: '>=0.10.0'} 412 | 413 | object-assign@4.1.1: 414 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 415 | engines: {node: '>=0.10.0'} 416 | 417 | object-inspect@1.13.4: 418 | resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 419 | engines: {node: '>= 0.4'} 420 | 421 | on-finished@2.4.1: 422 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 423 | engines: {node: '>= 0.8'} 424 | 425 | once@1.4.0: 426 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 427 | 428 | parseurl@1.3.3: 429 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 430 | engines: {node: '>= 0.8'} 431 | 432 | path-to-regexp@8.3.0: 433 | resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} 434 | 435 | picomatch@2.3.1: 436 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 437 | engines: {node: '>=8.6'} 438 | 439 | proxy-addr@2.0.7: 440 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 441 | engines: {node: '>= 0.10'} 442 | 443 | proxy-from-env@1.1.0: 444 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 445 | 446 | pstree.remy@1.1.8: 447 | resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} 448 | 449 | qs@6.14.0: 450 | resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 451 | engines: {node: '>=0.6'} 452 | 453 | range-parser@1.2.1: 454 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 455 | engines: {node: '>= 0.6'} 456 | 457 | raw-body@3.0.2: 458 | resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} 459 | engines: {node: '>= 0.10'} 460 | 461 | readdirp@3.6.0: 462 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 463 | engines: {node: '>=8.10.0'} 464 | 465 | router@2.2.0: 466 | resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} 467 | engines: {node: '>= 18'} 468 | 469 | safer-buffer@2.1.2: 470 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 471 | 472 | semver@7.7.3: 473 | resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 474 | engines: {node: '>=10'} 475 | hasBin: true 476 | 477 | send@1.2.0: 478 | resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} 479 | engines: {node: '>= 18'} 480 | 481 | serve-static@2.2.0: 482 | resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} 483 | engines: {node: '>= 18'} 484 | 485 | setprototypeof@1.2.0: 486 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 487 | 488 | side-channel-list@1.0.0: 489 | resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 490 | engines: {node: '>= 0.4'} 491 | 492 | side-channel-map@1.0.1: 493 | resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 494 | engines: {node: '>= 0.4'} 495 | 496 | side-channel-weakmap@1.0.2: 497 | resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 498 | engines: {node: '>= 0.4'} 499 | 500 | side-channel@1.1.0: 501 | resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 502 | engines: {node: '>= 0.4'} 503 | 504 | simple-update-notifier@2.0.0: 505 | resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} 506 | engines: {node: '>=10'} 507 | 508 | statuses@2.0.2: 509 | resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} 510 | engines: {node: '>= 0.8'} 511 | 512 | supports-color@5.5.0: 513 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 514 | engines: {node: '>=4'} 515 | 516 | to-regex-range@5.0.1: 517 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 518 | engines: {node: '>=8.0'} 519 | 520 | toidentifier@1.0.1: 521 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 522 | engines: {node: '>=0.6'} 523 | 524 | touch@3.1.1: 525 | resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} 526 | hasBin: true 527 | 528 | ts-node@10.9.2: 529 | resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} 530 | hasBin: true 531 | peerDependencies: 532 | '@swc/core': '>=1.2.50' 533 | '@swc/wasm': '>=1.2.50' 534 | '@types/node': '*' 535 | typescript: '>=2.7' 536 | peerDependenciesMeta: 537 | '@swc/core': 538 | optional: true 539 | '@swc/wasm': 540 | optional: true 541 | 542 | type-is@2.0.1: 543 | resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} 544 | engines: {node: '>= 0.6'} 545 | 546 | typescript@5.9.3: 547 | resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 548 | engines: {node: '>=14.17'} 549 | hasBin: true 550 | 551 | undefsafe@2.0.5: 552 | resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} 553 | 554 | undici-types@7.16.0: 555 | resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 556 | 557 | unpipe@1.0.0: 558 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 559 | engines: {node: '>= 0.8'} 560 | 561 | v8-compile-cache-lib@3.0.1: 562 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 563 | 564 | vary@1.1.2: 565 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 566 | engines: {node: '>= 0.8'} 567 | 568 | wrappy@1.0.2: 569 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 570 | 571 | yn@3.1.1: 572 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 573 | engines: {node: '>=6'} 574 | 575 | snapshots: 576 | 577 | '@cspotcode/source-map-support@0.8.1': 578 | dependencies: 579 | '@jridgewell/trace-mapping': 0.3.9 580 | 581 | '@jridgewell/resolve-uri@3.1.2': {} 582 | 583 | '@jridgewell/sourcemap-codec@1.5.5': {} 584 | 585 | '@jridgewell/trace-mapping@0.3.9': 586 | dependencies: 587 | '@jridgewell/resolve-uri': 3.1.2 588 | '@jridgewell/sourcemap-codec': 1.5.5 589 | 590 | '@tsconfig/node10@1.0.12': {} 591 | 592 | '@tsconfig/node12@1.0.11': {} 593 | 594 | '@tsconfig/node14@1.0.3': {} 595 | 596 | '@tsconfig/node16@1.0.4': {} 597 | 598 | '@types/body-parser@1.19.6': 599 | dependencies: 600 | '@types/connect': 3.4.38 601 | '@types/node': 24.10.1 602 | 603 | '@types/connect@3.4.38': 604 | dependencies: 605 | '@types/node': 24.10.1 606 | 607 | '@types/cors@2.8.19': 608 | dependencies: 609 | '@types/node': 24.10.1 610 | 611 | '@types/express-serve-static-core@5.1.0': 612 | dependencies: 613 | '@types/node': 24.10.1 614 | '@types/qs': 6.14.0 615 | '@types/range-parser': 1.2.7 616 | '@types/send': 1.2.1 617 | 618 | '@types/express@5.0.6': 619 | dependencies: 620 | '@types/body-parser': 1.19.6 621 | '@types/express-serve-static-core': 5.1.0 622 | '@types/serve-static': 2.2.0 623 | 624 | '@types/http-errors@2.0.5': {} 625 | 626 | '@types/node@24.10.1': 627 | dependencies: 628 | undici-types: 7.16.0 629 | 630 | '@types/qs@6.14.0': {} 631 | 632 | '@types/range-parser@1.2.7': {} 633 | 634 | '@types/send@1.2.1': 635 | dependencies: 636 | '@types/node': 24.10.1 637 | 638 | '@types/serve-static@2.2.0': 639 | dependencies: 640 | '@types/http-errors': 2.0.5 641 | '@types/node': 24.10.1 642 | 643 | accepts@2.0.0: 644 | dependencies: 645 | mime-types: 3.0.2 646 | negotiator: 1.0.0 647 | 648 | acorn-walk@8.3.4: 649 | dependencies: 650 | acorn: 8.15.0 651 | 652 | acorn@8.15.0: {} 653 | 654 | anymatch@3.1.3: 655 | dependencies: 656 | normalize-path: 3.0.0 657 | picomatch: 2.3.1 658 | 659 | arg@4.1.3: {} 660 | 661 | asynckit@0.4.0: {} 662 | 663 | axios@1.13.2: 664 | dependencies: 665 | follow-redirects: 1.15.11 666 | form-data: 4.0.5 667 | proxy-from-env: 1.1.0 668 | transitivePeerDependencies: 669 | - debug 670 | 671 | balanced-match@1.0.2: {} 672 | 673 | binary-extensions@2.3.0: {} 674 | 675 | body-parser@2.2.1: 676 | dependencies: 677 | bytes: 3.1.2 678 | content-type: 1.0.5 679 | debug: 4.4.3(supports-color@5.5.0) 680 | http-errors: 2.0.1 681 | iconv-lite: 0.7.0 682 | on-finished: 2.4.1 683 | qs: 6.14.0 684 | raw-body: 3.0.2 685 | type-is: 2.0.1 686 | transitivePeerDependencies: 687 | - supports-color 688 | 689 | brace-expansion@1.1.12: 690 | dependencies: 691 | balanced-match: 1.0.2 692 | concat-map: 0.0.1 693 | 694 | braces@3.0.3: 695 | dependencies: 696 | fill-range: 7.1.1 697 | 698 | bytes@3.1.2: {} 699 | 700 | call-bind-apply-helpers@1.0.2: 701 | dependencies: 702 | es-errors: 1.3.0 703 | function-bind: 1.1.2 704 | 705 | call-bound@1.0.4: 706 | dependencies: 707 | call-bind-apply-helpers: 1.0.2 708 | get-intrinsic: 1.3.0 709 | 710 | chokidar@3.6.0: 711 | dependencies: 712 | anymatch: 3.1.3 713 | braces: 3.0.3 714 | glob-parent: 5.1.2 715 | is-binary-path: 2.1.0 716 | is-glob: 4.0.3 717 | normalize-path: 3.0.0 718 | readdirp: 3.6.0 719 | optionalDependencies: 720 | fsevents: 2.3.3 721 | 722 | combined-stream@1.0.8: 723 | dependencies: 724 | delayed-stream: 1.0.0 725 | 726 | concat-map@0.0.1: {} 727 | 728 | content-disposition@1.0.1: {} 729 | 730 | content-type@1.0.5: {} 731 | 732 | cookie-signature@1.2.2: {} 733 | 734 | cookie@0.7.2: {} 735 | 736 | cors@2.8.5: 737 | dependencies: 738 | object-assign: 4.1.1 739 | vary: 1.1.2 740 | 741 | create-require@1.1.1: {} 742 | 743 | debug@4.4.3(supports-color@5.5.0): 744 | dependencies: 745 | ms: 2.1.3 746 | optionalDependencies: 747 | supports-color: 5.5.0 748 | 749 | delayed-stream@1.0.0: {} 750 | 751 | depd@2.0.0: {} 752 | 753 | diff@4.0.2: {} 754 | 755 | dotenv@17.2.3: {} 756 | 757 | dunder-proto@1.0.1: 758 | dependencies: 759 | call-bind-apply-helpers: 1.0.2 760 | es-errors: 1.3.0 761 | gopd: 1.2.0 762 | 763 | ee-first@1.1.1: {} 764 | 765 | encodeurl@2.0.0: {} 766 | 767 | es-define-property@1.0.1: {} 768 | 769 | es-errors@1.3.0: {} 770 | 771 | es-object-atoms@1.1.1: 772 | dependencies: 773 | es-errors: 1.3.0 774 | 775 | es-set-tostringtag@2.1.0: 776 | dependencies: 777 | es-errors: 1.3.0 778 | get-intrinsic: 1.3.0 779 | has-tostringtag: 1.0.2 780 | hasown: 2.0.2 781 | 782 | escape-html@1.0.3: {} 783 | 784 | etag@1.8.1: {} 785 | 786 | express@5.2.1: 787 | dependencies: 788 | accepts: 2.0.0 789 | body-parser: 2.2.1 790 | content-disposition: 1.0.1 791 | content-type: 1.0.5 792 | cookie: 0.7.2 793 | cookie-signature: 1.2.2 794 | debug: 4.4.3(supports-color@5.5.0) 795 | depd: 2.0.0 796 | encodeurl: 2.0.0 797 | escape-html: 1.0.3 798 | etag: 1.8.1 799 | finalhandler: 2.1.1 800 | fresh: 2.0.0 801 | http-errors: 2.0.1 802 | merge-descriptors: 2.0.0 803 | mime-types: 3.0.2 804 | on-finished: 2.4.1 805 | once: 1.4.0 806 | parseurl: 1.3.3 807 | proxy-addr: 2.0.7 808 | qs: 6.14.0 809 | range-parser: 1.2.1 810 | router: 2.2.0 811 | send: 1.2.0 812 | serve-static: 2.2.0 813 | statuses: 2.0.2 814 | type-is: 2.0.1 815 | vary: 1.1.2 816 | transitivePeerDependencies: 817 | - supports-color 818 | 819 | fill-range@7.1.1: 820 | dependencies: 821 | to-regex-range: 5.0.1 822 | 823 | finalhandler@2.1.1: 824 | dependencies: 825 | debug: 4.4.3(supports-color@5.5.0) 826 | encodeurl: 2.0.0 827 | escape-html: 1.0.3 828 | on-finished: 2.4.1 829 | parseurl: 1.3.3 830 | statuses: 2.0.2 831 | transitivePeerDependencies: 832 | - supports-color 833 | 834 | follow-redirects@1.15.11: {} 835 | 836 | form-data@4.0.5: 837 | dependencies: 838 | asynckit: 0.4.0 839 | combined-stream: 1.0.8 840 | es-set-tostringtag: 2.1.0 841 | hasown: 2.0.2 842 | mime-types: 2.1.35 843 | 844 | forwarded@0.2.0: {} 845 | 846 | fresh@2.0.0: {} 847 | 848 | fsevents@2.3.3: 849 | optional: true 850 | 851 | function-bind@1.1.2: {} 852 | 853 | get-intrinsic@1.3.0: 854 | dependencies: 855 | call-bind-apply-helpers: 1.0.2 856 | es-define-property: 1.0.1 857 | es-errors: 1.3.0 858 | es-object-atoms: 1.1.1 859 | function-bind: 1.1.2 860 | get-proto: 1.0.1 861 | gopd: 1.2.0 862 | has-symbols: 1.1.0 863 | hasown: 2.0.2 864 | math-intrinsics: 1.1.0 865 | 866 | get-proto@1.0.1: 867 | dependencies: 868 | dunder-proto: 1.0.1 869 | es-object-atoms: 1.1.1 870 | 871 | glob-parent@5.1.2: 872 | dependencies: 873 | is-glob: 4.0.3 874 | 875 | gopd@1.2.0: {} 876 | 877 | has-flag@3.0.0: {} 878 | 879 | has-symbols@1.1.0: {} 880 | 881 | has-tostringtag@1.0.2: 882 | dependencies: 883 | has-symbols: 1.1.0 884 | 885 | hasown@2.0.2: 886 | dependencies: 887 | function-bind: 1.1.2 888 | 889 | http-errors@2.0.1: 890 | dependencies: 891 | depd: 2.0.0 892 | inherits: 2.0.4 893 | setprototypeof: 1.2.0 894 | statuses: 2.0.2 895 | toidentifier: 1.0.1 896 | 897 | iconv-lite@0.7.0: 898 | dependencies: 899 | safer-buffer: 2.1.2 900 | 901 | ignore-by-default@1.0.1: {} 902 | 903 | inherits@2.0.4: {} 904 | 905 | ipaddr.js@1.9.1: {} 906 | 907 | is-binary-path@2.1.0: 908 | dependencies: 909 | binary-extensions: 2.3.0 910 | 911 | is-extglob@2.1.1: {} 912 | 913 | is-glob@4.0.3: 914 | dependencies: 915 | is-extglob: 2.1.1 916 | 917 | is-number@7.0.0: {} 918 | 919 | is-promise@4.0.0: {} 920 | 921 | make-error@1.3.6: {} 922 | 923 | math-intrinsics@1.1.0: {} 924 | 925 | media-typer@1.1.0: {} 926 | 927 | merge-descriptors@2.0.0: {} 928 | 929 | mime-db@1.52.0: {} 930 | 931 | mime-db@1.54.0: {} 932 | 933 | mime-types@2.1.35: 934 | dependencies: 935 | mime-db: 1.52.0 936 | 937 | mime-types@3.0.2: 938 | dependencies: 939 | mime-db: 1.54.0 940 | 941 | minimatch@3.1.2: 942 | dependencies: 943 | brace-expansion: 1.1.12 944 | 945 | ms@2.1.3: {} 946 | 947 | negotiator@1.0.0: {} 948 | 949 | nodemon@3.1.11: 950 | dependencies: 951 | chokidar: 3.6.0 952 | debug: 4.4.3(supports-color@5.5.0) 953 | ignore-by-default: 1.0.1 954 | minimatch: 3.1.2 955 | pstree.remy: 1.1.8 956 | semver: 7.7.3 957 | simple-update-notifier: 2.0.0 958 | supports-color: 5.5.0 959 | touch: 3.1.1 960 | undefsafe: 2.0.5 961 | 962 | normalize-path@3.0.0: {} 963 | 964 | object-assign@4.1.1: {} 965 | 966 | object-inspect@1.13.4: {} 967 | 968 | on-finished@2.4.1: 969 | dependencies: 970 | ee-first: 1.1.1 971 | 972 | once@1.4.0: 973 | dependencies: 974 | wrappy: 1.0.2 975 | 976 | parseurl@1.3.3: {} 977 | 978 | path-to-regexp@8.3.0: {} 979 | 980 | picomatch@2.3.1: {} 981 | 982 | proxy-addr@2.0.7: 983 | dependencies: 984 | forwarded: 0.2.0 985 | ipaddr.js: 1.9.1 986 | 987 | proxy-from-env@1.1.0: {} 988 | 989 | pstree.remy@1.1.8: {} 990 | 991 | qs@6.14.0: 992 | dependencies: 993 | side-channel: 1.1.0 994 | 995 | range-parser@1.2.1: {} 996 | 997 | raw-body@3.0.2: 998 | dependencies: 999 | bytes: 3.1.2 1000 | http-errors: 2.0.1 1001 | iconv-lite: 0.7.0 1002 | unpipe: 1.0.0 1003 | 1004 | readdirp@3.6.0: 1005 | dependencies: 1006 | picomatch: 2.3.1 1007 | 1008 | router@2.2.0: 1009 | dependencies: 1010 | debug: 4.4.3(supports-color@5.5.0) 1011 | depd: 2.0.0 1012 | is-promise: 4.0.0 1013 | parseurl: 1.3.3 1014 | path-to-regexp: 8.3.0 1015 | transitivePeerDependencies: 1016 | - supports-color 1017 | 1018 | safer-buffer@2.1.2: {} 1019 | 1020 | semver@7.7.3: {} 1021 | 1022 | send@1.2.0: 1023 | dependencies: 1024 | debug: 4.4.3(supports-color@5.5.0) 1025 | encodeurl: 2.0.0 1026 | escape-html: 1.0.3 1027 | etag: 1.8.1 1028 | fresh: 2.0.0 1029 | http-errors: 2.0.1 1030 | mime-types: 3.0.2 1031 | ms: 2.1.3 1032 | on-finished: 2.4.1 1033 | range-parser: 1.2.1 1034 | statuses: 2.0.2 1035 | transitivePeerDependencies: 1036 | - supports-color 1037 | 1038 | serve-static@2.2.0: 1039 | dependencies: 1040 | encodeurl: 2.0.0 1041 | escape-html: 1.0.3 1042 | parseurl: 1.3.3 1043 | send: 1.2.0 1044 | transitivePeerDependencies: 1045 | - supports-color 1046 | 1047 | setprototypeof@1.2.0: {} 1048 | 1049 | side-channel-list@1.0.0: 1050 | dependencies: 1051 | es-errors: 1.3.0 1052 | object-inspect: 1.13.4 1053 | 1054 | side-channel-map@1.0.1: 1055 | dependencies: 1056 | call-bound: 1.0.4 1057 | es-errors: 1.3.0 1058 | get-intrinsic: 1.3.0 1059 | object-inspect: 1.13.4 1060 | 1061 | side-channel-weakmap@1.0.2: 1062 | dependencies: 1063 | call-bound: 1.0.4 1064 | es-errors: 1.3.0 1065 | get-intrinsic: 1.3.0 1066 | object-inspect: 1.13.4 1067 | side-channel-map: 1.0.1 1068 | 1069 | side-channel@1.1.0: 1070 | dependencies: 1071 | es-errors: 1.3.0 1072 | object-inspect: 1.13.4 1073 | side-channel-list: 1.0.0 1074 | side-channel-map: 1.0.1 1075 | side-channel-weakmap: 1.0.2 1076 | 1077 | simple-update-notifier@2.0.0: 1078 | dependencies: 1079 | semver: 7.7.3 1080 | 1081 | statuses@2.0.2: {} 1082 | 1083 | supports-color@5.5.0: 1084 | dependencies: 1085 | has-flag: 3.0.0 1086 | 1087 | to-regex-range@5.0.1: 1088 | dependencies: 1089 | is-number: 7.0.0 1090 | 1091 | toidentifier@1.0.1: {} 1092 | 1093 | touch@3.1.1: {} 1094 | 1095 | ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): 1096 | dependencies: 1097 | '@cspotcode/source-map-support': 0.8.1 1098 | '@tsconfig/node10': 1.0.12 1099 | '@tsconfig/node12': 1.0.11 1100 | '@tsconfig/node14': 1.0.3 1101 | '@tsconfig/node16': 1.0.4 1102 | '@types/node': 24.10.1 1103 | acorn: 8.15.0 1104 | acorn-walk: 8.3.4 1105 | arg: 4.1.3 1106 | create-require: 1.1.1 1107 | diff: 4.0.2 1108 | make-error: 1.3.6 1109 | typescript: 5.9.3 1110 | v8-compile-cache-lib: 3.0.1 1111 | yn: 3.1.1 1112 | 1113 | type-is@2.0.1: 1114 | dependencies: 1115 | content-type: 1.0.5 1116 | media-typer: 1.1.0 1117 | mime-types: 3.0.2 1118 | 1119 | typescript@5.9.3: {} 1120 | 1121 | undefsafe@2.0.5: {} 1122 | 1123 | undici-types@7.16.0: {} 1124 | 1125 | unpipe@1.0.0: {} 1126 | 1127 | v8-compile-cache-lib@3.0.1: {} 1128 | 1129 | vary@1.1.2: {} 1130 | 1131 | wrappy@1.0.2: {} 1132 | 1133 | yn@3.1.1: {} 1134 | --------------------------------------------------------------------------------