From 56188a70da205796926b7a00ef0a92400859e08a Mon Sep 17 00:00:00 2001 From: Hayashi Date: Mon, 22 Jun 2026 11:38:12 +0800 Subject: [PATCH] Add Emby refresh actor figure method --- Emby刷新演员头像脚本.md | 1013 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1013 insertions(+) create mode 100644 Emby刷新演员头像脚本.md diff --git a/Emby刷新演员头像脚本.md b/Emby刷新演员头像脚本.md new file mode 100644 index 0000000..e102aec --- /dev/null +++ b/Emby刷新演员头像脚本.md @@ -0,0 +1,1013 @@ +# Emby刷新演员头像脚本 + +## 1. 脚本总结 + - 用 PersonId 直接请求图片 → 触发Emby下载,不用模拟点击 + - 智能生成搜索关键词 → 中文取前1-2个字,英文取前2个字母 + - 进度持久化 → 中断后可以继续,不会重复处理 + - 限流保护 → 429/403/网络500自动停止 + - 只处理有 Primary 标记的 → 避免无效请求 + - 适用范围: Emby 4.8.x / 4.9.x 通用 + - 需要科学上网的环境(脚本走系统代理) + - 大批量补全演员头像 + +说明: 整个补齐脚本分为两个阶段: +第一阶段: 补齐打开影片页就可以自动刷新的演员头像; +第二阶段: 补齐需要打开演员详情页才能刷新的演员头像. +脚本需要使用python,请自行安装。 + + +## 2. 第一阶段脚本 +```bash +import requests +import time +import json +import os +import sys +import re + +# ========================= +# 你需要修改的配置 +# ========================= + +EMBY_URL = "" +API_KEY = "" +USER_ID = "" + +# 每个搜索词之间等待多少秒 +SEARCH_TERM_DELAY_SECONDS = 5 + +# 同一个搜索词下,每个演员头像之间等待多少秒 +IMAGE_DELAY_SECONDS = 4 + +# 每个搜索词最多请求多少个带头像标记的演员 +# 先保守一点,避免一个关键词返回太多人 +MAX_IMAGES_PER_TERM = 50 + +# 每次搜索最多返回多少个演员 +SEARCH_LIMIT = 200 + +# 读取媒体库分页间隔 +PAGE_DELAY_SECONDS = 1 + +# 进度文件:记录已经搜索过的关键词、已经处理过的人物头像 +PROGRESS_FILE = r"D:\emby_person_image_warmer_progress.json" + +# 只从媒体库中收集演员 +TARGET_PERSON_TYPES = {"Actor"} + + +# ========================= +# 以下一般不用修改 +# ========================= + +session = requests.Session() +session.headers.update({ + "X-Emby-Token": API_KEY, + "User-Agent": "Mozilla/5.0 Emby-Web-Person-Image-Warmer/1.0" +}) + + +def load_progress(): + if not os.path.exists(PROGRESS_FILE): + return { + "terms": {}, + "persons": {} + } + + try: + with open(PROGRESS_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + + if "terms" not in data: + data["terms"] = {} + if "persons" not in data: + data["persons"] = {} + + return data + + except Exception: + return { + "terms": {}, + "persons": {} + } + + +def save_progress(progress): + with open(PROGRESS_FILE, "w", encoding="utf-8") as f: + json.dump(progress, f, ensure_ascii=False, indent=2) + + +def wait_seconds(seconds, reason): + print(f"{reason},等待 {seconds} 秒...") + time.sleep(seconds) + + +def get_all_media_items(): + """ + 获取所有电影和电视剧,用来收集演员名字。 + 这里只读取 People,不刷新元数据,不替换图片。 + """ + items = [] + start = 0 + limit = 100 + + while True: + r = session.get( + f"{EMBY_URL}/emby/Users/{USER_ID}/Items", + params={ + "UserId": USER_ID, + "Recursive": "true", + "IncludeItemTypes": "Movie,Series", + "Fields": "People", + "StartIndex": start, + "Limit": limit + }, + timeout=60 + ) + r.raise_for_status() + + data = r.json() + batch = data.get("Items", []) + items.extend(batch) + + print(f"已读取媒体条目:{len(items)} / {data.get('TotalRecordCount', '?')}") + + if len(batch) < limit: + break + + start += limit + wait_seconds(PAGE_DELAY_SECONDS, "读取下一批媒体前") + + return items + + +def collect_actor_names(items): + """ + 从媒体条目中收集演员名字。 + """ + names = set() + + for item in items: + for p in item.get("People", []) or []: + if p.get("Type") not in TARGET_PERSON_TYPES: + continue + + name = p.get("Name") + if not name: + continue + + name = name.strip() + if name: + names.add(name) + + return sorted(names) + + +def is_cjk_char(ch): + """ + 判断是否中日韩字符。 + """ + return ( + "\u4e00" <= ch <= "\u9fff" or + "\u3040" <= ch <= "\u30ff" or + "\uac00" <= ch <= "\ud7af" + ) + + +def make_search_terms_from_name(name): + """ + 从演员名字生成搜索关键词。 + + 例: + 刘若英 -> 刘, 刘若 + 刘诗诗 -> 刘, 刘诗 + Li Ying -> li, yi + Liu Shishi -> li, sh + Aoi Tsukasa -> ao, ts + 葵司 -> 葵, 葵司 + """ + terms = set() + clean = name.strip() + + if not clean: + return terms + + # 中日韩名字:取第一个字、前两个字 + cjk_chars = [ch for ch in clean if is_cjk_char(ch)] + + if cjk_chars: + terms.add(cjk_chars[0]) + + if len(cjk_chars) >= 2: + terms.add("".join(cjk_chars[:2])) + + # 拉丁字母名字:每个单词取前两个字母 + words = re.findall(r"[A-Za-zÀ-ÖØ-öø-ÿ]+", clean) + + for w in words: + w = w.lower() + + if len(w) >= 2: + terms.add(w[:2]) + elif len(w) == 1: + terms.add(w) + + return terms + + +def build_search_terms(actor_names): + """ + 根据所有演员名字生成搜索词列表。 + """ + terms = set() + + for name in actor_names: + terms.update(make_search_terms_from_name(name)) + + # 去掉太短且可能结果太杂的英文单字母 + cleaned = set() + for t in terms: + t = t.strip() + if not t: + continue + + # 英文单字母容易返回太多,跳过 + if re.fullmatch(r"[a-zA-Z]", t): + continue + + cleaned.add(t) + + return sorted(cleaned) + + +def search_persons(term): + """ + 模拟 Emby 搜索演员。 + """ + try: + r = session.get( + f"{EMBY_URL}/emby/Users/{USER_ID}/Items", + params={ + "UserId": USER_ID, + "Recursive": "true", + "IncludeItemTypes": "Person", + "SearchTerm": term, + "Fields": "ImageTags,PrimaryImageAspectRatio,ProviderIds", + "Limit": SEARCH_LIMIT + }, + timeout=90 + ) + + if r.status_code == 200: + data = r.json() + items = data.get("Items", []) + total = data.get("TotalRecordCount", len(items)) + + print(f"[搜索OK] 关键词={term} | 返回={len(items)} / 总数={total}") + return "ok", items, total + + if r.status_code == 429: + retry_after = r.headers.get("Retry-After") + print(f"[429 限流] 关键词={term}") + + if retry_after: + print(f"服务端要求等待 {retry_after} 秒。现在停止脚本。") + else: + print("检测到限流。现在停止脚本,建议稍后再跑。") + + return "rate_limited", [], 0 + + if r.status_code == 403: + print(f"[403 禁止访问] 关键词={term}") + print("检测到可能的封锁/权限问题。现在停止脚本。") + return "blocked", [], 0 + + if r.status_code == 500: + print(f"[搜索500] 关键词={term} | {r.text[:200]}") + return "server_error_500", [], 0 + + print(f"[搜索{r.status_code}] 关键词={term} | {r.text[:200]}") + return "fail", [], 0 + + except requests.exceptions.Timeout: + print(f"[搜索超时] 关键词={term}") + return "timeout", [], 0 + + except Exception as e: + print(f"[搜索错误] 关键词={term}: {e}") + return "fail", [], 0 + + +def request_person_image(person): + """ + 请求搜索结果里带 ImageTags.Primary 的演员头像。 + + 这是关键: + /emby/Items/{PersonId}/Images/Primary?tag=xxx + + 不刷新元数据。 + 不替换图片。 + 不删除图片。 + """ + name = person.get("Name") + person_id = str(person.get("Id")) + image_tags = person.get("ImageTags") or {} + primary_tag = image_tags.get("Primary") + + if not primary_tag: + print(f" [跳过无头像标记] {name} | PersonId={person_id}") + return "skip_no_tag" + + url = f"{EMBY_URL}/emby/Items/{person_id}/Images/Primary" + + params = { + "maxWidth": 400, + "quality": 90, + "tag": primary_tag + } + + try: + r = session.get(url, params=params, timeout=90) + + content_type = r.headers.get("Content-Type", "") + + if r.status_code == 200: + print(f" [图片OK] {name} | PersonId={person_id} | {len(r.content)} bytes | {content_type}") + return "ok" + + if r.status_code == 404: + print(f" [图片404] {name} | PersonId={person_id}") + return "not_found" + + if r.status_code == 429: + print(f" [429 限流] {name} | PersonId={person_id}") + return "rate_limited" + + if r.status_code == 403: + print(f" [403 禁止访问] {name} | PersonId={person_id}") + return "blocked" + + if r.status_code == 500: + text = r.text[:300] + print(f" [图片500] {name} | PersonId={person_id} | {text}") + + # 网络/代理/上游超时类 500,不记为成功,后面可以重试 + lower_text = text.lower() + if ( + "ssl connection" in lower_text or + "previous timeout" in lower_text or + "timeout" in lower_text or + "connection" in lower_text + ): + return "network_500" + + # Emby内部空引用一类,记录但不终止 + if "object reference" in lower_text: + return "server_error_500" + + return "server_error_500" + + print(f" [图片{r.status_code}] {name} | PersonId={person_id} | {r.text[:200]}") + return "fail" + + except requests.exceptions.Timeout: + print(f" [图片超时] {name} | PersonId={person_id}") + return "timeout" + + except Exception as e: + print(f" [图片请求错误] {name} | PersonId={person_id} | {e}") + return "fail" + + +if __name__ == "__main__": + progress = load_progress() + + print("开始读取媒体库,用来收集演员名字...") + items = get_all_media_items() + + print("\n开始收集演员名字...") + actor_names = collect_actor_names(items) + + print(f"共收集到演员数量:{len(actor_names)}") + + search_terms = build_search_terms(actor_names) + + print(f"生成搜索关键词数量:{len(search_terms)}") + print("前 50 个搜索关键词预览:") + for t in search_terms[:50]: + print("-", t) + + print("\n开始正式处理演员头像...\n") + + for term_index, term in enumerate(search_terms, 1): + if term in progress["terms"]: + old_status = progress["terms"][term].get("status") + print(f"{term_index}/{len(search_terms)} 跳过关键词:{term} | 已处理状态={old_status}") + continue + + print("=" * 70) + print(f"{term_index}/{len(search_terms)} 搜索关键词:{term}") + + search_status, people, total = search_persons(term) + + if search_status in {"rate_limited", "blocked"}: + save_progress(progress) + print("\n检测到限流/封锁风险,脚本停止。") + sys.exit(1) + + if search_status != "ok": + print("本关键词搜索失败,不写入关键词进度,下次会重试。") + wait_seconds(SEARCH_TERM_DELAY_SECONDS, "搜索词之间间隔") + continue + + # 只处理带 Primary 图片标记的人 + people_with_primary = [] + skipped_no_tag = 0 + skipped_done = 0 + + for p in people: + person_id = str(p.get("Id")) + image_tags = p.get("ImageTags") or {} + primary_tag = image_tags.get("Primary") + + if not primary_tag: + skipped_no_tag += 1 + continue + + # 已经处理过这个人物头像,跳过 + if person_id in progress["persons"]: + skipped_done += 1 + continue + + people_with_primary.append(p) + + print(f"本关键词返回人物:{len(people)}") + print(f"无头像标记跳过:{skipped_no_tag}") + print(f"已处理人物跳过:{skipped_done}") + print(f"本次需要请求头像:{len(people_with_primary)}") + + processed_count = 0 + ok_count = 0 + fail_count = 0 + network_500_count = 0 + server_500_count = 0 + + for idx, person in enumerate(people_with_primary[:MAX_IMAGES_PER_TERM], 1): + person_id = str(person.get("Id")) + name = person.get("Name") + + print(f" {idx}/{min(len(people_with_primary), MAX_IMAGES_PER_TERM)} 请求头像:{name} | PersonId={person_id}") + + result = request_person_image(person) + processed_count += 1 + + if result == "ok": + ok_count += 1 + progress["persons"][person_id] = { + "name": name, + "status": "ok", + "term": term, + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_progress(progress) + + elif result in {"not_found", "skip_no_tag", "server_error_500"}: + if result == "server_error_500": + server_500_count += 1 + else: + fail_count += 1 + + # 这类不是网络临时问题,可以记录,避免反复请求 + progress["persons"][person_id] = { + "name": name, + "status": result, + "term": term, + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_progress(progress) + + elif result == "network_500": + network_500_count += 1 + print(" 检测到网络/SSL/timeout 类 500,本人物不写入进度,下次可重试。") + + # 连续/多次网络失败时,建议停止,不要继续刺激上游 + if network_500_count >= 3: + save_progress(progress) + print("\n本关键词内出现 3 次网络类 500,脚本停止。请确认代理/Emby网络后再运行。") + sys.exit(1) + + elif result in {"rate_limited", "blocked"}: + save_progress(progress) + print("\n检测到限流/封锁风险,脚本停止。") + sys.exit(1) + + else: + fail_count += 1 + print(" 本次失败,不写入人物进度,下次会重试。") + + if idx < min(len(people_with_primary), MAX_IMAGES_PER_TERM): + wait_seconds(IMAGE_DELAY_SECONDS, "同一关键词下演员头像之间间隔") + + # 这个关键词已经搜索并处理过一轮,记录关键词进度 + progress["terms"][term] = { + "status": "done", + "returned": len(people), + "total": total, + "processed_images": processed_count, + "ok": ok_count, + "network_500": network_500_count, + "server_500": server_500_count, + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_progress(progress) + + print(f"关键词 {term} 完成:图片OK={ok_count},网络500={network_500_count},服务端500={server_500_count}") + + if term_index < len(search_terms): + wait_seconds(SEARCH_TERM_DELAY_SECONDS, "搜索词之间间隔") + + print("\n全部处理完成。") + print(f"进度文件已保存:{PROGRESS_FILE}") +``` + +# 3. 第二阶段脚本 +```bash +import requests +import time +import json +import os +import sys + +# ========================= +# 你需要修改的配置 +# ========================= + +EMBY_URL = "" +API_KEY = "" +USER_ID = "" + +# 第一阶段脚本的进度文件(如果存在,就会跳过已经成功的人) +PHASE1_PROGRESS_FILE = r"D:\emby_person_image_warmer_progress.json" + +# 第二阶段自己的进度文件 +PHASE2_PROGRESS_FILE = r"D:\emby_person_detail_warmer_phase2_progress.json" + +# 每次拉取多少个人物 +PAGE_LIMIT = 200 + +# 打开人物详情页后,等几秒再重新检查 +DETAIL_RECHECK_DELAY_SECONDS = 2 + +# 每处理一个人物后等待几秒 +PERSON_DELAY_SECONDS = 3 + +# 每处理多少个人物,休息一下 +BATCH_SIZE = 100 +BATCH_REST_SECONDS = 60 + + +# ========================= +# 以下一般不用修改 +# ========================= + +session = requests.Session() +session.headers.update({ + "X-Emby-Token": API_KEY, + "User-Agent": "Mozilla/5.0 Emby-Person-Detail-Warmer-Phase2/1.0" +}) + + +def load_json_file(path, default_value): + if not os.path.exists(path): + return default_value + + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return default_value + + +def save_json_file(path, data): + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + +def load_phase1_ok_persons(): + """ + 读取第一阶段已成功的人物,避免重复处理。 + """ + data = load_json_file(PHASE1_PROGRESS_FILE, {"terms": {}, "persons": {}}) + ok_persons = set() + + persons = data.get("persons", {}) + for person_id, info in persons.items(): + if info.get("status") == "ok": + ok_persons.add(str(person_id)) + + return ok_persons + + +def load_phase2_progress(): + data = load_json_file(PHASE2_PROGRESS_FILE, {"persons": {}}) + if "persons" not in data: + data["persons"] = {} + return data + + +def save_phase2_progress(progress): + save_json_file(PHASE2_PROGRESS_FILE, progress) + + +def wait_seconds(seconds, reason): + print(f"{reason},等待 {seconds} 秒...") + time.sleep(seconds) + + +def get_all_persons(): + """ + 直接读取 Emby 全部 Person 条目。 + """ + persons = [] + start = 0 + + while True: + r = session.get( + f"{EMBY_URL}/emby/Users/{USER_ID}/Items", + params={ + "UserId": USER_ID, + "Recursive": "true", + "IncludeItemTypes": "Person", + "Fields": "ImageTags,PrimaryImageAspectRatio,ProviderIds", + "StartIndex": start, + "Limit": PAGE_LIMIT + }, + timeout=90 + ) + + r.raise_for_status() + + data = r.json() + batch = data.get("Items", []) + total = data.get("TotalRecordCount", "?") + + persons.extend(batch) + + print(f"已读取人物:{len(persons)} / {total}") + + if len(batch) < PAGE_LIMIT: + break + + start += PAGE_LIMIT + time.sleep(0.5) + + return persons + + +def get_person_detail(person_id): + """ + 模拟打开人物详情页。 + """ + try: + r = session.get( + f"{EMBY_URL}/emby/Users/{USER_ID}/Items/{person_id}", + params={ + "Fields": "ProviderIds,ImageTags,PrimaryImageAspectRatio,Overview" + }, + timeout=90 + ) + + if r.status_code == 200: + return "ok", r.json() + + if r.status_code == 429: + return "rate_limited", None + + if r.status_code == 403: + return "blocked", None + + if r.status_code == 500: + text = r.text[:300].lower() + if ( + "ssl connection" in text or + "previous timeout" in text or + "timeout" in text or + "connection" in text + ): + return "network_500", None + return "server_error_500", None + + return "fail", None + + except requests.exceptions.Timeout: + return "timeout", None + + except Exception: + return "fail", None + + +def request_person_image(person_id, name, primary_tag): + """ + 请求人物头像: + /emby/Items/{PersonId}/Images/Primary?tag=xxx + """ + try: + r = session.get( + f"{EMBY_URL}/emby/Items/{person_id}/Images/Primary", + params={ + "maxWidth": 400, + "quality": 90, + "tag": primary_tag + }, + timeout=90 + ) + + if r.status_code == 200: + print(f" [图片OK] {name} | PersonId={person_id} | {len(r.content)} bytes") + return "ok" + + if r.status_code == 404: + print(f" [图片404] {name} | PersonId={person_id}") + return "not_found" + + if r.status_code == 429: + print(f" [429 限流] {name} | PersonId={person_id}") + return "rate_limited" + + if r.status_code == 403: + print(f" [403 禁止访问] {name} | PersonId={person_id}") + return "blocked" + + if r.status_code == 500: + text = r.text[:300] + print(f" [图片500] {name} | PersonId={person_id} | {text}") + + lower_text = text.lower() + if ( + "ssl connection" in lower_text or + "previous timeout" in lower_text or + "timeout" in lower_text or + "connection" in lower_text + ): + return "network_500" + + if "object reference" in lower_text: + return "server_error_500" + + return "server_error_500" + + print(f" [图片{r.status_code}] {name} | PersonId={person_id} | {r.text[:200]}") + return "fail" + + except requests.exceptions.Timeout: + print(f" [图片超时] {name} | PersonId={person_id}") + return "timeout" + + except Exception as e: + print(f" [图片请求错误] {name} | PersonId={person_id} | {e}") + return "fail" + + +if __name__ == "__main__": + print("当前运行目录:", os.getcwd()) + print("第一阶段进度文件:", PHASE1_PROGRESS_FILE) + print("第二阶段进度文件:", PHASE2_PROGRESS_FILE) + + phase1_ok_persons = load_phase1_ok_persons() + phase2_progress = load_phase2_progress() + + print(f"\n第一阶段已成功人物数量:{len(phase1_ok_persons)}") + + print("\n开始读取 Emby 全部人物...") + persons = get_all_persons() + + print(f"\n共读取到人物数量:{len(persons)}") + + need_process = [] + skipped_phase1_ok = 0 + skipped_phase2_done = 0 + skipped_already_has_primary = 0 + + for p in persons: + person_id = str(p.get("Id")) + name = p.get("Name", "") + image_tags = p.get("ImageTags") or {} + primary_tag = image_tags.get("Primary") + + # 第一阶段已经成功刷过 + if person_id in phase1_ok_persons: + skipped_phase1_ok += 1 + continue + + # 第二阶段已经处理过 + if person_id in phase2_progress["persons"]: + skipped_phase2_done += 1 + continue + + # 当前列表里已经有头像标记的,也先跳过 + # 因为第二阶段目标就是补“没有头像标记但点详情会出来”的人 + if primary_tag: + skipped_already_has_primary += 1 + continue + + need_process.append({ + "Id": person_id, + "Name": name + }) + + print(f"第一阶段已成功,跳过:{skipped_phase1_ok}") + print(f"第二阶段已处理,跳过:{skipped_phase2_done}") + print(f"当前本身已有头像标记,跳过:{skipped_already_has_primary}") + print(f"第二阶段需要处理:{len(need_process)}\n") + + processed = 0 + detail_success_count = 0 + image_ok_count = 0 + still_no_primary_count = 0 + network_500_count = 0 + server_500_count = 0 + fail_count = 0 + + for idx, person in enumerate(need_process, 1): + person_id = person["Id"] + name = person["Name"] + + print("=" * 70) + print(f"{idx}/{len(need_process)} 处理人物:{name} | PersonId={person_id}") + + # 第一次打开人物详情 + status1, detail1 = get_person_detail(person_id) + + if status1 in {"rate_limited", "blocked"}: + save_phase2_progress(phase2_progress) + print("\n检测到限流/封锁风险,脚本停止。") + sys.exit(1) + + if status1 == "network_500": + network_500_count += 1 + print(" 打开人物详情时发生网络类 500,本次不写进度,下次重试。") + if network_500_count >= 3: + save_phase2_progress(phase2_progress) + print("\n连续出现 3 次网络类 500,脚本停止。请确认代理/Emby 后再运行。") + sys.exit(1) + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + continue + + if status1 == "server_error_500": + server_500_count += 1 + print(" 打开人物详情时发生服务端 500,记录后跳过。") + phase2_progress["persons"][person_id] = { + "name": name, + "status": "server_error_500_on_detail", + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_phase2_progress(phase2_progress) + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + continue + + if status1 != "ok": + fail_count += 1 + print(" 打开人物详情失败,本次不写进度,下次重试。") + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + continue + + detail_success_count += 1 + + # 打开详情页后,等一会儿再重新读取一次详情 + wait_seconds(DETAIL_RECHECK_DELAY_SECONDS, "已打开人物详情,重新检查头像标记前") + + status2, detail2 = get_person_detail(person_id) + + if status2 in {"rate_limited", "blocked"}: + save_phase2_progress(phase2_progress) + print("\n检测到限流/封锁风险,脚本停止。") + sys.exit(1) + + if status2 == "network_500": + network_500_count += 1 + print(" 二次读取人物详情时发生网络类 500,本次不写进度,下次重试。") + if network_500_count >= 3: + save_phase2_progress(phase2_progress) + print("\n连续出现 3 次网络类 500,脚本停止。请确认代理/Emby 后再运行。") + sys.exit(1) + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + continue + + if status2 == "server_error_500": + server_500_count += 1 + print(" 二次读取人物详情时发生服务端 500,记录后跳过。") + phase2_progress["persons"][person_id] = { + "name": name, + "status": "server_error_500_on_recheck", + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_phase2_progress(phase2_progress) + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + continue + + if status2 != "ok": + fail_count += 1 + print(" 二次读取人物详情失败,本次不写进度,下次重试。") + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + continue + + image_tags = detail2.get("ImageTags") or {} + primary_tag = image_tags.get("Primary") + + if not primary_tag: + still_no_primary_count += 1 + print(" 打开人物详情后仍然没有头像标记,记录为 still_no_primary。") + + phase2_progress["persons"][person_id] = { + "name": name, + "status": "still_no_primary", + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_phase2_progress(phase2_progress) + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + continue + + # 已经拿到头像标记,正式请求头像 + print(f" 详情补全成功,发现头像标记:{primary_tag}") + image_result = request_person_image(person_id, name, primary_tag) + + if image_result == "ok": + image_ok_count += 1 + phase2_progress["persons"][person_id] = { + "name": name, + "status": "ok", + "primary_tag": primary_tag, + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_phase2_progress(phase2_progress) + + elif image_result == "network_500": + network_500_count += 1 + print(" 请求头像时发生网络类 500,不写进度,下次重试。") + if network_500_count >= 3: + save_phase2_progress(phase2_progress) + print("\n连续出现 3 次网络类 500,脚本停止。请确认代理/Emby 后再运行。") + sys.exit(1) + + elif image_result == "server_error_500": + server_500_count += 1 + phase2_progress["persons"][person_id] = { + "name": name, + "status": "server_error_500_on_image", + "primary_tag": primary_tag, + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_phase2_progress(phase2_progress) + + elif image_result in {"not_found"}: + phase2_progress["persons"][person_id] = { + "name": name, + "status": image_result, + "primary_tag": primary_tag, + "time": time.strftime("%Y-%m-%d %H:%M:%S") + } + save_phase2_progress(phase2_progress) + + elif image_result in {"rate_limited", "blocked"}: + save_phase2_progress(phase2_progress) + print("\n检测到限流/封锁风险,脚本停止。") + sys.exit(1) + + else: + fail_count += 1 + print(" 请求头像失败,本次不写进度,下次重试。") + + processed += 1 + + if idx < len(need_process): + wait_seconds(PERSON_DELAY_SECONDS, "人物之间间隔") + + if processed > 0 and processed % BATCH_SIZE == 0: + save_phase2_progress(phase2_progress) + wait_seconds(BATCH_REST_SECONDS, f"已处理 {processed} 个人物,本批休息") + + save_phase2_progress(phase2_progress) + + print("\n全部处理完成。") + print(f"成功打开详情:{detail_success_count}") + print(f"成功刷出头像:{image_ok_count}") + print(f"打开详情后仍无头像标记:{still_no_primary_count}") + print(f"网络类500:{network_500_count}") + print(f"服务端500:{server_500_count}") + print(f"其他失败:{fail_count}") + print(f"第二阶段进度文件:{PHASE2_PROGRESS_FILE}") +``` \ No newline at end of file