metube/LIBRARY_ORGANIZE_DESIGN.md

7.7 KiB

Library & Organize 功能设计文档

背景与目标

目前 MeTube 只有一个下载目录(DOWNLOAD_DIR),"文件夹" 只是其子目录。用户另有一个大型媒体库(数百个文件夹,按演员/主题组织),需要:

  1. 整理(Organize):把下载目录中的文件移动进媒体库的某个文件夹;目标文件夹选择要支持"快速匹配"(搜索 + 自动建议)。
  2. 浏览媒体库:按文件夹导航、页内播放视频、可删除文件。媒体库在体验上类似"已完成",但数据源是磁盘上的大库,而非下载记录。

已确认的关键决策

  • 文件整理进媒体库后,从"已完成"列表移除(媒体库是它的唯一家)。
  • 快速匹配 = 可搜索的文件夹选择器 + 基于标题/uploader 的自动建议,两者都要。
  • 播放走 aiohttp 静态路由(与下载目录一致,天然支持 Range 拖拽)。

总体架构

下载区 (DOWNLOAD_DIR)                媒体库 (LIBRARY_DIR, 新 volume)
┌────────────────────┐  organize   ┌──────────────────────────┐
│ 已完成条目 (shelve) │ ──────────→ │ 文件夹A/ 文件夹B/ ...     │
│ 文件 + 元数据       │  移动文件    │ 纯磁盘状态,无 DB          │
└────────────────────┘  删除 shelve  └──────────────────────────┘
        条目                                   ↑ 浏览/播放/删除
                                        (实时扫盘 + 短 TTL 缓存)

媒体库不建索引、不入库:状态实时从磁盘读取,文件夹列表做 ~60s 内存缓存。库可能很大,但 os.scandir 列目录是毫秒级;只有"文件夹列表 + 文件数"需要遍历一次顶层目录,缓存即可。

后端设计 (app/main.py 为主)

配置

  • 新增 LIBRARY_DIR(默认空 = 功能关闭)、PUBLIC_HOST_LIBRARY_URL(默认 library/)。
  • docker-compose.yml:挂载 ./library:/library,设 LIBRARY_DIR=/library。
  • 功能开关:LIBRARY_DIR 为空时,所有 /library/* API 返回 400,前端隐藏媒体库入口和"整理"按钮。configuration socket 事件已会把配置发给前端,UI 据此显隐。

API 一览

端点 方法 说明
/library/folders GET 递归列出库内文件夹(相对路径)+ 每个文件夹的视频文件数。内存缓存 ~60s,organize/delete 后主动失效。排除 CUSTOM_DIRS_EXCLUDE_REGEX 匹配的隐藏目录
/library/files?folder=<rel> GET 列出某文件夹内的视频文件:文件名、大小、mtime;按名称排序。folder 为空 = 库根目录
/library/organize POST {ids: [...], folder: '<rel>'}。仅接受 done 条目:逐个解析文件路径 → 校验/创建目标文件夹(必须在 LIBRARY_DIR 内)→ shutil.move(executor 中执行,跨盘时为拷贝+删除,大文件可能较慢)→ 冲突时按现有约定追加 5 位随机后缀并改名 → 从 done 存储删除 → 发送 cleared socket 事件让前端移除条目。逐项收集成败,返回 {status, moved: n, errors: [...]}
/library/delete POST {paths: [...]}(库内相对路径)。仅删文件、拒绝目录;删除后尝试清理变空的父目录(不删库根)
/library/thumbnail?path=<rel> GET 复用现有 _extract_video_frame(ffprobe 定位 25% + ffmpeg 抽帧),缓存键用 sha1('lib:'+relpath),与下载缩略图同目录但命名空间隔离。无本地文件概念之外的 fallback:失败返回占位 SVG

静态播放路由

  • routes.static(config.URL_PREFIX + 'library/', config.LIBRARY_DIR) — 注册在 /library/* 动态 API 之后(aiohttp 先注册先匹配,现有静态路由本就在文件末尾)。
  • 前端拼播放 URL:library/ + encodeURIComponent(folder + '/' + filename)。

路径安全

所有接受相对路径的端点统一走一个校验函数:realpath(join(LIBRARY_DIR, rel)) 必须以 realpath(LIBRARY_DIR) 为前缀,否则 400。与现有 __calc_download_path 同款逻辑。

自动建议匹配(前端实现)

文件夹列表本来就要发给前端,因此匹配放客户端,零额外请求:

  • 输入:待整理条目的 title 与 entry.uploader(uploader 通常就是演员名,是最强信号)。
  • 归一化:小写、去非字母数字、按空格分词。
  • 打分:uploader 与文件夹名完全相等(归一化后)= 最高分;子串包含次之;token 交集比例再次。取 Top 3 作为建议。

前端设计 (ui/src/app)

导航模式

  • AppComponent 增加 mode: 'downloads' | 'library'(持久化到 preferences)。
  • 侧边栏"状态"区下方新增"媒体库"入口(带视频总数);点击切到 library 模式,此时"文件夹"区改列媒体库文件夹(数据来自 /library/folders,复用现有 nav-item 样式与计数)。
  • 主区域:library 模式渲染新的 LibraryBrowserComponent,否则渲染现有的 active-downloads + download-list。

LibraryBrowserComponent(新)

  • 工具条:搜索框(按文件名过滤)、排序(名称/时间/大小)、视图切换(列表/网格)——均复用现有模式。
  • 条目卡片/行:缩略图(/library/thumbnail?path=)、文件名、大小、mtime;操作:播放(复用现有页内播放器 modal,URL 指向静态路由)、删除(confirm 后调 /library/delete)。
  • 数据量:单文件夹内文件数通常有限(几十~几百),无需虚拟滚动;若后续需要可加 cdk-virtual-scroll。

整理流程(已完成视图)

  • 批量操作条新增"整理到媒体库…"按钮(在"移动到…"旁),选中条目后点击打开整理面板:
    1. 建议文件夹(Top 3,客户端匹配,一键选择)
    2. 搜索框:输入即过滤全部库文件夹(ng-select 或自绘列表,复用样式)
    3. 新建文件夹:输入名创建(多层如 A/B 允许)
  • 确认 → POST /library/organize → 成功条目从"已完成"消失(cleared 事件驱动)+ toast 提示移动数量;部分失败时 toast 列出失败原因。
  • 单条整理:已完成行/卡片的操作区加"整理"图标,打开同一面板(预选该条目)。

边界与异常

  • LIBRARY_DIR 未配置:API 400 + UI 隐藏入口。
  • 目标/待删路径逃逸库根(../):400。
  • 整理 pending/downloading 条目:跳过并计入 errors(只有已完成文件可整理)。
  • 目标已存在同名文件:追加 _XXXXX 后缀(与下载区 move 行为一致),条目不丢失。
  • 跨文件系统移动大文件:executor 中执行,前端给 organizing 状态;失败保留原文件与条目。
  • 库文件被外部改动:文件夹缓存 60s 过期后自动反映;播放/删除前都实时校验路径存在。

实施步骤

  1. 后端:Config 增加 LIBRARY_DIR / PUBLIC_HOST_LIBRARY_URL;路径校验辅助函数。
  2. 后端:/library/folders、/library/files(含缓存)。
  3. 后端:/library/organize(复用 move/conflict 逻辑 + done 条目删除 + cleared 事件)。
  4. 后端:/library/delete、/library/thumbnail、静态路由。
  5. 前端:downloads.service.ts 增加 library 相关方法(或直接 HttpClient)。
  6. 前端:侧边栏模式切换 + 媒体库文件夹列表。
  7. 前端:LibraryBrowserComponent(列表/网格、播放、删除)。
  8. 前端:整理面板(建议 + 搜索 + 新建),接入批量条与行操作。
  9. 样式与中文文案,与现有设计语言对齐。
  10. 验证:curl 端到端(建库目录→整理→文件移动+条目消失→播放 URL 200 且支持 Range→删除生效);UI build;重启持久性检查。
  11. 文档:README 与 AGENTS.md 提及新环境变量;docker-compose.yml 增加挂载示例。

交付物

  • 本文档保存为仓库根的 LIBRARY_ORGANIZE_DESIGN.md(批准后开始实现时一并提交)。
  • 全部代码改动 + 本地容器验证。