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

126 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 层自有实现替代。