"""查询订阅工具""" import json from typing import Optional, Type from pydantic import BaseModel, Field from app.agent.tools.base import MoviePilotTool from app.agent.tools.tags import ToolTag from app.application.agentdata import SubscribePort as SubscribeOper from app.runtime.log import logger from app.schemas.subscribe import Subscribe as SubscribeSchema from app.schemas.types import ( MUSIC_ENTITY_RECORDING, MediaSource, MediaType, media_type_to_agent, ) from app.domain.media import normalize_music_type PAGE_SIZE = 100 QUERY_SUBSCRIBE_OUTPUT_FIELDS = [ "id", "name", "year", "type", "media_source", "media_id", "music_type", "total_tracks", "season", "total_episode", "start_episode", "lack_episode", "filter", "include", "exclude", "quality", "resolution", "effect", "audio_quality", "audio_format", "min_bitrate", "min_bit_depth", "min_sample_rate", "state", "last_update", "sites", "downloader", "best_version", "best_version_full", "current_priority", "current_audio_format", "current_bitrate", "current_bit_depth", "current_sample_rate", "episode_priority", "save_path", "custom_words", "media_category", "filter_groups", "episode_group", "poster", "backdrop", "description", "username", ] class QuerySubscribesInput(BaseModel): """查询订阅工具的输入参数模型""" status: Optional[str] = Field( "all", description="Filter subscriptions by status: 'R' for enabled subscriptions, 'S' for paused ones, 'all' for all subscriptions", ) media_type: Optional[str] = Field( "all", description="Allowed values: movie, tv, music, all" ) music_type: Optional[str] = Field( None, description="Optional music subscription filter: recording or album", ) media_source: Optional[MediaSource] = Field( None, description="Filter by media source" ) media_id: Optional[str] = Field(None, description="Filter by source-native media ID") page: Optional[int] = Field( 1, description="Page number for pagination (default: 1, 100 items per page)" ) class QuerySubscribesTool(MoviePilotTool): """查询电影、电视剧、单曲与专辑订阅。""" name: str = "query_subscribes" tags: list[str] = [ ToolTag.Read, ToolTag.Subscription, ] description: str = "Query subscription status and list user subscriptions. Returns full subscription parameters for each matched subscription. Supports pagination with 100 items per page." args_schema: Type[BaseModel] = QuerySubscribesInput def get_tool_message(self, **kwargs) -> Optional[str]: """根据查询参数生成友好的提示消息""" status = kwargs.get("status", "all") media_type = kwargs.get("media_type", "all") page = kwargs.get("page", 1) parts = ["查询订阅"] # 根据状态过滤条件生成提示 if status != "all": status_map = {"R": "已启用", "S": "已暂停"} parts.append(f"状态: {status_map.get(status, status)}") # 根据媒体类型过滤条件生成提示 if media_type != "all": parts.append(f"类型: {media_type}") parts.append(f"第{page}页") return " | ".join(parts) async def run( self, status: Optional[str] = "all", media_type: Optional[str] = "all", music_type: Optional[str] = None, media_source: Optional[MediaSource] = None, media_id: Optional[str] = None, page: Optional[int] = 1, **kwargs, ) -> str: """按状态、媒体身份及音乐实体类型筛选订阅。""" page = max(1, page or 1) logger.info( f"执行工具: {self.name}, 参数: status={status}, " f"media_type={media_type}, media_source={media_source}, " f"media_id={media_id}, page={page}" ) try: if media_type != "all" and not MediaType.from_agent(media_type): return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv', 'music', 'all'" normalized_music_type = None if music_type: normalized_music_type = normalize_music_type( music_type, allow_artist=False, ) if not normalized_music_type: return ( f"错误:无效的音乐实体类型 '{music_type}'," "支持的类型:'recording', 'album'" ) if media_type not in ("all", "music"): return "错误:music_type 仅能与 media_type='music' 或 'all' 一起使用" subscribe_oper = SubscribeOper() subscribes = await subscribe_oper.async_list() filtered_subscribes = [] for sub in subscribes: if status != "all" and sub.state != status: continue if ( media_type != "all" and sub.type != MediaType.from_agent(media_type).value ): continue if media_source is not None and sub.media_source != str(media_source): continue if media_id is not None and sub.media_id != media_id: continue if normalized_music_type: sub_music_type = sub.music_type or MUSIC_ENTITY_RECORDING if sub_music_type != normalized_music_type: continue filtered_subscribes.append(sub) if filtered_subscribes: total_count = len(filtered_subscribes) # 分页 start = (page - 1) * PAGE_SIZE end = start + PAGE_SIZE page_subscribes = filtered_subscribes[start:end] if not page_subscribes: total_pages = (total_count + PAGE_SIZE - 1) // PAGE_SIZE return f"第 {page} 页没有数据,共 {total_count} 条结果,共 {total_pages} 页。" full_subscribes = [] for subscribe in page_subscribes: payload = SubscribeSchema.model_validate( subscribe, from_attributes=True, ).model_dump( include=set(QUERY_SUBSCRIBE_OUTPUT_FIELDS), exclude_none=True ) # 手动总集数是运行锁状态,不属于公共订阅写入 Schema,查询时直接从实体读取。 payload["manual_total_episode"] = subscribe.manual_total_episode or 0 payload["type"] = media_type_to_agent(payload.get("type")) if payload["type"] == "music" and not payload.get("music_type"): payload["music_type"] = MUSIC_ENTITY_RECORDING full_subscribes.append(payload) result_json = json.dumps(full_subscribes, ensure_ascii=False, indent=2) total_pages = (total_count + PAGE_SIZE - 1) // PAGE_SIZE payload_msg = f"第 {page}/{total_pages} 页,当前页 {len(page_subscribes)} 条结果,共 {total_count} 条。" if page < total_pages: payload_msg += f" 可使用 page={page + 1} 获取下一页。" return f"{payload_msg}\n\n{result_json}" return "未找到相关订阅" except Exception as e: logger.error(f"查询订阅失败: {e}", exc_info=True) return f"查询订阅时发生错误: {str(e)}"