Plex 元数据:刮削、修补与 Kometa 自动化
Plex 元数据的工作原理
一个视频文件从进入媒体目录,到以完整的海报、简介、演员表出现在首页,中间要经过三个阶段:
flowchart LR
A["媒体文件<br/>Movies/大都会 (1927)/…mkv"] --> B["Scanner<br/>解析文件名与目录"]
B --> C["Agent<br/>匹配在线数据库"]
C --> D["TMDB / IMDb / TVDB<br/>fanart.tv 等数据源"]
D --> C
C --> E[("SQLite 数据库<br/>标题 / 简介 / 评分")]
C --> F["Metadata 目录<br/>海报 / 背景图等图片"]
- 扫描(Scan):扫描器(Scanner)只解析路径和文件名,从中提取标题、年份这些线索;它不联网,也不读取文件内容,产出只是一条候选信息,比如这个路径下可能是 1927 年的电影 Metropolis。因此文件名是
Metropolis (1927).mkv还是mtrpls.1080p.x264.mkv,对后续每一步的成功率影响都很大。 - 匹配(Match):代理(Agent)拿扫描得到的线索去在线数据库中搜索、打分,选定最可能的条目。目前新建电影库默认使用内建的 Plex Movie agent,数据聚合自 TMDB、IMDb、TVDB 等,海报和 logo 还会来自 fanart.tv 这类社区图库;老一代的插件式 agent(Plex Movie Legacy、The Movie Database 等)仍然可用,但官方已不再推荐。
- 刮削(Scrape):把选定条目的完整元数据拉取到本地,包括标题、简介、评分等文字信息,以及海报、背景图等图片资产。
匹配结果记录为一个 guid,这是条目与外部数据库之间的锚点。两代 agent 的 guid 格式不同,写脚本时两种都要兼容:
# Legacy agents expose the data source and external id directlycom.plexapp.agents.imdb://tt0017136?lang=encom.plexapp.agents.themoviedb://19?lang=en
# The new agent uses Plex's own id; external ids live in child fieldsplex://movie/5d7768254de0ee001fcc8f52 ├── imdb://tt0017136 ├── tmdb://19 └── tvdb://…与 guid 相对的是 ratingKey,即条目在这台服务器数据库中的自增主键。两者的区别在于可移植性:ratingKey 换一台服务器就变了,guid 指向的外部条目则不会变。用 API 操作单台服务器时,用 ratingKey 定位最快;要做跨服务器的配置或备份,就得存 guid。
刮削回来的数据存放在两个地方,都在 Plex 的数据目录下(Docker 部署通常挂载为 /config):
- 文字类元数据(标题、简介、评分、演员表)存入 SQLite 数据库
Plug-in Support/Databases/com.plexapp.plugins.library.db的metadata_items等表中; - 图片类资产(海报、背景图、logo)存入
Metadata/目录下按 hash 组织的 bundle。
知道存储位置有助于理解机制和排查问题,但不要绕过 Plex 直接修改数据库:表结构没有公开文档,一次不兼容的写入就可能损坏整个库。
手工修补与字段锁定
匹配和刮削不会每次都完美,总有需要手工干预的时候。在网页端打开条目的「编辑」界面,改标题、改简介、换海报,保存后字段旁会出现一个橙色的锁形图标。这个锁是 Plex 元数据管理中最关键的机制:锁定的字段在下次刷新元数据时不会被 agent 覆盖。手动编辑会自动锁定字段;也可以只点锁不改值,把 agent 当前刮削到的值固定下来。反过来,如果发现某个字段一直不随刷新更新,可以先检查它是否曾经被锁定。
如果错的是匹配本身,就用「修正匹配」(Fix Match)重新搜索,或者直接填入 TMDB/IMDb 链接。更换匹配后 guid 随之改变,所有未锁定的字段都会重新刮削。
用 plexapi 批量修补
界面操作适合零星几部,数量多了就该写脚本。python-plexapi 是 Plex API 最成熟的 Python 封装,网页端能做的操作基本都支持。连接只需要地址和 token,获取 token 最快的方式是在网页端任意条目上点「获取信息」再点「查看 XML」,地址栏里的 X-Plex-Token 参数就是 token:
from plexapi.server import PlexServer
plex = PlexServer("http://127.0.0.1:32400", "YOUR_PLEX_TOKEN")movies = plex.library.section("电影")找出缺图的条目
条目的 thumb 属性对应海报,art 对应背景图,为空就是灰色占位图的来源。遍历大库时把 container_size 调大,能明显减少 API 往返:
for movie in movies.all(container_size=500): missing = [] if not movie.thumb: missing.append("poster") if not movie.art: missing.append("art") if missing: print(movie.ratingKey, movie.title, movie.year, missing)老电影和版权状态存疑的影片最容易出现这种情况:agent 匹配到了正确的条目,但数据源本身就没有收录像样的图片。这种情况下反复刷新没有意义,只能自己找图补上。
从 TMDB 补齐海报
TMDB 的 /movie/{id}/images 接口在不带 language 参数时,会返回该条目在所有语言下的全部图片。先从 guid 里解析出 TMDB id,两代 agent 的格式都要兼容:
import re
_TMDB = re.compile(r"(?:themoviedb|tmdb)://(\d+)")
def tmdb_id(movie) -> int | None: guids = [movie.guid, *(g.id for g in movie.guids)] for g in guids: if m := _TMDB.search(g or ""): return int(m.group(1)) return NoneTMDB 给每张图都标注了语言(iso_639_1,为空表示无文字版本)和票数。实践下来比较合理的排序策略是:
- 海报优先英文版本,其次无文字版本,因为无字海报常常只是剧照;
- 背景图相反,优先无文字版本,它作为详情页的底图,越干净越好;
- 同语言组内再按票数和分辨率排序。
拿到图片 URL 后上传。uploadPoster 支持 url= 和 filepath= 两种参数:传 URL 意味着让 Plex 服务器自己去下载图片,如果服务器所在的网络访问不了 image.tmdb.org,上传就会失败;先在运行脚本的机器上把图片下载下来,再以文件形式推送给 Plex,就能绕开服务器侧的网络限制:
import osimport tempfile
import requests
def set_poster(movie, url: str) -> None: resp = requests.get(url, timeout=30) resp.raise_for_status() fd, path = tempfile.mkstemp(suffix=".jpg") with os.fdopen(fd, "wb") as fh: fh.write(resp.content) try: movie.uploadPoster(filepath=path) finally: os.remove(path)上传的图片会成为当前选中的海报,存入服务器的 Metadata bundle,不会在媒体目录里生成 poster.jpg 文件。
中文库不显示 logo
logo(clear logo)是详情页和部分客户端界面上代替标题文字的透明底图形。中文库的常见现象是:绝大多数条目不显示 logo,少数显示的又是 agent 自动选择的中文 logo,质量参差不齐。原因在于 Plex 对 logo 的语言策略与海报不同:logo 被当作图形化的标题处理,只采用与库的元数据语言一致的版本,不会像海报那样回退到英文。这一点官方文档没有明确说明,但中文库的实际表现和社区讨论都指向这个行为。于是 TMDB 上没有中文 logo 的电影就干脆不显示 logo,尽管编辑界面的候选列表里就有现成的英文版本。
既然候选列表里有图,那就用脚本批量把它选上。较新版本的 plexapi 提供了 logos() 和 setLogo():
for movie in movies.all(container_size=500): if movie.isLocked("clearLogo"): continue # skip manually locked logos logos = movie.logos() candidates = [ lg for lg in logos if not lg.selected and not str(lg.ratingKey).startswith("metadata://") ] if not candidates: continue # Prefer fanart.tv logos, usually higher quality than TMDB ones pick = next( (lg for lg in candidates if lg.provider == "fanarttv"), candidates[0] ) movie.setLogo(pick)setLogo 调用的是和网页端手动点选相同的接口,选中后 PMS 会自动锁定 clearLogo 字段,之后刷新元数据不会被清掉。候选列表中以 metadata:// 开头的条目是当前选中项的去重表示,跳过它才能保证换到另一张图。
批量统一中文标题
库里混着英文标题,想统一改成中文,几种直接的思路都不太可行:
- 刮削豆瓣:反爬严格,不适合批量稳定运行;
- 用 TMDB 的
zh-CN翻译:覆盖不全,还常混着港台译名; - 机器翻译:片名多为意译,机翻结果往往对不上通行译名。
比较可控的做法是维护一份本地对照表,人工或半自动填好中文名,再批量写回并锁定:
overrides = { "12345": "大都会", # ratingKey -> Chinese title "23456": "卡里加里博士的小屋",}
for rating_key, zh_title in overrides.items(): item = plex.fetchItem(int(rating_key)) item.edit(**{"title.value": zh_title, "title.locked": 1})title.locked: 1 等同于在界面上锁住标题字段;少了它,下次刷新元数据时标题又会变回去。
批量写操作的安全习惯
这类脚本都在直接修改服务器状态,上传和编辑都没有撤销操作,有几个习惯值得保持:
- 给脚本留一个
--dry-run模式,先打印执行计划,确认后再实际运行; - 正式运行前,先拿一两个
ratingKey小批量验证效果,确认无误再处理整个库; - 确认 PMS 的数据库定期备份处于开启状态(默认开启,见设置里的计划任务),元数据出了问题,它是最后的恢复手段。
Kometa:把元数据管理写成配置文件
上面的脚本解决的是一次性修补,还有一类需求是持续性的:IMDb Top 250 榜单变了,合集要跟着变;新入库的 4K 电影,海报上要自动加标;评分想统一换成 IMDb 的分数。这类需要定期对齐的工作,适合交给 Kometa。
Kometa 的前身是 Plex Meta Manager(PMM),2024 年 4 月随 v2.0 改名。它是一个 Python 批处理工具,读取 YAML 配置,连接 Plex 以及 TMDB、IMDb、Trakt、Letterboxd、MDBList 等外部数据源,主要做三类事情:
- 按各种来源建立合集(collection);
- 给海报叠加覆盖层(overlay);
- 批量执行元数据操作(operation)。
它不是常驻服务,每轮运行结束后即退出,等待下一个调度时刻。
flowchart TD A["config.yml<br/>库定义 / 数据源凭据"] --> B["Kometa 每日运行"] B --> C["拉取外部榜单<br/>IMDb / Letterboxd / Trakt…"] B --> D["查询 Plex 库内容"] C --> E["对比期望状态与现状"] D --> E E --> F["建立 / 更新合集"] E --> G["叠加 overlay 并上传海报"] E --> H["执行 mass 元数据操作"]
Docker 部署时用 KOMETA_TIMES 指定每天的运行时刻,KOMETA_RUN=False 避免容器重启时额外触发一轮运行:
services: kometa: image: kometateam/kometa:latest container_name: kometa environment: - TZ=Asia/Shanghai - KOMETA_TIMES=08:30 - KOMETA_RUN=False volumes: - /srv/kometa/config:/config restart: unless-stopped配置的入口是 config/config.yml,libraries 下的键必须和 Plex 里的库名完全一致,底部放 Plex 和各数据源的凭据:
libraries: 电影: collection_files: - default: imdb remove_overlays: false overlay_files: - default: resolutionplex: url: http://172.17.0.1:32400 token: YOUR_PLEX_TOKENtmdb: apikey: YOUR_TMDB_V3_KEY这份文件里带着 Plex token 和各家 API key,如果基础设施配置进了 Git,记得把它排除在外,仓库里只留脱敏的样板。
合集:从官方 defaults 到自定义 builder
- default: imdb 引用的是 Kometa 内置的 Defaults 文件,官方维护了几十个开箱即用的合集方案,覆盖各大榜单、奖项、流派、国家、电影宇宙等。每个 defaults 文件都暴露一组 template_variables 供定制,比如只要 IMDb Top 250、不要热门榜和低分榜:
libraries: 电影: collection_files: - default: imdb template_variables: use_all: false use_top: true # keep only the IMDb Top 250 collection use_separator: false # do not create separator collections - default: country template_variables: style: color # color flag poster style sort_by: random minimum_items: 10 # skip countries with fewer than 10 moviesdefaults 覆盖不到的需求就自己写合集文件。合集的核心概念是 builder,即用什么规则筛选电影。builder 分两类,行为差别很大:
plex_search、smart_filter这类基于库内条件的 builder,建的是 Plex 原生的智能合集,新入库的影片自动加入或移出,即使 Kometa 不运行也能保持最新;imdb_chart、letterboxd_list、trakt_list这类基于外部榜单的 builder,建的是普通合集,成员由每次运行时的榜单快照决定,Kometa 不运行就不会更新。
collections: 九十年代华语佳片: plex_search: # smart collection: auto-maintained by Plex all: country: China year.gte: 1990 year.lte: 1999 critic_rating.gte: 8 戛纳金棕榈: imdb_award: # regular collection: synced on each Kometa run event_id: ev0000147 # Cannes Film Festival event_year: all award_filter: Palme d'Or winning: true sync_mode: sync # remove items no longer on the list collection_order: custom在 config.yml 里用 - file: config/my-collections.yml 引用即可。
Overlay:给海报叠加信息层
overlay 是 Kometa 最具代表性的功能:在海报角落自动加上分辨率、音频编码、评分、获奖标识这类小徽章。它的机制值得了解,因为它会直接改写 Plex 里的海报。每次运行时:
- 计算哪些条目应该有哪些 overlay;
- 取干净的原图,叠加图层,把成品上传给 Plex;
- 备份原图,并给条目打上
Overlay标签用于追踪。
下次运行时,如果某个条目不再命中任何 overlay 规则,就用备份把原图还原回去。
libraries: 电影: remove_overlays: false # set true to remove all overlays and restore originals overlay_files: - default: resolution # 4K / 1080P badges - default: ratings template_variables: rating1: critic rating1_image: imdb所以叠加过 overlay 的条目,不要再在 Plex 界面里手动换海报。Kometa 依靠标签和备份来追踪状态,手动一换,它可能把带徽章的图当成原图,下一轮就会在徽章上再叠一层徽章。想换图,正确的入口是 Kometa 的 assets 目录,或者先移除该条目的 Overlay 标签再换。
Operations:批量元数据操作
operations 作用于整个库,适合统一各字段的数据来源。比如 Plex 自带的评分来源比较杂,想让详情页的评分一律显示 IMDb 分、流派标签一律跟随 TMDB:
libraries: 电影: operations: mass_critic_rating_update: imdb mass_genre_update: tmdbmass_*_update 系列覆盖了评分、流派、分级、上映日期、工作室等几乎所有字段,取值就是数据源名称。这类操作会直接修改库里每一个条目,第一次运行前想清楚,或者先在小库上试验。
调度与运行经验
用了一段时间后总结出几条经验,都和 Kometa 的批处理特性有关。
运行时刻要避开 Plex 自己的维护窗口。PMS 每天有一段计划任务时间(设置里的「计划任务」,默认在凌晨),数据库优化、深度媒体分析等任务都集中在这个时段执行,如果 Kometa 同时运行,两者竞争数据库资源,运行时间会成倍增长。把 KOMETA_TIMES 错开到维护窗口之后,差别很明显。
大型榜单也没有必要每天重建。IMDb Top 250、Letterboxd Top 500 这类榜单一个月也变动不了几部,但每次全量重建都要对 TMDB 和 Plex 发起持续几十分钟的请求。可以给这类合集文件单独设置低频调度,比如每月 1 号和 15 号各运行一次,其余日子跳过。delete_not_scheduled 默认为 false,不在调度中的文件只会被跳过,已建好的合集不会被删除:
libraries: 电影: collection_files: - default: country # library-based, runs daily - default: imdb template_variables: schedule: monthly(1), monthly(15) use_all: false use_top: true另外,Kometa 每次运行会把未显式配置的默认值写回 config.yml,线上那份配置会越来越长,自己维护的源文件保持最小化即可。我的完整配置(脱敏样板)放在 homelab-infra 仓库的 plex stack 里,配合 Komodo 做 GitOps 部署,相关背景见上一篇文章。
小结
Plex 元数据的问题,按性质分派给不同的工具处理:
- 零星几部的错配、改名、换图:网页端的编辑和修正匹配就够了,留意字段锁定的语义;
- 入库质量:文件命名决定匹配成功率,交给 Radarr、Sonarr 这类自动化改名工具最省心;
- 一次性的批量修补,比如缺图、logo、标题统一:写 plexapi 脚本,dry-run 先行;
- 持续性的合集维护、海报徽章、字段来源统一:交给 Kometa 按天调度;
- 直接打开 SQLite 改库:任何时候都不在选项里。
参考链接
- Metadata Agents(Plex 官方),agent 与数据源优先级
- Where is the Plex Metadata Stored?(Plexopedia),SQLite 数据库与 Metadata 目录
- python-plexapi 文档,Plex API 的 Python 封装
- TMDB API 文档,图片接口与语言字段
- Kometa Wiki,安装、配置与 defaults 总览
- Kometa Defaults Usage Guide,内置合集与 template_variables
- Kometa Overlays Guide,overlay 的工作机制与注意事项