Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei # Conflicts: # apps/server/src/modules/sources-identity/service/episode-resolution.ts
23 KiB
媒体整理(/console/organize)
原地入库为 v0.3.14 的待部署改动;当前生产 v0.3.13 的整理方式仍为复制/移动,独立就地刮削继续可用。
整理页分为「整理队列」与「整理记录」。队列显示等待、执行和等待继续的后台入库任务;记录显示复制/移动/原地入库的处理结果。文件管理的改名、删除、还原账本继续保存,通用记录接口仍可查询。
实现:
- 列表 / 统计 / 详情:
apps/server/src/modules/acquisition-organize/routes/organization.ts(筛选recordFilters,通配likePattern);公开字段在services/organize.ts的loadRecordsPublic。 - 删除:
apps/server/src/modules/file-manager/record-delete.ts(复用文件管理删除deleteFiles)。 - 契约:
packages/contracts/src/organization.ts(OrganizationListQuery、OrganizationStats*、OrganizationDelete*),端点Api.organization.stats / remove。 - 前端:
apps/web/app/routes/console/organize.tsx、organize.$id.tsx,components/organize/(删除确认框、标题 / 季集小件),批量重新整理走共用整理弹窗的{ kind: "records" }来源。
统一整理弹窗(v0.3.13)
手动选目录、文件管理器、下载详情与历史重新整理打开同一个弹窗。单文件只处理该文件,目录递归展开视频,多选合并去重。媒体类型可自动、电影或剧集;作品可自动识别、搜索本地/TMDB,也可选定类型后输入 TMDB ID 定位。手动指定作品与季号作用于整个本次范围,混合批次中需单独纠正的作品另行选取;文件明细可核对和修正季集、排除文件,没有另一套批量勾选语义。
各入口匹配相同来源配置,唯一匹配自动带出方式和分类/目标,多个匹配在弹窗内选定;不使用来源配置时沿用全局默认并手动指定。下载详情优先按已有 qB 标签匹配覆盖当前路径的来源。历史原配置仍覆盖可用输入时优先采用;成功移动后的目标不再属于旧来源时显示当前匹配配置或手动选项。
本次可选择原地入库、复制或移动。复制/移动可调整目标媒体库或具体目录、命名、目标冲突及整理后刮削。刮削默认沿用全局配置,显式开关只影响本次;整理成功的文件再进入现有 NFO/图片任务,TMDB 和 Local 均适用。做种义务未满足时不提供移动,复制与原地登记不改变视频文件,服务端继续核对实际状态。
手动来源发现真正成功历史时显示数量及「跳过成功记录」,默认关闭,开启后仅本次跳过;失败/部分成功记录不算完整成功,最新失败也不会遮盖较早成功。目标已存在策略单独处理。从历史记录进入直接采用重新整理语义。修改识别或选项后旧预览失效,需重新生成。
确认预览后只提供「加入整理队列」。后台受理成功后弹窗自动关闭,可以继续操作,进度到队列查看,整理后的刮削任务也显示在队列,整理详情单独展示刮削结果;部分受理时已受理文件从本次范围移除,剩余文件可修改后再预览提交。队列复用现有后台调度,不要求浏览器停留到文件处理结束。记录、队列及弹窗明细均复用统一分页。
整理时指定剧集组
对照 MP 的整理 / 重新整理选剧集组(TransferInfo.episode_group、ReorganizeDialog.vue)。作品的季集结构(TMDB 默认季 / 某个剧集组,见 identity.md「剧集组」)是作品级设置;整理弹窗里本次涉及的本地剧集作品如果有剧集组可选,多一项「季集结构」:
- 缺省「跟随作品(当前:X)」:请求不带
episodeStructures,冻结计划和以前一样(不记季集结构)。 - 临时选 TMDB 默认季或作品的其它剧集组(列表同
GET /media-entities/{id}/episode-groups):只这次按所选结构换算季集、定季目录与命名,作品设置不变。选的就是作品当前结构时等同跟随作品。 - 勾「同时设为作品的季集结构」(缺省不勾):预览不改作品;执行时等计划一致、Move 授权、做种义务这些执行前检查都通过后,才走作品级切换(
setEpisodeGroup:单元按 TMDB 原季集原地迁移、全量重算、刷新元数据,写审计),检查失败时作品结构不变;切换后按新结构(此时就是作品结构)再算一遍,前后路径须与预览一致,再整理。同一作品多组只切一次。
请求:POST /files/organize 的 episodeStructures: [{ mediaEntityId, groupId | null, applyToEntity? }](预览与执行同一请求体,进请求指纹)。组响应带 episodeStructure { groupId, name, temporary, appliedToEntity? },预览头部显示「季集结构:制作季(仅本次)」。
临时结构下的集数映射(episode-resolution.ts createStructureEpisodeMapper):
- 季表换成所选结构的季与集数;选剧集组时带每集播出日期与原季集对照(发布者按 TMDB 默认季编号的
S01E14换到组内S02E02)。 - 作品的映射规则是按作品当前结构写的:先按作品季表 + 规则跑一遍,命中规则(含特别篇 / 花絮 / 忽略)就用规则结果,季集再经 TMDB 原季集换到所选结构(换不过去的按待确认排除);没命中规则时按所选结构的季表判定。
- 资源的集数解析是按作品结构做的,临时结构下不沿用,只取发布时间;按所选结构判不出的文件照常排除为集数待确认,但不并进作品的「集数待确认」待处理。
- 向导里人工填的季集按所选结构理解(决定命名)。
资产覆盖落到哪个单元:作品的集单元是按作品当前结构编号的,所以文件的季集(所选结构)逐集换回作品结构再登记覆盖:所选结构季集 → TMDB 原季集(剧集组按组内位置,默认季即自身)→ 作品结构(作品是默认季时原季集即单元季集;作品是剧集组时按单元上记的 tmdbSeason / tmdbEpisode 对照)。区间里每一集都要换得到、落在同一季且连续;换不回(比如作品的剧集组不含这一集,或单元还没记原季集)时文件照常按所选结构命名入库,只是不登记集覆盖。例:作品是默认季(S1 = 25 集),临时按「制作季」整理 S01E14 → 命名 S02E02,覆盖挂作品的 S01E14 单元;勾「同时设为作品结构」→ 先切换(S01E14 单元原地变成 S02E02),命名 S02E02,覆盖挂迁移后的同一单元。
排查:冻结计划 plan.episodeStructure { groupId, name, entityGroupId, temporary, appliedToEntity? } 记这次用了哪套结构与当时作品的结构;临时结构下每个文件的 facts.structureEpisodes 是命名用的季集,facts.season / episodes 是换回作品结构后的单元季集(换不回时没有),episodeResolution.evidence 带 structure:<groupId|default>。
不做:切换结构后自动改名已整理的文件(仍用「重新整理」)、剧集组季的独立海报。测试:apps/server/test/file-manager/organize-episode-structure.test.ts、apps/web/app/components/organize-wizard/model.test.ts「季集结构」。
来源、分类与存量整理
设置 → 媒体库与目录中维护分类 YAML 和整理来源。新功能已于 2026-10-01 部署为 v0.3.9,迁移 0059_organize_source_snapshot 已执行,见 发布记录(已删除,见 git ceb66f8)。
- 一条来源配置选一个来源目录、一个媒体库目标目录、复制或移动方式。通过弹窗浏览已登记的目录选择,不需要填写宿主机任意路径。不同来源可以使用相同分类,规则只维护一次。
- 分类一级固定为
movie/tv。规则按顺序首次匹配;同一规则的字段同时满足,同字段逗号值为任一匹配,!排除。release_year支持年份或区间;原始 TMDB 详情的一级字段也可使用。 - 空条件是兜底,应放在最后。默认保留「欧美电影」「欧美剧集」承接所有未命中的习惯。TV 默认顺序为儿童剧集、动漫剧集、纪录片、综艺、华语剧集、亚洲剧集、欧美剧集。尚未取得 TMDB 详情不能冒充分类成功。
- 自动分类在目标目录下追加分类目录,再使用现有作品/季目录模板。固定分类直接以所选目标为分类目录,避免追加第二遍分类名称。预览显示完整目标路径。已有作品沿用路径绑定,保存新规则不会移动旧库。
- YAML 保存后用于后续计划,重启保留;语法或目录名不合法时拒绝保存,已有规则保留。
来源触发方式可以选手动、qB 标签或目录快照。qB 标签同时限定下载器实例和来源范围;只有完成的任务进入整理,去掉标签后停止接管,已有管理标签继续有效。
新增来源或更改来源路径时,先将当前视频文件记录为基线,这些文件不会因此自动入库。快照持久化在数据库,重启保留。后续新增或改变的文件,在连续两次扫描确认大小和修改时间稳定后再处理。已知 qB 任务由下载器接管流程负责,避免两种触发重复整理。下载器目录会查询当前全部 qB 任务,即使未打标签、尚无数据库任务行,也会等下载完成。下载器不可达或目录读取失败会记录错误并保留此前快照。
「整理存量」是可重复触发的一次性扫描:排除同一源文件事实已成功整理的文件及未完成下载,历史部分成功中的失败文件、同路径发生变化的文件仍可选,候选进入同一个整理弹窗,确认识别、操作和完整目标路径后执行。单次候选按识别上限分批;完成后再次触发可处理余下文件。这个操作不重建或清空基线。识别不确定时也可打开现有智能助手协助判断,再加入整理队列。
文件管理器选中文件或目录后点「整理」,会读取来源配置并按同根、目录边界和最长来源路径匹配所选全部项目。唯一匹配自动带入复制/移动方式、分类和目标策略;多个同等匹配让用户选择,也可不用配置手动指定。不同来源混合选择或没有匹配时使用共用整理弹窗的手动选项。媒体类型在预览时核对,配置目标可由本次明确的目标选择覆写。
实现复用既有识别、冻结计划、文件操作账本、整理记录与任务引擎;未增加软链接、硬链接或另一套 AI 执行流程。
手动下载、Local 元数据与一次性洗版(已部署 v0.3.10)
搜索、站点工作台和原站详情均可独立下载,不要求订阅。下载时默认正常入库,也可选择一次性洗版并勾选旧版本;只有一个旧版本时默认选中,多个由用户选择。媒体详情与下载窗口展示已有文件和规格。正常入库保留已有版本;不同版本路径由可编辑的命名模板决定,同名冲突仍需处理。
未识别资源确认后允许下载。整理时先匹配 TMDB,再由现有网络/Agent 元数据补全能力核对译名、原名和别名;能够找到正确条目时回到 TMDB,找不到且详情资料充分时使用 Local 身份。Local 共用现有分类、命名、复制/移动和 NFO/图片任务,并保留字段来源;信息不足或候选冲突保留待处理。Agent 搜寻依赖已配置的模型/搜索能力和现有每日补全预算;没有封面资料时不会虚构图片。Local 入库安排元数据文件写入,普通 TMDB 自动刮削保持原设置。
洗版在本次冻结计划中保存选中的旧文件。新文件核验登记成功后才清理所选旧版本,剧集只处理成功对应的季集与 Part;覆盖不完整的合并集保留。相同目标路径会先暂存旧文件,失败后恢复。旧版的私有字幕等随之清理,共享 NFO/图片、未选版本及下载缓存保留;按现有文件替换设置移入回收站或删除。
收尾核验与资产登记
-
目标路径已有位置时:本次是「替换」(向导 / 待处理选替换,或洗版选中的旧版本),或旧位置属于别的作品,退役旧位置(没有别的位置时旧资产退役)并新建资产,大小、校验、版本信息都是新文件的;否则沿用同一资产。沿用的位置之前被对账判为缺失 / 不可用时恢复可用,资产事实换成这次入库的文件。
-
文件级标题粗核:同一批视频(合集、混合包)里,文件名解析出的标题和作品标题 / 原名 / 别名明显不是同一部的(没有足够共同词; 剧集只看带 SxxEyy 的文件),排除为待确认
title_mismatch,不按本作品命名入库,记录部分完成并开待处理「有 N 个视频没有整理」, Move 时下载任务保留。只在同一批里至少有一个文件对得上作品时才判断别的文件(全都对不上多半是作品名的另一种写法,交给资源级识别); 用户在向导里逐文件指定过的不核。 -
ffprobe 判定「不是视频」(没有视频流,伪装或损坏)的文件:核验失败,已拷 / 搬进库的新文件和它的字幕等附属文件移入回收站(Move 时源已删,回收站里是唯一一份),附属文件结果记跳过
main_failed,开待处理「N 个文件不是视频,没有入库」。原地入库不动原文件;洗版替换照旧回滚旧文件。 -
跨卷 Move(复制 → 校验 → 删源)删源前一律全量 sha256 校验,不用抽样;复制与同卷移动仍按
verifyHash。 -
计划期判断「目标已存在且同尺寸是不是同一文件」的全量 sha256 按(路径、大小、修改时间、inode)缓存,向导预览 / 复核 / 执行不再各算一遍。
-
整理时补全元数据(
metadata.resolve_organize,模型 + 网页搜索)带 3 分钟超时,上游挂住不会一直占着文件道。 -
季号:解析器对有集号没季号的文件推定第 1 季,服务端与域层用同一个值(
organize-season-inference.test.ts)。 -
回收站清除先删文件再删行;清过期项时顺带删各库根
.umt-trash/replaced-*里过期、没有回收站登记的目录。
关联迁移 0060_manual_acquisition,已于 2026-10-01 随 v0.3.10 发布执行。业务依据与验收见 MP 对照 §5.6。
与 MP 的对照
MP 对照源:MoviePilot-Frontend/src/views/reorganize/TransferHistoryView.vue、components/dialog/TransferHistoryDeleteDialog.vue、app/api/endpoints/history.py、app/db/models/transferhistory.py。
| MP 功能 | 我们 | 说明 |
|---|---|---|
| 海报 | 新增 | 作品海报(media.posterPath),没有时显示首字占位 |
| 标题 / 年份 / 季集 | 已有 | 作品 (年) S01E02;未识别的显示「未识别 · 源文件名」 |
| 二级分类(华语电影等) | 已接入 | 分类规则在设置编辑 YAML;电影 / 剧集为母分类,综艺属于剧集 |
| 源路径 → 目标路径 | 新增 | 列表加「源 → 目标」列(只显示末两段,悬停看全路径);详情「整理前 / 整理后」已有 |
| 存储标签(本地 / SMB…) | 不做 | 只有本地存储,标签不提供信息 |
| 整理方式 复制 / 移动 | 已有 | 列表「方式」列;整理页聚焦复制/移动结果 |
| 软链接、Rclone 复制 / 移动 | 不做 | 整理服务不支持这几种方式 |
| 大小 | 新增 | 逐文件源大小之和(sizeBytes) |
| 状态 成功 / 失败 + 失败原因 | 已有,改 | 支持「部分成功」;执行中任务在队列查看;成功不加徽章;失败原因走 reasons.ts 一句话,原码进详情「技术信息」 |
| 时间 | 已有,改 | 相对时间,悬停看完整时间 |
| 来源 下载器 / 手动 / 自动 | 新增下载器 | 来源列加下载器名称;详情加「下载任务」一行,种子 hash 进「技术信息」 |
关键字搜索(标题、源、目标;* ? 通配) |
已有,补 | 补通配、逐文件的源 / 目标路径、种子 hash |
| 搜「成功」「失败」切状态 | 不做 | 有状态筛选和计数按钮 |
| 状态筛选 | 已有 | |
| 类型筛选 | 新增 | 电影 / 剧集 / 未识别(kind=movie|series|none) |
| 时间筛选 | 新增 | 今天 / 最近 7 天 / 最近 30 天(since)。MP 没有,任务单要求 |
| 统计(总数 / 成功 / 失败) | 新增 | 列表上方一行计数,只列非零项,点一下按该状态筛选(GET /organization-records/stats) |
| 分页、每页条数 | 已有(不同) | 共享分页组件,10/20/50/100 条和翻页;游标按需加载,无总数时显示「已加载」 |
| 列排序 | 不做 | 按时间倒序;分组模式下按作品聚合 |
| 分组模式(按标题) | 新增 | 「按作品分组」,组头可折叠 |
| 筛选写进地址栏 | 新增 | 状态 / 类型 / 来源 / 时间 / 关键字 / 分组都在 URL,刷新不丢 |
| 单条:重新整理 | 已有,补 | 详情已有;列表行菜单新增。走共用弹窗(配置、展开预览、加入队列) |
| 单条:智能助手整理 | 已有(不同) | 「让智能体排查」:失败 / 部分成功的记录在行菜单和详情里 |
| 单条:删除(只删记录 / 连源文件 / 连媒体库文件 / 全部) | 新增 | 确认框里两个勾选项,先核对再执行,写清删几个文件、多大、哪些删不了、会不会被自动整理再处理 |
| 查看详情 | 已有 | 详情加海报、大小、下载任务、删除按钮 |
| 批量选择 / 全选 | 新增 | 「批量」进入选择模式,表头全选 |
| 批量删除 | 新增 | 同单条删除确认框;删不了的逐条列出,其余照删 |
| 批量重新整理 | 新增 | 选中记录的可用文件一起进整理弹窗(成功移动取目标,其他取源);同属一个作品时预选作品 |
| 清空整理记录 | 不做 | 整理记录同时是文件变更的追溯记录;清理按保留期由计划页负责 |
需要处理(2026-10-06,待处理中心取消)
整理侧的问题(下载完识别不到、目标已存在 / 路径冲突、文件被排除、非视频、整理卡住 / 失败)不再进待处理中心,在整理记录里就地处理。
decision 表保留为内部机制(去重、触发智能体、自动关闭),整理侧事项的 subject 都是整理记录。
口径(attentionSql,services/organize-records.ts;列表 ?attention=true、统计 attention、GET /attention 的 organize 共用):
没被同一下载任务后来的整理记录取代,并且(有 subject 为这条记录的未关闭事项,或失败 / 部分完成且 attention_cleared_at 为空)。
行内动作(PublicOrganizationRecord.attention:topic + 精简事项 + agentIdentify):
| topic | 原因 | 动作 |
|---|---|---|
identity |
没识别出作品(organize_identity:*) |
指定作品(本地 / TMDB,/decisions/{id}/resolve → 钩子重新整理)、候选、让智能体识别、重新整理、忽略 |
conflict |
目标已存在 / 路径冲突(organize_blocked:*、path_conflict) |
事项选项:替换 / 保留两者 / 跳过;忽略 |
other |
被排除、非视频、卡住、执行失败… | 重新整理(整理向导)、忽略 |
- 忽略:
POST /organization-records/{id}/dismiss-attention记attention_cleared_at(迁移 0078),关联的未关闭事项一并dismissed。 - 让智能体识别:
POST /organization-records/{id}/agent-identify,见 agent.md「整理识别不到作品时叫智能体识别」;识别失败缺省会自动派一次。 - 旧链接
/console/inbox?kind=按问题类型跳转(routes/console/redirects.ts inboxRedirect):整理类 → 整理记录「需要处理」, 下载类 → 下载面板,健康类 → 运行状态#attention,集数待确认 → 订阅。
铃铛与汇总(GET /attention):铃铛只放等你批准的智能体操作(subject 为 agent_run 的审批 / 方案选择、计划审批、browser.waiting:*;
整理记录上的审批不进)与系统健康(source_unreachable、downloader_offline、cache_volume_unknown、space_insufficient、
library_root_offline:*,恢复后自动关闭即消失)。首页「要我处理」与运行状态「需要关注」放汇总计数,点进去到对应页面。
删除
POST /organization-records/delete {ids[≤200], deleteSource, deleteTarget, dryRun},scope organize.execute。
- 文件一律移入回收站,走文件管理删除(
deleteFiles,mode=trash,retireAssets=true):已入库文件同时从媒体库移除(位置 removed、资产 retired、满足的收录版本转需关注 + 待处理)。回收站里能还原。 - 源文件 = 该记录逐文件结果的
source_path(只认执行进程本地视图);媒体库文件 = 成功文件的target_path(相对库根时按库根拼)。已经不在的文件跳过、不拦。 - 只有复制 / 移动 / 原地入库 / 硬链接记录有文件可删;改名、移入回收站、还原类记录只删记录本身。
- 一条记录的任一文件不能删(做种未满足或下载中
download_active、绑定目录needs_unbind、不在登记目录outside_roots、目录离线等),这条记录连同它的文件都不动;还在整理的记录(running)不删。其余照删。 - 文件移入回收站会写一条「移入回收站」整理记录(同文件管理删除),
trashRecordId返回它。 - 下载器里的任务不删(同文件管理删除原则)。
- 删文件只允许管理员(控制台会话 / DEV key);API Key 只能删记录,删文件 403,需先走
/files/delete的一次性审批。 autoRetry:下载已完成但没入库(download_task.status=completed、本工具管理、没从下载器删除)且删完没有别的整理记录的下载任务数。自动整理扫描(index.ts的补漏)会在几分钟后重新整理它们——与 MP 删失败记录后重新整理一致;已入库(imported)的不会。- 删除不写增量读取的墓碑(
/organization-records/changes不会出现被删的记录);文件变化通过「移入回收站」记录可见。审计files.organize_records_delete。
已知缺口
- 删媒体库文件只删整理记录里的文件,目录里的 NFO、图片与空目录保留(MP 在没有剩余视频时会连目录删掉)。
- 分组只对已加载的记录生效(同 MP 分组模式)。
- 统计的「进行中」= 排队 + 执行中;点它时列表只筛执行中。
原地入库(v0.3.14 待部署)
文件管理器选择已登记媒体库目录中的视频或目录,进入整理弹窗,默认选择「原地入库 · 保留位置与名称」。识别作品、手动指定电影/剧集、TMDB 和季集的方式与复制/移动相同,生成预览后加入现有整理队列。
- 源与目标路径相同;不复制、不移动、不重命名,不清理媒体目录中的临时文件。配对字幕/音轨同步登记,文件名和内容保持原样。
- 登记视频资产、所在库根、实际相对路径、版本信息与季集覆盖,已有同作品资产不重复创建;不新建会改变以后整理目标的目录绑定。
- 原地模式不使用来源配置的搬运目标、命名模板或替换策略,弹窗隐藏这些选项;不支持借此洗版或替换已有文件。
- 开启「整理后刮削」时,仅针对本次成功登记的视频在原目录补写 NFO/图片,不递归刮削整个同目录或该作品的其它版本。只登记/只读库根可原地入库,刮削关闭。
- 文件必须位于已登记、启用的媒体库根;下载缓存仍使用复制/移动或独立「刮削」。希望将现有目录直接作为媒体库时,先在设置登记该目录,然后原地入库。
- 预览与入队复用原计划校验;执行时确认文件未变化,不把已经归属其它作品的文件悄悄改绑。结果标为「原地入库」,可从整理记录重新整理。
此功能代码已实现,当前生产仍为 v0.3.13;本会话不能访问 Docker 或测试 PostgreSQL,部署及真实数据库回归尚待补验,见发布记录。