149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
统计 Jellyfin 中每个演员出演的作品数量,找出超过指定阈值的演员。
|
||
结果保存为 JSON 和 Markdown 文档。
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
|
||
# ====== 配置 ======
|
||
JELLYFIN_BASE = "http://192.168.3.5:8096"
|
||
AUTH_HEADER = 'MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token="8cbcfe4d7d294c8aa00412ccc853459f"'
|
||
USER_ID = "0f2540701e884852a2d2a1b7e63299e8"
|
||
MIN_MOVIES = 5
|
||
OUTPUT_DIR = "/mnt/f/docs"
|
||
|
||
# 需要检查的媒体类型
|
||
MEDIA_TYPES = "Movie,Series,Episode,Video"
|
||
|
||
|
||
def api_get(path, params=None):
|
||
"""通用 GET 请求"""
|
||
url = JELLYFIN_BASE + path
|
||
if params:
|
||
parts = []
|
||
for k, v in params.items():
|
||
if isinstance(v, bool):
|
||
v = "true" if v else "false"
|
||
elif isinstance(v, int):
|
||
v = str(v)
|
||
parts.append(f"{k}={urllib.request.quote(str(v))}")
|
||
url += "?" + "&".join(parts)
|
||
|
||
req = urllib.request.Request(url)
|
||
req.add_header("X-Emby-Authorization", AUTH_HEADER)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
return json.loads(resp.read())
|
||
except urllib.error.HTTPError as e:
|
||
body = e.read().decode(errors="replace") if e.fp else ""
|
||
print(f" HTTP {e.code}: {e.reason} | {body[:200]}", file=sys.stderr)
|
||
return None
|
||
except Exception as e:
|
||
print(f" Error: {e}", file=sys.stderr)
|
||
return None
|
||
|
||
|
||
def main():
|
||
print("=== 获取所有演员 ===", file=sys.stderr)
|
||
|
||
# Persons API 无 limit 时返回全部
|
||
data = api_get("/Persons")
|
||
if not data:
|
||
print("ERROR: 无法获取演员列表", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
persons = data.get("Items", [])
|
||
total_count = data.get("TotalRecordCount", len(persons))
|
||
print(f"总计 {total_count} 个演员,获取到 {len(persons)} 条", file=sys.stderr)
|
||
|
||
results = []
|
||
total = len(persons)
|
||
|
||
for i, p in enumerate(persons, 1):
|
||
name = p["Name"]
|
||
pid = p["Id"]
|
||
has_image = bool(p.get("ImageTags", {}))
|
||
|
||
# 查询该演员出演的媒体数量
|
||
movie_data = api_get(f"/Users/{USER_ID}/Items", {
|
||
"personIds": pid,
|
||
"recursive": True,
|
||
"includeItemTypes": MEDIA_TYPES,
|
||
"limit": 0,
|
||
})
|
||
|
||
count = movie_data.get("TotalRecordCount", 0) if movie_data else 0
|
||
|
||
if count >= MIN_MOVIES:
|
||
results.append({
|
||
"name": name,
|
||
"id": pid,
|
||
"movie_count": count,
|
||
"has_image": has_image,
|
||
})
|
||
|
||
if i % 30 == 0 or i == total:
|
||
print(f" 进度: {i}/{total} | 已找到 {len(results)} 人超过 {MIN_MOVIES} 部", file=sys.stderr)
|
||
time.sleep(0.03)
|
||
|
||
# 排序:作品数降序 → 姓名升序
|
||
results.sort(key=lambda x: (-x["movie_count"], x["name"]))
|
||
|
||
# ====== JSON ======
|
||
json_path = os.path.join(OUTPUT_DIR, "jellyfin_actor_stats.json")
|
||
with open(json_path, "w", encoding="utf-8") as f:
|
||
json.dump({
|
||
"total_persons": total_count,
|
||
"fetched_persons": len(persons),
|
||
"threshold": MIN_MOVIES,
|
||
"actors_above_threshold": len(results),
|
||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||
"actors": results,
|
||
}, f, ensure_ascii=False, indent=2)
|
||
print(f"\n✅ JSON: {json_path}", file=sys.stderr)
|
||
|
||
# ====== Markdown ======
|
||
md_path = os.path.join(OUTPUT_DIR, "jellyfin_actor_stats.md")
|
||
with open(md_path, "w", encoding="utf-8") as f:
|
||
f.write("# Jellyfin 演员作品统计\n\n")
|
||
f.write(f"- **演员总数**:{total_count}\n")
|
||
f.write(f"- **统计阈值**:作品数 **> {MIN_MOVIES}** 部\n")
|
||
f.write(f"- **满足条件的演员**:**{len(results)}** 人\n")
|
||
f.write(f"- **统计时间**:{time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||
f.write("---\n\n")
|
||
|
||
# 用序号代表排名
|
||
rank = 0
|
||
prev = None
|
||
same_rank_start = 0
|
||
for idx, a in enumerate(results):
|
||
if a["movie_count"] != prev:
|
||
rank = idx + 1
|
||
prev = a["movie_count"]
|
||
img = "✅" if a["has_image"] else "❌"
|
||
f.write(f"| {rank} | {a['name']} | {a['movie_count']} | {img} |\n")
|
||
|
||
f.write("\n\n---\n\n")
|
||
f.write("*由脚本 `jellyfin_actor_stats.py` 自动生成*\n")
|
||
|
||
print(f"✅ Markdown: {md_path}", file=sys.stderr)
|
||
|
||
print(f"\n=== 结果摘要 ===", file=sys.stderr)
|
||
print(f"演员总数: {total_count}", file=sys.stderr)
|
||
print(f"超过 {MIN_MOVIES} 部: {len(results)} 人", file=sys.stderr)
|
||
|
||
top_n = min(20, len(results))
|
||
print(f"\n--- Top {top_n} ---", file=sys.stderr)
|
||
for a in results[:top_n]:
|
||
img = "🖼️" if a["has_image"] else " "
|
||
print(f" #{rank:3d} {a['name']:30s} {a['movie_count']:4d} 部 {img}", file=sys.stderr)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|