使用 qbt_migrate 批量修改 qbitorrent 保存路径


背景

最近,我着手搭建一套自动化媒体系统,旨在实现从媒体请求、资源索引、智能下载到最终播放的全流程自动化。这套系统的核心组件包括:

  • Jellyseerr: 负责用户媒体请求和发现。

  • Prowlarr: 作为统一的索引器(Indexer)管理器,为后续的 arr 系列软件提供种子站点接口。

  • Arr 系列:

    • Sonarr: 电视剧集自动化管理。

    • Radarr: 电影自动化管理。

    • Lidarr: 音乐自动化管理。

  • qBittorrent: PT 下载客户端,负责从各大站点下载资源。

  • Jellyfin: 媒体服务器,提供美观易用的播放界面。

这便是我追求的终极形态,一个高度整合与自动化的家庭媒体中心。

曾经的方案

在拥抱 arr 系列之前,我的下载流程相对简单:主要依赖 PT 助手 Plus 浏览器插件。我会通过它直接在 PT 站点搜索资源,然后将下载任务发送到 qBittorrent。qBittorrent 的下载目录直接设置为 Jellyfin 的媒体库目录。

所有的存储资源都通过 NFS (Network File System) 共享。例如,NFS 服务器上导出的下载_根_目录结构大致如下:

/export/download/
├── movie/
├── music/
└── tv/

而在客户端,这个 NFS 共享会被挂载到类似 /mnt/download 的路径,qBittorrent 和 Jellyfin 都直接使用这个路径下的子目录。

引入 Arr 系列后的挑战

Arr 系列软件的引入,旨在进一步提升媒体管理的自动化程度和智能化水平。它们主要负责以下几项关键任务:

  1. 媒体监控: 持续监控用户期望观看的媒体资源(电影、剧集、音乐),一旦发现有符合条件的资源发布,便自动通知下载器(如 qBittorrent)进行下载。

  2. 进度追踪: 监听下载器的下载进度,确保任务顺利完成。

  3. 智能整理: 下载完成后,arr 会执行整理操作。这包括将下载好的文件从临时下载目录移动到最终的媒体库目录。在移动过程中,arr 还会根据其从 TMDB、TVDB 等元数据服务商获取的信息,将文件和文件夹重命名为统一、规范的格式,极大地方便 Jellyfin 等播放软件的刮削和识别。

然而,arr 的整理机制对目录结构提出了新的要求:

  • 硬链接(Hard Link)的执念: 为了在不中断 PT 做种(seeding)的前提下完成媒体整理,arr 强烈推荐(甚至在某些配置下强制)使用硬链接。硬链接可以让同一个文件在文件系统中拥有多个路径入口,删除其中一个入口并不会影响文件实体和其他入口,也不会额外占用磁盘空间。这完美解决了“整理后无法继续做种”和“复制整理导致空间浪费”的两难问题。

  • 下载目录与媒体目录的分离: arr 官方不建议将下载目录和最终的媒体库目录设置在同一路径下或存在重叠。

这就引出了一个核心问题:我原先的 NFS 目录结构 download/movie 既是下载目录,又是媒体库目录,这显然不符合 arr 的最佳实践。

更重要的是,硬链接的创建有一个前提:源文件和目标链接必须位于同一个文件系统(挂载点)内。

如果我简单地在 OMV (OpenMediaVault) 或其他 NAS 系统上再创建一个名为 media 的 NFS 导出节点,例如:

  • /export/download -> 客户端挂载为 /download_mount

  • /export/media -> 客户端挂载为 /media_mount

即使 /export/download/export/media 在 NFS 服务器的底层实际上指向同一个物理磁盘或文件系统,但在 arr 软件(通常运行在 Docker 容器或单独的虚拟机中)看来,/download_mount/media_mount 是两个完全不同的挂载点(文件系统)。因此,尝试在它们之间创建硬链接将会失败,然后回退到复制模式。

解决方案:统一 NFS 根目录与 qBittorrent 路径迁移

为了满足硬链接的前提条件,并遵循 arr 的目录规范,我需要重新规划 NFS 的导出和客户端的目录结构。理想的结构应该如下:

在 NFS 服务器上,创建一个统一的根目录,例如 mediaRoot,然后将原来的下载目录和规划中的媒体目录都作为其子目录:

/export/mediaRoot/
├── download/ # qBittorrent 的下载目录
│ ├── movie/
│ ├── music/
│ └── tv/
└── media/ # arr 系列整理后的媒体库存放目录,供 Jellyfin 使用
├── Movies/ # Radarr 管理
├── TV Shows/ # Sonarr 管理
└── Music/ # Lidarr 管理

然后,NFS 只导出这一个 mediaRoot 根目录。

在客户端(运行 qBittorrent、arr 系列、Jellyfin 的机器或容器),将这个 /export/mediaRoot 统一挂载到一个路径,例如 /mnt/mediaRoot。这样一来:

  • qBittorrent 的下载路径配置为:/mnt/mediaRoot/download/movie 等。

  • arr 系列软件配置的下载目录是:/mnt/mediaRoot/download/...

  • arr 系列软件配置的媒体库(根)目录是:/mnt/mediaRoot/media/

  • Jellyfin 的媒体库也指向:/mnt/mediaRoot/media/...

由于所有的操作(下载、硬链接创建、媒体库读取)都在同一个挂载点 /mnt/mediaRoot 下进行,硬链接的创建便畅通无阻。

这个方案最大的挑战在于:我现有的 qBittorrent 中已经有大量的下载任务,它们内部记录的保存路径还是旧的 /download/movie/xxx 这样的格式。在调整了服务器上的物理文件位置和客户端的挂载结构后,这些任务会因为找不到文件而报错。我必须批量更新 qBittorrent 中所有任务的保存路径,将它们从例如 /download/tv/SomeShow 修改为 /mediaRoot/download/tv/SomeShow (这里的路径是 qBittorrent 视角下的路径,对应于客户端挂载后的路径 /mnt/mediaRoot/download/tv/SomeShow)。

幸运的是,社区早有解决方案:jslay88/qbt_migrate 这个强大的工具可以帮我完成这个艰巨的任务。

qbt_migrate 是如何工作的?

qBittorrent 会为每一个下载任务(无论是活跃的还是已完成的)生成一个 .fastresume 文件。这个文件以 Bencode 编码存储了任务的各种元数据,其中就包括了至关重要的 “save_path” (保存路径)字段。

qbt_migrate 工具的工作原理就是:

  1. 读取指定的 BT_backup 目录(存放所有 .fastresume 文件的地方)。

  2. 解析每一个 .fastresume 文件。

  3. 根据用户提供的旧路径和新路径规则,修改 “save_path” 字段。

  4. 将修改后的内容写回 .fastresume 文件。

特别注意:

  • 操作前必须彻底停止 qBittorrent 服务,否则可能导致配置文件损坏或数据丢失。

  • 务必备份 BT_backup 目录,以防万一出现意外,可以随时恢复。

我的 qBittorrent 是通过 LXC (Linux Containers) 在 Proxmox VE 上运行的,使用了社群的 PVE Helper Scripts 进行创建。经过一番查找,我发现其 .fastresume 文件(即 BT_backup 目录)的实际路径并非通常认为的 $HOME/.local/share/data/qBittorrent/BT_backup/home/user/.local/share/qBittorrent/BT_backup,而是在 LXC 容器内的 /.local/share/data/qBittorrent/BT_backup

具体迁移步骤

以下是我完成整个迁移过程的详细步骤。为了降低风险,我针对每个媒体分类(如电影、音乐、电视剧)分别执行了停止服务、路径迁移和重启服务的操作(即下述步骤 2、4、7)。

  1. 安装 qbt_migrate 工具: 推荐使用 pip 进行安装,确保您的环境中已安装 Python。

    pip install qbt_migrate
  2. 停止 qBittorrent 服务: 在运行 qBittorrent 的机器或 LXC 容器内执行(请根据您的实际服务名调整):

    sudo systemctl stop qbittorrent-nox.service
  3. 备份 BT_backup 目录: 找到 .fastresume 文件所在的 BT_backup 目录,并进行完整备份。这是非常重要的一步,以防操作失误导致数据丢失。例如,如果目录位于 /.local/share/data/qBittorrent/BT_backup

    sudo cp -ar /.local/share/data/qBittorrent/BT_backup /path/to/your/backup_location/BT_backup_OLD
  4. 执行 qbt_migrate 修改路径: 根据您的旧路径前缀和新路径前缀,使用 qbt_migrate 工具逐个分类执行路径替换。这里的路径是 qBittorrent 内部记录的路径。

    # 电影类
    qbt_migrate -b /.local/share/data/qBittorrent/BT_backup -e /download/movie -n /mediaRoot/download/movie
    # 音乐类
    qbt_migrate -b /.local/share/data/qBittorrent/BT_backup -e /download/music -n /mediaRoot/download/music
    # 电视剧类
    qbt_migrate -b /.local/share/data/qBittorrent/BT_backup -e /download/tv -n /mediaRoot/download/tv
    • -b--qb-backup-dir: 指定 qBittorrent 的 BT_backup 目录路径。

    • -e--existing-path: 现有的(旧的)路径前缀。

    • -n--new-path: 新的路径前缀。

  5. 在 NFS 服务器上迁移物理文件: 登录到您的 NFS 服务器(如 OMV),将实际文件移动到新的目录结构中。 重要: 确保 NFS 服务器上的目标路径与您在 qbt_migrate 中设置的_新路径的后半部分_以及您规划的_新 NFS 导出子路径_一致。

  6. 调整 NFS 导出配置与用户映射 (关键步骤): 在物理文件迁移和 qBittorrent 内部路径更新完成后,NFS 服务端和客户端的正确配置是确保一切顺利运行的最后一道关卡,也是核心环节。这不仅关乎所有服务能否正常访问新的统一路径 /export/mediaRoot,更直接决定了能否彻底解决因权限混乱导致的硬链接创建失败问题

    问题的根源:no_root_squash 与多用户写入冲突

    我之前的 NFS 导出参数中包含了 no_root_squash 选项。此选项的本意是允许客户端的 root 用户在挂载的 NFS 共享中也保留其 root 权限(即 UID 0, GID 0)。然而,在自动化媒体系统中,像 arr 系列服务、qBittorrent 或其他辅助脚本,它们在客户端可能以各自不同的用户身份运行(例如,qBittorrent 以用户 qb_user (UID 1001) 运行,Sonarr/Radarr 以用户 arr_user (UID 1002) 运行,甚至有时会是 root 用户)。当这些不同身份的服务通过 NFS 写入文件时,由于 no_root_squash (以及默认的UID/GID透传机制),这些文件在 NFS 服务器上会保留它们在客户端写入时的原始 UID 和 GID。这种文件所有权的不一致性,正是导致 arr 服务在尝试为 qBittorrent 下载的文件创建硬链接时,频繁遭遇“权限不足” (Permission Denied) 错误的主要原因。

    解决方案:统一用户映射与权限固化

    要根治此问题,需要两方面的调整:

    • NFS 导出选项调整:利用用户映射功能,强制将所有来自客户端的 NFS 访问都映射到 NFS 服务器上一个_统一的、预设的_用户和组。这通常通过 all_squashanonuidanongid 选项组合实现。
    • 服务器端文件系统权限固化:确保目标NFS导出目录及其内容在服务器上的所有权和权限与映射后的用户身份一致。

    基于此,我的最终 NFS 导出配置(例如在 OMV 的 /etc/exports 文件或其 WebUI 的高级选项中)从之前的类似 (rw,sync,no_subtree_check,no_root_squash) 修改为:

    /export/mediaRoot *(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=100)

    这里的关键参数解释如下:

    • all_squash: 此选项会“压扁”所有连接客户端的 UID 和 GID,将它们全部映射为匿名用户的 UID 和 GID。这是实现所有权统一的最直接手段。
    • anonuid=1000: 指定经过 all_squash 处理后的匿名用户的 UID 为 1000
    • anongid=100: 指定经过 all_squash 处理后的匿名用户的 GID 为 100

    重要提示

    • . 服务器端用户和组的存在性:请务必确保在 OMV (NFS 服务器) 上,UID 为 1000 的用户和 GID 为 100 的用户组确实存在。如果不存在,需要先创建它们。
    • . 服务器端目录权限:该用户 (UID 1000) 必须对 NFS 导出的根目录 /export/mediaRoot 及其所有子目录和文件拥有恰当的读、写、执行权限。

    为了确保第二点得到彻底执行,尤其是在调整了映射规则后,我在 NFS 服务器上额外执行了以下命令,以递归方式将 /export/mediaRoot 目录及其全部内容的所有权统一设置为映射后的 UID 和 GID:

    Terminal window
    sudo chown -R 1000:100 /export/mediaRoot
  7. 重启 qBittorrent 服务: 回到运行 qBittorrent 的机器或容器内:

    sudo systemctl start qbittorrent-nox.service
  8. 验证: 打开 qBittorrent WebUI,检查一些任务是否状态正常,路径是否已更新为新的 /mediaRoot/download/... 格式。可以尝试对一个已完成的下载任务执行“强制校验”,如果文件路径正确,校验应能顺利通过。同时,检查 arr 系列软件是否能正确识别和处理位于新下载路径下的文件,并能否成功创建硬链接到新的媒体库目录(例如 /mediaRoot/media/...)。

通过以上步骤,我成功地将我的下载目录结构迁移到了 arr 系列软件推荐的模式,并利用 qbt_migrate 工具无缝更新了 qBittorrent 中的大量任务信息,为后续全自动化媒体管理流程打下了坚实的基础。整个过程虽然涉及多个环节,但清晰的规划、正确的工具以及分阶段实施的策略使得迁移得以顺利完成。