├── .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 |
--------------------------------------------------------------------------------
/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 |
43 |
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 |
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 | 
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 |
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 |
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 |
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 |
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 |
256 | ) : (
257 | <>
258 | {filteredResults.length === 0 && results.length > 0 ? (
259 |
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 |
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 |
--------------------------------------------------------------------------------