├── dashboard ├── .nvmrc ├── public │ ├── _redirects │ ├── lives.jpg │ ├── mock │ │ └── middlewares.js │ ├── default-poster.svg │ └── vite.svg ├── src │ ├── types │ │ └── global.d.ts │ ├── assets │ │ ├── logo.png │ │ ├── icon_font │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.woff2 │ │ │ └── iconfont.css │ │ └── vue.svg │ ├── api │ │ ├── services │ │ │ ├── index.js │ │ │ └── sniffer.js │ │ ├── index.js │ │ ├── config.js │ │ ├── modules │ │ │ ├── proxy.js │ │ │ └── parse.js │ │ ├── request.js │ │ ├── README.md │ │ └── types │ │ │ └── index.js │ ├── mock │ │ ├── middlewares.js │ │ └── multiInputX.json │ ├── stores │ │ ├── categoryStore.js │ │ ├── toast.js │ │ ├── sidebarStore.js │ │ ├── paginationStore.js │ │ ├── visitedStore.js │ │ ├── siteStore.js │ │ ├── pageStateStore.js │ │ ├── downloadStore.js │ │ └── favoriteStore.js │ ├── utils │ │ ├── apiUtils.js │ │ ├── req.js │ │ ├── csp.js │ │ └── fileTypeUtils.js │ ├── App.vue │ ├── main.js │ ├── components │ │ ├── GlobalToast.vue │ │ ├── CategoryModal.vue │ │ ├── Footer.vue │ │ ├── PlayerSelector.vue │ │ ├── ScrollToBottom.vue │ │ ├── FolderBreadcrumb.vue │ │ ├── actions │ │ │ └── index.js │ │ └── players │ │ │ └── LiveProxySelector.vue │ ├── style.css │ ├── router │ │ └── index.js │ └── services │ │ └── resetService.js ├── .env.production.root ├── .vscode │ └── extensions.json ├── pnpm-workspace.yaml ├── .env.production.apps ├── postcss.config.js ├── .env.production ├── vercel.json ├── docs │ ├── mpv-protocol.reg │ ├── vlc-protocol.reg │ ├── README.md │ ├── 外部播放器配置说明.md │ ├── OPTIMIZATION_REPORT.md │ ├── nginx-root.conf │ ├── nginx-subdir.conf │ ├── API_REFACTOR_SUMMARY.md │ ├── NGINX_DEPLOYMENT.md │ ├── UPX_COMPRESSION_GUIDE.md │ ├── FASTIFY_DEPLOYMENT.md │ ├── BUILD_BINARY_GUIDE.md │ ├── DEPLOYMENT.md │ ├── apidoc.md │ ├── pvideo接口说明.md │ └── t4api.md ├── .gitignore ├── index.html ├── playwright.config.js ├── temp-server │ └── package.json ├── tailwind.config.js ├── vite.config.js ├── package.json ├── json │ └── live_cntv.txt └── build-binary.js ├── proxy ├── requirements.txt ├── start_proxy.py ├── config.py └── README.md ├── .idea ├── .gitignore ├── vcs.xml ├── jsLibraryMappings.xml ├── modules.xml └── DrPlayer.iml └── .gitignore /dashboard/.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /dashboard/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /proxy/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | httpx 3 | uvicorn 4 | psutil -------------------------------------------------------------------------------- /dashboard/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // 全局类型声明 2 | declare const __APP_VERSION__: string -------------------------------------------------------------------------------- /dashboard/.env.production.root: -------------------------------------------------------------------------------- 1 | # .env.production.root 2 | # 根目录部署配置 3 | VITE_BASE_PATH=./ -------------------------------------------------------------------------------- /dashboard/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /dashboard/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | # pnpm workspace configuration 2 | packages: 3 | - '.' 4 | -------------------------------------------------------------------------------- /dashboard/public/lives.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjdhnx/DrPlayer/HEAD/dashboard/public/lives.jpg -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /dashboard/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjdhnx/DrPlayer/HEAD/dashboard/src/assets/logo.png -------------------------------------------------------------------------------- /dashboard/.env.production.apps: -------------------------------------------------------------------------------- 1 | # .env.production.apps 2 | # 子目录部署配置 - 部署到 /apps/drplayer/ 目录 3 | VITE_BASE_PATH=/apps/drplayer/ -------------------------------------------------------------------------------- /dashboard/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /dashboard/src/assets/icon_font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjdhnx/DrPlayer/HEAD/dashboard/src/assets/icon_font/iconfont.ttf -------------------------------------------------------------------------------- /dashboard/src/assets/icon_font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjdhnx/DrPlayer/HEAD/dashboard/src/assets/icon_font/iconfont.woff -------------------------------------------------------------------------------- /dashboard/src/assets/icon_font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hjdhnx/DrPlayer/HEAD/dashboard/src/assets/icon_font/iconfont.woff2 -------------------------------------------------------------------------------- /dashboard/.env.production: -------------------------------------------------------------------------------- 1 | # .env.production 2 | # 生产环境配置 3 | # 子目录部署配置 - 例如部署到 /apps/ 目录 4 | # VITE_BASE_PATH=/apps/drplayer/ 5 | 6 | # 如果部署到根目录,使用以下配置: 7 | VITE_BASE_PATH=./ -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /dashboard/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "pnpm build", 3 | "outputDirectory": "dist", 4 | "installCommand": "pnpm install --frozen-lockfile", 5 | "framework": "vite", 6 | "rewrites": [{ "source": "/:path*", "destination": "/index.html" }] 7 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /dashboard/docs/mpv-protocol.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | 3 | [HKEY_CLASSES_ROOT\mpv] 4 | @="URL:MPV Protocol" 5 | "URL Protocol"="" 6 | 7 | [HKEY_CLASSES_ROOT\mpv\shell] 8 | 9 | [HKEY_CLASSES_ROOT\mpv\shell\open] 10 | 11 | [HKEY_CLASSES_ROOT\mpv\shell\open\command] 12 | @="\"C:\\Program Files\\mpv\\mpv.exe\" \"%1\"" -------------------------------------------------------------------------------- /dashboard/docs/vlc-protocol.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | 3 | [HKEY_CLASSES_ROOT\vlc] 4 | @="URL:VLC Protocol" 5 | "URL Protocol"="" 6 | 7 | [HKEY_CLASSES_ROOT\vlc\shell] 8 | 9 | [HKEY_CLASSES_ROOT\vlc\shell\open] 10 | 11 | [HKEY_CLASSES_ROOT\vlc\shell\open\command] 12 | @="\"C:\\Program Files\\VideoLAN\\VLC\\vlc.exe\" \"%1\"" -------------------------------------------------------------------------------- /dashboard/src/api/services/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 业务服务统一入口 3 | * 导出所有业务服务模块 4 | */ 5 | 6 | import videoService from './video' 7 | import siteService from './site' 8 | 9 | // 导出所有服务 10 | export { 11 | videoService, 12 | siteService 13 | } 14 | 15 | // 默认导出服务集合 16 | export default { 17 | video: videoService, 18 | site: siteService 19 | } -------------------------------------------------------------------------------- /dashboard/.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 | /dist-binary/ 26 | -------------------------------------------------------------------------------- /dashboard/docs/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 13 | 14 | 15 | -------------------------------------------------------------------------------- /dashboard/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/DrPlayer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /dashboard/src/stores/toast.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | // 全局Toast状态 4 | export const toastState = ref({ 5 | show: false, 6 | message: '', 7 | type: 'success', // success, error, warning, info 8 | duration: 3000 9 | }) 10 | 11 | // 显示Toast的方法 12 | export function showToast(message, type = 'success', duration = 3000) { 13 | toastState.value = { 14 | show: true, 15 | message, 16 | type, 17 | duration 18 | } 19 | 20 | // 自动隐藏 21 | setTimeout(() => { 22 | hideToast() 23 | }, duration) 24 | } 25 | 26 | // 隐藏Toast的方法 27 | export function hideToast() { 28 | toastState.value.show = false 29 | } -------------------------------------------------------------------------------- /dashboard/src/utils/apiUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API相关的工具函数 3 | */ 4 | 5 | /** 6 | * 处理extend参数 7 | * 如果extend是对象类型,转换为JSON字符串;如果已经是字符串,直接返回 8 | * @param {any} extend - extend参数 9 | * @returns {string|undefined} 处理后的extend参数 10 | */ 11 | export const processExtendParam = (extend) => { 12 | if (!extend) { 13 | return undefined 14 | } 15 | 16 | // 如果extend是对象类型,转换为JSON字符串 17 | if (typeof extend === 'object' && extend !== null) { 18 | try { 19 | return JSON.stringify(extend) 20 | } catch (error) { 21 | console.warn('extend参数JSON序列化失败:', error) 22 | return undefined 23 | } 24 | } 25 | 26 | // 如果已经是字符串,直接返回 27 | return extend 28 | } -------------------------------------------------------------------------------- /dashboard/src/stores/sidebarStore.js: -------------------------------------------------------------------------------- 1 | // src/stores/sidebarStore.js 2 | import { defineStore } from 'pinia'; 3 | 4 | export const useSidebarStore = defineStore('sidebar', { 5 | state: () => ({ 6 | isCollapsed: JSON.parse(localStorage.getItem('sidebar-collapsed') || 'false'), // 从 localStorage 获取初始状态 7 | }), 8 | actions: { 9 | toggleSidebar() { 10 | this.isCollapsed = !this.isCollapsed; 11 | localStorage.setItem('sidebar-collapsed', JSON.stringify(this.isCollapsed)); // 保存状态到 localStorage 12 | }, 13 | }, 14 | getters: { 15 | sidebarWidth: (state) => state.isCollapsed ? 80 : 250, // 收缩时宽度为 80px,展开时为 250px 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /dashboard/playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | export default defineConfig({ 5 | testDir: './tests', 6 | fullyParallel: true, 7 | forbidOnly: !!process.env.CI, 8 | retries: process.env.CI ? 2 : 0, 9 | workers: process.env.CI ? 1 : undefined, 10 | reporter: 'html', 11 | use: { 12 | baseURL: 'http://localhost:5173', 13 | trace: 'on-first-retry', 14 | }, 15 | 16 | projects: [ 17 | { 18 | name: 'chromium', 19 | use: { ...devices['Desktop Chrome'] }, 20 | }, 21 | ], 22 | 23 | webServer: { 24 | command: 'npm run dev', 25 | url: 'http://localhost:5173', 26 | reuseExistingServer: !process.env.CI, 27 | }, 28 | }); -------------------------------------------------------------------------------- /dashboard/src/api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API模块统一入口 3 | * 提供所有业务接口的统一导出 4 | */ 5 | 6 | // 基础配置和工具 7 | export { default as request } from './request' 8 | export { default as config } from './config' 9 | 10 | // 业务服务 11 | export { default as siteService } from './services/site' 12 | export { default as videoService } from './services/video' 13 | export { default as configService } from './services/config' 14 | 15 | // 业务接口模块 16 | export { default as moduleApi } from './modules/module' 17 | export { default as proxyApi } from './modules/proxy' 18 | export { default as parseApi } from './modules/parse' 19 | 20 | // 便捷导出常用接口 21 | export { 22 | getHomeData, 23 | getCategoryData, 24 | getVideoDetail, 25 | searchVideos, 26 | refreshModule 27 | } from './modules/module' 28 | 29 | export { 30 | proxyRequest 31 | } from './modules/proxy' 32 | 33 | export { 34 | parseVideo 35 | } from './modules/parse' -------------------------------------------------------------------------------- /dashboard/temp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drplayer-server", 3 | "version": "1.0.0", 4 | "description": "DrPlayer Production Server - Minimal build for PKG", 5 | "main": "./production-server.cjs", 6 | "bin": "./production-server.cjs", 7 | "type": "commonjs", 8 | "scripts": { 9 | "start": "node production-server.cjs", 10 | "pkg:win": "pkg . --target node18-win-x64 --output dist-binary/drplayer-server-win.exe", 11 | "pkg:win:optimized": "pkg . --target node18-win-x64 --output dist-binary/drplayer-server-win.exe --compress Brotli --public-packages \"*\" --public --options \"max-old-space-size=512\"" 12 | }, 13 | "dependencies": { 14 | "fastify": "^4.29.1", 15 | "@fastify/static": "^6.12.0" 16 | }, 17 | "engines": { 18 | "node": ">=18.0.0" 19 | }, 20 | "pkg": { 21 | "assets": [ 22 | "./apps/**/*" 23 | ], 24 | "scripts": [ 25 | "./production-server.cjs" 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /dashboard/src/stores/paginationStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const usePaginationStore = defineStore('pagination', { 4 | state: () => ({ 5 | statsText: '', // 翻页统计文本 6 | isVisible: false, // 是否显示统计信息 7 | currentRoute: '', // 当前路由,用于判断是否显示 8 | }), 9 | actions: { 10 | // 更新翻页统计信息 11 | updateStats(text) { 12 | this.statsText = text; 13 | this.isVisible = !!text; // 有文本时显示,无文本时隐藏 14 | }, 15 | // 清除翻页统计信息 16 | clearStats() { 17 | this.statsText = ''; 18 | this.isVisible = false; 19 | }, 20 | // 设置当前路由 21 | setCurrentRoute(route) { 22 | this.currentRoute = route; 23 | // 如果不是点播页面或搜索页面,清除统计信息 24 | if (route !== '/video' && route !== '/search') { 25 | this.clearStats(); 26 | } 27 | } 28 | }, 29 | getters: { 30 | // 是否应该显示统计信息(在点播页面或搜索页面且有统计文本) 31 | shouldShow: (state) => state.isVisible && (state.currentRoute === '/video' || state.currentRoute === '/search') 32 | } 33 | }); -------------------------------------------------------------------------------- /dashboard/src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /dashboard/src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | // import './style.css' 3 | import App from './App.vue' 4 | import router from './router' // 引入路由 5 | import ArcoVue from '@arco-design/web-vue' 6 | import ArcoVueIcon from '@arco-design/web-vue/es/icon'; 7 | import '@arco-design/web-vue/dist/arco.css' 8 | import '@/assets/icon_font/iconfont.css' // 引入iconfont样式 9 | import {createPinia} from 'pinia' 10 | import 'viewerjs/dist/viewer.css' 11 | import VueViewer from 'v-viewer' 12 | import ECharts from 'vue-echarts' 13 | import ActionComponents from '@/components/actions' 14 | import { use } from 'echarts/core' 15 | import { 16 | CanvasRenderer 17 | } from 'echarts/renderers' 18 | import { 19 | BarChart, 20 | LineChart, 21 | PieChart 22 | } from 'echarts/charts' 23 | import { 24 | GridComponent, 25 | TooltipComponent, 26 | LegendComponent, 27 | TitleComponent 28 | } from 'echarts/components' 29 | 30 | use([ 31 | CanvasRenderer, 32 | BarChart, 33 | LineChart, 34 | PieChart, 35 | GridComponent, 36 | TooltipComponent, 37 | LegendComponent, 38 | TitleComponent 39 | ]) 40 | 41 | const app = createApp(App) 42 | app.use(router) 43 | app.use(ArcoVue); 44 | app.use(ArcoVueIcon); 45 | app.use(createPinia()) 46 | app.use(VueViewer) 47 | app.use(ActionComponents) 48 | app.component('v-chart', ECharts) 49 | app.mount('#app') -------------------------------------------------------------------------------- /dashboard/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | primary: { 11 | 50: '#eff6ff', 12 | 100: '#dbeafe', 13 | 200: '#bfdbfe', 14 | 300: '#93c5fd', 15 | 400: '#60a5fa', 16 | 500: '#3b82f6', 17 | 600: '#2563eb', 18 | 700: '#1d4ed8', 19 | 800: '#1e40af', 20 | 900: '#1e3a8a', 21 | }, 22 | glass: { 23 | white: 'rgba(255, 255, 255, 0.1)', 24 | dark: 'rgba(0, 0, 0, 0.1)', 25 | } 26 | }, 27 | backdropBlur: { 28 | xs: '2px', 29 | }, 30 | animation: { 31 | 'fade-in': 'fadeIn 0.3s ease-out', 32 | 'slide-up': 'slideUp 0.3s ease-out', 33 | 'scale-in': 'scaleIn 0.2s ease-out', 34 | }, 35 | keyframes: { 36 | fadeIn: { 37 | '0%': { opacity: '0' }, 38 | '100%': { opacity: '1' }, 39 | }, 40 | slideUp: { 41 | '0%': { transform: 'translateY(20px)', opacity: '0' }, 42 | '100%': { transform: 'translateY(0)', opacity: '1' }, 43 | }, 44 | scaleIn: { 45 | '0%': { transform: 'scale(0.95)', opacity: '0' }, 46 | '100%': { transform: 'scale(1)', opacity: '1' }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | plugins: [], 52 | } -------------------------------------------------------------------------------- /dashboard/src/utils/req.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // 创建一个 Axios 实例 4 | const req = axios.create({ 5 | baseURL: 'http://127.0.0.1:9978', // 默认的 API 根地址 6 | timeout: 5000, // 请求超时限制 7 | headers: { 8 | 'Content-Type': 'application/json', // 默认请求头 9 | }, 10 | }); 11 | 12 | // 请求拦截器 13 | req.interceptors.request.use( 14 | (config) => { 15 | // 可以在请求发送前做一些处理,例如添加 token 16 | const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage 中 17 | if (token) { 18 | config.headers.Authorization = `Bearer ${token}`; 19 | } 20 | return config; 21 | }, 22 | (error) => { 23 | return Promise.reject(error); 24 | } 25 | ); 26 | 27 | // 响应拦截器 28 | req.interceptors.response.use( 29 | (response) => { 30 | // 如果有需要,可以在这里统一处理响应数据 31 | return response.data; 32 | }, 33 | (error) => { 34 | // 错误处理,例如 token 失效、网络错误等 35 | if (error.response) { 36 | // 服务器返回的错误信息 37 | console.error(`Error ${error.response.status}: ${error.response.data.message}`); 38 | } else { 39 | // 网络或其他错误 40 | console.error('Network error or timeout', error); 41 | } 42 | return Promise.reject(error); 43 | } 44 | ); 45 | 46 | if (process.env.NODE_ENV === 'development') { 47 | req.defaults.baseURL = 'http://127.0.0.1:9978'; // 开发环境使用本地服务器 48 | } else { 49 | req.defaults.baseURL = ''; // 生产环境使用线上 API 50 | } 51 | 52 | 53 | export default req; 54 | -------------------------------------------------------------------------------- /dashboard/src/components/GlobalToast.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /dashboard/vite.config.js: -------------------------------------------------------------------------------- 1 | import {defineConfig, loadEnv} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import path from 'path' 4 | import { readFileSync } from 'fs' 5 | 6 | // 读取 package.json 中的版本号 7 | const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8')) 8 | const version = packageJson.version 9 | 10 | // https://vite.dev/config/ 11 | export default defineConfig(({ command, mode }) => { 12 | // 加载环境变量 13 | const env = loadEnv(mode, process.cwd(), '') 14 | 15 | return { 16 | // 基础路径配置,支持子目录部署 17 | // 开发环境使用 '/',生产环境可以通过环境变量设置 18 | base: mode.includes('production') ? (env.VITE_BASE_PATH || './') : '/', 19 | 20 | // 定义全局变量 21 | define: { 22 | __APP_VERSION__: JSON.stringify(version) 23 | }, 24 | 25 | plugins: [ 26 | vue(), 27 | ], 28 | 29 | // 构建配置 30 | build: { 31 | // 输出目录 32 | outDir: 'dist', 33 | // 静态资源目录 34 | assetsDir: 'assets', 35 | // 生成相对路径的资源引用 36 | rollupOptions: { 37 | output: { 38 | // 静态资源文件名格式 39 | assetFileNames: 'assets/[name]-[hash][extname]', 40 | chunkFileNames: 'assets/[name]-[hash].js', 41 | entryFileNames: 'assets/[name]-[hash].js' 42 | } 43 | } 44 | }, 45 | 46 | optimizeDeps: { 47 | include: [ 48 | '@arco-design/web-vue/es/icon' 49 | ] 50 | }, 51 | 52 | resolve: { 53 | alias: { 54 | '@': path.resolve(__dirname, 'src'), // 配置别名 55 | }, 56 | }, 57 | } 58 | }) -------------------------------------------------------------------------------- /dashboard/src/style.css: -------------------------------------------------------------------------------- 1 | /* 引入现代化设计系统 */ 2 | @import './styles/design-system.css'; 3 | 4 | :root { 5 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 6 | line-height: 1.5; 7 | font-weight: 400; 8 | 9 | color-scheme: light dark; 10 | color: rgba(255, 255, 255, 0.87); 11 | background-color: #242424; 12 | 13 | font-synthesis: none; 14 | text-rendering: optimizeLegibility; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | a { 20 | font-weight: 500; 21 | color: #646cff; 22 | text-decoration: inherit; 23 | } 24 | a:hover { 25 | color: #535bf2; 26 | } 27 | 28 | body { 29 | margin: 0; 30 | display: flex; 31 | place-items: center; 32 | min-width: 320px; 33 | min-height: 100vh; 34 | } 35 | 36 | h1 { 37 | font-size: 3.2em; 38 | line-height: 1.1; 39 | } 40 | 41 | button { 42 | border-radius: 8px; 43 | border: 1px solid transparent; 44 | padding: 0.6em 1.2em; 45 | font-size: 1em; 46 | font-weight: 500; 47 | font-family: inherit; 48 | background-color: #1a1a1a; 49 | cursor: pointer; 50 | transition: border-color 0.25s; 51 | } 52 | button:hover { 53 | border-color: #646cff; 54 | } 55 | button:focus, 56 | button:focus-visible { 57 | outline: 4px auto -webkit-focus-ring-color; 58 | } 59 | 60 | .card { 61 | padding: 2em; 62 | } 63 | 64 | #app { 65 | max-width: 1280px; 66 | margin: 0 auto; 67 | padding: 2rem; 68 | text-align: center; 69 | } 70 | 71 | @media (prefers-color-scheme: light) { 72 | :root { 73 | color: #213547; 74 | background-color: #ffffff; 75 | } 76 | a:hover { 77 | color: #747bff; 78 | } 79 | button { 80 | background-color: #f9f9f9; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /dashboard/src/stores/visitedStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useVisitedStore = defineStore('visited', { 4 | state: () => ({ 5 | lastClickedVideoId: null, // 最后点击的视频ID 6 | lastClickedVideoName: null // 最后点击的视频名称 7 | }), 8 | 9 | getters: { 10 | // 检查是否是最后点击的视频 11 | isLastClicked: (state) => (videoId) => { 12 | return state.lastClickedVideoId === videoId; 13 | } 14 | }, 15 | 16 | actions: { 17 | // 记录最后点击的视频 18 | setLastClicked(videoId, videoName) { 19 | if (!videoId) return; 20 | 21 | this.lastClickedVideoId = videoId; 22 | this.lastClickedVideoName = videoName; 23 | 24 | // 保存到localStorage 25 | this.saveToStorage(); 26 | }, 27 | 28 | // 清除记录 29 | clear() { 30 | this.lastClickedVideoId = null; 31 | this.lastClickedVideoName = null; 32 | localStorage.removeItem('last-clicked-video'); 33 | }, 34 | 35 | // 保存到localStorage 36 | saveToStorage() { 37 | try { 38 | const data = { 39 | videoId: this.lastClickedVideoId, 40 | videoName: this.lastClickedVideoName 41 | }; 42 | localStorage.setItem('last-clicked-video', JSON.stringify(data)); 43 | } catch (error) { 44 | console.warn('保存最后点击视频失败:', error); 45 | } 46 | }, 47 | 48 | // 从localStorage加载 49 | loadFromStorage() { 50 | try { 51 | const stored = localStorage.getItem('last-clicked-video'); 52 | if (stored) { 53 | const data = JSON.parse(stored); 54 | this.lastClickedVideoId = data.videoId; 55 | this.lastClickedVideoName = data.videoName; 56 | } 57 | } catch (error) { 58 | console.warn('加载最后点击视频失败:', error); 59 | this.clear(); 60 | } 61 | } 62 | } 63 | }); -------------------------------------------------------------------------------- /dashboard/src/stores/siteStore.js: -------------------------------------------------------------------------------- 1 | // 在 Pinia store 中,currentSite 作为状态存储 2 | import {defineStore} from 'pinia' 3 | import {ref, watch} from 'vue' 4 | import siteService from '@/api/services/site' 5 | 6 | export const useSiteStore = defineStore('site', () => { 7 | // 使用 ref 创建响应式状态,优先从 siteService 获取 8 | const nowSite = ref(siteService.getCurrentSite() || JSON.parse(localStorage.getItem('site-nowSite')) || null) 9 | 10 | // 设置当前源并同步到 siteService 11 | const setCurrentSite = (site) => { 12 | nowSite.value = site // 更新响应式状态 13 | 14 | // 同步到两个存储系统 15 | localStorage.setItem('site-nowSite', JSON.stringify(site)) // 兼容旧系统 16 | 17 | // 同步到 siteService(如果传入的是完整站点对象) 18 | if (site && site.key) { 19 | siteService.setCurrentSite(site.key) 20 | } 21 | 22 | console.log('站点已切换:', site) 23 | } 24 | 25 | // 从 siteService 同步站点信息 26 | const syncFromSiteService = () => { 27 | const currentSite = siteService.getCurrentSite() 28 | if (currentSite && (!nowSite.value || currentSite.key !== nowSite.value.key)) { 29 | nowSite.value = currentSite 30 | localStorage.setItem('site-nowSite', JSON.stringify(currentSite)) 31 | console.log('从 siteService 同步站点:', currentSite) 32 | } 33 | } 34 | 35 | // 监听 siteService 的站点变化事件 36 | if (typeof window !== 'undefined') { 37 | window.addEventListener('siteChange', (event) => { 38 | const { site } = event.detail 39 | if (site && (!nowSite.value || site.key !== nowSite.value.key)) { 40 | nowSite.value = site 41 | localStorage.setItem('site-nowSite', JSON.stringify(site)) 42 | console.log('响应 siteService 站点变化:', site) 43 | } 44 | }) 45 | } 46 | 47 | // 初始化时同步一次 48 | syncFromSiteService() 49 | 50 | return { 51 | nowSite, 52 | setCurrentSite, 53 | syncFromSiteService, 54 | } 55 | }) -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drplayer", 3 | "private": true, 4 | "version": "1.0.3 20251017", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "build:root": "vite build --mode production.root", 10 | "build:apps": "vite build --mode production.apps", 11 | "build:binary": "node build-binary.js", 12 | "build:binary:win": "node build-binary.js", 13 | "build:binary:optimized": "node build-binary-optimized.js", 14 | "preview": "vite preview", 15 | "start": "node production-server.cjs", 16 | "mock1": "json-server src/mock/data.json --host 127.0.0.1 --port 9978 --middlewares src/mock/middlewares.js", 17 | "mock2": "json-server public/mock/data.json --host 127.0.0.1 --port 9978 --middlewares public/mock/middlewares.js" 18 | }, 19 | "engines": { 20 | "node": ">17 <23" 21 | }, 22 | "repository": "https://github.com/hjdhnx/DrPlayer.git", 23 | "author": "晚风拂柳颜 <434857005@qq.com>", 24 | "description": "道长修仙,法力无边。一统江湖,只需半年。\n君子袒蛋蛋,小人藏鸡鸡。", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@fastify/static": "^6.12.0", 28 | "artplayer": "^5.3.0", 29 | "artplayer-plugin-danmuku": "^5.2.0", 30 | "axios": "1.12.0", 31 | "crypto-js": "^4.2.0", 32 | "echarts": "^5.6.0", 33 | "fastify": "^4.29.1", 34 | "flv.js": "^1.6.2", 35 | "hls.js": "^1.6.13", 36 | "json-server": "^0.17.4", 37 | "mpegts.js": "^1.8.0", 38 | "pinia": "^2.2.6", 39 | "shaka-player": "^4.16.3", 40 | "v-viewer": "^3.0.22", 41 | "viewerjs": "^1.11.0", 42 | "vue": "^3.5.12", 43 | "vue-echarts": "^7.0.3", 44 | "vue-router": "^4.4.5", 45 | "vuedraggable": "^4.1.0" 46 | }, 47 | "devDependencies": { 48 | "@arco-design/web-vue": "^2.56.3", 49 | "@playwright/test": "^1.55.1", 50 | "@tailwindcss/postcss": "^4.1.13", 51 | "@vitejs/plugin-vue": "^6.0.1", 52 | "autoprefixer": "^10.4.21", 53 | "postcss": "^8.5.6", 54 | "tailwindcss": "^4.1.13", 55 | "unplugin-vue-components": "^0.27.4", 56 | "vite": "^7.1.7" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /dashboard/docs/外部播放器配置说明.md: -------------------------------------------------------------------------------- 1 | # 外部播放器配置说明 2 | 3 | 本文档说明如何配置VLC和MPV播放器以支持自定义协议唤起。 4 | 5 | ## 配置步骤 6 | 7 | ### 1. 修改注册表文件中的播放器路径 8 | 9 | 根据您的实际安装路径修改注册表文件: 10 | 11 | #### vlc-protocol.reg 12 | ```reg 13 | Windows Registry Editor Version 5.00 14 | 15 | [HKEY_CLASSES_ROOT\vlc] 16 | @="URL:VLC Protocol" 17 | "URL Protocol"="" 18 | 19 | [HKEY_CLASSES_ROOT\vlc\shell] 20 | 21 | [HKEY_CLASSES_ROOT\vlc\shell\open] 22 | 23 | [HKEY_CLASSES_ROOT\vlc\shell\open\command] 24 | @="\"C:\\Program Files\\VideoLAN\\VLC\\vlc.exe\" \"%1\"" 25 | ``` 26 | 27 | #### mpv-protocol.reg 28 | ```reg 29 | Windows Registry Editor Version 5.00 30 | 31 | [HKEY_CLASSES_ROOT\mpv] 32 | @="URL:MPV Protocol" 33 | "URL Protocol"="" 34 | 35 | [HKEY_CLASSES_ROOT\mpv\shell] 36 | 37 | [HKEY_CLASSES_ROOT\mpv\shell\open] 38 | 39 | [HKEY_CLASSES_ROOT\mpv\shell\open\command] 40 | @="\"C:\\Program Files\\mpv\\mpv.exe\" \"%1\"" 41 | ``` 42 | 43 | **注意**: 请根据您的实际安装路径修改播放器的路径。常见路径: 44 | - VLC: `C:\Program Files\VideoLAN\VLC\vlc.exe` 45 | - MPV: `C:\Program Files\mpv\mpv.exe` 或 `C:\mpv\mpv.exe` 46 | 47 | ### 2. 注册协议 48 | 49 | 以管理员身份运行以下注册表文件: 50 | 51 | 1. 右键点击 `vlc-protocol.reg` → 选择"合并" 52 | 2. 右键点击 `mpv-protocol.reg` → 选择"合并" 53 | 54 | ### 3. 测试配置 55 | 56 | 配置完成后,您可以在浏览器地址栏中测试: 57 | 58 | - VLC: `vlc://https://example.com/video.mp4` 59 | - MPV: `mpv://https://example.com/video.mp4` 60 | 61 | ## 工作原理 62 | 63 | 当您点击"用VLC播放"或"用MPV播放"按钮时: 64 | 65 | 1. JavaScript代码构造 `vlc://` 或 `mpv://` 协议URL 66 | 2. 浏览器识别自定义协议并查找注册表中的处理程序 67 | 3. 系统调用对应的播放器,并将完整URL作为参数传递 68 | 4. 播放器接收到URL参数(如 `vlc://https://example.com/video.mp4`) 69 | 5. 播放器会自动处理协议前缀,提取真实的视频URL进行播放 70 | 71 | ## 常见问题 72 | 73 | ### Q: 如何确认配置是否成功? 74 | A: 在浏览器地址栏输入测试URL,如果能够成功调起对应的播放器,说明配置成功。 75 | 76 | ### Q: 播放器路径不正确怎么办? 77 | A: 请根据您的实际安装路径修改注册表文件中的播放器路径。 78 | 79 | ### Q: 为什么有时候播放器无法打开视频? 80 | A: 可能是因为: 81 | 1. 视频URL需要特殊的请求头(如Referer) 82 | 2. 视频格式不被播放器支持 83 | 3. 网络连接问题 84 | 85 | ## 卸载协议 86 | 87 | 如果需要卸载协议,可以创建卸载的注册表文件: 88 | 89 | ```reg 90 | Windows Registry Editor Version 5.00 91 | 92 | [-HKEY_CLASSES_ROOT\vlc] 93 | [-HKEY_CLASSES_ROOT\mpv] 94 | ``` 95 | 96 | 保存为 `.reg` 文件并以管理员身份运行即可删除协议注册。 -------------------------------------------------------------------------------- /dashboard/src/mock/multiInputX.json: -------------------------------------------------------------------------------- 1 | { 2 | "vod_id": "{\"actionId\":\"多项输入\",\"type\":\"multiInputX\",\"canceledOnTouchOutside\":true,\"title\":\"Action多项输入(multiInputX)\",\"width\":716,\"height\":-300,\"bottom\":1,\"dimAmount\":0.3,\"msg\":\"“平行志愿”是普通高校招生平行志愿投档模式的简称,平行志愿投档又可分为按“院校+专业组”(以下简称院校专业组)平行志愿投档和按专业平行志愿投档两类。\",\"button\":3,\"input\":[{\"id\":\"item1\",\"name\":\"文件夹路径(文件夹选择)\",\"tip\":\"请输入文件夹路径\",\"value\":\"\",\"selectData\":\"[folder]\",\"validation\":\"\",\"inputType\":0,\"help\":\"“平行志愿”是普通高校招生平行志愿投档模式的简称,平行志愿投档又可分为按“院校+专业组”(以下简称院校专业组)平行志愿投档和按专业平行志愿投档两类。\"},{\"id\":\"item2\",\"name\":\"日期(日期选择)\",\"tip\":\"请输入项目2内容\",\"value\":\"\",\"selectData\":\"[calendar]\",\"validation\":\"\",\"inputType\":0},{\"id\":\"item3\",\"name\":\"文件路径(文件选择)\",\"tip\":\"请输入文件路径\",\"value\":\"\",\"selectData\":\"[file]\",\"inputType\":0},{\"id\":\"item4\",\"name\":\"多项选择\",\"tip\":\"请输入多项内容,以“,”分隔\",\"value\":\"\",\"selectData\":\"[请选择字母]a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z\",\"selectWidth\":640,\"multiSelect\":true,\"selectColumn\":4,\"inputType\":0},{\"id\":\"item5\",\"name\":\"项目5\",\"tip\":\"请输入项目5内容\",\"value\":\"\",\"multiLine\":5},{\"id\":\"item6\",\"name\":\"项目6\",\"tip\":\"请输入项目6内容,密码,inputType: 129\",\"value\":\"\",\"inputType\":129},{\"id\":\"item7\",\"name\":\"图像base64(图像文件选择)\",\"tip\":\"请输入项目7内容\",\"value\":\"\",\"selectData\":\"[image]\",\"multiLine\":3,\"inputType\":0},{\"id\":\"item8\",\"name\":\"单项选择\",\"tip\":\"请输入项目8内容\",\"value\":\"李四\",\"selectData\":\"[请选择]张三,李四,王五,赵六\",\"onlyQuickSelect\":true},{\"id\":\"item9\",\"name\":\"项目9\\n“平行志愿”是普通高校招生平行志愿投档模式的简称,平行志愿投档又可分为按“院校+专业组”(以下简称院校专业组)平行志愿投档和按专业平行志愿投档两类。\",\"tip\":\"请输入项目9内容\",\"value\":\"可歌可泣\",\"selectData\":\"[请选择]可爱,可惜,可人,可以,可歌可泣,可恶\",\"validation\":\"\",\"quickSelect\":true,\"inputType\":0,\"help\":\"

“平行志愿”

是普通高校 招生平行 志愿投档模式的简称,平行志愿投档又可分为按“院校+专业组”(以下简称院校专业组)平行志愿投档和按专业平行志愿         投档两类。\"},{\"id\":\"item10\",\"name\":\"项目10\",\"tip\":\"请输入项目10内容\",\"value\":\"\"},{\"id\":\"item11\",\"name\":\"项目11\",\"tip\":\"请输入项目11内容\",\"value\":\"\"}]}", 3 | "vod_name": "多项输入", 4 | "vod_tag": "action" 5 | } -------------------------------------------------------------------------------- /dashboard/src/api/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API配置文件 3 | * 定义接口相关的配置信息和常量 4 | */ 5 | 6 | /** 7 | * 获取API超时时间(毫秒) 8 | * 从设置中读取,如果没有设置则使用默认值30秒 9 | */ 10 | export const getApiTimeout = () => { 11 | try { 12 | const addressSettings = JSON.parse(localStorage.getItem('addressSettings') || '{}') 13 | const timeoutSeconds = addressSettings.apiTimeout || 30 14 | return timeoutSeconds * 1000 // 转换为毫秒 15 | } catch (error) { 16 | console.warn('Failed to get API timeout from settings, using default:', error) 17 | return 30000 // 默认30秒 18 | } 19 | } 20 | 21 | /** 22 | * 获取Action接口超时时间(毫秒) 23 | * Action接口通常需要更长的超时时间,默认100秒 24 | */ 25 | export const getActionTimeout = () => { 26 | try { 27 | const addressSettings = JSON.parse(localStorage.getItem('addressSettings') || '{}') 28 | const apiTimeoutSeconds = addressSettings.apiTimeout || 30 29 | // Action接口超时时间为普通接口的3倍,但最少100秒 30 | const actionTimeoutSeconds = Math.max(apiTimeoutSeconds * 3, 100) 31 | return actionTimeoutSeconds * 1000 // 转换为毫秒 32 | } catch (error) { 33 | console.warn('Failed to get Action timeout from settings, using default:', error) 34 | return 100000 // 默认100秒 35 | } 36 | } 37 | 38 | // 基础配置 39 | export const API_CONFIG = { 40 | // 基础URL配置 41 | BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5707', 42 | 43 | // 超时配置(动态获取) 44 | get TIMEOUT() { 45 | return getApiTimeout() 46 | }, 47 | 48 | // 请求头配置 49 | HEADERS: { 50 | 'Content-Type': 'application/json', 51 | 'Accept': 'application/json' 52 | } 53 | } 54 | 55 | // 接口路径常量 56 | export const API_PATHS = { 57 | // 模块数据接口 58 | MODULE: '/api', 59 | 60 | // 代理接口 61 | PROXY: '/proxy', 62 | 63 | // 解析接口 64 | PARSE: '/parse' 65 | } 66 | 67 | // 模块功能类型 68 | export const MODULE_ACTIONS = { 69 | PLAY: 'play', // 播放接口 70 | CATEGORY: 'category', // 分类接口 71 | DETAIL: 'detail', // 详情接口 72 | ACTION: 'action', // 动作接口 73 | SEARCH: 'search', // 搜索接口 74 | REFRESH: 'refresh', // 刷新接口 75 | HOME: 'home' // 首页接口(默认) 76 | } 77 | 78 | // 响应状态码 79 | export const RESPONSE_CODES = { 80 | SUCCESS: 200, 81 | NOT_FOUND: 404, 82 | SERVER_ERROR: 500 83 | } 84 | 85 | // 默认分页配置 86 | export const PAGINATION = { 87 | DEFAULT_PAGE: 1, 88 | DEFAULT_PAGE_SIZE: 20 89 | } 90 | 91 | export default { 92 | API_CONFIG, 93 | API_PATHS, 94 | MODULE_ACTIONS, 95 | RESPONSE_CODES, 96 | PAGINATION 97 | } -------------------------------------------------------------------------------- /dashboard/docs/OPTIMIZATION_REPORT.md: -------------------------------------------------------------------------------- 1 | # DrPlayer PKG 打包优化报告 2 | 3 | ## 📊 优化结果总览 4 | 5 | | 指标 | 原始版本 | 优化版本 | 改善幅度 | 6 | |------|----------|----------|----------| 7 | | 文件大小 | 45.70 MB | 38.85 MB | **-15.0%** | 8 | | 构建脚本 | `build:binary` | `build:binary:optimized` | 新增优化版本 | 9 | | 压缩技术 | 无 | Brotli + UPX检测 | 智能压缩 | 10 | 11 | ## 🔧 实施的优化措施 12 | 13 | ### 1. PKG 内置优化选项 14 | - **Brotli 压缩**: 启用 `--compress Brotli` 选项 15 | - **公共包优化**: 使用 `--public-packages "*"` 减少重复打包 16 | - **公共模式**: 启用 `--public` 加速打包过程 17 | - **内存限制**: 设置 `--options "max-old-space-size=512"` 优化内存使用 18 | 19 | ### 2. UPX 压缩集成 20 | - **智能检测**: 自动检测 UPX 压缩后的兼容性 21 | - **自动回滚**: 如果压缩后无法运行,自动恢复原文件 22 | - **备份机制**: 保留原始文件备份以确保安全 23 | - **兼容性优先**: 发现 pkg 生成的二进制文件与 UPX 存在兼容性问题 24 | 25 | ## 📁 新增文件 26 | 27 | ### 构建脚本 28 | - - 优化版构建脚本 29 | - 新增 npm 脚本: `npm run build:binary:optimized` 30 | 31 | ### 文档 32 | - - UPX 压缩使用指南 33 | - - 本优化报告 34 | 35 | ## 🚀 使用方法 36 | 37 | ### 优化构建 38 | ```bash 39 | # 使用优化版本构建 40 | npm run build:binary:optimized 41 | 42 | # 或直接运行脚本 43 | node build-binary-optimized.js 44 | ``` 45 | 46 | ### 标准构建(对比用) 47 | ```bash 48 | # 原始版本构建 49 | npm run build:binary 50 | ``` 51 | 52 | ## ⚠️ 重要发现 53 | 54 | ### UPX 兼容性问题 55 | 经过测试发现,**pkg 生成的二进制文件与 UPX 压缩存在兼容性问题**: 56 | 57 | 1. **问题现象**: UPX 压缩后的二进制文件无法正常启动 58 | 2. **错误信息**: `Pkg: Error reading from file.` 59 | 3. **根本原因**: pkg 使用特殊的文件格式和内部偏移量,UPX 压缩会破坏这些结构 60 | 4. **解决方案**: 实现了智能检测和自动回滚机制 61 | 62 | ### 最佳实践建议 63 | 1. **优先使用 pkg 内置优化**: Brotli 压缩等选项安全可靠 64 | 2. **谨慎使用 UPX**: 仅在确认兼容性后使用 65 | 3. **保留备份**: 始终保留原始文件备份 66 | 4. **测试验证**: 压缩后必须进行功能测试 67 | 68 | ## 📈 性能影响 69 | 70 | ### 文件大小 71 | - **原始**: 45.70 MB 72 | - **优化**: 38.85 MB 73 | - **减少**: 6.85 MB (15.0%) 74 | 75 | ### 启动性能 76 | - **功能完整性**: ✅ 完全保持 77 | - **启动时间**: 📈 略有改善(文件更小) 78 | - **运行稳定性**: ✅ 无影响 79 | 80 | ## 🔮 未来优化方向 81 | 82 | 1. **前端代码分割**: 减少 JavaScript bundle 大小 83 | 2. **依赖优化**: 移除不必要的依赖包 84 | 3. **资源压缩**: 优化图片和静态资源 85 | 4. **Tree Shaking**: 更精确的无用代码消除 86 | 87 | ## 📞 技术支持 88 | 89 | 如遇到问题,请参考: 90 | 1. - 详细的 UPX 使用指南 91 | 2. 构建脚本中的错误处理和日志输出 92 | 3. 备份文件恢复机制 93 | 94 | --- 95 | *报告生成时间: $(date)* 96 | *优化版本: v1.0* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | /dashboard/apps/drplayer/ 132 | -------------------------------------------------------------------------------- /dashboard/docs/nginx-root.conf: -------------------------------------------------------------------------------- 1 | # Nginx配置文件 - 根目录部署 2 | # 适用于将DrPlayer部署在域名根目录的情况 3 | # 例如: https://example.com/ 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; # 请替换为您的域名 8 | 9 | # 网站根目录,指向Vue构建后的dist目录 10 | root /var/www/html/drplayer; # 请替换为您的实际路径 11 | index index.html; 12 | 13 | # 启用gzip压缩 14 | gzip on; 15 | gzip_vary on; 16 | gzip_min_length 1024; 17 | gzip_proxied any; 18 | gzip_comp_level 6; 19 | gzip_types 20 | text/plain 21 | text/css 22 | text/xml 23 | text/javascript 24 | application/json 25 | application/javascript 26 | application/xml+rss 27 | application/atom+xml 28 | image/svg+xml; 29 | 30 | # 静态资源缓存配置 31 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 32 | expires 1y; 33 | add_header Cache-Control "public, immutable"; 34 | access_log off; 35 | } 36 | 37 | # API代理配置(如果需要) 38 | # location /api/ { 39 | # proxy_pass http://backend-server; 40 | # proxy_set_header Host $host; 41 | # proxy_set_header X-Real-IP $remote_addr; 42 | # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 43 | # proxy_set_header X-Forwarded-Proto $scheme; 44 | # } 45 | 46 | # Vue Router History模式支持 47 | # 所有不匹配静态文件的请求都返回index.html 48 | location / { 49 | try_files $uri $uri/ /index.html; 50 | 51 | # 安全头设置 52 | add_header X-Frame-Options "SAMEORIGIN" always; 53 | add_header X-Content-Type-Options "nosniff" always; 54 | add_header X-XSS-Protection "1; mode=block" always; 55 | add_header Referrer-Policy "strict-origin-when-cross-origin" always; 56 | } 57 | 58 | # 禁止访问隐藏文件 59 | location ~ /\. { 60 | deny all; 61 | access_log off; 62 | log_not_found off; 63 | } 64 | 65 | # 禁止访问备份文件 66 | location ~ ~$ { 67 | deny all; 68 | access_log off; 69 | log_not_found off; 70 | } 71 | 72 | # 错误页面 73 | error_page 404 /index.html; 74 | error_page 500 502 503 504 /index.html; 75 | } 76 | 77 | # HTTPS配置示例(可选) 78 | # server { 79 | # listen 443 ssl http2; 80 | # server_name localhost; # 请替换为您的域名 81 | # 82 | # ssl_certificate /path/to/your/certificate.crt; 83 | # ssl_certificate_key /path/to/your/private.key; 84 | # 85 | # # SSL配置 86 | # ssl_protocols TLSv1.2 TLSv1.3; 87 | # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; 88 | # ssl_prefer_server_ciphers off; 89 | # 90 | # # 其他配置与HTTP相同 91 | # root /var/www/html/drplayer; 92 | # index index.html; 93 | # 94 | # location / { 95 | # try_files $uri $uri/ /index.html; 96 | # } 97 | # } -------------------------------------------------------------------------------- /dashboard/src/assets/icon_font/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 5032989 */ 3 | src: url('iconfont.woff2?t=1760257974279') format('woff2'), 4 | url('iconfont.woff?t=1760257974279') format('woff'), 5 | url('iconfont.ttf?t=1760257974279') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-xiazai:before { 17 | content: "\fcb9"; 18 | } 19 | 20 | .icon-kuake:before { 21 | content: "\e67d"; 22 | } 23 | 24 | .icon-folder:before { 25 | content: "\e690"; 26 | } 27 | 28 | .icon-file:before { 29 | content: "\e692"; 30 | } 31 | 32 | .icon-file_zip:before { 33 | content: "\e698"; 34 | } 35 | 36 | .icon-file_excel:before { 37 | content: "\e699"; 38 | } 39 | 40 | .icon-file_ppt:before { 41 | content: "\e69a"; 42 | } 43 | 44 | .icon-file_word:before { 45 | content: "\e69b"; 46 | } 47 | 48 | .icon-file_pdf:before { 49 | content: "\e69c"; 50 | } 51 | 52 | .icon-file_music:before { 53 | content: "\e69d"; 54 | } 55 | 56 | .icon-file_video:before { 57 | content: "\e69e"; 58 | } 59 | 60 | .icon-file_img:before { 61 | content: "\e69f"; 62 | } 63 | 64 | .icon-file_ai:before { 65 | content: "\e6a1"; 66 | } 67 | 68 | .icon-file_psd:before { 69 | content: "\e6a2"; 70 | } 71 | 72 | .icon-file_bt:before { 73 | content: "\e6a3"; 74 | } 75 | 76 | .icon-file_txt:before { 77 | content: "\e6a4"; 78 | } 79 | 80 | .icon-file_exe:before { 81 | content: "\e6a5"; 82 | } 83 | 84 | .icon-file_html:before { 85 | content: "\e6a7"; 86 | } 87 | 88 | .icon-file_cad:before { 89 | content: "\e6a8"; 90 | } 91 | 92 | .icon-file_code:before { 93 | content: "\e6a9"; 94 | } 95 | 96 | .icon-file_flash:before { 97 | content: "\e6aa"; 98 | } 99 | 100 | .icon-file_iso:before { 101 | content: "\e6ab"; 102 | } 103 | 104 | .icon-file_cloud:before { 105 | content: "\e6ac"; 106 | } 107 | 108 | .icon-wenjianjia:before { 109 | content: "\e80c"; 110 | } 111 | 112 | .icon-zhuye:before { 113 | content: "\e71f"; 114 | } 115 | 116 | .icon-shezhi:before { 117 | content: "\e63a"; 118 | } 119 | 120 | .icon-shipinzhibo:before { 121 | content: "\e621"; 122 | } 123 | 124 | .icon-lishi:before { 125 | content: "\e61e"; 126 | } 127 | 128 | .icon-jiexi:before { 129 | content: "\e614"; 130 | } 131 | 132 | .icon-shoucang:before { 133 | content: "\e6c4"; 134 | } 135 | 136 | .icon-ceshi:before { 137 | content: "\efb1"; 138 | } 139 | 140 | .icon-dianbo:before { 141 | content: "\e770"; 142 | } 143 | 144 | .icon-zhuye1:before { 145 | content: "\e697"; 146 | } 147 | 148 | .icon-shugui:before { 149 | content: "\e71e"; 150 | } 151 | 152 | -------------------------------------------------------------------------------- /dashboard/src/router/index.js: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHistory} from 'vue-router'; 2 | import Home from '@/views/Home.vue'; 3 | import Video from '@/views/Video.vue'; 4 | import VideoDetail from '@/views/VideoDetail.vue'; 5 | import Live from '@/views/Live.vue'; 6 | import Parser from '@/views/Parser.vue'; 7 | import Collection from '@/views/Collection.vue'; 8 | import History from '@/views/History.vue'; 9 | import Settings from '@/views/Settings.vue'; 10 | import BookGallery from '@/views/BookGallery.vue'; 11 | import ActionTest from '@/views/ActionTest.vue'; 12 | import ActionDebugTest from '@/views/ActionDebugTest.vue'; 13 | import VideoTest from '@/views/VideoTest.vue'; 14 | import CSPTest from '@/views/CSPTest.vue'; 15 | import SearchAggregation from '@/views/SearchAggregation.vue'; 16 | 17 | 18 | const routes = [ 19 | {path: '/', component: Home, name: 'Home'}, 20 | {path: '/video', component: Video, name: 'Video'}, 21 | {path: '/video/:id', component: VideoDetail, name: 'VideoDetail', props: true}, 22 | {path: '/live', component: Live, name: 'Live'}, 23 | {path: '/settings', component: Settings, name: 'Settings'}, 24 | {path: '/collection', component: Collection, name: 'Collection'}, 25 | {path: '/book-gallery', component: BookGallery, name: 'BookGallery'}, 26 | {path: '/local-book-reader/:bookId', component: () => import('@/views/LocalBookReader.vue'), name: 'LocalBookReader', props: true}, 27 | {path: '/download-manager', component: () => import('@/components/downloader/NovelDownloader.vue'), name: 'DownloadManager'}, 28 | {path: '/history', component: History, name: 'History'}, 29 | {path: '/parser', component: Parser, name: 'Parser'}, 30 | {path: '/action-test', component: ActionTest, name: 'ActionTest'}, 31 | {path: '/action-debug-test', component: ActionDebugTest, name: 'ActionDebugTest'}, 32 | {path: '/video-test', component: VideoTest, name: 'VideoTest'}, 33 | {path: '/csp-test', component: CSPTest, name: 'CSPTest'}, 34 | {path: '/search', component: SearchAggregation, name: 'SearchAggregation'}, 35 | 36 | // 404 fallback路由 - 必须放在最后 37 | {path: '/:pathMatch(.*)*', redirect: '/'} 38 | ]; 39 | 40 | // 获取base路径,支持子目录部署 41 | const getBasePath = () => { 42 | // 在生产环境中,如果设置了VITE_BASE_PATH环境变量,使用它 43 | if (import.meta.env.PROD && import.meta.env.VITE_BASE_PATH) { 44 | return import.meta.env.VITE_BASE_PATH; 45 | } 46 | // 否则使用Vite的BASE_URL 47 | return import.meta.env.BASE_URL; 48 | }; 49 | 50 | const router = createRouter({ 51 | history: createWebHistory(getBasePath()), 52 | routes, 53 | // 滚动行为 54 | scrollBehavior(to, from, savedPosition) { 55 | if (savedPosition) { 56 | return savedPosition; 57 | } else { 58 | return { top: 0 }; 59 | } 60 | } 61 | }); 62 | 63 | // 路由守卫 - 可以在这里添加权限检查等逻辑 64 | router.beforeEach((to, from, next) => { 65 | // 设置页面标题 66 | if (to.name) { 67 | document.title = `DrPlayer - ${to.name}`; 68 | } 69 | next(); 70 | }); 71 | 72 | export default router; 73 | -------------------------------------------------------------------------------- /proxy/start_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 代理服务器启动脚本 4 | 提供配置管理和监控功能 5 | """ 6 | 7 | import argparse 8 | import asyncio 9 | import logging 10 | import signal 11 | import sys 12 | from pathlib import Path 13 | 14 | import uvicorn 15 | 16 | # 添加当前目录到Python路径 17 | sys.path.insert(0, str(Path(__file__).parent)) 18 | 19 | from proxy import app, MemoryMonitor 20 | 21 | # 配置日志 22 | logging.basicConfig( 23 | level=logging.INFO, 24 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 25 | handlers=[ 26 | logging.StreamHandler(), 27 | logging.FileHandler('proxy.log') 28 | ] 29 | ) 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | class ProxyServer: 34 | """代理服务器管理器""" 35 | 36 | def __init__(self, host="0.0.0.0", port=8000, workers=1): 37 | self.host = host 38 | self.port = port 39 | self.workers = workers 40 | self.server = None 41 | 42 | async def start(self): 43 | """启动服务器""" 44 | logger.info(f"启动代理服务器: {self.host}:{self.port}") 45 | 46 | config = uvicorn.Config( 47 | app, 48 | host=self.host, 49 | port=self.port, 50 | workers=self.workers, 51 | log_level="info", 52 | access_log=True, 53 | reload=False 54 | ) 55 | 56 | self.server = uvicorn.Server(config) 57 | 58 | # 设置信号处理 59 | signal.signal(signal.SIGINT, self._signal_handler) 60 | signal.signal(signal.SIGTERM, self._signal_handler) 61 | 62 | try: 63 | await self.server.serve() 64 | except KeyboardInterrupt: 65 | logger.info("收到中断信号,正在关闭服务器...") 66 | except Exception as e: 67 | logger.error(f"服务器启动失败: {e}") 68 | raise 69 | 70 | def _signal_handler(self, signum, frame): 71 | """信号处理器""" 72 | logger.info(f"收到信号 {signum},准备关闭服务器...") 73 | if self.server: 74 | self.server.should_exit = True 75 | 76 | async def stop(self): 77 | """停止服务器""" 78 | if self.server: 79 | self.server.should_exit = True 80 | logger.info("服务器已停止") 81 | 82 | def main(): 83 | """主函数""" 84 | parser = argparse.ArgumentParser(description="代理服务器") 85 | parser.add_argument("--host", default="0.0.0.0", help="绑定主机地址") 86 | parser.add_argument("--port", type=int, default=8000, help="绑定端口") 87 | parser.add_argument("--workers", type=int, default=1, help="工作进程数") 88 | parser.add_argument("--debug", action="store_true", help="启用调试模式") 89 | 90 | args = parser.parse_args() 91 | 92 | if args.debug: 93 | logging.getLogger().setLevel(logging.DEBUG) 94 | 95 | # 创建并启动服务器 96 | server = ProxyServer(args.host, args.port, args.workers) 97 | 98 | try: 99 | asyncio.run(server.start()) 100 | except KeyboardInterrupt: 101 | logger.info("服务器已停止") 102 | except Exception as e: 103 | logger.error(f"服务器运行出错: {e}") 104 | sys.exit(1) 105 | 106 | if __name__ == "__main__": 107 | main() -------------------------------------------------------------------------------- /proxy/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | 代理服务器配置文件 3 | 可以通过环境变量或配置文件覆盖默认设置 4 | """ 5 | 6 | import os 7 | from typing import Optional 8 | 9 | class Config: 10 | """配置类""" 11 | 12 | # 服务器配置 13 | HOST: str = os.getenv("PROXY_HOST", "0.0.0.0") 14 | PORT: int = int(os.getenv("PROXY_PORT", "8000")) 15 | WORKERS: int = int(os.getenv("PROXY_WORKERS", "1")) 16 | 17 | # 连接池配置 18 | MAX_CONNECTIONS: int = int(os.getenv("PROXY_MAX_CONNECTIONS", "100")) 19 | MAX_KEEPALIVE_CONNECTIONS: int = int(os.getenv("PROXY_MAX_KEEPALIVE_CONNECTIONS", "20")) 20 | KEEPALIVE_EXPIRY: float = float(os.getenv("PROXY_KEEPALIVE_EXPIRY", "30.0")) 21 | 22 | # 超时配置 23 | CONNECT_TIMEOUT: float = float(os.getenv("PROXY_CONNECT_TIMEOUT", "10.0")) 24 | READ_TIMEOUT: float = float(os.getenv("PROXY_READ_TIMEOUT", "60.0")) 25 | WRITE_TIMEOUT: float = float(os.getenv("PROXY_WRITE_TIMEOUT", "10.0")) 26 | POOL_TIMEOUT: float = float(os.getenv("PROXY_POOL_TIMEOUT", "5.0")) 27 | 28 | # 内存监控配置 29 | MEMORY_CHECK_INTERVAL: int = int(os.getenv("PROXY_MEMORY_CHECK_INTERVAL", "30")) 30 | MAX_MEMORY_USAGE: int = int(os.getenv("PROXY_MAX_MEMORY_USAGE", "500")) 31 | CLEANUP_THRESHOLD: int = int(os.getenv("PROXY_CLEANUP_THRESHOLD", "400")) 32 | 33 | # 日志配置 34 | LOG_LEVEL: str = os.getenv("PROXY_LOG_LEVEL", "INFO") 35 | LOG_FILE: Optional[str] = os.getenv("PROXY_LOG_FILE", "proxy.log") 36 | 37 | # 安全配置 38 | VERIFY_SSL: bool = os.getenv("PROXY_VERIFY_SSL", "false").lower() == "true" 39 | ALLOWED_HOSTS: Optional[str] = os.getenv("PROXY_ALLOWED_HOSTS") # 逗号分隔的主机列表 40 | 41 | # 性能配置 42 | CHUNK_SIZE: int = int(os.getenv("PROXY_CHUNK_SIZE", "8192")) 43 | ENABLE_COMPRESSION: bool = os.getenv("PROXY_ENABLE_COMPRESSION", "true").lower() == "true" 44 | 45 | @classmethod 46 | def get_allowed_hosts(cls) -> list: 47 | """获取允许的主机列表""" 48 | if cls.ALLOWED_HOSTS: 49 | return [host.strip() for host in cls.ALLOWED_HOSTS.split(",")] 50 | return ["*"] # 默认允许所有主机 51 | 52 | @classmethod 53 | def load_from_file(cls, config_file: str): 54 | """从配置文件加载配置""" 55 | try: 56 | import json 57 | with open(config_file, 'r', encoding='utf-8') as f: 58 | config_data = json.load(f) 59 | 60 | for key, value in config_data.items(): 61 | if hasattr(cls, key.upper()): 62 | setattr(cls, key.upper(), value) 63 | except FileNotFoundError: 64 | pass # 配置文件不存在时使用默认值 65 | except Exception as e: 66 | print(f"加载配置文件失败: {e}") 67 | 68 | @classmethod 69 | def save_to_file(cls, config_file: str): 70 | """保存配置到文件""" 71 | import json 72 | 73 | config_data = {} 74 | for attr in dir(cls): 75 | if attr.isupper() and not attr.startswith('_'): 76 | config_data[attr.lower()] = getattr(cls, attr) 77 | 78 | with open(config_file, 'w', encoding='utf-8') as f: 79 | json.dump(config_data, f, indent=2, ensure_ascii=False) 80 | 81 | # 默认配置实例 82 | config = Config() 83 | 84 | # 尝试加载配置文件 85 | config.load_from_file("proxy_config.json") -------------------------------------------------------------------------------- /dashboard/docs/nginx-subdir.conf: -------------------------------------------------------------------------------- 1 | # Nginx配置文件 - 子目录部署 2 | # 适用于将DrPlayer部署在域名子目录的情况 3 | # 例如: https://example.com/apps/drplayer/ 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; # 请替换为您的域名 8 | 9 | # 网站根目录 10 | root /var/www/html; 11 | index index.html; 12 | 13 | # 启用gzip压缩 14 | gzip on; 15 | gzip_vary on; 16 | gzip_min_length 1024; 17 | gzip_proxied any; 18 | gzip_comp_level 6; 19 | gzip_types 20 | text/plain 21 | text/css 22 | text/xml 23 | text/javascript 24 | application/json 25 | application/javascript 26 | application/xml+rss 27 | application/atom+xml 28 | image/svg+xml; 29 | 30 | # DrPlayer应用配置 - 子目录部署 31 | location /apps/drplayer/ { 32 | alias /var/www/html/drplayer/; # 请替换为您的实际路径 33 | 34 | # 静态资源缓存配置 35 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 36 | expires 1y; 37 | add_header Cache-Control "public, immutable"; 38 | access_log off; 39 | } 40 | 41 | # Vue Router History模式支持 42 | # 所有不匹配静态文件的请求都返回index.html 43 | try_files $uri $uri/ /apps/drplayer/index.html; 44 | 45 | # 安全头设置 46 | add_header X-Frame-Options "SAMEORIGIN" always; 47 | add_header X-Content-Type-Options "nosniff" always; 48 | add_header X-XSS-Protection "1; mode=block" always; 49 | add_header Referrer-Policy "strict-origin-when-cross-origin" always; 50 | } 51 | 52 | # 处理DrPlayer的根路径访问 53 | location = /apps/drplayer { 54 | return 301 /apps/drplayer/; 55 | } 56 | 57 | # API代理配置(如果需要) 58 | # location /apps/drplayer/api/ { 59 | # proxy_pass http://backend-server/api/; 60 | # proxy_set_header Host $host; 61 | # proxy_set_header X-Real-IP $remote_addr; 62 | # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 63 | # proxy_set_header X-Forwarded-Proto $scheme; 64 | # } 65 | 66 | # 其他应用或默认站点配置 67 | location / { 68 | try_files $uri $uri/ =404; 69 | } 70 | 71 | # 禁止访问隐藏文件 72 | location ~ /\. { 73 | deny all; 74 | access_log off; 75 | log_not_found off; 76 | } 77 | 78 | # 禁止访问备份文件 79 | location ~ ~$ { 80 | deny all; 81 | access_log off; 82 | log_not_found off; 83 | } 84 | 85 | # 错误页面 86 | error_page 404 /404.html; 87 | error_page 500 502 503 504 /50x.html; 88 | } 89 | 90 | # HTTPS配置示例(可选) 91 | # server { 92 | # listen 443 ssl http2; 93 | # server_name localhost; # 请替换为您的域名 94 | # 95 | # ssl_certificate /path/to/your/certificate.crt; 96 | # ssl_certificate_key /path/to/your/private.key; 97 | # 98 | # # SSL配置 99 | # ssl_protocols TLSv1.2 TLSv1.3; 100 | # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; 101 | # ssl_prefer_server_ciphers off; 102 | # 103 | # # 其他配置与HTTP相同 104 | # root /var/www/html; 105 | # index index.html; 106 | # 107 | # location /apps/drplayer/ { 108 | # alias /var/www/html/drplayer/; 109 | # try_files $uri $uri/ /apps/drplayer/index.html; 110 | # } 111 | # 112 | # location = /apps/drplayer { 113 | # return 301 /apps/drplayer/; 114 | # } 115 | # } -------------------------------------------------------------------------------- /dashboard/docs/API_REFACTOR_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # API 重构总结 2 | 3 | ## 重构概述 4 | 5 | 本次重构将原本分散在Vue组件中的API调用统一封装为专门的API服务模块,提高了代码的可维护性、可复用性和可测试性。 6 | 7 | ## 完成的工作 8 | 9 | ### 1. 分析后端API文档 ✅ 10 | - 详细分析了 `docs/apidoc.md` 和 `docs/t4api.md` 11 | - 理解了三个主要接口:模块数据接口(T4)、模块代理接口、解析接口 12 | - 掌握了各接口的参数、功能和响应格式 13 | 14 | ### 2. 设计API封装架构 ✅ 15 | 创建了分层的API架构: 16 | ``` 17 | src/api/ 18 | ├── index.js # 统一入口 19 | ├── config.js # 配置和常量 20 | ├── request.js # Axios封装 21 | ├── modules/ # 基础API模块 22 | ├── services/ # 业务服务层 23 | ├── utils/ # 工具函数 24 | └── types/ # 类型定义 25 | ``` 26 | 27 | ### 3. 实现基础API工具类 ✅ 28 | 29 | #### 核心文件: 30 | - **config.js**: API配置、路径、常量定义 31 | - **request.js**: Axios封装,包含拦截器和错误处理 32 | - **utils/index.js**: 数据处理和验证工具函数 33 | - **types/index.js**: 数据类型定义和工厂函数 34 | 35 | #### 基础API模块: 36 | - **modules/module.js**: T4模块数据接口封装 37 | - **modules/proxy.js**: 模块代理接口封装 38 | - **modules/parse.js**: 视频解析接口封装 39 | 40 | ### 4. 实现具体业务API模块 ✅ 41 | 42 | #### 业务服务层: 43 | - **services/video.js**: 视频相关业务逻辑 44 | - 推荐视频获取 45 | - 分类视频获取 46 | - 视频搜索 47 | - 视频详情 48 | - 播放地址获取 49 | - 视频解析 50 | - 5分钟缓存机制 51 | 52 | - **services/site.js**: 站点管理业务逻辑 53 | - 站点配置管理 54 | - 当前站点切换 55 | - 站点CRUD操作 56 | - 本地存储持久化 57 | 58 | ### 5. 重构现有Vue组件 ✅ 59 | 60 | #### 重构的组件: 61 | 1. **Video2.vue** 62 | - 替换 `req.get` 为 `siteService` 和 `videoService` 63 | - 优化站点配置获取逻辑 64 | - 改进分类列表获取方式 65 | 66 | 2. **VideoList.vue** 67 | - 使用 `videoService` 获取视频列表 68 | - 支持推荐和分类视频加载 69 | - 统一错误处理 70 | 71 | 3. **Video.vue** 72 | - 集成 `siteService` 进行站点管理 73 | - 使用 `videoService` 进行视频搜索 74 | - 同步多个状态管理store 75 | 76 | ## 技术改进 77 | 78 | ### 1. 统一的错误处理 79 | - 所有API调用都有统一的错误格式 80 | - 自动处理HTTP状态码和业务错误码 81 | - 友好的错误信息提示 82 | 83 | ### 2. 缓存机制 84 | - 视频服务实现5分钟缓存 85 | - 减少重复请求,提高性能 86 | - 自动缓存清理机制 87 | 88 | ### 3. 数据格式化 89 | - 统一的数据结构定义 90 | - 自动数据格式化和验证 91 | - 类型安全的数据处理 92 | 93 | ### 4. 配置管理 94 | - 集中的API配置管理 95 | - 环境变量支持 96 | - 灵活的参数配置 97 | 98 | ### 5. 拦截器机制 99 | - 自动添加认证token 100 | - 统一的请求头设置 101 | - 响应数据预处理 102 | 103 | ## 代码质量提升 104 | 105 | ### 1. 可维护性 106 | - 分层架构,职责清晰 107 | - 统一的代码风格 108 | - 完善的错误处理 109 | 110 | ### 2. 可复用性 111 | - 模块化设计 112 | - 通用的工具函数 113 | - 标准化的接口定义 114 | 115 | ### 3. 可测试性 116 | - 独立的服务模块 117 | - 纯函数设计 118 | - 依赖注入支持 119 | 120 | ### 4. 可扩展性 121 | - 插件化架构 122 | - 配置驱动 123 | - 标准化的扩展点 124 | 125 | ## 使用方式对比 126 | 127 | ### 重构前: 128 | ```javascript 129 | import req from '@/utils/req' 130 | 131 | // 分散的API调用 132 | const response = await req.get('/home') 133 | const data = response.data 134 | ``` 135 | 136 | ### 重构后: 137 | ```javascript 138 | import { videoService, siteService } from '@/api/services' 139 | 140 | // 语义化的业务方法 141 | const currentSite = siteService.getCurrentSite() 142 | const data = await videoService.getRecommendVideos(currentSite.key, { 143 | extend: currentSite.ext 144 | }) 145 | ``` 146 | 147 | ## 项目状态 148 | 149 | ✅ **开发服务器运行正常** - http://localhost:5174/ 150 | ✅ **无编译错误** 151 | ✅ **所有组件重构完成** 152 | ✅ **API封装架构完整** 153 | 154 | ## 后续建议 155 | 156 | 1. **添加单元测试**: 为API服务模块编写测试用例 157 | 2. **性能监控**: 添加API调用性能监控 158 | 3. **文档完善**: 补充更多使用示例和最佳实践 159 | 4. **类型定义**: 考虑使用TypeScript增强类型安全 160 | 5. **错误上报**: 集成错误监控和上报机制 161 | 162 | ## 文档 163 | 164 | - **API使用说明**: `src/api/README.md` 165 | - **重构总结**: `API_REFACTOR_SUMMARY.md` (本文档) 166 | 167 | --- 168 | 169 | **重构完成时间**: 2024年1月 170 | **重构范围**: 前端API调用层完整重构 171 | **影响组件**: Video2.vue, VideoList.vue, Video.vue 172 | **新增文件**: 15个API相关文件 173 | **代码质量**: 显著提升 -------------------------------------------------------------------------------- /dashboard/json/live_cntv.txt: -------------------------------------------------------------------------------- 1 | 央视频道,#genre# 2 | CCTV4K,https://www.yangshipin.cn/tv/home?pid=600002264 3 | CCTV1,https://www.yangshipin.cn/tv/home?pid=600001859 4 | CCTV2,https://www.yangshipin.cn/tv/home?pid=600001800 5 | CCTV3(VIP),https://www.yangshipin.cn/tv/home?pid=600001801 6 | CCTV4,https://www.yangshipin.cn/tv/home?pid=600001814 7 | CCTV5(限免),https://www.yangshipin.cn/tv/home?pid=600001818 8 | CCTV5+(限免),https://www.yangshipin.cn/tv/home?pid=600001817 9 | CCTV6(VIP),https://www.yangshipin.cn/tv/home?pid=600001802 10 | CCTV7,https://www.yangshipin.cn/tv/home?pid=600004092 11 | CCTV8(VIP),https://www.yangshipin.cn/tv/home?pid=600001803 12 | CCTV9,https://www.yangshipin.cn/tv/home?pid=600004078 13 | CCTV10,https://www.yangshipin.cn/tv/home?pid=600001805 14 | CCTV11,https://www.yangshipin.cn/tv/home?pid=600001806 15 | CCTV12,https://www.yangshipin.cn/tv/home?pid=600001807 16 | CCTV13,https://www.yangshipin.cn/tv/home?pid=600001811 17 | CCTV14,https://www.yangshipin.cn/tv/home?pid=600001809 18 | CCTV15,https://www.yangshipin.cn/tv/home?pid=600001815 19 | CCTV16-HD,https://www.yangshipin.cn/tv/home?pid=600098637 20 | CCTV16(4K)(VIP),https://www.yangshipin.cn/tv/home?pid=600099502 21 | CCTV17,https://www.yangshipin.cn/tv/home?pid=600001810 22 | CGTN,https://www.yangshipin.cn/tv/home?pid=600014550 23 | CGTN法语频道,https://www.yangshipin.cn/tv/home?pid=600084704 24 | CGTN俄语频道,https://www.yangshipin.cn/tv/home?pid=600084758 25 | CGTN阿拉伯语频道,https://www.yangshipin.cn/tv/home?pid=600084782 26 | CGTN西班牙语频道,https://www.yangshipin.cn/tv/home?pid=600084744 27 | CGTN外语纪录频道,https://www.yangshipin.cn/tv/home?pid=600084781 28 | CCTV风云剧场频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099658 29 | CCTV第一剧场频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099655 30 | CCTV怀旧剧场频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099620 31 | CCTV世界地理频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099637 32 | CCTV风云音乐频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099660 33 | CCTV兵器科技频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099649 34 | CCTV风云足球频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099636 35 | CCTV高尔夫·网球频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099659 36 | CCTV女性时尚频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099650 37 | CCTV央视文化精品频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099653 38 | CCTV央视台球频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099652 39 | CCTV电视指南频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099656 40 | CCTV卫生健康频道(VIP),https://www.yangshipin.cn/tv/home?pid=600099651 41 | 卫视频道,#genre# 42 | 北京卫视,https://www.yangshipin.cn/tv/home?pid=600002309 43 | 江苏卫视,https://www.yangshipin.cn/tv/home?pid=600002521 44 | 东方卫视,https://www.yangshipin.cn/tv/home?pid=600002483 45 | 浙江卫视,https://www.yangshipin.cn/tv/home?pid=600002520 46 | 湖南卫视,https://www.yangshipin.cn/tv/home?pid=600002475 47 | 湖北卫视,https://www.yangshipin.cn/tv/home?pid=600002508 48 | 广东卫视,https://www.yangshipin.cn/tv/home?pid=600002485 49 | 广西卫视,https://www.yangshipin.cn/tv/home?pid=600002509 50 | 黑龙江卫视,https://www.yangshipin.cn/tv/home?pid=600002498 51 | 海南卫视,https://www.yangshipin.cn/tv/home?pid=600002506 52 | 重庆卫视,https://www.yangshipin.cn/tv/home?pid=600002531 53 | 深圳卫视,https://www.yangshipin.cn/tv/home?pid=600002481 54 | 四川卫视,https://www.yangshipin.cn/tv/home?pid=600002516 55 | 河南卫视,https://www.yangshipin.cn/tv/home?pid=600002525 56 | 福建东南卫视,https://www.yangshipin.cn/tv/home?pid=600002484 57 | 贵州卫视,https://www.yangshipin.cn/tv/home?pid=600002490 58 | 江西卫视,https://www.yangshipin.cn/tv/home?pid=600002503 59 | 辽宁卫视,https://www.yangshipin.cn/tv/home?pid=600002505 60 | 安徽卫视,https://www.yangshipin.cn/tv/home?pid=600002532 61 | 河北卫视,https://www.yangshipin.cn/tv/home?pid=600002493 62 | 山东卫视,https://www.yangshipin.cn/tv/home?pid=600002513 -------------------------------------------------------------------------------- /dashboard/src/api/modules/proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 模块代理接口 3 | * 封装 /proxy/:module/* 相关的代理转发接口 4 | */ 5 | 6 | import { get, post, put, del } from '../request' 7 | import { API_PATHS } from '../config' 8 | 9 | /** 10 | * 构建代理接口URL 11 | * @param {string} module - 模块名称 12 | * @param {string} path - 代理路径 13 | * @returns {string} 完整的代理URL 14 | */ 15 | const buildProxyUrl = (module, path = '') => { 16 | const basePath = `${API_PATHS.PROXY}/${module}` 17 | return path ? `${basePath}/${path}` : basePath 18 | } 19 | 20 | /** 21 | * 代理GET请求 22 | * @param {string} module - 模块名称 23 | * @param {string} path - 代理路径 24 | * @param {object} params - 查询参数 25 | * @returns {Promise} 代理响应 26 | */ 27 | export const proxyGet = async (module, path = '', params = {}) => { 28 | const url = buildProxyUrl(module, path) 29 | return get(url, params) 30 | } 31 | 32 | /** 33 | * 代理POST请求 34 | * @param {string} module - 模块名称 35 | * @param {string} path - 代理路径 36 | * @param {object} data - 请求数据 37 | * @returns {Promise} 代理响应 38 | */ 39 | export const proxyPost = async (module, path = '', data = {}) => { 40 | const url = buildProxyUrl(module, path) 41 | return post(url, data) 42 | } 43 | 44 | /** 45 | * 代理PUT请求 46 | * @param {string} module - 模块名称 47 | * @param {string} path - 代理路径 48 | * @param {object} data - 请求数据 49 | * @returns {Promise} 代理响应 50 | */ 51 | export const proxyPut = async (module, path = '', data = {}) => { 52 | const url = buildProxyUrl(module, path) 53 | return put(url, data) 54 | } 55 | 56 | /** 57 | * 代理DELETE请求 58 | * @param {string} module - 模块名称 59 | * @param {string} path - 代理路径 60 | * @param {object} params - 查询参数 61 | * @returns {Promise} 代理响应 62 | */ 63 | export const proxyDelete = async (module, path = '', params = {}) => { 64 | const url = buildProxyUrl(module, path) 65 | return del(url, params) 66 | } 67 | 68 | /** 69 | * 通用代理请求 70 | * @param {string} module - 模块名称 71 | * @param {string} path - 代理路径 72 | * @param {object} options - 请求选项 73 | * @param {string} options.method - 请求方法 74 | * @param {object} options.params - 查询参数(GET请求) 75 | * @param {object} options.data - 请求数据(POST/PUT请求) 76 | * @returns {Promise} 代理响应 77 | */ 78 | export const proxyRequest = async (module, path = '', options = {}) => { 79 | const { method = 'GET', params, data } = options 80 | 81 | switch (method.toUpperCase()) { 82 | case 'GET': 83 | return proxyGet(module, path, params) 84 | case 'POST': 85 | return proxyPost(module, path, data) 86 | case 'PUT': 87 | return proxyPut(module, path, data) 88 | case 'DELETE': 89 | return proxyDelete(module, path, params) 90 | default: 91 | throw new Error(`不支持的请求方法: ${method}`) 92 | } 93 | } 94 | 95 | /** 96 | * 代理文件上传 97 | * @param {string} module - 模块名称 98 | * @param {string} path - 代理路径 99 | * @param {FormData} formData - 文件数据 100 | * @returns {Promise} 上传响应 101 | */ 102 | export const proxyUpload = async (module, path = '', formData) => { 103 | const url = buildProxyUrl(module, path) 104 | 105 | return post(url, formData, { 106 | headers: { 107 | 'Content-Type': 'multipart/form-data' 108 | } 109 | }) 110 | } 111 | 112 | /** 113 | * 代理文件下载 114 | * @param {string} module - 模块名称 115 | * @param {string} path - 代理路径 116 | * @param {object} params - 查询参数 117 | * @returns {Promise} 下载响应 118 | */ 119 | export const proxyDownload = async (module, path = '', params = {}) => { 120 | const url = buildProxyUrl(module, path) 121 | 122 | return get(url, params, { 123 | responseType: 'blob' 124 | }) 125 | } 126 | 127 | // 默认导出所有代理接口 128 | export default { 129 | proxyGet, 130 | proxyPost, 131 | proxyPut, 132 | proxyDelete, 133 | proxyRequest, 134 | proxyUpload, 135 | proxyDownload 136 | } -------------------------------------------------------------------------------- /dashboard/src/api/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础请求工具 3 | * 封装axios,提供统一的请求处理和错误处理 4 | */ 5 | 6 | import axios from 'axios' 7 | import { API_CONFIG, RESPONSE_CODES } from './config' 8 | 9 | // 创建axios实例 10 | const request = axios.create({ 11 | baseURL: API_CONFIG.BASE_URL, 12 | timeout: API_CONFIG.TIMEOUT, 13 | headers: API_CONFIG.HEADERS 14 | }) 15 | 16 | // 请求拦截器 17 | request.interceptors.request.use( 18 | (config) => { 19 | // 添加认证token(如果存在) 20 | const token = localStorage.getItem('token') 21 | if (token) { 22 | config.headers.Authorization = `Bearer ${token}` 23 | } 24 | 25 | // 添加时间戳防止缓存 26 | if (config.method === 'get') { 27 | config.params = { 28 | ...config.params, 29 | _t: Date.now() 30 | } 31 | } 32 | 33 | console.log('API Request:', config.method?.toUpperCase(), config.url, config.params || config.data) 34 | return config 35 | }, 36 | (error) => { 37 | console.error('Request Error:', error) 38 | return Promise.reject(error) 39 | } 40 | ) 41 | 42 | // 响应拦截器 43 | request.interceptors.response.use( 44 | (response) => { 45 | const { data, status } = response 46 | 47 | console.log('API Response:', response.config.url, data) 48 | 49 | // 检查HTTP状态码 50 | if (status !== 200) { 51 | throw new Error(`HTTP Error: ${status}`) 52 | } 53 | 54 | // 检查业务状态码 55 | if (data.code && data.code !== RESPONSE_CODES.SUCCESS) { 56 | throw new Error(data.msg || `Business Error: ${data.code}`) 57 | } 58 | 59 | return data 60 | }, 61 | (error) => { 62 | console.error('Response Error:', error) 63 | 64 | // 处理网络错误 65 | if (!error.response) { 66 | throw new Error('网络连接失败,请检查网络设置') 67 | } 68 | 69 | // 处理HTTP错误状态码 70 | const { status } = error.response 71 | switch (status) { 72 | case 404: 73 | throw new Error('请求的资源不存在') 74 | case 500: 75 | throw new Error('服务器内部错误') 76 | case 401: 77 | throw new Error('未授权访问') 78 | case 403: 79 | throw new Error('访问被禁止') 80 | default: 81 | throw new Error(`请求失败: ${status}`) 82 | } 83 | } 84 | ) 85 | 86 | /** 87 | * 通用请求方法 88 | * @param {string} url - 请求URL 89 | * @param {object} options - 请求选项 90 | * @returns {Promise} 请求结果 91 | */ 92 | export const apiRequest = async (url, options = {}) => { 93 | try { 94 | const response = await request({ 95 | url, 96 | ...options 97 | }) 98 | return response 99 | } catch (error) { 100 | console.error('API Request Failed:', error.message) 101 | throw error 102 | } 103 | } 104 | 105 | /** 106 | * GET请求 107 | * @param {string} url - 请求URL 108 | * @param {object} params - 查询参数 109 | * @returns {Promise} 请求结果 110 | */ 111 | export const get = (url, params = {}) => { 112 | return apiRequest(url, { 113 | method: 'GET', 114 | params 115 | }) 116 | } 117 | 118 | /** 119 | * POST请求 120 | * @param {string} url - 请求URL 121 | * @param {object} data - 请求数据 122 | * @returns {Promise} 请求结果 123 | */ 124 | export const post = (url, data = {}) => { 125 | return apiRequest(url, { 126 | method: 'POST', 127 | data 128 | }) 129 | } 130 | 131 | /** 132 | * PUT请求 133 | * @param {string} url - 请求URL 134 | * @param {object} data - 请求数据 135 | * @returns {Promise} 请求结果 136 | */ 137 | export const put = (url, data = {}) => { 138 | return apiRequest(url, { 139 | method: 'PUT', 140 | data 141 | }) 142 | } 143 | 144 | /** 145 | * DELETE请求 146 | * @param {string} url - 请求URL 147 | * @param {object} params - 查询参数 148 | * @returns {Promise} 请求结果 149 | */ 150 | export const del = (url, params = {}) => { 151 | return apiRequest(url, { 152 | method: 'DELETE', 153 | params 154 | }) 155 | } 156 | 157 | export default request -------------------------------------------------------------------------------- /dashboard/src/components/CategoryModal.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 66 | 67 | -------------------------------------------------------------------------------- /dashboard/src/api/modules/parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 解析接口 3 | * 封装 /parse/:jx 相关的视频解析接口 4 | */ 5 | 6 | import { get, post } from '../request' 7 | import { API_PATHS } from '../config' 8 | 9 | /** 10 | * 构建解析接口URL 11 | * @param {string} jx - 解析器名称 12 | * @returns {string} 完整的解析URL 13 | */ 14 | const buildParseUrl = (jx) => { 15 | return `${API_PATHS.PARSE}/${jx}` 16 | } 17 | 18 | /** 19 | * 解析视频地址 20 | * @param {string} jx - 解析器名称 21 | * @param {object} params - 解析参数 22 | * @param {string} params.url - 需要解析的视频URL 23 | * @param {string} params.flag - 解析标识(可选) 24 | * @param {object} params.headers - 自定义请求头(可选) 25 | * @returns {Promise} 解析结果 26 | */ 27 | export const parseVideo = async (jx, params) => { 28 | const { url, flag, headers, ...otherParams } = params 29 | 30 | if (!url) { 31 | throw new Error('视频URL不能为空') 32 | } 33 | 34 | const requestParams = { 35 | url, 36 | ...otherParams 37 | } 38 | 39 | if (flag) { 40 | requestParams.flag = flag 41 | } 42 | 43 | const requestOptions = {} 44 | if (headers) { 45 | requestOptions.headers = { 46 | ...requestOptions.headers, 47 | ...headers 48 | } 49 | } 50 | 51 | return get(buildParseUrl(jx), requestParams, requestOptions) 52 | } 53 | 54 | /** 55 | * POST方式解析视频(用于复杂参数) 56 | * @param {string} jx - 解析器名称 57 | * @param {object} data - 解析数据 58 | * @param {string} data.url - 需要解析的视频URL 59 | * @param {string} data.flag - 解析标识(可选) 60 | * @param {object} data.headers - 自定义请求头(可选) 61 | * @returns {Promise} 解析结果 62 | */ 63 | export const parseVideoPost = async (jx, data) => { 64 | const { url, headers, ...requestData } = data 65 | 66 | if (!url) { 67 | throw new Error('视频URL不能为空') 68 | } 69 | 70 | const requestOptions = {} 71 | if (headers) { 72 | requestOptions.headers = { 73 | ...requestOptions.headers, 74 | ...headers 75 | } 76 | } 77 | 78 | return post(buildParseUrl(jx), requestData, requestOptions) 79 | } 80 | 81 | /** 82 | * 批量解析视频 83 | * @param {string} jx - 解析器名称 84 | * @param {Array} urls - 视频URL数组 85 | * @param {object} options - 解析选项 86 | * @param {string} options.flag - 解析标识(可选) 87 | * @param {object} options.headers - 自定义请求头(可选) 88 | * @returns {Promise} 批量解析结果 89 | */ 90 | export const parseVideoBatch = async (jx, urls, options = {}) => { 91 | if (!Array.isArray(urls) || urls.length === 0) { 92 | throw new Error('视频URL数组不能为空') 93 | } 94 | 95 | const { flag, headers } = options 96 | 97 | const requestData = { 98 | urls, 99 | batch: true 100 | } 101 | 102 | if (flag) { 103 | requestData.flag = flag 104 | } 105 | 106 | const requestOptions = {} 107 | if (headers) { 108 | requestOptions.headers = { 109 | ...requestOptions.headers, 110 | ...headers 111 | } 112 | } 113 | 114 | return post(buildParseUrl(jx), requestData, requestOptions) 115 | } 116 | 117 | /** 118 | * 获取解析器信息 119 | * @param {string} jx - 解析器名称 120 | * @returns {Promise} 解析器信息 121 | */ 122 | export const getParserInfo = async (jx) => { 123 | return get(buildParseUrl(jx), { info: true }) 124 | } 125 | 126 | /** 127 | * 测试解析器可用性 128 | * @param {string} jx - 解析器名称 129 | * @param {string} testUrl - 测试URL(可选) 130 | * @returns {Promise} 测试结果 131 | */ 132 | export const testParser = async (jx, testUrl) => { 133 | const params = { test: true } 134 | 135 | if (testUrl) { 136 | params.url = testUrl 137 | } 138 | 139 | return get(buildParseUrl(jx), params) 140 | } 141 | 142 | /** 143 | * 通用解析接口调用 144 | * @param {string} jx - 解析器名称 145 | * @param {object} params - 请求参数 146 | * @param {string} method - 请求方法 ('GET' | 'POST') 147 | * @returns {Promise} 解析响应 148 | */ 149 | export const callParseApi = async (jx, params = {}, method = 'GET') => { 150 | const url = buildParseUrl(jx) 151 | 152 | if (method.toUpperCase() === 'POST') { 153 | return post(url, params) 154 | } else { 155 | return get(url, params) 156 | } 157 | } 158 | 159 | // 默认导出所有解析接口 160 | export default { 161 | parseVideo, 162 | parseVideoPost, 163 | parseVideoBatch, 164 | getParserInfo, 165 | testParser, 166 | callParseApi 167 | } -------------------------------------------------------------------------------- /proxy/README.md: -------------------------------------------------------------------------------- 1 | # 代理服务器优化版本 2 | 3 | ## 🚀 优化内容 4 | 5 | ### 内存泄漏问题解决 6 | 7 | 1. **连接池管理** 8 | - 实现了全局HTTP客户端单例,避免重复创建 9 | - 配置连接池限制:最大100个连接,保持20个活跃连接 10 | - 设置连接过期时间:30秒自动清理空闲连接 11 | 12 | 2. **资源自动释放** 13 | - 自定义`ProxyStreamingResponse`确保流式响应正确关闭 14 | - 实现`stream_response_with_cleanup`自动清理响应资源 15 | - 应用生命周期管理,启动和关闭时正确初始化和清理资源 16 | 17 | 3. **内存监控系统** 18 | - 实时监控内存使用情况(每30秒检查一次) 19 | - 内存使用超过500MB时自动触发清理 20 | - 内存使用超过400MB时执行轻量级垃圾回收 21 | - 提供手动清理接口:`POST /admin/cleanup` 22 | 23 | 4. **超时和错误处理** 24 | - 连接超时:10秒 25 | - 读取超时:60秒 26 | - 写入超时:10秒 27 | - 连接池超时:5秒 28 | - 详细的错误分类和处理 29 | 30 | ## 📊 性能提升 31 | 32 | - **内存使用优化**:解决了无限内存增长问题 33 | - **连接复用**:减少连接建立开销 34 | - **自动清理**:防止资源泄漏 35 | - **监控告警**:实时掌握服务器状态 36 | 37 | ## 🛠️ 安装和使用 38 | 39 | ### 1. 安装依赖 40 | 41 | ```bash 42 | cd proxy 43 | pip install -r requirements.txt 44 | ``` 45 | 46 | ### 2. 启动服务器 47 | 48 | #### 方式一:直接启动 49 | ```bash 50 | python proxy.py 51 | ``` 52 | 53 | #### 方式二:使用启动脚本(推荐) 54 | ```bash 55 | python start_proxy.py --host 0.0.0.0 --port 8000 56 | ``` 57 | 58 | #### 方式三:使用环境变量配置 59 | ```bash 60 | export PROXY_HOST=0.0.0.0 61 | export PROXY_PORT=8000 62 | export PROXY_MAX_MEMORY_USAGE=300 63 | python proxy.py 64 | ``` 65 | 66 | ### 3. 健康检查 67 | 68 | 访问 `http://localhost:8000/health` 查看服务器状态和内存使用情况。 69 | 70 | ### 4. 手动清理内存 71 | 72 | ```bash 73 | curl -X POST http://localhost:8000/admin/cleanup 74 | ``` 75 | 76 | ## ⚙️ 配置选项 77 | 78 | ### 环境变量配置 79 | 80 | | 变量名 | 默认值 | 说明 | 81 | |--------|--------|------| 82 | | `PROXY_HOST` | 0.0.0.0 | 绑定主机地址 | 83 | | `PROXY_PORT` | 8000 | 绑定端口 | 84 | | `PROXY_MAX_CONNECTIONS` | 100 | 最大连接数 | 85 | | `PROXY_MAX_KEEPALIVE_CONNECTIONS` | 20 | 最大保持连接数 | 86 | | `PROXY_KEEPALIVE_EXPIRY` | 30.0 | 连接保持时间(秒) | 87 | | `PROXY_CONNECT_TIMEOUT` | 10.0 | 连接超时(秒) | 88 | | `PROXY_READ_TIMEOUT` | 60.0 | 读取超时(秒) | 89 | | `PROXY_WRITE_TIMEOUT` | 10.0 | 写入超时(秒) | 90 | | `PROXY_MEMORY_CHECK_INTERVAL` | 30 | 内存检查间隔(秒) | 91 | | `PROXY_MAX_MEMORY_USAGE` | 500 | 最大内存使用(MB) | 92 | | `PROXY_CLEANUP_THRESHOLD` | 400 | 清理阈值(MB) | 93 | 94 | ### 配置文件 95 | 96 | 创建 `proxy_config.json` 文件: 97 | 98 | ```json 99 | { 100 | "host": "0.0.0.0", 101 | "port": 8000, 102 | "max_connections": 100, 103 | "max_keepalive_connections": 20, 104 | "keepalive_expiry": 30.0, 105 | "connect_timeout": 10.0, 106 | "read_timeout": 60.0, 107 | "write_timeout": 10.0, 108 | "memory_check_interval": 30, 109 | "max_memory_usage": 500, 110 | "cleanup_threshold": 400 111 | } 112 | ``` 113 | 114 | ## 📈 监控和日志 115 | 116 | ### 日志文件 117 | 118 | 服务器运行日志保存在 `proxy.log` 文件中,包含: 119 | - 请求日志 120 | - 内存使用情况 121 | - 错误信息 122 | - 清理操作记录 123 | 124 | ### 监控接口 125 | 126 | - `GET /health` - 健康检查和内存使用情况 127 | - `POST /admin/cleanup` - 手动触发内存清理 128 | 129 | ### 示例监控脚本 130 | 131 | ```bash 132 | #!/bin/bash 133 | # monitor.sh - 监控脚本示例 134 | 135 | while true; do 136 | response=$(curl -s http://localhost:8000/health) 137 | memory=$(echo $response | jq -r '.memory_usage_mb') 138 | 139 | echo "$(date): 内存使用 ${memory}MB" 140 | 141 | if (( $(echo "$memory > 400" | bc -l) )); then 142 | echo "内存使用过高,触发清理..." 143 | curl -X POST http://localhost:8000/admin/cleanup 144 | fi 145 | 146 | sleep 60 147 | done 148 | ``` 149 | 150 | ## 🔧 故障排除 151 | 152 | ### 常见问题 153 | 154 | 1. **内存使用仍然很高** 155 | - 检查 `PROXY_MAX_MEMORY_USAGE` 和 `PROXY_CLEANUP_THRESHOLD` 设置 156 | - 查看日志文件确认清理操作是否正常执行 157 | - 考虑降低 `PROXY_MAX_CONNECTIONS` 值 158 | 159 | 2. **连接超时** 160 | - 调整 `PROXY_CONNECT_TIMEOUT` 和 `PROXY_READ_TIMEOUT` 161 | - 检查目标服务器的响应时间 162 | 163 | 3. **服务器无响应** 164 | - 检查端口是否被占用 165 | - 查看日志文件中的错误信息 166 | - 尝试重启服务器 167 | 168 | ### 性能调优建议 169 | 170 | 1. **根据服务器配置调整连接数**: 171 | - 低配置服务器:`MAX_CONNECTIONS=50` 172 | - 高配置服务器:`MAX_CONNECTIONS=200` 173 | 174 | 2. **根据网络环境调整超时**: 175 | - 内网环境:较短的超时时间 176 | - 外网环境:较长的超时时间 177 | 178 | 3. **根据内存大小调整阈值**: 179 | - 小内存服务器:降低 `MAX_MEMORY_USAGE` 180 | - 大内存服务器:可以适当提高阈值 181 | 182 | ## 📝 更新日志 183 | 184 | ### v2.0.0 (当前版本) 185 | - ✅ 解决内存泄漏问题 186 | - ✅ 添加连接池管理 187 | - ✅ 实现内存监控系统 188 | - ✅ 优化资源释放机制 189 | - ✅ 添加健康检查接口 190 | - ✅ 完善错误处理 191 | - ✅ 添加配置管理 192 | 193 | ### v1.0.0 (原始版本) 194 | - ❌ 存在内存泄漏问题 195 | - ❌ 缺乏资源管理 196 | - ❌ 无监控机制 -------------------------------------------------------------------------------- /dashboard/docs/NGINX_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Vue SPA Nginx部署指南 2 | 3 | 本文档说明如何解决Vue单页应用(SPA)在静态部署时的404问题,并提供完整的Nginx配置方案。 4 | 5 | ## 问题原因 6 | 7 | Vue Router使用`createWebHistory`模式时,路由是通过浏览器的History API实现的。当用户直接访问或刷新非根路径页面时(如`/settings`),服务器会尝试查找对应的物理文件,但这些文件并不存在,因此返回404错误。 8 | 9 | ## 解决方案 10 | 11 | 通过Nginx配置`try_files`指令,将所有不匹配静态文件的请求重定向到`index.html`,让Vue Router接管路由处理。 12 | 13 | ## 部署方式 14 | 15 | ### 方式一:根目录部署 16 | 17 | 适用于将DrPlayer部署在域名根目录的情况,如:`https://example.com/` 18 | 19 | #### 1. 构建应用 20 | ```bash 21 | # 使用根目录打包脚本 22 | pnpm build:root 23 | ``` 24 | 25 | #### 2. 部署文件 26 | 将`dist`目录中的所有文件复制到服务器的网站根目录: 27 | ```bash 28 | # 示例路径 29 | /var/www/html/drplayer/ 30 | ``` 31 | 32 | #### 3. Nginx配置 33 | 使用提供的`nginx-root.conf`配置文件: 34 | ```bash 35 | # 复制配置文件 36 | sudo cp nginx-root.conf /etc/nginx/sites-available/drplayer 37 | sudo ln -s /etc/nginx/sites-available/drplayer /etc/nginx/sites-enabled/ 38 | 39 | # 或者直接编辑默认配置 40 | sudo nano /etc/nginx/sites-available/default 41 | ``` 42 | 43 | ### 方式二:子目录部署 44 | 45 | 适用于将DrPlayer部署在域名子目录的情况,如:`https://example.com/apps/drplayer/` 46 | 47 | #### 1. 构建应用 48 | ```bash 49 | # 使用子目录打包脚本 50 | pnpm build:apps 51 | ``` 52 | 53 | #### 2. 部署文件 54 | 将`dist`目录中的所有文件复制到服务器的指定子目录: 55 | ```bash 56 | # 示例路径 57 | /var/www/html/drplayer/ 58 | ``` 59 | 60 | #### 3. Nginx配置 61 | 使用提供的`nginx-subdir.conf`配置文件: 62 | ```bash 63 | # 复制配置文件 64 | sudo cp nginx-subdir.conf /etc/nginx/sites-available/drplayer-subdir 65 | sudo ln -s /etc/nginx/sites-available/drplayer-subdir /etc/nginx/sites-enabled/ 66 | ``` 67 | 68 | ## 配置文件说明 69 | 70 | ### 核心配置项 71 | 72 | 1. **try_files指令** 73 | ```nginx 74 | try_files $uri $uri/ /index.html; 75 | ``` 76 | 这是解决SPA路由404问题的关键配置。 77 | 78 | 2. **静态资源缓存** 79 | ```nginx 80 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 81 | expires 1y; 82 | add_header Cache-Control "public, immutable"; 83 | } 84 | ``` 85 | 86 | 3. **Gzip压缩** 87 | ```nginx 88 | gzip on; 89 | gzip_types text/plain text/css application/json application/javascript; 90 | ``` 91 | 92 | 4. **安全头设置** 93 | ```nginx 94 | add_header X-Frame-Options "SAMEORIGIN" always; 95 | add_header X-Content-Type-Options "nosniff" always; 96 | ``` 97 | 98 | ### 路径配置 99 | 100 | - **根目录部署**:修改`root`指令指向您的实际部署路径 101 | - **子目录部署**:修改`alias`指令指向您的实际部署路径 102 | 103 | ## 部署步骤 104 | 105 | ### 1. 准备服务器环境 106 | ```bash 107 | # 安装Nginx(Ubuntu/Debian) 108 | sudo apt update 109 | sudo apt install nginx 110 | 111 | # 安装Nginx(CentOS/RHEL) 112 | sudo yum install nginx 113 | # 或者 114 | sudo dnf install nginx 115 | ``` 116 | 117 | ### 2. 配置Nginx 118 | ```bash 119 | # 备份原配置 120 | sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.backup 121 | 122 | # 应用新配置 123 | sudo cp nginx-root.conf /etc/nginx/sites-available/drplayer 124 | sudo ln -s /etc/nginx/sites-available/drplayer /etc/nginx/sites-enabled/ 125 | 126 | # 测试配置 127 | sudo nginx -t 128 | 129 | # 重启Nginx 130 | sudo systemctl restart nginx 131 | ``` 132 | 133 | ### 3. 部署应用文件 134 | ```bash 135 | # 创建部署目录 136 | sudo mkdir -p /var/www/html/drplayer 137 | 138 | # 复制构建文件 139 | sudo cp -r dist/* /var/www/html/drplayer/ 140 | 141 | # 设置权限 142 | sudo chown -R www-data:www-data /var/www/html/drplayer 143 | sudo chmod -R 755 /var/www/html/drplayer 144 | ``` 145 | 146 | ### 4. 验证部署 147 | 1. 访问首页:`http://your-domain/` 148 | 2. 直接访问子页面:`http://your-domain/settings` 149 | 3. 刷新页面确认不会出现404错误 150 | 151 | ## 常见问题 152 | 153 | ### 1. 刷新页面仍然404 154 | - 检查Nginx配置中的`try_files`指令是否正确 155 | - 确认`root`或`alias`路径是否正确 156 | - 检查文件权限是否正确 157 | 158 | ### 2. 静态资源加载失败 159 | - 检查`base`路径配置是否与部署路径匹配 160 | - 确认静态资源文件是否存在 161 | - 检查Nginx静态文件配置 162 | 163 | ### 3. API请求失败 164 | - 配置API代理(如果后端API在不同端口) 165 | - 检查CORS设置 166 | - 确认API路径配置 167 | 168 | ## 性能优化建议 169 | 170 | 1. **启用Gzip压缩**:减少传输文件大小 171 | 2. **设置静态资源缓存**:提高加载速度 172 | 3. **使用CDN**:加速静态资源访问 173 | 4. **启用HTTP/2**:提高传输效率 174 | 5. **配置SSL证书**:启用HTTPS 175 | 176 | ## 安全建议 177 | 178 | 1. **隐藏Nginx版本信息** 179 | 2. **设置安全响应头** 180 | 3. **禁止访问敏感文件** 181 | 4. **配置防火墙规则** 182 | 5. **定期更新系统和软件** 183 | 184 | ## 监控和日志 185 | 186 | ```nginx 187 | # 访问日志 188 | access_log /var/log/nginx/drplayer_access.log; 189 | 190 | # 错误日志 191 | error_log /var/log/nginx/drplayer_error.log; 192 | ``` 193 | 194 | 通过以上配置,您的Vue SPA应用将能够正确处理所有路由,解决刷新页面404的问题。 -------------------------------------------------------------------------------- /dashboard/docs/UPX_COMPRESSION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # UPX压缩优化指南 2 | 3 | ## 概述 4 | 5 | UPX (Ultimate Packer for eXecutables) 是一个高性能的可执行文件压缩工具,可以将pkg生成的二进制文件体积减少50%-70%。 6 | 7 | ## 当前优化效果 8 | 9 | - **原始pkg打包**: ~45.70 MB 10 | - **pkg优化选项**: 预计减少10-20% 11 | - **UPX压缩**: 预计额外减少50-70% 12 | - **最终预期大小**: ~10-20 MB 13 | 14 | ## UPX安装 15 | 16 | ### Windows 17 | 18 | 1. **下载UPX** 19 | - 访问 https://upx.github.io/ 20 | - 下载最新版本的Windows版本 21 | - 解压到任意目录(如 `C:\tools\upx`) 22 | 23 | 2. **添加到PATH环境变量** 24 | ```cmd 25 | # 方法1: 通过系统设置 26 | # 控制面板 > 系统 > 高级系统设置 > 环境变量 27 | # 在PATH中添加UPX解压目录 28 | 29 | # 方法2: 通过PowerShell临时添加 30 | $env:PATH += ";C:\tools\upx" 31 | ``` 32 | 33 | 3. **验证安装** 34 | ```cmd 35 | upx --version 36 | ``` 37 | 38 | ### 使用Chocolatey安装(推荐) 39 | 40 | ```powershell 41 | # 安装Chocolatey(如果未安装) 42 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) 43 | 44 | # 安装UPX 45 | choco install upx 46 | ``` 47 | 48 | ### 使用Scoop安装 49 | 50 | ```powershell 51 | # 安装Scoop(如果未安装) 52 | iwr -useb get.scoop.sh | iex 53 | 54 | # 安装UPX 55 | scoop install upx 56 | ``` 57 | 58 | ## 使用方法 59 | 60 | ### 1. 基础优化构建 61 | 62 | ```bash 63 | # 使用pkg内置优化选项 64 | pnpm build:binary:optimized 65 | ``` 66 | 67 | ### 2. 手动UPX压缩 68 | 69 | ```bash 70 | # 先进行常规构建 71 | pnpm build:binary 72 | 73 | # 然后手动压缩 74 | upx --best dist-binary/drplayer-server-win.exe 75 | ``` 76 | 77 | ### 3. 不同压缩级别 78 | 79 | ```bash 80 | # 快速压缩(压缩率较低,速度快) 81 | upx --fast dist-binary/drplayer-server-win.exe 82 | 83 | # 更好的压缩(平衡压缩率和速度) 84 | upx --better dist-binary/drplayer-server-win.exe 85 | 86 | # 最佳压缩(压缩率最高,速度慢) 87 | upx --best dist-binary/drplayer-server-win.exe 88 | 89 | # 极限压缩(尝试所有方法,非常慢) 90 | upx --best --ultra-brute dist-binary/drplayer-server-win.exe 91 | ``` 92 | 93 | ## PKG优化选项说明 94 | 95 | ### 1. 压缩选项 96 | - `--compress Brotli`: 使用Brotli算法压缩文件存储 97 | - `--compress GZip`: 使用GZip算法压缩(兼容性更好) 98 | 99 | ### 2. 公共包选项 100 | - `--public-packages "*"`: 将所有包标记为公共,减少重复代码 101 | - `--public`: 加速打包并公开顶级项目源码 102 | 103 | ### 3. 字节码选项 104 | - `--no-bytecode`: 跳过字节码生成,包含源文件作为纯JS 105 | - 优点:减少体积,加快打包速度 106 | - 缺点:源码可见(如果这是问题的话) 107 | 108 | ### 4. Node.js选项 109 | - `--options "max-old-space-size=512"`: 限制内存使用 110 | - `--options "expose-gc"`: 暴露垃圾回收接口 111 | 112 | ## 性能影响 113 | 114 | ### UPX压缩的影响 115 | - **启动时间**: 增加200-400ms(解压缩时间) 116 | - **运行时性能**: 无影响(解压后在内存中运行) 117 | - **内存使用**: 无额外开销 118 | 119 | ### 优化建议 120 | 1. **开发环境**: 使用常规构建(快速) 121 | 2. **测试环境**: 使用pkg优化选项 122 | 3. **生产环境**: 使用完整优化(pkg + UPX) 123 | 124 | ## 故障排除 125 | 126 | ### UPX压缩失败 127 | ```bash 128 | # 如果UPX压缩失败,可能的原因: 129 | # 1. 文件被杀毒软件锁定 130 | # 2. 文件正在使用中 131 | # 3. 文件格式不支持 132 | 133 | # 解决方案: 134 | # 1. 临时关闭杀毒软件 135 | # 2. 确保没有运行该程序 136 | # 3. 使用 --force 选项强制压缩 137 | upx --best --force dist-binary/drplayer-server-win.exe 138 | ``` 139 | 140 | ### 压缩后程序无法运行 141 | ```bash 142 | # 如果压缩后程序无法运行: 143 | # 1. 尝试解压缩 144 | upx -d dist-binary/drplayer-server-win.exe 145 | 146 | # 2. 使用较低的压缩级别 147 | upx --fast dist-binary/drplayer-server-win.exe 148 | 149 | # 3. 检查是否与杀毒软件冲突 150 | ``` 151 | 152 | ## 自动化脚本配置 153 | 154 | 在 `build-binary-optimized.js` 中可以调整以下配置: 155 | 156 | ```javascript 157 | const config = { 158 | pkg: { 159 | compress: 'Brotli', // 'Brotli' | 'GZip' | null 160 | publicPackages: '*', // '*' | 'package1,package2' | null 161 | noBytecode: true, // true | false 162 | options: 'max-old-space-size=512' // Node.js选项 163 | }, 164 | upx: { 165 | enabled: true, // 是否启用UPX压缩 166 | level: 'best', // 'fast' | 'better' | 'best' 167 | backup: true // 是否保留原始文件备份 168 | } 169 | }; 170 | ``` 171 | 172 | ## 最佳实践 173 | 174 | 1. **渐进式优化**: 先应用pkg优化,再考虑UPX 175 | 2. **测试兼容性**: 在目标环境中测试压缩后的程序 176 | 3. **备份原文件**: 始终保留未压缩的版本作为备份 177 | 4. **监控性能**: 测量启动时间影响是否可接受 178 | 5. **CI/CD集成**: 在构建流水线中自动化压缩过程 179 | 180 | ## 预期效果 181 | 182 | | 优化方案 | 文件大小 | 压缩率 | 启动时间影响 | 183 | |---------|---------|--------|-------------| 184 | | 原始pkg | 45.70 MB | - | 基准 | 185 | | pkg优化 | ~36-40 MB | 10-20% | 无 | 186 | | pkg+UPX | ~10-20 MB | 50-70% | +200-400ms | 187 | 188 | 通过这些优化,你的二进制文件大小可以从45MB+减少到10-20MB,大幅降低分发成本和下载时间。 -------------------------------------------------------------------------------- /dashboard/docs/FASTIFY_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Vue SPA + Fastify 部署指南 2 | 3 | ## 问题说明 4 | 5 | Vue单页应用使用`createWebHistory`路由模式时,刷新页面会出现404错误。这是因为: 6 | 7 | 1. 用户访问 `/apps/drplayer/settings` 8 | 2. 浏览器向服务器请求 `/apps/drplayer/settings` 文件 9 | 3. 服务器找不到该文件,返回404 10 | 4. 需要让服务器将所有SPA路由请求都返回到 `index.html` 11 | 12 | ## 解决方案 13 | 14 | 通过Fastify提供静态文件服务 + SPA路由回退机制,完全可以解决刷新404问题。 15 | 16 | ## 配置步骤 17 | 18 | ### 1. 构建Vue应用 19 | 20 | ```bash 21 | # 方法1:使用构建脚本(推荐) 22 | node build-for-fastify.js 23 | 24 | # 方法2:手动设置环境变量 25 | set VITE_BASE_PATH=/apps/drplayer/ 26 | pnpm build 27 | ``` 28 | 29 | ### 2. 复制构建文件 30 | 31 | 将 `dist/` 目录的所有内容复制到您的后端 `apps/drplayer/` 目录: 32 | 33 | ``` 34 | your-backend/ 35 | ├── apps/ 36 | │ └── drplayer/ 37 | │ ├── index.html 38 | │ ├── assets/ 39 | │ │ ├── index-xxx.js 40 | │ │ └── index-xxx.css 41 | │ └── ... 42 | └── server.js 43 | ``` 44 | 45 | ### 3. 配置Fastify服务器 46 | 47 | #### 方法1:使用提供的路由模块(推荐) 48 | 49 | ```javascript 50 | import { addSPARoutes } from './fastify-spa-routes.js'; 51 | import fastifyStatic from '@fastify/static'; 52 | 53 | // 您现有的静态文件配置 54 | await fastify.register(fastifyStatic, { 55 | root: options.appsDir, 56 | prefix: '/apps/', 57 | decorateReply: false, 58 | }); 59 | 60 | // 添加SPA路由支持 61 | await fastify.register(addSPARoutes, { 62 | appsDir: options.appsDir 63 | }); 64 | ``` 65 | 66 | #### 方法2:直接在现有代码中添加 67 | 68 | ```javascript 69 | import path from 'path'; 70 | import fs from 'fs'; 71 | 72 | // 在您现有的Fastify应用中添加这些路由 73 | fastify.get('/apps/drplayer/*', async (request, reply) => { 74 | const requestedPath = request.params['*']; 75 | const fullPath = path.join(options.appsDir, 'drplayer', requestedPath); 76 | 77 | try { 78 | await fs.promises.access(fullPath); 79 | return reply.callNotFound(); // 让静态文件服务处理 80 | } catch (error) { 81 | // 返回index.html让Vue Router处理 82 | const indexPath = path.join(options.appsDir, 'drplayer', 'index.html'); 83 | const indexContent = await fs.promises.readFile(indexPath, 'utf8'); 84 | return reply 85 | .type('text/html') 86 | .header('Cache-Control', 'no-cache, no-store, must-revalidate') 87 | .send(indexContent); 88 | } 89 | }); 90 | 91 | // 处理根路径 92 | fastify.get('/apps/drplayer', async (request, reply) => { 93 | return reply.redirect(301, '/apps/drplayer/'); 94 | }); 95 | 96 | fastify.get('/apps/drplayer/', async (request, reply) => { 97 | const indexPath = path.join(options.appsDir, 'drplayer', 'index.html'); 98 | const indexContent = await fs.promises.readFile(indexPath, 'utf8'); 99 | return reply 100 | .type('text/html') 101 | .header('Cache-Control', 'no-cache, no-store, must-revalidate') 102 | .send(indexContent); 103 | }); 104 | ``` 105 | 106 | ## 工作原理 107 | 108 | 1. **静态文件服务**:`@fastify/static` 处理所有存在的静态文件(JS、CSS、图片等) 109 | 2. **路由回退**:当请求的文件不存在时,返回 `index.html` 110 | 3. **Vue Router接管**:`index.html` 加载后,Vue Router根据URL显示对应组件 111 | 112 | ## 路由优先级 113 | 114 | ``` 115 | 请求: /apps/drplayer/assets/index-xxx.js 116 | ↓ 117 | 静态文件存在 → 直接返回文件 118 | 119 | 请求: /apps/drplayer/settings 120 | ↓ 121 | 静态文件不存在 → 返回 index.html → Vue Router处理 122 | ``` 123 | 124 | ## 缓存策略 125 | 126 | - **HTML文件**:不缓存(`no-cache`),确保路由更新 127 | - **静态资源**:长期缓存(1年),提高性能 128 | 129 | ## 测试验证 130 | 131 | 1. 启动Fastify服务器 132 | 2. 访问 `http://localhost:5757/apps/drplayer/` 133 | 3. 导航到不同页面(如设置页面) 134 | 4. 刷新页面,确认不出现404错误 135 | 5. 检查浏览器开发者工具,确认静态资源正常加载 136 | 137 | ## 常见问题 138 | 139 | ### Q: 刷新后页面空白? 140 | A: 检查 `VITE_BASE_PATH` 是否正确设置为 `/apps/drplayer/` 141 | 142 | ### Q: 静态资源404? 143 | A: 确认构建时的base路径与Fastify的prefix匹配 144 | 145 | ### Q: API请求失败? 146 | A: 确保API路由在SPA路由之前注册,避免被SPA回退拦截 147 | 148 | ## 性能优化 149 | 150 | 1. **启用Gzip压缩**: 151 | ```javascript 152 | import fastifyCompress from '@fastify/compress'; 153 | await fastify.register(fastifyCompress); 154 | ``` 155 | 156 | 2. **设置静态资源缓存**: 157 | ```javascript 158 | await fastify.register(fastifyStatic, { 159 | // ... 其他配置 160 | setHeaders: (res, path) => { 161 | if (path.endsWith('.html')) { 162 | res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); 163 | } else if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) { 164 | res.setHeader('Cache-Control', 'public, max-age=31536000'); 165 | } 166 | } 167 | }); 168 | ``` 169 | 170 | ## 总结 171 | 172 | ✅ **可以解决404问题**:通过Fastify的路由回退机制 173 | ✅ **性能良好**:静态文件直接服务,只有路由请求才回退 174 | ✅ **配置简单**:只需添加几个路由处理器 175 | ✅ **开发友好**:支持热重载和开发服务器 -------------------------------------------------------------------------------- /dashboard/docs/BUILD_BINARY_GUIDE.md: -------------------------------------------------------------------------------- 1 | # DrPlayer 二进制打包指南 2 | 3 | ## 概述 4 | 5 | 本指南介绍如何将 DrPlayer Dashboard 打包为跨平台的独立可执行文件。 6 | 7 | ## 主要改进 8 | 9 | ### 1. 端口配置优化 10 | - **默认端口**: 从 8008 改为 9978 11 | - **智能端口检测**: 如果端口被占用,自动尝试下一个端口 (9979, 9980, ...) 12 | - **最大尝试次数**: 100个端口,确保能找到可用端口 13 | 14 | ### 2. PKG 兼容性优化 15 | - **环境检测**: 自动检测是否在 PKG 环境中运行 16 | - **路径处理**: PKG 环境中使用 `process.cwd()` 而非 `__dirname` 17 | - **构建跳过**: PKG 环境中跳过前端构建步骤 18 | - **错误处理**: PKG 环境中更宽松的错误处理 19 | 20 | ## 打包方法 21 | 22 | ### 方法一:使用自动化脚本 (推荐) 23 | 24 | #### Windows PowerShell 25 | ```powershell 26 | # 运行 PowerShell 脚本 27 | pnpm run build:binary:win 28 | 29 | # 或直接运行 30 | powershell -ExecutionPolicy Bypass -File build-binary.ps1 31 | ``` 32 | 33 | #### Node.js 脚本 (跨平台) 34 | ```bash 35 | # 运行 Node.js 脚本 36 | pnpm run build:binary 37 | 38 | # 或直接运行 39 | node build-binary.js 40 | ``` 41 | 42 | ### 方法二:手动打包 43 | 44 | #### 1. 安装 PKG 45 | ```bash 46 | npm install -g pkg 47 | ``` 48 | 49 | #### 2. 构建前端资源 50 | ```bash 51 | pnpm build:fastify 52 | ``` 53 | 54 | #### 3. 打包指定平台 55 | ```bash 56 | # Windows x64 57 | pnpm run pkg:win 58 | 59 | # Linux x64 60 | pnpm run pkg:linux 61 | 62 | # macOS x64 63 | pnpm run pkg:macos 64 | 65 | # 所有平台 66 | pnpm run pkg:all 67 | ``` 68 | 69 | ### 方法三:自定义打包 70 | ```bash 71 | # 基本命令 72 | pkg production-server.js --target node18-win-x64 --output drplayer-server.exe 73 | 74 | # 带压缩 75 | pkg production-server.js --target node18-win-x64 --output drplayer-server.exe --compress Brotli 76 | 77 | # 多平台 78 | pkg production-server.js --targets node18-win-x64,node18-linux-x64,node18-macos-x64 79 | ``` 80 | 81 | ## 支持的平台 82 | 83 | | 平台 | 架构 | 输出文件名 | 84 | |------|------|------------| 85 | | Windows | x64 | drplayer-server-win-x64.exe | 86 | | Linux | x64 | drplayer-server-linux-x64 | 87 | | macOS | x64 | drplayer-server-macos-x64 | 88 | | macOS | ARM64 | drplayer-server-macos-arm64 | 89 | 90 | ## 输出文件结构 91 | 92 | ``` 93 | dist-binary/ 94 | ├── drplayer-server-win-x64.exe # Windows 可执行文件 95 | ├── drplayer-server-linux-x64 # Linux 可执行文件 96 | ├── drplayer-server-macos-x64 # macOS Intel 可执行文件 97 | ├── drplayer-server-macos-arm64 # macOS Apple Silicon 可执行文件 98 | ├── start-windows.bat # Windows 启动脚本 99 | ├── start-linux.sh # Linux 启动脚本 100 | ├── start-macos.sh # macOS 启动脚本 101 | └── README.md # 使用说明 102 | ``` 103 | 104 | ## 使用方法 105 | 106 | ### Windows 107 | ```cmd 108 | # 方法1: 双击运行 109 | start-windows.bat 110 | 111 | # 方法2: 直接运行 112 | drplayer-server-win-x64.exe 113 | ``` 114 | 115 | ### Linux 116 | ```bash 117 | # 添加执行权限 118 | chmod +x drplayer-server-linux-x64 119 | chmod +x start-linux.sh 120 | 121 | # 运行 122 | ./start-linux.sh 123 | # 或 124 | ./drplayer-server-linux-x64 125 | ``` 126 | 127 | ### macOS 128 | ```bash 129 | # 添加执行权限 130 | chmod +x drplayer-server-macos-x64 131 | chmod +x start-macos.sh 132 | 133 | # 运行 134 | ./start-macos.sh 135 | # 或 136 | ./drplayer-server-macos-x64 137 | ``` 138 | 139 | ## 访问地址 140 | 141 | 服务器启动后,访问地址为: 142 | - **主页**: http://localhost:9978/ 143 | - **应用**: http://localhost:9978/apps/drplayer/ 144 | - **健康检查**: http://localhost:9978/health 145 | 146 | 如果端口 9978 被占用,服务器会自动尝试下一个可用端口。 147 | 148 | ## 注意事项 149 | 150 | ### 1. 文件依赖 151 | - 二进制文件包含了所有 Node.js 依赖 152 | - 静态文件(HTML、CSS、JS)需要在运行时存在 153 | - 首次运行会在当前目录创建 `apps` 文件夹 154 | 155 | ### 2. 权限要求 156 | - Linux/macOS 需要执行权限 157 | - Windows 可能需要管理员权限(取决于安装位置) 158 | 159 | ### 3. 防火墙设置 160 | - 确保防火墙允许对应端口的访问 161 | - 默认绑定到 `0.0.0.0`,支持外部访问 162 | 163 | ### 4. 性能优化 164 | - 使用 Brotli 压缩减小文件大小 165 | - 二进制文件启动速度比 Node.js 脚本稍慢 166 | 167 | ## 故障排除 168 | 169 | ### 1. 端口占用 170 | ``` 171 | 🔍 正在查找可用端口,起始端口: 9978 172 | 端口 9978 已被占用,尝试下一个端口... 173 | 端口 9979 已被占用,尝试下一个端口... 174 | ✅ 找到可用端口: 9980 175 | ``` 176 | 177 | ### 2. 文件缺失 178 | ``` 179 | ⚠️ dist目录不存在,跳过文件复制 180 | 📦 pkg环境中,请确保静态文件已正确打包 181 | ``` 182 | 183 | ### 3. 构建失败 184 | ``` 185 | ⚠️ 构建命令执行失败,可能是在打包环境中运行 186 | 📦 跳过构建步骤,使用预构建的文件 187 | ``` 188 | 189 | ## 开发建议 190 | 191 | ### 1. 预构建资源 192 | 在打包前确保运行: 193 | ```bash 194 | pnpm build:fastify 195 | ``` 196 | 197 | ### 2. 测试环境 198 | 在不同平台测试二进制文件: 199 | ```bash 200 | # 测试启动 201 | ./drplayer-server-linux-x64 202 | 203 | # 测试健康检查 204 | curl http://localhost:9978/health 205 | ``` 206 | 207 | ### 3. 自动化部署 208 | 可以将打包脚本集成到 CI/CD 流程中: 209 | ```yaml 210 | # GitHub Actions 示例 211 | - name: Build Binary 212 | run: | 213 | npm install -g pkg 214 | pnpm build:fastify 215 | pnpm run pkg:all 216 | ``` 217 | 218 | ## 版本信息 219 | 220 | - **Node.js 版本**: 18 221 | - **PKG 目标**: node18-* 222 | - **压缩算法**: Brotli 223 | - **默认端口**: 9978 224 | 225 | ## 更新日志 226 | 227 | ### v1.0.0 228 | - 初始版本 229 | - 支持跨平台打包 230 | - 智能端口检测 231 | - PKG 环境优化 -------------------------------------------------------------------------------- /dashboard/docs/DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # DrPlayer Dashboard 部署指南 2 | 3 | 本文档提供了 DrPlayer Dashboard 的完整部署指南,解决了子目录部署和SPA路由刷新的问题。 4 | 5 | ## 构建配置 6 | 7 | ### 1. 环境变量配置 8 | 9 | 在项目根目录创建 `.env.production` 文件: 10 | 11 | ```bash 12 | # 生产环境配置 13 | NODE_ENV=production 14 | 15 | # 如果部署到子目录,设置基础路径 16 | # 例如:部署到 /apps/ 目录下 17 | VITE_BASE_PATH=/apps/ 18 | 19 | # 如果部署到根目录,使用相对路径 20 | # VITE_BASE_PATH=./ 21 | ``` 22 | 23 | ### 2. 构建命令 24 | 25 | ```bash 26 | # 安装依赖 27 | npm install 28 | 29 | # 构建生产版本 30 | npm run build 31 | 32 | # 构建后的文件在 dist 目录中 33 | ``` 34 | 35 | ## 部署配置 36 | 37 | ### Nginx 配置 38 | 39 | #### 1. 根目录部署 40 | 41 | 如果部署到网站根目录(如 `https://example.com/`): 42 | 43 | ```nginx 44 | server { 45 | listen 80; 46 | server_name example.com; 47 | root /var/www/drplayer/dist; 48 | index index.html; 49 | 50 | # 处理静态资源 51 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 52 | expires 1y; 53 | add_header Cache-Control "public, immutable"; 54 | try_files $uri =404; 55 | } 56 | 57 | # 处理 SPA 路由 58 | location / { 59 | try_files $uri $uri/ /index.html; 60 | } 61 | 62 | # 安全配置 63 | location ~ /\. { 64 | deny all; 65 | } 66 | } 67 | ``` 68 | 69 | #### 2. 子目录部署 70 | 71 | 如果部署到子目录(如 `https://example.com/apps/`): 72 | 73 | ```nginx 74 | server { 75 | listen 80; 76 | server_name example.com; 77 | 78 | # 子目录配置 79 | location /apps/ { 80 | alias /var/www/drplayer/dist/; 81 | index index.html; 82 | 83 | # 处理静态资源 84 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 85 | expires 1y; 86 | add_header Cache-Control "public, immutable"; 87 | try_files $uri =404; 88 | } 89 | 90 | # 处理 SPA 路由 - 关键配置 91 | try_files $uri $uri/ /apps/index.html; 92 | } 93 | 94 | # 安全配置 95 | location ~ /\. { 96 | deny all; 97 | } 98 | } 99 | ``` 100 | 101 | #### 3. 使用 location 块的高级配置 102 | 103 | ```nginx 104 | server { 105 | listen 80; 106 | server_name example.com; 107 | 108 | location /apps { 109 | alias /var/www/drplayer/dist; 110 | 111 | # 重写规则处理子目录 112 | location ~ ^/apps/(.*)$ { 113 | try_files /$1 /$1/ /index.html; 114 | } 115 | 116 | # 静态资源缓存 117 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 118 | expires 1y; 119 | add_header Cache-Control "public, immutable"; 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | ### Apache 配置 126 | 127 | 如果使用 Apache 服务器,在 `dist` 目录创建 `.htaccess` 文件: 128 | 129 | ```apache 130 | # 根目录部署 131 | RewriteEngine On 132 | RewriteBase / 133 | 134 | # 处理静态资源 135 | RewriteCond %{REQUEST_FILENAME} !-f 136 | RewriteCond %{REQUEST_FILENAME} !-d 137 | RewriteRule . /index.html [L] 138 | 139 | # 子目录部署(例如 /apps/) 140 | # RewriteEngine On 141 | # RewriteBase /apps/ 142 | # RewriteCond %{REQUEST_FILENAME} !-f 143 | # RewriteCond %{REQUEST_FILENAME} !-d 144 | # RewriteRule . /apps/index.html [L] 145 | ``` 146 | 147 | ## 部署步骤 148 | 149 | ### 1. 准备构建 150 | 151 | ```bash 152 | # 1. 设置环境变量(根据部署目录) 153 | echo "VITE_BASE_PATH=/apps/" > .env.production 154 | 155 | # 2. 构建项目 156 | npm run build 157 | ``` 158 | 159 | ### 2. 上传文件 160 | 161 | ```bash 162 | # 将 dist 目录内容上传到服务器 163 | scp -r dist/* user@server:/var/www/drplayer/dist/ 164 | ``` 165 | 166 | ### 3. 配置服务器 167 | 168 | ```bash 169 | # 1. 配置 nginx 170 | sudo nano /etc/nginx/sites-available/drplayer 171 | sudo ln -s /etc/nginx/sites-available/drplayer /etc/nginx/sites-enabled/ 172 | sudo nginx -t 173 | sudo systemctl reload nginx 174 | 175 | # 2. 设置文件权限 176 | sudo chown -R www-data:www-data /var/www/drplayer/ 177 | sudo chmod -R 755 /var/www/drplayer/ 178 | ``` 179 | 180 | ## 常见问题解决 181 | 182 | ### 1. 资源加载 404 183 | 184 | **问题**:部署到子目录后,CSS/JS 文件加载失败 185 | 186 | **解决**: 187 | - 确保 `.env.production` 中设置了正确的 `VITE_BASE_PATH` 188 | - 检查 nginx 配置中的 `alias` 路径是否正确 189 | 190 | ### 2. 路由刷新 404 191 | 192 | **问题**:直接访问路由地址或刷新页面时出现 404 193 | 194 | **解决**: 195 | - 确保 nginx 配置了正确的 `try_files` 规则 196 | - 检查 `try_files` 中的 fallback 路径是否正确 197 | 198 | ### 3. 子目录路由问题 199 | 200 | **问题**:子目录部署时路由跳转不正确 201 | 202 | **解决**: 203 | - 确保 Vue Router 的 base 路径配置正确 204 | - 检查环境变量 `VITE_BASE_PATH` 是否正确设置 205 | 206 | ## 验证部署 207 | 208 | 部署完成后,验证以下功能: 209 | 210 | 1. **静态资源加载**:检查浏览器开发者工具,确保所有 CSS/JS 文件正常加载 211 | 2. **路由导航**:点击菜单项,确保路由跳转正常 212 | 3. **页面刷新**:在任意页面刷新,确保不出现 404 错误 213 | 4. **直接访问**:直接在地址栏输入路由地址,确保能正常访问 214 | 215 | ## 性能优化建议 216 | 217 | 1. **启用 Gzip 压缩** 218 | 2. **设置静态资源缓存** 219 | 3. **使用 CDN 加速** 220 | 4. **启用 HTTP/2** 221 | 222 | ```nginx 223 | # Gzip 压缩 224 | gzip on; 225 | gzip_vary on; 226 | gzip_min_length 1024; 227 | gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; 228 | 229 | # HTTP/2 230 | listen 443 ssl http2; 231 | ``` -------------------------------------------------------------------------------- /dashboard/docs/apidoc.md: -------------------------------------------------------------------------------- 1 | # drpyS接口文档 2 | 3 | 本文档基于 Fastify 实现整理,适合国内开发人员快速对接。 4 | 5 | ## 1. 接口概览 6 | 7 | | 接口名称 | 请求方式 | 地址示例 | 8 | |------------|------------|--------------------| 9 | | 模块数据接口(T4) | GET / POST | `/api/:module` | 10 | | 模块代理接口 | GET | `/proxy/:module/*` | 11 | | 解析接口 | GET | `/parse/:jx` | 12 | 13 | --- 14 | 15 | ## 2. 接口详情 16 | 17 | ### 2.1 模块数据接口(T4) 18 | 19 | - **URL**:`/api/:module` 20 | - **请求方式**:`GET` / `POST` 21 | - **鉴权**:需要 `validatePwd` 验证(通过请求参数如?pwd=dzyyds) 22 | - **Content-Type**: 23 | - `application/json` 24 | - `application/x-www-form-urlencoded` 25 | 26 | #### 路径参数 27 | 28 | | 参数名 | 类型 | 必填 | 说明 | 29 | |--------|--------|----|-----------------------| 30 | | module | string | 是 | 自定义源文件名称,例如 `腾云驾雾[官]` | 31 | 32 | #### 请求参数(query 或 body) 33 | 34 | 以下参数根据业务逻辑不同,**只需传递需要的字段**: 35 | 36 | | 参数名 | 类型 | 说明 | 37 | |---------|--------|----------------------------------------| 38 | | play | string | 播放链接标识 | 39 | | flag | string | 播放标志(配合 `play` 使用) | 40 | | ac | string | 动作类型,可配合 `t`、`ids`、`action` 等字段 | 41 | | t | string | 分类 ID(配合 `ac` 使用) | 42 | | ids | string | 详情 ID(逗号分隔) | 43 | | action | string | 执行动作名称 | 44 | | value | string | 执行动作值 | 45 | | wd | string | 搜索关键字 | 46 | | quick | number | 搜索模式(0 普通,1 快速) | 47 | | refresh | any | 强制刷新初始化 | 48 | | filter | number | 是否开启筛选(默认 1) | 49 | | pg | number | 页码,默认 1 | 50 | | ext | string | Base64 编码的 JSON 筛选参数 | 51 | | extend | string | 扩展参数(直接字符串,根据/config路由对应sites的ext属性传递) | 52 | | do | string | 自定义源适配器,默认ds,可不传 | 53 | 54 | #### 功能分支 55 | 56 | 接口会根据传参进入不同逻辑: 57 | 58 | 1. **播放**:`play` 存在 → 调用 `play` 方法 59 | 2. **分类**:`ac`、`t` 存在 → 调用 `cate` (ac=list) 60 | 3. **详情**:`ac`、`ids` 存在 → 调用 `detail` (ac=detail) 61 | 4. **动作**:`ac`、`action` 存在 → 调用 `action` (ac=action) 62 | 5. **搜索**:`wd` 存在 → 调用 `search` 63 | 6. **刷新**:`refresh` 存在 → 调用 `init` 64 | 7. **默认**:返回 `home` + `homeVod` 数据 65 | 66 | #### 返回示例 67 | 68 | ```json 69 | { 70 | "type": "影视", 71 | "class": [ 72 | { 73 | "type_id": "1", 74 | "type_name": "电影" 75 | }, 76 | { 77 | "type_id": "2", 78 | "type_name": "电视剧" 79 | } 80 | ], 81 | "filters": {}, 82 | "list": [ 83 | { 84 | "vod_id": "123", 85 | "vod_name": "示例视频", 86 | "vod_pic": "http://example.com/img.jpg", 87 | "vod_remarks": "更新至第1集" 88 | } 89 | ] 90 | } 91 | ``` 92 | 93 | [更多T4接口说明参考](./t4api.md) 94 | 95 | --- 96 | 97 | ### 2.2 模块代理接口 98 | 99 | - **URL**:`/proxy/:module/*` 100 | - **请求方式**:`GET` 101 | - **功能**:转发/代理模块相关资源(可处理 Range 请求,支持流媒体) 102 | - **路径参数**: 103 | | 参数名 | 类型 | 必填 | 说明 | 104 | | ------- | ------ | ---- | ---- | 105 | | module | string | 是 | 模块名称 | 106 | | * | string | 是 | 代理的目标路径 | 107 | 108 | - **查询参数**:与 `/api/:module` 相似,额外支持 `extend` 109 | - **返回值**: 110 | - 可能是二进制文件(图片、视频等) 111 | - 可能是 JSON / 文本 112 | - 可能 302 重定向到 `/mediaProxy` 流代理地址 113 | 114 | #### 返回示例(JSON) 115 | 116 | ```json 117 | { 118 | "code": 200, 119 | "msg": "成功", 120 | "data": "内容" 121 | } 122 | ``` 123 | 124 | --- 125 | 126 | ### 2.3 解析接口 127 | 128 | - **URL**:`/parse/:jx` 129 | - **请求方式**:`GET` 130 | - **功能**:调用解析脚本解析传入链接(支持跳转、JSON 输出) 131 | - **路径参数**: 132 | | 参数名 | 类型 | 必填 | 说明 | 133 | | ------ | ------ | ---- | ---- | 134 | | jx | string | 是 | 解析脚本名称(对应 `.js` 文件) | 135 | 136 | - **查询参数**: 137 | | 参数名 | 类型 | 必填 | 说明 | 138 | | ------ | ------ | ---- | ---- | 139 | | url | string | 是 | 待解析的链接 | 140 | | extend | string | 否 | 扩展参数 | 141 | 142 | - **返回值**: 143 | - `code`:200 成功,404 失败 144 | - `msg`:提示信息 145 | - `url`:解析后的地址 146 | - `cost`:解析耗时(毫秒) 147 | 148 | #### 返回示例(成功) 149 | 150 | ```json 151 | { 152 | "code": 200, 153 | "url": "http://example.com/play.m3u8", 154 | "msg": "jx1解析成功", 155 | "cost": 123 156 | } 157 | ``` 158 | 159 | #### 返回示例(失败) 160 | 161 | ```json 162 | { 163 | "code": 404, 164 | "url": "http://example.com", 165 | "msg": "jx1解析失败", 166 | "cost": 120 167 | } 168 | ``` 169 | 170 | --- 171 | 172 | ## 3. 错误返回格式 173 | 174 | ```json 175 | { 176 | "error": "错误描述信息" 177 | } 178 | ``` 179 | 180 | - 常见错误: 181 | - `Module xxx not found`:模块不存在 182 | - `解析 xxx not found`:解析脚本不存在 183 | - `Failed to process module`:模块执行出错 184 | - `Failed to proxy module`:代理执行出错 185 | 186 | --- 187 | 188 | ## 4. 开发注意事项 189 | 190 | 1. 所有模块和解析脚本必须存在于 `jsDir` / `jxDir` 对应目录下。 191 | 2. 访问 `/api/:module` 接口时需通过 `validatePwd` 验证。 192 | 3. `ext` 参数必须是 **Base64 编码的 JSON 字符串**,否则会报“筛选参数错误”。 193 | 4. 流媒体内容可能会通过 `/mediaProxy` 重定向处理。 194 | 5. 建议在请求时加上 `pg` 参数避免默认第一页。 195 | -------------------------------------------------------------------------------- /dashboard/src/api/README.md: -------------------------------------------------------------------------------- 1 | # API 封装使用说明 2 | 3 | 本项目已将所有API调用封装为统一的服务模块,便于维护和使用。 4 | 5 | ## 目录结构 6 | 7 | ``` 8 | src/api/ 9 | ├── index.js # 统一入口文件 10 | ├── config.js # API配置和常量 11 | ├── request.js # Axios封装和拦截器 12 | ├── modules/ # 基础API模块 13 | │ ├── module.js # T4模块数据接口 14 | │ ├── proxy.js # 模块代理接口 15 | │ └── parse.js # 解析接口 16 | ├── services/ # 业务服务层 17 | │ ├── index.js # 服务统一入口 18 | │ ├── video.js # 视频相关业务逻辑 19 | │ └── site.js # 站点管理业务逻辑 20 | ├── utils/ # 工具函数 21 | │ └── index.js # 数据处理和验证工具 22 | └── types/ # 数据类型定义 23 | └── index.js # 类型定义和工厂函数 24 | ``` 25 | 26 | ## 快速开始 27 | 28 | ### 1. 导入服务 29 | 30 | ```javascript 31 | // 导入所有服务 32 | import { videoService, siteService } from '@/api/services' 33 | 34 | // 或者单独导入 35 | import { videoService } from '@/api/services/video' 36 | import { siteService } from '@/api/services/site' 37 | ``` 38 | 39 | ### 2. 站点管理 40 | 41 | ```javascript 42 | // 获取所有站点 43 | const sites = siteService.getAllSites() 44 | 45 | // 设置当前站点 46 | siteService.setCurrentSite('site_key') 47 | 48 | // 获取当前站点 49 | const currentSite = siteService.getCurrentSite() 50 | 51 | // 添加新站点 52 | siteService.addSite({ 53 | key: 'new_site', 54 | name: '新站点', 55 | api: 'https://api.example.com', 56 | ext: 'some_extension' 57 | }) 58 | ``` 59 | 60 | ### 3. 视频数据获取 61 | 62 | ```javascript 63 | // 获取推荐视频 64 | const homeData = await videoService.getRecommendVideos('site_key', { 65 | extend: 'extension_data' 66 | }) 67 | 68 | // 获取分类视频 69 | const categoryData = await videoService.getCategoryVideos('site_key', { 70 | t: 'category_id', 71 | pg: 1, 72 | extend: 'extension_data' 73 | }) 74 | 75 | // 搜索视频 76 | const searchResult = await videoService.searchVideo('site_key', { 77 | wd: '搜索关键词', 78 | pg: 1, 79 | extend: 'extension_data' 80 | }) 81 | 82 | // 获取视频详情 83 | const videoDetail = await videoService.getVideoDetails('site_key', { 84 | ids: 'video_id', 85 | extend: 'extension_data' 86 | }) 87 | 88 | // 获取播放地址 89 | const playData = await videoService.getPlayUrl('site_key', { 90 | play: 'play_data', 91 | extend: 'extension_data' 92 | }) 93 | ``` 94 | 95 | ### 4. 视频解析 96 | 97 | ```javascript 98 | // 解析视频URL 99 | const parseResult = await videoService.parseVideoUrl('jx_key', { 100 | url: 'video_url' 101 | }) 102 | ``` 103 | 104 | ## 在Vue组件中使用 105 | 106 | ### 基本用法 107 | 108 | ```vue 109 | 116 | 117 | 145 | ``` 146 | 147 | ### 错误处理 148 | 149 | 所有API调用都包含了错误处理,会返回统一的错误格式: 150 | 151 | ```javascript 152 | try { 153 | const result = await videoService.getRecommendVideos('site_key') 154 | } catch (error) { 155 | // error.code - 错误代码 156 | // error.message - 错误信息 157 | // error.data - 额外错误数据 158 | console.error('API调用失败:', error.message) 159 | } 160 | ``` 161 | 162 | ### 缓存机制 163 | 164 | 视频服务包含5分钟的缓存机制,相同的请求在5分钟内会返回缓存结果,提高性能。 165 | 166 | ## 配置说明 167 | 168 | ### API配置 (config.js) 169 | 170 | ```javascript 171 | // 基础配置 172 | export const BASE_URL = process.env.VUE_APP_API_BASE_URL || '' 173 | export const TIMEOUT = 10000 174 | 175 | // API路径 176 | export const API_PATHS = { 177 | MODULE: '/api', // T4模块接口 178 | PROXY: '/proxy', // 代理接口 179 | PARSE: '/parse' // 解析接口 180 | } 181 | ``` 182 | 183 | ### 请求拦截器 184 | 185 | 请求会自动添加: 186 | - Authorization token(如果存在) 187 | - Cache-Control: no-cache 188 | - 统一的错误处理 189 | 190 | ## 迁移指南 191 | 192 | ### 从旧的req方式迁移 193 | 194 | **旧方式:** 195 | ```javascript 196 | import req from '@/utils/req' 197 | 198 | // 获取数据 199 | const response = await req.get('/home') 200 | const data = response.data 201 | ``` 202 | 203 | **新方式:** 204 | ```javascript 205 | import { videoService, siteService } from '@/api/services' 206 | 207 | // 获取数据 208 | const currentSite = siteService.getCurrentSite() 209 | const data = await videoService.getRecommendVideos(currentSite.key, { 210 | extend: currentSite.ext 211 | }) 212 | ``` 213 | 214 | ### 主要变化 215 | 216 | 1. **统一的服务接口**:不再直接调用HTTP方法,而是调用语义化的业务方法 217 | 2. **自动错误处理**:统一的错误格式和处理机制 218 | 3. **数据格式化**:返回的数据已经过格式化处理 219 | 4. **缓存支持**:自动缓存机制提高性能 220 | 5. **类型安全**:完整的类型定义和验证 221 | 222 | ## 注意事项 223 | 224 | 1. 所有API调用都需要先设置当前站点 225 | 2. 确保传入正确的extend参数 226 | 3. 处理好异步操作的错误情况 227 | 4. 合理使用缓存机制,避免频繁请求 -------------------------------------------------------------------------------- /dashboard/build-binary.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from 'child_process'; 4 | import { existsSync, rmSync, cpSync, mkdirSync, readdirSync, statSync } from 'fs'; 5 | import { join, dirname } from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | // 定义路径 12 | const rootDir = __dirname; 13 | const tempServerDir = join(rootDir, 'temp-server'); 14 | const distBinaryDir = join(rootDir, 'dist-binary'); 15 | const distDir = join(rootDir, 'dist'); 16 | const tempServerDistDir = join(tempServerDir, 'dist'); 17 | 18 | console.log('🚀 开始自动化打包流程...\n'); 19 | 20 | try { 21 | // 步骤 1: 前端构建 22 | console.log('📦 步骤 1: 构建前端项目...'); 23 | execSync('pnpm build:apps', { 24 | stdio: 'inherit', 25 | cwd: rootDir 26 | }); 27 | console.log('✅ 前端构建完成\n'); 28 | 29 | // 步骤 2: 清理 temp-server 中的旧文件 30 | console.log('🧹 步骤 2: 清理 temp-server 中的旧文件...'); 31 | 32 | // 清理 temp-server 中的旧 apps 目录 33 | const tempServerAppsDir = join(tempServerDir, 'apps'); 34 | if (existsSync(tempServerAppsDir)) { 35 | rmSync(tempServerAppsDir, { recursive: true, force: true }); 36 | console.log('✅ 已删除旧的 apps 目录'); 37 | } 38 | console.log(''); 39 | 40 | // 步骤 3: 复制构建文件到 temp-server/apps/drplayer 41 | console.log('📁 步骤 3: 复制构建文件到 temp-server...'); 42 | if (!existsSync(distDir)) { 43 | throw new Error('dist 目录不存在,请确保前端构建成功'); 44 | } 45 | 46 | // 确保 temp-server 目录存在 47 | if (!existsSync(tempServerDir)) { 48 | mkdirSync(tempServerDir, { recursive: true }); 49 | } 50 | 51 | // 创建 apps/drplayer 目录并复制 dist 内容 52 | const tempServerDrplayerDir = join(tempServerAppsDir, 'drplayer'); 53 | if (!existsSync(tempServerAppsDir)) { 54 | mkdirSync(tempServerAppsDir, { recursive: true }); 55 | } 56 | 57 | // 将 dist 目录的内容复制到 apps/drplayer 58 | cpSync(distDir, tempServerDrplayerDir, { recursive: true }); 59 | console.log('✅ 已将 dist 内容复制到 apps/drplayer'); 60 | console.log(''); 61 | 62 | // 步骤 4: 在 temp-server 目录中打包二进制文件 63 | console.log('⚙️ 步骤 4: 打包二进制文件...'); 64 | 65 | // 确保 temp-server 有 node_modules 66 | const tempServerNodeModules = join(tempServerDir, 'node_modules'); 67 | if (!existsSync(tempServerNodeModules)) { 68 | console.log('📦 安装 temp-server 依赖...'); 69 | execSync('pnpm install', { 70 | stdio: 'inherit', 71 | cwd: tempServerDir 72 | }); 73 | } 74 | 75 | // 执行 pkg 打包 76 | execSync('pnpm pkg:win', { 77 | stdio: 'inherit', 78 | cwd: tempServerDir 79 | }); 80 | console.log('✅ 二进制文件打包完成\n'); 81 | 82 | // 步骤 5: 移动二进制文件到 dist-binary 目录 83 | console.log('📦 步骤 5: 移动二进制文件到 dist-binary...'); 84 | 85 | // 确保 dist-binary 目录存在 86 | if (!existsSync(distBinaryDir)) { 87 | mkdirSync(distBinaryDir, { recursive: true }); 88 | } 89 | 90 | // 查找并移动二进制文件 91 | const tempDistBinaryDir = join(tempServerDir, 'dist-binary'); 92 | if (existsSync(tempDistBinaryDir)) { 93 | const files = readdirSync(tempDistBinaryDir); 94 | for (const file of files) { 95 | const srcPath = join(tempDistBinaryDir, file); 96 | const destPath = join(distBinaryDir, file); 97 | 98 | // 如果目标文件已存在,先删除 99 | if (existsSync(destPath)) { 100 | rmSync(destPath, { force: true }); 101 | } 102 | 103 | // 移动文件 104 | cpSync(srcPath, destPath, { recursive: true }); 105 | console.log(`✅ 已移动: ${file}`); 106 | } 107 | 108 | // 清理 temp-server 中的 dist-binary 目录 109 | try { 110 | rmSync(tempDistBinaryDir, { recursive: true, force: true }); 111 | } catch (error) { 112 | console.log(`⚠️ 无法删除临时目录 (可能有进程正在使用): ${error.message}`); 113 | } 114 | } 115 | 116 | // 步骤 6: 清理 temp-server 目录 117 | console.log('\n🧹 步骤 6: 清理 temp-server 临时文件...'); 118 | 119 | // 需要清理的目录列表 120 | const dirsToClean = [ 121 | join(tempServerDir, 'apps'), 122 | join(tempServerDir, 'dist-binary') 123 | ]; 124 | 125 | for (const dirPath of dirsToClean) { 126 | if (existsSync(dirPath)) { 127 | try { 128 | rmSync(dirPath, { recursive: true, force: true }); 129 | console.log(`✅ 已清理: ${dirPath.replace(tempServerDir, 'temp-server')}`); 130 | } catch (error) { 131 | console.log(`⚠️ 无法清理目录 ${dirPath.replace(tempServerDir, 'temp-server')}: ${error.message}`); 132 | } 133 | } 134 | } 135 | 136 | console.log('\n🎉 自动化打包流程完成!'); 137 | console.log(`📁 二进制文件位置: ${distBinaryDir}`); 138 | 139 | // 显示生成的文件 140 | if (existsSync(distBinaryDir)) { 141 | const files = readdirSync(distBinaryDir); 142 | if (files.length > 0) { 143 | console.log('\n📋 生成的文件:'); 144 | files.forEach(file => { 145 | const filePath = join(distBinaryDir, file); 146 | const stats = statSync(filePath); 147 | const size = (stats.size / 1024 / 1024).toFixed(2); 148 | console.log(` - ${file} (${size} MB)`); 149 | }); 150 | } 151 | } 152 | 153 | } catch (error) { 154 | console.error('\n❌ 打包过程中出现错误:'); 155 | console.error(error.message); 156 | process.exit(1); 157 | } -------------------------------------------------------------------------------- /dashboard/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 66 | 67 | 210 | -------------------------------------------------------------------------------- /dashboard/src/components/PlayerSelector.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 97 | 98 | -------------------------------------------------------------------------------- /dashboard/src/components/ScrollToBottom.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 151 | 152 | -------------------------------------------------------------------------------- /dashboard/src/utils/csp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CSP (Content Security Policy) 工具函数 3 | * 用于处理视频播放时的CSP策略和防盗链绕过 4 | */ 5 | 6 | /** 7 | * Referrer 策略选项 8 | */ 9 | export const REFERRER_POLICIES = { 10 | NO_REFERRER: 'no-referrer', 11 | NO_REFERRER_WHEN_DOWNGRADE: 'no-referrer-when-downgrade', 12 | ORIGIN: 'origin', 13 | ORIGIN_WHEN_CROSS_ORIGIN: 'origin-when-cross-origin', 14 | SAME_ORIGIN: 'same-origin', 15 | STRICT_ORIGIN: 'strict-origin', 16 | STRICT_ORIGIN_WHEN_CROSS_ORIGIN: 'strict-origin-when-cross-origin', 17 | UNSAFE_URL: 'unsafe-url' 18 | } 19 | 20 | /** 21 | * Referrer 策略选项列表(用于UI选择) 22 | */ 23 | export const REFERRER_POLICIES_LIST = [ 24 | { value: 'no-referrer', label: '不发送Referrer' }, 25 | { value: 'no-referrer-when-downgrade', label: 'HTTPS到HTTP时不发送' }, 26 | { value: 'origin', label: '只发送源域名' }, 27 | { value: 'origin-when-cross-origin', label: '跨域时只发送源域名' }, 28 | { value: 'same-origin', label: '同源时发送完整Referrer' }, 29 | { value: 'strict-origin', label: '严格源域名策略' }, 30 | { value: 'strict-origin-when-cross-origin', label: '跨域时严格控制' }, 31 | { value: 'unsafe-url', label: '总是发送完整Referrer(不安全)' } 32 | ] 33 | 34 | /** 35 | * 获取当前的referrer策略 36 | * @returns {string} 当前的referrer策略 37 | */ 38 | export function getCurrentReferrerPolicy() { 39 | const metaTag = document.querySelector('meta[name="referrer"]') 40 | return metaTag ? metaTag.getAttribute('content') : 'default' 41 | } 42 | 43 | /** 44 | * 设置全局referrer策略 45 | * @param {string} policy - referrer策略 46 | */ 47 | export function setGlobalReferrerPolicy(policy) { 48 | let metaTag = document.querySelector('meta[name="referrer"]') 49 | 50 | if (!metaTag) { 51 | metaTag = document.createElement('meta') 52 | metaTag.setAttribute('name', 'referrer') 53 | document.head.appendChild(metaTag) 54 | } 55 | 56 | metaTag.setAttribute('content', policy) 57 | console.log(`已设置全局referrer策略为: ${policy}`) 58 | } 59 | 60 | /** 61 | * 为特定的视频元素设置referrer策略 62 | * @param {HTMLVideoElement} videoElement - 视频元素 63 | * @param {string} policy - referrer策略 64 | */ 65 | export function setVideoReferrerPolicy(videoElement, policy) { 66 | if (videoElement && videoElement.tagName === 'VIDEO') { 67 | videoElement.setAttribute('referrerpolicy', policy) 68 | console.log(`已为视频元素设置referrer策略: ${policy}`) 69 | } 70 | } 71 | 72 | /** 73 | * 创建带有特定referrer策略的视频元素 74 | * @param {string} policy - referrer策略 75 | * @returns {HTMLVideoElement} 配置好的视频元素 76 | */ 77 | export function createVideoWithReferrerPolicy(policy = REFERRER_POLICIES.NO_REFERRER) { 78 | const video = document.createElement('video') 79 | video.setAttribute('referrerpolicy', policy) 80 | video.setAttribute('crossorigin', 'anonymous') 81 | return video 82 | } 83 | 84 | /** 85 | * 为fetch请求设置no-referrer策略 86 | * @param {string} url - 请求URL 87 | * @param {object} options - fetch选项 88 | * @returns {Promise} fetch请求 89 | */ 90 | export function fetchWithNoReferrer(url, options = {}) { 91 | return fetch(url, { 92 | ...options, 93 | referrerPolicy: REFERRER_POLICIES.NO_REFERRER 94 | }) 95 | } 96 | 97 | 98 | 99 | /** 100 | * 智能设置referrer策略 101 | * 根据CSP绕过配置来决定referrer策略 102 | * @param {string} videoUrl - 视频URL 103 | * @param {HTMLVideoElement} videoElement - 视频元素(可选) 104 | */ 105 | export function smartSetReferrerPolicy(videoUrl, videoElement = null) { 106 | // 获取CSP配置 107 | const config = getCSPConfig() 108 | 109 | let policy 110 | if (config.autoBypass) { 111 | // 如果启用了CSP绕过,使用配置中的referrer策略 112 | policy = config.referrerPolicy 113 | console.log(`根据CSP配置设置referrer策略: ${policy} (URL: ${videoUrl})`) 114 | } else { 115 | // 如果未启用CSP绕过,保持当前策略或使用默认策略 116 | const currentPolicy = getCurrentReferrerPolicy() 117 | policy = currentPolicy || REFERRER_POLICIES.STRICT_ORIGIN_WHEN_CROSS_ORIGIN 118 | console.log(`CSP绕过未启用,保持当前策略: ${policy} (URL: ${videoUrl})`) 119 | } 120 | 121 | // 设置全局策略 122 | setGlobalReferrerPolicy(policy) 123 | 124 | // 如果提供了视频元素,也为其设置策略 125 | if (videoElement) { 126 | setVideoReferrerPolicy(videoElement, policy) 127 | } 128 | 129 | return policy 130 | } 131 | 132 | /** 133 | * 重置referrer策略为默认值 134 | */ 135 | export function resetReferrerPolicy() { 136 | setGlobalReferrerPolicy(REFERRER_POLICIES.STRICT_ORIGIN_WHEN_CROSS_ORIGIN) 137 | } 138 | 139 | /** 140 | * CSP绕过配置对象 141 | */ 142 | export const CSP_BYPASS_CONFIG = { 143 | // 默认referrer策略 144 | referrerPolicy: REFERRER_POLICIES.NO_REFERRER, 145 | 146 | // 是否启用自动CSP绕过 147 | autoBypass: true, 148 | 149 | // 是否在播放失败时自动重试不同的策略 150 | autoRetry: true, 151 | 152 | // 重试策略列表 153 | retryPolicies: [ 154 | REFERRER_POLICIES.NO_REFERRER, 155 | REFERRER_POLICIES.ORIGIN, 156 | REFERRER_POLICIES.SAME_ORIGIN, 157 | REFERRER_POLICIES.UNSAFE_URL 158 | ] 159 | } 160 | 161 | /** 162 | * 获取CSP绕过配置 163 | * @returns {object} CSP配置对象 164 | */ 165 | export function getCSPConfig() { 166 | const stored = localStorage.getItem('csp_bypass_config') 167 | return stored ? { ...CSP_BYPASS_CONFIG, ...JSON.parse(stored) } : CSP_BYPASS_CONFIG 168 | } 169 | 170 | /** 171 | * 保存CSP绕过配置 172 | * @param {object} config - 配置对象 173 | */ 174 | export function saveCSPConfig(config) { 175 | localStorage.setItem('csp_bypass_config', JSON.stringify(config)) 176 | } 177 | 178 | /** 179 | * 应用CSP绕过策略到视频播放 180 | * @param {string} videoUrl - 视频URL 181 | * @param {HTMLVideoElement} videoElement - 视频元素 182 | * @returns {string} 应用的策略 183 | */ 184 | export function applyCSPBypass(videoUrl, videoElement) { 185 | const config = getCSPConfig() 186 | 187 | if (!config.autoBypass) { 188 | return getCurrentReferrerPolicy() 189 | } 190 | 191 | return smartSetReferrerPolicy(videoUrl, videoElement) 192 | } -------------------------------------------------------------------------------- /dashboard/docs/pvideo接口说明.md: -------------------------------------------------------------------------------- 1 | # MediaTool 参数说明和示例 2 | 3 | ## 1. 命令行支持的参数 4 | 5 | ### 可通过命令行传递的参数 6 | 7 | | 参数 | 命令行标志 | 默认值 | 说明 | 示例 | 8 | |------|------------|--------|------|------| 9 | | config | `-config` | `"config.json"` | 配置文件路径 | `./pvideo -config myconfig.json` | 10 | | port | `-port` | `"2525"` | 服务器端口 | `./pvideo -port 8080` | 11 | | site | `-site` | `""` | 反代域名 | `./pvideo -site https://mydomain.com` | 12 | | proxy | `-proxy` | `""` | 代理地址 | `./pvideo -proxy socks5://127.0.0.1:1080` | 13 | | debug | `-debug` | `false` | 调试模式 | `./pvideo -debug` | 14 | | dns | `-dns` | `""` | DNS服务器 | `./pvideo -dns 8.8.8.8` | 15 | 16 | ### 只能通过配置文件设置的参数 17 | 18 | | 参数 | 类型 | 默认值 | 说明 | 19 | |------|------|--------|------| 20 | | localUrl | string | `""` | 本地服务URL,用于生成代理链接 | 21 | | connect | int64 | `32` | 最大并发连接数 | 22 | | ssl | object | `null` | SSL证书配置(cert, key) | 23 | 24 | ### 命令行使用示例 25 | 26 | ```bash 27 | # 基本启动 28 | ./pvideo 29 | 30 | # 指定端口和调试模式 31 | ./pvideo -port 8080 -debug 32 | 33 | # 使用SOCKS5代理 34 | ./pvideo -proxy socks5://127.0.0.1:1080 -dns 8.8.8.8 35 | 36 | # 完整参数示例 37 | ./pvideo -config config.json -port 7777 -site https://mydomain.com -proxy http://proxy.example.com:8080 -debug -dns 8.8.8.8 38 | ``` 39 | 40 | ## 2. 多线程代理参数和示例 41 | 42 | ### 多线程代理支持的URL参数 43 | 44 | | 参数 | 必需 | 说明 | 示例值 | 45 | |------|------|------|--------| 46 | | url | ✅ | 目标文件URL | `https://example.com/video.mp4` | 47 | | form | ❌ | 编码格式 | `base64`(URL和header使用base64编码) | 48 | | header | ❌ | 自定义请求头 | JSON格式的请求头 | 49 | | thread | ❌ | 线程数 | `4`(默认根据文件大小自动计算) | 50 | | size | ❌ | 分块大小 | `128K`(默认), `256K`, `1M`, `2M`, `512B`, `1024` | 51 | | limit | ❌ | 限制条件 | `30S`(时间限制), `100C`(次数限制) | 52 | | single | ❌ | 单线程模式 | `true`, `false` | 53 | | mark | ❌ | 缓存标记 | 自定义缓存标识 | 54 | 55 | ### 多线程代理使用示例(也可以不需要带proxy路径:http://localhost:7777?url=https://example.com/video.mp4) 56 | 57 | #### 基本用法 58 | ``` 59 | http://localhost:7777/proxy?url=https://example.com/video.mp4 60 | ``` 61 | 62 | #### 自定义线程数和分块大小 63 | ``` 64 | http://localhost:7777/proxy?url=https://example.com/video.mp4&thread=8&size=256K 65 | ``` 66 | 67 | #### 使用自定义请求头 68 | ``` 69 | http://localhost:7777/proxy?url=https://example.com/video.mp4&header={"User-Agent":"Custom-Agent","Referer":"https://example.com"} 70 | ``` 71 | 72 | #### 使用base64编码(避免URL编码问题) 73 | ``` 74 | http://localhost:7777/proxy?url=aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ=&form=base64&header=eyJVc2VyLUFnZW50IjoiQ3VzdG9tLUFnZW50In0= 75 | ``` 76 | 77 | #### 设置时间限制(30秒后重新获取真实URL) 78 | ``` 79 | http://localhost:7777/proxy?url=https://example.com/video.mp4&limit=30S 80 | ``` 81 | 82 | #### 设置次数限制(100次请求后重新获取真实URL) 83 | ``` 84 | http://localhost:7777/proxy?url=https://example.com/video.mp4&limit=100C 85 | ``` 86 | 87 | #### 单线程模式(适用于例如网页请求) 88 | ``` 89 | http://localhost:7777/proxy?single=true&url=https://example.com/small-file.jpg 90 | ``` 91 | 92 | #### 使用缓存标记(mark参数) 93 | ``` 94 | # 任务1:普通下载 95 | http://localhost:7777/proxy?url=https://example.com/video.mp4&mark=normal_download 96 | 97 | # 任务2:带认证的下载(相同URL,不同处理方式) 98 | http://localhost:7777/proxy?url=https://example.com/video.mp4&mark=auth_download&header={"Authorization":"Bearer token123"} 99 | 100 | # 任务3:移动端下载(相同URL,不同User-Agent) 101 | http://localhost:7777/proxy?url=https://example.com/video.mp4&mark=mobile_download&header={"User-Agent":"Mobile-App/1.0"} 102 | ``` 103 | 104 | ### 缓存标记(mark)说明 105 | 106 | `mark` 参数是一个**缓存标识符**,用于区分和管理不同的下载任务: 107 | 108 | **主要作用**: 109 | 1. **缓存隔离**: 相同URL的不同任务使用独立缓存 110 | 2. **连接复用**: 相同mark的请求复用HTTP连接 111 | 3. **任务标识**: 便于调试和日志追踪 112 | 113 | **使用场景**: 114 | 115 | #### 场景1: 相同文件的不同下载方式 116 | ``` 117 | # 高清版本 118 | http://localhost:7777/proxy?url=https://cdn.example.com/video.mp4&mark=hd_version&size=1M 119 | 120 | # 标清版本(相同URL,不同处理参数) 121 | http://localhost:7777/proxy?url=https://cdn.example.com/video.mp4&mark=sd_version&size=256K 122 | ``` 123 | 124 | #### 场景2: 不同用户的相同资源 125 | ``` 126 | # 用户A下载 127 | http://localhost:7777/proxy?url=https://example.com/file.zip&mark=user_a&header={"Cookie":"session=abc123"} 128 | 129 | # 用户B下载 130 | http://localhost:7777/proxy?url=https://example.com/file.zip&mark=user_b&header={"Cookie":"session=def456"} 131 | ``` 132 | 133 | #### 场景3: 不同平台的相同内容 134 | ``` 135 | # PC端下载 136 | http://localhost:7777/proxy?url=https://example.com/app.apk&mark=pc_client 137 | 138 | # 移动端下载 139 | http://localhost:7777/proxy?url=https://example.com/app.apk&mark=mobile_client 140 | ``` 141 | 142 | **默认行为**: 143 | - 如果不指定 `mark` 参数,系统会使用完整的 `url` 作为缓存标记 144 | - 建议在有多种下载需求时主动设置 `mark` 参数 145 | 146 | 147 | 148 | ## 3. M3U8参数和示例 149 | 150 | ### M3U8支持的URL参数 151 | 152 | | 参数 | 必需 | 说明 | 示例值 | 153 | |------|------|------|--------| 154 | | url | ✅ | M3U8播放列表URL | `https://example.com/playlist.m3u8` | 155 | | form | ❌ | 编码格式 | `base64`(URL和header使用base64编码) | 156 | | header | ❌ | 自定义请求头 | JSON格式的请求头 | 157 | | type | ❌ | 文件类型标识 | `m3u8` | 158 | 159 | ### M3U8使用示例 160 | 161 | #### 基本用法 162 | ``` 163 | http://localhost:7777/m3u8?url=https://example.com/playlist.m3u8 164 | ``` 165 | 166 | #### 使用自定义请求头 167 | ``` 168 | http://localhost:7777/m3u8?url=https://example.com/playlist.m3u8&header={"User-Agent":"Mozilla/5.0","Referer":"https://example.com"} 169 | ``` 170 | 171 | #### 使用base64编码 172 | ``` 173 | http://localhost:7777/m3u8?url=aHR0cHM6Ly9leGFtcGxlLmNvbS9wbGF5bGlzdC5tM3U4&form=base64&header=eyJVc2VyLUFnZW50IjoiTW96aWxsYS81LjAifQ== 174 | ``` 175 | 176 | #### 指定类型标识 177 | ``` 178 | http://localhost:7777/m3u8?url=https://example.com/playlist.m3u8&type=m3u8 179 | ``` 180 | 181 | ### M3U8处理说明 182 | 183 | 1. **自动转换**: M3U8文件中的相对路径会自动转换为完整URL 184 | 2. **代理重写**: 所有媒体文件URL会被重写为通过本地代理访问 185 | 3. **嵌套支持**: 支持嵌套的M3U8文件(主播放列表包含子播放列表) 186 | 4. **加密支持**: 支持AES加密的M3U8流,密钥URL也会被代理 187 | 5. **Base64编码**: 当使用`form=base64`时,输出的代理URL也会使用base64编码 188 | -------------------------------------------------------------------------------- /dashboard/src/services/resetService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 重置服务 3 | * 负责应用的出厂重置功能 4 | */ 5 | 6 | import { Message, Modal } from '@arco-design/web-vue' 7 | import { saveCSPConfig, setGlobalReferrerPolicy } from '@/utils/csp' 8 | 9 | // 默认配置值 10 | const DEFAULT_CONFIGS = { 11 | // 地址设置默认值 12 | addressSettings: { 13 | vodConfig: '', 14 | liveConfig: '', 15 | proxyAccess: '', 16 | proxyAccessEnabled: false, 17 | proxyPlay: 'http://localhost:57572/proxy?form=base64&url=${url}&headers=${headers}&type=${type}#嗷呜', 18 | proxyPlayEnabled: false, 19 | proxySniff: 'http://localhost:57573/sniffer', 20 | proxySniffEnabled: false, 21 | snifferTimeout: 10, 22 | apiTimeout: 30 23 | }, 24 | 25 | // 应用设置默认值 26 | appSettings: { 27 | datasourceDisplay: true, 28 | windowPreview: true, 29 | playerType: 'ijk', 30 | adFilter: true, 31 | ijkCache: false, 32 | autoLive: false, 33 | secureDns: false, 34 | cspBypass: true, 35 | referrerPolicy: 'no-referrer', 36 | searchAggregation: false // 聚合搜索功能默认关闭 37 | }, 38 | 39 | // CSP配置默认值 40 | cspConfig: { 41 | enabled: true, 42 | referrerPolicy: 'no-referrer' 43 | }, 44 | 45 | // 跳过设置默认值 46 | skipSettings: {}, 47 | 48 | // 解析器配置默认值 49 | parserConfig: {}, 50 | 51 | // 页面状态默认值 52 | pageState: {}, 53 | 54 | // 聚合搜索设置默认值 55 | searchAggregationSettings: { 56 | selectedSources: [] // 默认没有选中任何搜索源 57 | }, 58 | 59 | // 侧边栏折叠状态默认值 60 | sidebarCollapsed: false, 61 | 62 | // 开发者调试设置默认值 63 | debugSettings: { 64 | enabled: false, 65 | url: 'http://localhost:5757/apps/websocket', 66 | resetting: false 67 | } 68 | } 69 | 70 | // 需要完全清空的数据键 71 | const CLEAR_DATA_KEYS = [ 72 | // 用户数据 73 | 'drplayer-favorites', // 收藏列表 74 | 'drplayer_watch_history', // 观看历史 75 | 'drplayer_histories', // 历史页面数据 76 | 'drplayer_daily_stats', // 每日统计 77 | 'drplayer_weekly_stats', // 周统计 78 | 'drplayer_parsers', // 解析器数据 79 | 80 | // 聚合搜索相关 81 | 'searchAggregationSettings', // 聚合搜索源选择设置 82 | 'pageState_searchAggregation', // 聚合搜索页面状态 83 | 'drplayer_search_history', // 搜索历史记录 84 | 85 | // 站点数据 86 | 'siteStore', // 站点存储 87 | 'drplayer_config_url', // 配置地址 88 | 'drplayer_live_config_url', // 直播配置地址 89 | 'drplayer_current_site', // 当前站点 90 | 'drplayer_sites', // 站点列表数据 91 | 'site-nowSite', // 当前站点(兼容旧系统) 92 | 93 | // 配置数据 94 | 'drplayer_config_data', // 配置数据缓存 95 | 'drplayer_config_fetch_time', // 配置获取时间 96 | 97 | // 播放器相关 98 | 'drplayer_preferred_player_type', // 首选播放器类型 99 | 'selectedParser', // 选中的解析器 100 | 'last-clicked-video', // 最后点击的视频 101 | 102 | // 其他设置和状态 103 | 'sidebar-collapsed', // 侧边栏折叠状态 104 | 105 | // 地址配置历史记录 106 | 'drplayer_vod_config_history', 107 | 'drplayer_live_config_history', 108 | 'drplayer_proxy_access_history', 109 | 'drplayer_proxy_play_history', 110 | 'drplayer_proxy_sniff_history', 111 | 112 | // 开发者调试设置相关 113 | 'debugSettings', 114 | 115 | // 悬浮组件相关 116 | 'floating-iframe-button-position', 117 | 'floating-iframe-window-position', 118 | 'floating-iframe-window-size' 119 | ] 120 | 121 | /** 122 | * 显示重置确认对话框 123 | */ 124 | export const showResetConfirmation = () => { 125 | return new Promise((resolve) => { 126 | Modal.confirm({ 127 | title: '确认重置', 128 | content: `此操作将执行完整的出厂重置,包括: 129 | 130 | • 重置所有配置接口地址为默认值 131 | • 清空收藏列表 132 | • 清空观看历史记录 133 | • 清空解析器数据 134 | • 清空站点数据 135 | • 重置所有应用设置 136 | 137 | ⚠️ 此操作不可撤销,请确认是否继续?`, 138 | width: 480, 139 | closable: true, 140 | okText: '确认重置', 141 | cancelText: '取消', 142 | okButtonProps: { 143 | status: 'danger' 144 | }, 145 | onOk: () => { 146 | resolve(true) 147 | }, 148 | onCancel: () => { 149 | resolve(false) 150 | } 151 | }) 152 | }) 153 | } 154 | 155 | /** 156 | * 执行完整的出厂重置 157 | */ 158 | export const performFactoryReset = async () => { 159 | try { 160 | // 1. 清空需要删除的数据 161 | CLEAR_DATA_KEYS.forEach(key => { 162 | localStorage.removeItem(key) 163 | }) 164 | 165 | // 2. 重置配置为默认值 166 | Object.entries(DEFAULT_CONFIGS).forEach(([key, defaultValue]) => { 167 | if (defaultValue !== null && defaultValue !== undefined) { 168 | localStorage.setItem(key, JSON.stringify(defaultValue)) 169 | } 170 | }) 171 | 172 | // 3. 重置CSP配置 173 | try { 174 | saveCSPConfig(DEFAULT_CONFIGS.cspConfig) 175 | setGlobalReferrerPolicy('no-referrer') 176 | } catch (error) { 177 | console.warn('CSP配置重置失败:', error) 178 | } 179 | 180 | // 4. 显示成功消息 181 | Message.success({ 182 | content: '出厂重置完成!应用已恢复到初始状态', 183 | duration: 3000 184 | }) 185 | 186 | // 5. 建议用户刷新页面 187 | setTimeout(() => { 188 | Modal.info({ 189 | title: '重置完成', 190 | content: '为确保所有更改生效,建议刷新页面。是否立即刷新?', 191 | okText: '立即刷新', 192 | cancelText: '稍后刷新', 193 | onOk: () => { 194 | window.location.reload() 195 | } 196 | }) 197 | }, 1000) 198 | 199 | return true 200 | } catch (error) { 201 | console.error('出厂重置失败:', error) 202 | Message.error({ 203 | content: '出厂重置失败,请重试', 204 | duration: 3000 205 | }) 206 | return false 207 | } 208 | } 209 | 210 | /** 211 | * 带确认的出厂重置函数 212 | */ 213 | export const factoryResetWithConfirmation = async () => { 214 | const confirmed = await showResetConfirmation() 215 | if (confirmed) { 216 | return await performFactoryReset() 217 | } 218 | return false 219 | } 220 | 221 | export default { 222 | showResetConfirmation, 223 | performFactoryReset, 224 | factoryResetWithConfirmation, 225 | DEFAULT_CONFIGS 226 | } -------------------------------------------------------------------------------- /dashboard/src/components/FolderBreadcrumb.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 116 | 117 | -------------------------------------------------------------------------------- /dashboard/src/api/types/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API类型定义 3 | * 定义接口相关的数据结构、枚举和常量 4 | */ 5 | 6 | // 视频类型枚举 7 | export const VIDEO_TYPES = { 8 | MOVIE: 'movie', // 电影 9 | TV: 'tv', // 电视剧 10 | VARIETY: 'variety', // 综艺 11 | CARTOON: 'cartoon', // 动漫 12 | CHILD: 'child', // 少儿 13 | DOCUMENTARY: 'doco', // 纪录片 14 | CHOICE: 'choice' // 精选 15 | } 16 | 17 | // 视频状态枚举 18 | export const VIDEO_STATUS = { 19 | NORMAL: 'normal', // 正常 20 | UPDATING: 'updating', // 更新中 21 | COMPLETED: 'completed', // 已完结 22 | PREVIEW: 'preview' // 预告 23 | } 24 | 25 | // 排序方式枚举 26 | export const SORT_TYPES = { 27 | TIME: 'time', // 按时间排序 28 | NAME: 'name', // 按名称排序 29 | HITS: 'hits', // 按点击量排序 30 | SCORE: 'score' // 按评分排序 31 | } 32 | 33 | // 排序顺序枚举 34 | export const SORT_ORDER = { 35 | ASC: 'asc', // 升序 36 | DESC: 'desc' // 降序 37 | } 38 | 39 | // 地区枚举 40 | export const REGIONS = { 41 | MAINLAND: 'mainland', // 大陆 42 | HONGKONG: 'hongkong', // 香港 43 | TAIWAN: 'taiwan', // 台湾 44 | KOREA: 'korea', // 韩国 45 | JAPAN: 'japan', // 日本 46 | USA: 'usa', // 美国 47 | UK: 'uk', // 英国 48 | FRANCE: 'france', // 法国 49 | GERMANY: 'germany', // 德国 50 | OTHER: 'other' // 其他 51 | } 52 | 53 | // 年份范围 54 | export const YEAR_RANGES = { 55 | RECENT: '2020-2024', // 近期 56 | CLASSIC: '2010-2019', // 经典 57 | OLD: '2000-2009', // 怀旧 58 | ANCIENT: '1990-1999' // 古典 59 | } 60 | 61 | // 请求状态枚举 62 | export const REQUEST_STATUS = { 63 | IDLE: 'idle', // 空闲 64 | LOADING: 'loading', // 加载中 65 | SUCCESS: 'success', // 成功 66 | ERROR: 'error' // 错误 67 | } 68 | 69 | // 缓存策略枚举 70 | export const CACHE_STRATEGY = { 71 | NO_CACHE: 'no-cache', // 不缓存 72 | CACHE_FIRST: 'cache-first', // 缓存优先 73 | NETWORK_FIRST: 'network-first', // 网络优先 74 | CACHE_ONLY: 'cache-only' // 仅缓存 75 | } 76 | 77 | /** 78 | * 视频信息数据结构 79 | */ 80 | export const createVideoInfo = () => ({ 81 | vod_id: '', // 视频ID 82 | vod_name: '', // 视频名称 83 | vod_pic: '', // 视频封面 84 | vod_remarks: '', // 视频备注 85 | vod_content: '', // 视频简介 86 | vod_play_from: '', // 播放来源 87 | vod_play_url: '', // 播放地址 88 | vod_time: '', // 更新时间 89 | vod_year: '', // 年份 90 | vod_area: '', // 地区 91 | vod_lang: '', // 语言 92 | vod_actor: '', // 演员 93 | vod_director: '', // 导演 94 | vod_writer: '', // 编剧 95 | vod_blurb: '', // 简介 96 | vod_class: '', // 分类 97 | vod_tag: '', // 标签 98 | vod_score: '', // 评分 99 | vod_hits: 0, // 点击量 100 | vod_duration: '', // 时长 101 | vod_total: 0, // 总集数 102 | vod_serial: 0, // 当前集数 103 | vod_tv: '', // 电视台 104 | vod_weekday: '', // 播出时间 105 | vod_status: VIDEO_STATUS.NORMAL 106 | }) 107 | 108 | /** 109 | * 分类信息数据结构 110 | */ 111 | export const createCategoryInfo = () => ({ 112 | type_id: '', // 分类ID 113 | type_name: '', // 分类名称 114 | type_sort: 0, // 排序 115 | type_status: 1 // 状态 116 | }) 117 | 118 | /** 119 | * 筛选条件数据结构 120 | */ 121 | export const createFilterInfo = () => ({ 122 | type: '', // 类型 123 | area: '', // 地区 124 | year: '', // 年份 125 | lang: '', // 语言 126 | sort: SORT_TYPES.TIME, // 排序方式 127 | order: SORT_ORDER.DESC // 排序顺序 128 | }) 129 | 130 | /** 131 | * 分页信息数据结构 132 | */ 133 | export const createPaginationInfo = () => ({ 134 | page: 1, // 当前页码 135 | pageSize: 20, // 每页数量 136 | total: 0, // 总数量 137 | totalPages: 0, // 总页数 138 | hasNext: false, // 是否有下一页 139 | hasPrev: false // 是否有上一页 140 | }) 141 | 142 | /** 143 | * 搜索参数数据结构 144 | */ 145 | export const createSearchParams = () => ({ 146 | keyword: '', // 搜索关键词 147 | type: '', // 搜索类型 148 | page: 1, // 页码 149 | pageSize: 20, // 每页数量 150 | filters: createFilterInfo() // 筛选条件 151 | }) 152 | 153 | /** 154 | * API响应数据结构 155 | */ 156 | export const createApiResponse = () => ({ 157 | code: 200, // 状态码 158 | msg: '', // 消息 159 | data: null, // 数据 160 | timestamp: Date.now() // 时间戳 161 | }) 162 | 163 | /** 164 | * 播放信息数据结构 165 | */ 166 | export const createPlayInfo = () => ({ 167 | url: '', // 播放地址 168 | type: '', // 播放类型 169 | headers: {}, // 请求头 170 | parse: false, // 是否需要解析 171 | jx: '' // 解析器 172 | }) 173 | 174 | /** 175 | * 站点信息数据结构 176 | */ 177 | export const createSiteInfo = () => ({ 178 | key: '', // 站点标识 179 | name: '', // 站点名称 180 | type: 0, // 站点类型 181 | api: '', // API地址 182 | searchable: 1, // 是否可搜索 183 | quickSearch: 1, // 是否支持快速搜索 184 | filterable: 1, // 是否可筛选 185 | order: 0, // 排序 186 | ext: '', // 扩展参数 187 | more: null // 额外配置信息(包含actions等) 188 | }) 189 | 190 | /** 191 | * 错误信息数据结构 192 | */ 193 | export const createErrorInfo = () => ({ 194 | code: '', // 错误码 195 | message: '', // 错误信息 196 | details: null, // 错误详情 197 | timestamp: Date.now() // 时间戳 198 | }) 199 | 200 | // 默认导出所有类型定义 201 | export default { 202 | VIDEO_TYPES, 203 | VIDEO_STATUS, 204 | SORT_TYPES, 205 | SORT_ORDER, 206 | REGIONS, 207 | YEAR_RANGES, 208 | REQUEST_STATUS, 209 | CACHE_STRATEGY, 210 | createVideoInfo, 211 | createCategoryInfo, 212 | createFilterInfo, 213 | createPaginationInfo, 214 | createSearchParams, 215 | createApiResponse, 216 | createPlayInfo, 217 | createSiteInfo, 218 | createErrorInfo 219 | } -------------------------------------------------------------------------------- /dashboard/src/stores/pageStateStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | // SessionStorage键名常量 4 | const STORAGE_KEY = 'drplayer_page_states'; 5 | 6 | // 从SessionStorage加载状态 7 | const loadFromStorage = () => { 8 | try { 9 | const stored = sessionStorage.getItem(STORAGE_KEY); 10 | if (stored) { 11 | const parsed = JSON.parse(stored); 12 | console.log('🔄 [存储] 从SessionStorage加载页面状态:', parsed); 13 | return parsed; 14 | } 15 | } catch (error) { 16 | console.error('从SessionStorage加载页面状态失败:', error); 17 | } 18 | 19 | // 返回默认状态 20 | return { 21 | // Video页面状态 22 | video: { 23 | activeKey: '', 24 | currentPage: 1, 25 | videos: [], 26 | hasMore: true, 27 | loading: false, 28 | scrollPosition: 0, 29 | lastUpdateTime: null 30 | }, 31 | // Home页面状态 32 | home: { 33 | scrollPosition: 0, 34 | lastUpdateTime: null 35 | }, 36 | // 搜索结果状态 37 | search: { 38 | keyword: '', 39 | currentPage: 1, 40 | videos: [], 41 | hasMore: true, 42 | loading: false, 43 | scrollPosition: 0, 44 | lastUpdateTime: null 45 | } 46 | }; 47 | }; 48 | 49 | // 保存到SessionStorage 50 | const saveToStorage = (pageStates) => { 51 | try { 52 | sessionStorage.setItem(STORAGE_KEY, JSON.stringify(pageStates)); 53 | console.log('🔄 [存储] 保存页面状态到SessionStorage:', pageStates); 54 | } catch (error) { 55 | console.error('保存页面状态到SessionStorage失败:', error); 56 | } 57 | }; 58 | 59 | export const usePageStateStore = defineStore('pageState', { 60 | state: () => ({ 61 | // 保存各个页面的状态,从SessionStorage初始化 62 | pageStates: loadFromStorage() 63 | }), 64 | 65 | actions: { 66 | // 保存页面状态 67 | savePageState(pageName, state) { 68 | if (!this.pageStates[pageName]) { 69 | this.pageStates[pageName] = {}; 70 | } 71 | 72 | // 合并状态,保留时间戳 73 | this.pageStates[pageName] = { 74 | ...this.pageStates[pageName], 75 | ...state, 76 | lastUpdateTime: Date.now() 77 | }; 78 | 79 | // 立即保存到SessionStorage 80 | saveToStorage(this.pageStates); 81 | 82 | console.log(`🔄 [状态保存] 页面状态 [${pageName}]:`, this.pageStates[pageName]); 83 | }, 84 | 85 | // 获取页面状态 86 | getPageState(pageName) { 87 | const state = this.pageStates[pageName]; 88 | console.log(`获取页面状态 [${pageName}]:`, state); 89 | return state || {}; 90 | }, 91 | 92 | // 清除页面状态 93 | clearPageState(pageName) { 94 | if (this.pageStates[pageName]) { 95 | this.pageStates[pageName] = {}; 96 | 97 | // 同步到SessionStorage 98 | saveToStorage(this.pageStates); 99 | 100 | console.log(`🔄 [状态清除] 页面状态 [${pageName}]`); 101 | } 102 | }, 103 | 104 | // 检查状态是否过期(超过30分钟) 105 | isStateExpired(pageName, maxAge = 30 * 60 * 1000) { 106 | const state = this.pageStates[pageName]; 107 | if (!state || !state.lastUpdateTime) { 108 | return true; 109 | } 110 | return Date.now() - state.lastUpdateTime > maxAge; 111 | }, 112 | 113 | // 保存Video页面特定状态 114 | saveVideoState(activeKey, currentPage, videos, hasMore, loading, scrollPosition = 0) { 115 | this.savePageState('video', { 116 | activeKey, 117 | currentPage, 118 | videos: [...videos], // 深拷贝数组 119 | hasMore, 120 | loading, 121 | scrollPosition 122 | }); 123 | }, 124 | 125 | // 保存搜索状态 126 | saveSearchState(keyword, currentPage, videos, hasMore, loading, scrollPosition = 0) { 127 | this.savePageState('search', { 128 | keyword, 129 | currentPage, 130 | videos: [...videos], // 深拷贝数组 131 | hasMore, 132 | loading, 133 | scrollPosition 134 | }); 135 | }, 136 | 137 | // 保存滚动位置 138 | saveScrollPosition(pageName, position) { 139 | if (this.pageStates[pageName]) { 140 | this.pageStates[pageName].scrollPosition = position; 141 | this.pageStates[pageName].lastUpdateTime = Date.now(); 142 | 143 | // 同步到SessionStorage 144 | saveToStorage(this.pageStates); 145 | } 146 | }, 147 | 148 | // 获取滚动位置 149 | getScrollPosition(pageName) { 150 | const state = this.pageStates[pageName]; 151 | return state ? state.scrollPosition || 0 : 0; 152 | }, 153 | 154 | // 重新从SessionStorage加载状态 155 | reloadFromStorage() { 156 | this.pageStates = loadFromStorage(); 157 | console.log('🔄 [存储] 重新加载页面状态:', this.pageStates); 158 | }, 159 | 160 | // 清除所有SessionStorage数据 161 | clearAllStorage() { 162 | try { 163 | sessionStorage.removeItem(STORAGE_KEY); 164 | this.pageStates = loadFromStorage(); // 重置为默认状态 165 | console.log('🔄 [存储] 清除所有页面状态'); 166 | } catch (error) { 167 | console.error('清除SessionStorage失败:', error); 168 | } 169 | } 170 | }, 171 | 172 | getters: { 173 | // 获取Video页面状态 174 | videoState: (state) => state.pageStates.video || {}, 175 | 176 | // 获取搜索状态 177 | searchState: (state) => state.pageStates.search || {}, 178 | 179 | // 获取Home页面状态 180 | homeState: (state) => state.pageStates.home || {} 181 | } 182 | }); -------------------------------------------------------------------------------- /dashboard/src/components/actions/index.js: -------------------------------------------------------------------------------- 1 | // Action组件统一导出文件 2 | 3 | // 导入所有组件 4 | import ActionRenderer from './ActionRenderer.vue' 5 | import ActionDialog from './ActionDialog.vue' 6 | import InputAction from './InputAction.vue' 7 | import MultiInputAction from './MultiInputAction.vue' 8 | import MenuAction from './MenuAction.vue' 9 | 10 | import MsgBoxAction from './MsgBoxAction.vue' 11 | import WebViewAction from './WebViewAction.vue' 12 | import HelpAction from './HelpAction.vue' 13 | 14 | // 导入类型定义和工具函数 15 | import * as types from './types.js' 16 | 17 | // 导入状态管理器 18 | import { 19 | ActionStateManager, 20 | actionStateManager, 21 | showAction, 22 | submitAction, 23 | cancelAction, 24 | actionError, 25 | currentAction, 26 | actionHistory, 27 | actionQueue, 28 | statistics, 29 | globalConfig 30 | } from './ActionStateManager.js' 31 | 32 | // 导入样式 33 | import './styles.css' 34 | 35 | // 组件列表 36 | const components = { 37 | ActionRenderer, 38 | ActionDialog, 39 | InputAction, 40 | MultiInputAction, 41 | MenuAction, 42 | MsgBoxAction, 43 | WebViewAction, 44 | HelpAction 45 | } 46 | 47 | // Vue插件安装函数 48 | const install = (app, options = {}) => { 49 | // 注册所有组件 50 | Object.keys(components).forEach(name => { 51 | app.component(name, components[name]) 52 | }) 53 | 54 | // 配置全局状态管理器 55 | if (options.config) { 56 | actionStateManager.updateConfig(options.config) 57 | } 58 | 59 | // 注册全局属性 60 | app.config.globalProperties.$actionManager = actionStateManager 61 | app.config.globalProperties.$showAction = showAction 62 | 63 | // 提供依赖注入 64 | app.provide('actionManager', actionStateManager) 65 | app.provide('showAction', showAction) 66 | } 67 | 68 | // 默认导出(Vue插件) 69 | export default { 70 | install, 71 | ...components 72 | } 73 | 74 | // 单独导出组件 75 | export { 76 | ActionRenderer, 77 | ActionDialog, 78 | InputAction, 79 | MultiInputAction, 80 | MenuAction, 81 | MsgBoxAction, 82 | WebViewAction, 83 | HelpAction 84 | } 85 | 86 | // 导出类型和工具 87 | export { 88 | types, 89 | ActionStateManager, 90 | actionStateManager, 91 | showAction, 92 | submitAction, 93 | cancelAction, 94 | actionError, 95 | currentAction, 96 | actionHistory, 97 | actionQueue, 98 | statistics, 99 | globalConfig 100 | } 101 | 102 | // 导出便捷方法 103 | export const Actions = { 104 | // 显示输入框 105 | input: (config) => showAction({ ...config, type: types.ActionType.INPUT }), 106 | 107 | // 显示多行编辑框 108 | edit: (config) => showAction({ ...config, type: types.ActionType.EDIT }), 109 | 110 | // 显示多输入框 111 | multiInput: (config) => showAction({ ...config, type: types.ActionType.MULTI_INPUT }), 112 | 113 | // 显示增强多输入框 114 | multiInputX: (config) => showAction({ ...config, type: types.ActionType.MULTI_INPUT_X }), 115 | 116 | // 显示菜单 117 | menu: (config) => showAction({ ...config, type: types.ActionType.MENU }), 118 | 119 | // 显示选择框 120 | select: (config) => showAction({ ...config, type: types.ActionType.SELECT }), 121 | 122 | // 显示消息框 123 | msgBox: (config) => showAction({ ...config, type: types.ActionType.MSGBOX }), 124 | 125 | // 显示网页视图 126 | webView: (config) => showAction({ ...config, type: types.ActionType.WEBVIEW }), 127 | 128 | // 显示帮助 129 | help: (config) => showAction({ ...config, type: types.ActionType.HELP }), 130 | 131 | // 显示确认对话框 132 | confirm: (message, title = '确认') => showAction({ 133 | actionId: `confirm-${Date.now()}`, 134 | type: types.ActionType.MSGBOX, 135 | msg: message, 136 | title, 137 | button: types.ButtonType.OK_CANCEL 138 | }), 139 | 140 | // 显示警告对话框 141 | alert: (message, title = '提示') => showAction({ 142 | actionId: `alert-${Date.now()}`, 143 | type: types.ActionType.MSGBOX, 144 | msg: message, 145 | title, 146 | button: types.ButtonType.OK_ONLY 147 | }), 148 | 149 | // 显示信息对话框 150 | info: (message, title = '信息') => showAction({ 151 | actionId: `info-${Date.now()}`, 152 | type: types.ActionType.MSGBOX, 153 | msg: message, 154 | title, 155 | button: types.ButtonType.OK_ONLY, 156 | icon: 'info' 157 | }), 158 | 159 | // 显示成功对话框 160 | success: (message, title = '成功') => showAction({ 161 | actionId: `success-${Date.now()}`, 162 | type: types.ActionType.MSGBOX, 163 | msg: message, 164 | title, 165 | button: types.ButtonType.OK_ONLY, 166 | icon: 'success' 167 | }), 168 | 169 | // 显示错误对话框 170 | error: (message, title = '错误') => showAction({ 171 | actionId: `error-${Date.now()}`, 172 | type: types.ActionType.MSGBOX, 173 | msg: message, 174 | title, 175 | button: types.ButtonType.OK_ONLY, 176 | icon: 'error' 177 | }), 178 | 179 | // 显示警告对话框 180 | warning: (message, title = '警告') => showAction({ 181 | actionId: `warning-${Date.now()}`, 182 | type: types.ActionType.MSGBOX, 183 | msg: message, 184 | title, 185 | button: types.ButtonType.OK_ONLY, 186 | icon: 'warning' 187 | }), 188 | 189 | // 显示加载对话框 190 | loading: (message = '加载中...', title = '请稍候') => showAction({ 191 | actionId: `loading-${Date.now()}`, 192 | type: types.ActionType.MSGBOX, 193 | msg: message, 194 | title, 195 | button: types.ButtonType.NONE, 196 | showProgress: true 197 | }), 198 | 199 | // 显示进度对话框 200 | progress: (message, title = '进度', progress = 0) => showAction({ 201 | actionId: `progress-${Date.now()}`, 202 | type: types.ActionType.MSGBOX, 203 | msg: message, 204 | title, 205 | button: types.ButtonType.CANCEL_ONLY, 206 | showProgress: true, 207 | progress 208 | }) 209 | } 210 | 211 | // 导出配置创建函数 212 | export const createActionConfig = types.createActionConfig 213 | export const createInputActionConfig = types.createInputActionConfig 214 | export const createMultiInputActionConfig = types.createMultiInputActionConfig 215 | export const createMenuActionConfig = types.createMenuActionConfig 216 | 217 | export const createMsgBoxActionConfig = types.createMsgBoxActionConfig 218 | export const createWebViewActionConfig = types.createWebViewActionConfig 219 | export const createHelpActionConfig = types.createHelpActionConfig -------------------------------------------------------------------------------- /dashboard/src/stores/downloadStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | import { downloadService } from '@/services/downloadService' 4 | import localBookService from '@/services/localBookService' 5 | import { Message, Modal } from '@arco-design/web-vue' 6 | 7 | export const useDownloadStore = defineStore('download', () => { 8 | // 任务列表 9 | const tasks = ref([]) 10 | 11 | // 加载状态 12 | const loading = ref(false) 13 | 14 | // 从downloadService加载任务 15 | const loadTasks = () => { 16 | tasks.value = downloadService.getAllTasks() 17 | } 18 | 19 | // 保存任务到downloadService 20 | const saveTasks = () => { 21 | downloadService.saveTasksToStorage() 22 | } 23 | 24 | // 添加下载任务 25 | const addTask = (taskData) => { 26 | const task = downloadService.createTask(taskData) 27 | loadTasks() // 重新加载任务列表 28 | return task 29 | } 30 | 31 | // 开始任务 32 | const startTask = (taskId) => { 33 | downloadService.startTask(taskId) 34 | loadTasks() 35 | } 36 | 37 | // 暂停任务 38 | const pauseTask = (taskId) => { 39 | downloadService.pauseTask(taskId) 40 | loadTasks() 41 | } 42 | 43 | // 恢复任务 44 | const resumeTask = (taskId) => { 45 | downloadService.startTask(taskId) 46 | loadTasks() 47 | } 48 | 49 | // 取消任务 50 | const cancelTask = (taskId) => { 51 | downloadService.cancelTask(taskId) 52 | loadTasks() 53 | } 54 | 55 | // 删除任务 56 | const deleteTask = (taskId) => { 57 | downloadService.deleteTask(taskId) 58 | loadTasks() 59 | } 60 | 61 | // 重试任务 62 | const retryTask = (taskId) => { 63 | downloadService.startTask(taskId) 64 | loadTasks() 65 | } 66 | 67 | // 重试章节 68 | const retryChapter = (taskId, chapterIndex) => { 69 | downloadService.retryChapter(taskId, chapterIndex) 70 | loadTasks() 71 | } 72 | 73 | // 导出任务 74 | const exportTask = async (taskId, options = {}) => { 75 | try { 76 | const task = downloadService.getTask(taskId) 77 | if (!task) { 78 | Message.error('任务不存在') 79 | return 80 | } 81 | 82 | if (task.status !== 'completed') { 83 | Message.error('只能导出已完成的任务') 84 | return 85 | } 86 | 87 | // 生成TXT内容 88 | const txtContent = downloadService.generateTxtContent(taskId) 89 | if (!txtContent) { 90 | Message.error('生成TXT内容失败') 91 | return 92 | } 93 | 94 | // 如果选择导出到书画柜 95 | if (options.exportToGallery) { 96 | const bookData = { 97 | title: task.novelTitle, 98 | author: task.novelAuthor || '未知', 99 | description: task.novelDescription || '', 100 | cover: task.novelCover || '', 101 | content: txtContent, 102 | fileName: `${task.settings.fileName || task.novelTitle}.txt`, 103 | addedAt: Date.now(), 104 | source: 'download' 105 | } 106 | 107 | // 尝试添加图书,检查是否重复 108 | const result = localBookService.addBookFromContent(bookData) 109 | 110 | if (result.success) { 111 | const action = result.isOverwrite ? '更新' : '添加' 112 | Message.success(`《${task.novelTitle}》已${action}到书画柜`) 113 | return { success: true, action: 'addToGallery', isOverwrite: result.isOverwrite } 114 | } else if (result.duplicate) { 115 | // 显示覆盖确认对话框 116 | return new Promise((resolve) => { 117 | Modal.confirm({ 118 | title: '图书已存在', 119 | content: `书画柜中已存在《${bookData.title}》(作者:${bookData.author}),是否要覆盖现有图书?`, 120 | okText: '覆盖', 121 | cancelText: '取消', 122 | onOk: () => { 123 | // 用户选择覆盖 124 | const overwriteResult = localBookService.addBookFromContent(bookData, { allowOverwrite: true }) 125 | if (overwriteResult.success) { 126 | Message.success(`《${task.novelTitle}》已更新到书画柜`) 127 | resolve({ success: true, action: 'addToGallery', isOverwrite: true }) 128 | } else { 129 | Message.error('更新图书失败') 130 | resolve({ success: false }) 131 | } 132 | }, 133 | onCancel: () => { 134 | Message.info('已取消导出') 135 | resolve({ success: false, cancelled: true }) 136 | } 137 | }) 138 | }) 139 | } else if (result.storageLimit) { 140 | // 存储空间不足 141 | Message.error(result.message) 142 | return { success: false, storageLimit: true } 143 | } else { 144 | Message.error(result.message || '添加图书失败') 145 | return { success: false } 146 | } 147 | } else { 148 | // 默认下载TXT文件 149 | const result = downloadService.exportToTxt(taskId) 150 | Message.success('TXT文件导出成功') 151 | return result 152 | } 153 | } catch (error) { 154 | console.error('导出任务失败:', error) 155 | Message.error('导出失败: ' + error.message) 156 | return null 157 | } 158 | } 159 | 160 | // 清理已完成的任务 161 | const clearCompleted = () => { 162 | const completedTasks = tasks.value.filter(task => task.status === 'completed') 163 | completedTasks.forEach(task => { 164 | downloadService.deleteTask(task.id) 165 | }) 166 | loadTasks() 167 | } 168 | 169 | // 计算属性 170 | const taskStats = computed(() => { 171 | return downloadService.getTaskStats() 172 | }) 173 | 174 | // 根据状态过滤任务 175 | const getTasksByStatus = (status) => { 176 | return downloadService.getTasksByStatus(status) 177 | } 178 | 179 | // 设置任务更新回调 180 | downloadService.setTaskUpdateCallback(() => { 181 | loadTasks() 182 | }) 183 | 184 | // 初始化时加载任务 185 | loadTasks() 186 | 187 | return { 188 | tasks, 189 | loading, 190 | taskStats, 191 | loadTasks, 192 | saveTasks, 193 | addTask, 194 | startTask, 195 | pauseTask, 196 | resumeTask, 197 | cancelTask, 198 | deleteTask, 199 | retryTask, 200 | retryChapter, 201 | exportTask, 202 | clearCompleted, 203 | getTasksByStatus 204 | } 205 | }) -------------------------------------------------------------------------------- /dashboard/src/utils/fileTypeUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件类型识别工具函数 3 | * 根据文件名返回对应的 iconfont 图标类名 4 | */ 5 | 6 | /** 7 | * 根据文件名获取对应的文件类型图标类名 8 | * @param {string} fileName - 文件名 9 | * @returns {string} 对应的 iconfont 图标类名 10 | */ 11 | export function getFileTypeIcon(fileName) { 12 | if (!fileName || typeof fileName !== 'string') { 13 | return 'icon-file' 14 | } 15 | 16 | // 获取文件扩展名(转为小写) 17 | const extension = fileName.toLowerCase().split('.').pop() 18 | 19 | // 文档类型 20 | const documentTypes = { 21 | 'doc': 'icon-file_word', 22 | 'docx': 'icon-file_word', 23 | 'xls': 'icon-file_excel', 24 | 'xlsx': 'icon-file_excel', 25 | 'ppt': 'icon-file_ppt', 26 | 'pptx': 'icon-file_ppt', 27 | 'pdf': 'icon-file_pdf', 28 | 'txt': 'icon-file_txt', 29 | 'rtf': 'icon-file_txt', 30 | 'md': 'icon-file_txt' 31 | } 32 | 33 | // 图片类型 34 | const imageTypes = { 35 | 'jpg': 'icon-file_img', 36 | 'jpeg': 'icon-file_img', 37 | 'png': 'icon-file_img', 38 | 'gif': 'icon-file_img', 39 | 'bmp': 'icon-file_img', 40 | 'svg': 'icon-file_img', 41 | 'webp': 'icon-file_img', 42 | 'ico': 'icon-file_img' 43 | } 44 | 45 | // 视频类型 46 | const videoTypes = { 47 | 'mp4': 'icon-file_video', 48 | 'avi': 'icon-file_video', 49 | 'mkv': 'icon-file_video', 50 | 'mov': 'icon-file_video', 51 | 'wmv': 'icon-file_video', 52 | 'flv': 'icon-file_video', 53 | 'webm': 'icon-file_video', 54 | 'm4v': 'icon-file_video', 55 | 'rmvb': 'icon-file_video', 56 | 'rm': 'icon-file_video' 57 | } 58 | 59 | // 音频类型 60 | const audioTypes = { 61 | 'mp3': 'icon-file_music', 62 | 'wav': 'icon-file_music', 63 | 'flac': 'icon-file_music', 64 | 'aac': 'icon-file_music', 65 | 'ogg': 'icon-file_music', 66 | 'wma': 'icon-file_music', 67 | 'm4a': 'icon-file_music' 68 | } 69 | 70 | // 压缩包类型 71 | const archiveTypes = { 72 | 'zip': 'icon-file_zip', 73 | 'rar': 'icon-file_zip', 74 | '7z': 'icon-file_zip', 75 | 'tar': 'icon-file_zip', 76 | 'gz': 'icon-file_zip', 77 | 'bz2': 'icon-file_zip' 78 | } 79 | 80 | // 可执行文件类型 81 | const executableTypes = { 82 | 'exe': 'icon-file_exe', 83 | 'msi': 'icon-file_exe', 84 | 'dmg': 'icon-file_exe', 85 | 'pkg': 'icon-file_exe', 86 | 'deb': 'icon-file_exe', 87 | 'rpm': 'icon-file_exe' 88 | } 89 | 90 | // 代码文件类型 91 | const codeTypes = { 92 | 'html': 'icon-file_html', 93 | 'htm': 'icon-file_html', 94 | 'css': 'icon-file_code', 95 | 'js': 'icon-file_code', 96 | 'ts': 'icon-file_code', 97 | 'vue': 'icon-file_code', 98 | 'jsx': 'icon-file_code', 99 | 'tsx': 'icon-file_code', 100 | 'php': 'icon-file_code', 101 | 'py': 'icon-file_code', 102 | 'java': 'icon-file_code', 103 | 'cpp': 'icon-file_code', 104 | 'c': 'icon-file_code', 105 | 'cs': 'icon-file_code', 106 | 'go': 'icon-file_code', 107 | 'rs': 'icon-file_code', 108 | 'swift': 'icon-file_code', 109 | 'kt': 'icon-file_code', 110 | 'rb': 'icon-file_code', 111 | 'json': 'icon-file_code', 112 | 'xml': 'icon-file_code', 113 | 'yaml': 'icon-file_code', 114 | 'yml': 'icon-file_code' 115 | } 116 | 117 | // 设计文件类型 118 | const designTypes = { 119 | 'ai': 'icon-file_ai', 120 | 'psd': 'icon-file_psd', 121 | 'sketch': 'icon-file_psd', 122 | 'fig': 'icon-file_psd', 123 | 'xd': 'icon-file_psd' 124 | } 125 | 126 | // CAD文件类型 127 | const cadTypes = { 128 | 'dwg': 'icon-file_cad', 129 | 'dxf': 'icon-file_cad', 130 | 'step': 'icon-file_cad', 131 | 'iges': 'icon-file_cad' 132 | } 133 | 134 | // Flash文件类型 135 | const flashTypes = { 136 | 'swf': 'icon-file_flash', 137 | 'fla': 'icon-file_flash' 138 | } 139 | 140 | // ISO文件类型 141 | const isoTypes = { 142 | 'iso': 'icon-file_iso', 143 | 'img': 'icon-file_iso', 144 | 'bin': 'icon-file_iso' 145 | } 146 | 147 | // BT种子文件 148 | const btTypes = { 149 | 'torrent': 'icon-file_bt' 150 | } 151 | 152 | // 云文件类型(一些云存储相关的文件) 153 | const cloudTypes = { 154 | 'cloud': 'icon-file_cloud', 155 | 'gdoc': 'icon-file_cloud', 156 | 'gsheet': 'icon-file_cloud', 157 | 'gslides': 'icon-file_cloud' 158 | } 159 | 160 | // 按优先级检查文件类型 161 | if (documentTypes[extension]) return documentTypes[extension] 162 | if (imageTypes[extension]) return imageTypes[extension] 163 | if (videoTypes[extension]) return videoTypes[extension] 164 | if (audioTypes[extension]) return audioTypes[extension] 165 | if (archiveTypes[extension]) return archiveTypes[extension] 166 | if (executableTypes[extension]) return executableTypes[extension] 167 | if (codeTypes[extension]) return codeTypes[extension] 168 | if (designTypes[extension]) return designTypes[extension] 169 | if (cadTypes[extension]) return cadTypes[extension] 170 | if (flashTypes[extension]) return flashTypes[extension] 171 | if (isoTypes[extension]) return isoTypes[extension] 172 | if (btTypes[extension]) return btTypes[extension] 173 | if (cloudTypes[extension]) return cloudTypes[extension] 174 | 175 | // 默认返回通用文件图标 176 | return 'icon-file' 177 | } 178 | 179 | /** 180 | * 检查项目是否为文件夹类型 181 | * @param {Object} item - 项目对象 182 | * @returns {boolean} 是否为文件夹 183 | */ 184 | export function isFolder(item) { 185 | return item && item.vod_tag && item.vod_tag.includes('folder') 186 | } 187 | 188 | /** 189 | * 检查项目是否为目录模式下的文件类型 190 | * @param {Object} item - 项目对象 191 | * @returns {boolean} 是否为目录模式下的文件 192 | */ 193 | export function isDirectoryFile(item) { 194 | return item && item.vod_tag && !item.vod_tag.includes('folder') 195 | } 196 | 197 | /** 198 | * 获取项目的显示图标类型 199 | * @param {Object} item - 项目对象 200 | * @returns {Object} 图标信息 { type: 'folder'|'file'|'image', iconClass?: string } 201 | */ 202 | export function getItemIconInfo(item) { 203 | if (!item) { 204 | return { type: 'image' } 205 | } 206 | 207 | if (isFolder(item)) { 208 | return { type: 'folder', iconClass: 'icon-wenjianjia' } 209 | } 210 | 211 | if (isDirectoryFile(item)) { 212 | return { type: 'file', iconClass: getFileTypeIcon(item.vod_name) } 213 | } 214 | 215 | return { type: 'image' } 216 | } -------------------------------------------------------------------------------- /dashboard/src/stores/favoriteStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | 4 | export const useFavoriteStore = defineStore('favorite', () => { 5 | // 收藏列表 6 | const favorites = ref([]) 7 | 8 | // 从localStorage加载收藏数据 9 | const loadFavorites = () => { 10 | try { 11 | const stored = localStorage.getItem('drplayer-favorites') 12 | if (stored) { 13 | favorites.value = JSON.parse(stored) 14 | } 15 | } catch (error) { 16 | console.error('加载收藏数据失败:', error) 17 | favorites.value = [] 18 | } 19 | } 20 | 21 | // 保存收藏数据到localStorage 22 | const saveFavorites = () => { 23 | try { 24 | localStorage.setItem('drplayer-favorites', JSON.stringify(favorites.value)) 25 | } catch (error) { 26 | console.error('保存收藏数据失败:', error) 27 | } 28 | } 29 | 30 | // 添加收藏 31 | const addFavorite = (videoData) => { 32 | const favoriteItem = { 33 | id: videoData.vod_id, 34 | name: videoData.vod_name, 35 | pic: videoData.vod_pic, 36 | year: videoData.vod_year, 37 | area: videoData.vod_area, 38 | type_name: videoData.type_name, 39 | remarks: videoData.vod_remarks, 40 | director: videoData.vod_director, 41 | actor: videoData.vod_actor, 42 | // 保存API调用信息,用于从收藏进入详情页 43 | api_info: { 44 | module: videoData.module || '', 45 | api_url: videoData.api_url || '', 46 | site_name: videoData.site_name || '', 47 | ext: videoData.ext || null // 添加站源扩展配置 48 | }, 49 | created_at: new Date().toISOString(), 50 | updated_at: new Date().toISOString() 51 | } 52 | 53 | // 检查是否已存在 54 | const existingIndex = favorites.value.findIndex(item => 55 | item.id === favoriteItem.id && 56 | item.api_info.api_url === favoriteItem.api_info.api_url 57 | ) 58 | 59 | if (existingIndex === -1) { 60 | favorites.value.unshift(favoriteItem) 61 | saveFavorites() 62 | return true 63 | } 64 | return false 65 | } 66 | 67 | // 移除收藏 68 | const removeFavorite = (videoId, apiUrl) => { 69 | const index = favorites.value.findIndex(item => 70 | item.id === videoId && item.api_info.api_url === apiUrl 71 | ) 72 | 73 | if (index !== -1) { 74 | favorites.value.splice(index, 1) 75 | saveFavorites() 76 | return true 77 | } 78 | return false 79 | } 80 | 81 | // 检查是否已收藏 82 | const isFavorited = (videoId, apiUrl) => { 83 | return favorites.value.some(item => 84 | item.id === videoId && item.api_info.api_url === apiUrl 85 | ) 86 | } 87 | 88 | // 获取收藏项 89 | const getFavorite = (videoId, apiUrl) => { 90 | return favorites.value.find(item => 91 | item.id === videoId && item.api_info.api_url === apiUrl 92 | ) 93 | } 94 | 95 | // 清空收藏 96 | const clearFavorites = () => { 97 | favorites.value = [] 98 | saveFavorites() 99 | } 100 | 101 | // 导出收藏数据 102 | const exportFavorites = () => { 103 | const exportData = { 104 | version: '1.0', 105 | export_time: new Date().toISOString(), 106 | favorites: favorites.value 107 | } 108 | 109 | const blob = new Blob([JSON.stringify(exportData, null, 2)], { 110 | type: 'application/json' 111 | }) 112 | 113 | const url = URL.createObjectURL(blob) 114 | const a = document.createElement('a') 115 | a.href = url 116 | a.download = `drplayer-favorites-${new Date().toISOString().split('T')[0]}.json` 117 | document.body.appendChild(a) 118 | a.click() 119 | document.body.removeChild(a) 120 | URL.revokeObjectURL(url) 121 | } 122 | 123 | // 导入收藏数据 124 | const importFavorites = (file) => { 125 | return new Promise((resolve, reject) => { 126 | const reader = new FileReader() 127 | 128 | reader.onload = (e) => { 129 | try { 130 | const importData = JSON.parse(e.target.result) 131 | 132 | // 验证数据格式 133 | if (!importData.favorites || !Array.isArray(importData.favorites)) { 134 | throw new Error('无效的收藏数据格式') 135 | } 136 | 137 | // 合并数据,避免重复 138 | let importCount = 0 139 | importData.favorites.forEach(item => { 140 | const exists = favorites.value.some(existing => 141 | existing.id === item.id && 142 | existing.api_info.api_url === item.api_info.api_url 143 | ) 144 | 145 | if (!exists) { 146 | favorites.value.push({ 147 | ...item, 148 | updated_at: new Date().toISOString() 149 | }) 150 | importCount++ 151 | } 152 | }) 153 | 154 | saveFavorites() 155 | resolve(importCount) 156 | } catch (error) { 157 | reject(error) 158 | } 159 | } 160 | 161 | reader.onerror = () => { 162 | reject(new Error('文件读取失败')) 163 | } 164 | 165 | reader.readAsText(file) 166 | }) 167 | } 168 | 169 | // 计算属性 170 | const favoriteCount = computed(() => favorites.value.length) 171 | 172 | const favoritesByType = computed(() => { 173 | const grouped = {} 174 | favorites.value.forEach(item => { 175 | // 根据站源名称中的标识进行分类 176 | const siteName = item.api_info?.site_name || '' 177 | let type = '影视' // 默认分类 178 | 179 | if (siteName.includes('[书]')) { 180 | type = '小说' 181 | } else if (siteName.includes('[画]')) { 182 | type = '漫画' 183 | } else if (siteName.includes('[密]')) { 184 | type = '密' 185 | } else if (siteName.includes('[听]')) { 186 | type = '音频' 187 | } else if (siteName.includes('[儿]')) { 188 | type = '少儿' 189 | } 190 | 191 | if (!grouped[type]) { 192 | grouped[type] = [] 193 | } 194 | grouped[type].push(item) 195 | }) 196 | return grouped 197 | }) 198 | 199 | // 初始化时加载数据 200 | loadFavorites() 201 | 202 | return { 203 | favorites, 204 | favoriteCount, 205 | favoritesByType, 206 | addFavorite, 207 | removeFavorite, 208 | isFavorited, 209 | getFavorite, 210 | clearFavorites, 211 | exportFavorites, 212 | importFavorites, 213 | loadFavorites, 214 | saveFavorites 215 | } 216 | }) -------------------------------------------------------------------------------- /dashboard/src/components/players/LiveProxySelector.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 174 | 175 | -------------------------------------------------------------------------------- /dashboard/src/api/services/sniffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 嗅探服务 API 3 | * 用于调用代理嗅探接口进行视频链接嗅探 4 | */ 5 | 6 | /** 7 | * 调用嗅探接口 8 | * @param {string|Object} urlOrParseData - 要嗅探的页面URL或T4解析数据 9 | * @param {Object} options - 嗅探选项 10 | * @param {string} options.snifferUrl - 嗅探器接口地址 11 | * @param {number} options.timeout - 嗅探器服务端超时时间(秒),来自设置页面的"嗅探超时"配置 12 | * @param {string} options.mode - 嗅探模式:0=单个链接,1=多个链接 13 | * @param {string} options.is_pc - 设备模拟:0=移动设备,1=PC 14 | * @returns {Promise} 嗅探结果 15 | */ 16 | export const sniffVideo = async (urlOrParseData, options = {}) => { 17 | const { 18 | snifferUrl = 'http://localhost:57573/sniffer', 19 | timeout = 10, 20 | mode = '0', 21 | is_pc = '0' 22 | } = options 23 | 24 | if (!urlOrParseData) { 25 | throw new Error('URL或解析数据参数不能为空') 26 | } 27 | 28 | if (!snifferUrl) { 29 | throw new Error('嗅探器接口地址不能为空') 30 | } 31 | 32 | try { 33 | let requestUrl 34 | 35 | // 检查是否是T4解析数据格式 36 | if (typeof urlOrParseData === 'object' && urlOrParseData.parse === 1) { 37 | // T4解析数据格式:{ parse: 1, url: string, js: string, parse_extra: string } 38 | const { url, js, parse_extra } = urlOrParseData 39 | 40 | if (!url) { 41 | throw new Error('T4解析数据中缺少URL') 42 | } 43 | 44 | // 确保URL是字符串格式 45 | const urlString = typeof url === 'string' ? url : (url.toString ? url.toString() : String(url)) 46 | 47 | console.log('处理T4解析数据:', urlOrParseData) 48 | console.log('提取的URL:', urlString) 49 | console.log('=== 调试结束 ===') 50 | 51 | // 构建基础参数 52 | const params = new URLSearchParams({ 53 | url: urlString, 54 | mode: mode, 55 | is_pc: is_pc, 56 | timeout: (timeout * 1000).toString() // 嗅探器服务端超时时间(毫秒) 57 | }) 58 | 59 | // 添加脚本参数 60 | if (js) { 61 | params.set('script', js) 62 | } 63 | 64 | // 构建请求URL 65 | requestUrl = `${snifferUrl}?${params.toString()}` 66 | 67 | // 添加额外参数(parse_extra) 68 | if (parse_extra) { 69 | // parse_extra 通常以 & 开头,直接拼接 70 | requestUrl += parse_extra 71 | } 72 | 73 | } else { 74 | // 普通URL格式 75 | const url = typeof urlOrParseData === 'string' ? urlOrParseData : urlOrParseData.toString() 76 | 77 | // 构建请求参数 78 | const params = new URLSearchParams({ 79 | url: url, 80 | mode: mode, 81 | is_pc: is_pc, 82 | timeout: (timeout * 1000).toString() // 嗅探器服务端超时时间(毫秒) 83 | }) 84 | 85 | requestUrl = `${snifferUrl}?${params.toString()}` 86 | } 87 | 88 | console.log('嗅探请求URL:', requestUrl) 89 | 90 | // 发起嗅探请求,设置客户端超时 91 | const controller = new AbortController() 92 | // 客户端超时 = 嗅探器超时 + 5秒网络缓冲时间,确保嗅探器有足够时间完成工作 93 | const timeoutId = setTimeout(() => controller.abort(), timeout * 1000 + 5000) 94 | 95 | const response = await fetch(requestUrl, { 96 | method: 'GET', 97 | signal: controller.signal, 98 | headers: { 99 | 'Accept': 'application/json', 100 | 'Content-Type': 'application/json' 101 | } 102 | }) 103 | 104 | clearTimeout(timeoutId) 105 | 106 | if (!response.ok) { 107 | throw new Error(`嗅探请求失败: ${response.status} ${response.statusText}`) 108 | } 109 | 110 | const result = await response.json() 111 | console.log('嗅探响应结果:', result) 112 | 113 | // 检查响应格式 114 | if (result.code !== 200) { 115 | throw new Error(result.msg || '嗅探失败') 116 | } 117 | 118 | // 返回标准化的结果 119 | return { 120 | success: true, 121 | data: result.data, 122 | message: result.msg || '嗅探成功', 123 | timestamp: result.timestamp 124 | } 125 | 126 | } catch (error) { 127 | console.error('嗅探请求失败:', error) 128 | 129 | // 处理不同类型的错误 130 | if (error.name === 'AbortError') { 131 | throw new Error(`嗅探超时(${timeout}秒)`) 132 | } else if (error.message.includes('Failed to fetch')) { 133 | throw new Error('无法连接到嗅探器服务,请检查嗅探器是否正常运行') 134 | } else { 135 | throw error 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * 从本地存储获取嗅探器配置 142 | * @returns {Object} 嗅探器配置 143 | */ 144 | export const getSnifferConfig = () => { 145 | try { 146 | const savedAddresses = localStorage.getItem('addressSettings') 147 | if (savedAddresses) { 148 | const parsed = JSON.parse(savedAddresses) 149 | return { 150 | enabled: parsed.proxySniffEnabled || false, 151 | url: parsed.proxySniff || 'http://localhost:57573/sniffer', 152 | timeout: parsed.snifferTimeout || 10 153 | } 154 | } 155 | } catch (error) { 156 | console.error('获取嗅探器配置失败:', error) 157 | } 158 | 159 | // 返回默认配置 160 | return { 161 | enabled: false, 162 | url: 'http://localhost:57573/sniffer', 163 | timeout: 10 164 | } 165 | } 166 | 167 | /** 168 | * 检查嗅探器是否启用 169 | * @returns {boolean} 是否启用 170 | */ 171 | export const isSnifferEnabled = () => { 172 | const config = getSnifferConfig() 173 | 174 | // 检查URL是否有效 175 | const hasValidUrl = config.url && config.url.trim() !== '' && config.url !== 'undefined' 176 | 177 | if (!hasValidUrl) { 178 | return false 179 | } 180 | 181 | // 检查是否有保存的设置 182 | try { 183 | const savedAddresses = localStorage.getItem('addressSettings') 184 | if (savedAddresses) { 185 | const parsed = JSON.parse(savedAddresses) 186 | // 如果用户明确保存了设置,使用保存的enabled状态 187 | return parsed.proxySniffEnabled === true 188 | } 189 | } catch (error) { 190 | console.error('检查嗅探器配置失败:', error) 191 | } 192 | 193 | // 如果没有保存的设置,但有有效的URL(包括默认URL),认为嗅探可用 194 | // 这样可以处理用户打开开关但未保存设置的情况 195 | return true 196 | } 197 | 198 | /** 199 | * 嗅探视频链接(带配置检查) 200 | * @param {string} url - 要嗅探的页面URL 201 | * @param {Object} customOptions - 自定义选项(可选) 202 | * @returns {Promise} 嗅探结果 203 | */ 204 | export const sniffVideoWithConfig = async (url, customOptions = {}) => { 205 | // 获取配置 206 | const config = getSnifferConfig() 207 | 208 | if (!config.enabled) { 209 | throw new Error('嗅探功能未启用') 210 | } 211 | 212 | if (!config.url) { 213 | throw new Error('嗅探器接口地址未配置') 214 | } 215 | 216 | // 合并配置和自定义选项 217 | const options = { 218 | snifferUrl: config.url, 219 | timeout: config.timeout, 220 | ...customOptions 221 | } 222 | 223 | return await sniffVideo(url, options) 224 | } 225 | 226 | export default { 227 | sniffVideo, 228 | sniffVideoWithConfig, 229 | getSnifferConfig, 230 | isSnifferEnabled 231 | } -------------------------------------------------------------------------------- /dashboard/docs/t4api.md: -------------------------------------------------------------------------------- 1 | # T4服务接口对接文档 2 | 3 | ## 概述 4 | 5 | 本文档描述了基于 `/api/:module` 路径的接口服务。该服务根据不同的参数组合调用不同的功能模块,包括播放、分类、详情、动作、搜索、刷新和默认首页数据。所有接口均支持GET和POST请求方式。 6 | 7 | ## 通用说明 8 | 9 | - **请求方式**:GET 或 POST 10 | - **基础路径**:`/api/:module`,其中 `:module` 为模块名称(如 `vod1`) 11 | - **参数传递**: 12 | - GET请求:参数通过URL的query string传递 13 | - POST请求:参数可通过`application/x-www-form-urlencoded`或`application/json`格式传递 14 | - **安全验证**:所有接口请求都会经过`validatePwd`中间件验证(具体验证逻辑由实现方决定) 15 | - **错误响应**: 16 | ```json 17 | { 18 | "error": "错误信息" 19 | } 20 | ``` 21 | - **通用传参**: 若这个接口需要extend传参,确保每个地方调用都在各自的请求参数基础上加上原本的extend传参 22 | 23 | ## 接口列表 24 | 25 | ### 1. 播放接口 26 | 27 | **功能说明**:根据播放ID和源标识获取视频播放地址及信息。 28 | 29 | #### 请求参数 30 | 31 | | 参数名 | 必填 | 类型 | 说明 | 32 | |------|----|--------|--------------------------| 33 | | play | 是 | string | 播放ID | 34 | | flag | 否 | string | 源标识,详情接口返回播放信息里有,播放时建议加上 | 35 | 36 | #### 请求示例 37 | 38 | ```http 39 | GET /api/vod1?play=123&flag=youku 40 | ``` 41 | 42 | 或 43 | 44 | ```http 45 | POST /api/vod1 46 | Content-Type: application/x-www-form-urlencoded 47 | play=123&flag=youku 48 | ``` 49 | 50 | #### 响应示例 51 | 52 | ```json 53 | { 54 | "url": "https://example.com/video.mp4", 55 | "type": "hls", 56 | "headers": { 57 | "Referer": "https://example.com" 58 | } 59 | } 60 | ``` 61 | 62 | ### 2. 分类接口 63 | 64 | **功能说明**:获取指定分类下的视频列表,支持分页和筛选条件。 65 | 66 | #### 请求参数 67 | 68 | | 参数名 | 必填 | 类型 | 说明 | 69 | |-----|----|---------|-------------------------------| 70 | | ac | 是 | string | 固定值为 `list`(实际参数中需存在`ac`和`t`) | 71 | | t | 是 | string | 分类ID | 72 | | pg | 否 | integer | 页码,默认1 | 73 | | ext | 否 | string | 筛选条件(base64编码的JSON字符串) | 74 | 75 | #### 请求示例 76 | 77 | ```http 78 | GET /api/vod1?ac=cate&t=1&pg=2&ext=eyJuYW1lIjoi5paw5qW8In0= 79 | ``` 80 | 81 | #### 响应示例 82 | 83 | ```json 84 | { 85 | "page": 2, 86 | "pagecount": 10, 87 | "list": [ 88 | { 89 | "vod_id": "101", 90 | "vod_name": "电影名称", 91 | "vod_pic": "https://example.com/pic.jpg" 92 | } 93 | ] 94 | } 95 | ``` 96 | 97 | ### 3. 详情接口 98 | 99 | **功能说明**:获取一个或多个视频的详细信息。 100 | 101 | #### 请求参数 102 | 103 | | 参数名 | 必填 | 类型 | 说明 | 104 | |-----|----|--------|-----------------------------------| 105 | | ac | 是 | string | 固定值为 `detail`(实际参数中需存在`ac`和`ids`) | 106 | | ids | 是 | string | 视频ID,多个用逗号分隔 | 107 | 108 | #### 请求示例 109 | 110 | ```http 111 | GET /api/vod1?ac=detail&ids=101,102 112 | ``` 113 | 114 | 或 115 | 116 | ```http 117 | POST /api/vod1 118 | Content-Type: application/json 119 | { 120 | "ac": "detail", 121 | "ids": "101,102" 122 | } 123 | ``` 124 | 125 | #### 响应示例 126 | 127 | ```json 128 | [ 129 | { 130 | "vod_id": "101", 131 | "vod_name": "电影名称", 132 | "vod_actor": "主演", 133 | "vod_content": "剧情简介", 134 | "vod_play_url": "播放地址" 135 | } 136 | ] 137 | ``` 138 | 139 | ### 4. 动作接口 140 | 141 | **功能说明**:执行特定动作(如收藏、点赞等)。 142 | 143 | #### 请求参数 144 | 145 | | 参数名 | 必填 | 类型 | 说明 | 146 | |--------|----|--------|--------------------------------------| 147 | | ac | 是 | string | 固定值为 `action`(实际参数中需存在`ac`和`action`) | 148 | | action | 是 | string | 动作类型(如 `like`, `collect`) | 149 | | value | 是 | string | 动作值(如 `1` 表示执行) | 150 | 151 | #### 请求示例 152 | 153 | ```http 154 | GET /api/vod1?ac=action&action=like&value=1 155 | ``` 156 | 157 | #### 响应示例 158 | 159 | ```json 160 | { 161 | "code": 200, 162 | "msg": "操作成功" 163 | } 164 | ``` 165 | 166 | [交互UI说明参考此处](./ruleAttr.md) 167 | 168 | ### 5. 搜索接口 169 | 170 | **功能说明**:根据关键词搜索视频,支持快速搜索和分页。 171 | 172 | #### 请求参数 173 | 174 | | 参数名 | 必填 | 类型 | 说明 | 175 | |-------|----|---------|-----------------| 176 | | wd | 是 | string | 搜索关键词 | 177 | | quick | 否 | integer | 快速搜索模式(0或1,默认0) | 178 | | pg | 否 | integer | 页码,默认1 | 179 | 180 | #### 请求示例 181 | 182 | ```http 183 | GET /api/vod1?wd=电影&quick=1&pg=1 184 | ``` 185 | 186 | #### 响应示例 187 | 188 | ```json 189 | { 190 | "list": [ 191 | { 192 | "vod_id": "201", 193 | "vod_name": "搜索到的电影", 194 | "vod_remarks": "6.5分" 195 | } 196 | ], 197 | "total": 50 198 | } 199 | ``` 200 | 201 | ### 6. 刷新接口 202 | 203 | **功能说明**:强制刷新模块的初始化数据。 204 | 205 | #### 请求参数 206 | 207 | | 参数名 | 必填 | 类型 | 说明 | 208 | |---------|----|--------|-----------| 209 | | refresh | 是 | string | 任意值(存在即可) | 210 | 211 | #### 请求示例 212 | 213 | ```http 214 | GET /api/vod1?refresh=1 215 | ``` 216 | 217 | #### 响应示例 218 | 219 | ```json 220 | { 221 | "code": 200, 222 | "msg": "刷新成功", 223 | "data": { 224 | "lastUpdate": "2023-08-01 12:00:00" 225 | } 226 | } 227 | ``` 228 | 229 | ### 7. 默认首页接口 230 | 231 | **功能说明**:获取模块的首页数据(包括home和homeVod数据)。当没有匹配到上述任何功能时,调用此接口。 232 | 233 | #### 请求参数 234 | 235 | | 参数名 | 必填 | 类型 | 说明 | 236 | |--------|----|---------|------------------| 237 | | filter | 否 | integer | 过滤条件(1表示启用,默认启用) | 238 | 239 | #### 请求示例 240 | 241 | ```http 242 | GET /api/vod1 243 | ``` 244 | 245 | 或 246 | 247 | ```http 248 | GET /api/vod1?filter=1 249 | ``` 250 | 251 | #### 响应示例 252 | 253 | ```json 254 | { 255 | "class": [ 256 | { 257 | "type_id": "choice", 258 | "type_name": "精选" 259 | }, 260 | { 261 | "type_id": "movie", 262 | "type_name": "电影" 263 | }, 264 | { 265 | "type_id": "tv", 266 | "type_name": "电视剧" 267 | }, 268 | { 269 | "type_id": "variety", 270 | "type_name": "综艺" 271 | }, 272 | { 273 | "type_id": "cartoon", 274 | "type_name": "动漫" 275 | }, 276 | { 277 | "type_id": "child", 278 | "type_name": "少儿" 279 | }, 280 | { 281 | "type_id": "doco", 282 | "type_name": "纪录片" 283 | } 284 | ], 285 | "filters": { 286 | }, 287 | "list": [ 288 | { 289 | "vod_id": "301", 290 | "vod_name": "首页推荐电影", 291 | "vod_pic": "https://example.com/recommend.jpg" 292 | } 293 | ] 294 | } 295 | ``` 296 | 297 | ## 错误状态码 298 | 299 | | 状态码 | 含义 | 说明 | 300 | |-----|----------------|-----------| 301 | | 404 | Not Found | 模块不存在 | 302 | | 500 | Internal Error | 服务器内部处理错误 | 303 | 304 | ## 注意事项 305 | 306 | 1. 参数`ext`(在分类接口中)是base64编码的JSON字符串,用于传递筛选条件。 307 | 2. 分页参数`pg`从1开始。 308 | 3. 参数`extend`(接口数据扩展)从sites的ext直接取字符串,若有就每个接口都加上。 309 | 开发人员可参考此文档进行对接。 310 | 4. 除`action`外的接口,尽量都用`get`协议,action由于传值可能较大推荐使用`post` --------------------------------------------------------------------------------