178 lines
5.5 KiB
Python
178 lines
5.5 KiB
Python
#!/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()
|