在 NAS 或迷你主机上用 Docker 部署 Plex Media Server,并写一个“命名检查器”。Plex 能不能认出你的片子,主要看文件夹和文件名是否符合官方规范。检查器会扫描整个媒体目录,标出不规范的文件,给出改名建议,确认后再一键改名。
先看清成品
- 容器: 在 box 上用官方镜像
plexinc/pms-docker启动,/identity返回version="1.43.5.11029-6bf8af3f0"、claimed="0"。没有认领服务器(需要登录 Plex 账号拿 claim token)。在未认领的状态下,通过 API 创建媒体库返回 400,所以媒体库扫描和识别都没有在 box 上测过。- 命名检查器: pytest 3 项通过。测试素材是用 ffmpeg 生成的测试图样(合法的合成素材)。
--apply实测把Test.Pattern.2026.1080p.WEB.mkv改成了Test Pattern (2026)/Test Pattern (2026).mkv,把第一季/下的剧集移到了Season 01/,改完复查显示“全部符合”。docker compose config校验通过。
注意:只给媒体服务器放你有权使用的内容,比如自己拍的视频、自己购买并合法备份的光盘、公共领域或采用开放授权的作品。不要安装或分享盗版插件和资源,不要把媒体库共享给家庭以外的人,硬件转码等付费功能请按官方方式订阅(Plex Pass,价格以官网为准,本文不列)。遵守《Plex 服务条款》。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
docker-compose.yml |
官方镜像、host 网络、媒体目录只读挂载、claim token 放在 .env 里 |
naming_check.py |
按官方命名规范检查电影和剧集,给出改名建议,--apply 才会执行 |
test_naming.py |
规范、不规范和“片名里带年份”几种情况的测试 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| 硬件 | NAS 或迷你主机(x86 或 ARM64) | 数据库放在 SSD 上,翻页和搜索会快很多 |
| Docker | 带 Compose 插件 | docker compose version 能正常输出 |
| Plex 账号 | 用来认领服务器 | 在 plex.tv/claim 获取 claim token |
| Python | 3.8 以上 | 只用标准库 |
官方命名规范(已和 Plex 帮助中心核对)
| 类型 | 写法 |
|---|---|
| 电影 | /Movies/片名 (年份)/片名 (年份).ext;可以加 {imdb-tt0372784} 或 {tmdb-272} 帮助匹配 |
| 分段电影 | 片名 (年份) - pt1.ext(还支持 cd、disc、disk、dvd、part 加数字) |
| 剧集 | /TV Shows/剧名 (年份)/Season 02/剧名 (年份) - s02e17 - 可选信息.ext |
| 注意 | 季文件夹必须用英文单词 Season,即使内容是别的语言;官方建议剧名后面带上年份 |
文件结构
36-plex-A-src/
├── docker-compose.yml
├── .env.example
├── naming_check.py
└── test_naming.py
第 1 步:部署
docker-compose.yml:
# Plex Media Server(官方镜像 plexinc/pms-docker),适合 NAS 或迷你主机。
# 首次启动前,到 https://www.plex.tv/claim 获取 claim token,填进 .env 的 PLEX_CLAIM(token 有效期很短,拿到后马上启动)。
services:
plex:
image: plexinc/pms-docker
container_name: plex
network_mode: host # 官方推荐 host 网络,局域网发现最省事
environment:
- TZ=Asia/Shanghai
- PLEX_CLAIM=${PLEX_CLAIM}
- PLEX_UID=1000 # 改成对媒体目录有读权限的用户
- PLEX_GID=1000
volumes:
- ./config:/config # 数据库和元数据,放在 SSD 上
- ./transcode:/transcode # 转码临时目录
- /srv/media:/data:ro # 媒体目录只读挂载,Plex 不需要写权限
restart: unless-stopped
cp .env.example .env # 把 PLEX_CLAIM 改成刚从 https://www.plex.tv/claim 拿到的值
docker compose up -d
curl http://127.0.0.1:32400/identity
box 上的实测结果(未认领):
<MediaContainer size="0" apiVersion="1.2.3" claimed="0" machineIdentifier="2a7582…" version="1.43.5.11029-6bf8af3f0">
认领之后,在浏览器打开 http://主机IP:32400/web,添加媒体库,把 Movies 指向 /data/Movies,TV Shows 指向 /data/TV Shows。
第 2 步:命名检查器
naming_check.py:
"""按 Plex 官方命名规范检查媒体库目录,并给出改名建议(默认只打印,不改文件)。
电影:Movies/片名 (年份)/片名 (年份).ext,可以带 {imdb-tt...} 或 {tmdb-...},分段用 " - pt1" / cd1 等后缀
剧集:TV Shows/剧名 (年份)/Season 01/剧名 (年份) - s01e01 - 可选信息.ext(季文件夹必须用英文 Season)
用法:python naming_check.py 媒体根目录 [--apply] --apply 会按建议改名(只改能确定的)"""
import re, sys
from pathlib import Path
VIDEO = {".mkv", ".mp4", ".m4v", ".avi", ".mov", ".ts", ".webm"}
ID = r"(?: \{(?:imdb-tt\d+|tmdb-\d+|tvdb-\d+)\})?"
MOVIE = re.compile(rf"^(?P<t>.+?) \((?P<y>\d{{4}})\){ID}(?: - (?:cd|disc|disk|dvd|part|pt)\d+)?$")
EP = re.compile(r"[sS](\d{1,2})[eE](\d{1,3})")
YEAR = re.compile(r"(?<=[ ._(\[])((?:19|20)\d{2})(?=[)\]]|[ ._]|$)")
def guess_movie(stem):
"""取最后一个“像年份”的片段作为年份,它前面的部分作为片名。
(取第一个的话,Blade.Runner.2049.2017 会被认成 2049 年的 Blade Runner。)"""
ys = list(YEAR.finditer(stem))
if not ys: return None
y = ys[-1]
title = re.sub(r"[._]+", " ", stem[:y.start()]).strip(" ([-")
return (title, y[1]) if title else None
def check(root):
root, issues = Path(root), []
for f in sorted(p for p in root.rglob("*") if p.suffix.lower() in VIDEO):
rel = f.relative_to(root).parts
if rel[0] == "Movies":
ok = len(rel) == 3 and MOVIE.match(f.stem) and MOVIE.match(rel[1]) and f.stem.startswith(rel[1])
if not ok:
g = guess_movie(f.stem)
fix = root / "Movies" / f"{g[0]} ({g[1]})" / f"{g[0]} ({g[1]}){f.suffix}" if g else None
issues.append((f, "电影命名不符合规范", fix))
elif rel[0] == "TV Shows":
m = EP.search(f.name)
if not m: issues.append((f, "文件名里没有 sXXeYY", None)); continue
s, e = int(m[1]), int(m[2]); show = rel[1]
want_dir = f"Season {s:02d}"
if len(rel) != 4 or rel[2] != want_dir:
issues.append((f, f"应该放在 {want_dir} 文件夹里(季文件夹要用英文 Season)",
root / "TV Shows" / show / want_dir / f"{show} - s{s:02d}e{e:02d}{f.suffix}"))
else:
issues.append((f, "不在 Movies 或 TV Shows 下面", None))
return issues
if __name__ == "__main__":
issues = check(sys.argv[1])
for f, why, fix in issues:
print(f"✗ {f}\n {why}" + (f"\n → {fix}" if fix else "\n → 需要手动处理"))
if fix and "--apply" in sys.argv:
if fix.exists(): print(" 目标已存在,跳过"); continue
fix.parent.mkdir(parents=True, exist_ok=True); f.rename(fix); print(" 已改名")
print(f"检查完成:{len(issues)} 个问题" if issues else "✓ 全部符合 Plex 命名规范")
第 3 步:测试和运行
test_naming.py:
from pathlib import Path
from naming_check import check
def make(root, *paths):
for p in paths: (root / p).parent.mkdir(parents=True, exist_ok=True); (root / p).write_bytes(b"")
def test_good(tmp_path):
make(tmp_path, "Movies/Avatar (2009)/Avatar (2009).mkv", "Movies/Batman Begins (2005) {tmdb-272}/Batman Begins (2005) {tmdb-272}.mp4",
"Movies/The Dark Knight (2008)/The Dark Knight (2008) - pt1.mp4",
"TV Shows/Band of Brothers (2001)/Season 01/Band of Brothers (2001) - s01e01 - Currahee.mkv")
assert check(tmp_path) == []
def test_bad(tmp_path):
make(tmp_path, "Movies/test.pattern.2026.1080p.mkv", "TV Shows/Lab Show (2026)/第一季/Lab Show (2026) - S01E03.mkv",
"TV Shows/Lab Show (2026)/Season 01/Lab Show 第4集.mkv", "Downloads/x.mp4")
got = {str(f.relative_to(tmp_path)): (why, fix and str(fix.relative_to(tmp_path))) for f, why, fix in check(tmp_path)}
assert got["Movies/test.pattern.2026.1080p.mkv"][1] == "Movies/test pattern (2026)/test pattern (2026).mkv"
assert got["TV Shows/Lab Show (2026)/第一季/Lab Show (2026) - S01E03.mkv"][1] == "TV Shows/Lab Show (2026)/Season 01/Lab Show (2026) - s01e03.mkv"
assert got["TV Shows/Lab Show (2026)/Season 01/Lab Show 第4集.mkv"][1] is None
assert "Downloads/x.mp4" in got
def test_year_in_title():
from naming_check import guess_movie
assert guess_movie("Blade.Runner.2049.2017.1080p.BluRay") == ("Blade Runner 2049", "2017")
assert guess_movie("2001.A.Space.Odyssey.1968") == ("2001 A Space Odyssey", "1968")
assert guess_movie("1917") is None
$ python naming_check.py /srv/media --apply
✗ /srv/media/Movies/Test.Pattern.2026.1080p.WEB.mkv
电影命名不符合规范
→ /srv/media/Movies/Test Pattern (2026)/Test Pattern (2026).mkv
已改名
✗ /srv/media/TV Shows/Lab Show (2026)/第一季/Lab Show (2026) - s01e02.mkv
应该放在 Season 01 文件夹里(季文件夹要用英文 Season)
→ /srv/media/TV Shows/Lab Show (2026)/Season 01/Lab Show (2026) - s01e02.mkv
已改名
检查完成:2 个问题
$ python naming_check.py /srv/media
✓ 全部符合 Plex 命名规范
检查点:先不加 --apply 运行一遍,逐条确认建议没问题,再执行改名。推断不出来的文件(比如“第4集”这种中文写法)只会标记出来,不会被改动。
调试示例:片名里本身带年份
第一版用“第一个像年份的数字”作为年份,实际运行结果是:
Blade.Runner.2049.2017.1080p.BluRay -> ('Blade Runner', '2049')
片名里的 2049 被当成了年份。改成取最后一个像年份的片段之后,结果变成 ('Blade Runner 2049', '2017')。2001.A.Space.Odyssey.1968 也能正确识别,测试里加了这两个例子。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 打开网页后找不到服务器 | claim token 已过期,或者没有填 | 重新获取 token,删掉 config 后重新启动 |
| 电影匹配错了 | 同名电影很多 | 在文件夹名里加 {tmdb-ID} |
| 剧集没有分季 | 季文件夹不是 Season NN |
用检查器改名后重新扫描 |
| 容器读不到文件 | PLEX_UID 或 PLEX_GID 对媒体目录没有权限 | id 查一下,改成对应的值 |
模板规则
- 媒体目录只读挂载;改名用检查器在宿主机上完成。
- 先看建议,再加
--apply。 - 只整理你有权使用的内容。
项目资源
Plex 能不能把片子认对,主要看文件名,而不是刮削器。花五分钟跑一遍命名检查器,比事后在网页里一部一部手动修正省事得多。