手机里导出来的家庭视频,文件名五花八门:
VID_20260503_101500.mp4、IMG_8841.MOV、从聊天软件里保存的clip_from_chat.mp4。这个脚本读出每段视频的拍摄时间,按年/年月/日期 时间 原文件名整理好(只复制,不动原文件),再让 Emby 的“家庭视频”库自动创建并重新扫描。
先看清成品
- 测试: pytest 1 项通过,覆盖 UTC 转本地时区、按文件名兜底两种情况。
- 素材: 4 段 ffmpeg 生成的测试片,带不同的
creation_time元数据。- Emby 实测: 在 Emby 4.10.2.0 上整理并创建了“家庭视频”库(collectionType=homevideos),4 个条目都以正确的名称入库。
- 过程中发现 Emby 把某个视频显示成了 “05”,在全新实例上复现后修复(见调试示例)。
注意:媒体库里只放你有权使用的内容,比如自己拍的家庭视频、自己购买的影片的合法备份、开放授权的作品。不要安装盗版插件,不要分享资源。硬件加速转码属于 Emby Premiere 的功能(官方 Premiere 页面写明“Hardware Accelerated Transcoding”),请按官方方式订阅,价格以官网为准。
隐私提醒:家庭视频是敏感数据。这个库不要开放给外部用户,远程访问参考 39-emby-C 和 38-jellyfin-C 的方式做好 HTTPS 和访问控制。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
organize_home_videos.py |
读取拍摄时间:元数据 → 文件名 → 修改时间,并标注时间来源;默认只打印计划,--apply 才复制,--emby-refresh 创建库并扫描 |
test_organize.py |
时区转换和文件名兜底的测试 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| ffprobe | 随 ffmpeg 一起安装 | 用来读取 creation_time |
| Python | 3.9 以上(用到 zoneinfo) | 调用 Emby 时需要 requests |
| Emby | A 篇创建的 API 密钥 | 填到 EMBY_API_KEY |
文件结构
39-emby-B-src/
├── organize_home_videos.py
└── test_organize.py
第 1 步:脚本
organize_home_videos.py:
"""把手机导出的家庭视频按拍摄日期整理成 年/年月 文件夹,再让 Emby 的“家庭视频”库重新扫描。
拍摄时间的来源,按优先级:视频元数据 creation_time(UTC,转换成本地时区)→ 文件名里的 YYYYMMDD_HHMMSS → 文件修改时间(会标注“不可靠”)
默认只打印计划;加 --apply 才会复制(原文件保留不动)。
用法:python organize_home_videos.py 手机导出目录 家庭视频目录 [--tz Asia/Shanghai] [--apply]
[--emby-refresh](需要 EMBY_URL、EMBY_API_KEY;第一次运行会自动创建“家庭视频”库,库路径通过 EMBY_LIB_PATH 指定)"""
import json, os, re, shutil, subprocess, sys
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
EXT = {".mp4", ".mov", ".m4v", ".3gp", ".mkv"}
NAME_TS = re.compile(r"(20\d{2})(\d{2})(\d{2})[_-]?(\d{2})(\d{2})(\d{2})")
def shot_time(p, tz):
out = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format_tags=creation_time", "-of", "json", str(p)],
capture_output=True, text=True).stdout
ct = json.loads(out or "{}").get("format", {}).get("tags", {}).get("creation_time")
m = NAME_TS.search(p.name)
if ct:
t = datetime.fromisoformat(ct.replace("Z", "+00:00")).astimezone(tz)
if m: # 两个来源都有时,互相核对一下
diff = abs((datetime(*map(int, m.groups()), tzinfo=tz) - t).total_seconds()) / 3600
if diff >= 0.02: return t, f"元数据;⚠ 和文件名相差 {diff:.1f} 小时,检查相机或手机的时区设置"
return t, "元数据"
if m: return datetime(*map(int, m.groups()), tzinfo=tz), "文件名"
return datetime.fromtimestamp(p.stat().st_mtime, tz), "修改时间(不可靠)"
def plan(src, dst, tz):
for p in sorted(Path(src).iterdir()):
if p.suffix.lower() not in EXT: continue
t, how = shot_time(p, tz)
# 月份文件夹用“2026年05月”:用“2026-05”时,Emby 会把同一文件夹里的某个视频显示成“05”(见调试示例)
yield p, Path(dst) / f"{t:%Y}" / f"{t:%Y}年{t:%m}月" / f"{t:%Y-%m-%d %H.%M.%S} {p.stem}{p.suffix.lower()}", how
def emby_refresh(lib_path):
import requests
base, h = os.environ["EMBY_URL"].rstrip("/") + "/emby", {"X-Emby-Token": os.environ["EMBY_API_KEY"]}
libs = requests.get(base + "/Library/VirtualFolders", headers=h, timeout=15).json()
if not any(l["Name"] == "家庭视频" for l in libs):
requests.post(base + "/Library/VirtualFolders", headers=h, timeout=15,
params={"name": "家庭视频", "collectionType": "homevideos", "refreshLibrary": "true"},
json={"LibraryOptions": {"PathInfos": [{"Path": lib_path}]}}).raise_for_status()
print("✓ 已创建 Emby 库:家庭视频")
requests.post(base + "/Library/Refresh", headers=h, timeout=15).raise_for_status(); print("✓ 已请求 Emby 重新扫描")
if __name__ == "__main__":
a = sys.argv[1:]; tz = ZoneInfo(a[a.index("--tz") + 1]) if "--tz" in a else ZoneInfo("Asia/Shanghai")
for src, dst, how in plan(a[0], a[1], tz):
print(f"{src.name} → {dst.relative_to(a[1])} [{how}]")
if "--apply" in a:
if dst.exists(): print(" 已存在,跳过"); continue
dst.parent.mkdir(parents=True, exist_ok=True); shutil.copy2(src, dst)
if "--emby-refresh" in a: emby_refresh(os.environ.get("EMBY_LIB_PATH", "/mnt/media/Home Videos"))
第 2 步:先看计划,再执行
$ python organize_home_videos.py ~/phone-export "/srv/media/Home Videos"
IMG_8841.MOV → 2025/2025年12月/2025-12-25 03.05.00 IMG_8841.mov [元数据]
VID_20260503_101500.mp4 → 2026/2026年05月/2026-05-03 18.15.00 VID_20260503_101500.mp4 [元数据;⚠ 和文件名相差 8.0 小时,检查相机或手机的时区设置]
VID_20260503_103000.mp4 → 2026/2026年05月/2026-05-03 18.30.00 VID_20260503_103000.mp4 [元数据;⚠ 和文件名相差 8.0 小时,检查相机或手机的时区设置]
clip_from_chat.mp4 → 2026/2026年07月/2026-07-01 08.00.00 clip_from_chat.mp4 [修改时间(不可靠)]
$ EMBY_URL=http://127.0.0.1:8096 EMBY_API_KEY=… EMBY_LIB_PATH="/mnt/media/Home Videos" python organize_home_videos.py ~/phone-export "/srv/media/Home Videos" --apply --emby-refresh
✓ 已创建 Emby 库:家庭视频
✓ 已请求 Emby 重新扫描
- 元数据是 UTC 时间:
IMG_8841的元数据是 2025-12-24 19:05(UTC),换成上海时间是 12 月 25 日凌晨 3 点 05 分,归到了 12 月 25 日。 - 8 小时的提示: 这个 ⚠ 是我自己造的测试数据导致的:文件名里写的是本地时间,元数据里我也写成了同一个钟点,却标成了 UTC。真实手机上如果出现这种提示,通常说明设备的时区设置有问题。
test_organize.py:
import subprocess
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
from organize_home_videos import plan
def clip(p, ct=None):
md = ["-metadata", f"creation_time={ct}"] if ct else []
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "testsrc=size=160x120:duration=1", "-c:v", "libx264", "-preset", "ultrafast", *md, str(p)], check=True)
def test_timezone_and_fallback(tmp_path):
s = tmp_path / "s"; s.mkdir()
clip(s / "IMG_1.MOV", "2025-12-24T19:05:00Z") # UTC 晚上 7 点 = 上海 12 月 25 日凌晨 3 点
clip(s / "VID_20260503_101500.mp4") # 没有元数据,靠文件名
got = {p.name: (str(d.relative_to(tmp_path / "d")), how) for p, d, how in plan(s, tmp_path / "d", ZoneInfo("Asia/Shanghai"))}
assert got["IMG_1.MOV"] == ("2025/2025年12月/2025-12-25 03.05.00 IMG_1.mov", "元数据")
assert got["VID_20260503_101500.mp4"] == ("2026/2026年05月/2026-05-03 10.15.00 VID_20260503_101500.mp4", "文件名")
检查点:Emby 里“家庭视频”库的条目名称就是整理后的文件名。用 /Items?IncludeItemTypes=Video 查到 4 个条目,名称都正确。
调试示例:有一个视频在 Emby 里叫 “05”
第一版用 年/年-月/ 的文件夹结构,也就是 2026/2026-05/。扫描后,这个文件夹里的第一个视频在 Emby 里显示的名字是 “05”:
'05' | 2026/2026-05/2026-05-03 18.15.00 VID_20260503_101500.mp4
'2026-05-03 18.30.00 VID_20260503_103000' | 2026/2026-05/2026-05-03 18.30.00 VID_20260503_103000.mp4
我把时间格式从 181500 换成 18.15.00,结果一样。在全新的 Emby 实例上,两种时间格式也都能复现,说明不是旧数据缓存导致的。把月份文件夹改成 2026年05月 以后,同样两段视频的名称就正常了(在全新实例上验证过)。具体是 Emby 的哪条文件名解析规则造成的,我没有查到官方说明,所以只记录实测结果:在家庭视频库里,月份文件夹不要用 YYYY-MM 这种写法。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 时间差了 8 小时 | 元数据是 UTC,没有转换时区 | 用 --tz 指定你所在的时区 |
| 标注“修改时间(不可靠)” | 聊天软件转存时去掉了元数据 | 手动确认日期后改名 |
| 条目名称很奇怪 | 文件夹名和文件名的前缀一样 | 月份文件夹用 YYYY年MM月 |
| 重复运行会不会重复复制 | 目标文件已经存在时会跳过 | 安全,可以重复运行 |
模板规则
- 只复制,不移动,原文件一律保留。
- 每一个推断出来的时间都标明来源;来源不可靠的要明确提示。
- 家庭视频库只对家人开放。
项目资源
| 用途 | 地址 |
|---|---|
| API 密钥认证(官方) | https://dev.emby.media/doc/restapi/API-Key-Authentication.html |
| ffprobe 文档 | https://ffmpeg.org/ffprobe.html |
| Emby Premiere 功能列表 | https://emby.media/premiere.html |
整理家庭视频的关键是“时间从哪里来”:脚本把每个时间的来源都写清楚,你只需要核对标了 ⚠ 的那几条。