immich 脚本

为指定人物创建一个属于他的共享相册。

#!/usr/bin/env python3
import requests
import sys
import argparse
import os
from dotenv import load_dotenv

# 加载 .env
load_dotenv()

IMMICH_URL = os.getenv("IMMICH_SERVER_URL", "").rstrip("/")
API_KEY = os.getenv("IMMICH_API_KEY", "")

if not IMMICH_URL or not API_KEY:
    print("❌ 请在 .env 文件中设置 IMMICH_SERVER_URL 和 IMMICH_API_KEY")
    print("示例:")
    print("IMMICH_SERVER_URL=http://192.168.31.10:3001")
    print("IMMICH_API_KEY=your_real_key")
    sys.exit(1)

HEADERS = {
    "Accept": "application/json",
    "x-api-key": API_KEY,
}


def get_all_people():
    """获取所有人脸人物(兼容包装格式 { "people": [...] })"""
    url = f"{IMMICH_URL}/api/people"
    resp = requests.get(url, headers=HEADERS, timeout=10)
    resp.raise_for_status()

    data = resp.json()

    # 如果返回的是 { "people": [...] },提取列表
    if isinstance(data, dict) and "people" in data:
        people_list = data["people"]
    elif isinstance(data, list):
        # 兼容旧版直接返回数组的情况
        people_list = data
    else:
        raise ValueError(f"❌ 无法识别 /api/people 的返回格式。返回内容: {data}")

    # 确保结果是列表
    if not isinstance(people_list, list):
        raise ValueError("❌ 'people' 字段不是列表")

    return people_list


def find_person_by_name(people, name):
    for person in people:
        if name.lower() in (person.get("name") or "").lower():
            return person
    return None



def search_assets_by_person_id(person_id):
    """通过 timeline 接口获取人物所有照片(适配 v1.138.1 扁平化响应)"""
    all_asset_ids = []

    # Step 1: 获取时间桶列表
    buckets_url = f"{IMMICH_URL}/api/timeline/buckets"
    params = {
        "personId": person_id,
        "visibility": "timeline"
    }
    resp = requests.get(buckets_url, headers=HEADERS, params=params)
    resp.raise_for_status()
    buckets = resp.json()

    if not buckets:
        print("⚠️  该人物没有任何照片")
        return []

    print(f"📅 发现 {len(buckets)} 个时间桶,开始加载...")

    # Step 2: 遍历每个时间桶
    for bucket in buckets:
        time_bucket = bucket["timeBucket"]
        bucket_url = f"{IMMICH_URL}/api/timeline/bucket"
        params2 = {
            "personId": person_id,
            "timeBucket": time_bucket,
            "visibility": "timeline"
        }
        resp2 = requests.get(bucket_url, headers=HEADERS, params=params2)
        resp2.raise_for_status()
        data = resp2.json()

        # ✅ 关键修正:直接从 data["id"] 获取 asset ID 列表
        asset_ids = data.get("id", [])
        if not isinstance(asset_ids, list):
            asset_ids = []  # 安全兜底

        all_asset_ids.extend(asset_ids)
        print(f"  📂 {time_bucket[:7]}: 获取 {len(asset_ids)} 张")

    print(f"✅ 总共找到 {len(all_asset_ids)} 张包含该人物的照片/视频")
    return all_asset_ids

def create_shared_album(album_name):
    """创建共享相册(新版路径:/api/albums)"""
    url = f"{IMMICH_URL}/api/albums"
    payload = {
        "albumName": album_name,
        "description": f"Auto-generated for: {album_name}",
        "shared": True
    }
    resp = requests.post(url, headers=HEADERS, json=payload, timeout=10)
    resp.raise_for_status()
    album = resp.json()
    print(f"✅ 相册 '{album_name}' 创建成功,ID: {album['id']}")
    return album["id"], album.get("shareLink")

def add_assets_to_album(album_id, asset_ids):
    """向相册添加资产(Immich v1.138.1 正确方式)"""
    if not asset_ids:
        print("⚠️  没有资产需要添加到相册")
        return

    # ✅ 注意路径是 /api/albums/{id}/assets,方法是 POST
    url = f"{IMMICH_URL}/api/albums/{album_id}/assets"
    payload = {"ids": asset_ids}

    try:
        resp = requests.put(url, headers=HEADERS, json=payload, timeout=60)
        resp.raise_for_status()
        print(f"✅ 成功向相册添加 {len(asset_ids)} 张照片")
    except requests.exceptions.HTTPError as e:
        print(f"❌ 添加资产失败 (HTTP {e.response.status_code})")
        print("错误详情:", e.response.text)
        sys.exit(1)

def main(target_name, album_name=None):
    print(f"🔍 查找人物: '{target_name}'")
    
    try:
        people = get_all_people()
    except requests.exceptions.ConnectionError:
        print(f"❌ 无法连接 Immich: {IMMICH_URL}")
        sys.exit(1)
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 401:
            print("❌ API 密钥无效,请检查 IMMICH_API_KEY")
        else:
            print(f"❌ API 错误: {e}")
        sys.exit(1)

    person = find_person_by_name(people, target_name)
    if not person:
        print(f"❌ 未找到 '{target_name}',可用人物:")
        for p in people:
            print(f"  - {p.get('name', 'Unnamed')}")
        sys.exit(1)

    print(f"👤 找到: {person['name']} (ID: {person['id']})")

    asset_ids = search_assets_by_person_id(person["id"])
    if not asset_ids:
        print("⚠️  该人物暂无照片")
        sys.exit(0)

    if not album_name:
        album_name = f"{person['name']}的照片"
    album_id, share_link = create_shared_album(album_name)
    add_assets_to_album(album_id, asset_ids)

    print("\n🎉 完成!")
    if share_link:
        print(f"🔗 分享链接: {share_link}")
    else:
        print(f"🔗 备用链接: {IMMICH_URL}/share/{album_id}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="分享 Immich 中某个人物的所有照片")
    parser.add_argument("name", help="人物名称(模糊匹配)")
    parser.add_argument("-a", "--album", help="自定义相册名")
    args = parser.parse_args()
    main(args.name, args.album)