add dingdingmsg

This commit is contained in:
XiLin
2024-10-10 13:16:48 +08:00
parent 05497d2e65
commit 0773bff16d
5 changed files with 405 additions and 51 deletions

BIN
icons/Dingding_A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -840,5 +840,15 @@
"v1.1": "支持将极影视评分修改为豆瓣评分",
"v1.0": "同步极影视在看/已看状态到豆瓣"
}
},
"DingdingMsg": {
"name": "钉钉机器人",
"description": "支持使用钉钉机器人发送消息通知。",
"labels": "消息通知,钉钉机器人",
"version": "1.12",
"icon": "Dingding_A.png",
"author": "nnlegenda",
"level": 1,
"v2": true
}
}

View File

@@ -34,5 +34,17 @@
"history": {
"v2.5": "MoviePilot V2 版本站点自动签到插件"
}
},
"DownloadSiteTag": {
"name": "下载任务分类与标签",
"description": "自动给下载任务分类与打站点标签、剧集名称标签",
"labels": "下载管理",
"version": "2.2",
"icon": "Youtube-dl_B.png",
"author": "叮叮当",
"level": 1,
"history": {
"v2.2": "MoviePilot V2 版本下载任务分类与标签插件"
}
}
}

View File

@@ -1,25 +1,28 @@
import datetime
import pytz
import threading
from typing import List, Tuple, Dict, Any, Optional
from app.core.context import Context
from app.core.event import eventmanager, Event
from app.schemas.types import EventType, MediaType
from app.core.config import settings
from app.log import logger
from app.plugins import _PluginBase
from app.modules.qbittorrent import Qbittorrent
from app.modules.transmission import Transmission
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.models.downloadhistory import DownloadHistory
import pytz
from app.helper.sites import SitesHelper
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from app.helper.sites import SitesHelper
from app.core.config import settings
from app.core.context import Context
from app.core.event import eventmanager, Event
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.models.downloadhistory import DownloadHistory
from app.helper.downloader import DownloaderHelper
from app.log import logger
from app.plugins import _PluginBase
from app.schemas import ServiceInfo
from app.schemas.types import EventType, MediaType
from app.utils.string import StringUtils
class DownloadSiteTag(_PluginBase):
# region 全局定义
# 插件名称
plugin_name = "下载任务分类与标签"
# 插件描述
@@ -27,7 +30,7 @@ class DownloadSiteTag(_PluginBase):
# 插件图标
plugin_icon = "Youtube-dl_B.png"
# 插件版本
plugin_version = "2.1"
plugin_version = "2.2"
# 插件作者
plugin_author = "叮叮当"
# 作者主页
@@ -44,10 +47,9 @@ class DownloadSiteTag(_PluginBase):
# 退出事件
_event = threading.Event()
# 私有属性
downloader_qb = None
downloader_tr = None
downloadhistory_oper = None
sites_helper = None
downloaderhelper = None
_scheduler = None
_enabled = False
_onlyonce = False
@@ -61,11 +63,42 @@ class DownloadSiteTag(_PluginBase):
_category_movie = None
_category_tv = None
_category_anime = None
_downloaders = None
# Property
@property
def service_infos(self) -> 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)
# 分类, 如果勾选开关的话 <tr暂不支持> 因允许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': [

View File

@@ -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}&timestamp={timestamp}&sign={sign}'
return full_url