30 KiB
30 KiB
Emby刷新演员头像脚本
1. 脚本总结
- 用 PersonId 直接请求图片 → 触发Emby下载,不用模拟点击
- 智能生成搜索关键词 → 中文取前1-2个字,英文取前2个字母
- 进度持久化 → 中断后可以继续,不会重复处理
- 限流保护 → 429/403/网络500自动停止
- 只处理有 Primary 标记的 → 避免无效请求
- 适用范围: Emby 4.8.x / 4.9.x 通用
- 需要科学上网的环境(脚本走系统代理)
- 大批量补全演员头像
说明: 整个补齐脚本分为两个阶段:
第一阶段: 补齐打开影片页就可以自动刷新的演员头像;
第二阶段: 补齐需要打开演员详情页才能刷新的演员头像.
脚本需要使用python,请自行安装。
2. 第一阶段脚本
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. 第二阶段脚本
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}")