115 网盘自动转存系统 · 离线下载 · QMediaSync 联动
把 xinyuLo/bdsavepro(百度网盘版)的思路搬到 115 网盘上:
定时把 115 分享链接里的文件转存到自己的目录,转存到新文件后自动通知 QMediaSync 刮削生成 strm 并整理分类;
同时把 115 的离线下载能力(磁力 / ed2k / HTTP)也接了进来。
mkdir -p config log && docker compose up -d访问 http://你的NAS IP:5000,默认账号 admin / 密码 zxcvbnm。
更新:docker compose pull && docker compose up -d
- 后端:Flask + APScheduler + SQLite
- 网盘 SDK:p115client(ChenyangGao,MIT),
requirements.txt里锁定到实测通过的版本0.0.9.6.5.1,保证构建可复现 - 前端:零构建单页(一个 HTML,不需要 npm / Node 构建)
- 登录 115:扫码登录(也可手动粘贴 Cookie)
- 需要 Python ≥ 3.12(
p115client硬性要求)
| 模块 | 能力 |
|---|---|
| 账号 | 扫码登录(多设备身份可选)、手动粘贴 Cookie、多账号切换、Cookie 有效性校验、空间用量显示 |
| 转存任务 | 多任务、启用/停用、单任务 cron 或跟随全局、正则过滤、正则替换 |
| 转存范围 | 可只转存分享里的指定文件夹(逐层下钻 + 跨层级多选),不选则整条转存 |
| 去重 | 按文件名 / 文件名+大小 / SHA1(115 秒传 SHA1 一致,改名也能拦住);支持独立的「对比路径」 |
| 排除清单 | 按文件名排除,命中即跳过 |
| 离线下载 | 提交磁力 / ed2k / HTTP / FTP / 种子链接到 115 云端;列出进度;删除 / 重试 / 清空 |
| 文件管理 | 浏览目录、新建文件夹、重命名、移动、复制、删除、搜索、生成分享链接 |
| 转存日志 | 每次执行留档(SQLite),含配置快照、分级日志、去重跳过清单、实际新增清单,每任务保留最近 10 次 |
| 通知 | Bark / PushPlus / 钉钉 / 飞书 / 企业微信(机器人+应用)/ Telegram / Server酱 / ntfy / Gotify / PushDeer / SMTP / 自定义 Webhook |
| 空间告警 | 定时巡检,超过阈值推送 |
| QMediaSync 联动 | 手动绑定「转存任务 / 离线下载」↔「QMS 刮削目录」,有新文件时自动触发,支持手动触发与触发日志 |
| 备份与恢复 | 面板内导出 / 导入备份,支持服务器本地多份留存、按内容选择性恢复、恢复前自动存档 |
镜像已发布,匿名可拉取,不需要登录:
mkdir -p config log
# 把 docker-compose.yml 放到当前目录,直接启动
docker compose up -d访问 http://你的NAS IP:5000,默认账号 admin / 密码 zxcvbnm(登录后请立刻在「系统设置」改掉)。
没有现成文件的话,新建 docker-compose.yml,把下面这段完整粘进去:
services:
save115:
image: ghcr.io/xiaoming677/115savepro:latest
container_name: 115savepro
restart: unless-stopped
ports:
- "5000:5000"
volumes:
- ./config:/app/config
- ./log:/app/log
environment:
- TZ=Asia/Shanghai然后启动:
mkdir -p config log
docker compose up -d两个最容易踩的坑
- 缩进必须用空格,YAML 不认 Tab。上面每一层是 2 个空格。 用 Tab 缩进会报
top-level object must be a mapping。- 服务名是
save115而不是115savepro—— 以数字开头的键会被部分 compose 实现和网页版编辑器误解析。容器名仍然是115savepro,使用上无感知。
想改用 Docker Hub 的镜像(国内通常更快),把 image: 那行替换成:
image: xiaoming677/115savepro:latest想让 QMS 同机访问更省事,把注释打开:
network_mode: hostmkdir -p config log
docker run -d --name 115savepro --restart unless-stopped \
-p 5000:5000 \
-v "$PWD/config:/app/config" \
-v "$PWD/log:/app/log" \
-e TZ=Asia/Shanghai \
ghcr.io/xiaoming677/115savepro:latest
-v的写法是宿主机路径:容器内路径,中间的:是分隔符。$PWD是当前目录,所以要先cd到数据目录所在位置再执行。 Windows CMD 用%cd%,PowerShell 用${PWD}。
mkdir -p config log
docker build -t 115savepro:latest .
docker run -d --name 115savepro --restart unless-stopped \
-p 5000:5000 \
-v "$PWD/config:/app/config" \
-v "$PWD/log:/app/log" \
-e TZ=Asia/Shanghai \
115savepro:latestpip install -r requirements.txt
python web_app.py # 默认 0.0.0.0:5000,可用 PORT / HOST 环境变量覆盖需要 Python ≥ 3.12(
p115client的硬性要求,Docker 镜像用的是python:3.12-slim)。
| 来源 | 地址 | 需要登录 |
|---|---|---|
| GHCR(默认,零配置) | ghcr.io/xiaoming677/115savepro:latest |
否,公开可拉 |
| Docker Hub | xiaoming677/115savepro:latest |
否(国内拉取通常更快) |
| 本地构建 | 115savepro:latest |
否 |
在 docker-compose.yml 所在目录执行:
docker compose pull && docker compose up -d以后更新就重复这一条命令。 就这样,不用改任何配置。
**「系统设置 → 版本更新」**里可以直接检查并更新,不用碰 Docker 界面:
- 检查更新 —— 零配置。容器通网即可用,自动对比 GitHub 上的最新版本与更新说明。
- 一键更新 —— 需要额外挂载 docker.sock(见下)。点一下按钮就自动完成 「拉新镜像 → 重建容器」,页面会自动等待服务恢复并提示新版本号。
启用一键更新:在 docker-compose.yml 的 volumes: 下加一行,然后 docker compose up -d:
volumes:
- ./config:/app/config
- ./log:/app/log
- /var/run/docker.sock:/var/run/docker.sock # ← 加这一行
⚠️ 安全提示:挂载docker.sock等于把宿主机 Docker 的控制权交给这个容器 (等价于 root 权限)。不需要一键更新就不要加 —— 检查更新、以及上面那条手动 更新命令都不受任何影响。
它是怎么做到的(值得一读,因为有点绕):容器没法自己重建自己 —— 一旦被 停止,进程就结束了,后续动作没人执行。所以点「立即更新」后,程序会通过 Docker Engine API 启动一个一次性助手容器,由它在你退出之后继续干活:
sleep 3 # 留出时间让页面收到响应
cd <compose 项目目录>
docker compose pull
docker compose up -d --force-recreate之所以走 docker compose 而不用 API 手拼容器参数,是因为 compose 文件里有权威配置 ——
端口、挂载、环境变量、网络模式全部以它为准,不会因为重建而丢失或走样。
几个实现细节:
- 助手镜像用
docker:cli(自带 compose 插件),执行完自动删除;想换镜像可以设环境变量UPDATE_ASSISTANT_IMAGE - 项目目录自动识别:读容器标签
com.docker.compose.project.working_dir, 所以不管你是在飞牛界面粘贴 YAML 还是 SSH 手写,位置都能自动找到 - 检查更新失败不影响使用:容器访问 GitHub 可能被网络限制,界面会提示去浏览器看 Releases
- 更新过程有日志:写进
config/update.log,面板里点「上次更新日志」就能看
飞牛自带的 Docker 没有「自动检测镜像更新」这个功能(社区已确认的缺陷)—— 界面上显示「已是最新」是假象:它只比对本地已有镜像,根本不会去远端查询。 所以必须手动触发一次拉取:
- 飞牛桌面 → Docker → 左侧 镜像
- 点「拉取」,输入
xiaoming677/115savepro:latest(先试这条;ghcr.io 在国内常拉不动) - 拉取成功后,左侧切到 Compose → 找到本项目 → 点 停止
- 项目右侧 三个点 → 重构(部分版本叫「重新构建」或「更新镜像」)
第 2 步如果失败,改试 ghcr.io/xiaoming677/115savepro:latest —— 哪个能拉下来就用哪个。
也可以直接改 YAML 换源:Compose → 项目右侧 三个点 → 详情 →
把 image: 那行改成 xiaoming677/115savepro:latest → 保存 → 再点 三个点 → 重构。
嫌飞牛自带界面麻烦的话,可以装 Portainer:它的「Recreate」按钮会 自动比对远端有没有新版本,比自己点省事。
三种方式,任选其一:
| 方式 | 操作 |
|---|---|
| 看页面 | 打开面板,左下角会显示当前版本号(如 v1.2.0) |
| 看接口 | 浏览器访问 http://你的IP:5000/api/version,返回 {"version":"1.2.0", ...} |
| 看容器 | docker inspect 115savepro --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' |
第 1 步:确认容器当前到底跑的是哪个版本
# 容器镜像里带的版本标签(最准,不依赖页面显示)
docker inspect 115savepro --format '镜像={{.Config.Image}} 版本={{index .Config.Labels "org.opencontainers.image.version"}}'
# 本地已有哪些相关镜像
docker images | grep -i 115savepro
# 程序自报的版本
curl -s http://127.0.0.1:5000/api/version; echo第 2 步:按结果对症处理
| 现象 | 原因 | 处理 |
|---|---|---|
| 本地镜像有新的,但容器标签还是旧的 | 镜像拉到了,容器没重建 | docker compose up -d --force-recreate |
本地镜像也是旧的,pull 说 up to date |
镜像源连不上,或返回了缓存的旧摘要 | 见下面「拉不动镜像」 |
容器标签里没有 version 这一项 |
你跑的是自己 docker build 出来的镜像 |
见下面「本地构建的镜像」 |
拉不动镜像
ghcr.io 在国内经常连不上,典型表现是 docker compose pull 卡住、超时,或者静默告诉你 up to date 但其实没拉到新版本。先直接测试哪个源通:
docker pull xiaoming677/115savepro:latest # Docker Hub
docker pull ghcr.io/xiaoming677/115savepro:latest # GHCR哪条成功就用哪个。 比如只有 Docker Hub 能拉,就把 docker-compose.yml 里那行改成:
image: xiaoming677/115savepro:latest然后:
docker compose pull && docker compose up -d --force-recreate两个都拉不动,就是 NAS 的 DNS / 网络问题:给 Docker 配国内镜像加速器,或检查这台机器上的代理(Clash 之类的分流规则有时会把 registry 也拦掉)。
本地构建的镜像
如果你改过 docker-compose.yml,把 image: 指向了本地名字(如 115savepro:latest),那 pull 对你完全没用 —— 它不会去远端找。要么改用已发布的镜像地址(推荐,以后更新只需一条命令),要么每次自己重新构建:
docker compose up -d --builddocker compose up -d 说容器没重建?
compose 只在镜像摘要变化时才重建。如果镜像确实更新了但仍没重建,用 --force-recreate。
我是用 docker build 自己构建的镜像,怎么办?
那 pull 对你没用,要重新构建:
docker compose up -d --build # compose 项目里带 build: . 时
# 或
docker build -t 115savepro:latest . && docker compose up -d --force-recreate建议直接改用已发布的镜像(把 image: 指向 ghcr.io/... 或 xiaoming677/...),
这样以后更新只要 pull 一条命令。
数据会不会丢?
不会。账号 Cookie、转存任务、历史、QMS 绑定全部在 ./config 挂载卷里,
升级镜像只换程序不碰数据。稳妥起见可以先备份:
tar czf 115savepro-config-$(date +%Y%m%d).tar.gz config/旧镜像占空间怎么办?
docker image prune -f # 清理悬空镜像(安全)
# 想更彻底地清掉未被使用的镜像:
docker image prune -a -f
-a会删掉所有没有被容器使用的镜像,执行前确认没有别的镜像在用。
- 如果你已经用 CloudDrive2 把 115 挂载到本机,QMediaSync 的刮削目录要指向挂载后的本地路径,
而 115SavePro 里的「保存路径」填的是 115 网盘里的路径(例如/我的影视/剧集)。两者是同一个位置的两个视角。 - 如果 QMediaSync 也在这台 NAS 的 Docker 里,把
docker-compose.yml里的network_mode: host打开,
然后在 QMS 连接设置里填127.0.0.1+ QMS 端口(默认12333)即可,最省事。
左侧「账号管理」→「扫码登录 115」:
- 选登录设备(不确定就保持默认「115生活_支付宝小程序」)
- 页面上会出现二维码,用 115 手机 App 扫码
- 手机上点「确认登录」,页面会提示登录成功
- 点「保存账号」
扫码轮询会把状态实时显示出来:等待扫码 → 已扫码待确认 → 登录成功。
也支持「手动粘贴 Cookie」:浏览器登录 115 网页版 → F12 → 应用/存储 → Cookie,复制UID、CID、SEID、KID四项。
「转存任务」→「新建任务」:
| 字段 | 说明 |
|---|---|
| 分享链接 | 必填。支持 https://115.com/s/xxxxx?password=abcd 这种带提取码的完整链接 |
| 提取码 | 链接里没带时才需要填 |
| 任务名称 | 留空会自动抓取分享标题 |
| 保存路径 | 必填。115 网盘里的目录,不存在会自动创建。可点「选择」用目录选择器 |
| 对比路径 | 可选。去重时跟这个目录比;留空就与保存路径比 |
| 去重模式 | 按文件名 / 文件名+大小 / SHA1 |
| 定时规则 | cron 表达式,留空则跟随「系统设置」里的全局定时 |
| 文件过滤正则 | 只转存文件名匹配的项,例如 .*\.(mp4|mkv)$ |
| 排除文件清单 | 每行一个文件名,命中即跳过 |
| 指定转存文件夹 | 不选 = 整条分享全部转存;选了就只转存选中的项 |
目录选择器怎么用
点「选择」打开后:
- 打开时会自动定位到输入框里已填的路径,不用从根目录一层层点
- 点一行 = 进入下一层(行尾有「进入 ›」提示),面包屑可以点回上层
- 到目标目录后点「选择当前目录」才会把路径填回去
- 想放一个还不存在的目录?点「新建目录」,建完会直接进去
也可以完全不用选择器,直接在输入框里手打路径(如
/我的影视/剧集/2026), 转存时不存在会自动创建。
cron 示例
| 表达式 | 含义 |
|---|---|
*/30 * * * * |
每 30 分钟 |
0 */6 * * * |
每 6 小时 |
0 10 * * * |
每天 10:00 |
0 8,12,20 * * * |
每天 8 点、12 点、20 点 |
创建后点任务行里的「执行」可以立刻跑一次,会弹出实时日志。
「离线下载」→「新建下载」,把磁力 / ed2k / HTTP 链接每行一个贴进去,选个保存目录后提交。
115 会在云端下载,下载完成后落到你的网盘目录里。
系统默认每 5 分钟巡检一次已完成的离线任务,完成时会触发绑定到「离线下载」的 QMS 刮削,并推送一条通知。
巡检频率在「系统设置 → 离线下载巡检」里改。
「文件管理」里可以像网盘客户端一样浏览目录,勾选后批量移动 / 复制 / 删除 / 生成分享链接。
思路和上游项目一致:手动把「转存任务」或「离线下载」绑定到「QMS 的某个刮削目录」,
之后只有绑定了的刮削任务会被触发,避免整库空跑。
登录 QMediaSync → 系统设置 → API 密钥 → 创建,记下 qms_ 开头的那串字符。
「连接 QMediaSync」:
| 字段 | 说明 |
|---|---|
| 地址 / 端口 | QMS 的地址。默认端口 12333。填实际能访问到的地址(同机 host 网络就填 127.0.0.1,跨容器填 NAS 内网 IP) |
| 鉴权方式 | API Key(推荐)或 账号密码 |
| 启用 QMS 联动 | 总开关 |
| 转存到新文件后自动触发 | 关掉就只保留手动触发 |
| 触发前延时 | 挂载盘刷新有延迟时,可以设几秒再触发 |
点「测试连接」确认通,再点「保存配置」。点「查看刮削任务」可以看 QMS 里现有的刮削目录。
点「创建连接」:
- 绑定对象:
某个转存任务→ 该任务转存到新文件时触发离线下载完成→ 任意离线任务下载完成时触发
- ① 先生成 strm(可选) → 选一个 QMS 的同步路径(见下一节)
- ② 刮削任务 → 从 QMS 拉到的刮削目录列表里选
连接列表里可以:触发(手动跑一次)、编辑、日志(最近 30 条)、停用/启用、删除。
页面顶部还有「立即触发全部」。
为什么需要这一步:QMS 里「生成 strm」和「刮削」是两个独立动作 ——
前者把 115 上的视频映射成本地 .strm 文件,后者才去 TMDB 抓元数据。
顺序不能反:没有 strm,就刮不到东西。 如果转存完直接触发刮削,
新转存的文件很可能因为 strm 还没生成而被整个漏掉。
所以每条连接可以额外绑定一个同步路径,把流程变成两步:
转存到新文件 → ① 触发生成 strm → 等待 → ② 触发刮削
等待方式(推荐「轮询」):
| 方式 | 说明 |
|---|---|
| 轮询等待(推荐) | 每隔几秒查一次同步路径的 is_running,等它真正跑完再刮削。最可靠 |
| 固定延迟 | 简单等 N 秒再刮削。QMS 规模小、strm 生成快时够用 |
| 不等待 | 触发完 strm 立即刮削(老行为,可能刮漏) |
「轮询」最长等待默认 900 秒。超时不算失败 —— 会照常继续刮削, 因为已经生成的那部分 strm 依然值得刮,触发日志里会写明。
不绑定同步路径时行为与以前完全一致(直接刮削),老配置不用改。
同步路径在哪找:QMS 的「同步」页面里建的那些。本项目页面上的 「查看同步路径」按钮可以直接列出(含 115 远程路径 → 本地 strm 路径), 以及当前是否正在运行。
触发日志会记录完整流程,例如:
生成 strm:已加入同步队列 → 等待 strm 完成:strm 同步已完成(用时 42 秒) → 刮削:已开始
| 情况 | 是否触发 |
|---|---|
| 转存到新文件 + 有绑定 | ✅ |
| 转存了但全是已存在(去重跳过) | ❌ 不触发,避免空跑 |
| 任务没有绑定任何连接 | ❌ |
| 总开关或「自动触发」关掉 | ❌ |
| 该条连接被停用 | ❌ |
| 离线任务完成 + 绑定了「离线下载完成」 | ✅(去重,同一个任务只触发一次) |
QMS 配置和绑定关系存在
config/history.db(SQLite)里,不会被转存过程中的config.json回写覆盖。
| 路径 | 内容 | 是否需要备份 |
|---|---|---|
config/config.json |
账号 Cookie、转存任务、通知配置 | ✅ 必须 |
config/history.db |
转存历史、QMS 配置与绑定、离线完成去重记录 | ✅ 必须 |
config/backups/ |
面板生成的备份文件 | 可以不用 |
config/config.template.json |
配置模板 | 不用 |
log/ |
运行日志(按天切割,保留 14 天) | 可选 |
手工备份就是打包 config/ 目录,换机器时把 config/ 放回去即可。
更省事的做法:「系统设置 → 备份与恢复」。
导出能选包含哪些内容:
| 选项 | 说明 |
|---|---|
| 包含账号 Cookie | 默认开。关掉后导出的文件不含登录凭据,可以放心发给别人帮忙排查问题(恢复后需重新登录 115) |
| 包含后台密码 | 默认开 |
| 包含历史记录 | 默认开 |
两个按钮:
- 导出并下载 —— 浏览器直接下载一个 JSON 文件
- 备份到服务器 —— 存在
config/backups/下,最多保留 20 份,自动清理最旧的
⚠️ 默认导出的备份里含有 115 账号登录凭据,和密码一样重要,别随手发人。config/backups/已在.gitignore/.dockerignore里排除,不会被提交或打进镜像。
两种入口:
- 从文件恢复 —— 选择之前下载的备份文件
- 在下方列表里点「恢复」 —— 直接用服务器上的某一份
点完之后会先预览:显示备份时间、程序版本、里面有几个账号 / 任务 / QMS 连接,然后让你选:
① 恢复方式
| 方式 | 行为 |
|---|---|
| 覆盖(默认) | 用备份内容替换现有配置,结果和备份时完全一致 |
| 合并 | 账号 / 转存任务 / QMS 连接按 ID 合并,当前多出来的部分保留 |
② 恢复内容(可多选,按需来)
账号(含 Cookie) / 转存任务 / QMS 联动配置 / 通知渠道 / 系统设置 / 后台登录账号密码 / 历史记录
比如只想把通知渠道搬过来,就只勾「通知渠道」—— 账号、任务都不动。
- 点下「确认恢复」前会先把当前状态自动存一份到
config/backups/(来源标为「恢复前自动存档」), 拿错文件可以原样退回来 - 备份文件必须通过
app/kind/format三个字段校验,误传别的 JSON 会被直接拒绝 - 来自更高版本的备份会被明确拦下(提示先升级程序),而不是半懂不懂地写进去
- 恢复完会列出每一项都做了什么(恢复了几条、跳过了什么、为什么跳过)
- cookie 已失效的账号:恢复的 Cookie 是备份那一刻的,如果期间在别处登录过、或 Cookie 过期, 在「账号管理」里点「校验」重新确认,必要时重新登录
- 定时任务:恢复后会自动重载 cron,不用重启容器
- QMS 连接:如果换了机器、QMS 地址变了,恢复后记得改一下地址再测连接
Q:扫码后一直停在「等待扫码」?
大概率是容器访问不了 qrcodeapi.115.com。检查容器的 DNS / 代理设置(注意:如果这台机器上跑了 Clash 之类的代理,容器内的 115 接口也走代理,可能反而更慢或失败)。
Q:扫码时手机响了一声,但之后提示「参数错误」?
「参数错误」意味着二维码被识别了,但换取登录凭据的设备身份被拒。 v1.3.0 起系统会自动依次尝试多个设备身份(支付宝小程序 / 微信小程序 / 安卓端 / iOS / Windows 端 / 网页端…),第一个成功的就采用 —— 不需要你手动换设备重扫。
如果全部失败,点弹窗里的「复制诊断」:诊断里会列出每个设备分别报什么错, 一眼就能区分是设备不匹配、还是 115 风控。
另外注意:
web不在 115 官方的扫码设备清单内,且最容易触发「IP登录异常」 (次日零点才解禁),所以 v1.3.0 已把它降到列表最后,默认改用支付宝小程序。
Q:目录选择器 / 文件管理里看不到目录、进不去子目录?
先确认面板左下角的版本号 ≥ v1.7.3(旧版本有这个 bug,见更新日志)。
如果已经是最新版还是不对:
- 在显示「该目录下没有子目录」的地方点「点这里诊断」, 会拉出 115 原样返回的数据,点「复制全部」发出来即可
- 也可以直接访问
http://你的IP:5000/api/dir/debug?cid=0(把cid换成你进不去的那个目录 id)
诊断内容里有三个关键信息:每一次请求实际发的参数、115 返回的
state/count/message、以及 raw_sample(原始数据的前 3 条)。
有了 raw_sample 就能看出目录条目到底带哪些字段,一次定位根因。
Q:扫码登录一直失败,怎么办?
按这个顺序排查:
- 点「接口自检」 —— 现在检查 4 项:取 token / 二维码 / 状态轮询 / 时间同步。
- 四项全绿 → 代码和网络都没问题,问题在 115 侧(风控或账号状态)
- 时间项标红 → 系统时间偏差超过 5 分钟会导致凭证签名校验失败, 去飞牛「系统设置 → 时间」校准或配置 NTP
- 确认手机上点了「确认登录」 —— 只扫码不确认拿不到凭据,这是最常见的漏操作
- 点「复制诊断」发出来 —— 含每步原始响应 + 每个设备的尝试结果
- 换网络 / 关掉代理再试 —— 115 对 IP 有风控,报「老乡验证失败」
(
errno=40101017)或「IP登录异常」时尤其如此 - 兜底:用「手动粘贴 Cookie」 —— 浏览器登录 115 网页版 → F12 → Network →
点任意一个请求 → 复制请求头里的整段
Cookie:(含UID/CID/SEID/KID), 整段粘进去即可,程序会自动识别。转存功能只需要 Cookie,完全不依赖扫码。
Q:执行任务报「登录状态失效」?
115 的 Cookie 会过期。去「账号管理」点「校验」确认,失效了就重新扫码登录。
Q:转存明明有文件,却提示「没有新文件需要转存」?
这是去重生效了。去重是拿分享里的顶层项和保存路径下的直接子项比名字。
如果你希望在新目录里再存一份,把「对比路径」改成一个空目录,或改个保存路径。
Q:115 转存有没有次数/风控限制?
有。115 对分享接收有频率限制,短时间大量接收会失败。
建议把定时间隔拉开一些(比如 30 分钟以上),不要多个任务同分钟同时跑。
Q:任务跑失败了想看细节?
任务行点「日志」看实时输出;跑完的记录在「转存日志」里,点「查看」能看到当时的配置快照、分级日志、去重跳过清单、实际新增清单。
Q:QMS 连接测试失败?
- 确认地址端口能 ping 通 / telnet 通;
- 确认 API Key 是在 QMS「系统设置 → API 密钥」里创建的;
- 同机 Docker 场景,填
127.0.0.1往往不行(容器内的 127.0.0.1 是自己),要用 NAS 内网 IP 或改用 host 网络。
Q:为什么 QMS 触发了但没生成 strm?
本项目只负责通知 QMS 去跑某个刮削任务,strm 的生成规则、源目录、媒体类型都在 QMS 那边配置。
要确认 QMS 里那个刮削任务的「源目录」指向的是能读到 115 文件的路径(挂载盘)。
Q:改密码?
「系统设置 → 登录密码」。默认 admin / zxcvbnm 务必改掉。
| 项 | bdsavepro(上游) | 115SavePro(本项目) |
|---|---|---|
| 网盘 | 百度网盘(baidupcs-py,BDUSS/STOKEN) |
115 网盘(p115client,UID/CID/SEID/KID) |
| 登录 | 只能手填 Cookie | 扫码登录为主,Cookie 为辅 |
| 去重键 | MD5 | SHA1(115 秒传机制,改名也能去重)/ 文件名+大小 |
| 转存范围 | 逐层下钻多选 | 逐层下钻多选 + 跨层级混选 |
| 离线下载 | 无 | 有(磁力 / ed2k / HTTP / 种子) |
| 文件管理 | 无 | 有(浏览 / 重命名 / 移动 / 复制 / 删除 / 分享) |
| 前端 | Vue 3 + Element Plus(需 Node 构建) | 零构建单页 HTML,镜像更小、部署更简单 |
| QMS 联动 | 手动绑定任务 ↔ 刮削目录 | 手动绑定任务/离线下载 ↔ 刮削目录 + 离线完成自动触发 |
| 通知渠道 | 25+ | 13 个主流渠道(含自定义 Webhook) |
上游项目里「调度 / 通知 / 历史 / QMS 客户端」这几层是不依赖网盘类型的,
它们只认一个约定:存储层暴露 transfer_share(task),返回 {success, message, file_count, new_items, ...}。
所以移植时只需要把存储层整个换掉(本项目 storage_115.py),上层几乎原样沿用即可。
仓库里带两个 GitHub Actions(.github/workflows/):
| 工作流 | 触发 | 作用 |
|---|---|---|
docker-publish.yml |
推送到 main / 推 v* tag / 手动 |
构建 Docker 镜像,推到 Docker Hub 和 GHCR |
ci.yml |
每次 push / PR | Python 语法 + 前端 JS 语法检查 + 防敏感文件误提交 |
镜像标签规则:
| 触发方式 | 产生的标签 |
|---|---|
推送到 main |
latest、sha-xxxxxx |
推 v1.2.0 这个 tag |
1.2.0、1.2、latest |
| 目标 | 是否需要配置 | 说明 |
|---|---|---|
GHCRghcr.io/<owner>/115savepro |
不需要 | 用仓库自带的 GITHUB_TOKEN,开箱即用。首次发布后包默认是私有的,想公开给别人拉需要手动改包可见性(见下) |
Docker Hub<用户名>/115savepro |
需要两个 Secret | 见下方「配置 Docker Hub」 |
工作流会先探测 Secrets:没配就自动只推 GHCR,不会报错(会在日志里给出提示)。
如果你的仓库是公开的,但别人拉不到镜像,多半是因为 GHCR 包默认私有:
- 打开
https://github.com/users/xiaoming677/packages/container/115savepro/settings - 页面底部 Danger Zone → Change visibility → 选 Public → 输入包名确认
自己用(
docker login ghcr.io或本机已登录)不需要这步。
只需要 1 个 Secret + 1 个 Variable(用户名不是机密,所以放 Variable 更合适):
① 在 Docker Hub 建 Access Token Docker Hub → 右上角头像 → Account settings → Personal access tokens → New Access Token, 权限选 Read & Write,建好立刻复制(只显示一次)。
② 在 GitHub 仓库里配置
仓库 → Settings → Secrets and variables → Actions:
| 标签页 | 类型 | 名称 | 值 |
|---|---|---|---|
| Variables | New repository variable | DOCKERHUB_USERNAME |
你的 Docker Hub 用户名 |
| Secrets | New repository secret | DOCKERHUB_TOKEN |
上一步复制的 Token |
⚠️ 两个容易踩的坑:
- 别建到 Environments 标签页里 —— 环境级 Secrets 只有工作流显式声明
environment:才读得到,本工作流没声明。要用 Actions 标签页。- 名称必须一字不差(全大写、下划线),值才填实际内容。
③ 重新触发构建
Actions → Build and Push Docker Image → Run workflow。
成功后推送目标会变成 Docker Hub + GHCR,日志里 登录 Docker Hub 会从 skipped 变成 success。
# 用户名(值直接用参数给,不会填错输入框)
gh secret set DOCKERHUB_USERNAME -R <owner>/<repo> # 或 gh variable set,两者都支持
# Token(交互式隐藏输入,不会留在命令历史里)
gh secret set DOCKERHUB_TOKEN -R <owner>/<repo>
# 核对
gh secret list -R <owner>/<repo>
gh variable list -R <owner>/<repo>说明工作流没读到配置。逐项检查:
gh api repos/<owner>/<repo>/actions/secrets --jq '.total_count' # 仓库级 Secrets
gh api repos/<owner>/<repo>/actions/variables --jq '.total_count' # 仓库级 Variables
gh api repos/<owner>/<repo>/environments # 是不是建到环境里了仓库级为 0 但环境里有内容 → 就是建错标签页了,见上面
# 改完代码
git add -A
git commit -m "feat: 新增 xxx 功能"
git push
# 要发正式版就打个 tag
git tag v1.1.0
git push origin v1.1.0- 只
git push→ 更新latest,NAS 上执行docker compose pull && docker compose up -d即完成升级 - 打 tag → 额外产出带版本号的镜像,方便回滚到指定版本
用户数据都在
config/挂载卷里,升级镜像不会丢数据。
找到「进不去子目录」的真正根因了 —— 两个,其中一个是我上一版引入的。
靠 v1.7.2 加的自助诊断,拿到了 115 返回的原始数据,一眼就看出问题:
根因一:pc(提取码)目录也有
v1.7.2 我把「有 pc → 判为文件」当成了硬信号。但真实数据里目录长这样:
{"cid": "3378630208007515606", "pid": "0", "n": "04_影视",
"pc": "fbd53em77w07qrw31h", "fc": 0} ← 目录也有 pc,且没有 fid / sha结果所有目录又被判成了文件,从列表里消失。这一版把 pc 从判据里彻底去掉,
改成:
| 顺序 | 条件 | 结论 |
|---|---|---|
| 1 | 有非空 sha / sha1 |
文件 |
| 2 | 有 fid 且没有 pid |
文件 |
| 3 | 其余 | 目录 |
根因二(更关键):cid 是 19 位整数,超出 JS 的安全整数范围
115 的 cid 是 3378630208007515606 这样的 19 位数,而 JavaScript 的
Number.MAX_SAFE_INTEGER 只有 16 位(9007199254740991)。
JSON 把它当数字传给浏览器时,JSON.parse 会静默丢精度:
真实 cid : 3378630208007515606
JS 解析后: 3378630208007515648 ← 差了 42
于是点一个目录时,实际请求的是另一个 cid,返回空 —— 表现正是「进不去子目录」。 这也解释了为什么它从第一版就存在,而且跟目录判定无关。
修法:id 全链路改用字符串承载。后端 id / parent_id / cid 一律输出字符串,
前端所有 Number(cid) 转换全部去掉,需要整数时由后端转(Python 的 int 是精确的)。
这个精度问题同样影响文件操作:勾选文件后删除/移动/重命名时, 丢精度的
fid可能操作到另一个文件。所以一并修掉了。
其他
only_dir的兜底保留(nf=1返回空则去掉nf重来并按is_dir过滤)- 自助诊断保留:目录列表为空时有「点这里诊断」,可拉出 115 原始响应
- 回归测试改用真实响应样本固化,两个根因都有对应用例
升级后建议:如果之前建的任务用过「指定转存文件夹」,那些选中项的 id 可能是 早期丢精度的值,进编辑里重新选一次即可。
继续加固目录列表:不再依赖单一参数,并加了自助诊断。
v1.7.1 修了「目录被误判成文件」和「type=0 应改 nf=1」,但如果 115 那边
对 nf 参数返回空列表,表现依然是「进不去子目录」。这一版把这种可能性也堵掉了:
only_dir增加本地过滤兜底:先带nf=1请求;若返回空,去掉nf再请求一次, 然后自己按目录/文件过滤。「只看目录」不再依赖任何服务端参数- 目录判定改为多重信号(顺序:有
sha/sha1/pc→ 文件;有fid且无pid→ 文件; 其余 → 目录)。这样即使某些接口形态下目录也带fid,也能靠pid认出来 - 新增自助诊断:目录列表为空时,界面会给出「点这里诊断」,
一键拉出 115 原样返回的 JSON(含每一次请求实际发的参数、
state/count、 以及原始数据样本),可直接复制发给开发者定位 - 新增
GET /api/dir/debug?cid=N,同上,便于脚本化排查
为什么加诊断:目录这类问题几乎都出在「115 返回的字段形态」上, 光看归一化后的结果猜不出根因。把原始响应拿出来,一次就能定位。
修复「保存路径只能选到第一层目录,进不去子目录」。
根因有两个,都在目录列表这一层:
- 目录被误判成文件。 原来用 p115client 的
normalize_attr_simple判目录, 它优先看fc字段(is_dir = int(fc) == 0)。而fc在不同接口里语义不一致 —— 只要它不是 0,目录就会被判成文件,紧接着取info['fid']抛KeyError, 被容错分支吞掉后统一变成「文件」,于是非空目录从列表里整体消失, 选择器里自然什么都进不去。 现在改为只认「有没有fid」(115 网页接口的稳定规则:文件有fid,目录没有), 与 p115client 自己overview_attr()的判定方式一致。 - 「只看目录」的参数用错了。 原来给 115 传的是
type=0,但那个参数的含义是 「文件类型=全部」,仍然属于按类型筛选 —— 而按类型筛选时目录还需要额外stdir=1才会带出来,很容易变成「一个目录都看不到」。 现在改用官方专用参数nf=1(不要显示文件)。
同一处修复也让文件管理页受益:之前那里的非空目录也被当成文件显示。
顺带:
- 目录选择器打开时自动定位到输入框里已填的路径,不再从根目录一层层点
- 选择器里新增「新建目录」(建完直接进去)和「刷新」,行尾提示「进入 ›」, 底部固定显示当前路径
- 进空目录不再报错,正常显示「该目录下没有子目录」
- 分页
limit夹到 115 的上限 1150;非法cid返回明确提示而不是 500
新增「备份与恢复」,在面板里就能导出和还原全部设置。
- 导出:可选是否包含「账号 Cookie / 后台密码 / 历史记录」三项;
支持「导出并下载」或「备份到服务器」(存
config/backups/,最多留 20 份) - 恢复:可从文件恢复,也可直接在列表里挑一份服务器备份恢复
- 恢复方式二选一:覆盖(结果与备份时一致)/ 合并(账号、任务、QMS 连接按 ID 合并,现有内容保留)
- 可按内容选择恢复:账号 / 转存任务 / QMS 联动 / 通知渠道 / 系统设置 / 后台登录密码 / 历史记录,可单独只恢复其中一项
- 恢复前自动存档当前状态(来源标为「恢复前自动存档」),选错文件能退回
- 安全校验:备份文件需通过
app/kind/format校验,误传别的 JSON 会被拒绝; 来自更高版本的备份会明确拦下并提示先升级 - 关闭「包含账号 Cookie」后导出的文件不含登录凭据,可以放心发给别人排查问题
config/backups/已加入.gitignore与.dockerignore—— 备份含 Cookie,不会被提交或打进镜像
实现细节:备份 =
config.json+ SQLite 里的 QMS 配置与历史(task_history/qms_trigger_log/offline_log)。恢复走storage.reload()+ 重载定时任务, 不用重启容器。
保存任务后自动执行,一并串起 QMS 全流程
以前建完任务还要手动点一次「执行」,现在保存即执行:
新建/编辑任务并保存 → 自动执行转存 → 转存到新文件 → 自动生成 strm → 等待 → 刮削
全程不用再手动点。前提是在「连接 QMediaSync」里为该任务绑好了连接 (想先出 strm 就再绑一个同步路径,见上文)。
- 可在「系统设置 → 全局定时」里关掉这个默认行为;新建/编辑任务的对话框里 也有「保存后立即执行一次」勾选框,可对单个任务临时取消
- 停用的任务不会自动跑;已在执行中的任务不会重复触发(有运行态检查)
- 保存接口的返回增加
auto_started字段,前端据此提示「已保存,并已开始执行」, 而不是笼统一句「已保存」 - 默认开启;想回到旧行为:设置里关掉,或建任务时取消勾选
关于 QMS 自动触发:转存后触发 QMS 的能力一直都有,但要同时满足三个条件 —— ① 「启用 QMS 联动」开着;② 「转存到新文件后自动触发」开着; ③ 该任务在「连接 QMediaSync」里绑定了刮削目录(想要先出 strm 就再绑一个同步路径)。 任一条不满足都不会触发。 另外只有转存到新文件(去重后确实有新增)才触发, 全是已存在时会跳过以避免空跑。
一键更新不可用时,明确告诉你原因
原来只有一句「未挂载 /var/run/docker.sock」,无法区分三种完全不同的情况, 用户根本不知道该往哪修。现在拆开说:
| 情况 | 真实原因 | 该怎么办 |
|---|---|---|
| 容器内没有该文件 | 挂载没生效 —— 改完 compose 没重建容器 | 三个点 →「重构」/ docker compose up -d --force-recreate |
| 有文件但没权限 | 容器用户不在 docker 组 | 去掉 user:,或加 group_add |
| 有权限但连不上 | socket 在、daemon 异常 | 会打印 Docker 返回的状态码与内容 |
最容易被忽略的是第一条:只点「启动」不会重新挂载 volumes,必须重建容器。
另外:
- 找不到
/var/run/docker.sock时会在/var/run、/run扫一遍, 发现别的docker.sock就提示「路径可能写歪了」 - 容器 ID 识别增加从
/proc/self/cgroup解析的兜底(compose 自定义 hostname 时) - 「不可用」提示改为常驻显示(以前只在有新版本时才出现)
GET /api/update/env新增socket_exists/socket_found/running_as_root
QMediaSync 联动升级:先生成 strm,等它落地后再刮削
QMS 里「生成 strm」和「刮削」是两个独立动作 —— 前者把 115 上的视频映射成本地
.strm,后者才去 TMDB 抓元数据。顺序不能反:没有 strm 就刮不到东西。
之前版本转存完直接触发刮削,新转存的文件很容易因为 strm 还没生成而被漏掉。
现在每条连接可以额外绑定一个「同步路径」,流程变成:
转存到新文件 → ① 触发生成 strm → 等待 → ② 触发刮削
等待策略三选一:
| 方式 | 说明 |
|---|---|
| 轮询(默认,推荐) | 轮询同步路径的 is_running(QMS 语义:0 未运行 / 1 队列中 / 2 运行中),等它真正归零再刮削 |
| 固定延迟 | 等指定秒数再刮削 |
| 不等待 | 触发后立即刮削(等价旧行为) |
- 轮询最长等待默认 900 秒,超时不算失败 —— 会照常继续刮削(已生成的 strm 仍值得刮),并在触发日志里写明
- 实现上有个坑要绕:刚触发的瞬间任务可能还没入队,立刻查
is_running会看到 0, 被误判成"已完成"。所以先等一小会儿再开始轮询,并用「是否见过运行态」来区分 「秒完」和「还没开始」 - 新增「查看同步路径」弹窗:可看 115 远程路径 → 本地 strm 路径的映射与运行状态
- 连接支持编辑(改绑同步路径 / 换刮削任务 / 调等待策略)
- 触发日志记录完整流程,例如:
生成 strm:已加入同步队列 → 等待 strm 完成:用时 42 秒 → 刮削:已开始 - 完全向后兼容:不绑定同步路径时行为与以前一致,老配置不用改
新增接口:GET /api/qms/sync-paths、POST /api/qms/link/update。
用到的新 QMS API:GET /sync/path-list、POST /sync/path/start。
新增「版本更新」:面板内检查 + 一键更新
「系统设置 → 版本更新」现在可以:
- 检查更新(零配置)—— 对比 GitHub Releases 上的最新版,显示版本号、发布时间与更新说明
- 一键更新(需额外挂载
docker.sock)—— 点一下就自动完成「拉新镜像 → 重建容器」, 页面自动等待服务恢复,不用再去飞牛 Docker 界面手动拉取 + 重构
实现要点:容器无法自己重建自己。 一旦被停止,进程就结束了,后续动作没人执行。
所以点「立即更新」后会启动一个一次性助手容器(docker:cli,自带 compose 插件),
由它在旧容器退出之后执行:
sleep 3 && cd <compose 项目目录> && docker compose pull && docker compose up -d --force-recreate走 docker compose 而不是用 Docker API 手拼容器参数,是因为 compose 文件里有权威配置 ——
端口、挂载、环境变量、网络模式全部以它为准,重建后不会走样。项目目录自动读取容器标签
com.docker.compose.project.working_dir,飞牛界面粘贴 YAML 的场景也能正确定位。
助手容器用 AutoRemove,执行完自动删除;更新过程写入 config/update.log,
可在面板里点「上次更新日志」查看。
不挂 docker.sock 也能用:检查更新照常工作,界面会直接给出手动更新命令。
(挂载 docker.sock 相当于把宿主机 Docker 的控制权交给该容器,UI 与文档里都标注了这一点。)
GET /api/update/env 会明确报告「缺什么、怎么补」,而不是笼统一句"不支持"。
扫码登录:一次扫码自动轮试多个设备身份
- 扫码确认后,系统会依次用 alipaymini / wechatmini / android / 115android / ios / ipad / os_windows / web 尝试换取 Cookie,第一个成功的即采用。 不再需要用户手动换设备重扫,也顺带产出「每个设备分别报什么错」的完整明细。
- 明显是「还没确认 / 被风控」时提前退出,不浪费请求。
- 每次尝试的结果都写进诊断日志。
默认登录设备改回 alipaymini
- 依据 xiaoya-alist 修 115 扫码的 commit:115 的扫码设备清单里
windows/mac/linux/wechatmini/alipaymini 等是官方支持的,
web不在清单内, 且 p115client 文档警告app="web"最容易触发「IP登录异常」(次日零点才解禁)。 故把web降到列表最后。
修复诊断日志「只有 2 条」的 bug
- 原因:等待扫码阶段 115 返回
data={},raw_status为None, 与初始值相同 → 第 3 步完全不写日志,错误也被「等待扫码」吞掉。 改用哨兵初值,并强制「接口报错时必记」。
自检新增第 4 项:时间同步检查
- 对比容器时间与 115 服务器时间(HTTP
Date头)。偏差超过 5 分钟会导致 扫码凭证签名校验失败 —— 这是排查「参数错误」时最容易被忽略的一环。
修复扫码登录「参数错误」—— 默认登录设备改为「网页端」
复盘两次失败发现共同点是 alipaymini(支付宝小程序)这个命名空间:
| 版本 | 取 token 用的 app | 换取凭据用的 app | 结果 |
|---|---|---|---|
| v1.0.0 | web |
alipaymini |
参数错误 |
| v1.1.0 | alipaymini |
alipaymini |
参数错误 |
而症状是「手机扫得出声音、但确认后失败」—— 这正是小程序端命名空间签发的二维码,
用 115 主 App 扫无法完成确认的表现。115 官方网页版用的是 web 命名空间,
而本项目的场景(电脑屏幕显示二维码 → 手机 App 扫码)与官网登录完全一致,
所以默认改为 web,并按其可用性重排了设备列表。
新增诊断能力(这次能定位问题靠的就是它)
- 「扫码登录」弹窗里新增实时诊断日志:token / 二维码 / 状态轮询 / 换取凭据 四步的原始 115 响应全部记录并展示,含时间戳
- 「复制诊断」按钮:一键复制全部诊断信息,直接发给开发者定位
- 修复一个隐蔽 bug:状态轮询接口报错时被静默当成「还在等待扫码」, 现在会把 115 的 message + errno 明确显示出来
- 「重新生成二维码」按钮,换设备后不用关闭弹窗重开(切换下拉框也会自动重新生成)
- 新增版本号显示:面板左下角显示当前版本,
GET /api/version也可查,docker inspect能看到构建时间和 commit —— 更新后一眼确认有没有生效 - 镜像补充 OCI 标准 LABEL(版本、源码地址、构建时间、commit)
- README 补充「更新到最新版」完整说明:如何确认更新生效、常见问题、 数据备份、清理旧镜像
修复 115 扫码登录「参数错误」
- 修正扫码登录的登录设备(app)不一致问题:原先二维码是用
web身份生成的, 但换取 Cookie 时用的是页面上选的设备(如alipaymini),115 会把这种 不匹配视为参数错误而拒绝。现在 token / 二维码 / 换取凭据三步统一使用同一个 app。 - 扫码失败时不再只给一句笼统提示,而是透出 115 返回的原始 message 与 errno, 并针对「参数错误」「IP登录异常」等情况给出对应建议。
- 新增扫码登录自检(「扫码登录 115」弹窗里的「接口自检」按钮): 不需要真的扫码,直接验证 token / 二维码 / 状态轮询三个接口是否都通, 方便快速判断是代码问题还是 115 风控。
- 手动粘贴 Cookie 的入口保留,作为扫码被风控拦截时的兜底方案。
说明:如果自检三步全通过、但扫码确认后仍拿不到凭据,通常是 115 对本机 IP 的 风控(表现为「老乡验证失败」
errno=40101017或「IP登录异常」)。 换网络/挂代理,或直接用「手动粘贴 Cookie」。另外建议把登录设备换成115生活_支付宝小程序(默认值,比网页端更不容易触发风控)。
- 从 0 实现 115 网盘存储层(基于
p115client),支持扫码登录与 Cookie 导入、多账号切换 - 转存任务:定时执行、启用/停用、正则过滤与替换、排除清单、指定转存文件夹(逐层下钻 + 跨层级多选)
- 去重三档:文件名 / 文件名+大小 / SHA1(115 秒传机制下改了文件名也能拦住),支持独立的对比路径
- 离线下载:磁力 / ed2k / HTTP / FTP / 种子提交到 115 云端,进度查看、删除、重试、清空
- 文件管理:浏览、新建文件夹、重命名、移动、复制、删除、搜索、生成分享链接
- QMediaSync 联动:手动绑定「转存任务 / 离线下载」↔「QMS 刮削目录」, 仅在转存到新文件时触发(避免空跑),离线下载完成自动触发,带触发日志
- 转存历史落 SQLite(配置快照 + 分级日志 + 去重跳过清单 + 实际新增清单),每任务保留最近 10 次
- 13 个通知渠道、空间用量告警、零构建单页前端(免 npm 构建)、Docker 化 + GitHub Actions 自动构建
本项目为二次开发作品,沿用上游的 AGPL-3.0(见 LICENSE 与 NOTICE)。
p115client 为 MIT。115 网盘接口为非官方公开接口,请合理使用,注意账号安全与风控。