我用 TrueNAS + 万兆折腾了一整套视频资料库:从目录学到前端选择引用
一次完整的个人媒体库建设经验分享:硬件选型、目录规范的设计思路、元数据的持久化策略、前端工具的选择逻辑。技术不复杂,思路是主角。
〇、写在前面先交代背景:我收藏了很多年来"攒"下来的影片资料,从最早随手丢桌面,到后来硬盘里各种"新建文件夹 (2)",再到买 NAS 才发现自己真正缺的不是存储空间,而是
一套能长期执行的整理规则。
这篇分享不讲怎么装 TrueNAS,不贴配置文件,也不深入任何一段具体代码(那些脚本基本都是 AI 帮忙写的,复制粘贴层面的东西网上遍地是)——我想讲的,是这一整套东西里
最后沉淀下来的、最值的那些"规定":为什么目录要长这样、为什么元数据要这么存、为什么爬取和存储要分开在两台机器上做。
如果你也在攒一个几千上万部的库、也开始被"找不到、搜不出、重复存"折磨,这篇里的思路应该能直接搬走。
一、先说说痛点:一万部之后,我卡在哪三个问题,一个比一个疼。盘够大之前你可能感受不到,但库一旦过千,这三件事会按顺序找上门:
第一个疼:文件名。 下载来的名字五花八门——有全平台水印的、带域名推广的、日期地址乱插的、"1024 社区防屏蔽"版本满天飞。同一个片子你能见到七八种命名风格。文件名不干净,后面一切都免谈。
第二个疼:没有元数据可用。 这类资源不走任何公开刮削库——这不是某个软件的问题,是类型决定的,谁来了都刮不出来。标题、演员、系列、片商,这些信息只躺在文件名里,想看就得一条条人脑翻译,看过的资料没有任何结构可查。
第三个疼:找不到。 想找"我看过的一个系列",记忆里的线索往往是"片商是谁""哪位演员""系列第几部"——这几个维度,通用前端没有一个能舒服地查。
后来我悟到一件事:
这三个疼其实是同一个问题——我从来没把"库"当回事。 上一步和下一步都是随机的:文件名随机的、目录随机的、元数据随机的。所以我做的第一件事不是挑软件,而是把整理的对象当成一个
数据库来设计:目录结构就是表结构,文件名就是主键,元数据就是字段。想通了这一点,后面的选型反而都变得简单。
二、整体架构:一张图先上全景,数据的一生是这样的。看懂这张图,后面每一节都对应图上的一段:
- 外部资源(三方资源站,多渠道合并)
- │
- ▼ 爬取(浏览器自动化脚本,规则+账号态)
- Windows 素材入"中间库"(MySQL,只存文本数据:番号/磁力/标签映射)
- │
- ▼ 本地处理盘(盘符随挂载变)
- 04_temp ← 所有新下载先进这里,是唯一入口
- │ 清洗文件名 → 识别番号 → 生成 nfo → 归档目录
- ▼
- 本地"已规范库"(目录结构与最终库完全同构)
- │ 人工筛查一遍(知道哪片要哪片不要)
- ▼ FastCopy(SMB + 万兆)
- TrueNAS SCALE —— 目录树与本地同构
- │
- ▼
- Stash 前端:浏览 / 检索 / 播放
图里最值得记住的一点:
NAS 出现的位置在整条链的最末端。前面所有挖掘/清洗/归档的环节都发生在本地,NAS 收到的永远是"已经整形完毕"的文件树——这个顺序不是偶然,是后面很多决策的总前提。
两个设计决定值得先说:
第一,计算和存储彻底分离。 所有重活(爬取、清洗、改名、生成 nfo、目录归位)全部在一台 Windows 桌机上完成——浏览器自动化这东西在 Windows 上最省心、SSD 快、出问题重启就行。NAS 那边
只做一件事:被写。TrueNAS 从头到尾只扮演"最终的家"。这个选择的直接好处是:流水线任何一环实验失败,都不伤库。
第二,规则先于工具。 我没有先选软件再迁就目录,而是先定死目录规范,再让所有工具(包括 AI 生成的脚本)俯首听命。后面你会看到,几乎所有"妙处"都来自这条原则。
三、硬件与网络:把钱花在刀刃上- TrueNAS SCALE 一台,存储层是 ZFS"软阵列"(我这块是 Z2 级别:同组允许坏两块)。SCALE 是 Linux 底座,装服务和跑容器都顺手。
- Windows 桌机 当"干活的骡子":所有 Python/Jupyter 都在这台机器上。
- 万兆交换机 + 万兆网卡,NAS 和桌机各一条。
先泼一盆冷水:即使是万兆交换机,SMB
实际稳定在 200 MB/s 左右——不是网卡不行,也不是交换机不行,是
机械盘到了自己的物理顶速。这给后来人省一笔钱:如果你的池子是机械盘,网络再快也没用;想满万兆先想清楚要不要 SSD 层。这个认知我是在 FastCopy 的进度条上学会的。
还有一个容易忽略的点:
Z: 这块 Windows 的网络盘,和本地处理盘的目录树是同构的——本地怎么长,NAS 上就怎么长。这不是巧合,是刻意设计(后面目录规范一节展开它的收益)。
四、目录规范:这套系统的心脏(全文重心)4.1 先分类:状态桶 + 区域先看"人生阶段"。我的桶分两段:
工作台的桶只存在于本地处理盘,
终态的桶才是 NAS 上的库存。先看工作台(Z: 上没有这两个):
- 04_temp ← 处理中:新货的唯一入口,一切从这里开始
- 05_unwashed ← 未清洗:下载堆进来的原始面貌
再看真正的库(Windows 的 Z: 上长这样,本地处理盘同构部分也一致):
- 00_collection ← 收藏:反复看的价值片,长期留存
- 01_seen ← 看过:不再追新但保留
- 02_unseen ← 感兴趣但未看:有特征、值得期待那一类
- 03_cold ← 无特征的未看:特征不明、先冻起来不占心智
- 09_trash ← 看过但不好的:见 4.5,它的用法很特别
为什么用"桶"而不是只有可见/不可见两类?因为
一个文件的整理过程本身就是流水的阶段:进 04_temp 时它是"原材料",在 05_unwashed 里还是"带皮的",清洗归位后进 02_unseen(看着有特征)或 03_cold(看不出所以然),看完按感受分三路——好片升级 00_collection、普通片回落 01_seen、烂片扔 09_trash。
桶是一张粒度合适的看板:随便打开这棵树的任一层,我立刻知道"还有多少库存没清""哪些可以开始冷处理"。
同一棵树,每个桶里再分区域:
JP / AS / EU (+ UNKN) ← 三路资源的处理逻辑很不一样
区域层级同时是元数据的一部分——后面 nfo 里的
<mpaa> 字段,对应的就是这一层。
三个区域的驱动方式
完全不同:JP 是"番号世界",靠规则吃饭,是这套系统的心脏;EU 靠片商驱动;AS 是一堆番号规则对不上的杂项。下面我把
JP 展开细说(本文重心),EU 简单说一说,AS 提一嘴。
4.2 JP:番号世界(重心)番号库的层级是这样的:
- 01_seen\JP\01_STD\
- └─ A\ 一级=番号首字母目录
- └─ 16-0\ 二级=分片 XX-0
- └─ ABF-000211\ 三级=每番号一个文件夹
- ├─ ABF-211_破坏版.mp4
- └─ movie.nfo
分片号怎么算出来的?核心一步是"掐尾":把番号的
末 4 个字符临时摘出去(少数格式特殊的前缀,如 10musume,另在一张特殊表里按掐 7 位处理),剩下的主体串——它的首字符可能不是字母而是数字,数字 10 个(0-9)加上字母 26 个(A-Z)合计正好 36 个字符,构成一个完备的
36 进制字符集——于是这个"掐了尾的主体串"可以当作一个 36 进制数读出来,
再对 100 取模,得到
00~99 中的一个分片号。
这套算法的妙处不在"打散",而在
保连号:末 4 位就是留给连续号码的"预留窗"——窗内一万个号码,无论怎么连号(补新番号、合辑拆 AB 碟、连续刊号),只要是同一主体串下的连续段,掐尾之后完全一样,
必然全部同片。而取模只是给每个主体串一个固定落点,纯函数、离线可算、不查任何库。
溢出的规则也和这个窗对得上:单片先放 1000 个,放不下就按文件名升序续开
88-1、
88-2……。序号本身没有写死的上限,实际库里绝大多数分片连
-1 都用不到(我这边最多也就见到
-2)。所以 JP 这边分片后缀是
一位数(
16-0),不补零——这和 EU 那边手工数字夹的写法是两回事。
配套两条铁律:
- 夹名 = 补零后的完整番号(ABW-000101);
- 文件名 = 短番号 + 尾缀(ABW-101_有码.mkv / ABF-211_破坏版.mp4)。
为什么这么定?文件夹靠字母序排列、文件靠播放器显示——
全番号补零保证同一部片不管经过几次不同来源的下载/改名,都只会落到同一个夹里,永不分裂;而
短番号文件名更人性,播放器标题栏和手机上看起来都更清爽。
当然,番号世界里也有野路子:FC2 这种自带固定前缀、号码又长(七八位)的,不补零(FC2-15500261);还有些双段番号(比如 HEYDOUGA-4017 那一族的 XX 号)。这些特殊格式靠一份
我自己维护的识别清单统一管理——新见到一个特殊前缀,往清单里加一行即可,其他一切不动。
4.3 EU:片商驱动(简单说一说)EU 走的是另一条路,不看番号,看片商:
EU\01_STD\<首字母>\<片商>\...
比如
EU\01_STD\X\X-Art\、
EU\01_STD\P\Passion-HD.com\,每集一个夹、一视频一 nfo。
片商条目一多(几百上千条),我就在条目层之前
手工加一层数字夹分组:
X-Art\00\、
Cosmid\00\ 这样。原因同上,1个文件夹里不超过1000个,这样windows查看的时候不会卡。
4.4 AS:杂项(提一嘴)AS 是番号规则对不上的那一堆,下面分了
01_STD / 02_探花 / 03_SELF / 04_系列片 / 09_UNKN。其中
02_探花 按拍摄者人名建夹、多个视频平铺(这类还没有 nfo,是待整理的欠账);
04_系列片 按系列名建夹、下分集。这里不展开,知道它和 JP 是两种逻辑就行。
4.5 09_trash:全库去重的最后一道闸桶里比较特殊的一个,是 09_trash。它不是垃圾堆,
是黑名单。
看过的片子,不好看的,我的处理不是简单删掉,而是
把整个目录挪进 trash:目录留下、内容清空(只留一份 movie.nfo 当"户籍")。为什么?因为这类资源有一个特点:它会源源不断地换个马甲再来——换一个站重新上传、改个说明就变成"修复版""未删减""4K 重制",一次一次把它推回你的 04_temp。而人脑记不住"哎我半年前看过这个,不行"。
有了 trash,这件事交给脚本做:入库前查一遍字典,命中即跳过。
目录留下来了,等于给每个烂片注销了户籍,以后不管它换几次马甲,都进不来。这比"删除+凭记忆防购"靠谱得多——记忆会漏,trash 不会。
需要说清楚一点:这套去重是
按番号认的。同番号重复会被拦下(旧片住旧夹、新马甲被认出);但如果有人换了个完全不同的番号重新上,那就不在这道闸的管辖范围内了。
4.6 真实目录树节选(感受一下最终形态)- Z:\00_collection
- ├─ AS\04_系列片\韩国演艺圈\韩国演艺圈Vol14\
- │ ├─ 韩国演艺圈Vol14.mp4
- │ └─ movie.nfo
- └─ JP\01_STD\F\00-0\FC2-15500261\
- ├─ FC2-PPV-15500261.mp4
- └─ movie.nfo
- Z:\01_seen\JP\01_STD\A\16-0\ABF-000211\
- ├─ ABF-211_破坏版.mp4
- └─ movie.nfo
- Z:\02_unseen\EU\01_STD\X\X-Art\15.02.07.Cock Sucking ( and Fucking) Competena and Aubrey\
- ├─ ...mp4
- └─ movie.nfo
番号类是干净的"
一夹、一视频、一份 nfo"。这个"一件一夹"的友好形态,是后面所有搜索/迁移/检查的前提。
五、元数据策略:movie.nfo 是"前端无关资产"这套库里我最坚持的一件事是
nfo(每个夹一份
movie.nfo,Kodi 元数据方言)。它记账的内容:标题、mpaa(对应区域层级)、set(系列)、studio(片商)、actor(演员)、genre、tags。
为什么坚持"一个视频的全部信息收进一份 nfo"?因为这是元数据工具生态里最务实的公约:
宽读严写——Kodi、Jellyfin 这样的工具会读各种来源拼出展示,但要写回时只有一个最宽容、最通用的落点,就是 movie.nfo。你把元数据统一写成每夹一份 movie.nfo,就意味着
任何一家"读它=读库、写它=记账"的工具来访问我的树,规则都简单到一行:去的是夹、读的是同一文件、写也是同一文件。我不用为每个工具维护一套扫描配置,也不用担心它突然升级后扫法变了——文件的形状不变,工具随便换。
为什么坚持给每个夹写 nfo?一句话:
前端会过时,文件不会过时。 我从前用 Jellyfin,界面检索跟不上;今天用 Stash;明天可能用别的谁。把元数据以 XML 形式
持久写进文件树,意味着"我这些年积累的演员/标签/系列信息"是独立于任何软件的资产。将来哪怕一切重来,把目录往任何一个支持 Kodi 风格 nfo 的前端一放,库立刻可用。这个特性在中长期存储规划里被低估得厉害:
元数据的私人成本和文件本体同样大,甚至更大——它需要你亲自挖、亲自校对,才会可靠。
生成 nfo 时的填表逻辑值得展开一句。我维护了四份"词表"文件:演员表、片商表、标签表、垃圾词表。这几份表基本是我这几年的
手工+半自动积累的活字典:从文件名、从站点信息、从磁力备注、从各种抓取的条目里攒。识别逻辑则是一个朴素的多轮匹配:在标题里先找
演员(长名字覆盖短名字,避免同名重叠)、再去找标签、再找片商;完全弄不懂的剩余词,就交给一个日文分词器拿出一批"关键词候选"备用——这一段我直接跟 AI 说"用 fugashi 把剩余词分词,返回唯一集合"这样的白话,脚本生成完事。
规范我来定,实现交给 AI——这是我处理这类"繁琐但一句话能说清"任务的新姿态,效率高得离谱。
nfo 里的
<mpaa> 和目录结构还有一处
互相校验:生成时按所在区域写死,入库前检验脚本再
按路径的区域层复核——缺
<mpaa> 的按路径补上,写了 mpaa 但缺对应 tag 的也补一个 tag。它管的是"缺不缺",不负责揪出"写错区域"那种,那要靠人工兜底。
补一个真实的坑:番号世界里文件名花样多,XML 不接纳的那几个字符恰恰经常出现——X-Art 的标题里带
&,还有全角
| 这类聚拢分隔符。裸
& 写进 XML 是非法的,部分前端读到就拒收整个文件。所以生成端对
& 做了转义(不同生成器口径略有出入,有的落成
and 、有的落成
&),另外配了一个兜底脚本专修历史遗留的裸
&(把转义不了的替换成 and,修复前先用解析器验证一次)——新坑在生成端堵住,存量逐个收拾,两头闭环。
关于"搜刮",先说一句实话:没有"全类型、免维护、还合法"的万能源。 通用刮削库对这类资源是裸的,这已经说过;就算专门去找,也只是分几档:官方(DMM/FANZA)最权威,但基本只覆盖有码主流,无码、FC2、破坏版、探花这些根本不收;r18、javinfo 这类聚合 API 好用来着,可同样偏有码,还要 key、按次计费;JavDB、JavBus、MissAV 这类网站字段最全,却扛不住 Cloudflare 反爬、结构说变就变,还有 ToS 的雷。真正稳的做法只有一个:
把元数据当成自己的资产——要么自己维护词表、从文件名里抠(我现在的做法),要么把外部源"
抓一次、落进自己的库(以番号为键)",之后生成 nfo 只读自己的库,网站挂了也不慌。这也正是第十节要说的那个理由。
六、前端变迁:Jellyfin → Stash,一次大理念差距先把一个误解拆掉:Jellyfin 不是"刮得不好"——
AV 这类资源根本没法刮,任何依赖公开刮削库的系统对这个类型都是裸的。真正让我弃用 Jellyfin 的是另一件事:
界面对检索不友好。它是给全家共用看电影设计的浏览方式(满屏海报、按库分类、靠标签补一点点),面对一个几千部的番号库,"按片商找""按标签找""按演员+系列交叉找"几乎都做不顺手。资料并不错位,
只是我根本看不到自己的库长什么样。
后来迁到
Stash。突出的变化:
- 它的标签体系是第一等公民,又轻又朴素,扫文件名、读 nfo,做完可以随时改;
- 以"场景 / performer / studio / tag"四维交叉检索,正好补上通用前端想象不到的检索粒度;
- 不依赖 Windows:它可以直接跑在 TrueNAS 上,一个 NAS 里按需起多个实例,不同端口各管各的库——本地 Windows 桌机手头没有 Stash 也不影响任何人看片,真的把这个"前端"变成了 NAS 上的一项长期服务,它跟库不是同一台机器的就更让人放心;
- Web UI 自带,桌面/手机/电视都能访问,响应速度不卡。
强调一句:
换前端的前提,恰恰是先有前两节说的"统一目录 + 持久 movie.nfo"。因为 Stash 吃下这棵树时,目录本来就是规范的、nfo 本来就在夹里,迁移的全部工作量只在"适配",不在"返工"。而 movie.nfo"一次写全端通用"的特性,也让我
敢于一直换前端而不必带着旧包袱——这一点在切换工具时直接盈利。
七、入库传输:万兆 FastCopy 与"必经的人工筛查"本地一切就绪后,
FastCopy + SMB 万兆入 TrueNAS。实际速度 ~200 MB/s——上文说过,机械盘顶速,无需焦虑。工具上 robocopy 也能干同样的活,但我实测它
明显比 FastCopy 慢,于是固定用 FastCopy:它把校验、包含/排除列表、差量覆盖这些选项放在明面上,配上一个能边看边控制的传输窗口,正好对上我对这一步的心理预期——"
放行前再人工确认一遍"。
顺带说说为什么
必须在本地全规范之后才传,而不是让流水线直接在 NAS 的共享盘上跑:
- 网络盘是保证不了交互质量的:Jupyter 工作区重复 cell 跑脚本、浏览器自动化抓站点时,直接在 SMB 上做重 rename/move,掉线/锁相关的小概率事件会产生大量半残目录。
- 本地规范后,NAS 上的只是成品。上传就是纯粹的拷贝,出错就重来,没有中间态。
- 目录同构让 FastCopy 可以直接目录级对齐(同路径、同覆盖规则),也让我"人工筛查"从慢备份演化成了快速筛选。
至于筛查,其实大部分已经交给脚本了:入库前有一套流水线把产物和库存字典逐番号比对——同番号挑优劣、合并 nfo、去重、黑名单拦截、再搬运回写,全是自动的。人真正要看的只剩最后的兜底清单:重复版本里拿不准的、缺 movie.nfo 的、疑似撞黑名单的,脚本会把它们列出来,我点名处理。不做这步的下场我试过——出现"同一个番号既有原版夹、又有
_baofeng 副本夹"这种叠罗汉。
最后那 10% 的判断,我宁可花五分钟手动过一遍,也不愿意让自动化盲目跑下去。八、冗余与防呆:够用就好先报现状,避免误导:
本人只有 RAIDZ2,其他都没配。Z2 是底座(同组允许坏两块,"盘会坏"的工程答案),剩下的我的取舍很简单,按成本排:
- 快照——唯一的"防自己"。库最大的风险不是硬盘,是我自己的批量改名/正则替换;写时复制快照每天一张留两周,几 GB 的代价,手滑半小时拉回。
- scrub——Z2 的双校验不定期读全盘就不会自动修复位翻转,每月排一次,无感。
- SMART + 邮件告警——15 分钟顺手事,换盘从容。
- 给"不可再生数据"做异地小副本——仅限 movie.nfo 全量、四份词表、数据库 dump 这几百 MB,网盘/U 盘即可;全库异地副本不值,番号就是索引,丢了都能再弄回来。
上面都是"要不要补"通用建议,按你的成本和精力取几档,我只走到了 Z2。
九、三条可复用的心法(收尾)剥到最后,整套东西其实就三条:
一、目录即数据库。 你的目录树就是 schema,你的命名就是主键,路径就是检索条件。先把这套规则定死,一切工具都是这套规则的执行者——包括你还没想到的部分。
二、元数据即文件。 别把"我这库的资料"寄存在某个软件里。把一份 nfo 深埋在每一个夹里,前端是壳,随时可换;信息是骨,不可再生。
三、预演优于后悔。 我这套库里,凡是会动文件/动目录的步骤,基本都留了一条"DRY_RUN"开关:默认跑预演、只打印不执行(具体默认值视脚本而定),人工确认无误才放开。它没有一次出现在英雄高光的场景,它的价值是
给两年间所有冲动时的误操作兜底——这条是最老土的"安全带",也是救过我最多次的体验。
十、最后一个理由:自己写脚本,别把数据交出去说了这么多工具和结构,还有一个当初没写进去、但越想越重要的理由——
这类东西,最好自己弄脚本。社区里的工具有的过于简单,有的是第三方工具我不太相信。
原因很实在:这类资源天生不适合托付给三方公司或在线服务。你把数据传上去,就等于把"我收藏了什么"交到别人手里,服务哪天改规则、涨价、关停,你都只能跟着走。自己写脚本,数据从头到尾在自己的盘上,工具链也是自己的,没有谁能把你的库"下架"。
有人会担心"我不会写啊"。放到现在这个 AI 时代,这已经不太是问题了:用免费工具,把需求用白话讲清楚——"把文件名里的番号规范化,
只移动、不删除"——脚本基本就能跑起来。
规范我来定,实现交给 AI,这是我这套东西最省力的地方。
最后一条保险是给"手滑"准备的:
让脚本只移动、不删除, 或者
先干跑演示,不实际执行我几乎所有整理脚本的默认动作都是 move 而不是 delete,删除一律留给人手动确认。这样最坏的结果也就是"文件挪错了地方",而不是"文件没了"。
规则定好、脚本自己写、默认只移动——这三件事凑齐,一个能安心用很多年的库,基本就稳了。
第一次分享,有写到的没写到的,各位担待。如果哪里有误,欢迎指正。望各位坚持深蹲提纲,夜夜七次郎。
赞(32)