云端部署

在 NAS 或迷你主机上用 Docker 部署 Plex Media Server,并写一个“命名检查器”

在 NAS 或迷你主机用 Docker 部署 Plex,并通过 Python 检查电影与剧集文件命名;先预览改名建议,确认后才执行重命名。

BINBHO · 图文教程

在 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。
  • 只整理你有权使用的内容。

项目资源

用途 地址
电影命名 https://support.plex.tv/articles/naming-and-organizing-your-movie-media-files/
剧集命名 https://support.plex.tv/articles/naming-and-organizing-your-tv-show-files/
官方 Docker 镜像 https://github.com/plexinc/pms-docker
Plex 服务条款 https://www.plex.tv/about/privacy-legal/plex-terms-of-service/

Plex 能不能把片子认对,主要看文件名,而不是刮削器。花五分钟跑一遍命名检查器,比事后在网页里一部一部手动修正省事得多。