Skip to main content
Glama
README.md
# Local Media Manager

本地媒体库索引与整理工具:扫描视频文件 → 识别番号 → 落库 → 查询 / 质检 / 整理。

面向「文件名里带番号」的本地媒体库(数万文件量级)。

## 目的

**存储疯涨的年代,本地囤积已经不是最优解。**

一部片几 GB 到几十 GB,攒到几千部就是几 TB 硬盘。
但这些内容里真正稀缺的**不是文件本身,而是「这是哪一部片」这个信息** ——
文件在网上随时能重新拿到,编号和磁力却得自己攒。

所以这个工具做的事是:**硬盘里只留截图、封面和磁力链接,删掉源文件**。
让互联网上的分享代替本地囤积,把几 TB 压回几百 MB。

**为什么可行** —— 磁力链接是内容的**内容寻址引用**,只要还有人在做种就一直有效。
留住它,就等于留住了「随时重新取回这部片」的能力,而不必一直占着硬盘。

于是核心成为**保卡**:卡 = 编号 + 元数据 + 截图封面 + 磁力。
删源文件只是磁盘管理,**卡片始终保留**。

实测比例约 **1 : 2300** —— 918 GB 的源文件,对应的卡片资料(封面 + 截图)合计 400 MB。

## 特点

**① 自定义扫描目录,抓出番号**

目录范围完全自己定,还能按盘分档 —— 整盘扫 / 只扫指定目录 / 跳过。
(慢盘、病盘尤其需要「只扫已知目录」,不必为了几个文件全文遍历。)

识别从文件名解析编号,文件名认不出时回退到父目录名。
结果是结构化的编号,不是一堆文件名。

**② 在线匹配 JavDB**

编号拿去 JavDB 匹配元数据:标题、发行日期、时长、演员、标签、评分。
匹配不上的不入库 —— 宁可缺,不可混入错的数据。

**③ 抓封面、截图、评论**

每个编号抓回来的素材:封面、内容截图、评论文本,
都存本地,离线也能看。评论数还参与置信分。

**④ 存磁力链接**

抓取并落库该编号的磁力(含中字 / 高清 / 体积标记),
可一键导出成文本档留底 —— 磁力本身只是 hash,丢了得重找。

**⑤ 一键清理本地资源**

对**高置信度**的条目(档位 ≥ 高 + 番号核对通过 + 有已验证磁力 +
文件格式典型),一键把本地源文件**送进回收站**,腾出空间
(**需清空回收站才真正释放** —— 程序不代清)。

四个约束:

- **走回收站**,不是永久删除 —— 随时可恢复
- **只在能进回收站的地方删** —— 动手前查一次目标位置:不是本地内置盘
  (移动盘 / U 盘 / 网络盘),或文件系统不是 NTFS / ReFS(exFAT、FAT32
  没有回收站),**一律拒绝删除**。移动盘通常是备份盘,在上面省空间本身
  就没意义。判据是**运行时查出来的**,不依赖配置里写了什么
- **卡片不删** —— 删的只是源文件,编号 / 元数据 / 截图 / 磁力全部保留
- **没磁力不许删** —— 拿不回来的东西不给删;判定没跑过的也一律不可删

(宁可漏删,不可错删。逐条手动删的门槛更低,但同样受磁力门控。)

**⑥ 还有**

格式白名单、增量扫描(跳过未变更文件)、质检(缺元数据 / 缺封面 / 重复)、
Web UI + MCP 工具层(供 Agent 客户端调用)。

## 实现

**分层**

```
core/       解析、扫描、匹配、落库、置信分
services/   扫描 / 抓取 / 文件管理 / 备份
web/        FastAPI + Jinja2(页面与 REST)
adapters/   数据源适配(JavDB / JavBus)
```

**存储** —— SQLite 三个库分工:主库(编号 / 文件 / 元数据 / 磁力 / 评论)、
扫描索引(增量用)、v1 旧库(只读迁移源)。

**两条硬规矩**

1. **编号目录不参与匹配**(除一层回退)—— 目录名比文件名更容易误匹配
2. **没有磁力就不许删** —— 删了就找不回来的东西,不该由工具替人决定

**零外部程序依赖** —— 时长探测与元数据抓取都是纯 Python 实现,
不需要 ffmpeg,也不需要额外的 CLI 或常驻服务。

**前端** —— 首屏只渲染一页卡片,其余随滚动按需追加。
几百上千张一次性渲染会明显卡顿。

## 引用的项目

元数据来自 JavDB 与 JavBus。两者的**接口契约**参考了下面两个 MIT 项目,
按其契约用 Python 重实现(未复制其代码):

| 项目 | 许可 | 参考了什么 |
|---|---|---|
| [javdb-cli](https://github.com/FlanChanXwO/javdb-cli) | MIT | JavDB App API 的端点、参数与签名算法 |
| [javbus-api](https://github.com/ovnrain/javbus-api) | MIT | JavBus 的页面结构、端点与请求头 |

> ⚠️ javdb-cli 的签名常量系其从 JavDB 官方 APK 逆向所得。本项目按 MIT
> 条款复用其接口契约;该协议随时可能被上游改动而失效。

时长探测不需要 ffmpeg:自写的容器头解析覆盖 mp4 / mkv / avi / wmv 四种容器,
与 ffprobe 在真实语料上实测**最大相对差 0.0034%**(判定阈值是 10%)。

## Install

```bash
pip install -r requirements.txt
```

## 配置

编辑 `config.yaml`:

```yaml
scan_paths:
  - "X:\\"                   # 扫描根目录(可多个)

video_extensions: []         # 追加格式(**追加**语义)
excluded_dir_segments: []    # 追加排除目录(**追加**语义)
database: "storage/library_v2.db"
index_db: "storage/file_index_v2.db"
dictionary: "data/dictionary.json"
```

`video_extensions` 与 `excluded_dir_segments` 都是**往上加**,不是替换。
选追加而不是覆盖,是因为覆盖语义下**少写一项会静默漏扫整类文件**;追加最坏只是多扫。

Web UI 里还能改数据源后端与代理(`/settings`)——
代理地址由使用者自己填,程序不预设任何端口。

## 运行

```bash
# 命令行扫描
python main.py "你的目录"

# Web UI
python -m uvicorn web.app:app --host 127.0.0.1 --port 8811
```

## 开发

```bash
pytest tests/ -q
```

测试结果按**有没有本地语料**分两种(语料是私有的,不在仓库里):

| 环境 | 结果 |
|---|---|
| 无本地语料(干净克隆,默认) | `846 passed, 8 skipped` |
| 有本地语料(开发机) | `854 passed` |

跳过的那 8 条依赖真实的媒体库/校验缓存,靠 `pytest.mark.skipif` 自动跳过;
要用自己的语料跑,用环境变量指过去:

```bash
LMM_REAL_DB=/path/to/library_v2.db \
LMM_LEGACY_LIBRARY_DB=/path/to/library.db \
LMM_VERIFY_CACHE=/path/to/verify_cache.json \
pytest tests/ -q
```

## 已知缺口

- 扫描缓存未接线,重复扫描会重算(大库性能受影响)
- 定时扫描、目录监听未接线,不随服务启动
- 暂无「重命名计划(只出计划不执行)」与「按演员/标签检索」
- 宣传视频的 HLS 录制依赖外部工具(其余功能不需要)

## License

MIT

## 友链

本开源项目已链接并认可 [LINUX DO 社区](https://linux.do)。