Immich Skill
将图像和视频上传到 Immich 服务器。
路径约定
{SKILL_DIR}= 本 skill 所在目录{SCRIPTS_DIR}={SKILL_DIR}/scripts/
工作流程
1. 配置文件
在当前工作目录、skill 目录、Git 项目根目录或 ~/.agents/agent_config.toml 中配置。查找优先级依次为:当前工作目录、skill 目录、Git 项目根目录、全局配置。
添加:
[immich]
base_url = "https://your-immich-server.com"
api_key = "your-api-key"
default_album = "My Photos" # 可选
public_album_url = "https://your-immich-server.com/s/shared-album-key" # 可选
asset_time_source = "upload" # 可选,upload(默认)或 source
2. 上传本地文件
使用 --project 指定 scripts 目录即可从任意工作目录调用 CLI。配置仍按前述
优先级查找,最终兜底为 ~/.agents/agent_config.toml:
# 上传单个文件
uv run --project {SCRIPTS_DIR} immich upload "{LOCAL_PHOTO}"
# 指定 album
uv run --project {SCRIPTS_DIR} immich upload "{LOCAL_VIDEO}" --album "Vacation"
# 单文件上传并保留网络来源的完整原始描述
uv run --project {SCRIPTS_DIR} immich upload "{LOCAL_VIDEO}" --description "原标题 #话题1 #话题2"
# 显式指定 video-downloader 元数据侧车(通常无需指定,会自动查找相邻文件)
uv run --project {SCRIPTS_DIR} immich upload "{LOCAL_VIDEO}" --metadata-file "{METADATA_FILE}"
# 本次上传改用媒体拍摄/创建时间
uv run --project {SCRIPTS_DIR} immich upload "{LOCAL_PHOTO}" --asset-time source
# 批量上传
uv run --project {SCRIPTS_DIR} immich upload "{LOCAL_IMAGE_1}" "{LOCAL_IMAGE_2}" --album "Trip"
2a. fallback:用 curl 直接上传
如果 Python 脚本上传失败(如遇到时区缺失的 400 错误,见陷阱 #2), 可以用 curl 作为 fallback,之后再调用 API 加入相册:
UPLOAD_AT=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
curl -s -X POST "${BASE_URL}/api/assets" \
-H "x-api-key: ${API_KEY}" \
-F "assetData=@/path/to/video.mp4;type=video/mp4" \
-F "deviceAssetId=hermes-$(date +%s)" \
-F "deviceId=hermes-agent" \
-F "fileCreatedAt=${UPLOAD_AT}" \
-F "fileModifiedAt=${UPLOAD_AT}"
视频可能包含旧的 creation_time,Immich 后台提取元数据后会覆盖上述时间;
curl fallback 必须等
GET /api/assets/{id} 返回 hasMetadata=true,再执行:
curl -s -X PATCH "${BASE_URL}/api/assets/${ASSET_ID}" \
-H "x-api-key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"dateTimeOriginal\": \"${UPLOAD_AT}\"}"
加入默认相册(先查 album ID,再 PUT):
# 查 album ID
ALBUM_ID=$(curl -s "${BASE_URL}/api/albums" -H "x-api-key: ${API_KEY}" | python3 -c "import sys,json;albums=json.load(sys.stdin);print(next(a['id'] for a in albums if a['albumName']=='ALBUM_NAME'))")
# 加入相册
curl -s -X PUT "${BASE_URL}/api/albums/${ALBUM_ID}/assets" \
-H "x-api-key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"ids\": [\"ASSET_ID\"]}"
# 成功加入公开相册后,向用户展示资源链接
echo "Public URL: ${PUBLIC_ALBUM_URL%/}/photos/${ASSET_ID}"
3. 网络资源下载后上传
本 skill 不下载网络资源。用户提供视频 URL 并要求上传 Immich 时,按以下顺序组合两个 skill:
- 使用
video-downloader检查 backend 并完成下载。 - 从下载结果中取得准确的本地媒体文件路径,并保留相邻的
<媒体文件名>.metadata.json。 - 将媒体路径传给本 skill 的
upload命令。未显式提供--description时,Immich 自动读取侧车,并把可获得的标题、作者、平台、 发布时间、时长、视频 ID、原始文案、话题和来源页写入 Description。 - 上传到默认公开相册后,将
public_url返回给用户。
下载文件默认保留。只有用户明确要求清理时,才在确认 Immich 上传成功后删除。
用户直接提供本地文件或附件时,跳过 video-downloader,直接上传。
侧车使用 video-downloader.metadata/v1 schema,字段契约见
video-downloader skill 的 references/metadata-handoff.md。显式
--description 优先于自动侧车;--metadata-file 可为单文件上传指定非相邻
侧车。侧车不存在时保持原有本地上传行为,不猜测视频信息。
4. 批量上传
批量上传指定目录下的文件(默认从 ~/Downloads 上传 mp4 文件):
# 上传 ~/Downloads 下所有 mp4 文件
uv run immich batch-upload
# 上传指定目录下的所有 mp4 和 jpg 文件
uv run immich batch-upload /path/to/photos jpg mp4
# 递归上传所有视频文件(包括子目录)
uv run immich batch-upload /path/to/videos mp4 mkv mov --recursive --album "Videos"
# 上传后不删除本地文件
uv run immich batch-upload --no-delete
批量上传会为每个媒体文件分别查找相邻侧车。启用默认删除行为时,上传成功的 媒体及其侧车会一起删除。
5. 初始化和测试
uv run immich init
6. 给已存在的 asset 补 description
Immich 没法改 originalFileName,但可以在 asset 详情面板的"Description"
字段里写任意文本(实际存储在 asset_exif.description)。如果之前的
上传因为 sanitize 把文件名改成 test.mp4,可以用这个子命令把原始
文件名、作者、来源 URL 写进去:
uv run --project {SCRIPTS_DIR} immich update-description <ASSET_UUID> "原文件名: xxx.mp4
抖音作者: 某某
抖音ID: 7659048818268179754
原始 URL: https://v.douyin.com/xxxxx/"
排障参考
- ghcr.io 镜像加速 & Immich v3 数据库迁移(pgvecto-rs → VectorChord):
references/ghcr-mirroring-and-immich-migration.md - Immich API 已验证的坑(
originalFileName不可改、时区必带、中文 文件名实际支持、duplicate/replaced状态码、description存在asset_exif而非asset,以及一个通用的 4xx 排障脚本):references/api-pitfalls-and-debugging.md
配置说明
| 配置项 | 必需 | 说明 |
|---|---|---|
base_url |
是 | Immich 服务器地址,不要包含 /api 后缀;客户端会自动添加 |
api_key |
是 | Immich API 密钥 |
default_album |
否 | 默认上传的 Album 名称 |
public_album_url |
否 | 默认相册的公开分享地址;成功加入该相册后生成资源公开链接 |
asset_time_source |
否 | 时间线时间来源:upload(默认,本次上传时间)或 source(媒体/文件原始时间) |
已知陷阱
originalFileName不可通过 API 改名。 Immich 的UpdateAssetDto字段(isFavorite、visibility、dateTimeOriginal、latitude、longitude、rating、description、livePhotoVideoId)里没有originalFileName。PUT/PATCH /api/assets/{id}即使带这个字段 也只更新updatedAt,文件名不变。想改名必须删除后重新上传。fileCreatedAt/fileModifiedAt必须带时区,且媒体元数据可能覆盖它们。 Immich 的 DTO 校验 ISO 8601 datetime 必须带时区(Z或+08:00)。datetime.fromtimestamp(mtime).isoformat()在 Linux 上返回2025-07-12T18:49:05.130080(无时区),服务器返回HTTP 400 {"message":"Validation failed", ...}。client.py::upload_asset现在用datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat().replace("+00:00","Z")生成2025-07-12T10:49:05.130080Z才合法。默认upload策略还会等待hasMetadata=true后使用运行机器的本地时区偏移 PATCHdateTimeOriginal, 避免 MP4 内嵌发布时间覆盖上传时间或造成时间线分组偏移。非 ASCII 文件名实际是支持的。 之前
client.py用re.sub(r'[^\x00-\x7F]', '_', filename)把中文文件名替换成 ASCII 下划线(test.mp4),但这个 sanitize 是错误的——Immich 服务器 端能正确处理中文 multipartfilename字段,库里生日视频.MOV、IMG_3129.mov等中英文混合文件名都正常存储。已移除该 sanitize。 真正的 400 原因是 #2 的时区,不是文件名。base_url不要包含/api后缀。 客户端会自动拼接/api/assets等路径。如果配置中写了/api,最终 URL 会变成/api/api/assets→ 404。必须使用
default_album配置。 如果用户在agent_config.toml中设置了default_album,上传时应使用该相册。Python 脚本通过get_default_album()自动读取。curl fallback 方式需要手动查 album ID 并调用加入相册 API。上传返回
duplicate是正常成功。 服务器对已存在 checksum 的 文件返回HTTP 200 {"status":"duplicate","id":"<uuid>"}。client.py::upload_asset已把这种情况标准化成{"status":"duplicate","id":"<uuid>"}返回值,不抛异常。公开链接只属于默认相册。 配置
public_album_url后,资源成功加入default_album才会返回public_url,格式为{public_album_url}/photos/{asset_id}。上传到其他相册或加入相册失败时 不应展示公开链接;duplicate资源成功加入默认相册后仍应展示。默认时间策略是本次上传时间。
asset_time_source = "upload"对新资源和duplicate都按本次命令开始上传的时间更新 Immich 时间线。需要保留照片拍摄时间 或视频内嵌创建时间时,配置source或单次使用--asset-time source。下载视频的详细描述来自相邻侧车。
video-downloader会生成<媒体文件名>.metadata.json。Immich 在没有显式--description时自动 读取并格式化;缺失字段会省略,侧车不存在时不改变普通本地上传行为。
Python API
from immich.config import load_config, get_immich_config
from immich.client import ImmichClient
from immich.uploader import ImmichUploader
# 加载配置
load_config()
# 使用客户端
async with ImmichClient() as client:
uploader = ImmichUploader(client)
# 上传本地文件
result = await uploader.upload_file(
Path("photo.jpg"),
album_name="My Photos",
)
print(result.get("public_url"))
# 上传多个文件(并行)
await uploader.upload_files([Path("a.jpg"), Path("b.png")], album_name="Photos")