Jellyfin 媒体库重命名工具 配置 在 中添加 OMDb 配置(从 https://www.omdbapi.com/apikey.aspx 免费申请 API Key): 配置文件搜索顺序:当前目录 → skill 目录 → git 根目录。 执行方式 命令 clean — 批量清理空格 遍历指定目录下的所有子文件夹,去掉文件夹名和文件名中的空格,图片重命名为 。 rename — 智能重命名(Jellyfin 规范 + IMDB ID) 解析 BT/字幕组风格的文件夹名,查询 OMDb API 获取 IMDB ID,按 Jellyfin 标准重命名。 输出格式 : - 电影文件夹: - 电影视频文件: - 剧集文件夹: - 剧集视频文件: - 图片(poster): 典型工作流 1. 先预览 :总是先用 查看解析结果是否正确 2. 处理候选列表 :若 API 返回多个候选,从列表选择正确的 IMDB ID 后用 重试 3. 批量处理 :使用 时,失败的文件夹会在末尾汇总,逐一用 补处理 4. 确认执行 :预览无误后去掉 加 执行 支持的文件格式 - 视频: - 图片(poster): - 字幕: (保留原文件名不处理) ---

)\n\n\ndef _find_config() -> dict:\n \"\"\"返回 [jellyfin] 配置段,包含 omdb.api_key 和 omdb.base_url。\"\"\"\n skill_dir = Path(__file__).resolve().parent.parent\n candidates = [\n Path.cwd() / \"agent_config.toml\",\n skill_dir / \"agent_config.toml\",\n ]\n for parent in Path.cwd().parents:\n if (parent / \".git\").exists():\n candidates.append(parent / \"agent_config.toml\")\n break\n for candidate in candidates:\n if candidate.exists():\n try:\n config = tomllib.loads(candidate.read_text(encoding=\"utf-8\"))\n return config.get(\"jellyfin\", {})\n except Exception:\n pass\n return {}\n\n\ndef _get_omdb_config(cfg: dict) -> tuple[str, str]:\n \"\"\"从配置中提取 OMDb base_url 和 api_key。\n 支持两种格式:\n [jellyfin.omdb] api_key = \"...\" (新格式)\n [jellyfin] omdb_api_key = \"...\" (旧格式兼容)\n \"\"\"\n omdb = cfg.get(\"omdb\", {})\n api_key = omdb.get(\"api_key\") or cfg.get(\"omdb_api_key\", \"\")\n base_url = omdb.get(\"base_url\", \"http://www.omdbapi.com\").rstrip(\"/\")\n return base_url, api_key\n\n\ndef _parse_folder_name(name: str) -> dict:\n \"\"\"解析 BT/字幕组风格的文件夹名,提取标题、年份、媒体类型和集数信息。\"\"\"\n tokens = re.split(r'[._]+', name.strip())\n\n year = None\n year_idx = None\n for i, t in enumerate(tokens):\n if YEAR_PATTERN.match(t):\n year = int(t)\n year_idx = i\n break\n\n title_tokens = tokens[:year_idx] if year_idx is not None else tokens\n after_tokens = tokens[year_idx + 1:] if year_idx is not None else []\n\n cjk_parts = []\n eng_parts = []\n for t in title_tokens:\n cjk = re.sub(r'[^一-鿿㐀-䶿]', '', t)\n eng = re.sub(r'[一-鿿㐀-䶿]', '', t).strip()\n if cjk:\n cjk_parts.append(cjk)\n if eng:\n eng_parts.append(eng)\n\n media_type = 'movie'\n season = 1\n ep_start = ep_end = None\n\n for t in after_tokens:\n m = EPISODE_PATTERN.search(t)\n if m:\n media_type = 'series'\n ep_start = int(m.group(1))\n if m.group(2):\n ep_end = int(m.group(2))\n sm = SEASON_PATTERN.search(t)\n if sm:\n season = int(sm.group(1))\n break\n\n return {\n 'original': name,\n 'year': year,\n 'cjk_title': ''.join(cjk_parts),\n 'eng_title': ' '.join(eng_parts),\n 'media_type': media_type,\n 'season': season,\n 'ep_start': ep_start,\n 'ep_end': ep_end,\n }\n\n\ndef _query_omdb(title: str, year: int | None, media_type: str, base_url: str, api_key: str) -> dict:\n \"\"\"查询 OMDb API,返回 {found, imdb_id, title, year, candidates}。\"\"\"\n if not api_key:\n click.echo(\"错误:未配置 OMDb API Key,请在 agent_config.toml 中设置 [jellyfin.omdb] api_key\", err=True)\n sys.exit(1)\n\n omdb_type = 'series' if media_type == 'series' else 'movie'\n url = f\"{base_url}/\"\n\n try:\n with httpx.Client(timeout=15.0) as client:\n params: dict = {'apikey': api_key, 't': title, 'type': omdb_type}\n if year:\n params['y'] = year\n r = client.get(url, params=params)\n r.raise_for_status()\n d = r.json()\n if d.get('Response') == 'True':\n return {\n 'found': True,\n 'imdb_id': d['imdbID'],\n 'title': d['Title'],\n 'year': d['Year'].split('–')[0].strip(),\n 'candidates': [],\n }\n\n r2 = client.get(url, params={'apikey': api_key, 's': title, 'type': omdb_type})\n r2.raise_for_status()\n d2 = r2.json()\n candidates = []\n if d2.get('Response') == 'True':\n candidates = [\n {'imdb_id': x['imdbID'], 'title': x['Title'], 'year': x['Year']}\n for x in d2.get('Search', [])\n ]\n return {'found': False, 'imdb_id': '', 'title': '', 'year': '', 'candidates': candidates}\n\n except httpx.ConnectError:\n click.echo(f\"错误:无法连接 OMDb API ({base_url}),请检查网络\", err=True)\n sys.exit(1)\n except httpx.HTTPStatusError as e:\n click.echo(f\"OMDb API 错误 {e.response.status_code}\", err=True)\n sys.exit(1)\n\n\ndef _lookup_by_imdb_id(imdb_id: str, base_url: str, api_key: str) -> dict | None:\n \"\"\"通过 IMDB ID 查询官方标题和年份。\"\"\"\n if not api_key:\n return None\n try:\n with httpx.Client(timeout=15.0) as client:\n r = client.get(f\"{base_url}/\", params={'apikey': api_key, 'i': imdb_id})\n d = r.json()\n if d.get('Response') == 'True':\n return {'title': d['Title'], 'year': d['Year'].split('–')[0].strip()}\n except Exception:\n pass\n return None\n\n\ndef _parse_ep_from_name(name: str) -> tuple[int | None, int | None]:\n \"\"\"从文件名中解析集数,返回 (ep_start, ep_end)。\"\"\"\n m = EPISODE_PATTERN.search(Path(name).stem)\n if m:\n return int(m.group(1)), int(m.group(2)) if m.group(2) else None\n return None, None\n\n\ndef _ep_filename(show: str, season: int, ep: int, ep_end: int | None, ext: str) -> str:\n \"\"\"构建剧集文件名。\"\"\"\n if ep_end:\n return f\"{show} S{season:02d}E{ep:02d}-E{ep_end:02d}{ext}\"\n return f\"{show} S{season:02d}E{ep:02d}{ext}\"\n\n\[email protected]()\ndef cli():\n \"\"\"Jellyfin 媒体库文件重命名工具。\"\"\"\n pass\n\n\[email protected]()\[email protected]('directory', default='.', type=click.Path(exists=True, file_okay=False))\[email protected]('--yes', '-y', is_flag=True, help='跳过确认,直接执行')\[email protected]('--dry-run', is_flag=True, help='只预览,不执行')\ndef clean(directory, yes, dry_run):\n \"\"\"批量清理:去掉文件夹/文件名中的空格,图片重命名为 poster。\"\"\"\n base = Path(directory).resolve()\n file_renames = []\n folder_renames = []\n\n for name in sorted(os.listdir(base)):\n folder = base / name\n if not folder.is_dir():\n continue\n videos, images = [], []\n for f in sorted(os.listdir(folder)):\n ext = Path(f).suffix.lower()\n if ext in VIDEO_EXTS:\n videos.append(f)\n elif ext in IMAGE_EXTS:\n images.append(f)\n if not videos and not images:\n continue\n\n first_stem = None\n for v in videos:\n t = v.replace(' ', '')\n if v != t:\n file_renames.append((folder, v, t))\n if first_stem is None:\n first_stem = Path(t).stem\n for img in images:\n t = f\"{first_stem}-poster{Path(img).suffix}\" if first_stem else img.replace(' ', '')\n if img != t:\n file_renames.append((folder, img, t))\n new_name = name.replace(' ', '')\n if new_name != name:\n folder_renames.append((base, name, new_name))\n\n if not file_renames and not folder_renames:\n click.echo(\"没有需要重命名的内容。\")\n return\n\n if file_renames:\n click.echo(\"=\" * 60)\n click.echo(\"文件重命名:\")\n click.echo(\"-\" * 60)\n for folder, old, new in file_renames:\n click.echo(f\" [{folder.name}]\\n {old}\\n → {new}\\n\")\n if folder_renames:\n click.echo(\"=\" * 60)\n click.echo(\"文件夹重命名:\")\n click.echo(\"-\" * 60)\n for _, old, new in folder_renames:\n click.echo(f\" {old}\\n→ {new}\\n\")\n\n if dry_run:\n click.echo(\"[预览模式,未执行]\")\n return\n if not yes:\n if click.prompt(\"确认执行以上重命名?[y/N]\", default='N').lower() != 'y':\n click.echo(\"已取消。\")\n return\n\n for folder, old, new in file_renames:\n (folder / old).rename(folder / new)\n for parent, old, new in folder_renames:\n (parent / old).rename(parent / new)\n click.echo(\"重命名完成!\")\n\n\[email protected]()\[email protected]('folder', type=click.Path(exists=True, file_okay=False))\[email protected]('--batch', is_flag=True, help='批量模式:遍历 folder 下所有子目录')\[email protected]('--type', 'media_type', type=click.Choice(['movie', 'series', 'auto']), default='auto',\n help='媒体类型(默认 auto 自动检测)')\[email protected]('--title', 'title_override', default=None, help='手动指定英文搜索标题')\[email protected]('--year', 'year_override', type=int, default=None, help='手动指定年份')\[email protected]('--imdb-id', 'imdb_id_override', default=None, help='手动指定 IMDB ID(跳过 API 搜索)')\[email protected]('--yes', '-y', is_flag=True, help='跳过确认,直接执行')\[email protected]('--dry-run', is_flag=True, help='只预览,不执行')\ndef rename(folder, batch, media_type, title_override, year_override, imdb_id_override, yes, dry_run):\n \"\"\"智能重命名:按 Jellyfin 规范重命名电影/剧集文件夹及内部文件。\n\n 解析 BT/字幕组风格文件夹名(如 Movie.Name.2020.1080P.X264),\n 查询 OMDb API 获取 IMDB ID,重命名为 Jellyfin 标准格式。\n \"\"\"\n cfg = _find_config()\n base_url, api_key = _get_omdb_config(cfg)\n base = Path(folder).resolve()\n\n if batch:\n subdirs = sorted([d for d in base.iterdir() if d.is_dir()])\n failures = []\n for d in subdirs:\n click.echo(f\"\\n{'='*60}\\n处理:{d.name}\\n{'='*60}\")\n ok = _rename_one(d, media_type, title_override, year_override,\n imdb_id_override, yes, dry_run, base_url, api_key)\n if not ok:\n failures.append(d.name)\n if failures:\n click.echo(f\"\\n{'='*60}\")\n click.echo(f\"以下 {len(failures)} 个文件夹需要手动处理:\")\n for n in failures:\n click.echo(f\" - {n}\")\n click.echo(\"使用 --imdb-id 手动指定后重试。\")\n else:\n _rename_one(base, media_type, title_override, year_override,\n imdb_id_override, yes, dry_run, base_url, api_key)\n\n\ndef _rename_one(\n folder: Path,\n media_type: str,\n title_override: str | None,\n year_override: int | None,\n imdb_id_override: str | None,\n yes: bool,\n dry_run: bool,\n base_url: str,\n api_key: str,\n) -> bool:\n \"\"\"处理单个文件夹的重命名,返回 True 表示成功。\"\"\"\n parsed = _parse_folder_name(folder.name)\n search_title = title_override or parsed['eng_title'] or parsed['cjk_title']\n year = year_override or parsed['year']\n actual_type = parsed['media_type'] if media_type == 'auto' else media_type\n\n click.echo(\n f\"解析:中文={parsed['cjk_title'] or '(无)'} 英文={parsed['eng_title'] or '(无)'} \"\n f\"年份={year or '?'} 类型={actual_type}\"\n )\n if actual_type == 'series' and parsed['ep_start']:\n ep_info = f\"{parsed['ep_start']}-{parsed['ep_end']}\" if parsed['ep_end'] else str(parsed['ep_start'])\n click.echo(f\" 集数:第 {parsed['season']} 季 EP{ep_info}\")\n\n if not search_title:\n click.echo(\"错误:无法解析标题,请用 --title 手动指定\", err=True)\n return False\n\n # 获取官方标题和 IMDB ID\n if imdb_id_override:\n info = _lookup_by_imdb_id(imdb_id_override, base_url, api_key)\n if info:\n official_title, official_year = info['title'], info['year']\n else:\n official_title = title_override or search_title\n official_year = str(year) if year else '????'\n imdb_id = imdb_id_override\n click.echo(f\"使用 IMDB ID {imdb_id} → {official_title} ({official_year})\")\n else:\n click.echo(f\"查询 OMDb:「{search_title}」({year}, {actual_type})...\")\n result = _query_omdb(search_title, year, actual_type, base_url, api_key)\n if not result['found']:\n if result['candidates']:\n click.echo(\"找到多个候选,请用 --imdb-id 指定:\")\n for i, c in enumerate(result['candidates'][:10], 1):\n click.echo(f\" {i}. {c['title']} ({c['year']}) [{c['imdb_id']}]\")\n else:\n click.echo(\"未找到匹配结果,请用 --title 调整搜索词或 --imdb-id 手动指定\", err=True)\n return False\n official_title = result['title']\n official_year = result['year']\n imdb_id = result['imdb_id']\n click.echo(f\"找到:{official_title} ({official_year}) [{imdb_id}]\")\n\n # 若原文件夹名包含中文标题,则保留并拼接在官方英文标题前\n cjk_prefix = f\"{parsed['cjk_title']} \" if parsed['cjk_title'] else \"\"\n display_title = f\"{cjk_prefix}{official_title}\"\n new_folder_name = f\"{display_title} ({official_year}) [imdbid-{imdb_id}]\"\n new_folder = folder.parent / new_folder_name\n\n # 规划文件重命名\n file_renames: list[tuple[Path, Path]] = []\n for f in sorted(folder.iterdir()):\n if f.is_dir():\n continue\n ext = f.suffix.lower()\n if ext in VIDEO_EXTS:\n if actual_type == 'series':\n ep, ep_end_local = _parse_ep_from_name(f.name)\n if ep is not None:\n new_name = _ep_filename(display_title, parsed['season'], ep, ep_end_local, f.suffix)\n else:\n new_name = f.name\n else:\n new_name = f\"{new_folder_name}{f.suffix}\"\n file_renames.append((f, folder / new_name))\n elif ext in IMAGE_EXTS:\n file_renames.append((f, folder / f\"{new_folder_name}-poster{f.suffix}\"))\n\n # 显示预览\n click.echo(f\"\\n重命名计划:\")\n click.echo(f\" 文件夹:{folder.name}\")\n click.echo(f\" → {new_folder_name}\")\n changed = [(s, d) for s, d in file_renames if s.name != d.name]\n if changed:\n click.echo(f\" 文件({len(changed)} 个变更):\")\n for s, d in changed:\n click.echo(f\" {s.name}\")\n click.echo(f\" → {d.name}\")\n\n if dry_run:\n click.echo(\"\\n[预览模式,未执行]\")\n return True\n\n if not yes:\n if click.prompt(\"\\n确认执行?[y/N]\", default='N').lower() != 'y':\n click.echo(\"已取消。\")\n return False\n\n # 先重命名文件,再重命名文件夹\n for src, dst in file_renames:\n if src != dst:\n src.rename(dst)\n if folder != new_folder:\n folder.rename(new_folder)\n\n click.echo(f\"完成!→ {new_folder_name}\")\n return True\n\n\nif __name__ == '__main__':\n cli()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15357,"content_sha256":"a34c030f887841d06651d287ee53cbd568f6f1a869b6bc1c5c3f608675f0749f"},{"filename":"scripts/pyproject.toml","content":"[project]\nname = \"jellyfin-tool\"\nversion = \"0.1.0\"\ndescription = \"Jellyfin 媒体库文件重命名工具:按 Jellyfin 命名规范重命名电影/剧集文件夹和文件。\"\nrequires-python = \">=3.13\"\ndependencies = [\n \"httpx>=0.27.0\",\n \"click>=8.0.0\",\n]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n","content_type":"text/plain; charset=utf-8","language":"toml","size":340,"content_sha256":"db1d7ae7a2e2aeef54b1cc66a11ded8d774c8eac25c3527020288c73b1037d3a"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Jellyfin 媒体库重命名工具","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"配置","type":"text"}]},{"type":"paragraph","content":[{"text":"在 ","type":"text"},{"text":"agent_config.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" 中添加 OMDb 配置(从 https://www.omdbapi.com/apikey.aspx 免费申请 API Key):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"toml"},"content":[{"text":"[jellyfin.omdb]\nbase_url = \"http://www.omdbapi.com\"\napi_key = \"your_api_key_here\"","type":"text"}]},{"type":"paragraph","content":[{"text":"配置文件搜索顺序:当前目录 → skill 目录 → git 根目录。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"执行方式","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 变量定义(SKILL_DIR 为本 skill 的绝对路径)\nSKILL_DIR=\"/path/to/skills/jellyfin\"\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" \u003ccommand> [options]","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"命令","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"clean — 批量清理空格","type":"text"}]},{"type":"paragraph","content":[{"text":"遍历指定目录下的所有子文件夹,去掉文件夹名和文件名中的空格,图片重命名为 ","type":"text"},{"text":"{视频名}-poster{ext}","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 预览(不执行)\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" clean /path/to/media --dry-run\n\n# 执行(跳过确认,适合 agent 使用)\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" clean /path/to/media --yes","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"rename — 智能重命名(Jellyfin 规范 + IMDB ID)","type":"text"}]},{"type":"paragraph","content":[{"text":"解析 BT/字幕组风格的文件夹名,查询 OMDb API 获取 IMDB ID,按 Jellyfin 标准重命名。","type":"text"}]},{"type":"paragraph","content":[{"text":"输出格式","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"电影文件夹:","type":"text"},{"text":"Movie Name (year) [imdbid-ttXXXXXXXX]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"电影视频文件:","type":"text"},{"text":"Movie Name (year) [imdbid-ttXXXXXXXX].mkv","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"剧集文件夹:","type":"text"},{"text":"Show Name (year) [imdbid-ttXXXXXXXX]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"剧集视频文件:","type":"text"},{"text":"Show Name S01E01.mkv","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"图片(poster):","type":"text"},{"text":"Name (year) [imdbid-ttXXXXXXXX]-poster.jpg","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 单个文件夹(自动检测是电影还是剧集)\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" rename \"/path/to/Movie.Name.2020.1080P\" --dry-run\n\n# 批量处理整个目录\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" rename /path/to/Movies/ --batch --dry-run\n\n# 手动指定标题(当自动解析不准时)\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" rename \"/path/to/folder\" --title \"The Matrix\" --year 1999\n\n# 手动指定 IMDB ID(当 API 返回候选列表时使用)\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" rename \"/path/to/folder\" --imdb-id tt0133093 --yes\n\n# 强制指定为剧集类型\nuv run --project \"$SKILL_DIR/scripts\" \"$SKILL_DIR/scripts/jellyfin_tool.py\" rename \"/path/to/folder\" --type series","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"典型工作流","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先预览","type":"text","marks":[{"type":"strong"}]},{"text":":总是先用 ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" 查看解析结果是否正确","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"处理候选列表","type":"text","marks":[{"type":"strong"}]},{"text":":若 API 返回多个候选,从列表选择正确的 IMDB ID 后用 ","type":"text"},{"text":"--imdb-id","type":"text","marks":[{"type":"code_inline"}]},{"text":" 重试","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"批量处理","type":"text","marks":[{"type":"strong"}]},{"text":":使用 ","type":"text"},{"text":"--batch","type":"text","marks":[{"type":"code_inline"}]},{"text":" 时,失败的文件夹会在末尾汇总,逐一用 ","type":"text"},{"text":"--imdb-id","type":"text","marks":[{"type":"code_inline"}]},{"text":" 补处理","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"确认执行","type":"text","marks":[{"type":"strong"}]},{"text":":预览无误后去掉 ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" 加 ","type":"text"},{"text":"--yes","type":"text","marks":[{"type":"code_inline"}]},{"text":" 执行","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"支持的文件格式","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"视频:","type":"text"},{"text":".mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".mkv","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".avi","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".wmv","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".mov","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".ts","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".m2ts","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".flv","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".rmvb","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"图片(poster):","type":"text"},{"text":".jpg","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".jpeg","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".png","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".webp","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".gif","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".tbn","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"字幕:","type":"text"},{"text":".srt","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".ass","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".ssa","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".sub","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":".vtt","type":"text","marks":[{"type":"code_inline"}]},{"text":"(保留原文件名不处理)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"jellyfin","author":"@skillopedia","source":{"stars":2,"repo_name":"skills","origin_url":"https://github.com/zrong/skills/blob/HEAD/jellyfin/SKILL.md","repo_owner":"zrong","body_sha256":"b397fe0903106a4094cbf20445689a392c23467dc5cb386475a0c0bdb31d20b2","cluster_key":"2a1540abc137b2d0ff15057536a29ed5929d90b60e12af25c36ea950edbabedf","clean_bundle":{"format":"clean-skill-bundle-v1","source":"zrong/skills/jellyfin/SKILL.md","attachments":[{"id":"5f5f5183-9d8f-5b84-a5c2-a78256f2f3e9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5f5f5183-9d8f-5b84-a5c2-a78256f2f3e9/attachment.py","path":"scripts/jellyfin_tool.py","size":15357,"sha256":"a34c030f887841d06651d287ee53cbd568f6f1a869b6bc1c5c3f608675f0749f","contentType":"text/x-python; charset=utf-8"},{"id":"5195f443-df97-5749-9f79-9098e3adf33e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5195f443-df97-5749-9f79-9098e3adf33e/attachment.toml","path":"scripts/pyproject.toml","size":340,"sha256":"db1d7ae7a2e2aeef54b1cc66a11ded8d774c8eac25c3527020288c73b1037d3a","contentType":"text/plain; charset=utf-8"}],"bundle_sha256":"7800b5211161ff4ea46f7856e82350fb7799a5b87c43cacfcfaf93d9c9a79524","attachment_count":2,"text_attachments":2,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"jellyfin/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"general","category_label":"General"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"general","import_tag":"clean-skills-v1","description":"Jellyfin 媒体库文件命名工具。当用户需要按照 Jellyfin 命名规范重命名电影/剧集文件夹和文件、获取 IMDB ID、处理 BT/字幕组风格的媒体文件名(含点分隔、中英混合、质量标记如 1080P.X264.AAC)时使用。支持 clean(批量去除空格)和 rename(智能重命名 + IMDB ID 查询,支持电影和剧集)。"}},"renderedAt":1782987894872}

Jellyfin 媒体库重命名工具 配置 在 中添加 OMDb 配置(从 https://www.omdbapi.com/apikey.aspx 免费申请 API Key): 配置文件搜索顺序:当前目录 → skill 目录 → git 根目录。 执行方式 命令 clean — 批量清理空格 遍历指定目录下的所有子文件夹,去掉文件夹名和文件名中的空格,图片重命名为 。 rename — 智能重命名(Jellyfin 规范 + IMDB ID) 解析 BT/字幕组风格的文件夹名,查询 OMDb API 获取 IMDB ID,按 Jellyfin 标准重命名。 输出格式 : - 电影文件夹: - 电影视频文件: - 剧集文件夹: - 剧集视频文件: - 图片(poster): 典型工作流 1. 先预览 :总是先用 查看解析结果是否正确 2. 处理候选列表 :若 API 返回多个候选,从列表选择正确的 IMDB ID 后用 重试 3. 批量处理 :使用 时,失败的文件夹会在末尾汇总,逐一用 补处理 4. 确认执行 :预览无误后去掉 加 执行 支持的文件格式 - 视频: - 图片(poster): - 字幕: (保留原文件名不处理) ---