#!/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()