#!/usr/bin/env python3 """Jellyfin 重复电影检测脚本 通过文件名、ProviderIds、文件大小和时长判断重复 """ import requests import sys from collections import defaultdict SERVER_URL = "http://172.25.208.1:8096" API_KEY = "8cbcfe4d7d294c8aa00412ccc853459f" HEADERS = { "X-Emby-Authorization": f'MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token="{API_KEY}"', "Accept": "application/json" } def get_all_movies(): """获取所有电影""" movies = [] start_index = 0 limit = 100 while True: response = requests.get( f"{SERVER_URL}/Items", params={ "includeItemTypes": "Movie", "recursive": "true", "fields": "Path,ProviderIds,MediaSources,RunTimeTicks,Overview,ProductionYear", "limit": limit, "startIndex": start_index, "enableTotalRecordCount": "true" }, headers=HEADERS ) if response.status_code != 200: print(f"请求失败: {response.status_code} {response.text[:200]}") break data = response.json() items = data.get("Items", []) movies.extend(items) total = data.get("TotalRecordCount", 0) start_index += limit print(f" 已获取 {len(movies)}/{total} 部电影...", end="\r") if start_index >= total: break print(f"\n 共获取 {len(movies)} 部电影") return movies def find_duplicates(movies): """查找重复电影""" duplicates = [] # 1. 相同文件名(不同路径) name_map = defaultdict(list) for m in movies: name = (m.get("Name") or "").strip().lower() year = m.get("ProductionYear") key = (name, year) name_map[key].append(m) for key, items in name_map.items(): if len(items) > 1: duplicates.append({ "type": "相同名称+年份", "reason": f"电影名 '{items[0].get('Name')}' ({key[1]}) 存在 {len(items)} 个版本", "items": items }) # 2. 相同 ProviderIds (IMDb/TMDb) provider_map = defaultdict(list) for m in movies: providers = m.get("ProviderIds") or {} for pid_type in ["Imdb", "Tmdb", "Tvdb"]: pid = providers.get(pid_type) if pid: provider_map[(pid_type, pid)].append(m) for key, items in provider_map.items(): if len(items) > 1: duplicates.append({ "type": "相同 ProviderId", "reason": f"{key[0]} = {key[1]} 存在 {len(items)} 个版本", "items": items }) # 3. 相同文件大小和时长(不同路径 = 同文件被多次导入) size_map = defaultdict(list) for m in movies: sources = m.get("MediaSources") or [] if sources: size = sources[0].get("Size") runtime = m.get("RunTimeTicks") if size and runtime: # 允许 1% 的误差 size_key = (size // 1000000, runtime // 1000000) size_map[size_key].append((m, size, runtime)) for (size_gb, runtime_key), items in size_map.items(): if len(items) > 1: # 检查是否来自不同路径 paths = set(item[0].get("MediaSources", [{}])[0].get("Path", "") for item in items) if len(paths) == 1: continue # 同一文件,不重复标记 duplicates.append({ "type": "相同文件大小+时长", "reason": f"约 {size_gb/1000:.1f}GB / 约 {runtime_key//10000:.0f}分钟 存在 {len(items)} 个版本", "items": [item[0] for item in items] }) return duplicates def main(): print("=" * 60) print("Jellyfin 重复电影检测") print("=" * 60) print("\n📦 获取所有电影...") movies = get_all_movies() if not movies: print("❌ 未获取到电影数据") return print(f"\n🔍 开始检测重复...") all_dups = find_duplicates(movies) if not all_dups: print("✅ 未发现重复电影") return print(f"\n⚠️ 发现 {len(all_dups)} 组重复\n") print("=" * 60) for i, dup in enumerate(all_dups, 1): print(f"\n[{i}] {dup['type']}") print(f" 原因: {dup['reason']}") print(f" {'─' * 50}") for j, item in enumerate(dup['items'], 1): name = item.get("Name", "未知") year = item.get("ProductionYear", "未知") sources = item.get("MediaSources") or [] path = sources[0].get("Path", "未知路径") if sources else "未知路径" size = sources[0].get("Size", 0) if sources else 0 size_str = f"{size / 1024**3:.2f} GB" if size else "未知大小" movie_id = item.get("Id", "未知") overview = (item.get("Overview") or "")[:100] print(f" [{j}] {name}") print(f" ID: {movie_id}") print(f" 年份: {year}") print(f" 路径: {path}") print(f" 大小: {size_str}") if overview: print(f" 简介: {overview}") print() print("=" * 60) print(f"总计 {len(all_dups)} 组重复,涉及 { len(set(item.get('Id') for dup in all_dups for item in dup['items'])) } 部电影") print("=" * 60) if __name__ == "__main__": main()