From 0773bff16d7176229cc4563cd9326b4c46cd559b Mon Sep 17 00:00:00 2001 From: XiLin Date: Thu, 10 Oct 2024 13:16:48 +0800 Subject: [PATCH] add dingdingmsg --- icons/Dingding_A.png | Bin 0 -> 15938 bytes package.json | 10 + package.v2.json | 12 ++ plugins.v2/downloadsitetag/__init__.py | 165 ++++++++++----- plugins/dingdingmsg/__init__.py | 269 +++++++++++++++++++++++++ 5 files changed, 405 insertions(+), 51 deletions(-) create mode 100644 icons/Dingding_A.png create mode 100644 plugins/dingdingmsg/__init__.py diff --git a/icons/Dingding_A.png b/icons/Dingding_A.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa27a65ee8ad490210fefb6291cc760606218aa GIT binary patch literal 15938 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 Optional[Dict[str, ServiceInfo]]: + """ + 服务信息 + """ + if not self._downloaders: + logger.warning("尚未配置下载器,请检查配置") + return None + + services = self.downloaderhelper.get_services(name_filters=self._downloaders) + if not services: + logger.warning("获取下载器实例失败,请检查配置") + return None + + active_services = {} + for service_name, service_info in services.items(): + if service_info.instance.is_inactive(): + logger.warning(f"下载器 {service_name} 未连接,请检查配置") + else: + active_services[service_name] = service_info + + if not active_services: + logger.warning("没有已连接的下载器,请检查配置") + return None + + return active_services + + # endregion def init_plugin(self, config: dict = None): - self.downloader_qb = Qbittorrent() - self.downloader_tr = Transmission() self.downloadhistory_oper = DownloadHistoryOper() + self.downloaderhelper = DownloaderHelper() self.sites_helper = SitesHelper() # 读取配置 if config: @@ -81,14 +114,7 @@ class DownloadSiteTag(_PluginBase): self._category_movie = config.get("category_movie") or "电影" self._category_tv = config.get("category_tv") or "电视" self._category_anime = config.get("category_anime") or "动漫" - if not ("interval_cron" in config): - # 新版本v1.6更新插件配置默认配置 - config["interval"] = self._interval - config["interval_cron"] = self._interval_cron - config["interval_time"] = self._interval_time - config["interval_unit"] = self._interval_unit - self.update_config(config) - logger.warn(f"{self.LOG_TAG}新版本v{self.plugin_version} 配置修正 ...") + self._downloaders = config.get("downloaders") # 停止现有任务 self.stop_service() @@ -179,6 +205,8 @@ class DownloadSiteTag(_PluginBase): """ 补全下载历史的标签与分类 """ + if not self.service_infos: + return logger.info(f"{self.LOG_TAG}开始执行 ...") # 记录处理的种子, 供辅种(无下载历史)使用 dispose_history = {} @@ -192,21 +220,21 @@ class DownloadSiteTag(_PluginBase): "agsvpt.trackers.work": "agsvpt.com", "tracker.cinefiles.info": "audiences.me", } - for DOWNLOADER in ["qbittorrent", "transmission"]: - logger.info(f"{self.LOG_TAG}开始扫描下载器 {DOWNLOADER} ...") + for name, service in self.service_infos.items(): + logger.info(f"{self.LOG_TAG}开始扫描下载器 {name} ...") # 获取下载器中的种子 - downloader_obj = self._get_downloader(DOWNLOADER) + downloader_obj = service.instance if not downloader_obj: - logger.error(f"{self.LOG_TAG} 获取下载器失败 {DOWNLOADER}") + logger.error(f"{self.LOG_TAG} 获取下载器失败 {name}") continue torrents, error = downloader_obj.get_torrents() # 如果下载器获取种子发生错误 或 没有种子 则跳过 if error or not torrents: continue - logger.info(f"{self.LOG_TAG}按时间重新排序 {DOWNLOADER} 种子数:{len(torrents)}") + logger.info(f"{self.LOG_TAG}按时间重新排序 {name} 种子数:{len(torrents)}") # 按添加时间进行排序, 时间靠前的按大小和名称加入处理历史, 判定为原始种子, 其他为辅种 - torrents = self._torrents_sort(torrents=torrents, dl_type=DOWNLOADER) - logger.info(f"{self.LOG_TAG}下载器 {DOWNLOADER} 分析种子信息中 ...") + torrents = self._torrents_sort(torrents=torrents, dl_type=service.type) + logger.info(f"{self.LOG_TAG}下载器 {name} 分析种子信息中 ...") for torrent in torrents: try: if self._event.is_set(): @@ -214,14 +242,14 @@ class DownloadSiteTag(_PluginBase): f"{self.LOG_TAG}停止服务") return # 获取已处理种子的key (size, name) - _key = self._torrent_key(torrent=torrent, dl_type=DOWNLOADER) + _key = self._torrent_key(torrent=torrent, dl_type=service.type) # 获取种子hash - _hash = self._get_hash(torrent=torrent, dl_type=DOWNLOADER) + _hash = self._get_hash(torrent=torrent, dl_type=service.type) if not _hash: continue # 获取种子当前标签 - torrent_tags = self._get_label(torrent=torrent, dl_type=DOWNLOADER) - torrent_cat = self._get_category(torrent=torrent, dl_type=DOWNLOADER) + torrent_tags = self._get_label(torrent=torrent, dl_type=service.type) + torrent_cat = self._get_category(torrent=torrent, dl_type=service.type) # 提取种子hash对应的下载历史 history: DownloadHistory = self.downloadhistory_oper.get_by_hash(_hash) if not history: @@ -241,7 +269,7 @@ class DownloadSiteTag(_PluginBase): history.torrent_site = None # 如果站点名称为空, 尝试通过trackers识别 elif not history.torrent_site: - trackers = self._get_trackers(torrent=torrent, dl_type=DOWNLOADER) + trackers = self._get_trackers(torrent=torrent, dl_type=service.type) for tracker in trackers: # 检查tracker是否包含特定的关键字,并进行相应的映射 for key, mapped_domain in tracker_mappings.items(): @@ -267,7 +295,7 @@ class DownloadSiteTag(_PluginBase): if self._enabled_media_tag and history.title: _tags.append(history.title) # 分类, 如果勾选开关的话 因允许mtype为空时运行到此, 因此需要判断mtype不为空。为防止不必要的识别, 种子已经存在分类torrent_cat时 也不执行 - if DOWNLOADER == "qbittorrent" and self._enabled_category and not torrent_cat and history.type: + if service.type == "qbittorrent" and self._enabled_category and not torrent_cat and history.type: # 如果是电视剧 需要区分是否动漫 genre_ids = None # 因允许tmdbid为空时运行到此, 因此需要判断tmdbid不为空 @@ -289,7 +317,7 @@ class DownloadSiteTag(_PluginBase): if not _cat and not _tags: continue # 执行通用方法, 设置种子标签与分类 - self._set_torrent_info(DOWNLOADER=DOWNLOADER, _hash=_hash, _torrent=torrent, _tags=_tags, _cat=_cat, + self._set_torrent_info(service=service, _hash=_hash, _torrent=torrent, _tags=_tags, _cat=_cat, _original_tags=torrent_tags) except Exception as e: logger.error( @@ -432,15 +460,16 @@ class DownloadSiteTag(_PluginBase): print(str(e)) return None - def _set_torrent_info(self, DOWNLOADER: str, _hash: str, _torrent: Any = None, _tags=None, _cat: str = None, + def _set_torrent_info(self, service: ServiceInfo, _hash: str, _torrent: Any = None, _tags=None, _cat: str = None, _original_tags: list = None): """ 设置种子标签与分类 """ - # 当前下载器 + if not service or not service.instance: + return if _tags is None: _tags = [] - downloader_obj = self._get_downloader(DOWNLOADER) + downloader_obj = service.instance if not _torrent: _torrent, error = downloader_obj.get_torrents(ids=_hash) if not _torrent or error: @@ -451,9 +480,9 @@ class DownloadSiteTag(_PluginBase): f"{self.LOG_TAG}设置种子标签与分类: {_hash} 查询到 {len(_torrent)} 个种子") _torrent = _torrent[0] # 判断是否可执行 - if DOWNLOADER and downloader_obj and _hash and _torrent: + if _hash and _torrent: # 下载器api不通用, 因此需分开处理 - if DOWNLOADER == "qbittorrent": + if service.type == "qbittorrent": # 设置标签 if _tags: downloader_obj.set_torrents_tag(ids=_hash, tags=_tags) @@ -463,7 +492,7 @@ class DownloadSiteTag(_PluginBase): try: _torrent.setCategory(category=_cat) except Exception as e: - logger.warn(f"下载器 {DOWNLOADER} 种子id: {_hash} 设置分类 {_cat} 失败:{str(e)}, " + logger.warn(f"下载器 {service.name} 种子id: {_hash} 设置分类 {_cat} 失败:{str(e)}, " f"尝试创建分类再设置 ...") downloader_obj.qbc.torrents_createCategory(name=_cat) _torrent.setCategory(category=_cat) @@ -472,16 +501,16 @@ class DownloadSiteTag(_PluginBase): if _tags: # _original_tags = None表示未指定, 因此需要获取原始标签 if _original_tags is None: - _original_tags = self._get_label(torrent=_torrent, dl_type=DOWNLOADER) + _original_tags = self._get_label(torrent=_torrent, dl_type=service.type) # 如果原始标签不是空的, 那么合并原始标签 if _original_tags: _tags = list(set(_original_tags).union(set(_tags))) downloader_obj.set_torrent_tag(ids=_hash, tags=_tags) logger.warn( - f"{self.LOG_TAG}下载器: {DOWNLOADER} 种子id: {_hash} {(' 标签: ' + ','.join(_tags)) if _tags else ''} {(' 分类: ' + _cat) if _cat else ''}") + f"{self.LOG_TAG}下载器: {service.name} 种子id: {_hash} {(' 标签: ' + ','.join(_tags)) if _tags else ''} {(' 分类: ' + _cat) if _cat else ''}") @eventmanager.register(EventType.DownloadAdded) - def DownloadAdded(self, event: Event): + def download_added(self, event: Event): """ 添加下载事件 """ @@ -492,6 +521,16 @@ class DownloadSiteTag(_PluginBase): return try: + downloader = event.event_data.get("downloader") + if not downloader: + logger.info("触发添加下载事件,但没有获取到下载器信息,跳过后续处理") + return + + service = self.service_infos.get(downloader) + if not service: + logger.info(f"触发添加下载事件,但没有监听下载器 {downloader},跳过后续处理") + return + context: Context = event.event_data.get("context") _hash = event.event_data.get("hash") _torrent = context.torrent_info @@ -509,7 +548,7 @@ class DownloadSiteTag(_PluginBase): _cat = self._genre_ids_get_cat(_media.type, _media.genre_ids) if _hash and (_tags or _cat): # 执行通用方法, 设置种子标签与分类 - self._set_torrent_info(DOWNLOADER=settings.DEFAULT_DOWNLOADER, _hash=_hash, _tags=_tags, _cat=_cat) + self._set_torrent_info(service=service, _hash=_hash, _tags=_tags, _cat=_cat) except Exception as e: logger.error( f"{self.LOG_TAG}分析下载事件时发生了错误: {str(e)}") @@ -597,8 +636,7 @@ class DownloadSiteTag(_PluginBase): { 'component': 'VCol', 'props': { - 'cols': 12, - 'md': 12 + 'cols': 12 }, 'content': [ { @@ -612,6 +650,31 @@ class DownloadSiteTag(_PluginBase): } ] }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'multiple': True, + 'chips': True, + 'clearable': True, + 'model': 'downloaders', + 'label': '下载器', + 'items': [{"title": config.name, "value": config.name} + for config in self.downloaderhelper.get_configs().values()] + } + } + ] + } + ] + }, { 'component': 'VRow', 'content': [ diff --git a/plugins/dingdingmsg/__init__.py b/plugins/dingdingmsg/__init__.py new file mode 100644 index 0000000..280548f --- /dev/null +++ b/plugins/dingdingmsg/__init__.py @@ -0,0 +1,269 @@ +import re +import time +import hmac +import hashlib +import base64 +import urllib.parse + +from app.plugins import _PluginBase +from app.core.event import eventmanager, Event +from app.schemas.types import EventType, NotificationType +from app.utils.http import RequestUtils +from typing import Any, List, Dict, Tuple +from app.log import logger + + +class DingdingMsg(_PluginBase): + # 插件名称 + plugin_name = "钉钉机器人" + # 插件描述 + plugin_desc = "支持使用钉钉机器人发送消息通知。" + # 插件图标 + plugin_icon = "Dingding_A.png" + # 插件版本 + plugin_version = "1.12" + # 插件作者 + plugin_author = "nnlegenda" + # 作者主页 + author_url = "https://github.com/nnlegenda" + # 插件配置项ID前缀 + plugin_config_prefix = "dingdingmsg_" + # 加载顺序 + plugin_order = 25 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _enabled = False + _token = None + _secret = None + _msgtypes = [] + + def init_plugin(self, config: dict = None): + if config: + self._enabled = config.get("enabled") + self._token = config.get("token") + self._secret = config.get("secret") + self._msgtypes = config.get("msgtypes") or [] + + def get_state(self) -> bool: + return self._enabled and (True if self._token else False) and (True if self._secret else False) + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + # 编历 NotificationType 枚举,生成消息类型选项 + MsgTypeOptions = [] + for item in NotificationType: + MsgTypeOptions.append({ + "title": item.value, + "value": item.name + }) + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'token', + 'label': '钉钉机器人token', + 'placeholder': 'xxxxxx', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'secret', + 'label': '加签', + 'placeholder': 'SECxxx', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'multiple': True, + 'chips': True, + 'model': 'msgtypes', + 'label': '消息类型', + 'items': MsgTypeOptions + } + } + ] + } + ] + }, + ] + } + ], { + "enabled": False, + 'token': '', + 'msgtypes': [] + } + + def get_page(self) -> List[dict]: + pass + + @eventmanager.register(EventType.NoticeMessage) + def send(self, event: Event): + """ + 消息发送事件 + """ + if not self.get_state(): + return + + if not event.event_data: + return + + msg_body = event.event_data + # 渠道 + channel = msg_body.get("channel") + if channel: + return + # 类型 + msg_type: NotificationType = msg_body.get("type") + # 标题 + title = msg_body.get("title") + # 文本 + text = msg_body.get("text") + # 封面 + cover = msg_body.get("image") + + if not title and not text: + logger.warn("标题和内容不能同时为空") + return + + if (msg_type and self._msgtypes + and msg_type.name not in self._msgtypes): + logger.info(f"消息类型 {msg_type.value} 未开启消息发送") + return + + sc_url = self.url_sign(self._token, self._secret) + + try: + + if text: + # 对text进行Markdown特殊字符转义 + text = re.sub(r"([_`])", r"\\\1", text) + else: + text = "" + + if cover: + data = { + "msgtype": "markdown", + "markdown": { + "title": title, + "text": "### %s\n\n" + "![Cover](%s)\n\n" + "> %s\n\n > MoviePilot %s\n" % (title, cover, text, msg_type.value) + } + } + else: + data = { + "msgtype": "markdown", + "markdown": { + "title": title, + "text": "### %s\n\n" + "> %s\n\n > MoviePilot %s\n" % (title, text, msg_type.value) + } + } + res = RequestUtils(content_type="application/json").post_res(sc_url, json=data) + if res and res.status_code == 200: + ret_json = res.json() + errno = ret_json.get('errcode') + error = ret_json.get('errmsg') + if errno == 0: + logger.info("钉钉机器人消息发送成功") + else: + logger.warn(f"钉钉机器人消息发送失败,错误码:{errno},错误原因:{error}") + elif res is not None: + logger.warn(f"钉钉机器人消息发送失败,错误码:{res.status_code},错误原因:{res.reason}") + else: + logger.warn("钉钉机器人消息发送失败,未获取到返回信息") + except Exception as msg_e: + logger.error(f"钉钉机器人消息发送失败,{str(msg_e)}") + + def stop_service(self): + """ + 退出插件 + """ + pass + + def url_sign(self, access_token: str, secret: str) -> str: + """ + 加签 + """ + # 生成时间戳和签名 + timestamp = str(round(time.time() * 1000)) + secret_enc = secret.encode('utf-8') + string_to_sign = '{}\n{}'.format(timestamp, secret) + string_to_sign_enc = string_to_sign.encode('utf-8') + hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() + sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) + # 组合请求的完整 URL + full_url = f'https://oapi.dingtalk.com/robot/send?access_token={access_token}×tamp={timestamp}&sign={sign}' + return full_url