m3u8局域网视频缓存系统AI项目文档

lizhi0710
2026-06-01 / 0 评论 / 90 阅读

HCData 视频缓存管理系统 - 项目开发文档


一、项目概述

1.1 项目名称

HCData — 局域网视频缓存管理与点播系统

1.2 项目定位

HCData 是一个运行在 Windows 平台上的轻量级 Web 服务端系统,用于管理和缓存网络视频资源(主要是 m3u8/HLS 流)。系统通过 Web 管理后台提供可视化操作界面,支持视频下载、缓存管理、在线播放、磁盘管理等功能,并可通过局域网供多设备访问。

1.3 核心场景

  • 将网络 m3u8 视频流下载并缓存为本地 MP4 文件
  • 通过局域网内任意设备浏览器访问并播放已缓存视频
  • 首次播放未缓存视频时,自动触发后台下载,下次访问可"秒播"
  • 管理下载任务队列,监控下载进度和系统状态

1.4 项目特点

  • 零构建部署:无需 webpack/vite 等前端构建工具,所有 HTML/CSS/JS 内联在单文件中
  • 单文件后端:整个服务端逻辑集中在 server.js(1463 行),无路由拆分文件
  • 嵌入式数据库:SQLite 单文件数据库,无需安装 MySQL/Redis 等服务
  • 局域网优先:自动检测局域网 IP,视频文件通过局域网 URL 直接访问
  • 智能缓存:首次播放走网络流,后台自动缓存,二次播放秒播
  • 实时推送:基于 SSE 的下载进度实时推送,无需轮询

二、技术架构

2.1 技术栈

层级技术说明
后端运行时Node.jsJavaScript 运行时环境
Web 框架Express 5.2.1HTTP 服务与 RESTful API
数据库SQLite3 (better-sqlite3 12.10.0)本地嵌入式数据库
下载引擎N_m3u8DL-RE (20251027)高性能 m3u8 下载工具
视频处理FFmpeg视频合并/转码(N_m3u8DL-RE 依赖)
前端播放器DPlayer + MuiPlayer + hls.js视频播放(HLS/MP4)
实时通信SSE (Server-Sent Events)服务端向客户端实时推送下载进度
构建方式无构建纯原生 HTML/CSS/JS,无前端打包工具

2.2 系统架构图

┌─────────────────────────────────────────────────────────┐
│                     浏览器(客户端)                       │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │ 当前任务  │  │  已缓存   │  │   日志   │  │  设置   │ │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘ │
│              SSE 实时事件流 / RESTful API 调用             │
└──────────────────────┬──────────────────────────────────┘
                       │ HTTP
┌──────────────────────┴──────────────────────────────────┐
│                   Express 服务端 (port 3000)              │
│  ┌────────────────────────────────────────────────────┐ │
│  │              RESTful API 层                         │ │
│  │  /api/tasks  /api/cached  /api/settings  /api/logs │ │
│  │  /api/download-video  /api/check-video  /api/video │ │
│  │  /api/stats  /api/disk-space  /api/drives          │ │
│  └────────────────────────────────────────────────────┘ │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │  SSE 推送模块 │  │  下载队列引擎 │  │  文件管理模块 │ │
│  └──────────────┘  └──────────────┘  └──────────────┘ │
│  ┌────────────────────────────────────────────────────┐ │
│  │          SQLite3 数据库 (hcdata.db)                 │ │
│  │  config | download_tasks | video_downloads | logs  │ │
│  └────────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────┘
                       │ 调用外部工具
          ┌────────────┴────────────┐
          │  N_m3u8DL-RE.exe        │
          │  FFmpeg (合并/转码)      │
          └─────────────────────────┘

2.3 server.js 模块拆解

server.js 是整个系统的核心文件(1463 行),内部按功能划分为以下逻辑模块:

模块行号范围职责
SSE 推送模块11-39管理 SSE 连接池,实现 broadcastSSE() 广播函数
数据目录初始化41-45确保 data/ 目录存在
数据库初始化47-103创建 4 张数据表(config/download_tasks/video_downloads/system_logs)
日志工具106-117writeLog() 函数,自动保留最近 500 条
数据库迁移119-125为旧版 video_downloads 表补充 play_count 字段
静态资源 & 中间件127-128express.json() + express.static('public')
系统信息 API130-202获取局域网 IP、磁盘空间、盘符列表
配置管理 API204-334通用 Key-Value 配置读写(单个/批量/全部)
日志管理 API336-375日志列表查询(分页)和清空
下载任务 API377-455任务列表、添加、更新进度
缓存管理 API457-552单个删除、一键清空已缓存视频
视频播放核心554-635存储路径管理、文件名生成、缓存检查
下载队列引擎637-699临时目录、队列管理、并发控制、processQueue()
启动恢复701-734initDownloadQueue() 服务启动时恢复未完成任务
下载执行引擎736-1192startDownload() 核心函数:spawn 进程、进度解析、文件转移、超时处理
下载入口 API1194-1242POST /api/download-video 去重、入队
状态查询 API1244-1318下载状态、已缓存列表(分页+文件大小)
视频流服务1320-1430播放地址生成、Range 请求支持、CORS 头
服务启动 & 优雅关闭1432-1463监听端口、初始化队列、SIGINT/SIGTERM/SIGBREAK 处理

2.4 目录结构

e:\071058s-260801\
├── hcdata-web/                    # 项目主目录
│   ├── server.js                  # 后端服务主文件(核心,1463行)
│   ├── check-db.js                # 数据库检查工具(辅助脚本)
│   ├── package.json               # Node.js 依赖配置
│   ├── data/                      # 数据目录(运行时生成)
│   │   └── hcdata.db              # SQLite 数据库文件
│   ├── public/                    # 前端静态资源
│   │   ├── index.html             # 管理后台主页面(1346行)
│   │   ├── player.html            # 视频播放页面(256行)
│   │   └── assets/                # 第三方前端库
│   │       ├── DPlayer.min.js     # DPlayer 播放器
│   │       ├── DPlayer.min.css
│   │       ├── mui-player.min.js  # MuiPlayer 播放器
│   │       ├── mui-player.min.css
│   │       ├── mui-player-desktop-plugin.min.js
│   │       └── hls.min.js         # HLS 流媒体解析库
│   └── node_modules/              # Node.js 依赖包
├── tools/                         # 外部工具目录
│   ├── N_m3u8DL-RE.exe            # m3u8 下载器
│   ├── N_m3u8DL-RE参数说明.txt     # 下载器参数文档
│   ├── ffmpeg.exe                 # 视频处理工具
│   ├── ffplay.exe                 # 视频播放工具
│   ├── ffprobe.exe                # 视频探测工具
│   ├── avcodec-62.dll             # FFmpeg 依赖库
│   ├── avdevice-62.dll
│   ├── avfilter-11.dll
│   ├── avformat-62.dll
│   ├── avutil-60.dll
│   ├── swresample-6.dll
│   └── swscale-9.dll
├── datatmp/tmp/                   # 下载临时文件目录(运行时使用)
├── 启动服务端.bat                   # Windows 一键启动脚本
└── 更新日志.md                     # 版本更新记录

三、数据库设计

3.1 数据库文件

  • 路径: hcdata-web/data/hcdata.db
  • 引擎: SQLite3(通过 better-sqlite3 同步 API 操作)

3.2 数据表结构

3.2.1 config — 通用配置表

存储系统所有可配置项,采用 Key-Value 模式。

字段类型说明
idINTEGER (PK, AUTO)主键
keyTEXT (UNIQUE, NOT NULL)配置键名
valueTEXT (NOT NULL)配置值
created_atDATETIME创建时间
updated_atDATETIME更新时间

已知配置项:

key说明默认值
storage_path视频文件存储根路径未设置(需用户在设置页选择盘符)
max_concurrent最大并发下载任务数4
thread_count单任务下载线程数4
nm_user_agentN_m3u8DL-RE 请求 User-Agent

3.2.2 download_tasks — 下载任务表

记录当前正在进行的下载任务(完成后删除)。

字段类型说明
idINTEGER (PK, AUTO)主键
nameTEXT (NOT NULL)任务名称
urlTEXT (NOT NULL)视频 URL
statusTEXT状态:waiting / downloading / merging / error
progressINTEGER进度百分比 (0-100)
speedTEXT下载速度显示文本
sizeTEXT文件总大小
downloadedTEXT已下载大小
created_atDATETIME创建时间
设计说明:此表仅保存"进行中"的任务,下载完成后记录会被删除,转移到 video_downloads 表。

3.2.3 video_downloads — 视频下载记录表

保存所有视频的下载记录(包括历史)。

字段类型说明
idINTEGER (PK, AUTO)主键
urlTEXT (UNIQUE, NOT NULL)视频 URL(唯一约束,用于去重)
nameTEXT视频名称
local_pathTEXT本地文件绝对路径
statusTEXT状态:pending / downloading / completed / error
progressINTEGER进度百分比
messageTEXT状态消息
formatTEXT输出格式(默认 mp4
play_countINTEGER播放次数(用于统计)
created_atDATETIME创建时间
updated_atDATETIME更新时间

3.2.4 system_logs — 系统日志表

字段类型说明
idINTEGER (PK, AUTO)主键
levelTEXT日志级别:info / warn / error
messageTEXT (NOT NULL)日志内容
created_atDATETIME创建时间
自动清理:日志超过 500 条时自动删除最早的记录。

四、API 接口文档

4.1 系统信息类

GET /api/local-ip

获取服务器局域网 IP 地址。

响应:

{ "success": true, "ip": "192.168.1.100" }

GET /api/disk-space

获取存储盘符的磁盘空间信息。

响应:

{
  "success": true,
  "drive": "D:",
  "freeSpace": "120.50 GB",
  "totalSize": "500.00 GB",
  "usedSpace": "379.50 GB",
  "usedPercent": "75.9"
}

GET /api/drives

获取电脑所有可用盘符列表。

响应:

{ "success": true, "drives": ["C:", "D:", "E:"] }

GET /api/stats

获取仪表盘统计数据(已完成数、点播总次数、命中率)。

响应:

{
  "success": true,
  "stats": {
    "completed": 42,
    "playCountTotal": 128,
    "hitRate": "304.8"
  }
}

4.2 配置管理类

GET /api/config

获取存储路径配置。

GET /api/settings

获取所有配置项。

GET /api/settings/:key

获取指定配置项。

POST /api/settings/:key

保存/更新指定配置项。

请求体:

{ "value": "4" }

POST /api/settings

批量保存配置项。

请求体:

{ "max_concurrent": "4", "thread_count": "8" }

POST /api/save-drive

设置存储盘符并创建 hcdata 文件夹。

请求体:

{ "drive": "D:" }

4.3 下载任务类

GET /api/tasks

获取当前进行中的下载任务列表(状态为 waiting/downloading/merging)。

POST /api/tasks

添加下载任务。

请求体:

{ "name": "视频名称", "url": "https://example.com/video.m3u8", "size": "500MB" }

PUT /api/tasks/:id

更新下载任务进度。

POST /api/download-video

启动视频下载(核心接口)。自动去重,加入下载队列。

请求体:

{ "url": "https://example.com/video.m3u8", "name": "视频名称" }

响应:

{ "success": true, "message": "下载任务已添加到队列", "taskId": 5 }

GET /api/download-status?url=xxx

查询指定 URL 的下载状态。

GET /api/check-video?url=xxx

检查本地是否已有指定 URL 的视频缓存。

响应(已缓存):

{
  "success": true,
  "exists": true,
  "format": "mp4",
  "path": "D:\\hcdata\\abc123\\143025.mp4",
  "relativePath": "abc123/143025.mp4"
}

4.4 缓存管理类

GET /api/cached?page=1&limit=99

获取已缓存视频列表(分页)。

DELETE /api/cached/:id

删除指定缓存记录及其本地文件。

DELETE /api/cached

一键清空所有已缓存视频。

4.5 视频播放类

GET /api/video?url=xxx

获取视频播放地址(返回局域网可访问的 URL),同时增加播放计数。

GET /hcdata/*filepath

视频文件静态服务路由,支持 HTTP Range 请求(断点续播/拖拽进度条)。

4.6 日志管理类

GET /api/logs?page=1&limit=100

获取系统日志(分页,按时间倒序)。

DELETE /api/logs

清空所有日志。

4.7 SSE 实时推送

GET /api/events

SSE 事件流端点,推送以下事件:

事件名触发时机数据
connected客户端连接成功{"status":"ok"}
task-update下载进度更新{taskId, status, progress, speed, ...}
task-completed下载任务完成{taskId, url, localPath}

五、核心业务流程

5.1 视频下载流程

用户提交URL
    │
    ▼
检查去重(download_tasks + video_downloads)
    │ 已存在 → 返回"已在队列"或"已下载"
    │ 不存在 ↓
    ▼
插入 download_tasks 表(status=waiting)
    │
    ▼
加入内存下载队列 downloadQueue
    │
    ▼
processQueue() 检查并发数
    │ 未达上限 → 立即开始 startDownload()
    │ 已达上限 → 等待队列
    ▼
调用 N_m3u8DL-RE.exe(spawn 子进程)
    │ 参数:URL、保存目录、线程数、UA 等
    │
    ▼
实时解析 stdout/stderr 输出
    │ 提取进度百分比、速度、TS块号
    │ 更新 download_tasks 表
    │ SSE 推送 task-update 事件
    │
    ▼
进程退出(exit code 0 = 成功)
    │
    ▼
在临时目录递归查找视频文件
    │
    ▼
safeMoveFile() 转移到最终存储路径
    │ 路径格式:{storagePath}/{URL的MD5}/{时分秒}.mp4
    │
    ▼
更新 video_downloads 表(status=completed)
删除 download_tasks 记录
SSE 推送 task-completed 事件
    │
    ▼
延迟 8 秒后清理临时目录

5.2 视频播放流程

用户点击"播放"按钮
    │
    ▼
打开 player.html?url={encodedURL}
    │
    ▼
调用 /api/check-video 检查本地缓存
    │
    ├── 已缓存(MP4)──→ "秒播模式"
    │       │
    │       ▼
    │   调用 /api/video 获取局域网播放URL
    │       │
    │       ▼
    │   使用 MuiPlayer 播放本地 MP4
    │   (播放次数 +1)
    │
    └── 未缓存 ──→ "后台加速中..."
            │
            ▼
        使用 DPlayer + hls.js 直接播放 m3u8 流
            │
            ▼
        同时调用 /api/download-video 提交后台下载
        (下次访问即可秒播)

5.3 下载队列管理

                  ┌─────────────────────┐
                  │   downloadQueue[]    │ ← 新任务入队
                  └─────────┬───────────┘
                            │ shift()
                            ▼
              ┌─────────────────────────┐
              │  activeDownloads 计数器  │
              │  上限 = max_concurrent   │
              └─────────┬───────────────┘
                        │
         ┌──────────────┼──────────────┐
         ▼              ▼              ▼
    ┌─────────┐   ┌─────────┐   ┌─────────┐
    │ 任务 A  │   │ 任务 B  │   │ 任务 C  │   ... 最多 N 个并发
    │ 线程: T  │   │ 线程: T  │   │ 线程: T  │
    └─────────┘   └─────────┘   └─────────┘
         │              │              │
         ▼              ▼              ▼
     完成后 activeDownloads--
     调用 processQueue() 取下一个

5.4 文件存储策略

存储根路径(用户设置)
  D:\hcdata\
      │
      ├── {MD5_hash_1}\           # 每个URL对应一个哈希目录
      │   ├── 143025.mp4          # 文件名 = 下载时间(时分秒)
      │   └── 151200.mp4
      │
      ├── {MD5_hash_2}\
      │   └── 092530.mp4
      │
      └── ...

临时下载目录:
  datatmp\tmp\
      ├── {MD5_hash}\             # N_m3u8DL-RE 下载时的临时目录
      │   └── {MD5_hash}.mp4     # 下载完成后的临时文件
      └── tmp\                    # 分片临时目录
          └── ...                 # 下载完成后延迟 8 秒清理

关键设计:

  • URL 通过 MD5 哈希生成唯一目录名,避免文件名冲突
  • 文件名为下载时间(HHmmss 格式),同一 URL 多次下载不会覆盖
  • 跨盘符文件移动使用 safeMoveFile() 兼容 EXDEV 错误(先复制后删除)
  • 临时文件延迟清理,避免影响其他正在进行的任务

5.5 服务启动流程

服务启动时按以下顺序初始化:

1. 创建 Express 应用,设置端口 3000
2. 初始化 SSE 客户端集合 (sseClients = new Set())
3. 创建 data/ 目录(如不存在)
4. 打开 SQLite 数据库 (hcdata.db)
5. 执行 CREATE TABLE IF NOT EXISTS 创建 4 张表
6. 执行数据库迁移(为旧表添加 play_count 字段)
7. 注册中间件(JSON 解析、静态资源服务)
8. 注册所有 API 路由
9. 调用 app.listen() 开始监听
10. 写入启动日志
11. 调用 initDownloadQueue() 恢复未完成任务

initDownloadQueue() 恢复逻辑:

  1. 查询 download_tasks 表中状态为 waitingdownloading 的所有记录
  2. 将这些任务的状态重置为 waiting,进度归零
  3. 依次加入内存队列 downloadQueue[]
  4. 调用 processQueue() 开始处理
设计意图:服务异常退出后重启,未完成的下载任务不会丢失,会自动重新排队。

5.6 优雅关闭流程

收到 SIGINT / SIGTERM / SIGBREAK 信号
    │
    ▼
gracefulShutdown(signal)
    │
    ├── 打印关闭提示
    ├── 关闭数据库连接 (db.close())
    └── process.exit(0)

信号处理:

  • SIGINT:Ctrl+C 触发
  • SIGTERM:系统终止信号
  • SIGBREAK:Windows 平台特有,Ctrl+Break 触发
注意:当前优雅关闭未等待进行中的下载任务完成,直接退出。进行中的 N_m3u8DL-RE 子进程会随父进程退出而终止。

5.7 合并超时处理机制

N_m3u8DL-RE 下载完成后需要合并 TS 分片,这一步可能卡住。程序实现了超时兑底:

进度 >= 99%
    │
    ▼
记录 mergeStartTime,启动 30 秒间隔检查器
    │
    ▼
每 30 秒检查:Date.now() - mergeStartTime > 5 分钟?
    │
    ├── 未超时 → 继续等待
    │
    └── 已超时 →
            │
            ▼
        递归查找临时目录中的视频文件
            │
            ├── 找到文件 → 强制转移到最终路径,标记完成
            └── 未找到 → 标记为 error,终止进程

超时处理中的文件查找策略:

  1. 先查 tempSubDir(任务专属临时目录)
  2. 再查 tempPath(整个临时下载目录)
  3. 最后查 tempOutputPath(固定预期路径)
  4. 支持的文件扩展名:.mp4, .mkv, .ts, .flv, .m4s

六、核心函数详解

6.1 startDownload(task) — 下载执行引擎

这是系统最核心的函数,负责从启动下载到文件转移的完整流程。

参数:

{
    url: string,      // 视频 m3u8 URL
    name: string,     // 视频名称
    taskId: number    // download_tasks 表中的 ID
}

执行步骤:

步骤操作说明
1计算路径生成 URL 的 MD5 哈希、最终存储路径、临时下载路径
2更新任务状态download_tasks.status = 'downloading'
3写入视频记录video_downloads 表插入/更新记录
4构建命令行参数URL、保存目录、线程数、UA 等
5spawn 子进程启动 N_m3u8DL-RE.exe
6注册进程映射activeDownloadProcesses.set(taskId, {child, ...})
7监听 stdout/stderr实时解析进度
8监听 close 事件处理成功/失败
9成功时转移文件safeMoveFile() 到最终路径
10更新数据库video_downloads.status = 'completed'
11清理临时文件延迟 8 秒后清理
12推送 SSEtask-completed 事件
13触发下一任务activeDownloads--; processQueue()

路径计算逻辑:

// 最终存储路径
const finalDir = path.join(storagePath, urlHash);        // D:\hcdata\{md5}\
const timeStr = formatTime(new Date());                   // 如 "143025"
const finalPath = path.join(finalDir, timeStr + '.mp4');  // D:\hcdata\{md5}\143025.mp4

// 临时下载路径
const tempSubDir = path.join(tempPath, urlHash);          // datatmp\tmp\{md5}\
const tempOutputPath = path.join(tempSubDir, urlHash + '.mp4');

6.2 parseProgress(output) — 进度解析函数

从 N_m3u8DL-RE 的原始输出中提取结构化进度信息。

处理流程:

原始输出(含 ANSI 转义码)
    │
    ▼
去除 ANSI 转义码:output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
替换 \r 为 \n
    │
    ▼
正则提取进度百分比:/(\d+\.?\d*)%/
正则提取 TS 块进度:/(\d+)\/(\d+)\s+\d+\.?\d*%/
正则提取下载速度:/([\d.]+\s*[KMGT]?Bps)/
    │
    ▼
根据进度判断状态:
  - progress >= 99  → status = 'merging', speed = '合并中...'
  - progress >= 95  → speed = '即将完成...'
  - 其他            → status = 'downloading'
    │
    ▼
更新 download_tasks 表
更新 video_downloads 表
SSE 广播 task-update 事件

为什么同时监听 stdout 和 stderr:
N_m3u8DL-RE 在某些情况下会将进度信息输出到 stderr 而非 stdout。为确保不丢失进度更新,两个流都调用 parseProgress() 解析。

6.3 processQueue() — 队列调度器

async function processQueue() {
    while (activeDownloads < getMaxConcurrent() && downloadQueue.length > 0) {
        const task = downloadQueue.shift();  // 从队头取出
        activeDownloads++;
        
        startDownload(task)
            .catch(err => console.error('下载任务失败:', err))
            .finally(() => {
                activeDownloads--;   // 无论成功失败都释放槽位
                processQueue();      // 递归检查队列
            });
    }
}

关键特性:

  • 使用 while 循环一次性填充所有可用槽位
  • getMaxConcurrent() 每次从数据库读取,支持运行时动态调整
  • finally 块确保异常时也能释放槽位并触发下一轮调度
  • 递归调用实现链式调度

6.4 safeMoveFile(src, dest) — 跨盘符文件移动

function safeMoveFile(src, dest) {
    try {
        fs.renameSync(src, dest);      // 同盘符:直接重命名(零拷贝)
        return true;
    } catch (err) {
        if (err.code === 'EXDEV') {    // 跨盘符:EXDEV 错误
            fs.copyFileSync(src, dest); // 先复制
            fs.unlinkSync(src);         // 再删除源
            return true;
        }
        throw err;                      // 其他错误向上抛出
    }
}

为什么需要这个函数:
临时下载目录在 datatmp\tmp\(通常在 C 盘),而最终存储路径可能在 D/E 等任意盘符。fs.renameSync() 在跨盘符时会抛出 EXDEV 错误,需要回退到复制+删除方案。

6.5 getVideoFileName(url) — URL 哈希生成

function getVideoFileName(url) {
    const hash = crypto.createHash('md5').update(url).digest('hex');
    return hash;  // 32 位十六进制字符串
}

用途: 将任意长度的 URL 映射为固定长度的唯一目录名,避免 URL 中的特殊字符导致文件系统问题。


七、SSE 实时推送机制

7.1 服务端实现

const sseClients = new Set();  // 存储所有 SSE 连接的 response 对象

// 客户端连接
app.get('/api/events', (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'X-Accel-Buffering': 'no'  // 禁止 Nginx 缓冲
    });
    res.write('event: connected\ndata: {"status":"ok"}\n\n');
    sseClients.add(res);
    req.on('close', () => sseClients.delete(res));
});

// 广播函数
function broadcastSSE(event, data) {
    const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
    sseClients.forEach(client => {
        try { client.write(message); }
        catch (e) { sseClients.delete(client); }
    });
}

设计要点:

  • 使用 Set 存储连接,自动去重
  • 写入失败时自动移除断开的连接
  • X-Accel-Buffering: no 防止反向代理缓冲 SSE 数据
  • 每个 SSE 消息以 \n\n 结尾(SSE 协议规范)

7.2 客户端实现

const eventSource = new EventSource('/api/events');

eventSource.addEventListener('connected', () => {
    console.log('SSE 已连接');
    loadTasks();  // 连接成功后立即刷新任务列表
});

eventSource.addEventListener('task-update', (e) => {
    loadTasks();  // 每次进度更新都刷新任务列表
    if (data.status === 'merging' || data.progress >= 99) {
        loadStats();  // 合并阶段同时刷新统计
    }
});

eventSource.addEventListener('task-completed', (e) => {
    loadTasks();
    loadStats();
    loadCached();     // 刷新已缓存列表
    loadDiskSpace();  // 刷新磁盘空间
});

eventSource.onerror = () => {
    eventSource.close();
    setTimeout(() => location.reload(), 3000);  // 3 秒后整页刷新重连
};

7.3 SSE 事件触发点

触发位置事件名触发时机
parseProgress()task-update每次解析到进度更新
startDownload() 成功task-completed文件转移完成,数据库更新后

7.4 备用轮询机制

为防止 SSE 失效,前端同时使用定时轮询作为备用:

轮询目标间隔说明
loadTasks()5 秒任务列表(主刷新渠道)
loadDiskSpace()10 秒磁盘空间
loadLogs()15 秒系统日志
loadCached()60 秒已缓存列表

八、安全机制

8.1 路径遍历防护

视频文件服务路由 /hcdata/*filepath 实现了路径安全检查:

app.get('/hcdata/*filepath', (req, res) => {
    const filePath = decodeURIComponent(req.params.filepath.join('/'));
    const fullPath = path.join(storagePath, filePath);
    
    // 安全检查:确保解析后的文件路径在存储目录内
    const resolvedPath = path.resolve(fullPath);
    const resolvedStorage = path.resolve(storagePath);
    if (!resolvedPath.startsWith(resolvedStorage)) {
        return res.status(403).json({ success: false, error: '拒绝访问' });
    }
    // ...
});

防护原理: 攻击者可能构造 ../../etc/passwd 这样的路径来访问系统文件。通过 path.resolve() 将路径解析为绝对路径后,检查是否以存储根路径开头,防止目录遍历攻击。

8.2 XSS 防护

前端渲染用户输入(视频名称、URL)时使用 escapeHtml() 函数:

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;  // 浏览器自动转义 HTML 实体
    return div.innerHTML;
}

所有动态内容在插入 DOM 前都经过此函数处理,防止跨站脚本攻击。

8.3 输入验证

所有 API 接口都进行基础输入验证:

  • 必填字段检查:if (!url) return res.status(400).json(...)
  • 类型检查:if (!drive) ...if (value === undefined || value === null) ...
  • 数值范围限制:并发数 1-10、线程数 1-32
  • URL 参数解码:decodeURIComponent()

九、前端页面详细说明

9.1 管理后台 (index.html) — CSS 设计体系

CSS 变量主题系统:

:root {
    --bg-base: #0d1117;       /* 页面底色(最深) */
    --bg-surface: #161b22;    /* 卡片/面板背景 */
    --bg-elevated: #1c2128;   /* 悬浮/高亮背景 */
    --border: #30363d;        /* 边框颜色 */
    --text-primary: #e6edf3;  /* 主文本 */
    --text-secondary: #8b949e;/* 次要文本 */
    --accent: #58a6ff;        /* 强调色(蓝) */
    --success: #3fb950;       /* 成功色(绿) */
    --warning: #d29922;       /* 警告色(黄) */
    --error: #f85149;         /* 错误色(红) */
}

设计原则:

  • 三层背景深度:base → surface → elevated,营造层次感
  • 所有 border-radius: 0,方正风格
  • 无阴影效果,依赖边框分割
  • 颜色与 GitHub Dark 主题相近

9.2 管理后台 — 布局结构

┌────────────────────────────────────────────────────┐
│                      body (flex)                    │
│  ┌──────────┐  ┌────────────────────────────┐ │
│  │  sidebar  │  │      main-content           │ │
│  │  (220px)  │  │      (flex: 1)              │ │
│  │           │  │                             │ │
│  │  ┌─────┐ │  │  ┌─────────────────────┐ │ │
│  │  │ Logo │ │  │  │     page-title       │ │ │
│  │  └─────┘ │  │  └─────────────────────┘ │ │
│  │  ┌─────┐ │  │  ┌─────────────────────┐ │ │
│  │  │统计区│ │  │  │                     │ │ │
│  │  │ IP  │ │  │  │   当前任务/已缓存  │ │ │
│  │  │已完成│ │  │  │   /日志/设置     │ │ │
│  │  │存储路径│ │  │  │                     │ │ │
│  │  │磁盘剩余│ │  │  │                     │ │ │
│  │  │点播次数│ │  │  │                     │ │ │
│  │  │命中率│ │  │  │                     │ │ │
│  │  └─────┘ │  │  └─────────────────────┘ │ │
│  │  ┌─────┐ │  │                             │ │
│  │  │导航菜单│  │                             │ │
│  │  │当前任务│  │                             │ │
│  │  │已缓存 │  │                             │ │
│  │  │日志   │  │                             │ │
│  │  │设置   │  │                             │ │
│  │  └─────┘ │  │                             │ │
│  └──────────┘  └────────────────────────────┘ │
└────────────────────────────────────────────────────┘

四个功能页面:

页面导航项功能说明
当前任务📊 当前任务显示正在下载/等待/合并的任务列表,含进度条、速度、TS块进度
已缓存💾 已缓存已下载完成的视频列表,支持播放、删除、分页浏览、一键清空
日志📋 日志系统运行日志查看,支持分页、清空
设置⚙️ 设置存储盘符选择、并发下载数设置、单任务线程数设置

页面切换机制:

  • 4 个 .page div 同时存在,通过 .active 类控制显示/隐藏
  • 导航点击时移除所有 .active,只给目标页添加
  • 无动画过渡,瞬间切换

9.3 管理后台 — 前端状态管理

全局变量:

let tasks = [];              // 当前下载任务列表(内存缓存)
let cachedCurrentPage = 1;   // 已缓存列表当前页码
let logCurrentPage = 1;      // 日志列表当前页码
const cachedPageSize = 24;   // 已缓存每页条数
const logPageSize = 100;     // 日志每页条数

数据加载函数一览:

函数调用时机请求 API
loadSettingsDrives()页面初始化GET /api/drives
loadConfig()页面初始化GET /api/config
loadMaxConcurrent()页面初始化GET /api/settings/max_concurrent
loadThreadCount()页面初始化GET /api/settings/thread_count
loadDiskSpace()初始化 + 每 10 秒GET /api/disk-space
loadTasks()初始化 + 每 5 秒 + SSEGET /api/tasks
loadCached(page)初始化 + 每 60 秒 + SSEGET /api/cached
loadLogs(page)初始化 + 每 15 秒GET /api/logs
loadLocalIP()页面初始化GET /api/local-ip
loadStats()初始化 + SSEGET /api/stats

9.4 管理后台 — 各页面详细功能

当前任务页

表格列定义(CSS Grid):

grid-template-columns: 2fr 100px 130px 100px;
宽度内容
下载地址自适应视频名称 + URL(溢出截断)
进度100px状态点 + 百分比
速度/TS块130px实时速度 + TS块进度
状态100px下载中/等待中/合并中

状态点颜色:

  • downloading → 绿色 (--success)
  • waiting → 黄色 (--warning)
  • merging → 蓝色 (--accent)

已缓存页

表格列定义:

grid-template-columns: 2fr 1fr 100px 1fr 120px;
内容
文件名视频名称(title 显示完整 URL)
大小文件大小(MB)
播放次数N 次
下载时间MM-DD HH:mm 格式
操作播放按钮 + 删除按钮

分页控件:

  • 显示总记录数
  • 页码按钮(最多显示当前后各 2 页)
  • 超出范围用省略号表示
  • 上/下一页按钮

日志页

表格列定义:

grid-template-columns: 120px 80px 1fr;
内容
时间MM-DD HH:mm:ss
级别INFO/WARN/ERROR(带颜色)
内容日志消息(自动换行)

级别颜色映射:

const levelColors = {
    info: 'var(--accent)',    // 蓝色
    warn: 'var(--warning)',   // 黄色
    error: 'var(--error)'     // 红色
};

设置页

三个设置卡片:

卡片控件保存 API范围
默认保存路径盘符下拉框 + 保存按钮POST /api/save-drive系统盘符列表
同时下载任务数数字输入框 + 保存按钮POST /api/settings/max_concurrent1-10
单任务下载线程数数字输入框 + 保存按钮POST /api/settings/thread_count1-32

9.5 播放页面 (player.html) — 详细流程

初始化流程:

页面加载
    │
    ▼
从 URL 参数获取视频源地址
    const m3u8Url = getQueryParam('url');
    │
    ├── 无 URL → 显示错误信息 "错误: 未提供视频地址"
    │
    └── 有 URL →
            │
            ▼
        调用 /api/check-video?url=xxx
            │
            ├── 已缓存 (format=mp4) →
            │       │
            │       ▼
            │   updateSourceTag('cached')  // 显示"秒播模式"
            │       │
            │       ▼
            │   调用 /api/video?url=xxx 获取局域网播放 URL
            │       │
            │       ▼
            │   createMuiPlayer(videoUrl)  // MuiPlayer 播放
            │
            └── 未缓存 / 请求失败 →
                    │
                    ▼
                updateSourceTag('network')  // 显示"后台加速中..."
                    │
                    ▼
                createDPlayer(m3u8Url, true)  // DPlayer+hls.js 播放网络流
                    │
                    ▼
                submitDownloadTask(m3u8Url)  // 后台提交下载

播放器创建对比:

特性DPlayerMuiPlayer
使用场景网络 m3u8 流本地 MP4 文件
HLS 支持通过 hls.js customType原生支持
自动播放autoplay: trueautoplay: true
桌面插件MuiPlayerDesktopPlugin
控件默认控件可自定义 headControls/footerControls

9.6 视频流服务 — Range 请求支持

GET /hcdata/*filepath 路由支持 HTTP Range 请求,实现视频拖拽进度条和断点续播:

客户端请求:
GET /hcdata/abc123/143025.mp4
Range: bytes=1048576-2097151

服务端响应:
HTTP/1.1 206 Partial Content
Content-Range: bytes 1048576-2097151/5242880
Accept-Ranges: bytes
Content-Length: 1048576
Content-Type: video/mp4

CORS 头设置:

res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Range, Content-Type');
res.header('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Accept-Ranges');

十、API 接口详细参考

10.1 统一响应格式

成功响应:

{ "success": true, "data_field": "...", "message": "操作成功" }

失败响应:

{ "success": false, "error": "错误描述信息" }

HTTP 状态码使用:

状态码场景
200成功
400参数缺失/无效
403路径遍历拒绝
404资源不存在
500服务器内部错误

10.2 下载相关 API 详细逻辑

POST /api/download-video — 完整去重逻辑

收到请求 { url, name }
    │
    ▼
检查 download_tasks 表是否有相同 URL
    │
    ├── status=completed → 返回 { exists: true, message: '该视频已下载完成' }
    ├── status=downloading/waiting → 返回 { exists: true, message: '该视频已在下载队列中' }
    │
    └── 不存在 ↓
            │
            ▼
        检查 video_downloads 表
            │
            ├── status=completed → 返回 { exists: true, message: '该视频已下载完成' }
            │
            └── 不存在/其他状态 ↓
                    │
                    ▼
                插入 download_tasks(status=waiting)
                加入 downloadQueue[]
                调用 processQueue()
                返回 { success: true, taskId }

GET /api/check-video?url=xxx — 三级缓存检查

检查顺序:
1. 数据库记录 → video_downloads 表中 url 对应记录的 local_path 是否存在
2. 哈希目录 → {storagePath}/{md5}/ 目录下是否有 .mp4 文件
3. 兼容旧路径 → {storagePath}/{md5}.mp4 或 {md5}.m3u8 是否存在

GET /api/cached — 响应结构

{
    "success": true,
    "videos": [
        {
            "id": 1,
            "url": "https://example.com/video.m3u8",
            "name": "视频名称",
            "local_path": "D:\\hcdata\\abc123\\143025.mp4",
            "status": "completed",
            "progress": 100,
            "message": "下载完成",
            "format": "mp4",
            "play_count": 5,
            "created_at": "2026-07-31 14:30:25",
            "updated_at": "2026-07-31 14:35:10",
            "file_size": "256.78 MB"
        }
    ],
    "pagination": { "page": 1, "limit": 99, "total": 42, "totalPages": 1 }
}
file_size 字段是服务端实时读取文件系统获取的,非数据库存储。

10.3 缓存管理 API 详细逻辑

DELETE /api/cached/:id — 删除流程

1. 查询 video_downloads 获取记录
2. 删除本地文件 (fs.unlinkSync)
3. 尝试删除空目录 (fs.rmSync)
4. 删除 video_downloads 记录
5. 删除关联的 download_tasks 记录(按 URL 匹配)
6. 写入日志

DELETE /api/cached — 一键清空流程

1. 查询所有 status='completed' 的记录
2. 逐个删除本地文件和空目录
3. 删除所有 completed 的 video_downloads 记录
4. 删除所有 completed 的 download_tasks 记录
5. 返回删除统计 { deletedCount, errorCount }

十一、临时文件清理策略

11.1 临时文件产生来源

N_m3u8DL-RE 下载过程中会产生以下临时文件:

文件类型位置说明
下载输出文件datatmp\tmp\{md5}\{md5}.mp4下载完成后的合并文件
TS 分片datatmp\tmp\tmp\ 下以 md5 前 8 位命名的子目录下载的原始分片
元数据 JSONdatatmp\tmp\{md5}\{md5}.json解析信息(如果开启了 write-meta-json)

11.2 清理时机

程序采用“延迟清理”策略,避免影响其他并发任务:

任务完成
    │
    ▼
立即转移视频文件到最终存储路径
    │
    ▼
启动 8 秒延迟定时器
    │
    ▼
8 秒后执行清理:
    ├── 清理任务专属临时目录 (tempSubDir)
    ├── 清理 tmp 目录中匹配 urlHash 前 8 位的子目录
    └── 清理 tmp 目录根下匹配 urlHash 前 8 位的文件

11.3 清理函数详解

function cleanupTempDir(dir) {
    if (!fs.existsSync(dir)) return;
    const items = fs.readdirSync(dir);
    for (const item of items) {
        const itemPath = path.join(dir, item);
        const stat = fs.statSync(itemPath);
        if (stat.isDirectory()) {
            cleanupTempDir(itemPath);  // 递归清理子目录
            fs.rmdirSync(itemPath);
        } else {
            fs.unlinkSync(itemPath);    // 删除文件
        }
    }
}

匹配规则: 通过 tmpItem.includes(urlHash.substring(0, 8)) 匹配当前任务的分片目录,避免误删其他任务的临时文件。

11.4 异常残留处理

如果服务异常退出(如断电、强制关闭),临时文件可能残留。当前处理方式:

  • 无自动清理机制:残留文件需手动删除
  • 不影响功能:新下载任务会创建新的临时目录,不会读取旧残留

十二、系统数据流总览

12.1 完整数据流图

用户浏览器
    │
    │ ① 访问管理后台
    ▼
Express 静态服务 (public/index.html)
    │
    │ ② 请求下载任务列表
    ▼
GET /api/tasks → SQLite download_tasks 表 → JSON 响应
    │
    │ ③ SSE 连接
    ▼
GET /api/events → 保持连接 → 实时推送进度
    │
    │ ④ 提交下载任务(通过播放器页面或 API)
    ▼
POST /api/download-video
    │
    ├── 去重检查 (download_tasks + video_downloads)
    ├── 插入 download_tasks 表
    ├── 加入 downloadQueue[]
    └── processQueue() → startDownload()
            │
            │ ⑤ 调用外部工具
            ▼
        spawn N_m3u8DL-RE.exe
            │
            │ ⑥ 实时解析进度
            ▼
        stdout/stderr → parseProgress()
            │
            ├── 更新 download_tasks 表
            ├── 更新 video_downloads 表
            └── SSE 广播 task-update
            │
            │ ⑦ 下载完成
            ▼
        safeMoveFile() 转移文件
            │
            ├── 更新 video_downloads (status=completed)
            ├── 删除 download_tasks 记录
            ├── SSE 广播 task-completed
            └── 8 秒后清理临时文件
    │
    │ ⑧ 用户点击播放
    ▼
GET /api/check-video → 检查缓存
    │
    ├── 已缓存 → GET /api/video → 返回局域网 URL
    │       │
    │       ▼
    │   GET /hcdata/*filepath → Range 请求支持 → 视频流
    │
    └── 未缓存 → 返回 m3u8 原始 URL + 后台下载

12.2 数据库表关系

config ──────────────────┐
  (key-value)            │
  storage_path ────────┐ │
  max_concurrent       │ │
  thread_count         │ │
  nm_user_agent        │ │
                          │ │
download_tasks ──────┐ │ │
  (进行中的任务)    │ │ │
  id, url, name     │ │ │
  status, progress  │ │ │
  完成后删除 ───────┤ │ │
                          │ │
video_downloads ────┤ │ │
  (全部历史记录)    │ │ │
  url (UNIQUE) ─────┘ │ │
  local_path ──────────┘ │
  status, play_count     │
  完成后保留 ───────────┘

system_logs
  (运行日志)
  自动保留 500 条

十三、外部工具集成

13.1 N_m3u8DL-RE

版本: 20251027 (Beta)

调用方式: 通过 Node.js child_process.spawn() 启动子进程

核心参数:

参数说明
<input>视频 URLm3u8 地址
--save-dirdatatmp\tmp输出目录
--save-nameURL 的 MD5 哈希保存文件名
--tmp-dirdatatmp\tmp\tmp临时分片目录
--thread-count配置值(默认 4)下载线程数
--del-after-donefalse不自动删除临时文件(由程序管理清理)
--headerUser-Agent: xxx可选,自定义请求头

进度解析:

  • 同时监听 stdout 和 stderr
  • 先去除 ANSI 转义码
  • 正则提取进度百分比 (\d+\.?\d*%)
  • 正则提取 TS 块进度 (\d+/\d+)
  • 正则提取下载速度 ([KMGT]?Bps)

13.2 FFmpeg

N_m3u8DL-RE 内部依赖 FFmpeg 进行视频合并。项目 tools 目录包含完整的 FFmpeg 可执行文件及依赖 DLL。


十四、启动与部署

14.1 环境要求

  • 操作系统: Windows(使用了 wmic、Windows 信号等)
  • 运行时: Node.js(需支持 ES5+ 语法)
  • 浏览器: 任意现代浏览器(Chrome/Edge/Firefox/Safari)

14.2 安装步骤

cd hcdata-web
npm install

14.3 启动方式

方式一:批处理启动

双击 启动服务端.bat

该脚本会:

  1. 设置 UTF-8 编码
  2. 切换到 hcdata-web 目录
  3. 自动打开浏览器访问 http://localhost:3000
  4. 启动 Node.js 服务

方式二:命令行启动

cd hcdata-web
node server.js

14.4 访问地址

14.5 首次使用配置

  1. 进入"设置"页面
  2. 选择视频文件存储盘符
  3. 点击"保存"(会自动创建 {盘符}:\hcdata 目录)
  4. 可选:调整并发下载数和线程数

十五、关键设计决策

15.1 为什么用 SQLite 而非文件存储配置

  • 支持并发读写(better-sqlite3 同步 API 无竞态问题)
  • 事务支持(批量保存配置)
  • 结构化查询(分页、条件过滤)
  • 单文件部署,无需额外数据库服务

15.2 为什么用 SSE 而非 WebSocket

  • 单向推送即可满足需求(服务端 → 客户端)
  • 基于 HTTP,无需额外协议支持
  • 浏览器原生 EventSource API,代码更简洁
  • 断线自动重连

15.3 为什么双播放器方案

  • DPlayer + hls.js:对 m3u8 流兼容性最好
  • MuiPlayer:MP4 本地文件播放体验更好(支持更多控件)
  • 根据缓存状态自动切换,用户无感知

15.4 文件命名策略

  • URL → MD5 哈希作为目录名:保证唯一性,避免特殊字符
  • 时间戳作为文件名:同一 URL 可多次下载不覆盖
  • 延迟清理临时文件:避免并发任务间的文件冲突

15.5 下载队列设计

  • 内存队列 + 数据库双写:保证重启后可恢复
  • 服务启动时扫描未完成的任务,重新加入队列
  • 支持配置并发数(1-10)和单任务线程数(1-32)

十六、辅助工具

16.1 check-db.js

数据库检查脚本,用于调试查看数据库状态:

node check-db.js

输出所有表名和 download_tasks 表的全部记录。

16.2 启动服务端.bat

Windows 一键启动脚本,自动打开浏览器。


十七、已知限制与注意事项

17.1 平台限制

  1. 仅支持 Windows:使用了 wmic 获取磁盘信息、Windows 信号处理(SIGBREAK)
  2. 无跨平台支持wmic logicaldisk 是 Windows 特有命令

17.2 安全限制

  1. 无用户认证:局域网内任何人可访问管理后台,无登录/权限机制
  2. 无 HTTPS:仅支持 HTTP 协议,数据传输未加密
  3. CORS 全开Access-Control-Allow-Origin: *,任何来源可调用 API

17.3 性能限制

  1. 数据库单文件:高并发场景下可能有锁竞争(当前场景可忽略)
  2. 同步数据库 API:better-sqlite3 同步操作会阻塞 Node.js 事件循环(但 SQLite 操作通常很快)
  3. 无连接数限制:SSE 连接数无上限,大量客户端可能导致内存问题

17.4 可靠性限制

  1. 临时文件清理:依赖延迟定时器(8 秒),异常退出可能残留临时文件
  2. 合并超时:N_m3u8DL-RE 合并阶段可能卡住,程序设了 5 分钟超时兑底
  3. 磁盘空间:大量下载可能导致磁盘满,侧边栏有实时显示但无预警机制
  4. 优雅关闭不完善:关闭服务时不等待进行中的下载任务完成,子进程直接终止

17.5 功能限制

  1. 无搜索功能:已缓存列表不支持搜索/过滤
  2. 无批量导入:只能逐个提交下载任务,不支持批量导入 URL 列表
  3. 无下载重试:下载失败后不会自动重试,需手动重新提交
  4. 无速度限制:单任务下载无限速配置(N_m3u8DL-RE 支持但程序未使用)

十八、依赖清单

Node.js 依赖 (package.json)

包名版本用途
express^5.2.1Web 服务框架
better-sqlite3^12.10.0SQLite 数据库驱动
node-gyp^12.3.0better-sqlite3 编译依赖

外部工具

工具版本用途
N_m3u8DL-RE20251027m3u8/HLS/DASH 视频下载
FFmpeg最新视频合并/转码(被 N_m3u8DL-RE 调用)

前端库

库名用途
DPlayer视频播放器(网络流播放)
MuiPlayer视频播放器(本地文件播放)
MuiPlayerDesktopPluginMuiPlayer 桌面端插件
hls.jsHLS 流媒体解析

十九、开发约定

19.1 代码风格

  • 后端:CommonJS 模块 (require/module.exports)
  • 前端:原生 JavaScript,无框架,内联 <script> 标签
  • CSS:内联 <style> 标签,CSS 变量管理主题色
  • 无代码构建步骤

19.2 API 响应格式

所有 API 统一返回 JSON:

{ "success": true/false, "data...": "...", "error": "错误信息" }

19.3 错误处理

  • API 层统一 try-catch,返回 500 状态码
  • 下载任务错误记录到数据库和日志
  • 前端 fetch 调用统一 catch 处理

文档编写日期:2026年8月1日
基于项目源码版本:hcdata-web v1.0.0

0

评论

博主关闭了所有页面的评论