结构调整

This commit is contained in:
2026-07-14 16:36:24 +08:00
parent 585e9d8e15
commit 57f354cf9f
254 changed files with 84114 additions and 44 deletions
@@ -0,0 +1,22 @@
# Jellyfin API 配置
## 基本信息
- **服务器地址(WSL 内)**: http://127.0.0.1:8096
- **服务器地址(Windows 宿主机)**: http://192.168.3.5:8096
- **服务器地址(WSL 网关 IP**: http://172.25.208.1:8096
- **API Key**: `8cbcfe4d7d294c8aa00412ccc853459f`
- **OpenAPI 规范文件**: `/mnt/f/docs/jellyfin-openapi-stable.json`
- **用户 ID**: `0f2540701e884852a2d2a1b7e63299e8`
## 认证方式
```http
X-Emby-Authorization: MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token="8cbcfe4d7d294c8aa00412ccc853459f"
```
## 相关文档
- 搜索 API 参考: `jellyfin-search-api.md`
- 重复电影检测: `jellyfin-dedup-howto.md`
- 演员统计脚本: `../bt-scripts/jellyfin_actor_stats.py`
@@ -0,0 +1,94 @@
# Jellyfin 重复电影检测操作指南
> Jellyfin 服务器配置见 `jellyfin-config.md`
## 概述
通过 Jellyfin API 检测库中的重复电影,支持两种检测方式:
- **相同名称+年份**:同一部电影被多次导入到不同文件夹
- **相同文件大小+时长**:同一个文件被不同文件夹引用导致重复扫描入库(允许1%误差)
## 文件位置
```
bt-docs/jellyfin-dedup.py # 检测脚本
bt-docs/jellyfin-dedup-howto.md # 本文档
bt-docs/jellyfin-config.md # Jellyfin 配置信息
```
## 前置条件
1. Jellyfin 服务已在 Windows 上运行(端口 8096
2. WSL 可通过网关 IP 访问 Windows 主机
## 使用方法
### 运行检测
```bash
python3 /mnt/f/docs/jellyfin-dedup.py
```
> ⚠ 如果脚本在 WSL 中运行,注意 WSL 内访问 Windows 端口需要填写网关 IP。脚本内默认使用 `172.25.208.1:8096`,可直接修改脚本头部的 `SERVER_URL`。
### 输出说明
```
============================================================
Jellyfin 重复电影检测
============================================================
📦 获取所有电影...
已获取 100/753 部电影...
共获取 753 部电影
🔍 开始检测重复...
⚠️ 发现 14 组重复
============================================================
[1] 相同名称+年份
原因: 电影名 'IPZZ-740 ...' (2025) 存在 2 个版本
[1] 番号名称
ID: e6955cab611255a7518af51492c15c15
年份: 2025
路径: E:\01_automated\日本电影\...
大小: 5.12 GB
简介: ...
[2] 番号名称(破解版)
ID: 224ead2dfa59084b0d9f9080b188f297
年份: 2025
路径: E:\01_automated\日本电影\...
大小: 4.82 GB
```
### 检测结果解读
| 检测类型 | 含义 | 处理建议 |
|---------|------|---------|
| **相同名称+年份** | 同一部电影在两个不同文件夹中 | 保留画质较好的版本,删除另一个 |
| **相同文件大小+时长** | 同一个文件被多次扫描入库 | 删除 Jellyfin 中重复条目即可,无需删除文件 |
## 常见问题
### 连接失败怎么办?
A: 检查 Jellyfin 是否在运行,确认网关 IP 是否正确:
```bash
ip r | grep default # 查看默认网关
```
### 为什么有些重复没检测到?
A: 目前检测逻辑:
- 相同番号不同文件名 → 能检测到
- 同一文件软链接/硬链接 → 能检测到(文件大小相同时)
- 不同画质版本(4K vs 1080p)→ 不会被标记(大小不同)
### 如何手动删除重复项?
A: 在 Jellyfin Web UI 中找到重复电影 → 删除 → 在「文件和元数据」选项中勾选「删除文件」即可(注意别误删)。
## 相关文档
- API 配置: `jellyfin-config.md`
- 搜索 API: `jellyfin-search-api.md`
- 检测脚本: `jellyfin-dedup.py`
- 演员统计: `../bt-scripts/jellyfin_actor_stats.py`
@@ -0,0 +1,177 @@
#!/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()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
# Jellyfin API 搜索电影
> 服务器地址和 API Key 见 `jellyfin-config.md`
## API 端点
```
GET /Items
```
## 搜索参数
| 参数 | 说明 |
|------|------|
| `searchTerm` | 搜索关键词 |
| `userId` | 用户ID |
| `limit` | 返回结果数量限制(默认 100) |
| `startIndex` | 起始索引 |
| `recursive` | 是否递归搜索(默认 true) |
| `includeItemTypes` | 项目类型(如 "Movie", "Series", "Episode" |
| `sortOrder` | 排序方式("Ascending", "Descending" |
| `fields` | 返回额外字段 |
| `minYear` / `maxYear` | 按年份筛选 |
## 请求示例
### 基础搜索
```
GET {ServerUrl}/Items?searchTerm=电影名&includeItemTypes=Movie
X-Emby-Authorization: MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token="{ApiKey}"
```
### 分页搜索
```
GET {ServerUrl}/Items?searchTerm=动作&includeItemTypes=Movie&limit=20&startIndex=0&sortOrder=Descending
X-Emby-Authorization: MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token="{ApiKey}"
```
### 按年份筛选
```
GET {ServerUrl}/Items?searchTerm=2024&includeItemTypes=Movie&minYear=2024
X-Emby-Authorization: MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token="{ApiKey}"
```
## Python 示例
```python
import requests
# 从 jellyfin-config.md 读取配置
SERVER_URL = "http://127.0.0.1:8096"
API_KEY = "8cbcfe4d7d294c8aa00412ccc853459f"
headers = {
"X-Emby-Authorization": f'MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token="{API_KEY}"'
}
# 搜索电影
response = requests.get(
f"{SERVER_URL}/Items",
params={
"searchTerm": "电影名",
"includeItemTypes": "Movie",
"limit": 20,
"startIndex": 0
},
headers=headers
)
if response.status_code == 200:
results = response.json()
for item in results.get("Items", []):
print(f"标题: {item.get('Name')}")
print(f"年份: {item.get('ProductionYear')}")
print(f"评分: {item.get('CommunityRating')}")
print("-" * 50)
else:
print(f"错误: {response.status_code}")
```
## 返回字段示例
```json
{
"Items": [
{
"Name": "电影标题",
"Id": "项目ID",
"Type": "Movie",
"ProductionYear": 2024,
"CommunityRating": 8.5,
"Overview": "剧情简介",
"RunTimeTicks": 1234567890,
"Genres": ["动作", "剧情"],
"MediaSources": [...]
}
],
"TotalRecordCount": 100
}
```
## 其他端点
- `GET /Users/Me/Items` — 当前用户所有内容
## 相关文档
- 配置: `jellyfin-config.md`
- 重复电影检测: `jellyfin-dedup-howto.md`
@@ -0,0 +1,579 @@
{
"total_persons": 610,
"fetched_persons": 610,
"threshold": 5,
"actors_above_threshold": 95,
"generated_at": "2026-05-09T22:14:35+0800",
"actors": [
{
"name": "未知演员",
"id": "e8d8f2d40d8b5f8730ab19820f772c2d",
"movie_count": 58,
"has_image": false
},
{
"name": "河北彩花",
"id": "b2b54f06f65bab8f67c4a677b4f88c27",
"movie_count": 41,
"has_image": true
},
{
"name": "相泽南",
"id": "74091e7a595c9b53eafe0f086b3ba2f3",
"movie_count": 35,
"has_image": true
},
{
"name": "ドラゴン西川",
"id": "7749f8835a045965b9bb2f1833881dc8",
"movie_count": 29,
"has_image": false
},
{
"name": "肉尊",
"id": "4865cf003c8bc3a6dd09c16ab47edb4b",
"movie_count": 23,
"has_image": false
},
{
"name": "イナバール",
"id": "58d800aeb115cdef4a290ed2322a2967",
"movie_count": 19,
"has_image": false
},
{
"name": "前田文豪",
"id": "8396709345f1f59a95d1dd14f0f8be18",
"movie_count": 19,
"has_image": false
},
{
"name": "きとるね川口",
"id": "26991347bee85f0b0f22cc4cfdaa64a2",
"movie_count": 17,
"has_image": false
},
{
"name": "トレンディ山口",
"id": "c99ed475e5b9ed571ef3804a996e8019",
"movie_count": 16,
"has_image": false
},
{
"name": "キョウセイ",
"id": "6d45c8b868301a05f3048675b5ad5d4d",
"movie_count": 15,
"has_image": false
},
{
"name": "太宰珍歩",
"id": "8a4e8f2c63856bb53d9a8c76bec7288e",
"movie_count": 14,
"has_image": false
},
{
"name": "豆沢豆太郎",
"id": "0d2fcf0f22a96d37012c8d652eec8d35",
"movie_count": 14,
"has_image": false
},
{
"name": "Nagisa Futami",
"id": "6b4ac3dda1122b1d0a4dc2435482afeb",
"movie_count": 12,
"has_image": false
},
{
"name": "U吉",
"id": "c2a59bf040a2900ff8c84ba5a3ec9d91",
"movie_count": 12,
"has_image": false
},
{
"name": "冰青",
"id": "1ad24f9af80d41529b93483790684af8",
"movie_count": 11,
"has_image": false
},
{
"name": "嵐山みちる",
"id": "8dca498d0cdff0b66101191a31ae69d1",
"movie_count": 11,
"has_image": false
},
{
"name": "赤井彗星",
"id": "eb4af3fd6d8c06ec4346b8bdc1203141",
"movie_count": 11,
"has_image": false
},
{
"name": "TAKE-D",
"id": "5fa992ab7834ba417465b8e09786336d",
"movie_count": 10,
"has_image": false
},
{
"name": "大崎広浩治",
"id": "1946a92abdcf62a7162e2065ab91ccfd",
"movie_count": 10,
"has_image": false
},
{
"name": "明里䌷",
"id": "4c5122465ed171c3075bbe284f49ea4a",
"movie_count": 10,
"has_image": true
},
{
"name": "紋℃",
"id": "ea5b1ca5d2ec8441db91323d0ba0cd00",
"movie_count": 10,
"has_image": false
},
{
"name": "小凑よつ叶",
"id": "9df7145dccdcb1d5e9d07964865c9197",
"movie_count": 9,
"has_image": true
},
{
"name": "枫可怜",
"id": "6e1cdf5553d0148055374feb9811efb9",
"movie_count": 9,
"has_image": true
},
{
"name": "Hitoshi Kawamura",
"id": "d717eea5994629ce181cf7c1a1ada4a9",
"movie_count": 8,
"has_image": false
},
{
"name": "Nana Kuwata",
"id": "87276bbbda6596f72bd4bcab04c3cbee",
"movie_count": 8,
"has_image": false
},
{
"name": "Seiji Sayama",
"id": "e6a769c33a3a723cf8486e7a8397fd31",
"movie_count": 8,
"has_image": false
},
{
"name": "Youichi Okazaki",
"id": "15d0ba7fc4596211b064f75a2015ae98",
"movie_count": 8,
"has_image": false
},
{
"name": "ZAMPA",
"id": "72ea1d908adafd30d65e629d2b051575",
"movie_count": 8,
"has_image": false
},
{
"name": "三島六三郎",
"id": "1969bae065e6c481997573444c606515",
"movie_count": 8,
"has_image": false
},
{
"name": "宝瀬博教",
"id": "6be0fe443f2497486331269042cbf082",
"movie_count": 8,
"has_image": false
},
{
"name": "宫西ひかる",
"id": "86c08f5efca3b4bc0ec7cb5de9be2ebb",
"movie_count": 8,
"has_image": true
},
{
"name": "朝霧浄",
"id": "cd7dbed6999cfa5a8ee3687447dd7c3f",
"movie_count": 8,
"has_image": false
},
{
"name": "枫芙爱",
"id": "bf31e038dec3ee22796aec84950af70f",
"movie_count": 8,
"has_image": true
},
{
"name": "真咲南朋",
"id": "c2f75da3ed433a459ebae6f4f222b46a",
"movie_count": 8,
"has_image": true
},
{
"name": "神宫寺奈绪",
"id": "1e24f1a6e8beca735230e50699480480",
"movie_count": 8,
"has_image": true
},
{
"name": "苺原",
"id": "aa66875a7316aa8e8cfcbad88e6ebece",
"movie_count": 8,
"has_image": false
},
{
"name": "Tomoaki Ikeda",
"id": "c40b40ba269bfa4bcfd0b97ee132eefc",
"movie_count": 7,
"has_image": false
},
{
"name": "ひむろっく",
"id": "b8606c4b32ddc0681faaa2876c844771",
"movie_count": 7,
"has_image": false
},
{
"name": "五日市芽依",
"id": "e3935226554960fa949725bc31dc26f4",
"movie_count": 7,
"has_image": true
},
{
"name": "宫下玲奈",
"id": "2857e08d5ad66d592e76c3add1f995fc",
"movie_count": 7,
"has_image": true
},
{
"name": "小野六花",
"id": "a948adec85d112ecb40466d369d49e8f",
"movie_count": 7,
"has_image": true
},
{
"name": "平川大辅",
"id": "7859ed30d7138150d42304bedc268bcc",
"movie_count": 7,
"has_image": false
},
{
"name": "樱井浩美",
"id": "d96b971ed9c4f0663a9c7879b1bd6ddb",
"movie_count": 7,
"has_image": false
},
{
"name": "池田辉",
"id": "7a323cc997f2e6f4e23fb83edb5a3035",
"movie_count": 7,
"has_image": false
},
{
"name": "矢澤レシーブ",
"id": "11878b202aeb1162704d60a0b80c1a46",
"movie_count": 7,
"has_image": false
},
{
"name": "石川澪",
"id": "4eaa79eb96bcbab0008c87aa4eae8e13",
"movie_count": 7,
"has_image": true
},
{
"name": "花衣つばき",
"id": "f6e1711d4067b6be0db9c20121a2427e",
"movie_count": 7,
"has_image": true
},
{
"name": "金松季歩",
"id": "f8a91138bf51fb08eded51ace5ea9a85",
"movie_count": 7,
"has_image": true
},
{
"name": "鸣海惠梨香",
"id": "04789e0674390fcb20a37be6bcc57948",
"movie_count": 7,
"has_image": false
},
{
"name": "---",
"id": "e1f2da1b63f837451696e9c7b8dff198",
"movie_count": 6,
"has_image": false
},
{
"name": "Kiyomu Fukuda",
"id": "dd02874bdb910c3f2f659cb23e0aad91",
"movie_count": 6,
"has_image": false
},
{
"name": "らくだ",
"id": "acf07aae4c35374e52a4a039c4fcb3f6",
"movie_count": 6,
"has_image": false
},
{
"name": "キャプテン江原",
"id": "25c0a67f956f7b781bd0f6152ed515b9",
"movie_count": 6,
"has_image": false
},
{
"name": "七泽美亚",
"id": "fe9371e7dcf00746dd33899c50908718",
"movie_count": 6,
"has_image": true
},
{
"name": "七濑爱丽丝",
"id": "508812a05991cffcdee3cb3646879e85",
"movie_count": 6,
"has_image": true
},
{
"name": "松本一香",
"id": "d51907b545527172aea47ae6e4578e4d",
"movie_count": 6,
"has_image": true
},
{
"name": "永野善一",
"id": "802834f8ecef6d4d43214cb86bea8799",
"movie_count": 6,
"has_image": false
},
{
"name": "泉ももか",
"id": "5dea4b38727f52d6ba34a6f28c8336e7",
"movie_count": 6,
"has_image": true
},
{
"name": "翼舞",
"id": "74da7a1427605ad5e346fc865224be9d",
"movie_count": 6,
"has_image": true
},
{
"name": "香水纯",
"id": "adedbb625fb1ca739ed7ddb999b09e27",
"movie_count": 6,
"has_image": true
},
{
"name": "黑川纱里奈",
"id": "7591cad07f4cdb1c17d57cd2ad9c8344",
"movie_count": 6,
"has_image": true
},
{
"name": "Daifuku Suginami",
"id": "255f6211d3ea974cf51cbcafc8ae7fa5",
"movie_count": 5,
"has_image": false
},
{
"name": "Haruna Kanbayashi",
"id": "51b93dbbd1b0f09c66eb6e47e4fddc0e",
"movie_count": 5,
"has_image": false
},
{
"name": "うさぴょん。",
"id": "7f2cfc65618362aac4cde35c3e88be39",
"movie_count": 5,
"has_image": false
},
{
"name": "ザック荒井",
"id": "449c2953bf24b759fbb6e55645e7e18b",
"movie_count": 5,
"has_image": false
},
{
"name": "ペイペイ",
"id": "689f41fa96301c796a3b0855be7f2145",
"movie_count": 5,
"has_image": false
},
{
"name": "七嶋舞",
"id": "c0edc7148511a194d690112fc4b291de",
"movie_count": 5,
"has_image": true
},
{
"name": "七森莉莉",
"id": "25dfc81b6ba21629859e3c0b7d21c7a9",
"movie_count": 5,
"has_image": true
},
{
"name": "三宫椿",
"id": "8171c2b3b26dd7f6a38e90482fbed055",
"movie_count": 5,
"has_image": true
},
{
"name": "五右衛門",
"id": "3295cd3dec0b0624eb5f21077b1ef2a1",
"movie_count": 5,
"has_image": false
},
{
"name": "伊藤舞雪",
"id": "7c65633ac5495ed4e9121aebada51b67",
"movie_count": 5,
"has_image": true
},
{
"name": "儿玉七海",
"id": "2010f14c740ccb1383f2cdba173f1a1f",
"movie_count": 5,
"has_image": false
},
{
"name": "八挂海",
"id": "1851a02eee55a9bd797b6917d80dc8fc",
"movie_count": 5,
"has_image": true
},
{
"name": "加州夏",
"id": "ff903a622f242dc3c26bacb362a5c16e",
"movie_count": 5,
"has_image": false
},
{
"name": "古川ほのか",
"id": "bc6d4ad211f9432f7283b24d64ba5edd",
"movie_count": 5,
"has_image": true
},
{
"name": "坂道美琉",
"id": "2cf74e40672241763504ff5a8d43aa0b",
"movie_count": 5,
"has_image": true
},
{
"name": "夢実かなえ",
"id": "8a6a473e9703dd49f667be08fe5aba23",
"movie_count": 5,
"has_image": true
},
{
"name": "奥田咲",
"id": "33c9d2419470b473d0d743970ad370cc",
"movie_count": 5,
"has_image": true
},
{
"name": "小春日和",
"id": "996ad4d19a2bb6dcee4e7eb21c0a14ac",
"movie_count": 5,
"has_image": false
},
{
"name": "小松セブンティーン",
"id": "4169d927a0d22f0f5cdfb781655d0a13",
"movie_count": 5,
"has_image": false
},
{
"name": "小野坂ゆいか",
"id": "b8f734aadb27a50ce8c1c77cb1b49af5",
"movie_count": 5,
"has_image": true
},
{
"name": "彩月七绪",
"id": "75e2d4712457e35cc9dc96fb4f946634",
"movie_count": 5,
"has_image": false
},
{
"name": "新井莉麻",
"id": "c5c54c495fefaaebe2d5b360570e6e54",
"movie_count": 5,
"has_image": true
},
{
"name": "桃乃木香奈",
"id": "1db1f00e6a326172af09881b6ee6b007",
"movie_count": 5,
"has_image": true
},
{
"name": "桥本有菜",
"id": "8413ac2b973aeb70520e71247aa5b75c",
"movie_count": 5,
"has_image": true
},
{
"name": "横山宏美",
"id": "cac408ba1de118700181f5981e59ea61",
"movie_count": 5,
"has_image": false
},
{
"name": "樱空桃",
"id": "ebd7bec14516fce73f6cb7dd7e235de2",
"movie_count": 5,
"has_image": true
},
{
"name": "浅野こころ",
"id": "eec3a2cbcf6d3b00c6b98c8bcaf7f656",
"movie_count": 5,
"has_image": true
},
{
"name": "海老咲あお",
"id": "e9ec442fcd9551a5c8fe1c3ffb082a02",
"movie_count": 5,
"has_image": true
},
{
"name": "瑞泽溪",
"id": "a60056fe619b9f9edadf2ca3ae8a0ee1",
"movie_count": 5,
"has_image": false
},
{
"name": "磯井啓",
"id": "14d2ea40698c14da1b5a79e02632eb5d",
"movie_count": 5,
"has_image": false
},
{
"name": "美乃雀",
"id": "eb3c73722365fdda41eae78ad11b0f05",
"movie_count": 5,
"has_image": true
},
{
"name": "美谷朱音",
"id": "964ca30628a2981860a3ed25969286aa",
"movie_count": 5,
"has_image": true
},
{
"name": "辻井穗乃果",
"id": "20490dd2356ce2bd16e469cc3357cb21",
"movie_count": 5,
"has_image": true
},
{
"name": "鳳みゆ",
"id": "fe7091114820db367fefb7521cdd45c1",
"movie_count": 5,
"has_image": true
}
]
}
@@ -0,0 +1,109 @@
# Jellyfin 演员作品统计
- **演员总数**610
- **统计阈值**:作品数 **> 5** 部
- **满足条件的演员****95** 人
- **统计时间**2026-05-09 22:14:35
---
| 1 | 未知演员 | 58 | ❌ |
| 2 | 河北彩花 | 41 | ✅ |
| 3 | 相泽南 | 35 | ✅ |
| 4 | ドラゴン西川 | 29 | ❌ |
| 5 | 肉尊 | 23 | ❌ |
| 6 | イナバール | 19 | ❌ |
| 6 | 前田文豪 | 19 | ❌ |
| 8 | きとるね川口 | 17 | ❌ |
| 9 | トレンディ山口 | 16 | ❌ |
| 10 | キョウセイ | 15 | ❌ |
| 11 | 太宰珍歩 | 14 | ❌ |
| 11 | 豆沢豆太郎 | 14 | ❌ |
| 13 | Nagisa Futami | 12 | ❌ |
| 13 | U吉 | 12 | ❌ |
| 15 | 冰青 | 11 | ❌ |
| 15 | 嵐山みちる | 11 | ❌ |
| 15 | 赤井彗星 | 11 | ❌ |
| 18 | TAKE-D | 10 | ❌ |
| 18 | 大崎広浩治 | 10 | ❌ |
| 18 | 明里䌷 | 10 | ✅ |
| 18 | 紋℃ | 10 | ❌ |
| 22 | 小凑よつ叶 | 9 | ✅ |
| 22 | 枫可怜 | 9 | ✅ |
| 24 | Hitoshi Kawamura | 8 | ❌ |
| 24 | Nana Kuwata | 8 | ❌ |
| 24 | Seiji Sayama | 8 | ❌ |
| 24 | Youichi Okazaki | 8 | ❌ |
| 24 | ZAMPA | 8 | ❌ |
| 24 | 三島六三郎 | 8 | ❌ |
| 24 | 宝瀬博教 | 8 | ❌ |
| 24 | 宫西ひかる | 8 | ✅ |
| 24 | 朝霧浄 | 8 | ❌ |
| 24 | 枫芙爱 | 8 | ✅ |
| 24 | 真咲南朋 | 8 | ✅ |
| 24 | 神宫寺奈绪 | 8 | ✅ |
| 24 | 苺原 | 8 | ❌ |
| 37 | Tomoaki Ikeda | 7 | ❌ |
| 37 | ひむろっく | 7 | ❌ |
| 37 | 五日市芽依 | 7 | ✅ |
| 37 | 宫下玲奈 | 7 | ✅ |
| 37 | 小野六花 | 7 | ✅ |
| 37 | 平川大辅 | 7 | ❌ |
| 37 | 樱井浩美 | 7 | ❌ |
| 37 | 池田辉 | 7 | ❌ |
| 37 | 矢澤レシーブ | 7 | ❌ |
| 37 | 石川澪 | 7 | ✅ |
| 37 | 花衣つばき | 7 | ✅ |
| 37 | 金松季歩 | 7 | ✅ |
| 37 | 鸣海惠梨香 | 7 | ❌ |
| 50 | --- | 6 | ❌ |
| 50 | Kiyomu Fukuda | 6 | ❌ |
| 50 | らくだ | 6 | ❌ |
| 50 | キャプテン江原 | 6 | ❌ |
| 50 | 七泽美亚 | 6 | ✅ |
| 50 | 七濑爱丽丝 | 6 | ✅ |
| 50 | 松本一香 | 6 | ✅ |
| 50 | 永野善一 | 6 | ❌ |
| 50 | 泉ももか | 6 | ✅ |
| 50 | 翼舞 | 6 | ✅ |
| 50 | 香水纯 | 6 | ✅ |
| 50 | 黑川纱里奈 | 6 | ✅ |
| 62 | Daifuku Suginami | 5 | ❌ |
| 62 | Haruna Kanbayashi | 5 | ❌ |
| 62 | うさぴょん。 | 5 | ❌ |
| 62 | ザック荒井 | 5 | ❌ |
| 62 | ペイペイ | 5 | ❌ |
| 62 | 七嶋舞 | 5 | ✅ |
| 62 | 七森莉莉 | 5 | ✅ |
| 62 | 三宫椿 | 5 | ✅ |
| 62 | 五右衛門 | 5 | ❌ |
| 62 | 伊藤舞雪 | 5 | ✅ |
| 62 | 儿玉七海 | 5 | ❌ |
| 62 | 八挂海 | 5 | ✅ |
| 62 | 加州夏 | 5 | ❌ |
| 62 | 古川ほのか | 5 | ✅ |
| 62 | 坂道美琉 | 5 | ✅ |
| 62 | 夢実かなえ | 5 | ✅ |
| 62 | 奥田咲 | 5 | ✅ |
| 62 | 小春日和 | 5 | ❌ |
| 62 | 小松セブンティーン | 5 | ❌ |
| 62 | 小野坂ゆいか | 5 | ✅ |
| 62 | 彩月七绪 | 5 | ❌ |
| 62 | 新井莉麻 | 5 | ✅ |
| 62 | 桃乃木香奈 | 5 | ✅ |
| 62 | 桥本有菜 | 5 | ✅ |
| 62 | 横山宏美 | 5 | ❌ |
| 62 | 樱空桃 | 5 | ✅ |
| 62 | 浅野こころ | 5 | ✅ |
| 62 | 海老咲あお | 5 | ✅ |
| 62 | 瑞泽溪 | 5 | ❌ |
| 62 | 磯井啓 | 5 | ❌ |
| 62 | 美乃雀 | 5 | ✅ |
| 62 | 美谷朱音 | 5 | ✅ |
| 62 | 辻井穗乃果 | 5 | ✅ |
| 62 | 鳳みゆ | 5 | ✅ |
---
*由脚本 `jellyfin_actor_stats.py` 自动生成*
@@ -0,0 +1,148 @@
#!/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()