1013 lines
30 KiB
Markdown
1013 lines
30 KiB
Markdown
# 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}")
|
||
``` |