diff --git a/package.v2.json b/package.v2.json index a8e11d1..f33e09c 100644 --- a/package.v2.json +++ b/package.v2.json @@ -93,11 +93,12 @@ "name": "媒体库服务器通知", "description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。", "labels": "消息通知,媒体库", - "version": "1.7.1", + "version": "1.8", "icon": "mediaplay.png", "author": "jxxghp", "level": 1, "history": { + "v1.8": "当整理路径中没有tmdbid时,会尝试从媒体服务器中获取", "v1.7.1": "未获取到tmdb信息则按原有逻辑发送;电影显示海报", "v1.7": "对TV剧集入库事件进行聚合,避免消息轰炸。更新后如果打不开插件,请重置插件", "v1.6": "查询剧集图片兼容没有季集信息的情况", diff --git a/plugins.v2/mediaservermsg/__init__.py b/plugins.v2/mediaservermsg/__init__.py index b3e038e..f195981 100644 --- a/plugins.v2/mediaservermsg/__init__.py +++ b/plugins.v2/mediaservermsg/__init__.py @@ -10,7 +10,7 @@ from app.helper.mediaserver import MediaServerHelper from app.log import logger from app.modules.themoviedb import CategoryHelper from app.plugins import _PluginBase -from app.schemas import WebhookEventInfo, ServiceInfo +from app.schemas import WebhookEventInfo, ServiceInfo, MediaServerItem from app.schemas.types import EventType, MediaType, MediaImageType, NotificationType from app.utils.web import WebUtils @@ -37,7 +37,7 @@ class MediaServerMsg(_PluginBase): # 插件图标 plugin_icon = "mediaplay.png" # 插件版本 - plugin_version = "1.7.1" + plugin_version = "1.8" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -407,152 +407,208 @@ class MediaServerMsg(_PluginBase): Args: event (Event): Webhook事件对象 """ - # 检查插件是否启用 - if not self._enabled: - logger.debug("插件未启用") - return + try: + # 检查插件是否启用 + if not self._enabled: + logger.debug("插件未启用") + return - # 获取事件数据 - event_info: WebhookEventInfo = event.event_data - if not event_info: - logger.debug("事件数据为空") - return + # 获取事件数据 + event_info: WebhookEventInfo = getattr(event, 'event_data', None) + if not event_info: + logger.debug("事件数据为空") + return - # 打印event_info用于调试 - logger.debug(f"收到Webhook事件: {event_info}") + # 打印event_info用于调试 + logger.debug(f"收到Webhook事件: {event_info}") - # 检查事件类型是否在支持范围内 - if not self._webhook_actions.get(event_info.event): - logger.debug(f"事件类型 {event_info.event} 不在支持范围内") - return + # 检查事件类型是否在支持范围内 + event_type = getattr(event_info, 'event', None) + if not event_type or not self._webhook_actions.get(event_type): + logger.debug(f"事件类型 {event_type} 不在支持范围内") + return - # 检查事件类型是否在用户配置的允许范围内 - # 将配置的类型预处理为一个扁平集合,提高查找效率 - allowed_types = set() - for _type in self._types: - allowed_types.update(_type.split("|")) + # 检查事件类型是否在用户配置的允许范围内 + # 将配置的类型预处理为一个扁平集合,提高查找效率 + allowed_types = set() + for _type in self._types: + allowed_types.update(_type.split("|")) - if event_info.event not in allowed_types: - logger.info(f"未开启 {event_info.event} 类型的消息通知") - return + if event_type not in allowed_types: + logger.info(f"未开启 {event_type} 类型的消息通知") + return - # 验证媒体服务器配置 - if not self.service_infos(): - logger.info(f"未开启任一媒体服务器的消息通知") - return + # 验证媒体服务器配置 + if not self.service_infos(): + logger.info(f"未开启任一媒体服务器的消息通知") + return - if event_info.server_name and not self.service_info(name=event_info.server_name): - logger.info(f"未开启媒体服务器 {event_info.server_name} 的消息通知") - return + server_name = getattr(event_info, 'server_name', None) + if server_name and not self.service_info(name=server_name): + logger.info(f"未开启媒体服务器 {server_name} 的消息通知") + return - if event_info.channel and not self.service_infos(type_filter=event_info.channel): - logger.info(f"未开启媒体服务器类型 {event_info.channel} 的消息通知") - return + channel = getattr(event_info, 'channel', None) + if channel and not self.service_infos(type_filter=channel): + logger.info(f"未开启媒体服务器类型 {channel} 的消息通知") + return - # TV剧集结入库聚合处理 - logger.debug("检查是否需要进行TV剧集聚合处理") - logger.debug(f"event_info.event={event_info.event}, item_type={event_info.item_type}") - logger.debug(f"json_object存在: {bool(event_info.json_object)}, 类型: {type(event_info.json_object)}") + # TV剧集结入库聚合处理 + logger.debug("检查是否需要进行TV剧集聚合处理") - # 判断是否需要进行TV剧集入库聚合处理 - if (self._aggregate_enabled and - event_info.event == "library.new" and - event_info.item_type in ["TV", "SHOW"] and - event_info.json_object and - isinstance(event_info.json_object, dict)): + def should_aggregate_tv() -> bool: + """判断是否需要进行TV剧集聚合处理""" + if not self._aggregate_enabled: + return False - logger.debug("满足TV剧集聚合条件,尝试获取series_id") - series_id = self._get_series_id(event_info) - logger.debug(f"获取到的series_id: {series_id}") - if series_id: - logger.debug(f"开始聚合处理,series_id={series_id}") - self._aggregate_tv_episodes(series_id, event_info) - logger.debug("TV剧集消息已处理并返回") - return # TV剧集消息已处理,直接返回 + if event_type != "library.new": + return False + + item_type = getattr(event_info, 'item_type', None) + if item_type not in ["TV", "SHOW"]: + return False + + json_object = getattr(event_info, 'json_object', None) + if not json_object or not isinstance(json_object, dict): + return False + + return True + + # 判断是否需要进行TV剧集入库聚合处理 + if should_aggregate_tv(): + logger.debug("满足TV剧集聚合条件,尝试获取series_id") + series_id = self._get_series_id(event_info) + logger.debug(f"获取到的series_id: {series_id}") + if series_id: + logger.debug(f"开始聚合处理,series_id={series_id}") + self._aggregate_tv_episodes(series_id, event_info) + logger.debug("TV剧集消息已处理并返回") + return # TV剧集消息已处理,直接返回 + else: + logger.debug("未能获取到有效的series_id") + + logger.debug("未进行聚合处理,继续普通消息处理流程") + item_id = getattr(event_info, 'item_id', '') + client = getattr(event_info, 'client', '') + user_name = getattr(event_info, 'user_name', '') + expiring_key = f"{item_id}-{client}-{user_name}" + + # 过滤停止播放重复消息 + if str(event_type) == "playback.stop" and expiring_key in self._webhook_msg_keys.keys(): + # 刷新过期时间 + self.__add_element(expiring_key) + return + + # 构造消息标题 + item_type = getattr(event_info, 'item_type', '') + item_name = getattr(event_info, 'item_name', '') + + message_title = "" + event_action = self._webhook_actions.get(event_type, event_type) + if item_type in ["TV", "SHOW"]: + message_title = f"{event_action}剧集 {item_name}" + elif item_type == "MOV": + message_title = f"{event_action}电影 {item_name}" + elif item_type == "AUD": + message_title = f"{event_action}有声书 {item_name}" else: - logger.debug("未能获取到有效的series_id") + message_title = f"{event_action}" - logger.debug("未进行聚合处理,继续普通消息处理流程") - expiring_key = f"{event_info.item_id}-{event_info.client}-{event_info.user_name}" - # 过滤停止播放重复消息 - if str(event_info.event) == "playback.stop" and expiring_key in self._webhook_msg_keys.keys(): - # 刷新过期时间 - self.__add_element(expiring_key) - return + # 构造消息内容 + message_texts = [] + user_name = getattr(event_info, 'user_name', None) + if user_name: + message_texts.append(f"用户:{user_name}") - # 构造消息标题 - if event_info.item_type in ["TV", "SHOW"]: - message_title = f"{self._webhook_actions.get(event_info.event)}剧集 {event_info.item_name}" - elif event_info.item_type == "MOV": - message_title = f"{self._webhook_actions.get(event_info.event)}电影 {event_info.item_name}" - elif event_info.item_type == "AUD": - message_title = f"{self._webhook_actions.get(event_info.event)}有声书 {event_info.item_name}" - else: - message_title = f"{self._webhook_actions.get(event_info.event)}" + device_name = getattr(event_info, 'device_name', None) + client = getattr(event_info, 'client', None) + if device_name: + message_texts.append(f"设备:{client or ''} {device_name}") + elif client: + message_texts.append(f"设备:{client}") - # 构造消息内容 - message_texts = [] - if event_info.user_name: - message_texts.append(f"用户:{event_info.user_name}") - if event_info.device_name: - message_texts.append(f"设备:{event_info.client} {event_info.device_name}") - if event_info.ip: - message_texts.append(f"IP地址:{event_info.ip} {WebUtils.get_location(event_info.ip)}") - if event_info.percentage: - percentage = round(float(event_info.percentage), 2) - message_texts.append(f"进度:{percentage}%") - if event_info.overview: - message_texts.append(f"剧情:{event_info.overview}") - message_texts.append(f"时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") + ip = getattr(event_info, 'ip', None) + if ip: + try: + location = WebUtils.get_location(ip) + message_texts.append(f"IP地址:{ip} {location}") + except Exception as e: + logger.debug(f"获取IP位置信息时出错: {str(e)}") + message_texts.append(f"IP地址:{ip}") - # 消息内容 - message_content = "\n".join(message_texts) + percentage = getattr(event_info, 'percentage', None) + if percentage: + try: + percentage_val = round(float(percentage), 2) + message_texts.append(f"进度:{percentage_val}%") + except (ValueError, TypeError): + pass + + overview = getattr(event_info, 'overview', None) + if overview: + message_texts.append(f"剧情:{overview}") + + message_texts.append(f"时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") + + # 消息内容 + message_content = "\n".join(message_texts) + + # 处理消息图片 + image_url = getattr(event_info, 'image_url', None) + tmdb_id = getattr(event_info, 'tmdb_id', None) + season_id = getattr(event_info, 'season_id', None) + episode_id = getattr(event_info, 'episode_id', None) - # 处理消息图片 - image_url = event_info.image_url - if not image_url and event_info.tmdb_id: # 查询电影图片 - if event_info.item_type == "MOV" : - image_url = self.chain.obtain_specific_image( - mediaid=event_info.tmdb_id, - mtype=MediaType.MOVIE, - image_type=MediaImageType.Poster - ) + if item_type == "MOV" and tmdb_id: + try: + image_url = self.chain.obtain_specific_image( + mediaid=tmdb_id, + mtype=MediaType.MOVIE, + image_type=MediaImageType.Poster + ) + except Exception as e: + logger.debug(f"获取电影图片时出错: {str(e)}") # 查询剧集图片 - elif event_info.item_type in ["TV", "SHOW"]: - season_id = event_info.season_id if event_info.season_id else None - episode_id = event_info.episode_id if event_info.episode_id else None + elif tmdb_id: + try: + specific_image = self.chain.obtain_specific_image( + mediaid=tmdb_id, + mtype=MediaType.TV, + image_type=MediaImageType.Backdrop, + season=season_id, + episode=episode_id + ) + if specific_image: + image_url = specific_image + except Exception as e: + logger.debug(f"获取剧集图片时出错: {str(e)}") - specific_image = self.chain.obtain_specific_image( - mediaid=event_info.tmdb_id, - mtype=MediaType.TV, - image_type=MediaImageType.Backdrop, - season=season_id, - episode=episode_id - ) - if specific_image: - image_url = specific_image - # 使用默认图片 - if not image_url: - image_url = self._webhook_images.get(event_info.channel) + # 使用默认图片 + if not image_url: + channel = getattr(event_info, 'channel', '') + image_url = self._webhook_images.get(channel) - # 处理播放链接 - play_link = None - if self._add_play_link: - play_link = self._get_play_link(event_info) + # 处理播放链接 + play_link = None + if self._add_play_link: + play_link = self._get_play_link(event_info) - # 更新播放状态缓存 - if str(event_info.event) == "playback.stop": - # 停止播放消息,添加到过期字典 - self.__add_element(expiring_key) - if str(event_info.event) == "playback.start": - # 开始播放消息,删除过期字典 - self.__remove_element(expiring_key) + # 更新播放状态缓存 + if str(event_type) == "playback.stop": + # 停止播放消息,添加到过期字典 + self.__add_element(expiring_key) + if str(event_type) == "playback.start": + # 开始播放消息,删除过期字典 + self.__remove_element(expiring_key) - # 发送消息 - self.post_message(mtype=NotificationType.MediaServer, - title=message_title, text=message_content, image=image_url, link=play_link) + # 发送消息 + self.post_message(mtype=NotificationType.MediaServer, + title=message_title, text=message_content, image=image_url, link=play_link) + + except Exception as e: + logger.error(f"处理Webhook事件时发生错误: {str(e)}", exc_info=True) def _get_series_id(self, event_info: WebhookEventInfo) -> Optional[str]: """ @@ -569,15 +625,23 @@ class MediaServerMsg(_PluginBase): Returns: Optional[str]: 剧集ID或None(如果无法获取) """ - # 从json_object中提取series_id - if event_info.json_object and isinstance(event_info.json_object, dict): - item = event_info.json_object.get("Item", {}) - series_id = item.get("SeriesId") or item.get("SeriesName") - if series_id: - return series_id + try: + # 从json_object中提取series_id + json_object = getattr(event_info, 'json_object', None) + if json_object and isinstance(json_object, dict): + item = json_object.get("Item", {}) + series_id = item.get("SeriesId") or item.get("SeriesName") + if series_id: + return str(series_id) - # fallback到event_info中的series_id - return getattr(event_info, "series_id", None) + # fallback到event_info中的series_id + series_id = getattr(event_info, "series_id", None) + if series_id: + return str(series_id) + except Exception as e: + logger.debug(f"获取剧集ID时出错: {str(e)}") + + return None def _aggregate_tv_episodes(self, series_id: str, event_info: WebhookEventInfo): """ @@ -593,6 +657,12 @@ class MediaServerMsg(_PluginBase): """ try: logger.debug(f"开始执行聚合处理: series_id={series_id}") + + # 参数校验 + if not series_id: + logger.warning("无效的series_id") + return + # 初始化该series_id的消息列表 if series_id not in self._pending_messages: logger.debug(f"为series_id={series_id}初始化消息列表") @@ -605,15 +675,23 @@ class MediaServerMsg(_PluginBase): # 如果已经有定时器,取消它并重新设置 if series_id in self._aggregate_timers: logger.debug(f"取消已存在的定时器: {series_id}") - self._aggregate_timers[series_id].cancel() + try: + self._aggregate_timers[series_id].cancel() + except Exception as e: + logger.debug(f"取消定时器时出错: {str(e)}") # 设置新的定时器 logger.debug(f"设置新的定时器,将在 {self._aggregate_time} 秒后触发") - timer = threading.Timer(self._aggregate_time, self._send_aggregated_message, [series_id]) - self._aggregate_timers[series_id] = timer - timer.start() + try: + timer = threading.Timer(self._aggregate_time, self._send_aggregated_message, [series_id]) + self._aggregate_timers[series_id] = timer + timer.start() + except Exception as e: + logger.error(f"设置定时器时出错: {str(e)}") + # 如果定时器设置失败,直接发送消息 + self._send_aggregated_message(series_id) - logger.debug(f"已添加剧集 {series_id} 的消息到聚合队列,当前队列长度: {len(self._pending_messages[series_id])},定时器将在 {self._aggregate_time} 秒后触发") + logger.debug(f"已添加剧集 {series_id} 的消息到聚合队列,当前队列长度: {len(self._pending_messages.get(series_id, []))},定时器将在 {self._aggregate_time} 秒后触发") logger.debug(f"完成聚合处理: series_id={series_id}") except Exception as e: logger.error(f"聚合处理过程中出现异常: {str(e)}", exc_info=True) @@ -634,180 +712,250 @@ class MediaServerMsg(_PluginBase): if series_id not in self._pending_messages or not self._pending_messages[series_id]: logger.debug(f"消息队列为空或不存在: {series_id}") # 清除定时器引用 - if series_id in self._aggregate_timers: - del self._aggregate_timers[series_id] + self._aggregate_timers.pop(series_id, None) return events = self._pending_messages.pop(series_id) logger.debug(f"从队列中获取 {len(events)} 条消息: {series_id}") # 清除定时器引用 - if series_id in self._aggregate_timers: - del self._aggregate_timers[series_id] + self._aggregate_timers.pop(series_id, None) # 构造聚合消息 if not events: logger.debug(f"事件列表为空: {series_id}") return - # 使用第一个事件的信息作为基础 - first_event = events[0] - - # 预计算事件数量,避免重复调用len(events) - events_count = len(events) - is_multiple_episodes = events_count > 1 - - # 尝试从item_path中提取tmdb_id - tmdb_pattern = r'[\[{](?:tmdbid|tmdb)[=-](\d+)[\]}]' - if match := re.search(tmdb_pattern, first_event.item_path): - first_event.tmdb_id = match.group(1) - logger.info(f"从路径提取到tmdb_id: {first_event.tmdb_id}") - else: - logger.info(f"未从路径中提取到tmdb_id: {first_event.item_path}") - # 通过TMDB ID获取详细信息 - tmdb_info = None - overview = None try: - if not first_event.tmdb_id: - logger.debug("tmdb_id为空,使用原有逻辑发送消息") - # 使用原有逻辑构造消息 - message_title = f"📺 {self._webhook_actions.get(first_event.event)}剧集:{first_event.item_name}" - message_texts = [] - message_texts.append(f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") + # 使用第一个事件的信息作为基础 + first_event = events[0] - # 收集集数信息 - episode_details = [] - for event in events: - if event.season_id is not None and event.episode_id is not None: - episode_details.append(f"S{int(event.season_id):02d}E{int(event.episode_id):02d}") + # 预计算事件数量,避免重复调用len(events) + events_count = len(events) + is_multiple_episodes = events_count > 1 - if episode_details: - message_texts.append(f"📺 季集:{', '.join(episode_details)}") - - message_content = "\n".join(message_texts) - - # 使用默认图片 - image_url = first_event.image_url or self._webhook_images.get(first_event.channel) - - # 处理播放链接 - play_link = None - if self._add_play_link: - play_link = self._get_play_link(first_event) - - # 发送消息 - self.post_message(mtype=NotificationType.MediaServer, - title=message_title, - text=message_content, - image=image_url, - link=play_link) - return - if first_event.item_type in ["TV", "SHOW"]: - logger.debug("查询TV类型的TMDB信息") - tmdb_info = self._get_tmdb_info( - tmdb_id=first_event.tmdb_id, - mtype=MediaType.TV, - season=first_event.season_id - ) - logger.debug(f"从TMDB获取到的信息: {tmdb_info}") - except Exception as e: - logger.debug(f"获取TMDB信息时出错: {str(e)}") - - if first_event.overview: - overview = first_event.overview - elif tmdb_info: - if is_multiple_episodes: - if tmdb_info.get('overview'): - overview = tmdb_info.get('overview') - logger.debug(f"从TMDB获取到overview: {overview}") - else: - logger.debug("未能从TMDB获取到有效的overview信息") + # 尝试从item_path中提取tmdb_id + tmdb_pattern = r'[\[{](?:tmdbid|tmdb)[=-](\d+)[\]}]' + if match := re.search(tmdb_pattern, first_event.item_path): + first_event.tmdb_id = match.group(1) + logger.info(f"从路径提取到tmdb_id: {first_event.tmdb_id}") else: - if (tmdb_info.get('episodes') and tmdb_info.get('episodes')[int(first_event.episode_id)-1] - and tmdb_info.get('episodes')[int(first_event.episode_id)-1].get('overview')): - overview = tmdb_info.get('episodes')[int(first_event.episode_id)-1].get('overview') - elif tmdb_info.get('overview'): - overview = tmdb_info.get('overview') - else: - logger.debug("未能从TMDB获取到有效的overview信息") - else: - logger.debug("未能从TMDB获取到有效的overview信息") + logger.info(f"未从路径中提取到tmdb_id: {first_event.item_path}") + logger.info(f"尝试从媒体服务获取tmdb_id") + # 获取渠道并尝试获取媒体服务 + media_service = self.service_info(name=first_event.server_name) + if media_service: + service = media_service.instance + # 从first_event中获取item_id + item_id = first_event.item_id + if service and item_id: + info: MediaServerItem = service.get_iteminfo(item_id) + if info and info.tmdbid: + logger.info(f"从媒体服务器中获取到tmdb_id: {info.tmdbid}") + first_event.tmdb_id = info.tmdbid + else: + logger.info(f"从媒体服务器中未获取到tmdb_id") - events[0] = first_event - # 消息标题 - message_title = f"📺 {self._webhook_actions.get(first_event.event)}剧集:{first_event.item_name.split(' ', 1)[0]}" + # 通过TMDB ID获取详细信息 + tmdb_info = None + overview = None - if is_multiple_episodes: - message_title += f" 等{events_count}个文件" - - logger.debug(f"构建消息标题: {message_title}") - - # 消息内容 - message_texts = [] - # 时间信息放在最前面 - message_texts.append(f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") - # 添加每个集数的信息并合并连续集数 - episodes_detail = self._merge_continuous_episodes(events) - message_texts.append(f"📺 季集:{episodes_detail}") - # 确定二级分类 - cat = None - if tmdb_info.get('media_type') == MediaType.TV: - cat = self.category.get_tv_category(tmdb_info) - else: - cat = self.category.get_movie_category(tmdb_info) - if cat: - message_texts.append(f"📚 分类:{cat}") - # 评分信息 - if tmdb_info and tmdb_info.get('vote_average'): - rating = round(float(tmdb_info.get('vote_average')), 1) - message_texts.append(f"⭐ 评分:{rating}/10") - # 类型信息 - genres可能是字典列表或字符串列表 - if tmdb_info.get('genres'): - genres_list = [] - for genre in tmdb_info.get('genres')[:3]: - if isinstance(genre, dict): - genres_list.append(genre.get('name', '')) + # 安全地获取概述信息 + def safe_get_overview(tmdb_data, event_data, multiple_eps): + """安全地获取剧集概述""" + if event_data.overview: + return event_data.overview + elif tmdb_data: + if multiple_eps: + return tmdb_data.get('overview', '') else: - genres_list.append(str(genre)) - if genres_list: - genre_text = '、'.join(genres_list) - message_texts.append(f"🎭 类型:{genre_text}") - if overview: - # 限制overview只显示前100个字符,超出部分用...代替 - if len(overview) > 100: - overview = overview[:100] + "..." - message_texts.append(f"📖 剧情:{overview}") + # 单集情况下尝试获取具体集数的概述 + episodes = tmdb_data.get('episodes', []) + if (episodes and + hasattr(event_data, 'episode_id') and + event_data.episode_id is not None): + try: + ep_index = int(event_data.episode_id) - 1 + if 0 <= ep_index < len(episodes): + episode_info = episodes[ep_index] + return episode_info.get('overview', tmdb_data.get('overview', '')) + except (ValueError, TypeError): + pass + # 如果无法获取单集概述,回退到剧集整体概述 + return tmdb_data.get('overview', '') + return '' - # 消息内容 - message_content = "\n".join(message_texts) - logger.debug(f"构建消息内容: {message_content}") + try: + if not first_event.tmdb_id: + logger.debug("tmdb_id为空,使用原有逻辑发送消息") + # 使用原有逻辑构造消息 + message_title = f"📺 {self._webhook_actions.get(first_event.event)}剧集:{first_event.item_name}" + message_texts = [] + message_texts.append(f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") - # 消息图片 - image_url = first_event.image_url - logger.debug(f"初始图片URL: {image_url}") + # 收集集数信息 + episode_details = [] + for event in events: + if (hasattr(event, 'season_id') and event.season_id is not None and + hasattr(event, 'episode_id') and event.episode_id is not None): + try: + episode_details.append(f"S{int(event.season_id):02d}E{int(event.episode_id):02d}") + except (ValueError, TypeError): + pass - if not image_url and tmdb_info and tmdb_info.get('poster_path') and not is_multiple_episodes: - # 剧集图片 - image_url = self.backdrop_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{tmdb_info.get('poster_path')}" - logger.debug(f"使用剧集图片URL: {image_url}") - elif not image_url and tmdb_info and tmdb_info.get('backdrop_path') and is_multiple_episodes: - # 使用TMDB背景 - image_url = self.backdrop_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{tmdb_info.get('backdrop_path')}" - logger.debug(f"使用TMDB背景URL: {image_url}") - # 使用默认图片 - if not image_url: - image_url = self._webhook_images.get(first_event.channel) - logger.debug(f"使用默认图片URL: {image_url}") + if episode_details: + message_texts.append(f"📺 季集:{', '.join(episode_details)}") - # 处理播放链接 - play_link = None - if self._add_play_link: - play_link = self._get_play_link(first_event) + message_content = "\n".join(message_texts) - # 发送聚合消息 - logger.debug(f"准备发送消息 - 标题: {message_title}, 内容: {message_content}, 图片: {image_url}") - self.post_message(mtype=NotificationType.MediaServer, - title=message_title, text=message_content, image=image_url, link=play_link) + # 使用默认图片 + image_url = getattr(first_event, 'image_url', None) or self._webhook_images.get(getattr(first_event, 'channel', '')) - logger.info(f"已发送聚合消息:{message_title}") + # 处理播放链接 + play_link = None + if self._add_play_link: + play_link = self._get_play_link(first_event) + + # 发送消息 + self.post_message(mtype=NotificationType.MediaServer, + title=message_title, + text=message_content, + image=image_url, + link=play_link) + return + + if first_event.item_type in ["TV", "SHOW"]: + logger.debug("查询TV类型的TMDB信息") + tmdb_info = self._get_tmdb_info( + tmdb_id=first_event.tmdb_id, + mtype=MediaType.TV, + season=first_event.season_id + ) + logger.debug(f"从TMDB获取到的信息: {tmdb_info}") + except Exception as e: + logger.error(f"获取TMDB信息时出错: {str(e)}") + + overview = safe_get_overview(tmdb_info, first_event, is_multiple_episodes) + + # 消息标题 + show_name = first_event.item_name + # 从json_object中提取SeriesName作为剧集名称 + try: + if (hasattr(first_event, 'json_object') and + first_event.json_object and + isinstance(first_event.json_object, dict)): + item = first_event.json_object.get("Item", {}) + series_name = item.get("SeriesName") + if series_name: + show_name = series_name + except Exception as e: + logger.error(f"从json_object提取SeriesName时出错: {str(e)}") + + message_title = f"📺 {self._webhook_actions.get(first_event.event, '新入库')}剧集:{show_name}" + + if is_multiple_episodes: + message_title += f" {events_count}个文件" + + logger.debug(f"构建消息标题: {message_title}") + + # 消息内容 + message_texts = [] + # 时间信息放在最前面 + message_texts.append(f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") + # 添加每个集数的信息并合并连续集数 + episodes_detail = self._merge_continuous_episodes(events) + message_texts.append(f"📺 季集:{episodes_detail}") + + # 确定二级分类 + cat = None + if tmdb_info: + try: + if tmdb_info.get('media_type') == MediaType.TV: + cat = self.category.get_tv_category(tmdb_info) + else: + cat = self.category.get_movie_category(tmdb_info) + except Exception as e: + logger.debug(f"获取分类时出错: {str(e)}") + + if cat: + message_texts.append(f"📚 分类:{cat}") + + # 评分信息 + if tmdb_info and tmdb_info.get('vote_average'): + try: + rating = round(float(tmdb_info.get('vote_average')), 1) + message_texts.append(f"⭐ 评分:{rating}/10") + except (ValueError, TypeError): + pass + + # 类型信息 - genres可能是字典列表或字符串列表 + genres = tmdb_info.get('genres', []) + if genres: + try: + genres_list = [] + for genre in genres[:3]: + if isinstance(genre, dict): + genre_name = genre.get('name', '') + if genre_name: + genres_list.append(genre_name) + else: + genre_str = str(genre) + if genre_str: + genres_list.append(genre_str) + if genres_list: + genre_text = '、'.join(genres_list) + message_texts.append(f"🎭 类型:{genre_text}") + except Exception as e: + logger.debug(f"处理类型信息时出错: {str(e)}") + + if overview: + # 限制overview只显示前100个字符,超出部分用...代替 + try: + if len(overview) > 100: + overview = overview[:100] + "..." + message_texts.append(f"📖 剧情:{overview}") + except Exception as e: + logger.debug(f"处理剧情简介时出错: {str(e)}") + + # 消息内容 + message_content = "\n".join(message_texts) + logger.debug(f"构建消息内容: {message_content}") + + # 消息图片 + image_url = getattr(first_event, 'image_url', None) + logger.debug(f"初始图片URL: {image_url}") + + if not image_url and tmdb_info: + try: + if not is_multiple_episodes and tmdb_info.get('poster_path'): + # 剧集图片 + image_url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{tmdb_info.get('poster_path')}" + logger.debug(f"使用剧集图片URL: {image_url}") + elif is_multiple_episodes and tmdb_info.get('backdrop_path'): + # 使用TMDB背景 + image_url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{tmdb_info.get('backdrop_path')}" + logger.debug(f"使用TMDB背景URL: {image_url}") + except Exception as e: + logger.debug(f"处理图片URL时出错: {str(e)}") + + # 使用默认图片 + if not image_url: + channel = getattr(first_event, 'channel', '') + image_url = self._webhook_images.get(channel) + logger.debug(f"使用默认图片URL: {image_url}") + + # 处理播放链接 + play_link = None + if self._add_play_link: + play_link = self._get_play_link(first_event) + + # 发送聚合消息 + logger.debug(f"准备发送消息 - 标题: {message_title}, 内容: {message_content}, 图片: {image_url}") + self.post_message(mtype=NotificationType.MediaServer, + title=message_title, text=message_content, image=image_url, link=play_link) + + logger.info(f"已发送聚合消息:{message_title}") + except Exception as e: + logger.error(f"发送聚合消息时发生错误: {str(e)}", exc_info=True) def _merge_continuous_episodes(self, events: List[WebhookEventInfo]) -> str: """ @@ -824,81 +972,116 @@ class MediaServerMsg(_PluginBase): """ # 按季分组集数信息 season_episodes = {} - tmdb_info = self._get_tmdb_info( - tmdb_id=events[0].tmdb_id, - mtype=MediaType.TV, - season=events[0].season_id - ) + + # 安全获取tmdb_info + tmdb_info = {} + try: + if events and hasattr(events[0], 'tmdb_id') and events[0].tmdb_id: + tmdb_info = self._get_tmdb_info( + tmdb_id=events[0].tmdb_id, + mtype=MediaType.TV, + season=events[0].season_id + ) or {} + except Exception as e: + logger.debug(f"获取TMDB信息时出错: {str(e)}") + for event in events: # 提取季号和集号 season, episode = None, None episode_name = "" - if event.json_object and isinstance(event.json_object, dict): - item = event.json_object.get("Item", {}) - season = item.get("ParentIndexNumber") - episode = item.get("IndexNumber") - if episode is not None and int(episode) <= len(tmdb_info.get('episodes')): - episode_name = tmdb_info.get("episodes")[int(episode)-1].get('name') - else: - episode_name = item.get("Name", "") + try: + if (hasattr(event, 'json_object') and + event.json_object and + isinstance(event.json_object, dict)): + item = event.json_object.get("Item", {}) + season = item.get("ParentIndexNumber") + episode = item.get("IndexNumber") - # 如果无法从json_object获取信息,则尝试从event_info直接获取 - if season is None: - season = getattr(event, "season_id", None) - if episode is None: - episode = getattr(event, "episode_id", None) - if not episode_name: - episode_name = getattr(event, "item_name", "") + # 安全地获取剧集名称 + if episode is not None: + try: + episodes_list = tmdb_info.get('episodes', []) + ep_index = int(episode) - 1 + if 0 <= ep_index < len(episodes_list): + episode_data = episodes_list[ep_index] + episode_name = episode_data.get('name', '') + except (ValueError, TypeError, IndexError): + pass - # 确保季号和集号都存在 - if season is not None and episode is not None: - if season not in season_episodes: - season_episodes[season] = [] - season_episodes[season].append({ - "episode": episode, - "name": episode_name - }) + if not episode_name: + episode_name = item.get("Name", "") + # 如果无法从json_object获取信息,则尝试从event_info直接获取 + if season is None: + season = getattr(event, "season_id", None) + if episode is None: + episode = getattr(event, "episode_id", None) + if not episode_name: + episode_name = getattr(event, "item_name", "") + + # 确保季号和集号都存在 + if season is not None and episode is not None: + season_key = int(season) + episode_key = int(episode) + + if season_key not in season_episodes: + season_episodes[season_key] = [] + season_episodes[season_key].append({ + "episode": episode_key, + "name": episode_name or "" + }) + except Exception as e: + logger.debug(f"处理事件信息时出错: {str(e)}") + continue # 对每季的集数进行排序并合并连续区间 merged_details = [] - for season in sorted(season_episodes.keys()): - episodes = season_episodes[season] - # 按集号排序 - episodes.sort(key=lambda x: x["episode"]) + try: + for season in sorted(season_episodes.keys()): + episodes = season_episodes[season] + # 按集号排序 + episodes.sort(key=lambda x: x["episode"]) - # 合并连续集数 - if not episodes: - continue + # 合并连续集数 + if not episodes: + continue - # 初始化第一个区间 - start = episodes[0]["episode"] - end = episodes[0]["episode"] - episode_names = [episodes[0]["name"]] + # 初始化第一个区间 + start = episodes[0]["episode"] + end = episodes[0]["episode"] + episode_names = [episodes[0]["name"]] - for i in range(1, len(episodes)): - current = episodes[i]["episode"] - # 如果当前集号与上一集连续 - if current == end + 1: - end = current - episode_names.append(episodes[i]["name"]) - else: - # 保存当前区间 - if start == end: - merged_details.append(f"S{season:02d}E{start:02d} {episode_names[0]}") + for i in range(1, len(episodes)): + current = episodes[i]["episode"] + # 如果当前集号与上一集连续 + if current == end + 1: + end = current + episode_names.append(episodes[i]["name"]) else: - # 合并区间 - merged_details.append(f"S{season:02d}E{start:02d}-E{end:02d}") - # 开始新区间 - start = end = current - episode_names = [episodes[i]["name"]] + # 保存当前区间 + if start == end: + merged_details.append(f"S{season:02d}E{start:02d} {episode_names[0]}") + else: + # 合并区间 + merged_details.append(f"S{season:02d}E{start:02d}-E{end:02d}") + # 开始新区间 + start = end = current + episode_names = [episodes[i]["name"]] - # 添加最后一个区间 - if start == end: - merged_details.append(f"S{season:02d}E{start:02d} {episode_names[-1]}") - else: - merged_details.append(f"S{season:02d}E{start:02d}-E{end:02d}") + # 添加最后一个区间 + if start == end: + merged_details.append(f"S{season:02d}E{start:02d} {episode_names[-1] if episode_names else ''}") + else: + merged_details.append(f"S{season:02d}E{start:02d}-E{end:02d}") + except Exception as e: + logger.error(f"合并集数信息时出错: {str(e)}") + # 出错时返回简单的集数列表 + simple_details = [] + for season in sorted(season_episodes.keys()): + for episode_info in season_episodes[season]: + simple_details.append(f"S{season:02d}E{episode_info['episode']:02d}") + return ", ".join(simple_details) return ", ".join(merged_details) @@ -930,22 +1113,31 @@ class MediaServerMsg(_PluginBase): Returns: List[str]: 未过期的元素键值列表 """ - current_time = time.time() - # 创建新的字典,只保留未过期的元素 - valid_keys = [] - expired_keys = [] + try: + current_time = time.time() + # 创建新的字典,只保留未过期的元素 + valid_keys = [] + expired_keys = [] - for key, expiration_time in self._webhook_msg_keys.items(): - if expiration_time > current_time: - valid_keys.append(key) - else: - expired_keys.append(key) + for key, expiration_time in self._webhook_msg_keys.items(): + try: + if expiration_time > current_time: + valid_keys.append(key) + else: + expired_keys.append(key) + except Exception as e: + logger.debug(f"检查过期时间时出错: {str(e)}") + # 出错时保守处理,认为已过期 + expired_keys.append(key) - # 从字典中移除过期元素 - for key in expired_keys: - del self._webhook_msg_keys[key] + # 从字典中移除过期元素 + for key in expired_keys: + self._webhook_msg_keys.pop(key, None) - return valid_keys + return valid_keys + except Exception as e: + logger.error(f"获取有效元素时出错: {str(e)}") + return [] def _get_play_link(self, event_info: WebhookEventInfo) -> Optional[str]: """ @@ -957,19 +1149,40 @@ class MediaServerMsg(_PluginBase): Returns: Optional[str]: 播放链接,如果无法获取则返回None """ - play_link = None - if event_info.server_name: - service = self.service_infos().get(event_info.server_name) - if service: - play_link = service.instance.get_play_url(event_info.item_id) - elif event_info.channel: - services = MediaServerHelper().get_services(type_filter=event_info.channel) - for service in services.values(): - play_link = service.instance.get_play_url(event_info.item_id) - if play_link: - break + try: + server_name = getattr(event_info, 'server_name', None) + item_id = getattr(event_info, 'item_id', None) - return play_link + if not item_id: + return None + + if server_name: + service = self.service_infos().get(server_name) if self.service_infos() else None + if service: + try: + return service.instance.get_play_url(item_id) + except Exception as e: + logger.debug(f"获取播放链接时出错: {str(e)}") + + channel = getattr(event_info, 'channel', None) + if channel: + try: + services = MediaServerHelper().get_services(type_filter=channel) + for service in services.values(): + try: + play_link = service.instance.get_play_url(item_id) + if play_link: + return play_link + except Exception as e: + logger.debug(f"从{service}获取播放链接时出错: {str(e)}") + continue + except Exception as e: + logger.debug(f"获取媒体服务器服务时出错: {str(e)}") + + except Exception as e: + logger.debug(f"获取播放链接时发生未知错误: {str(e)}") + + return None @cached( region="MediaServerMsg", # 缓存区域,用于隔离不同插件的缓存 @@ -1007,14 +1220,30 @@ class MediaServerMsg(_PluginBase): 2. 所有正在进行的定时器被取消 3. 清空所有内部缓存数据 """ - # 发送所有待处理的聚合消息 - for series_id in list(self._pending_messages.keys()): - # 直接发送消息而不依赖定时器 - self._send_aggregated_message(series_id) + try: + # 发送所有待处理的聚合消息 + pending_series_ids = list(self._pending_messages.keys()) + for series_id in pending_series_ids: + # 直接发送消息而不依赖定时器 + try: + self._send_aggregated_message(series_id) + except Exception as e: + logger.error(f"发送聚合消息时出错: {str(e)}") - # 取消所有定时器 - for timer in self._aggregate_timers.values(): - timer.cancel() - self._aggregate_timers.clear() - self._pending_messages.clear() - self._get_tmdb_info.cache_clear() + # 取消所有定时器 + for timer in self._aggregate_timers.values(): + try: + timer.cancel() + except Exception as e: + logger.debug(f"取消定时器时出错: {str(e)}") + + self._aggregate_timers.clear() + self._pending_messages.clear() + + # 清理缓存 + try: + self._get_tmdb_info.cache_clear() + except Exception as e: + logger.debug(f"清理缓存时出错: {str(e)}") + except Exception as e: + logger.error(f"插件停止时发生错误: {str(e)}", exc_info=True)