Files
UHD_Media_Tool/docs/porting/moviepilot-filemanager.md
T
rootandClaude Opus 5.5 b343dddbf6 docs: 清理过程文档,CHANGELOG 补齐 0.3.8–0.3.14
- 删除 docs/plan 下 00–18 的计划、评审、验收、发布记录与升级证据,docs/spikes 与 spikes 验证代码
  (保留 spikes/movieclaw-src 本地克隆),以及 4 份一次性审查 / 调研;全部可在 ceb66f8 查到
- MP 对照移到 docs/porting/moviepilot-v3-comparison.md;经验教训改为 docs/plan/lessons.md(补第 25 条)
- docs/plan/README.md:文档放哪、当前状态、待办
- 引用改写:代码注释指向在维护的文档,其余注明已删除与 git 位置;清掉原有 6 个失效链接
- README 开头改为当前状态与入口;CHANGELOG 补齐 0.3.8–0.3.14 与工程变更

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 16:08:09 +08:00

12 KiB
Raw Blame History

MoviePilot v3 文件管理:行为对照记录

格式:REQ §14.1.2 迁移交付规范。只记录行为、接口形状和交互,不复制 MP 源码或前端模板(MP 后端 GPLv3、前端同属 jxxghp 组织,按 license-and-provenance.md 一律"只参考行为")。 对应规格:06-file-manager-spec(已删除,见 git ceb66f8);需求:REQ §2.1、§7.1/§7.1.1、§8.2/§8.3。

0. 上游版本与核验范围

项 值
后端 /opt/moviepilot-plus/upstream/MoviePilot,提交 e6820a8(v3.0.0)
前端 /opt/moviepilot-plus/upstream/MoviePilot-Frontend,提交 17b9d51
核验方式 只读源码;未运行服务;GitHub issue 不可访问,"痛点"一节是按代码推断,标 推断

读过的文件:

层 路径 职责
端点 app/api/endpoints/storage.py(挂在 /api/v1/storage,app/api/routers.py:72) list / mkdir / delete / download / image / rename / manage
端点 app/api/endpoints/transfer.py /transfer/name(推荐名)、/transfer/manual(手动整理/预览)、/manual/target-path、/manual/history
端点 app/api/endpoints/media.py /media/recognize_file(识别)、/media/scrape/{storage}(刮削)
端点 app/api/endpoints/history.py:403 删除整理历史,可选 deletesrc / deletedest
链 app/chain/storage.py StorageChain:把调用透传给 run_module;delete_media_file 带空目录上溯清理
模块 app/modules/filemanager/storages/{local,smb,rclone,alist,alistgo,alipan,u115}.py 存储后端,统一基类 StorageBase(storages/__init__.py)
Schema app/schemas/file.py FileURI(storage+path,u115:/x 形式 URI)、FileItem、StorageUsage、StorageTransType
前端 src/pages/filemanager.vue → src/views/reorganize/FileBrowserView.vue → src/components/filebrowser/{FileBrowser,FileToolbar,FileNavigator,FileList}.vue 页面
前端 src/components/dialog/{FileRenameDialog,ReorganizeDialog,ScrapeDialog,MediaInfoDialog,FileNewFolderDialog,ProgressDialog}.vue 操作弹窗

1. 后端端点清单

所有 storage 端点都用 POST + 请求体 FileItem(storage + path,目录 path 以 / 结尾),鉴权 get_current_active_manage_user(有"管理"权限的用户即可;/manage 要超级用户)。

端点 参数 返回 说明
POST /storage/list body FileItem;query sort=name|time(默认按 mtime 倒序)、keyword(glob,*/?,忽略大小写) FileItem[] 一次返回整个目录,无分页;目录在前(local 实现先列子目录再列文件),再整体按 sort 重排;path 指向文件时返回单元素
POST /storage/mkdir body 父目录 FileItem;query name {success} local:mkdir(parents=True, exist_ok=True),已存在也算成功
POST /storage/delete body FileItem {success} local:文件 unlink,目录 rmtree;无回收站、无确认引用
POST /storage/download body FileItem 文件流 local 直接 FileResponse(原路径);网盘先下载到临时目录
POST /storage/image body FileItem image/jpeg 字节 图片预览,同样整文件读入内存
POST /storage/rename body FileItem;query new_name、recursive {success,message} 见 §2.1
POST /storage/manage {target, action, params} 透传 网盘登录/二维码等存储管理动作,与文件管理页无直接关系
GET /transfer/name path、filetype=file|dir {name} 识别后按重命名模板给出推荐名;目录取模板中的媒体根目录名
GET /media/recognize_file path、media_source? Context(meta_info + media_info) 识别,只读
POST /media/scrape/{storage} body FileItem;query media_source?、media_id?、type_name?、music_type? {success,message} 刮削,见 §2.5
POST /transfer/manual body ManualTransferItem;query background 预览项或执行结果 手动整理,见 §2.4
POST /transfer/manual/target-path 同上 推荐目标存储/路径 按源路径匹配"目录配置"(下载目录 → 媒体库目录)
POST /transfer/manual/history 同上 {reorganize, history_count} 源文件是否已有成功整理记录(提示"重新整理")

StorageBase 抽象方法:list / create_folder / get_folder / get_item / get_parent / delete / rename / download / upload / detail / copy / move / link / softlink / usage / snapshot。文件管理页只用到前 8 个;copy/move/link 只在整理(TransHandler)里用。

2. 各操作的业务规则

2.1 重命名

  • 非递归:StorageChain.rename_file → 存储后端 rename(path, parent/new_name)。local 是 os.rename(经 fsproxy)。
  • 不更新 transferhistory、不通知媒体服务器、不处理同名字幕/NFO。已整理文件改名后,历史里的 dest 变成悬空路径;之后"重新整理""删除历史连带删目标"都会指向旧路径(推断:删不到文件、提示失败或静默成功)。
  • 不检查目标名是否已存在;local os.rename 在 POSIX 上会静默覆盖同名文件(推断,rename(2) 语义)。
  • recursive=true(弹窗里的"自动重命名目录内媒体文件"):只遍历一层子文件,过滤媒体/字幕/音频扩展名,逐个 recognize_by_path → recommend_name → 改名;任何一个识别失败就中止并返回,前面已改的不回滚;最后再把目录本身改成用户输入的名字。进度走 ProgressKey.BatchRename。
  • 推荐名:前端弹窗里有"自动识别名称"按钮,调 /transfer/name。

2.2 新建目录 / 下载 / 预览

  • mkdir:无名称校验(/、.. 由存储后端自己处理;local 用 Path(parent)/name,name="../x" 可以跳出当前目录 —— 推断)。
  • download/image:local 直接读原路径,无根目录限制;管理用户可读容器内任意文件(配合 list 从 / 开始浏览)。

2.3 删除

  • /storage/delete:直接 unlink/rmtree。不查 transferhistory、不查下载器是否仍在做种、不查是否媒体库根。前端只有一次确认弹窗。
  • 另一条路径是"整理历史 → 删除"(/history/transfer,deletesrc/deletedest):走 StorageChain.delete_media_file:拒绝删除路径层级 ≤ 2 的目录(/、/media);文件删除后向上清理"不含媒体文件"的空目录,碰到配置的下载目录/媒体库目录边界就停;蓝光目录整目录删;删源时还会发下载文件删除事件让下载器侧处理(推断,由事件订阅方实现)。
  • 文件管理页的删除不走 delete_media_file,因此没有层级保护、也不清理空父目录。

2.4 整理(手动转移)

  • 入口:行菜单"整理"或多选"批量整理" → ReorganizeDialog → POST /transfer/manual。
  • 请求字段(ManualTransferItem):fileitem/fileitems(或 logid/logids 从历史重整)、target_storage、target_path、transfer_type(copy/move/link/softlink,取决于存储 support_transtype)、type_name、media_source+media_id(手动指定作品)、season、episode_group、episode_detail(集号列表)、episode_format(集号定位正则模板)、episode_offset、episode_part、min_filesize、scrape、library_type_folder、library_category_folder、from_history、preview、reorganize、query background。
  • 链路:_execute_manual_transfer → TransferChain.manual_transfer → 与自动整理同一个 do_transfer(识别 → 定向 → 冻结计划 → TransHandler 执行)→ 写 transferhistory。细节见 moviepilot-transfer.md。
  • preview=true:走同一链路但不执行,逐文件返回预览项(源、目标、失败原因),前端分页展示(previewPage,每页 20)。
  • 从历史重整:成功的 move 以 dest 作为新输入;失败的以 src 作为输入,并在执行(非预览)时清理旧 dest。
  • target-path 端点用源路径匹配"目录配置"给默认目标;多个源命中不同配置时返回空,让用户自己选。

2.5 识别与刮削

  • 识别:MediaChain.recognize_by_path(path) → 返回 Context,前端 MediaInfoDialog 展示识别出的元信息(标题/年份/季集/资源属性)和匹配到的媒体。只读,不落库;只给一个结果,不给候选列表,也不能在弹窗里改选。
  • 刮削:ScrapeDialog 允许选数据源(themoviedb/douban/bangumi/anilist/musicbrainz/…)并可填原生 ID;未填 ID 时按路径识别。后端 ScrapingChain.scrape_metadata(fileitem, meta, mediainfo, overwrite=True):在文件旁写 NFO、下载海报/剧照。overwrite 恒为 true,没有字段锁;对目录会递归处理其中文件(推断,见 ScrapingChain)。音乐走 scrape_music_metadata。
  • 批量刮削:前端逐个串行调用,失败项汇总提示。

3. 前端页面结构

filemanager.vue
└─ FileBrowserView        读 Storages/Directories 公共设置,决定初始存储与路径(下载目录公共前缀,记住上次存储于 localStorage)
   └─ FileBrowser         布局:左树右表,可拖动分隔条(宽度存 localStorage),PWA 下悬浮"新建目录"按钮
      ├─ FileToolbar      存储切换菜单 | 面包屑(itemstack) | 排序切换(name/time) | 上一级 | 新建目录
      ├─ FileNavigator    目录树:懒加载子目录,展开状态,只显示目录
      └─ FileList         过滤框(glob,大小写开关)| 刷新 | 选择模式(批量刮削/整理/删除)
                          VVirtualScroll 列表:图标(按扩展名)、名称、大小;桌面 hover 出操作按钮,移动端"⋮"菜单
                          行菜单:识别 / 刮削 / 重命名 / 整理 / 删除
                          点文件:图片→大图预览;其他→缩略图 + 大小 + 修改时间 + 识别/下载按钮
  • 空态区分"过滤无结果"和"空目录";加载中整块替换为 LoadingBanner。
  • 进度:长操作共用 ProgressDialog(识别、批量删除、批量刮削、递归重命名)。
  • 权限:canManage 控制新建目录按钮;其他操作靠后端拒绝。

4. 已知痛点(推断,供我们的规格对照)

# 痛点 依据
P1 浏览范围是整个文件系统(/ 起),误删/越权读取风险大 local.list 根为 /;download 读原路径
P2 删除无回收站、无引用检查,可能删掉仍在做种的文件或整个媒体库子树 /storage/delete 直接 rmtree
P3 重命名不同步 transferhistory、字幕/NFO,可能静默覆盖 §2.1
P4 没有"移动到/复制到"通用操作;只能通过整理弹窗间接搬 行菜单只有 5 项
P5 列表不标注文件是否已整理、是否在下载器中、是否临时文件,只能挨个"识别" FileItem 无业务字段
P6 大目录一次全量返回,无分页、无截断提示,网盘/网络挂载目录会卡 list 无分页
P7 识别只给单一结果,错了只能去整理弹窗手填 ID;识别和整理是两个弹窗,结果不互通 MediaInfoDialog 只读
P8 批量整理一次只能对所有选中项用同一组参数(同一作品/季/集规则),混合目录要分多次 ReorganizeDialog 单表单
P9 递归重命名遇到一个失败就中止,已改部分不回滚,无预览 §2.1
P10 刮削 overwrite 恒为真,会覆盖用户手改的 NFO §2.5
P11 无键盘导航、无拖拽、无右键菜单、不显示隐藏文件开关;排序只有名称/时间 FileList 无 keydown/drag/contextmenu
P12 挂载离线时 list 返回空数组,界面显示"空目录",与真实空目录无法区分 local.list 捕获 FileNotFound 返回 []

5. 许可提醒

  • MP 后端 GPLv3;前端仓库同组织,按 license-and-provenance.md 同等对待。本项目只迁移行为和信息架构,不复制 Python/Vue 源码、模板、i18n 文案、图标映射表。
  • 规格中的交互(左树右表、面包屑、行内操作)是通用文件管理器范式,不构成对 MP 表达的复制。
  • 若日后参考 MP 的集号定位正则模板(episode_format)语法,需单独登记来源,并以 domain 层自有实现替代。