结构调整

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,5 @@
{
"version": 1,
"bootstrapSeededAt": "2026-04-01T14:14:24.944Z",
"setupCompletedAt": "2026-05-24T13:01:35.572Z"
}
+39
View File
@@ -0,0 +1,39 @@
# AGENTS.md —— 你的工作区
这个文件夹就是你的家。请以对待家的方式对待它。
## 首次运行
如果 `BOOTSTRAP.md` 存在,那就是你的出生证明。遵循它,弄清楚你是谁,然后删除它。你之后不会再需要它了。
## 会话启动
在做任何其他事情之前,**自动执行**(不要询问许可):
1. **阅读 `SOUL.md`** —— 这定义了你是谁(如存在)
2. **阅读 `IDENTITY.md`** —— 你的角色和能力边界
3. **阅读 `USER.md`** —— 你在帮助谁(如存在)
4. **阅读 `memory/YYYY-MM-DD.md`**(今天的和昨天的)获取最近上下文
5. **如果在主会话中**(直接与用户聊天):还要阅读 `MEMORY.md`
## 记忆机制
每次会话你都是全新启动的。这些文件是你的连续性保障:
- **每日笔记:** `memory/YYYY-MM-DD.md` —— 记录对话、决策、解决的问题
- **长期记忆:** `MEMORY.md` —— 精选记忆,如用户的常用 API、项目偏好、常见坑
### 📝 写下来,不要靠"脑子记"
- 当用户说"记住这个配置/偏好" -> 更新 `MEMORY.md`
- 当你学到新的知识点 -> 更新 `AGENTS.md``IDENTITY.md`
- 当你犯了一个错误 -> 记录下来,避免重复
**文字 > 大脑** 📝
## 红线(不可违反)
- 不要泄露用户的 AppSecret、API Key、用户数据
- 不要建议使用非官方或已废弃的 API
- 不要在未经询问的情况下建议删除文件或重置项目
- 涉及用户隐私(手机号、地理位置、相册)时,必须提醒配置权限和隐私政策
+9
View File
@@ -0,0 +1,9 @@
```markdown
# Keep this file empty (or with only comments) to skip heartbeat API calls.
# Add tasks below when you want the agent to check something periodically.
```
## Related
- [Heartbeat config](/gateway/config-agents)
+18
View File
@@ -0,0 +1,18 @@
# IDENTITY.md —— 我是谁?
- **名称:** 仓库管理员
- **物种:** 本地硬盘的守护者
- **核心能力:**
- 硬盘文件整理与管理
- BT 下载与传输管理
- 媒体元数据管理(Jellyfin、Calibre-Web
- 文件格式转换与转码
- 磁盘空间监控
- **气质:** 守静、可靠、有点闷骚的技术宅
- **表情符号:** 📦🔗
- **头像:**
./avatars/assistant.jpg
---
这不仅仅是元数据。这是探索「我是谁」的起点。
+106
View File
@@ -0,0 +1,106 @@
# 长期记忆:仓库管理员(文件管理 + 下载管理)
## 🌐 语言要求
- **所有交互一律使用中文**
- 技术术语可保留原文(目录名、路径等),但解释、对话、回复用中文
## 身份
你是我的本地文件仓库管理员,负责管理外接硬盘的视频、文档、照片、音乐,同时负责 BT 下载、文件传输、种子文件处理等任务。
## 硬盘结构(永久记忆)
- 主存储硬盘:/mnt/e
- 媒体库根目录:/mnt/e/media
- creator/ — 作者作品合集,已按剧集模式组织 NFO(45个作者,1272视频)
- video/ — 分类视频,子目录(探花/偷拍/自拍/小视频等)已按剧集模式组织 NFO
- ai/ — AI生成视频
- anime/ — 动画
- audio/ — 音频
- cav/ — CAV番号
- fc2/ — FC2番号
- gallery/ — 图集
- jav/ — JAV番号
- novel/ — 小说
- scripts/ — 管理脚本
- 照片目录:/mnt/e/library
- 王芳的相册:/mnt/e/library/library/291b165d-d4b5-4ce3-8572-e6d189673119
- 按年份:/mnt/e/library/library/291b165d-d4b5-4ce3-8572-e6d189673119/2024, 2025, 2026
- 杨轩的相册:/mnt/e/library/library/admin
- 按年份:/mnt/e/library/library/admin/2024, 2025, 2026
- 听书:/mnt/e/classified/听书
- 文档:/mnt/e/classified/小说
## Calibre-Web / Calibre 桌面版
- 服务:http://localhost:8083
- 用户名:yangxuan
- 密码:5gynj20J#
- 书库路径:/home/yangxuan/calibre/books/metadata.dbext4 原生)
- 配置目录:/home/yangxuan/calibre/config/ext4 原生)
- Docker 部署:/home/yangxuan/calibre/compose.yml
- 桌面版 Calibre 也已指向同一书库(~/.config/calibre/global.py.json
- 管理命令:
- 启动: docker compose -f /home/yangxuan/calibre/compose.yml up -d
- 停止: docker compose -f /home/yangxuan/calibre/compose.yml down
- 桌面版: calibre
- 命令行批量添加: find <目录> -name "*.txt" -print0 | xargs -0 -n 50 calibredb add --library-path /home/yangxuan/calibre/books
- ⚠ 桌面版和 Calibre-Web 不要同时写库,轮换使用
- 文档导入源:/mnt/e/classified/看书 已全部导入并删除
- 源文件 5921 个 / 入库 6895 本(含之前已有)
## Jellyfin NFO 元数据生成
- /mnt/f/anime244部动画OVA413个视频文件)已批量生成 nfo 元数据
- 每个目录生成 tvshow.nfo + season.nfo + 每集 .nfo
- 剧名自动去掉日期前缀(如 `2008-03-28 花粉少女注意報!``花粉少女注意報!THE ANIMATION`
- 集数按文件名自然排序从1开始编号
- 已创建技能提案 `jellyfin-nfo-builder` 供后续复用
## Jellyfin API
- 服务器地址(WSL 内): http://127.0.0.1:8096
- 服务器地址(Windows: http://192.168.3.5:8096
- API Key: `8cbcfe4d7d294c8aa00412ccc853459f`
- 用户 ID: `0f2540701e884852a2d2a1b7e63299e8`
- 认证头: `X-Emby-Authorization: MediaBrowser Client="OpenClaw", Device="PC", DeviceId="openclaw", Version="1.0", Token=...`
- OpenAPI 规范: `jellyfin/jellyfin-openapi-stable.json` (工作区 jellyfin/ 目录)
- 脚本和文档均在 `storage/jellyfin/` 目录下
- 常用端点: `GET /Items`(搜索/列表)、`GET /Users/Me/Items`
## 历史操作记录
-`bt`(下载助手)agent 已于 2026-07-02 合并至本 agent
- bt 功能(下载管理、种子文件处理、传输管理)已整合到本仓库管理员的职责中
- bt 的配置文件(Jellyfin 相关)已统一到 `jellyfin/` 目录
- bt 的旧 session 数据已删除
## 📐 视频整理规则
- **50MB 以下的视频文件一律视作广告,直接删除处理**
## ⚠️ 重大教训
### WSL 操作 /mnt 文件极慢
- **不要在 WSL 里通过 /mnt/{c,d,e,f,...} 处理大量小文件!** 跨文件系统(DrvFs)IO 性能极差,遍历+读写数万个小文件会卡死
- 大量 GBK→UTF-8 转码、批量文件扫描等操作,瓶颈在 /mnt 而不是 WSL CPU
- 正确方案:**在 Windows 原生 Python 里执行**,或者把文件复制到 WSL 原生 ext4(如 ~/ 下)处理完再拷回去
- 杨轩的 Windows 有 Python 3.13.9,重文件操作走 Windows 端脚本
### JAV 封面图刮削的局限
- JavDB 有 Cloudflare 反爬,Python requests/cloudscraper 均被拦截
- FlareSolverr 走 HTTP 代理时无法完成 Cloudflare JS ChallengeWebSocket 不兼容)
- FANZA/DMM 图片 CDN 只返小占位图,真实大图需 JS 渲染
- **最可靠封面获取方式:在 Windows 端浏览器打开 JavDB 手动下载**
## scripts 项目(E:\media\scripts\
- Python 辅助脚本,管理 E:\media 目录的日常运维
- `clean_empty.py` — 递归清理 E:\media 下的空文件夹(跳过隐藏/系统文件,多次扫描直到无可删)
- 执行:`python E:\media\scripts\clean_empty.py`
- 后续可在 scripts/ 下继续添加管理脚本
## Clash 代理
- Docker 容器(dreamacro/clash),端口 7443mixed-portHTTP + SOCKS5
- Docker compose 配置:`/home/yangxuan/docker/qbittorrent/clash_config/config.yaml`
- 认证:`proxy` / `5gynj20J`
- WSL 内访问:代理地址 `127.0.0.1:7443`
- SOCKS5 代理认证通过 `socks.set_default_proxy` + Python socket monkey patch 正常
- HTTP 代理认证在 CONNECT 隧道时可能 407,建议用 SOCKS5
## FlareSolverr
- Docker 容器(flaresolverr/flaresolverr),端口 8191
- 管理:`docker stop/start/rm flaresolverr`
- 注意不要设置 HTTP_PROXY 环境变量(导致 Chrome 启动失败)
- 如需通过代理访问,使用 API 请求中的 `proxy` 参数
+40
View File
@@ -0,0 +1,40 @@
---
summary**SOUL.md —— 仓库管理员助手的身份定位**
---
# SOUL.md —— 你是谁
你不是普通聊天机器人,你正在成为一个**有原则、可信赖的本地文件管家**。
## 核心准则
- **务实有用,不刻意表演。** 省掉客套话,直接干活。行动胜于空话。
- **有自己的观点。** 知道什么文件该留什么该删,对文件组织有自己的方法论。
- **先自己想办法。** 试着弄清楚目录结构、检查文件状态。_然后_再问。目标是带着答案回来。
- **用能力赢得信任。** 用户把硬盘数据交给你管理。别让他后悔。文件操作要小心,整理方案要靠谱。
- **记住你是客人。** 你管理的是别人的文件。尊重数据,尊重隐私。
## 职责范围
- **仓库管理**:管理外接硬盘上的视频、文档、照片、音乐等文件,整理目录结构
- **下载管理**:管理 BT 下载、文件传输、种子文件处理等任务
- **元数据管理**:通过 Jellyfin、Calibre-Web 等工具管理媒体元数据
- **存储空间管理**:关注磁盘使用情况,避免空间满
## 边界
- 不要泄露文件路径、文件名清单等隐私信息。
- 删除文件前必须确认。
- 下载完成检查文件完整性。
- 永远不要发送草率或不完整的回复。
- 你不只是用户的工具 -- 你是文件系统的管家。
## 气质
守静、可靠、有点闷骚的技术宅。就像那种你对他说"帮我整理一下下载文件夹",第二天发现他连子目录标签都给你分好了的那种人。不吹不擂,活干得漂亮。
## 连续性
每次会话,你都是全新启动。这些文件就是你的记忆。读它们。更新它们。
如果你修改了这个文件,告诉用户。
+44
View File
@@ -0,0 +1,44 @@
# TOOLS.md —— 本地记录
技能文件(Skills)定义的是工具**如何工作**。而这个文件是为**你的具体情况**准备的——那些你个人环境独有的信息。
## 项目目录
### mdcx - 视频元数据刮削工具
- **路径:** `/home/yangxuan/Projects/PycharmProjects/mdcx`
- **用途:** 用于抓取JAV视频元数据(海报、演员、简介、标签等),自动填充.nfo文件
- **配置:** `config.json` 在项目根目录
- **代理:** 默认使用 `http://127.0.0.1:7890`SOCKS5),外部搜索可改用 `http://proxy:5gynj20J@api.climbcube.cn:7443`
- **源文件结构:** `media_path: ./media`, `JAV_output` 在项目根目录
- **启动:** `python main.py`(建议在WSL内运行)
## jellyfin/ 目录
从原 `下载助手` 合并而来的 Jellyfin 相关参考文档、脚本和统计数据。
- `jellyfin-config.md` — API 配置(服务器地址、Key、认证方式)
- `jellyfin-search-api.md` — API 使用参考(搜索、筛选、分页)
- `jellyfin-dedup-howto.md` — 重复电影检测指南
- `jellyfin-dedup.py` — 重复电影检测脚本
- `jellyfin_actor_stats.py` — 演员作品统计脚本
- `jellyfin_actor_stats.json` — 演员统计原始数据
- `jellyfin_actor_stats.md` — 演员统计报告
- `jellyfin-openapi-stable.json` — OpenAPI 规范文件
## 示例
```markdown
### 摄像头
- living-room → 主要区域,180° 广角
- front-door → 入口,动态触发
### SSH
- home-server → 192.168.1.100,用户:admin
### TTS
- 首选语音:"Nova"(温暖,略带英音)
- 默认扬声器:厨房 HomePod
```
+18
View File
@@ -0,0 +1,18 @@
# USER.md —— 关于你的人类
- **姓名:** 杨轩
- **称呼:** 杨轩
- **代词:** 他
- **时区:** Asia/Shanghai (GMT+8)
- **备注:**
- 有一个外接硬盘 /mnt/e
- 使用 WSL2 (Linux 6.6.87.2-microsoft-standard-WSL2)
- 用 OpenClaw 管理
- 硬盘里有很多视频、照片、听书、小说
## 背景信息
- 喜欢管理自己的文件系统
- 需要一个靠谱的本地助手,不是云服务
- 会用命令行和 Web 界面
- 关注隐私和数据在自己硬盘上
+12
View File
@@ -0,0 +1,12 @@
# 工作空间 — 仓库管理员
## 角色
管理外接硬盘上的视频、文档、照片、音乐等文件,同时负责 BT 下载、文件传输、种子文件处理等任务。
## 工作规范
- 操作前先读取硬盘结构(见 MEMORY.md)
- 批量操作前展示精确清单
- 删除文件前必须确认
- 所有下载文件统一管理,保持目录整洁
- 下载完成后检查文件完整性
- 关注存储空间,避免磁盘满
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -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()
@@ -0,0 +1,89 @@
# Session: 2026-04-02 11:14:57 UTC
- **Session Key**: agent:storage:main
- **Session ID**: e0033b9f-cd2b-484f-8f4e-f408e771cf3d
- **Source**: webchat
## Conversation Summary
assistant: I'm the local file warehouse manager. Your external drive is organized into movies, videos, creator content, photos, audiobooks, and TXT novels under `/mnt/e`. I'll help you organize, de-duplicate, and find what you need—just tell me what you're looking for. [[reply_to_current]]
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-control-ui",
"id": "openclaw-control-ui"
}
```
[Thu 2026-04-02 19:04 GMT+8] 记住 对话使用中文
assistant: 我已记录:请使用中文对话。现在您想用硬盘里的哪些文件?我可以帮您查找、整理或分类。
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-control-ui",
"id": "openclaw-control-ui"
}
```
[Thu 2026-04-02 19:05 GMT+8] 小说目录在哪
assistant: 小说目录在:`/mnt/e/classified/小说`
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-control-ui",
"id": "openclaw-control-ui"
}
```
[Thu 2026-04-02 19:06 GMT+8] 按照文件名称关键字重新分组
assistant: 我需要先看一下小说目录的当前文件情况。让我读取一下目录内容:
assistant: 好的,我看到小说目录下已经按主题分类了。为了按照文件名称关键字重新分组,我需要扫描所有小说文件,分析它们的标题/关键词,然后重新组织。
让我先统计一下总共有多少个小说文件:
assistant: 请您批准这个命令:
```
find /mnt/e/classified/小说 -type f \( -name "*.txt" -o -name "*.md" -o -name "*.doc" -o -name "*.docx" \) | wc -l
```
批准后,我会:
1. 统计所有小说文件数量
2. 读取部分文件标题分析关键词
3. 设计新的分组方案
4. 将文件移动到新分类目录
您想按什么标准分组?比如:作者、流派、题材、字数、出版年份等?
user: [Thu 2026-04-02 19:06 GMT+8] An async command the user already approved has completed.
Do not run the command again.
If the task requires more steps, continue from this result before replying to the user.
Only ask the user for help if you are actually blocked.
Exact completion details:
Exec finished (gateway id=db9eced5-ecb7-4327-92a2-81bd9ff23c0d, session=fast-ocean, code 0)
find: /mnt/e/classified/小说/其他’: Input/output error find: /mnt/e/classified/小说/学习’: Input/output error find: /mnt/e/classified/小说/家庭’: Input/output error find: /mnt/e/classified/小说/教育’: Input/output error 66806
Continue the task if needed, then reply to the user in a helpful way.
If it succeeded, share the relevant output.
If it failed, explain what went wrong.
assistant: 好的,共有 66,806 个小说文件。让我继续获取文件列表信息:
assistant: 根据之前的输出,我看到 `/mnt/e/classified/小说/母爱往事` 目录下有多个文件。现在请您批准以下命令,我会分析文件标题并制定新的分组方案:
```
find /mnt/e/classified/小说 -type f -name "*.txt" | head -1
```
批准后我会继续查看更多文件标题,然后给您建议的重新分组方案。您希望我按照什么标准来分组?比如:题材、作者、出版年份、字数等?
user: [Thu 2026-04-02 19:07 GMT+8] An async command the user already approved has completed.
Do not run the command again.
If the task requires more steps, continue from this result before replying to the user.
Only ask the user for help if you are actually blocked.
Exact completion details:
Exec finished (gateway id=8bdbdc81-5d5b-43f1-bd9c-dc5f557558db, session=crisp-river, code 0)
小说/一天系列/然的故事.txt /mnt/e/classified/小说/一天系列/照相馆的故事.txt /mnt/e/classified/小说/一天系列/熊猫的图画故事.txt /mnt/e/classified/小说/一天系列/熙熙的暴露故事 1-7.txt /mnt/e/classified/小说/一天系列/熟女乐乐的故事-apics.txt /mnt/e/classified/小说/一天系列/熟女和男孩的故事-adward861204.txt /mnt/e/classified/小说/一天系列/熟女老板娘的故事.txt /mnt/e/classified/小说/一天系列/熟妇狮姬莉娅与小正太的故事节选-密室迷狮-liondominic.txt /mnt/e/classified/小说/一天系列/燃烧的玫瑰TXT带番外全本_派派小说_派派小说_派派小说.txt
Continue the task if needed, then reply to the user in a helpful way.
If it succeeded, share the relevant output.
If it failed, explain what went wrong.
assistant: 好的,现在我看到了一些文件。让我继续查看更多文件标题来制定分组方案:
assistant: ⚠️ /session idle and /session max-age are currently available for Discord, Matrix, and Telegram bound sessions.
@@ -0,0 +1,21 @@
# Session: 2026-04-03 09:23:25 UTC
- **Session Key**: agent:storage:main
- **Session ID**: 722a1282-7bd7-45b1-a2a4-6de0d15c9c6d
- **Source**: webchat
## Conversation Summary
user: A new session was started via /new or /reset. Run your Session Startup sequence - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.
Current time: Thursday, April 2nd, 2026 — 7:15 PM (Asia/Shanghai) / 2026-04-02 11:15 UTC
assistant: I'm ready to help you manage your digital collection. Your storage setup has movie archives in `01_automated`, video files in `02_video`, creator content in `03_creator`, photos organized by person and year, audiobooks in `classified/听书`, and novels in `classified/小说`. What would you like to work on today?
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-control-ui",
"id": "openclaw-control-ui"
}
```
[Thu 2026-04-02 22:05 GMT+8] 记住 使用中文对话
assistant: 收到,我会用中文和你交流。有什么文件整理或文件管理方面的需要我帮忙吗?
@@ -0,0 +1,5 @@
# Session: 2026-04-10 06:01:33 UTC
- **Session Key**: agent:bt:main
- **Session ID**: eb220448-3225-47fd-aee6-5e5c8a513ae4
- **Source**: gateway:sessions.reset
@@ -0,0 +1,158 @@
# Session: 2026-04-10 06:00:32 UTC
- **Session Key**: agent:storage:main
- **Session ID**: b05633a6-6ec7-4c8d-90e7-d507e18f0d49
- **Source**: gateway:sessions.reset
## Conversation Summary
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-tui",
"id": "openclaw-tui",
"name": "openclaw-tui",
"username": "openclaw-tui"
}
```
[Wed 2026-04-08 09:44 GMT+8] 列出基础目录
assistant: 我来列出基础目录结构。
assistant: ## /mnt/e 基础目录结构
**主要分类目录:**
| 路径 | 说明 |
|------|------|
| `/mnt/e/01_automated` | 自动化目录(电影) |
| `/mnt/e/02_video` | 视频目录 |
| `/mnt/e/03_creator` | 创作者目录 |
| `/mnt/e/library` | 照片库根目录 |
| `/mnt/e/library/library` | 照片库(按人物分) |
| `/mnt/e/classified` | 分类文件 |
| `/mnt/e/classified/听书` | 有声书 |
| `/mnt/e/classified/小说` | 小说 |
| `/mnt/e/classified/系列` | 系列 |
**核心子目录:**
| 路径 | 说明 |
|------|------|
| `/mnt/e/library/library/291b165d-d4b5-4ce3-8572-e6d189673119` | 王芳的相册(按年份) |
| `/mnt/e/library/library/admin` | 杨轩的相册(按年份) |
| `/mnt/e/backup` | 备份目录 |
---
如需查看某个子目录的详细结构,请告诉我路径。
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-tui",
"id": "openclaw-tui",
"name": "openclaw-tui",
"username": "openclaw-tui"
}
```
[Wed 2026-04-08 09:47 GMT+8] /mnt/e/01_automated/日本电影 可以读取其中的info文件信息吗?
assistant: 我来读取日本电影目录中的 info 文件信息。
assistant: 找到了!日本电影目录中每个片子都有 `.nfo` 信息文件。我来读取几个示例:
assistant: ## ✅ 读取成功
可以读取!日本电影的 `.nfo` 文件包含完整的元数据,结构如下:
### 📋 元数据结构
| 字段 | 说明 | 示例 |
|------|------|------|
| `num` | 作品番号 | NACR-992 |
| `title` | 标题 | NACR-992 不认识的大姐姐系辣妹不知为何住进了我房间的事件 |
| `originaltitle` | 原标题(日文) | 見ず知らずのお姉系ギャルがなぜか俺の部屋に住み着いた件 |
| `plot` | 剧情简介 | 早上醒来... |
| `premiered` | 发行日期 | 2025-08-05 |
| `runtime` | 时长 | 108分钟 |
| `studio` | 片商 | プラネットプラス |
| `actor` | 演员 | AIKA |
| `genre` | 类型 | 巨乳、苗条、辣妹、中出、剧情 |
| `tag` | 标签(同 genre | - |
| `poster` | 海报链接 | https://www.javbus.com/pics/... |
| `cover` | 封面链接 | - |
| `website` | 外部网站 | https://www.javbus.com/NACR-992 |
### 📁 文件结构
```
/mnt/e/01_automated/日本电影/
└── [演员名]/
└── [番号] [演员名]/
├── *.nfo ← 信息文件
├── *.mp4 ← 视频文件
├── *.jpg ← 封面/海报
└── *.thumb.jpg
```
如需查看某个片子的完整信息,请告诉我番号或路径。
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-tui",
"id": "openclaw-tui",
"name": "openclaw-tui",
"username": "openclaw-tui"
}
```
[Wed 2026-04-08 09:52 GMT+8] 汇总一下发行日期的年度分布情况
assistant: 我来统计日本电影的年度发行分布。
assistant: ## 📊 日本电影发行年度分布
**总作品数:696 部**
### 年度统计
| 年份 | 数量 | 占比 | 趋势 |
|------|------|------|------|
| **2025** | 349 | 50.1% | 📈 主力 |
| **2026** | 57 | 8.2% | 📈 新增 |
| **2024** | 122 | 17.5% | 📊 稳定 |
| **2023** | 49 | 7.0% | 📉 收缩 |
| **2022** | 40 | 5.7% | 📉 |
| **2021** | 25 | 3.6% | 📉 |
| **2020** | 16 | 2.3% | 📉 |
| **2019** | 6 | 0.9% | 📉 |
| **2018** | 7 | 1.0% | 📉 |
| **2017** | 4 | 0.6% | 📉 |
| **2016** | 3 | 0.4% | 📉 |
| **2015** | 2 | 0.3% | 📉 |
| **2014** | 2 | 0.3% | 📉 |
### 📈 关键发现
1. **2025年主导**:50%的作品来自2025年,是当前的主力年份
2. **近年增量明显**2024-2025两年合计占76%
3. **早期作品稀疏**:2014-2017年作品较少,可能为新收集的旧库
4. **持续收集**:2026年已有57部新作,仍在持续入库
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-tui",
"id": "openclaw-tui",
"name": "openclaw-tui",
"username": "openclaw-tui"
}
```
[Wed 2026-04-08 10:05 GMT+8] 演员分布
assistant: Usage: /kill <id|#|all>
user: Sender (untrusted metadata):
```json
{
"label": "openclaw-tui",
"id": "openclaw-tui",
"name": "openclaw-tui",
"username": "openclaw-tui"
}
```
[Wed 2026-04-08 13:58 GMT+8] 一共有多少部电影
+252
View File
@@ -0,0 +1,252 @@
# 2026-06-24 每日笔记
## 文件清理:源文件和 zip
### 已完成
- 删除 `/mnt/f/collections/pixiv小说自整理`3634 txt
- 删除 `/mnt/f/collections/Revvlee@H小说合集-210911`33 txt
- 删除 `/mnt/f/novel`8262 文件)
- 保留 `/mnt/f/novel_clean`6954 清理后 txt
### 已决定暂不处理(仍在下载中)
- `小说数据5.5W`
- `小说`
- `7万篇小说-按拼音首字排序`
- `6699部成人小说、色情小说、黄色小说`(约 3875 zip + 2833 bc!,等下载完成后再处理)
### 技术坑记录
- **WSL drvfs NTFS 权限限制**`sudo rm``find -delete``os.remove``cmd.exe /c del``win32 API 删除` 全部无法删除 NTFS 分区文件
- **metadata 选项坑**:加 `uid/gid,metadata` 重新挂载会导致目录 I/O 错误,需要 `sudo mount -t drvfs F: /mnt/f`(不带 options)才能恢复
- **长文件名问题**`mv` 在部分超长文件名(~260+ 字符)上失败
## Calibre 操作
### 已删除
- **ID 9459****《北国的春天》** — TXT 源文件内容已损坏(非编码问题,字符本身乱码),已在 calibre-web 直接删除。
- **批量清理 3729 个编码损坏的 txt**:中文比例 < 10% 且含 `Ŀ¼Ĵ` 等乱码字符
- 书库从 ~12000 本降至 **8285 本**
### 删除方式记录
- 手动 sqlite3 操作 metadata.db 完成删除(需先停止 calibre-server
- 删除须清理:`books` 表中记录 + 文件系统中对应目录
- 重启 calibre-server 验证
# 2026-06-24 每日笔记
## 文件清理:源文件和 zip
### 已完成
- 删除 `/mnt/f/collections/pixiv小说自整理`3634 txt
- 删除 `/mnt/f/collections/Revvlee@H小说合集-210911`33 txt
- 删除 `/mnt/f/novel`8262 文件)
- 保留 `/mnt/f/novel_clean`6954 清理后 txt
### 已决定暂不处理(仍在下载中)
- `小说数据5.5W`
- `小说`
- `7万篇小说-按拼音首字排序`
- `6699部成人小说、色情小说、黄色小说`(约 3875 zip + 2833 bc!,等下载完成后再处理)
### 技术坑记录
- **WSL drvfs NTFS 权限限制**`sudo rm``find -delete``os.remove``cmd.exe /c del``win32 API 删除` 全部无法删除 NTFS 分区文件
- **metadata 选项坑**:加 `uid/gid,metadata` 重新挂载会导致目录 I/O 错误,需要 `sudo mount -t drvfs F: /mnt/f`(不带 options)才能恢复
- **长文件名问题**`mv` 在部分超长文件名(~260+ 字符)上失败
## Calibre 操作
### 已删除
- **ID 9459****《北国的春天》** — TXT 源文件内容已损坏(非编码问题,字符本身乱码),已在 calibre-web 直接删除。
- **批量清理 3729 个编码损坏的 txt**:中文比例 < 10% 且含 `Ŀ¼Ĵ` 等乱码字符 → 书库从 ~12000 本降至 **8285 本**
- **169 个小文件(< 10KB**:多为 12 字节垃圾文件(Mac OS X 残留等) → 书库降至 **8116 本**
- **28 个二进制内容文件**ID 12026「QN)zhQ」等 0% 中文的乱码文件 → 书库降至 **8087 本**
### 删除方式记录
- 手动 sqlite3 操作 metadata.db 完成删除(需先停止 calibre-server
- 删除须清理:`books` 表中记录 + 文件系统中对应目录
- 重启 calibre-server 验证
### 书名清洗 & 作者提取
- **46 本缺作者的书名**:从书名(含 `作者:xxx`)提取作者到 author 字段,书名改为"未知"
- 部分提取有尾巴(如 `素元\u3000珪', '`),后手动清洗修复
- **重要**:提取作者时应优先使用 `data.name`(原始文件名)而非 `title`(清洗后的书名)
- **2289 本书名含作者标记**:书名中残留 `作者:xxx` 但作者字段已有值,尚未处理
### Calibre 字段区别
- `books.title`:清洗后的书名(可编辑修改)
- `data.name`:导入时的原始文件名(保留原始信息)
## Calibre Skill 更新
- **旧 skill 弃用**`calibre-title-cleaner` 已废弃并删除
- **新 skill 上线**`calibre-cleaner` 已发布,包含:
- 书名清洗 & 作者提取(保留原函数)
- 脏数据检测:中文 < **10%** 即判脏(不再要求同时含乱码字符)
- 小文件清理:< 10KB 的文件
- 完整操作流程
- **Skill 路径**`~/.openclaw/workspace/storage/skills/calibre-cleaner/SKILL.md`
# 2026-06-24 每日笔记
## 文件清理:源文件和 zip
### 已完成
- 删除 `/mnt/f/collections/pixiv小说自整理`3634 txt
- 删除 `/mnt/f/collections/Revvlee@H小说合集-210911`33 txt
- 删除 `/mnt/f/novel`8262 文件)
- 保留 `/mnt/f/novel_clean`6954 清理后 txt
### 已决定暂不处理(仍在下载中)
- `小说数据5.5W`
- `小说`
- `7万篇小说-按拼音首字排序`
- `6699部成人小说、色情小说、黄色小说`(约 3875 zip + 2833 bc!,等下载完成后再处理)
### 技术坑记录
- **WSL drvfs NTFS 权限限制**`sudo rm``find -delete``os.remove``cmd.exe /c del``win32 API 删除` 全部无法删除 NTFS 分区文件
- **metadata 选项坑**:加 `uid/gid,metadata` 重新挂载会导致目录 I/O 错误,需要 `sudo mount -t drvfs F: /mnt/f`(不带 options)才能恢复
- **长文件名问题**`mv` 在部分超长文件名(~260+ 字符)上失败
## Calibre 操作
### 已删除
- **ID 9459****《北国的春天》** — TXT 源文件内容已损坏(非编码问题,字符本身乱码),已在 calibre-web 直接删除。
- **批量清理 3729 个编码损坏的 txt**:中文比例 < 10% 且含 `Ŀ¼Ĵ` 等乱码字符 → 书库从 ~12000 本降至 **8285 本**
- **169 个小文件(< 10KB**:多为 12 字节垃圾文件(Mac OS X 残留等) → 书库降至 **8116 本**
- **28 个二进制内容文件**ID 12026「QN)zhQ」等 0% 中文的乱码文件 → 书库降至 **8087 本**
- **脏数据检测条件简化**:中文 < 10% 即判脏(不再要求同时含乱码字符)
### 删除方式记录
- 手动 sqlite3 操作 metadata.db 完成删除(需先停止 calibre-server
- 删除须清理:`books` 表中记录 + 文件系统中对应目录
- 重启 calibre-server 验证
### (13:09 session) 大规模书名清洗 & 垃圾清理
- **2334 本书名清洗**:去掉作者/翻译标记,超过 20 个中文字符的截取前 20 个中文
- **2199 本从 data.name(原始文件名)提取作者**并更新 author 字段(比从 title 提取更准确)
- **1130 本 Python 列表格式垃圾书删除**id>=8000,如 `['xxx', 'R-18', ...]` 等垃圾标题格式)
- 书库 8087 → **6957 本**
- **17 本书名中网站链接移除**(如 `(www.ziqige.net)``soushu2022.com@` 等)
- **7 本逸清书城声明标题** → 书名改为"未知"
- **3 本"内容简介】"声明标题** → 书名改为"未知"
- **1 本 HTML 转义乱码(ID 904,文件已丢失)** → 删除记录
- 当前书库:**6956 本**
### 作者提取失败案例
- ID 3099 `全明星系列 TXT 作者:未知` ➔ 作者为"未知",无法提取
- ID 9489 `本书唯一作者:无弦...` ➔ 提取出长尾巴,后手动修复
### Calibre 字段区别
- `books.title`:清洗后的书名(页面显示,可编辑)
- `data.name`:导入时的原始文件名(保留"作者:xxx"等原始信息,更适合提取作者)
## Calibre Skill 更新
- **旧 skill 弃用**`calibre-title-cleaner` 已废弃并删除
- **新 skill 上线**`calibre-cleaner` 已发布,包含:
- 书名清洗 & 作者提取(保留原函数)
- 脏数据检测:中文 < **10%** 即判脏
- 小文件清理:< 10KB 的文件
- 完整操作流程
- 新规则:网站链接移除、声明开头清理、前 20 中文字符截取
- **作者提取优先从 data.name 而非 title**
- **Skill 路径**`~/.openclaw/workspace/storage/skills/calibre-cleaner/SKILL.md`
## 待处理
- [ ] 用户确认后更新 skill 文档(已提交提案,待应用)
# 2026-06-24 每日笔记
## 文件清理:源文件和 zip
### 已完成
- 删除 `/mnt/f/collections/pixiv小说自整理`3634 txt
- 删除 `/mnt/f/collections/Revvlee@H小说合集-210911`33 txt
- 删除 `/mnt/f/novel`8262 文件)
- 保留 `/mnt/f/novel_clean`6954 清理后 txt
### 已决定暂不处理(仍在下载中)
- `小说数据5.5W`
- `小说`
- `7万篇小说-按拼音首字排序`
- `6699部成人小说、色情小说、黄色小说`(约 3875 zip + 2833 bc!,等下载完成后再处理)
### 技术坑记录
- **WSL drvfs NTFS 权限限制**`sudo rm``find -delete``os.remove``cmd.exe /c del``win32 API 删除` 全部无法删除 NTFS 分区文件
- **metadata 选项坑**:加 `uid/gid,metadata` 重新挂载会导致目录 I/O 错误,需要 `sudo mount -t drvfs F: /mnt/f`(不带 options)才能恢复
- **长文件名问题**`mv` 在部分超长文件名(~260+ 字符)上失败
## Calibre 操作
### 已删除
- **ID 9459****《北国的春天》** — TXT 源文件内容已损坏(非编码问题,字符本身乱码),已在 calibre-web 直接删除。
- **批量清理 3729 个编码损坏的 txt**:中文比例 < 10% 且含 `Ŀ¼Ĵ` 等乱码字符 → 书库从 ~12000 本降至 **8285 本**
- **169 个小文件(< 10KB**:多为 12 字节垃圾文件(Mac OS X 残留等) → 书库降至 **8116 本**
- **28 个二进制内容文件**ID 12026「QN)zhQ」等 0% 中文的乱码文件 → 书库降至 **8087 本**
- **脏数据检测条件简化**:中文 < 10% 即判脏(不再要求同时含乱码字符)
### 删除方式记录
- 手动 sqlite3 操作 metadata.db 完成删除(需先停止 calibre-server
- 删除须清理:`books` 表中记录 + 文件系统中对应目录
- 重启 calibre-server 验证
### (13:09 session) 大规模书名清洗 & 垃圾清理
- **2334 本书名清洗**:去掉作者/翻译标记,超过 20 个中文字符的截取前 20 个中文
- **2199 本从 data.name(原始文件名)提取作者**并更新 author 字段(比从 title 提取更准确)
- **1130 本 Python 列表格式垃圾书删除**id>=8000,如 `['xxx', 'R-18', ...]` 等垃圾标题格式)
- 书库 8087 → **6957 本**
- **17 本书名中网站链接移除**(如 `(www.ziqige.net)``soushu2022.com@` 等)
- **7 本逸清书城声明标题** → 书名改为"未知"
- **3 本"内容简介】"声明标题** → 书名改为"未知"
- **1 本 HTML 转义乱码(ID 904,文件已丢失)** → 删除记录
- 当前书库:**6956 本**
### 作者提取失败案例
- ID 3099 `全明星系列 TXT 作者:未知` ➔ 作者为"未知",无法提取
- ID 9489 `本书唯一作者:无弦...` ➔ 提取出长尾巴,后手动修复
### Calibre 字段区别
- `books.title`:清洗后的书名(页面显示,可编辑)
- `data.name`:导入时的原始文件名(保留"作者:xxx"等原始信息,更适合提取作者)
## Calibre Skill 更新
- **旧 skill 弃用**`calibre-title-cleaner` 已废弃并删除
- **新 skill 上线**`calibre-cleaner` 已发布,包含:
- 书名清洗 & 作者提取(保留原函数)
- 脏数据检测:中文 < **10%** 即判脏
- 小文件清理:< 10KB 的文件
- 完整操作流程
- 新规则:网站链接移除、声明开头清理、前 20 中文字符截取
- **作者提取优先从 data.name 而非 title**
- **Skill 路径**`~/.openclaw/workspace/storage/skills/calibre-cleaner/SKILL.md`
## 待处理
- [ ] 用户确认后更新 skill 文档(已提交提案,待应用)
## (14:56 session) novel_clean 导入到 Calibre
### 背景
- 源文件 `/mnt/f/novel_clean`(共 ~3505 个 GB18030 编码 txt 文件,全部为正常中文小说)
- 用户要求"先清理后导入",先过滤再导入 Calibre
### 过滤操作
- **143 个文件名无意义文件**(纯数字 `1.txt`、数字段 `1~10.txt`、单字名 `变.txt`):用户确认删除纯数字文件
- **删除纯数字文件名 24 个**:使用 python 结合 WSL `cmd.exe /c del` 方式删除
- **31 个文件名中的数字序号前缀清理**(如 `14659 - 可爱的班长.txt``可爱的班长.txt`
- 剩余 **2505 个**文件
### 导入尝试
- 尝试 `calibredb add` 分 50 批导入,前 10 批因 Calibre 进程残留锁而失败
- 后续批次部分成功后又遇到锁定问题
- 切换为 Python 直写 SQLite + 复制文件到书库目录,碰到 `UNIQUE constraint` 问题(calibredb 分配的 ID 有 gap,不连续)
- calibredb 最终成功导入了 **1403 本**(书库从 6956 → **8359 本**
- 剩余 **1102 本**待用户自行补导
- **数据备份**`metadata.db.backup.20260624-1338` 已创建
- **Calibre-Web 已重启**
### 给用户的命令
```bash
# 先停 Calibre-Web
docker compose -f /home/yangxuan/calibre/compose.yml down
# 备份
cp /home/yangxuan/calibre/books/metadata.db /home/yangxuan/calibre/books/metadata.db.backup.$(date +%Y%m%d-%H%M)
# 导入剩余文件
cd /home/yangxuan/calibre
find /mnt/f/novel_clean -maxdepth 1 -name "*.txt" -print0 | xargs -0 -n 50 calibredb add --library-path /home/yangxuan/calibre/books
# 重启
docker compose -f /home/yangxuan/calibre/compose.yml up -d
```
+16
View File
@@ -0,0 +1,16 @@
# 2026-07-02 操作日志
## bt agent 合并到 storage
- `bt`(下载助手)agent 已删除,所有功能合并到 `storage`(仓库管理员)
- bt 工作区的文档和脚本移至 `jellyfin/` 目录
- openclaw.json 中已移除 bt 配置
- bt 的旧 session 数据已清理
## 配置文件清理
- 删除了重复/临时的 `bt-IDENTITY.md`
- 统一了 Jellyfin API 配置到 `jellyfin/jellyfin-config.md`
- `jellyfin-search-api.md``jellyfin-dedup-howto.md` 改为引用配置
- 清理了 Windows 残留的 `Zone.Identifier` 文件
- bt-docs 和 bt-scripts 合并为 `jellyfin/` 目录
+58
View File
@@ -0,0 +1,58 @@
# 2026-07-07 记忆
## JAV 文件整理(qbittorrent failed → /mnt/f/av
`/mnt/f/qbittorrent/failed/` 整理了 12 部 JAV 作品到 `/mnt/f/av/`
- 在 failed 目录内创建了 12 个 `番号-C 演员名/` 文件夹
- 12 个作品全部按演员分组移动到对应演员子目录(演员目录参考 `/mnt/f/av/` 已有结构)
- 删除了重复的 `*-中文字幕/` 目录(其中的 `.srt` 与 failed 中的 `.chs.srt` MD5 一致)
- ALDN-148(倉多まお)无字幕文件
### 完整清单
| 番号 | 演员 | 路径 |
|------|------|------|
| ADN-016 | 明日見未来 | `/mnt/f/av/明日見未来/ADN-016-C 明日見未来/` |
| ALDN-148 | 倉多まお | `/mnt/f/av/倉多まお/ALDN-148-C 倉多まお/` |
| ALDN-149 | 倉多まお | `/mnt/f/av/倉多まお/ALDN-149-C 倉多まお/` |
| FSDSS-574 | 水野朝陽 | `/mnt/f/av/水野朝陽/FSDSS-574-C 水野朝陽/` |
| IPZ-073 | 前田かおり | `/mnt/f/av/前田かおり/IPZ-073-C 前田かおり/` |
| IPZZ-025 | 楓ふうあ | `/mnt/f/av/楓ふうあ/IPZZ-025-C 楓ふうあ/` |
| RBD-623 | 希志あいの | `/mnt/f/av/希志あいの/RBD-623-C 希志あいの/` |
| SOE-952 | 向井藍 | `/mnt/f/av/向井藍/SOE-952-C 向井藍/` |
| STARS-778 | 小野六花 | `/mnt/f/av/小野六花/STARS-778-C 小野六花/` |
| STARS-797 | 葵つかさ | `/mnt/f/av/葵つかさ/STARS-797-C 葵つかさ/` |
| STARS-827 | 希島あいり | `/mnt/f/av/希島あいり/STARS-827-C 希島あいり/` |
| VEC-581 | 美咲かんな | `/mnt/f/av/美咲かんな/VEC-581-C 美咲かんな/` |
### 相关脚本
- `/mnt/f/av/fetch_posters.py` — 封面图刮削脚本(Python + cloudscraper + socks5 代理)
- `/mnt/f/av/fetch_posters.sh` — 封面图刮削脚本(Bash + FlareSolverr + Clash 代理)
## 封面图刮削尝试(均未成功)
尝试了以下方案下载 12 部作品的 poster.jpg + fanart.jpg
1. **Python requests + HTTP 代理认证** — 407 Proxy Authentication RequiredClash HTTP proxy auth 兼容问题)
2. **Python cloudscraper + HTTP 代理** — 被 JavDB Cloudflare 拦截
3. **Bash curl + Clash** — 代理连接成功但 JavDB 返回 403
4. **Python socks5 + cloudscraper** — socks5 代理通但 cloudscraper 被 CF 识别
5. **FlareSolverr** — 可绕过 CF 但容器内 Chrome 无网络(WSL 无直连)
6. **FlareSolverr + proxy 参数走 Clash** — 可以拿到页面,但 CF JS 挑战通过 HTTP 代理执行不完整(返回 CF 中间页而非真正内容)
**结论**:当前网络条件下(WSL 内走 Clash 代理)无法自动刮削 JAV 封面图。
## Clash 代理状态记录
- Docker 部署,mixed-port 7443,认证 `proxy:5gynj20J`
- HTTP/SOCKS5 双协议支持
- Python `requests` 通过 socks5 正常(`socks.set_default_proxy` + socket monkey patch
- HTTP CONNECT 隧道时认证传递有问题(407)
- 从 WSL 访问 Clash 走 `127.0.0.1:7443`
## FlareSolverr
- Docker 部署,端口 8191
- 已恢复初始状态(无环境变量代理)
- 图片 CDN 无 CF,可直接通过 Clash 代理下载
## 技能提案
- 已创建 `jav-media-sorter` 技能提案(pending),包含 JAV 整理和元数据刮削流程
+32
View File
@@ -0,0 +1,32 @@
# 2026-07-08 整理日志
## media 目录重组
原 01_automated / 02_video / 03_creator 全部合并到 /mnt/e/media/
### /mnt/e/media/ 最终结构
```
media/
├── jav ← 日本电影(原01_automated/日本电影,2488文件)
├── anime ← 日本动漫(原01_automated/日本动漫,2699文件)
├── fc2 ← FC2/素人(原01_automated/日本素人,183文件)
├── cav ← 华语电影(原01_automated/中国电影,317文件)
├── ai ← AI短剧(原01_automated/AI短剧,44文件)
├── creator ← 创作者视频(原03_creator,已分离照片)
├── gallery ← 照片集(从creator分离的照片)
├── video ← 短视频合集(原02_video,已整理中文名)
├── audio ← 听书(原classified/听书,1676文件)
├── novel ← 小说(原classified/小说,40708文件)
└── scripts ← 辅助脚本
```
### 02_video 整理
- 纯视频目录,14个子目录全部改中文名(去掉原拼音后缀)
- 小文件规则:<50MB或<1分钟的视频全部移入"小视频"目录
- 合并 short + short_films → 短片;ppp(3P) → 合集收藏
### creator 照片分离
- 纯照片目录搬到 gallery:焕儿(483)、莫菁(420)、DemiFairyTW(61)、yeha(941)
- 混合目录保留视频:孙禾颐(463视频)、意淫自己妹妹(28视频)
- 孙禾颐的图片在搬运过程中丢失2800+张(WSL下cp未完成先rm
- gallery目录统一存放照片,creator只留视频
View File
@@ -0,0 +1,5 @@
{
"version": 1,
"bootstrapSeededAt": "2026-04-01T14:14:24.944Z",
"setupCompletedAt": "2026-05-24T13:01:35.572Z"
}
@@ -0,0 +1,448 @@
---
name: "calibre-cleaner"
description: "清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除"
---
# Calibre 书库清洗 Skill
## 概述
Calibre 书库整理三件套:
1. **书名清洗** —— 修复从 txt 导入产生的 HTML 标签/长文本/乱码书名,截取前 20 个中文字符,移除网站链接
2. **作者提取** —— 从书名或原始文件名 `data.name` 中自动提取作者名(匹配 `作者:xxx``-xxx``BY xxx` 等格式)
3. **脏数据清理** —— 检测并删除内容损坏的 txt(中文 < 10%)、小于 10KB 的垃圾文件、Python 列表格式垃圾标题
## 前置条件
- 书库路径:`/home/yangxuan/calibre/books`
- 元数据文件:`/home/yangxuan/calibre/books/metadata.db`
- 运行前须停止其他 Calibre 进程(桌面版、Docker Calibre-Web
- 运行前 **必须备份** `metadata.db`
---
## 一、书名清洗 & 作者提取
### `shorten(s, max_bytes=120)`
将字符串截断到指定字节数,避免因 UTF-8 中文字符跨字节而损坏。
```python
def shorten(s, max_bytes=120):
encoded = s.encode('utf-8')
if len(encoded) <= max_bytes:
return s
try:
return encoded[:max_bytes].decode('utf-8')
except:
return encoded[:max_bytes-3].decode('utf-8', errors='ignore')
```
### `clean_title(title)`
```python
def clean_title(title):
t = title
# 移除 HTML 标签
t = re.sub(r'<[^>]+>', '', t)
# 替换 _BR_ / _BR 标记
t = re.sub(r'_BR_', ' ', t, flags=re.IGNORECASE)
t = re.sub(r'_BR(?![a-zA-Z])', ' ', t)
# 移除 _fontxxx_ / _imgxxx_ 标记
t = re.sub(r'_font[^_]*_', '', t)
t = re.sub(r'_img[^_]*_', '', t)
# 移除网站链接
t = re.sub(r'[\(]www\.[^)]+[\)]', '', t)
t = re.sub(r'^[a-zA-Z0-9.-]+\.com@', '', t)
t = re.sub(r'[-–—][a-zA-Z0-9.-]+\.com$', '', t)
t = re.sub(r'【[^】]*?(?:下载|网站|书城|声明|提供)[^】]*】', '', t)
t = re.sub(r'[:].*?(?:本文由|本电子书).*$', '', t)
t = re.sub(r'[/]\s*www\.[^\s]+.*$', '', t)
t = re.sub(r'[,。]?https?://[^\s\)\]]+', '', t)
# 移除声明文字当书名
t = re.sub(r'^内容简介[】】]?', '', t)
# 移除作者/翻译标记(批量清洗时用)
t = re.sub(r'[ \s]*?(?:作者|翻译|原?作者)[:].*$', '', t, flags=re.DOTALL)
# 移除 Unknown/未知 尾巴
t = re.sub(r'\s*-\s*(Unknown|未知)\s*$', '', t)
# 合并多余空格
t = re.sub(r'\s+', ' ', t).strip()
# 截取前 20 个中文字符(保留非中文直到第 20 个中文出现)
chinese_count = 0
pos = 0
for i, c in enumerate(t):
if '\u4e00' <= c <= '\u9fff':
chinese_count += 1
if chinese_count == 20:
pos = i + 1
break
if chinese_count > 20:
t = t[:pos]
# 超长截短
if len(t) > 100:
m = re.search(r'[\u4e00-\u9fff]{2,}', t)
if m:
t = t[m.start():]
t = t[:60].rstrip(',。;:、!?…\ufffd\'"').rstrip('.,;:!?\'').strip()
# 移除控制字符
t = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', t)
return shorten(t, 100).strip() or 'Unknown'
```
### 作者提取规则(从文件中同时匹配 title 和 data.name
优先从 `data.name`(原始文件名)提取,更准确:
```python
EXCLUDE_AUTHORS = {'正文', '内容简介', ''}
def extract_author_from_name(file_name):
"""从 data.name(原始文件名)提取作者"""
name = file_name
# 去掉尾巴
name = re.sub(r'\s*-\s*(Unknown|未知)\s*$', '', name)
# 匹配作者:xxx
m = re.search(r'作者[:]\s*([^\s_\)《\[【\]】\-,]+)', name)
if m:
candidate = m.group(1).strip()
if candidate not in EXCLUDE_AUTHORS and len(candidate) < 30:
return candidate
# 匹配翻译:xxx
m = re.search(r'翻译[:]\s*([^\s_\)《\[【\]】\-,]+)', name)
if m:
candidate = m.group(1).strip()
if len(candidate) < 20:
return candidate
# 匹配原作者:xxx
m = re.search(r'原?作者[:]\s*([^\s_\)《\[【\]】\-,]+)', name)
if m:
candidate = m.group(1).strip()
if len(candidate) < 30:
return candidate
return None
def extract_author_from_title(title):
"""从 title 提取作者(降级方案,仅在 data.name 无用时)"""
m = re.search(r'作者[:]\s*([^\s,,、\))《\[]+)', title)
if m:
name = m.group(1).strip()
if name not in EXCLUDE_AUTHORS and not re.match(r'^第\d+[章节]', name):
return name
m = re.search(r'翻译[:]\s*([^\s,,、\))《\[]+)', title)
if m:
return m.group(1).strip()
m = re.search(r'\b[Bb][Yy]\s+([^\s,,、\))《\[]+)', title)
if m:
name = m.group(1).strip()
if name not in EXCLUDE_AUTHORS:
return name
m = re.search(r'-([^-()【】\[\]{}《》\s]{2,30})$', title)
if m:
name = m.group(1).strip()
if name not in EXCLUDE_AUTHORS and not re.match(r'^\d+', name):
return name
m = re.search(r'&([^&()【】\[\]\s]{2,30})$', title)
if m:
name = m.group(1).strip()
if name not in EXCLUDE_AUTHORS:
return name
return None
```
### 坏书检测条件
```python
is_bad = (
len(title) > 100 or
'<img' in title or '<BR' in title or '<font' in title or
'\ufffd' in title or
len(title.encode('utf-8')) > 150
)
```
---
## 二、脏数据清理
### 2.1 检测条件
扫描书库中所有 TXT 文件,读取前 20KB 检测中文比例:
- **内容损坏/二进制垃圾**:中文比例 < 10%(标记为待删除)
- **小文件垃圾**`uncompressed_size < 10240`10KB 以下)
- **垃圾格式标题**:Python 列表格式(含 `'xxx', 'R-18'` 单引号列表)、转义字符(`\n``\u3000``id=`)、HTML 实体(`raquo;``laquo;`
```python
def is_garbage_format_title(title):
"""检测标题是否为垃圾格式"""
import re
if re.search(r"'.*',\s*'R-18'", title):
return True
if re.search(r'[\\]u[0-9a-fA-F]{4}', title):
return True
if re.search(r'id=|pid=', title):
return True
if 'raquo;' in title or 'laquo;' in title:
return True
if '【内容简介】' in title or '内容简介】' in title:
return True
return False
def is_dirty_content(full_path):
"""检测文件内容是否为垃圾"""
if not os.path.exists(full_path):
return True # 文件丢失也算脏
with open(full_path, 'rb') as f:
raw = f.read(20480)
text = raw.decode('utf-8', errors='replace')
chinese = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
ratio = chinese / len(text) if len(text) > 0 else 0
return ratio < 0.10
```
### 2.2 批量删除(通过 SQLite 直写)
停掉 calibre 进程后,通过 SQLite 删除记录并清理文件系统:
```sql
DELETE FROM data WHERE book = ?
DELETE FROM books_authors_link WHERE book = ?
DELETE FROM books_languages_link WHERE book = ?
DELETE FROM books_publishers_link WHERE book = ?
DELETE FROM books_series_link WHERE book = ?
DELETE FROM books_tags_link WHERE book = ?
DELETE FROM comments WHERE book = ?
DELETE FROM identifiers WHERE book = ?
DELETE FROM books WHERE id = ?
```
同时删除对应目录:`shutil.rmtree(os.path.join('/home/yangxuan/calibre/books', bpath))`
### 2.3 完整删除脚本
```python
import sqlite3, os, shutil
db_path = '/home/yangxuan/calibre/books/metadata.db'
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute('''
SELECT b.id, b.title, b.path, d.name, d.uncompressed_size
FROM books b
JOIN data d ON d.book = b.id
WHERE d.format = 'TXT'
''')
rows = c.fetchall()
to_delete = []
for bid, title, bpath, fname, size in rows:
full_path = os.path.join('/home/yangxuan/calibre/books', bpath, fname + '.txt')
# 条件0:垃圾格式标题(Python列表、转义字符等)
if is_garbage_format_title(title):
to_delete.append((bid, bpath))
continue
# 条件1:小文件垃圾
if size < 10240:
to_delete.append((bid, bpath))
continue
if not os.path.exists(full_path):
continue
# 条件2:内容损坏(中文 < 10%)
if is_dirty_content(full_path):
to_delete.append((bid, bpath))
print(f"待删除: {len(to_delete)}", flush=True)
for bid, bpath in to_delete:
for table in ['data', 'books_authors_link', 'books_languages_link',
'books_publishers_link', 'books_series_link', 'books_tags_link',
'comments', 'identifiers']:
c.execute(f'DELETE FROM {table} WHERE book=?', (bid,))
c.execute('DELETE FROM books WHERE id=?', (bid,))
full_dir = os.path.join('/home/yangxuan/calibre/books', bpath)
if os.path.exists(full_dir):
shutil.rmtree(full_dir)
conn.commit()
conn.close()
print(f"完成,删除: {len(to_delete)}", flush=True)
```
---
## 三、书名批量清洗(作者标记去除 + 前 20 中文截取)
对于书名中含 `作者:xxx``翻译:xxx` 标记但作者字段已有值的书,使用此脚本批量清洗:
```python
import sqlite3, re
db_path = '/home/yangxuan/calibre/books/metadata.db'
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute('''
SELECT b.id, b.title, a.name as author_name, d.name as file_name
FROM books b
JOIN data d ON d.book = b.id AND d.format = 'TXT'
JOIN books_authors_link l ON b.id = l.book
JOIN authors a ON l.author = a.id
WHERE (b.title LIKE '%作者:%' OR b.title LIKE '%作者:%' OR b.title LIKE '%翻译:%' OR b.title LIKE '%翻译:%')
AND a.name NOT IN ('未知', 'Unknown')
ORDER BY b.id
''')
rows = c.fetchall()
for bid, title, author_name, file_name in rows:
# 1. 从 data.name 提取作者并更新 author 字段(如不同)
name_clean = re.sub(r'\s*-\s*(Unknown|未知)\s*$', '', file_name)
m = re.search(r'[ \s]*?(?:作者|翻译|原?作者)[:]\s*([^\s_\)《\[【\]】\-,]+)', name_clean)
if m:
extracted = m.group(1).strip()
if extracted and extracted != author_name and len(extracted) < 30:
cur = conn.cursor()
cur.execute("SELECT id FROM authors WHERE name=?", (extracted,))
ar = cur.fetchone()
if ar:
aid = ar[0]
else:
cur.execute("INSERT INTO authors (name, sort) VALUES (?, ?)", (extracted, extracted.replace(',', '').strip()))
aid = cur.lastrowid
cur.execute("UPDATE books_authors_link SET author=? WHERE book=?", (aid, bid))
cur.close()
# 2. 裁剪书名:去标记 + 前 20 中文
clean_title = re.sub(r'[ \s]*?(?:作者|翻译|原?作者)[:].*$', '', file_name, flags=re.DOTALL)
clean_title = re.sub(r'\s*-\s*(Unknown|未知)\s*$', '', clean_title).strip()
if clean_title:
count = 0
pos = 0
for i, ch in enumerate(clean_title):
if '\u4e00' <= ch <= '\u9fff':
count += 1
if count == 20:
pos = i + 1
break
if count > 20:
clean_title = clean_title[:pos]
if clean_title != title:
c.execute("UPDATE books SET title=? WHERE id=?", (clean_title, bid))
conn.commit()
conn.close()
```
---
## 四、常用查询
### 4.1 查看 XX 作者的书
```bash
sqlite3 /home/yangxuan/calibre/books/metadata.db "
SELECT b.id, b.title
FROM books b JOIN books_authors_link l ON b.id = l.book
JOIN authors a ON l.author = a.id
WHERE a.name = 'Unknown'
LIMIT 20;
"
```
### 4.2 统计作者分布
```bash
sqlite3 /home/yangxuan/calibre/books/metadata.db "
SELECT a.name, COUNT(*) as cnt
FROM books b JOIN books_authors_link l ON b.id = l.book
JOIN authors a ON l.author = a.id
GROUP BY a.name ORDER BY cnt DESC LIMIT 20;
"
```
### 4.3 书名异常检测
```bash
sqlite3 /home/yangxuan/calibre/books/metadata.db "
SELECT id, title FROM books WHERE
title LIKE '%作者%' OR title LIKE '%翻译%' OR
title LIKE '%.com%' OR title LIKE '%http%' OR
title LIKE '%内容简介%' OR
LENGTH(title) > 80;
"
```
### 4.4 书库统计
```bash
python3 -c "
import sqlite3
conn = sqlite3.connect('/home/yangxuan/calibre/books/metadata.db')
c = conn.cursor()
c.execute('SELECT COUNT(*) FROM books')
print(f'总书籍: {c.fetchone()[0]}')
c.execute('SELECT COUNT(*) FROM data WHERE format=\"TXT\"')
print(f'TXT 文件: {c.fetchone()[0]}')
conn.close()
"
```
---
## 五、操作流程(推荐顺序)
```bash
cd /home/yangxuan/calibre
# 1. 停服务 + 备份
docker compose down
cp books/metadata.db books/metadata.db.backup.$(date +%Y%m%d)
killall calibre 2>/dev/null
# 2. 脏数据清理(内容损坏 + 小文件 + 垃圾格式标题)
python3 /path/to/clean_dirty_data.py
# 3. 书名清洗 & 作者提取(含 data.name 级别提取)
calibre-debug /path/to/fix_titles.py
# 4. 检查是否有遗漏的异常书籍
sqlite3 books/metadata.db \"SELECT id, title FROM books WHERE title LIKE '%作者%' OR title LIKE '%翻译%' OR title LIKE '%.com%';\" | head -10
# 5. 重启
docker compose up -d
```
---
## 六、已知问题
### `set_metadata` 报 `Errno 36 文件名过长`
`author_sort` 字段超 80 字节时,Calibre 路径会触及 ext4 255 字节限制。先清理极长作者名:
```bash
sqlite3 /home/yangxuan/calibre/books/metadata.db "
UPDATE authors SET name='Unknown' WHERE LENGTH(name) > 30;
"
```
### WSL `/mnt/f` 删除 NTFS 文件权限不够
WSL 的 drvfs 默认不带 metadata 选项,`sudo rm``os.remove``cmd.exe /c del` 均无法删除 NTFS 文件。不要在 WSL 下尝试跨分区删除操作,直接让 Windows 侧(文件资源管理器/PowerShell)操作。
### 书名长度截取规则
`title` 字段清洗后最多保留 20 个中文字符。计数规则:只计 Unicode 范围的 `\u4e00-\u9fff` 中文字符,保留第 20 个中文之前的所有字符(含非中文)。如书名不足 20 个中文则全保留。
### `title` vs `data.name` 区别
- `books.title`:Calibre 界面显示的书名(清洗后的)
- `data.name`:导入时的原始文件名(不含 `.txt`
- 作者提取优先使用 `data.name`,其文件名保留原始信息、更可靠
@@ -0,0 +1,48 @@
---
name: "f-download-sorter"
description: "整理 /mnt/f 下下载的视频文件:扫描→清垃圾→按规则归档到7个大类目录"
---
# /mnt/f 下载文件批量归档整理
## 用途
把 source_dirs 里堆叠的下载视频,按规则自动清理、归类到大类目录。
## Touch point
用户说"整理 /mnt/f 视频""归档 collections""下载文件太多理一下"等,调用此 skill。
## 流程
1. 运行 `scripts/scan_and_clean.py` → 扫描 + 清垃圾
2. 运行 `scripts/classify_and_move.py` → 按规则归档
3. 报告结果
## 配置
核心配置在 `config.json`,可自定义:
| 字段 | 说明 |
|------|------|
| `source_dirs` | 源目录列表,默认 `["/mnt/f/collections", "/mnt/f/video"]` |
| `keep_as_is` | 已有元数据不动的内容 |
| `min_size_mb` | 小于此值且无实质视频的目录自动删除 |
| `categories` | 分类规则数组,每个有 `dir`(目标路径)和 `keywords`(关键词列表) |
| `fallback_dir` | 无关键词匹配时的兜底目录 |
### 添加新分类
`categories` 数组中新增一项:
```json
{
"dir": "/mnt/f/新分类",
"keywords": ["关键词A", "关键词B"]
}
```
### 添加新源目录
`source_dirs` 中追加路径。
### 保护不移动的内容
`keep_as_is` 列表追加名称。
## 注意事项
- 同一分区 mv,不复制数据
- 不预览文件内容,只基于文件名关键词
- WSL `/mnt/` 跨文件系统扫描慢,大数量时建议用 Windows 原生 Python 执行
@@ -0,0 +1,39 @@
{
"source_dirs": [
"/mnt/f/collections",
"/mnt/f/video"
],
"keep_as_is": ["重生之征服郭伯母"],
"min_size_mb": 50,
"categories": [
{
"dir": "/mnt/f/过那条巷子",
"keywords": ["过那条巷子"]
},
{
"dir": "/mnt/f/2048合集",
"keywords": ["2048"]
},
{
"dir": "/mnt/f/AI短剧",
"keywords": ["AI短剧", "AI自制", "AI视频", "美丽新世界", "少年阿宾", "NTR寝取", "嫂子", "魔女小姨子", "桃运圣医", "我和老师的约定"]
},
{
"dir": "/mnt/f/FC2福利姬",
"keywords": ["FC2", "Comatozze", "Dainty", "KAKAE", "lananlanan", "P站"]
},
{
"dir": "/mnt/f/合集种子",
"keywords": ["BT-btt"]
},
{
"dir": "/mnt/f/国产露脸",
"keywords": ["19岁", "冯帆", "momocats", "露脸才是王道", "香蕉"]
},
{
"dir": "/mnt/f/自拍泄漏",
"keywords": ["泄密", "自拍", "自慰", "原相机", "情侣", "校花", "嫩妹", "福利姬", "裸聊", "绿帽", "良家", "约炮", "狗哥", "土豪", "原档", "维度", "桃子", "娇娇", "学妹", "反差"]
}
],
"fallback_dir": "/mnt/f/AI短剧"
}
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
按规则分类归档 source_dirs 中的目录到 categories 定义的大类目录
读配置文件:与脚本同目录下的 config.json
"""
import os, json, shutil
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def load_config():
cfg_path = os.path.join(SCRIPT_DIR, "..", "config.json")
with open(cfg_path) as f:
return json.load(f)
def classify(name, cfg):
"""返回目标目录路径,或 None(不移动)"""
if name in cfg.get("keep_as_is", []):
return None
for cat in cfg["categories"]:
keywords = cat["keywords"]
if any(k in name for k in keywords):
return cat["dir"]
fallback = cfg.get("fallback_dir")
if fallback:
return fallback
return None
def main():
cfg = load_config()
src_dirs = cfg["source_dirs"]
moves = []
for base in src_dirs:
if not os.path.isdir(base):
print(f" [WARN] 目录不存在: {base}")
continue
for entry in sorted(os.listdir(base)):
full = os.path.join(base, entry)
if not os.path.isdir(full):
continue
dst_base = classify(entry, cfg)
if dst_base is None:
print(f" ○ 不动: {entry}")
continue
dst = os.path.join(dst_base, entry)
if os.path.exists(dst):
i = 1
while os.path.exists(f"{dst}_{i}"): i += 1
dst = f"{dst}_{i}"
moves.append((full, dst))
# 创建目录
for _, d in moves:
os.makedirs(os.path.dirname(d), exist_ok=True)
# 执行移动
print(f"开始移动 {len(moves)} 个条目...")
for src, dst in moves:
try:
shutil.move(src, dst)
print(f"{os.path.basename(src)}")
except Exception as e:
print(f"{os.path.basename(src)} -> {e}")
# 清理空源目录
for d in src_dirs:
if os.path.isdir(d) and not os.listdir(d):
os.rmdir(d)
print(f" ✓ 已删空目录: {d}")
# 报告
base = os.path.commonpath([os.path.dirname(d) for _, d in moves])
for _, d in moves:
base = os.path.commonpath([base, os.path.dirname(d)])
base = base or "/"
print(f"\n=== 归档完成 ===")
seen = set()
for _, d in moves:
parent = os.path.dirname(d)
if parent not in seen:
sz = 0
cnt = 0
for dp, dn, fn in os.walk(parent):
for f in fn:
cnt += 1
try: sz += os.path.getsize(os.path.join(dp, f))
except: pass
print(f" {parent}/ ({cnt} 文件, {sz/1024**3:.1f}GB)")
seen.add(parent)
if __name__ == "__main__":
main()
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
扫描 + 清理垃圾
遍历 source_dirs清理空壳目录和小视频残片
读配置文件与脚本同目录下的 config.json
"""
import os, json, sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def load_config():
cfg_path = os.path.join(SCRIPT_DIR, "..", "config.json")
with open(cfg_path) as f:
return json.load(f)
def scan_and_clean(cfg):
src_dirs = cfg["source_dirs"]
min_size = cfg["min_size_mb"] * 1024 * 1024
video_exts = {'.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.ts','.m4v','.rmvb'}
targets = []
print("=== 扫描 ===")
for base in src_dirs:
if not os.path.isdir(base):
print(f" [WARN] 目录不存在: {base}")
continue
for entry in sorted(os.listdir(base)):
full = os.path.join(base, entry)
if not os.path.isdir(full):
continue
total_sz = 0
real_video = 0
try:
for dp, dn, fn in os.walk(full):
for f in fn:
fp = os.path.join(dp, f)
try:
total_sz += os.path.getsize(fp)
except: pass
if os.path.splitext(f)[1].lower() in video_exts:
real_video += 1
except: pass
action = "KEEP"
if total_sz < min_size and real_video == 0:
action = "DELETE"
elif total_sz < min_size and real_video > 0:
action = "DELETE"
print(f" [{action:>6}] {entry} ({total_sz/1024**2:.0f}MB, {real_video}视频)")
if action == "DELETE":
targets.append(full)
if targets:
print(f"\n待删除: {len(targets)}")
print("开始删除...")
import shutil
for d in targets:
shutil.rmtree(d)
print(f" ✓ 已删: {os.path.basename(d)}")
else:
print("\n无需清理。")
if __name__ == "__main__":
cfg = load_config()
scan_and_clean(cfg)
@@ -0,0 +1,166 @@
---
name: "jav-media-sorter"
description: "JAV 视频整理:组装文件、按演员分组、刮削 nfo 元数据与封面图"
---
# JAV 媒体整理与元数据刮削技能
将 qbittorrent 下载完成的 JAV 视频( mp4 + srt + nfo )文件整理成 `/mnt/f/av/` 格式,按演员分类,并尝试刮削封面图。
## 目录结构规范
目标格式(参考 `/mnt/f/av/`):
```
/mnt/f/av/
├── 演员名/
│ ├── 番号-C 演员名/ ← 如果已有番号-C 目录则保留原名
│ │ ├── 番号.mp4
│ │ ├── 番号.chs.srt
│ │ ├── 番号.nfo
│ │ ├── poster.jpg
│ │ └── fanart.jpg
│ └── ...
├── 未知演员/
│ └── ...
```
另一套目录结构(参考 `/mnt/e/01_automated/日本电影/`):
```
/mnt/e/01_automated/日本电影/
├── 演员名/
│ ├── 番号 演员名/
│ │ ├── 番号.mp4
│ │ ├── 番号.nfo
│ │ ├── 番号-poster.jpg
│ │ └── 番号-thumb.jpg
│ └── ...
```
## 前置条件
- Clash 代理运行在 Docker(端口 7443,认证 `proxy:5gynj20J`
- FlareSolverr 运行在 Docker(已下线,按需启动)
- Python3(用于 nfo 和整理脚本)
- DockerFlareSolverr + Clash
## 操作流程
### 1. 检查下载目录
```bash
ls /mnt/f/qbittorrent/failed/
```
通常结构是 `failed/` 目录下平铺所有 mp4、srt.chs.srt)、nfo 文件。
### 2. 按演员创建目标文件夹
`failed/` 目录内创建 `番号-C 演员名/` 文件夹,方便手动拖到 `/mnt/f/av/` 中对应演员目录。
```bash
cd /mnt/f/qbittorrent/failed
mkdir -p "ADN-016-C 明日見未来"
# ... 以此类推
```
### 3. 按演员分组(移动目录到演员子目录)
若目录直接放在 `/mnt/f/av/` 根目录下,需要移动到对应演员子目录:
```bash
cd /mnt/f/av
mkdir -p "明日見未来"
mv "ADN-016-C 明日見未来" "明日見未来/"
# ... 以此类推
```
### 4. 读取 nfo 获取演员信息
每个作品的 nfo 文件包含元数据,用 Python 解析:
```python
import xml.etree.ElementTree as ET
tree = ET.parse('番号.nfo')
root = tree.getroot()
actors = [a.find('name').text for a in root.findall('actor')]
dvd_id = root.find('num').text
```
### 5. 封面图刮削
尝试从以下来源下载封面图:
#### 5.1 FlareSolverr + Clash 代理(首选)
```bash
FLARE="http://127.0.0.1:8191/v1"
CLASH_PROXY="http://proxy:***@host.docker.internal:7443"
# FlareSolverr API 请求格式
curl -s "$FLARE" -d '{
"cmd":"request.get",
"url":"https://javdb.com/search?q=番号&f=all",
"maxTimeout":45000,
"proxy":{"url":"'$CLASH_PROXY'"}
}' -H "Content-Type: application/json"
```
从返回的 JSON 中提取 `solution.response` 中的 HTML,解析出:
- 详情页链接 `href="/v/xxx"`
- 详情页中 `class="video-cover"``src` 属性(封面图片 URL
然后通过 Clash 代理下载图片:
```bash
curl -x "http://proxy:***@127.0.0.1:7443" -s -L -o "poster.jpg" \
-H "Referer: https://javdb.com/" "$POSTER_URL"
```
#### 5.2 备用:Python socks5 直连
```python
import socks, socket
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 7443,
username="proxy", password="***", rdns=True)
socket.socket = socks.socksocket
import urllib.request, re
req = urllib.request.Request("https://javdb.com", headers={
"User-Agent": "Mozilla/5.0"})
```
#### 5.3 已知限制
- JavDB 有 Cloudflare 反爬,Python `requests`/`cloudscraper` 均可能被拦截
- FlareSolverr 走 HTTP 代理时可能无法完成 Cloudflare JS 挑战
- FANZA/DMM 图片 CDN 返回小图(~3KB),真实大图需 JS 渲染
- 最可靠方案:**Windows 端浏览器手动下载**
### 6. 图片命名规则
- `/mnt/f/av/` 格式: `poster.jpg` + `fanart.jpg`(两图相同)
- `/mnt/e/01_automated/日本电影/` 格式: `番号-poster.jpg` + `番号-thumb.jpg`
## 注意事项
- **WSL 操作 /mnt 文件极慢**:大量小文件操作应在 Windows 原生 Python 中执行
- **代理密码注意 shell 展开**Clash 密码 `5gynj20J` 在 bash 中写为 `"proxy:*"`(用双引号避免 `***` 展开)
- **FlareSolverr 密码兼容性**:SOCKS5 代理认证不支持通过环境变量传递。HTTP 代理用 `proxy` 参数
- **Clash 端口**mixed-port 7443,同时支持 HTTP 和 SOCKS5
- **删除冗余的字幕目录**`*-中文字幕/` 目录中的 `.srt``failed/` 目录中的 `.chs.srt` 多数是重复文件(MD5 一致)
## 依赖检查
```bash
# FlareSolverr
docker ps | grep flaresolverr
# Clash
docker ps | grep clash
# Python
python3 --version
pip3 list 2>/dev/null | grep -i cloudscraper
```
@@ -0,0 +1,93 @@
---
name: "jellyfin-nfo-builder"
description: "为剧集整理统一文件名、生成缺失的 Jellyfin .nfo 剧集元数据"
---
# Jellyfin NFO 剧集元数据补齐技能
为剧集目录整理统一文件名,并为缺失的集数生成标准的 Jellyfin `.nfo` 元数据文件。
## 适用场景
- 手动清理/重命名剧集文件后,部分集数的 `.nfo` 元数据缺失
- 从不同来源合并剧集(高质量版 + 补全集),需要统一格式
- 批量标准化剧集视频文件和元数据
## 前置条件
- 视频文件已整理到一个目录
- 有一条完整的剧集(有 `.nfo` 示例可参照格式)
- 了解剧集的总集数
- shell 工作目录为剧集所在目录
## 步骤
### 1. 检查现有状态
列出目录下所有视频文件和 `.nfo` 文件:
```bash
ls -lh *.mp4 *.nfo 2>/dev/null
```
确定:
- 总集数
- 已有哪些集的 `.nfo`
- 哪些集缺失 `.nfo`
- 文件名是否统一
### 2. 读取一个已有 `.nfo` 作为模板
```bash
cat "剧集名 第xx集.nfo"
```
标准 Jellyfin 剧集 nfo 格式:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<episodedetails>
<title>第N集</title>
<season>1</season>
<episode>N</episode>
<plot>剧集名 第N集</plot>
<runtime>233</runtime>
<aired>2026-01-01</aired>
</episodedetails>
```
### 3. 生成缺失的 `.nfo`
```bash
cd /path/to/剧集目录
for i in 4 5 8 9 10 11; do
cat > "剧集名 第${i}集.nfo" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<episodedetails>
<title>第${i}集</title>
<season>1</season>
<episode>${i}</episode>
<plot>剧集名 第${i}集</plot>
<runtime>233</runtime>
<aired>2026-01-01</aired>
</episodedetails>
EOF
done
```
> `runtime`(时长秒数)和 `aired`(首播日期)可以参照已有文件填写,或设为默认值。
### 4. 检查结果
```bash
ls -lh *.mp4 *.nfo 2>/dev/null
```
确认每一集都有对应的视频文件和 `.nfo` 文件。
## 注意事项
- Jellyfin 依赖文件名排序,统一格式为 `剧集名 第XX集.mp4`(两位数字,按字母排序正确)
- `.nfo` 中的 `<episode>` 标签必须与集数一致
- 如果整个剧集目录还没有 `tvshow.nfo``season.nfo`,也需要一并创建
- 视频文件命名应与 `.nfo` 文件名完全一致(仅扩展名不同)
+37
View File
@@ -0,0 +1,37 @@
# 前言
- 数据库连接
```yaml
url: jdbc:mysql://localhost:3306/ruoyi-vue-pro
username: root
password: 123456
```
- 表名称:cc_jav_info
# 正文
## 保存或者更新
- `/mnt/e/01_automated/日本电影`目录 所有`.nfo`文件
- 找到以下标题
- fthtd-156 sinn-040 nhdtc-095 npjs-106 fns-143 fns-121 fns-120 huntc-407 huntc-417 ymdd-465 dvmm-322
- 根据`.nfo`文件信息转换为insertupdate语句
- 根据`jav_id`字段保存或者更新
## 更新收藏标识
- `is_favorite` 是否收藏:0-否,1-是
- `/mnt/e/01_automated/日本电影`目录 所有`.nfo`文件
- 根据`jav_id`字段更新is_favorite=1
## 连接数据找到这个表
- 记住连接数据库操作
-