Merge pull request #5602 from PKC278/v2

This commit is contained in:
jxxghp
2026-03-22 07:08:41 +08:00
committed by GitHub
4 changed files with 196 additions and 132 deletions
+9 -9
View File
@@ -24,7 +24,7 @@ class AddDownloadInput(BaseModel):
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context") explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
torrent_url: List[str] = Field( torrent_url: List[str] = Field(
..., ...,
description="One or more torrent_url values. Values matching the hash:id pattern from get_search_results are treated as internal references; other values must be direct torrent URLs or magnet links." description="One or more torrent_url values. Supports refs from get_search_results (`hash:id`) and magnet links."
) )
downloader: Optional[str] = Field(None, downloader: Optional[str] = Field(None,
description="Name of the downloader to use (optional, uses default if not specified)") description="Name of the downloader to use (optional, uses default if not specified)")
@@ -36,7 +36,7 @@ class AddDownloadInput(BaseModel):
class AddDownloadTool(MoviePilotTool): class AddDownloadTool(MoviePilotTool):
name: str = "add_download" name: str = "add_download"
description: str = "Add torrent download task to the configured downloader (qBittorrent, Transmission, etc.) using hash:id references from get_search_results or direct torrent URLs / magnet links." description: str = "Add torrent download tasks using refs from get_search_results or magnet links."
args_schema: Type[BaseModel] = AddDownloadInput args_schema: Type[BaseModel] = AddDownloadInput
def get_tool_message(self, **kwargs) -> Optional[str]: def get_tool_message(self, **kwargs) -> Optional[str]:
@@ -49,7 +49,7 @@ class AddDownloadTool(MoviePilotTool):
if self._is_torrent_ref(torrent_urls[0]): if self._is_torrent_ref(torrent_urls[0]):
message = f"正在添加下载任务: 资源 {torrent_urls[0]}" message = f"正在添加下载任务: 资源 {torrent_urls[0]}"
else: else:
message = "正在添加下载任务: 直链或磁力链接" message = "正在添加下载任务: 磁力链接"
else: else:
message = f"正在批量添加下载任务: 共 {len(torrent_urls)} 个资源" message = f"正在批量添加下载任务: 共 {len(torrent_urls)} 个资源"
else: else:
@@ -74,12 +74,12 @@ class AddDownloadTool(MoviePilotTool):
return bool(re.fullmatch(r"[0-9a-f]{7}:\d+", str(torrent_ref).strip())) return bool(re.fullmatch(r"[0-9a-f]{7}:\d+", str(torrent_ref).strip()))
@staticmethod @staticmethod
def _is_direct_download_url(torrent_url: Optional[str]) -> bool: def _is_magnet_link_input(torrent_url: Optional[str]) -> bool:
"""判断是否为允许直传下载器的下载内容""" """判断输入是否为允许直接添加的磁力链接"""
if not torrent_url: if not torrent_url:
return False return False
value = str(torrent_url).strip() value = str(torrent_url).strip()
return value.startswith("http://") or value.startswith("https://") or value.startswith("magnet:") return value.startswith("magnet:")
@classmethod @classmethod
def _resolve_cached_context(cls, torrent_ref: str) -> Optional[Context]: def _resolve_cached_context(cls, torrent_ref: str) -> Optional[Context]:
@@ -127,7 +127,7 @@ class AddDownloadTool(MoviePilotTool):
prefix = "添加种子任务失败:" prefix = "添加种子任务失败:"
if normalized_error.startswith(prefix): if normalized_error.startswith(prefix):
normalized_error = normalized_error[len(prefix):].lstrip() normalized_error = normalized_error[len(prefix):].lstrip()
if AddDownloadTool._is_direct_download_url(normalized_error): if AddDownloadTool._is_magnet_link_input(normalized_error):
normalized_error = "" normalized_error = ""
if normalized_error: if normalized_error:
return f"{torrent_ref} {normalized_error}" return f"{torrent_ref} {normalized_error}"
@@ -227,9 +227,9 @@ class AddDownloadTool(MoviePilotTool):
media_info=media_info media_info=media_info
) )
else: else:
if not self._is_direct_download_url(torrent_input): if not self._is_magnet_link_input(torrent_input):
failed_messages.append( failed_messages.append(
f"{torrent_input} 不是有效的下载内容,非 hash:id 时仅支持 http://、https:// 或 magnet: 开头" f"{torrent_input} 不是有效的下载内容,非 hash:id 时仅支持 magnet: 开头"
) )
continue continue
download_dir = self._resolve_direct_download_dir(save_path) download_dir = self._resolve_direct_download_dir(save_path)
+102 -49
View File
@@ -1,16 +1,78 @@
"""查询媒体库工具""" """查询媒体库工具"""
import json import json
from typing import Optional, Type from collections import OrderedDict
from typing import Optional, Type, Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.chain.mediaserver import MediaServerChain from app.chain.mediaserver import MediaServerChain
from app.helper.mediaserver import MediaServerHelper
from app.log import logger from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent from app.schemas.types import MediaType, media_type_to_agent
def _sort_seasons(seasons: Optional[dict]) -> dict:
"""按季号、集号升序整理季集信息,保证输出稳定。"""
if not seasons:
return {}
def _sort_key(value):
try:
return int(value)
except (TypeError, ValueError):
return str(value)
return OrderedDict(
(season, sorted(episodes, key=_sort_key))
for season, episodes in sorted(seasons.items(), key=lambda item: _sort_key(item[0]))
)
def _filter_regular_seasons(seasons: Optional[dict]) -> OrderedDict:
"""仅保留正片季,忽略 season 0 等特殊季。"""
sorted_seasons = _sort_seasons(seasons)
regular_seasons = OrderedDict()
for season, episodes in sorted_seasons.items():
try:
season_number = int(season)
except (TypeError, ValueError):
continue
if season_number > 0:
regular_seasons[season_number] = episodes
return regular_seasons
def _build_tv_server_result(existing_seasons: OrderedDict, total_seasons: OrderedDict) -> dict[str, Any]:
"""构建单个服务器的电视剧存在性结果。"""
seasons_result = OrderedDict()
missing_seasons = []
all_seasons = sorted(set(total_seasons.keys()) | set(existing_seasons.keys()))
for season in all_seasons:
existing_episodes = existing_seasons.get(season, [])
total_episodes = total_seasons.get(season)
if total_episodes is not None:
missing_episodes = [episode for episode in total_episodes if episode not in existing_episodes]
total_episode_count = len(total_episodes)
else:
missing_episodes = None
total_episode_count = None
seasons_result[str(season)] = {
"existing_episodes": existing_episodes,
"total_episodes": total_episode_count,
"missing_episodes": missing_episodes
}
if total_episodes is not None and not existing_episodes:
missing_seasons.append(season)
return {
"seasons": seasons_result,
"missing_seasons": missing_seasons
}
class QueryLibraryExistsInput(BaseModel): class QueryLibraryExistsInput(BaseModel):
"""查询媒体库工具的输入参数模型""" """查询媒体库工具的输入参数模型"""
explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context") explanation: str = Field(..., description="Clear explanation of why this tool is being used in the current context")
@@ -21,7 +83,7 @@ class QueryLibraryExistsInput(BaseModel):
class QueryLibraryExistsTool(MoviePilotTool): class QueryLibraryExistsTool(MoviePilotTool):
name: str = "query_library_exists" name: str = "query_library_exists"
description: str = "Check whether a specific media resource already exists in the media library (Plex, Emby, Jellyfin) by media ID. Requires tmdb_id or douban_id (can be obtained from search_media tool) for accurate matching." description: str = "Check whether media already exists in Plex, Emby, or Jellyfin by media ID. Results are grouped by media server; TV results include existing episodes, total episodes, and missing episodes/seasons. Requires tmdb_id or douban_id from search_media."
args_schema: Type[BaseModel] = QueryLibraryExistsInput args_schema: Type[BaseModel] = QueryLibraryExistsInput
def get_tool_message(self, **kwargs) -> Optional[str]: def get_tool_message(self, **kwargs) -> Optional[str]:
@@ -63,62 +125,53 @@ class QueryLibraryExistsTool(MoviePilotTool):
media_id = f"TMDB={tmdb_id}" if tmdb_id else f"豆瓣={douban_id}" media_id = f"TMDB={tmdb_id}" if tmdb_id else f"豆瓣={douban_id}"
return f"未识别到媒体信息: {media_id}" return f"未识别到媒体信息: {media_id}"
# 2. 调用媒体服务器接口实时查询存在信息 # 2. 遍历所有媒体服务器,分别查询存在信息
existsinfo = media_chain.media_exists(mediainfo=mediainfo) server_results = OrderedDict()
media_server_helper = MediaServerHelper()
total_seasons = _filter_regular_seasons(mediainfo.seasons)
global_existsinfo = media_chain.media_exists(mediainfo=mediainfo)
if not existsinfo: for service_name in sorted(media_server_helper.get_services().keys()):
existsinfo = media_chain.media_exists(mediainfo=mediainfo, server=service_name)
if not existsinfo:
continue
if existsinfo.type == MediaType.TV:
existing_seasons = _filter_regular_seasons(existsinfo.seasons)
server_results[service_name] = _build_tv_server_result(
existing_seasons=existing_seasons,
total_seasons=total_seasons
)
else:
server_results[service_name] = {
"exists": True
}
if global_existsinfo:
fallback_server_name = global_existsinfo.server or "local"
if fallback_server_name not in server_results:
if global_existsinfo.type == MediaType.TV:
server_results[fallback_server_name] = _build_tv_server_result(
existing_seasons=_filter_regular_seasons(global_existsinfo.seasons),
total_seasons=total_seasons
)
else:
server_results[fallback_server_name] = {
"exists": True
}
if not server_results:
return "媒体库中未找到相关媒体" return "媒体库中未找到相关媒体"
# 3. 如果找到了,获取详细信息并组装结果 # 3. 组装统一的存在性结果,不查询媒体服务器详情
result_items = []
if existsinfo.itemid and existsinfo.server:
iteminfo = media_chain.iteminfo(server=existsinfo.server, item_id=existsinfo.itemid)
if iteminfo:
# 使用 model_dump() 转换为字典格式
item_dict = iteminfo.model_dump(exclude_none=True)
# 对于电视剧,补充已存在的季集详情及进度统计
if existsinfo.type == MediaType.TV:
# 注入已存在集信息 (Dict[int, list])
item_dict["seasoninfo"] = existsinfo.seasons
# 统计库中已存在的季集总数
if existsinfo.seasons:
item_dict["existing_episodes_count"] = sum(len(e) for e in existsinfo.seasons.values())
item_dict["seasons_existing_count"] = {str(s): len(e) for s, e in existsinfo.seasons.items()}
# 如果识别到了元数据,补充总计对比和进度概览
if mediainfo.seasons:
item_dict["seasons_total_count"] = {str(s): len(e) for s, e in mediainfo.seasons.items()}
# 进度概览,例如 "Season 1": "3/12"
item_dict["seasons_progress"] = {
f"{s}": f"{len(existsinfo.seasons.get(s, []))}/{len(mediainfo.seasons.get(s, []))}"
for s in mediainfo.seasons.keys() if (s in existsinfo.seasons or s > 0)
}
result_items.append(item_dict)
if result_items:
return json.dumps(result_items, ensure_ascii=False)
# 如果找到了但没有获取到 iteminfo,返回基本信息
result_dict = { result_dict = {
"title": mediainfo.title, "title": mediainfo.title,
"year": mediainfo.year, "year": mediainfo.year,
"type": media_type_to_agent(existsinfo.type), "type": media_type_to_agent(mediainfo.type),
"server": existsinfo.server, "servers": server_results
"server_type": existsinfo.server_type,
"itemid": existsinfo.itemid,
"seasons": existsinfo.seasons if existsinfo.seasons else {}
} }
if existsinfo.type == MediaType.TV and existsinfo.seasons:
result_dict["existing_episodes_count"] = sum(len(e) for e in existsinfo.seasons.values())
result_dict["seasons_existing_count"] = {str(s): len(e) for s, e in existsinfo.seasons.items()}
if mediainfo.seasons:
result_dict["seasons_total_count"] = {str(s): len(e) for s, e in mediainfo.seasons.items()}
return json.dumps([result_dict], ensure_ascii=False) return json.dumps([result_dict], ensure_ascii=False)
except Exception as e: except Exception as e:
logger.error(f"查询媒体库失败: {e}", exc_info=True) logger.error(f"查询媒体库失败: {e}", exc_info=True)
return f"查询媒体库时发生错误: {str(e)}" return f"查询媒体库时发生错误: {str(e)}"
+38 -24
View File
@@ -8,7 +8,35 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.db.subscribe_oper import SubscribeOper from app.db.subscribe_oper import SubscribeOper
from app.log import logger from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent from app.schemas.subscribe import Subscribe as SubscribeSchema
from app.schemas.types import MediaType
QUERY_SUBSCRIBE_OUTPUT_FIELDS = [
"id",
"name",
"year",
"type",
"season",
"total_episode",
"start_episode",
"lack_episode",
"filter",
"include",
"exclude",
"quality",
"resolution",
"effect",
"state",
"last_update",
"sites",
"downloader",
"best_version",
"save_path",
"custom_words",
"media_category",
"filter_groups",
"episode_group"
]
class QuerySubscribesInput(BaseModel): class QuerySubscribesInput(BaseModel):
@@ -24,7 +52,7 @@ class QuerySubscribesInput(BaseModel):
class QuerySubscribesTool(MoviePilotTool): class QuerySubscribesTool(MoviePilotTool):
name: str = "query_subscribes" name: str = "query_subscribes"
description: str = "Query subscription status and list all user subscriptions. Shows active subscriptions, their download status, and configuration details." description: str = "Query subscription status and list user subscriptions. Returns full subscription parameters for each matched subscription."
args_schema: Type[BaseModel] = QuerySubscribesInput args_schema: Type[BaseModel] = QuerySubscribesInput
def get_tool_message(self, **kwargs) -> Optional[str]: def get_tool_message(self, **kwargs) -> Optional[str]:
@@ -69,28 +97,14 @@ class QuerySubscribesTool(MoviePilotTool):
# 限制最多50条结果 # 限制最多50条结果
total_count = len(filtered_subscribes) total_count = len(filtered_subscribes)
limited_subscribes = filtered_subscribes[:50] limited_subscribes = filtered_subscribes[:50]
# 精简字段,只保留关键信息 full_subscribes = [
simplified_subscribes = [] SubscribeSchema.model_validate(s, from_attributes=True).model_dump(
for s in limited_subscribes: include=set(QUERY_SUBSCRIBE_OUTPUT_FIELDS),
simplified = { exclude_none=True
"id": s.id, )
"name": s.name, for s in limited_subscribes
"year": s.year, ]
"type": media_type_to_agent(s.type), result_json = json.dumps(full_subscribes, ensure_ascii=False, indent=2)
"season": s.season,
"tmdbid": s.tmdbid,
"doubanid": s.doubanid,
"bangumiid": s.bangumiid,
"poster": s.poster,
"vote": s.vote,
"state": s.state,
"total_episode": s.total_episode,
"lack_episode": s.lack_episode,
"last_update": s.last_update,
"username": s.username
}
simplified_subscribes.append(simplified)
result_json = json.dumps(simplified_subscribes, ensure_ascii=False, indent=2)
# 如果结果被裁剪,添加提示信息 # 如果结果被裁剪,添加提示信息
if total_count > 50: if total_count > 50:
return f"注意:查询结果共找到 {total_count} 条,为节省上下文空间,仅显示前 50 条结果。\n\n{result_json}" return f"注意:查询结果共找到 {total_count} 条,为节省上下文空间,仅显示前 50 条结果。\n\n{result_json}"
+47 -50
View File
@@ -1,6 +1,6 @@
--- ---
name: moviepilot-cli name: moviepilot-cli
description: Use this skill when the user wants to find, download, or subscribe to a movie or TV show (including anime); asks about download or subscription status; needs to check or organize the media library; or mentions MoviePilot directly. Covers the full media acquisition workflow via MoviePilot — searching TMDB, filtering and downloading torrents from PT indexer sites, managing subscriptions for automatic episode tracking, and handling library organization, site accounts, filter rules, and schedulers. description: Use this skill when the user wants to find, download, or subscribe to a movie or TV show (including anime); asks about download or subscription status; needs to check or organize the media library; or mentions MoviePilot directly. Covers the full media acquisition workflow via MoviePilot — searching TMDB, filtering and downloading torrents from indexer sites, managing subscriptions for automatic episode tracking, and handling library organization, site accounts, filter rules, and schedulers.
--- ---
# MoviePilot CLI # MoviePilot CLI
@@ -16,30 +16,18 @@ node scripts/mp-cli.js list # list all available commands
node scripts/mp-cli.js show <command> # show parameters, required fields, and usage node scripts/mp-cli.js show <command> # show parameters, required fields, and usage
``` ```
Always run `show <command>` before calling a command. Do not guess parameter names or argument formats. Always run `show <command>` before calling a command. Parameter names vary per command and are not inferable — do not guess.
## Command Groups ## Command Groups
| Category | Commands | **Media Search:** search_media, recognize_media, query_media_detail, get_recommendations, search_person, search_person_credits
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | **Torrent:** search_torrents, get_search_results
| Media Search | search_media, recognize_media, query_media_detail, get_recommendations, search_person, search_person_credits | **Download:** add_download, query_download_tasks, delete_download, query_downloaders
| Torrent | search_torrents, get_search_results | **Subscription:** add_subscribe, query_subscribes, update_subscribe, delete_subscribe, search_subscribe, query_subscribe_history, query_popular_subscribes, query_subscribe_shares
| Download | add_download, query_download_tasks, delete_download, query_downloaders | **Library:** query_library_exists, query_library_latest, transfer_file, scrape_metadata, query_transfer_history
| Subscription | add_subscribe, query_subscribes, update_subscribe, delete_subscribe, search_subscribe, query_subscribe_history, query_popular_subscribes, query_subscribe_shares | **Files:** list_directory, query_directory_settings
| Library | query_library_exists, query_library_latest, transfer_file, scrape_metadata, query_transfer_history | **Sites:** query_sites, query_site_userdata, test_site, update_site, update_site_cookie
| Files | list_directory, query_directory_settings | **System:** query_schedulers, run_scheduler, query_workflows, run_workflow, query_rule_groups, query_episode_schedule, send_message
| Sites | query_sites, query_site_userdata, test_site, update_site, update_site_cookie |
| System | query_schedulers, run_scheduler, query_workflows, run_workflow, query_rule_groups, query_episode_schedule, send_message |
## Gotchas
- **Don't guess command parameters.** Parameter names vary per command and are not inferrable. Always run `show <command>` first.
- **`search_torrents` results are cached server-side.** `get_search_results` reads from that cache — always run `search_torrents` first in the same session before filtering.
- **Omitting `sites` uses the user's configured default sites**, not all available sites. Only call `query_sites` and pass `sites=` when the user explicitly asks for a specific site.
- **TMDB season numbers don't always match fan-labeled seasons.** Anime and long-running shows often split one TMDB season into parts. Always validate with `query_media_detail` when the user mentions a specific season.
- **`add_download` is irreversible without manual cleanup.** Always present torrent details and wait for explicit confirmation before calling it.
- **`get_search_results` filter params are ANDed.** Combining multiple fields can silently exclude valid results. If results come back empty, drop the most restrictive filter and retry before reporting failure.
- **`volume_factor` and `freedate_diff` indicate promotional status.** `volume_factor` describes the discount type (e.g. `免费` = free download, `2X` = double upload only, `2X免费` = free download + double upload, `普通` = no discount). `freedate_diff` is the remaining free window (e.g. `2天3小时`); empty means no active promotion. Always include both fields when presenting results — they are critical for the user to pick the best-value torrent.
## Common Workflows ## Common Workflows
@@ -49,41 +37,43 @@ Always run `show <command>` before calling a command. Do not guess parameter nam
# 1. Search TMDB to get tmdb_id # 1. Search TMDB to get tmdb_id
node scripts/mp-cli.js search_media title="流浪地球2" media_type="movie" node scripts/mp-cli.js search_media title="流浪地球2" media_type="movie"
# [TV only, only if user specified a season] Validate season — see "Season Validation" section below # [TV only, only if user specified a season] the user's season may not match TMDB; validate first, see "Season Validation" section below
node scripts/mp-cli.js query_media_detail tmdb_id=... media_type="tv" node scripts/mp-cli.js query_media_detail tmdb_id=... media_type="tv"
# 2. Search torrents using tmdb_id — results are cached server-side # 2. Search torrents — results are cached server-side; get_search_results reads from this cache
# Response includes available filter options (resolution, release group, etc.) # Omitting sites= uses the user's configured default sites; only pass it when explicitly requested
# [Optional] If the user specifies sites, first run query_sites to get IDs, then pass them via sites param node scripts/mp-cli.js query_sites # get site IDs (only if needed)
node scripts/mp-cli.js query_sites # get site IDs node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" # default sites
node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" # use user's default sites node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" sites='1,3' # specific sites
node scripts/mp-cli.js search_torrents tmdb_id=791373 media_type="movie" sites='1,3' # override with specific sites
# 3. Present ALL available filter_options to the user and ask which ones to apply # 3. Present the filter_options from step 2's response to the user and ask which ones to apply
# Show every field and its values — do not pre-select or omit any # Show every field and its values — do not pre-select or omit any
# e.g. "分辨率: 1080p, 2160p;字幕组: CMCT, PTer;请问需要筛选哪些条件?" # e.g. "分辨率: 1080p, 2160p;字幕组: CMCT, PTer;请问需要筛选哪些条件?"
# 4. Filter cached results based on user preferences and your own judgment # [Optional] If you need to review the available filter_options again without re-running search_torrents
# Filter params are ANDed — if results come back empty, drop the most restrictive field and retry
node scripts/mp-cli.js get_search_results resolution='1080p'
# [Optional] Re-check available filter options from cached results (same shape as search_torrents; returns filter options only)
node scripts/mp-cli.js get_search_results show_filter_options=true node scripts/mp-cli.js get_search_results show_filter_options=true
# 4. Filter cached results based on user preferences and your own judgment
# Each param is an array (OR within a field, AND across fields): resolution='1080p,2160p' free_state='免费,50%'
# IMPORTANT: filter_options keys are camelCase (freeState) but params are snake_case (free_state)
# If results come back empty, drop the most restrictive field and retry
node scripts/mp-cli.js get_search_results resolution='2160p'
node scripts/mp-cli.js get_search_results resolution='1080p,2160p' free_state='免费,50%'
# 5. Present ALL filtered results as a numbered list — do not pre-select or discard any # 5. Present ALL filtered results as a numbered list — do not pre-select or discard any
# Show for each: index, title, size, seeders, resolution, release group, volume_factor, freedate_diff # Show for each: index, title, size, seeders, resolution, release group, volume_factor, freedate_diff
# volume_factor: 免费=free download, 50%=download counted at 50% size, 2X=double upload, 2X免费=both, 普通=no discount
# freedate_diff: remaining free window (e.g. "2天3小时"); empty = no active promotion
# Let the user pick by number; only then proceed to step 6 # Let the user pick by number; only then proceed to step 6
# 6. After user confirms selection, check library and subscriptions before downloading # 6. After user confirms selection, check library and subscriptions before downloading
node scripts/mp-cli.js query_library_exists tmdb_id=123456 media_type="movie" node scripts/mp-cli.js query_library_exists tmdb_id=123456 media_type="movie" # or "tv"
node scripts/mp-cli.js query_subscribes tmdb_id=123456 node scripts/mp-cli.js query_subscribes tmdb_id=123456
# If already in library or subscribed, warn the user and ask for confirmation to proceed # If already in library or subscribed, warn the user and ask for confirmation to proceed
# 7. Add download # 7. Add download — this is irreversible without manual cleanup, always confirm with user first
# Single item: node scripts/mp-cli.js add_download torrent_url="abc1234:1" # single
node scripts/mp-cli.js add_download torrent_url="abc1234:1" node scripts/mp-cli.js add_download torrent_url="abc1234:1,def5678:2" # multiple
# Multiple items:
node scripts/mp-cli.js add_download torrent_url="abc1234:1,def5678:2"
``` ```
### Add Subscription ### Add Subscription
@@ -92,8 +82,15 @@ node scripts/mp-cli.js add_download torrent_url="abc1234:1,def5678:2"
# 1. Search to get tmdb_id (required for accurate identification) # 1. Search to get tmdb_id (required for accurate identification)
node scripts/mp-cli.js search_media title="黑镜" media_type="tv" node scripts/mp-cli.js search_media title="黑镜" media_type="tv"
# 2. Subscribe — the system will auto-download new episodes # 2. Check library and existing subscriptions (same as Search and Download step 6)
# 3. Subscribe — the system will auto-download new episodes
# If the user specified a season, run Season Validation first to get the correct season + start_episode
# Pass start_episode= when the user's requested season maps to a mid-season episode range on TMDB
# (e.g. fans call it "Season 2" but TMDB only has Season 1, episodes starting from ep 13)
node scripts/mp-cli.js add_subscribe title="黑镜" year="2011" media_type="tv" tmdb_id=42009 node scripts/mp-cli.js add_subscribe title="黑镜" year="2011" media_type="tv" tmdb_id=42009
node scripts/mp-cli.js add_subscribe title="黑镜" year="2011" media_type="tv" tmdb_id=42009 season=4 # specific season
node scripts/mp-cli.js add_subscribe title="某动漫" year="2024" media_type="tv" tmdb_id=12345 season=1 start_episode=13 # mid-season start
``` ```
### Manage Subscriptions ### Manage Subscriptions
@@ -107,7 +104,7 @@ node scripts/mp-cli.js delete_subscribe subscribe_id=123
## Season Validation (only when user specifies a season) ## Season Validation (only when user specifies a season)
Skip this section if the user did not mention a specific season. Productions sometimes release a show in multiple cours under one season; online communities and torrent sites may label each cour as a separate season.
**Step 1 — Verify the season exists:** **Step 1 — Verify the season exists:**
@@ -118,7 +115,7 @@ node scripts/mp-cli.js query_media_detail tmdb_id=<id> media_type="tv"
Check `season_info` against the season the user requested: Check `season_info` against the season the user requested:
- **Season exists:** use that season number directly, then proceed to torrent search. - **Season exists:** use that season number directly, then proceed to torrent search.
- **Season does not exist:** anime and long-running shows often split one TMDB season into multiple parts that fans call separate seasons. Use the latest available season number and continue to Step 2. - **Season does not exist:** use the latest available season number and continue to Step 2.
**Step 2 — Identify the correct episode range:** **Step 2 — Identify the correct episode range:**
@@ -126,12 +123,12 @@ Check `season_info` against the season the user requested:
node scripts/mp-cli.js query_episode_schedule tmdb_id=<id> season=<latest_season> node scripts/mp-cli.js query_episode_schedule tmdb_id=<id> season=<latest_season>
``` ```
Use `air_date` to find a block of recently-aired episodes that likely corresponds to what the user calls the missing season. If no such block exists, tell the user the content is unavailable. Otherwise, confirm the episode range with the user before proceeding to torrent search. Use `air_date` to find a block of recently-aired episodes that likely corresponds to what the user calls the missing season. If no such block exists, tell the user the content is unavailable. Otherwise, confirm the episode range with the user, then use the `season=` filter in `get_search_results` to narrow torrent results to that range (e.g. `season='S01 E13,S01 E14'`).
## Error Handling ## Error Handling
| Error | Resolution | **No search results:** Retry with an alternative title (e.g. English title). If still empty, ask the user to confirm the title or provide the TMDB ID directly.
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No search results | Retry with an alternative title (e.g. English title). If still empty, ask the user to confirm the title or provide the TMDB ID directly. | **Download failure:** Run `query_downloaders` to check downloader health, then `query_download_tasks` to check if the task already exists (duplicate tasks are rejected). If both are normal, report findings to the user and mention it may be a network error — suggest retrying later.
| Download failure | Run `query_downloaders` to check downloader health, then `query_download_tasks` to check if the task already exists (duplicate tasks are rejected). If both are normal, report findings to the user, suggest checking storage space, and mention it may be a network error — suggest retrying later. |
| Missing configuration | Ask the user for the backend host and API key. Once provided, run `node scripts/mp-cli.js -h <HOST> -k <KEY>` (no command) to save the config persistently — subsequent commands will use it automatically. | **Missing configuration:** Ask the user for the backend host and API key. Once provided, run `node scripts/mp-cli.js -h <HOST> -k <KEY>` (no command) to save the config persistently — subsequent commands will use it automatically.